├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
└── src
├── Adapters
├── Archive
│ ├── Api.php
│ ├── Detectors
│ │ ├── AuthorName.php
│ │ ├── Code.php
│ │ ├── Description.php
│ │ ├── ProviderName.php
│ │ ├── PublishedTime.php
│ │ └── Title.php
│ └── Extractor.php
├── Bandcamp
│ ├── Detectors
│ │ └── ProviderName.php
│ └── Extractor.php
├── CadenaSer
│ ├── Detectors
│ │ └── Code.php
│ └── Extractor.php
├── Facebook
│ ├── Detectors
│ │ └── Title.php
│ ├── Extractor.php
│ └── OEmbed.php
├── Flickr
│ ├── Detectors
│ │ └── Code.php
│ └── Extractor.php
├── Gist
│ ├── Api.php
│ ├── Detectors
│ │ ├── AuthorName.php
│ │ ├── AuthorUrl.php
│ │ ├── Code.php
│ │ └── PublishedTime.php
│ └── Extractor.php
├── Github
│ ├── Detectors
│ │ └── Code.php
│ └── Extractor.php
├── Ideone
│ ├── Detectors
│ │ └── Code.php
│ └── Extractor.php
├── ImageShack
│ ├── Api.php
│ ├── Detectors
│ │ ├── AuthorName.php
│ │ ├── AuthorUrl.php
│ │ ├── Description.php
│ │ ├── Image.php
│ │ ├── ProviderName.php
│ │ ├── PublishedTime.php
│ │ └── Title.php
│ └── Extractor.php
├── Instagram
│ ├── Extractor.php
│ └── OEmbed.php
├── Pinterest
│ ├── Detectors
│ │ └── Code.php
│ └── Extractor.php
├── Sassmeister
│ ├── Detectors
│ │ └── Code.php
│ └── Extractor.php
├── Slides
│ ├── Detectors
│ │ └── Code.php
│ └── Extractor.php
├── Snipplr
│ ├── Detectors
│ │ └── Code.php
│ └── Extractor.php
├── Twitch
│ ├── Detectors
│ │ └── Code.php
│ └── Extractor.php
├── Twitter
│ ├── Api.php
│ ├── Detectors
│ │ ├── AuthorName.php
│ │ ├── AuthorUrl.php
│ │ ├── Description.php
│ │ ├── Image.php
│ │ ├── ProviderName.php
│ │ ├── PublishedTime.php
│ │ └── Title.php
│ └── Extractor.php
├── Wikipedia
│ ├── Api.php
│ ├── Detectors
│ │ ├── Description.php
│ │ └── Title.php
│ └── Extractor.php
└── Youtube
│ ├── Detectors
│ └── Feeds.php
│ └── Extractor.php
├── ApiTrait.php
├── Detectors
├── AuthorName.php
├── AuthorUrl.php
├── Cms.php
├── Code.php
├── Description.php
├── Detector.php
├── Favicon.php
├── Feeds.php
├── Icon.php
├── Image.php
├── Keywords.php
├── Language.php
├── Languages.php
├── License.php
├── ProviderName.php
├── ProviderUrl.php
├── PublishedTime.php
├── Redirect.php
├── Title.php
└── Url.php
├── Document.php
├── Embed.php
├── EmbedCode.php
├── Extractor.php
├── ExtractorFactory.php
├── Http
├── Crawler.php
├── CurlClient.php
├── CurlDispatcher.php
├── FactoryDiscovery.php
├── NetworkException.php
└── RequestException.php
├── HttpApiTrait.php
├── LinkedData.php
├── Metas.php
├── OEmbed.php
├── QueryResult.php
├── functions.php
└── resources
├── oembed.php
└── suffix.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/).
6 |
7 | ## [4.4.17] - 2025-05-13
8 | ### Fixed
9 | - Adapters hostname detection [#556].
10 |
11 | ## [4.4.16] - 2025-05-09
12 | ### Fixed
13 | - Adapters hostname detection [#555].
14 |
15 | ## [4.4.15] - 2025-01-02
16 | ### Fixed
17 | - Type bug [#553].
18 |
19 | ## [4.4.14] - 2024-12-04
20 | ### Fixed
21 | - Php 8.4 support [#551].
22 |
23 | ## [4.4.13] - 2024-11-21
24 | ### Fixed
25 | - Php 8.4 support [#548].
26 |
27 | ## [4.4.12] - 2024-07-24
28 | ### Fixed
29 | - X.com (Twitter) [#540]
30 | - Updated oembed resources.
31 |
32 | ## [4.4.11] - 2024-06-10
33 | ### Fixed
34 | - Updated oEmbed entry points [#537]
35 |
36 | ## [4.4.10] - 2023-12-10
37 | ### Fixed
38 | - PHP 7.4 support
39 | - Use correct method for string length [#529]
40 |
41 | ## [4.4.9] - 2023-12-01
42 | ### Fixed
43 | - Performance and memory leak issues [#525], [#527].
44 |
45 | ## [4.4.8] - 2023-05-22
46 | ### Fixed
47 | - Support for `psr/http-message@2` [#514], [#515]
48 |
49 | ## [4.4.7] - 2022-12-12
50 | ### Fixed
51 | - Href attributes with `undefined` values [#501], [#502]
52 | - Deprecated warning for var interpolation in PHP 8.2 [#506]
53 | - Prevent unsupported operand types exception [#507]
54 |
55 | ## [4.4.6] - 2022-10-02
56 | ### Fixed
57 | - Some code issues detected by phpstan: [#495], [#496], [#497], [#498].
58 | - Fix for quotation marks in redirect URL [#499]
59 |
60 | ## [4.4.5] - 2022-09-06
61 | ### Fixed
62 | - Updated oembed endpoints [#494]
63 |
64 | ## [4.4.4] - 2022-04-13
65 | ### Fixed
66 | - Error getting data from Linked data [#481].
67 |
68 | ## [4.4.3] - 2022-03-13
69 | ### Fixed
70 | - PHP 8.1 deprecation notice [#480].
71 |
72 | ## [4.4.2] - 2022-02-13
73 | ### Added
74 | - Options to customize the CurlClient to perform http queries [#474].
75 |
76 | ## [4.4.1] - 2022-02-06
77 | ### Fixed
78 | - PHP 8.1 deprecation notice [#473].
79 |
80 | ## [4.4.0] - 2022-01-08
81 | ### Added
82 | - New settings option `twitter:token` to use Twitter API to get the data [#364] [#468].
83 |
84 | ### Fixed
85 | - Headers not sent properly by curl [#466], [#467].
86 |
87 | ## [4.3.5] - 2021-10-10
88 | ### Fixed
89 | - Updated oEmbed endpoints
90 | - Fixed embed code for Instagram [#456], [#459]
91 |
92 | ### Security
93 | - Fixed a possible XML Quadratic Blowup vulnerability.
94 |
95 | ## [4.3.4] - 2021-06-22
96 | ### Fixed
97 | - Urls of images should include the same url for the `$info->image` value. [#452]
98 |
99 | ## [4.3.3] - 2021-06-22
100 | ### Fixed
101 | - Facebook embed redirects to `/login`. [#450], [#451]
102 |
103 | ## [4.3.2] - 2021-04-04
104 | ### Fixed
105 | - Add configured oEmbed query parameters to all oEmbed endpoints [#437]
106 | - Updated oEmbed endpoints.
107 | - Replaced Travis with Github workflows for testing
108 |
109 | ## [4.3.1] - 2021-03-21
110 | ### Added
111 | - Support for binary files (video, audio, images, etc) [#412] [#413]
112 |
113 | ### Fixed
114 | - Oembed for facebook photos [#405] [#406]
115 | - Oembed for facebook videos [#432] [#433]
116 | - Added more ways to detect data using meta tags [#427]
117 | - Bandcamp provider name [#429] [#430]
118 |
119 | ## [4.3.0] - 2020-11-04
120 | ### Added
121 | - New function `$embed->setSettings()` to pass the settings before get the site info
122 |
123 | ### Fixed
124 | - PHP 8 compatibility [#394]
125 | - Facebook and Instagram adapted to the new API changes [#392] [#399]
126 |
127 | ## [4.2.7] - 2020-09-23
128 | ### Added
129 | - New option `twitch:parent` to fix Twitch embed with iframes [#384]
130 |
131 | ### Fixed
132 | - Added `datePublished` check to `PublishedTime` extractor [#385] [#386]
133 | - Added `@property-read` for IDE suppport [#387] [#388]
134 |
135 | ## [4.2.6] - 2020-08-28
136 | ### Fixed
137 | - Code width and height when the provided value is not numeric (ex: 100%) [#380]
138 |
139 | ## [4.2.5] - 2020-08-01
140 | ### Fixed
141 | - Github TypeError exception with some urls [#375]
142 |
143 | ## [4.2.4] - 2020-07-06
144 | ### Fixed
145 | - Ignore invalid urls instead throw an exception
146 | - Updated oembed list of endpoints
147 |
148 | ## [4.2.3] - 2020-06-12
149 | ### Fixed
150 | - Suppport for other non-latin alphabets such Persian or Arabic [#366]
151 |
152 | ## [4.2.2] - 2020-05-31
153 | ### Fixed
154 | - Provided a fallback for oEmbed compatible sites like Instagram that redirects to login page [#357]
155 |
156 | ## [4.2.1] - 2020-05-25
157 | ### Fixed
158 | - Redirect urls like `t.co`.
159 |
160 | ## [4.2.0] - 2020-05-23
161 | ### Added
162 | - Added the `ignored_errors` settings to ignore some curls errors instead throw an exception [#355]
163 | - Support for Twitch embeds [#332]
164 |
165 | ### Fixed
166 | - Ignored linkedData errors [#356]
167 |
168 | ## [4.1.1] - 2020-04-24
169 | ### Added
170 | - Updated oembed endpoints from `oembed.com`
171 | - Add support for tiktok.com
172 |
173 | ## [4.1.0] - 2020-04-19
174 | ### Added
175 | - Ability to send settings to `CurlClient`. Added the `cookies_path` setting to customize the file used for cookies. [#345]
176 | - `Document::selectCss()` function to select elements using css selectors instead xpath (it requires `symfony/css-selector`)
177 | - `Document::removeCss()` function to remove elements using css selectors instead xpath (it requires `symfony/css-selector`)
178 | - Ability to configure OEmbed parameters from the outside using the `oembed:query_parameters` setting [#346]
179 |
180 | ## [4.0.0] - 2020-03-13
181 | Full library refactoring.
182 |
183 | ### Added
184 | - Support for multiple parallel request with `curl_multi`
185 | - Support for PSR-7 Http Messages, PSR-17 Http Factories and PSR-18 Http Client
186 | - `cms` value
187 | - `language` to detect the page language
188 | - `languages` to detect urls to versions in different languages
189 | - `favicon` to detect small favicons (16 or 32px)
190 | - `icon` to detect big icons (from 48px)
191 |
192 | ### Changed
193 | - Changed providers (oEmbed, Html, OpenGraph etc) by independent detectors (title, url, language etc).
194 | - The `tags` value is renamed to `keywords`
195 | - Use Psr standards instead custom interfaces.
196 | - Improved tests using cached responses.
197 |
198 | ### Removed
199 | - Support for PHP<7.4
200 | - `type` value (is was very confusing)
201 | - `images` value
202 | - `providerImage` (use `favicon` or `icon` instead)
203 | - Support for files (pdf, jpg, video, etc).
204 |
205 | [#332]: https://github.com/oscarotero/Embed/issues/332
206 | [#345]: https://github.com/oscarotero/Embed/issues/345
207 | [#346]: https://github.com/oscarotero/Embed/issues/346
208 | [#355]: https://github.com/oscarotero/Embed/issues/355
209 | [#356]: https://github.com/oscarotero/Embed/issues/356
210 | [#357]: https://github.com/oscarotero/Embed/issues/357
211 | [#364]: https://github.com/oscarotero/Embed/issues/364
212 | [#366]: https://github.com/oscarotero/Embed/issues/366
213 | [#375]: https://github.com/oscarotero/Embed/issues/375
214 | [#380]: https://github.com/oscarotero/Embed/issues/380
215 | [#384]: https://github.com/oscarotero/Embed/issues/384
216 | [#385]: https://github.com/oscarotero/Embed/issues/385
217 | [#386]: https://github.com/oscarotero/Embed/issues/386
218 | [#387]: https://github.com/oscarotero/Embed/issues/387
219 | [#388]: https://github.com/oscarotero/Embed/issues/388
220 | [#392]: https://github.com/oscarotero/Embed/issues/392
221 | [#394]: https://github.com/oscarotero/Embed/issues/394
222 | [#399]: https://github.com/oscarotero/Embed/issues/399
223 | [#405]: https://github.com/oscarotero/Embed/issues/405
224 | [#406]: https://github.com/oscarotero/Embed/issues/406
225 | [#412]: https://github.com/oscarotero/Embed/issues/412
226 | [#413]: https://github.com/oscarotero/Embed/issues/413
227 | [#427]: https://github.com/oscarotero/Embed/issues/427
228 | [#429]: https://github.com/oscarotero/Embed/issues/429
229 | [#430]: https://github.com/oscarotero/Embed/issues/430
230 | [#432]: https://github.com/oscarotero/Embed/issues/432
231 | [#433]: https://github.com/oscarotero/Embed/issues/433
232 | [#437]: https://github.com/oscarotero/Embed/issues/437
233 | [#450]: https://github.com/oscarotero/Embed/issues/450
234 | [#451]: https://github.com/oscarotero/Embed/issues/451
235 | [#452]: https://github.com/oscarotero/Embed/issues/452
236 | [#456]: https://github.com/oscarotero/Embed/issues/456
237 | [#459]: https://github.com/oscarotero/Embed/issues/459
238 | [#466]: https://github.com/oscarotero/Embed/issues/466
239 | [#467]: https://github.com/oscarotero/Embed/issues/467
240 | [#468]: https://github.com/oscarotero/Embed/issues/468
241 | [#473]: https://github.com/oscarotero/Embed/issues/473
242 | [#474]: https://github.com/oscarotero/Embed/issues/474
243 | [#480]: https://github.com/oscarotero/Embed/issues/480
244 | [#481]: https://github.com/oscarotero/Embed/issues/481
245 | [#494]: https://github.com/oscarotero/Embed/issues/494
246 | [#495]: https://github.com/oscarotero/Embed/issues/495
247 | [#496]: https://github.com/oscarotero/Embed/issues/496
248 | [#497]: https://github.com/oscarotero/Embed/issues/497
249 | [#498]: https://github.com/oscarotero/Embed/issues/498
250 | [#499]: https://github.com/oscarotero/Embed/issues/499
251 | [#501]: https://github.com/oscarotero/Embed/issues/501
252 | [#502]: https://github.com/oscarotero/Embed/issues/502
253 | [#506]: https://github.com/oscarotero/Embed/issues/506
254 | [#507]: https://github.com/oscarotero/Embed/issues/507
255 | [#514]: https://github.com/oscarotero/Embed/issues/514
256 | [#515]: https://github.com/oscarotero/Embed/issues/515
257 | [#525]: https://github.com/oscarotero/Embed/issues/525
258 | [#527]: https://github.com/oscarotero/Embed/issues/527
259 | [#529]: https://github.com/oscarotero/Embed/issues/529
260 | [#537]: https://github.com/oscarotero/Embed/issues/537
261 | [#540]: https://github.com/oscarotero/Embed/issues/540
262 | [#548]: https://github.com/oscarotero/Embed/issues/548
263 | [#551]: https://github.com/oscarotero/Embed/issues/551
264 | [#553]: https://github.com/oscarotero/Embed/issues/553
265 | [#555]: https://github.com/oscarotero/Embed/issues/555
266 | [#556]: https://github.com/oscarotero/Embed/issues/556
267 |
268 | [4.4.17]: https://github.com/oscarotero/Embed/compare/v4.4.16...v4.4.17
269 | [4.4.16]: https://github.com/oscarotero/Embed/compare/v4.4.15...v4.4.16
270 | [4.4.15]: https://github.com/oscarotero/Embed/compare/v4.4.14...v4.4.15
271 | [4.4.14]: https://github.com/oscarotero/Embed/compare/v4.4.13...v4.4.14
272 | [4.4.13]: https://github.com/oscarotero/Embed/compare/v4.4.12...v4.4.13
273 | [4.4.12]: https://github.com/oscarotero/Embed/compare/v4.4.11...v4.4.12
274 | [4.4.11]: https://github.com/oscarotero/Embed/compare/v4.4.10...v4.4.11
275 | [4.4.10]: https://github.com/oscarotero/Embed/compare/v4.4.9...v4.4.10
276 | [4.4.9]: https://github.com/oscarotero/Embed/compare/v4.4.8...v4.4.9
277 | [4.4.8]: https://github.com/oscarotero/Embed/compare/v4.4.7...v4.4.8
278 | [4.4.7]: https://github.com/oscarotero/Embed/compare/v4.4.6...v4.4.7
279 | [4.4.6]: https://github.com/oscarotero/Embed/compare/v4.4.5...v4.4.6
280 | [4.4.5]: https://github.com/oscarotero/Embed/compare/v4.4.4...v4.4.5
281 | [4.4.4]: https://github.com/oscarotero/Embed/compare/v4.4.3...v4.4.4
282 | [4.4.3]: https://github.com/oscarotero/Embed/compare/v4.4.2...v4.4.3
283 | [4.4.2]: https://github.com/oscarotero/Embed/compare/v4.4.1...v4.4.2
284 | [4.4.1]: https://github.com/oscarotero/Embed/compare/v4.4.0...v4.4.1
285 | [4.4.0]: https://github.com/oscarotero/Embed/compare/v4.3.5...v4.4.0
286 | [4.3.5]: https://github.com/oscarotero/Embed/compare/v4.3.4...v4.3.5
287 | [4.3.4]: https://github.com/oscarotero/Embed/compare/v4.3.3...v4.3.4
288 | [4.3.3]: https://github.com/oscarotero/Embed/compare/v4.3.2...v4.3.3
289 | [4.3.2]: https://github.com/oscarotero/Embed/compare/v4.3.1...v4.3.2
290 | [4.3.1]: https://github.com/oscarotero/Embed/compare/v4.3.0...v4.3.1
291 | [4.3.0]: https://github.com/oscarotero/Embed/compare/v4.2.7...v4.3.0
292 | [4.2.7]: https://github.com/oscarotero/Embed/compare/v4.2.6...v4.2.7
293 | [4.2.6]: https://github.com/oscarotero/Embed/compare/v4.2.5...v4.2.6
294 | [4.2.5]: https://github.com/oscarotero/Embed/compare/v4.2.4...v4.2.5
295 | [4.2.4]: https://github.com/oscarotero/Embed/compare/v4.2.3...v4.2.4
296 | [4.2.3]: https://github.com/oscarotero/Embed/compare/v4.2.2...v4.2.3
297 | [4.2.2]: https://github.com/oscarotero/Embed/compare/v4.2.1...v4.2.2
298 | [4.2.1]: https://github.com/oscarotero/Embed/compare/v4.2.0...v4.2.1
299 | [4.2.0]: https://github.com/oscarotero/Embed/compare/v4.1.1...v4.2.0
300 | [4.1.1]: https://github.com/oscarotero/Embed/compare/v4.1.0...v4.1.1
301 | [4.1.0]: https://github.com/oscarotero/Embed/compare/v4.0.0...v4.1.0
302 | [4.0.0]: https://github.com/oscarotero/Embed/releases/tag/v4.0.0
303 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Oscar Otero Marzoa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Searching MAINTAINER
2 |
3 | After 11 years since the first version of Embed was released, I don't have the time or motivation to continue maintaining this project. I rarely write PHP code and am not aware of the latest features of PHP. If anyone wants to continue maintaining and evolving this library, please open an issue or contact me.
4 |
5 | Meanwhile, I'll continue accepting PR from the community (I don't want this project to die), but won't be actively working on improving it. Thanks!
6 |
7 | # Embed
8 |
9 |
10 | [![Latest Version on Packagist][ico-version]][link-packagist]
11 | [![Total Downloads][ico-downloads]][link-packagist]
12 | [![Monthly Downloads][ico-m-downloads]][link-packagist]
13 | [![Software License][ico-license]](LICENSE)
14 |
15 | PHP library to get information from any web page (using oembed, opengraph, twitter-cards, scrapping the html, etc). It's compatible with any web service (youtube, vimeo, flickr, instagram, etc) and has adapters to some sites like (archive.org, github, facebook, etc).
16 |
17 | Requirements:
18 |
19 | * PHP 7.4+
20 | * Curl library installed
21 | * PSR-17 implementation. By default these libraries are detected automatically:
22 | * [laminas/laminas-diactoros](https://github.com/laminas/laminas-diactoros)
23 | * [guzzle/psr7](https://github.com/guzzle/psr7)
24 | * [nyholm/psr7](https://github.com/Nyholm/psr7)
25 | * [sunrise/http-message](https://github.com/sunrise-php/http-message)
26 |
27 | > If you need PHP 5.5-7.3 support, [use the 3.x version](https://github.com/oscarotero/Embed/tree/v3.x)
28 |
29 | ## Online demo
30 |
31 | Run `php -S localhost:8888 demo/index.php`
32 |
33 | ## Video Tutorial
34 | [
](https://youtu.be/4YCLRpKY1cs)
35 |
36 |
37 | ## Installation
38 |
39 | This package is installable and autoloadable via Composer as [embed/embed](https://packagist.org/packages/embed/embed).
40 |
41 | ```
42 | $ composer require embed/embed
43 | ```
44 |
45 | ## Usage
46 |
47 | ```php
48 | use Embed\Embed;
49 |
50 | $embed = new Embed();
51 |
52 | //Load any url:
53 | $info = $embed->get('https://www.youtube.com/watch?v=PP1xn5wHtxE');
54 |
55 | //Get content info
56 |
57 | $info->title; //The page title
58 | $info->description; //The page description
59 | $info->url; //The canonical url
60 | $info->keywords; //The page keywords
61 |
62 | $info->image; //The thumbnail or main image
63 |
64 | $info->code->html; //The code to embed the image, video, etc
65 | $info->code->width; //The exact width of the embed code (if exists)
66 | $info->code->height; //The exact height of the embed code (if exists)
67 | $info->code->ratio; //The percentage of height / width to emulate the aspect ratio using paddings.
68 |
69 | $info->authorName; //The resource author
70 | $info->authorUrl; //The author url
71 |
72 | $info->cms; //The cms used
73 | $info->language; //The language of the page
74 | $info->languages; //The alternative languages
75 |
76 | $info->providerName; //The provider name of the page (Youtube, Twitter, Instagram, etc)
77 | $info->providerUrl; //The provider url
78 | $info->icon; //The big icon of the site
79 | $info->favicon; //The favicon of the site (an .ico file or a png with up to 32x32px)
80 |
81 | $info->publishedTime; //The published time of the resource
82 | $info->license; //The license url of the resource
83 | $info->feeds; //The RSS/Atom feeds
84 | ```
85 |
86 | ## Parallel multiple requests
87 |
88 | ```php
89 | use Embed\Embed;
90 |
91 | $embed = new Embed();
92 |
93 | //Load multiple urls asynchronously:
94 | $infos = $embed->getMulti(
95 | 'https://www.youtube.com/watch?v=PP1xn5wHtxE',
96 | 'https://twitter.com/carlosmeixidefl/status/1230894146220625933',
97 | 'https://en.wikipedia.org/wiki/Tordoia',
98 | );
99 |
100 | foreach ($infos as $info) {
101 | echo $info->title;
102 | }
103 | ```
104 |
105 | ## Document
106 |
107 | The document is the object that store the html code of the page. You can use it to extract extra info from the html code:
108 |
109 | ```php
110 | //Get the document object
111 | $document = $info->getDocument();
112 |
113 | $document->link('image_src'); //Returns the href of a
114 | $document->getDocument(); //Returns the DOMDocument instance
115 | $html = (string) $document; //Returns the html code
116 |
117 | $document->select('.//h1'); //Search
118 | ```
119 |
120 | You can perform xpath queries in order to select specific elements. A search always return an instance of a `Embed\QueryResult`:
121 |
122 | ```php
123 | //Search the A elements
124 | $result = $document->select('.//a');
125 |
126 | //Filter the results
127 | $result->filter(fn ($node) => $node->getAttribute('href'));
128 |
129 | $id = $result->str('id'); //Return the id of the first result as string
130 | $text = $result->str(); //Return the content of the first result
131 |
132 | $ids = $result->strAll('id'); //Return an array with the ids of all results as string
133 | $texts = $result->strAll(); //Return an array with the content of all results as string
134 |
135 | $tabindex = $result->int('tabindex'); //Return the tabindex attribute of the first result as integer
136 | $number = $result->int(); //Return the content of the first result as integer
137 |
138 | $href = $result->url('href'); //Return the href attribute of the first result as url (converts relative urls to absolutes)
139 | $url = $result->url(); //Return the content of the first result as url
140 |
141 | $node = $result->node(); //Return the first node found (DOMElement)
142 | $nodes = $result->nodes(); //Return all nodes found
143 | ```
144 |
145 | ## Metas
146 |
147 | For convenience, the object `Metas` stores the value of all `` elements located in the html, so you can get the values easier. The key of every meta is get from the `name`, `property` or `itemprop` attributes and the value is get from `content`.
148 |
149 | ```php
150 | //Get the Metas object
151 | $metas = $info->getMetas();
152 |
153 | $metas->all(); //Return all values
154 | $metas->get('og:title'); //Return a key value
155 | $metas->str('og:title'); //Return the value as string (remove html tags)
156 | $metas->html('og:description'); //Return the value as html
157 | $metas->int('og:video:width'); //Return the value as integer
158 | $metas->url('og:url'); //Return the value as full url (converts relative urls to absolutes)
159 | ```
160 |
161 | ## OEmbed
162 |
163 | In addition to the html and metas, this library uses [oEmbed](https://oembed.com/) endpoints to get additional data. You can get this data as following:
164 |
165 | ```php
166 | //Get the oEmbed object
167 | $oembed = $info->getOEmbed();
168 |
169 | $oembed->all(); //Return all raw data
170 | $oembed->get('title'); //Return a key value
171 | $oembed->str('title'); //Return the value as string (remove html tags)
172 | $oembed->html('html'); //Return the value as html
173 | $oembed->int('width'); //Return the value as integer
174 | $oembed->url('url'); //Return the value as full url (converts relative urls to absolutes)
175 | ```
176 |
177 | Additional oEmbed parameters (like instagrams `hidecaption`) can also be provided:
178 | ```php
179 | $embed = new Embed();
180 |
181 | $result = $embed->get('https://www.instagram.com/p/B_C0wheCa4V/');
182 | $result->setSettings([
183 | 'oembed:query_parameters' => ['hidecaption' => true]
184 | ]);
185 | $oembed = $info->getOEmbed();
186 | ```
187 |
188 | ## LinkedData
189 |
190 | Another API available by default, used to extract info using the [JsonLD](https://www.w3.org/TR/json-ld/) schema.
191 |
192 | ```php
193 | //Get the linkedData object
194 | $ld = $info->getLinkedData();
195 |
196 | $ld->all(); //Return all data
197 | $ld->get('name'); //Return a key value
198 | $ld->str('name'); //Return the value as string (remove html tags)
199 | $ld->html('description'); //Return the value as html
200 | $ld->int('width'); //Return the value as integer
201 | $ld->url('url'); //Return the value as full url (converts relative urls to absolutes)
202 | ```
203 |
204 | ## Other APIs
205 |
206 | Some sites like Wikipedia or Archive.org provide a custom API that is used to fetch more reliable data. You can get the API object with the method `getApi()` but note that not all results have this method. The Api object has the same methods than oEmbed:
207 |
208 | ```php
209 | //Get the API object
210 | $api = $info->getApi();
211 |
212 | $api->all(); //Return all raw data
213 | $api->get('title'); //Return a key value
214 | $api->str('title'); //Return the value as string (remove html tags)
215 | $api->html('html'); //Return the value as html
216 | $api->int('width'); //Return the value as integer
217 | $api->url('url'); //Return the value as full url (converts relative urls to absolutes)
218 | ```
219 |
220 | ## Extending Embed
221 |
222 | Depending of your needs, you may want to extend this library with extra features or change the way it makes some operations.
223 |
224 | ### PSR
225 |
226 | Embed use some PSR standards to be the most interoperable possible:
227 |
228 | - [PSR-7](https://www.php-fig.org/psr/psr-7/) Standard interfaces to represent http requests, responses and uris
229 | - [PSR-17](https://www.php-fig.org/psr/psr-17/) Standard factories to create PSR-7 objects
230 | - [PSR-18](https://www.php-fig.org/psr/psr-18/) Standard interface to send a http request and return a response
231 |
232 | Embed comes with a CURL client compatible with PSR-18 but you need to install a PSR-7 / PSR-17 library. [Here you can see a list of popular libraries](https://github.com/middlewares/awesome-psr15-middlewares#psr-7-implementations) and the library can detect automatically 'laminas\diactoros', 'guzzleHttp\psr7', 'slim\psr7', 'nyholm\psr7' and 'sunrise\http' (in this order). If you want to use a different PSR implementation, you can do it in this way:
233 |
234 | ```php
235 | use Embed\Embed;
236 | use Embed\Http\Crawler;
237 |
238 | $client = new CustomHttpClient();
239 | $requestFactory = new CustomRequestFactory();
240 | $uriFactory = new CustomUriFactory();
241 |
242 | //The Crawler is responsible for perform http queries
243 | $crawler = new Crawler($client, $requestFactory, $uriFactory);
244 |
245 | //Create an embed instance passing the Crawler
246 | $embed = new Embed($crawler);
247 | ```
248 |
249 | ### Adapters
250 |
251 | There are some sites with special needs: because they provide public APIs that allows to extract more info (like Wikipedia or Archive.org) or because we need to change how to extract the data in this particular site. For all that cases we have the adapters, that are classes extending the default classes to provide extra functionality.
252 |
253 | Before creating an adapter, you need to understand how Embed work: when you execute this code, you get a `Extractor` class
254 |
255 | ```php
256 | //Get the Extractor with all info
257 | $info = $embed->get($url);
258 |
259 | //The extractor have document and oembed:
260 | $document = $info->getDocument();
261 | $oembed = $info->getOEmbed();
262 | ```
263 |
264 | The `Extractor` class has many `Detectors`. Each detector is responsible to detect a specific piece of info. For example, there's a detector for the title, other for description, image, code, etc.
265 |
266 | So, an adapter is basically an extractor created specifically for a site. It can contains also custom detectors or apis. If you see the `src/Adapters` folder you can see all adapters.
267 |
268 | If you create an adapter, you need also register to Embed, so it knows in which website needs to use. To do that, there's the `ExtractorFactory` object, that is responsible for instantiate the right extractor for each site.
269 |
270 | ```php
271 | use Embed\Embed;
272 |
273 | $embed = new Embed();
274 |
275 | $factory = $embed->getExtractorFactory();
276 |
277 | //Use this MySite adapter for mysite.com
278 | $factory->addAdapter('mysite.com', MySite::class);
279 |
280 | //Remove the adapter for pinterest.com, so it will use the default extractor
281 | $factory->removeAdapter('pinterest.com');
282 |
283 | //Change the default extractor
284 | $factory->setDefault(CustomExtractor::class);
285 | ```
286 |
287 | ### Detectors
288 |
289 | Embed comes with several predefined detectors, but you may want to change or add more. Just create a class extending `Embed\Detectors\Detector` class and register it in the extractor factory. For example:
290 |
291 | ```php
292 | use Embed\Embed;
293 | use Embed\Detectors\Detector;
294 |
295 | class Robots extends Detector
296 | {
297 | public function detect(): ?string
298 | {
299 | $response = $this->extractor->getResponse();
300 | $metas = $this->extractor->getMetas();
301 |
302 | return $response->getHeaderLine('x-robots-tag'),
303 | ?: $metas->str('robots');
304 | }
305 | }
306 |
307 | //Register the detector
308 | $embed = new Embed();
309 | $embed->getExtractorFactory()->addDetector('robots', Robots::class);
310 |
311 | //Use it
312 | $info = $embed->get('http://example.com');
313 | $robots = $info->robots;
314 | ```
315 |
316 | ### Settings
317 |
318 | If you need to pass settings to the CurlClient to perform http queries:
319 |
320 | ```php
321 | use Embed\Embed;
322 | use Embed\Http\Crawler;
323 | use Embed\Http\CurlClient;
324 |
325 | $client = new CurlClient();
326 | $client->setSettings([
327 | 'cookies_path' => $cookies_path,
328 | 'ignored_errors' => [18],
329 | 'max_redirs' => 3, // see CURLOPT_MAXREDIRS
330 | 'connect_timeout' => 2, // see CURLOPT_CONNECTTIMEOUT
331 | 'timeout' => 2, // see CURLOPT_TIMEOUT
332 | 'ssl_verify_host' => 2, // see CURLOPT_SSL_VERIFYHOST
333 | 'ssl_verify_peer' => 1, // see CURLOPT_SSL_VERIFYPEER
334 | 'follow_location' => true, // see CURLOPT_FOLLOWLOCATION
335 | 'user_agent' => 'Mozilla', // see CURLOPT_USERAGENT
336 | ]);
337 |
338 | $embed = new Embed(new Crawler($client));
339 | ```
340 |
341 | If you need to pass settings to your detectors, you can add settings to the `ExtractorFactory`:
342 |
343 | ```php
344 | use Embed\Embed;
345 |
346 | $embed = new Embed();
347 | $embed->setSettings([
348 | 'oembed:query_parameters' => [], //Extra parameters send to oembed
349 | 'twitch:parent' => 'example.com', //Required to embed twitch videos as iframe
350 | 'facebook:token' => '1234|5678', //Required to embed content from Facebook
351 | 'instagram:token' => '1234|5678', //Required to embed content from Instagram
352 | 'twitter:token' => 'asdf', //Improve the data from twitter
353 | ]);
354 | $info = $embed->get($url);
355 | ```
356 |
357 | Note: The built-in detectors does not require settings. This feature is only for convenience if you create a specific detector that requires settings.
358 |
359 | ---
360 |
361 | [ico-version]: https://poser.pugx.org/embed/embed/v/stable
362 | [ico-license]: https://poser.pugx.org/embed/embed/license
363 | [ico-downloads]: https://poser.pugx.org/embed/embed/downloads
364 | [ico-m-downloads]: https://poser.pugx.org/embed/embed/d/monthly
365 |
366 | [link-packagist]: https://packagist.org/packages/embed/embed
367 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "embed/embed",
3 | "type": "library",
4 | "description": "PHP library to retrieve page info using oembed, opengraph, etc",
5 | "keywords": [
6 | "oembed",
7 | "opengraph",
8 | "twitter cards",
9 | "embed",
10 | "embedly"
11 | ],
12 | "homepage": "https://github.com/oscarotero/Embed",
13 | "license": "MIT",
14 | "authors": [
15 | {
16 | "name": "Oscar Otero",
17 | "email": "oom@oscarotero.com",
18 | "homepage": "http://oscarotero.com",
19 | "role": "Developer"
20 | }
21 | ],
22 | "support": {
23 | "email": "oom@oscarotero.com",
24 | "issues": "https://github.com/oscarotero/Embed/issues"
25 | },
26 | "require": {
27 | "php": "^7.4|^8",
28 | "ext-curl": "*",
29 | "ext-dom": "*",
30 | "ext-json": "*",
31 | "ext-mbstring": "*",
32 | "composer/ca-bundle": "^1.0",
33 | "oscarotero/html-parser": "^0.1.4",
34 | "psr/http-message": "^1.0|^2.0",
35 | "psr/http-client": "^1.0",
36 | "psr/http-factory": "^1.0",
37 | "ml/json-ld": "^1.1"
38 | },
39 | "require-dev": {
40 | "phpunit/phpunit": "^9.0",
41 | "friendsofphp/php-cs-fixer": "^2.0",
42 | "nyholm/psr7": "^1.2",
43 | "oscarotero/php-cs-fixer-config": "^1.0",
44 | "brick/varexporter": "^0.3.1",
45 | "symfony/css-selector": "^5.0"
46 | },
47 | "suggest": {
48 | "symfony/css-selector": "If you want to get elements using css selectors"
49 | },
50 | "autoload": {
51 | "psr-4": {
52 | "Embed\\": "src"
53 | },
54 | "files": [
55 | "src/functions.php"
56 | ]
57 | },
58 | "autoload-dev": {
59 | "psr-4": {
60 | "Embed\\Tests\\": "tests/"
61 | }
62 | },
63 | "scripts": {
64 | "demo": "php -S localhost:8888 demo/index.php",
65 | "test": "phpunit",
66 | "cs-fix": "php-cs-fixer fix",
67 | "update-resources": [
68 | "php scripts/update-oembed.php",
69 | "php scripts/update-suffix.php"
70 | ]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Adapters/Archive/Api.php:
--------------------------------------------------------------------------------
1 | endpoint = $this->extractor->getUri()->withQuery('output=json');
15 |
16 | return $this->fetchJSON($this->endpoint);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Adapters/Archive/Detectors/AuthorName.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('metadata', 'creator')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/Archive/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | extractor->getUri();
16 | $path = $uri->getPath();
17 |
18 | if (!matchPath('/details/*', $path)) {
19 | return null;
20 | }
21 |
22 | $src = $uri->withPath(str_replace('/details/', '/embed/', $path));
23 | $width = 640;
24 | $height = 480;
25 |
26 | $html = html('iframe', [
27 | 'src' => $src,
28 | 'width' => $width,
29 | 'height' => $height,
30 | 'style' => 'border:none',
31 | 'frameborder' => 0,
32 | 'allowTransparency' => 'true',
33 | ]);
34 |
35 | return new EmbedCode($html, $width, $height);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Adapters/Archive/Detectors/Description.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('metadata', 'extract')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/Archive/Detectors/ProviderName.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
14 |
15 | return $api->time('metadata', 'publicdate')
16 | ?: $api->time('metadata', 'addeddate')
17 | ?: $api->time('metadata', 'date')
18 | ?: parent::detect();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Adapters/Archive/Detectors/Title.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('metadata', 'title')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/Archive/Extractor.php:
--------------------------------------------------------------------------------
1 | api;
15 | }
16 |
17 | public function createCustomDetectors(): array
18 | {
19 | $this->api = new Api($this);
20 |
21 | return [
22 | 'title' => new Detectors\Title($this),
23 | 'description' => new Detectors\Description($this),
24 | 'code' => new Detectors\Code($this),
25 | 'authorName' => new Detectors\AuthorName($this),
26 | 'providerName' => new Detectors\ProviderName($this),
27 | 'publishedTime' => new Detectors\PublishedTime($this),
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Adapters/Bandcamp/Detectors/ProviderName.php:
--------------------------------------------------------------------------------
1 | new Detectors\ProviderName($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/CadenaSer/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
18 | }
19 |
20 | private function fallback(): ?EmbedCode
21 | {
22 | $uri = $this->extractor->getUri();
23 |
24 | if (!matchPath('/audio/*', $uri->getPath())) {
25 | return null;
26 | }
27 |
28 | $path = cleanPath('/widget/'.$uri->getPath());
29 | $src = $uri->withPath($path);
30 |
31 | $html = html('iframe', [
32 | 'src' => $src,
33 | 'frameborder' => 0,
34 | 'width' => '100%',
35 | 'height' => '360',
36 | 'allowTransparency' => 'true',
37 | ]);
38 |
39 | return new EmbedCode($html, null, 360);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Adapters/CadenaSer/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Code($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/Facebook/Detectors/Title.php:
--------------------------------------------------------------------------------
1 | extractor->getDocument();
16 | $oembed = $this->extractor->getOEmbed();
17 |
18 | return $oembed->str('title')
19 | ?: $document->select('.//head/title')->str();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Adapters/Facebook/Extractor.php:
--------------------------------------------------------------------------------
1 | oembed = new OEmbed($this);
13 |
14 | return [
15 | 'title' => new Detectors\Title($this),
16 | ];
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Adapters/Facebook/OEmbed.php:
--------------------------------------------------------------------------------
1 | extractor->getSetting('facebook:token');
18 |
19 | if (!$token) {
20 | return null;
21 | }
22 |
23 | $uri = $this->extractor->getUri();
24 | if (strpos($uri->getPath(), 'login') !== false) {
25 | parse_str($uri->getQuery(), $params);
26 | if (!empty($params['next'])) {
27 | $uri = $this->extractor->getCrawler()->createUri($params['next']);
28 | }
29 | }
30 | $queryParameters = $this->getOembedQueryParameters((string) $uri);
31 | $queryParameters['access_token'] = $token;
32 |
33 | return $this->extractor->getCrawler()
34 | ->createUri($this->getEndpointByPath($uri->getPath()))
35 | ->withQuery(http_build_query($queryParameters));
36 | }
37 |
38 | private function getEndpointByPath(string $path): string
39 | {
40 | /* Videos
41 | https://www.facebook.com/{page-name}/videos/{video-id}/
42 | https://www.facebook.com/{username}/videos/{video-id}/
43 | https://www.facebook.com/video.php?id={video-id}
44 | https://www.facebook.com/video.php?v={video-id}
45 | */
46 | if (strpos($path, '/video.php') === 0
47 | || strpos($path, '/videos/') !== false
48 | ) {
49 | return self::ENDPOINT_VIDEO;
50 | }
51 |
52 | /* Posts
53 | https://www.facebook.com/{page-name}/posts/{post-id}
54 | https://www.facebook.com/{username}/posts/{post-id}
55 | https://www.facebook.com/{username}/activity/{activity-id}
56 | https://www.facebook.com/photo.php?fbid={photo-id}
57 | https://www.facebook.com/photos/{photo-id}
58 | https://www.facebook.com/permalink.php?story_fbid={post-id}
59 | https://www.facebook.com/media/set?set={set-id}
60 | https://www.facebook.com/questions/{question-id}
61 | https://www.facebook.com/notes/{username}/{note-url}/{note-id}
62 |
63 | Not in the facebook docs:
64 | https://www.facebook.com/{page-name}/photos/{post-id}/{photo-id}
65 | */
66 | if (strpos($path, '/photo.php') === 0
67 | || strpos($path, '/photos/') !== false
68 | || strpos($path, '/permalink.php') === 0
69 | || strpos($path, '/media/') === 0
70 | || strpos($path, '/questions/') === 0
71 | || strpos($path, '/notes/') === 0
72 | || strpos($path, '/posts/') !== false
73 | || strpos($path, '/activity/') !== false
74 | ) {
75 | return self::ENDPOINT_POST;
76 | }
77 |
78 | return self::ENDPOINT_PAGE;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Adapters/Flickr/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
18 | }
19 |
20 | private function fallback(): ?EmbedCode
21 | {
22 | $uri = $this->extractor->getUri();
23 |
24 | if (!matchPath('/photos/*', $uri->getPath())) {
25 | return null;
26 | }
27 |
28 | $path = cleanPath($uri->getPath().'/player');
29 | $src = $uri->withPath($path);
30 | $width = 640;
31 | $height = 425;
32 |
33 | $html = html('iframe', [
34 | 'src' => $src,
35 | 'width' => $width,
36 | 'height' => $height,
37 | 'style' => 'border:none',
38 | 'frameborder' => 0,
39 | 'allowTransparency' => 'true',
40 | ]);
41 |
42 | return new EmbedCode($html, $width, $height);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Adapters/Flickr/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Code($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/Gist/Api.php:
--------------------------------------------------------------------------------
1 | extractor->getUri();
15 | $this->endpoint = $uri->withPath($uri->getPath().'.json');
16 |
17 | return $this->fetchJSON($this->endpoint);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Adapters/Gist/Detectors/AuthorName.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('owner')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/Gist/Detectors/AuthorUrl.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
14 | $owner = $api->str('owner');
15 |
16 | if ($owner) {
17 | return $this->extractor->getCrawler()->createUri("https://github.com/{$owner}");
18 | }
19 |
20 | return parent::detect();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Adapters/Gist/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
16 | }
17 |
18 | private function fallback(): ?EmbedCode
19 | {
20 | $api = $this->extractor->getApi();
21 |
22 | $code = $api->html('div');
23 | $stylesheet = $api->str('stylesheet');
24 |
25 | if ($code && $stylesheet) {
26 | return new EmbedCode(
27 | html('link', ['rel' => 'stylesheet', 'href' => $stylesheet]).$code
28 | );
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Adapters/Gist/Detectors/PublishedTime.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
14 |
15 | return $api->time('created_at')
16 | ?: parent::detect();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Adapters/Gist/Extractor.php:
--------------------------------------------------------------------------------
1 | api;
15 | }
16 |
17 | public function createCustomDetectors(): array
18 | {
19 | $this->api = new Api($this);
20 |
21 | return [
22 | 'authorName' => new Detectors\AuthorName($this),
23 | 'authorUrl' => new Detectors\AuthorUrl($this),
24 | 'publishedTime' => new Detectors\PublishedTime($this),
25 | 'code' => new Detectors\Code($this),
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Adapters/Github/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
17 | }
18 |
19 | private function fallback(): ?EmbedCode
20 | {
21 | $uri = $this->extractor->getUri();
22 | $path = $uri->getPath();
23 |
24 | if (!matchPath('/*/*/blob/*', $path)) {
25 | return null;
26 | }
27 |
28 | $dirs = explode('/', $path);
29 |
30 | $username = $dirs[1];
31 | $repo = $dirs[2];
32 | $ref = $dirs[4];
33 | $file = implode('/', array_slice($dirs, 5));
34 | $extension = pathinfo($file, PATHINFO_EXTENSION);
35 |
36 | switch ($extension) {
37 | case 'geojson':
38 | //https://help.github.com/articles/mapping-geojson-files-on-github/#embedding-your-map-elsewhere
39 | return new EmbedCode(html('script', ['src' => "https://embed.githubusercontent.com/view/geojson/{$username}/{$repo}/{$ref}/{$file}"]));
40 | case 'stl':
41 | //https://help.github.com/articles/3d-file-viewer/#embedding-your-model-elsewhere
42 | return new EmbedCode(html('script', ['src' => "https://embed.githubusercontent.com/view/3d/{$username}/{$repo}/{$ref}/{$file}"]));
43 | }
44 |
45 | return null;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Adapters/Github/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Code($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/Ideone/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
16 | }
17 |
18 | private function fallback(): ?EmbedCode
19 | {
20 | $uri = $this->extractor->getUri();
21 | $id = explode('/', $uri->getPath())[1];
22 |
23 | if (empty($id)) {
24 | return null;
25 | }
26 |
27 | return new EmbedCode(
28 | html('script', ['src' => "https://ideone.com/e.js/{$id}"])
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Adapters/Ideone/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Code($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/ImageShack/Api.php:
--------------------------------------------------------------------------------
1 | extractor->getUri();
17 |
18 | if (!matchPath('/i/*', $uri->getPath())) {
19 | $uri = $this->extractor->getRequest()->getUri();
20 |
21 | if (!matchPath('/i/*', $uri->getPath())) {
22 | return [];
23 | }
24 | }
25 |
26 | $id = getDirectory($uri->getPath(), 1);
27 |
28 | if (empty($id)) {
29 | return [];
30 | }
31 |
32 | $this->endpoint = $this->extractor->getCrawler()->createUri("https://api.imageshack.com/v2/images/{$id}");
33 | $data = $this->fetchJSON($this->endpoint);
34 | return $data['result'] ?? [];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Adapters/ImageShack/Detectors/AuthorName.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('owner', 'username')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/ImageShack/Detectors/AuthorUrl.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
14 | $owner = $api->str('owner', 'username');
15 |
16 | if ($owner) {
17 | return $this->extractor->getCrawler()->createUri("https://imageshack.com/{$owner}");
18 | }
19 |
20 | return parent::detect();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Adapters/ImageShack/Detectors/Description.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('description')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/ImageShack/Detectors/Image.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
14 |
15 | return $api->url('direct_link')
16 | ?: parent::detect();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Adapters/ImageShack/Detectors/ProviderName.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
14 |
15 | return $api->time('creation_date')
16 | ?: parent::detect();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Adapters/ImageShack/Detectors/Title.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('title')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/ImageShack/Extractor.php:
--------------------------------------------------------------------------------
1 | api;
15 | }
16 |
17 | public function createCustomDetectors(): array
18 | {
19 | $this->api = new Api($this);
20 |
21 | return [
22 | 'authorName' => new Detectors\AuthorName($this),
23 | 'authorUrl' => new Detectors\AuthorUrl($this),
24 | 'description' => new Detectors\Description($this),
25 | 'image' => new Detectors\Image($this),
26 | 'providerName' => new Detectors\ProviderName($this),
27 | 'publishedTime' => new Detectors\PublishedTime($this),
28 | 'title' => new Detectors\Title($this),
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Adapters/Instagram/Extractor.php:
--------------------------------------------------------------------------------
1 | oembed = new OEmbed($this);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Adapters/Instagram/OEmbed.php:
--------------------------------------------------------------------------------
1 | extractor->getSetting('instagram:token');
16 |
17 | if (!$token) {
18 | return null;
19 | }
20 |
21 | $uri = $this->extractor->getUri();
22 | if (strpos($uri->getPath(), 'login') !== false) {
23 | $uri = $this->extractor->getRequest()->getUri();
24 | }
25 |
26 | $queryParameters = $this->getOembedQueryParameters((string) $uri);
27 | $queryParameters['access_token'] = $token;
28 |
29 | return $this->extractor->getCrawler()
30 | ->createUri(self::ENDPOINT)
31 | ->withQuery(http_build_query($queryParameters));
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Adapters/Pinterest/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
17 | }
18 |
19 | private function fallback(): ?EmbedCode
20 | {
21 | $uri = $this->extractor->getUri();
22 |
23 | if (!matchPath('/pin/*', $uri->getPath())) {
24 | return null;
25 | }
26 |
27 | $html = [
28 | html('a', [
29 | 'data-pin-do' => 'embedPin',
30 | 'href' => $uri,
31 | ]),
32 | html('script', [
33 | 'async' => true,
34 | 'defer' => true,
35 | 'src' => '//assets.pinterest.com/js/pinit.js',
36 | ]),
37 | ];
38 |
39 | return new EmbedCode(implode('', $html), 236, 442);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Adapters/Pinterest/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Code($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/Sassmeister/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
17 | }
18 |
19 | private function fallback(): ?EmbedCode
20 | {
21 | $uri = $this->extractor->getUri();
22 |
23 | if (!matchPath('/gist/*', $uri->getPath())) {
24 | return null;
25 | }
26 |
27 | $id = explode('/', $uri->getPath())[2];
28 | $height = 480;
29 |
30 | $html = [
31 | html('p', [
32 | 'class' => 'sassmeister',
33 | 'data-gist-id' => $id,
34 | 'data-height' => $height,
35 | 'data-theme' => 'tomorrow',
36 | ], 'Play with this gist on SassMeister.'),
37 | html('script', [
38 | 'src' => 'http://cdn.sassmeister.com/js/embed.js',
39 | 'async' => true,
40 | ]),
41 | ];
42 |
43 | return new EmbedCode(implode('', $html), null, $height);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Adapters/Sassmeister/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Code($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/Slides/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
17 | }
18 |
19 | private function fallback(): EmbedCode
20 | {
21 | $uri = $this->extractor->getUri();
22 |
23 | $path = cleanPath($uri->getPath().'/embed');
24 | $src = $uri->withPath($path);
25 | $width = 576;
26 | $height = 420;
27 |
28 | $html = html('iframe', [
29 | 'src' => $src,
30 | 'width' => $width,
31 | 'height' => $height,
32 | 'style' => 'border:none',
33 | 'frameborder' => 0,
34 | 'allowTransparency' => 'true',
35 | ]);
36 |
37 | return new EmbedCode($html, $width, $height);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Adapters/Slides/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Code($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/Snipplr/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
17 | }
18 |
19 | private function fallback(): ?EmbedCode
20 | {
21 | $uri = $this->extractor->getUri();
22 |
23 | if (!matchPath('/view/*', $uri->getPath())) {
24 | return null;
25 | }
26 |
27 | $id = explode('/', $uri->getPath())[2];
28 |
29 | $html = [
30 | html('div', [
31 | 'id' => "snipplr_embed_{$id}",
32 | 'class' => 'snipplr_embed',
33 | ], 'View this snippet on Snipplr'),
34 | html('script', [
35 | 'type' => 'text/javascript',
36 | 'src' => 'https://snipplr.com/js/embed.js',
37 | ]),
38 | html('script', [
39 | 'type' => 'text/javascript',
40 | 'src' => "https://snipplr.com/json/{$id}",
41 | ]),
42 | ];
43 |
44 | return new EmbedCode(implode('', $html));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Adapters/Snipplr/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Code($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/Twitch/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | fallback();
16 | }
17 |
18 | private function fallback(): ?EmbedCode
19 | {
20 | $path = $this->extractor->getUri()->getPath();
21 | $parent = $this->extractor->getSetting('twitch:parent');
22 |
23 | if ($id = self::getVideoId($path)) {
24 | $code = $parent
25 | ? self::generateIframeCode(['id' => $id, 'parent' => $parent])
26 | : self::generateJsCode('video', $id);
27 | return new EmbedCode($code, 620, 378);
28 | }
29 |
30 | if ($id = self::getChannelId($path)) {
31 | $code = $parent
32 | ? self::generateIframeCode(['channel' => $id, 'parent' => $parent])
33 | : self::generateJsCode('channel', $id);
34 | return new EmbedCode($code, 620, 378);
35 | }
36 |
37 | return null;
38 | }
39 |
40 | private static function getVideoId(string $path): ?string
41 | {
42 | if (preg_match('#^/videos/(\d+)$#', $path, $matches)) {
43 | return $matches[1];
44 | }
45 |
46 | return null;
47 | }
48 |
49 | private static function getChannelId(string $path): ?string
50 | {
51 | if (preg_match('#^/(\w+)$#', $path, $matches)) {
52 | return $matches[1];
53 | }
54 |
55 | return null;
56 | }
57 |
58 | private static function generateIframeCode(array $params): string
59 | {
60 | $query = http_build_query(['autoplay' => 'false'] + $params);
61 |
62 | return html('iframe', [
63 | 'src' => "https://player.twitch.tv/?{$query}",
64 | 'frameborder' => 0,
65 | 'allowfullscreen' => 'true',
66 | 'scrolling' => 'no',
67 | 'height' => 378,
68 | 'width' => 620,
69 | ]);
70 | }
71 |
72 | private static function generateJsCode($key, $value)
73 | {
74 | return <<
76 |
77 |
80 | HTML;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Adapters/Twitch/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Code($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Adapters/Twitter/Api.php:
--------------------------------------------------------------------------------
1 | extractor->getSetting('twitter:token');
16 |
17 | if (!$token) {
18 | return [];
19 | }
20 |
21 | $uri = $this->extractor->getUri();
22 |
23 | $id = getDirectory($uri->getPath(), 2);
24 |
25 | if (empty($id)) {
26 | return [];
27 | }
28 |
29 | $this->extractor->getCrawler()->addDefaultHeaders(array('Authorization' => "Bearer $token"));
30 | $this->endpoint = $this->extractor->getCrawler()->createUri("https://api.twitter.com/2/tweets/{$id}?expansions=author_id,attachments.media_keys&tweet.fields=created_at&media.fields=preview_image_url,url&user.fields=id,name");
31 |
32 | return $this->fetchJSON($this->endpoint);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Adapters/Twitter/Detectors/AuthorName.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('includes', 'users', '0', 'name')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/Twitter/Detectors/AuthorUrl.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
14 | $username = $api->str('includes', 'users', '0', 'username');
15 |
16 | if ($username) {
17 | return $this->extractor->getCrawler()->createUri("https://twitter.com/{$username}");
18 | }
19 |
20 | return parent::detect();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Adapters/Twitter/Detectors/Description.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('data', 'text')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/Twitter/Detectors/Image.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
14 | $preview = $api->url('includes', 'media', '0', 'preview_image_url');
15 |
16 | if ($preview) {
17 | return $preview;
18 | }
19 |
20 | $regular = $api->url('includes', 'media', '0', 'url');
21 |
22 | if ($regular) {
23 | return $regular;
24 | }
25 |
26 | return parent::detect();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Adapters/Twitter/Detectors/ProviderName.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
14 |
15 | return $api->time('data', 'created_at')
16 | ?: parent::detect();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Adapters/Twitter/Detectors/Title.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 | $name = $api->str('includes', 'users', '0', 'name');
14 |
15 | if ($name) {
16 | return "Tweet by $name";
17 | }
18 |
19 | return parent::detect();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Adapters/Twitter/Extractor.php:
--------------------------------------------------------------------------------
1 | api;
15 | }
16 |
17 | public function createCustomDetectors(): array
18 | {
19 | $this->api = new Api($this);
20 |
21 | return [
22 | 'authorName' => new Detectors\AuthorName($this),
23 | 'authorUrl' => new Detectors\AuthorUrl($this),
24 | 'description' => new Detectors\Description($this),
25 | 'image' => new Detectors\Image($this),
26 | 'providerName' => new Detectors\ProviderName($this),
27 | 'publishedTime' => new Detectors\PublishedTime($this),
28 | 'title' => new Detectors\Title($this),
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Adapters/Wikipedia/Api.php:
--------------------------------------------------------------------------------
1 | extractor->getUri();
17 |
18 | if (!matchPath('/wiki/*', $uri->getPath())) {
19 | return [];
20 | }
21 |
22 | $titles = getDirectory($uri->getPath(), 1);
23 |
24 | $this->endpoint = $uri
25 | ->withPath('/w/api.php')
26 | ->withQuery(http_build_query([
27 | 'action' => 'query',
28 | 'format' => 'json',
29 | 'continue' => '',
30 | 'titles' => $titles,
31 | 'prop' => 'extracts',
32 | 'exchars' => 1000,
33 | ]));
34 |
35 | $data = $this->fetchJSON($this->endpoint);
36 | $pages = $data['query']['pages'] ?? null;
37 |
38 | return $pages ? current($pages) : null;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Adapters/Wikipedia/Detectors/Description.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('extract')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/Wikipedia/Detectors/Title.php:
--------------------------------------------------------------------------------
1 | extractor->getApi();
13 |
14 | return $api->str('title')
15 | ?: parent::detect();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Adapters/Wikipedia/Extractor.php:
--------------------------------------------------------------------------------
1 | api;
15 | }
16 |
17 | public function createCustomDetectors(): array
18 | {
19 | $this->api = new Api($this);
20 |
21 | return [
22 | 'title' => new Detectors\Title($this),
23 | 'description' => new Detectors\Description($this),
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Adapters/Youtube/Detectors/Feeds.php:
--------------------------------------------------------------------------------
1 | fallback();
20 | }
21 |
22 | private function fallback(): array
23 | {
24 | $uri = $this->extractor->getUri();
25 |
26 | if (!matchPath('/channel/*', $uri->getPath())) {
27 | return [];
28 | }
29 |
30 | $id = getDirectory($uri->getPath(), 1);
31 | $feed = $this->extractor->getCrawler()->createUri("https://www.youtube.com/feeds/videos.xml?channel_id={$id}");
32 |
33 | return [$feed];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Adapters/Youtube/Extractor.php:
--------------------------------------------------------------------------------
1 | new Detectors\Feeds($this),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/ApiTrait.php:
--------------------------------------------------------------------------------
1 | extractor = $extractor;
18 | }
19 |
20 | public function all(): array
21 | {
22 | if (!isset($this->data)) {
23 | $this->data = $this->fetchData();
24 | }
25 |
26 | return $this->data;
27 | }
28 |
29 | public function get(string ...$keys)
30 | {
31 | $data = $this->all();
32 |
33 | foreach ($keys as $key) {
34 | if (!isset($data[$key])) {
35 | return null;
36 | }
37 |
38 | $data = $data[$key];
39 | }
40 |
41 | return $data;
42 | }
43 |
44 | public function str(string ...$keys): ?string
45 | {
46 | $value = $this->get(...$keys);
47 |
48 | if (is_array($value)) {
49 | $value = array_shift($value);
50 | }
51 |
52 | return $value ? clean((string) $value) : null;
53 | }
54 |
55 | public function strAll(string ...$keys): array
56 | {
57 | $all = (array) $this->get(...$keys);
58 | return array_filter(array_map(fn ($value) => clean($value), $all));
59 | }
60 |
61 | public function html(string ...$keys): ?string
62 | {
63 | $value = $this->get(...$keys);
64 |
65 | if (is_array($value)) {
66 | $value = array_shift($value);
67 | }
68 |
69 | return $value ? clean((string) $value, true) : null;
70 | }
71 |
72 | public function int(string ...$keys): ?int
73 | {
74 | $value = $this->get(...$keys);
75 |
76 | if (is_array($value)) {
77 | $value = array_shift($value);
78 | }
79 |
80 | return is_numeric($value) ? (int) $value : null;
81 | }
82 |
83 | public function url(string ...$keys): ?UriInterface
84 | {
85 | $url = $this->str(...$keys);
86 |
87 | try {
88 | return $url ? $this->extractor->resolveUri($url) : null;
89 | } catch (Throwable $error) {
90 | return null;
91 | }
92 | }
93 |
94 | public function time(string ...$keys): ?DateTime
95 | {
96 | $time = $this->str(...$keys);
97 | $datetime = $time ? date_create($time) : null;
98 |
99 | if (!$datetime && $time && ctype_digit($time)) {
100 | $datetime = date_create_from_format('U', $time);
101 | }
102 |
103 | return ($datetime && $datetime->getTimestamp() > 0) ? $datetime : null;
104 | }
105 |
106 | abstract protected function fetchData(): array;
107 | }
108 |
--------------------------------------------------------------------------------
/src/Detectors/AuthorName.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
11 | $metas = $this->extractor->getMetas();
12 |
13 | return $oembed->str('author_name')
14 | ?: $metas->str(
15 | 'article:author',
16 | 'book:author',
17 | 'sailthru.author',
18 | 'lp.article:author',
19 | 'twitter:creator',
20 | 'dcterms.creator',
21 | 'author'
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Detectors/AuthorUrl.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
13 |
14 | return $oembed->url('author_url')
15 | ?: $this->detectFromTwitter();
16 | }
17 |
18 | private function detectFromTwitter(): ?UriInterface
19 | {
20 | $metas = $this->extractor->getMetas();
21 | $crawler = $this->extractor->getCrawler();
22 |
23 | $user = $metas->str('twitter:creator');
24 |
25 | return $user
26 | ? $crawler->createUri(sprintf('https://twitter.com/%s', ltrim($user, '@')))
27 | : null;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Detectors/Cms.php:
--------------------------------------------------------------------------------
1 | extractor->url->getHost());
16 |
17 | if ($cms) {
18 | return $cms;
19 | }
20 |
21 | $document = $this->extractor->getDocument();
22 | $generators = $document->select('.//meta', ['name' => 'generator'])->strAll('content');
23 |
24 | foreach ($generators as $generator) {
25 | if ($cms = self::detectFromGenerator($generator)) {
26 | return $cms;
27 | }
28 | }
29 |
30 | return null;
31 | }
32 |
33 | private static function detectFromHost(string $host): ?string
34 | {
35 | if (strpos($host, '.blogspot.com') !== false) {
36 | return self::BLOGSPOT;
37 | }
38 |
39 | if (strpos($host, '.wordpress.com') !== false) {
40 | return self::WORDPRESS;
41 | }
42 |
43 | return null;
44 | }
45 |
46 | private static function detectFromGenerator(string $generator): ?string
47 | {
48 | $generator = strtolower($generator);
49 |
50 | if ($generator === 'blogger') {
51 | return self::BLOGSPOT;
52 | }
53 |
54 | if (strpos($generator, 'mediawiki') === 0) {
55 | return self::MEDIAWIKI;
56 | }
57 |
58 | if (strpos($generator, 'wordpress') === 0) {
59 | return self::WORDPRESS;
60 | }
61 |
62 | if (strpos($generator, 'opennemas') === 0) {
63 | return self::OPENNEMAS;
64 | }
65 |
66 | return null;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Detectors/Code.php:
--------------------------------------------------------------------------------
1 | detectFromEmbed()
14 | ?: $this->detectFromOpenGraph()
15 | ?: $this->detectFromTwitter()
16 | ?: $this->detectFromContentType();
17 | }
18 |
19 | private function detectFromEmbed(): ?EmbedCode
20 | {
21 | $oembed = $this->extractor->getOEmbed();
22 | $html = $oembed->html('html');
23 |
24 | if (!$html) {
25 | return null;
26 | }
27 |
28 | return new EmbedCode(
29 | $html,
30 | $oembed->int('width'),
31 | $oembed->int('height')
32 | );
33 | }
34 |
35 | private function detectFromOpenGraph(): ?EmbedCode
36 | {
37 | $metas = $this->extractor->getMetas();
38 |
39 | $url = $metas->url('og:video:secure_url', 'og:video:url', 'og:video');
40 |
41 | if (!$url) {
42 | return null;
43 | }
44 |
45 | if (!($type = pathinfo($url->getPath(), PATHINFO_EXTENSION))) {
46 | $type = $metas->str('og:video_type');
47 | }
48 |
49 | $width = $metas->int('twitter:player:width');
50 | $height = $metas->int('twitter:player:height');
51 |
52 | switch ($type) {
53 | case 'swf':
54 | case 'application/x-shockwave-flash':
55 | return null; //Ignore flash
56 | case 'mp4':
57 | case 'ogg':
58 | case 'ogv':
59 | case 'webm':
60 | case 'application/mp4':
61 | case 'video/mp4':
62 | case 'video/ogg':
63 | case 'video/ogv':
64 | case 'video/webm':
65 | $code = html('video', [
66 | 'src' => $url,
67 | 'width' => $width,
68 | 'height' => $height,
69 | ]);
70 | break;
71 | default:
72 | $code = html('iframe', [
73 | 'src' => $url,
74 | 'frameborder' => 0,
75 | 'width' => $width,
76 | 'height' => $height,
77 | 'allowTransparency' => 'true',
78 | ]);
79 | }
80 |
81 | return new EmbedCode($code, $width, $height);
82 | }
83 |
84 | private function detectFromTwitter(): ?EmbedCode
85 | {
86 | $metas = $this->extractor->getMetas();
87 |
88 | $url = $metas->url('twitter:player');
89 |
90 | if (!$url) {
91 | return null;
92 | }
93 |
94 | $width = $metas->int('twitter:player:width');
95 | $height = $metas->int('twitter:player:height');
96 |
97 | $code = html('iframe', [
98 | 'src' => $url,
99 | 'frameborder' => 0,
100 | 'width' => $width,
101 | 'height' => $height,
102 | 'allowTransparency' => 'true',
103 | ]);
104 |
105 | return new EmbedCode($code, $width, $height);
106 | }
107 |
108 | private function detectFromContentType()
109 | {
110 | if (!$this->extractor->getResponse()->hasHeader('content-type')) {
111 | return null;
112 | }
113 |
114 | $contentType = $this->extractor->getResponse()->getHeader('content-type')[0];
115 | $isBinary = !preg_match('/(text|html|json)/', strtolower($contentType));
116 | if (!$isBinary) {
117 | return null;
118 | }
119 |
120 | $url = $this->extractor->getRequest()->getUri();
121 |
122 | if (strpos($contentType, 'video/') === 0 || $contentType === 'application/mp4') {
123 | $code = html('video', [
124 | 'src' => $url,
125 | 'controls' => true,
126 | ]);
127 | } elseif (strpos($contentType, 'audio/') === 0) {
128 | $code = html('audio', [
129 | 'src' => $url,
130 | 'controls' => true,
131 | ]);
132 | } elseif (strpos($contentType, 'image/') === 0) {
133 | $code = html('img', [
134 | 'src' => $url,
135 | ]);
136 | } else {
137 | return null;
138 | }
139 |
140 | return new EmbedCode($code);
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/Detectors/Description.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
11 | $metas = $this->extractor->getMetas();
12 | $ld = $this->extractor->getLinkedData();
13 |
14 | return $oembed->str('description')
15 | ?: $metas->str(
16 | 'og:description',
17 | 'twitter:description',
18 | 'lp:description',
19 | 'description',
20 | 'article:description',
21 | 'dcterms.description',
22 | 'sailthru.description',
23 | 'excerpt',
24 | 'article.summary'
25 | )
26 | ?: $ld->str('description');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Detectors/Detector.php:
--------------------------------------------------------------------------------
1 | extractor = $extractor;
16 | }
17 |
18 | public function get()
19 | {
20 | if (!isset($this->cache)) {
21 | $this->cache = [
22 | 'cached' => true,
23 | 'value' => $this->detect(),
24 | ];
25 | }
26 |
27 | return $this->cache['value'];
28 | }
29 |
30 | abstract public function detect();
31 | }
32 |
--------------------------------------------------------------------------------
/src/Detectors/Favicon.php:
--------------------------------------------------------------------------------
1 | extractor->getDocument();
13 |
14 | return $document->link('shortcut icon')
15 | ?: $document->link('icon')
16 | ?: $this->extractor->getUri()->withPath('/favicon.ico')->withQuery('');
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Detectors/Feeds.php:
--------------------------------------------------------------------------------
1 | extractor->getDocument();
23 | $feeds = [];
24 |
25 | foreach (self::$types as $type) {
26 | $href = $document->link('alternate', ['type' => $type]);
27 |
28 | if ($href) {
29 | $feeds[] = $href;
30 | }
31 | }
32 |
33 | return $feeds;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Detectors/Icon.php:
--------------------------------------------------------------------------------
1 | extractor->getDocument();
13 |
14 | return $document->link('apple-touch-icon-precomposed')
15 | ?: $document->link('apple-touch-icon')
16 | ?: $document->link('icon', ['sizes' => '144x144'])
17 | ?: $document->link('icon', ['sizes' => '96x96'])
18 | ?: $document->link('icon', ['sizes' => '48x48']);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Detectors/Image.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
13 | $document = $this->extractor->getDocument();
14 | $metas = $this->extractor->getMetas();
15 | $ld = $this->extractor->getLinkedData();
16 |
17 | return $oembed->url('image')
18 | ?: $oembed->url('thumbnail')
19 | ?: $oembed->url('thumbnail_url')
20 | ?: $metas->url('og:image', 'og:image:url', 'og:image:secure_url', 'twitter:image', 'twitter:image:src', 'lp:image')
21 | ?: $document->link('image_src')
22 | ?: $ld->url('image.url')
23 | ?: $this->detectFromContentType();
24 | }
25 |
26 | private function detectFromContentType()
27 | {
28 | if (!$this->extractor->getResponse()->hasHeader('content-type')) {
29 | return null;
30 | }
31 |
32 | $contentType = $this->extractor->getResponse()->getHeader('content-type')[0];
33 |
34 | if (strpos($contentType, 'image/') === 0) {
35 | return $this->extractor->getUri();
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Detectors/Keywords.php:
--------------------------------------------------------------------------------
1 | extractor->getMetas();
12 | $ld = $this->extractor->getLinkedData();
13 |
14 | $types = [
15 | 'keywords',
16 | 'og:video:tag',
17 | 'og:article:tag',
18 | 'og:video:tag',
19 | 'og:book:tag',
20 | 'lp.article:section',
21 | 'dcterms.subject',
22 | ];
23 |
24 | foreach ($types as $type) {
25 | $value = $metas->strAll($type);
26 |
27 | if ($value) {
28 | $tags = array_merge($tags, self::toArray($value));
29 | }
30 | }
31 |
32 | $value = $ld->strAll('keywords');
33 |
34 | if ($value) {
35 | $tags = array_merge($tags, self::toArray($value));
36 | }
37 |
38 | $tags = array_map('mb_strtolower', $tags);
39 | $tags = array_unique($tags);
40 | $tags = array_filter($tags);
41 | $tags = array_values($tags);
42 |
43 | return $tags;
44 | }
45 |
46 | private static function toArray(array $keywords): array
47 | {
48 | $all = [];
49 |
50 | foreach ($keywords as $keyword) {
51 | $tags = explode(',', $keyword);
52 | $tags = array_map('trim', $tags);
53 | $tags = array_filter(
54 | $tags,
55 | fn ($value) => !empty($value) && substr($value, -3) !== '...'
56 | );
57 |
58 | $all = array_merge($all, $tags);
59 | }
60 |
61 | return $all;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Detectors/Language.php:
--------------------------------------------------------------------------------
1 | extractor->getDocument();
11 | $metas = $this->extractor->getMetas();
12 | $ld = $this->extractor->getLinkedData();
13 |
14 | return $document->select('/html')->str('lang')
15 | ?: $document->select('/html')->str('xml:lang')
16 | ?: $metas->str('language', 'lang', 'og:locale', 'dc:language')
17 | ?: $document->select('.//meta', ['http-equiv' => 'content-language'])->str('content')
18 | ?: $ld->str('inLanguage');
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Detectors/Languages.php:
--------------------------------------------------------------------------------
1 | extractor->getDocument();
16 | $languages = [];
17 |
18 | foreach ($document->select('.//link[@hreflang]')->nodes() as $node) {
19 | $language = $node->getAttribute('hreflang');
20 | $href = $node->getAttribute('href');
21 |
22 | if (isEmpty($language, $href)) {
23 | continue;
24 | }
25 |
26 | $languages[$language] = $this->extractor->resolveUri($href);
27 | }
28 |
29 | return $languages;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Detectors/License.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
11 | $metas = $this->extractor->getMetas();
12 |
13 | return $oembed->str('license_url')
14 | ?: $metas->str('copyright');
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Detectors/ProviderName.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
13 | $metas = $this->extractor->getMetas();
14 |
15 | return $oembed->str('provider_name')
16 | ?: $metas->str(
17 | 'og:site_name',
18 | 'dcterms.publisher',
19 | 'publisher',
20 | 'article:publisher'
21 | )
22 | ?: ucfirst($this->fallback());
23 | }
24 |
25 | private function fallback(): string
26 | {
27 | $host = $this->extractor->getUri()->getHost();
28 |
29 | $host = array_reverse(explode('.', $host));
30 |
31 | switch (count($host)) {
32 | case 1:
33 | return $host[0];
34 | case 2:
35 | return $host[1];
36 | default:
37 | $tld = $host[1].'.'.$host[0];
38 | $suffixes = self::getSuffixes();
39 |
40 | if (in_array($tld, $suffixes, true)) {
41 | return $host[2];
42 | }
43 |
44 | return $host[1];
45 | }
46 | }
47 |
48 | private static function getSuffixes(): array
49 | {
50 | if (!isset(self::$suffixes)) {
51 | self::$suffixes = require dirname(__DIR__).'/resources/suffix.php';
52 | }
53 |
54 | return self::$suffixes;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Detectors/ProviderUrl.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
13 | $metas = $this->extractor->getMetas();
14 |
15 | return $oembed->url('provider_url')
16 | ?: $metas->url('og:website')
17 | ?: $this->fallback();
18 | }
19 |
20 | private function fallback(): UriInterface
21 | {
22 | return $this->extractor->getUri()->withPath('')->withQuery('')->withFragment('');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Detectors/PublishedTime.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
13 | $metas = $this->extractor->getMetas();
14 | $ld = $this->extractor->getLinkedData();
15 |
16 | return $oembed->time('pubdate')
17 | ?: $metas->time(
18 | 'article:published_time',
19 | 'created',
20 | 'date',
21 | 'datepublished',
22 | 'music:release_date',
23 | 'video:release_date',
24 | 'newsrepublic:publish_date'
25 | )
26 | ?: $ld->time(
27 | 'pagePublished',
28 | 'datePublished'
29 | )
30 | ?: $this->detectFromPath()
31 | ?: $metas->time(
32 | 'pagerender',
33 | 'pub_date',
34 | 'publication-date',
35 | 'lp.article:published_time',
36 | 'lp.article:modified_time',
37 | 'publish-date',
38 | 'rc.datecreation',
39 | 'timestamp',
40 | 'sailthru.date',
41 | 'article:modified_time',
42 | 'dcterms.date'
43 | );
44 | }
45 |
46 | /**
47 | * Some sites using WordPress have the published time in the url
48 | * For example: mysite.com/2020/05/19/post-title
49 | */
50 | private function detectFromPath(): ?DateTime
51 | {
52 | $path = $this->extractor->getUri()->getPath();
53 |
54 | if (preg_match('#/(19|20)\d{2}/[0-1]?\d/[0-3]?\d/#', $path, $matches)) {
55 | return date_create_from_format('/Y/m/d/', $matches[0]) ?: null;
56 | }
57 |
58 | return null;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Detectors/Redirect.php:
--------------------------------------------------------------------------------
1 | extractor->getDocument();
13 | $value = $document->select('.//meta', ['http-equiv' => 'refresh'])->str('content');
14 |
15 | return $value ? $this->extract($value) : null;
16 | }
17 |
18 | private function extract(string $value): ?UriInterface
19 | {
20 | if (preg_match('/url=(.+)$/i', $value, $match)) {
21 | return $this->extractor->resolveUri(trim($match[1], '\'"'));
22 | }
23 |
24 | return null;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Detectors/Title.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
11 | $document = $this->extractor->getDocument();
12 | $metas = $this->extractor->getMetas();
13 |
14 | return $oembed->str('title')
15 | ?: $metas->str(
16 | 'og:title',
17 | 'twitter:title',
18 | 'lp:title',
19 | 'dcterms.title',
20 | 'article:title',
21 | 'headline',
22 | 'article.headline',
23 | 'parsely-title'
24 | )
25 | ?: $document->select('.//head/title')->str();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Detectors/Url.php:
--------------------------------------------------------------------------------
1 | extractor->getOEmbed();
13 |
14 | return $oembed->url('url')
15 | ?: $oembed->url('web_page')
16 | ?: $this->extractor->getUri();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Document.php:
--------------------------------------------------------------------------------
1 | extractor = $extractor;
24 |
25 | $html = (string) $extractor->getResponse()->getBody();
26 | $html = str_replace('
', "\n
", $html);
27 | $html = str_replace('
getResponse()->getHeaderLine('content-type');
31 | preg_match('/charset=(?:"|\')?(.*?)(?=$|\s|;|"|\'|>)/i', $contentType, $match);
32 | if (!empty($match[1])) {
33 | $encoding = trim($match[1], ',');
34 | try {
35 | $ret = mb_encoding_aliases($encoding ?? '');
36 | if ($ret === false) {
37 | $encoding = null;
38 | }
39 | } catch (\ValueError $exception) {
40 | $encoding = null;
41 | }
42 | }
43 | if (is_null($encoding) && !empty($html)) {
44 | preg_match('/charset=(?:"|\')?(.*?)(?=$|\s|;|"|\'|>)/i', $html, $match);
45 | if (!empty($match[1])) {
46 | $encoding = trim($match[1], ',');
47 | }
48 | try {
49 | $ret = mb_encoding_aliases($encoding ?? '');
50 | if ($ret === false) {
51 | $encoding = null;
52 | }
53 | } catch (\ValueError $exception) {
54 | $encoding = null;
55 | }
56 | }
57 | $this->document = !empty($html) ? Parser::parse($html, $encoding) : new DOMDocument();
58 | $this->initXPath();
59 | }
60 |
61 | private function initXPath()
62 | {
63 | $this->xpath = new DOMXPath($this->document);
64 | $this->xpath->registerNamespace('php', 'http://php.net/xpath');
65 | $this->xpath->registerPhpFunctions();
66 | }
67 |
68 | public function __clone()
69 | {
70 | $this->document = clone $this->document;
71 | $this->initXPath();
72 | }
73 |
74 | public function remove(string $query): void
75 | {
76 | $nodes = iterator_to_array($this->xpath->query($query), false);
77 |
78 | foreach ($nodes as $node) {
79 | $node->parentNode->removeChild($node);
80 | }
81 | }
82 |
83 | public function removeCss(string $query): void
84 | {
85 | $this->remove(self::cssToXpath($query));
86 | }
87 |
88 | public function getDocument(): DOMDocument
89 | {
90 | return $this->document;
91 | }
92 |
93 | /**
94 | * Helper to build xpath queries easily and case insensitive
95 | */
96 | private static function buildQuery(string $startQuery, array $attributes): string
97 | {
98 | $selector = [$startQuery];
99 |
100 | foreach ($attributes as $name => $value) {
101 | $selector[] = sprintf('[php:functionString("strtolower", @%s)="%s"]', $name, mb_strtolower($value));
102 | }
103 |
104 | return implode('', $selector);
105 | }
106 |
107 | /**
108 | * Select a element in the dom
109 | */
110 | public function select(string $query, ?array $attributes = null, ?DOMNode $context = null): QueryResult
111 | {
112 | if (!empty($attributes)) {
113 | $query = self::buildQuery($query, $attributes);
114 | }
115 |
116 | return new QueryResult($this->xpath->query($query, $context), $this->extractor);
117 | }
118 |
119 | /**
120 | * Select a element in the dom using a css selector
121 | */
122 | public function selectCss(string $query, ?DOMNode $context = null): QueryResult
123 | {
124 | return $this->select(self::cssToXpath($query), null, $context);
125 | }
126 |
127 | /**
128 | * Shortcut to select a element and return the href
129 | */
130 | public function link(string $rel, array $extra = []): ?UriInterface
131 | {
132 | return $this->select('.//link', ['rel' => $rel] + $extra)->url('href');
133 | }
134 |
135 | public function __toString(): string
136 | {
137 | return Parser::stringify($this->getDocument());
138 | }
139 |
140 | private static function cssToXpath(string $selector): string
141 | {
142 | if (!isset(self::$cssConverter)) {
143 | if (!class_exists(CssSelectorConverter::class)) {
144 | throw new RuntimeException('You need to install "symfony/css-selector" to use css selectors');
145 | }
146 |
147 | self::$cssConverter = new CssSelectorConverter();
148 | }
149 |
150 | return self::$cssConverter->toXpath($selector);
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Embed.php:
--------------------------------------------------------------------------------
1 | crawler = $crawler ?: new Crawler();
18 | $this->extractorFactory = $extractorFactory ?: new ExtractorFactory();
19 | }
20 |
21 | public function get(string $url): Extractor
22 | {
23 | $request = $this->crawler->createRequest('GET', $url);
24 | $response = $this->crawler->sendRequest($request);
25 |
26 | return $this->extract($request, $response);
27 | }
28 |
29 | /**
30 | * @return Extractor[]
31 | */
32 | public function getMulti(string ...$urls): array
33 | {
34 | $requests = array_map(
35 | fn ($url) => $this->crawler->createRequest('GET', $url),
36 | $urls
37 | );
38 |
39 | $responses = $this->crawler->sendRequests(...$requests);
40 |
41 | $return = [];
42 |
43 | foreach ($responses as $k => $response) {
44 | $return[] = $this->extract($requests[$k], $responses[$k]);
45 | }
46 |
47 | return $return;
48 | }
49 |
50 | public function getCrawler(): Crawler
51 | {
52 | return $this->crawler;
53 | }
54 |
55 | public function getExtractorFactory(): ExtractorFactory
56 | {
57 | return $this->extractorFactory;
58 | }
59 |
60 | public function setSettings(array $settings): void
61 | {
62 | $this->extractorFactory->setSettings($settings);
63 | }
64 |
65 | private function extract(RequestInterface $request, ResponseInterface $response, bool $redirect = true): Extractor
66 | {
67 | $uri = $this->crawler->getResponseUri($response) ?: $request->getUri();
68 |
69 | $extractor = $this->extractorFactory->createExtractor($uri, $request, $response, $this->crawler);
70 |
71 | if (!$redirect || !$this->mustRedirect($extractor)) {
72 | return $extractor;
73 | }
74 |
75 | $request = $this->crawler->createRequest('GET', $extractor->redirect);
76 | $response = $this->crawler->sendRequest($request);
77 |
78 | return $this->extract($request, $response, false);
79 | }
80 |
81 | private function mustRedirect(Extractor $extractor): bool
82 | {
83 | if (!empty($extractor->getOembed()->all())) {
84 | return false;
85 | }
86 |
87 | return $extractor->redirect !== null;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/EmbedCode.php:
--------------------------------------------------------------------------------
1 | html = $html;
19 | $this->width = $width;
20 | $this->height = $height;
21 |
22 | if ($width && $height) {
23 | $this->ratio = round(($height / $width) * 100, 3);
24 | }
25 | }
26 |
27 | public function __toString(): string
28 | {
29 | return $this->html;
30 | }
31 |
32 | #[ReturnTypeWillChange]
33 | public function jsonSerialize()
34 | {
35 | return [
36 | 'html' => $this->html,
37 | 'width' => $this->width,
38 | 'height' => $this->height,
39 | 'ratio' => $this->ratio,
40 | ];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Extractor.php:
--------------------------------------------------------------------------------
1 | uri = $uri;
95 | $this->request = $request;
96 | $this->response = $response;
97 | $this->crawler = $crawler;
98 |
99 | //APIs
100 | $this->document = new Document($this);
101 | $this->oembed = new OEmbed($this);
102 | $this->linkedData = new LinkedData($this);
103 | $this->metas = new Metas($this);
104 |
105 | //Detectors
106 | $this->authorName = new AuthorName($this);
107 | $this->authorUrl = new AuthorUrl($this);
108 | $this->cms = new Cms($this);
109 | $this->code = new Code($this);
110 | $this->description = new Description($this);
111 | $this->favicon = new Favicon($this);
112 | $this->feeds = new Feeds($this);
113 | $this->icon = new Icon($this);
114 | $this->image = new Image($this);
115 | $this->keywords = new Keywords($this);
116 | $this->language = new Language($this);
117 | $this->languages = new Languages($this);
118 | $this->license = new License($this);
119 | $this->providerName = new ProviderName($this);
120 | $this->providerUrl = new ProviderUrl($this);
121 | $this->publishedTime = new PublishedTime($this);
122 | $this->redirect = new Redirect($this);
123 | $this->title = new Title($this);
124 | $this->url = new Url($this);
125 | }
126 |
127 | public function __get(string $name)
128 | {
129 | $detector = $this->customDetectors[$name] ?? $this->$name ?? null;
130 |
131 | if (!$detector || !($detector instanceof Detector)) {
132 | throw new DomainException(sprintf('Invalid key "%s". No detector found for this value', $name));
133 | }
134 |
135 | return $detector->get();
136 | }
137 |
138 | public function createCustomDetectors(): array
139 | {
140 | return [];
141 | }
142 |
143 | public function addDetector(string $name, Detector $detector): void
144 | {
145 | $this->customDetectors[$name] = $detector;
146 | }
147 |
148 | public function setSettings(array $settings): void
149 | {
150 | $this->settings = $settings;
151 | }
152 |
153 | public function getSettings(): array
154 | {
155 | return $this->settings;
156 | }
157 |
158 | public function getSetting(string $key)
159 | {
160 | return $this->settings[$key] ?? null;
161 | }
162 |
163 | public function getDocument(): Document
164 | {
165 | return $this->document;
166 | }
167 |
168 | public function getOEmbed(): OEmbed
169 | {
170 | return $this->oembed;
171 | }
172 |
173 | public function getLinkedData(): LinkedData
174 | {
175 | return $this->linkedData;
176 | }
177 |
178 | public function getMetas(): Metas
179 | {
180 | return $this->metas;
181 | }
182 |
183 | public function getRequest(): RequestInterface
184 | {
185 | return $this->request;
186 | }
187 |
188 | public function getResponse(): ResponseInterface
189 | {
190 | return $this->response;
191 | }
192 |
193 | public function getUri(): UriInterface
194 | {
195 | return $this->uri;
196 | }
197 |
198 | /**
199 | * @param UriInterface|string $uri
200 | */
201 | public function resolveUri($uri): UriInterface
202 | {
203 | if (is_string($uri)) {
204 | if (!isHttp($uri)) {
205 | throw new InvalidArgumentException(sprintf('Uri string must use http or https scheme (%s)', $uri));
206 | }
207 |
208 | $uri = $this->crawler->createUri($uri);
209 | }
210 |
211 | if (!($uri instanceof UriInterface)) {
212 | throw new InvalidArgumentException('Uri must be a string or an instance of UriInterface');
213 | }
214 |
215 | return resolveUri($this->uri, $uri);
216 | }
217 |
218 | public function getCrawler(): Crawler
219 | {
220 | return $this->crawler;
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/src/ExtractorFactory.php:
--------------------------------------------------------------------------------
1 | Adapters\Slides\Extractor::class,
16 | 'pinterest.com' => Adapters\Pinterest\Extractor::class,
17 | 'flickr.com' => Adapters\Flickr\Extractor::class,
18 | 'snipplr.com' => Adapters\Snipplr\Extractor::class,
19 | 'play.cadenaser.com' => Adapters\CadenaSer\Extractor::class,
20 | 'ideone.com' => Adapters\Ideone\Extractor::class,
21 | 'gist.github.com' => Adapters\Gist\Extractor::class,
22 | 'github.com' => Adapters\Github\Extractor::class,
23 | 'wikipedia.org' => Adapters\Wikipedia\Extractor::class,
24 | 'archive.org' => Adapters\Archive\Extractor::class,
25 | 'sassmeister.com' => Adapters\Sassmeister\Extractor::class,
26 | 'facebook.com' => Adapters\Facebook\Extractor::class,
27 | 'instagram.com' => Adapters\Instagram\Extractor::class,
28 | 'imageshack.com' => Adapters\ImageShack\Extractor::class,
29 | 'youtube.com' => Adapters\Youtube\Extractor::class,
30 | 'twitch.tv' => Adapters\Twitch\Extractor::class,
31 | 'bandcamp.com' => Adapters\Bandcamp\Extractor::class,
32 | 'twitter.com' => Adapters\Twitter\Extractor::class,
33 | 'x.com' => Adapters\Twitter\Extractor::class,
34 | ];
35 | private array $customDetectors = [];
36 | private array $settings;
37 |
38 | public function __construct(?array $settings = [])
39 | {
40 | $this->settings = $settings ?? [];
41 | }
42 |
43 | public function createExtractor(UriInterface $uri, RequestInterface $request, ResponseInterface $response, Crawler $crawler): Extractor
44 | {
45 | $host = $uri->getHost();
46 | $class = $this->default;
47 |
48 | foreach ($this->adapters as $adapterHost => $adapter) {
49 | // Check if $host is the same domain as $adapterHost.
50 | if ($host === $adapterHost) {
51 | $class = $adapter;
52 | break;
53 | }
54 |
55 | // Check if $host is a subdomain of $adapterHost.
56 | if (substr($host, -strlen($adapterHost) - 1) === ".{$adapterHost}") {
57 | $class = $adapter;
58 | break;
59 | }
60 | }
61 |
62 | /** @var Extractor $extractor */
63 | $extractor = new $class($uri, $request, $response, $crawler);
64 | $extractor->setSettings($this->settings);
65 |
66 | foreach ($this->customDetectors as $name => $detector) {
67 | $extractor->addDetector($name, new $detector($extractor));
68 | }
69 |
70 | foreach ($extractor->createCustomDetectors() as $name => $detector) {
71 | $extractor->addDetector($name, $detector);
72 | }
73 |
74 | return $extractor;
75 | }
76 |
77 | public function addAdapter(string $pattern, string $class): void
78 | {
79 | $this->adapters[$pattern] = $class;
80 | }
81 |
82 | public function addDetector(string $name, string $class): void
83 | {
84 | $this->customDetectors[$name] = $class;
85 | }
86 |
87 | public function removeAdapter(string $pattern): void
88 | {
89 | unset($this->adapters[$pattern]);
90 | }
91 |
92 | public function setDefault(string $class): void
93 | {
94 | $this->default = $class;
95 | }
96 |
97 | public function setSettings(array $settings): void
98 | {
99 | $this->settings = $settings;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Http/Crawler.php:
--------------------------------------------------------------------------------
1 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:73.0) Gecko/20100101 Firefox/73.0',
20 | 'Cache-Control' => 'max-age=0',
21 | ];
22 |
23 | public function __construct(?ClientInterface $client = null, ?RequestFactoryInterface $requestFactory = null, ?UriFactoryInterface $uriFactory = null)
24 | {
25 | $this->client = $client ?: new CurlClient();
26 | $this->requestFactory = $requestFactory ?: FactoryDiscovery::getRequestFactory();
27 | $this->uriFactory = $uriFactory ?: FactoryDiscovery::getUriFactory();
28 | }
29 |
30 | public function addDefaultHeaders(array $headers): void
31 | {
32 | $this->defaultHeaders = $headers + $this->defaultHeaders;
33 | }
34 |
35 | /**
36 | * @param UriInterface|string $uri The URI associated with the request.
37 | */
38 | public function createRequest(string $method, $uri): RequestInterface
39 | {
40 | $request = $this->requestFactory->createRequest($method, $uri);
41 |
42 | foreach ($this->defaultHeaders as $name => $value) {
43 | $request = $request->withHeader($name, $value);
44 | }
45 |
46 | return $request;
47 | }
48 |
49 | public function createUri(string $uri = ''): UriInterface
50 | {
51 | return $this->uriFactory->createUri($uri);
52 | }
53 |
54 | public function sendRequest(RequestInterface $request): ResponseInterface
55 | {
56 | return $this->client->sendRequest($request);
57 | }
58 |
59 | public function sendRequests(RequestInterface ...$requests): array
60 | {
61 | if ($this->client instanceof CurlClient) {
62 | return $this->client->sendRequests(...$requests);
63 | }
64 |
65 | return array_map(
66 | fn ($request) => $this->client->sendRequest($request),
67 | $requests
68 | );
69 | }
70 |
71 | public function getResponseUri(ResponseInterface $response): ?UriInterface
72 | {
73 | $location = $response->getHeaderLine('Content-Location');
74 |
75 | return $location ? $this->uriFactory->createUri($location) : null;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Http/CurlClient.php:
--------------------------------------------------------------------------------
1 | responseFactory = $responseFactory ?: FactoryDiscovery::getResponseFactory();
22 | }
23 |
24 | public function setSettings(array $settings): void
25 | {
26 | $this->settings = $settings + $this->settings;
27 | }
28 |
29 | public function sendRequest(RequestInterface $request): ResponseInterface
30 | {
31 | $responses = CurlDispatcher::fetch($this->settings, $this->responseFactory, $request);
32 |
33 | return $responses[0];
34 | }
35 |
36 | public function sendRequests(RequestInterface ...$request): array
37 | {
38 | return CurlDispatcher::fetch($this->settings, $this->responseFactory, ...$request);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Http/CurlDispatcher.php:
--------------------------------------------------------------------------------
1 | curl);
38 | return [$connection->getResponse($responseFactory)];
39 | }
40 |
41 | //Init connections
42 | $multi = curl_multi_init();
43 | $connections = [];
44 |
45 | foreach ($requests as $request) {
46 | $connection = new static($settings, $request);
47 | curl_multi_add_handle($multi, $connection->curl);
48 |
49 | $connections[] = $connection;
50 | }
51 |
52 | //Run
53 | $active = null;
54 | do {
55 | $status = curl_multi_exec($multi, $active);
56 |
57 | if ($active) {
58 | curl_multi_select($multi);
59 | }
60 |
61 | $info = curl_multi_info_read($multi);
62 |
63 | if ($info) {
64 | foreach ($connections as $connection) {
65 | if ($connection->curl === $info['handle']) {
66 | $connection->result = $info['result'];
67 | break;
68 | }
69 | }
70 | }
71 | } while ($active && $status == CURLM_OK);
72 |
73 | //Close connections
74 | foreach ($connections as $connection) {
75 | curl_multi_remove_handle($multi, $connection->curl);
76 | }
77 |
78 | curl_multi_close($multi);
79 |
80 | return array_map(
81 | fn ($connection) => $connection->getResponse($responseFactory),
82 | $connections
83 | );
84 | }
85 |
86 | private function __construct(array $settings, RequestInterface $request, ?StreamFactoryInterface $streamFactory = null)
87 | {
88 | $this->request = $request;
89 | $this->curl = curl_init((string) $request->getUri());
90 | $this->settings = $settings;
91 | $this->streamFactory = $streamFactory ?? FactoryDiscovery::getStreamFactory();
92 |
93 | $cookies = $settings['cookies_path'] ?? str_replace('//', '/', sys_get_temp_dir().'/embed-cookies.txt');
94 |
95 | curl_setopt_array($this->curl, [
96 | CURLOPT_HTTPHEADER => $this->getRequestHeaders(),
97 | CURLOPT_POST => strtoupper($request->getMethod()) === 'POST',
98 | CURLOPT_MAXREDIRS => $settings['max_redirs'] ?? 10,
99 | CURLOPT_CONNECTTIMEOUT => $settings['connect_timeout'] ?? 10,
100 | CURLOPT_TIMEOUT => $settings['timeout'] ?? 10,
101 | CURLOPT_RETURNTRANSFER => true,
102 | CURLOPT_SSL_VERIFYHOST => $settings['ssl_verify_host'] ?? 0,
103 | CURLOPT_SSL_VERIFYPEER => $settings['ssl_verify_peer'] ?? false,
104 | CURLOPT_ENCODING => '',
105 | CURLOPT_CAINFO => CaBundle::getSystemCaRootBundlePath(),
106 | CURLOPT_AUTOREFERER => true,
107 | CURLOPT_FOLLOWLOCATION => $settings['follow_location'] ?? true,
108 | CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
109 | CURLOPT_USERAGENT => $settings['user_agent'] ?? $request->getHeaderLine('User-Agent'),
110 | CURLOPT_COOKIEJAR => $cookies,
111 | CURLOPT_COOKIEFILE => $cookies,
112 | CURLOPT_HEADERFUNCTION => [$this, 'writeHeader'],
113 | CURLOPT_WRITEFUNCTION => [$this, 'writeBody'],
114 | ]);
115 | }
116 |
117 | private function getResponse(ResponseFactoryInterface $responseFactory): ResponseInterface
118 | {
119 | $info = curl_getinfo($this->curl);
120 |
121 | if ($this->error) {
122 | $this->error(curl_strerror($this->error), $this->error);
123 | }
124 |
125 | if (curl_errno($this->curl)) {
126 | $this->error(curl_error($this->curl), curl_errno($this->curl));
127 | }
128 |
129 | curl_close($this->curl);
130 |
131 | $response = $responseFactory->createResponse($info['http_code']);
132 |
133 | foreach ($this->headers as $header) {
134 | list($name, $value) = $header;
135 | $response = $response->withAddedHeader($name, $value);
136 | }
137 |
138 | $response = $response
139 | ->withAddedHeader('Content-Location', $info['url'])
140 | ->withAddedHeader('X-Request-Time', sprintf('%.3f ms', $info['total_time']));
141 |
142 | if ($this->body) {
143 | //5Mb max
144 | $this->body->rewind();
145 | $response = $response->withBody($this->body);
146 | $this->body = null;
147 | }
148 |
149 | return $response;
150 | }
151 |
152 | private function error(string $message, int $code)
153 | {
154 | $ignored = $this->settings['ignored_errors'] ?? null;
155 |
156 | if ($ignored === true || (is_array($ignored) && in_array($code, $ignored))) {
157 | return;
158 | }
159 |
160 | if ($this->isBinary && $code === CURLE_WRITE_ERROR) {
161 | // The write callback aborted the request to prevent a download of the binary file
162 | return;
163 | }
164 |
165 | throw new NetworkException($message, $code, $this->request);
166 | }
167 |
168 | private function getRequestHeaders(): array
169 | {
170 | $headers = [];
171 |
172 | foreach ($this->request->getHeaders() as $name => $values) {
173 | switch (strtolower($name)) {
174 | case 'user-agent':
175 | break;
176 | default:
177 | $headers[] = $name . ':' . implode(', ', $values);
178 | }
179 | }
180 |
181 | return $headers;
182 | }
183 |
184 | private function writeHeader($curl, $string): int
185 | {
186 | if (preg_match('/^([\w-]+):(.*)$/', $string, $matches)) {
187 | $name = strtolower($matches[1]);
188 | $value = trim($matches[2]);
189 | $this->headers[] = [$name, $value];
190 |
191 | if ($name === 'content-type') {
192 | $this->isBinary = !preg_match('/(text|html|json)/', strtolower($value));
193 | }
194 | } elseif ($this->headers) {
195 | $key = array_key_last($this->headers);
196 | $this->headers[$key][1] .= ' '.trim($string);
197 | }
198 |
199 | return strlen($string);
200 | }
201 |
202 | private function writeBody($curl, $string): int
203 | {
204 | if ($this->isBinary) {
205 | return -1;
206 | }
207 |
208 | if (!$this->body) {
209 | $this->body = $this->streamFactory->createStreamFromFile('php://temp', 'w+');
210 | }
211 |
212 | if ($this->body->getSize() > self::$contentLengthThreshold) {
213 | return strlen($string);
214 | }
215 |
216 | return $this->body->write($string);
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/Http/FactoryDiscovery.php:
--------------------------------------------------------------------------------
1 | request = $request;
18 | }
19 |
20 | public function getRequest(): RequestInterface
21 | {
22 | return $this->request;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Http/RequestException.php:
--------------------------------------------------------------------------------
1 | request = $request;
17 | }
18 |
19 | public function getRequest(): RequestInterface
20 | {
21 | return $this->request;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/HttpApiTrait.php:
--------------------------------------------------------------------------------
1 | endpoint;
18 | }
19 |
20 | private function fetchJSON(UriInterface $uri): array
21 | {
22 | $crawler = $this->extractor->getCrawler();
23 | $request = $crawler->createRequest('GET', $uri);
24 | $response = $crawler->sendRequest($request);
25 |
26 | try {
27 | return json_decode((string) $response->getBody(), true) ?: [];
28 | } catch (Exception $exception) {
29 | return [];
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/LinkedData.php:
--------------------------------------------------------------------------------
1 | getGraph();
25 |
26 | if (!$graph) {
27 | return null;
28 | }
29 |
30 | foreach ($keys as $key) {
31 | $subkeys = explode('.', $key);
32 |
33 | foreach ($graph->getNodes() as $node) {
34 | $value = self::getValue($node, ...$subkeys);
35 |
36 | if ($value) {
37 | return $value;
38 | }
39 | }
40 | }
41 |
42 | return null;
43 | }
44 |
45 | public function getAll()
46 | {
47 | if (!isset($this->allData)) {
48 | $this->fetchData();
49 | }
50 |
51 | return $this->allData;
52 | }
53 |
54 | private function getGraph(?string $name = null): ?GraphInterface
55 | {
56 | if (!isset($this->document)) {
57 | try {
58 | $this->document = LdDocument::load(json_encode($this->all()));
59 | } catch (Throwable $throwable) {
60 | $this->document = LdDocument::load('{}');
61 | return null;
62 | }
63 | }
64 |
65 | return $this->document->getGraph($name);
66 | }
67 |
68 | protected function fetchData(): array
69 | {
70 | $this->allData = [];
71 |
72 | $document = $this->extractor->getDocument();
73 | $nodes = $document->select('.//script', ['type' => 'application/ld+json'])->strAll();
74 |
75 | if (empty($nodes)) {
76 | return [];
77 | }
78 |
79 | try {
80 | $data = [];
81 | $request_uri = (string)$this->extractor->getUri();
82 | foreach ($nodes as $node) {
83 | $ldjson = json_decode($node, true);
84 | if (!empty($ldjson)) {
85 |
86 | // some pages with multiple ld+json blocks will put
87 | // each block into an array (Flickr does this). Most
88 | // appear to put an object in each ld+json block. To
89 | // prevent them from stepping on one another, the ones
90 | // that are not arrays will be put into an array.
91 | if (!array_is_list($ldjson)) {
92 | $ldjson = [$ldjson];
93 | }
94 |
95 | foreach ($ldjson as $node) {
96 | if (empty($data)) {
97 | $data = $node;
98 | } elseif (isset($node['mainEntityOfPage'])) {
99 | $url = '';
100 | if (is_string($node['mainEntityOfPage'])) {
101 | $url = $node['mainEntityOfPage'];
102 | } elseif (isset($node['mainEntityOfPage']['@id'])) {
103 | $url = $node['mainEntityOfPage']['@id'];
104 | }
105 | if (!empty($url) && $url == $request_uri) {
106 | $data = $node;
107 | }
108 | }
109 | }
110 |
111 |
112 | $this->allData = array_merge($this->allData, $ldjson);
113 | }
114 | }
115 |
116 | return $data;
117 | } catch (Exception $exception) {
118 | return [];
119 | }
120 | }
121 |
122 | private static function getValue(Node $node, string ...$keys)
123 | {
124 | foreach ($keys as $key) {
125 | if (is_array($node)) {
126 | $node = array_shift($node);
127 | }
128 | if (!$node instanceof Node) {
129 | return null;
130 | }
131 |
132 | $node = $node->getProperty("http://schema.org/{$key}");
133 |
134 | if (!$node) {
135 | return null;
136 | }
137 | }
138 |
139 | return self::detectValue($node);
140 | }
141 |
142 | private static function detectValue($value)
143 | {
144 | if (is_array($value)) {
145 | return array_map(
146 | fn ($val) => self::detectValue($val),
147 | array_values($value)
148 | );
149 | }
150 |
151 | if (is_scalar($value)) {
152 | return $value;
153 | }
154 |
155 | if ($value instanceof Node) {
156 | return $value->getId();
157 | }
158 |
159 | return $value->getValue();
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Metas.php:
--------------------------------------------------------------------------------
1 | extractor->getDocument();
14 |
15 | foreach ($document->select('.//meta')->nodes() as $node) {
16 | $type = $node->getAttribute('name') ?: $node->getAttribute('property') ?: $node->getAttribute('itemprop');
17 | $value = $node->getAttribute('content');
18 |
19 | if (!empty($value) && !empty($type)) {
20 | $type = strtolower($type);
21 | $data[$type] ??= [];
22 | $data[$type][] = $value;
23 | }
24 | }
25 |
26 | return $data;
27 | }
28 |
29 | public function get(string ...$keys)
30 | {
31 | $data = $this->all();
32 |
33 | foreach ($keys as $key) {
34 | $values = $data[$key] ?? null;
35 |
36 | if ($values) {
37 | return $values;
38 | }
39 | }
40 |
41 | return null;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/OEmbed.php:
--------------------------------------------------------------------------------
1 | $url, 'format' => 'json'];
29 |
30 | return array_merge($queryParameters, $this->extractor->getSetting('oembed:query_parameters') ?? []);
31 | }
32 |
33 | protected function fetchData(): array
34 | {
35 | $this->endpoint = $this->detectEndpoint();
36 |
37 | if (empty($this->endpoint)) {
38 | return [];
39 | }
40 |
41 | $crawler = $this->extractor->getCrawler();
42 | $request = $crawler->createRequest('GET', $this->endpoint);
43 | $response = $crawler->sendRequest($request);
44 |
45 | if (self::isXML($request->getUri())) {
46 | return $this->extractXML((string) $response->getBody());
47 | }
48 |
49 | return $this->extractJSON((string) $response->getBody());
50 | }
51 |
52 | protected function detectEndpoint(): ?UriInterface
53 | {
54 | $document = $this->extractor->getDocument();
55 |
56 | $endpoint = $document->link('alternate', ['type' => 'application/json+oembed'])
57 | ?: $document->link('alternate', ['type' => 'text/json+oembed'])
58 | ?: $document->link('alternate', ['type' => 'application/xml+oembed'])
59 | ?: $document->link('alternate', ['type' => 'text/xml+oembed'])
60 | ?: null;
61 |
62 | if ($endpoint === null) {
63 | return $this->detectEndpointFromProviders();
64 | }
65 |
66 | // Add configured OEmbed query parameters
67 | parse_str($endpoint->getQuery(), $query);
68 | $query = array_merge($query, $this->extractor->getSetting('oembed:query_parameters') ?? []);
69 | $endpoint = $endpoint->withQuery(http_build_query($query));
70 |
71 | return $endpoint;
72 | }
73 |
74 | private function detectEndpointFromProviders(): ?UriInterface
75 | {
76 | $url = (string) $this->extractor->getUri();
77 |
78 | if ($endpoint = $this->detectEndpointFromUrl($url)) {
79 | return $endpoint;
80 | }
81 |
82 | $initialUrl = (string) $this->extractor->getRequest()->getUri();
83 |
84 | if ($initialUrl !== $url && ($endpoint = $this->detectEndpointFromUrl($initialUrl))) {
85 | $this->defaults['url'] = $initialUrl;
86 | return $endpoint;
87 | }
88 |
89 | return null;
90 | }
91 |
92 | private function detectEndpointFromUrl(string $url): ?UriInterface
93 | {
94 | $endpoint = self::searchEndpoint(self::getProviders(), $url);
95 |
96 | if (!$endpoint) {
97 | return null;
98 | }
99 |
100 | return $this->extractor->getCrawler()
101 | ->createUri($endpoint)
102 | ->withQuery(http_build_query($this->getOembedQueryParameters($url)));
103 | }
104 |
105 | private static function searchEndpoint(array $providers, string $url): ?string
106 | {
107 | foreach ($providers as $endpoint => $patterns) {
108 | foreach ($patterns as $pattern) {
109 | if (preg_match($pattern, $url)) {
110 | return $endpoint;
111 | }
112 | }
113 | }
114 |
115 | return null;
116 | }
117 |
118 | private static function isXML(UriInterface $uri): bool
119 | {
120 | $extension = pathinfo($uri->getPath(), PATHINFO_EXTENSION);
121 |
122 | if (strtolower($extension) === 'xml') {
123 | return true;
124 | }
125 |
126 | parse_str($uri->getQuery(), $params);
127 | $format = $params['format'] ?? null;
128 |
129 | if ($format && strtolower($format) === 'xml') {
130 | return true;
131 | }
132 |
133 | return false;
134 | }
135 |
136 | private function extractXML(string $xml): array
137 | {
138 | try {
139 | // Remove the DOCTYPE declaration for to prevent XML Quadratic Blowup vulnerability
140 | $xml = preg_replace('/^]*+>/i', '', $xml, 1);
141 | $data = [];
142 | $errors = libxml_use_internal_errors(true);
143 | $content = new SimpleXMLElement($xml);
144 | libxml_use_internal_errors($errors);
145 |
146 | foreach ($content as $element) {
147 | $value = trim((string) $element);
148 |
149 | if (stripos($value, 'getName();
154 | $data[$name] = $value;
155 | }
156 |
157 | return $data ? ($data + $this->defaults) : [];
158 | } catch (Exception $exception) {
159 | return [];
160 | }
161 | }
162 |
163 | private function extractJSON(string $json): array
164 | {
165 | try {
166 | $data = json_decode($json, true);
167 |
168 | return is_array($data) ? ($data + $this->defaults) : [];
169 | } catch (Exception $exception) {
170 | return [];
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/QueryResult.php:
--------------------------------------------------------------------------------
1 | nodes = iterator_to_array($result, false);
20 | $this->extractor = $extractor;
21 | }
22 |
23 | public function node(): ?DOMElement
24 | {
25 | return $this->nodes[0] ?? null;
26 | }
27 |
28 | public function nodes(): array
29 | {
30 | return $this->nodes;
31 | }
32 |
33 | public function filter(Closure $callback): self
34 | {
35 | $this->nodes = array_filter($this->nodes, $callback);
36 |
37 | return $this;
38 | }
39 |
40 | public function get(?string $attribute = null)
41 | {
42 | $node = $this->node();
43 |
44 | if (!$node) {
45 | return null;
46 | }
47 |
48 | return $attribute ? self::getAttribute($node, $attribute) : $node->nodeValue;
49 | }
50 |
51 | public function getAll(?string $attribute = null): array
52 | {
53 | $nodes = $this->nodes();
54 |
55 | return array_filter(
56 | array_map(
57 | fn ($node) => $attribute ? self::getAttribute($node, $attribute) : $node->nodeValue,
58 | $nodes
59 | )
60 | );
61 | }
62 |
63 | public function str(?string $attribute = null): ?string
64 | {
65 | $value = $this->get($attribute);
66 |
67 | return $value ? clean($value) : null;
68 | }
69 |
70 | public function strAll(?string $attribute = null): array
71 | {
72 | return array_filter(array_map(fn ($value) => clean($value), $this->getAll($attribute)));
73 | }
74 |
75 | public function int(?string $attribute = null): ?int
76 | {
77 | $value = $this->get($attribute);
78 |
79 | return $value ? (int) $value : null;
80 | }
81 |
82 | public function url(?string $attribute = null): ?UriInterface
83 | {
84 | $value = $this->get($attribute);
85 |
86 | if (!$value) {
87 | return null;
88 | }
89 |
90 | try {
91 | return $this->extractor->resolveUri($value);
92 | } catch (Throwable $error) {
93 | return null;
94 | }
95 | }
96 |
97 | private static function getAttribute(DOMElement $node, string $name): ?string
98 | {
99 | //Don't use $node->getAttribute() because it does not work with namespaces (ex: xml:lang)
100 | $attributes = $node->attributes;
101 |
102 | for ($i = 0; $i < $attributes->length; ++$i) {
103 | $attribute = $attributes->item($i);
104 |
105 | if ($attribute->name === $name) {
106 | return $attribute->nodeValue;
107 | }
108 | }
109 |
110 | return null;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/functions.php:
--------------------------------------------------------------------------------
1 | $value) {
26 | if ($value === null) {
27 | continue;
28 | } elseif ($value === true) {
29 | $html .= " $name";
30 | } elseif ($value !== false) {
31 | $html .= ' '.$name.'="'.htmlspecialchars((string) $value).'"';
32 | }
33 | }
34 |
35 | if ($tagName === 'img') {
36 | return "$html />";
37 | }
38 |
39 | return "{$html}>{$content}{$tagName}>";
40 | }
41 |
42 | /**
43 | * Resolve a uri within this document
44 | * (useful to get absolute uris from relative)
45 | */
46 | function resolveUri(UriInterface $base, UriInterface $uri): UriInterface
47 | {
48 | $uri = $uri->withPath(resolvePath($base->getPath(), $uri->getPath()));
49 |
50 | if (!$uri->getHost()) {
51 | $uri = $uri->withHost($base->getHost());
52 | }
53 |
54 | if (!$uri->getScheme()) {
55 | $uri = $uri->withScheme($base->getScheme());
56 | }
57 |
58 | return $uri
59 | ->withPath(cleanPath($uri->getPath()))
60 | ->withFragment('');
61 | }
62 |
63 | function isHttp(string $uri): bool
64 | {
65 | if (preg_match('/^(\w+):/', $uri, $matches)) {
66 | return in_array(strtolower($matches[1]), ['http', 'https']);
67 | }
68 |
69 | return true;
70 | }
71 |
72 | function resolvePath(string $base, string $path): string
73 | {
74 | if ($path === '') {
75 | return '';
76 | }
77 |
78 | if ($path[0] === '/') {
79 | return $path;
80 | }
81 |
82 | if (substr($base, -1) !== '/') {
83 | $position = strrpos($base, '/');
84 | $base = substr($base, 0, $position);
85 | }
86 |
87 | $path = "{$base}/{$path}";
88 |
89 | $parts = array_filter(explode('/', $path), 'strlen');
90 | $absolutes = [];
91 |
92 | foreach ($parts as $part) {
93 | if ('.' == $part) {
94 | continue;
95 | }
96 |
97 | if ('..' == $part) {
98 | array_pop($absolutes);
99 | continue;
100 | }
101 |
102 | $absolutes[] = $part;
103 | }
104 |
105 | return implode('/', $absolutes);
106 | }
107 |
108 | function cleanPath(string $path): string
109 | {
110 | if ($path === '') {
111 | return '/';
112 | }
113 |
114 | $path = preg_replace('|[/]{2,}|', '/', $path);
115 |
116 | if (strpos($path, ';jsessionid=') !== false) {
117 | $path = preg_replace('/^(.*)(;jsessionid=.*)$/i', '$1', $path);
118 | }
119 |
120 | return $path;
121 | }
122 |
123 | function matchPath(string $pattern, string $subject): bool
124 | {
125 | $pattern = str_replace('\\*', '.*', preg_quote($pattern, '|'));
126 |
127 | return (bool) preg_match("|^{$pattern}$|i", $subject);
128 | }
129 |
130 | function getDirectory(string $path, int $position): ?string
131 | {
132 | $dirs = explode('/', $path);
133 | return $dirs[$position + 1] ?? null;
134 | }
135 |
136 | /**
137 | * Determine whether at least one of the supplied variables is empty.
138 | *
139 | * @param mixed ...$values The values to check.
140 | *
141 | * @return boolean
142 | */
143 | function isEmpty(...$values): bool
144 | {
145 | $skipValues = array(
146 | 'undefined',
147 | );
148 |
149 | foreach ($values as $value) {
150 | if (empty($value) || in_array($value, $skipValues)) {
151 | return true;
152 | }
153 | }
154 |
155 | return false;
156 | }
157 |
158 | if (!function_exists("array_is_list")) {
159 | /**
160 | * Polyfil for https://www.php.net/manual/en/function.array-is-list.php
161 | * which is only available in PHP 8.1+
162 | *
163 | * @param array $array The array
164 | *
165 | * @return bool
166 | */
167 | function array_is_list(array $array): bool
168 | {
169 | $i = -1;
170 | foreach ($array as $k => $v) {
171 | ++$i;
172 | if ($k !== $i) {
173 | return false;
174 | }
175 | }
176 | return true;
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/resources/oembed.php:
--------------------------------------------------------------------------------
1 | [
6 | '|^https?://www\\.23hq\\.com/.*/photo/.*$|i',
7 | ],
8 | 'https://playout.3qsdn.com/oembed' => [
9 | '|^https?://playout\\.3qsdn\\.com/embed/.*$|i',
10 | ],
11 | 'https://api.abraia.me/oembed' => [
12 | '|^https?://store\\.abraia\\.me/.*$|i',
13 | ],
14 | 'https://oembed.acast.com/v1/embed-player' => [
15 | '|^https?://play\\.acast\\.com/s/.*$|i',
16 | ],
17 | 'https://secure.actblue.com/cf/oembed' => [
18 | '|^https?://secure\\.actblue\\.com/donate/.*$|i',
19 | ],
20 | 'https://adilo.bigcommand.com/web/oembed' => [
21 | '|^https?://adilo\\.bigcommand\\.com/watch/.*$|i',
22 | ],
23 | 'https://openapi.afreecatv.com/oembed/embedinfo' => [
24 | '|^https?://vod\\.afreecatv\\.com/player/$|i',
25 | '|^https?://v\\.afree\\.ca/ST/$|i',
26 | '|^https?://vod\\.afreecatv\\.com/ST/$|i',
27 | '|^https?://vod\\.afreecatv\\.com/PLAYER/STATION/$|i',
28 | '|^https?://play\\.afreecatv\\.com/$|i',
29 | ],
30 | 'https://viewer.altium.com/shell/oembed' => [
31 | '|^https?://altium\\.com/viewer/.*$|i',
32 | ],
33 | 'https://api.altrulabs.com/api/v1/social/oembed' => [
34 | '|^https?://app\\.altrulabs\\.com/.*/.*\\?answer_id\\=.*$|i',
35 | '|^https?://app\\.altrulabs\\.com/player/.*$|i',
36 | ],
37 | 'https://live.amcharts.com/oembed' => [
38 | '|^https?://live\\.amcharts\\.com/.*$|i',
39 | ],
40 | 'https://api.amtraker.com/v3/oembed' => [
41 | '|^https?://amtraker\\.com/trains/.*$|i',
42 | '|^https?://amtraker\\.com/trains/.*/.*$|i',
43 | '|^https?://.*\\.amtraker\\.com/trains/.*$|i',
44 | '|^https?://.*\\.amtraker\\.com/trains/.*/.*$|i',
45 | ],
46 | 'https://animatron.com/oembed/json' => [
47 | '|^https?://www\\.animatron\\.com/project/.*$|i',
48 | '|^https?://animatron\\.com/project/.*$|i',
49 | ],
50 | 'http://animoto.com/oembeds/create' => [
51 | '|^https?://animoto\\.com/play/.*$|i',
52 | ],
53 | 'https://api.anniemusic.app/api/v1/oembed' => [
54 | '|^https?://anniemusic\\.app/t/.*$|i',
55 | '|^https?://anniemusic\\.app/p/.*$|i',
56 | ],
57 | 'https://storymaps.arcgis.com/oembed' => [
58 | '|^https?://storymaps\\.arcgis\\.com/stories/.*$|i',
59 | ],
60 | 'https://app.archivos.digital/oembed/' => [
61 | '|^https?://app\\.archivos\\.digital/app/view/.*$|i',
62 | ],
63 | 'https://studio.assemblrworld.com/api/oembed' => [
64 | '|^https?://.*\\.studio\\.assemblrworld\\.com/creation/.*$|i',
65 | '|^https?://studio\\.assemblrworld\\.com/creation/.*$|i',
66 | '|^https?://.*\\.app\\-edu\\.assemblrworld\\.com/Creation/.*$|i',
67 | '|^https?://app\\-edu\\.assemblrworld\\.com/Creation/.*$|i',
68 | '|^https?://assemblr\\.world/.*$|i',
69 | '|^https?://editor\\.assemblrworld\\.com/.*$|i',
70 | '|^https?://.*\\.assemblrworld\\.com/creation/.*$|i',
71 | '|^https?://.*\\.assemblrworld\\.com/Creation/.*$|i',
72 | ],
73 | 'https://api.audio.com/oembed' => [
74 | '|^https?://audio\\.com/.*$|i',
75 | '|^https?://www\\.audio\\.com/.*$|i',
76 | ],
77 | 'https://audioboom.com/publishing/oembed.json' => [
78 | '|^https?://audioboom\\.com/channels/.*$|i',
79 | '|^https?://audioboom\\.com/channel/.*$|i',
80 | '|^https?://audioboom\\.com/playlists/.*$|i',
81 | '|^https?://audioboom\\.com/podcasts/.*$|i',
82 | '|^https?://audioboom\\.com/podcast/.*$|i',
83 | '|^https?://audioboom\\.com/posts/.*$|i',
84 | '|^https?://audioboom\\.com/episodes/.*$|i',
85 | ],
86 | 'https://audioclip.naver.com/oembed' => [
87 | '|^https?://audioclip\\.naver\\.com/channels/.*/clips/.*$|i',
88 | '|^https?://audioclip\\.naver\\.com/audiobooks/.*$|i',
89 | ],
90 | 'https://audiomack.com/oembed' => [
91 | '|^https?://audiomack\\.com/.*/song/.*$|i',
92 | '|^https?://audiomack\\.com/.*/album/.*$|i',
93 | '|^https?://audiomack\\.com/.*/playlist/.*$|i',
94 | ],
95 | 'https://podcasts.audiomeans.fr/services/oembed' => [
96 | '|^https?://podcasts\\.audiomeans\\.fr/.*$|i',
97 | ],
98 | 'https://stage-embed.avocode.com/api/oembed' => [
99 | '|^https?://app\\.avocode\\.com/view/.*$|i',
100 | ],
101 | 'https://backtracks.fm/oembed' => [
102 | '|^https?://backtracks\\.fm/.*/.*/e/.*$|i',
103 | '|^https?://backtracks\\.fm/.*/s/.*/.*$|i',
104 | '|^https?://backtracks\\.fm/.*/.*/.*/.*/e/.*/.*$|i',
105 | '|^https?://backtracks\\.fm/.*$|i',
106 | ],
107 | 'https://balsamiq.cloud/oembed' => [
108 | '|^https?://balsamiq\\.cloud/.*$|i',
109 | ],
110 | 'https://api.beams.fm/oEmbed' => [
111 | '|^https?://beams\\.fm/.*$|i',
112 | ],
113 | 'https://www.beautiful.ai/api/oembed' => [
114 | '|^https?://www\\.beautiful\\.ai/.*$|i',
115 | ],
116 | 'https://www.behance.net/services/oembed' => [
117 | '|^https?://www\\.behance\\.net/gallery/.*/.*$|i',
118 | '|^https?://www\\.behance\\.net/.*/services/.*/.*$|i',
119 | ],
120 | 'https://biqapp.com/api/v1/video/oembed' => [
121 | '|^https?://cloud\\.biqapp\\.com/.*$|i',
122 | ],
123 | 'https://blackfire.io/oembed' => [
124 | '|^https?://blackfire\\.io/profiles/.*/graph$|i',
125 | '|^https?://blackfire\\.io/profiles/compare/.*/graph$|i',
126 | ],
127 | 'https://blogcast.host/oembed' => [
128 | '|^https?://blogcast\\.host/embed/.*$|i',
129 | '|^https?://blogcast\\.host/embedly/.*$|i',
130 | ],
131 | 'https://embed.bsky.app/oembed' => [
132 | '|^https?://bsky\\.app/profile/.*/post/.*$|i',
133 | ],
134 | 'https://bookingmood.com/api/oembed' => [
135 | '|^https?://www\\.bookingmood\\.com/embed/.*/.*$|i',
136 | ],
137 | 'http://boxofficebuz.com/oembed' => [
138 | '|^https?://boxofficebuz\\.com.*$|i',
139 | ],
140 | 'https://view.briovr.com/api/v1/worlds/oembed/' => [
141 | '|^https?://view\\.briovr\\.com/api/v1/worlds/oembed/.*$|i',
142 | ],
143 | 'https://www.bumper.com/oembed/bumper' => [
144 | '|^https?://www\\.bumper\\.com/oembed/bumper$|i',
145 | '|^https?://www\\.bumper\\.com/oembed\\-s/bumper$|i',
146 | ],
147 | 'https://video.bunnycdn.com/OEmbed' => [
148 | '|^https?://iframe\\.mediadelivery\\.net/.*$|i',
149 | '|^https?://video\\.bunnycdn\\.com/.*$|i',
150 | ],
151 | 'https://buttondown.email/embed' => [
152 | '|^https?://buttondown\\.email/.*$|i',
153 | ],
154 | 'https://cmc.byzart.eu/oembed/' => [
155 | '|^https?://cmc\\.byzart\\.eu/files/.*$|i',
156 | ],
157 | 'http://cacoo.com/oembed.json' => [
158 | '|^https?://cacoo\\.com/diagrams/.*$|i',
159 | ],
160 | 'https://www.canva.com/_oembed' => [
161 | '|^https?://www\\.canva\\.com/design/.*/view$|i',
162 | ],
163 | 'https://minesweeper.today/api/oembed' => [
164 | '|^https?://minesweeper\\.today/.*$|i',
165 | ],
166 | 'http://img.catbo.at/oembed.json' => [
167 | '|^https?://img\\.catbo\\.at/.*$|i',
168 | ],
169 | 'https://api.celero.io/api/oembed' => [
170 | '|^https?://embeds\\.celero\\.io/.*$|i',
171 | ],
172 | 'http://view.ceros.com/oembed' => [
173 | '|^https?://view\\.ceros\\.com/.*$|i',
174 | ],
175 | 'https://www.chainflix.net/video/oembed' => [
176 | '|^https?://chainflix\\.net/video/.*$|i',
177 | '|^https?://chainflix\\.net/video/embed/.*$|i',
178 | '|^https?://.*\\.chainflix\\.net/video/.*$|i',
179 | '|^https?://.*\\.chainflix\\.net/video/embed/.*$|i',
180 | ],
181 | 'http://embed.chartblocks.com/1.0/oembed' => [
182 | '|^https?://public\\.chartblocks\\.com/c/.*$|i',
183 | ],
184 | 'http://chirb.it/oembed.json' => [
185 | '|^https?://chirb\\.it/.*$|i',
186 | ],
187 | 'https://chroco.ooo/embed' => [
188 | '|^https?://chroco\\.ooo/mypage/.*$|i',
189 | '|^https?://chroco\\.ooo/story/.*$|i',
190 | ],
191 | 'https://www.circuitlab.com/circuit/oembed/' => [
192 | '|^https?://www\\.circuitlab\\.com/circuit/.*$|i',
193 | ],
194 | 'https://www.clipland.com/api/oembed' => [
195 | '|^https?://www\\.clipland\\.com/v/.*$|i',
196 | ],
197 | 'http://api.clyp.it/oembed/' => [
198 | '|^https?://clyp\\.it/.*$|i',
199 | '|^https?://clyp\\.it/playlist/.*$|i',
200 | ],
201 | 'https://app.ilovecoco.video/api/oembed.json' => [
202 | '|^https?://app\\.ilovecoco\\.video/.*/embed$|i',
203 | ],
204 | 'https://codehs.com/api/sharedprogram/1/oembed/' => [
205 | '|^https?://codehs\\.com/editor/share_abacus/.*$|i',
206 | ],
207 | 'https://codepen.io/api/oembed' => [
208 | '|^https?://codepen\\.io/.*$|i',
209 | ],
210 | 'https://codepoints.net/api/v1/oembed' => [
211 | '|^https?://codepoints\\.net/.*$|i',
212 | '|^https?://www\\.codepoints\\.net/.*$|i',
213 | ],
214 | 'https://codesandbox.io/oembed' => [
215 | '|^https?://codesandbox\\.io/s/.*$|i',
216 | '|^https?://codesandbox\\.io/embed/.*$|i',
217 | ],
218 | 'http://www.collegehumor.com/oembed.json' => [
219 | '|^https?://www\\.collegehumor\\.com/video/.*$|i',
220 | ],
221 | 'https://commaful.com/api/oembed/' => [
222 | '|^https?://commaful\\.com/play/.*$|i',
223 | ],
224 | 'http://coub.com/api/oembed.json' => [
225 | '|^https?://coub\\.com/view/.*$|i',
226 | '|^https?://coub\\.com/embed/.*$|i',
227 | ],
228 | 'http://crowdranking.com/api/oembed.json' => [
229 | '|^https?://crowdranking\\.com/.*/.*$|i',
230 | ],
231 | 'https://crumb.sh/oembed/' => [
232 | '|^https?://crumb\\.sh/.*$|i',
233 | ],
234 | 'https://gql.cueup.io/oembed' => [
235 | '|^https?://cueup\\.io/user/.*/sounds/.*$|i',
236 | ],
237 | 'https://api.curated.co/oembed' => [
238 | '|^https?://.*\\.curated\\.co/.*$|i',
239 | ],
240 | 'https://app.customerdb.com/embed' => [
241 | '|^https?://app\\.customerdb\\.com/share/.*$|i',
242 | ],
243 | 'https://app.dadan.io/api/video/oembed' => [
244 | '|^https?://app\\.dadan\\.io/.*$|i',
245 | '|^https?://stage\\.dadan\\.io/.*$|i',
246 | ],
247 | 'https://www.dailymotion.com/services/oembed' => [
248 | '|^https?://www\\.dailymotion\\.com/video/.*$|i',
249 | '|^https?://geo\\.dailymotion\\.com/player\\.html\\?video\\=.*$|i',
250 | ],
251 | 'https://dalexni.com/oembed/' => [
252 | '|^https?://dalexni\\.com/i/.*$|i',
253 | ],
254 | 'https://api.datawrapper.de/v3/oembed/' => [
255 | '|^https?://datawrapper\\.dwcdn\\.net/.*$|i',
256 | ],
257 | 'https://embed.deseret.com/' => [
258 | '|^https?://.*\\.deseret\\.com/.*$|i',
259 | ],
260 | 'http://backend.deviantart.com/oembed' => [
261 | '|^https?://.*\\.deviantart\\.com/art/.*$|i',
262 | '|^https?://.*\\.deviantart\\.com/.*\\#/d.*$|i',
263 | '|^https?://fav\\.me/.*$|i',
264 | '|^https?://sta\\.sh/.*$|i',
265 | '|^https?://.*\\.deviantart\\.com/.*/art/.*$|i',
266 | ],
267 | 'https://www.ultimedia.com/api/search/oembed' => [
268 | '|^https?://www\\.ultimedia\\.com/central/video/edit/id/.*/topic_id/.*/$|i',
269 | '|^https?://www\\.ultimedia\\.com/default/index/videogeneric/id/.*/showtitle/1/viewnc/1$|i',
270 | '|^https?://www\\.ultimedia\\.com/default/index/videogeneric/id/.*$|i',
271 | ],
272 | 'https://www.docdroid.net/api/oembed' => [
273 | '|^https?://.*\\.docdroid\\.net/.*$|i',
274 | '|^https?://docdro\\.id/.*$|i',
275 | '|^https?://.*\\.docdroid\\.com/.*$|i',
276 | ],
277 | 'https://www.docswell.com/service/oembed' => [
278 | '|^https?://docswell\\.com/s/.*/.*$|i',
279 | '|^https?://www\\.docswell\\.com/s/.*/.*$|i',
280 | ],
281 | 'http://dotsub.com/services/oembed' => [
282 | '|^https?://dotsub\\.com/view/.*$|i',
283 | ],
284 | 'https://dreambroker.com/channel/oembed' => [
285 | '|^https?://www\\.dreambroker\\.com/channel/.*/.*$|i',
286 | ],
287 | 'https://api.d.tube/oembed' => [
288 | '|^https?://d\\.tube/v/.*$|i',
289 | ],
290 | 'https://api.echoeshq.com/oembed' => [
291 | '|^https?://app\\.echoeshq\\.com/embed/.*$|i',
292 | ],
293 | 'https://www.edumedia-sciences.com/oembed.json' => [
294 | '|^https?://www\\.edumedia\\-sciences\\.com/.*$|i',
295 | ],
296 | 'https://www.edumedia-sciences.com/oembed.xml' => [
297 | '|^https?://www\\.edumedia\\-sciences\\.com/.*$|i',
298 | ],
299 | 'http://egliseinfo.catholique.fr/api/oembed' => [
300 | '|^https?://egliseinfo\\.catholique\\.fr/.*$|i',
301 | ],
302 | 'https://embedery.com/api/oembed' => [
303 | '|^https?://embedery\\.com/widget/.*$|i',
304 | ],
305 | 'https://ethfiddle.com/services/oembed/' => [
306 | '|^https?://ethfiddle\\.com/.*$|i',
307 | ],
308 | 'https://evt.live/api/oembed' => [
309 | '|^https?://evt\\.live/.*$|i',
310 | '|^https?://evt\\.live/.*/.*$|i',
311 | '|^https?://live\\.eventlive\\.pro/.*$|i',
312 | '|^https?://live\\.eventlive\\.pro/.*/.*$|i',
313 | ],
314 | 'https://api.everviz.com/oembed' => [
315 | '|^https?://app\\.everviz\\.com/embed/.*$|i',
316 | ],
317 | 'https://oembed.ex.co/item' => [
318 | '|^https?://app\\.ex\\.co/stories/.*$|i',
319 | '|^https?://www\\.playbuzz\\.com/.*$|i',
320 | ],
321 | 'https://eyrie.io/v1/oembed' => [
322 | '|^https?://eyrie\\.io/board/.*$|i',
323 | '|^https?://eyrie\\.io/sparkfun/.*$|i',
324 | ],
325 | 'https://graph.facebook.com/v16.0/oembed_post' => [
326 | '|^https?://www\\.facebook\\.com/.*/posts/.*$|i',
327 | '|^https?://www\\.facebook\\.com/.*/activity/.*$|i',
328 | '|^https?://www\\.facebook\\.com/.*/photos/.*$|i',
329 | '|^https?://www\\.facebook\\.com/photo\\.php\\?fbid\\=.*$|i',
330 | '|^https?://www\\.facebook\\.com/photos/.*$|i',
331 | '|^https?://www\\.facebook\\.com/permalink\\.php\\?story_fbid\\=.*$|i',
332 | '|^https?://www\\.facebook\\.com/media/set\\?set\\=.*$|i',
333 | '|^https?://www\\.facebook\\.com/questions/.*$|i',
334 | '|^https?://www\\.facebook\\.com/notes/.*/.*/.*$|i',
335 | ],
336 | 'https://graph.facebook.com/v16.0/oembed_video' => [
337 | '|^https?://www\\.facebook\\.com/.*/videos/.*$|i',
338 | '|^https?://www\\.facebook\\.com/video\\.php\\?id\\=.*$|i',
339 | '|^https?://www\\.facebook\\.com/video\\.php\\?v\\=.*$|i',
340 | ],
341 | 'https://graph.facebook.com/v16.0/oembed_page' => [
342 | '|^https?://www\\.facebook\\.com/.*$|i',
343 | ],
344 | 'https://app.getfader.com/api/oembed' => [
345 | '|^https?://app\\.getfader\\.com/projects/.*/publish$|i',
346 | ],
347 | 'https://faithlifetv.com/api/oembed' => [
348 | '|^https?://faithlifetv\\.com/items/.*$|i',
349 | '|^https?://faithlifetv\\.com/items/resource/.*/.*$|i',
350 | '|^https?://faithlifetv\\.com/media/.*$|i',
351 | '|^https?://faithlifetv\\.com/media/assets/.*$|i',
352 | '|^https?://faithlifetv\\.com/media/resource/.*/.*$|i',
353 | ],
354 | 'https://www.figma.com/api/oembed' => [
355 | '|^https?://www\\.figma\\.com/file/.*$|i',
356 | ],
357 | 'https://www.fireworktv.com/oembed' => [
358 | '|^https?://.*\\.fireworktv\\.com/.*$|i',
359 | '|^https?://.*\\.fireworktv\\.com/embed/.*/v/.*$|i',
360 | ],
361 | 'https://www.fite.tv/oembed' => [
362 | '|^https?://www\\.fite\\.tv/watch/.*$|i',
363 | ],
364 | 'https://flat.io/services/oembed' => [
365 | '|^https?://flat\\.io/score/.*$|i',
366 | '|^https?://.*\\.flat\\.io/score/.*$|i',
367 | ],
368 | 'https://www.flickr.com/services/oembed/' => [
369 | '|^https?://.*\\.flickr\\.com/photos/.*$|i',
370 | '|^https?://flic\\.kr/p/.*$|i',
371 | '|^https?://.*\\..*\\.flickr\\.com/.*/.*$|i',
372 | ],
373 | 'https://app.flourish.studio/api/v1/oembed' => [
374 | '|^https?://public\\.flourish\\.studio/visualisation/.*$|i',
375 | '|^https?://public\\.flourish\\.studio/story/.*$|i',
376 | ],
377 | 'https://flowhub.org/o/embed' => [
378 | '|^https?://flowhub\\.org/f/.*$|i',
379 | '|^https?://flowhub\\.org/s/.*$|i',
380 | ],
381 | 'https://fooday.app/oembed' => [
382 | '|^https?://fooday\\.app/.*/reviews/.*$|i',
383 | '|^https?://fooday\\.app/.*/spots/.*$|i',
384 | ],
385 | 'https://fiso.foxsports.com.au/oembed' => [
386 | '|^https?://fiso\\.foxsports\\.com\\.au/isomorphic\\-widget/.*$|i',
387 | ],
388 | 'https://framebuzz.com/oembed/' => [
389 | '|^https?://framebuzz\\.com/v/.*$|i',
390 | ],
391 | 'https://api.framer.com/web/oembed' => [
392 | '|^https?://framer\\.com/share/.*$|i',
393 | '|^https?://framer\\.com/embed/.*$|i',
394 | ],
395 | 'http://api.geograph.org.uk/api/oembed' => [
396 | '|^https?://.*\\.geograph\\.org\\.uk/.*$|i',
397 | '|^https?://.*\\.geograph\\.co\\.uk/.*$|i',
398 | '|^https?://.*\\.geograph\\.ie/.*$|i',
399 | '|^https?://.*\\.wikimedia\\.org/.*_geograph\\.org\\.uk_.*$|i',
400 | ],
401 | 'http://www.geograph.org.gg/api/oembed' => [
402 | '|^https?://.*\\.geograph\\.org\\.gg/.*$|i',
403 | '|^https?://.*\\.geograph\\.org\\.je/.*$|i',
404 | '|^https?://channel\\-islands\\.geograph\\.org/.*$|i',
405 | '|^https?://channel\\-islands\\.geographs\\.org/.*$|i',
406 | '|^https?://.*\\.channel\\.geographs\\.org/.*$|i',
407 | ],
408 | 'http://geo.hlipp.de/restapi.php/api/oembed' => [
409 | '|^https?://geo\\-en\\.hlipp\\.de/.*$|i',
410 | '|^https?://geo\\.hlipp\\.de/.*$|i',
411 | '|^https?://germany\\.geograph\\.org/.*$|i',
412 | ],
413 | 'http://embed.gettyimages.com/oembed' => [
414 | '|^https?://gty\\.im/.*$|i',
415 | ],
416 | 'https://www.gifnote.com/services/oembed' => [
417 | '|^https?://www\\.gifnote\\.com/play/.*$|i',
418 | ],
419 | 'https://giphy.com/services/oembed' => [
420 | '|^https?://giphy\\.com/gifs/.*$|i',
421 | '|^https?://giphy\\.com/clips/.*$|i',
422 | '|^https?://gph\\.is/.*$|i',
423 | '|^https?://media\\.giphy\\.com/media/.*/giphy\\.gif$|i',
424 | ],
425 | 'https://gloria.tv/oembed/' => [
426 | '|^https?://gloria\\.tv/.*$|i',
427 | ],
428 | 'https://embed.gmetri.com/oembed/' => [
429 | '|^https?://view\\.gmetri\\.com/.*$|i',
430 | '|^https?://.*\\.gmetri\\.com/.*$|i',
431 | ],
432 | 'https://app.gong.io/oembed' => [
433 | '|^https?://app\\.gong\\.io/call\\?id\\=.*$|i',
434 | ],
435 | 'https://api.grain.com/_/api/oembed' => [
436 | '|^https?://grain\\.co/highlight/.*$|i',
437 | '|^https?://grain\\.co/share/.*$|i',
438 | '|^https?://grain\\.com/share/.*$|i',
439 | ],
440 | 'https://api.luminery.com/oembed' => [
441 | '|^https?://gtchannel\\.com/watch/.*$|i',
442 | ],
443 | 'https://api.gumlet.com/v1/oembed' => [
444 | '|^https?://gumlet\\.tv/watch/.*$|i',
445 | '|^https?://www\\.gumlet\\.com/watch/.*$|i',
446 | '|^https?://play\\.gumlet\\.io/embed/.*$|i',
447 | ],
448 | 'https://api.gyazo.com/api/oembed' => [
449 | '|^https?://gyazo\\.com/.*$|i',
450 | ],
451 | 'https://api.hash.ai/oembed' => [
452 | '|^https?://core\\.hash\\.ai/@.*$|i',
453 | ],
454 | 'https://hearthis.at/oembed/?format=json' => [
455 | '|^https?://hearthis\\.at/.*/.*/$|i',
456 | '|^https?://hearthis\\.at/.*/set/.*/$|i',
457 | ],
458 | 'https://heyzine.com/api1/oembed' => [
459 | '|^https?://heyzine\\.com/flip\\-book/.*$|i',
460 | '|^https?://.*\\.hflip\\.co/.*$|i',
461 | '|^https?://.*\\.aflip\\.in/.*$|i',
462 | ],
463 | 'https://player.hihaho.com/services/oembed' => [
464 | '|^https?://player\\.hihaho\\.com/.*$|i',
465 | ],
466 | 'https://www.hippovideo.io/services/oembed' => [
467 | '|^https?://.*\\.hippovideo\\.io/.*$|i',
468 | ],
469 | 'https://homey.app/api/oembed/flow' => [
470 | '|^https?://homey\\.app/f/.*$|i',
471 | '|^https?://homey\\.app/.*/flow/.*$|i',
472 | ],
473 | 'https://portal.hopvue.com/api/oembed/' => [
474 | '|^https?://.*\\.hopvue\\.com/.*$|i',
475 | ],
476 | 'http://huffduffer.com/oembed' => [
477 | '|^https?://huffduffer\\.com/.*/.*$|i',
478 | ],
479 | 'http://www.hulu.com/api/oembed.json' => [
480 | '|^https?://www\\.hulu\\.com/watch/.*$|i',
481 | ],
482 | 'https://oembed.ideamapper.com/oembed' => [
483 | '|^https?://oembed\\.ideamapper\\.com/.*$|i',
484 | ],
485 | 'https://oembed.idomoo.com/oembed' => [
486 | '|^https?://.*\\.idomoo\\.com/.*$|i',
487 | ],
488 | 'http://www.ifixit.com/Embed' => [
489 | '|^https?://www\\.ifixit\\.com/Guide/View/.*$|i',
490 | ],
491 | 'http://www.ifttt.com/oembed/' => [
492 | '|^https?://ifttt\\.com/recipes/.*$|i',
493 | ],
494 | 'https://www.iheart.com/oembed' => [
495 | '|^https?://www\\.iheart\\.com/podcast/.*/.*$|i',
496 | ],
497 | 'https://qr.imenupro.com/api/oembed' => [
498 | '|^https?://qr\\.imenupro\\.com/.*$|i',
499 | ],
500 | 'https://oembed.incredible.dev/oembed' => [
501 | '|^https?://incredible\\.dev/watch/.*$|i',
502 | ],
503 | 'https://player.indacolive.com/services/oembed' => [
504 | '|^https?://player\\.indacolive\\.com/player/jwp/clients/.*$|i',
505 | ],
506 | 'https://infogram.com/oembed' => [
507 | '|^https?://infogram\\.com/.*$|i',
508 | ],
509 | 'https://infoveave.net/services/oembed/' => [
510 | '|^https?://.*\\.infoveave\\.net/E/.*$|i',
511 | '|^https?://.*\\.infoveave\\.net/P/.*$|i',
512 | ],
513 | 'https://www.injurymap.com/services/oembed' => [
514 | '|^https?://www\\.injurymap\\.com/exercises/.*$|i',
515 | ],
516 | 'https://www.inoreader.com/oembed/api/' => [
517 | '|^https?://www\\.inoreader\\.com/oembed/$|i',
518 | ],
519 | 'http://api.inphood.com/oembed' => [
520 | '|^https?://.*\\.inphood\\.com/.*$|i',
521 | ],
522 | 'https://widgets.insighttimer.com/services/oembed' => [
523 | '|^https?://insighttimer\\.com/.*$|i',
524 | ],
525 | 'https://graph.facebook.com/v16.0/instagram_oembed' => [
526 | '|^https?://instagram\\.com/.*/p/.*,$|i',
527 | '|^https?://www\\.instagram\\.com/.*/p/.*,$|i',
528 | '|^https?://instagram\\.com/p/.*$|i',
529 | '|^https?://instagr\\.am/p/.*$|i',
530 | '|^https?://www\\.instagram\\.com/p/.*$|i',
531 | '|^https?://www\\.instagr\\.am/p/.*$|i',
532 | '|^https?://instagram\\.com/tv/.*$|i',
533 | '|^https?://instagr\\.am/tv/.*$|i',
534 | '|^https?://www\\.instagram\\.com/tv/.*$|i',
535 | '|^https?://www\\.instagr\\.am/tv/.*$|i',
536 | '|^https?://www\\.instagram\\.com/reel/.*$|i',
537 | '|^https?://instagram\\.com/reel/.*$|i',
538 | '|^https?://instagr\\.am/reel/.*$|i',
539 | ],
540 | 'https://www.insticator.com/oembed' => [
541 | '|^https?://ppa\\.insticator\\.com/embed\\-unit/.*$|i',
542 | ],
543 | 'https://issuu.com/oembed' => [
544 | '|^https?://issuu\\.com/.*/docs/.*$|i',
545 | ],
546 | 'https://samay.itabtechinfosys.com/oembed/' => [
547 | '|^https?://samay\\.itabtechinfosys\\.com/.*$|i',
548 | ],
549 | 'https://create.storage.api.itemis.io/api/embed' => [
550 | '|^https?://play\\.itemis\\.io/.*$|i',
551 | ],
552 | 'https://api.jovian.com/oembed.json' => [
553 | '|^https?://jovian\\.ml/.*$|i',
554 | '|^https?://jovian\\.ml/viewer.*$|i',
555 | '|^https?://.*\\.jovian\\.ml/.*$|i',
556 | '|^https?://jovian\\.ai/.*$|i',
557 | '|^https?://jovian\\.ai/viewer.*$|i',
558 | '|^https?://.*\\.jovian\\.ai/.*$|i',
559 | '|^https?://jovian\\.com/.*$|i',
560 | '|^https?://jovian\\.com/viewer.*$|i',
561 | '|^https?://.*\\.jovian\\.com/.*$|i',
562 | ],
563 | 'https://tv.kakao.com/oembed' => [
564 | '|^https?://tv\\.kakao\\.com/channel/.*/cliplink/.*$|i',
565 | '|^https?://tv\\.kakao\\.com/m/channel/.*/cliplink/.*$|i',
566 | '|^https?://tv\\.kakao\\.com/channel/v/.*$|i',
567 | '|^https?://tv\\.kakao\\.com/channel/.*/livelink/.*$|i',
568 | '|^https?://tv\\.kakao\\.com/m/channel/.*/livelink/.*$|i',
569 | '|^https?://tv\\.kakao\\.com/channel/l/.*$|i',
570 | ],
571 | 'http://www.kickstarter.com/services/oembed' => [
572 | '|^https?://www\\.kickstarter\\.com/projects/.*$|i',
573 | ],
574 | 'https://www.kidoju.com/api/oembed' => [
575 | '|^https?://www\\.kidoju\\.com/en/x/.*/.*$|i',
576 | '|^https?://www\\.kidoju\\.com/fr/x/.*/.*$|i',
577 | ],
578 | 'https://halaman.email/service/oembed' => [
579 | '|^https?://halaman\\.email/form/.*$|i',
580 | '|^https?://aplikasi\\.kirim\\.email/form/.*$|i',
581 | ],
582 | 'https://embed.kit.co/oembed' => [
583 | '|^https?://kit\\.co/.*/.*$|i',
584 | ],
585 | 'http://www.kitchenbowl.com/oembed' => [
586 | '|^https?://www\\.kitchenbowl\\.com/recipe/.*$|i',
587 | ],
588 | 'https://api.kmdr.sh/services/oembed' => [
589 | '|^https?://app\\.kmdr\\.sh/h/.*$|i',
590 | '|^https?://app\\.kmdr\\.sh/history/.*$|i',
591 | ],
592 | 'https://jdr.knacki.info/oembed' => [
593 | '|^https?://jdr\\.knacki\\.info/meuh/.*$|i',
594 | ],
595 | 'https://api.spoonacular.com/knowledge/oembed' => [
596 | '|^https?://knowledgepad\\.co/\\#/knowledge/.*$|i',
597 | ],
598 | 'https://embed.kooapp.com/services/oembed' => [
599 | '|^https?://.*\\.kooapp\\.com/koo/.*$|i',
600 | ],
601 | 'https://kurozora.app/oembed' => [
602 | '|^https?://kurozora\\.app/episodes.*$|i',
603 | '|^https?://kurozora\\.app/songs.*$|i',
604 | ],
605 | 'http://learningapps.org/oembed.php' => [
606 | '|^https?://learningapps\\.org/.*$|i',
607 | ],
608 | 'https://umotion-test.univ-lemans.fr/oembed' => [
609 | '|^https?://umotion\\-test\\.univ\\-lemans\\.fr/video/.*$|i',
610 | ],
611 | 'https://pod.univ-lille.fr/video/oembed' => [
612 | '|^https?://pod\\.univ\\-lille\\.fr/video/.*$|i',
613 | ],
614 | 'https://place.line.me/oembed' => [
615 | '|^https?://place\\.line\\.me/businesses/.*$|i',
616 | ],
617 | 'https://livestream.com/oembed' => [
618 | '|^https?://livestream\\.com/accounts/.*/events/.*$|i',
619 | '|^https?://livestream\\.com/accounts/.*/events/.*/videos/.*$|i',
620 | '|^https?://livestream\\.com/.*/events/.*$|i',
621 | '|^https?://livestream\\.com/.*/events/.*/videos/.*$|i',
622 | '|^https?://livestream\\.com/.*/.*$|i',
623 | '|^https?://livestream\\.com/.*/.*/videos/.*$|i',
624 | ],
625 | 'https://embed.lottiefiles.com/oembed' => [
626 | '|^https?://lottiefiles\\.com/.*$|i',
627 | '|^https?://.*\\.lottiefiles\\.com/.*$|i',
628 | '|^https?://.*\\.lottie\\.host/.*$|i',
629 | '|^https?://lottie\\.host/.*$|i',
630 | ],
631 | 'https://app.ludus.one/oembed' => [
632 | '|^https?://app\\.ludus\\.one/.*$|i',
633 | ],
634 | 'https://admin.lumiere.is/api/services/oembed' => [
635 | '|^https?://.*\\.lumiere\\.is/v/.*$|i',
636 | ],
637 | 'http://mathembed.com/oembed' => [
638 | '|^https?://mathembed\\.com/latex\\?inputText\\=.*$|i',
639 | ],
640 | 'https://my.matterport.com/api/v1/models/oembed/' => [
641 | '|^https?://matterport\\.com/.*$|i',
642 | ],
643 | 'https://me.me/oembed' => [
644 | '|^https?://me\\.me/i/.*$|i',
645 | ],
646 | 'https://mdstrm.com/oembed' => [
647 | '|^https?://mdstrm\\.com/embed/.*$|i',
648 | '|^https?://mdstrm\\.com/live\\-stream/.*$|i',
649 | '|^https?://mdstrm\\.com/image/.*$|i',
650 | ],
651 | 'https://medienarchiv.zhdk.ch/oembed.json' => [
652 | '|^https?://medienarchiv\\.zhdk\\.ch/entries/.*$|i',
653 | ],
654 | 'https://mermaid.ink/services/oembed' => [
655 | '|^https?://mermaid\\.ink/img/.*$|i',
656 | '|^https?://mermaid\\.ink/svg/.*$|i',
657 | ],
658 | 'https://web.microsoftstream.com/oembed' => [
659 | '|^https?://.*\\.microsoftstream\\.com/video/.*$|i',
660 | '|^https?://.*\\.microsoftstream\\.com/channel/.*$|i',
661 | ],
662 | 'https://oembed.minervaknows.com' => [
663 | '|^https?://www\\.minervaknows\\.com/featured\\-recipes/.*$|i',
664 | '|^https?://www\\.minervaknows\\.com/themes/.*$|i',
665 | '|^https?://www\\.minervaknows\\.com/themes/.*/recipes/.*$|i',
666 | '|^https?://app\\.minervaknows\\.com/recipes/.*$|i',
667 | '|^https?://app\\.minervaknows\\.com/recipes/.*/follow$|i',
668 | ],
669 | 'https://miro.com/api/v1/oembed' => [
670 | '|^https?://miro\\.com/app/board/.*$|i',
671 | ],
672 | 'https://www.mixcloud.com/oembed/' => [
673 | '|^https?://www\\.mixcloud\\.com/.*/.*/$|i',
674 | ],
675 | 'https://mixpanel.com/api/app/embed/oembed/' => [
676 | '|^https?://mixpanel\\.com/.*$|i',
677 | ],
678 | 'http://api.mobypicture.com/oEmbed' => [
679 | '|^https?://www\\.mobypicture\\.com/user/.*/view/.*$|i',
680 | '|^https?://moby\\.to/.*$|i',
681 | ],
682 | 'https://musicboxmaniacs.com/embed/' => [
683 | '|^https?://musicboxmaniacs\\.com/explore/melody/.*$|i',
684 | ],
685 | 'https://mybeweeg.com/services/oembed' => [
686 | '|^https?://mybeweeg\\.com/w/.*$|i',
687 | ],
688 | 'https://namchey.com/api/oembed' => [
689 | '|^https?://namchey\\.com/embeds/.*$|i',
690 | ],
691 | 'https://www.nanoo.tv/services/oembed' => [
692 | '|^https?://.*\\.nanoo\\.tv/link/.*$|i',
693 | '|^https?://nanoo\\.tv/link/.*$|i',
694 | '|^https?://.*\\.nanoo\\.pro/link/.*$|i',
695 | '|^https?://nanoo\\.pro/link/.*$|i',
696 | '|^https?://media\\.zhdk\\.ch/signatur/.*$|i',
697 | '|^https?://new\\.media\\.zhdk\\.ch/signatur/.*$|i',
698 | ],
699 | 'https://api.nb.no/catalog/v1/oembed' => [
700 | '|^https?://www\\.nb\\.no/items/.*$|i',
701 | ],
702 | 'https://naturalatlas.com/oembed.json' => [
703 | '|^https?://naturalatlas\\.com/.*$|i',
704 | '|^https?://naturalatlas\\.com/.*/.*$|i',
705 | '|^https?://naturalatlas\\.com/.*/.*/.*$|i',
706 | '|^https?://naturalatlas\\.com/.*/.*/.*/.*$|i',
707 | ],
708 | 'https://ndla.no/oembed' => [
709 | '|^https?://ndla\\.no/.*$|i',
710 | '|^https?://ndla\\.no/article/.*$|i',
711 | '|^https?://ndla\\.no/audio/.*$|i',
712 | '|^https?://ndla\\.no/concept/.*$|i',
713 | '|^https?://ndla\\.no/image/.*$|i',
714 | '|^https?://ndla\\.no/video/.*$|i',
715 | ],
716 | 'https://api.neetorecord.com/api/v1/oembed' => [
717 | '|^https?://.*\\.neetorecord\\.com/watch/.*$|i',
718 | ],
719 | 'http://www.nfb.ca/remote/services/oembed/' => [
720 | '|^https?://.*\\.nfb\\.ca/film/.*$|i',
721 | ],
722 | 'https://oembed.nopaste.ml' => [
723 | '|^https?://nopaste\\.ml/.*$|i',
724 | ],
725 | 'https://api.observablehq.com/oembed' => [
726 | '|^https?://observablehq\\.com/@.*/.*$|i',
727 | '|^https?://observablehq\\.com/d/.*$|i',
728 | '|^https?://observablehq\\.com/embed/.*$|i',
729 | ],
730 | 'https://www.odds.com.au/api/oembed/' => [
731 | '|^https?://www\\.odds\\.com\\.au/.*$|i',
732 | '|^https?://odds\\.com\\.au/.*$|i',
733 | ],
734 | 'https://song.link/oembed' => [
735 | '|^https?://song\\.link/.*$|i',
736 | '|^https?://album\\.link/.*$|i',
737 | '|^https?://artist\\.link/.*$|i',
738 | '|^https?://playlist\\.link/.*$|i',
739 | '|^https?://pods\\.link/.*$|i',
740 | '|^https?://mylink\\.page/.*$|i',
741 | '|^https?://odesli\\.co/.*$|i',
742 | ],
743 | 'https://odysee.com/$/oembed' => [
744 | '|^https?://odysee\\.com/.*/.*$|i',
745 | '|^https?://odysee\\.com/.*$|i',
746 | ],
747 | 'http://official.fm/services/oembed.json' => [
748 | '|^https?://official\\.fm/tracks/.*$|i',
749 | '|^https?://official\\.fm/playlists/.*$|i',
750 | ],
751 | 'https://omniscope.me/_global_/oembed/json' => [
752 | '|^https?://omniscope\\.me/.*$|i',
753 | ],
754 | 'https://omny.fm/oembed' => [
755 | '|^https?://omny\\.fm/shows/.*$|i',
756 | ],
757 | 'http://orbitvu.co/service/oembed' => [
758 | '|^https?://orbitvu\\.co/001/.*/ov3601/view$|i',
759 | '|^https?://orbitvu\\.co/001/.*/ov3601/.*/view$|i',
760 | '|^https?://orbitvu\\.co/001/.*/ov3602/.*/view$|i',
761 | '|^https?://orbitvu\\.co/001/.*/2/orbittour/.*/view$|i',
762 | '|^https?://orbitvu\\.co/001/.*/1/2/orbittour/.*/view$|i',
763 | ],
764 | 'https://origits.net/oembed' => [
765 | '|^https?://origits\\.com/v/.*$|i',
766 | ],
767 | 'https://origits.com/oembed' => [
768 | '|^https?://origits\\.com/v/.*$|i',
769 | ],
770 | 'https://outplayed.tv/oembed' => [
771 | '|^https?://outplayed\\.tv/media/.*$|i',
772 | ],
773 | 'https://overflow.io/services/oembed' => [
774 | '|^https?://overflow\\.io/s/.*$|i',
775 | '|^https?://overflow\\.io/embed/.*$|i',
776 | ],
777 | 'https://core.oz.com/oembed' => [
778 | '|^https?://www\\.oz\\.com/.*/video/.*$|i',
779 | ],
780 | 'https://padlet.com/oembed/' => [
781 | '|^https?://padlet\\.com/.*$|i',
782 | ],
783 | 'https://api-v2.pandavideo.com.br/oembed' => [
784 | '|^https?://.*\\.tv\\.pandavideo\\.com\\.br/embed/\\?v\\=.*$|i',
785 | '|^https?://.*\\.tv\\.pandavideo\\.com\\.br/.*/playlist\\.m3u8$|i',
786 | '|^https?://dashboard\\.pandavideo\\.com\\.br/\\#/videos/.*$|i',
787 | ],
788 | 'https://www.pastery.net/oembed' => [
789 | '|^https?://pastery\\.net/.*$|i',
790 | '|^https?://www\\.pastery\\.net/.*$|i',
791 | ],
792 | 'https://api.picturelfy.com/service/oembed/' => [
793 | '|^https?://www\\.picturelfy\\.com/p/.*$|i',
794 | ],
795 | 'https://piggy.to/oembed' => [
796 | '|^https?://piggy\\.to/@.*/.*$|i',
797 | '|^https?://piggy\\.to/view/.*$|i',
798 | ],
799 | 'https://builder.pikasso.xyz/api/oembed' => [
800 | '|^https?://.*\\.builder\\.pikasso\\.xyz/embed/.*$|i',
801 | ],
802 | 'https://beta.pingvp.com.kpnis.nl/p/oembed.php' => [
803 | '|^https?://www\\.pingvp\\.com/.*$|i',
804 | ],
805 | 'https://tools.pinpoll.com/oembed' => [
806 | '|^https?://tools\\.pinpoll\\.com/embed/.*$|i',
807 | ],
808 | 'https://www.pinterest.com/oembed.json' => [
809 | '|^https?://www\\.pinterest\\.com/.*$|i',
810 | ],
811 | 'https://player.pitchhub.com/en/public/oembed' => [
812 | '|^https?://player\\.pitchhub\\.com/en/public/player/.*$|i',
813 | ],
814 | 'https://store.pixdor.com/oembed' => [
815 | '|^https?://store\\.pixdor\\.com/place\\-marker\\-widget/.*/show$|i',
816 | '|^https?://store\\.pixdor\\.com/map/.*/show$|i',
817 | ],
818 | 'https://app.plusdocs.com/oembed' => [
819 | '|^https?://app\\.plusdocs\\.com/.*/snapshots/.*$|i',
820 | '|^https?://app\\.plusdocs\\.com/.*/pages/edit/.*$|i',
821 | '|^https?://app\\.plusdocs\\.com/.*/pages/share/.*$|i',
822 | ],
823 | 'https://api.podbean.com/v1/oembed' => [
824 | '|^https?://.*\\.podbean\\.com/e/.*$|i',
825 | ],
826 | 'http://polldaddy.com/oembed/' => [
827 | '|^https?://.*\\.polldaddy\\.com/s/.*$|i',
828 | '|^https?://.*\\.polldaddy\\.com/poll/.*$|i',
829 | '|^https?://.*\\.polldaddy\\.com/ratings/.*$|i',
830 | ],
831 | 'https://api.portfolium.com/oembed' => [
832 | '|^https?://portfolium\\.com/entry/.*$|i',
833 | ],
834 | 'https://gateway.cobalt.run/present/decks/oembed' => [
835 | '|^https?://present\\.do/decks/.*$|i',
836 | ],
837 | 'https://prezi.com/v/oembed' => [
838 | '|^https?://prezi\\.com/v/.*$|i',
839 | '|^https?://.*\\.prezi\\.com/v/.*$|i',
840 | ],
841 | 'https://qtpi.gg/fashion/oembed' => [
842 | '|^https?://qtpi\\.gg/fashion/.*$|i',
843 | ],
844 | 'http://www.quiz.biz/api/oembed' => [
845 | '|^https?://www\\.quiz\\.biz/quizz\\-.*\\.html$|i',
846 | ],
847 | 'http://www.quizz.biz/api/oembed' => [
848 | '|^https?://www\\.quizz\\.biz/quizz\\-.*\\.html$|i',
849 | ],
850 | 'https://oembed.radiopublic.com/oembed' => [
851 | '|^https?://play\\.radiopublic\\.com/.*$|i',
852 | '|^https?://radiopublic\\.com/.*$|i',
853 | '|^https?://www\\.radiopublic\\.com/.*$|i',
854 | '|^https?://.*\\.radiopublic\\.com/.*$|i',
855 | ],
856 | 'https://pub.raindrop.io/api/oembed' => [
857 | '|^https?://raindrop\\.io/.*$|i',
858 | '|^https?://raindrop\\.io/.*/.*$|i',
859 | '|^https?://raindrop\\.io/.*/.*/.*$|i',
860 | '|^https?://raindrop\\.io/.*/.*/.*/.*$|i',
861 | ],
862 | 'https://animatron.com/oembed' => [
863 | '|^https?://www\\.rcvis\\.com/v/.*$|i',
864 | '|^https?://www\\.rcvis\\.com/visualize\\=.*$|i',
865 | '|^https?://www\\.rcvis\\.com/ve/.*$|i',
866 | '|^https?://www\\.rcvis\\.com/visualizeEmbedded\\=.*$|i',
867 | ],
868 | 'https://www.reddit.com/oembed' => [
869 | '|^https?://reddit\\.com/r/.*/comments/.*/.*$|i',
870 | '|^https?://www\\.reddit\\.com/r/.*/comments/.*/.*$|i',
871 | ],
872 | 'http://publisher.releasewire.com/oembed/' => [
873 | '|^https?://rwire\\.com/.*$|i',
874 | ],
875 | 'https://replit.com/data/oembed' => [
876 | '|^https?://repl\\.it/@.*/.*$|i',
877 | '|^https?://replit\\.com/@.*/.*$|i',
878 | ],
879 | 'https://www.reverbnation.com/oembed' => [
880 | '|^https?://www\\.reverbnation\\.com/.*$|i',
881 | '|^https?://www\\.reverbnation\\.com/.*/songs/.*$|i',
882 | ],
883 | 'http://roomshare.jp/en/oembed.json' => [
884 | '|^https?://roomshare\\.jp/post/.*$|i',
885 | '|^https?://roomshare\\.jp/en/post/.*$|i',
886 | ],
887 | 'https://roosterteeth.com/oembed' => [
888 | '|^https?://roosterteeth\\.com/.*$|i',
889 | ],
890 | 'https://rumble.com/api/Media/oembed.json' => [
891 | '|^https?://rumble\\.com/.*$|i',
892 | ],
893 | 'https://embed.runkit.com/oembed' => [
894 | '|^https?://embed\\.runkit\\.com/.*,$|i',
895 | ],
896 | 'https://octopus.saooti.com/oembed' => [
897 | '|^https?://octopus\\.saooti\\.com/main/pub/podcast/.*$|i',
898 | ],
899 | 'http://videos.sapo.pt/oembed' => [
900 | '|^https?://videos\\.sapo\\.pt/.*$|i',
901 | ],
902 | 'https://api.screen9.com/oembed' => [
903 | '|^https?://console\\.screen9\\.com/.*$|i',
904 | '|^https?://.*\\.screen9\\.tv/.*$|i',
905 | ],
906 | 'https://api.screencast.com/external/oembed' => [
907 | '|^https?://www\\.screencast\\.com/.*$|i',
908 | ],
909 | 'http://www.screenr.com/api/oembed.json' => [
910 | '|^https?://www\\.screenr\\.com/.*/$|i',
911 | ],
912 | 'https://scribblemaps.com/api/services/oembed.json' => [
913 | '|^https?://www\\.scribblemaps\\.com/maps/view/.*$|i',
914 | '|^https?://scribblemaps\\.com/maps/view/.*$|i',
915 | ],
916 | 'http://www.scribd.com/services/oembed/' => [
917 | '|^https?://www\\.scribd\\.com/doc/.*$|i',
918 | ],
919 | 'https://embed.sendtonews.com/services/oembed' => [
920 | '|^https?://embed\\.sendtonews\\.com/oembed/.*$|i',
921 | ],
922 | 'https://shared-file-kappa.vercel.app/file/api/oembed' => [
923 | '|^https?://shared\\-file\\-kappa\\.vercel\\.app/file/.*$|i',
924 | ],
925 | 'https://shopshare.tv/api/shopcast/oembed' => [
926 | '|^https?://shopshare\\.tv/shopboard/.*$|i',
927 | '|^https?://shopshare\\.tv/shopcast/.*$|i',
928 | ],
929 | 'https://www.shortnote.jp/oembed/' => [
930 | '|^https?://www\\.shortnote\\.jp/view/notes/.*$|i',
931 | ],
932 | 'http://shoudio.com/api/oembed' => [
933 | '|^https?://shoudio\\.com/.*$|i',
934 | '|^https?://shoud\\.io/.*$|i',
935 | ],
936 | 'https://api.getshow.io/oembed.json' => [
937 | '|^https?://app\\.getshow\\.io/iframe/.*$|i',
938 | '|^https?://.*\\.getshow\\.io/share/.*$|i',
939 | ],
940 | 'https://showtheway.io/oembed' => [
941 | '|^https?://showtheway\\.io/to/.*$|i',
942 | ],
943 | 'https://simplecast.com/oembed' => [
944 | '|^https?://simplecast\\.com/s/.*$|i',
945 | ],
946 | 'https://onsizzle.com/oembed' => [
947 | '|^https?://onsizzle\\.com/i/.*$|i',
948 | ],
949 | 'http://sketchfab.com/oembed' => [
950 | '|^https?://sketchfab\\.com/.*models/.*$|i',
951 | '|^https?://sketchfab\\.com/.*/folders/.*$|i',
952 | ],
953 | 'https://www.slideshare.net/api/oembed/2' => [
954 | '|^https?://www\\.slideshare\\.net/.*/.*$|i',
955 | '|^https?://fr\\.slideshare\\.net/.*/.*$|i',
956 | '|^https?://de\\.slideshare\\.net/.*/.*$|i',
957 | '|^https?://es\\.slideshare\\.net/.*/.*$|i',
958 | '|^https?://pt\\.slideshare\\.net/.*/.*$|i',
959 | ],
960 | 'https://smashnotes.com/services/oembed' => [
961 | '|^https?://smashnotes\\.com/p/.*$|i',
962 | '|^https?://smashnotes\\.com/p/.*/e/.* \\- smashnotes\\.com/p/.*/e/.*/s/.*$|i',
963 | ],
964 | 'https://open.smeme.com/api/oembed' => [
965 | '|^https?://open\\.smeme\\.com/.*$|i',
966 | ],
967 | 'https://www.smrthi.com/api/oembed' => [
968 | '|^https?://www\\.smrthi\\.com/book/.*$|i',
969 | ],
970 | 'https://api.smugmug.com/services/oembed/' => [
971 | '|^https?://.*\\.smugmug\\.com/.*$|i',
972 | ],
973 | 'https://www.socialexplorer.com/services/oembed/' => [
974 | '|^https?://www\\.socialexplorer\\.com/.*/explore$|i',
975 | '|^https?://www\\.socialexplorer\\.com/.*/view$|i',
976 | '|^https?://www\\.socialexplorer\\.com/.*/edit$|i',
977 | '|^https?://www\\.socialexplorer\\.com/.*/embed$|i',
978 | ],
979 | 'https://soundcloud.com/oembed' => [
980 | '|^https?://soundcloud\\.com/.*$|i',
981 | '|^https?://on\\.soundcloud\\.com/.*$|i',
982 | '|^https?://soundcloud\\.app\\.goog\\.gl/.*$|i',
983 | ],
984 | 'https://speakerdeck.com/oembed.json' => [
985 | '|^https?://speakerdeck\\.com/.*/.*$|i',
986 | ],
987 | 'https://open.spotify.com/oembed' => [
988 | '|^https?://open\\.spotify\\.com/.*$|i',
989 | '|^https?://spotify\\:.*$|i',
990 | ],
991 | 'https://api.spotlightr.com/getOEmbed' => [
992 | '|^https?://.*\\.spotlightr\\.com/watch/.*$|i',
993 | '|^https?://.*\\.spotlightr\\.com/publish/.*$|i',
994 | '|^https?://.*\\.cdn\\.spotlightr\\.com/watch/.*$|i',
995 | '|^https?://.*\\.cdn\\.spotlightr\\.com/publish/.*$|i',
996 | ],
997 | 'https://api.spreaker.com/oembed' => [
998 | '|^https?://.*\\.spreaker\\.com/.*$|i',
999 | ],
1000 | 'http://sproutvideo.com/oembed.json' => [
1001 | '|^https?://sproutvideo\\.com/videos/.*$|i',
1002 | '|^https?://.*\\.vids\\.io/videos/.*$|i',
1003 | ],
1004 | 'https://api.spyke.social/embed/oembed' => [
1005 | '|^https?://spyke\\.social/p/.*$|i',
1006 | '|^https?://spyke\\.social/u/.*$|i',
1007 | '|^https?://spyke\\.social/g/.*$|i',
1008 | '|^https?://spyke\\.social/c/.*$|i',
1009 | '|^https?://www\\.spyke\\.social/p/.*$|i',
1010 | '|^https?://www\\.spyke\\.social/u/.*$|i',
1011 | '|^https?://www\\.spyke\\.social/g/.*$|i',
1012 | '|^https?://www\\.spyke\\.social/c/.*$|i',
1013 | ],
1014 | 'https://purl.stanford.edu/embed.json' => [
1015 | '|^https?://purl\\.stanford\\.edu/.*$|i',
1016 | ],
1017 | 'https://api.streamable.com/oembed.json' => [
1018 | '|^https?://streamable\\.com/.*$|i',
1019 | ],
1020 | 'https://streamio.com/api/v1/oembed' => [
1021 | '|^https?://s3m\\.io/.*$|i',
1022 | '|^https?://23m\\.io/.*$|i',
1023 | ],
1024 | 'https://subscribi.io/api/oembed' => [
1025 | '|^https?://subscribi\\.io/api/oembed.*$|i',
1026 | ],
1027 | 'https://www.sudomemo.net/oembed' => [
1028 | '|^https?://www\\.sudomemo\\.net/watch/.*$|i',
1029 | '|^https?://flipnot\\.es/.*$|i',
1030 | ],
1031 | 'https://www.sutori.com/api/oembed' => [
1032 | '|^https?://www\\.sutori\\.com/story/.*$|i',
1033 | ],
1034 | 'https://sway.com/api/v1.0/oembed' => [
1035 | '|^https?://sway\\.com/.*$|i',
1036 | '|^https?://www\\.sway\\.com/.*$|i',
1037 | ],
1038 | 'https://sway.office.com/api/v1.0/oembed' => [
1039 | '|^https?://sway\\.office\\.com/.*$|i',
1040 | ],
1041 | 'https://69jr5v75rc.execute-api.eu-west-1.amazonaws.com/prod/v2/oembed' => [
1042 | '|^https?://share\\.synthesia\\.io/.*$|i',
1043 | ],
1044 | 'https://www.ted.com/services/v1/oembed.json' => [
1045 | '|^https?://ted\\.com/talks/.*$|i',
1046 | '|^https?://www\\.ted\\.com/talks/.*$|i',
1047 | ],
1048 | 'https://www.nytimes.com/svc/oembed/json/' => [
1049 | '|^https?://www\\.nytimes\\.com/svc/oembed$|i',
1050 | '|^https?://nytimes\\.com/.*$|i',
1051 | '|^https?://.*\\.nytimes\\.com/.*$|i',
1052 | ],
1053 | 'https://theysaidso.com/extensions/oembed/' => [
1054 | '|^https?://theysaidso\\.com/image/.*$|i',
1055 | ],
1056 | 'https://www.tickcounter.com/oembed' => [
1057 | '|^https?://www\\.tickcounter\\.com/widget/.*$|i',
1058 | '|^https?://www\\.tickcounter\\.com/countdown/.*$|i',
1059 | '|^https?://www\\.tickcounter\\.com/countup/.*$|i',
1060 | '|^https?://www\\.tickcounter\\.com/ticker/.*$|i',
1061 | '|^https?://www\\.tickcounter\\.com/clock/.*$|i',
1062 | '|^https?://www\\.tickcounter\\.com/worldclock/.*$|i',
1063 | ],
1064 | 'https://www.tiktok.com/oembed' => [
1065 | '|^https?://www\\.tiktok\\.com/.*$|i',
1066 | '|^https?://www\\.tiktok\\.com/.*/video/.*$|i',
1067 | ],
1068 | 'https://tonicaudio.com/oembed' => [
1069 | '|^https?://tonicaudio\\.com/take/.*$|i',
1070 | '|^https?://tonicaudio\\.com/song/.*$|i',
1071 | '|^https?://tnic\\.io/song/.*$|i',
1072 | '|^https?://tnic\\.io/take/.*$|i',
1073 | ],
1074 | 'https://widget.toornament.com/oembed' => [
1075 | '|^https?://www\\.toornament\\.com/tournaments/.*/information$|i',
1076 | '|^https?://www\\.toornament\\.com/tournaments/.*/registration/$|i',
1077 | '|^https?://www\\.toornament\\.com/tournaments/.*/matches/schedule$|i',
1078 | '|^https?://www\\.toornament\\.com/tournaments/.*/stages/.*/$|i',
1079 | ],
1080 | 'http://www.topy.se/oembed/' => [
1081 | '|^https?://www\\.topy\\.se/image/.*$|i',
1082 | ],
1083 | 'https://app-test.totango.com/oembed' => [
1084 | '|^https?://app\\-test\\.totango\\.com/.*$|i',
1085 | ],
1086 | 'https://trackspace.upitup.com/oembed' => [
1087 | '|^https?://trackspace\\.upitup\\.com/.*$|i',
1088 | ],
1089 | 'https://trinitymedia.ai/services/oembed' => [
1090 | '|^https?://trinitymedia\\.ai/player/.*$|i',
1091 | '|^https?://trinitymedia\\.ai/player/.*/.*$|i',
1092 | '|^https?://trinitymedia\\.ai/player/.*/.*/.*$|i',
1093 | ],
1094 | 'https://www.tumblr.com/oembed/1.0' => [
1095 | '|^https?://.*\\.tumblr\\.com/post/.*$|i',
1096 | ],
1097 | 'https://www.tuxx.be/services/oembed' => [
1098 | '|^https?://www\\.tuxx\\.be/.*$|i',
1099 | ],
1100 | 'https://play.tvcf.co.kr/rest/oembed' => [
1101 | '|^https?://play\\.tvcf\\.co\\.kr/.*$|i',
1102 | '|^https?://.*\\.tvcf\\.co\\.kr/.*$|i',
1103 | ],
1104 | 'https://twinmotion.unrealengine.com/oembed' => [
1105 | '|^https?://twinmotion\\.unrealengine\\.com/presentation/.*$|i',
1106 | '|^https?://twinmotion\\.unrealengine\\.com/panorama/.*$|i',
1107 | ],
1108 | 'https://publish.twitter.com/oembed' => [
1109 | '|^https?://twitter\\.com/.*$|i',
1110 | '|^https?://twitter\\.com/.*/status/.*$|i',
1111 | '|^https?://.*\\.twitter\\.com/.*/status/.*$|i',
1112 | ],
1113 | 'https://play.typecast.ai/oembed' => [
1114 | '|^https?://play\\.typecast\\.ai/s/.*$|i',
1115 | '|^https?://play\\.typecast\\.ai/e/.*$|i',
1116 | '|^https?://play\\.typecast\\.ai/.*$|i',
1117 | ],
1118 | 'https://typlog.com/oembed' => [
1119 | '|^https?://typlog\\.com.*$|i',
1120 | ],
1121 | 'https://uapod.univ-antilles.fr/oembed' => [
1122 | '|^https?://uapod\\.univ\\-antilles\\.fr/video/.*$|i',
1123 | ],
1124 | 'https://map.cam.ac.uk/oembed/' => [
1125 | '|^https?://map\\.cam\\.ac\\.uk/.*$|i',
1126 | ],
1127 | 'https://mediatheque.univ-paris1.fr/oembed' => [
1128 | '|^https?://mediatheque\\.univ\\-paris1\\.fr/video/.*$|i',
1129 | ],
1130 | 'https://pod.u-pec.fr/oembed' => [
1131 | '|^https?://pod\\.u\\-pec\\.fr/video/.*$|i',
1132 | ],
1133 | 'http://www.ustream.tv/oembed' => [
1134 | '|^https?://.*\\.ustream\\.tv/.*$|i',
1135 | '|^https?://.*\\.ustream\\.com/.*$|i',
1136 | ],
1137 | 'https://app.ustudio.com/api/v2/oembed' => [
1138 | '|^https?://.*\\.ustudio\\.com/embed/.*$|i',
1139 | '|^https?://.*\\.ustudio\\.com/embed/.*/.*$|i',
1140 | ],
1141 | 'https://api.veer.tv/oembed' => [
1142 | '|^https?://veer\\.tv/videos/.*$|i',
1143 | ],
1144 | 'https://api.veervr.tv/oembed' => [
1145 | '|^https?://veervr\\.tv/videos/.*$|i',
1146 | ],
1147 | 'https://www.vevo.com/oembed' => [
1148 | '|^https?://www\\.vevo\\.com/.*$|i',
1149 | ],
1150 | 'https://videfit.com/oembed' => [
1151 | '|^https?://videfit\\.com/videos/.*$|i',
1152 | ],
1153 | 'https://vidmount.com/oembed' => [
1154 | '|^https?://vidmount\\.com/.*$|i',
1155 | ],
1156 | 'https://api.vidyard.com/dashboard/v1.1/oembed' => [
1157 | '|^https?://.*\\.vidyard\\.com/.*$|i',
1158 | '|^https?://.*\\.hubs\\.vidyard\\.com/.*$|i',
1159 | ],
1160 | 'https://vimeo.com/api/oembed.json' => [
1161 | '|^https?://vimeo\\.com/.*$|i',
1162 | '|^https?://vimeo\\.com/album/.*/video/.*$|i',
1163 | '|^https?://vimeo\\.com/channels/.*/.*$|i',
1164 | '|^https?://vimeo\\.com/groups/.*/videos/.*$|i',
1165 | '|^https?://vimeo\\.com/ondemand/.*/.*$|i',
1166 | '|^https?://player\\.vimeo\\.com/video/.*$|i',
1167 | '|^https?://vimeo\\.com/event/.*/.*$|i',
1168 | ],
1169 | 'https://play.viostream.com/oembed' => [
1170 | '|^https?://share\\.viostream\\.com/.*$|i',
1171 | ],
1172 | 'https://www.viously.com/oembed' => [
1173 | '|^https?://www\\.viously\\.com/.*/.*$|i',
1174 | ],
1175 | 'https://vizydrop.com/oembed' => [
1176 | '|^https?://vizydrop\\.com/shared/.*$|i',
1177 | ],
1178 | 'https://vlipsy.com/oembed' => [
1179 | '|^https?://vlipsy\\.com/.*$|i',
1180 | ],
1181 | 'https://www.vlive.tv/oembed' => [
1182 | '|^https?://www\\.vlive\\.tv/video/.*$|i',
1183 | ],
1184 | 'https://embed.vouchfor.com/v1/oembed' => [
1185 | '|^https?://.*\\.vouchfor\\.com/.*$|i',
1186 | ],
1187 | 'https://data.voxsnap.com/oembed' => [
1188 | '|^https?://article\\.voxsnap\\.com/.*/.*$|i',
1189 | ],
1190 | 'https://waltrack.net/oembed' => [
1191 | '|^https?://waltrack\\.net/product/.*$|i',
1192 | ],
1193 | 'https://embed.wave.video/oembed' => [
1194 | '|^https?://watch\\.wave\\.video/.*$|i',
1195 | '|^https?://embed\\.wave\\.video/.*$|i',
1196 | ],
1197 | 'https://www.web3isgoinggreat.com/api/oembed' => [
1198 | '|^https?://www\\.web3isgoinggreat\\.com/\\?id\\=.*$|i',
1199 | '|^https?://www\\.web3isgoinggreat\\.com/single/.*$|i',
1200 | '|^https?://www\\.web3isgoinggreat\\.com/embed/.*$|i',
1201 | ],
1202 | 'https://play.wecandeo.com/oembed/' => [
1203 | '|^https?://play\\.wecandeo\\.com/video/v/.*$|i',
1204 | ],
1205 | 'https://whimsical.com/api/oembed' => [
1206 | '|^https?://whimsical\\.com/.*$|i',
1207 | ],
1208 | 'https://fast.wistia.com/oembed.json' => [
1209 | '|^https?://fast\\.wistia\\.com/embed/iframe/.*$|i',
1210 | '|^https?://fast\\.wistia\\.com/embed/playlists/.*$|i',
1211 | '|^https?://.*\\.wistia\\.com/medias/.*$|i',
1212 | ],
1213 | 'https://app.wizer.me/api/oembed.json' => [
1214 | '|^https?://.*\\.wizer\\.me/learn/.*$|i',
1215 | '|^https?://.*\\.wizer\\.me/preview/.*$|i',
1216 | ],
1217 | 'https://wokwi.com/api/oembed' => [
1218 | '|^https?://wokwi\\.com/share/.*$|i',
1219 | ],
1220 | 'https://www.wolframcloud.com/oembed' => [
1221 | '|^https?://.*\\.wolframcloud\\.com/.*$|i',
1222 | ],
1223 | 'http://public-api.wordpress.com/oembed/' => [
1224 | '|^https?://wordpress\\.com/.*$|i',
1225 | '|^https?://.*\\.wordpress\\.com/.*$|i',
1226 | '|^https?://.*\\..*\\.wordpress\\.com/.*$|i',
1227 | '|^https?://wp\\.me/.*$|i',
1228 | ],
1229 | 'https://publish.x.com/oembed' => [
1230 | '|^https?://x\\.com/.*$|i',
1231 | '|^https?://x\\.com/.*/status/.*$|i',
1232 | '|^https?://.*\\.x\\.com/.*/status/.*$|i',
1233 | ],
1234 | 'https://www.youtube.com/oembed' => [
1235 | '|^https?://.*\\.youtube\\.com/watch.*$|i',
1236 | '|^https?://.*\\.youtube\\.com/v/.*$|i',
1237 | '|^https?://youtu\\.be/.*$|i',
1238 | '|^https?://.*\\.youtube\\.com/playlist\\?list\\=.*$|i',
1239 | '|^https?://youtube\\.com/playlist\\?list\\=.*$|i',
1240 | '|^https?://.*\\.youtube\\.com/shorts.*$|i',
1241 | '|^https?://youtube\\.com/shorts.*$|i',
1242 | '|^https?://.*\\.youtube\\.com/embed/.*$|i',
1243 | ],
1244 | 'https://www.yumpu.com/services/oembed' => [
1245 | '|^https?://www\\.yumpu\\.com/.*/document/view/.*/.*$|i',
1246 | ],
1247 | 'https://app.zeplin.io/embed' => [
1248 | '|^https?://app\\.zeplin\\.io/project/.*/screen/.*$|i',
1249 | '|^https?://app\\.zeplin\\.io/project/.*/screen/.*/version/.*$|i',
1250 | '|^https?://app\\.zeplin\\.io/project/.*/styleguide/components\\?coid\\=.*$|i',
1251 | '|^https?://app\\.zeplin\\.io/styleguide/.*/components\\?coid\\=.*$|i',
1252 | ],
1253 | 'https://app.zingsoft.com/oembed' => [
1254 | '|^https?://app\\.zingsoft\\.com/embed/.*$|i',
1255 | '|^https?://app\\.zingsoft\\.com/view/.*$|i',
1256 | ],
1257 | 'https://api.znipe.tv/v3/oembed/' => [
1258 | '|^https?://.*\\.znipe\\.tv/.*$|i',
1259 | ],
1260 | 'https://srv2.zoomable.ca/oembed' => [
1261 | '|^https?://srv2\\.zoomable\\.ca/viewer\\.php.*$|i',
1262 | ],
1263 | ];
1264 |
--------------------------------------------------------------------------------