Last active
June 2, 2020 06:13
-
-
Save westonruter/f9ee9ea717d52471bae092879e3d52b0 to your computer and use it in GitHub Desktop.
[OBSOLETE] See https://github.com/ampproject/amp-wp/issues/1389 See also AMP Mobile Redirection plugin: https://gist.github.com/westonruter/787c875e5ab724808c26014153e46ec6
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
<?php | |
/** | |
* AMP-To-AMP plugin initialization file. | |
* | |
* @package AMP_To_AMP | |
* @author Weston Ruter, Google | |
* @link https://gist.github.com/westonruter/f9ee9ea717d52471bae092879e3d52b0 | |
* @license GPL-2.0-or-later | |
* @copyright 2019 Google Inc. | |
* | |
* @wordpress-plugin | |
* Plugin Name: AMP-to-AMP Linking | |
* Plugin URI: https://gist.github.com/westonruter/f9ee9ea717d52471bae092879e3d52b0 | |
* Description: Make all inter-site URLs on AMP pages link to other AMP pages. In paired/classic modes, the ?amp query var is added to all links (in classic, only in the content). Additionally, initial support for <a href="https://github.com/ampproject/amphtml/issues/12496">AMP-to-AMP (A2A) linking</a> is implemented: in all modes, the <code>amphtml</code> relation is added to all frontend links (though just for the content in classic mode), and native/paired mode includes the A2A meta tag is added. Nevertheless, do note that A2A is not known to be implemented yet in any AMP viewer. | |
* Version: 0.2.5 | |
* Author: Weston Ruter, Google | |
* Author URI: https://weston.ruter.net/ | |
* License: GNU General Public License v2 (or later) | |
* License URI: http://www.gnu.org/licenses/gpl-2.0.html | |
*/ | |
namespace AMP_To_AMP; | |
/** | |
* Filter content sanitizers to include the A2A sanitizer. | |
* | |
* @param array $sanitizers Sanitizers. | |
* @return array Sanitizers. | |
*/ | |
function filter_content_sanitizers( $sanitizers ) { | |
if ( ! class_exists( 'AMP_Link_Sanitizer' ) ) { // Support AMP v1.4 | |
require_once __DIR__ . '/class-sanitizer.php'; | |
$sanitizers[ __NAMESPACE__ . '\Sanitizer' ] = array( | |
'add_query_vars' => ! amp_is_canonical(), | |
'has_theme_support' => current_theme_supports( 'amp' ), | |
); | |
} | |
return $sanitizers; | |
} | |
add_filter( 'amp_content_sanitizers', __NAMESPACE__ . '\filter_content_sanitizers' ); | |
/** | |
* Paired Browsing. | |
*/ | |
require_once __DIR__ . '/paired-browsing.php'; |
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
<?php | |
/** | |
* Class Sanitizer | |
* | |
* @package AMP_To_AMP | |
* @link https://gist.github.com/westonruter/f9ee9ea717d52471bae092879e3d52b0 | |
* @license GPL-2.0-or-later | |
* @copyright 2019 Google Inc. | |
*/ | |
namespace AMP_To_AMP; | |
/** | |
* Class Sanitizer | |
*/ | |
class Sanitizer extends \AMP_Base_Sanitizer { | |
/** | |
* Default A2A meta tag content. | |
* | |
* @var string | |
*/ | |
const DEFAULT_A2A_META_CONTENT = 'AMP-Redirect-To; AMP.navigateTo'; | |
/** | |
* Placeholder for default args, to be set in child classes. | |
* | |
* @var array | |
*/ | |
protected $DEFAULT_ARGS = array( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase | |
'add_amphtml_rel' => true, | |
'add_query_vars' => true, // Set to false when in native mode. Overridden in \AMP_To_AMP\filter_content_sanitizers(). | |
'has_theme_support' => false, // Set to true when theme has 'amp' support. Overridden in \AMP_To_AMP\filter_content_sanitizers(). | |
'add_a2a_meta' => self::DEFAULT_A2A_META_CONTENT, // Only relevant when theme support is present. | |
); | |
/** | |
* Home host. | |
* | |
* @var string | |
*/ | |
protected $home_host; | |
/** | |
* Content path. | |
* | |
* @var string | |
*/ | |
protected $content_path; | |
/** | |
* Admin path. | |
* | |
* @var string | |
*/ | |
protected $admin_path; | |
/** | |
* Sanitizer constructor. | |
* | |
* @param \DOMDocument $dom Document. | |
* @param array $args Args. | |
*/ | |
public function __construct( \DOMDocument $dom, array $args = array() ) { | |
parent::__construct( $dom, $args ); | |
$this->home_host = wp_parse_url( home_url(), PHP_URL_HOST ); | |
$this->content_path = wp_parse_url( content_url( '/' ), PHP_URL_PATH ); | |
$this->admin_path = wp_parse_url( admin_url(), PHP_URL_PATH ); | |
} | |
/** | |
* Sanitize. | |
*/ | |
public function sanitize() { | |
if ( $this->args['has_theme_support'] && $this->args['add_a2a_meta'] ) { | |
$this->add_a2a_meta( $this->args['add_a2a_meta'] ); | |
} | |
$this->process_links(); | |
} | |
/** | |
* Add the amp-to-amp-navigation meta tag. | |
* | |
* @param string $content The content for the meta tag, for example 'AMP-Redirect-To; AMP.navigateTo'. | |
* @return \DOMElement|null The added meta element if successful. | |
*/ | |
public function add_a2a_meta( $content = self::DEFAULT_A2A_META_CONTENT ) { | |
$head = $this->dom->documentElement->getElementsByTagName( 'head' )->item( 0 ); | |
if ( ! $head || ! $content ) { | |
return null; | |
} | |
$meta = $this->dom->createElement( 'meta' ); | |
$meta->setAttribute( 'name', 'amp-to-amp-navigation' ); | |
$meta->setAttribute( 'content', $content ); | |
$head->appendChild( $meta ); | |
return $meta; | |
} | |
/** | |
* Process links by adding rel=amphtml and AMP query var. | |
*/ | |
public function process_links() { | |
/** | |
* Element. | |
* | |
* @var \DOMElement $element | |
*/ | |
$xpath = new \DOMXPath( $this->dom ); | |
// Remove admin bar from DOM to prevent mutating it. | |
$admin_bar_container = $this->dom->getElementById( 'wpadminbar' ); | |
$admin_bar_placeholder = null; | |
if ( $admin_bar_container ) { | |
$admin_bar_placeholder = $this->dom->createComment( 'wpadminbar' ); | |
$admin_bar_container->parentNode->replaceChild( $admin_bar_placeholder, $admin_bar_container ); | |
} | |
foreach ( $xpath->query( '//*[ local-name() = "a" or local-name() = "area"][ @href ][ substring( @href, 1, 1 ) != "#" ]' ) as $element ) { | |
$href = $element->getAttribute( 'href' ); | |
if ( $this->is_frontend_url( $href ) ) { | |
if ( $this->args['add_amphtml_rel'] ) { | |
$rel = $element->hasAttribute( 'rel' ) ? $element->getAttribute( 'rel' ) . ' ' : ''; | |
$rel .= 'amphtml'; | |
$element->setAttribute( 'rel', $rel ); | |
} | |
if ( $this->args['add_query_vars'] ) { | |
$href = add_query_arg( amp_get_slug(), '', $href ); | |
$element->setAttribute( 'href', $href ); | |
$element->setAttribute( 'data-href', $href ); | |
} | |
} | |
} | |
foreach ( $xpath->query( '//form[ @action ][ translate( @method, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "get" ]' ) as $element ) { | |
if ( $this->is_frontend_url( $element->getAttribute( 'action' ) ) ) { | |
$input = $this->dom->createElement( 'input' ); | |
$input->setAttribute( 'name', amp_get_slug() ); | |
$input->setAttribute( 'value', '' ); | |
$input->setAttribute( 'type', 'hidden' ); | |
$element->appendChild( $input ); | |
} | |
} | |
// Replace the admin bar after mutations are done. | |
if ( $admin_bar_container && $admin_bar_placeholder ) { | |
$admin_bar_placeholder->parentNode->replaceChild( $admin_bar_container, $admin_bar_placeholder ); | |
} | |
} | |
/** | |
* Determine whether a URL is for the frontend. | |
* | |
* @param string $url URL. | |
* @return bool Whether it is a frontend URL. | |
*/ | |
public function is_frontend_url( $url ) { | |
$parsed_url = wp_parse_url( $url ); | |
// Skip adding query var to links on other URLs. | |
if ( ! empty( $parsed_url['host'] ) && $this->home_host !== $parsed_url['host'] ) { | |
return false; | |
} | |
// Skip adding query var to PHP files (e.g. wp-login.php). | |
if ( ! empty( $parsed_url['path'] ) && preg_match( '/\.php$/', $parsed_url['path'] ) ) { | |
return false; | |
} | |
// Skip adding query var to feed URLs. | |
if ( ! empty( $parsed_url['path'] ) && preg_match( ':/feed/(\w+/)?$:', $parsed_url['path'] ) ) { | |
return false; | |
} | |
// Skip adding query var to the admin. | |
if ( ! empty( $parsed_url['path'] ) && false !== strpos( $parsed_url['path'], $this->admin_path ) ) { | |
return false; | |
} | |
// Skip adding query var to content links (e.g. images). | |
if ( ! empty( $parsed_url['path'] ) && false !== strpos( $parsed_url['path'], $this->content_path ) ) { | |
return false; | |
} | |
return true; | |
} | |
} |
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
/* global ampSlug, ampPairedBrowsingQueryVar */ | |
let ampLastVisitedUrl; | |
let ampWindow, nonAmpWindow; | |
const nonAmpIframe = document.getElementById( 'non-amp' ); | |
const ampIframe = document.getElementById( 'amp' ); | |
const iframeLoadedPromises = [ | |
new Promise( ( resolve, reject ) => { | |
nonAmpIframe.addEventListener( 'load', resolve ); | |
setTimeout( reject, 10000 ); | |
} ), | |
new Promise( ( resolve, reject ) => { | |
ampIframe.addEventListener( 'load', resolve ); | |
setTimeout( reject, 10000 ); | |
} ) | |
]; | |
function windowIsAmpEndpoint( win ) { | |
return win.ampPairedBrowsingClientData.is_amp_endpoint; | |
} | |
const checkConnectedIframes = () => { | |
ampIframe.classList.toggle( | |
'disconnected', | |
! ( nonAmpIframe.contentWindow && nonAmpIframe.contentWindow.ampPairedBrowsingClientData ) | |
); | |
nonAmpIframe.classList.toggle( | |
'disconnected', | |
! ( ampIframe.contentWindow && ampIframe.contentWindow.ampPairedBrowsingClientData ) | |
); | |
}; | |
Promise.all( iframeLoadedPromises ).then( () => { | |
setInterval( checkConnectedIframes, 1000 ); | |
} ); | |
function removeAmpQueryVar( url ) { | |
const modifiedUrl = new URL( url ); | |
modifiedUrl.searchParams.delete( ampSlug ); | |
modifiedUrl.searchParams.delete( 'amp_validate' ); | |
return modifiedUrl.href; | |
} | |
function addAmpQueryVar( url ) { | |
const modifiedUrl = new URL( url ); | |
modifiedUrl.searchParams.set( ampSlug, '' ); | |
return modifiedUrl.href; | |
} | |
function addPairedBrowsingQueryVar( url ) { | |
const modifiedUrl = new URL( url ); | |
modifiedUrl.searchParams.set( ampPairedBrowsingQueryVar, '1' ); | |
return modifiedUrl.href; | |
} | |
function hasAmpQueryVar( url ) { | |
const parsedUrl = new URL( url ); | |
return parsedUrl.searchParams.has( ampSlug ); | |
} | |
function removeUrlHash( url ) { | |
const parsedUrl = new URL( url ); | |
parsedUrl.hash = ''; | |
return parsedUrl.href; | |
} | |
function registerClientWindow( win ) { | |
const url = new URL( win.location ); | |
// @todo De-duplicate code. | |
if ( win === ampIframe.contentWindow ) { | |
// @todo Handle case where AMP is disabled. Ensure disabled iframe. | |
// @todo Handle case where there are rejected validation errors in Transitional mode, in which an infinite redirect/reload loop occurs. | |
// Force the AMP iframe to always have an AMP URL, if an AMP version is available. | |
if ( ! windowIsAmpEndpoint( win ) && win.document.querySelector( 'head > link[rel=amphtml]' ) ) { | |
win.location.replace( addAmpQueryVar( win.location ) ); | |
return; | |
} | |
ampWindow = win; | |
ampWindow.addEventListener( | |
'scroll', | |
() => { | |
if ( nonAmpWindow && nonAmpWindow.scrollTo ) { | |
nonAmpWindow.scrollTo( ampWindow.scrollX, ampWindow.scrollY ); | |
} | |
}, | |
{ passive: true } | |
); | |
checkConnectedIframes(); | |
// Make sure the non-AMP iframe is set to match. | |
if ( nonAmpWindow && nonAmpWindow.location && removeAmpQueryVar( removeUrlHash( nonAmpWindow.location ) ) !== removeAmpQueryVar( removeUrlHash( ampWindow.location ) ) ) { | |
nonAmpWindow.location.replace( removeAmpQueryVar( ampWindow.location ) ); | |
nonAmpWindow = null; | |
} | |
document.title = '🔄 ' + ampWindow.document.title; | |
history.replaceState( {}, "", addPairedBrowsingQueryVar( removeAmpQueryVar( ampWindow.location ) ) ); | |
// Set the initial scroll position to match the other window's scroll position. | |
if ( nonAmpWindow ) { | |
ampWindow.scrollTo( nonAmpWindow.scrollX, nonAmpWindow.scrollY ); | |
} | |
ampLastVisitedUrl = ampWindow.location.href; | |
} else if ( win === nonAmpIframe.contentWindow ) { | |
// Force the non-AMP iframe to always have a non-AMP URL. | |
if ( windowIsAmpEndpoint( win ) ) { | |
win.location.replace( removeAmpQueryVar( win.location ) ); | |
return; | |
} | |
nonAmpWindow = win; | |
nonAmpWindow.addEventListener( | |
'scroll', | |
() => { | |
if ( ampWindow && ampWindow.scrollTo ) { | |
ampWindow.scrollTo( nonAmpWindow.scrollX, nonAmpWindow.scrollY ); | |
} | |
}, | |
{ passive: true } | |
); | |
checkConnectedIframes(); | |
// Make sure the AMP iframe is set to match. | |
if ( ampWindow && ampWindow.location && removeAmpQueryVar( removeUrlHash( ampWindow.location ) ) !== removeAmpQueryVar( removeUrlHash( nonAmpWindow.location ) ) ) { | |
ampWindow.location.replace( addAmpQueryVar( nonAmpWindow.location ) ); | |
ampWindow = null; | |
} | |
document.title = '🔄 ' + nonAmpWindow.document.title; | |
history.replaceState( {}, "", addPairedBrowsingQueryVar( removeAmpQueryVar( nonAmpWindow.location ) ) ); | |
// Set the initial scroll position to match the other window's scroll position. | |
if ( ampWindow ) { | |
nonAmpWindow.scrollTo( ampWindow.scrollX, ampWindow.scrollY ); | |
} | |
} | |
} |
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
/* global ampPairedBrowsingClientData */ | |
if ( parent.registerClientWindow ) { | |
parent.registerClientWindow( window ); | |
document.addEventListener( 'DOMContentLoaded', () => { | |
if ( ampPairedBrowsingClientData.is_amp_endpoint ) { | |
// Hide the paired browsing menu item if in the paired browsing interface. | |
const pairedBrowsingMenuItem = document.getElementById( 'wp-admin-bar-amp-paired-browsing' ); | |
if ( pairedBrowsingMenuItem ) { | |
pairedBrowsingMenuItem.remove(); | |
} | |
const ampViewBrowsingItem = document.getElementById( 'wp-admin-bar-amp-view' ); | |
if ( ampViewBrowsingItem ) { | |
ampViewBrowsingItem.remove(); | |
} | |
} else { | |
// Override the entire AMP menu item with just "Non-AMP". There should be no link to the AMP version since it is already being shown. | |
const ampMenuItem = document.getElementById( 'wp-admin-bar-amp' ); | |
if ( ampMenuItem ) { | |
ampMenuItem.innerHTML = 'Non-AMP'; | |
} | |
} | |
} ); | |
} | |
// If AMP, make sure that the ?amp query var is injected. | |
// Send a message to the parent to say that we have loaded? Tell the parent whether it has the |
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
<?php | |
/** | |
* Paired Browsing. | |
* | |
* @package AMP_To_AMP | |
* @author Weston Ruter, Google | |
* @link https://gist.github.com/westonruter/f9ee9ea717d52471bae092879e3d52b0 | |
* @license GPL-2.0-or-later | |
* @copyright 2019 Google Inc. | |
*/ | |
namespace AMP_To_AMP; | |
use AMP_Theme_Support, WP_Admin_Bar; | |
const PAIRED_BROWSING_QUERY_VAR = 'amp-paired-browsing'; | |
/** | |
* Initialize. | |
*/ | |
function init() { | |
// Abort if the required version of AMP is not installed. | |
if ( ! defined( 'AMP_Theme_Support::TRANSITIONAL_MODE_SLUG' ) || ! method_exists( 'AMP_Theme_Support', 'get_support_mode' ) ) { | |
return; | |
} | |
// Abort if not in transitional mode or there is no paired available. | |
if ( AMP_Theme_Support::TRANSITIONAL_MODE_SLUG !== AMP_Theme_Support::get_support_mode() || ! AMP_Theme_Support::is_paired_available() ) { | |
return; | |
} | |
add_action( 'admin_bar_menu', __NAMESPACE__ . '\add_menu_bar_item', 101 ); | |
add_action( 'admin_bar_init', __NAMESPACE__ . '\enqueue_script' ); | |
add_action( 'parse_request', __NAMESPACE__ . '\serve_paired_browsing_experience' ); | |
} | |
add_action( 'init', __NAMESPACE__ . '\init' ); | |
/** | |
* Add menu bar item. | |
* | |
* @param WP_Admin_Bar $wp_admin_bar Admin bar. | |
* @see amp_add_admin_bar_view_link() | |
*/ | |
function add_menu_bar_item( WP_Admin_Bar $wp_admin_bar ) { | |
$wp_admin_bar->add_node( | |
[ | |
'id' => 'amp-paired-browsing', | |
'parent' => 'amp', | |
'title' => __( 'Start paired browsing', 'amp-to-amp' ), | |
'href' => remove_query_arg( amp_get_slug(), add_query_arg( PAIRED_BROWSING_QUERY_VAR, '1' ) ), | |
'meta' => [ | |
'target' => 'amp-paired-browsing', | |
], | |
] | |
); | |
} | |
/** | |
* Enqueue script. | |
*/ | |
function enqueue_script() { | |
if ( is_admin() ) { | |
return; | |
} | |
wp_enqueue_script( | |
'amp-paired-browsing', | |
plugin_dir_url( __FILE__ ) . '/paired-browsing-client.js', | |
[ 'admin-bar' ], | |
'0.1', | |
true | |
); | |
$data = [ | |
'is_amp_endpoint' => \is_amp_endpoint(), | |
]; | |
wp_add_inline_script( | |
'amp-paired-browsing', | |
sprintf( 'var ampPairedBrowsingClientData = %s;', wp_json_encode( $data ) ), | |
'before' | |
); | |
// @todo Make it so that AMP is shown in admin bar on mobile. | |
// wp_add_inline_style( 'admin-bar', '@media screen and (max-width: 782px) { #wpadminbar #wp-admin-bar-amp { display:block; position:static; } }' ); | |
} | |
/** | |
* Serve paired browsing experience. | |
*/ | |
function serve_paired_browsing_experience() { | |
if ( ! isset( $_GET[ PAIRED_BROWSING_QUERY_VAR ] ) ) { | |
return; | |
} | |
if ( ! is_user_logged_in() ) { | |
auth_redirect(); | |
} | |
if ( ! is_admin_bar_showing() ) { | |
wp_die( esc_html__( 'The admin bar must be showing to use paired browsing mode.' ) ); | |
} | |
$url = remove_query_arg( PAIRED_BROWSING_QUERY_VAR ); | |
$amp_url = add_query_arg( amp_get_slug(), '1', $url ); | |
$url = remove_query_arg( 'amp_validate', $url ); | |
?> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="<?php bloginfo( 'charset' ); ?>"> | |
<title><?php esc_html_e( 'Loading...', 'amp-to-amp' ); ?></title> | |
<style> | |
html,body { | |
margin: 0; | |
padding: 0; | |
} | |
iframe { | |
border: 0; | |
position: absolute; | |
width: calc( 50% - 1px ); | |
height: 100%; | |
top: 0; | |
bottom: 0; | |
} | |
iframe.disconnected { | |
opacity: 0.5; | |
} | |
#non-amp { | |
left: 0; | |
right: auto; | |
border-right: solid 1px #DDD; | |
background-color: red; | |
} | |
#amp { | |
border-left: solid 1px #DDD; | |
left: auto; | |
right: 0; | |
background-color: green; | |
} | |
</style> | |
</head> | |
<body> | |
<iframe src="<?php echo esc_url( $url ); ?>" id="non-amp" sandbox="allow-forms allow-scripts allow-same-origin allow-popups"></iframe> | |
<iframe src="<?php echo esc_url( $amp_url ); ?>" id="amp" sandbox="allow-forms allow-scripts allow-same-origin allow-popups"></iframe> | |
<script> | |
const ampSlug = <?php echo wp_json_encode( amp_get_slug() ); ?>; | |
const ampPairedBrowsingQueryVar = <?php echo wp_json_encode( PAIRED_BROWSING_QUERY_VAR ); ?>; | |
</script> | |
<script src="<?php echo esc_url( plugin_dir_url( __FILE__ ) . 'paired-browsing-app.js' ); ?>"></script> | |
</body> | |
</html> | |
<?php | |
exit; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment