├── 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}"; 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 | --------------------------------------------------------------------------------