Add embed/embed

This commit is contained in:
Jay 2021-11-15 14:10:52 +01:00
parent 92ef29d3e0
commit a0394b2361
99 changed files with 13651 additions and 1 deletions

4
.gitignore vendored
View file

@ -1,3 +1,5 @@
/vendor
/vendor/*
!/vendor/embed
/.vscode
/includes/config/config.php

174
vendor/embed/embed/CHANGELOG.md vendored Normal file
View file

@ -0,0 +1,174 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [4.3.5] - 2021-10-10
### Fixed
- Updated oEmbed endpoints
- Fixed embed code for Instagram [#456], [#459]
### Security
- Fixed a possible XML Quadratic Blowup vulnerability.
## [4.3.4] - 2021-06-22
### Fixed
- Urls of images should include the same url for the `$info->image` value. [#452]
## [4.3.3] - 2021-06-22
### Fixed
- Facebook embed redirects to `/login`. [#450], [#451]
## [4.3.2] - 2021-04-04
### Fixed
- Add configured oEmbed query parameters to all oEmbed endpoints [#437]
- Updated oEmbed endpoints.
- Replaced Travis with Github workflows for testing
## [4.3.1] - 2021-03-21
### Added
- Support for binary files (video, audio, images, etc) [#412] [#413]
### Fixed
- Oembed for facebook photos [#405] [#406]
- Oembed for facebook videos [#432] [#433]
- Added more ways to detect data using meta tags [#427]
- Bandcamp provider name [#429] [#430]
## [4.3.0] - 2020-11-04
### Added
- New function `$embed->setSettings()` to pass the settings before get the site info
### Fixed
- PHP 8 compatibility [#394]
- Facebook and Instagram adapted to the new API changes [#392] [#399]
## [4.2.7] - 2020-09-23
### Added
- New option `twitch:parent` to fix Twitch embed with iframes [#384]
### Fixed
- Added `datePublished` check to `PublishedTime` extractor [#385] [#386]
- Added `@property-read` for IDE suppport [#387] [#388]
## [4.2.6] - 2020-08-28
### Fixed
- Code width and height when the provided value is not numeric (ex: 100%) [#380]
## [4.2.5] - 2020-08-01
### Fixed
- Github TypeError exception with some urls [#375]
## [4.2.4] - 2020-07-06
### Fixed
- Ignore invalid urls instead throw an exception
- Updated oembed list of endpoints
## [4.2.3] - 2020-06-12
### Fixed
- Suppport for other non-latin alphabets such Persian or Arabic [#366]
## [4.2.2] - 2020-05-31
### Fixed
- Provided a fallback for oEmbed compatible sites like Instagram that redirects to login page [#357]
## [4.2.1] - 2020-05-25
### Fixed
- Redirect urls like `t.co`.
## [4.2.0] - 2020-05-23
### Added
- Added the `ignored_errors` settings to ignore some curls errors instead throw an exception [#355]
- Support for Twitch embeds [#332]
### Fixed
- Ignored linkedData errors [#356]
## [4.1.1] - 2020-04-24
### Added
- Updated oembed endpoints from `oembed.com`
- Add support for tiktok.com
## [4.1.0] - 2020-04-19
### Added
- Ability to send settings to `CurlClient`. Added the `cookies_path` setting to customize the file used for cookies. [#345]
- `Document::selectCss()` function to select elements using css selectors instead xpath (it requires `symfony/css-selector`)
- `Document::removeCss()` function to remove elements using css selectors instead xpath (it requires `symfony/css-selector`)
- Ability to configure OEmbed parameters from the outside using the `oembed:query_parameters` setting [#346]
## 4.0.0 - 2020-03-13
Full library refactoring.
### Added
- Support for multiple parallel request with `curl_multi`
- Support for PSR-7 Http Messages, PSR-17 Http Factories and PSR-18 Http Client
- `cms` value
- `language` to detect the page language
- `languages` to detect urls to versions in different languages
- `favicon` to detect small favicons (16 or 32px)
- `icon` to detect big icons (from 48px)
### Changed
- Changed providers (oEmbed, Html, OpenGraph etc) by independent detectors (title, url, language etc).
- The `tags` value is renamed to `keywords`
- Use Psr standards instead custom interfaces.
- Improved tests using cached responses.
### Removed
- Support for PHP<7.4
- `type` value (is was very confusing)
- `images` value
- `providerImage` (use `favicon` or `icon` instead)
- Support for files (pdf, jpg, video, etc).
[#332]: https://github.com/oscarotero/Embed/issues/332
[#345]: https://github.com/oscarotero/Embed/issues/345
[#346]: https://github.com/oscarotero/Embed/issues/346
[#355]: https://github.com/oscarotero/Embed/issues/355
[#356]: https://github.com/oscarotero/Embed/issues/356
[#357]: https://github.com/oscarotero/Embed/issues/357
[#366]: https://github.com/oscarotero/Embed/issues/366
[#375]: https://github.com/oscarotero/Embed/issues/375
[#380]: https://github.com/oscarotero/Embed/issues/380
[#384]: https://github.com/oscarotero/Embed/issues/384
[#385]: https://github.com/oscarotero/Embed/issues/385
[#386]: https://github.com/oscarotero/Embed/issues/386
[#387]: https://github.com/oscarotero/Embed/issues/387
[#388]: https://github.com/oscarotero/Embed/issues/388
[#392]: https://github.com/oscarotero/Embed/issues/392
[#394]: https://github.com/oscarotero/Embed/issues/394
[#399]: https://github.com/oscarotero/Embed/issues/399
[#405]: https://github.com/oscarotero/Embed/issues/405
[#406]: https://github.com/oscarotero/Embed/issues/406
[#412]: https://github.com/oscarotero/Embed/issues/412
[#413]: https://github.com/oscarotero/Embed/issues/413
[#427]: https://github.com/oscarotero/Embed/issues/427
[#429]: https://github.com/oscarotero/Embed/issues/429
[#430]: https://github.com/oscarotero/Embed/issues/430
[#432]: https://github.com/oscarotero/Embed/issues/432
[#433]: https://github.com/oscarotero/Embed/issues/433
[#437]: https://github.com/oscarotero/Embed/issues/437
[#450]: https://github.com/oscarotero/Embed/issues/450
[#451]: https://github.com/oscarotero/Embed/issues/451
[#452]: https://github.com/oscarotero/Embed/issues/452
[#456]: https://github.com/oscarotero/Embed/issues/456
[#459]: https://github.com/oscarotero/Embed/issues/459
[4.3.5]: https://github.com/oscarotero/Embed/compare/v4.3.4...v4.3.5
[4.3.4]: https://github.com/oscarotero/Embed/compare/v4.3.3...v4.3.4
[4.3.3]: https://github.com/oscarotero/Embed/compare/v4.3.2...v4.3.3
[4.3.2]: https://github.com/oscarotero/Embed/compare/v4.3.1...v4.3.2
[4.3.1]: https://github.com/oscarotero/Embed/compare/v4.3.0...v4.3.1
[4.3.0]: https://github.com/oscarotero/Embed/compare/v4.2.7...v4.3.0
[4.2.7]: https://github.com/oscarotero/Embed/compare/v4.2.6...v4.2.7
[4.2.6]: https://github.com/oscarotero/Embed/compare/v4.2.5...v4.2.6
[4.2.5]: https://github.com/oscarotero/Embed/compare/v4.2.4...v4.2.5
[4.2.4]: https://github.com/oscarotero/Embed/compare/v4.2.3...v4.2.4
[4.2.3]: https://github.com/oscarotero/Embed/compare/v4.2.2...v4.2.3
[4.2.2]: https://github.com/oscarotero/Embed/compare/v4.2.1...v4.2.2
[4.2.1]: https://github.com/oscarotero/Embed/compare/v4.2.0...v4.2.1
[4.2.0]: https://github.com/oscarotero/Embed/compare/v4.1.1...v4.2.0
[4.1.1]: https://github.com/oscarotero/Embed/compare/v4.1.0...v4.1.1
[4.1.0]: https://github.com/oscarotero/Embed/compare/v4.0.0...v4.1.0

21
vendor/embed/embed/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Oscar Otero Marzoa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

350
vendor/embed/embed/README.md vendored Normal file
View file

@ -0,0 +1,350 @@
# Embed
[![Latest Version on Packagist][ico-version]][link-packagist]
[![Total Downloads][ico-downloads]][link-packagist]
[![Monthly Downloads][ico-m-downloads]][link-packagist]
[![Software License][ico-license]](LICENSE)
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).
Requirements:
* PHP 7.4+
* Curl library installed
* PSR-17 implementation. By default these libraries are detected automatically:
* [laminas/laminas-diactoros](https://github.com/laminas/laminas-diactoros)
* [guzzle/psr7](https://github.com/guzzle/psr7) (Only the unreleased version 2.x, installed as `dev-master`)
* [nyholm/psr7](https://github.com/Nyholm/psr7)
* [sunrise/http-message](https://github.com/sunrise-php/http-message)
> If you need PHP 5.5-7.3 support, [use the 3.x version](https://github.com/oscarotero/Embed/tree/v3.x)
## Online demo
http://oscarotero.com/embed/demo
## Installation
This package is installable and autoloadable via Composer as [embed/embed](https://packagist.org/packages/embed/embed).
```
$ composer require embed/embed
```
## Usage
```php
use Embed\Embed;
$embed = new Embed();
//Load any url:
$info = $embed->get('https://www.youtube.com/watch?v=PP1xn5wHtxE');
//Get content info
$info->title; //The page title
$info->description; //The page description
$info->url; //The canonical url
$info->keywords; //The page keywords
$info->image; //The thumbnail or main image
$info->code->html; //The code to embed the image, video, etc
$info->code->width; //The exact width of the embed code (if exists)
$info->code->height; //The exact height of the embed code (if exists)
$info->code->aspectRatio; //The aspect ratio (width/height)
$info->authorName; //The resource author
$info->authorUrl; //The author url
$info->cms; //The cms used
$info->language; //The language of the page
$info->languages; //The alternative languages
$info->providerName; //The provider name of the page (Youtube, Twitter, Instagram, etc)
$info->providerUrl; //The provider url
$info->icon; //The big icon of the site
$info->favicon; //The favicon of the site (an .ico file or a png with up to 32x32px)
$info->publishedTime; //The published time of the resource
$info->license; //The license url of the resource
$info->feeds; //The RSS/Atom feeds
```
## Parallel multiple requests
```php
use Embed\Embed;
$embed = new Embed();
//Load multiple urls asynchronously:
$infos = $embed->getMulti(
'https://www.youtube.com/watch?v=PP1xn5wHtxE',
'https://twitter.com/carlosmeixidefl/status/1230894146220625933',
'https://en.wikipedia.org/wiki/Tordoia',
);
foreach ($infos as $info) {
echo $info->title;
}
```
## Document
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:
```php
//Get the document object
$document = $info->getDocument();
$document->link('image_src'); //Returns the href of a <link>
$document->getDocument(); //Returns the DOMDocument instance
$html = (string) $document; //Returns the html code
$document->select('.//h1'); //Search
```
You can perform xpath queries in order to select specific elements. A search always return an instance of a `Embed\QueryResult`:
```php
//Search the A elements
$result = $document->select('.//a');
//Filter the results
$result->filter(fn ($node) => $node->getAttribute('href'));
$id = $result->str('id'); //Return the id of the first result as string
$text = $result->str(); //Return the content of the first result
$ids = $result->strAll('id'); //Return an array with the ids of all results as string
$texts = $result->strAll(); //Return an array with the content of all results as string
$tabindex = $result->int('tabindex'); //Return the tabindex attribute of the first result as integer
$number = $result->int(); //Return the content of the first result as integer
$href = $result->url('href'); //Return the href attribute of the first result as url (converts relative urls to absolutes)
$url = $result->url(); //Return the content of the first result as url
$node = $result->node(); //Return the first node found (DOMElement)
$nodes = $result->nodes(); //Return all nodes found
```
## Metas
For convenience, the object `Metas` stores the value of all `<meta>` 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`.
```php
//Get the Metas object
$metas = $info->getMetas();
$metas->all(); //Return all values
$metas->get('og:title'); //Return a key value
$metas->str('og:title'); //Return the value as string (remove html tags)
$metas->html('og:description'); //Return the value as html
$metas->int('og:video:width'); //Return the value as integer
$metas->url('og:url'); //Return the value as full url (converts relative urls to absolutes)
```
## OEmbed
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:
```php
//Get the oEmbed object
$oembed = $info->getOEmbed();
$oembed->all(); //Return all raw data
$oembed->get('title'); //Return a key value
$oembed->str('title'); //Return the value as string (remove html tags)
$oembed->html('html'); //Return the value as html
$oembed->int('width'); //Return the value as integer
$oembed->url('url'); //Return the value as full url (converts relative urls to absolutes)
```
Additional oEmbed parameters (like instagrams `hidecaption`) can also be provided:
```php
$embed = new Embed();
$result = $embed->get('https://www.instagram.com/p/B_C0wheCa4V/');
$result->setSettings([
'oembed:query_parameters' => ['hidecaption' => true]
]);
$oembed = $info->getOEmbed();
```
## LinkedData
Another API available by default, used to extract info using the [JsonLD](https://www.w3.org/TR/json-ld/) schema.
```php
//Get the linkedData object
$ld = $info->getLinkedData();
$ld->all(); //Return all data
$ld->get('name'); //Return a key value
$ld->str('name'); //Return the value as string (remove html tags)
$ld->html('description'); //Return the value as html
$ld->int('width'); //Return the value as integer
$ld->url('url'); //Return the value as full url (converts relative urls to absolutes)
```
## Other APIs
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:
```php
//Get the API object
$api = $info->getApi();
$api->all(); //Return all raw data
$api->get('title'); //Return a key value
$api->str('title'); //Return the value as string (remove html tags)
$api->html('html'); //Return the value as html
$api->int('width'); //Return the value as integer
$api->url('url'); //Return the value as full url (converts relative urls to absolutes)
```
## Extending Embed
Depending of your needs, you may want to extend this library with extra features or change the way it makes some operations.
### PSR
Embed use some PSR standards to be the most interoperable possible:
- [PSR-7](https://www.php-fig.org/psr/psr-7/) Standard interfaces to represent http requests, responses and uris
- [PSR-17](https://www.php-fig.org/psr/psr-17/) Standard factories to create PSR-7 objects
- [PSR-18](https://www.php-fig.org/psr/psr-18/) Standard interface to send a http request and return a response
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:
```php
use Embed\Embed;
use Embed\Http\Crawler;
$client = new CustomHttpClient();
$requestFactory = new CustomRequestFactory();
$uriFactory = new CustomUriFactory();
//The Crawler is responsible for perform http queries
$crawler = new Crawler($client, $requestFactory, $uriFactory);
//Create an embed instance passing the Crawler
$embed = new Embed($crawler);
```
### Adapters
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.
Before creating an adapter, you need to understand how Embed work: when you execute this code, you get a `Extractor` class
```php
//Get the Extractor with all info
$info = $embed->get($url);
//The extractor have document and oembed:
$document = $info->getDocument();
$oembed = $info->getOEmbed();
```
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.
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.
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.
```php
use Embed\Embed;
$embed = new Embed();
$factory = $embed->getExtractorFactory();
//Use this MySite adapter for mysite.com
$factory->addAdapter('mysite.com', MySite::class);
//Remove the adapter for pinterest.com, so it will use the default extractor
$factory->removeAdapter('pinterest.com');
//Change the default extractor
$factory->setDefault(CustomExtractor::class);
```
### Detectors
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:
```php
use Embed\Embed;
use Embed\Detectors\Detector;
class Robots extends Detector
{
public function detect(): ?string
{
$response = $this->extractor->getResponse();
$metas = $this->extractor->getMetas();
return $response->getHeaderLine('x-robots-tag'),
?: $metas->str('robots');
}
}
//Register the detector
$embed = new Embed();
$embed->getExtractorFactory()->addDetector('robots', Robots::class);
//Use it
$info = $embed->get('http://example.com');
$robots = $info->robots;
```
### Settings
If you need to pass settings to the CurlClient to perform http queries:
```php
use Embed\Embed;
use Embed\Http\Crawler;
use Embed\Http\CurlClient;
$client = new CurlClient();
$client->setSettings([
'cookies_path' => $cookies_path,
'ignored_errors' => [18]
]);
$embed = new Embed(new Crawler($client));
```
If you need to pass settings to your detectors, you can add settings to the `ExtractorFactory`:
```php
use Embed\Embed;
$embed = new Embed();
$embed->setSettings([
'oembed:query_parameters' => [], //Extra parameters send to oembed
'twitch:parent' => 'example.com', //Required to embed twitch videos as iframe
'facebook:token' => '1234|5678', //Required to embed content from Facebook
'instagram:token' => '1234|5678', //Required to embed content from Instagram
]);
$info = $embed->get($url);
```
Note: The built-in detectors does not require settings. This feature is only for convenience if you create a specific detector that requires settings.
---
If this library is useful for you, say thanks [buying me a beer :beer:](https://www.paypal.me/oscarotero)!
[ico-version]: https://poser.pugx.org/embed/embed/v/stable
[ico-license]: https://poser.pugx.org/embed/embed/license
[ico-downloads]: https://poser.pugx.org/embed/embed/downloads
[ico-m-downloads]: https://poser.pugx.org/embed/embed/d/monthly
[link-packagist]: https://packagist.org/packages/embed/embed

72
vendor/embed/embed/composer.json vendored Normal file
View file

@ -0,0 +1,72 @@
{
"name": "embed/embed",
"type": "library",
"description": "PHP library to retrieve page info using oembed, opengraph, etc",
"keywords": [
"oembed",
"opengraph",
"twitter cards",
"embed",
"embedly"
],
"homepage": "https://github.com/oscarotero/Embed",
"license": "MIT",
"authors": [
{
"name": "Oscar Otero",
"email": "oom@oscarotero.com",
"homepage": "http://oscarotero.com",
"role": "Developer"
}
],
"support": {
"email": "oom@oscarotero.com",
"issues": "https://github.com/oscarotero/Embed/issues"
},
"require": {
"php": "^7.4|^8",
"ext-curl": "*",
"ext-dom": "*",
"ext-json": "*",
"ext-mbstring": "*",
"composer/ca-bundle": "^1.0",
"oscarotero/html-parser": "^0.1.4",
"psr/http-message": "^1.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"ml/json-ld": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"friendsofphp/php-cs-fixer": "^2.0",
"nyholm/psr7": "^1.2",
"oscarotero/php-cs-fixer-config": "^1.0",
"brick/varexporter": "^0.3.1",
"symfony/css-selector": "^5.0"
},
"suggest": {
"symfony/css-selector": "If you want to get elements using css selectors"
},
"autoload": {
"psr-4": {
"Embed\\": "src"
},
"files": [
"src/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"Embed\\Tests\\": "tests/"
}
},
"scripts": {
"demo": "php -S localhost:8888 demo/index.php",
"test": "phpunit",
"cs-fix": "php-cs-fixer fix",
"update-resources": [
"php scripts/update-oembed.php",
"php scripts/update-suffix.php"
]
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Archive;
use Embed\HttpApiTrait;
class Api
{
use HttpApiTrait;
protected function fetchData(): array
{
$this->endpoint = $this->extractor->getUri()->withQuery('output=json');
return $this->fetchJSON($this->endpoint);
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Archive\Detectors;
use Embed\Detectors\AuthorName as Detector;
class AuthorName extends Detector
{
public function detect(): ?string
{
$api = $this->extractor->getApi();
return $api->str('metadata', 'creator')
?: parent::detect();
}
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Archive\Detectors;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
use function Embed\matchPath;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
$uri = $this->extractor->getUri();
$path = $uri->getPath();
if (!matchPath('/details/*', $path)) {
return null;
}
$src = $uri->withPath(str_replace('/details/', '/embed/', $path));
$width = 640;
$height = 480;
$html = html('iframe', [
'src' => $src,
'width' => $width,
'height' => $height,
'style' => 'border:none',
'frameborder' => 0,
'allowTransparency' => 'true',
]);
return new EmbedCode($html, $width, $height);
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Archive\Detectors;
use Embed\Detectors\Description as Detector;
class Description extends Detector
{
public function detect(): ?string
{
$api = $this->extractor->getApi();
return $api->str('metadata', 'extract')
?: parent::detect();
}
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Archive\Detectors;
use Embed\Detectors\ProviderName as Detector;
class ProviderName extends Detector
{
public function detect(): string
{
return 'Internet Archive';
}
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Archive\Detectors;
use Datetime;
use Embed\Detectors\PublishedTime as Detector;
class PublishedTime extends Detector
{
public function detect(): ?Datetime
{
$api = $this->extractor->getApi();
return $api->time('metadata', 'publicdate')
?: $api->time('metadata', 'addeddate')
?: $api->time('metadata', 'date')
?: parent::detect();
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Archive\Detectors;
use Embed\Detectors\Title as Detector;
class Title extends Detector
{
public function detect(): ?string
{
$api = $this->extractor->getApi();
return $api->str('metadata', 'title')
?: parent::detect();
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Archive;
use Embed\Extractor as Base;
class Extractor extends Base
{
private Api $api;
public function getApi(): Api
{
return $this->api;
}
public function createCustomDetectors(): array
{
$this->api = new Api($this);
return [
'title' => new Detectors\Title($this),
'description' => new Detectors\Description($this),
'code' => new Detectors\Code($this),
'authorName' => new Detectors\AuthorName($this),
'providerName' => new Detectors\ProviderName($this),
'publishedTime' => new Detectors\PublishedTime($this),
];
}
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Bandcamp\Detectors;
use Embed\Detectors\ProviderName as Detector;
class ProviderName extends Detector
{
public function detect(): string
{
return 'Bandcamp';
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Bandcamp;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'providerName' => new Detectors\ProviderName($this),
];
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\CadenaSer\Detectors;
use function Embed\cleanPath;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
use function Embed\matchPath;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$uri = $this->extractor->getUri();
if (!matchPath('/audio/*', $uri->getPath())) {
return null;
}
$path = cleanPath('/widget/'.$uri->getPath());
$src = $uri->withPath($path);
$html = html('iframe', [
'src' => $src,
'frameborder' => 0,
'width' => '100%',
'height' => '360',
'allowTransparency' => 'true',
]);
return new EmbedCode($html, null, 360);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\CadenaSer;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Facebook\Detectors;
use Embed\Detectors\Title as Detector;
class Title extends Detector
{
/**
* Do not use og:title and twitter:title
*/
public function detect(): ?string
{
$document = $this->extractor->getDocument();
$oembed = $this->extractor->getOEmbed();
return $oembed->str('title')
?: $document->select('.//head/title')->str();
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Facebook;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
$this->oembed = new OEmbed($this);
return [
'title' => new Detectors\Title($this),
];
}
}

View file

@ -0,0 +1,80 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Facebook;
use Embed\OEmbed as Base;
use Psr\Http\Message\UriInterface;
class OEmbed extends Base
{
const ENDPOINT_PAGE = 'https://graph.facebook.com/v11.0/oembed_page';
const ENDPOINT_POST = 'https://graph.facebook.com/v11.0/oembed_post';
const ENDPOINT_VIDEO = 'https://graph.facebook.com/v11.0/oembed_video';
protected function detectEndpoint(): ?UriInterface
{
$token = $this->extractor->getSetting('facebook:token');
if (!$token) {
return null;
}
$uri = $this->extractor->getUri();
if (strpos($uri->getPath(), 'login') !== false) {
parse_str($uri->getQuery(), $params);
if (!empty($params['next'])) {
$uri = $this->extractor->getCrawler()->createUri($params['next']);
}
}
$queryParameters = $this->getOembedQueryParameters((string) $uri);
$queryParameters['access_token'] = $token;
return $this->extractor->getCrawler()
->createUri($this->getEndpointByPath($uri->getPath()))
->withQuery(http_build_query($queryParameters));
}
private function getEndpointByPath(string $path): string
{
/* Videos
https://www.facebook.com/{page-name}/videos/{video-id}/
https://www.facebook.com/{username}/videos/{video-id}/
https://www.facebook.com/video.php?id={video-id}
https://www.facebook.com/video.php?v={video-id}
*/
if (strpos($path, '/video.php') === 0
|| strpos($path, '/videos/') !== false
) {
return self::ENDPOINT_VIDEO;
}
/* Posts
https://www.facebook.com/{page-name}/posts/{post-id}
https://www.facebook.com/{username}/posts/{post-id}
https://www.facebook.com/{username}/activity/{activity-id}
https://www.facebook.com/photo.php?fbid={photo-id}
https://www.facebook.com/photos/{photo-id}
https://www.facebook.com/permalink.php?story_fbid={post-id}
https://www.facebook.com/media/set?set={set-id}
https://www.facebook.com/questions/{question-id}
https://www.facebook.com/notes/{username}/{note-url}/{note-id}
Not in the facebook docs:
https://www.facebook.com/{page-name}/photos/{post-id}/{photo-id}
*/
if (strpos($path, '/photo.php') === 0
|| strpos($path, '/photos/') !== false
|| strpos($path, '/permalink.php') === 0
|| strpos($path, '/media/') === 0
|| strpos($path, '/questions/') === 0
|| strpos($path, '/notes/') === 0
|| strpos($path, '/posts/') !== false
|| strpos($path, '/activity/') !== false
) {
return self::ENDPOINT_POST;
}
return self::ENDPOINT_PAGE;
}
}

