Skip to content

Commit

Permalink
Merge pull request #805 from thephpleague/embed-extension
Browse files Browse the repository at this point in the history
Add Embed extension
  • Loading branch information
colinodell authored Feb 12, 2022
2 parents 888d68e + aeaf122 commit fd9d49e
Show file tree
Hide file tree
Showing 33 changed files with 1,283 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi

## [Unreleased][unreleased]

### Added

- Added new `EmbedExtension` (#805)

## [2.2.1] - 2022-01-25

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@
"league/config": "^1.1.1",
"psr/event-dispatcher": "^1.0",
"symfony/deprecation-contracts": "^2.1 || ^3.0",
"symfony/polyfill-php80": "^1.15"
"symfony/polyfill-php80": "^1.16"
},
"require-dev": {
"ext-json": "*",
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.30.0",
"commonmark/commonmark.js": "0.30.0",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
"github/gfm": "0.29.0",
"michelf/php-markdown": "^1.4",
"nyholm/psr7": "^1.5",
"phpstan/phpstan": "^0.12.88 || ^1.0.0",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
Expand Down
152 changes: 152 additions & 0 deletions docs/2.3/extensions/embed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
layout: default
title: Embed Extension
description: The EmbedExtension supports embedding rich content from other websites.
---

# Embed Extension

This extension can embed rich content (like videos, tweets, etc.) from other websites.

The syntax is very simple - simply place any `https://` URL on its own line like this:

```md
Check out this video!

https://www.youtube.com/watch?v=dQw4w9WgXcQ
```

If the link points to embeddable content, it will be replaced with the rich HTML needed to embed it:

```html
<p>Check out this video:</p>
<iframe width="200" height="113" src="https://www.youtube.com/embed/dQw4w9WgXcQ?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
```

## Installation

This extension is bundled with `league/commonmark`. This library can be installed via Composer:

```bash
composer require league/commonmark
```

You'll also need to install a third-party [OEmbed](https://www.oembed.com/) library - see the [**Adapter**](#adapter) section below.

## Usage

Configure your `Environment` as usual and add the `EmbedExtension` provided by this package:

```php
use Embed\Embed;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Embed\EmbedExtension;
use League\CommonMark\MarkdownConverter;

// Define your configuration
$config = [
'embed' => [
'adapter' => new OscaroteroEmbedAdapter(), // See the "Adapter" documentation below
'allowed_domains' => ['youtube.com', 'twitter.com', 'github.com'],
'fallback' => 'link',
],
];

// Configure the Environment with all whatever other extensions you want
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());

// Add this extension
$environment->addExtension(new EmbedExtension());

// Instantiate the converter engine and start converting some Markdown!
$converter = new MarkdownConverter($environment);
echo $converter->convert('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
```

## Configuration

This extension supports the following configuration options under the `embed` configuration:

### `adapter` option

Any instance of `EmbedAdapterInterface` - see the "**[Adapter](#adapter)**" section below.

### `allowed_domains` option

This option defines a list of hosts that you wish to allow embedding content from. For example, setting this to
`['youtube.com']` would only allow videos from YouTube to be embedded.
It's extremely important that you only include websites you trust since they'll be providing HTML that is directly embedded in your website.

Any subdomains of these domains will also be allowed. For example, `['youtube.com']` would allow embedding from `youtube.com` or `www.youtube.com`.

As an additional safety measure, we recommend that you also use a [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
to prevent unexpected content from being embedded.

By default, this option is an empty array (`[]`), which means that all domains are allowed.

### `fallback` option

This options defines the behavior when a URL cannot be embedded, either because it's not in the list of `allowed_domains`,
or because the `adapter` could not find embeddable content for that URL.

There are two possible values for this option:

- `'link'` - the URL will be kept in the document as a link (**default**)
-`'remove'` - the URL will be completely removed from the document

## Adapter

`league/commonmark` doesn't know how to obtain the embeddable HTML for a given URL - this must be done by an external library.

### `embed/embed` Adapter

We do provide an adapter for the popular [`embed/embed`](https://github.com/oscarotero/Embed) library. if you'd like to use that. We like this library
because it supports fetching multiple URLs in parallel, which is ideal for performance, and it supports a wide range
of embeddable content.

To use that library, you'll need to `composer install embed/embed` and then pass `new OscaroteroEmbedAdapter()` as the `adapter`
configuration option, as shown in the [**Usage**](#usage) section above.

Need to customize the maximum width/height of the embedded content? You can do that by instantiating the service provided by
`embed/embed`, [configuring it as needed](https://github.com/oscarotero/Embed#settings), and passing that customized instance into the adapter:

```php
use Embed\Embed;
use League\CommonMark\Extension\Embed\Bridge\OscaroteroEmbedAdapter;

// Configure the Embed library itself
$embedLibrary = new Embed();
$embedLibrary->setSettings([
'oembed:query_parameters' => [
'maxwidth' => 800,
'maxheight' => 600,
],
'twitch:parent' => 'example.com',
'facebook:token' => '1234|5678',
'instagram:token' => '1234|5678',
'twitter:token' => 'asdf',
]);

// Inject it into our adapter
$config = [
'adapter' => new OscaroteroEmbedAdapter($embedLibrary),
];

// Instantiate your CommonMark environment and converter like usual
// ...
```

### Custom Adapter

If you prefer to use a different library, you'll need to implement our `EmbedAdapterInterface` yourself with
[whatever OEmbed library](https://packagist.org/?tags=oembed) you choose.

## Tips

If you need to wrap the HTML in a container tag, consider using the [`HtmlDecorator` renderer](/2.3/customization/rendering/#wrapping-elements-with-htmldecorator):

```php
$environment->addRenderer(Embed::class, new HtmlDecorator(new EmbedRenderer(), 'div', ['class' => 'embeded-content']));
```
2 changes: 2 additions & 0 deletions docs/2.3/extensions/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ to enhance your experience out-of-the-box depending on your specific use-cases.
| [Default Attributes] | Easily apply default HTML classes using configuration options to match your site's styles | `2.0.0` | |
| [Description Lists] | Create `<dl>` description lists using Markdown Extra's syntax | `2.0.0` | |
| [Disallowed Raw HTML] | Disables certain kinds of HTML tags that could affect page rendering | `1.3.0` | <i class="fab fa-github"></i> |
| [Embed] | Embed rich content (like videos, tweets, and more) from other websites | `2.3.0` | |
| [External Links] | Tags external links with additional markup | `1.3.0` | |
| [Footnotes] | Add footnote references throughout the document and show a listing of them at the bottom | `1.5.0` | |
| [Front Matter] | Parses YAML front matter from your Markdown input | `2.0.0` | |
Expand Down Expand Up @@ -102,6 +103,7 @@ See the [Custom Extensions](/2.3/customization/extensions/) page for details on
[Default Attributes]: /2.3/extensions/default-attributes/
[Description Lists]: /2.3/extensions/description-lists/
[Disallowed Raw HTML]: /2.3/extensions/disallowed-raw-html/
[Embed]: /2.3/extensions/embed/
[External Links]: /2.3/extensions/external-links/
[Footnotes]: /2.3/extensions/footnotes/
[Front Matter]: /2.3/extensions/front-matter/
Expand Down
1 change: 1 addition & 0 deletions docs/_data/menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ version:
'Default Attributes': '/2.3/extensions/default-attributes/'
'Description Lists': '/2.3/extensions/description-lists/'
'Disallowed Raw HTML': '/2.3/extensions/disallowed-raw-html/'
'Embed': '/2.3/extensions/embed/'
'External Links': '/2.3/extensions/external-links/'
'Footnotes': '/2.3/extensions/footnotes/'
'Heading Permalinks': '/2.3/extensions/heading-permalinks/'
Expand Down
49 changes: 49 additions & 0 deletions src/Extension/Embed/Bridge/OscaroteroEmbedAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\Embed\Bridge;

use Embed\Embed as EmbedLib;
use League\CommonMark\Extension\Embed\Embed;
use League\CommonMark\Extension\Embed\EmbedAdapterInterface;

final class OscaroteroEmbedAdapter implements EmbedAdapterInterface
{
private EmbedLib $embedLib;

public function __construct(?EmbedLib $embed = null)
{
if ($embed === null) {
if (! \class_exists(EmbedLib::class)) {
throw new \RuntimeException('The embed/embed package is not installed. Please install it with Composer to use this adapter.');
}

$embed = new EmbedLib();
}

$this->embedLib = $embed;
}

/**
* {@inheritDoc}
*/
public function updateEmbeds(array $embeds): void
{
$extractors = $this->embedLib->getMulti(...\array_map(static fn (Embed $embed) => $embed->getUrl(), $embeds));
foreach ($extractors as $i => $extractor) {
if ($extractor->code !== null) {
$embeds[$i]->setEmbedCode($extractor->code->html);
}
}
}
}
50 changes: 50 additions & 0 deletions src/Extension/Embed/DomainFilteringAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\Embed;

class DomainFilteringAdapter implements EmbedAdapterInterface
{
private EmbedAdapterInterface $decorated;

private string $regex;

/**
* @param string[] $allowedDomains
*/
public function __construct(EmbedAdapterInterface $decorated, array $allowedDomains)
{
$this->decorated = $decorated;
$this->regex = self::createRegex($allowedDomains);
}

/**
* {@inheritDoc}
*/
public function updateEmbeds(array $embeds): void
{
$this->decorated->updateEmbeds(\array_filter($embeds, function (Embed $embed): bool {
return \preg_match($this->regex, $embed->getUrl()) === 1;
}));
}

/**
* @param string[] $allowedDomains
*/
private static function createRegex(array $allowedDomains): string
{
$allowedDomains = \array_map('preg_quote', $allowedDomains);

return '/^(?:https?:\/\/)?(?:[^.]+\.)*(' . \implode('|', $allowedDomains) . ')/';
}
}
50 changes: 50 additions & 0 deletions src/Extension/Embed/Embed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\Embed;

use League\CommonMark\Node\Block\AbstractBlock;

final class Embed extends AbstractBlock
{
private string $url;
private ?string $embedCode;

public function __construct(string $url, ?string $embedCode = null)
{
parent::__construct();

$this->url = $url;
$this->embedCode = $embedCode;
}

public function getUrl(): string
{
return $this->url;
}

public function setUrl(string $url): void
{
$this->url = $url;
}

public function getEmbedCode(): ?string
{
return $this->embedCode;
}

public function setEmbedCode(?string $embedCode): void
{
$this->embedCode = $embedCode;
}
}
25 changes: 25 additions & 0 deletions src/Extension/Embed/EmbedAdapterInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\Embed;

/**
* Interface for a service which updates the embed code(s) for the given array of embeds
*/
interface EmbedAdapterInterface
{
/**
* @param Embed[] $embeds
*/
public function updateEmbeds(array $embeds): void;
}
Loading

0 comments on commit fd9d49e

Please sign in to comment.