-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add resource hints transformers #179
Merged
Merged
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
6fa218e
Add initial scaffolding for link management
schlessera 82eeaf4
Ignore HTML special char entity encoding mismatches in assertSimilarM…
schlessera b734f1e
Add tests for Google Fonts preconnect
schlessera b835bad
Add logic to detect usage of Google Fonts
schlessera 4ef17b8
Change LinkManager link placement
schlessera 3bfa743
Improve preconnect generation according to best practices
schlessera e4e611d
Use resource hint terminology
schlessera 2152d8c
Add ResourceHintManager::addPreload() helper
schlessera 6a4c48b
Add scaffolding for adding AMP runtime script and style preloads
schlessera 51611f0
Rename ResourceHints into GooglePreconnects to split up transformers
schlessera 5772a5c
Harden resource hint manager reference node logic
schlessera 5280f04
Add AmpRuntimePreloads transformer
schlessera dda1127
Fix line length CS issue in Dom\Document
schlessera c6191d1
Rename GoogleFontsPreconnectTest
schlessera 40a50c4
Add TODO for AMP runtime is needed condition
schlessera e3fc16c
Add tests for AMP runtime CSS preload
schlessera b0f6d93
Add GoogleFontsPreconnect spec tests
schlessera dfcbd14
Fix test failures
schlessera e1d4687
Rename property $linkManager -> $resourceHints
schlessera 2c1588c
Add tests for resource hint manager
schlessera 08285b0
Include AmpRuntimePreloads in list of default transformers
schlessera 26cd737
Add covers annotations for AmpRuntimePreloadsTest
schlessera 34b9189
Make URL check for Google API domain stricter
schlessera 1cb6796
Rename ResourceHintManager back to LinkManager
schlessera 5a5dd19
Rephrase crossorigin argument
schlessera 6c9aae1
Improve docblocks
schlessera ddf7ee4
Add missing resource hints
schlessera 50956fe
Adapt tests
schlessera 657f2ab
Rename Google Fonts constants
schlessera 0890944
Use renamed constants
schlessera 300a078
Add modulepreload
schlessera f8170a3
Add replacement and removal to link manager
schlessera 4cd2c0d
Make RewriteAmpUrls use link manager
schlessera 3b6d5a9
Ensure that the host is only replaced once
schlessera 52c903d
Fix PHPStan issue
schlessera 5831c16
Fix missing import
schlessera c3a2159
Remove unused variables
schlessera File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,311 @@ | ||
<?php | ||
|
||
namespace AmpProject\Dom; | ||
|
||
use AmpProject\Attribute; | ||
use AmpProject\Exception\FailedToCreateLink; | ||
use AmpProject\RequestDestination; | ||
use AmpProject\Tag; | ||
use DOMNode; | ||
|
||
/** | ||
* Link manager class that is used to manage the <link> tags within a document's <head>. | ||
* | ||
* These can be used for example to give the browser hints about how to prioritize resources. | ||
* | ||
* @package ampproject/amp-toolbox | ||
*/ | ||
final class LinkManager | ||
{ | ||
|
||
/** | ||
* List of relations currently managed by the link manager. | ||
* | ||
* @var array<string> | ||
*/ | ||
const MANAGED_RELATIONS = [ | ||
Attribute::REL_DNS_PREFETCH, | ||
Attribute::REL_MODULEPRELOAD, | ||
Attribute::REL_PRECONNECT, | ||
Attribute::REL_PREFETCH, | ||
Attribute::REL_PRELOAD, | ||
Attribute::REL_PRERENDER, | ||
]; | ||
|
||
/** | ||
* Document to manage the links for. | ||
* | ||
* @var Document | ||
*/ | ||
private $document; | ||
|
||
/** | ||
* Reference node to attach the resource hint to. | ||
* | ||
* @var DOMNode|null | ||
*/ | ||
private $referenceNode; | ||
|
||
/** | ||
* Collection of links already attached to the document. | ||
* | ||
* The key of the array is a concatenation of the HREF and the REL attributes. | ||
* | ||
* @var Element[] | ||
*/ | ||
private $links = []; | ||
|
||
/** | ||
* LinkManager constructor. | ||
* | ||
* @param Document $document | ||
*/ | ||
public function __construct(Document $document) | ||
{ | ||
$this->document = $document; | ||
$this->detectExistingLinks(); | ||
} | ||
|
||
private function detectExistingLinks() | ||
{ | ||
$node = $this->document->head->firstChild; | ||
while ($node) { | ||
$nextSibling = $node->nextSibling; | ||
if ( | ||
! $node instanceof Element | ||
|| | ||
$node->tagName !== Tag::LINK | ||
) { | ||
$node = $nextSibling; | ||
continue; | ||
} | ||
|
||
$key = $this->getKey($node); | ||
|
||
if ($key !== '') { | ||
$this->links[$this->getKey($node)] = $node; | ||
} | ||
|
||
$node = $nextSibling; | ||
} | ||
} | ||
|
||
/** | ||
* Get the key to use for storing the element in the links cache. | ||
* | ||
* @param Element $element Element to get the key for. | ||
* @return string Key to use. Returns an empty string for invalid elements. | ||
*/ | ||
private function getKey(Element $element) | ||
{ | ||
$href = $element->getAttribute(Attribute::HREF); | ||
$rel = $element->getAttribute(Attribute::REL); | ||
|
||
if (empty($href) || ! in_array($rel, self::MANAGED_RELATIONS, true)) { | ||
return ''; | ||
} | ||
|
||
return "{$href}{$rel}"; | ||
} | ||
|
||
/** | ||
* Add a dns-prefetch resource hint. | ||
* | ||
* @see https://www.w3.org/TR/resource-hints/#dns-prefetch | ||
* | ||
* @param string $href Origin to prefetch the DNS for. | ||
*/ | ||
public function addDnsPrefetch($href) | ||
{ | ||
$this->add(Attribute::REL_DNS_PREFETCH, $href); | ||
} | ||
|
||
/** | ||
* Add a modulepreload declarative fetch primitive. | ||
* | ||
* @see https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload | ||
* | ||
* @param string $href Modular resource to preload. | ||
* @param string|null $type Optional. Type of the resource. Defaults to not specified, which equals 'script'. | ||
* @param bool|string $crossorigin Optional. Whether and how to configure CORS. Accepts a boolean for adding a | ||
* boolean crossorigin flag, or a string to set a specific crossorigin strategy. | ||
* Allowed values are 'anonymous' and 'use-credentials'. Defaults to true. | ||
*/ | ||
public function addModulePreload($href, $type = null, $crossorigin = true) | ||
{ | ||
$attributes = []; | ||
|
||
if ($type !== null) { | ||
$attributes = [Attribute::AS_ => $type]; | ||
} | ||
|
||
if ($crossorigin !== false) { | ||
$attributes[Attribute::CROSSORIGIN] = is_string($crossorigin) ? $crossorigin : null; | ||
} | ||
|
||
$this->add(Attribute::REL_MODULEPRELOAD, $href, $attributes); | ||
} | ||
|
||
/** | ||
* Add a preconnect resource hint. | ||
* | ||
* @see https://www.w3.org/TR/resource-hints/#dfn-preconnect | ||
* | ||
* @param string $href Origin to preconnect to. | ||
* @param bool|string $crossorigin Optional. Whether and how to configure CORS. Accepts a boolean for adding a | ||
* boolean crossorigin flag, or a string to set a specific crossorigin strategy. | ||
* Allowed values are 'anonymous' and 'use-credentials'. Defaults to true. | ||
*/ | ||
public function addPreconnect($href, $crossorigin = true) | ||
{ | ||
$this->add( | ||
Attribute::REL_PRECONNECT, | ||
$href, | ||
$crossorigin !== false ? [Attribute::CROSSORIGIN => (is_string($crossorigin) ? $crossorigin : null)] : [] | ||
); | ||
|
||
// Use dns-prefetch as fallback for browser that don't support preconnect. | ||
// See https://web.dev/preconnect-and-dns-prefetch/#resolve-domain-name-early-with-reldns-prefetch | ||
$this->addDnsPrefetch($href); | ||
} | ||
|
||
/** | ||
* Add a prefetch resource hint. | ||
* | ||
* @see https://www.w3.org/TR/resource-hints/#prefetch | ||
* | ||
* @param string $href URL to the resource to prefetch. | ||
* @param string $type Optional. Type of the resource. Defaults to type 'image'. | ||
* @param bool|string $crossorigin Optional. Whether and how to configure CORS. Accepts a boolean for adding a | ||
* boolean crossorigin flag, or a string to set a specific crossorigin strategy. | ||
* Allowed values are 'anonymous' and 'use-credentials'. Defaults to true. | ||
*/ | ||
public function addPrefetch($href, $type = RequestDestination::IMAGE, $crossorigin = true) | ||
{ | ||
// TODO: Should we enforce a valid $type here? | ||
|
||
$attributes = [Attribute::AS_ => $type]; | ||
|
||
if ($crossorigin !== false) { | ||
$attributes[Attribute::CROSSORIGIN] = is_string($crossorigin) ? $crossorigin : null; | ||
} | ||
|
||
$this->add(Attribute::REL_PREFETCH, $href, $attributes); | ||
} | ||
|
||
/** | ||
* Add a preload declarative fetch primitive. | ||
* | ||
* @see https://www.w3.org/TR/preload/ | ||
* | ||
* @param string $href Resource to preload. | ||
* @param string $type Optional. Type of the resource. Defaults to type 'image'. | ||
* @param string|null $media Optional. Media query to add to the preload. Defaults to none. | ||
* @param bool|string $crossorigin Optional. Whether and how to configure CORS. Accepts a boolean for adding a | ||
* boolean crossorigin flag, or a string to set a specific crossorigin strategy. | ||
* Allowed values are 'anonymous' and 'use-credentials'. Defaults to true. | ||
*/ | ||
public function addPreload($href, $type = RequestDestination::IMAGE, $media = null, $crossorigin = true) | ||
{ | ||
// TODO: Should we enforce a valid $type here? | ||
|
||
$attributes = [Attribute::AS_ => $type]; | ||
|
||
if (!empty($media)) { | ||
$attributes[Attribute::MEDIA] = $media; | ||
} | ||
|
||
if ($crossorigin !== false) { | ||
$attributes[Attribute::CROSSORIGIN] = is_string($crossorigin) ? $crossorigin : null; | ||
} | ||
|
||
$this->add(Attribute::REL_PRELOAD, $href, $attributes); | ||
} | ||
|
||
/** | ||
* Add a prerender resource hint. | ||
* | ||
* @see https://www.w3.org/TR/resource-hints/#prerender | ||
* | ||
* @param string $href URL of the page to prerender. | ||
*/ | ||
public function addPrerender($href) | ||
{ | ||
$this->add(Attribute::REL_PRERENDER, $href); | ||
} | ||
|
||
/** | ||
* Add a link to the document. | ||
* | ||
* @param string $rel A 'rel' string. | ||
* @param string $href URL to link to. | ||
* @param string[] $attributes Associative array of attributes and their values. | ||
*/ | ||
public function add($rel, $href, $attributes = []) | ||
{ | ||
$link = $this->document->createElement(Tag::LINK); | ||
$link->setAttribute(Attribute::REL, $rel); | ||
$link->setAttribute(Attribute::HREF, $href); | ||
foreach ($attributes as $attribute => $value) { | ||
$link->setAttribute($attribute, $value); | ||
} | ||
|
||
$this->remove($rel, $href); | ||
|
||
if (!isset($this->referenceNode)) { | ||
$this->referenceNode = $this->document->viewport; | ||
} | ||
|
||
if ($this->referenceNode) { | ||
$link = $this->document->head->insertBefore($link, $this->referenceNode->nextSibling); | ||
} else { | ||
$link = $this->document->head->appendChild($link); | ||
} | ||
|
||
if (! $link instanceof Element) { | ||
throw FailedToCreateLink::forLink($link); | ||
} | ||
|
||
$this->links[$this->getKey($link)] = $link; | ||
|
||
$this->referenceNode = $link; | ||
} | ||
|
||
/** | ||
* Get a specific link from the link manager. | ||
* | ||
* @param string $rel Relation to fetch. | ||
* @param string $href Reference to fetch. | ||
* @return Element|null Requested link as a Dom\Element, or null if not found. | ||
*/ | ||
public function get($rel, $href) | ||
{ | ||
$key = "{$href}{$rel}"; | ||
|
||
if (! array_key_exists($key, $this->links)) { | ||
return null; | ||
} | ||
|
||
return $this->links[$key]; | ||
} | ||
|
||
/** | ||
* Remove a specific link from the document. | ||
* | ||
* @param string $rel Relation of the link to remove. | ||
* @param string $href Reference of the link to remove. | ||
*/ | ||
public function remove($rel, $href) | ||
{ | ||
$key = "{$href}{$rel}"; | ||
|
||
if (! array_key_exists($key, $this->links)) { | ||
return; | ||
} | ||
|
||
$link = $this->links[$key]; | ||
$link->parentNode->removeChild($link); | ||
|
||
unset($this->links[$key]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?php | ||
|
||
namespace AmpProject\Exception; | ||
|
||
use RuntimeException; | ||
|
||
/** | ||
* Exception thrown when a link could not be created. | ||
* | ||
* @package ampproject/amp-toolbox | ||
*/ | ||
final class FailedToCreateLink extends RuntimeException implements AmpException | ||
{ | ||
|
||
/** | ||
* Instantiate a FailedToCreateLink exception for a link that could not be created. | ||
* | ||
* @param mixed $link Link that was not as expected. | ||
* @return self | ||
*/ | ||
public static function forLink($link) | ||
{ | ||
$type = is_object($link) ? get_class($link) : gettype($link); | ||
$message = "Failed to create a link via the link manager. " | ||
. "Expected to produce an 'AmpProject\\Dom\\Element', got '$type' instead."; | ||
|
||
return new self($message); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would this condition ever happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you do something fishy with the document, you might end up with a
DOMElement
that is not anElement
.Also, PhpStan is happier with this restriction.