View file

@ -0,0 +1,44 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Flickr\Detectors;
use function Embed\cleanPath;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
use function Embed\matchPath;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$uri = $this->extractor->getUri();
if (!matchPath('/photos/*', $uri->getPath())) {
return null;
}
$path = cleanPath($uri->getPath().'/player');
$src = $uri->withPath($path);
$width = 640;
$height = 425;
$html = html('iframe', [
'src' => $src,
'width' => $width,
'height' => $height,
'style' => 'border:none',
'frameborder' => 0,
'allowTransparency' => 'true',
]);
return new EmbedCode($html, $width, $height);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Flickr;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Gist;
use Embed\HttpApiTrait;
class Api
{
use HttpApiTrait;
protected function fetchData(): array
{
$uri = $this->extractor->getUri();
$this->endpoint = $uri->withPath($uri->getPath().'.json');
return $this->fetchJSON($this->endpoint);
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Gist\Detectors;
use Embed\Detectors\AuthorName as Detector;
class AuthorName extends Detector
{
public function detect(): ?string
{
$api = $this->extractor->getApi();
return $api->str('owner')
?: parent::detect();
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Gist\Detectors;
use Embed\Detectors\AuthorUrl as Detector;
use Psr\Http\Message\UriInterface;
class AuthorUrl extends Detector
{
public function detect(): ?UriInterface
{
$api = $this->extractor->getApi();
$owner = $api->str('owner');
if ($owner) {
return $this->extractor->getCrawler()->createUri("https://github.com/{$owner}");
}
return parent::detect();
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Gist\Detectors;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$api = $this->extractor->getApi();
$code = $api->html('div');
$stylesheet = $api->str('stylesheet');
if ($code && $stylesheet) {
return new EmbedCode(
html('link', ['rel' => 'stylesheet', 'href' => $stylesheet]).$code
);
}
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Gist\Detectors;
use Datetime;
use Embed\Detectors\PublishedTime as Detector;
class PublishedTime extends Detector
{
public function detect(): ?Datetime
{
$api = $this->extractor->getApi();
return $api->time('created_at')
?: parent::detect();
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Gist;
use Embed\Extractor as Base;
class Extractor extends Base
{
private Api $api;
public function getApi(): Api
{
return $this->api;
}
public function createCustomDetectors(): array
{
$this->api = new Api($this);
return [
'authorName' => new Detectors\AuthorName($this),
'authorUrl' => new Detectors\AuthorUrl($this),
'publishedTime' => new Detectors\PublishedTime($this),
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Github\Detectors;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
use function Embed\matchPath;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$uri = $this->extractor->getUri();
$path = $uri->getPath();
if (!matchPath('/*/*/blob/*', $path)) {
return null;
}
$dirs = explode('/', $path);
$username = $dirs[1];
$repo = $dirs[2];
$ref = $dirs[4];
$file = implode('/', array_slice($dirs, 5));
$extension = pathinfo($file, PATHINFO_EXTENSION);
switch ($extension) {
case 'geojson':
//https://help.github.com/articles/mapping-geojson-files-on-github/#embedding-your-map-elsewhere
return new EmbedCode(html('script', ['src' => "https://embed.githubusercontent.com/view/geojson/{$username}/{$repo}/{$ref}/{$file}"]));
case 'stl':
//https://help.github.com/articles/3d-file-viewer/#embedding-your-model-elsewhere
return new EmbedCode(html('script', ['src' => "https://embed.githubusercontent.com/view/3d/{$username}/{$repo}/{$ref}/{$file}"]));
}
return null;
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Github;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Ideone\Detectors;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$uri = $this->extractor->getUri();
$id = explode('/', $uri->getPath())[1];
if (empty($id)) {
return null;
}
return new EmbedCode(
html('script', ['src' => "https://ideone.com/e.js/{$id}"])
);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Ideone;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\ImageShack;
use function Embed\getDirectory;
use Embed\HttpApiTrait;
use function Embed\matchPath;
class Api
{
use HttpApiTrait;
protected function fetchData(): array
{
$uri = $this->extractor->getUri();
if (!matchPath('/i/*', $uri->getPath())) {
$uri = $this->extractor->getRequest()->getUri();
if (!matchPath('/i/*', $uri->getPath())) {
return [];
}
}
$id = getDirectory($uri->getPath(), 1);
if (empty($id)) {
return [];
}
$this->endpoint = $this->extractor->getCrawler()->createUri("https://api.imageshack.com/v2/images/{$id}");
$data = $this->fetchJSON($this->endpoint);
return $data['result'] ?? [];
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\ImageShack\Detectors;
use Embed\Detectors\AuthorName as Detector;
class AuthorName extends Detector
{
public function detect(): ?string
{
$api = $this->extractor->getApi();
return $api->str('owner', 'username')
?: parent::detect();
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\ImageShack\Detectors;
use Embed\Detectors\AuthorUrl as Detector;
use Psr\Http\Message\UriInterface;
class AuthorUrl extends Detector
{
public function detect(): ?UriInterface
{
$api = $this->extractor->getApi();
$owner = $api->str('owner', 'username');
if ($owner) {
return $this->extractor->getCrawler()->createUri("https://imageshack.com/{$owner}");
}
return parent::detect();
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\ImageShack\Detectors;
use Embed\Detectors\Description as Detector;
class Description extends Detector
{
public function detect(): ?string
{
$api = $this->extractor->getApi();
return $api->str('description')
?: parent::detect();
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\ImageShack\Detectors;
use Embed\Detectors\Image as Detector;
use Psr\Http\Message\UriInterface;
class Image extends Detector
{
public function detect(): ?UriInterface
{
$api = $this->extractor->getApi();
return $api->url('direct_link')
?: parent::detect();
}
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\ImageShack\Detectors;
use Embed\Detectors\ProviderName as Detector;
class ProviderName extends Detector
{
public function detect(): string
{
return 'ImageShack';
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\ImageShack\Detectors;
use Datetime;
use Embed\Detectors\PublishedTime as Detector;
class PublishedTime extends Detector
{
public function detect(): ?Datetime
{
$api = $this->extractor->getApi();
return $api->time('creation_date')
?: parent::detect();
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\ImageShack\Detectors;
use Embed\Detectors\Title as Detector;
class Title extends Detector
{
public function detect(): ?string
{
$api = $this->extractor->getApi();
return $api->str('title')
?: parent::detect();
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\ImageShack;
use Embed\Extractor as Base;
class Extractor extends Base
{
private Api $api;
public function getApi(): Api
{
return $this->api;
}
public function createCustomDetectors(): array
{
$this->api = new Api($this);
return [
'authorName' => new Detectors\AuthorName($this),
'authorUrl' => new Detectors\AuthorUrl($this),
'description' => new Detectors\Description($this),
'image' => new Detectors\Image($this),
'providerName' => new Detectors\ProviderName($this),
'publishedTime' => new Detectors\PublishedTime($this),
'title' => new Detectors\Title($this),
];
}
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Instagram;
use Embed\Extractor as Base;
use Embed\Http\Crawler;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
class Extractor extends Base
{
public function __construct(UriInterface $uri, RequestInterface $request, ResponseInterface $response, Crawler $crawler)
{
parent::__construct($uri, $request, $response, $crawler);
$this->oembed = new OEmbed($this);
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Instagram;
use Embed\OEmbed as Base;
use Psr\Http\Message\UriInterface;
class OEmbed extends Base
{
const ENDPOINT = 'https://graph.facebook.com/v8.0/instagram_oembed';
protected function detectEndpoint(): ?UriInterface
{
$token = $this->extractor->getSetting('instagram:token');
if (!$token) {
return null;
}
$uri = $this->extractor->getUri();
if (strpos($uri->getPath(), 'login') !== false) {
$uri = $this->extractor->getRequest()->getUri();
}
$queryParameters = $this->getOembedQueryParameters((string) $uri);
$queryParameters['access_token'] = $token;
return $this->extractor->getCrawler()
->createUri(self::ENDPOINT)
->withQuery(http_build_query($queryParameters));
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Pinterest\Detectors;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
use function Embed\matchPath;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$uri = $this->extractor->getUri();
if (!matchPath('/pin/*', $uri->getPath())) {
return null;
}
$html = [
html('a', [
'data-pin-do' => 'embedPin',
'href' => $uri,
]),
html('script', [
'async' => true,
'defer' => true,
'src' => '//assets.pinterest.com/js/pinit.js',
]),
];
return new EmbedCode(implode('', $html), 236, 442);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Pinterest;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Sassmeister\Detectors;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
use function Embed\matchPath;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$uri = $this->extractor->getUri();
if (!matchPath('/gist/*', $uri->getPath())) {
return null;
}
$id = explode('/', $uri->getPath())[2];
$height = 480;
$html = [
html('p', [
'class' => 'sassmeister',
'data-gist-id' => $id,
'data-height' => $height,
'data-theme' => 'tomorrow',
], '<a href="http://sassmeister.com/gist/'.$id.'">Play with this gist on SassMeister.</a>'),
html('script', [
'src' => 'http://cdn.sassmeister.com/js/embed.js',
'async' => true,
]),
];
return new EmbedCode(implode('', $html), null, $height);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Sassmeister;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Slides\Detectors;
use function Embed\cleanPath;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$uri = $this->extractor->getUri();
$path = cleanPath($uri->getPath().'/embed');
$src = $uri->withPath($path);
$width = 576;
$height = 420;
$html = html('iframe', [
'src' => $src,
'width' => $width,
'height' => $height,
'style' => 'border:none',
'frameborder' => 0,
'allowTransparency' => 'true',
]);
return new EmbedCode($html, $width, $height);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Slides;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Snipplr\Detectors;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
use function Embed\matchPath;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$uri = $this->extractor->getUri();
if (!matchPath('/view/*', $uri->getPath())) {
return null;
}
$id = explode('/', $uri->getPath())[2];
$html = [
html('div', [
'id' => "snipplr_embed_{$id}",
'class' => 'snipplr_embed',
], '<a target="blank" href="https://snipplr.com/view/'.$id.'">View this snippet</a> on Snipplr'),
html('script', [
'type' => 'text/javascript',
'src' => 'https://snipplr.com/js/embed.js',
]),
html('script', [
'type' => 'text/javascript',
'src' => "https://snipplr.com/json/{$id}",
]),
];
return new EmbedCode(implode('', $html));
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Snipplr;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,82 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Twitch\Detectors;
use Embed\Detectors\Code as Detector;
use Embed\EmbedCode;
use function Embed\html;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): ?EmbedCode
{
$path = $this->extractor->getUri()->getPath();
$parent = $this->extractor->getSetting('twitch:parent');
if ($id = self::getVideoId($path)) {
$code = $parent
? self::generateIframeCode(['id' => $id, 'parent' => $parent])
: self::generateJsCode('video', $id);
return new EmbedCode($code, 620, 378);
}
if ($id = self::getChannelId($path)) {
$code = $parent
? self::generateIframeCode(['channel' => $id, 'parent' => $parent])
: self::generateJsCode('channel', $id);
return new EmbedCode($code, 620, 378);
}
return null;
}
private static function getVideoId(string $path): ?string
{
if (preg_match('#^/videos/(\d+)$#', $path, $matches)) {
return $matches[1];
}
return null;
}
private static function getChannelId(string $path): ?string
{
if (preg_match('#^/(\w+)$#', $path, $matches)) {
return $matches[1];
}
return null;
}
private static function generateIframeCode(array $params): string
{
$query = http_build_query(['autoplay' => 'false'] + $params);
return html('iframe', [
'src' => "https://player.twitch.tv/?{$query}",
'frameborder' => 0,
'allowfullscreen' => 'true',
'scrolling' => 'no',
'height' => 378,
'width' => 620,
]);
}
private static function generateJsCode($key, $value)
{
return <<<HTML
<div id="twitch-embed"></div>
<script src="https://player.twitch.tv/js/embed/v1.js"></script>
<script type="text/javascript">
new Twitch.Player("twitch-embed", { {$key}: "{$value}" });
</script>
HTML;
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Twitch;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'code' => new Detectors\Code($this),
];
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Wikipedia;
use function Embed\getDirectory;
use Embed\HttpApiTrait;
use function Embed\matchPath;
class Api
{
use HttpApiTrait;
protected function fetchData(): array
{
$uri = $this->extractor->getUri();
if (!matchPath('/wiki/*', $uri->getPath())) {
return [];
}
$titles = getDirectory($uri->getPath(), 1);
$this->endpoint = $uri
->withPath('/w/api.php')
->withQuery(http_build_query([
'action' => 'query',
'format' => 'json',
'continue' => '',
'titles' => $titles,
'prop' => 'extracts',
'exchars' => 1000,
]));
$data = $this->fetchJSON($this->endpoint);
$pages = $data['query']['pages'] ?? null;
return $pages ? current($pages) : null;
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Wikipedia\Detectors;
use Embed\Detectors\Description as Detector;
class Description extends Detector
{
public function detect(): ?string
{
$api = $this->extractor->getApi();
return $api->str('extract')
?: parent::detect();
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Wikipedia\Detectors;
use Embed\Detectors\Title as Detector;
class Title extends Detector
{
public function detect(): ?string
{
$api = $this->extractor->getApi();
return $api->str('title')
?: parent::detect();
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Wikipedia;
use Embed\Extractor as Base;
class Extractor extends Base
{
private Api $api;
public function getApi(): Api
{
return $this->api;
}
public function createCustomDetectors(): array
{
$this->api = new Api($this);
return [
'title' => new Detectors\Title($this),
'description' => new Detectors\Description($this),
];
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Youtube\Detectors;
use Embed\Detectors\Feeds as Detector;
use function Embed\getDirectory;
use function Embed\matchPath;
use Psr\Http\Message\UriInterface;
class Feeds extends Detector
{
/**
* @return UriInterface[]
*/
public function detect(): array
{
return parent::detect()
?: $this->fallback();
}
private function fallback(): array
{
$uri = $this->extractor->getUri();
if (!matchPath('/channel/*', $uri->getPath())) {
return [];
}
$id = getDirectory($uri->getPath(), 1);
$feed = $this->extractor->getCrawler()->createUri("https://www.youtube.com/feeds/videos.xml?channel_id={$id}");
return [$feed];
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Adapters\Youtube;
use Embed\Extractor as Base;
class Extractor extends Base
{
public function createCustomDetectors(): array
{
return [
'feeds' => new Detectors\Feeds($this),
];
}
}

107
vendor/embed/embed/src/ApiTrait.php vendored Normal file
View file

@ -0,0 +1,107 @@
<?php
declare(strict_types = 1);
namespace Embed;
use Datetime;
use Psr\Http\Message\UriInterface;
use Throwable;
trait ApiTrait
{
protected Extractor $extractor;
private array $data;
public function __construct(Extractor $extractor)
{
$this->extractor = $extractor;
}
public function all(): array
{
if (!isset($this->data)) {
$this->data = $this->fetchData();
}
return $this->data;
}
public function get(string ...$keys)
{
$data = $this->all();
foreach ($keys as $key) {
if (!isset($data[$key])) {
return null;
}
$data = $data[$key];
}
return $data;
}
public function str(string ...$keys): ?string
{
$value = $this->get(...$keys);
if (is_array($value)) {
$value = array_shift($value);
}
return $value ? clean((string) $value) : null;
}
public function strAll(string ...$keys): array
{
$all = (array) $this->get(...$keys);
return array_filter(array_map(fn ($value) => clean($value), $all));
}
public function html(string ...$keys): ?string
{
$value = $this->get(...$keys);
if (is_array($value)) {
$value = array_shift($value);
}
return $value ? clean((string) $value, true) : null;
}
public function int(string ...$keys): ?int
{
$value = $this->get(...$keys);
if (is_array($value)) {
$value = array_shift($value);
}
return is_numeric($value) ? (int) $value : null;
}
public function url(string ...$keys): ?UriInterface
{
$url = $this->str(...$keys);
try {
return $url ? $this->extractor->resolveUri($url) : null;
} catch (Throwable $error) {
return null;
}
}
public function time(string ...$keys): ?Datetime
{
$time = $this->str(...$keys);
$datetime = $time ? date_create($time) : null;
if (!$datetime && ctype_digit($time)) {
$datetime = date_create_from_format('U', $time);
}
return ($datetime && $datetime->getTimestamp() > 0) ? $datetime : null;
}
abstract protected function fetchData(): array;
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class AuthorName extends Detector
{
public function detect(): ?string
{
$oembed = $this->extractor->getOEmbed();
$metas = $this->extractor->getMetas();
return $oembed->str('author_name')
?: $metas->str(
'article:author',
'book:author',
'sailthru.author',
'lp.article:author',
'twitter:creator',
'dcterms.creator',
'author'
);
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Psr\Http\Message\UriInterface;
class AuthorUrl extends Detector
{
public function detect(): ?UriInterface
{
$oembed = $this->extractor->getOEmbed();
return $oembed->url('author_url')
?: $this->detectFromTwitter();
}
private function detectFromTwitter(): ?UriInterface
{
$metas = $this->extractor->getMetas();
$crawler = $this->extractor->getCrawler();
$user = $metas->str('twitter:creator');
return $user
? $crawler->createUri(sprintf('https://twitter.com/%s', ltrim($user, '@')))
: null;
}
}

View file

@ -0,0 +1,68 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class Cms extends Detector
{
public const BLOGSPOT = 'blogspot';
public const WORDPRESS = 'wordpress';
public const MEDIAWIKI = 'mediawiki';
public const OPENNEMAS = 'opennemas';
public function detect(): ?string
{
$cms = self::detectFromHost($this->extractor->url->getHost());
if ($cms) {
return $cms;
}
$document = $this->extractor->getDocument();
$generators = $document->select('.//meta', ['name' => 'generator'])->strAll('content');
foreach ($generators as $generator) {
if ($cms = self::detectFromGenerator($generator)) {
return $cms;
}
}
return null;
}
private static function detectFromHost(string $host): ?string
{
if (strpos($host, '.blogspot.com') !== false) {
return self::BLOGSPOT;
}
if (strpos($host, '.wordpress.com') !== false) {
return self::WORDPRESS;
}
return null;
}
private static function detectFromGenerator(string $generator): ?string
{
$generator = strtolower($generator);
if ($generator === 'blogger') {
return self::BLOGSPOT;
}
if (strpos($generator, 'mediawiki') === 0) {
return self::MEDIAWIKI;
}
if (strpos($generator, 'wordpress') === 0) {
return self::WORDPRESS;
}
if (strpos($generator, 'opennemas') === 0) {
return self::OPENNEMAS;
}
return null;
}
}

View file

@ -0,0 +1,142 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Embed\EmbedCode;
use function Embed\html;
class Code extends Detector
{
public function detect(): ?EmbedCode
{
return $this->detectFromEmbed()
?: $this->detectFromOpenGraph()
?: $this->detectFromTwitter()
?: $this->detectFromContentType();
}
private function detectFromEmbed(): ?EmbedCode
{
$oembed = $this->extractor->getOEmbed();
$html = $oembed->html('html');
if (!$html) {
return null;
}
return new EmbedCode(
$html,
$oembed->int('width'),
$oembed->int('height')
);
}
private function detectFromOpenGraph(): ?EmbedCode
{
$metas = $this->extractor->getMetas();
$url = $metas->url('og:video:secure_url', 'og:video:url', 'og:video');
if (!$url) {
return null;
}
if (!($type = pathinfo($url->getPath(), PATHINFO_EXTENSION))) {
$type = $metas->str('og:video_type');
}
$width = $metas->int('twitter:player:width');
$height = $metas->int('twitter:player:height');
switch ($type) {
case 'swf':
case 'application/x-shockwave-flash':
return null; //Ignore flash
case 'mp4':
case 'ogg':
case 'ogv':
case 'webm':
case 'application/mp4':
case 'video/mp4':
case 'video/ogg':
case 'video/ogv':
case 'video/webm':
$code = html('video', [
'src' => $url,
'width' => $width,
'height' => $height,
]);
break;
default:
$code = html('iframe', [
'src' => $url,
'frameborder' => 0,
'width' => $width,
'height' => $height,
'allowTransparency' => 'true',
]);
}
return new EmbedCode($code, $width, $height);
}
private function detectFromTwitter(): ?EmbedCode
{
$metas = $this->extractor->getMetas();
$url = $metas->url('twitter:player');
if (!$url) {
return null;
}
$width = $metas->int('twitter:player:width');
$height = $metas->int('twitter:player:height');
$code = html('iframe', [
'src' => $url,
'frameborder' => 0,
'width' => $width,
'height' => $height,
'allowTransparency' => 'true',
]);
return new EmbedCode($code, $width, $height);
}
private function detectFromContentType()
{
if (!$this->extractor->getResponse()->hasHeader('content-type')) {
return null;
}
$contentType = $this->extractor->getResponse()->getHeader('content-type')[0];
$isBinary = !preg_match('/(text|html|json)/', strtolower($contentType));
if (!$isBinary) {
return null;
}
$url = $this->extractor->getRequest()->getUri();
if (strpos($contentType, 'video/') === 0 || $contentType === 'application/mp4') {
$code = html('video', [
'src' => $url,
'controls' => true,
]);
} elseif (strpos($contentType, 'audio/') === 0) {
$code = html('audio', [
'src' => $url,
'controls' => true,
]);
} elseif (strpos($contentType, 'image/') === 0) {
$code = html('img', [
'src' => $url,
]);
} else {
return null;
}
return new EmbedCode($code);
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class Description extends Detector
{
public function detect(): ?string
{
$oembed = $this->extractor->getOEmbed();
$metas = $this->extractor->getMetas();
$ld = $this->extractor->getLinkedData();
return $oembed->str('description')
?: $metas->str(
'og:description',
'twitter:description',
'lp:description',
'description',
'article:description',
'dcterms.description',
'sailthru.description',
'excerpt',
'article.summary'
)
?: $ld->str('description');
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Embed\Extractor;
abstract class Detector
{
protected Extractor $extractor;
private array $cache;
public function __construct(Extractor $extractor)
{
$this->extractor = $extractor;
}
public function get()
{
if (!isset($this->cache)) {
$this->cache = [
'cached' => true,
'value' => $this->detect(),
];
}
return $this->cache['value'];
}
abstract public function detect();
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Psr\Http\Message\UriInterface;
class Favicon extends Detector
{
public function detect(): UriInterface
{
$document = $this->extractor->getDocument();
return $document->link('shortcut icon')
?: $document->link('icon')
?: $this->extractor->getUri()->withPath('/favicon.ico')->withQuery('');
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class Feeds extends Detector
{
private static $types = [
'application/atom+xml',
'application/json',
'application/rdf+xml',
'application/rss+xml',
'application/xml',
'text/xml',
];
/**
* @return \Psr\Http\Message\UriInterface[]
*/
public function detect(): array
{
$document = $this->extractor->getDocument();
$feeds = [];
foreach (self::$types as $type) {
$href = $document->link('alternate', ['type' => $type]);
if ($href) {
$feeds[] = $href;
}
}
return $feeds;
}
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Psr\Http\Message\UriInterface;
class Icon extends Detector
{
public function detect(): ?UriInterface
{
$document = $this->extractor->getDocument();
return $document->link('apple-touch-icon-precomposed')
?: $document->link('apple-touch-icon')
?: $document->link('icon', ['sizes' => '144x144'])
?: $document->link('icon', ['sizes' => '96x96'])
?: $document->link('icon', ['sizes' => '48x48']);
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Psr\Http\Message\UriInterface;
class Image extends Detector
{
public function detect(): ?UriInterface
{
$oembed = $this->extractor->getOEmbed();
$document = $this->extractor->getDocument();
$metas = $this->extractor->getMetas();
$ld = $this->extractor->getLinkedData();
return $oembed->url('image')
?: $oembed->url('thumbnail')
?: $oembed->url('thumbnail_url')
?: $metas->url('og:image', 'og:image:url', 'og:image:secure_url', 'twitter:image', 'twitter:image:src', 'lp:image')
?: $document->link('image_src')
?: $ld->url('image.url')
?: $this->detectFromContentType();
}
private function detectFromContentType()
{
if (!$this->extractor->getResponse()->hasHeader('content-type')) {
return null;
}
$contentType = $this->extractor->getResponse()->getHeader('content-type')[0];
if (strpos($contentType, 'image/') === 0) {
return $this->extractor->getUri();
}
}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class Keywords extends Detector
{
public function detect(): array
{
$tags = [];
$metas = $this->extractor->getMetas();
$ld = $this->extractor->getLinkedData();
$types = [
'keywords',
'og:video:tag',
'og:article:tag',
'og:video:tag',
'og:book:tag',
'lp.article:section',
'dcterms.subject',
];
foreach ($types as $type) {
$value = $metas->strAll($type);
if ($value) {
$tags = array_merge($tags, self::toArray($value));
}
}
$value = $ld->strAll('keywords');
if ($value) {
$tags = array_merge($tags, self::toArray($value));
}
$tags = array_map('mb_strtolower', $tags);
$tags = array_unique($tags);
$tags = array_filter($tags);
$tags = array_values($tags);
return $tags;
}
private static function toArray(array $keywords): array
{
$all = [];
foreach ($keywords as $keyword) {
$tags = explode(',', $keyword);
$tags = array_map('trim', $tags);
$tags = array_filter(
$tags,
fn ($value) => !empty($value) && substr($value, -3) !== '...'
);
$all = array_merge($all, $tags);
}
return $all;
}
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class Language extends Detector
{
public function detect(): ?string
{
$document = $this->extractor->getDocument();
$metas = $this->extractor->getMetas();
$ld = $this->extractor->getLinkedData();
return $document->select('/html')->str('lang')
?: $document->select('/html')->str('xml:lang')
?: $metas->str('language', 'lang', 'og:locale', 'dc:language')
?: $document->select('.//meta', ['http-equiv' => 'content-language'])->str('content')
?: $ld->str('inLanguage');
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class Languages extends Detector
{
/**
* @return \Psr\Http\Message\UriInterface[]
*/
public function detect(): array
{
$document = $this->extractor->getDocument();
$languages = [];
foreach ($document->select('.//link[@hreflang]')->nodes() as $node) {
$language = $node->getAttribute('hreflang');
$href = $node->getAttribute('href');
if (!$language || !$href) {
continue;
}
$languages[$language] = $this->extractor->resolveUri($href);
}
return $languages;
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class License extends Detector
{
public function detect(): ?string
{
$oembed = $this->extractor->getOEmbed();
$metas = $this->extractor->getMetas();
return $oembed->str('license_url')
?: $metas->str('copyright');
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class ProviderName extends Detector
{
private static array $suffixes;
public function detect(): string
{
$oembed = $this->extractor->getOEmbed();
$metas = $this->extractor->getMetas();
return $oembed->str('provider_name')
?: $metas->str(
'og:site_name',
'dcterms.publisher',
'publisher',
'article:publisher'
)
?: ucfirst($this->fallback());
}
private function fallback(): string
{
$host = $this->extractor->getUri()->getHost();
$host = array_reverse(explode('.', $host));
switch (count($host)) {
case 1:
return $host[0];
case 2:
return $host[1];
default:
$tld = $host[1].'.'.$host[0];
$suffixes = self::getSuffixes();
if (in_array($tld, $suffixes, true)) {
return $host[2];
}
return $host[1];
}
}
private static function getSuffixes(): array
{
if (!isset(self::$suffixes)) {
self::$suffixes = require dirname(__DIR__).'/resources/suffix.php';
}
return self::$suffixes;
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Psr\Http\Message\UriInterface;
class ProviderUrl extends Detector
{
public function detect(): UriInterface
{
$oembed = $this->extractor->getOEmbed();
$metas = $this->extractor->getMetas();
return $oembed->url('provider_url')
?: $metas->url('og:website')
?: $this->fallback();
}
private function fallback(): UriInterface
{
return $this->extractor->getUri()->withPath('')->withQuery('')->withFragment('');
}
}

View file

@ -0,0 +1,60 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Datetime;
class PublishedTime extends Detector
{
public function detect(): ?Datetime
{
$oembed = $this->extractor->getOEmbed();
$metas = $this->extractor->getMetas();
$ld = $this->extractor->getLinkedData();
return $oembed->time('pubdate')
?: $metas->time(
'article:published_time',
'created',
'date',
'datepublished',
'music:release_date',
'video:release_date',
'newsrepublic:publish_date'
)
?: $ld->time(
'pagePublished',
'datePublished'
)
?: $this->detectFromPath()
?: $metas->time(
'pagerender',
'pub_date',
'publication-date',
'lp.article:published_time',
'lp.article:modified_time',
'publish-date',
'rc.datecreation',
'timestamp',
'sailthru.date',
'article:modified_time',
'dcterms.date'
);
}
/**
* Some sites using WordPress have the published time in the url
* For example: mysite.com/2020/05/19/post-title
*/
private function detectFromPath(): ?Datetime
{
$path = $this->extractor->getUri()->getPath();
if (preg_match('#/(19|20)\d{2}/[0-1]?\d/[0-3]?\d/#', $path, $matches)) {
return date_create_from_format('/Y/m/d/', $matches[0]) ?: null;
}
return null;
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Psr\Http\Message\UriInterface;
class Redirect extends Detector
{
public function detect(): ?UriInterface
{
$document = $this->extractor->getDocument();
$value = $document->select('.//meta', ['http-equiv' => 'refresh'])->str('content');
return $value ? $this->extract($value) : null;
}
private function extract(string $value): ?UriInterface
{
if (preg_match('/url=(.+)$/i', $value, $match)) {
return $this->extractor->resolveUri($match[1]);
}
return null;
}
}

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
class Title extends Detector
{
public function detect(): ?string
{
$oembed = $this->extractor->getOEmbed();
$document = $this->extractor->getDocument();
$metas = $this->extractor->getMetas();
return $oembed->str('title')
?: $metas->str(
'og:title',
'twitter:title',
'lp:title',
'dcterms.title',
'article:title',
'headline',
'article.headline',
'parsely-title'
)
?: $document->select('.//head/title')->str();
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
namespace Embed\Detectors;
use Psr\Http\Message\UriInterface;
class Url extends Detector
{
public function detect(): UriInterface
{
$oembed = $this->extractor->getOEmbed();
return $oembed->url('url')
?: $oembed->url('web_page')
?: $this->extractor->getUri();
}
}

124
vendor/embed/embed/src/Document.php vendored Normal file
View file

@ -0,0 +1,124 @@
<?php
declare(strict_types = 1);
namespace Embed;
use DOMDocument;
use DOMNode;
use DOMXPath;
use HtmlParser\Parser;
use Psr\Http\Message\UriInterface;
use RuntimeException;
use Symfony\Component\CssSelector\CssSelectorConverter;
class Document
{
private static CssSelectorConverter $cssConverter;
private Extractor $extractor;
private DOMDocument $document;
private DOMXPath $xpath;
public function __construct(Extractor $extractor)
{
$this->extractor = $extractor;
$html = (string) $extractor->getResponse()->getBody();
$html = str_replace('<br>', "\n<br>", $html);
$html = str_replace('<br ', "\n<br ", $html);
$this->document = !empty($html) ? Parser::parse($html) : new DOMDocument();
$this->initXPath();
}
private function initXPath()
{
$this->xpath = new DOMXPath($this->document);
$this->xpath->registerNamespace('php', 'http://php.net/xpath');
$this->xpath->registerPhpFunctions();
}
public function __clone()
{
$this->document = clone $this->document;
$this->initXPath();
}
public function remove(string $query): void
{
$nodes = iterator_to_array($this->xpath->query($query), false);
foreach ($nodes as $node) {
$node->parentNode->removeChild($node);
}
}
public function removeCss(string $query): void
{
$this->remove(self::cssToXpath($query));
}
public function getDocument(): DOMDocument
{
return $this->document;
}
/**
* Helper to build xpath queries easily and case insensitive
*/
private static function buildQuery(string $startQuery, array $attributes): string
{
$selector = [$startQuery];
foreach ($attributes as $name => $value) {
$selector[] = sprintf('[php:functionString("strtolower", @%s)="%s"]', $name, mb_strtolower($value));
}
return implode('', $selector);
}
/**
* Select a element in the dom
*/
public function select(string $query, array $attributes = null, DOMNode $context = null): QueryResult
{
if (!empty($attributes)) {
$query = self::buildQuery($query, $attributes);
}
return new QueryResult($this->xpath->query($query, $context), $this->extractor);
}
/**
* Select a element in the dom using a css selector
*/
public function selectCss(string $query, DOMNode $context = null): QueryResult
{
return $this->select(self::cssToXpath($query), null, $context);
}
/**
* Shortcut to select a <link> element and return the href
*/
public function link(string $rel, array $extra = []): ?UriInterface
{
return $this->select('.//link', ['rel' => $rel] + $extra)->url('href');
}
public function __toString(): string
{
return Parser::stringify($this->getDocument());
}
private static function cssToXpath(string $selector): string
{
if (!isset(self::$cssConverter)) {
if (!class_exists(CssSelectorConverter::class)) {
throw new RuntimeException('You need to install "symfony/css-selector" to use css selectors');
}
self::$cssConverter = new CssSelectorConverter();
}
return self::$cssConverter->toXpath($selector);
}
}

89
vendor/embed/embed/src/Embed.php vendored Normal file
View file

@ -0,0 +1,89 @@
<?php
declare(strict_types = 1);
namespace Embed;
use Embed\Http\Crawler;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class Embed
{
private Crawler $crawler;
private ExtractorFactory $extractorFactory;
public function __construct(Crawler $crawler = null, ExtractorFactory $extractorFactory = null)
{
$this->crawler = $crawler ?: new Crawler();
$this->extractorFactory = $extractorFactory ?: new ExtractorFactory();
}
public function get(string $url): Extractor
{
$request = $this->crawler->createRequest('GET', $url);
$response = $this->crawler->sendRequest($request);
return $this->extract($request, $response);
}
/**
* @return Extractor[]
*/
public function getMulti(string ...$urls): array
{
$requests = array_map(
fn ($url) => $this->crawler->createRequest('GET', $url),
$urls
);
$responses = $this->crawler->sendRequests(...$requests);
$return = [];
foreach ($responses as $k => $response) {
$return[] = $this->extract($requests[$k], $responses[$k]);
}
return $return;
}
public function getCrawler(): Crawler
{
return $this->crawler;
}
public function getExtractorFactory(): ExtractorFactory
{
return $this->extractorFactory;
}
public function setSettings(array $settings): void
{
$this->extractorFactory->setSettings($settings);
}
private function extract(RequestInterface $request, ResponseInterface $response, bool $redirect = true): Extractor
{
$uri = $this->crawler->getResponseUri($response) ?: $request->getUri();
$extractor = $this->extractorFactory->createExtractor($uri, $request, $response, $this->crawler);
if (!$redirect || !$this->mustRedirect($extractor)) {
return $extractor;
}
$request = $this->crawler->createRequest('GET', $extractor->redirect);
$response = $this->crawler->sendRequest($request);
return $this->extract($request, $response, false);
}
private function mustRedirect(Extractor $extractor): bool
{
if (!empty($extractor->getOembed()->all())) {
return false;
}
return $extractor->redirect !== null;
}
}

40
vendor/embed/embed/src/EmbedCode.php vendored Normal file
View file

@ -0,0 +1,40 @@
<?php
declare(strict_types = 1);
namespace Embed;
use JsonSerializable;
class EmbedCode implements JsonSerializable
{
public string $html;
public ?int $width;
public ?int $height;
public ?float $ratio = null;
public function __construct(string $html, int $width = null, int $height = null)
{
$this->html = $html;
$this->width = $width;
$this->height = $height;
if ($width && $height) {
$this->ratio = round(($height / $width) * 100, 3);
}
}
public function __toString(): string
{
return $this->html;
}
public function jsonSerialize()
{
return [
'html' => $this->html,
'width' => $this->width,
'height' => $this->height,
'ratio' => $this->ratio,
];
}
}

222
vendor/embed/embed/src/Extractor.php vendored Normal file
View file

@ -0,0 +1,222 @@
<?php
declare(strict_types = 1);
namespace Embed;
use DateTime;
use DomainException;
use Embed\Detectors\AuthorName;
use Embed\Detectors\AuthorUrl;
use Embed\Detectors\Cms;
use Embed\Detectors\Code;
use Embed\Detectors\Description;
use Embed\Detectors\Detector;
use Embed\Detectors\Favicon;
use Embed\Detectors\Feeds;
use Embed\Detectors\Icon;
use Embed\Detectors\Image;
use Embed\Detectors\Keywords;
use Embed\Detectors\Language;
use Embed\Detectors\Languages;
use Embed\Detectors\License;
use Embed\Detectors\ProviderName;
use Embed\Detectors\ProviderUrl;
use Embed\Detectors\PublishedTime;
use Embed\Detectors\Redirect;
use Embed\Detectors\Title;
use Embed\Detectors\Url;
use Embed\Http\Crawler;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
/**
* Class to extract the info
*
* @property string|null $authorName
* @property UriInterface|null $authorUrl
* @property string|null $cms
* @property EmbedCode|null $code
* @property string|null $description
* @property UriInterface $favicon
* @property array|UriInterface[] $feeds
* @property UriInterface|null $icon
* @property UriInterface|null $image
* @property array|string[] $keywords
* @property string|null $language
* @property array|UriInterface[] $languages
* @property string|null $license
* @property string $providerName
* @property UriInterface $providerUrl
* @property DateTime|null $publishedTime
* @property UriInterface|null $redirect
* @property string|null $title
* @property UriInterface $url
*/
class Extractor
{
private RequestInterface $request;
private ResponseInterface $response;
private UriInterface $uri;
private Crawler $crawler;
protected Document $document;
protected OEmbed $oembed;
protected LinkedData $linkedData;
protected Metas $metas;
private array $settings = [];
private array $customDetectors = [];
protected AuthorName $authorName;
protected AuthorUrl $authorUrl;
protected Cms $cms;
protected Code $code;
protected Description $description;
protected Favicon $favicon;
protected Feeds $feeds;
protected Icon $icon;
protected Image $image;
protected Keywords $keywords;
protected Language $language;
protected Languages $languages;
protected License $license;
protected ProviderName $providerName;
protected ProviderUrl $providerUrl;
protected PublishedTime $publishedTime;
protected Redirect $redirect;
protected Title $title;
protected Url $url;
public function __construct(UriInterface $uri, RequestInterface $request, ResponseInterface $response, Crawler $crawler)
{
$this->uri = $uri;
$this->request = $request;
$this->response = $response;
$this->crawler = $crawler;
//APIs
$this->document = new Document($this);
$this->oembed = new OEmbed($this);
$this->linkedData = new LinkedData($this);
$this->metas = new Metas($this);
//Detectors
$this->authorName = new AuthorName($this);
$this->authorUrl = new AuthorUrl($this);
$this->cms = new Cms($this);
$this->code = new Code($this);
$this->description = new Description($this);
$this->favicon = new Favicon($this);
$this->feeds = new Feeds($this);
$this->icon = new Icon($this);
$this->image = new Image($this);
$this->keywords = new Keywords($this);
$this->language = new Language($this);
$this->languages = new Languages($this);
$this->license = new License($this);
$this->providerName = new ProviderName($this);
$this->providerUrl = new ProviderUrl($this);
$this->publishedTime = new PublishedTime($this);
$this->redirect = new Redirect($this);
$this->title = new Title($this);
$this->url = new Url($this);
}
public function __get(string $name)
{
$detector = $this->customDetectors[$name] ?? $this->$name ?? null;
if (!$detector || !($detector instanceof Detector)) {
throw new DomainException(sprintf('Invalid key "%s". No detector found for this value', $name));
}
return $detector->get();
}
public function createCustomDetectors(): array
{
return [];
}
public function addDetector(string $name, Detector $detector): void
{
$this->customDetectors[$name] = $detector;
}
public function setSettings(array $settings): void
{
$this->settings = $settings;
}
public function getSettings(): array
{
return $this->settings;
}
public function getSetting(string $key)
{
return $this->settings[$key] ?? null;
}
public function getDocument(): Document
{
return $this->document;
}
public function getOEmbed(): OEmbed
{
return $this->oembed;
}
public function getLinkedData(): LinkedData
{
return $this->linkedData;
}
public function getMetas(): Metas
{
return $this->metas;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
public function getResponse(): ResponseInterface
{
return $this->response;
}
public function getUri(): UriInterface
{
return $this->uri;
}
/**
* @param UriInterface|string $uri
*/
public function resolveUri($uri): UriInterface
{
if (is_string($uri)) {
if (!isHttp($uri)) {
throw new InvalidArgumentException(sprintf('Uri string must use http or https scheme (%s)', $uri));
}
$uri = $this->crawler->createUri($uri);
}
if (!($uri instanceof UriInterface)) {
throw new InvalidArgumentException('Uri must be a string or an instance of UriInterface');
}
return resolveUri($this->uri, $uri);
}
public function getCrawler(): Crawler
{
return $this->crawler;
}
}

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types = 1);
namespace Embed;
use Embed\Http\Crawler;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
class ExtractorFactory
{
private string $default = Extractor::class;
private array $adapters = [
'slides.com' => Adapters\Slides\Extractor::class,
'pinterest.com' => Adapters\Pinterest\Extractor::class,
'flickr.com' => Adapters\Flickr\Extractor::class,
'snipplr.com' => Adapters\Snipplr\Extractor::class,
'play.cadenaser.com' => Adapters\CadenaSer\Extractor::class,
'ideone.com' => Adapters\Ideone\Extractor::class,
'gist.github.com' => Adapters\Gist\Extractor::class,
'github.com' => Adapters\Github\Extractor::class,
'wikipedia.org' => Adapters\Wikipedia\Extractor::class,
'archive.org' => Adapters\Archive\Extractor::class,
'sassmeister.com' => Adapters\Sassmeister\Extractor::class,
'facebook.com' => Adapters\Facebook\Extractor::class,
'instagram.com' => Adapters\Instagram\Extractor::class,
'imageshack.com' => Adapters\ImageShack\Extractor::class,
'youtube.com' => Adapters\Youtube\Extractor::class,
'twitch.tv' => Adapters\Twitch\Extractor::class,
'bandcamp.com' => Adapters\Bandcamp\Extractor::class,
];
private array $customDetectors = [];
private array $settings;
public function __construct(?array $settings = [])
{
$this->settings = $settings ?? [];
}
public function createExtractor(UriInterface $uri, RequestInterface $request, ResponseInterface $response, Crawler $crawler): Extractor
{
$host = $uri->getHost();
$class = $this->default;
foreach ($this->adapters as $adapterHost => $adapter) {
if (substr($host, -strlen($adapterHost)) === $adapterHost) {
$class = $adapter;
break;
}
}
/** @var Extractor $extractor */
$extractor = new $class($uri, $request, $response, $crawler);
$extractor->setSettings($this->settings);
foreach ($this->customDetectors as $name => $detector) {
$extractor->addDetector($name, new $detector($extractor));
}
foreach ($extractor->createCustomDetectors() as $name => $detector) {
$extractor->addDetector($name, $detector);
}
return $extractor;
}
public function addAdapter(string $pattern, string $class): void
{
$this->adapters[$pattern] = $class;
}
public function addDetector(string $name, string $class): void
{
$this->customDetectors[$name] = $class;
}
public function removeAdapter(string $pattern): void
{
unset($this->adapters[$pattern]);
}
public function setDefault(string $class): void
{
$this->default = $class;
}
public function setSettings(array $settings): void
{
$this->settings = $settings;
}
}

77
vendor/embed/embed/src/Http/Crawler.php vendored Normal file
View file

@ -0,0 +1,77 @@
<?php
declare(strict_types = 1);
namespace Embed\Http;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
class Crawler implements ClientInterface, RequestFactoryInterface, UriFactoryInterface
{
private RequestFactoryInterface $requestFactory;
private UriFactoryInterface $uriFactory;
private ClientInterface $client;
private array $defaultHeaders = [
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:73.0) Gecko/20100101 Firefox/73.0',
'Cache-Control' => 'max-age=0',
];
public function __construct(ClientInterface $client = null, RequestFactoryInterface $requestFactory = null, UriFactoryInterface $uriFactory = null)
{
$this->client = $client ?: new CurlClient();
$this->requestFactory = $requestFactory ?: FactoryDiscovery::getRequestFactory();
$this->uriFactory = $uriFactory ?: FactoryDiscovery::getUriFactory();
}
public function addDefaultHeaders(array $headers): void
{
$this->defaultHeaders = $headers + $this->defaultHeaders;
}
/**
* @param UriInterface|string $uri The URI associated with the request.
*/
public function createRequest(string $method, $uri): RequestInterface
{
$request = $this->requestFactory->createRequest($method, $uri);
foreach ($this->defaultHeaders as $name => $value) {
$request = $request->withHeader($name, $value);
}
return $request;
}
public function createUri(string $uri = ''): UriInterface
{
return $this->uriFactory->createUri($uri);
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
return $this->client->sendRequest($request);
}
public function sendRequests(RequestInterface ...$requests): array
{
if ($this->client instanceof CurlClient) {
return $this->client->sendRequests(...$requests);
}
return array_map(
fn ($request) => $this->client->sendRequest($request),
$requests
);
}
public function getResponseUri(ResponseInterface $response): ?UriInterface
{
$location = $response->getHeaderLine('Content-Location');
return $location ? $this->uriFactory->createUri($location) : null;
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types = 1);
namespace Embed\Http;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class to fetch html pages
*/
final class CurlClient implements ClientInterface
{
private ResponseFactoryInterface $responseFactory;
private array $settings = [];
public function __construct(ResponseFactoryInterface $responseFactory = null)
{
$this->responseFactory = $responseFactory ?: FactoryDiscovery::getResponseFactory();
}
public function setSettings(array $settings): void
{
$this->settings = $settings + $this->settings;
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
$responses = CurlDispatcher::fetch($this->settings, $this->responseFactory, $request);
return $responses[0];
}
public function sendRequests(RequestInterface ...$request): array
{
return CurlDispatcher::fetch($this->settings, $this->responseFactory, ...$request);
}
}

View file

@ -0,0 +1,206 @@
<?php
declare(strict_types = 1);
namespace Embed\Http;
use Composer\CaBundle\CaBundle;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class to fetch html pages
*/
final class CurlDispatcher
{
private RequestInterface $request;
private $curl;
private array $headers = [];
private $isBinary = false;
private $body;
private ?int $error = null;
private array $settings;
/**
* @return ResponseInterface[]
*/
public static function fetch(array $settings, ResponseFactoryInterface $responseFactory, RequestInterface ...$requests): array
{
if (count($requests) === 1) {
$connection = new static($settings, $requests[0]);
return [$connection->exec($responseFactory)];
}
//Init connections
$multi = curl_multi_init();
$connections = [];
foreach ($requests as $request) {
$connection = new static($settings, $request);
curl_multi_add_handle($multi, $connection->curl);
$connections[] = $connection;
}
//Run
$active = null;
do {
$status = curl_multi_exec($multi, $active);
if ($active) {
curl_multi_select($multi);
}
$info = curl_multi_info_read($multi);
if ($info) {
foreach ($connections as $connection) {
if ($connection->curl === $info['handle']) {
$connection->result = $info['result'];
break;
}
}
}
} while ($active && $status == CURLM_OK);
//Close connections
foreach ($connections as $connection) {
curl_multi_remove_handle($multi, $connection->curl);
}
curl_multi_close($multi);
return array_map(
fn ($connection) => $connection->exec($responseFactory),
$connections
);
}
private function __construct(array $settings, RequestInterface $request)
{
$this->request = $request;
$this->curl = curl_init((string) $request->getUri());
$this->settings = $settings;
$cookies = $settings['cookies_path'] ?? str_replace('//', '/', sys_get_temp_dir().'/embed-cookies.txt');
curl_setopt_array($this->curl, [
CURLOPT_HTTPHEADER => $this->getRequestHeaders(),
CURLOPT_POST => strtoupper($request->getMethod()) === 'POST',
CURLOPT_MAXREDIRS => 10,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 10,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_ENCODING => '',
CURLOPT_CAINFO => CaBundle::getSystemCaRootBundlePath(),
CURLOPT_AUTOREFERER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
CURLOPT_USERAGENT => $request->getHeaderLine('User-Agent'),
CURLOPT_COOKIEJAR => $cookies,
CURLOPT_COOKIEFILE => $cookies,
CURLOPT_HEADERFUNCTION => [$this, 'writeHeader'],
CURLOPT_WRITEFUNCTION => [$this, 'writeBody'],
]);
}
private function exec(ResponseFactoryInterface $responseFactory): ResponseInterface
{
curl_exec($this->curl);
$info = curl_getinfo($this->curl);
if ($this->error) {
$this->error(curl_strerror($this->error), $this->error);
}
if (curl_errno($this->curl)) {
$this->error(curl_error($this->curl), curl_errno($this->curl));
}
curl_close($this->curl);
$response = $responseFactory->createResponse($info['http_code']);
foreach ($this->headers as $header) {
list($name, $value) = $header;
$response = $response->withAddedHeader($name, $value);
}
$response = $response
->withAddedHeader('Content-Location', $info['url'])
->withAddedHeader('X-Request-Time', sprintf('%.3f ms', $info['total_time']));
if ($this->body) {
//5Mb max
$response->getBody()->write(stream_get_contents($this->body, 5000000, 0));
}
return $response;
}
private function error(string $message, int $code)
{
$ignored = $this->settings['ignored_errors'] ?? null;
if ($ignored === true || (is_array($ignored) && in_array($code, $ignored))) {
return;
}
if ($this->isBinary && $code === CURLE_WRITE_ERROR) {
// The write callback aborted the request to prevent a download of the binary file
return;
}
throw new NetworkException($message, $code, $this->request);
}
private function getRequestHeaders(): array
{
$headers = [];
foreach ($this->request->getHeaders() as $name => $values) {
switch (strtolower($name)) {
case 'user-agent':
break;
default:
$headers[$name] = implode(', ', $values);
}
}
return $headers;
}
private function writeHeader($curl, $string): int
{
if (preg_match('/^([\w-]+):(.*)$/', $string, $matches)) {
$name = strtolower($matches[1]);
$value = trim($matches[2]);
$this->headers[] = [$name, $value];
if ($name === 'content-type') {
$this->isBinary = !preg_match('/(text|html|json)/', strtolower($value));
}
} elseif ($this->headers) {
$key = array_key_last($this->headers);
$this->headers[$key][1] .= ' '.trim($string);
}
return strlen($string);
}
private function writeBody($curl, $string): int
{
if ($this->isBinary) {
return -1;
}
if (!$this->body) {
$this->body = fopen('php://temp', 'w+');
}
return fwrite($this->body, $string);
}
}

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types = 1);
namespace Embed\Http;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use RuntimeException;
abstract class FactoryDiscovery
{
private const REQUEST = [
'Laminas\Diactoros\RequestFactory',
'GuzzleHttp\Psr7\HttpFactory',
'Slim\Psr7\Factory\RequestFactory',
'Nyholm\Psr7\Factory\Psr17Factory',
'Sunrise\Http\Message\RequestFactory',
];
private const RESPONSE = [
'Laminas\Diactoros\ResponseFactory',
'GuzzleHttp\Psr7\HttpFactory',
'Slim\Psr7\Factory\ResponseFactory',
'Nyholm\Psr7\Factory\Psr17Factory',
'Sunrise\Http\Message\ResponseFactory',
];
private const URI = [
'Laminas\Diactoros\UriFactory',
'GuzzleHttp\Psr7\HttpFactory',
'Slim\Psr7\Factory\UriFactory',
'Nyholm\Psr7\Factory\Psr17Factory',
'Sunrise\Http\Message\UriFactory',
];
public static function getRequestFactory(): RequestFactoryInterface
{
if ($class = self::searchClass(self::REQUEST)) {
return new $class();
}
throw new RuntimeException('No RequestFactoryInterface detected');
}
public static function getResponseFactory(): ResponseFactoryInterface
{
if ($class = self::searchClass(self::RESPONSE)) {
return new $class();
}
throw new RuntimeException('No ResponseFactoryInterface detected');
}
public static function getUriFactory(): UriFactoryInterface
{
if ($class = self::searchClass(self::URI)) {
return new $class();
}
}
private static function searchClass($classes): ?string
{
foreach ($classes as $class) {
if (class_exists($class)) {
return $class;
}
}
return null;
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);
namespace Embed\Http;
use Exception;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Message\RequestInterface;
final class NetworkException extends Exception implements NetworkExceptionInterface
{
private RequestInterface $request;
public function __construct(string $message, int $code, RequestInterface $request)
{
parent::__construct($message, $code);
$this->request = $request;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types = 1);
namespace Embed\Http;
use Exception;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
final class RequestException extends Exception implements RequestExceptionInterface
{
private RequestInterface $request;
public function __construct(string $message, int $code, RequestInterface $request)
{
$this->request = $request;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
}

32
vendor/embed/embed/src/HttpApiTrait.php vendored Normal file
View file

@ -0,0 +1,32 @@
<?php
declare(strict_types = 1);
namespace Embed;
use Exception;
use Psr\Http\Message\UriInterface;
trait HttpApiTrait
{
use ApiTrait;
private ?UriInterface $endpoint;
public function getEndpoint(): ?UriInterface
{
return $this->endpoint;
}
private function fetchJSON(UriInterface $uri): array
{
$crawler = $this->extractor->getCrawler();
$request = $crawler->createRequest('GET', $uri);
$response = $crawler->sendRequest($request);
try {
return json_decode((string) $response->getBody(), true) ?: [];
} catch (Exception $exception) {
return [];
}
}
}

104
vendor/embed/embed/src/LinkedData.php vendored Normal file
View file

@ -0,0 +1,104 @@
<?php
declare(strict_types = 1);
namespace Embed;
use Exception;
use ML\JsonLD\Document as LdDocument;
use ML\JsonLD\DocumentInterface;
use ML\JsonLD\GraphInterface;
use ML\JsonLD\Node;
use Throwable;
class LinkedData
{
use ApiTrait;
private ?DocumentInterface $document;
private function get(string ...$keys)
{
$graph = $this->getGraph();
if (!$graph) {
return null;
}
foreach ($keys as $key) {
$subkeys = explode('.', $key);
foreach ($graph->getNodes() as $node) {
$value = self::getValue($node, ...$subkeys);
if ($value) {
return $value;
}
}
}
return null;
}
private function getGraph(string $name = null): ?GraphInterface
{
if (!isset($this->document)) {
try {
$this->document = LdDocument::load(json_encode($this->all()));
} catch (Throwable $throwable) {
$this->document = LdDocument::load('{}');
return null;
}
}
return $this->document->getGraph();
}
protected function fetchData(): array
{
$document = $this->extractor->getDocument();
$content = $document->select('.//script', ['type' => 'application/ld+json'])->str();
if (empty($content)) {
return [];
}
try {
return json_decode($content, true) ?: [];
} catch (Exception $exception) {
return [];
}
}
private static function getValue(Node $node, string ...$keys)
{
foreach ($keys as $key) {
$node = $node->getProperty("http://schema.org/{$key}");
if (!$node) {
return null;
}
}
return self::detectValue($node);
}
private static function detectValue($value)
{
if (is_array($value)) {
return array_map(
fn ($val) => self::detectValue($val),
array_values($value)
);
}
if (is_scalar($value)) {
return $value;
}
if ($value instanceof Node) {
return $value->getId();
}
return $value->getValue();
}
}

43
vendor/embed/embed/src/Metas.php vendored Normal file
View file

@ -0,0 +1,43 @@
<?php
declare(strict_types = 1);
namespace Embed;
class Metas
{
use ApiTrait;
protected function fetchData(): array
{
$data = [];
$document = $this->extractor->getDocument();
foreach ($document->select('.//meta')->nodes() as $node) {
$type = $node->getAttribute('name') ?: $node->getAttribute('property') ?: $node->getAttribute('itemprop');
$value = $node->getAttribute('content');
if (!empty($value) && !empty($type)) {
$type = strtolower($type);
$data[$type] ??= [];
$data[$type][] = $value;
}
}
return $data;
}
public function get(string ...$keys)
{
$data = $this->all();
foreach ($keys as $key) {
$values = $data[$key] ?? null;
if ($values) {
return $values;
}
}
return null;
}
}

173
vendor/embed/embed/src/OEmbed.php vendored Normal file
View file

@ -0,0 +1,173 @@
<?php
declare(strict_types = 1);
namespace Embed;
use Exception;
use Psr\Http\Message\UriInterface;
use SimpleXMLElement;
class OEmbed
{
use HttpApiTrait;
private static $providers;
private array $defaults = [];
private static function getProviders(): array
{
if (!is_array(self::$providers)) {
self::$providers = require __DIR__.'/resources/oembed.php';
}
return self::$providers;
}
public function getOembedQueryParameters(string $url): array
{
$queryParameters = ['url' => $url, 'format' => 'json'];
return array_merge($queryParameters, $this->extractor->getSetting('oembed:query_parameters') ?? []);
}
protected function fetchData(): array
{
$this->endpoint = $this->detectEndpoint();
if (empty($this->endpoint)) {
return [];
}
$crawler = $this->extractor->getCrawler();
$request = $crawler->createRequest('GET', $this->endpoint);
$response = $crawler->sendRequest($request);
if (self::isXML($request->getUri())) {
return $this->extractXML((string) $response->getBody());
}
return $this->extractJSON((string) $response->getBody());
}
protected function detectEndpoint(): ?UriInterface
{
$document = $this->extractor->getDocument();
$endpoint = $document->link('alternate', ['type' => 'application/json+oembed'])
?: $document->link('alternate', ['type' => 'text/json+oembed'])
?: $document->link('alternate', ['type' => 'application/xml+oembed'])
?: $document->link('alternate', ['type' => 'text/xml+oembed'])
?: null;
if ($endpoint === null) {
return $this->detectEndpointFromProviders();
}
// Add configured OEmbed query parameters
parse_str($endpoint->getQuery(), $query);
$query = array_merge($query, $this->extractor->getSetting('oembed:query_parameters') ?? []);
$endpoint = $endpoint->withQuery(http_build_query($query));
return $endpoint;
}
private function detectEndpointFromProviders(): ?UriInterface
{
$url = (string) $this->extractor->getUri();
if ($endpoint = $this->detectEndpointFromUrl($url)) {
return $endpoint;
}
$initialUrl = (string) $this->extractor->getRequest()->getUri();
if ($initialUrl !== $url && ($endpoint = $this->detectEndpointFromUrl($initialUrl))) {
$this->defaults['url'] = $initialUrl;
return $endpoint;
}
return null;
}
private function detectEndpointFromUrl(string $url): ?UriInterface
{
$endpoint = self::searchEndpoint(self::getProviders(), $url);
if (!$endpoint) {
return null;
}
return $this->extractor->getCrawler()
->createUri($endpoint)
->withQuery(http_build_query($this->getOembedQueryParameters($url)));
}
private static function searchEndpoint(array $providers, string $url): ?string
{
foreach ($providers as $endpoint => $patterns) {
foreach ($patterns as $pattern) {
if (preg_match($pattern, $url)) {
return $endpoint;
}
}
}
return null;
}
private static function isXML(UriInterface $uri): bool
{
$extension = pathinfo($uri->getPath(), PATHINFO_EXTENSION);
if (strtolower($extension) === 'xml') {
return true;
}
parse_str($uri->getQuery(), $params);
$format = $params['format'] ?? null;
if ($format && strtolower($format) === 'xml') {
return true;
}
return false;
}
private function extractXML(string $xml): array
{
try {
// Remove the DOCTYPE declaration for to prevent XML Quadratic Blowup vulnerability
$xml = preg_replace('/^<!DOCTYPE[^>]*+>/i', '', $xml, 1);
$data = [];
$errors = libxml_use_internal_errors(true);
$content = new SimpleXMLElement($xml);
libxml_use_internal_errors($errors);
foreach ($content as $element) {
$value = trim((string) $element);
if (stripos($value, '<![CDATA[') === 0) {
$value = substr($value, 9, -3);
}
$name = $element->getName();
$data[$name] = $value;
}
return $data ? ($data + $this->defaults) : [];
} catch (Exception $exception) {
return [];
}
}
private function extractJSON(string $json): array
{
try {
$data = json_decode($json, true);
return $data ? ($data + $this->defaults) : [];
} catch (Exception $exception) {
return [];
}
}
}

112
vendor/embed/embed/src/QueryResult.php vendored Normal file
View file

@ -0,0 +1,112 @@
<?php
declare(strict_types = 1);
namespace Embed;
use Closure;
use DOMElement;
use DOMNodeList;
use Psr\Http\Message\UriInterface;
use Throwable;
class QueryResult
{
private Extractor $extractor;
private array $nodes = [];
public function __construct(DOMNodeList $result, Extractor $extractor)
{
$this->nodes = iterator_to_array($result, false);
$this->extractor = $extractor;
}
public function node(): ?DOMElement
{
return $this->nodes[0] ?? null;
}
public function nodes(): array
{
return $this->nodes;
}
public function filter(Closure $callback): self
{
$this->nodes = array_filter($this->nodes, $callback);
return $this;
}
public function get(string $attribute = null)
{
$node = $this->node();
if (!$node) {
return null;
}
return $attribute ? self::getAttribute($node, $attribute) : $node->nodeValue;
}
public function getAll(string $attribute = null): array
{
$nodes = $this->nodes();
return array_filter(
array_map(
fn ($node) => $attribute ? self::getAttribute($node, $attribute) : $node->nodeValue,
$nodes
)
);
}
public function str(string $attribute = null): ?string
{
$value = $this->get($attribute);
return $value ? clean($value) : null;
}
public function strAll(string $attribute = null): array
{
return array_filter(array_map(fn ($value) => clean($value), $this->getAll($attribute)));
}
public function int(string $attribute = null): ?int
{
$value = $this->get($attribute);
return $value ? (int) $value : null;
}
public function url(string $attribute = null): ?UriInterface
{
$value = $this->get($attribute);
if (!$value) {
return null;
}
try {
return $this->extractor->resolveUri($value);
} catch (Throwable $error) {
return null;
}
}
private static function getAttribute(DOMElement $node, string $name): ?string
{
//Don't use $node->getAttribute() because it does not work with namespaces (ex: xml:lang)
$attributes = $node->attributes;
for ($i = 0; $i < $attributes->length; ++$i) {
$attribute = $attributes->item($i);
if ($attribute->name === $name) {
return $attribute->nodeValue;
}
}
return null;
}
}

134
vendor/embed/embed/src/functions.php vendored Normal file
View file

@ -0,0 +1,134 @@
<?php
declare(strict_types = 1);
namespace Embed;
use Psr\Http\Message\UriInterface;
function clean(string $value, bool $allowHTML = false): ?string
{
$value = trim($value);
if (!$allowHTML) {
$value = html_entity_decode($value);
$value = strip_tags($value);
}
$value = trim(preg_replace('/\s+/u', ' ', $value));
return $value === '' ? null : $value;
}
function html(string $tagName, array $attributes, string $content = null): string
{
$html = "<{$tagName}";
foreach ($attributes as $name => $value) {
if ($value === null) {
continue;
} elseif ($value === true) {
$html .= " $name";
} elseif ($value !== false) {
$html .= ' '.$name.'="'.htmlspecialchars((string) $value).'"';
}
}
if ($tagName === 'img') {
return "${html} />";
}
return "{$html}>{$content}</{$tagName}>";
}
/**
* Resolve a uri within this document
* (useful to get absolute uris from relative)
*/
function resolveUri(UriInterface $base, UriInterface $uri): UriInterface
{
$uri = $uri->withPath(resolvePath($base->getPath(), $uri->getPath()));
if (!$uri->getHost()) {
$uri = $uri->withHost($base->getHost());
}
if (!$uri->getScheme()) {
$uri = $uri->withScheme($base->getScheme());
}
return $uri
->withPath(cleanPath($uri->getPath()))
->withFragment('');
}
function isHttp(string $uri): bool
{
if (preg_match('/^(\w+):/', $uri, $matches)) {
return in_array(strtolower($matches[1]), ['http', 'https']);
}
return true;
}
function resolvePath(string $base, string $path): string
{
if ($path === '') {
return '';
}
if ($path[0] === '/') {
return $path;
}
if (substr($base, -1) !== '/') {
$position = strrpos($base, '/');
$base = substr($base, 0, $position);
}
$path = "{$base}/{$path}";
$parts = array_filter(explode('/', $path), 'strlen');
$absolutes = [];
foreach ($parts as $part) {
if ('.' == $part) {
continue;
}
if ('..' == $part) {
array_pop($absolutes);
continue;
}
$absolutes[] = $part;
}
return implode('/', $absolutes);
}
function cleanPath(string $path): string
{
if ($path === '') {
return '/';
}
$path = preg_replace('|[/]{2,}|', '/', $path);
if (strpos($path, ';jsessionid=') !== false) {
$path = preg_replace('/^(.*)(;jsessionid=.*)$/i', '$1', $path);
}
return $path;
}
function matchPath(string $pattern, string $subject): bool
{
$pattern = str_replace('\\*', '.*', preg_quote($pattern, '|'));
return (bool) preg_match("|^{$pattern}$|i", $subject);
}
function getDirectory(string $path, int $position): ?string
{
$dirs = explode('/', $path);
return $dirs[$position + 1] ?? null;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff