PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@ -0,0 +1,620 @@
<?php
/**
* Plugin Name: AMP
* Description: Enable AMP on your WordPress site, the WordPress way.
* Plugin URI: https://amp-wp.org
* Author: AMP Project Contributors
* Author URI: https://github.com/ampproject/amp-wp/graphs/contributors
* Version: 1.1.1
* Text Domain: amp
* Domain Path: /languages/
* License: GPLv2 or later
*
* @package AMP
*/
/**
* Print admin notice regarding having an old version of PHP.
*
* @since 0.7
*/
function _amp_print_php_version_admin_notice() {
?>
<div class="notice notice-error">
<p>
<?php
printf(
/* translators: %s: required PHP version */
esc_html__( 'The AMP plugin requires PHP %s. Please contact your host to update your PHP version.', 'amp' ),
'5.4+'
);
?>
</p>
</div>
<?php
}
if ( version_compare( phpversion(), '5.4', '<' ) ) {
add_action( 'admin_notices', '_amp_print_php_version_admin_notice' );
return;
}
/**
* Print admin notice regarding DOM extension is not installed.
*
* @since 1.0
*/
function _amp_print_php_dom_document_notice() {
?>
<div class="notice notice-error">
<p>
<?php
printf(
/* translators: %s: PHP extension name */
esc_html__( 'The AMP plugin requires the %s extension in PHP. Please contact your host to install this extension.', 'amp' ),
'DOM'
);
?>
</p>
</div>
<?php
}
if ( ! class_exists( 'DOMDocument' ) ) {
add_action( 'admin_notices', '_amp_print_php_dom_document_notice' );
return;
}
/**
* Print admin notice regarding DOM extension is not installed.
*
* @since 1.0.1
*/
function _amp_print_php_missing_iconv_notice() {
?>
<div class="notice notice-error">
<p>
<?php
printf(
/* translators: %s: PHP extension name */
esc_html__( 'The AMP plugin requires the %s extension in PHP. Please contact your host to install this extension.', 'amp' ),
'iconv'
);
?>
</p>
</div>
<?php
}
if ( ! function_exists( 'iconv' ) ) {
add_action( 'admin_notices', '_amp_print_php_missing_iconv_notice' );
return;
}
/**
* Print admin notice when composer install has not been performed.
*
* @since 1.0
*/
function _amp_print_build_needed_notice() {
?>
<div class="notice notice-error">
<p>
<?php
printf(
/* translators: %s: composer install && npm install && npm run build */
__( 'You appear to be running the AMP plugin from source. Please do %s to finish installation.', 'amp' ), // phpcs:ignore WordPress.Security.EscapeOutput
'<code>composer install && npm install && npm run build</code>'
);
?>
</p>
</div>
<?php
}
if ( ! file_exists( __DIR__ . '/vendor/autoload.php' ) || ! file_exists( __DIR__ . '/vendor/sabberworm/php-css-parser' ) || ! file_exists( __DIR__ . '/assets/js/amp-block-editor-toggle-compiled.js' ) ) {
add_action( 'admin_notices', '_amp_print_build_needed_notice' );
return;
}
define( 'AMP__FILE__', __FILE__ );
define( 'AMP__DIR__', dirname( __FILE__ ) );
define( 'AMP__VERSION', '1.1.1' );
/**
* Print admin notice if plugin installed with incorrect slug (which impacts WordPress's auto-update system).
*
* @since 1.0
*/
function _amp_incorrect_plugin_slug_admin_notice() {
$actual_slug = basename( AMP__DIR__ );
?>
<div class="notice notice-warning">
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: %1$s is the current directory name, and %2$s is the required directory name */
__( 'You appear to have installed the AMP plugin incorrectly. It is currently installed in the <code>%1$s</code> directory, but it needs to be placed in a directory named <code>%2$s</code>. Please rename the directory. This is important for WordPress plugin auto-updates.', 'amp' ),
$actual_slug,
'amp'
)
);
?>
</p>
</div>
<?php
}
if ( 'amp' !== basename( AMP__DIR__ ) ) {
add_action( 'admin_notices', '_amp_incorrect_plugin_slug_admin_notice' );
}
require_once AMP__DIR__ . '/includes/class-amp-autoloader.php';
AMP_Autoloader::register();
require_once AMP__DIR__ . '/back-compat/back-compat.php';
require_once AMP__DIR__ . '/includes/amp-helper-functions.php';
require_once AMP__DIR__ . '/includes/admin/functions.php';
register_activation_hook( __FILE__, 'amp_activate' );
/**
* Handle activation of plugin.
*
* @since 0.2
*/
function amp_activate() {
amp_after_setup_theme();
if ( ! did_action( 'amp_init' ) ) {
amp_init();
}
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'amp_deactivate' );
/**
* Handle deactivation of plugin.
*
* @since 0.2
*/
function amp_deactivate() {
// We need to manually remove the amp endpoint.
global $wp_rewrite;
foreach ( $wp_rewrite->endpoints as $index => $endpoint ) {
if ( amp_get_slug() === $endpoint[1] ) {
unset( $wp_rewrite->endpoints[ $index ] );
break;
}
}
flush_rewrite_rules();
}
/*
* Register AMP scripts regardless of whether AMP is enabled or it is the AMP endpoint
* for the sake of being able to use AMP components on non-AMP documents ("dirty AMP").
*/
add_action( 'wp_default_scripts', 'amp_register_default_scripts' );
// Ensure async and custom-element/custom-template attributes are present on script tags.
add_filter( 'script_loader_tag', 'amp_filter_script_loader_tag', PHP_INT_MAX, 2 );
// Ensure crossorigin=anonymous is added to font links.
add_filter( 'style_loader_tag', 'amp_filter_font_style_loader_tag_with_crossorigin_anonymous', 10, 4 );
/**
* Set up AMP.
*
* This function must be invoked through the 'after_setup_theme' action to allow
* the AMP setting to declare the post types support earlier than plugins/theme.
*
* @since 0.6
*/
function amp_after_setup_theme() {
amp_get_slug(); // Ensure AMP_QUERY_VAR is set.
/**
* Filters whether AMP is enabled on the current site.
*
* Useful if the plugin is network activated and you want to turn it off on select sites.
*
* @since 0.2
*/
if ( false === apply_filters( 'amp_is_enabled', true ) ) {
return;
}
add_action( 'init', 'amp_init', 0 ); // Must be 0 because widgets_init happens at init priority 1.
}
add_action( 'after_setup_theme', 'amp_after_setup_theme', 5 );
/**
* Init AMP.
*
* @since 0.1
*/
function amp_init() {
/**
* Triggers on init when AMP plugin is active.
*
* @since 0.3
*/
do_action( 'amp_init' );
add_rewrite_endpoint( amp_get_slug(), EP_PERMALINK );
add_filter( 'allowed_redirect_hosts', array( 'AMP_HTTP', 'filter_allowed_redirect_hosts' ) );
AMP_HTTP::purge_amp_query_vars();
AMP_HTTP::send_cors_headers();
AMP_HTTP::handle_xhr_request();
AMP_Theme_Support::init();
AMP_Validation_Manager::init();
AMP_Service_Worker::init();
add_action( 'init', array( 'AMP_Post_Type_Support', 'add_post_type_support' ), 1000 ); // After post types have been defined.
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'amp', new AMP_CLI() );
}
add_filter( 'request', 'amp_force_query_var_value' );
add_action( 'admin_init', 'AMP_Options_Manager::register_settings' );
add_action( 'wp_loaded', 'amp_editor_core_blocks' );
add_action( 'wp_loaded', 'amp_post_meta_box' );
add_action( 'wp_loaded', 'amp_editor_core_blocks' );
add_action( 'wp_loaded', 'amp_add_options_menu' );
add_action( 'wp_loaded', 'amp_admin_pointer' );
add_action( 'parse_query', 'amp_correct_query_when_is_front_page' );
// Redirect the old url of amp page to the updated url.
add_filter( 'old_slug_redirect_url', 'amp_redirect_old_slug_to_new_url' );
if ( class_exists( 'Jetpack' ) && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) && version_compare( JETPACK__VERSION, '6.2-alpha', '<' ) ) {
require_once AMP__DIR__ . '/jetpack-helper.php';
}
// Add actions for legacy post templates.
add_action( 'wp', 'amp_maybe_add_actions' );
/*
* Broadcast plugin updates.
* Note that AMP_Options_Manager::get_option( 'version', '0.0' ) cannot be used because
* version was new option added, and in that case default would never be used for a site
* upgrading from a version prior to 1.0. So this is why get_option() is currently used.
*/
$options = get_option( AMP_Options_Manager::OPTION_NAME, array() );
$old_version = isset( $options['version'] ) ? $options['version'] : '0.0';
if ( AMP__VERSION !== $old_version ) {
/**
* Triggers when after amp_init when the plugin version has updated.
*
* @param string $old_version Old version.
*/
do_action( 'amp_plugin_update', $old_version );
AMP_Options_Manager::update_option( 'version', AMP__VERSION );
}
}
/**
* Make sure the `amp` query var has an explicit value.
*
* This avoids issues when filtering the deprecated `query_string` hook.
*
* @since 0.3.3
*
* @param array $query_vars Query vars.
* @return array Query vars.
*/
function amp_force_query_var_value( $query_vars ) {
if ( isset( $query_vars[ amp_get_slug() ] ) && '' === $query_vars[ amp_get_slug() ] ) {
$query_vars[ amp_get_slug() ] = 1;
}
return $query_vars;
}
/**
* Conditionally add AMP actions or render the transitional mode template(s).
*
* If the request is for an AMP page and this is in 'canonical mode,' redirect to the non-AMP page.
* It won't need this plugin's template system, nor the frontend actions like the 'rel' link.
*
* @deprecated This function is not used when 'amp' theme support is added.
* @global WP_Query $wp_query
* @since 0.2
* @return void
*/
function amp_maybe_add_actions() {
// Short-circuit when theme supports AMP, as everything is handled by AMP_Theme_Support.
if ( current_theme_supports( AMP_Theme_Support::SLUG ) ) {
return;
}
// The remaining logic here is for transitional mode running in themes that don't support AMP, the template system in AMP<=0.6.
global $wp_query;
if ( ! ( is_singular() || $wp_query->is_posts_page ) || is_feed() ) {
return;
}
$is_amp_endpoint = is_amp_endpoint();
/**
* Queried post object.
*
* @var WP_Post $post
*/
$post = get_queried_object();
if ( ! post_supports_amp( $post ) ) {
if ( $is_amp_endpoint ) {
/*
* Temporary redirect is used for admin users because reader mode and AMP support can be enabled by user at any time,
* so they will be able to make AMP available for this URL and see the change without wrestling with the redirect cache.
*/
wp_safe_redirect( get_permalink( $post->ID ), current_user_can( 'manage_options' ) ? 302 : 301 );
exit;
}
return;
}
if ( $is_amp_endpoint ) {
// Prevent infinite URL space under /amp/ endpoint.
global $wp;
wp_parse_str( $wp->matched_query, $path_args );
if ( isset( $path_args[ amp_get_slug() ] ) && '' !== $path_args[ amp_get_slug() ] ) {
wp_safe_redirect( amp_get_permalink( $post->ID ), 301 );
exit;
}
amp_prepare_render();
} else {
amp_add_frontend_actions();
}
}
/**
* Fix up WP_Query for front page when amp query var is present.
*
* Normally the front page would not get served if a query var is present other than preview, page, paged, and cpage.
*
* @since 0.6
* @see WP_Query::parse_query()
* @link https://github.com/WordPress/wordpress-develop/blob/0baa8ae85c670d338e78e408f8d6e301c6410c86/src/wp-includes/class-wp-query.php#L951-L971
*
* @param WP_Query $query Query.
*/
function amp_correct_query_when_is_front_page( WP_Query $query ) {
$is_front_page_query = (
$query->is_main_query()
&&
$query->is_home()
&&
// Is AMP endpoint.
false !== $query->get( amp_get_slug(), false )
&&
// Is query not yet fixed uo up to be front page.
! $query->is_front_page()
&&
// Is showing pages on front.
'page' === get_option( 'show_on_front' )
&&
// Has page on front set.
get_option( 'page_on_front' )
&&
// See line in WP_Query::parse_query() at <https://github.com/WordPress/wordpress-develop/blob/0baa8ae/src/wp-includes/class-wp-query.php#L961>.
0 === count( array_diff( array_keys( wp_parse_args( $query->query ) ), array( amp_get_slug(), 'preview', 'page', 'paged', 'cpage' ) ) )
);
if ( $is_front_page_query ) {
$query->is_home = false;
$query->is_page = true;
$query->is_singular = true;
$query->set( 'page_id', get_option( 'page_on_front' ) );
}
}
/**
* Whether this is in 'canonical mode'.
*
* Themes can register support for this with `add_theme_support( AMP_Theme_Support::SLUG )`:
*
* add_theme_support( AMP_Theme_Support::SLUG );
*
* This will serve templates in native AMP, allowing you to use AMP components in your theme templates.
* If you want to make available in transitional mode, where templates are served in AMP or non-AMP documents, do:
*
* add_theme_support( AMP_Theme_Support::SLUG, array(
* 'paired' => true,
* ) );
*
* Transitional mode is also implied if you define a template_dir:
*
* add_theme_support( AMP_Theme_Support::SLUG, array(
* 'template_dir' => 'amp',
* ) );
*
* If you want to have AMP-specific templates in addition to serving native AMP, do:
*
* add_theme_support( AMP_Theme_Support::SLUG, array(
* 'paired' => false,
* 'template_dir' => 'amp',
* ) );
*
* If you want to force AMP to always be served on a given template, you can use the templates_supported arg,
* for example to always serve the Category template in AMP:
*
* add_theme_support( AMP_Theme_Support::SLUG, array(
* 'templates_supported' => array(
* 'is_category' => true,
* ),
* ) );
*
* Or if you want to force AMP to be used on all templates:
*
* add_theme_support( AMP_Theme_Support::SLUG, array(
* 'templates_supported' => 'all',
* ) );
*
* @see AMP_Theme_Support::read_theme_support()
* @return boolean Whether this is in AMP 'canonical' mode, that is whether it is native and there is not separate AMP URL current URL.
*/
function amp_is_canonical() {
if ( ! current_theme_supports( AMP_Theme_Support::SLUG ) ) {
return false;
}
$args = AMP_Theme_Support::get_theme_support_args();
if ( isset( $args['paired'] ) ) {
return empty( $args['paired'] );
}
// If there is a template_dir, then transitional mode is implied.
return empty( $args['template_dir'] );
}
/**
* Load classes.
*
* @since 0.2
* @deprecated As of 0.6 since autoloading is now employed.
*/
function amp_load_classes() {
_deprecated_function( __FUNCTION__, '0.6' );
}
/**
* Add frontend actions.
*
* @since 0.2
*/
function amp_add_frontend_actions() {
add_action( 'wp_head', 'amp_add_amphtml_link' );
}
/**
* Add post template actions.
*
* @since 0.2
* @deprecated This function is not used when 'amp' theme support is added.
*/
function amp_add_post_template_actions() {
require_once AMP__DIR__ . '/includes/amp-post-template-functions.php';
amp_post_template_init_hooks();
}
/**
* Add action to do post template rendering at template_redirect action.
*
* @since 0.2
* @since 1.0 The amp_render() function is called at template_redirect action priority 11 instead of priority 10.
* @deprecated This function is not used when 'amp' theme support is added.
*/
function amp_prepare_render() {
add_action( 'template_redirect', 'amp_render', 11 );
}
/**
* Render AMP for queried post.
*
* @since 0.1
* @deprecated This function is not used when 'amp' theme support is added.
*/
function amp_render() {
// Note that queried object is used instead of the ID so that the_preview for the queried post can apply.
$post = get_queried_object();
if ( $post instanceof WP_Post ) {
amp_render_post( $post );
exit;
}
}
/**
* Render AMP post template.
*
* @since 0.5
* @deprecated This function is not used when 'amp' theme support is added.
*
* @param WP_Post|int $post Post.
* @global WP_Query $wp_query
*/
function amp_render_post( $post ) {
global $wp_query;
if ( ! ( $post instanceof WP_Post ) ) {
$post = get_post( $post );
if ( ! $post ) {
return;
}
}
$post_id = $post->ID;
/*
* If amp_render_post is called directly outside of the standard endpoint, is_amp_endpoint() will return false,
* which is not ideal for any code that expects to run in an AMP context.
* Let's force the value to be true while we render AMP.
*/
$was_set = isset( $wp_query->query_vars[ amp_get_slug() ] );
if ( ! $was_set ) {
$wp_query->query_vars[ amp_get_slug() ] = true;
}
// Prevent New Relic from causing invalid AMP responses due the NREUM script it injects after the meta charset.
if ( extension_loaded( 'newrelic' ) ) {
newrelic_disable_autorum();
}
/**
* Fires before rendering a post in AMP.
*
* This action is not triggered when 'amp' theme support is present. Instead, you should use 'template_redirect' action and check if `is_amp_endpoint()`.
*
* @since 0.2
*
* @param int $post_id Post ID.
*/
do_action( 'pre_amp_render_post', $post_id );
amp_add_post_template_actions();
$template = new AMP_Post_Template( $post );
$template->load();
if ( ! $was_set ) {
unset( $wp_query->query_vars[ amp_get_slug() ] );
}
}
/**
* Bootstraps the AMP customizer.
*
* Uses the priority of 12 for the 'after_setup_theme' action.
* Many themes run `add_theme_support()` on the 'after_setup_theme' hook, at the default priority of 10.
* And that function's documentation suggests adding it to that action.
* So this enables themes to `add_theme_support( AMP_Theme_Support::SLUG )`.
* And `amp_init_customizer()` will be able to recognize theme support by calling `amp_is_canonical()`.
*
* @since 0.4
*/
function _amp_bootstrap_customizer() {
add_action( 'after_setup_theme', 'amp_init_customizer', 12 );
}
add_action( 'plugins_loaded', '_amp_bootstrap_customizer', 9 ); // Should be hooked before priority 10 on 'plugins_loaded' to properly unhook core panels.
/**
* Redirects the old AMP URL to the new AMP URL.
*
* If post slug is updated the amp page with old post slug will be redirected to the updated url.
*
* @since 0.5
* @deprecated This function is irrelevant when 'amp' theme support is added.
*
* @param string $link New URL of the post.
* @return string URL to be redirected.
*/
function amp_redirect_old_slug_to_new_url( $link ) {
if ( is_amp_endpoint() && ! amp_is_canonical() ) {
if ( current_theme_supports( AMP_Theme_Support::SLUG ) ) {
$link = add_query_arg( amp_get_slug(), '', $link );
} else {
$link = trailingslashit( trailingslashit( $link ) . amp_get_slug() );
}
}
return $link;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
.column-error_status .dashicons-editor-help {
color: #767676;
}
.column-sources .dashicons,
.column-sources_with_invalid_output .dashicons {
margin-right: 5px;
}
.column-source .dashicons-admin-plugins,
.column-sources_with_invalid_output .dashicons-admin-plugins {
color: #64a2e9;
}
.column-source .dashicons-admin-appearance,
.column-sources_with_invalid_output .dashicons-admin-appearance {
color: #ebb04f;
}
.column-source, .dashicons-wordpress-alt,
.column-sources_with_invalid_output .dashicons-wordpress-alt {
color: #92b371;
}
.amp-logo-icon {
background-image: url( '../images/amp-logo-icon.svg' );
background-color: transparent;
background-size: 20px 20px;
height: 20px;
width: 20px;
display: inline-block;
}
.column-error_status .error-status {
line-height: 20px;
display: inline-block;
position: relative;
vertical-align: top;
margin-left: 10px;
}
td.column-found_elements_and_attributes {
color: #970010;
}
td.column-found_elements_and_attributes div {
margin-bottom: 0.6rem;
}
.column-error_status .dashicons-flag.new {
color: #d98501;
}
.column-error_status .dashicons-yes.new {
color: #ff0000;
}
.column-error_status .dashicons-warning.rejected {
color: #68c6ff;
}
.column-sources .source,
.column-sources_with_invalid_output .source {
margin-bottom: 10px;
display: block;
}
.wrap .wp-heading-inline + .page-title-action {
margin-left: 1rem;
}

View File

@ -0,0 +1,123 @@
.amp-toggle {
position: relative;
display: inline-block;
width: 30px;
height: 15px;
top: 15px;
left: 130px;
}
.amp-toggle input,
.amp-toggle input.disabled {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
margin: 0;
padding: 0;
z-index: 1;
}
.amp-toggle .slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 34px;
background-color: #555D66;
-webkit-transition: .3s;
transition: .3s;
transition-property: background-color, transform, -webkit-transform, -ms-transform, opacity;
}
.amp-toggle input:focus,
.amp-toggle input:active {
outline: none;
}
.amp-toggle input:hover + .slider,
.amp-toggle input:focus + .slider,
.amp-toggle input:active + .slider {
box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
}
.amp-toggle .slider:before {
position: absolute;
content: "";
height: 13px;
width: 13px;
top: 1px;
left: 1px;
border-radius: 50%;
background-color: transparent;
background-image: url( '../images/amp-white-icon.svg' );
background-size: 13px 13px;
-webkit-transition: .3s;
transition: .3s;
}
.amp-toggle input:checked + .slider {
background-color: #0379C4;
}
.amp-toggle input:checked + .slider:before {
-webkit-transform: translateX( 15px );
-ms-transform: translateX( 15px );
transform: translateX( 15px );
}
.amp-toggle input.disabled + .slider {
opacity: 0.7;
}
.amp-toggle .tooltip {
position: absolute;
bottom: 25px;
left: -115px;
width: 230px;
font-size: 13px;
text-align: center;
color: #FFFFFF;
background: #191E23;
padding: 15px;
z-index: 1;
cursor: default;
display: none;
}
.amp-toggle .tooltip a {
color: #00a0d2;
}
.amp-toggle .tooltip a:hover,
.amp-toggle .tooltip a:focus,
.amp-toggle .tooltip a:active {
color: #54cbf1;
}
.amp-toggle .tooltip:before {
position: absolute;
bottom: -8px;
left: 120px;
content: "";
border: solid;
border-color: #191E23 transparent;
border-width: 8px 8px 0 8px;
}
.js .accordion-section-title:after {
z-index: 0;
}
#customize-footer-actions .collapse-sidebar-label {
font-size: 11px;
margin-left: -3px;
}
.devices-wrapper .preview-desktop {
border-left: 1px solid #DDDDDD !important;
}
.wp-full-overlay-footer .devices button:before {
vertical-align: initial;
}

View File

@ -0,0 +1,30 @@
/*
* Prevent cases of amp-img converted from img to appear with stretching by using object-fit to scale.
* See <https://github.com/ampproject/amphtml/issues/21371#issuecomment-475443219>.
* Also use object-fit:contain in worst case scenario when we can't figure out dimensions for an image.
*/
amp-img.amp-wp-enforced-sizes[layout=intrinsic] > img,
.amp-wp-unknown-size > img {
object-fit: contain;
}
amp-fit-text blockquote,
amp-fit-text h1,
amp-fit-text h2,
amp-fit-text h3,
amp-fit-text h4,
amp-fit-text h5,
amp-fit-text h6 {
font-size: inherit;
}
/**
* Override a style rule in Twenty Sixteen and Twenty Seventeen.
* It set display:none for audio elements.
* This selector is the same, though it adds body and uses amp-audio instead of audio.
*/
body amp-audio:not([controls]) {
display: inline-block;
height: auto;
}

View File

@ -0,0 +1,4 @@
.is-amp-fit-text + .blocks-font-size > .components-font-size-picker__buttons,
.is-amp-fit-text + .blocks-font-size > .components-font-size-picker__custom-input {
display: none;
}

View File

@ -0,0 +1,19 @@
/**
* For the custom AMP implementation of the 'playlist' shortcode.
*/
.wp-playlist .wp-playlist-current-item img {
margin-right: 0;
}
.wp-playlist .wp-playlist-current-item amp-img {
float: left;
margin-right: 10px;
}
.wp-playlist audio {
display: block;
}
.wp-playlist .amp-carousel-button {
visibility: hidden;
}

View File

@ -0,0 +1,79 @@
/**
* 1.0 AMP preview.
*
* Submit box preview buttons.
*/
/* Core preview button */
.wp-core-ui #preview-action.has-amp-preview #post-preview {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
float: none;
}
/* AMP preview button */
.wp-core-ui #amp-post-preview.preview {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
text-indent: -9999px;
padding-right: 14px;
padding-left: 14px;
position: relative;
}
.wp-core-ui #amp-post-preview.preview::after {
content: "icon";
background: no-repeat center url( '../images/amp-icon.svg' );
background-size: 14px !important;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: block;
position: absolute;
}
.wp-core-ui #amp-post-preview.preview.disabled::after {
opacity: 0.6;
}
/* AMP status */
.misc-amp-status .amp-icon {
float: left;
background: transparent url( '../images/amp-icon.svg' ) no-repeat left;
background-size: 17px;
width: 17px;
height: 17px;
margin: 0 8px 0 1px;
}
#amp-status-select fieldset {
margin: 7px 0 0 1px;
}
#amp-status-select .notice {
margin: 10px 0 -5px 3px;
}
.amp-status-actions {
margin-top: 10px;
}
@media screen and ( max-width: 782px ) {
#amp-status-select {
line-height: 280%;
}
}
.amp-block-validation-errors {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
}
.amp-block-validation-errors .amp-block-validation-errors__summary {
margin: 0.5em 0;
padding: 2px;
}
.amp-block-validation-errors .amp-block-validation-errors__list {
padding-left: 2.5em;
}

View File

@ -0,0 +1,279 @@
#col-left {
display: none;
}
#col-right {
float: none;
width: auto;
}
/* Move the 'All dates' filter to the right of the new status and type filters */
#filter-by-date {
float: none;
}
/* Improve column widths */
td.column-details pre,
td.column-sources pre {
overflow: auto;
}
th.column-created_date_gmt,
th.column-error_type {
width: 15%;
}
td.column-error .error-code {
font-family: Consolas, Monaco, monospace;
}
th.column-status {
width: 15%;
}
.fixed th.column-posts {
width: 10%;
}
/* Details column */
.column-details .details-attributes__summary {
display: flex;
justify-content: space-between;
align-items: center;
}
details[open] .details-attributes__summary {
font-weight: 600;
margin-bottom: 15px;
}
.column-details .details-attributes__summary::-webkit-details-marker,
.column-details .notice details > summary::-webkit-details-marker {
display: none;
}
.details-attributes__summary::after,
.single-error-detail-summary::after {
order: 99;
width: 12px;
height: 12px;
background-image: url("../images/down-triangle.svg");
background-size: cover;
background-position: center;
content: "";
}
tr.expanded .details-attributes__summary::after,
details.single-error-detail[open] .single-error-detail-summary::after {
transform: rotate(180deg);
}
.notice .detailed {
padding-left: 15px;
}
.notice .detailed details {
padding-bottom: 16px;
}
.notice .detailed details .detailed {
padding-left: 32px;
font-family: Consolas, Monaco, monospace;
}
.details-attributes__title code,
.notice .detailed summary code {
display: inline-block;
min-width: 240px;
margin-left: 18px;
font-weight: 600;
}
.details-attributes__title code {
margin-left: 0;
}
.details-attributes__list {
margin-top: 0;
padding-left: 0;
list-style: none;
font-family: Consolas, Monaco, monospace;
}
.details-attributes__list li {
word-break: break-all;
}
.details-attributes__attr {
font-weight: 600;
}
.column-sources_with_invalid_output details[open] .details-attributes__summary {
margin-bottom: 5px;
}
.column-sources_with_invalid_output details > div {
padding-left: 25px;
}
/* Error details toggle button */
.manage-column.column-sources_with_invalid_output .error-details-toggle {
margin: 0;
}
.error-details-toggle {
float: right;
display: flex;
flex-direction: column;
height: 14px;
padding: 0;
margin-top: 4px;
background: none;
border: none;
cursor: pointer;
}
.error-details-toggle.is-open {
transform: rotate(180deg);
}
.column-details .error-details-toggle::before,
.column-details .error-details-toggle::after {
width: 12px;
height: 12px;
background-image: url("../images/down-triangle.svg");
background-size: cover;
background-position: center;
content: "";
}
/* Status text icons */
.status-text {
display: flex;
align-items: center;
padding-bottom: 0.6rem;
}
.status-text::before {
margin-right: 10px;
background-size: 20px 20px;
height: 20px;
width: 20px;
content: "";
min-width: 20px;
}
.status-text.sanitized::before {
background-image: url("../images/amp-logo-icon.svg");
}
.status-text.new::before {
background-image: url("../images/baseline-error.svg");
}
.status-text.new.new-accepted::before, .status-text.new.accepted::before{
background-image: url("../images/baseline-error-green.svg");
}
.status-text.new.new-rejected::before, .status-text.new.rejected::before {
background-image: url("../images/baseline-error-red.svg");
}
.status-text.accepted::before {
background-image: url("../images/baseline-check-circle-green.svg");
}
.status-text.rejected::before {
background-image: url("../images/error-rejected.svg");
}
.single-error-detail {
margin: 5px 0 5px 0;
}
.single-error-detail-summary:after {
display: inline-block;
}
.single-error-detail-summary strong {
margin-right: 10px;
font-size: 15px;
}
.single-error-detail ul.secondary-details-array .details-attributes__attr {
margin-left: 20px;
}
.single-error-detail ul.secondary-details-array .details-attributes__value {
margin-left: 30px;
}
.single-error-detail .details-attributes__value {
margin-left: 10px;
}
body.taxonomy-amp_validation_error .wp-list-table .new th,
body.taxonomy-amp_validation_error .wp-list-table .new td,
tr.expanded.new + tr > td:first-of-type,
body.post-type-amp_validated_url .wp-list-table .new th,
body.post-type-amp_validated_url .wp-list-table .new td {
background-color: #fef7f1;
}
body.taxonomy-amp_validation_error .wp-list-table .new th.check-column,
tr.expanded.new + tr > td:first-of-type,
body.post-type-amp_validated_url .wp-list-table .new th.check-column {
border-left: 4px solid #d54e21;
}
body.taxonomy-amp_validation_error .wp-list-table .new th.check-column input {
margin-left: 4px;
}
.row-actions .amp_validation_error_accept > a {
color: #006505;
}
.row-actions .amp_validation_error_reject > a {
color: #a00;
}
.notice.accept-reject-error {
display: flex;
margin-bottom: 0;
}
.notice.accept-reject-error > p {
display: inline-block;
font-size: 14px;
flex-grow: 10;
margin-right: 20px;
}
.notice.accept-reject-error > .button {
display: inline-block;
margin: 5px 5px 0 5px;
padding: 0 26px 2px;
flex-grow: 1;
text-align: center;
}
.notice.accept-reject-error > .button.accept {
/* @todo Add green colors */
}
.notice.accept-reject-error > .button.reject {
/* @todo Add red colors */
}
.notice.error-details {
margin-top: 1px;
}
.wp-heading-inline .status-text {
display: inline-flex;
margin-left: 10px;
vertical-align: middle;
padding-bottom: 0;
}
.wp-heading-inline code {
font-size: 1rem;
}
/** Details post action. */
.details button {
display: inline-block;
background: none;
border: none;
padding: 0;
text-align: left;
color: #0073aa;
cursor: pointer;
text-decoration: underline;
}

View File

@ -0,0 +1,164 @@
/** Arrow icon on title in error column. */
.column-error > .single-url-detail-toggle {
position: relative;
width: 100%;
padding: 5px 36px 5px 0;
background: none;
border: none;
text-align: left;
line-height: 1.682;
color: #0073aa;
cursor: pointer;
}
.column-error > .single-url-detail-toggle::after {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
width: 12px;
height: 18px;
margin-top: 5px;
background-image: url("../images/down-triangle.svg");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
content: "";
}
tr.expanded .single-url-detail-toggle::after {
transform: rotate(180deg);
}
/** Striped table overrides. */
table.striped > tbody > tr.odd {
background: #f9f9f9;
}
table.striped > tbody > tr.even {
background: #fff;
}
/** Hide original details content. */
.details-attributes > .detailed {
display: none;
}
/** Details post action. */
.details button {
display: inline-block;
background: none;
border: none;
padding: 0;
text-align: left;
color: #0073aa;
cursor: pointer;
text-decoration: underline;
}
/** Details row styles. */
.details details.details-attributes:hover {
cursor: pointer;
}
.details ul.detailed {
padding: 0 32px;
margin-top: 0;
}
.details div.detailed {
padding-left: 30px;
margin-top: 10px;
font-family: Consolas, Monaco, monospace;
}
.details .detailed details {
padding-bottom: 16px;
}
.details .detailed summary code {
display: inline-block;
min-width: 240px;
margin-left: 12px;
font-weight: 600;
}
.column-status select {
vertical-align: top;
}
.column-status img {
width: 1.5rem;
margin-top: 0.2rem;
}
#number-errors {
text-align: center;
background-color: #d3d3d3b8;
color: #1e8cbecc;
}
#url-post-filter {
float: none;
display: inline;
}
.tablenav.top,
.tablenav.bottom {
display: none;
}
.amp-validated-url a {
text-decoration: none;
}
.curtime.misc-pub-section {
margin-top: 0.5rem;
}
/* Give enough width to prevent the widest column status, 'New Accepted,' from forcing the <select> below the icon. */
.wp-list-table th.column-status {
width: 150px;
}
.wp-list-table th.column-sources_with_invalid_output {
width: 20%;
}
.wp-list-table th.column-error {
width: 25%;
}
.wp-list-table th.column-details {
width: 20%;
}
/** Add space between list table and the filter and search box above it. */
#post-body-content button.action,
#post-body-content #url-post-filter,
#post-body-content .search-box {
margin-bottom: 4px;
}
#post-body-content button.reject {
margin-left: 4px;
}
#accept-reject-buttons:not(.hidden) {
display: inline-block;
}
#vertical-divider {
display: inline-block;
vertical-align: middle;
width: 15px;
margin-right: 15px;
height: 30px;
border-right: 1px solid #a0a5aa;
}
.amp-validation-error-status {
width: auto;
float: none;
}

View File

@ -0,0 +1,7 @@
/* @todo This should be moved to admin-tables.css which is then enqueued on both screens. */
.tooltip-button {
margin: 0 6px;
cursor: pointer;
color: #767676;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="62px" height="62px" viewBox="0 0 62 62" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AMP-Icon</title>
<g id="AMP-Icon" fill="#82878c">
<path d="M41.6288667,28.1614333 L28.6243667,49.8035667 L26.2683667,49.8035667 L28.5975,35.7016667 L21.3838,35.7109667 C21.3838,35.7109667 21.3156,35.7130333 21.2835667,35.7130333 C20.6336,35.7130333 20.1076333,35.1870667 20.1076333,34.5371 C20.1076333,34.2581 20.367,33.7858667 20.367,33.7858667 L33.3291333,12.1695667 L35.7244,12.1799 L33.3363667,26.3035 L40.5872667,26.2942 C40.5872667,26.2942 40.6647667,26.2931667 40.7019667,26.2931667 C41.3519333,26.2931667 41.8779,26.8191333 41.8779,27.4691 C41.8779,27.7326 41.7745667,27.9640667 41.6278333,28.1604 L41.6288667,28.1614333 Z M31,0 C13.8787,0 0,13.8797333 0,31 C0,48.1213 13.8787,62 31,62 C48.1202667,62 62,48.1213 62,31 C62,13.8797333 48.1202667,0 31,0 L31,0 Z" id="Fill-1"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1012 B

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<g id="AMP-Logo-Icon" fill="#0075C2">
<path d="M13.3,9.1l-4,6.6H8.5l0.7-4.3l-2.2,0h0c-0.2,0-0.4-0.2-0.4-0.4c0-0.1,0.1-0.2,0.1-0.2l4-6.6l0.7,0l-0.7,4.3l2.2,0l0,0
c0.2,0,0.4,0.2,0.4,0.4C13.4,9,13.3,9.1,13.3,9.1L13.3,9.1z M10,0.5c-5.3,0-9.6,4.3-9.6,9.5c0,5.3,4.3,9.5,9.6,9.5
c5.3,0,9.6-4.3,9.6-9.5C19.6,4.7,15.3,0.5,10,0.5z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 684 B

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="908.3604125976562 898.2000122070312 183.05340576171875 200.39996337890625" style="enable-background:new 0 0 2000 2000;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.8;fill:none;stroke:url(#SVGID_1_);stroke-width:9.6838;stroke-miterlimit:10;enable-background:new ;}
.st1{opacity:0.8;fill:none;stroke:url(#SVGID_2_);stroke-width:10.1244;stroke-miterlimit:10;enable-background:new ;}
.st2{fill:none;stroke:url(#SVGID_3_);stroke-width:2.4095;stroke-miterlimit:10;}
.st3{fill:url(#SVGID_4_);}
.st4{fill:#FFFFFF;stroke:url(#SVGID_5_);stroke-width:2.4095;stroke-miterlimit:10;}
.st5{fill:none;stroke:url(#SVGID_6_);stroke-width:2.4095;stroke-miterlimit:10;}
.st6{fill:#0DD7FF;fill-opacity:0.7;}
.st7{opacity:0.7;fill:#0DD7FF;enable-background:new ;}
.st8{fill:url(#SVGID_7_);fill-opacity:0.75;}
.st9{fill:none;stroke:url(#SVGID_8_);stroke-width:2.4095;stroke-linecap:round;stroke-miterlimit:10;}
.st10{fill:none;stroke:url(#SVGID_9_);stroke-width:2.4095;stroke-linecap:round;stroke-miterlimit:10;}
.st11{fill:#FFFFFF;fill-opacity:0.75;stroke:url(#SVGID_10_);stroke-width:2.4095;stroke-miterlimit:10;}
.st12{fill:url(#SVGID_11_);fill-opacity:0.75;}
.st13{fill:none;stroke:url(#SVGID_12_);stroke-width:2.4095;stroke-linecap:round;stroke-miterlimit:10;}
.st14{fill:none;stroke:url(#SVGID_13_);stroke-width:2.4095;stroke-linecap:round;stroke-miterlimit:10;}
.st15{fill:#FFFFFF;}
.st16{fill:#167DD2;}
.st17{opacity:0.3;fill:#0DD7FF;enable-background:new ;}
.st18{opacity:0.5;fill:none;stroke:#FFFFFF;stroke-width:2.4095;stroke-linecap:round;stroke-miterlimit:10;}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-12002.9561" y1="893.2319" x2="-11948.1289" y2="893.2319" gradientTransform="matrix(0.3305 -0.9438 -0.9438 -0.3305 5754.7773 -10044.6777)">
<stop offset="0" style="stop-color:#0389FF"/>
<stop offset="0.5" style="stop-color:#0DD7FF"/>
<stop offset="1" style="stop-color:#FFFFFF"/>
</linearGradient>
<line class="st0" x1="954.3" y1="935.3" x2="953.8" y2="989.9"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-12020.958" y1="879.0432" x2="-11920.0166" y2="879.0432" gradientTransform="matrix(0.3305 -0.9438 -0.9438 -0.3305 5754.7773 -10044.6777)">
<stop offset="0" style="stop-color:#0389FF"/>
<stop offset="0.5" style="stop-color:#0DD7FF"/>
<stop offset="1" style="stop-color:#FFFFFF"/>
</linearGradient>
<line class="st1" x1="969.7" y1="910.9" x2="968.5" y2="1014"/>
<g>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1008.2053" y1="1053.8057" x2="1008.2053" y2="1103.8019" gradientTransform="matrix(1 0 0 -1 0 2002)">
<stop offset="0" style="stop-color:#1C79C4"/>
<stop offset="0.51" style="stop-color:#0389FF"/>
<stop offset="1" style="stop-color:#0DD7FF"/>
</linearGradient>
<line class="st2" x1="1008.2" y1="948.2" x2="1008.2" y2="900.8"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="1008.2053" y1="1053.8057" x2="1008.2053" y2="1103.8019" gradientTransform="matrix(1 0 0 -1 0 2002)">
<stop offset="0" style="stop-color:#1C79C4"/>
<stop offset="0.51" style="stop-color:#0389FF"/>
<stop offset="1" style="stop-color:#0DD7FF"/>
</linearGradient>
<polygon class="st3" points="1018.1,908.8 1016.3,910.4 1008.2,901.7 1000.1,910.4 998.4,908.8 1008.2,898.2 "/>
</g>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="941.6317" y1="1014.0682" x2="1040.9495" y2="1014.0682">
<stop offset="0" style="stop-color:#187CCE"/>
<stop offset="1" style="stop-color:#0DD5FE"/>
</linearGradient>
<polygon class="st4" points="942.8,1098.6 1039.7,1045.5 1039.7,929.6 942.8,982.7 "/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="943.079" y1="1027.4312" x2="1041.1453" y2="1027.4312" gradientTransform="matrix(1 0 0 -1 0 2002)">
<stop offset="0" style="stop-color:#1C79C4"/>
<stop offset="0.51" style="stop-color:#0389FF"/>
<stop offset="1" style="stop-color:#0DD7FF"/>
</linearGradient>
<line class="st5" x1="1040.6" y1="948" x2="943.7" y2="1001.1"/>
<ellipse transform="matrix(0.1104 -0.9939 0.9939 0.1104 -124.5549 1829.4717)" class="st6" cx="959.7" cy="984.3" rx="2.8" ry="2.8"/>
<ellipse transform="matrix(0.1094 -0.994 0.994 0.1094 -133.3753 1826.4724)" class="st6" cx="952.6" cy="987.7" rx="2.8" ry="2.8"/>
<ellipse transform="matrix(0.1094 -0.994 0.994 0.1094 -111.5842 1834.7726)" class="st6" cx="968.1" cy="979.7" rx="2.8" ry="2.8"/>
<ellipse transform="matrix(0.1172 -0.9931 0.9931 0.1172 23.327 1872.063)" class="st7" cx="1064.6" cy="922.9" rx="3.4" ry="3.5"/>
<ellipse transform="matrix(0.1172 -0.9931 0.9931 0.1172 -144.5006 1781.6594)" class="st7" cx="929.9" cy="972.1" rx="3.4" ry="3.5"/>
<ellipse transform="matrix(0.1172 -0.9931 0.9931 0.1172 -261.0735 1874.2338)" class="st7" cx="923.7" cy="1084" rx="3.4" ry="3.5"/>
<ellipse transform="matrix(0.1172 -0.9931 0.9931 0.1172 -116.1821 1770.166)" class="st7" cx="937.6" cy="950.4" rx="3.4" ry="3.5"/>
<ellipse transform="matrix(0.1172 -0.9931 0.9931 0.1172 -177.6883 1787.7156)" class="st7" cx="916.7" cy="993.8" rx="3.4" ry="3.5"/>
<ellipse transform="matrix(0.1172 -0.9931 0.9931 0.1172 2.8353 1930.9661)" class="st7" cx="1087.5" cy="963.9" rx="3.4" ry="3.5"/>
<ellipse transform="matrix(0.1172 -0.9931 0.9931 0.1172 14.4208 1901.3986)" class="st7" cx="1076.7" cy="942.6" rx="3.4" ry="3.5"/>
<ellipse transform="matrix(0.1172 -0.9931 0.9931 0.1172 -255.1249 1856.6938)" class="st7" cx="916.8" cy="1071.8" rx="3.4" ry="3.5"/>
<ellipse transform="matrix(0.1172 -0.9931 0.9931 0.1172 -141.707 1747.8772)" class="st7" cx="912.3" cy="953.6" rx="3.4" ry="3.5"/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="988.4651" y1="1030.0048" x2="988.4651" y2="980.6591">
<stop offset="0" style="stop-color:#187CCE"/>
<stop offset="0" style="stop-color:#187FD0"/>
<stop offset="1" style="stop-color:#0DD5FE"/>
</linearGradient>
<polygon class="st8" points="972.5,1030 1004.4,1013.6 1004.4,980.7 972.5,997 "/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="976.1646" y1="969.6558" x2="1000.2975" y2="969.6558" gradientTransform="matrix(1 0 0 -1 0 2002)">
<stop offset="0" style="stop-color:#1C79C4"/>
<stop offset="0.51" style="stop-color:#0389FF"/>
<stop offset="1" style="stop-color:#0DD7FF"/>
</linearGradient>
<line class="st9" x1="977.4" y1="1038.1" x2="999.1" y2="1026.6"/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="976.1646" y1="961.2227" x2="1000.2975" y2="961.2227" gradientTransform="matrix(1 0 0 -1 0 2002)">
<stop offset="0" style="stop-color:#1C79C4"/>
<stop offset="0.51" style="stop-color:#0389FF"/>
<stop offset="1" style="stop-color:#0DD7FF"/>
</linearGradient>
<line class="st10" x1="977.4" y1="1046.5" x2="999.1" y2="1035"/>
<g>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="1016.4717" y1="1038.5374" x2="1063.432" y2="1038.5374">
<stop offset="0" style="stop-color:#187CCE"/>
<stop offset="1" style="stop-color:#0DD5FE"/>
</linearGradient>
<polygon class="st11" points="1062.2,981.4 1017.7,1004.3 1017.7,1095.7 1062.2,1072.8 "/>
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="1039.6508" y1="1046.2155" x2="1039.6508" y2="996.8698">
<stop offset="0" style="stop-color:#187CCE"/>
<stop offset="0" style="stop-color:#187FD0"/>
<stop offset="1" style="stop-color:#0DD5FE"/>
</linearGradient>
<polygon class="st12" points="1023.7,1046.2 1055.6,1029.9 1055.6,996.9 1023.7,1013.2 "/>
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="1023.5676" y1="954.687" x2="1056.0122" y2="954.687" gradientTransform="matrix(1 0 0 -1 0 2002)">
<stop offset="0" style="stop-color:#1C79C4"/>
<stop offset="0.51" style="stop-color:#0389FF"/>
<stop offset="1" style="stop-color:#0DD7FF"/>
</linearGradient>
<line class="st13" x1="1024.8" y1="1055.3" x2="1054.8" y2="1039.4"/>
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="1023.5676" y1="945.752" x2="1056.0122" y2="945.752" gradientTransform="matrix(1 0 0 -1 0 2002)">
<stop offset="0" style="stop-color:#1C79C4"/>
<stop offset="0.51" style="stop-color:#0389FF"/>
<stop offset="1" style="stop-color:#0DD7FF"/>
</linearGradient>
<line class="st14" x1="1024.8" y1="1064.2" x2="1054.8" y2="1048.3"/>
<path class="st15" d="M1043.8,1018.9l-5,11.6l-0.9,0.3l0.9-6.7l-2.8,0.9l0,0c-0.2,0.1-0.5-0.1-0.5-0.4c0-0.1,0.1-0.4,0.1-0.4
l5-11.6l0.9-0.3l-0.9,6.8l2.8-0.9l0,0c0.3-0.1,0.5,0.1,0.5,0.4C1043.8,1018.6,1043.8,1018.8,1043.8,1018.9L1043.8,1018.9z"/>
</g>
<polygon class="st16" points="968.6,1083 943.7,1097.1 943.7,1002.4 968.6,989 "/>
<polygon class="st17" points="1013.1,975.4 1032.9,964.4 1032.9,973.6 1013.6,984.7 "/>
<polygon class="st17" points="1013.1,988.5 1032.9,977.4 1032.9,982.8 1013.6,993.7 "/>
<path class="st15" d="M956.2,1000c-4.7,2.2-8.6,8.1-8.6,13.1c0,5,3.9,7.4,8.6,5.2c4.7-2.2,8.6-8.1,8.6-13.1
C964.8,1000.2,960.9,997.8,956.2,1000z M948.4,1012.7c0-1.2,0.2-2.4,0.7-3.7l3.7,9.1C950.2,1018,948.4,1016,948.4,1012.7z
M956.2,1017.4c-0.8,0.3-1.5,0.6-2.2,0.7l2.3-8.2l2.4,5.8c0,0,0,0.1,0.1,0.1C958,1016.4,957.1,1017,956.2,1017.4z M957.2,1004.8
c0.5-0.2,0.9-0.5,0.9-0.5c0.4-0.2,0.4-0.9-0.1-0.7c0,0-1.3,0.7-2.1,1.1c-0.8,0.3-2,0.8-2,0.8c-0.4,0.2-0.5,0.9,0,0.7
c0,0,0.4-0.1,0.8-0.3l1.2,3l-1.7,6.2l-2.8-7.6c0.5-0.2,0.9-0.5,0.9-0.5c0.4-0.2,0.4-0.9-0.1-0.7c0,0-1.3,0.7-2.1,1.1
c-0.1,0.1-0.3,0.1-0.5,0.2c1.4-2.9,3.8-5.4,6.5-6.7c2-0.9,3.9-1,5.2-0.2c0,0-0.1,0-0.1,0c-0.8,0.3-1.3,1.3-1.3,2.1
c0,0.7,0.4,1.1,0.8,1.6c0.3,0.4,0.6,1,0.6,2c0,0.7-0.3,1.6-0.6,2.9l-0.8,3.1L957.2,1004.8z M963,1002.1c0.6,0.9,1,2.1,1,3.5
c0,3-1.5,6.4-3.8,8.9l2.3-8.4c0.4-1.4,0.6-2.4,0.6-3.2C963,1002.6,963,1002.3,963,1002.1z"/>
<path class="st18" d="M970.6,1021.2"/>
<line class="st18" x1="948.6" y1="1028.4" x2="963.6" y2="1020.5"/>
<line class="st18" x1="948.6" y1="1036.3" x2="963.6" y2="1028.4"/>
<line class="st18" x1="948.6" y1="1044.3" x2="963.6" y2="1036.3"/>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="62px" height="62px" viewBox="0 0 62 62" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AMP-White-Icon</title>
<g id="amp-logo-internal-site" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AMP-Brand-White-Icon" fill="#FFFFFF">
<path d="M41.6288667,28.1614333 L28.6243667,49.8035667 L26.2683667,49.8035667 L28.5975,35.7016667 L21.3838,35.7109667 C21.3838,35.7109667 21.3156,35.7130333 21.2835667,35.7130333 C20.6336,35.7130333 20.1076333,35.1870667 20.1076333,34.5371 C20.1076333,34.2581 20.367,33.7858667 20.367,33.7858667 L33.3291333,12.1695667 L35.7244,12.1799 L33.3363667,26.3035 L40.5872667,26.2942 C40.5872667,26.2942 40.6647667,26.2931667 40.7019667,26.2931667 C41.3519333,26.2931667 41.8779,26.8191333 41.8779,27.4691 C41.8779,27.7326 41.7745667,27.9640667 41.6278333,28.1604 L41.6288667,28.1614333 Z M31,0 C13.8787,0 0,13.8797333 0,31 C0,48.1213 13.8787,62 31,62 C48.1202667,62 62,48.1213 62,31 C62,13.8797333 48.1202667,0 31,0 L31,0 Z" id="Fill-1"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="#85b649" />
</svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
</style>
<path class="st0" d="M0,0h20v20H0V0z"/>
<g id="AMP-Warning" fill="#84b741">
<path d="M10,1c-5,0-9,4.1-9,9s4.1,9,9,9s9-4.1,9-9S15,1,10,1z M10.9,14.6H9.1v-1.8H11v1.8H10.9z M10.9,10.9H9.1V5.4H11v5.4H10.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
</style>
<path class="st0" d="M0,0h20v20H0V0z"/>
<g id="AMP-Warning" fill="#FF0000">
<path d="M10,1c-5,0-9,4.1-9,9s4.1,9,9,9s9-4.1,9-9S15,1,10,1z M10.9,14.6H9.1v-1.8H11v1.8H10.9z M10.9,10.9H9.1V5.4H11v5.4H10.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
</style>
<path class="st0" d="M0,0h20v20H0V0z"/>
<g id="AMP-Warning" fill="#E38000">
<path d="M10,1c-5,0-9,4.1-9,9s4.1,9,9,9s9-4.1,9-9S15,1,10,1z M10.9,14.6H9.1v-1.8H11v1.8H10.9z M10.9,10.9H9.1V5.4H11v5.4H10.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -0,0 +1,3 @@
<svg height="1024" width="767.5" xmlns="http://www.w3.org/2000/svg">
<path d="M0 384l383.75 383.75L767.5 384H0z" fill="#0073aa" />
</svg>

After

Width:  |  Height:  |  Size: 140 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="0" fill="none" width="20" height="20"/><g fill="#FF0E57"><path d="M17 10c0-3.87-3.14-7-7-7-3.87 0-7 3.13-7 7s3.13 7 7 7c3.86 0 7-3.13 7-7zm-6.3 1.48H9.14v-.43c0-.38.08-.7.24-.98s.46-.57.88-.89c.41-.29.68-.53.81-.71.14-.18.2-.39.2-.62 0-.25-.09-.44-.28-.58-.19-.13-.45-.19-.79-.19-.58 0-1.25.19-2 .57l-.64-1.28c.87-.49 1.8-.74 2.77-.74.81 0 1.45.2 1.92.58.48.39.71.91.71 1.55 0 .43-.09.8-.29 1.11-.19.32-.57.67-1.11 1.06-.38.28-.61.49-.71.63-.1.15-.15.34-.15.57v.35zm-1.47 2.74c-.18-.17-.27-.42-.27-.73 0-.33.08-.58.26-.75s.43-.25.77-.25c.32 0 .57.09.75.26s.27.42.27.74c0 .3-.09.55-.27.72-.18.18-.43.27-.75.27-.33 0-.58-.09-.76-.26z"/></g></svg>

After

Width:  |  Height:  |  Size: 713 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="0" fill="none" width="20" height="20"/><g fill="#FF0000"><path d="M12.12 10l3.53 3.53-2.12 2.12L10 12.12l-3.54 3.54-2.12-2.12L7.88 10 4.34 6.46l2.12-2.12L10 7.88l3.54-3.53 2.12 2.12z"/></g></svg>

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

View File

@ -0,0 +1,37 @@
/**
* Adds an admin pointer that describes new features in 1.0.
*/
/* exported ampAdminPointer */
/* global ajaxurl, jQuery */
var ampAdminPointer = ( function( $ ) { // eslint-disable-line no-unused-vars
'use strict';
return {
/**
* Loads the pointer.
*
* @param {Object} data - Module data.
* @return {void}
*/
load: function load( data ) {
var options = $.extend(
data.pointer.options,
{
/**
* Makes a POST request to store the pointer ID as dismissed for this user.
*/
close: function() {
$.post( ajaxurl, {
pointer: data.pointer.pointer_id,
action: 'dismiss-wp-pointer'
} );
}
}
);
$( data.pointer.target ).pointer( options ).pointer( 'open' );
}
};
}( jQuery ) );

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,534 @@
/**
* Validates blocks for AMP compatibility.
*
* This uses the REST API response from saving a page to find validation errors.
* If one exists for a block, it display it inline with a Notice component.
*/
/* exported ampBlockValidation */
/* global wp, _ */
var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars
'use strict';
var module = {
/**
* Data exported from server.
*
* @param {Object}
*/
data: {
i18n: {},
ampValidityRestField: '',
isSanitizationAutoAccepted: false
},
/**
* Name of the store.
*
* @param {string}
*/
storeName: 'amp/blockValidation',
/**
* Holds the last states which are used for comparisons.
*
* @param {Object}
*/
lastStates: {
noticesAreReset: false,
validationErrors: [],
blockOrder: [],
blockValidationErrors: {}
},
/**
* Boot module.
*
* @param {Object} data - Module data.
* @return {void}
*/
boot: function boot( data ) {
module.data = data;
wp.i18n.setLocaleData( module.data.i18n, 'amp' );
wp.hooks.addFilter(
'editor.BlockEdit',
'amp/add-notice',
module.conditionallyAddNotice,
99 // eslint-disable-line
);
module.store = module.registerStore();
wp.data.subscribe( module.handleValidationErrorsStateChange );
},
/**
* Register store.
*
* @return {Object} Store.
*/
registerStore: function registerStore() {
return wp.data.registerStore( module.storeName, {
reducer: function( _state, action ) {
var state = _state || {
blockValidationErrorsByClientId: {}
};
switch ( action.type ) {
case 'UPDATE_BLOCKS_VALIDATION_ERRORS':
return _.extend( {}, state, {
blockValidationErrorsByClientId: action.blockValidationErrorsByClientId
} );
default:
return state;
}
},
actions: {
updateBlocksValidationErrors: function( blockValidationErrorsByClientId ) {
return {
type: 'UPDATE_BLOCKS_VALIDATION_ERRORS',
blockValidationErrorsByClientId: blockValidationErrorsByClientId
};
}
},
selectors: {
getBlockValidationErrors: function( state, clientId ) {
return state.blockValidationErrorsByClientId[ clientId ] || [];
}
}
} );
},
/**
* Checks if AMP is enabled for this post.
*
* @return {boolean} Returns true when the AMP toggle is on; else, false is returned.
*/
isAMPEnabled: function isAMPEnabled() {
var meta = wp.data.select( 'core/editor' ).getEditedPostAttribute( 'meta' );
if ( meta && meta.amp_status && window.wpAmpEditor.possibleStati.includes( meta.amp_status ) ) {
return 'enabled' === meta.amp_status;
}
return window.wpAmpEditor.defaultStatus;
},
/**
* Checks if the validate errors state change handler should wait before processing.
*
* @return {boolean} Whether should wait.
*/
waitToHandleStateChange: function waitToHandleStateChange() {
var currentPost;
// @todo Gutenberg currently is not persisting isDirty state if changes are made during save request. Block order mismatch.
// We can only align block validation errors with blocks in editor when in saved state, since only here will the blocks be aligned with the validation errors.
if ( wp.data.select( 'core/editor' ).isEditedPostDirty() || ( ! wp.data.select( 'core/editor' ).isEditedPostDirty() && wp.data.select( 'core/editor' ).isEditedPostNew() ) ) {
return true;
}
// Wait for the current post to be set up.
currentPost = wp.data.select( 'core/editor' ).getCurrentPost();
if ( ! currentPost.hasOwnProperty( 'id' ) ) {
return true;
}
return false;
},
/**
* Handle state change regarding validation errors.
*
* This is essentially a JS implementation of \AMP_Validation_Manager::print_edit_form_validation_status() in PHP.
*
* @return {void}
*/
handleValidationErrorsStateChange: function handleValidationErrorsStateChange() {
var currentPost, validationErrors, blockValidationErrors, noticeOptions, noticeMessage, blockErrorCount, ampValidity, rejectedErrors;
if ( ! module.isAMPEnabled() ) {
if ( ! module.lastStates.noticesAreReset ) {
module.lastStates.validationErrors = [];
module.lastStates.noticesAreReset = true;
module.resetWarningNotice();
module.resetBlockNotices();
}
return;
}
if ( module.waitToHandleStateChange() ) {
return;
}
currentPost = wp.data.select( 'core/editor' ).getCurrentPost();
ampValidity = currentPost[ module.data.ampValidityRestField ] || {};
// Show all validation errors which have not been explicitly acknowledged as accepted.
validationErrors = _.map(
_.filter( ampValidity.results, function( result ) {
// @todo Show VALIDATION_ERROR_ACK_REJECTED_STATUS differently since moderated?
return (
0 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS */ === result.status ||
1 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS */ === result.status ||
2 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_REJECTED_STATUS */ === result.status // eslint-disable-line no-magic-numbers
);
} ),
function( result ) {
return result.error;
}
);
// Short-circuit if there was no change to the validation errors.
if ( ! module.didValidationErrorsChange( validationErrors ) ) {
if ( ! validationErrors.length && ! module.lastStates.noticesAreReset ) {
module.lastStates.noticesAreReset = true;
module.resetWarningNotice();
}
return;
}
module.lastStates.validationErrors = validationErrors;
module.lastStates.noticesAreReset = false;
// Remove any existing notice.
module.resetWarningNotice();
noticeMessage = wp.i18n.sprintf(
/* translators: %s: number of issues */
wp.i18n._n(
'There is %s issue from AMP validation which needs review.',
'There are %s issues from AMP validation which need review.',
validationErrors.length,
'amp'
),
validationErrors.length
);
try {
blockValidationErrors = module.getBlocksValidationErrors();
module.lastStates.blockValidationErrors = blockValidationErrors.byClientId;
wp.data.dispatch( module.storeName ).updateBlocksValidationErrors( blockValidationErrors.byClientId );
blockErrorCount = validationErrors.length - blockValidationErrors.other.length;
if ( blockErrorCount > 0 ) {
noticeMessage += ' ' + wp.i18n.sprintf(
/* translators: %s: number of block errors. */
wp.i18n._n(
'%s issue is directly due to content here.',
'%s issues are directly due to content here.',
blockErrorCount,
'amp'
),
blockErrorCount
);
} else if ( validationErrors.length === 1 ) {
noticeMessage += ' ' + wp.i18n.__( 'The issue is not directly due to content here.', 'amp' );
} else {
noticeMessage += ' ' + wp.i18n.__( 'The issues are not directly due to content here.', 'amp' );
}
} catch ( e ) {
// Clear out block validation errors in case the block sand errors cannot be aligned.
module.resetBlockNotices();
if ( validationErrors.length === 1 ) {
noticeMessage += ' ' + wp.i18n.__( 'The issue may not be due to content here', 'amp' );
} else {
noticeMessage += ' ' + wp.i18n.__( 'Some issues may be due to content here.', 'amp' );
}
}
rejectedErrors = _.filter( ampValidity.results, function( result ) {
return (
0 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS */ === result.status ||
2 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_REJECTED_STATUS */ === result.status // eslint-disable-line no-magic-numbers
);
} );
noticeMessage += ' ';
// Auto-acceptance is from either checking 'Automatically accept sanitization...' or from being in Native mode.
if ( module.data.isSanitizationAutoAccepted ) {
if ( 0 === rejectedErrors.length ) {
noticeMessage += wp.i18n.__( 'However, your site is configured to automatically accept sanitization of the offending markup.', 'amp' );
} else {
noticeMessage += wp.i18n._n(
'Your site is configured to automatically accept sanitization errors, but this error could be from when auto-acceptance was not selected, or from manually rejecting an error.',
'Your site is configured to automatically accept sanitization errors, but these errors could be from when auto-acceptance was not selected, or from manually rejecting an error.',
validationErrors.length,
'amp'
);
}
} else {
noticeMessage += wp.i18n.__( 'Non-accepted validation errors prevent AMP from being served, and the user will be redirected to the non-AMP version.', 'amp' );
}
noticeOptions = {
id: 'amp-errors-notice'
};
if ( ampValidity.review_link ) {
noticeOptions.actions = [
{
label: wp.i18n.__( 'Review issues', 'amp' ),
url: ampValidity.review_link
}
];
}
// Display notice if there were validation errors.
if ( validationErrors.length > 0 ) {
wp.data.dispatch( 'core/notices' ).createNotice( 'warning', noticeMessage, noticeOptions );
}
module.validationWarningNoticeId = noticeOptions.id;
},
/**
* Checks if the validation errors have changed.
*
* @param {Object[]} validationErrors A list of validation errors.
* @return {boolean|*} Returns true when the validation errors change.
*/
didValidationErrorsChange: function didValidationErrorsChange( validationErrors ) {
if ( module.areBlocksOutOfSync() ) {
module.lastStates.validationErrors = [];
}
return (
module.lastStates.validationErrors.length !== validationErrors.length ||
( validationErrors && ! _.isEqual( module.lastStates.validationErrors, validationErrors ) )
);
},
/**
* Checks if the block order is out of sync.
*
* Block change on page load and can get out of sync during normal editing and saving processes. This method gives a check to determine if an "out of sync" condition occurred.
*
* @return {boolean} Whether out of sync.
*/
areBlocksOutOfSync: function areBlocksOutOfSync() {
var blockOrder = wp.data.select( 'core/editor' ).getBlockOrder();
if ( module.lastStates.blockOrder.length !== blockOrder.length || ! _.isEqual( module.lastStates.blockOrder, blockOrder ) ) {
module.lastStates.blockOrder = blockOrder;
return true;
}
return false;
},
/**
* Resets the validation warning notice.
*
* @return {void}
*/
resetWarningNotice: function resetWarningNotice() {
if ( module.validationWarningNoticeId ) {
wp.data.dispatch( 'core/notices' ).removeNotice( module.validationWarningNoticeId );
module.validationWarningNoticeId = null;
}
},
/**
* Resets the block level validation errors.
*
* @return {void}
*/
resetBlockNotices: function resetBlockNotices() {
wp.data.dispatch( module.storeName ).updateBlocksValidationErrors( {} );
},
/**
* Get flattened block order.
*
* @param {Object[]} blocks - List of blocks which maty have nested blocks inside them.
* @return {string[]} Block IDs in flattened order.
*/
getFlattenedBlockOrder: function getFlattenedBlockOrder( blocks ) {
var blockOrder = [];
_.each( blocks, function( block ) {
blockOrder.push( block.clientId );
if ( block.innerBlocks.length > 0 ) {
Array.prototype.push.apply( blockOrder, module.getFlattenedBlockOrder( block.innerBlocks ) );
}
} );
return blockOrder;
},
/**
* Update blocks' validation errors in the store.
*
* @return {Object} Validation errors grouped by block ID other ones.
*/
getBlocksValidationErrors: function getBlocksValidationErrors() {
var acceptedStatus, blockValidationErrorsByClientId, editorSelect, currentPost, blockOrder, validationErrors, otherValidationErrors;
acceptedStatus = 3; // eslint-disable-line no-magic-numbers
editorSelect = wp.data.select( 'core/editor' );
currentPost = editorSelect.getCurrentPost();
validationErrors = _.map(
_.filter( currentPost[ module.data.ampValidityRestField ].results, function( result ) {
return result.term_status !== acceptedStatus; // If not accepted by the user.
} ),
function( result ) {
return result.error;
}
);
blockOrder = module.getFlattenedBlockOrder( editorSelect.getBlocks() );
otherValidationErrors = [];
blockValidationErrorsByClientId = {};
_.each( blockOrder, function( clientId ) {
blockValidationErrorsByClientId[ clientId ] = [];
} );
_.each( validationErrors, function( validationError ) {
var i, source, clientId, block, matched;
if ( ! validationError.sources ) {
otherValidationErrors.push( validationError );
return;
}
// Find the inner-most nested block source only; ignore any nested blocks.
matched = false;
for ( i = validationError.sources.length - 1; 0 <= i; i-- ) {
source = validationError.sources[ i ];
// Skip sources that are not for blocks.
if ( ! source.block_name || _.isUndefined( source.block_content_index ) || currentPost.id !== source.post_id ) {
continue;
}
// Look up the block ID by index, assuming the blocks of content in the editor are the same as blocks rendered on frontend.
clientId = blockOrder[ source.block_content_index ];
if ( _.isUndefined( clientId ) ) {
throw new Error( 'undefined_block_index' );
}
// Sanity check that block exists for clientId.
block = editorSelect.getBlock( clientId );
if ( ! block ) {
throw new Error( 'block_lookup_failure' );
}
// Check the block type in case a block is dynamically added/removed via the_content filter to cause alignment error.
if ( block.name !== source.block_name ) {
throw new Error( 'ordered_block_alignment_mismatch' );
}
blockValidationErrorsByClientId[ clientId ].push( validationError );
matched = true;
// Stop looking for sources, since we aren't looking for parent blocks.
break;
}
if ( ! matched ) {
otherValidationErrors.push( validationError );
}
} );
return {
byClientId: blockValidationErrorsByClientId,
other: otherValidationErrors
};
},
/**
* Get message for validation error.
*
* @param {Object} validationError - Validation error.
* @param {string} validationError.code - Validation error code.
* @param {string} [validationError.node_name] - Node name.
* @param {string} [validationError.message] - Validation error message.
* @return {wp.element.Component[]|string[]} Validation error message.
*/
getValidationErrorMessage: function getValidationErrorMessage( validationError ) {
if ( validationError.message ) {
return validationError.message;
}
if ( 'invalid_element' === validationError.code && validationError.node_name ) {
return [
wp.i18n.__( 'Invalid element: ' ),
wp.element.createElement( 'code', { key: 'name' }, validationError.node_name )
];
} else if ( 'invalid_attribute' === validationError.code && validationError.node_name ) {
return [
wp.i18n.__( 'Invalid attribute: ' ),
wp.element.createElement( 'code', { key: 'name' }, validationError.parent_name ? wp.i18n.sprintf( '%s[%s]', validationError.parent_name, validationError.node_name ) : validationError.node_name )
];
}
return [
wp.i18n.__( 'Error code: ', 'amp' ),
wp.element.createElement( 'code', { key: 'name' }, validationError.code || wp.i18n.__( 'unknown' ) )
];
},
/**
* Wraps the edit() method of a block, and conditionally adds a Notice.
*
* @param {Function} BlockEdit - The original edit() method of the block.
* @return {Function} The edit() method, conditionally wrapped in a notice for AMP validation error(s).
*/
conditionallyAddNotice: function conditionallyAddNotice( BlockEdit ) {
return function( ownProps ) {
var validationErrors,
mergedProps;
function AmpNoticeBlockEdit( props ) {
var edit, details;
edit = wp.element.createElement(
BlockEdit,
props
);
if ( 0 === props.ampBlockValidationErrors.length ) {
return edit;
}
details = wp.element.createElement( 'details', { className: 'amp-block-validation-errors' }, [
wp.element.createElement( 'summary', { key: 'summary', className: 'amp-block-validation-errors__summary' }, wp.i18n.sprintf(
wp.i18n._n(
'There is %s issue from AMP validation.',
'There are %s issues from AMP validation.',
props.ampBlockValidationErrors.length,
'amp'
),
props.ampBlockValidationErrors.length
) ),
wp.element.createElement(
'ul',
{ key: 'list', className: 'amp-block-validation-errors__list' },
_.map( props.ampBlockValidationErrors, function( error, key ) {
return wp.element.createElement( 'li', { key: key }, module.getValidationErrorMessage( error ) );
} )
)
] );
return wp.element.createElement(
wp.element.Fragment, {},
wp.element.createElement(
wp.components.Notice,
{
status: 'warning',
isDismissible: false
},
details
),
edit
);
}
if ( ! module.lastStates.blockValidationErrors[ ownProps.clientId ] ) {
validationErrors = wp.data.select( module.storeName ).getBlockValidationErrors( ownProps.clientId );
module.lastStates.blockValidationErrors[ ownProps.clientId ] = validationErrors;
}
mergedProps = _.extend( {}, ownProps, {
ampBlockValidationErrors: module.lastStates.blockValidationErrors[ ownProps.clientId ]
} );
return AmpNoticeBlockEdit( mergedProps );
};
}
};
return module;
}() );

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,350 @@
/* exported ampCustomizeControls */
/* eslint no-magic-numbers: [ "error", { "ignore": [ 0, 1, 250] } ] */
var ampCustomizeControls = ( function( api, $ ) { // eslint-disable-line no-unused-vars
'use strict';
var component = {
data: {
queryVar: 'amp',
panelId: '',
ampUrl: '',
l10n: {
unavailableMessage: '',
unavailableLinkText: ''
}
},
tooltipTimeout: 5000,
tooltipVisible: new api.Value( false ),
tooltipFocused: new api.Value( 0 )
};
/**
* Boot using data sent inline.
*
* @param {Object} data Object data.
* @return {void}
*/
component.boot = function boot( data ) {
component.data = data;
function initPanel() {
api.panel( component.data.panelId, component.panelReady );
}
if ( api.state ) {
component.addState();
api.bind( 'ready', initPanel );
} else { // WP<4.9.
api.bind( 'ready', function() {
component.addState(); // Needed for WP<4.9.
initPanel();
} );
}
};
/**
* Add state for AMP.
*
* @return {void}
*/
component.addState = function addState() {
api.state.add( 'ampEnabled', new api.Value( false ) );
api.state.add( 'ampAvailable', new api.Value( false ) );
};
/**
* Check if the URL is AMPified.
*
* @param {string} url URL.
* @return {boolean} whether it is an AMP URL.
*/
component.isAmpUrl = function isAmpUrl( url ) {
var urlParser = document.createElement( 'a' ),
regexEndpoint = new RegExp( '\\/' + component.data.queryVar + '\\/?$' );
urlParser.href = url;
if ( ! _.isUndefined( wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) )[ component.data.queryVar ] ) ) {
return true;
}
return regexEndpoint.test( urlParser.pathname );
};
/**
* Create an non-AMP version of a URL.
*
* @param {string} url URL.
* @return {string} non-AMPified URL.
*/
component.unampifyUrl = function unampifyUrl( url ) {
var urlParser = document.createElement( 'a' ),
regexEndpoint = new RegExp( '\\/' + component.data.queryVar + '\\/?$' ),
params;
urlParser.href = url;
urlParser.pathname = urlParser.pathname.replace( regexEndpoint, '' );
if ( 1 < urlParser.search.length ) {
params = wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) );
delete params[ component.data.queryVar ];
urlParser.search = $.param( params );
}
return urlParser.href;
};
/**
* Create an AMP version of a URL.
*
* @param {string} url URL.
* @return {string} AMPified URL.
*/
component.ampifyUrl = function ampifyUrl( url ) {
var urlParser = document.createElement( 'a' );
urlParser.href = component.unampifyUrl( url );
if ( urlParser.search.length ) {
urlParser.search += '&';
}
urlParser.search += component.data.queryVar + '=1';
return urlParser.href;
};
/**
* Try to close the tooltip after a given timeout.
*
* @return {void}
*/
component.tryToCloseTooltip = function tryToCloseTooltip() {
clearTimeout( component.tooltipTimeoutId );
component.tooltipTimeoutId = setTimeout( function() {
if ( ! component.tooltipVisible.get() ) {
return;
}
if ( 0 < component.tooltipFocused.get() ) {
component.tryToCloseTooltip();
} else {
component.tooltipVisible.set( false );
}
}, component.tooltipTimeout );
};
/**
* Make current URL AMPified if toggle is on.
*
* @param {string} url URL.
* @return {string} AMPified URL.
*/
component.setCurrentAmpUrl = function setCurrentAmpUrl( url ) {
var enabled = api.state( 'ampEnabled' ).get();
if ( ! enabled && component.isAmpUrl( url ) ) {
return component.unampifyUrl( url );
} else if ( enabled && ! component.isAmpUrl( url ) ) {
return component.ampifyUrl( url );
}
return url;
};
/**
* Swap to AMP version of URL in preview.
*
* @return {void}
*/
component.updatePreviewUrl = function updatePreviewUrl() {
api.previewer.previewUrl.set( component.setCurrentAmpUrl( api.previewer.previewUrl.get() ) );
};
/**
* Enable AMP and navigate to the given URL.
*
* @param {string} url - URL.
* @return {void}
*/
component.enableAndNavigateToUrl = function enableAndNavigateToUrl( url ) {
api.state( 'ampEnabled' ).set( true );
api.previewer.previewUrl.set( url );
};
/**
* Update panel notifications.
*
* @return {void}
*/
component.updatePanelNotifications = function updatePanelNotifications() {
var panel = api.panel( component.data.panelId ),
containers;
containers = panel.sections().concat( [ panel ] );
if ( api.state( 'ampAvailable' ).get() ) {
_.each( containers, function( container ) {
container.notifications.remove( 'amp_unavailable' );
} );
} else {
_.each( containers, function( container ) {
container.notifications.add( new api.Notification( 'amp_unavailable', {
message: component.data.l10n.unavailableMessage,
type: 'info',
linkText: component.data.l10n.unavailableLinkText,
url: component.data.ampUrl,
templateId: 'customize-amp-unavailable-notification',
render: function() {
var li = api.Notification.prototype.render.call( this );
li.find( 'a' ).on( 'click', function( event ) {
event.preventDefault();
component.enableAndNavigateToUrl( this.href );
} );
return li;
}
} ) );
} );
}
};
/**
* Hook up all AMP preview interactions once panel is ready.
*
* @param {wp.customize.Panel} panel The AMP panel.
* @return {void}
*/
component.panelReady = function panelReady( panel ) {
var ampToggleContainer, checkbox, tooltip, tooltipLink;
ampToggleContainer = $( wp.template( 'customize-amp-enabled-toggle' )( {
message: component.data.l10n.unavailableMessage,
linkText: component.data.l10n.unavailableLinkText,
url: component.data.ampUrl
} ) );
checkbox = ampToggleContainer.find( 'input[type=checkbox]' );
tooltip = ampToggleContainer.find( '.tooltip' );
tooltipLink = tooltip.find( 'a' );
// AMP panel triggers the input toggle for AMP preview.
panel.expanded.bind( function( expanded ) {
if ( ! expanded ) {
return;
}
if ( api.state( 'ampAvailable' ).get() ) {
api.state( 'ampEnabled' ).set( panel.expanded.get() );
} else if ( ! panel.notifications ) {
/*
* This is only done if panel notifications aren't supported.
* If they are (as of 4.9) then a notification will be shown
* in the panel and its sections when AMP is not available.
*/
setTimeout( function() {
component.tooltipVisible.set( true );
}, 250 );
}
} );
if ( panel.notifications ) {
api.state( 'ampAvailable' ).bind( component.updatePanelNotifications );
component.updatePanelNotifications();
api.section.bind( 'add', component.updatePanelNotifications );
}
// Enable AMP toggle if available and mobile device selected.
api.previewedDevice.bind( function( device ) {
if ( api.state( 'ampAvailable' ).get() ) {
api.state( 'ampEnabled' ).set( 'mobile' === device );
}
} );
// Message coming from previewer.
api.previewer.bind( 'amp-status', function( data ) {
api.state( 'ampAvailable' ).set( data.available );
} );
function setInitialAmpEnabledState( data ) {
api.state( 'ampEnabled' ).set( data.enabled );
api.previewer.unbind( 'amp-status', setInitialAmpEnabledState );
}
api.previewer.bind( 'amp-status', setInitialAmpEnabledState );
/*
* Persist the presence or lack of the amp=1 param when navigating in the preview,
* even if current page is not yet supported.
*/
api.previewer.previewUrl.validate = ( function( prevValidate ) {
return function( value ) {
var val = prevValidate.call( this, value );
if ( val ) {
val = component.setCurrentAmpUrl( val );
}
return val;
};
}( api.previewer.previewUrl.validate ) );
// Listen for ampEnabled state changes.
api.state( 'ampEnabled' ).bind( function( enabled ) {
checkbox.prop( 'checked', enabled );
component.updatePreviewUrl();
} );
// Listen for ampAvailable state changes.
api.state( 'ampAvailable' ).bind( function( available ) {
checkbox.toggleClass( 'disabled', ! available );
// Show the unavailable tooltip if AMP is enabled.
if ( api.state( 'ampEnabled' ).get() ) {
component.tooltipVisible.set( ! available );
}
} );
// Adding checkbox toggle before device selection.
$( '.devices-wrapper' ).before( ampToggleContainer );
// User clicked link within tooltip, go to linked post in preview.
tooltipLink.on( 'click', function( event ) {
event.preventDefault();
component.enableAndNavigateToUrl( this.href );
} );
// Toggle visibility of tooltip based on tooltipVisible state.
component.tooltipVisible.bind( function( visible ) {
tooltip.attr( 'aria-hidden', visible ? 'false' : 'true' );
if ( visible ) {
$( document ).on( 'click.amp-toggle-outside', function( event ) {
if ( ! $.contains( ampToggleContainer[ 0 ], event.target ) ) {
component.tooltipVisible.set( false );
}
} );
tooltip.fadeIn();
component.tryToCloseTooltip();
} else {
tooltip.fadeOut();
component.tooltipFocused.set( 0 );
$( document ).off( 'click.amp-toggle-outside' );
}
} );
// Handle click on checkbox to either enable the AMP preview or show the tooltip.
checkbox.on( 'click', function() {
this.checked = ! this.checked; // Undo what we just did, since state is managed in ampAvailable change handler.
if ( api.state( 'ampAvailable' ).get() ) {
api.state( 'ampEnabled' ).set( ! api.state( 'ampEnabled' ).get() );
} else {
component.tooltipVisible.set( true );
}
} );
// Keep track of the user's state interacting with the tooltip.
tooltip.on( 'mouseenter', function() {
if ( ! api.state( 'ampAvailable' ).get() ) {
component.tooltipVisible.set( true );
}
component.tooltipFocused.set( component.tooltipFocused.get() + 1 );
} );
tooltip.on( 'mouseleave', function() {
component.tooltipFocused.set( component.tooltipFocused.get() - 1 );
} );
tooltipLink.on( 'focus', function() {
if ( ! api.state( 'ampAvailable' ).get() ) {
component.tooltipVisible.set( true );
}
component.tooltipFocused.set( component.tooltipFocused.get() + 1 );
} );
tooltipLink.on( 'blur', function() {
component.tooltipFocused.set( component.tooltipFocused.get() - 1 );
} );
};
return component;
}( wp.customize, jQuery ) );

View File

@ -0,0 +1,25 @@
/* exported ampCustomizePreview */
var ampCustomizePreview = ( function( api ) { // eslint-disable-line no-unused-vars
'use strict';
var component = {};
/**
* Boot using data sent inline.
*
* @param {Object} data - PHP exports.
* @param {boolean} data.available - Whether AMP is available.
* @param {boolean} data.enabled - Whether AMP is enabled.
* @return {void}
*/
component.boot = function boot( data ) {
api.bind( 'preview-ready', function() {
api.preview.bind( 'active', function() {
api.preview.send( 'amp-status', data );
} );
} );
};
return component;
}( wp.customize ) );

View File

@ -0,0 +1,48 @@
/* global amp_customizer_design, console */
( function( $ ) {
'use strict';
// Nav bar text color.
wp.customize( 'amp_customizer[header_color]', function( value ) {
value.bind( function( to ) {
$( '.amp-wp-header a' ).css( 'color', to );
$( '.amp-wp-header div' ).css( 'color', to );
$( '.amp-wp-header .amp-wp-site-icon' ).css( 'border-color', to ).css( 'background-color', to );
} );
} );
// Nav bar background color.
wp.customize( 'amp_customizer[header_background_color]', function( value ) {
value.bind( function( to ) {
$( 'html, .amp-wp-header' ).css( 'background-color', to );
$( '.amp-wp-article a, .amp-wp-article a:visited, .amp-wp-footer a, .amp-wp-footer a:visited' ).css( 'color', to );
$( 'blockquote, .amp-wp-byline amp-img' ).css( 'border-color', to );
} );
} );
// AMP background color scheme.
wp.customize( 'amp_customizer[color_scheme]', function( value ) {
value.bind( function( to ) {
var colors = amp_customizer_design.color_schemes[ to ]; // eslint-disable-line
if ( ! colors ) {
console.error( 'Selected color scheme "%s" not registered.', to ); // eslint-disable-line
return;
}
$( 'body' ).css( 'background-color', colors.theme_color );
$( 'body, a:hover, a:active, a:focus, blockquote, .amp-wp-article, .amp-wp-title' ).css( 'color', colors.text_color );
$( '.amp-wp-meta, .wp-caption .wp-caption-text, .amp-wp-tax-category, .amp-wp-tax-tag, .amp-wp-footer p' ).css( 'color', colors.muted_text_color );
$( '.wp-caption .wp-caption-text, .amp-wp-comments-link a, .amp-wp-footer' ).css( 'border-color', colors.border_color );
$( '.amp-wp-iframe-placeholder, amp-carousel, amp-iframe, amp-youtube, amp-instagram, amp-vine' ).css( 'background-color', colors.border_color );
} );
} );
// Site title.
wp.customize( 'blogname', function( setting ) {
setting.bind( function( title ) {
$( '.amp-wp-header .amp-site-title, .amp-wp-footer h2' ).text( title );
} );
} );
}( jQuery ) );

View File

@ -0,0 +1,847 @@
/* exported ampEditorBlocks */
/* eslint no-magic-numbers: [ "error", { "ignore": [ 1, -1, 0, 4 ] } ] */
var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars
var component, __;
__ = wp.i18n.__;
component = {
/**
* Holds data.
*/
data: {
ampLayoutOptions: [
{
value: 'nodisplay',
label: __( 'No Display', 'amp' ),
notAvailable: [
'core-embed/vimeo',
'core-embed/dailymotion',
'core-embed/hulu',
'core-embed/reddit',
'core-embed/soundcloud'
]
},
{
// Not supported by amp-audio and amp-pixel.
value: 'fixed',
label: __( 'Fixed', 'amp' ),
notAvailable: [
'core-embed/soundcloud'
]
},
{
// To ensure your AMP element displays, you must specify a width and height for the containing element.
value: 'responsive',
label: __( 'Responsive', 'amp' ),
notAvailable: [
'core-embed/soundcloud'
]
},
{
value: 'fixed-height',
label: __( 'Fixed height', 'amp' ),
notAvailable: []
},
{
value: 'fill',
label: __( 'Fill', 'amp' ),
notAvailable: [
'core-embed/soundcloud'
]
},
{
value: 'flex-item',
label: __( 'Flex Item', 'amp' ),
notAvailable: [
'core-embed/soundcloud'
]
},
{
// Not supported by video.
value: 'intrinsic',
label: __( 'Intrinsic', 'amp' ),
notAvailable: [
'core-embed/youtube',
'core-embed/facebook',
'core-embed/instagram',
'core-embed/vimeo',
'core-embed/dailymotion',
'core-embed/hulu',
'core-embed/reddit',
'core-embed/soundcloud'
]
}
],
defaultWidth: 608, // Max-width in the editor.
defaultHeight: 400,
mediaBlocks: [
'core/image',
'core/video'
],
textBlocks: [
'core/paragraph',
'core/heading',
'core/code',
'core/quote',
'core/subhead'
],
ampSettingsLabel: __( 'AMP Settings' ),
fontSizes: {
small: 14,
larger: 48
},
ampPanelLabel: __( 'AMP Settings' )
},
hasThemeSupport: true
};
/**
* Add filters.
*
* @param {Object} data Data.
*/
component.boot = function boot( data ) {
if ( data ) {
_.extend( component.data, data );
}
wp.hooks.addFilter( 'blocks.registerBlockType', 'ampEditorBlocks/addAttributes', component.addAMPAttributes );
wp.hooks.addFilter( 'blocks.getSaveElement', 'ampEditorBlocks/filterSave', component.filterBlocksSave );
wp.hooks.addFilter( 'editor.BlockEdit', 'ampEditorBlocks/filterEdit', component.filterBlocksEdit );
wp.hooks.addFilter( 'blocks.getSaveContent.extraProps', 'ampEditorBlocks/addExtraAttributes', component.addAMPExtraProps );
};
/**
* Check if layout is available for the block.
*
* @param {string} blockName Block name.
* @param {Object} option Layout option object.
* @return {boolean} If is available.
*/
component.isLayoutAvailable = function isLayoutAvailable( blockName, option ) {
return -1 === option.notAvailable.indexOf( blockName );
};
/**
* Get layout options depending on the block.
*
* @param {string} blockName Block name.
* @return {[*]} Options.
*/
component.getLayoutOptions = function getLayoutOptions( blockName ) {
var layoutOptions = [
{
value: '',
label: __( 'Default', 'amp' )
}
];
_.each( component.data.ampLayoutOptions, function( option ) {
if ( component.isLayoutAvailable( blockName, option ) ) {
layoutOptions.push( {
value: option.value,
label: option.label
} );
}
} );
return layoutOptions;
};
/**
* Add extra data-amp-layout attribute to save to DB.
*
* @param {Object} props Properties.
* @param {Object} blockType Block type.
* @param {Object} attributes Attributes.
* @return {Object} Props.
*/
component.addAMPExtraProps = function addAMPExtraProps( props, blockType, attributes ) {
var ampAttributes = {};
// Shortcode props are handled differently.
if ( 'core/shortcode' === blockType.name ) {
return props;
}
// AMP blocks handle layout and other props on their own.
if ( 'amp/' === blockType.name.substr( 0, 4 ) ) {
return props;
}
if ( attributes.ampLayout ) {
ampAttributes[ 'data-amp-layout' ] = attributes.ampLayout;
}
if ( attributes.ampNoLoading ) {
ampAttributes[ 'data-amp-noloading' ] = attributes.ampNoLoading;
}
if ( attributes.ampLightbox ) {
ampAttributes[ 'data-amp-lightbox' ] = attributes.ampLightbox;
}
if ( attributes.ampCarousel ) {
ampAttributes[ 'data-amp-carousel' ] = attributes.ampCarousel;
}
return _.extend( ampAttributes, props );
};
/**
* Add AMP attributes (in this test case just ampLayout) to every core block.
*
* @param {Object} settings Settings.
* @param {string} name Block name.
* @return {Object} Settings.
*/
component.addAMPAttributes = function addAMPAttributes( settings, name ) {
// AMP Carousel settings.
if ( 'core/shortcode' === name || 'core/gallery' === name ) {
if ( ! settings.attributes ) {
settings.attributes = {};
}
settings.attributes.ampCarousel = {
type: 'boolean'
};
settings.attributes.ampLightbox = {
type: 'boolean'
};
}
// Add AMP Lightbox settings.
if ( 'core/image' === name ) {
if ( ! settings.attributes ) {
settings.attributes = {};
}
settings.attributes.ampLightbox = {
type: 'boolean'
};
}
// Fit-text for text blocks.
if ( -1 !== component.data.textBlocks.indexOf( name ) ) {
if ( ! settings.attributes ) {
settings.attributes = {};
}
settings.attributes.ampFitText = {
default: false
};
settings.attributes.minFont = {
default: component.data.fontSizes.small,
source: 'attribute',
selector: 'amp-fit-text',
attribute: 'min-font-size'
};
settings.attributes.maxFont = {
default: component.data.fontSizes.larger,
source: 'attribute',
selector: 'amp-fit-text',
attribute: 'max-font-size'
};
settings.attributes.height = {
default: 50,
source: 'attribute',
selector: 'amp-fit-text',
attribute: 'height'
};
}
// Layout settings for embeds and media blocks.
if ( 0 === name.indexOf( 'core-embed' ) || -1 !== component.data.mediaBlocks.indexOf( name ) ) {
if ( ! settings.attributes ) {
settings.attributes = {};
}
settings.attributes.ampLayout = {
type: 'string'
};
settings.attributes.ampNoLoading = {
type: 'boolean'
};
}
return settings;
};
/**
* Filters blocks edit function of all blocks.
*
* @param {Function} BlockEdit Edit function.
* @return {Function} Edit function.
*/
component.filterBlocksEdit = function filterBlocksEdit( BlockEdit ) {
var el = wp.element.createElement;
return function( props ) {
var attributes = props.attributes,
name = props.name,
ampLayout,
inspectorControls;
ampLayout = attributes.ampLayout;
if ( 'core/shortcode' === name ) {
// Lets remove amp-carousel from edit view.
if ( component.hasGalleryShortcodeCarouselAttribute( attributes.text || '' ) ) {
props.setAttributes( { text: component.removeAmpCarouselFromShortcodeAtts( attributes.text ) } );
}
// Lets remove amp-lightbox from edit view.
if ( component.hasGalleryShortcodeLightboxAttribute( attributes.text || '' ) ) {
props.setAttributes( { text: component.removeAmpLightboxFromShortcodeAtts( attributes.text ) } );
}
inspectorControls = component.setUpShortcodeInspectorControls( props );
if ( '' === inspectorControls ) {
// Return original.
return [
el( BlockEdit, _.extend( {
key: 'original'
}, props ) )
];
}
} else if ( 'core/gallery' === name ) {
inspectorControls = component.setUpGalleryInpsectorControls( props );
} else if ( 'core/image' === name ) {
inspectorControls = component.setUpImageInpsectorControls( props );
} else if ( -1 !== component.data.mediaBlocks.indexOf( name ) || 0 === name.indexOf( 'core-embed/' ) ) {
inspectorControls = component.setUpInspectorControls( props );
} else if ( -1 !== component.data.textBlocks.indexOf( name ) ) {
inspectorControls = component.setUpTextBlocksInspectorControls( props );
}
// Return just inspector controls in case of 'nodisplay'.
if ( ampLayout && 'nodisplay' === ampLayout ) {
return [
inspectorControls
];
}
return [
el( BlockEdit, _.extend( {
key: 'original'
}, props ) ),
inspectorControls
];
};
};
/**
* Set width and height in case of image block.
*
* @param {Object} props Props.
* @param {string} layout Layout.
*/
component.setImageBlockLayoutAttributes = function setImageBlockLayoutAttributes( props, layout ) {
var attributes = props.attributes;
switch ( layout ) {
case 'fixed-height':
if ( ! attributes.height ) {
props.setAttributes( { height: component.data.defaultHeight } );
}
// Lightbox doesn't work with fixed height, so unset it.
if ( attributes.ampLightbox ) {
props.setAttributes( { ampLightbox: false } );
}
break;
case 'fixed':
if ( ! attributes.height ) {
props.setAttributes( { height: component.data.defaultHeight } );
}
if ( ! attributes.width ) {
props.setAttributes( { width: component.data.defaultWidth } );
}
break;
}
};
/**
* Default setup for inspector controls.
*
* @param {Object} props Props.
* @return {Object|Element|*|{$$typeof, type, key, ref, props, _owner}} Inspector Controls.
*/
component.setUpInspectorControls = function setUpInspectorControls( props ) {
var isSelected = props.isSelected,
el = wp.element.createElement,
InspectorControls = wp.editor.InspectorControls,
PanelBody = wp.components.PanelBody;
return isSelected && (
el( InspectorControls, { key: 'inspector' },
el( PanelBody, { title: component.data.ampPanelLabel },
component.getAmpLayoutControl( props ),
component.getAmpNoloadingToggle( props )
)
)
);
};
/**
* Get AMP Layout select control.
*
* @param {Object} props Props.
* @return {Object} Element.
*/
component.getAmpLayoutControl = function getAmpLayoutControl( props ) {
var ampLayout = props.attributes.ampLayout,
el = wp.element.createElement,
SelectControl = wp.components.SelectControl,
name = props.name,
label = __( 'AMP Layout' );
if ( 'core/image' === name ) {
label = __( 'AMP Layout (modifies width/height)' );
}
return el( SelectControl, {
label: label,
value: ampLayout,
options: component.getLayoutOptions( name ),
onChange: function( value ) {
props.setAttributes( { ampLayout: value } );
if ( 'core/image' === props.name ) {
component.setImageBlockLayoutAttributes( props, value );
}
}
} );
};
/**
* Get AMP Noloading toggle control.
*
* @param {Object} props Props.
* @return {Object} Element.
*/
component.getAmpNoloadingToggle = function getAmpNoloadingToggle( props ) {
var ampNoLoading = props.attributes.ampNoLoading,
el = wp.element.createElement,
ToggleControl = wp.components.ToggleControl,
label = __( 'AMP Noloading' );
return el( ToggleControl, {
label: label,
checked: ampNoLoading,
onChange: function() {
props.setAttributes( { ampNoLoading: ! ampNoLoading } );
}
} );
};
/**
* Setup inspector controls for text blocks.
*
* @todo Consider wrapping the render function to delete the original font size in text settings when ampFitText.
*
* @param {Object} props Props.
* @return {Object|Element|*|{$$typeof, type, key, ref, props, _owner}} Inspector Controls.
*/
component.setUpTextBlocksInspectorControls = function setUpInspectorControls( props ) {
var inspectorPanelBodyArgs,
ampFitText = props.attributes.ampFitText,
minFont = props.attributes.minFont,
maxFont = props.attributes.maxFont,
height = props.attributes.height,
isSelected = props.isSelected,
el = wp.element.createElement,
InspectorControls = wp.editor.InspectorControls,
TextControl = wp.components.TextControl,
FontSizePicker = wp.components.FontSizePicker,
ToggleControl = wp.components.ToggleControl,
PanelBody = wp.components.PanelBody,
label = __( 'Use AMP Fit Text' ),
FONT_SIZES = [
{
name: 'small',
shortName: __( 'S' ),
size: 14
},
{
name: 'regular',
shortName: __( 'M' ),
size: 16
},
{
name: 'large',
shortName: __( 'L' ),
size: 36
},
{
name: 'larger',
shortName: __( 'XL' ),
size: 48
}
];
if ( ! isSelected ) {
return null;
}
inspectorPanelBodyArgs = [
PanelBody,
{ title: component.data.ampSettingsLabel, className: ampFitText ? 'is-amp-fit-text' : '' },
el( ToggleControl, {
label: label,
checked: ampFitText,
onChange: function() {
props.setAttributes( { ampFitText: ! ampFitText } );
}
} )
];
if ( ampFitText ) {
inspectorPanelBodyArgs.push.apply( inspectorPanelBodyArgs, [
el( TextControl, {
label: __( 'Height' ),
value: height,
min: 1,
onChange: function( nextHeight ) {
props.setAttributes( { height: nextHeight } );
}
} ),
parseInt( maxFont ) > parseInt( height ) && el(
wp.components.Notice,
{
status: 'error',
isDismissible: false
},
__( 'The height must be greater than the max font size.' )
),
el( PanelBody, { title: __( 'Minimum font size' ) },
el( FontSizePicker, {
fallbackFontSize: 14,
value: minFont,
fontSizes: FONT_SIZES,
onChange: function( nextMinFont ) {
if ( ! nextMinFont ) {
nextMinFont = component.data.fontSizes.small; // @todo Supplying fallbackFontSize should be done automatically by the component?
}
if ( parseInt( nextMinFont ) <= parseInt( maxFont ) ) {
props.setAttributes( { minFont: nextMinFont } );
}
}
} )
),
parseInt( minFont ) > parseInt( maxFont ) && el(
wp.components.Notice,
{
status: 'error',
isDismissible: false
},
__( 'The min font size must less than the max font size.' )
),
el( PanelBody, { title: __( 'Maximum font size' ) },
el( FontSizePicker, {
value: maxFont,
fallbackFontSize: 48,
fontSizes: FONT_SIZES,
onChange: function( nextMaxFont ) {
if ( ! nextMaxFont ) {
nextMaxFont = component.data.fontSizes.larger; // @todo Supplying fallbackFontSize should be done automatically by the component?
}
props.setAttributes( {
maxFont: nextMaxFont,
height: Math.max( nextMaxFont, height )
} );
}
} )
)
] );
}
return (
el( InspectorControls, { key: 'inspector' },
el.apply( null, inspectorPanelBodyArgs )
)
);
};
/**
* Set up inspector controls for shortcode block.
* Adds ampCarousel attribute in case of gallery shortcode.
*
* @param {Object} props Props.
* @return {Object} Inspector controls.
*/
component.setUpShortcodeInspectorControls = function setUpShortcodeInspectorControls( props ) {
var isSelected = props.isSelected,
el = wp.element.createElement,
InspectorControls = wp.editor.InspectorControls,
PanelBody = wp.components.PanelBody;
if ( component.isGalleryShortcode( props.attributes ) ) {
return isSelected && (
el( InspectorControls, { key: 'inspector' },
el( PanelBody, { title: component.data.ampPanelLabel },
component.data.hasThemeSupport && component.getAmpCarouselToggle( props ),
component.getAmpLightboxToggle( props )
)
)
);
}
return '';
};
/**
* Get AMP Lightbox toggle control.
*
* @param {Object} props Props.
* @return {Object} Element.
*/
component.getAmpLightboxToggle = function getAmpLightboxToggle( props ) {
var ampLightbox = props.attributes.ampLightbox,
el = wp.element.createElement,
ToggleControl = wp.components.ToggleControl,
label = __( 'Add lightbox effect' );
return el( ToggleControl, {
label: label,
checked: ampLightbox,
onChange: function( nextValue ) {
props.setAttributes( { ampLightbox: ! ampLightbox } );
if ( nextValue ) {
// Lightbox doesn't work with fixed height, so change.
if ( 'fixed-height' === props.attributes.ampLayout ) {
props.setAttributes( { ampLayout: 'fixed' } );
}
// In case of lightbox set linking images to 'none'.
if ( props.attributes.linkTo && 'none' !== props.attributes.linkTo ) {
props.setAttributes( { linkTo: 'none' } );
}
}
}
} );
};
/**
* Get AMP Carousel toggle control.
*
* @param {Object} props Props.
* @return {Object} Element.
*/
component.getAmpCarouselToggle = function getAmpCarouselToggle( props ) {
var ampCarousel = props.attributes.ampCarousel,
el = wp.element.createElement,
ToggleControl = wp.components.ToggleControl,
label = __( 'Display as carousel' );
return el( ToggleControl, {
label: label,
checked: ampCarousel,
onChange: function() {
props.setAttributes( { ampCarousel: ! ampCarousel } );
}
} );
};
/**
* Set up inspector controls for Image block.
*
* @param {Object} props Props.
* @return {Object} Inspector Controls.
*/
component.setUpImageInpsectorControls = function setUpImageInpsectorControls( props ) {
var isSelected = props.isSelected,
el = wp.element.createElement,
InspectorControls = wp.editor.InspectorControls,
PanelBody = wp.components.PanelBody;
return isSelected && (
el( InspectorControls, { key: 'inspector' },
el( PanelBody, { title: component.data.ampPanelLabel },
component.getAmpLayoutControl( props ),
component.getAmpNoloadingToggle( props ),
component.getAmpLightboxToggle( props )
)
)
);
};
/**
* Set up inspector controls for Gallery block.
* Adds ampCarousel attribute for displaying the output as amp-carousel.
*
* @param {Object} props Props.
* @return {Object} Inspector controls.
*/
component.setUpGalleryInpsectorControls = function setUpGalleryInpsectorControls( props ) {
var isSelected = props.isSelected,
el = wp.element.createElement,
InspectorControls = wp.editor.InspectorControls,
PanelBody = wp.components.PanelBody;
return isSelected && (
el( InspectorControls, { key: 'inspector' },
el( PanelBody, { title: component.data.ampPanelLabel },
component.data.hasThemeSupport && component.getAmpCarouselToggle( props ),
component.getAmpLightboxToggle( props )
)
)
);
};
/**
* Filters blocks' save function.
*
* @param {Object} element Element.
* @param {string} blockType Block type.
* @param {Object} attributes Attributes.
* @return {Object} Output element.
*/
component.filterBlocksSave = function filterBlocksSave( element, blockType, attributes ) {
var text = attributes.text || '',
fitTextProps = {
layout: 'fixed-height',
children: element
};
if ( 'core/shortcode' === blockType.name && component.isGalleryShortcode( attributes ) ) {
if ( ! attributes.ampLightbox ) {
if ( component.hasGalleryShortcodeLightboxAttribute( attributes.text || '' ) ) {
text = component.removeAmpLightboxFromShortcodeAtts( attributes.text );
}
}
if ( attributes.ampCarousel ) {
// If the text contains amp-carousel or amp-lightbox, lets remove it.
if ( component.hasGalleryShortcodeCarouselAttribute( text ) ) {
text = component.removeAmpCarouselFromShortcodeAtts( text );
}
// If lightbox is not set, we can return here.
if ( ! attributes.ampLightbox ) {
if ( attributes.text !== text ) {
return wp.element.createElement(
wp.element.RawHTML,
{},
text
);
}
// Else lets return original.
return element;
}
} else if ( ! component.hasGalleryShortcodeCarouselAttribute( attributes.text || '' ) ) {
// Add amp-carousel=false attribute to the shortcode.
text = attributes.text.replace( '[gallery', '[gallery amp-carousel=false' );
} else {
text = attributes.text;
}
if ( attributes.ampLightbox && ! component.hasGalleryShortcodeLightboxAttribute( text ) ) {
text = text.replace( '[gallery', '[gallery amp-lightbox=true' );
}
if ( attributes.text !== text ) {
return wp.element.createElement(
wp.element.RawHTML,
{},
text
);
}
} else if ( -1 !== component.data.textBlocks.indexOf( blockType.name ) && attributes.ampFitText ) {
if ( attributes.minFont ) {
fitTextProps[ 'min-font-size' ] = attributes.minFont;
}
if ( attributes.maxFont ) {
fitTextProps[ 'max-font-size' ] = attributes.maxFont;
}
if ( attributes.height ) {
fitTextProps.height = attributes.height;
}
return wp.element.createElement( 'amp-fit-text', fitTextProps );
}
return element;
};
/**
* Check if AMP Lightbox is set.
*
* @param {Object} attributes Attributes.
* @return {boolean} If is set.
*/
component.hasAmpLightboxSet = function hasAmpLightboxSet( attributes ) {
return attributes.ampLightbox && false !== attributes.ampLightbox;
};
/**
* Check if AMP Carousel is set.
*
* @param {Object} attributes Attributes.
* @return {boolean} If is set.
*/
component.hasAmpCarouselSet = function hasAmpCarouselSet( attributes ) {
return attributes.ampCarousel && false !== attributes.ampCarousel;
};
/**
* Check if AMP NoLoading is set.
*
* @param {Object} attributes Attributes.
* @return {boolean} If is set.
*/
component.hasAmpNoLoadingSet = function hasAmpNoLoadingSet( attributes ) {
return attributes.ampNoLoading && false !== attributes.ampNoLoading;
};
/**
* Check if AMP Layout is set.
*
* @param {Object} attributes Attributes.
* @return {boolean} If AMP Layout is set.
*/
component.hasAmpLayoutSet = function hasAmpLayoutSet( attributes ) {
return attributes.ampLayout && attributes.ampLayout.length;
};
/**
* Removes amp-carousel=false from attributes.
*
* @param {string} shortcode Shortcode text.
* @return {string} Modified shortcode.
*/
component.removeAmpCarouselFromShortcodeAtts = function removeAmpCarouselFromShortcodeAtts( shortcode ) {
return shortcode.replace( ' amp-carousel=false', '' );
};
/**
* Removes amp-lightbox=true from attributes.
*
* @param {string} shortcode Shortcode text.
* @return {string} Modified shortcode.
*/
component.removeAmpLightboxFromShortcodeAtts = function removeAmpLightboxFromShortcodeAtts( shortcode ) {
return shortcode.replace( ' amp-lightbox=true', '' );
};
/**
* Check if shortcode includes amp-carousel attribute.
*
* @param {string} text Shortcode.
* @return {boolean} If has amp-carousel.
*/
component.hasGalleryShortcodeCarouselAttribute = function hasGalleryShortcodeCarouselAttribute( text ) {
return -1 !== text.indexOf( 'amp-carousel=false' );
};
/**
* Check if shortcode includes amp-lightbox attribute.
*
* @param {string} text Shortcode.
* @return {boolean} If has amp-lightbox.
*/
component.hasGalleryShortcodeLightboxAttribute = function hasGalleryShortcodeLightboxAttribute( text ) {
return -1 !== text.indexOf( 'amp-lightbox=true' );
};
/**
* Check if shortcode is gallery shortcode.
*
* @param {Object} attributes Attributes.
* @return {boolean} If is gallery shortcode.
*/
component.isGalleryShortcode = function isGalleryShortcode( attributes ) {
return attributes.text && -1 !== attributes.text.indexOf( 'gallery' );
};
return component;
}() );

View File

@ -0,0 +1,179 @@
/* exported ampPostMetaBox */
/**
* AMP Post Meta Box.
*
* @todo Rename this to be just the ampEditPostScreen?
*
* @since 0.6
*/
var ampPostMetaBox = ( function( $ ) { // eslint-disable-line no-unused-vars
'use strict';
var component = {
/**
* Holds data.
*
* @since 0.6
*/
data: {
canonical: false, // Overridden by amp_is_canonical().
previewLink: '',
enabled: true, // Overridden by post_supports_amp( $post ).
canSupport: true, // Overridden by count( AMP_Post_Type_Support::get_support_errors( $post ) ) === 0.
statusInputName: '',
l10n: {
ampPreviewBtnLabel: ''
}
},
/**
* Toggle animation speed.
*
* @since 0.6
*/
toggleSpeed: 200,
/**
* Core preview button selector.
*
* @since 0.6
*/
previewBtnSelector: '#post-preview',
/**
* AMP preview button selector.
*
* @since 0.6
*/
ampPreviewBtnSelector: '#amp-post-preview'
};
/**
* Boot plugin.
*
* @since 0.6
* @param {Object} data Object data.
* @return {void}
*/
component.boot = function boot( data ) {
component.data = data;
$( document ).ready( function() {
component.statusRadioInputs = $( '[name="' + component.data.statusInputName + '"]' );
if ( component.data.enabled && ! component.data.canonical ) {
component.addPreviewButton();
}
component.listen();
} );
};
/**
* Events listener.
*
* @since 0.6
* @return {void}
*/
component.listen = function listen() {
$( component.ampPreviewBtnSelector ).on( 'click.amp-post-preview', function( e ) {
e.preventDefault();
component.onAmpPreviewButtonClick();
} );
component.statusRadioInputs.prop( 'disabled', true ); // Prevent cementing setting default status as overridden status.
$( '.edit-amp-status, [href="#amp_status"]' ).click( function( e ) {
e.preventDefault();
component.statusRadioInputs.prop( 'disabled', false );
component.toggleAmpStatus( $( e.target ) );
} );
$( '#submitpost input[type="submit"]' ).on( 'click', function() {
$( component.ampPreviewBtnSelector ).addClass( 'disabled' );
} );
};
/**
* Add AMP Preview button.
*
* @since 0.6
* @return {void}
*/
component.addPreviewButton = function addPreviewButton() {
var previewBtn = $( component.previewBtnSelector );
previewBtn
.clone()
.insertAfter( previewBtn )
.prop( {
href: component.data.previewLink,
id: component.ampPreviewBtnSelector.replace( '#', '' )
} )
.text( component.data.l10n.ampPreviewBtnLabel )
.parent()
.addClass( 'has-amp-preview' );
};
/**
* AMP Preview button click handler.
*
* We trigger the Core preview link for events propagation purposes.
*
* @since 0.6
* @return {void}
*/
component.onAmpPreviewButtonClick = function onAmpPreviewButtonClick() {
var $input;
// Flag the AMP preview referer.
$input = $( '<input>' )
.prop( {
type: 'hidden',
name: 'amp-preview',
value: 'do-preview'
} )
.insertAfter( component.ampPreviewBtnSelector );
// Trigger Core preview button and remove AMP flag.
$( component.previewBtnSelector ).click();
$input.remove();
};
/**
* Add AMP status toggle.
*
* @since 0.6
* @param {Object} $target Event target.
* @return {void}
*/
component.toggleAmpStatus = function toggleAmpStatus( $target ) {
var $container = $( '#amp-status-select' ),
status = $container.data( 'amp-status' ),
$checked,
editAmpStatus = $( '.edit-amp-status' );
// Don't modify status on cancel button click.
if ( ! $target.hasClass( 'button-cancel' ) ) {
status = component.statusRadioInputs.filter( ':checked' ).val();
}
$checked = $( '#amp-status-' + status );
// Toggle elements.
editAmpStatus.fadeToggle( component.toggleSpeed, function() {
if ( editAmpStatus.is( ':visible' ) ) {
editAmpStatus.focus();
} else {
$container.find( 'input[type="radio"]' ).first().focus();
}
} );
$container.slideToggle( component.toggleSpeed );
// Update status.
if ( component.data.canSupport ) {
$container.data( 'amp-status', status );
$checked.prop( 'checked', true );
$( '.amp-status-text' ).text( $checked.next().text() );
}
};
return component;
}( window.jQuery ) );

View File

@ -0,0 +1,9 @@
/* global URLS */
// See AMP_Service_Workers::add_amp_runtime_caching() and <https://github.com/ampproject/amp-by-example/blob/a4d798cac6a534e0c46e78944a2718a8dab3c057/boilerplate-generator/templates/files/serviceworkerJs.js#L9-L22>.
{
self.addEventListener( 'install', event => {
event.waitUntil(
caches.open( wp.serviceWorker.core.cacheNames.runtime ).then( cache => cache.addAll( URLS ) )
);
} );
}

View File

@ -0,0 +1,409 @@
/* exported ampValidatedUrlPostEditScreen */
const ampValidatedUrlPostEditScreen = ( function() { // eslint-disable-line no-unused-vars
let component = {
data: {
l10n: {
unsaved_changes: '',
showing_number_errors: '',
page_heading: '',
show_all: '',
amp_enabled: false
}
}
};
/**
* The id for the 'Showing x of y errors' notice.
*
* @var {string}
*/
component.idNumberErrors = 'number-errors';
/**
* The id for the 'Show all' button.
*
* @var {string}
*/
component.showAllId = 'show-all-errors';
/**
* Boot.
*
* @param {Object} data Data.
* @param {Object} data.l10n Translations.
*/
component.boot = function boot( data ) {
Object.assign( component.data, data );
component.handleShowAll();
component.handleFiltering();
component.handleSearching();
component.handleStatusChange();
component.handleBulkActions();
component.changeHeading();
component.watchForUnsavedChanges();
component.showAMPIconIfEnabled();
};
/**
* Add prompt when leaving page due to unsaved changes.
*/
component.addBeforeUnloadPrompt = function addBeforeUnloadPrompt() {
if ( component.beforeUnloadPromptAdded ) {
return;
}
window.addEventListener( 'beforeunload', component.onBeforeUnload );
// Remove prompt when clicking trash or update.
document.querySelector( '#major-publishing-actions' ).addEventListener( 'click', function() {
window.removeEventListener( 'beforeunload', component.onBeforeUnload );
} );
component.beforeUnloadPromptAdded = true;
};
/**
* Watch for unsaved changes.
*
* Add an beforeunload warning when attempting to leave the page when there are unsaved changes,
* unless the user is pressing the trash link or update button.
*/
component.watchForUnsavedChanges = function watchForUnsavedChanges() {
const onChange = function( event ) {
if ( event.target.matches( 'select' ) ) {
document.getElementById( 'post' ).removeEventListener( 'change', onChange );
component.addBeforeUnloadPrompt();
}
};
document.getElementById( 'post' ).addEventListener( 'change', onChange );
};
/**
* Show message at beforeunload.
*
* @param {Event} event - The beforeunload event.
* @return {string} Message.
*/
component.onBeforeUnload = function onBeforeUnload( event ) {
event.preventDefault();
event.returnValue = component.data.l10n.unsaved_changes;
return component.data.l10n.unsaved_changes;
};
/**
* Updates the <tr> with 'Showing x of y validation errors' at the top of the list table with the current count.
* If this does not exist yet, it creates the element.
*
* @param {number} numberErrorsDisplaying - The number of errors displaying.
* @param {number} totalErrors - The total number of errors, displaying or not.
*/
component.updateShowingErrorsRow = function updateShowingErrorsRow( numberErrorsDisplaying, totalErrors ) {
const showAllButton = document.getElementById( component.showAllId );
let thead, th,
tr = document.getElementById( component.idNumberErrors );
const theadQuery = document.getElementsByTagName( 'thead' );
// Only create the <tr> if it does not exist yet.
if ( theadQuery[ 0 ] && ! tr ) {
thead = theadQuery[ 0 ];
tr = document.createElement( 'tr' );
th = document.createElement( 'th' );
th.setAttribute( 'id', component.idNumberErrors );
th.setAttribute( 'colspan', '6' );
tr.appendChild( th );
thead.appendChild( tr );
}
// If all of the errors are displaying, hide the 'Show all' button and the count notice.
if ( showAllButton && numberErrorsDisplaying === totalErrors ) {
showAllButton.classList.add( 'hidden' );
tr.classList.add( 'hidden' );
} else if ( null !== numberErrorsDisplaying ) {
// Update the number of errors displaying and create a 'Show all' button if it does not exist yet.
document.getElementById( component.idNumberErrors ).innerText = component.data.l10n.showing_number_errors.replace( '%1$s', numberErrorsDisplaying );
document.getElementById( component.idNumberErrors ).classList.remove( 'hidden' );
component.conditionallyCreateShowAllButton();
if ( document.getElementById( component.showAllId ) ) {
document.getElementById( component.showAllId ).classList.remove( 'hidden' );
}
}
};
/**
* Conditionally creates and appends a 'Show all' button.
*/
component.conditionallyCreateShowAllButton = function conditionallyCreateShowAllButton() {
const buttonContainer = document.getElementById( 'url-post-filter' );
let showAllButton = document.getElementById( component.showAllId );
// There is no 'Show all' <button> yet, but there is a container element for it, create the <button>
if ( ! showAllButton && buttonContainer ) {
showAllButton = document.createElement( 'button' );
showAllButton.id = component.showAllId;
showAllButton.classList.add( 'button' );
showAllButton.innerText = component.data.l10n.show_all;
buttonContainer.appendChild( showAllButton );
}
};
/**
* On clicking the 'Show all' <button>, this displays all of the validation errors.
* Then, it hides this 'Show all' <button> and the notice for the number of errors showing.
*/
component.handleShowAll = function handleShowAll() {
const onClick = function( event ) {
const validationErrors = document.querySelectorAll( '[data-error-type]' );
if ( ! event.target.matches( '#' + component.showAllId ) ) {
return;
}
event.preventDefault();
// Iterate through all of the errors, and remove the 'hidden' class.
validationErrors.forEach( function( element ) {
element.parentElement.parentElement.classList.remove( 'hidden' );
} );
/*
* Update the notice to indicate that all of the errors are displaying.
* Like 'Showing 5 of 5 validation errors'.
*/
component.updateShowingErrorsRow( validationErrors.length, validationErrors.length );
// Hide this 'Show all' button.
event.target.classList.add( 'hidden' );
// Change the value of the error type <select> element to 'All Error Types'.
document.getElementById( 'amp_validation_error_type' ).selectedIndex = 0;
};
document.getElementById( 'url-post-filter' ).addEventListener( 'click', onClick );
};
/**
* Handles filtering by error type, triggered by clicking 'Apply Filter'.
*
* Gets the value of the error type <select> element.
* And hides all <tr> elements that do not have the same type of this value.
* If 'All Error Types' is selected, this displays all errors.
*/
component.handleFiltering = function handleFiltering() {
const onChange = function( event ) {
const showAllButton = document.getElementById( component.showAllId );
if ( ! event.target.matches( 'select' ) ) {
return;
}
event.preventDefault();
const isAllErrorTypesSelected = ( '-1' === event.target.value );
const errorTypeQuery = document.querySelectorAll( '[data-error-type]' );
// If the user has chosen 'All Error Types' from the <select>, hide the 'Show all' button.
if ( isAllErrorTypesSelected && showAllButton ) {
showAllButton.classList.add( 'hidden' );
}
/*
* Iterate through all of the <tr> elements in the list table.
* If the error type does not match the value (selected error type), hide them.
*/
let numberErrorsDisplaying = 0;
errorTypeQuery.forEach( function( element ) {
const errorType = element.getAttribute( 'data-error-type' );
// If 'All Error Types' was selected, this should display all errors.
if ( isAllErrorTypesSelected || ! event.target.value || event.target.value === errorType ) {
element.parentElement.parentElement.classList.remove( 'hidden' );
numberErrorsDisplaying++;
} else {
element.parentElement.parentElement.classList.add( 'hidden' );
}
} );
component.updateShowingErrorsRow( numberErrorsDisplaying, errorTypeQuery.length );
};
document.getElementById( 'amp_validation_error_type' ).addEventListener( 'change', onChange );
};
/**
* Handles searching for errors via the <input> and the 'Search Errors' <button>.
*/
component.handleSearching = function handleSearching() {
const onClick = function( event ) {
event.preventDefault();
if ( ! event.target.matches( 'input' ) ) {
return;
}
const searchQuery = document.getElementById( 'invalid-url-search-search-input' ).value;
const detailsQuery = document.querySelectorAll( 'tbody .column-details' );
/*
* Iterate through the 'Details' column of each row.
* If the search query is not present, hide the row.
*/
let numberErrorsDisplaying = 0;
detailsQuery.forEach( function( element ) {
let isSearchQueryPresent = false;
element.querySelectorAll( '.detailed' ).forEach( function( detailed ) {
if ( -1 !== detailed.innerText.indexOf( searchQuery ) ) {
isSearchQueryPresent = true;
}
} );
if ( isSearchQueryPresent ) {
element.parentElement.classList.remove( 'hidden' );
numberErrorsDisplaying++;
} else {
element.parentElement.classList.add( 'hidden' );
}
} );
component.updateShowingErrorsRow( numberErrorsDisplaying, detailsQuery.length );
};
document.getElementById( 'search-submit' ).addEventListener( 'click', onClick );
};
/**
* Update icon for select element.
*
* @param {HTMLSelectElement} select Select element.
*/
component.updateSelectIcon = function updateSelectIcon( select ) {
const newOption = select.options[ select.selectedIndex ];
if ( newOption ) {
const iconSrc = newOption.getAttribute( 'data-status-icon' );
select.parentNode.querySelector( 'img' ).setAttribute( 'src', iconSrc );
}
};
/**
* Handles a change in the error status, like from 'New' to 'Accepted'.
*
* Gets the data-status-icon value from the newly-selected <option>.
* And sets this as the src of the status icon <img>.
*/
component.handleStatusChange = function handleStatusChange() {
const setRowStatusClass = function( { row, select } ) {
const acceptedValue = 3;
const rejectedValue = 2;
const status = parseInt( select.options[ select.selectedIndex ].value );
row.classList.toggle( 'new', isNaN( status ) );
row.classList.toggle( 'accepted', acceptedValue === status );
row.classList.toggle( 'rejected', rejectedValue === status );
};
const onChange = function( { event, row, select } ) {
if ( event.target.matches( 'select' ) ) {
component.updateSelectIcon( event.target );
setRowStatusClass( { row, select } );
}
};
document.querySelectorAll( 'tr[id^="tag-"]' ).forEach( function( row ) {
const select = row.querySelector( '.amp-validation-error-status' );
if ( select ) {
setRowStatusClass( { row, select } );
select.addEventListener( 'change', function( event ) {
onChange( { event, row, select } );
} );
}
} );
};
/**
* On checking a bulk action checkbox, this ensures that the 'Accept' and 'Reject' buttons are present. Handle clicking on buttons.
*
* They're hidden until one of these boxes is checked.
* Also, on unchecking the last checked box, this hides these buttons.
*/
component.handleBulkActions = function handleBulkActions() {
const acceptButton = document.querySelector( 'button.action.accept' );
const rejectButton = document.querySelector( 'button.action.reject' );
const acceptAndRejectContainer = document.getElementById( 'accept-reject-buttons' );
const onChange = function( event ) {
let areThereCheckedBoxes;
if ( ! event.target.matches( '[type=checkbox]' ) ) {
return;
}
if ( event.target.checked ) {
// This checkbox was checked, so ensure the buttons display.
acceptAndRejectContainer.classList.remove( 'hidden' );
} else {
/*
* This checkbox was unchecked.
* So find if there are any other checkboxes that are checked.
* If not, hide the 'Accept' and 'Reject' buttons.
*/
areThereCheckedBoxes = false;
document.querySelectorAll( '.check-column [type=checkbox]' ).forEach( function( element ) {
if ( element.checked ) {
areThereCheckedBoxes = true;
}
} );
if ( ! areThereCheckedBoxes ) {
acceptAndRejectContainer.classList.add( 'hidden' );
}
}
};
document.querySelectorAll( '.check-column [type=checkbox]' ).forEach( function( element ) {
element.addEventListener( 'change', onChange );
} );
// Handle click on accept button.
acceptButton.addEventListener( 'click', function() {
Array.prototype.forEach.call( document.querySelectorAll( 'select.amp-validation-error-status' ), function( select ) {
if ( select.closest( 'tr' ).querySelector( '.check-column input[type=checkbox]' ).checked ) {
select.value = '3';
component.updateSelectIcon( select );
component.addBeforeUnloadPrompt();
}
} );
} );
// Handle click on reject button.
rejectButton.addEventListener( 'click', function() {
Array.prototype.forEach.call( document.querySelectorAll( 'select.amp-validation-error-status' ), function( select ) {
if ( select.closest( 'tr' ).querySelector( '.check-column input[type=checkbox]' ).checked ) {
select.value = '2';
component.updateSelectIcon( select );
component.addBeforeUnloadPrompt();
}
} );
} );
};
/**
* Changes the page heading and document title, as this doesn't look to be possible with a PHP filter.
*/
component.changeHeading = function changeHeading() {
const headingQuery = document.getElementsByClassName( 'wp-heading-inline' );
if ( headingQuery[ 0 ] && component.data.l10n.page_heading ) {
headingQuery[ 0 ].innerText = component.data.l10n.page_heading;
document.title = component.data.l10n.page_heading + document.title;
}
};
/**
* Adds the AMP icon to the page heading if AMP is enabled on this URL.
*/
component.showAMPIconIfEnabled = function() {
const heading = document.querySelector( 'h1.wp-heading-inline' );
if ( heading && true === component.data.l10n.amp_enabled ) {
const ampIcon = document.createElement( 'span' );
ampIcon.classList.add( 'status-text', 'sanitized' );
heading.appendChild( ampIcon );
}
};
return component;
}() );

View File

@ -0,0 +1,34 @@
/* exported ampValidatedUrlsIndex */
const ampValidatedUrlsIndex = ( function() { // eslint-disable-line no-unused-vars
let component = {
classes: {}
};
/**
* The class for the new status
*
* @type {string}
*/
component.classes.new = 'new';
/**
* Boot.
*/
component.boot = function boot() {
component.highlightRowsWithNewStatus();
};
/**
* Highlight rows with new status.
*/
component.highlightRowsWithNewStatus = function highlightRowsWithNewStatus() {
document.querySelectorAll( 'tr[id^="post-"]' ).forEach( function( row ) {
if ( row.querySelector( 'span.status-text.' + component.classes.new ) ) {
row.classList.add( 'new' );
}
} );
};
return component;
}() );

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
// WIP Pointer function
function sourcesPointer() {
jQuery( document ).on( 'click', '.tooltip-button', function() {
jQuery( this ).pointer( {
content: jQuery( this ).next( '.tooltip' ).attr( 'data-content' ),
position: {
edge: 'left',
align: 'center'
},
pointerClass: 'wp-pointer wp-pointer--tooltip'
} ).pointer( 'open' );
} );
}
// Run at DOM ready.
jQuery( sourcesPointer );

View File

@ -0,0 +1 @@
!function(e){var n={};function t(d){if(n[d])return n[d].exports;var o=n[d]={i:d,l:!1,exports:{}};return e[d].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,d){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:d})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var d=Object.create(null);if(t.r(d),Object.defineProperty(d,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(d,o,function(n){return e[n]}.bind(null,o));return d},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=9)}({9:function(module,__webpack_exports__,__webpack_require__){"use strict";eval("__webpack_require__.r(__webpack_exports__);\n\n// CONCATENATED MODULE: ./node_modules/@wordpress/dom-ready/build-module/index.js\n/**\n * Specify a function to execute when the DOM is fully loaded.\n *\n * @param {Function} callback A function to execute after the DOM is ready.\n *\n * @example\n * ```js\n * import domReady from '@wordpress/dom-ready';\n *\n * domReady( function() {\n * \t//do something after DOM loads.\n * } );\n * ```\n *\n * @return {void}\n */\nvar domReady = function domReady(callback) {\n if (document.readyState === 'complete' || // DOMContentLoaded + Images/Styles/etc loaded, so we call directly.\n document.readyState === 'interactive' // DOMContentLoaded fires at this point, so we call directly.\n ) {\n return callback();\n } // DOMContentLoaded has not fired yet, delay callback until then.\n\n\n document.addEventListener('DOMContentLoaded', callback);\n};\n\n/* harmony default export */ var build_module = (domReady);\n//# sourceMappingURL=index.js.map\n// CONCATENATED MODULE: ./assets/src/wp-dom-ready.js\n\n\nif (!window.wp) {\n\twindow.wp = {};\n}\n\nwp.domReady = build_module;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOS5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy8uL25vZGVfbW9kdWxlcy9Ad29yZHByZXNzL2RvbS1yZWFkeS9idWlsZC1tb2R1bGUvaW5kZXguanM/ZGE4MSIsIndlYnBhY2s6Ly8vLi9hc3NldHMvc3JjL3dwLWRvbS1yZWFkeS5qcz8yMTJmIl0sInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogU3BlY2lmeSBhIGZ1bmN0aW9uIHRvIGV4ZWN1dGUgd2hlbiB0aGUgRE9NIGlzIGZ1bGx5IGxvYWRlZC5cbiAqXG4gKiBAcGFyYW0ge0Z1bmN0aW9ufSBjYWxsYmFjayBBIGZ1bmN0aW9uIHRvIGV4ZWN1dGUgYWZ0ZXIgdGhlIERPTSBpcyByZWFkeS5cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBganNcbiAqIGltcG9ydCBkb21SZWFkeSBmcm9tICdAd29yZHByZXNzL2RvbS1yZWFkeSc7XG4gKlxuICogZG9tUmVhZHkoIGZ1bmN0aW9uKCkge1xuICogXHQvL2RvIHNvbWV0aGluZyBhZnRlciBET00gbG9hZHMuXG4gKiB9ICk7XG4gKiBgYGBcbiAqXG4gKiBAcmV0dXJuIHt2b2lkfVxuICovXG52YXIgZG9tUmVhZHkgPSBmdW5jdGlvbiBkb21SZWFkeShjYWxsYmFjaykge1xuICBpZiAoZG9jdW1lbnQucmVhZHlTdGF0ZSA9PT0gJ2NvbXBsZXRlJyB8fCAvLyBET01Db250ZW50TG9hZGVkICsgSW1hZ2VzL1N0eWxlcy9ldGMgbG9hZGVkLCBzbyB3ZSBjYWxsIGRpcmVjdGx5LlxuICBkb2N1bWVudC5yZWFkeVN0YXRlID09PSAnaW50ZXJhY3RpdmUnIC8vIERPTUNvbnRlbnRMb2FkZWQgZmlyZXMgYXQgdGhpcyBwb2ludCwgc28gd2UgY2FsbCBkaXJlY3RseS5cbiAgKSB7XG4gICAgICByZXR1cm4gY2FsbGJhY2soKTtcbiAgICB9IC8vIERPTUNvbnRlbnRMb2FkZWQgaGFzIG5vdCBmaXJlZCB5ZXQsIGRlbGF5IGNhbGxiYWNrIHVudGlsIHRoZW4uXG5cblxuICBkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdET01Db250ZW50TG9hZGVkJywgY2FsbGJhY2spO1xufTtcblxuZXhwb3J0IGRlZmF1bHQgZG9tUmVhZHk7XG4vLyMgc291cmNlTWFwcGluZ1VSTD1pbmRleC5qcy5tYXAiLCJpbXBvcnQgZG9tUmVhZHkgZnJvbSAnQHdvcmRwcmVzcy9kb20tcmVhZHknO1xuXG5pZiAoIXdpbmRvdy53cCkge1xuXHR3aW5kb3cud3AgPSB7fTtcbn1cblxud3AuZG9tUmVhZHkgPSBkb21SZWFkeTsiXSwibWFwcGluZ3MiOiI7OztBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDNUJBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9\n")}});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
<?php
/**
* Functions for managing legacy templates
*
* @package AMP
*/
/**
* Adds hooks to use legacy templates.
*
* If you want to use the template that shipped with v0.3 and earlier, you can use this to force that.
* Note that this may not stick around forever, so use caution and `function_exists`.
*/
function amp_backcompat_use_v03_templates() {
add_filter( 'amp_customizer_is_enabled', '__return_false' );
add_filter( 'amp_post_template_dir', '_amp_backcompat_use_v03_templates_callback', 0 ); // Early in case there are other overrides.
}
/**
* Callback for getting the legacy templates directory.
*
* @access private
*
* @param string $templates Template directory.
* @return string Legacy template directory.
*/
function _amp_backcompat_use_v03_templates_callback( $templates ) {
return AMP__DIR__ . '/back-compat/templates-v0-3';
}

View File

@ -0,0 +1,21 @@
<?php
/**
* Legacy template for the AMP title bar.
*
* @package AMP
*/
$site_icon_url = $this->get( 'site_icon_url' );
?>
<nav class="amp-wp-title-bar">
<div>
<a href="<?php echo esc_url( $this->get( 'home_url' ) ); ?>">
<?php if ( $site_icon_url ) : ?>
<amp-img src="<?php echo esc_url( $site_icon_url ); ?>" width="32" height="32" class="amp-wp-site-icon"></amp-img>
<?php endif; ?>
<?php echo esc_html( $this->get( 'blog_name' ) ); ?>
</a>
</div>
</nav>

View File

@ -0,0 +1,21 @@
<?php
/**
* Legacy template for the AMP author byline.
*
* @package AMP
*/
$post_author = $this->get( 'post_author' );
$avatar_url = get_avatar_url(
$post_author->user_email,
array(
'size' => 24,
)
);
?>
<li class="amp-wp-byline">
<?php if ( function_exists( 'get_avatar_url' ) ) : ?>
<amp-img src="<?php echo esc_url( $avatar_url ); ?>" width="24" height="24" layout="fixed"></amp-img>
<?php endif; ?>
<span class="amp-wp-author"><?php echo esc_html( $post_author->display_name ); ?></span>
</li>

View File

@ -0,0 +1,23 @@
<?php
/**
* Legacy template for the AMP post taxonomy term lists.
*
* @package AMP
*/
$categories = get_the_category_list( _x( ', ', 'Used between list items, there is a space after the comma.', 'amp' ) );
?>
<?php if ( $categories ) : ?>
<li class="amp-wp-tax-category">
<span class="screen-reader-text">Categories:</span>
<?php echo $categories; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</li>
<?php endif; ?>
<?php $tags = get_the_tag_list( '', _x( ', ', 'Used between list items, there is a space after the comma.', 'amp' ) ); ?>
<?php if ( $tags && ! is_wp_error( $tags ) ) : ?>
<li class="amp-wp-tax-tag">
<span class="screen-reader-text">Tags:</span>
<?php echo $tags; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</li>
<?php endif; ?>

View File

@ -0,0 +1,21 @@
<?php
/**
* Legacy template for the AMP post date.
*
* @package AMP
*/
?>
<li class="amp-wp-posted-on">
<time datetime="<?php echo esc_attr( date( 'c', $this->get( 'post_publish_timestamp' ) ) ); ?>">
<?php
echo esc_html(
sprintf(
/* translators: %s: the human-readable time difference. */
__( '%s ago', 'amp' ),
human_time_diff( $this->get( 'post_publish_timestamp' ), current_time( 'timestamp' ) )
)
);
?>
</time>
</li>

View File

@ -0,0 +1,32 @@
<?php
/**
* Legacy template for the AMP post.
*
* @package AMP
*/
?>
<!doctype html>
<html amp <?php language_attributes(); ?>>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
<?php do_action( 'amp_post_template_head', $this ); ?>
<style amp-custom>
<?php $this->load_parts( array( 'style' ) ); ?>
<?php do_action( 'amp_post_template_css', $this ); ?>
</style>
</head>
<body>
<?php $this->load_parts( array( 'header-bar' ) ); ?>
<div class="amp-wp-content">
<h1 class="amp-wp-title"><?php echo wp_kses_data( $this->get( 'post_title' ) ); ?></h1>
<ul class="amp-wp-meta">
<?php $this->load_parts( apply_filters( 'amp_post_template_meta_parts', array( 'meta-author', 'meta-time', 'meta-taxonomy' ) ) ); ?>
</ul>
<?php echo $this->get( 'post_amp_content' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<?php do_action( 'amp_post_template_footer', $this ); ?>
</body>
</html>

View File

@ -0,0 +1,252 @@
<?php
/**
* Legacy template for the AMP stylesheet.
*
* @package AMP
*/
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
?>
/* Merriweather fonts */
@font-face {
font-family:'Merriweather';
src:url('https://s1.wp.com/i/fonts/merriweather/merriweather-regular-webfont.woff2') format('woff2'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-regular-webfont.woff') format('woff'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-regular-webfont.ttf') format('truetype'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-regular-webfont.svg#merriweatherregular') format('svg');
font-weight:400;
font-style:normal;
}
@font-face {
font-family:'Merriweather';
src:url('https://s1.wp.com/i/fonts/merriweather/merriweather-italic-webfont.woff2') format('woff2'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-italic-webfont.woff') format('woff'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-italic-webfont.ttf') format('truetype'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-italic-webfont.svg#merriweatheritalic') format('svg');
font-weight:400;
font-style:italic;
}
@font-face {
font-family:'Merriweather';
src:url('https://s1.wp.com/i/fonts/merriweather/merriweather-bold-webfont.woff2') format('woff2'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-bold-webfont.woff') format('woff'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-bold-webfont.ttf') format('truetype'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-bold-webfont.svg#merriweatherbold') format('svg');
font-weight:700;
font-style:normal;
}
@font-face {
font-family:'Merriweather';
src:url('https://s1.wp.com/i/fonts/merriweather/merriweather-bolditalic-webfont.woff2') format('woff2'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-bolditalic-webfont.woff') format('woff'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-bolditalic-webfont.ttf') format('truetype'),
url('https://s1.wp.com/i/fonts/merriweather/merriweather-bolditalic-webfont.svg#merriweatherbold_italic') format('svg');
font-weight:700;
font-style:italic;
}
/* Generic WP styling */
amp-img.alignright { float: right; margin: 0 0 1em 1em; }
amp-img.alignleft { float: left; margin: 0 1em 1em 0; }
amp-img.aligncenter { display: block; margin-left: auto; margin-right: auto; }
.alignright { float: right; }
.alignleft { float: left; }
.aligncenter { display: block; margin-left: auto; margin-right: auto; }
.wp-caption.alignleft { margin-right: 1em; }
.wp-caption.alignright { margin-left: 1em; }
.amp-wp-enforced-sizes {
/** Our sizes fallback is 100vw, and we have a padding on the container; the max-width here prevents the element from overflowing. **/
max-width: 100%;
}
.amp-wp-unknown-size img {
/** Worst case scenario when we can't figure out dimensions for an image. **/
/** Force the image into a box of fixed dimensions and use object-fit to scale. **/
object-fit: contain;
}
/* Template Styles */
.amp-wp-content, .amp-wp-title-bar div {
<?php $content_max_width = absint( $this->get( 'content_max_width' ) ); ?>
<?php if ( $content_max_width > 0 ) : ?>
max-width: <?php echo sprintf( '%dpx', $content_max_width ); ?>;
margin: 0 auto;
<?php endif; ?>
}
body {
font-family: 'Merriweather', Serif;
font-size: 16px;
line-height: 1.8;
background: #fff;
color: #3d596d;
padding-bottom: 100px;
}
.amp-wp-content {
padding: 16px;
overflow-wrap: break-word;
word-wrap: break-word;
font-weight: 400;
color: #3d596d;
}
.amp-wp-title {
margin: 36px 0 0 0;
font-size: 36px;
line-height: 1.258;
font-weight: 700;
color: #2e4453;
}
.amp-wp-meta {
margin-bottom: 16px;
}
p,
ol,
ul,
figure {
margin: 0 0 24px 0;
}
a,
a:visited {
color: #0087be;
}
a:hover,
a:active,
a:focus {
color: #33bbe3;
}
/* UI Fonts */
.amp-wp-meta,
nav.amp-wp-title-bar,
.wp-caption-text {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen-Sans", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif;
font-size: 15px;
}
/* Meta */
ul.amp-wp-meta {
padding: 24px 0 0 0;
margin: 0 0 24px 0;
}
ul.amp-wp-meta li {
list-style: none;
display: inline-block;
margin: 0;
line-height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
ul.amp-wp-meta li:before {
content: "\2022";
margin: 0 8px;
}
ul.amp-wp-meta li:first-child:before {
display: none;
}
.amp-wp-meta,
.amp-wp-meta a {
color: #4f748e;
}
.amp-wp-meta .screen-reader-text {
/* from twentyfifteen */
clip: rect(1px, 1px, 1px, 1px);
height: 1px;
overflow: hidden;
position: absolute;
width: 1px;
}
.amp-wp-byline amp-img {
border-radius: 50%;
border: 0;
background: #f3f6f8;
position: relative;
top: 6px;
margin-right: 6px;
}
/* Titlebar */
nav.amp-wp-title-bar {
background: <?php echo esc_html( $this->get_customizer_setting( 'navbar_background', self::DEFAULT_NAVBAR_BACKGROUND ) ); // not ideal for escaping here, but better than nothing? ?>;
padding: 0 16px;
}
nav.amp-wp-title-bar div {
line-height: 54px;
color: <?php echo esc_html( $this->get_customizer_setting( 'navbar_color', self::DEFAULT_NAVBAR_COLOR ) ); ?>;
}
nav.amp-wp-title-bar a {
color: <?php echo esc_html( $this->get_customizer_setting( 'navbar_color', self::DEFAULT_NAVBAR_COLOR ) ); ?>;
text-decoration: none;
}
nav.amp-wp-title-bar .amp-wp-site-icon {
/** site icon is 32px **/
float: left;
margin: 11px 8px 0 0;
border-radius: 50%;
}
/* Captions */
.wp-caption-text {
padding: 8px 16px;
font-style: italic;
}
/* Quotes */
blockquote {
padding: 16px;
margin: 8px 0 24px 0;
border-left: 2px solid #87a6bc;
color: #4f748e;
background: #e9eff3;
}
blockquote p:last-child {
margin-bottom: 0;
}
/* Other Elements */
amp-carousel {
background: #000;
}
amp-iframe,
amp-youtube,
amp-instagram,
amp-vine {
background: #f3f6f8;
}
amp-carousel > amp-img > img {
object-fit: contain;
}
.amp-wp-iframe-placeholder {
background: #f3f6f8 url( <?php echo esc_url( $this->get( 'placeholder_image_url' ) ); ?> ) no-repeat center 40%;
background-size: 48px 48px;
min-height: 48px;
}

View File

@ -0,0 +1,149 @@
<?php
/**
* Admin pointer class.
*
* @package AMP
* @since 1.0
*/
/**
* AMP_Admin_Pointer class.
*
* Outputs an admin pointer to show the new features of v1.0.
* Based on https://code.tutsplus.com/articles/integrating-with-wordpress-ui-admin-pointers--wp-26853
*
* @since 1.0
*/
class AMP_Admin_Pointer {
/**
* The ID of the template mode admin pointer.
*
* @var string
*/
const TEMPLATE_POINTER_ID = 'amp_template_mode_pointer_10';
/**
* The slug of the script.
*
* @var string
*/
const SCRIPT_SLUG = 'amp-admin-pointer';
/**
* The slug of the tooltip script.
*
* @var string
*/
const TOOLTIP_SLUG = 'amp-validation-tooltips';
/**
* Initializes the class.
*
* @since 1.0
*/
public function init() {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_pointer' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'register_tooltips' ) );
}
/**
* Enqueues the pointer assets.
*
* If the pointer has not been dismissed, enqueues the style and script.
* And outputs the pointer data for the script.
*
* @since 1.0
*/
public function enqueue_pointer() {
if ( $this->is_pointer_dismissed() ) {
return;
}
wp_enqueue_style( 'wp-pointer' );
wp_enqueue_script(
self::SCRIPT_SLUG,
amp_get_asset_url( 'js/' . self::SCRIPT_SLUG . '.js' ),
array( 'jquery', 'wp-pointer' ),
AMP__VERSION,
true
);
wp_add_inline_script(
self::SCRIPT_SLUG,
sprintf( 'ampAdminPointer.load( %s );', wp_json_encode( $this->get_pointer_data() ) )
);
}
/**
* Registers style and script for tooltips.
*
* @since 1.0
*/
public function register_tooltips() {
wp_register_style(
self::TOOLTIP_SLUG,
amp_get_asset_url( 'css/' . self::TOOLTIP_SLUG . '.css' ),
array( 'wp-pointer' ),
AMP__VERSION
);
wp_register_script(
self::TOOLTIP_SLUG,
amp_get_asset_url( 'js/' . self::TOOLTIP_SLUG . '.js' ),
array( 'jquery', 'wp-pointer' ),
AMP__VERSION,
true
);
}
/**
* Whether the AMP admin pointer has been dismissed.
*
* @since 1.0
* @return boolean Is dismissed.
*/
protected function is_pointer_dismissed() {
// Consider dismissed in v1.1, since admin pointer is only to educate about the new modes in 1.0.
if ( version_compare( strtok( AMP__VERSION, '-' ), '1.1', '>=' ) ) {
return true;
}
$dismissed = get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true );
if ( empty( $dismissed ) ) {
return false;
}
$dismissed = explode( ',', strval( $dismissed ) );
return in_array( self::TEMPLATE_POINTER_ID, $dismissed, true );
}
/**
* Gets the pointer data to pass to the script.
*
* @since 1.0
* @return array Pointer data.
*/
public function get_pointer_data() {
return array(
'pointer' => array(
'pointer_id' => self::TEMPLATE_POINTER_ID,
'target' => '#toplevel_page_amp-options',
'options' => array(
'content' => sprintf(
'<h3>%s</h3><p><strong>%s</strong></p><p>%s</p>',
__( 'AMP', 'amp' ),
__( 'New AMP Template Modes', 'amp' ),
__( 'You can now reuse your theme\'s templates and styles in AMP responses, in both &#8220;Transitional&#8221; and &#8220;Native&#8221; modes.', 'amp' )
),
'position' => array(
'edge' => 'left',
'align' => 'middle',
),
),
),
);
}
}

View File

@ -0,0 +1,332 @@
<?php
/**
* Class AMP_Template_Customizer
*
* @package AMP
*/
/**
* AMP class that implements a template style editor in the Customizer.
*
* A direct, formed link to the AMP editor in the Customizer is added via
* {@see amp_customizer_editor_link()} as a submenu to the Appearance menu.
*
* @since 0.4
*/
class AMP_Template_Customizer {
/**
* AMP template editor panel ID.
*
* @since 0.4
* @var string
*/
const PANEL_ID = 'amp_panel';
/**
* Customizer instance.
*
* @since 0.4
* @access protected
* @var WP_Customize_Manager $wp_customize
*/
protected $wp_customize;
/**
* Initialize the template Customizer feature class.
*
* @static
* @since 0.4
* @access public
*
* @param WP_Customize_Manager $wp_customize Customizer instance.
*/
public static function init( $wp_customize ) {
$self = new self();
$self->wp_customize = $wp_customize;
/**
* Fires when the AMP Template Customizer initializes.
*
* In practice the `customize_register` hook should be used instead.
*
* @since 0.4
* @param AMP_Template_Customizer $self Instance.
*/
do_action( 'amp_customizer_init', $self );
$self->register_settings();
$self->register_ui();
add_action( 'customize_controls_enqueue_scripts', array( $self, 'add_customizer_scripts' ) );
add_action( 'customize_controls_print_footer_scripts', array( $self, 'print_controls_templates' ) );
add_action( 'customize_preview_init', array( $self, 'init_preview' ) );
}
/**
* Init Customizer preview.
*
* @since 0.4
* @global WP_Customize_Manager $wp_customize
*/
public function init_preview() {
add_action( 'amp_post_template_head', 'wp_no_robots' );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) );
add_action( 'amp_customizer_enqueue_preview_scripts', array( $this, 'enqueue_preview_scripts' ) );
// Output scripts and styles which will break AMP validation only when preview is opened with controls for manipulation.
if ( $this->wp_customize->get_messenger_channel() ) {
add_action( 'amp_post_template_head', array( $this->wp_customize, 'customize_preview_loading_style' ) );
add_action( 'amp_post_template_css', array( $this, 'add_customize_preview_styles' ) );
add_action( 'amp_post_template_head', array( $this->wp_customize, 'remove_frameless_preview_messenger_channel' ) );
add_action( 'amp_post_template_footer', array( $this, 'add_preview_scripts' ) );
}
}
/**
* Sets up the AMP Customizer preview.
*/
public function register_ui() {
$this->wp_customize->add_panel(
self::PANEL_ID,
array(
'type' => 'amp',
'title' => __( 'AMP', 'amp' ),
/* translators: placeholder is URL to AMP project. */
'description' => sprintf( __( '<a href="%s" target="_blank">The AMP Project</a> is a Google-led initiative that dramatically improves loading speeds on phones and tablets. You can use the Customizer to preview changes to your AMP template before publishing them.', 'amp' ), 'https://ampproject.org' ),
)
);
/**
* Fires after the AMP panel has been registered for plugins to add additional controls.
*
* In practice the `customize_register` hook should be used instead.
*
* @since 0.4
* @param WP_Customize_Manager $manager Manager.
*/
do_action( 'amp_customizer_register_ui', $this->wp_customize );
}
/**
* Registers settings for customizing AMP templates.
*
* @since 0.4
*/
public function register_settings() {
/**
* Fires when plugins should register settings for AMP.
*
* In practice the `customize_register` hook should be used instead.
*
* @since 0.4
* @param WP_Customize_Manager $manager Manager.
*/
do_action( 'amp_customizer_register_settings', $this->wp_customize );
}
/**
* Load up AMP scripts needed for Customizer integrations.
*
* @since 0.6
*/
public function add_customizer_scripts() {
if ( ! amp_is_canonical() ) {
wp_enqueue_script(
'amp-customize-controls',
amp_get_asset_url( 'js/amp-customize-controls.js' ),
array( 'jquery', 'customize-controls' ),
AMP__VERSION,
true
);
wp_add_inline_script(
'amp-customize-controls',
sprintf(
'ampCustomizeControls.boot( %s );',
wp_json_encode(
array(
'queryVar' => amp_get_slug(),
'panelId' => self::PANEL_ID,
'ampUrl' => amp_admin_get_preview_permalink(),
'l10n' => array(
'unavailableMessage' => __( 'AMP is not available for the page currently being previewed.', 'amp' ),
'unavailableLinkText' => __( 'Navigate to an AMP compatible page', 'amp' ),
),
)
)
)
);
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_enqueue_style(
'amp-customizer',
amp_get_asset_url( 'css/amp-customizer.css' )
);
}
/**
* Fires when plugins should register settings for AMP.
*
* In practice the `customize_controls_enqueue_scripts` hook should be used instead.
*
* @since 0.4
* @param WP_Customize_Manager $manager Manager.
*/
do_action( 'amp_customizer_enqueue_scripts', $this->wp_customize );
}
/**
* Enqueues scripts used in both the AMP and non-AMP Customizer preview.
*
* @since 0.6
* @global WP_Query $wp_query
*/
public function enqueue_preview_scripts() {
global $wp_query;
// Bail if user can't customize anyway.
if ( ! current_user_can( 'customize' ) ) {
return;
}
wp_enqueue_script(
'amp-customize-preview',
amp_get_asset_url( 'js/amp-customize-preview.js' ),
array( 'jquery', 'customize-preview' ),
AMP__VERSION,
true
);
if ( current_theme_supports( AMP_Theme_Support::SLUG ) ) {
$availability = AMP_Theme_Support::get_template_availability();
$available = $availability['supported'];
} elseif ( is_singular() || $wp_query->is_posts_page ) {
/**
* Queried object.
*
* @var WP_Post $queried_object
*/
$queried_object = get_queried_object();
$available = post_supports_amp( $queried_object );
} else {
$available = false;
}
wp_add_inline_script(
'amp-customize-preview',
sprintf(
'ampCustomizePreview.boot( %s );',
wp_json_encode(
array(
'available' => $available,
'enabled' => is_amp_endpoint(),
)
)
)
);
}
/**
* Add AMP Customizer preview styles.
*/
public function add_customize_preview_styles() {
?>
/* Text meant only for screen readers; this is needed for wp.a11y.speak() */
.screen-reader-text {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
-webkit-clip-path: inset(50%);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
word-wrap: normal !important;
}
body.wp-customizer-unloading {
opacity: 0.25 !important; /* Because AMP sets body to opacity:1 once layout complete. */
}
<?php
}
/**
* Enqueues scripts and does wp_print_footer_scripts() so we can output customizer scripts.
*
* This breaks AMP validation in the customizer but is necessary for the live preview.
*
* @since 0.6
*/
public function add_preview_scripts() {
// Bail if user can't customize anyway.
if ( ! current_user_can( 'customize' ) ) {
return;
}
wp_enqueue_script( 'customize-selective-refresh' );
wp_enqueue_script( 'amp-customize-preview' );
/**
* Fires when plugins should enqueue their own scripts for the AMP Customizer preview.
*
* @since 0.4
* @param WP_Customize_Manager $wp_customize Manager.
*/
do_action( 'amp_customizer_enqueue_preview_scripts', $this->wp_customize );
$this->wp_customize->customize_preview_settings();
$this->wp_customize->selective_refresh->export_preview_data();
wp_print_footer_scripts();
}
/**
* Print templates needed for AMP in Customizer.
*
* @since 0.6
*/
public function print_controls_templates() {
?>
<script type="text/html" id="tmpl-customize-amp-enabled-toggle">
<div class="amp-toggle">
<# var elementIdPrefix = _.uniqueId( 'customize-amp-enabled-toggle' ); #>
<div id="{{ elementIdPrefix }}tooltip" aria-hidden="true" class="tooltip" role="tooltip">
{{ data.message }}
<# if ( data.url ) { #>
<a href="{{ data.url }}">{{ data.linkText }}</a>
<# } #>
</div>
<input id="{{ elementIdPrefix }}checkbox" type="checkbox" class="disabled" aria-describedby="{{ elementIdPrefix }}tooltip">
<span class="slider"></span>
<label for="{{ elementIdPrefix }}checkbox" class="screen-reader-text"><?php esc_html_e( 'AMP preview enabled', 'amp' ); ?></label>
</div>
</script>
<script type="text/html" id="tmpl-customize-amp-unavailable-notification">
<li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
<div class="notification-message">
{{ data.message }}
<# if ( data.url ) { #>
<a href="{{ data.url }}">{{ data.linkText }}</a>
<# } #>
</div>
</li>
</script>
<?php
}
/**
* Whether the Customizer is AMP. This is always true since the AMP Customizer has been merged with the main Customizer.
*
* @deprecated 0.6
* @return bool
*/
public static function is_amp_customizer() {
_deprecated_function( __METHOD__, '0.6' );
return true;
}
}

View File

@ -0,0 +1,210 @@
<?php
/**
* AMP Editor Blocks extending.
*
* @package AMP
* @since 1.0
*/
/**
* Class AMP_Editor_Blocks
*/
class AMP_Editor_Blocks {
/**
* List of AMP scripts that need to be printed when AMP components are used in non-AMP document context ("dirty AMP").
*
* @var array
*/
public $content_required_amp_scripts = array();
/**
* AMP components that have blocks.
*
* @var array
*/
public $amp_blocks = array(
'amp-mathml',
'amp-timeago',
'amp-o2-player',
'amp-ooyala-player',
'amp-reach-player',
'amp-springboard-player',
'amp-jwplayer',
'amp-brid-player',
'amp-ima-video',
'amp-fit-text',
);
/**
* Init.
*/
public function init() {
if ( function_exists( 'register_block_type' ) ) {
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) );
add_filter( 'wp_kses_allowed_html', array( $this, 'whitelist_block_atts_in_wp_kses_allowed_html' ), 10, 2 );
/*
* Dirty AMP is required when a site is in native mode but not all templates are being served
* as AMP. In particular, if a single post is using AMP-specific Gutenberg Blocks which make
* use of AMP components, and the singular template is served as AMP but the blog page is not,
* then the non-AMP blog page need to load the AMP runtime scripts so that the AMP components
* in the posts displayed there will be rendered properly. This is only relevant on native AMP
* sites because the AMP Gutenberg blocks are only made available in that mode; they are not
* presented in the Gutenberg inserter in transitional mode. In general, using AMP components in
* non-AMP documents is still not officially supported, so it's occurrence is being minimized
* as much as possible. For more, see <https://github.com/ampproject/amp-wp/issues/1192>.
*/
if ( amp_is_canonical() ) {
add_filter( 'the_content', array( $this, 'tally_content_requiring_amp_scripts' ) );
add_action( 'wp_print_footer_scripts', array( $this, 'print_dirty_amp_scripts' ) );
}
}
}
/**
* Whitelist elements and attributes used for AMP.
*
* This prevents AMP markup from being deleted in
*
* @param array $tags Array of allowed post tags.
* @param string $context Context.
* @return mixed Modified array.
*/
public function whitelist_block_atts_in_wp_kses_allowed_html( $tags, $context ) {
if ( 'post' !== $context ) {
return $tags;
}
foreach ( $tags as &$tag ) {
if ( ! is_array( $tag ) ) {
continue;
}
$tag['data-amp-layout'] = true;
$tag['data-amp-noloading'] = true;
$tag['data-amp-lightbox'] = true;
$tag['data-close-button-aria-label'] = true;
}
foreach ( $this->amp_blocks as $amp_block ) {
if ( ! isset( $tags[ $amp_block ] ) ) {
$tags[ $amp_block ] = array();
}
// @todo The global attributes included here should be matched up with what is actually used by each block.
$tags[ $amp_block ] = array_merge(
array_fill_keys(
array(
'layout',
'width',
'height',
'class',
),
true
),
$tags[ $amp_block ]
);
$amp_tag_specs = AMP_Allowed_Tags_Generated::get_allowed_tag( $amp_block );
foreach ( $amp_tag_specs as $amp_tag_spec ) {
if ( ! isset( $amp_tag_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ] ) ) {
continue;
}
$tags[ $amp_block ] = array_merge(
$tags[ $amp_block ],
array_fill_keys( array_keys( $amp_tag_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ] ), true )
);
}
}
return $tags;
}
/**
* Enqueue filters for extending core blocks attributes.
* Has to be loaded before registering the blocks in registerCoreBlocks.
*/
public function enqueue_block_editor_assets() {
// Enqueue script and style for AMP-specific blocks.
if ( amp_is_canonical() ) {
wp_enqueue_style(
'amp-editor-blocks-style',
amp_get_asset_url( 'css/amp-editor-blocks.css' ),
array(),
AMP__VERSION
);
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter
wp_enqueue_script(
'amp-editor-blocks-build',
amp_get_asset_url( 'js/amp-blocks-compiled.js' ),
array( 'wp-editor', 'wp-blocks', 'lodash', 'wp-i18n', 'wp-element', 'wp-components' ),
AMP__VERSION
);
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( 'amp-editor-blocks-build', 'amp' );
}
}
wp_enqueue_script(
'amp-editor-blocks',
amp_get_asset_url( 'js/amp-editor-blocks.js' ),
array( 'underscore', 'wp-hooks', 'wp-i18n', 'wp-components' ),
AMP__VERSION,
true
);
wp_add_inline_script(
'amp-editor-blocks',
sprintf(
'ampEditorBlocks.boot( %s );',
wp_json_encode(
array(
'hasThemeSupport' => current_theme_supports( AMP_Theme_Support::SLUG ),
)
)
)
);
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( 'amp-editor-blocks', 'amp' );
} elseif ( function_exists( 'wp_get_jed_locale_data' ) || function_exists( 'gutenberg_get_jed_locale_data' ) ) {
$locale_data = function_exists( 'wp_get_jed_locale_data' ) ? wp_get_jed_locale_data( 'amp' ) : gutenberg_get_jed_locale_data( 'amp' );
wp_add_inline_script(
'wp-i18n',
'wp.i18n.setLocaleData( ' . wp_json_encode( $locale_data ) . ', "amp" );',
'after'
);
}
}
/**
* Tally the AMP component scripts that are needed in a dirty AMP document.
*
* @param string $content Content.
* @return string Content (unmodified).
*/
public function tally_content_requiring_amp_scripts( $content ) {
if ( ! is_amp_endpoint() ) {
$pattern = sprintf( '/<(%s)\b.*?>/s', join( '|', $this->amp_blocks ) );
if ( preg_match_all( $pattern, $content, $matches ) ) {
$this->content_required_amp_scripts = array_merge(
$this->content_required_amp_scripts,
$matches[1]
);
}
}
return $content;
}
/**
* Print AMP scripts required for AMP components used in a non-AMP document (dirty AMP).
*/
public function print_dirty_amp_scripts() {
if ( ! is_amp_endpoint() && ! empty( $this->content_required_amp_scripts ) ) {
wp_scripts()->do_items( $this->content_required_amp_scripts );
}
}
}

View File

@ -0,0 +1,394 @@
<?php
/**
* AMP meta box settings.
*
* @package AMP
* @since 0.6
*/
/**
* Post meta box class.
*
* @since 0.6
*/
class AMP_Post_Meta_Box {
/**
* Assets handle.
*
* @since 0.6
* @var string
*/
const ASSETS_HANDLE = 'amp-post-meta-box';
/**
* Block asset handle.
*
* @since 1.0
* @var string
*/
const BLOCK_ASSET_HANDLE = 'amp-block-editor-toggle-compiled';
/**
* The enabled status post meta value.
*
* @since 0.6
* @var string
*/
const ENABLED_STATUS = 'enabled';
/**
* The disabled status post meta value.
*
* @since 0.6
* @var string
*/
const DISABLED_STATUS = 'disabled';
/**
* The status post meta key.
*
* @since 0.6
* @var string
*/
const STATUS_POST_META_KEY = 'amp_status';
/**
* The field name for the enabled/disabled radio buttons.
*
* @since 0.6
* @var string
*/
const STATUS_INPUT_NAME = 'amp_status';
/**
* The nonce name.
*
* @since 0.6
* @var string
*/
const NONCE_NAME = 'amp-status-nonce';
/**
* The nonce action.
*
* @since 0.6
* @var string
*/
const NONCE_ACTION = 'amp-update-status';
/**
* Initialize.
*
* @since 0.6
*/
public function init() {
register_meta(
'post',
self::STATUS_POST_META_KEY,
array(
'sanitize_callback' => array( $this, 'sanitize_status' ),
'type' => 'string',
'description' => __( 'AMP status.', 'amp' ),
'show_in_rest' => true,
'single' => true,
)
);
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_assets' ) );
add_action( 'post_submitbox_misc_actions', array( $this, 'render_status' ) );
add_action( 'save_post', array( $this, 'save_amp_status' ) );
add_filter( 'preview_post_link', array( $this, 'preview_post_link' ) );
}
/**
* Sanitize status.
*
* @param string $status Status.
* @return string Sanitized status. Empty string when invalid.
*/
public function sanitize_status( $status ) {
$status = strtolower( trim( $status ) );
if ( ! in_array( $status, array( self::ENABLED_STATUS, self::DISABLED_STATUS ), true ) ) {
/*
* In lieu of actual validation being available, clear the status entirely
* so that the underlying default status will be used instead.
* In the future it would be ideal if register_meta() accepted a
* validate_callback as well which the REST API could leverage.
*/
$status = '';
}
return $status;
}
/**
* Enqueue admin assets.
*
* @since 0.6
*/
public function enqueue_admin_assets() {
$post = get_post();
$screen = get_current_screen();
$validate = (
isset( $screen->base )
&&
'post' === $screen->base
&&
is_post_type_viewable( $post->post_type )
);
if ( ! $validate ) {
return;
}
wp_enqueue_style(
self::ASSETS_HANDLE,
amp_get_asset_url( 'css/amp-post-meta-box.css' ),
false,
AMP__VERSION
);
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter
wp_enqueue_script(
self::ASSETS_HANDLE,
amp_get_asset_url( 'js/amp-post-meta-box.js' ),
array( 'jquery' ),
AMP__VERSION
);
if ( current_theme_supports( AMP_Theme_Support::SLUG ) ) {
$availability = AMP_Theme_Support::get_template_availability( $post );
$support_errors = $availability['errors'];
} else {
$support_errors = AMP_Post_Type_Support::get_support_errors( $post );
}
wp_add_inline_script(
self::ASSETS_HANDLE,
sprintf(
'ampPostMetaBox.boot( %s );',
wp_json_encode(
array(
'previewLink' => esc_url_raw( add_query_arg( amp_get_slug(), '', get_preview_post_link( $post ) ) ),
'canonical' => amp_is_canonical(),
'enabled' => empty( $support_errors ),
'canSupport' => 0 === count( array_diff( $support_errors, array( 'post-status-disabled' ) ) ),
'statusInputName' => self::STATUS_INPUT_NAME,
'l10n' => array(
'ampPreviewBtnLabel' => __( 'Preview changes in AMP (opens in new window)', 'amp' ),
),
)
)
)
);
}
/**
* Enqueues block assets.
*
* @since 1.0
*/
public function enqueue_block_assets() {
$post = get_post();
if ( ! is_post_type_viewable( $post->post_type ) ) {
return;
}
wp_enqueue_script(
self::BLOCK_ASSET_HANDLE,
amp_get_asset_url( 'js/' . self::BLOCK_ASSET_HANDLE . '.js' ),
array( 'wp-hooks', 'wp-i18n', 'wp-components' ),
AMP__VERSION,
true
);
$status_and_errors = $this->get_status_and_errors( $post );
$enabled_status = $status_and_errors['status'];
$error_messages = $this->get_error_messages( $status_and_errors['status'], $status_and_errors['errors'] );
$script_data = array(
'possibleStati' => array( self::ENABLED_STATUS, self::DISABLED_STATUS ),
'defaultStatus' => $enabled_status,
'errorMessages' => $error_messages,
);
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( self::BLOCK_ASSET_HANDLE, 'amp' );
} elseif ( function_exists( 'wp_get_jed_locale_data' ) ) {
$script_data['i18n'] = wp_get_jed_locale_data( 'amp' );
} elseif ( function_exists( 'gutenberg_get_jed_locale_data' ) ) {
$script_data['i18n'] = gutenberg_get_jed_locale_data( 'amp' );
}
wp_add_inline_script(
self::BLOCK_ASSET_HANDLE,
sprintf( 'var wpAmpEditor = %s;', wp_json_encode( $script_data ) ),
'before'
);
}
/**
* Render AMP status.
*
* @since 0.6
* @param WP_Post $post Post.
*/
public function render_status( $post ) {
$verify = (
isset( $post->ID )
&&
is_post_type_viewable( $post->post_type )
&&
current_user_can( 'edit_post', $post->ID )
);
if ( true !== $verify ) {
return;
}
$status_and_errors = $this->get_status_and_errors( $post );
$status = $status_and_errors['status'];
$errors = $status_and_errors['errors'];
$error_messages = $this->get_error_messages( $status, $errors );
$labels = array(
'enabled' => __( 'Enabled', 'amp' ),
'disabled' => __( 'Disabled', 'amp' ),
);
// The preceding variables are used inside the following amp-status.php template.
include AMP__DIR__ . '/templates/admin/amp-status.php';
}
/**
* Gets the AMP enabled status and errors.
*
* @since 1.0
* @param WP_Post $post The post to check.
* @return array {
* The status and errors.
*
* @type string $status The AMP enabled status.
* @type string[] $errors AMP errors.
* }
*/
public function get_status_and_errors( $post ) {
/*
* When theme support is present then theme templates can be served in AMP and we check first if the template is available.
* Checking for template availability will include a check for get_support_errors. Otherwise, if theme support is not present
* then we just check get_support_errors.
*/
if ( current_theme_supports( AMP_Theme_Support::SLUG ) ) {
$availability = AMP_Theme_Support::get_template_availability( $post );
$status = $availability['supported'] ? self::ENABLED_STATUS : self::DISABLED_STATUS;
$errors = array_diff( $availability['errors'], array( 'post-status-disabled' ) ); // Subtract the status which the metabox will allow to be toggled.
if ( true === $availability['immutable'] ) {
$errors[] = 'status_immutable';
}
} else {
$errors = AMP_Post_Type_Support::get_support_errors( $post );
$status = empty( $errors ) ? self::ENABLED_STATUS : self::DISABLED_STATUS;
$errors = array_diff( $errors, array( 'post-status-disabled' ) ); // Subtract the status which the metabox will allow to be toggled.
}
return compact( 'status', 'errors' );
}
/**
* Gets the AMP enabled error message(s).
*
* @since 1.0
* @param string $status The AMP enabled status.
* @param array $errors The AMP enabled errors.
* @return array $error_messages The error messages, as an array of strings.
*/
public function get_error_messages( $status, $errors ) {
$error_messages = array();
if ( in_array( 'status_immutable', $errors, true ) ) {
if ( self::ENABLED_STATUS === $status ) {
$error_messages[] = __( 'Your site does not allow AMP to be disabled.', 'amp' );
} else {
$error_messages[] = __( 'Your site does not allow AMP to be enabled.', 'amp' );
}
}
if ( in_array( 'template_unsupported', $errors, true ) || in_array( 'no_matching_template', $errors, true ) ) {
$error_messages[] = sprintf(
/* translators: %s is a link to the AMP settings screen */
__( 'There are no <a href="%s">supported templates</a> to display this in AMP.', 'amp' ),
esc_url( admin_url( 'admin.php?page=' . AMP_Options_Manager::OPTION_NAME ) )
);
}
if ( in_array( 'password-protected', $errors, true ) ) {
$error_messages[] = __( 'AMP cannot be enabled on password protected posts.', 'amp' );
}
if ( in_array( 'post-type-support', $errors, true ) ) {
$error_messages[] = sprintf(
/* translators: %s is a link to the AMP settings screen */
__( 'AMP cannot be enabled because this <a href="%s">post type does not support it</a>.', 'amp' ),
esc_url( admin_url( 'admin.php?page=' . AMP_Options_Manager::OPTION_NAME ) )
);
}
if ( in_array( 'skip-post', $errors, true ) ) {
$error_messages[] = __( 'A plugin or theme has disabled AMP support.', 'amp' );
}
if ( count( array_diff( $errors, array( 'status_immutable', 'page-on-front', 'page-for-posts', 'password-protected', 'post-type-support', 'skip-post', 'template_unsupported', 'no_matching_template' ) ) ) > 0 ) {
$error_messages[] = __( 'Unavailable for an unknown reason.', 'amp' );
}
return $error_messages;
}
/**
* Save AMP Status.
*
* @since 0.6
* @param int $post_id The Post ID.
*/
public function save_amp_status( $post_id ) {
$verify = (
isset( $_POST[ self::NONCE_NAME ] )
&&
isset( $_POST[ self::STATUS_INPUT_NAME ] )
&&
wp_verify_nonce( sanitize_key( wp_unslash( $_POST[ self::NONCE_NAME ] ) ), self::NONCE_ACTION )
&&
current_user_can( 'edit_post', $post_id )
&&
! wp_is_post_revision( $post_id )
&&
! wp_is_post_autosave( $post_id )
);
if ( true === $verify ) {
update_post_meta(
$post_id,
self::STATUS_POST_META_KEY,
$_POST[ self::STATUS_INPUT_NAME ] // Note: The sanitize_callback has been supplied in the register_meta() call above.
);
}
}
/**
* Modify post preview link.
*
* Add the AMP query var is the amp-preview flag is set.
*
* @since 0.6
*
* @param string $link The post preview link.
* @return string Preview URL.
*/
public function preview_post_link( $link ) {
$is_amp = (
isset( $_POST['amp-preview'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
&&
'do-preview' === sanitize_key( wp_unslash( $_POST['amp-preview'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
);
if ( $is_amp ) {
$link = add_query_arg( amp_get_slug(), true, $link );
}
return $link;
}
}

View File

@ -0,0 +1,201 @@
<?php
/**
* Callbacks for adding AMP-related things to the admin.
*
* @package AMP
*/
/**
* Obsolete constant for flagging when Customizer is opened for AMP.
*
* @deprecated
* @var string
*/
define( 'AMP_CUSTOMIZER_QUERY_VAR', 'customize_amp' );
/**
* Sets up the AMP template editor for the Customizer.
*
* If this is in AMP canonical mode, exit.
* There's no need for the 'AMP' Customizer panel,
* And this does not need to toggle between the AMP and normal display.
*/
function amp_init_customizer() {
if ( amp_is_canonical() ) {
return;
}
// Fire up the AMP Customizer.
add_action( 'customize_register', array( 'AMP_Template_Customizer', 'init' ), 500 );
// Add some basic design settings + controls to the Customizer.
add_action( 'amp_init', array( 'AMP_Customizer_Design_Settings', 'init' ) );
// Add a link to the Customizer.
add_action( 'admin_menu', 'amp_add_customizer_link' );
}
/**
* Get permalink for the first AMP-eligible post.
*
* @return string|null URL on success, null if none found.
*/
function amp_admin_get_preview_permalink() {
/**
* Filter the post type to retrieve the latest for use in the AMP template customizer.
*
* @param string $post_type Post type slug. Default 'post'.
*/
$post_type = (string) apply_filters( 'amp_customizer_post_type', 'post' );
// Make sure the desired post type is actually supported, and if so, prefer it.
$supported_post_types = get_post_types_by_support( AMP_Post_Type_Support::SLUG );
if ( in_array( $post_type, $supported_post_types, true ) ) {
$supported_post_types = array_unique( array_merge( array( $post_type ), $supported_post_types ) );
}
// Bail if there are no supported post types.
if ( empty( $supported_post_types ) ) {
return null;
}
// If theme support is present, then bail if the singular template is not supported.
if ( current_theme_supports( AMP_Theme_Support::SLUG ) ) {
$supported_templates = AMP_Theme_Support::get_supportable_templates();
if ( empty( $supported_templates['is_singular']['supported'] ) ) {
return null;
}
}
$post_ids = get_posts(
array(
'no_found_rows' => true,
'suppress_filters' => false,
'post_status' => 'publish',
'post_password' => '',
'post_type' => $supported_post_types,
'posts_per_page' => 1,
'fields' => 'ids',
// @todo This should eventually do a meta_query to make sure there are none that have AMP_Post_Meta_Box::STATUS_POST_META_KEY = DISABLED_STATUS.
)
);
if ( empty( $post_ids ) ) {
return false;
}
$post_id = $post_ids[0];
return amp_get_permalink( $post_id );
}
/**
* Registers a submenu page to access the AMP template editor panel in the Customizer.
*/
function amp_add_customizer_link() {
/** This filter is documented in includes/settings/class-amp-customizer-design-settings.php */
if ( ! apply_filters( 'amp_customizer_is_enabled', true ) || current_theme_supports( AMP_Theme_Support::SLUG ) ) {
return;
}
$menu_slug = add_query_arg(
array(
'autofocus[panel]' => AMP_Template_Customizer::PANEL_ID,
'url' => rawurlencode( amp_admin_get_preview_permalink() ),
'return' => rawurlencode( admin_url() ),
),
'customize.php'
);
// Add the theme page.
add_theme_page(
__( 'AMP', 'amp' ),
__( 'AMP', 'amp' ),
'edit_theme_options',
$menu_slug
);
}
/**
* Registers AMP settings.
*/
function amp_add_options_menu() {
if ( ! is_admin() ) {
return;
}
/**
* Filter whether to enable the AMP settings.
*
* @since 0.5
* @param bool $enable Whether to enable the AMP settings. Default true.
*/
$short_circuit = apply_filters( 'amp_options_menu_is_enabled', true );
if ( true !== $short_circuit ) {
return;
}
$amp_options = new AMP_Options_Menu();
$amp_options->init();
}
/**
* Add custom analytics.
*
* This is currently only used for legacy AMP post templates.
*
* @since 0.5
* @see amp_get_analytics()
*
* @param array $analytics Analytics.
* @return array Analytics.
*/
function amp_add_custom_analytics( $analytics = array() ) {
$analytics = amp_get_analytics( $analytics );
/**
* Add amp-analytics tags.
*
* This filter allows you to easily insert any amp-analytics tags without needing much heavy lifting.
* This filter should be used to alter entries for legacy AMP templates.
*
* @since 0.4
*
* @param array $analytics An associative array of the analytics entries we want to output. Each array entry must have a unique key, and the value should be an array with the following keys: `type`, `attributes`, `script_data`. See readme for more details.
* @param WP_Post $post The current post.
*/
$analytics = apply_filters( 'amp_post_template_analytics', $analytics, get_queried_object() );
return $analytics;
}
/**
* Bootstrap AMP post meta box.
*
* This function must be invoked only once through the 'wp_loaded' action.
*
* @since 0.6
*/
function amp_post_meta_box() {
$post_meta_box = new AMP_Post_Meta_Box();
$post_meta_box->init();
}
/**
* Bootstrap AMP Editor core blocks.
*/
function amp_editor_core_blocks() {
$editor_blocks = new AMP_Editor_Blocks();
$editor_blocks->init();
}
/**
* Bootstrap the AMP admin pointer class.
*
* @since 1.0
*/
function amp_admin_pointer() {
$admin_pointer = new AMP_Admin_Pointer();
$admin_pointer->init();
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Callbacks for adding AMP-related things to the main theme.
*
* @deprecated Function in this file has been moved to amp-helper-functions.php.
* @package AMP
*/
_deprecated_file(
__FILE__,
'1.0',
null,
sprintf(
/* translators: 1: amp_add_amphtml_link(). 2: amp-helper-functions.php */
esc_html__( 'Use %1$s function which is already included from %2$s', 'amp' ),
'amp_add_amphtml_link()',
'amp-helper-functions.php'
)
);
/**
* Add amphtml link to frontend.
*
* @deprecated
*
* @since 0.2
* @since 1.0 Deprecated
* @see amp_add_amphtml_link()
*/
function amp_frontend_add_canonical() {
_deprecated_function( __FUNCTION__, '1.0', 'amp_add_amphtml_link' );
amp_add_amphtml_link();
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,142 @@
<?php
/**
* Callbacks for adding content to an AMP template.
*
* @package AMP
*/
/**
* Register hooks.
*/
function amp_post_template_init_hooks() {
add_action( 'amp_post_template_head', 'amp_post_template_add_title' );
add_action( 'amp_post_template_head', 'amp_post_template_add_canonical' );
add_action( 'amp_post_template_head', 'amp_post_template_add_scripts' );
add_action( 'amp_post_template_head', 'amp_post_template_add_fonts' );
add_action( 'amp_post_template_head', 'amp_post_template_add_boilerplate_css' );
add_action( 'amp_post_template_head', 'amp_print_schemaorg_metadata' );
add_action( 'amp_post_template_head', 'amp_add_generator_metadata' );
add_action( 'amp_post_template_head', 'wp_generator' );
add_action( 'amp_post_template_css', 'amp_post_template_add_styles', 99 );
add_action( 'amp_post_template_data', 'amp_post_template_add_analytics_script' );
add_action( 'amp_post_template_footer', 'amp_post_template_add_analytics_data' );
}
/**
* Add title.
*
* @param AMP_Post_Template $amp_template template.
*/
function amp_post_template_add_title( $amp_template ) {
?>
<title><?php echo esc_html( $amp_template->get( 'document_title' ) ); ?></title>
<?php
}
/**
* Add canonical link.
*
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_canonical( $amp_template ) {
?>
<link rel="canonical" href="<?php echo esc_url( $amp_template->get( 'canonical_url' ) ); ?>" />
<?php
}
/**
* Print scripts.
*
* @see amp_register_default_scripts()
* @see amp_filter_script_loader_tag()
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_scripts( $amp_template ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo amp_render_scripts(
array_merge(
array(
// Just in case the runtime has been overridden by amp_post_template_data filter.
'amp-runtime' => $amp_template->get( 'amp_runtime_script' ),
),
$amp_template->get( 'amp_component_scripts', array() )
)
);
}
/**
* Print fonts.
*
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_fonts( $amp_template ) {
$font_urls = $amp_template->get( 'font_urls', array() );
foreach ( $font_urls as $slug => $url ) {
printf( '<link rel="stylesheet" href="%s">', esc_url( esc_url( $url ) ) ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
}
}
/**
* Print boilerplate CSS.
*
* @since 0.3
* @see amp_get_boilerplate_code()
*/
function amp_post_template_add_boilerplate_css() {
echo amp_get_boilerplate_code(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Print Schema.org metadata.
*
* @deprecated Since 0.7
*/
function amp_post_template_add_schemaorg_metadata() {
_deprecated_function( __FUNCTION__, '0.7', 'amp_print_schemaorg_metadata' );
amp_print_schemaorg_metadata();
}
/**
* Print styles.
*
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_styles( $amp_template ) {
$stylesheets = $amp_template->get( 'post_amp_stylesheets' );
if ( ! empty( $stylesheets ) ) {
echo '/* Inline stylesheets */' . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo implode( '', $stylesheets ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$styles = $amp_template->get( 'post_amp_styles' );
if ( ! empty( $styles ) ) {
echo '/* Inline styles */' . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
foreach ( $styles as $selector => $declarations ) {
$declarations = implode( ';', $declarations ) . ';';
printf( '%1$s{%2$s}', $selector, $declarations ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
}
/**
* Add analytics scripts.
*
* @param array $data Data.
* @return array Data.
*/
function amp_post_template_add_analytics_script( $data ) {
if ( ! empty( $data['amp_analytics'] ) ) {
$data['amp_component_scripts']['amp-analytics'] = 'https://cdn.ampproject.org/v0/amp-analytics-0.1.js';
}
return $data;
}
/**
* Print analytics data.
*
* @since 0.3.2
*/
function amp_post_template_add_analytics_data() {
$analytics = amp_add_custom_analytics();
amp_print_analytics( $analytics );
}

View File

@ -0,0 +1,163 @@
<?php
/**
* Class AMP_Autoloader
*
* @package AMP
*/
/**
* Autoload the classes used by the AMP plugin.
*
* Class AMP_Autoloader
*/
class AMP_Autoloader {
/**
* Map of Classname to relative filepath sans extension.
*
* @note We omitted the leading slash and the .php extension from each
* relative filepath because they are redundant and to include
* them would take up unnecessary bytes of memory at runtime.
*
* @example Format (note no leading / and no .php extension):
*
* array(
* 'Class_Name1' => 'subdir-of-includes/filename1',
* 'Class_Name2' => '2nd-subdir-of-includes/filename2',
* );
*
* @var string[]
*/
private static $classmap = array(
'AMP_Editor_Blocks' => 'includes/admin/class-amp-editor-blocks',
'AMP_Theme_Support' => 'includes/class-amp-theme-support',
'AMP_Service_Worker' => 'includes/class-amp-service-worker',
'AMP_HTTP' => 'includes/class-amp-http',
'AMP_Comment_Walker' => 'includes/class-amp-comment-walker',
'AMP_Template_Customizer' => 'includes/admin/class-amp-customizer',
'AMP_Post_Meta_Box' => 'includes/admin/class-amp-post-meta-box',
'AMP_Admin_Pointer' => 'includes/admin/class-amp-admin-pointer',
'AMP_Post_Type_Support' => 'includes/class-amp-post-type-support',
'AMP_Base_Embed_Handler' => 'includes/embeds/class-amp-base-embed-handler',
'AMP_DailyMotion_Embed_Handler' => 'includes/embeds/class-amp-dailymotion-embed',
'AMP_Facebook_Embed_Handler' => 'includes/embeds/class-amp-facebook-embed',
'AMP_Gallery_Embed_Handler' => 'includes/embeds/class-amp-gallery-embed',
'AMP_Gfycat_Embed_Handler' => 'includes/embeds/class-amp-gfycat-embed-handler',
'AMP_Hulu_Embed_Handler' => 'includes/embeds/class-amp-hulu-embed-handler',
'AMP_Imgur_Embed_Handler' => 'includes/embeds/class-amp-imgur-embed-handler',
'AMP_Core_Block_Handler' => 'includes/embeds/class-amp-core-block-handler',
'AMP_Instagram_Embed_Handler' => 'includes/embeds/class-amp-instagram-embed',
'AMP_Issuu_Embed_Handler' => 'includes/embeds/class-amp-issuu-embed-handler',
'AMP_Meetup_Embed_Handler' => 'includes/embeds/class-amp-meetup-embed-handler',
'AMP_Pinterest_Embed_Handler' => 'includes/embeds/class-amp-pinterest-embed',
'AMP_Playlist_Embed_Handler' => 'includes/embeds/class-amp-playlist-embed-handler',
'AMP_Reddit_Embed_Handler' => 'includes/embeds/class-amp-reddit-embed-handler',
'AMP_SoundCloud_Embed_Handler' => 'includes/embeds/class-amp-soundcloud-embed',
'AMP_Tumblr_Embed_Handler' => 'includes/embeds/class-amp-tumblr-embed-handler',
'AMP_Twitter_Embed_Handler' => 'includes/embeds/class-amp-twitter-embed',
'AMP_Vimeo_Embed_Handler' => 'includes/embeds/class-amp-vimeo-embed',
'AMP_Vine_Embed_Handler' => 'includes/embeds/class-amp-vine-embed',
'AMP_YouTube_Embed_Handler' => 'includes/embeds/class-amp-youtube-embed',
'AMP_Analytics_Options_Submenu' => 'includes/options/class-amp-analytics-options-submenu',
'AMP_Options_Menu' => 'includes/options/class-amp-options-menu',
'AMP_Options_Manager' => 'includes/options/class-amp-options-manager',
'AMP_Analytics_Options_Submenu_Page' => 'includes/options/views/class-amp-analytics-options-submenu-page',
'AMP_Options_Menu_Page' => 'includes/options/views/class-amp-options-menu-page',
'AMP_Rule_Spec' => 'includes/sanitizers/class-amp-rule-spec',
'AMP_Allowed_Tags_Generated' => 'includes/sanitizers/class-amp-allowed-tags-generated',
'AMP_Audio_Sanitizer' => 'includes/sanitizers/class-amp-audio-sanitizer',
'AMP_Base_Sanitizer' => 'includes/sanitizers/class-amp-base-sanitizer',
'AMP_Blacklist_Sanitizer' => 'includes/sanitizers/class-amp-blacklist-sanitizer',
'AMP_Block_Sanitizer' => 'includes/sanitizers/class-amp-block-sanitizer',
'AMP_Gallery_Block_Sanitizer' => 'includes/sanitizers/class-amp-gallery-block-sanitizer',
'AMP_Iframe_Sanitizer' => 'includes/sanitizers/class-amp-iframe-sanitizer',
'AMP_Img_Sanitizer' => 'includes/sanitizers/class-amp-img-sanitizer',
'AMP_Nav_Menu_Toggle_Sanitizer' => 'includes/sanitizers/class-amp-nav-menu-toggle-sanitizer',
'AMP_Nav_Menu_Dropdown_Sanitizer' => 'includes/sanitizers/class-amp-nav-menu-dropdown-sanitizer',
'AMP_Comments_Sanitizer' => 'includes/sanitizers/class-amp-comments-sanitizer',
'AMP_Form_Sanitizer' => 'includes/sanitizers/class-amp-form-sanitizer',
'AMP_O2_Player_Sanitizer' => 'includes/sanitizers/class-amp-o2-player-sanitizer',
'AMP_Playbuzz_Sanitizer' => 'includes/sanitizers/class-amp-playbuzz-sanitizer',
'AMP_Style_Sanitizer' => 'includes/sanitizers/class-amp-style-sanitizer',
'AMP_Script_Sanitizer' => 'includes/sanitizers/class-amp-script-sanitizer',
'AMP_Embed_Sanitizer' => 'includes/sanitizers/class-amp-embed-sanitizer',
'AMP_Tag_And_Attribute_Sanitizer' => 'includes/sanitizers/class-amp-tag-and-attribute-sanitizer',
'AMP_Video_Sanitizer' => 'includes/sanitizers/class-amp-video-sanitizer',
'AMP_Core_Theme_Sanitizer' => 'includes/sanitizers/class-amp-core-theme-sanitizer',
'AMP_Noscript_Fallback' => 'includes/sanitizers/trait-amp-noscript-fallback',
'AMP_Customizer_Design_Settings' => 'includes/settings/class-amp-customizer-design-settings',
'AMP_Customizer_Settings' => 'includes/settings/class-amp-customizer-settings',
'AMP_Content' => 'includes/templates/class-amp-content',
'AMP_Content_Sanitizer' => 'includes/templates/class-amp-content-sanitizer',
'AMP_Post_Template' => 'includes/templates/class-amp-post-template',
'AMP_DOM_Utils' => 'includes/utils/class-amp-dom-utils',
'AMP_HTML_Utils' => 'includes/utils/class-amp-html-utils',
'AMP_Image_Dimension_Extractor' => 'includes/utils/class-amp-image-dimension-extractor',
'AMP_Validation_Manager' => 'includes/validation/class-amp-validation-manager',
'AMP_Validated_URL_Post_Type' => 'includes/validation/class-amp-validated-url-post-type',
'AMP_Validation_Error_Taxonomy' => 'includes/validation/class-amp-validation-error-taxonomy',
'AMP_CLI' => 'includes/class-amp-cli',
'AMP_String_Utils' => 'includes/utils/class-amp-string-utils',
'AMP_WP_Utils' => 'includes/utils/class-amp-wp-utils',
'AMP_Widget_Archives' => 'includes/widgets/class-amp-widget-archives',
'AMP_Widget_Categories' => 'includes/widgets/class-amp-widget-categories',
'AMP_Widget_Text' => 'includes/widgets/class-amp-widget-text',
'WPCOM_AMP_Polldaddy_Embed' => 'wpcom/class-amp-polldaddy-embed',
'AMP_Test_Stub_Sanitizer' => 'tests/stubs',
'AMP_Test_World_Sanitizer' => 'tests/stubs',
);
/**
* Is registered.
*
* @var bool
*/
public static $is_registered = false;
/**
* Perform the autoload on demand when requested by PHP runtime.
*
* Design Goal: Execute as few lines of code as possible each call.
*
* @since 0.6
*
* @param string $class_name Class name.
*/
protected static function autoload( $class_name ) {
if ( ! isset( self::$classmap[ $class_name ] ) ) {
return;
}
$filepath = self::$classmap[ $class_name ];
require AMP__DIR__ . "/{$filepath}.php";
}
/**
* Registers this autoloader to PHP.
*
* @since 0.6
*
* Called at the end of this file; calling a second time has no effect.
*/
public static function register() {
if ( file_exists( AMP__DIR__ . '/vendor/autoload.php' ) ) {
require_once AMP__DIR__ . '/vendor/autoload.php';
}
if ( ! self::$is_registered ) {
spl_autoload_register( array( __CLASS__, 'autoload' ) );
self::$is_registered = true;
}
}
/**
* Allows an extensions plugin to register a class and its file for autoloading
*
* @since 0.6
*
* @param string $class_name Full classname (include namespace if applicable).
* @param string $filepath Absolute filepath to class file, including .php extension.
*/
public static function register_autoload_class( $class_name, $filepath ) {
self::$classmap[ $class_name ] = '!' . $filepath;
}
}

View File

@ -0,0 +1,675 @@
<?php
/**
* Class AMP_CLI
*
* @package AMP
*/
/**
* Class AMP_CLI
*
* Registers and implements a WP-CLI command to crawl the entire site for AMP validity.
* To use this, run: wp amp validate-site.
*
* @since 1.0
*/
class AMP_CLI {
/**
* The WP-CLI flag to force validation.
*
* By default, the WP-CLI command does not validate templates that the user has opted-out of.
* For example, by unchecking 'Categories' in 'AMP Settings' > 'Supported Templates'.
* But with this flag, validation will ignore these options.
*
* @var string
*/
const FLAG_NAME_FORCE_VALIDATION = 'force';
/**
* The WP-CLI argument to validate based on certain conditionals
*
* For example, --include=is_tag,is_author
* Normally, this script will validate all of the templates that don't have AMP disabled.
* But this allows validating only specific templates.
*
* @var string
*/
const INCLUDE_ARGUMENT = 'include';
/**
* The WP-CLI argument for the maximum URLs to validate for each type.
*
* If this is passed in the command,
* it overrides the value of self::$maximum_urls_to_validate_for_each_type.
*
* @var string
*/
const LIMIT_URLS_ARGUMENT = 'limit';
/**
* The WP CLI progress bar.
*
* @see https://make.wordpress.org/cli/handbook/internal-api/wp-cli-utils-make-progress-bar/
* @var \cli\progress\Bar|\WP_CLI\NoOp
*/
public static $wp_cli_progress;
/**
* The total number of validation errors, regardless of whether they were accepted.
*
* @var int
*/
public static $total_errors = 0;
/**
* The total number of unaccepted validation errors.
*
* If an error has been accepted in the /wp-admin validation UI,
* it won't count toward this.
*
* @var int
*/
public static $unaccepted_errors = 0;
/**
* The number of URLs crawled, regardless of whether they have validation errors.
*
* @var int
*/
public static $number_crawled = 0;
/**
* Whether to force crawling of URLs.
*
* By default, this script only crawls URLs that support AMP,
* where the user has not opted-out of AMP for the URL.
* For example, by un-checking 'Posts' in 'AMP Settings' > 'Supported Templates'.
* Or un-checking 'Enable AMP' in the post's editor.
*
* @var int
*/
public static $force_crawl_urls = false;
/**
* A whitelist of conditionals to use for validation.
*
* Usually, this script will validate all of the templates that don't have AMP disabled.
* But this allows validating based on only these conditionals.
* This is set if the WP-CLI command has an --include argument.
*
* @var array
*/
public static $include_conditionals = array();
/**
* The maximum number of URLs to validate for each type.
*
* Templates are each a separate type, like those for is_category() and is_tag().
* Also, each post type is a separate type.
* This value is overridden if the WP-CLI command has an --limit argument, like --limit=10.
*
* @var int
*/
public static $limit_type_validate_count;
/**
* The validation counts by type, like template or post type.
*
* @var array[][] {
* Validity by type.
*
* @type int $valid The number of valid URLs for this type.
* @type int $total The total number of URLs for this type, valid or invalid.
* }
*/
public static $validity_by_type = array();
/**
* Crawl the entire site to get AMP validation results.
*
* ## OPTIONS
*
* [--limit=<count>]
* : The maximum number of URLs to validate for each template and content type.
* ---
* default: 100
* ---
*
* [--include=<templates>]
* : Only validates a URL if one of the conditionals is true.
*
* [--force]
* : Force validation of URLs even if their associated templates or object types do not have AMP enabled.
*
* ## EXAMPLES
*
* wp amp validate-site --include=is_author,is_tag
*
* @subcommand validate-site
* @param array $args Positional args.
* @param array $assoc_args Associative args.
* @throws Exception If an error happens.
*/
public function validate_site( $args, $assoc_args ) {
unset( $args );
self::$include_conditionals = array();
self::$force_crawl_urls = false;
self::$limit_type_validate_count = (int) $assoc_args[ self::LIMIT_URLS_ARGUMENT ];
/*
* Handle the argument and flag passed to the command: --include and --force.
* If the self::INCLUDE_ARGUMENT is present, force crawling or URLs.
* The WP-CLI command should indicate which templates are crawled, not the /wp-admin options.
*/
if ( ! empty( $assoc_args[ self::INCLUDE_ARGUMENT ] ) ) {
self::$include_conditionals = explode( ',', $assoc_args[ self::INCLUDE_ARGUMENT ] );
self::$force_crawl_urls = true;
} elseif ( isset( $assoc_args[ self::FLAG_NAME_FORCE_VALIDATION ] ) ) {
self::$force_crawl_urls = true;
}
if ( ! current_theme_supports( AMP_Theme_Support::SLUG ) ) {
if ( self::$force_crawl_urls ) {
/*
* There is no theme support added programmatically or via options.
* So make sure that theme support is present so that AMP_Validation_Manager::validate_url()
* will use a canonical URL as the basis for obtaining validation results.
*/
add_theme_support( AMP_Theme_Support::SLUG );
} else {
WP_CLI::error(
sprintf(
'Your templates are currently in Reader mode, which does not allow crawling the site. Please pass the --%s flag in order to force crawling for validation.',
self::FLAG_NAME_FORCE_VALIDATION
)
);
}
}
$number_urls_to_crawl = self::count_urls_to_validate();
if ( ! $number_urls_to_crawl ) {
if ( ! empty( self::$include_conditionals ) ) {
WP_CLI::error(
sprintf(
'The templates passed via the --%s argument did not match any URLs. You might try passing different templates to it.',
self::INCLUDE_ARGUMENT
)
);
} else {
WP_CLI::error(
sprintf(
'All of your templates might be unchecked in AMP Settings > Supported Templates. You might pass --%s to this command.',
self::FLAG_NAME_FORCE_VALIDATION
)
);
}
}
WP_CLI::log( 'Crawling the site for AMP validity.' );
self::$wp_cli_progress = WP_CLI\Utils\make_progress_bar(
sprintf( 'Validating %d URLs...', $number_urls_to_crawl ),
$number_urls_to_crawl
);
self::crawl_site();
self::$wp_cli_progress->finish();
$key_template_type = 'Template or content type';
$key_url_count = 'URL Count';
$key_validity_rate = 'Validity Rate';
$table_validation_by_type = array();
foreach ( self::$validity_by_type as $type_name => $validity ) {
$table_validation_by_type[] = array(
$key_template_type => $type_name,
$key_url_count => $validity['total'],
$key_validity_rate => sprintf( '%d%%', 100.0 * ( $validity['valid'] / $validity['total'] ) ),
);
}
if ( empty( $table_validation_by_type ) ) {
WP_CLI::error( 'No validation results were obtained from the URLs.' );
return;
}
WP_CLI::success(
sprintf(
'%3$d crawled URLs have unaccepted issue(s) out of %2$d total with AMP validation issue(s); %1$d URLs were crawled.',
self::$number_crawled,
self::$total_errors,
self::$unaccepted_errors
)
);
// Output a table of validity by template/content type.
WP_CLI\Utils\format_items(
'table',
$table_validation_by_type,
array( $key_template_type, $key_url_count, $key_validity_rate )
);
$url_more_details = add_query_arg(
'post_type',
AMP_Validated_URL_Post_Type::POST_TYPE_SLUG,
admin_url( 'edit.php' )
);
WP_CLI::line( sprintf( 'For more details, please see: %s', $url_more_details ) );
}
/**
* Reset all validation data on a site.
*
* This deletes all amp_validated_url posts and all amp_validation_error terms.
*
* ## OPTIONS
*
* [--yes]
* : Proceed to empty the site validation data without a confirmation prompt.
*
* ## EXAMPLES
*
* wp amp reset-site-validation --yes
*
* @subcommand reset-site-validation
* @param array $args Positional args. Unused.
* @param array $assoc_args Associative args.
* @throws Exception If an error happens.
*/
public function reset_site_validation( $args, $assoc_args ) {
unset( $args );
global $wpdb;
WP_CLI::confirm( 'Are you sure you want to empty all amp_validated_url posts and amp_validation_error taxonomy terms?', $assoc_args );
// Delete all posts.
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = %s", AMP_Validated_URL_Post_Type::POST_TYPE_SLUG ) );
$query = $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = %s", AMP_Validated_URL_Post_Type::POST_TYPE_SLUG );
$posts = new WP_CLI\Iterators\Query( $query, 10000 );
$progress = WP_CLI\Utils\make_progress_bar(
sprintf( 'Deleting %d amp_validated_url posts...', $count ),
$count
);
while ( $posts->valid() ) {
$post_id = $posts->current()->ID;
$posts->next();
wp_delete_post( $post_id, true );
$progress->tick();
}
$progress->finish();
// Delete all terms. Note that many terms should get deleted when their post counts go to zero above.
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $wpdb->term_taxonomy WHERE taxonomy = %s", AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ) );
$query = $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE taxonomy = %s", AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG );
$terms = new WP_CLI\Iterators\Query( $query, 10000 );
$progress = WP_CLI\Utils\make_progress_bar(
sprintf( 'Deleting %d amp_taxonomy_error terms...', $count ),
$count
);
while ( $terms->valid() ) {
$term_id = $terms->current()->term_id;
$terms->next();
wp_delete_term( $term_id, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG );
$progress->tick();
}
$progress->finish();
WP_CLI::success( 'All AMP validation data has been removed.' );
}
/**
* Gets the total number of URLs to validate.
*
* By default, this only counts AMP-enabled posts and terms.
* But if $force_crawl_urls is true, it counts all of them, regardless of their AMP status.
* It also uses self::$maximum_urls_to_validate_for_each_type,
* which can be overridden with a command line argument.
*
* @return int The number of URLs to validate.
*/
public static function count_urls_to_validate() {
/*
* If the homepage is set to 'Your latest posts,' start the $total_count at 1.
* Otherwise, it will probably be counted in the query for pages below.
*/
$total_count = 'posts' === get_option( 'show_on_front' ) && self::is_template_supported( 'is_home' ) ? 1 : 0;
$amp_enabled_taxonomies = array_filter(
get_taxonomies( array( 'public' => true ) ),
array( 'AMP_CLI', 'does_taxonomy_support_amp' )
);
// Count all public taxonomy terms.
foreach ( $amp_enabled_taxonomies as $taxonomy ) {
$term_query = new WP_Term_Query(
array(
'taxonomy' => $taxonomy,
'fields' => 'ids',
'number' => self::$limit_type_validate_count,
)
);
// If $term_query->terms is an empty array, passing it to count() will throw an error.
$total_count += ! empty( $term_query->terms ) ? count( $term_query->terms ) : 0;
}
// Count posts by type, like post, page, attachment, etc.
$public_post_types = get_post_types( array( 'public' => true ), 'names' );
foreach ( $public_post_types as $post_type ) {
$posts = self::get_posts_that_support_amp( self::get_posts_by_type( $post_type ) );
$total_count += ! empty( $posts ) ? count( $posts ) : 0;
}
// Count author pages, like https://example.com/author/admin/.
$total_count += count( self::get_author_page_urls() );
// Count a single example date page, like https://example.com/?year=2019.
if ( self::get_date_page() ) {
$total_count++;
}
// Count a single example search page, like https://example.com/?s=example.
if ( self::get_search_page() ) {
$total_count++;
}
return $total_count;
}
/**
* Gets the posts IDs that support AMP.
*
* By default, this only gets the post IDs if they support AMP.
* This means that 'Posts' isn't deselected in 'AMP Settings' > 'Supported Templates'.
* And 'Enable AMP' isn't unchecked in the post's editor.
* But if $force_crawl_urls is true, this simply returns all of the IDs.
*
* @param array $ids THe post IDs to check for AMP support.
* @return array The post IDs that support AMP, or an empty array.
*/
public static function get_posts_that_support_amp( $ids ) {
if ( ! self::is_template_supported( 'is_singular' ) ) {
return array();
}
if ( self::$force_crawl_urls ) {
return $ids;
}
return array_filter(
$ids,
function( $id ) {
return post_supports_amp( $id );
}
);
}
/**
* Gets whether the taxonomy supports AMP.
*
* This only gets the term IDs if they support AMP.
* If their taxonomy is unchecked in 'AMP Settings' > 'Supported Templates,' this does not return them.
* For example, if 'Categories' is unchecked.
* This can be overridden by passing the self::FLAG_NAME_FORCE_VALIDATION argument to the WP-CLI command.
*
* @param string $taxonomy The taxonomy.
* @return boolean Whether the taxonomy supports AMP.
*/
public static function does_taxonomy_support_amp( $taxonomy ) {
if ( 'post_tag' === $taxonomy ) {
$taxonomy = 'tag';
}
$taxonomy_key = 'is_' . $taxonomy;
$custom_taxonomy_key = sprintf( 'is_tax[%s]', $taxonomy );
return self::is_template_supported( $taxonomy_key ) || self::is_template_supported( $custom_taxonomy_key );
}
/**
* Gets whether the template is supported.
*
* If the user has passed an include argument to the WP-CLI command, use that to find if this template supports AMP.
* For example, wp amp validate-site --include=is_tag,is_category
* would return true only if is_tag() or is_category().
* But passing the self::FLAG_NAME_FORCE_VALIDATION argument to the WP-CLI command overrides this.
*
* @param string $template The template to check.
* @return bool Whether the template is supported.
*/
public static function is_template_supported( $template ) {
// If the --include argument is present in the WP-CLI command, this template conditional must be present in it.
if ( ! empty( self::$include_conditionals ) ) {
return in_array( $template, self::$include_conditionals, true );
}
if ( self::$force_crawl_urls ) {
return true;
}
$supportable_templates = AMP_Theme_Support::get_supportable_templates();
// Check whether this taxonomy's template is supported, including in the 'AMP Settings' > 'Supported Templates' UI.
return ! empty( $supportable_templates[ $template ]['supported'] );
}
/**
* Gets the IDs of public, published posts.
*
* @param string $post_type The post type.
* @param int|null $offset The offset of the query (optional).
* @param int|null $number The number of posts to query for (optional).
* @return int[] $post_ids The post IDs in an array.
*/
public static function get_posts_by_type( $post_type, $offset = null, $number = null ) {
$args = array(
'post_type' => $post_type,
'posts_per_page' => is_int( $number ) ? $number : self::$limit_type_validate_count,
'post_status' => 'publish',
'orderby' => 'ID',
'order' => 'DESC',
'fields' => 'ids',
);
if ( is_int( $offset ) ) {
$args['offset'] = $offset;
}
// Attachment posts usually have the post_status of 'inherit,' so they can use the status of the post they're attached to.
if ( 'attachment' === $post_type ) {
$args['post_status'] = 'inherit';
}
$query = new WP_Query( $args );
return $query->posts;
}
/**
* Gets the front-end links for taxonomy terms.
* For example, https://example.org/?cat=2
*
* @param string $taxonomy The name of the taxonomy, like 'category' or 'post_tag'.
* @param int|string $offset The number at which to offset the query (optional).
* @param int $number The maximum amount of links to get (optional).
* @return string[] The term links, as an array of strings.
*/
public static function get_taxonomy_links( $taxonomy, $offset = '', $number = 1 ) {
return array_map(
'get_term_link',
get_terms(
array_merge(
compact( 'taxonomy', 'offset', 'number' ),
array(
'orderby' => 'id',
)
)
)
);
}
/**
* Gets the author page URLs, like https://example.com/author/admin/.
*
* Accepts an $offset parameter, for the query of authors.
* 0 is the first author in the query, and 1 is the second.
*
* @param int|string $offset The offset for the URL to query for, should be an int if passing an argument.
* @param int|string $number The total number to query for, should be an int if passing an argument.
* @return array The author page URLs, or an empty array.
*/
public static function get_author_page_urls( $offset = '', $number = '' ) {
$author_page_urls = array();
if ( ! self::is_template_supported( 'is_author' ) ) {
return $author_page_urls;
}
$number = ! empty( $number ) ? $number : self::$limit_type_validate_count;
foreach ( get_users( compact( 'offset', 'number' ) ) as $author ) {
$author_page_urls[] = get_author_posts_url( $author->ID, $author->user_nicename );
}
return $author_page_urls;
}
/**
* Gets a single search page URL, like https://example.com/?s=example.
*
* @return string|null An example search page, or null.
*/
public static function get_search_page() {
if ( ! self::is_template_supported( 'is_search' ) ) {
return null;
}
return add_query_arg( 's', 'example', home_url( '/' ) );
}
/**
* Gets a single date page URL, like https://example.com/?year=2018.
*
* @return string|null An example search page, or null.
*/
public static function get_date_page() {
if ( ! self::is_template_supported( 'is_date' ) ) {
return null;
}
return add_query_arg( 'year', date( 'Y' ), home_url( '/' ) );
}
/**
* Validates the URLs of the entire site.
*
* Includes the URLs of public, published posts, public taxonomies, and other templates.
* This validates one of each type at a time,
* and iterates until it reaches the maximum number of URLs for each type.
*/
public static function crawl_site() {
/*
* If 'Your homepage displays' is set to 'Your latest posts', validate the homepage.
* It will not be part of the page validation below.
*/
if ( 'posts' === get_option( 'show_on_front' ) && self::is_template_supported( 'is_home' ) ) {
self::validate_and_store_url( home_url( '/' ), 'home' );
}
$amp_enabled_taxonomies = array_filter(
get_taxonomies( array( 'public' => true ) ),
array( 'AMP_CLI', 'does_taxonomy_support_amp' )
);
$public_post_types = get_post_types( array( 'public' => true ), 'names' );
// Validate one URL of each template/content type, then another URL of each type on the next iteration.
for ( $i = 0; $i < self::$limit_type_validate_count; $i++ ) {
// Validate all public, published posts.
foreach ( $public_post_types as $post_type ) {
$post_ids = self::get_posts_that_support_amp( self::get_posts_by_type( $post_type, $i, 1 ) );
if ( ! empty( $post_ids[0] ) ) {
self::validate_and_store_url( get_permalink( $post_ids[0] ), $post_type );
}
}
foreach ( $amp_enabled_taxonomies as $taxonomy ) {
$taxonomy_links = self::get_taxonomy_links( $taxonomy, $i, 1 );
$link = reset( $taxonomy_links );
if ( ! empty( $link ) ) {
self::validate_and_store_url( $link, $taxonomy );
}
}
$author_page_urls = self::get_author_page_urls( $i, 1 );
if ( ! empty( $author_page_urls[0] ) ) {
self::validate_and_store_url( $author_page_urls[0], 'author' );
}
}
// Only validate 1 date and 1 search page.
$url = self::get_date_page();
if ( $url ) {
self::validate_and_store_url( $url, 'date' );
}
$url = self::get_search_page();
if ( $url ) {
self::validate_and_store_url( $url, 'search' );
}
}
/**
* Validates the URL, stores the results, and increments the counts.
*
* @param string $url The URL to validate.
* @param string $type The type of template, post, or taxonomy.
*/
public static function validate_and_store_url( $url, $type ) {
$validity = AMP_Validation_Manager::validate_url( $url );
/*
* If the request to validate this returns a WP_Error, return.
* One cause of an error is if the validation request results in a 404 response code.
*/
if ( is_wp_error( $validity ) ) {
WP_CLI::warning( sprintf( 'Validate URL error (%1$s): %2$s URL: %3$s', $validity->get_error_code(), $validity->get_error_message(), $url ) );
return;
}
if ( self::$wp_cli_progress ) {
self::$wp_cli_progress->tick();
}
$validation_errors = wp_list_pluck( $validity['results'], 'error' );
AMP_Validated_URL_Post_Type::store_validation_errors(
$validation_errors,
$validity['url'],
wp_array_slice_assoc( $validity, array( 'queried_object' ) )
);
$unaccepted_error_count = count(
array_filter(
$validation_errors,
function( $error ) {
$validation_status = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization( $error );
return (
AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_ACCEPTED_STATUS !== $validation_status['term_status']
&&
AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS !== $validation_status['term_status']
);
}
)
);
if ( count( $validation_errors ) > 0 ) {
self::$total_errors++;
}
if ( $unaccepted_error_count > 0 ) {
self::$unaccepted_errors++;
}
self::$number_crawled++;
if ( ! isset( self::$validity_by_type[ $type ] ) ) {
self::$validity_by_type[ $type ] = array(
'valid' => 0,
'total' => 0,
);
}
self::$validity_by_type[ $type ]['total']++;
if ( 0 === $unaccepted_error_count ) {
self::$validity_by_type[ $type ]['valid']++;
}
}
}

View File

@ -0,0 +1,127 @@
<?php
/**
* Class AMP_Comment_Walker
*
* @deprecated 1.1.0 This functionality was moved to AMP_Comments_Sanitizer
* @package AMP
*/
/* translators: 1: AMP_Comment_Walker. 2: AMP_Comments_Sanitizer. */
_deprecated_file( __FILE__, '1.1', null, sprintf( esc_html__( '%1$s functionality has been moved to %2$s.', 'amp' ), 'AMP_Comment_Walker', 'AMP_Comments_Sanitizer' ) );
/**
* Class AMP_Comment_Walker
*
* Walker to wrap comments in mustache tags for amp-template.
*
* @deprecated 1.1.0 This functionality was moved to AMP_Comments_Sanitizer
*/
class AMP_Comment_Walker extends Walker_Comment {
/**
* The original comments arguments.
*
* @since 0.7
* @var array
*/
public $args;
/**
* Holds the timestamp of the most recent comment in a thread.
*
* @since 0.7
* @var array
*/
private $comment_thread_age = array();
/**
* Starts the element output.
*
* @since 0.7.0
*
* @see Walker::start_el()
* @see wp_list_comments()
* @global int $comment_depth
* @global WP_Comment $comment
*
* @param string $output Used to append additional content. Passed by reference.
* @param WP_Comment $comment Comment data object.
* @param int $depth Optional. Depth of the current comment in reference to parents. Default 0.
* @param array $args Optional. An array of arguments. Default empty array.
* @param int $id Optional. ID of the current comment. Default 0 (unused).
*/
public function start_el( &$output, $comment, $depth = 0, $args = array(), $id = 0 ) {
$new_out = '';
parent::start_el( $new_out, $comment, $depth, $args, $id );
if ( 'div' === $args['style'] ) {
$tag = '<div';
} else {
$tag = '<li';
}
$new_tag = $tag . ' data-sort-time="' . esc_attr( strtotime( $comment->comment_date ) ) . '"';
if ( ! empty( $this->comment_thread_age[ $comment->comment_ID ] ) ) {
$new_tag .= ' data-update-time="' . esc_attr( $this->comment_thread_age[ $comment->comment_ID ] ) . '"';
}
$output .= $new_tag . substr( ltrim( $new_out ), strlen( $tag ) );
}
/**
* Output amp-list template code and place holder for comments.
*
* @since 0.7
* @see Walker::paged_walk()
*
* @param WP_Comment[] $elements List of comment Elements.
* @param int $max_depth The maximum hierarchical depth.
* @param int $page_num The specific page number, beginning with 1.
* @param int $per_page Per page counter.
*
* @return string XHTML of the specified page of elements.
*/
public function paged_walk( $elements, $max_depth, $page_num, $per_page ) {
if ( empty( $elements ) || $max_depth < -1 ) {
return '';
}
$this->build_thread_latest_date( $elements );
$args = array_slice( func_get_args(), 4 );
return parent::paged_walk( $elements, $max_depth, $page_num, $per_page, $args[0] );
}
/**
* Find the timestamp of the latest child comment of a thread to set the updated time.
*
* @since 0.7
*
* @param WP_Comment[] $elements The list of comments to get thread times for.
* @param int $time $the timestamp to check against.
* @param bool $is_child Flag used to set the the value or return the time.
* @return int Latest time.
*/
protected function build_thread_latest_date( $elements, $time = 0, $is_child = false ) {
foreach ( $elements as $element ) {
$children = $element->get_children();
$this_time = strtotime( $element->comment_date );
if ( ! empty( $children ) ) {
$this_time = $this->build_thread_latest_date( $children, $this_time, true );
}
if ( $this_time > $time ) {
$time = $this_time;
}
if ( false === $is_child ) {
$this->comment_thread_age[ $element->comment_ID ] = $time;
}
}
return $time;
}
}

View File

@ -0,0 +1,448 @@
<?php
/**
* Class AMP_HTTP
*
* @since 1.0
* @package AMP
*/
/**
* Class AMP_HTTP
*/
class AMP_HTTP {
/**
* Headers sent (or attempted to be sent).
*
* @since 1.0
* @see AMP_HTTP::send_header()
* @var array[]
*/
public static $headers_sent = array();
/**
* AMP-specific query vars that were purged.
*
* @since 0.7
* @since 1.0 Moved to AMP_HTTP class.
* @see AMP_HTTP::purge_amp_query_vars()
* @var string[]
*/
public static $purged_amp_query_vars = array();
/**
* Send an HTTP response header.
*
* This largely exists to facilitate unit testing but it also provides a better interface for sending headers.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class.
*
* @param string $name Header name.
* @param string $value Header value.
* @param array $args {
* Args to header().
*
* @type bool $replace Whether to replace a header previously sent. Default true.
* @type int $status_code Status code to send with the sent header.
* }
* @return bool Whether the header was sent.
*/
public static function send_header( $name, $value, $args = array() ) {
$args = array_merge(
array(
'replace' => true,
'status_code' => null,
),
$args
);
self::$headers_sent[] = array_merge( compact( 'name', 'value' ), $args );
if ( headers_sent() ) {
return false;
}
header(
sprintf( '%s: %s', $name, $value ),
$args['replace'],
$args['status_code']
);
return true;
}
/**
* Send Server-Timing header.
*
* If WP_DEBUG is not enabled and an admin user (who can manage_options) is not logged-in, the Server-Header will not be sent.
*
* @since 1.0
*
* @param string $name Name.
* @param float $duration Duration. If negative, will be added to microtime( true ). Optional.
* @param string $description Description. Optional.
* @return bool Return value of send_header call. If WP_DEBUG is not enabled or admin user (who can manage_options) is not logged-in, this will always return false.
*/
public static function send_server_timing( $name, $duration = null, $description = null ) {
if ( ! WP_DEBUG && ! current_user_can( 'manage_options' ) ) {
return false;
}
$value = $name;
if ( isset( $description ) ) {
$value .= sprintf( ';desc="%s"', str_replace( array( '\\', '"' ), '', substr( $description, 0, 100 ) ) );
}
if ( isset( $duration ) ) {
if ( $duration < 0 ) {
$duration = microtime( true ) + $duration;
}
$value .= sprintf( ';dur=%f', $duration * 1000 );
}
return self::send_header( 'Server-Timing', $value, array( 'replace' => false ) );
}
/**
* Remove query vars that come in requests such as for amp-live-list.
*
* WordPress should generally not respond differently to requests when these parameters
* are present. In some cases, when a query param such as __amp_source_origin is present
* then it would normally get included into pagination links generated by get_pagenum_link().
* The whitelist sanitizer empties out links that contain this string as it matches the
* blacklisted_value_regex. So by preemptively scrubbing any reference to these query vars
* we can ensure that WordPress won't end up referencing them in any way.
*
* @since 0.7
* @since 1.0 Moved to AMP_HTTP class.
*/
public static function purge_amp_query_vars() {
$query_vars = array(
'__amp_source_origin',
'_wp_amp_action_xhr_converted',
'amp_latest_update_time',
'amp_last_check_time',
);
// Scrub input vars.
foreach ( $query_vars as $query_var ) {
if ( ! isset( $_GET[ $query_var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
continue;
}
self::$purged_amp_query_vars[ $query_var ] = wp_unslash( $_GET[ $query_var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
unset( $_REQUEST[ $query_var ], $_GET[ $query_var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$scrubbed = true;
}
if ( isset( $scrubbed ) ) {
$build_query = function ( $query ) use ( $query_vars ) {
$pattern = '/^(' . join( '|', $query_vars ) . ')(?==|$)/';
$pairs = array();
foreach ( explode( '&', $query ) as $pair ) {
if ( ! preg_match( $pattern, $pair ) ) {
$pairs[] = $pair;
}
}
return join( '&', $pairs );
};
// Scrub QUERY_STRING.
if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
$_SERVER['QUERY_STRING'] = $build_query( $_SERVER['QUERY_STRING'] );
}
// Scrub REQUEST_URI.
if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
list( $path, $query ) = explode( '?', $_SERVER['REQUEST_URI'], 2 );
$pairs = $build_query( $query );
$_SERVER['REQUEST_URI'] = $path;
if ( ! empty( $pairs ) ) {
$_SERVER['REQUEST_URI'] .= "?{$pairs}";
}
}
}
}
/**
* Filter the allowed redirect hosts to include AMP caches.
*
* @since 1.0
*
* @param array $allowed_hosts Allowed hosts.
* @return array Allowed redirect hosts.
*/
public static function filter_allowed_redirect_hosts( $allowed_hosts ) {
return array_merge( $allowed_hosts, self::get_amp_cache_hosts() );
}
/**
* Get list of AMP cache hosts (that is, CORS origins).
*
* @since 1.0
* @link https://www.ampproject.org/docs/fundamentals/amp-cors-requests#1)-allow-requests-for-specific-cors-origins
*
* @return array AMP cache hosts.
*/
public static function get_amp_cache_hosts() {
$hosts = array();
// Google AMP Cache (legacy).
$hosts[] = 'cdn.ampproject.org';
// From the publishers own origins.
$domains = array_unique(
array(
wp_parse_url( site_url(), PHP_URL_HOST ),
wp_parse_url( home_url(), PHP_URL_HOST ),
)
);
/*
* From AMP docs:
* "When possible, the Google AMP Cache will create a subdomain for each AMP document's domain by first converting it
* from IDN (punycode) to UTF-8. The caches replaces every - (dash) with -- (2 dashes) and replace every . (dot) with
* - (dash). For example, pub.com will map to pub-com.cdn.ampproject.org."
*/
foreach ( $domains as $domain ) {
if ( function_exists( 'idn_to_utf8' ) ) {
// The third parameter is set explicitly to prevent issues with newer PHP versions compiled with an old ICU version.
// phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated
$domain = idn_to_utf8( $domain, IDNA_DEFAULT, defined( 'INTL_IDNA_VARIANT_UTS46' ) ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003 );
}
$subdomain = str_replace( '-', '--', $domain );
$subdomain = str_replace( '.', '-', $subdomain );
// Google AMP Cache subdomain.
$hosts[] = sprintf( '%s.cdn.ampproject.org', $subdomain );
// Cloudflare AMP Cache.
$hosts[] = sprintf( '%s.amp.cloudflare.com', $subdomain );
// Bing AMP Cache.
$hosts[] = sprintf( '%s.bing-amp.com', $subdomain );
}
return $hosts;
}
/**
* Send cors headers.
*
* From the AMP docs:
* Restrict requests to source origins
* In all fetch requests, the AMP Runtime passes the "__amp_source_origin" query parameter, which contains
* the value of the source origin (for example, "https://publisher1.com").
*
* To restrict requests to only source origins, check that the value of the "__amp_source_origin" parameter
* is within a set of the Publisher's own origins.
*
* Access-Control-Allow-Origin: <origin>
* This header is a W3 CORS Spec requirement, where origin refers to the requesting origin that was allowed
* via the CORS Origin request header (for example, "https://<publisher's subdomain>.cdn.ampproject.org").
*
* Although the W3 CORS spec allows the value of * to be returned in the response, for improved security, you should:
*
* - If the Origin header is present, validate and echo the value of the Origin header.
* - If the Origin header isn't present, validate and echo the value of the "__amp_source_origin".
*
* (Otherwise, no Access-Control-Allow-Origin header is sent.)
*
* AMP-Access-Control-Allow-Source-Origin: <source-origin>
* This header allows the specified source-origin to read the authorization response. The source-origin is
* the value specified and verified in the "__amp_source_origin" URL parameter (for example, "https://publisher1.com").
*
* Access-Control-Expose-Headers: AMP-Access-Control-Allow-Source-Origin
* This header simply allows the CORS response to contain the AMP-Access-Control-Allow-Source-Origin header.
*
* @link https://www.ampproject.org/docs/fundamentals/amp-cors-requests
* @since 1.0
*/
public static function send_cors_headers() {
$origin = null;
$source_origin = null;
if ( isset( $_SERVER['HTTP_ORIGIN'] ) ) {
$origin = wp_validate_redirect( wp_sanitize_redirect( esc_url_raw( wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ) ) );
}
if ( isset( self::$purged_amp_query_vars['__amp_source_origin'] ) ) {
$source_origin = wp_validate_redirect( wp_sanitize_redirect( esc_url_raw( self::$purged_amp_query_vars['__amp_source_origin'] ) ) );
}
if ( ! $origin ) {
$origin = $source_origin;
}
if ( $origin ) {
self::send_header( 'Access-Control-Allow-Origin', $origin, array( 'replace' => false ) );
self::send_header( 'Access-Control-Allow-Credentials', 'true' );
self::send_header( 'Vary', 'Origin', array( 'replace' => false ) );
}
if ( $source_origin ) {
self::send_header( 'AMP-Access-Control-Allow-Source-Origin', $source_origin );
self::send_header( 'Access-Control-Expose-Headers', 'AMP-Access-Control-Allow-Source-Origin', array( 'replace' => false ) );
}
}
/**
* Hook into a POST form submissions, such as the comment form or some other form submission.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class. Extracted some logic to send_cors_headers method.
*/
public static function handle_xhr_request() {
$is_amp_xhr = (
! empty( self::$purged_amp_query_vars['_wp_amp_action_xhr_converted'] )
&&
( ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] )
);
if ( ! $is_amp_xhr ) {
return;
}
// Intercept POST requests which redirect.
add_filter( 'wp_redirect', array( __CLASS__, 'intercept_post_request_redirect' ), PHP_INT_MAX );
// Add special handling for redirecting after comment submission.
add_filter( 'comment_post_redirect', array( __CLASS__, 'filter_comment_post_redirect' ), PHP_INT_MAX, 2 );
// Add die handler for AMP error display, most likely due to problem with comment.
add_filter(
'wp_die_handler',
function () {
return array( __CLASS__, 'handle_wp_die' );
}
);
}
/**
* Intercept the response to a POST request.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class.
* @see wp_redirect()
*
* @param string $location The location to redirect to.
*/
public static function intercept_post_request_redirect( $location ) {
// Make sure relative redirects get made absolute.
$parsed_location = array_merge(
array(
'scheme' => 'https',
'host' => wp_parse_url( home_url(), PHP_URL_HOST ),
'path' => isset( $_SERVER['REQUEST_URI'] ) ? strtok( wp_unslash( $_SERVER['REQUEST_URI'] ), '?' ) : '/',
),
wp_parse_url( $location )
);
$absolute_location = '';
if ( 'https' === $parsed_location['scheme'] ) {
$absolute_location .= $parsed_location['scheme'] . ':';
}
$absolute_location .= '//' . $parsed_location['host'];
if ( isset( $parsed_location['port'] ) ) {
$absolute_location .= ':' . $parsed_location['port'];
}
$absolute_location .= $parsed_location['path'];
if ( isset( $parsed_location['query'] ) ) {
$absolute_location .= '?' . $parsed_location['query'];
}
if ( isset( $parsed_location['fragment'] ) ) {
$absolute_location .= '#' . $parsed_location['fragment'];
}
self::send_header( 'AMP-Redirect-To', $absolute_location );
self::send_header( 'Access-Control-Expose-Headers', 'AMP-Redirect-To', array( 'replace' => false ) );
wp_send_json_success();
}
/**
* New error handler for AMP form submission.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class.
* @see wp_die()
*
* @param WP_Error|string $error The error to handle.
* @param string|int $title Optional. Error title. If `$message` is a `WP_Error` object,
* error data with the key 'title' may be used to specify the title.
* If `$title` is an integer, then it is treated as the response
* code. Default empty.
* @param string|array|int $args {
* Optional. Arguments to control behavior. If `$args` is an integer, then it is treated
* as the response code. Default empty array.
*
* @type int $response The HTTP response code. Default 200 for Ajax requests, 500 otherwise.
* }
*/
public static function handle_wp_die( $error, $title = '', $args = array() ) {
if ( is_int( $title ) ) {
$status_code = $title;
} elseif ( is_int( $args ) ) {
$status_code = $args;
} elseif ( is_array( $args ) && isset( $args['response'] ) ) {
$status_code = $args['response'];
} else {
$status_code = 500;
}
status_header( $status_code );
if ( is_wp_error( $error ) ) {
$error = $error->get_error_message();
}
// Message will be shown in template defined by AMP_Theme_Support::amend_comment_form().
wp_send_json(
array(
'error' => amp_wp_kses_mustache( $error ),
)
);
}
/**
* Handle comment_post_redirect to ensure page reload is done when comments_live_list is not supported, while sending back a success message when it is.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class.
*
* @param string $url Comment permalink to redirect to.
* @param WP_Comment $comment Posted comment.
*
* @return string|null URL if redirect to be done; otherwise function will exist.
*/
public static function filter_comment_post_redirect( $url, $comment ) {
$theme_support = AMP_Theme_Support::get_theme_support_args();
// Cause a page refresh if amp-live-list is not implemented for comments via add_theme_support( AMP_Theme_Support::SLUG, array( 'comments_live_list' => true ) ).
if ( empty( $theme_support['comments_live_list'] ) ) {
/*
* Add the comment ID to the URL to force AMP to refresh the page.
* This is ideally a temporary workaround to deal with https://github.com/ampproject/amphtml/issues/14170
*/
$url = add_query_arg( 'comment', $comment->comment_ID, $url );
// Pass URL along to wp_redirect().
return $url;
}
// Create a success message to display to the user.
if ( '1' === (string) $comment->comment_approved ) {
$message = __( 'Your comment has been posted.', 'amp' );
} else {
$message = __( 'Your comment is awaiting moderation.', 'amp' );
}
/**
* Filters the message when comment submitted success message when
*
* @since 0.7
*/
$message = apply_filters( 'amp_comment_posted_message', $message, $comment );
// Message will be shown in template defined by AMP_Theme_Support::amend_comment_form().
wp_send_json(
array(
'message' => amp_wp_kses_mustache( $message ),
)
);
return null;
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* AMP Post type support.
*
* @package AMP
* @since 0.6
*/
/**
* Class AMP_Post_Type_Support.
*/
class AMP_Post_Type_Support {
/**
* Post type support slug.
*
* @var string
*/
const SLUG = 'amp';
/**
* Get post types that plugin supports out of the box (which cannot be disabled).
*
* @deprecated
* @return string[] Post types.
*/
public static function get_builtin_supported_post_types() {
_deprecated_function( __METHOD__, '1.0' );
return array_filter( array( 'post' ), 'post_type_exists' );
}
/**
* Get post types that are eligible for AMP support.
*
* @since 0.6
* @return string[] Post types eligible for AMP.
*/
public static function get_eligible_post_types() {
return array_values(
get_post_types(
array(
'public' => true,
),
'names'
)
);
}
/**
* Declare support for post types.
*
* This function should only be invoked through the 'after_setup_theme' action to
* allow plugins/theme to overwrite the post types support.
*
* @since 0.6
*/
public static function add_post_type_support() {
if ( current_theme_supports( AMP_Theme_Support::SLUG ) && AMP_Options_Manager::get_option( 'all_templates_supported' ) ) {
$post_types = self::get_eligible_post_types();
} else {
$post_types = AMP_Options_Manager::get_option( 'supported_post_types', array() );
}
foreach ( $post_types as $post_type ) {
add_post_type_support( $post_type, self::SLUG );
}
}
/**
* Return error codes for why a given post does not have AMP support.
*
* @since 0.6
*
* @param WP_Post|int $post Post.
* @return array Error codes for why a given post does not have AMP support.
*/
public static function get_support_errors( $post ) {
if ( ! ( $post instanceof WP_Post ) ) {
$post = get_post( $post );
}
$errors = array();
if ( ! post_type_supports( $post->post_type, self::SLUG ) ) {
$errors[] = 'post-type-support';
}
if ( post_password_required( $post ) ) {
$errors[] = 'password-protected';
}
/**
* Filters whether to skip the post from AMP.
*
* @since 0.3
*
* @param bool $skipped Skipped.
* @param int $post_id Post ID.
* @param WP_Post $post Post.
*/
if ( isset( $post->ID ) && true === apply_filters( 'amp_skip_post', false, $post->ID, $post ) ) {
$errors[] = 'skip-post';
}
$status = get_post_meta( $post->ID, AMP_Post_Meta_Box::STATUS_POST_META_KEY, true );
if ( $status ) {
if ( AMP_Post_Meta_Box::DISABLED_STATUS === $status ) {
$errors[] = 'post-status-disabled';
}
} else {
/*
* Disabled by default for custom page templates, page on front and page for posts, unless 'amp' theme
* support is present (in which case AMP_Theme_Support::get_template_availability() determines availability).
*/
$enabled = (
current_theme_supports( AMP_Theme_Support::SLUG )
||
(
! (bool) get_page_template_slug( $post )
&&
! (
'page' === $post->post_type
&&
'page' === get_option( 'show_on_front' )
&&
in_array(
(int) $post->ID,
array(
(int) get_option( 'page_on_front' ),
(int) get_option( 'page_for_posts' ),
),
true
)
)
)
);
/**
* Filters whether default AMP status should be enabled or not.
*
* @since 0.6
*
* @param string $status Status.
* @param WP_Post $post Post.
*/
$enabled = apply_filters( 'amp_post_status_default_enabled', $enabled, $post );
if ( ! $enabled ) {
$errors[] = 'post-status-disabled';
}
}
return $errors;
}
}

View File

@ -0,0 +1,347 @@
<?php
/**
* AMP Service Workers.
*
* @package AMP
* @since 1.1
*/
/**
* Class AMP_Service_Worker.
*/
class AMP_Service_Worker {
/**
* Query var that is used to signal a request to install the service worker in an iframe.
*
* @link https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#data-iframe-src-(optional)
*/
const INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR = 'amp_install_service_worker_iframe';
/**
* Init.
*/
public static function init() {
if ( ! class_exists( 'WP_Service_Workers' ) ) {
return;
}
// Shim support for service worker installation from PWA feature plugin.
add_filter( 'query_vars', array( __CLASS__, 'add_query_var' ) );
add_action( 'parse_request', array( __CLASS__, 'handle_service_worker_iframe_install' ) );
add_action( 'wp', array( __CLASS__, 'add_install_hooks' ) );
$theme_support = AMP_Theme_Support::get_theme_support_args();
if ( isset( $theme_support['service_worker'] ) && false === $theme_support['service_worker'] ) {
return;
}
/*
* The default-enabled options reflect which features are not commented-out in the AMP-by-Example service worker.
* See <https://github.com/ampproject/amp-by-example/blob/e093edb401b1617859b5365e80b639d81b06f058/boilerplate-generator/templates/files/serviceworkerJs.js>.
*/
$enabled_options = array(
'cdn_script_caching' => true,
'image_caching' => false,
'google_fonts_caching' => false,
);
if ( isset( $theme_support['service_worker'] ) && is_array( $theme_support['service_worker'] ) ) {
$enabled_options = array_merge(
$enabled_options,
$theme_support['service_worker']
);
}
if ( $enabled_options['cdn_script_caching'] ) {
add_action( 'wp_front_service_worker', array( __CLASS__, 'add_cdn_script_caching' ) );
}
if ( $enabled_options['image_caching'] ) {
add_action( 'wp_front_service_worker', array( __CLASS__, 'add_image_caching' ) );
}
if ( $enabled_options['google_fonts_caching'] ) {
add_action( 'wp_front_service_worker', array( __CLASS__, 'add_google_fonts_caching' ) );
}
}
/**
* Add query var for iframe service worker request.
*
* @param array $vars Query vars.
* @return array Amended query vars.
*/
public static function add_query_var( $vars ) {
$vars[] = self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR;
return $vars;
}
/**
* Add runtime caching for scripts loaded from the AMP CDN with a stale-while-revalidate strategy.
*
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js
*
* @param WP_Service_Worker_Scripts $service_workers Service worker registry.
*/
public static function add_cdn_script_caching( $service_workers ) {
if ( ! ( $service_workers instanceof WP_Service_Worker_Scripts ) ) {
/* translators: %s: WP_Service_Worker_Cache_Registry. */
_doing_it_wrong( __METHOD__, sprintf( esc_html__( 'Please update to PWA v0.2. Expected argument to be %s.', 'amp' ), 'WP_Service_Worker_Cache_Registry' ), '1.1' );
return;
}
// Add AMP scripts to runtime cache which will then get stale-while-revalidate strategy.
$service_workers->register(
'amp-cdn-runtime-caching',
function() {
$urls = AMP_Service_Worker::get_precached_script_cdn_urls();
if ( empty( $urls ) ) {
return '';
}
$js = file_get_contents( AMP__DIR__ . '/assets/js/amp-service-worker-runtime-precaching.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents
$js = preg_replace( '#/\*\s*global.+?\*/#', '', $js );
$js = str_replace(
'URLS',
wp_json_encode( $urls ),
$js
);
return $js;
}
);
// Serve the AMP Runtime from cache and check for an updated version in the background. See <https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L54-L58>.
$service_workers->caching_routes()->register(
'^https:\/\/cdn\.ampproject\.org\/.*',
array(
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_STALE_WHILE_REVALIDATE,
)
);
}
/**
* Add runtime image caching from the origin with a cache-first strategy.
*
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L60-L74
*
* @param WP_Service_Worker_Scripts $service_workers Service workers.
*/
public static function add_image_caching( $service_workers ) {
if ( ! ( $service_workers instanceof WP_Service_Worker_Scripts ) ) {
_doing_it_wrong( __METHOD__, esc_html__( 'Please update to PWA v0.2. Expected argument to be WP_Service_Worker_Scripts.', 'amp' ), '1.1' );
return;
}
$service_workers->caching_routes()->register(
'^' . preg_quote( set_url_scheme( content_url( '/' ), 'https' ), '/' ) . '[^\?]+?\.(?:png|gif|jpg|jpeg|svg|webp)(\?.*)?$',
array(
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST,
'cacheName' => 'images',
'plugins' => array(
'cacheableResponse' => array(
'statuses' => array( 0, 200 ),
),
'expiration' => array(
'maxEntries' => 60,
'maxAgeSeconds' => MONTH_IN_SECONDS,
),
),
)
);
}
/**
* Add runtime caching of Google Fonts with stale-while-revalidate strategy for stylesheets and cache-first strategy for webfont files.
*
* @link https://developers.google.com/web/tools/workbox/guides/common-recipes#google_fonts
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L76-L103
* @link https://github.com/xwp/pwa-wp/blob/master/integrations/class-wp-service-worker-fonts-integration.php
*
* @param WP_Service_Worker_Scripts $service_workers Service workers.
*/
public static function add_google_fonts_caching( $service_workers ) {
if ( ! ( $service_workers instanceof WP_Service_Worker_Scripts ) ) {
_doing_it_wrong( __METHOD__, esc_html__( 'Please update to PWA v0.2. Expected argument to be WP_Service_Worker_Scripts.', 'amp' ), '1.1' );
return;
}
// The PWA plugin also automatically adds runtime caching for Google Fonts when WP_SERVICE_WORKER_INTEGRATIONS_ENABLED is set.
if ( class_exists( 'WP_Service_Worker_Fonts_Integration' ) ) {
return;
}
// Cache the Google Fonts stylesheets with a stale while revalidate strategy.
$service_workers->caching_routes()->register(
'^https:\/\/fonts\.googleapis\.com',
array(
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_STALE_WHILE_REVALIDATE,
'cacheName' => 'google-fonts-stylesheets',
)
);
// Cache the Google Fonts webfont files with a cache first strategy for 1 year.
$service_workers->caching_routes()->register(
'^https:\/\/fonts\.gstatic\.com',
array(
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST,
'cacheName' => 'google-fonts-webfonts',
'plugins' => array(
'cacheableResponse' => array(
'statuses' => array( 0, 200 ),
),
'expiration' => array(
'maxAgeSeconds' => YEAR_IN_SECONDS,
'maxEntries' => 30,
),
),
)
);
}
/**
* Register URLs that will be precached in the runtime cache. (Yes, this sounds somewhat strange.)
*
* Note that the PWA plugin handles the precaching of custom logo, custom header,
* and custom background. The PWA plugin also handles precaching & serving of the
* offline/500 error pages and enabling navigation preload.
*
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L9-L22
* @see AMP_Service_Worker::add_cdn_script_caching()
*
* @return array Runtime pre-cached URLs.
*/
public static function get_precached_script_cdn_urls() {
// List of AMP scripts that we know will be used in WordPress always.
$precached_handles = array(
'amp-runtime',
'amp-bind', // Used by comments.
'amp-form', // Used by comments.
'amp-install-serviceworker',
);
$theme_support = AMP_Theme_Support::get_theme_support_args();
if ( ! empty( $theme_support['comments_live_list'] ) ) {
$precached_handles[] = 'amp-live-list';
}
if ( amp_get_analytics() ) {
$precached_handles[] = 'amp-analytics';
}
$urls = array();
foreach ( $precached_handles as $handle ) {
if ( wp_script_is( $handle, 'registered' ) ) {
$urls[] = wp_scripts()->registered[ $handle ]->src;
}
}
return $urls;
}
/**
* Add hooks to install the service worker from AMP page.
*/
public static function add_install_hooks() {
if ( current_theme_supports( 'amp' ) && is_amp_endpoint() ) {
add_action( 'wp_footer', array( __CLASS__, 'install_service_worker' ) );
// Prevent validation error due to the script that installs the service worker on non-AMP pages.
$priority = has_action( 'wp_print_scripts', 'wp_print_service_workers' );
if ( false !== $priority ) {
remove_action( 'wp_print_scripts', 'wp_print_service_workers', $priority );
}
}
// Reader mode integration.
add_action( 'amp_post_template_footer', array( __CLASS__, 'install_service_worker' ) );
add_filter(
'amp_post_template_data',
function ( $data ) {
$data['amp_component_scripts']['amp-install-serviceworker'] = true;
return $data;
}
);
}
/**
* Install service worker(s).
*
* @since 1.1
* @see wp_print_service_workers()
* @link https://github.com/xwp/pwa-wp
*/
public static function install_service_worker() {
if ( ! function_exists( 'wp_service_workers' ) || ! function_exists( 'wp_get_service_worker_url' ) ) {
return;
}
$src = wp_get_service_worker_url( WP_Service_Workers::SCOPE_FRONT );
$iframe_src = add_query_arg(
self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR,
WP_Service_Workers::SCOPE_FRONT,
home_url( '/', 'https' )
);
?>
<amp-install-serviceworker
src="<?php echo esc_url( $src ); ?>"
data-iframe-src="<?php echo esc_url( $iframe_src ); ?>"
layout="nodisplay"
>
</amp-install-serviceworker>
<?php
}
/**
* Handle request to install service worker via iframe.
*
* @see wp_print_service_workers()
* @link https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#data-iframe-src-(optional)
*/
public static function handle_service_worker_iframe_install() {
if ( ! isset( $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ] ) ) {
return;
}
$scope = intval( $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ] );
if ( WP_Service_Workers::SCOPE_ADMIN !== $scope && WP_Service_Workers::SCOPE_FRONT !== $scope ) {
wp_die(
esc_html__( 'No service workers registered for the requested scope.', 'amp' ),
esc_html__( 'Service Worker Installation', 'amp' ),
array( 'response' => 404 )
);
}
$front_scope = home_url( '/', 'relative' );
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><?php esc_html_e( 'Service Worker Installation', 'amp' ); ?></title>
</head>
<body>
<?php esc_html_e( 'Installing service worker...', 'amp' ); ?>
<?php
printf(
'<script>navigator.serviceWorker.register( %s, %s );</script>',
wp_json_encode( wp_get_service_worker_url( $scope ) ),
wp_json_encode( array( 'scope' => $front_scope ) )
);
?>
</body>
</html>
<?php
// Die in a way that can be unit tested.
add_filter(
'wp_die_handler',
function() {
return function() {
die();
};
},
1
);
wp_die();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
<?php
/**
* Deprecated functions.
*
* @package AMP
*/
/**
* Load classes for FasterImage.
*
* @deprecated This is obsolete now that there is an autoloader.
*/
function amp_load_fasterimage_classes() {
_deprecated_function( __FUNCTION__, '0.6' );
}
/**
* Get FasterImage client for user agent.
*
* @deprecated This function is no longer used in favor of just instantiating the class.
*
* @param string $user_agent User Agent.
* @return \FasterImage\FasterImage Instance.
*/
function amp_get_fasterimage_client( $user_agent ) {
_deprecated_function( __FUNCTION__, '1.0' );
return new FasterImage\FasterImage( $user_agent );
}

View File

@ -0,0 +1,80 @@
<?php
/**
* Class AMP_Base_Embed_Handler
*
* Used by some children.
*
* @package AMP
*/
/**
* Class AMP_Base_Embed_Handler
*/
abstract class AMP_Base_Embed_Handler {
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 480;
/**
* Default arguments.
*
* @var array
*/
protected $args = array();
/**
* Whether or not conversion was completed.
*
* @var boolean
*/
protected $did_convert_elements = false;
/**
* Registers embed.
*/
abstract public function register_embed();
/**
* Unregisters embed.
*/
abstract public function unregister_embed();
/**
* Constructor.
*
* @param array $args Height and width for embed.
*/
public function __construct( $args = array() ) {
$this->args = wp_parse_args(
$args,
array(
'width' => $this->DEFAULT_WIDTH,
'height' => $this->DEFAULT_HEIGHT,
)
);
}
/**
* Get mapping of AMP component names to AMP script URLs.
*
* This is normally no longer needed because the whitelist
* sanitizer will automatically detect the need for them via
* the spec.
*
* @see AMP_Tag_And_Attribute_Sanitizer::get_scripts()
* @return array Scripts.
*/
public function get_scripts() {
return array();
}
}

View File

@ -0,0 +1,131 @@
<?php
/**
* Class AMP_Core_Block_Handler
*
* @package AMP
*/
/**
* Class AMP_Core_Block_Handler
*
* @since 1.0
*/
class AMP_Core_Block_Handler extends AMP_Base_Embed_Handler {
/**
* Methods to ampify blocks.
*
* @var array
*/
protected $block_ampify_methods = array(
'core/categories' => 'ampify_categories_block',
'core/archives' => 'ampify_archives_block',
);
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'render_block', array( $this, 'filter_rendered_block' ), 0, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'render_block', array( $this, 'filter_rendered_block' ), 0 );
}
/**
* Filters the content of a single block to make it AMP valid.
*
* @param string $block_content The block content about to be appended.
* @param array $block The full block, including name and attributes.
* @return string Filtered block content.
*/
public function filter_rendered_block( $block_content, $block ) {
if ( ! isset( $block['blockName'] ) ) {
return $block_content;
}
if ( isset( $this->block_ampify_methods[ $block['blockName'] ] ) ) {
$block_content = call_user_func(
array( $this, $this->block_ampify_methods[ $block['blockName'] ] ),
$block_content
);
} elseif ( 'core/image' === $block['blockName'] || 'core/audio' === $block['blockName'] ) {
/*
* While the video block placeholder just outputs an empty video element, the placeholders for image and
* audio blocks output empty <img> and <audio> respectively. These will result in AMP validation errors,
* so we need to empty out the block content to prevent this from happening. Note that <source> is used
* for <img> because eventually the image block could use <picture>.
*/
if ( ! preg_match( '/src=|<source/', $block_content ) ) {
$block_content = '';
}
}
return $block_content;
}
/**
* Fix rendering of categories block when displayAsDropdown.
*
* This excludes the disallowed JS scrips, adds <form> tags, and uses on:change for <select>.
*
* @see render_block_core_categories()
*
* @param string $block_content Block content.
* @return string Rendered.
*/
public function ampify_categories_block( $block_content ) {
static $block_id = 0;
$block_id++;
$form_id = "wp-block-categories-dropdown-{$block_id}-form";
// Remove output of build_dropdown_script_block_core_categories().
$block_content = preg_replace( '#<script.+?</script>#s', '', $block_content );
$form = sprintf(
'<form action="%s" method="get" target="_top" id="%s">',
esc_url( home_url() ),
esc_attr( $form_id )
);
$block_content = preg_replace(
'#(<select)(.+</select>)#s',
$form . '$1' . sprintf( ' on="change:%1$s.submit"', esc_attr( $form_id ) ) . '$2</form>',
$block_content,
1
);
return $block_content;
}
/**
* Fix rendering of archives block when displayAsDropdown.
*
* This replaces disallowed script with the use of on:change for <select>.
*
* @see render_block_core_archives()
*
* @param string $block_content Block content.
* @return string Rendered.
*/
public function ampify_archives_block( $block_content ) {
// Eliminate use of uniqid(). Core should be using wp_unique_id() here.
static $block_id = 0;
$block_id++;
$block_content = preg_replace( '/(?<="wp-block-archives-)\w+(?=")/', $block_id, $block_content );
// Replace onchange with on attribute.
$block_content = preg_replace(
'/onchange=".+?"/',
'on="change:AMP.navigateTo(url=event.value)"',
$block_content
);
return $block_content;
}
}

View File

@ -0,0 +1,164 @@
<?php
/**
* Class AMP_DailyMotion_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_DailyMotion_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds
*/
class AMP_DailyMotion_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?:\/\/(www\.)?dailymotion\.com\/video\/.*#i';
const RATIO = 0.5625;
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 338;
/**
* AMP_DailyMotion_Embed_Handler constructor.
*
* @param array $args Height, width and maximum width for embed.
*/
public function __construct( $args = array() ) {
parent::__construct( $args );
if ( isset( $this->args['content_max_width'] ) ) {
$max_width = $this->args['content_max_width'];
$this->args['width'] = $max_width;
$this->args['height'] = round( $max_width * self::RATIO );
}
}
/**
* Register embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-dailymotion', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
add_shortcode( 'dailymotion', array( $this, 'shortcode' ) );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-dailymotion', -1 );
remove_shortcode( 'dailymotion' );
}
/**
* Gets AMP-compliant markup for the Dailymotion shortcode.
*
* @param array $attr The Dailymotion attributes.
* @return string Dailymotion shortcode markup.
*/
public function shortcode( $attr ) {
$video_id = false;
if ( isset( $attr['id'] ) ) {
$video_id = $attr['id'];
} elseif ( isset( $attr[0] ) ) {
$video_id = $attr[0];
} elseif ( function_exists( 'shortcode_new_to_old_params' ) ) {
$video_id = shortcode_new_to_old_params( $attr );
}
if ( empty( $video_id ) ) {
return '';
}
return $this->render(
array(
'video_id' => $video_id,
)
);
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @param array $matches URL pattern matches.
* @param array $attr Shortcode attribues.
* @param string $url URL.
* @param string $rawattr Unmodified shortcode attributes.
* @return string Rendered oEmbed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
$video_id = $this->get_video_id_from_url( $url );
return $this->render(
array(
'video_id' => $video_id,
)
);
}
/**
* Render.
*
* @param array $args Args.
* @return string Rendered.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'video_id' => false,
)
);
if ( empty( $args['video_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-dailymotion',
array(
'data-videoid' => $args['video_id'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Determine the video ID from the URL.
*
* @param string $url URL.
* @return integer Video ID.
*/
private function get_video_id_from_url( $url ) {
$parsed_url = wp_parse_url( $url );
parse_str( $parsed_url['path'], $path );
$tok = explode( '/', $parsed_url['path'] );
$tok = explode( '_', $tok[2] );
$video_id = $tok[0];
return $video_id;
}
}

View File

@ -0,0 +1,238 @@
<?php
/**
* Class AMP_Facebook_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Facebook_Embed_Handler
*/
class AMP_Facebook_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?://(www\.)?facebook\.com/.*#i';
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 400;
/**
* Tag.
*
* @var string embed HTML blockquote tag to identify and replace with AMP version.
*/
protected $sanitize_tag = 'div';
/**
* Tag.
*
* @var string AMP amp-facebook tag
*/
private $amp_tag = 'amp-facebook';
/**
* Registers embed.
*/
public function register_embed() {
wp_embed_register_handler( $this->amp_tag, self::URL_PATTERN, array( $this, 'oembed' ), -1 );
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( $this->amp_tag, -1 );
}
/**
* WordPress OEmbed rendering callback.
*
* @param array $matches URL pattern matches.
* @param array $attr Matched attributes.
* @param string $url Matched URL.
* @param string $rawattr Raw attributes string.
* @return string HTML markup for rendered embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render( array( 'url' => $url ) );
}
/**
* Gets the rendered embed markup.
*
* @param array $args Embed rendering arguments.
* @return string HTML markup for rendered embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
)
);
if ( empty( $args['url'] ) ) {
return '';
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
$this->amp_tag,
array(
'data-href' => $args['url'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Sanitized <div class="fb-video" data-href=> tags to <amp-facebook>.
*
* @param DOMDocument $dom DOM.
*/
public function sanitize_raw_embeds( $dom ) {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $dom->getElementsByTagName( $this->sanitize_tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
$embed_type = $this->get_embed_type( $node );
if ( null !== $embed_type ) {
$this->create_amp_facebook_and_replace_node( $dom, $node, $embed_type );
}
}
/*
* Remove the fb-root div and the Facebook Connect JS script since irrelevant.
* <div id="fb-root"></div>
* <script async defer crossorigin="anonymous" src="https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v3.2"></script>
*/
$fb_root = $dom->getElementById( 'fb-root' );
if ( $fb_root ) {
$xpath = new DOMXPath( $dom );
$scripts = array();
foreach ( $xpath->query( '//script[ starts-with( @src, "https://connect.facebook.net" ) and contains( @src, "sdk.js" ) ]' ) as $script ) {
$scripts[] = $script;
}
foreach ( $scripts as $script ) {
$script->parentNode->removeChild( $script );
}
$fb_root->parentNode->removeChild( $fb_root );
}
}
/**
* Get embed type.
*
* @param DOMElement $node The DOMNode to adjust and replace.
* @return string|null Embed type or null if not detected.
*/
private function get_embed_type( $node ) {
$class_attr = $node->getAttribute( 'class' );
if ( null === $class_attr || ! $node->hasAttribute( 'data-href' ) ) {
return null;
}
if ( false !== strpos( $class_attr, 'fb-post' ) ) {
return 'post';
} elseif ( false !== strpos( $class_attr, 'fb-video' ) ) {
return 'video';
} elseif ( false !== strpos( $class_attr, 'fb-page' ) ) {
return 'page';
} elseif ( false !== strpos( $class_attr, 'fb-like' ) ) {
return 'like';
} elseif ( false !== strpos( $class_attr, 'fb-comments' ) ) {
return 'comments';
} elseif ( false !== strpos( $class_attr, 'fb-comment-embed' ) ) {
return 'comment';
}
return null;
}
/**
* Create amp-facebook and replace node.
*
* @param DOMDocument $dom The HTML Document.
* @param DOMElement $node The DOMNode to adjust and replace.
* @param string $embed_type Embed type.
*/
private function create_amp_facebook_and_replace_node( $dom, $node, $embed_type ) {
$attributes = array(
'layout' => 'responsive',
'width' => $node->hasAttribute( 'data-width' ) ? $node->getAttribute( 'data-width' ) : $this->DEFAULT_WIDTH,
'height' => $node->hasAttribute( 'data-height' ) ? $node->getAttribute( 'data-height' ) : $this->DEFAULT_HEIGHT,
);
$node->removeAttribute( 'data-width' );
$node->removeAttribute( 'data-height' );
foreach ( $node->attributes as $attribute ) {
if ( 'data-' === substr( $attribute->nodeName, 0, 5 ) ) {
$attributes[ $attribute->nodeName ] = $attribute->nodeValue;
}
}
if ( 'page' === $embed_type ) {
$amp_tag = 'amp-facebook-page';
} elseif ( 'like' === $embed_type ) {
$amp_tag = 'amp-facebook-like';
} elseif ( 'comments' === $embed_type ) {
$amp_tag = 'amp-facebook-comments';
} else {
$amp_tag = $this->amp_tag;
$attributes['data-embed-as'] = $embed_type;
}
$amp_facebook_node = AMP_DOM_Utils::create_node(
$dom,
$amp_tag,
$attributes
);
$fallback = null;
foreach ( $node->childNodes as $child_node ) {
if ( $child_node instanceof DOMElement && false !== strpos( $child_node->getAttribute( 'class' ), 'fb-xfbml-parse-ignore' ) ) {
$fallback = $child_node;
$child_node->parentNode->removeChild( $child_node );
$fallback->setAttribute( 'fallback', '' );
break;
}
}
$node->parentNode->replaceChild( $amp_facebook_node, $node );
if ( $fallback ) {
$amp_facebook_node->appendChild( $fallback );
}
$this->did_convert_elements = true;
}
}

View File

@ -0,0 +1,271 @@
<?php
/**
* Class AMP_Gallery_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Gallery_Embed_Handler
*
* @since 0.2
*/
class AMP_Gallery_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'post_gallery', array( $this, 'maybe_override_gallery' ), 10, 2 );
add_action( 'wp_print_styles', array( $this, 'print_styles' ) );
}
/**
* Unregister embed.
*/
public function unregister_embed() {}
/**
* Shortcode handler.
*
* @param array $attr Shortcode attributes.
* @return string Rendered gallery.
*/
public function shortcode( $attr ) {
$post = get_post();
if ( ! empty( $attr['ids'] ) ) {
// 'ids' is explicitly ordered, unless you specify otherwise.
if ( empty( $attr['orderby'] ) ) {
$attr['orderby'] = 'post__in';
}
$attr['include'] = $attr['ids'];
}
$atts = shortcode_atts(
array(
'order' => 'ASC',
'orderby' => 'menu_order ID',
'id' => $post ? $post->ID : 0,
'include' => '',
'exclude' => '',
'size' => array( $this->args['width'], $this->args['height'] ),
'link' => 'none',
),
$attr,
'gallery'
);
if ( ! empty( $attr['amp-lightbox'] ) ) {
$atts['lightbox'] = filter_var( $attr['amp-lightbox'], FILTER_VALIDATE_BOOLEAN );
}
$id = intval( $atts['id'] );
if ( ! empty( $atts['include'] ) ) {
$attachments = get_posts(
array(
'include' => $atts['include'],
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
'order' => $atts['order'],
'orderby' => $atts['orderby'],
'fields' => 'ids',
)
);
} elseif ( ! empty( $atts['exclude'] ) ) {
$attachments = get_children(
array(
'post_parent' => $id,
'exclude' => $atts['exclude'],
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
'order' => $atts['order'],
'orderby' => $atts['orderby'],
'fields' => 'ids',
)
);
} else {
$attachments = get_children(
array(
'post_parent' => $id,
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
'order' => $atts['order'],
'orderby' => $atts['orderby'],
'fields' => 'ids',
)
);
}
if ( empty( $attachments ) ) {
return '';
}
$urls = array();
foreach ( $attachments as $attachment_id ) {
list( $url, $width, $height ) = wp_get_attachment_image_src( $attachment_id, $atts['size'], true );
if ( ! $url ) {
continue;
}
$href = null;
if ( empty( $atts['lightbox'] ) ) {
if ( ! empty( $atts['link'] ) && 'file' === $atts['link'] ) {
$href = $url;
} elseif ( ! empty( $atts['link'] ) && 'post' === $atts['link'] ) {
$href = get_attachment_link( $attachment_id );
}
}
$urls[] = array(
'href' => $href,
'url' => $url,
'width' => $width,
'height' => $height,
);
}
$args = array(
'images' => $urls,
);
if ( ! empty( $atts['lightbox'] ) ) {
$args['lightbox'] = true;
$lightbox_tag = AMP_HTML_Utils::build_tag(
'amp-image-lightbox',
array(
'id' => AMP_Base_Sanitizer::AMP_IMAGE_LIGHTBOX_ID,
'layout' => 'nodisplay',
'data-close-button-aria-label' => __( 'Close', 'amp' ),
)
);
/* We need to add lightbox tag, too. @todo Could there be a better alternative for this? */
return $this->render( $args ) . $lightbox_tag;
}
return $this->render( $args );
}
/**
* Override the output of gallery_shortcode() if amp-carousel is not false.
*
* The 'Gallery' widget also uses this function.
* This ensures that it outputs an <amp-carousel>.
*
* @param string $html Markup to filter, possibly ''.
* @param array $attributes Shortcode attributes.
* @return string $html Markup for the gallery.
*/
public function maybe_override_gallery( $html, $attributes ) {
$is_lightbox = isset( $attributes['amp-lightbox'] ) && true === filter_var( $attributes['amp-lightbox'], FILTER_VALIDATE_BOOLEAN );
if ( isset( $attributes['amp-carousel'] ) && false === filter_var( $attributes['amp-carousel'], FILTER_VALIDATE_BOOLEAN ) ) {
if ( true === $is_lightbox ) {
remove_filter( 'post_gallery', array( $this, 'maybe_override_gallery' ), 10 );
$attributes['link'] = 'none';
$html = '<ul class="amp-lightbox">' . gallery_shortcode( $attributes ) . '</ul>';
add_filter( 'post_gallery', array( $this, 'maybe_override_gallery' ), 10, 2 );
}
return $html;
} elseif ( isset( $attributes['size'] ) && 'thumbnail' === $attributes['size'] ) {
/*
* If the 'gallery' shortcode has a 'size' attribute of 'thumbnail', prevent outputting an <amp-carousel>.
* That will often get thumbnail images around 150 x 150,
* while the <amp-carousel> usually has a width of 600 and a height of 480.
* That often means very low-resolution images.
* So fall back to the normal 'gallery' shortcode callback, gallery_shortcode().
*/
return '';
}
return $this->shortcode( $attributes );
}
/**
* Render.
*
* @param array $args Args.
* @return string Rendered.
*/
public function render( $args ) {
$this->did_convert_elements = true;
$args = wp_parse_args(
$args,
array(
'images' => false,
)
);
if ( empty( $args['images'] ) ) {
return '';
}
$images = array();
foreach ( $args['images'] as $props ) {
$image_atts = array(
'src' => $props['url'],
'width' => $props['width'],
'height' => $props['height'],
'layout' => 'responsive',
);
if ( ! empty( $args['lightbox'] ) ) {
$image_atts['lightbox'] = '';
$image_atts['on'] = 'tap:' . AMP_Img_Sanitizer::AMP_IMAGE_LIGHTBOX_ID;
$image_atts['role'] = 'button';
$image_atts['tabindex'] = 0;
}
$image = AMP_HTML_Utils::build_tag(
'amp-img',
$image_atts
);
if ( ! empty( $props['href'] ) ) {
$image = AMP_HTML_Utils::build_tag(
'a',
array(
'href' => $props['href'],
),
$image
);
}
$images[] = $image;
}
return AMP_HTML_Utils::build_tag(
'amp-carousel',
array(
'width' => $this->args['width'],
'height' => $this->args['height'],
'type' => 'slides',
'layout' => 'responsive',
),
implode( PHP_EOL, $images )
);
}
/**
* Prints the Gallery block styling.
*
* It would be better to print this in AMP_Gallery_Block_Sanitizer,
* but by the time that runs, it's too late.
* This rule is copied exactly from block-library/style.css, but the selector here has amp-img >.
* The image sanitizer normally converts the <img> from that original stylesheet <amp-img>,
* but that doesn't have the same effect as applying it to the <img>.
*
* @return void
*/
public function print_styles() {
?>
<style>
.wp-block-gallery.is-cropped .blocks-gallery-item amp-img > img {
object-fit: cover;
}
</style>
<?php
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* Class AMP_Gfycat_Embed_Handler
*
* @package AMP
* @since 1.0
*/
/**
* Class AMP_Gfycat_Embed_Handler
*/
class AMP_Gfycat_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-gfycat.
*
* @var string
*/
const URL_PATTERN = '#https?://(www\.)?gfycat\.com/.*#i';
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 3 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Gfycat to prepare it for AMP.
*
* @param mixed $return The shortcode callback function to call.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @return string Embed.
*/
public function filter_embed_oembed_html( $return, $url, $attr ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'gfycat.com' ) ) {
if ( preg_match( '/width=["\']?(\d+)/', $return, $matches ) ) {
$attr['width'] = $matches[1];
}
if ( preg_match( '/height=["\']?(\d+)/', $return, $matches ) ) {
$attr['height'] = $matches[1];
}
if ( empty( $attr['height'] ) ) {
return $return;
}
$attributes = wp_array_slice_assoc( $attr, array( 'width', 'height' ) );
if ( empty( $attr['width'] ) ) {
$attributes['layout'] = 'fixed-height';
$attributes['width'] = 'auto';
}
$pieces = explode( '/detail/', $parsed_url['path'] );
if ( ! isset( $pieces[1] ) ) {
if ( ! preg_match( '/\/([A-Za-z0-9]+)/', $parsed_url['path'], $matches ) ) {
return $return;
}
$attributes['data-gfyid'] = $matches[1];
} else {
$attributes['data-gfyid'] = $pieces[1];
}
$return = AMP_HTML_Utils::build_tag(
'amp-gfycat',
$attributes
);
}
return $return;
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* Class AMP_Hulu_Embed_Handler
*
* @package AMP
* @since 1.0
*/
/**
* Class AMP_Hulu_Embed_Handler
*/
class AMP_Hulu_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-hulu.
*
* @var string
*/
const URL_PATTERN = '#https?://(www\.)?hulu\.com/.*#i';
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 600;
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 3 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Hulu to prepare it for AMP.
*
* @param mixed $return The shortcode callback function to call.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @return string Embed.
*/
public function filter_embed_oembed_html( $return, $url, $attr ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'hulu.com' ) ) {
if ( preg_match( '/width=["\']?(\d+)/', $return, $matches ) ) {
$attr['width'] = $matches[1];
}
if ( preg_match( '/height=["\']?(\d+)/', $return, $matches ) ) {
$attr['height'] = $matches[1];
}
if ( empty( $attr['height'] ) ) {
return $return;
}
$attributes = wp_array_slice_assoc( $attr, array( 'width', 'height' ) );
if ( empty( $attr['width'] ) ) {
$attributes['layout'] = 'fixed-height';
$attributes['width'] = 'auto';
}
$pieces = explode( '/watch/', $parsed_url['path'] );
if ( ! isset( $pieces[1] ) ) {
if ( ! preg_match( '/\/([A-Za-z0-9]+)/', $parsed_url['path'], $matches ) ) {
return $return;
}
$attributes['data-eid'] = $matches[1];
} else {
$attributes['data-eid'] = $pieces[1];
}
$return = AMP_HTML_Utils::build_tag(
'amp-hulu',
$attributes
);
}
return $return;
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* Class AMP_Imgur_Embed_Handler
*
* @package AMP
* @since 1.0
*/
/**
* Class AMP_Imgur_Embed_Handler
*/
class AMP_Imgur_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-imgur.
*
* @var string
*/
const URL_PATTERN = '#https?://(www\.)?imgur\.com/.*#i';
/**
* Register embed.
*/
public function register_embed() {
if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '4.9', '<' ) ) {
// Before 4.9 the embedding Imgur is not working properly, register embed for that case.
wp_embed_register_handler( 'amp-imgur', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
} else {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 3 );
}
}
/**
* Unregister embed.
*/
public function unregister_embed() {
if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '4.9', '<' ) ) {
wp_embed_unregister_handler( 'amp-imgur', -1 );
} else {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
}
/**
* Oembed.
*
* @param array $matches Matches.
* @param array $attr Attributes.
* @param string $url URL.
* @param array $rawattr Raw attributes.
* @return string Embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render( array( 'url' => $url ) );
}
/**
* Render embed.
*
* @param array $args Args.
* @return string Embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
)
);
if ( empty( $args['url'] ) ) {
return '';
}
$this->did_convert_elements = true;
$id = $this->get_imgur_id_from_url( $args['url'] );
if ( false === $id ) {
return '';
}
return AMP_HTML_Utils::build_tag(
'amp-imgur',
array(
'width' => $this->args['width'],
'height' => $this->args['height'],
'data-imgur-id' => $id,
)
);
}
/**
* Filter oEmbed HTML for Imgur to prepare it for AMP.
*
* @param mixed $return The shortcode callback function to call.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @return string Embed.
*/
public function filter_embed_oembed_html( $return, $url, $attr ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'imgur.com' ) ) {
if ( preg_match( '/width=["\']?(\d+)/', $return, $matches ) ) {
$attr['width'] = $matches[1];
}
if ( preg_match( '/height=["\']?(\d+)/', $return, $matches ) ) {
$attr['height'] = $matches[1];
}
if ( empty( $attr['height'] ) ) {
return $return;
}
$attributes = wp_array_slice_assoc( $attr, array( 'width', 'height' ) );
if ( empty( $attr['width'] ) ) {
$attributes['layout'] = 'fixed-height';
$attributes['width'] = 'auto';
}
$attributes['data-imgur-id'] = $this->get_imgur_id_from_url( $url );
if ( false === $attributes['data-imgur-id'] ) {
return $return;
}
$return = AMP_HTML_Utils::build_tag(
'amp-imgur',
$attributes
);
}
return $return;
}
/**
* Get Imgur ID from URL.
*
* @param string $url URL.
* @return bool|string ID / false.
*/
protected function get_imgur_id_from_url( $url ) {
$parsed_url = wp_parse_url( $url );
$pieces = explode( '/gallery/', $parsed_url['path'] );
if ( ! isset( $pieces[1] ) ) {
if ( ! preg_match( '/\/([A-Za-z0-9]+)/', $parsed_url['path'], $matches ) ) {
return false;
}
return $matches[1];
} else {
return $pieces[1];
}
}
}

View File

@ -0,0 +1,256 @@
<?php
/**
* Class AMP_Instagram_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Instagram_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds
*/
class AMP_Instagram_Embed_Handler extends AMP_Base_Embed_Handler {
const SHORT_URL_HOST = 'instagr.am';
const URL_PATTERN = '#http(s?)://(www\.)?instagr(\.am|am\.com)/p/([^/?]+)#i';
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 600;
/**
* Tag.
*
* @var string embed HTML blockquote tag to identify and replace with AMP version.
*/
protected $sanitize_tag = 'blockquote';
/**
* Tag.
*
* @var string AMP amp-facebook tag
*/
private $amp_tag = 'amp-instagram';
/**
* Registers embed.
*/
public function register_embed() {
wp_embed_register_handler( $this->amp_tag, self::URL_PATTERN, array( $this, 'oembed' ), -1 );
add_shortcode( 'instagram', array( $this, 'shortcode' ) );
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( $this->amp_tag, -1 );
remove_shortcode( 'instagram' );
}
/**
* WordPress shortcode rendering callback.
*
* @param array $attr Shortcode attributes.
* @return string HTML markup for rendered embed.
*/
public function shortcode( $attr ) {
$url = false;
$instagram_id = false;
if ( isset( $attr['url'] ) ) {
$url = trim( $attr['url'] );
}
if ( empty( $url ) ) {
return '';
}
$instagram_id = $this->get_instagram_id_from_url( $url );
return $this->render(
array(
'url' => $url,
'instagram_id' => $instagram_id,
)
);
}
/**
* WordPress OEmbed rendering callback.
*
* @param array $matches URL pattern matches.
* @param array $attr Matched attributes.
* @param string $url Matched URL.
* @param string $rawattr Raw attributes string.
* @return string HTML markup for rendered embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render(
array(
'url' => $url,
'instagram_id' => end( $matches ),
)
);
}
/**
* Gets the rendered embed markup.
*
* @param array $args Embed rendering arguments.
* @return string HTML markup for rendered embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
'instagram_id' => false,
)
);
if ( empty( $args['instagram_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
$this->amp_tag,
array(
'data-shortcode' => $args['instagram_id'],
'data-captioned' => '',
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Get Instagram ID from URL.
*
* @param string $url URL.
* @return string|false The ID parsed from the URL or false if not found.
*/
private function get_instagram_id_from_url( $url ) {
$found = preg_match( self::URL_PATTERN, $url, $matches );
if ( ! $found ) {
return false;
}
return end( $matches );
}
/**
* Sanitized <blockquote class="instagram-media"> tags to <amp-instagram>
*
* @param DOMDocument $dom DOM.
*/
public function sanitize_raw_embeds( $dom ) {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $dom->getElementsByTagName( $this->sanitize_tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
if ( $node->hasAttribute( 'data-instgrm-permalink' ) ) {
$this->create_amp_instagram_and_replace_node( $dom, $node );
}
}
}
/**
* Make final modifications to DOMNode
*
* @param DOMDocument $dom The HTML Document.
* @param DOMElement $node The DOMNode to adjust and replace.
*/
private function create_amp_instagram_and_replace_node( $dom, $node ) {
$instagram_id = $this->get_instagram_id_from_url( $node->getAttribute( 'data-instgrm-permalink' ) );
$node_args = array(
'data-shortcode' => $instagram_id,
'layout' => 'responsive',
'width' => $this->DEFAULT_WIDTH,
'height' => $this->DEFAULT_HEIGHT,
);
if ( true === $node->hasAttribute( 'data-instgrm-captioned' ) ) {
$node_args['data-captioned'] = '';
}
$new_node = AMP_DOM_Utils::create_node( $dom, $this->amp_tag, $node_args );
$this->sanitize_embed_script( $node );
$node->parentNode->replaceChild( $new_node, $node );
$this->did_convert_elements = true;
}
/**
* Removes Instagram's embed <script> tag.
*
* @param DOMElement $node The DOMNode to whose sibling is the instagram script.
*/
private function sanitize_embed_script( $node ) {
$next_element_sibling = $node->nextSibling;
while ( $next_element_sibling && ! ( $next_element_sibling instanceof DOMElement ) ) {
$next_element_sibling = $next_element_sibling->nextSibling;
}
$script_src = 'instagram.com/embed.js';
// Handle case where script is wrapped in paragraph by wpautop.
if ( $next_element_sibling instanceof DOMElement && 'p' === $next_element_sibling->nodeName ) {
$children = $next_element_sibling->getElementsByTagName( '*' );
if ( 1 === $children->length && 'script' === $children->item( 0 )->nodeName && false !== strpos( $children->item( 0 )->getAttribute( 'src' ), $script_src ) ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
return;
}
}
// Handle case where script is immediately following.
$is_embed_script = (
$next_element_sibling
&&
'script' === strtolower( $next_element_sibling->nodeName )
&&
false !== strpos( $next_element_sibling->getAttribute( 'src' ), $script_src )
);
if ( $is_embed_script ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
}
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* Class AMP_Issuu_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Issuu_Embed_Handler
*/
class AMP_Issuu_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-iframe.
*
* @const string
*/
const URL_PATTERN = '#https?://(www\.)?issuu\.com/.+/docs/.+#i';
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 3 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Meetup to prepare it for AMP.
*
* @param mixed $return The shortcode callback function to call.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @return string Embed.
*/
public function filter_embed_oembed_html( $return, $url, $attr ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'issuu.com' ) ) {
if ( preg_match( '/width\s*:\s*(\d+)px/', $return, $matches ) ) {
$attr['width'] = $matches[1];
}
if ( preg_match( '/height\s*:\s*(\d+)px/', $return, $matches ) ) {
$attr['height'] = $matches[1];
}
$return = AMP_HTML_Utils::build_tag(
'amp-iframe',
array(
'width' => $attr['width'],
'height' => $attr['height'],
'src' => $url,
'sandbox' => 'allow-scripts allow-same-origin',
)
);
}
return $return;
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Class AMP_Meetup_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Meetup_Embed_Handler
*/
class AMP_Meetup_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Meetup to prepare it for AMP.
*
* @param string $cache Cache for oEmbed.
* @param string $url Embed URL.
* @return string Embed.
*/
public function filter_embed_oembed_html( $cache, $url ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'meetup.com' ) ) {
// Supply the width/height so that we don't have to make requests to look them up later.
$cache = str_replace( '<img ', '<img width="50" height="50" ', $cache );
}
return $cache;
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* Class AMP_Pinterest_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Pinterest_Embed_Handler
*/
class AMP_Pinterest_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?://(www\.)?pinterest\.com/pin/.*#i';
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 450;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 750;
/**
* Registers embed.
*/
public function register_embed() {
wp_embed_register_handler(
'amp-pinterest',
self::URL_PATTERN,
array( $this, 'oembed' ),
-1
);
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-pinterest', -1 );
}
/**
* WordPress OEmbed rendering callback.
*
* @param array $matches URL pattern matches.
* @param array $attr Matched attributes.
* @param string $url Matched URL.
* @param string $rawattr Raw attributes string.
* @return string HTML markup for rendered embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render( array( 'url' => $url ) );
}
/**
* Gets the rendered embed markup.
*
* @param array $args Embed rendering arguments.
* @return string HTML markup for rendered embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
)
);
if ( empty( $args['url'] ) ) {
return '';
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-pinterest',
array(
'width' => $this->args['width'],
'height' => $this->args['height'],
'data-do' => 'embedPin',
'data-url' => $args['url'],
)
);
}
}

View File

@ -0,0 +1,324 @@
<?php
/**
* Class AMP_Playlist_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Playlist_Embed_Handler
*
* Creates AMP-compatible markup for the WordPress 'playlist' shortcode.
*
* @package AMP
*/
class AMP_Playlist_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* The tag of the shortcode.
*
* @var string
*/
const SHORTCODE = 'playlist';
/**
* The default height of the thumbnail image for 'audio' playlist tracks.
*
* @var int
*/
const DEFAULT_THUMB_HEIGHT = 64;
/**
* The default width of the thumbnail image for 'audio' playlist tracks.
*
* @var int
*/
const DEFAULT_THUMB_WIDTH = 48;
/**
* The max width of the audio thumbnail image.
*
* This corresponds to the max-width in wp-mediaelement.css:
* .wp-playlist .wp-playlist-current-item img
*
* @var int
*/
const THUMB_MAX_WIDTH = 60;
/**
* The height of the carousel.
*
* @var int
*/
const CAROUSEL_HEIGHT = 160;
/**
* The pattern to get the playlist data.
*
* @var string
*/
const PLAYLIST_REGEX = ':<script type="application/json" class="wp-playlist-script">(.+?)</script>:s';
/**
* The ID of individual playlist.
*
* @var int
*/
public static $playlist_id = 0;
/**
* The removed shortcode callback.
*
* @var callable
*/
public $removed_shortcode_callback;
/**
* Registers the playlist shortcode.
*
* @global array $shortcode_tags
* @return void
*/
public function register_embed() {
global $shortcode_tags;
if ( shortcode_exists( self::SHORTCODE ) ) {
$this->removed_shortcode_callback = $shortcode_tags[ self::SHORTCODE ];
}
add_shortcode( self::SHORTCODE, array( $this, 'shortcode' ) );
remove_action( 'wp_playlist_scripts', 'wp_playlist_scripts' );
}
/**
* Unregisters the playlist shortcode.
*
* @return void
*/
public function unregister_embed() {
if ( $this->removed_shortcode_callback ) {
add_shortcode( self::SHORTCODE, $this->removed_shortcode_callback );
$this->removed_shortcode_callback = null;
}
add_action( 'wp_playlist_scripts', 'wp_playlist_scripts' );
}
/**
* Enqueues the playlist styling.
*
* @return void
*/
public function enqueue_styles() {
wp_enqueue_style(
'amp-playlist-shortcode',
amp_get_asset_url( 'css/amp-playlist-shortcode.css' ),
array( 'wp-mediaelement' ),
AMP__VERSION
);
}
/**
* Gets AMP-compliant markup for the playlist shortcode.
*
* Uses the JSON that wp_playlist_shortcode() produces.
* Gets the markup, based on the type of playlist.
*
* @param array $attr The playlist attributes.
* @return string Playlist shortcode markup.
*/
public function shortcode( $attr ) {
$data = $this->get_data( $attr );
if ( isset( $data['type'] ) && ( 'audio' === $data['type'] ) ) {
return $this->audio_playlist( $data );
} elseif ( isset( $data['type'] ) && ( 'video' === $data['type'] ) ) {
return $this->video_playlist( $data );
}
}
/**
* Gets an AMP-compliant audio playlist.
*
* @param array $data Data.
* @return string Playlist shortcode markup, or an empty string.
*/
public function audio_playlist( $data ) {
if ( ! isset( $data['tracks'] ) ) {
return '';
}
self::$playlist_id++;
$container_id = 'wpPlaylist' . self::$playlist_id . 'Carousel';
$state_id = 'wpPlaylist' . self::$playlist_id;
$amp_state = array(
'selectedIndex' => 0,
);
$this->enqueue_styles();
ob_start();
?>
<div class="wp-playlist wp-audio-playlist wp-playlist-light">
<amp-state id="<?php echo esc_attr( $state_id ); ?>">
<script type="application/json"><?php echo wp_json_encode( $amp_state ); ?></script>
</amp-state>
<amp-carousel id="<?php echo esc_attr( $container_id ); ?>" [slide]="<?php echo esc_attr( $state_id . '.selectedIndex' ); ?>" height="<?php echo esc_attr( self::CAROUSEL_HEIGHT ); ?>" width="auto" type="slides">
<?php
foreach ( $data['tracks'] as $track ) :
$title = $this->get_title( $track );
$image_url = isset( $track['thumb']['src'] ) ? $track['thumb']['src'] : '';
$dimensions = $this->get_thumb_dimensions( $track );
?>
<div>
<div class="wp-playlist-current-item">
<?php if ( $image_url ) : ?>
<amp-img src="<?php echo esc_url( $image_url ); ?>" height="<?php echo esc_attr( $dimensions['height'] ); ?>" width="<?php echo esc_attr( $dimensions['width'] ); ?>"></amp-img>
<?php endif; ?>
<div class="wp-playlist-caption">
<span class="wp-playlist-item-meta wp-playlist-item-title"><?php echo esc_html( $title ); ?></span>
</div>
</div>
<amp-audio width="auto" height="50" src="<?php echo esc_url( $track['src'] ); ?>"></amp-audio>
</div>
<?php endforeach; ?>
</amp-carousel>
<?php $this->print_tracks( $state_id, $data['tracks'] ); ?>
</div>
<?php
return ob_get_clean();
}
/**
* Gets an AMP-compliant video playlist.
*
* This uses similar markup to the native playlist shortcode output.
* So the styles from wp-mediaelement.min.css will apply to it.
*
* @global int $content_width
* @param array $data Data.
* @return string $video_playlist Markup for the video playlist.
*/
public function video_playlist( $data ) {
global $content_width;
if ( ! isset( $data['tracks'], $data['tracks'][0]['src'] ) ) {
return '';
}
self::$playlist_id++;
$state_id = 'wpPlaylist' . self::$playlist_id;
$amp_state = array(
'selectedIndex' => 0,
);
foreach ( $data['tracks'] as $index => $track ) {
$amp_state[ $index ] = array(
'videoUrl' => $track['src'],
'thumb' => isset( $track['thumb']['src'] ) ? $track['thumb']['src'] : '',
);
}
$dimensions = isset( $data['tracks'][0]['dimensions']['resized'] ) ? $data['tracks'][0]['dimensions']['resized'] : null;
$width = isset( $dimensions['width'] ) ? $dimensions['width'] : $content_width;
$height = isset( $dimensions['height'] ) ? $dimensions['height'] : null;
$src_bound = sprintf( '%s[%s.selectedIndex].videoUrl', $state_id, $state_id );
$this->enqueue_styles();
ob_start();
?>
<div class="wp-playlist wp-video-playlist wp-playlist-light">
<amp-state id="<?php echo esc_attr( $state_id ); ?>">
<script type="application/json"><?php echo wp_json_encode( $amp_state ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></script>
</amp-state>
<amp-video id="amp-video" src="<?php echo esc_url( $data['tracks'][0]['src'] ); ?>" [src]="<?php echo esc_attr( $src_bound ); ?>" width="<?php echo esc_attr( $width ); ?>" height="<?php echo esc_attr( $height ); ?>" controls></amp-video>
<?php $this->print_tracks( $state_id, $data['tracks'] ); ?>
</div>
<?php
return ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Gets the thumbnail image dimensions, including height and width.
*
* If the width is higher than the maximum width,
* reduces it to the maximum width.
* And it proportionally reduces the height.
*
* @param array $track The data for the track.
* @return array {
* Dimensions.
*
* @type int $height Image height.
* @type int $width Image width.
* }
*/
public function get_thumb_dimensions( $track ) {
$original_height = isset( $track['thumb']['height'] ) ? intval( $track['thumb']['height'] ) : self::DEFAULT_THUMB_HEIGHT;
$original_width = isset( $track['thumb']['width'] ) ? intval( $track['thumb']['width'] ) : self::DEFAULT_THUMB_WIDTH;
if ( $original_width > self::THUMB_MAX_WIDTH ) {
$ratio = $original_width / self::THUMB_MAX_WIDTH;
$height = intval( $original_height / $ratio );
} else {
$height = $original_height;
}
$width = min( self::THUMB_MAX_WIDTH, $original_width );
return compact( 'height', 'width' );
}
/**
* Outputs the playlist tracks, based on the type of playlist.
*
* These typically appear below the player.
* Clicking a track triggers the player to appear with its src.
*
* @param string $state_id The ID of the container.
* @param array $tracks Tracks.
* @return void
*/
public function print_tracks( $state_id, $tracks ) {
?>
<div class="wp-playlist-tracks">
<?php foreach ( $tracks as $index => $track ) : ?>
<?php
$on = 'tap:AMP.setState(' . wp_json_encode( array( $state_id => array( 'selectedIndex' => $index ) ) ) . ')';
$initial_class = 0 === $index ? 'wp-playlist-item wp-playlist-playing' : 'wp-playlist-item';
$bound_class = sprintf( '%d == %s.selectedIndex ? "wp-playlist-item wp-playlist-playing" : "wp-playlist-item"', $index, $state_id );
?>
<div class="<?php echo esc_attr( $initial_class ); ?>" [class]="<?php echo esc_attr( $bound_class ); ?>" >
<a class="wp-playlist-caption" on="<?php echo esc_attr( $on ); ?>">
<?php echo esc_html( strval( $index + 1 ) . '.' ); ?> <span class="wp-playlist-item-title"><?php echo esc_html( $this->get_title( $track ) ); ?></span>
</a>
<?php if ( isset( $track['meta']['length_formatted'] ) ) : ?>
<div class="wp-playlist-item-length"><?php echo esc_html( $track['meta']['length_formatted'] ); ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
}
/**
* Gets the data for the playlist.
*
* @see wp_playlist_shortcode()
* @param array $attr The shortcode attributes.
* @return array $data The data for the playlist.
*/
public function get_data( $attr ) {
$markup = wp_playlist_shortcode( $attr );
preg_match( self::PLAYLIST_REGEX, $markup, $matches );
if ( empty( $matches[1] ) ) {
return array();
}
return json_decode( $matches[1], true );
}
/**
* Gets the title for the track.
*
* @param array $track The track data.
* @return string $title The title of the track.
*/
public function get_title( $track ) {
if ( ! empty( $track['caption'] ) ) {
return $track['caption'];
} elseif ( ! empty( $track['title'] ) ) {
return $track['title'];
}
return '';
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Class AMP_Reddit_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Reddit_Embed_Handler
*/
class AMP_Reddit_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-reddit.
*
* @const string
*/
const URL_PATTERN = '#https?://(www\.)?reddit\.com/r/[^/]+/comments/.*#i';
/**
* Register embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-reddit', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-reddit', -1 );
}
/**
* Embed found with matching URL callback.
*
* @param array $matches URL regex matches.
* @param array $attr Additional parameters.
* @param array $url URL.
* @return string Embed.
*/
public function oembed( $matches, $attr, $url ) {
return $this->render( array( 'url' => $url ) );
}
/**
* Output the Reddit amp element.
*
* @param array $args parameters used for output.
* @return string Rendered content.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
)
);
if ( empty( $args['url'] ) ) {
return '';
}
// @todo Sizing is not yet correct. See <https://github.com/ampproject/amphtml/issues/11869>.
return AMP_HTML_Utils::build_tag(
'amp-reddit',
array(
'layout' => 'responsive',
'data-embedtype' => 'post',
'width' => '100',
'height' => '100',
'data-src' => $args['url'],
)
);
}
}

View File

@ -0,0 +1,193 @@
<?php
/**
* Class AMP_SoundCloud_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_SoundCloud_Embed_Handler
*
* @since 0.5
*/
class AMP_SoundCloud_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 200;
/**
* Register embed.
*/
public function register_embed() {
add_shortcode( 'soundcloud', array( $this, 'shortcode' ) );
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_shortcode( 'soundcloud' );
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @deprecated Core's oEmbed handler is now used instead, with embed_oembed_html filter used to convert to AMP.
* @param array $matches URL pattern matches.
* @param array $attr Shortcode attribues.
* @param string $url URL.
* @return string Rendered oEmbed.
*/
public function oembed( $matches, $attr, $url ) {
_deprecated_function( __METHOD__, '0.6' );
unset( $matches, $attr );
$track_id = $this->get_track_id_from_url( $url );
return $this->render( compact( 'track_id' ) );
}
/**
* Filter oEmbed HTML for SoundCloud to convert to AMP.
*
* @param string $cache Cache for oEmbed.
* @param string $url Embed URL.
* @return string Embed.
*/
public function filter_embed_oembed_html( $cache, $url ) {
$parsed_url = wp_parse_url( $url );
if ( false === strpos( $parsed_url['host'], 'soundcloud.com' ) ) {
return $cache;
}
return $this->parse_amp_component_from_iframe( $cache );
}
/**
* Parse AMP component from iframe.
*
* @param string $html HTML.
* @return string AMP component or empty if unable to determine SoundCloud ID.
*/
private function parse_amp_component_from_iframe( $html ) {
$embed = '';
if ( preg_match( '#<iframe.+?src="(?P<url>.+?)".*>#', $html, $matches ) ) {
$src = html_entity_decode( $matches['url'], ENT_QUOTES );
$query = array();
parse_str( wp_parse_url( $src, PHP_URL_QUERY ), $query );
if ( ! empty( $query['url'] ) ) {
$embed = $this->render(
array(
'track_id' => $this->get_track_id_from_url( $query['url'] ),
)
);
}
}
return $embed;
}
/**
* Render shortcode.
*
* @param array $attr Shortcode attributes.
* @param string $content Shortcode content.
* @return string Rendered shortcode.
*/
public function shortcode( $attr, $content = null ) {
$output = '';
if ( function_exists( 'soundcloud_shortcode' ) ) {
if ( empty( $attr['url'] ) && ! empty( $attr['id'] ) ) {
$attr['url'] = 'https://api.soundcloud.com/tracks/' . intval( $attr['id'] );
}
$output = soundcloud_shortcode( $attr, $content );
$output = $this->parse_amp_component_from_iframe( $output );
} else {
$url = null;
if ( isset( $attr['id'] ) ) {
$url = 'https://w.soundcloud.com/player/?url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F' . intval( $attr['id'] );
}
if ( isset( $attr['url'] ) ) {
$url = $attr['url'];
} elseif ( isset( $attr[0] ) ) {
$url = $attr[0];
} elseif ( function_exists( 'shortcode_new_to_old_params' ) ) {
$url = shortcode_new_to_old_params( $attr );
}
if ( $url ) {
$output = $this->render_embed_fallback( $url );
}
}
return $output;
}
/**
* Render embed.
*
* @param array $args Args.
* @return string Rendered embed.
* @global WP_Embed $wp_embed
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'track_id' => false,
'url' => null,
)
);
if ( empty( $args['track_id'] ) ) {
return $this->render_embed_fallback( $args['url'] );
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-soundcloud',
array(
'data-trackid' => $args['track_id'],
'layout' => 'fixed-height',
'height' => $this->args['height'],
)
);
}
/**
* Render embed fallback.
*
* @param string $url URL.
* @returns string
*/
private function render_embed_fallback( $url ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $url ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $url )
);
}
/**
* Get track_id from URL.
*
* @param string $url URL.
*
* @return string Track ID or empty string if none matched.
*/
private function get_track_id_from_url( $url ) {
$parsed_url = wp_parse_url( $url );
if ( ! preg_match( '#tracks/(?P<track_id>[^/]+)#', $parsed_url['path'], $matches ) ) {
return '';
}
return $matches['track_id'];
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* Class AMP_Tumblr_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Tumblr_Embed_Handler
*/
class AMP_Tumblr_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Tumblr to prepare it for AMP.
*
* @param string $cache Cache for oEmbed.
* @param string $url Embed URL.
* @return string Embed.
*/
public function filter_embed_oembed_html( $cache, $url ) {
$parsed_url = wp_parse_url( $url );
if ( false === strpos( $parsed_url['host'], 'tumblr.com' ) ) {
return $cache;
}
// @todo The iframe will not get sized properly.
if ( preg_match( '#data-href="(?P<href>https://embed.tumblr.com/embed/post/\w+/\w+)"#', $cache, $matches ) ) {
$cache = AMP_HTML_Utils::build_tag(
'amp-iframe',
array(
'width' => $this->args['width'],
'height' => $this->args['height'],
'layout' => 'responsive',
'sandbox' => 'allow-scripts allow-popups', // The allow-scripts is needed to allow the iframe to render; allow-popups needed to allow clicking.
'src' => $matches['href'],
),
sprintf( '<a placeholder href="%s">Tumblr</a>', $url )
);
}
return $cache;
}
}

View File

@ -0,0 +1,313 @@
<?php
/**
* Class AMP_Twitter_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Twitter_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds
*/
class AMP_Twitter_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* URL pattern for a Tweet URL.
*
* @since 0.2
* @var string
*/
const URL_PATTERN = '#https?:\/\/twitter\.com(?:\/\#\!\/|\/)(?P<username>[a-zA-Z0-9_]{1,20})\/status(?:es)?\/(?P<tweet>\d+)#i';
/**
* URL pattern for a Twitter timeline.
*
* @since 1.0
* @var string
*/
const URL_PATTERN_TIMELINE = '#https?:\/\/twitter\.com(?:\/\#\!\/|\/)(?P<username>[a-zA-Z0-9_]{1,20})(?:$|\/(?P<type>likes|lists)(\/(?P<id>[a-zA-Z0-9_-]+))?)#i';
/**
* Tag.
*
* @var string embed HTML blockquote tag to identify and replace with AMP version.
*/
protected $sanitize_tag = 'blockquote';
/**
* Tag.
*
* @var string AMP amp-facebook tag
*/
private $amp_tag = 'amp-twitter';
/**
* Registers embed.
*/
public function register_embed() {
add_shortcode( 'tweet', array( $this, 'shortcode' ) ); // Note: This is a Jetpack shortcode.
wp_embed_register_handler( 'amp-twitter', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
wp_embed_register_handler( 'amp-twitter-timeline', self::URL_PATTERN_TIMELINE, array( $this, 'oembed_timeline' ), -1 );
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
remove_shortcode( 'tweet' ); // Note: This is a Jetpack shortcode.
wp_embed_unregister_handler( 'amp-twitter', -1 );
wp_embed_unregister_handler( 'amp-twitter-timeline', -1 );
}
/**
* Gets AMP-compliant markup for the Twitter shortcode.
*
* Note that this shortcode is is defined in Jetpack.
*
* @param array $attr The Twitter attributes.
* @return string Twitter shortcode markup.
*/
public function shortcode( $attr ) {
$attr = wp_parse_args(
$attr,
array(
'tweet' => false,
)
);
if ( empty( $attr['tweet'] ) && ! empty( $attr[0] ) ) {
$attr['tweet'] = $attr[0];
}
$id = false;
if ( is_numeric( $attr['tweet'] ) ) {
$id = $attr['tweet'];
} else {
preg_match( self::URL_PATTERN, $attr['tweet'], $matches );
if ( isset( $matches['tweet'] ) && is_numeric( $matches['tweet'] ) ) {
$id = $matches['tweet'];
}
if ( empty( $id ) ) {
return '';
}
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
$this->amp_tag,
array(
'data-tweetid' => $id,
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @param array $matches URL pattern matches.
* @return string Rendered oEmbed.
*/
public function oembed( $matches ) {
$id = false;
if ( isset( $matches['tweet'] ) && is_numeric( $matches['tweet'] ) ) {
$id = $matches['tweet'];
}
if ( ! $id ) {
return '';
}
return $this->shortcode( array( 'tweet' => $id ) );
}
/**
* Render oEmbed for a timeline.
*
* @since 1.0
*
* @param array $matches URL pattern matches.
* @return string Rendered oEmbed.
*/
public function oembed_timeline( $matches ) {
if ( ! isset( $matches['username'] ) ) {
return '';
}
$attributes = array(
'data-timeline-source-type' => 'profile',
'data-timeline-screen-name' => $matches['username'],
);
if ( isset( $matches['type'] ) ) {
switch ( $matches['type'] ) {
case 'likes':
$attributes['data-timeline-source-type'] = 'likes';
break;
case 'lists':
if ( ! isset( $matches['id'] ) ) {
return '';
}
$attributes['data-timeline-source-type'] = 'list';
$attributes['data-timeline-slug'] = $matches['id'];
$attributes['data-timeline-owner-screen-name'] = $attributes['data-timeline-screen-name'];
unset( $attributes['data-timeline-screen-name'] );
break;
default:
return '';
}
}
$attributes['layout'] = 'responsive';
$attributes['width'] = $this->args['width'];
$attributes['height'] = $this->args['height'];
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag( $this->amp_tag, $attributes );
}
/**
* Sanitized <blockquote class="twitter-tweet"> tags to <amp-twitter>.
*
* @param DOMDocument $dom DOM.
*/
public function sanitize_raw_embeds( $dom ) {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $dom->getElementsByTagName( $this->sanitize_tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
if ( $this->is_tweet_raw_embed( $node ) ) {
$this->create_amp_twitter_and_replace_node( $dom, $node );
}
}
}
/**
* Checks whether it's a twitter blockquote or not
*
* @param DOMElement $node The DOMNode to adjust and replace.
* @return bool Whether node is for raw embed.
*/
private function is_tweet_raw_embed( $node ) {
$class_attr = $node->getAttribute( 'class' );
return null !== $class_attr && false !== strpos( $class_attr, 'twitter-tweet' );
}
/**
* Make final modifications to DOMNode
*
* @param DOMDocument $dom The HTML Document.
* @param DOMElement $node The DOMNode to adjust and replace.
*/
private function create_amp_twitter_and_replace_node( $dom, $node ) {
$tweet_id = $this->get_tweet_id( $node );
if ( ! $tweet_id ) {
return;
}
$new_node = AMP_DOM_Utils::create_node(
$dom,
$this->amp_tag,
array(
'width' => $this->DEFAULT_WIDTH,
'height' => $this->DEFAULT_HEIGHT,
'layout' => 'responsive',
'data-tweetid' => $tweet_id,
)
);
$this->sanitize_embed_script( $node );
$node->parentNode->replaceChild( $new_node, $node );
$this->did_convert_elements = true;
}
/**
* Extracts Tweet id.
*
* @param DOMElement $node The DOMNode to adjust and replace.
* @return string Tweet ID.
*/
private function get_tweet_id( $node ) {
/**
* DOMNode
*
* @var DOMNodeList $anchors
*/
$anchors = $node->getElementsByTagName( 'a' );
/**
* Anchor.
*
* @type DOMElement $anchor
*/
foreach ( $anchors as $anchor ) {
$found = preg_match( self::URL_PATTERN, $anchor->getAttribute( 'href' ), $matches );
if ( $found ) {
return $matches['tweet'];
}
}
return null;
}
/**
* Removes Twitter's embed <script> tag.
*
* @param DOMElement $node The DOMNode to whose sibling is the instagram script.
*/
private function sanitize_embed_script( $node ) {
$next_element_sibling = $node->nextSibling;
while ( $next_element_sibling && ! ( $next_element_sibling instanceof DOMElement ) ) {
$next_element_sibling = $next_element_sibling->nextSibling;
}
$script_src = 'platform.twitter.com/widgets.js';
// Handle case where script is wrapped in paragraph by wpautop.
if ( $next_element_sibling instanceof DOMElement && 'p' === $next_element_sibling->nodeName ) {
$children = $next_element_sibling->getElementsByTagName( '*' );
if ( 1 === $children->length && 'script' === $children->item( 0 )->nodeName && false !== strpos( $children->item( 0 )->getAttribute( 'src' ), $script_src ) ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
return;
}
}
// Handle case where script is immediately following.
$is_embed_script = (
$next_element_sibling
&&
'script' === strtolower( $next_element_sibling->nodeName )
&&
false !== strpos( $next_element_sibling->getAttribute( 'src' ), $script_src )
);
if ( $is_embed_script ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
}
}
}

View File

@ -0,0 +1,196 @@
<?php
/**
* Class AMP_Vimeo_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Vimeo_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds
*/
class AMP_Vimeo_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?:\/\/(www\.)?vimeo\.com\/.*#i';
const RATIO = 0.5625;
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 338;
/**
* AMP_Vimeo_Embed_Handler constructor.
*
* @param array $args Height, width and maximum width for embed.
*/
public function __construct( $args = array() ) {
parent::__construct( $args );
if ( isset( $this->args['content_max_width'] ) ) {
$max_width = $this->args['content_max_width'];
$this->args['width'] = $max_width;
$this->args['height'] = round( $max_width * self::RATIO );
}
}
/**
* Register embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-vimeo', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
add_shortcode( 'vimeo', array( $this, 'shortcode' ) );
add_filter( 'wp_video_shortcode_override', array( $this, 'video_override' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-vimeo', -1 );
remove_shortcode( 'vimeo' );
}
/**
* Gets AMP-compliant markup for the Vimeo shortcode.
*
* @param array $attr The Vimeo attributes.
* @return string Vimeo shortcode markup.
*/
public function shortcode( $attr ) {
$video_id = false;
if ( isset( $attr['id'] ) ) {
$video_id = $attr['id'];
} elseif ( isset( $attr['url'] ) ) {
$video_id = $this->get_video_id_from_url( $attr['url'] );
} elseif ( isset( $attr[0] ) ) {
$video_id = $this->get_video_id_from_url( $attr[0] );
} elseif ( function_exists( 'shortcode_new_to_old_params' ) ) {
$video_id = shortcode_new_to_old_params( $attr );
}
if ( empty( $video_id ) ) {
return '';
}
return $this->render(
array(
'video_id' => $video_id,
)
);
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @param array $matches URL pattern matches.
* @param array $attr Shortcode attribues.
* @param string $url URL.
* @param string $rawattr Unmodified shortcode attributes.
* @return string Rendered oEmbed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
$video_id = $this->get_video_id_from_url( $url );
return $this->render(
array(
'url' => $url,
'video_id' => $video_id,
)
);
}
/**
* Render.
*
* @param array $args Args.
* @return string Rendered.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'video_id' => false,
)
);
if ( empty( $args['video_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-vimeo',
array(
'data-videoid' => $args['video_id'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Determine the video ID from the URL.
*
* @param string $url URL.
* @return integer Video ID.
*/
private function get_video_id_from_url( $url ) {
$parsed_url = wp_parse_url( $url );
parse_str( $parsed_url['path'], $path );
$video_id = '';
if ( $path ) {
$tok = explode( '/', $parsed_url['path'] );
$video_id = end( $tok );
}
return $video_id;
}
/**
* Override the output of Vimeo videos.
*
* This overrides the value in wp_video_shortcode().
* The pattern matching is copied from WP_Widget_Media_Video::render().
*
* @param string $html Empty variable to be replaced with shortcode markup.
* @param array $attr The shortcode attributes.
* @return string|null $markup The markup to output.
*/
public function video_override( $html, $attr ) {
if ( ! isset( $attr['src'] ) ) {
return $html;
}
$src = $attr['src'];
$vimeo_pattern = '#^https?://(.+\.)?vimeo\.com/.*#';
if ( 1 === preg_match( $vimeo_pattern, $src ) ) {
return $this->shortcode( array( $src ) );
}
return $html;
}
}

View File

@ -0,0 +1,98 @@
<?php
/**
* Class AMP_Vine_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Vine_Embed_Handler
*/
class AMP_Vine_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?://vine\.co/v/([^/?]+)#i';
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 400;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 400;
/**
* Registers embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-vine', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-vine', -1 );
}
/**
* WordPress OEmbed rendering callback.
*
* @param array $matches URL pattern matches.
* @param array $attr Matched attributes.
* @param string $url Matched URL.
* @param string $rawattr Raw attributes string.
* @return string HTML markup for rendered embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render(
array(
'url' => $url,
'vine_id' => end( $matches ),
)
);
}
/**
* Gets the rendered embed markup.
*
* @param array $args Embed rendering arguments.
* @return string HTML markup for rendered embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
'vine_id' => false,
)
);
if ( empty( $args['vine_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-vine',
array(
'data-vineid' => $args['vine_id'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
}

View File

@ -0,0 +1,222 @@
<?php
/**
* Class AMP_YouTube_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_YouTube_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds.
*/
class AMP_YouTube_Embed_Handler extends AMP_Base_Embed_Handler {
const SHORT_URL_HOST = 'youtu.be';
// Only handling single videos. Playlists are handled elsewhere.
const URL_PATTERN = '#https?://(?:www\.)?(?:youtube.com/(?:v/|e/|embed/|watch[/\#?])|youtu\.be/).*#i';
const RATIO = 0.5625;
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 338;
/**
* AMP_YouTube_Embed_Handler constructor.
*
* @param array $args Height, width and maximum width for embed.
*/
public function __construct( $args = array() ) {
parent::__construct( $args );
if ( isset( $this->args['content_max_width'] ) ) {
$max_width = $this->args['content_max_width'];
$this->args['width'] = $max_width;
$this->args['height'] = round( $max_width * self::RATIO );
}
}
/**
* Register embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-youtube', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
add_shortcode( 'youtube', array( $this, 'shortcode' ) );
add_filter( 'wp_video_shortcode_override', array( $this, 'video_override' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-youtube', -1 );
remove_shortcode( 'youtube' );
}
/**
* Gets AMP-compliant markup for the YouTube shortcode.
*
* @param array $attr The YouTube attributes.
* @return string YouTube shortcode markup.
*/
public function shortcode( $attr ) {
$url = false;
$video_id = false;
if ( isset( $attr[0] ) ) {
$url = ltrim( $attr[0], '=' );
} elseif ( function_exists( 'shortcode_new_to_old_params' ) ) {
$url = shortcode_new_to_old_params( $attr );
}
if ( empty( $url ) ) {
return '';
}
$video_id = $this->get_video_id_from_url( $url );
return $this->render(
array(
'url' => $url,
'video_id' => $video_id,
)
);
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @param array $matches URL pattern matches.
* @param array $attr Shortcode attribues.
* @param string $url URL.
* @param string $rawattr Unmodified shortcode attributes.
* @return string Rendered oEmbed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->shortcode( array( $url ) );
}
/**
* Render.
*
* @param array $args Args.
* @return string Rendered.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'video_id' => false,
)
);
if ( empty( $args['video_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-youtube',
array(
'data-videoid' => $args['video_id'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Determine the video ID from the URL.
*
* @param string $url URL.
* @return integer Video ID.
*/
private function get_video_id_from_url( $url ) {
$video_id = false;
$parsed_url = wp_parse_url( $url );
if ( self::SHORT_URL_HOST === substr( $parsed_url['host'], -strlen( self::SHORT_URL_HOST ) ) ) {
/* youtu.be/{id} */
$parts = explode( '/', $parsed_url['path'] );
if ( ! empty( $parts ) ) {
$video_id = $parts[1];
}
} else {
/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
The query looks like ?v={id} or ?list={id} */
parse_str( $parsed_url['query'], $query_args );
if ( isset( $query_args['v'] ) ) {
$video_id = $this->sanitize_v_arg( $query_args['v'] );
}
}
if ( empty( $video_id ) ) {
/* The path looks like /(v|e|embed)/{id} */
$parts = explode( '/', $parsed_url['path'] );
if ( in_array( $parts[1], array( 'v', 'e', 'embed' ), true ) ) {
$video_id = $parts[2];
}
}
return $video_id;
}
/**
* Sanitize the v= argument in the URL.
*
* @param string $value query parameters.
* @return string First set of query parameters.
*/
private function sanitize_v_arg( $value ) {
// Deal with broken params like `?v=123?rel=0`.
if ( false !== strpos( $value, '?' ) ) {
$value = strtok( $value, '?' );
}
return $value;
}
/**
* Override the output of YouTube videos.
*
* This overrides the value in wp_video_shortcode().
* The pattern matching is copied from WP_Widget_Media_Video::render().
*
* @param string $html Empty variable to be replaced with shortcode markup.
* @param array $attr The shortcode attributes.
* @return string|null $markup The markup to output.
*/
public function video_override( $html, $attr ) {
if ( ! isset( $attr['src'] ) ) {
return $html;
}
$src = $attr['src'];
$youtube_pattern = '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#';
if ( 1 === preg_match( $youtube_pattern, $src ) ) {
return $this->shortcode( array( $src ) );
}
return $html;
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* Class AMP_Analytics_Options_Submenu
*
* @package AMP
*/
/**
* Class AMP_Analytics_Options_Submenu
*/
class AMP_Analytics_Options_Submenu {
/**
* Parent menu slug for the submenu.
*
* @var string
*/
private $parent_menu_slug;
/**
* Slug for the submenu.
*
* @var string
*/
private $menu_slug;
/**
* Menu page instance for rendering the content.
*
* @var AMP_Analytics_Options_Submenu_Page
*/
private $menu_page;
/**
* Constructor.
*
* @param string $parent_menu_slug Slug of the parent menu item.
*/
public function __construct( $parent_menu_slug ) {
$this->parent_menu_slug = $parent_menu_slug;
$this->menu_slug = 'amp-analytics-options';
$this->menu_page = new AMP_Analytics_Options_Submenu_Page();
}
/**
* Adds the submenu item and adds necessary hooks.
*/
public function init() {
$this->add_submenu();
add_action(
'admin_print_styles-amp_page_' . $this->menu_slug,
array( $this, 'amp_options_styles' )
);
}
/**
* Adds the submenu page and registers the rendering callback.
*/
private function add_submenu() {
add_submenu_page(
$this->parent_menu_slug,
__( 'AMP Analytics Options', 'amp' ),
__( 'Analytics', 'amp' ),
'manage_options',
$this->menu_slug,
array( $this->menu_page, 'render' )
);
}
/**
* Prints extra styles for the page content.
*/
public function amp_options_styles() {
?>
<style>
.analytics-data-container .button.delete,
.analytics-data-container .button.delete:hover,
.analytics-data-container .button.delete:active,
.analytics-data-container .button.delete:focus {
background: red;
border-color: red;
text-shadow: 0 0 0;
margin: 0 5px;
}
.amp-analytics-options.notice {
width: 300px;
}
</style>
<?php
}
}

View File

@ -0,0 +1,665 @@
<?php
/**
* Class AMP_Options_Manager.
*
* @package AMP
*/
/**
* Class AMP_Options_Manager
*/
class AMP_Options_Manager {
/**
* Option name.
*
* @var string
*/
const OPTION_NAME = 'amp-options';
/**
* Default option values.
*
* @var array
*/
protected static $defaults = array(
'theme_support' => 'disabled',
'supported_post_types' => array( 'post' ),
'analytics' => array(),
'auto_accept_sanitization' => true,
'accept_tree_shaking' => true,
'disable_admin_bar' => false,
'all_templates_supported' => true,
'supported_templates' => array( 'is_singular' ),
'enable_response_caching' => true,
'version' => AMP__VERSION,
);
/**
* Register settings.
*/
public static function register_settings() {
register_setting(
self::OPTION_NAME,
self::OPTION_NAME,
array(
'type' => 'array',
'sanitize_callback' => array( __CLASS__, 'validate_options' ),
)
);
add_action( 'update_option_' . self::OPTION_NAME, array( __CLASS__, 'maybe_flush_rewrite_rules' ), 10, 2 );
add_action( 'admin_notices', array( __CLASS__, 'render_welcome_notice' ) );
add_action( 'admin_notices', array( __CLASS__, 'persistent_object_caching_notice' ) );
add_action( 'admin_notices', array( __CLASS__, 'render_cache_miss_notice' ) );
add_action( 'admin_notices', array( __CLASS__, 'render_php_css_parser_conflict_notice' ) );
}
/**
* Flush rewrite rules if the supported_post_types have changed.
*
* @since 0.6.2
*
* @param array $old_options Old options.
* @param array $new_options New options.
*/
public static function maybe_flush_rewrite_rules( $old_options, $new_options ) {
$old_post_types = isset( $old_options['supported_post_types'] ) ? $old_options['supported_post_types'] : array();
$new_post_types = isset( $new_options['supported_post_types'] ) ? $new_options['supported_post_types'] : array();
sort( $old_post_types );
sort( $new_post_types );
if ( $old_post_types !== $new_post_types ) {
flush_rewrite_rules( false );
}
}
/**
* Get plugin options.
*
* @return array Options.
*/
public static function get_options() {
$options = get_option( self::OPTION_NAME, array() );
if ( empty( $options ) ) {
$options = array();
}
self::$defaults['enable_response_caching'] = wp_using_ext_object_cache();
return array_merge( self::$defaults, $options );
}
/**
* Get plugin option.
*
* @param string $option Plugin option name.
* @param bool $default Default value.
*
* @return mixed Option value.
*/
public static function get_option( $option, $default = false ) {
$amp_options = self::get_options();
if ( ! isset( $amp_options[ $option ] ) ) {
return $default;
}
return $amp_options[ $option ];
}
/**
* Validate options.
*
* @param array $new_options Plugin options.
* @return array Options.
*/
public static function validate_options( $new_options ) {
$options = self::get_options();
if ( ! current_user_can( 'manage_options' ) ) {
return $options;
}
// Theme support.
$recognized_theme_supports = array(
'disabled',
'paired',
'native',
);
if ( isset( $new_options['theme_support'] ) && in_array( $new_options['theme_support'], $recognized_theme_supports, true ) ) {
$options['theme_support'] = $new_options['theme_support'];
// If this option was changed, display a notice with the new template mode.
if ( self::get_option( 'theme_support' ) !== $new_options['theme_support'] ) {
add_action( 'update_option_' . self::OPTION_NAME, array( __CLASS__, 'handle_updated_theme_support_option' ) );
}
}
$options['auto_accept_sanitization'] = ! empty( $new_options['auto_accept_sanitization'] );
$options['accept_tree_shaking'] = ! empty( $new_options['accept_tree_shaking'] );
$options['disable_admin_bar'] = ! empty( $new_options['disable_admin_bar'] );
// Validate post type support.
$options['supported_post_types'] = array();
if ( isset( $new_options['supported_post_types'] ) ) {
foreach ( $new_options['supported_post_types'] as $post_type ) {
if ( ! post_type_exists( $post_type ) ) {
add_settings_error( self::OPTION_NAME, 'unknown_post_type', __( 'Unrecognized post type.', 'amp' ) );
} else {
$options['supported_post_types'][] = $post_type;
}
}
}
$theme_support_args = AMP_Theme_Support::get_theme_support_args();
$is_template_support_required = ( isset( $theme_support_args['templates_supported'] ) && 'all' === $theme_support_args['templates_supported'] );
if ( ! $is_template_support_required && ! isset( $theme_support_args['available_callback'] ) ) {
$options['all_templates_supported'] = ! empty( $new_options['all_templates_supported'] );
// Validate supported templates.
$options['supported_templates'] = array();
if ( isset( $new_options['supported_templates'] ) ) {
$options['supported_templates'] = array_intersect(
$new_options['supported_templates'],
array_keys( AMP_Theme_Support::get_supportable_templates() )
);
}
}
// Validate analytics.
if ( isset( $new_options['analytics'] ) ) {
foreach ( $new_options['analytics'] as $id => $data ) {
// Check save/delete pre-conditions and proceed if correct.
if ( empty( $data['type'] ) || empty( $data['config'] ) ) {
add_settings_error( self::OPTION_NAME, 'missing_analytics_vendor_or_config', __( 'Missing vendor type or config.', 'amp' ) );
continue;
}
// Validate JSON configuration.
$is_valid_json = AMP_HTML_Utils::is_valid_json( $data['config'] );
if ( ! $is_valid_json ) {
add_settings_error( self::OPTION_NAME, 'invalid_analytics_config_json', __( 'Invalid analytics config JSON.', 'amp' ) );
continue;
}
$entry_vendor_type = preg_replace( '/[^a-zA-Z0-9_\-]/', '', $data['type'] );
$entry_config = trim( $data['config'] );
if ( ! empty( $data['id'] ) && '__new__' !== $data['id'] ) {
$entry_id = sanitize_key( $data['id'] );
} else {
// Generate a hash string to uniquely identify this entry.
$entry_id = substr( md5( $entry_vendor_type . $entry_config ), 0, 12 );
// Avoid duplicates.
if ( isset( $options['analytics'][ $entry_id ] ) ) {
add_settings_error( self::OPTION_NAME, 'duplicate_analytics_entry', __( 'Duplicate analytics entry found.', 'amp' ) );
continue;
}
}
if ( isset( $data['delete'] ) ) {
unset( $options['analytics'][ $entry_id ] );
} else {
$options['analytics'][ $entry_id ] = array(
'type' => $entry_vendor_type,
'config' => $entry_config,
);
}
}
}
// Store the current version with the options so we know the format.
$options['version'] = AMP__VERSION;
// Handle the caching option.
$options['enable_response_caching'] = (
wp_using_ext_object_cache()
&&
! empty( $new_options['enable_response_caching'] )
);
if ( $options['enable_response_caching'] ) {
AMP_Theme_Support::reset_cache_miss_url_option();
}
return $options;
}
/**
* Check for errors with updating the supported post types.
*
* @since 0.6
* @see add_settings_error()
*/
public static function check_supported_post_type_update_errors() {
// If all templates are supported then skip check since all post types are also supported. This option only applies with native/transitional theme support.
if ( self::get_option( 'all_templates_supported', false ) && 'disabled' !== self::get_option( 'theme_support' ) ) {
return;
}
$supported_types = self::get_option( 'supported_post_types', array() );
foreach ( AMP_Post_Type_Support::get_eligible_post_types() as $name ) {
$post_type = get_post_type_object( $name );
if ( empty( $post_type ) ) {
continue;
}
$post_type_supported = post_type_supports( $post_type->name, AMP_Post_Type_Support::SLUG );
$is_support_elected = in_array( $post_type->name, $supported_types, true );
$error = null;
$code = null;
if ( $is_support_elected && ! $post_type_supported ) {
/* translators: %s: Post type name. */
$error = __( '"%s" could not be activated because support is removed by a plugin or theme', 'amp' );
$code = sprintf( '%s_activation_error', $post_type->name );
} elseif ( ! $is_support_elected && $post_type_supported ) {
/* translators: %s: Post type name. */
$error = __( '"%s" could not be deactivated because support is added by a plugin or theme', 'amp' );
$code = sprintf( '%s_deactivation_error', $post_type->name );
}
if ( isset( $error, $code ) ) {
add_settings_error(
self::OPTION_NAME,
$code,
sprintf(
$error,
isset( $post_type->label ) ? $post_type->label : $post_type->name
)
);
}
}
}
/**
* Update plugin option.
*
* @param string $option Plugin option name.
* @param mixed $value Plugin option value.
*
* @return bool Whether update succeeded.
*/
public static function update_option( $option, $value ) {
$amp_options = self::get_options();
$amp_options[ $option ] = $value;
return update_option( self::OPTION_NAME, $amp_options, false );
}
/**
* Handle analytics submission.
*/
public static function handle_analytics_submit() {
// Request must come from user with right capabilities.
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Sorry, you do not have the necessary permissions to perform this action', 'amp' ) );
}
// Ensure request is coming from analytics option form.
check_admin_referer( 'analytics-options', 'analytics-options' );
if ( isset( $_POST['amp-options']['analytics'] ) ) {
self::update_option( 'analytics', wp_unslash( $_POST['amp-options']['analytics'] ) );
$errors = get_settings_errors( self::OPTION_NAME );
if ( empty( $errors ) ) {
add_settings_error( self::OPTION_NAME, 'settings_updated', __( 'The analytics entry was successfully saved!', 'amp' ), 'updated' );
$errors = get_settings_errors( self::OPTION_NAME );
}
set_transient( 'settings_errors', $errors );
}
/*
* Redirect to keep the user in the analytics options page.
* Wrap in is_admin() to enable phpunit tests to exercise this code.
*/
wp_safe_redirect( admin_url( 'admin.php?page=amp-analytics-options&settings-updated=1' ) );
exit;
}
/**
* Update analytics options.
*
* @codeCoverageIgnore
* @deprecated
* @param array $data Unsanitized unslashed data.
* @return bool Whether options were updated.
*/
public static function update_analytics_options( $data ) {
_deprecated_function( __METHOD__, '0.6', __CLASS__ . '::update_option' );
return self::update_option( 'analytics', wp_unslash( $data ) );
}
/**
* Renders the welcome notice on the 'AMP Settings' page.
*
* Uses the user meta values for the dismissed WP pointers.
* So once the user dismisses this notice, it will never appear again.
*/
public static function render_welcome_notice() {
if ( 'toplevel_page_' . self::OPTION_NAME !== get_current_screen()->id ) {
return;
}
$notice_id = 'amp-welcome-notice-1';
$dismissed = get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true );
if ( in_array( $notice_id, explode( ',', strval( $dismissed ) ), true ) ) {
return;
}
?>
<div class="amp-welcome-notice notice notice-info is-dismissible" id="<?php echo esc_attr( $notice_id ); ?>">
<div class="notice-dismiss"></div>
<div class="amp-welcome-icon-holder">
<img class="amp-welcome-icon" src="<?php echo esc_url( amp_get_asset_url( 'images/amp-welcome-icon.svg' ) ); ?>" alt="<?php esc_html_e( 'Illustration of WordPress running AMP plugin.', 'amp' ); ?>" />
</div>
<h1><?php esc_html_e( 'Welcome to AMP for WordPress', 'amp' ); ?></h1>
<h3><?php esc_html_e( 'Bring the speed and features of the open source AMP project to your site, complete with the tools to support content authoring and website development.', 'amp' ); ?></h3>
<h3><?php esc_html_e( 'From granular controls that help you create AMP content, to Core Gutenberg support, to a sanitizer that only shows visitors error-free pages, to a full error workflow for developers, this release enables rich, performant experiences for your WordPress site.', 'amp' ); ?></h3>
<a href="https://amp-wp.org/getting-started/" target="_blank" class="button button-primary"><?php esc_html_e( 'Learn More', 'amp' ); ?></a>
</div>
<script>
jQuery( function( $ ) {
// On dismissing the notice, make a POST request to store this notice with the dismissed WP pointers so it doesn't display again.
$( <?php echo wp_json_encode( "#$notice_id" ); ?> ).on( 'click', '.notice-dismiss', function() {
$.post( ajaxurl, {
pointer: <?php echo wp_json_encode( $notice_id ); ?>,
action: 'dismiss-wp-pointer'
} );
} );
} );
</script>
<style type="text/css">
.amp-welcome-notice {
padding: 38px;
}
.amp-welcome-notice + .notice {
clear: both;
}
.amp-welcome-icon-holder {
width: 200px;
height: 200px;
float: left;
margin: 0 38px 38px 0;
}
.amp-welcome-icon {
width: 100%;
height: 100%;
display: block;
}
.amp-welcome-notice h1 {
font-weight: bold;
}
.amp-welcome-notice h3 {
font-size: 16px;
font-weight: 500;
}
</style>
<?php
}
/**
* Outputs an admin notice if persistent object cache is not present.
*
* @return void
*/
public static function persistent_object_caching_notice() {
if ( ! wp_using_ext_object_cache() && 'toplevel_page_' . self::OPTION_NAME === get_current_screen()->id ) {
printf(
'<div class="notice notice-warning"><p>%s</p></div>',
sprintf(
/* translators: %s: Persistent object cache support URL */
__( 'The AMP plugin performs at its best when persistent object cache is enabled. <a href="%s">More details</a>', 'amp' ), // phpcs:ignore WordPress.Security.EscapeOutput
esc_url( __( 'https://codex.wordpress.org/Class_Reference/WP_Object_Cache#Persistent_Caching', 'amp' ) )
)
);
}
}
/**
* Render the cache miss admin notice.
*
* @return void
*/
public static function render_cache_miss_notice() {
if ( 'toplevel_page_' . self::OPTION_NAME !== get_current_screen()->id ) {
return;
}
if ( ! self::show_response_cache_disabled_notice() ) {
return;
}
printf(
'<div class="notice notice-warning is-dismissible"><p>%s</p></div>',
sprintf(
/* translators: %s: post-processor cache support URL */
__( 'The AMP plugin&lsquo;s post-processor cache was disabled due to the detection of highly-variable content. <a href="%s">More details</a>', 'amp' ), // phpcs:ignore WordPress.Security.EscapeOutput
esc_url( __( 'https://github.com/ampproject/amp-wp/wiki/Post-Processor-Cache', 'amp' ) )
)
);
}
/**
* Render PHP-CSS-Parser conflict notice.
*
* @return void
*/
public static function render_php_css_parser_conflict_notice() {
if ( 'toplevel_page_' . self::OPTION_NAME !== get_current_screen()->id ) {
return;
}
if ( AMP_Style_Sanitizer::has_required_php_css_parser() ) {
return;
}
try {
$reflection = new ReflectionClass( 'Sabberworm\CSS\CSSList\CSSList' );
$source_dir = str_replace(
trailingslashit( WP_CONTENT_DIR ),
'',
preg_replace( '#/vendor/sabberworm/.+#', '', $reflection->getFileName() )
);
printf(
'<div class="notice notice-warning"><p>%s</p></div>',
sprintf(
/* translators: %s: path to the conflicting library */
__( 'A conflicting version of PHP-CSS-Parser appears to be installed by another plugin or theme (located in %s). Because of this, CSS processing will be limited, and tree shaking will not be available.', 'amp' ), // phpcs:ignore WordPress.Security.EscapeOutput
'<code>' . esc_html( $source_dir ) . '</code>'
)
);
} catch ( ReflectionException $e ) {
printf(
'<div class="notice notice-warning"><p>%s</p></div>',
esc_html__( 'PHP-CSS-Parser is not available so CSS processing will not be available.', 'amp' )
);
}
}
/**
* Show the response cache disabled notice.
*
* @since 1.0
*
* @return bool
*/
public static function show_response_cache_disabled_notice() {
return (
wp_using_ext_object_cache()
&&
! self::get_option( 'enable_response_caching' )
&&
AMP_Theme_Support::exceeded_cache_miss_threshold()
);
}
/**
* Adds a message for an update of the theme support setting.
*/
public static function handle_updated_theme_support_option() {
$template_mode = self::get_option( 'theme_support' );
// Make sure post type support has been added for sake of amp_admin_get_preview_permalink().
foreach ( AMP_Post_Type_Support::get_eligible_post_types() as $post_type ) {
remove_post_type_support( $post_type, AMP_Post_Type_Support::SLUG );
}
AMP_Post_Type_Support::add_post_type_support();
// Ensure theme support flags are set properly according to the new mode so that proper AMP URL can be generated.
$has_theme_support = ( 'native' === $template_mode || 'paired' === $template_mode );
if ( $has_theme_support ) {
$theme_support = current_theme_supports( AMP_Theme_Support::SLUG );
if ( ! is_array( $theme_support ) ) {
$theme_support = array();
}
$theme_support['paired'] = 'paired' === $template_mode;
add_theme_support( AMP_Theme_Support::SLUG, $theme_support );
} else {
remove_theme_support( AMP_Theme_Support::SLUG ); // So that the amp_get_permalink() will work for reader mode URL.
}
$url = amp_admin_get_preview_permalink();
$notice_type = 'updated';
$review_messages = array();
if ( $url && $has_theme_support ) {
$validation = AMP_Validation_Manager::validate_url( $url );
if ( is_wp_error( $validation ) ) {
$review_messages[] = esc_html(
sprintf(
/* translators: 1: error message. 2: error code. */
__( 'However, there was an error when checking the AMP validity for your site.', 'amp' ),
$validation->get_error_message(),
$validation->get_error_code()
)
);
$error_message = $validation->get_error_message();
if ( $error_message ) {
$review_messages[] = $error_message;
} else {
/* translators: %s is the error code */
$review_messages[] = esc_html( sprintf( __( 'Error code: %s.', 'amp' ), $validation->get_error_code() ) );
}
$notice_type = 'error';
} elseif ( is_array( $validation ) ) {
$new_errors = 0;
$rejected_errors = 0;
$errors = wp_list_pluck( $validation['results'], 'error' );
foreach ( $errors as $error ) {
$sanitization = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization( $error );
$is_new_rejected = AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS === $sanitization['status'];
if ( $is_new_rejected || AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS === $sanitization['status'] ) {
$new_errors++;
}
if ( $is_new_rejected || AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_REJECTED_STATUS === $sanitization['status'] ) {
$rejected_errors++;
}
}
$invalid_url_post_id = AMP_Validated_URL_Post_Type::store_validation_errors( $errors, $url );
$invalid_url_screen_url = ! is_wp_error( $invalid_url_post_id ) ? get_edit_post_link( $invalid_url_post_id, 'raw' ) : null;
if ( $rejected_errors > 0 ) {
$notice_type = 'error';
$message = wp_kses_post(
sprintf(
/* translators: %s is count of rejected errors */
_n(
'However, AMP is not yet available due to %s validation error (for one URL at least).',
'However, AMP is not yet available due to %s validation errors (for one URL at least).',
number_format_i18n( $rejected_errors ),
'amp'
),
$rejected_errors,
esc_url( $invalid_url_screen_url )
)
);
if ( $invalid_url_screen_url ) {
$message .= ' ' . wp_kses_post(
sprintf(
/* translators: %s is URL to review issues */
_n(
'<a href="%s">Review Issue</a>.',
'<a href="%s">Review Issues</a>.',
$rejected_errors,
'amp'
),
esc_url( $invalid_url_screen_url )
)
);
}
$review_messages[] = $message;
} else {
$message = wp_kses_post(
sprintf(
/* translators: %s is an AMP URL */
__( 'View an <a href="%s">AMP version of your site</a>.', 'amp' ),
esc_url( $url )
)
);
if ( $new_errors > 0 && $invalid_url_screen_url ) {
$message .= ' ' . wp_kses_post(
sprintf(
/* translators: 1: URL to review issues. 2: count of new errors. */
_n(
'Please also <a href="%1$s">review %2$s issue</a> which may need to be fixed (for one URL at least).',
'Please also <a href="%1$s">review %2$s issues</a> which may need to be fixed (for one URL at least).',
$new_errors,
'amp'
),
esc_url( $invalid_url_screen_url ),
number_format_i18n( $new_errors )
)
);
}
$review_messages[] = $message;
}
}
}
switch ( $template_mode ) {
case 'native':
$message = esc_html__( 'Native mode activated!', 'amp' );
if ( $review_messages ) {
$message .= ' ' . join( ' ', $review_messages );
}
break;
case 'paired':
$message = esc_html__( 'Transitional mode activated!', 'amp' );
if ( $review_messages ) {
$message .= ' ' . join( ' ', $review_messages );
}
break;
case 'disabled':
$message = wp_kses_post(
sprintf(
/* translators: %s is an AMP URL */
__( 'Reader mode activated! View the <a href="%s">AMP version of a recent post</a>. It is recommended that you upgrade to Native or Transitional mode.', 'amp' ),
esc_url( $url )
)
);
break;
}
if ( isset( $message ) ) {
add_settings_error( self::OPTION_NAME, 'template_mode_updated', $message, $notice_type );
}
}
}

View File

@ -0,0 +1,616 @@
<?php
/**
* AMP Options.
*
* @package AMP
*/
/**
* AMP_Options_Menu class.
*/
class AMP_Options_Menu {
/**
* The AMP svg menu icon.
*
* @var string
*/
const ICON_BASE64_SVG = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iNjJweCIgaGVpZ2h0PSI2MnB4IiB2aWV3Qm94PSIwIDAgNjIgNjIiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+ICAgICAgICA8dGl0bGU+QU1QLUJyYW5kLUJsYWNrLUljb248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iYW1wLWxvZ28taW50ZXJuYWwtc2l0ZSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+ICAgICAgICA8ZyBpZD0iQU1QLUJyYW5kLUJsYWNrLUljb24iIGZpbGw9IiMwMDAwMDAiPiAgICAgICAgICAgIDxwYXRoIGQ9Ik00MS42Mjg4NjY3LDI4LjE2MTQzMzMgTDI4LjYyNDM2NjcsNDkuODAzNTY2NyBMMjYuMjY4MzY2Nyw0OS44MDM1NjY3IEwyOC41OTc1LDM1LjcwMTY2NjcgTDIxLjM4MzgsMzUuNzEwOTY2NyBDMjEuMzgzOCwzNS43MTA5NjY3IDIxLjMxNTYsMzUuNzEzMDMzMyAyMS4yODM1NjY3LDM1LjcxMzAzMzMgQzIwLjYzMzYsMzUuNzEzMDMzMyAyMC4xMDc2MzMzLDM1LjE4NzA2NjcgMjAuMTA3NjMzMywzNC41MzcxIEMyMC4xMDc2MzMzLDM0LjI1ODEgMjAuMzY3LDMzLjc4NTg2NjcgMjAuMzY3LDMzLjc4NTg2NjcgTDMzLjMyOTEzMzMsMTIuMTY5NTY2NyBMMzUuNzI0NCwxMi4xNzk5IEwzMy4zMzYzNjY3LDI2LjMwMzUgTDQwLjU4NzI2NjcsMjYuMjk0MiBDNDAuNTg3MjY2NywyNi4yOTQyIDQwLjY2NDc2NjcsMjYuMjkzMTY2NyA0MC43MDE5NjY3LDI2LjI5MzE2NjcgQzQxLjM1MTkzMzMsMjYuMjkzMTY2NyA0MS44Nzc5LDI2LjgxOTEzMzMgNDEuODc3OSwyNy40NjkxIEM0MS44Nzc5LDI3LjczMjYgNDEuNzc0NTY2NywyNy45NjQwNjY3IDQxLjYyNzgzMzMsMjguMTYwNCBMNDEuNjI4ODY2NywyOC4xNjE0MzMzIFogTTMxLDAgQzEzLjg3ODcsMCAwLDEzLjg3OTczMzMgMCwzMSBDMCw0OC4xMjEzIDEzLjg3ODcsNjIgMzEsNjIgQzQ4LjEyMDI2NjcsNjIgNjIsNDguMTIxMyA2MiwzMSBDNjIsMTMuODc5NzMzMyA0OC4xMjAyNjY3LDAgMzEsMCBMMzEsMCBaIiBpZD0iRmlsbC0xIj48L3BhdGg+ICAgICAgICA8L2c+ICAgIDwvZz48L3N2Zz4=';
/**
* Initialize.
*/
public function init() {
add_action( 'admin_post_amp_analytics_options', 'AMP_Options_Manager::handle_analytics_submit' );
add_action( 'admin_menu', array( $this, 'add_menu_items' ), 9 );
$plugin_file = preg_replace( '#.+/(?=.+?/.+?)#', '', AMP__FILE__ );
add_filter( "plugin_action_links_{$plugin_file}", array( $this, 'add_plugin_action_links' ) );
}
/**
* Add plugin action links.
*
* @param array $links Links.
* @return array Modified links.
*/
public function add_plugin_action_links( $links ) {
return array_merge(
array(
'settings' => sprintf(
'<a href="%1$s">%2$s</a>',
esc_url( add_query_arg( 'page', AMP_Options_Manager::OPTION_NAME, admin_url( 'admin.php' ) ) ),
__( 'Settings', 'amp' )
),
),
$links
);
}
/**
* Add menu.
*/
public function add_menu_items() {
add_menu_page(
__( 'AMP Options', 'amp' ),
__( 'AMP', 'amp' ),
'edit_posts',
AMP_Options_Manager::OPTION_NAME,
array( $this, 'render_screen' ),
self::ICON_BASE64_SVG
);
add_submenu_page(
AMP_Options_Manager::OPTION_NAME,
__( 'AMP Settings', 'amp' ),
__( 'General', 'amp' ),
'edit_posts',
AMP_Options_Manager::OPTION_NAME
);
add_settings_section(
'general',
false,
'__return_false',
AMP_Options_Manager::OPTION_NAME
);
add_settings_field(
'theme_support',
__( 'Template Mode', 'amp' ),
array( $this, 'render_theme_support' ),
AMP_Options_Manager::OPTION_NAME,
'general',
array(
'class' => 'theme_support',
)
);
add_settings_field(
'validation',
__( 'Validation Handling', 'amp' ),
array( $this, 'render_validation_handling' ),
AMP_Options_Manager::OPTION_NAME,
'general',
array(
'class' => 'amp-validation-field',
)
);
add_settings_field(
'supported_templates',
__( 'Supported Templates', 'amp' ),
array( $this, 'render_supported_templates' ),
AMP_Options_Manager::OPTION_NAME,
'general',
array(
'class' => 'amp-template-support-field',
)
);
if ( wp_using_ext_object_cache() ) {
add_settings_field(
'caching',
__( 'Caching', 'amp' ),
array( $this, 'render_caching' ),
AMP_Options_Manager::OPTION_NAME,
'general',
array(
'class' => 'amp-caching-field',
)
);
}
$submenus = array(
new AMP_Analytics_Options_Submenu( AMP_Options_Manager::OPTION_NAME ),
);
// Create submenu items and calls on the Submenu Page object to render the actual contents of the page.
foreach ( $submenus as $submenu ) {
$submenu->init();
}
}
/**
* Render theme support.
*
* @since 1.0
*/
public function render_theme_support() {
$theme_support = AMP_Options_Manager::get_option( 'theme_support' );
/* translators: %s: URL to the documentation. */
$native_description = sprintf( __( 'Integrates AMP as the framework for your site by using the actives theme templates and styles to render AMP responses. This means your site is <b>AMP-first</b> and your canonical URLs are AMP! Depending on your theme/plugins, a varying level of <a href="%s">development work</a> may be required.', 'amp' ), esc_url( 'https://amp-wp.org/documentation/developing-wordpress-amp-sites/' ) );
/* translators: %s: URL to the documentation. */
$transitional_description = sprintf( __( 'Uses the active themes templates to generate non-AMP and AMP versions of your content, allowing for each canonical URL to have a corresponding (paired) AMP URL. This mode is useful to progressively transition towards a fully AMP-first site. Depending on your theme/plugins, a varying level of <a href="%s">development work</a> may be required.', 'amp' ), esc_url( 'https://amp-wp.org/documentation/developing-wordpress-amp-sites/' ) );
$reader_description = __( 'Formerly called the <b>classic mode</b>, this mode generates paired AMP content using simplified templates which may not match the look-and-feel of your site. Only posts/pages can be served as AMP in Reader mode. No redirection is performed for mobile visitors; AMP pages are served by AMP consumption platforms.', 'amp' );
/* translators: %s: URL to the ecosystem page. */
$ecosystem_description = sprintf( __( 'For a list of themes and plugins that are known to be AMP compatible, please see the <a href="%s">ecosystem page</a>.' ), esc_url( 'https://amp-wp.org/ecosystem/' ) );
$builtin_support = in_array( get_template(), AMP_Core_Theme_Sanitizer::get_supported_themes(), true );
?>
<?php if ( current_theme_supports( AMP_Theme_Support::SLUG ) && ! AMP_Theme_Support::is_support_added_via_option() ) : ?>
<div class="notice notice-info notice-alt inline">
<p><?php esc_html_e( 'Your active theme has built-in AMP support.', 'amp' ); ?></p>
</div>
<p>
<?php echo wp_kses_post( $ecosystem_description ); ?>
</p>
<p>
<?php if ( amp_is_canonical() ) : ?>
<strong><?php esc_html_e( 'Native:', 'amp' ); ?></strong>
<?php echo wp_kses_post( $native_description ); ?>
<?php else : ?>
<strong><?php esc_html_e( 'Transitional:', 'amp' ); ?></strong>
<?php echo wp_kses_post( $transitional_description ); ?>
<?php endif; ?>
</p>
<?php else : ?>
<fieldset <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php if ( $builtin_support ) : ?>
<div class="notice notice-success notice-alt inline">
<p><?php esc_html_e( 'Your active theme is known to work well in transitional or native mode.', 'amp' ); ?></p>
</div>
<?php endif; ?>
<p>
<?php echo wp_kses_post( $ecosystem_description ); ?>
</p>
<dl>
<dt>
<input type="radio" id="theme_support_native" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[theme_support]' ); ?>" value="native" <?php checked( $theme_support, 'native' ); ?>>
<label for="theme_support_native">
<strong><?php esc_html_e( 'Native', 'amp' ); ?></strong>
</label>
</dt>
<dd>
<?php echo wp_kses_post( $native_description ); ?>
</dd>
<dt>
<input type="radio" id="theme_support_transitional" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[theme_support]' ); ?>" value="paired" <?php checked( $theme_support, 'paired' ); ?>>
<label for="theme_support_transitional">
<strong><?php esc_html_e( 'Transitional', 'amp' ); ?></strong>
</label>
</dt>
<dd>
<?php echo wp_kses_post( $transitional_description ); ?>
</dd>
<dt>
<input type="radio" id="theme_support_disabled" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[theme_support]' ); ?>" value="disabled" <?php checked( $theme_support, 'disabled' ); ?>>
<label for="theme_support_disabled">
<strong><?php esc_html_e( 'Reader', 'amp' ); ?></strong>
</label>
</dt>
<dd>
<?php echo wp_kses_post( $reader_description ); ?>
<?php if ( ! current_theme_supports( AMP_Theme_Support::SLUG ) && wp_count_posts( AMP_Validated_URL_Post_Type::POST_TYPE_SLUG )->publish > 0 ) : ?>
<div class="notice notice-info inline notice-alt">
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: %1: link to invalid URLs. 2: link to validation errors. */
__( 'View current site compatibility results for native and transitional modes: %1$s and %2$s.', 'amp' ),
sprintf(
'<a href="%s">%s</a>',
esc_url( add_query_arg( 'post_type', AMP_Validated_URL_Post_Type::POST_TYPE_SLUG, admin_url( 'edit.php' ) ) ),
esc_html( get_post_type_object( AMP_Validated_URL_Post_Type::POST_TYPE_SLUG )->labels->name )
),
sprintf(
'<a href="%s">%s</a>',
esc_url(
add_query_arg(
array(
'taxonomy' => AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG,
'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG,
),
admin_url( 'edit-tags.php' )
)
),
esc_html( get_taxonomy( AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG )->labels->name )
)
)
);
?>
</p>
</div>
<?php endif; ?>
</dd>
</dl>
</fieldset>
<?php endif; ?>
<?php
}
/**
* Post types support section renderer.
*
* @todo If dirty AMP is ever allowed (that is, post-processed documents which can be served with non-sanitized valdation errors), then automatically forcing sanitization in native should be able to be turned off.
*
* @since 1.0
*/
public function render_validation_handling() {
?>
<fieldset <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php
$auto_sanitization = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization(
array(
'code' => 'non_existent',
)
);
remove_filter( 'amp_validation_error_sanitized', array( 'AMP_Validation_Manager', 'filter_tree_shaking_validation_error_as_accepted' ) );
$tree_shaking_sanitization = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization(
array(
'code' => AMP_Style_Sanitizer::TREE_SHAKING_ERROR_CODE,
)
);
$forced_sanitization = 'with_filter' === $auto_sanitization['forced'];
$forced_tree_shaking = $forced_sanitization || 'with_filter' === $tree_shaking_sanitization['forced'];
?>
<?php if ( $forced_sanitization ) : ?>
<div class="notice notice-info notice-alt inline">
<p><?php esc_html_e( 'Your install is configured via a theme or plugin to automatically sanitize any AMP validation error that is encountered.', 'amp' ); ?></p>
</div>
<input type="hidden" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[auto_accept_sanitization]' ); ?>" value="<?php echo AMP_Options_Manager::get_option( 'auto_accept_sanitization' ) ? 'on' : ''; ?>">
<?php else : ?>
<div class="amp-auto-accept-sanitize-canonical notice notice-info notice-alt inline">
<p><?php esc_html_e( 'All new validation errors are automatically accepted when in native mode.', 'amp' ); ?></p>
</div>
<div class="amp-auto-accept-sanitize">
<p>
<label for="auto_accept_sanitization">
<input id="auto_accept_sanitization" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[auto_accept_sanitization]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'auto_accept_sanitization' ) ); ?>>
<?php esc_html_e( 'Automatically accept sanitization for any newly encountered AMP validation errors.', 'amp' ); ?>
</label>
</p>
<p class="description">
<?php esc_html_e( 'This will ensure your responses are always valid AMP but some important content may get stripped out (e.g. scripts).', 'amp' ); ?>
<?php
echo wp_kses_post(
sprintf(
/* translators: %s is URL to validation errors screen */
__( 'Existing validation errors which you have already rejected will not be modified (you may want to consider <a href="%s">bulk-accepting them</a>).', 'amp' ),
esc_url(
add_query_arg(
array(
'taxonomy' => AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG,
'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG,
),
admin_url( 'edit-tags.php' )
)
)
)
)
?>
</p>
</div>
<?php endif; ?>
<?php if ( $forced_tree_shaking ) : ?>
<input type="hidden" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[accept_tree_shaking]' ); ?>" value="<?php echo AMP_Options_Manager::get_option( 'accept_tree_shaking' ) ? 'on' : ''; ?>">
<?php else : ?>
<div class="amp-tree-shaking">
<p>
<label for="accept_tree_shaking">
<input id="accept_tree_shaking" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[accept_tree_shaking]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'accept_tree_shaking' ) ); ?>>
<?php esc_html_e( 'Automatically remove CSS rules that are not relevant to a given page (tree shaking).', 'amp' ); ?>
</label>
</p>
<p class="description">
<?php esc_html_e( 'AMP limits the total amount of CSS to no more than 50KB; any more than this will cause a validation error. The need to tree shake the CSS is not done by default because in some situations (in particular for dynamic content) it can result in CSS rules being removed that are needed.', 'amp' ); ?>
</p>
</div>
<?php endif; ?>
<script>
(function( $ ) {
var getThemeSupportMode = function() {
var checkedInput = $( 'input[type=radio][name="amp-options[theme_support]"]:checked' );
if ( 0 === checkedInput.length ) {
return <?php echo wp_json_encode( amp_is_canonical() ? 'native' : 'paired' ); ?>;
}
return checkedInput.val();
};
var updateTreeShakingHiddenClass = function() {
var checkbox = $( '#auto_accept_sanitization' );
$( '.amp-tree-shaking' ).toggleClass( 'hidden', checkbox.prop( 'checked' ) && 'native' !== getThemeSupportMode() );
};
var updateHiddenClasses = function() {
var themeSupportMode = getThemeSupportMode();
$( '.amp-auto-accept-sanitize' ).toggleClass( 'hidden', 'native' === themeSupportMode );
$( '.amp-validation-field' ).toggleClass( 'hidden', 'disabled' === themeSupportMode );
$( '.amp-auto-accept-sanitize-canonical' ).toggleClass( 'hidden', 'native' !== themeSupportMode );
updateTreeShakingHiddenClass();
};
$( 'input[type=radio][name="amp-options[theme_support]"]' ).change( updateHiddenClasses );
$( '#auto_accept_sanitization' ).change( updateTreeShakingHiddenClass );
updateHiddenClasses();
})( jQuery );
</script>
<p>
<label for="disable_admin_bar">
<input id="disable_admin_bar" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[disable_admin_bar]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'disable_admin_bar' ) ); ?>>
<?php esc_html_e( 'Disable admin bar on AMP pages.', 'amp' ); ?>
</label>
</p>
<p class="description">
<?php esc_html_e( 'An additional stylesheet is required to properly render the admin bar. If the additional stylesheet causes the total CSS to surpass 50KB then the admin bar should be disabled to prevent a validation error or an unstyled admin bar in AMP responses.', 'amp' ); ?>
</p>
</fieldset>
<?php
}
/**
* Supported templates section renderer.
*
* @since 1.0
*/
public function render_supported_templates() {
$theme_support_args = AMP_Theme_Support::get_theme_support_args();
?>
<?php if ( ! isset( $theme_support_args['available_callback'] ) ) : ?>
<fieldset id="all_templates_supported_fieldset" <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php if ( isset( $theme_support_args['templates_supported'] ) && 'all' === $theme_support_args['templates_supported'] ) : ?>
<div class="notice notice-info notice-alt inline">
<p>
<?php esc_html_e( 'The current theme requires all templates to support AMP.', 'amp' ); ?>
</p>
</div>
<?php else : ?>
<p>
<label for="all_templates_supported">
<input id="all_templates_supported" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[all_templates_supported]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'all_templates_supported' ) ); ?>>
<?php esc_html_e( 'Serve all templates as AMP regardless of what is being queried.', 'amp' ); ?>
</label>
</p>
<p class="description">
<?php esc_html_e( 'This will allow all of the URLs on your site to be served as AMP by default.', 'amp' ); ?>
</p>
<?php endif; ?>
</fieldset>
<?php else : ?>
<div class="notice notice-warning notice-alt inline">
<p>
<?php
printf(
/* translators: %s: available_callback */
esc_html__( 'Your theme is using the deprecated %s argument for AMP theme support.', 'amp' ),
'available_callback'
);
?>
</p>
</div>
<?php endif; ?>
<fieldset id="supported_post_types_fieldset" <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php $element_name = AMP_Options_Manager::OPTION_NAME . '[supported_post_types][]'; ?>
<h4 class="title"><?php esc_html_e( 'Content Types', 'amp' ); ?></h4>
<p>
<?php esc_html_e( 'The following content types will be available as AMP:', 'amp' ); ?>
</p>
<ul>
<?php foreach ( array_map( 'get_post_type_object', AMP_Post_Type_Support::get_eligible_post_types() ) as $post_type ) : ?>
<li>
<?php $element_id = AMP_Options_Manager::OPTION_NAME . "-supported_post_types-{$post_type->name}"; ?>
<input
type="checkbox"
id="<?php echo esc_attr( $element_id ); ?>"
name="<?php echo esc_attr( $element_name ); ?>"
value="<?php echo esc_attr( $post_type->name ); ?>"
<?php checked( post_type_supports( $post_type->name, AMP_Post_Type_Support::SLUG ) ); ?>
>
<label for="<?php echo esc_attr( $element_id ); ?>">
<?php echo esc_html( $post_type->label ); ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</fieldset>
<?php if ( ! isset( $theme_support_args['available_callback'] ) ) : ?>
<fieldset id="supported_templates_fieldset" <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<style>
#supported_templates_fieldset ul ul {
margin-left: 40px;
}
</style>
<h4 class="title"><?php esc_html_e( 'Templates', 'amp' ); ?></h4>
<?php
self::list_template_conditional_options( AMP_Theme_Support::get_supportable_templates() );
?>
<script>
// Let clicks on parent items automatically cause the children checkboxes to have same checked state applied.
(function ( $ ) {
$( '#supported_templates_fieldset input[type=checkbox]' ).on( 'click', function() {
$( this ).siblings( 'ul' ).find( 'input[type=checkbox]' ).prop( 'checked', this.checked );
} );
})( jQuery );
</script>
</fieldset>
<script>
// Update the visibility of the fieldsets based on the selected template mode and then whether all templates are indicated to be supported.
(function ( $ ) {
var templateModeInputs, themeSupportDisabledInput, allTemplatesSupportedInput, supportForced;
templateModeInputs = $( 'input[type=radio][name="amp-options[theme_support]"]' );
themeSupportDisabledInput = $( '#theme_support_disabled' );
allTemplatesSupportedInput = $( '#all_templates_supported' );
supportForced = <?php echo wp_json_encode( current_theme_supports( AMP_Theme_Support::SLUG ) && ! AMP_Theme_Support::is_support_added_via_option() ); ?>;
function isThemeSupportDisabled() {
return ! supportForced && themeSupportDisabledInput.prop( 'checked' );
}
function updateFieldsetVisibility() {
var allTemplatesSupported = 0 === allTemplatesSupportedInput.length || allTemplatesSupportedInput.prop( 'checked' );
$( '#all_templates_supported_fieldset, #supported_post_types_fieldset > .title' ).toggleClass(
'hidden',
isThemeSupportDisabled()
);
$( '#supported_post_types_fieldset' ).toggleClass(
'hidden',
allTemplatesSupported && ! isThemeSupportDisabled()
);
$( '#supported_templates_fieldset' ).toggleClass(
'hidden',
allTemplatesSupported || isThemeSupportDisabled()
);
}
templateModeInputs.on( 'change', updateFieldsetVisibility );
allTemplatesSupportedInput.on( 'click', updateFieldsetVisibility );
updateFieldsetVisibility();
})( jQuery );
</script>
<?php endif; ?>
<?php
}
/**
* Render the caching settings section.
*
* @since 1.0
*
* @todo Change the messaging and description to be user-friendly and helpful.
*/
public function render_caching() {
?>
<fieldset <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php if ( AMP_Options_Manager::show_response_cache_disabled_notice() ) : ?>
<div class="notice notice-info notice-alt inline">
<p><?php esc_html_e( 'The post-processor cache was disabled due to detecting randomly generated content found on', 'amp' ); ?> <a href="<?php echo esc_url( get_option( AMP_Theme_Support::CACHE_MISS_URL_OPTION, '' ) ); ?>"><?php esc_html_e( 'on this web page.', 'amp' ); ?></a></p>
<p><?php esc_html_e( 'Randomly generated content was detected on this web page. To avoid filling up the cache with unusable content, the AMP plugin\'s post-processor cache was automatically disabled.', 'amp' ); ?>
<a href="<?php echo esc_url( 'https://github.com/ampproject/amp-wp/wiki/Post-Processor-Cache' ); ?>"><?php esc_html_e( 'Read more', 'amp' ); ?></a>.</p>
</div>
<?php endif; ?>
<p>
<label for="enable_response_caching">
<input id="enable_response_caching" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[enable_response_caching]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'enable_response_caching' ) ); ?>>
<?php esc_html_e( 'Enable post-processor caching.', 'amp' ); ?>
</label>
</p>
<p class="description"><?php esc_html_e( 'This will enable post-processor caching to speed up processing an AMP response after WordPress renders a template.', 'amp' ); ?></p>
</fieldset>
<?php
}
/**
* List template conditional options.
*
* @param array $options Options.
* @param string|null $parent ID of the parent option.
*/
private function list_template_conditional_options( $options, $parent = null ) {
$element_name = AMP_Options_Manager::OPTION_NAME . '[supported_templates][]';
?>
<ul>
<?php foreach ( $options as $id => $option ) : ?>
<?php
$element_id = AMP_Options_Manager::OPTION_NAME . '-supported-templates-' . $id;
if ( $parent ? empty( $option['parent'] ) || $parent !== $option['parent'] : ! empty( $option['parent'] ) ) {
continue;
}
// Skip showing an option if it doesn't have a label.
if ( empty( $option['label'] ) ) {
continue;
}
?>
<li>
<?php if ( empty( $option['immutable'] ) ) : ?>
<input
type="checkbox"
id="<?php echo esc_attr( $element_id ); ?>"
name="<?php echo esc_attr( $element_name ); ?>"
value="<?php echo esc_attr( $id ); ?>"
<?php checked( ! empty( $option['user_supported'] ) ); ?>
>
<?php else : // Persist user selection even when checkbox disabled, when selection forced by theme/filter. ?>
<input
type="checkbox"
id="<?php echo esc_attr( $element_id ); ?>"
<?php checked( ! empty( $option['supported'] ) ); ?>
<?php disabled( true ); ?>
>
<?php if ( ! empty( $option['user_supported'] ) ) : ?>
<input type="hidden" name="<?php echo esc_attr( $element_name ); ?>" value="<?php echo esc_attr( $id ); ?>">
<?php endif; ?>
<?php endif; ?>
<label for="<?php echo esc_attr( $element_id ); ?>">
<?php echo esc_html( $option['label'] ); ?>
</label>
<?php if ( ! empty( $option['description'] ) ) : ?>
<span class="description">
&mdash; <?php echo wp_kses_post( $option['description'] ); ?>
</span>
<?php endif; ?>
<?php self::list_template_conditional_options( $options, $id ); ?>
</li>
<?php endforeach; ?>
</ul>
<?php
}
/**
* Display Settings.
*
* @since 0.6
*/
public function render_screen() {
if ( ! empty( $_GET['settings-updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
AMP_Options_Manager::check_supported_post_type_update_errors();
}
?>
<?php if ( ! current_user_can( 'manage_options' ) ) : ?>
<div class="notice notice-info">
<p><?php esc_html_e( 'You do not have permission to modify these settings. They are shown here for your reference. Please contact your administrator to make changes.', 'amp' ); ?></p>
</div>
<?php endif; ?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors(); ?>
<form id="amp-settings" action="options.php" method="post">
<?php
settings_fields( AMP_Options_Manager::OPTION_NAME );
do_settings_sections( AMP_Options_Manager::OPTION_NAME );
if ( current_user_can( 'manage_options' ) ) {
submit_button();
}
?>
</form>
</div>
<?php
}
}

View File

@ -0,0 +1,203 @@
<?php
/**
* Class AMP_Analytics_Options_Submenu_Page
*
* @package AMP
*/
/**
* Class AMP_Analytics_Options_Submenu_Page
*/
class AMP_Analytics_Options_Submenu_Page {
/**
* Render entry.
*
* @param string $id Entry ID.
* @param string $type Entry type.
* @param string $config Entry config as serialized JSON.
*/
private function render_entry( $id = '', $type = '', $config = '{}' ) {
$is_existing_entry = ! empty( $id );
if ( $is_existing_entry ) {
$entry_slug = sprintf( '%s%s', ( $type ? $type . '-' : '' ), substr( $id, - 6 ) );
/* translators: %s: the entry slug. */
$analytics_title = sprintf( __( 'Analytics: %s', 'amp' ), $entry_slug );
} else {
$analytics_title = __( 'Add new entry:', 'amp' );
$id = '__new__';
}
// Tidy-up the JSON for display.
if ( $config ) {
$options = ( 128 /* JSON_PRETTY_PRINT */ | 64 /* JSON_UNESCAPED_SLASHES */ );
$config = wp_json_encode( json_decode( $config ), $options );
}
$id_base = sprintf( '%s[analytics][%s]', AMP_Options_Manager::OPTION_NAME, $id );
?>
<div class="analytics-data-container">
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<h2>
<?php echo esc_html( $analytics_title ); ?>
</h2>
<div class="options">
<p>
<label>
<?php esc_html_e( 'Type:', 'amp' ); ?>
<input class="option-input" type="text" required name="<?php echo esc_attr( $id_base . '[type]' ); ?>" placeholder="<?php esc_attr_e( 'e.g. googleanalytics', 'amp' ); ?>" value="<?php echo esc_attr( $type ); ?>" />
</label>
<label>
<?php esc_html_e( 'ID:', 'amp' ); ?>
<input type="text" value="<?php echo esc_attr( $is_existing_entry ? $id : '' ); ?>" readonly />
</label>
<input type="hidden" name="<?php echo esc_attr( $id_base . '[id]' ); ?>" value="<?php echo esc_attr( $id ); ?>" />
</p>
<p>
<label>
<?php esc_html_e( 'JSON Configuration:', 'amp' ); ?>
<br />
<textarea
rows="10"
cols="100"
name="<?php echo esc_attr( $id_base . '[config]' ); ?>"
class="amp-analytics-input"
placeholder="{...}"
required
><?php echo esc_textarea( $config ); ?></textarea>
</label>
</p>
<input type="hidden" name="action" value="amp_analytics_options">
</div>
<p>
<?php
wp_nonce_field( 'analytics-options', 'analytics-options' );
submit_button( esc_html__( 'Save', 'amp' ), 'primary', 'save', false );
if ( $is_existing_entry ) {
submit_button( esc_html__( 'Delete', 'amp' ), 'delete button-primary', esc_attr( $id_base . '[delete]' ), false );
}
?>
</p>
</form>
</div><!-- #analytics-data-container -->
<?php
}
/**
* Render title.
*
* @param bool $has_entries Whether there are entries.
*/
public function render_title( $has_entries = false ) {
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors(); ?>
<details <?php echo ! $has_entries ? 'open' : ''; ?>>
<summary>
<?php esc_html_e( 'Learn about analytics for AMP.', 'amp' ); ?>
</summary>
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: 1: AMP Analytics docs URL. 2: AMP for WordPress analytics docs URL. 3: AMP analytics code reference. 4: amp-analytics, 5: {. 6: }. 7: <script>, 8: googleanalytics. 9: AMP analytics vendor docs URL. 10: UA-XXXXX-Y. */
__( 'For Google Analytics, please see <a href="%1$s" target="_blank">Adding Analytics to your AMP pages</a>; see also the <a href="%2$s" target="_blank">Analytics wiki page</a> and the AMP project\'s <a href="%3$s" target="_blank">%4$s documentation</a>. The analytics configuration supplied below must take the form of JSON objects, which begin with a %5$s and end with a %6$s. Do not include any HTML tags like %4$s or %7$s. A common entry would have the type %8$s (see <a href="%9$s" target="_blank">available vendors</a>) and a configuration that looks like the following (where %10$s is replaced with your own site\'s account number):', 'amp' ),
__( 'https://developers.google.com/analytics/devguides/collection/amp-analytics/', 'amp' ),
__( 'https://amp-wp.org/documentation/playbooks/analytics/', 'amp' ),
__( 'https://www.ampproject.org/docs/reference/components/amp-analytics', 'amp' ),
'<code>amp-analytics</code>',
'<code>{</code>',
'<code>}</code>',
'<code>&lt;script&gt;</code>',
'<code>googleanalytics</code>',
__( 'https://www.ampproject.org/docs/analytics/analytics-vendors', 'amp' ),
'<code>UA-XXXXX-Y</code>'
)
);
?>
<pre>{
"vars": {
"account": "UA-XXXXX-Y"
},
"triggers": {
"trackPageview": {
"on": "visible",
"request": "pageview"
}
}
}</pre>
</p>
</details>
</div><!-- .wrap -->
<?php
}
/**
* Render styles.
*/
protected function render_styles() {
?>
<style>
.amp-analytics-input {
font-family: monospace;
}
.amp-analytics-input:invalid {
border-color: red;
}
</style>
<?php
}
/**
* Render scripts.
*/
protected function render_scripts() {
?>
<script>
Array.prototype.forEach.call( document.querySelectorAll( '.amp-analytics-input' ), function( textarea ) {
textarea.addEventListener( 'input', function() {
if ( ! this.value ) {
this.setCustomValidity( '' );
return;
}
try {
var value = JSON.parse( this.value );
if ( null === value || typeof value !== 'object' || Array.isArray( value ) ) {
this.setCustomValidity( <?php echo wp_json_encode( __( 'A JSON object is required, e.g. {...}', 'amp' ) ); ?> )
} else {
this.setCustomValidity( '' );
}
} catch ( e ) {
this.setCustomValidity( e.message )
}
} );
} );
</script>
<?php
}
/**
* Render.
*/
public function render() {
$this->render_styles();
$analytics_entries = AMP_Options_Manager::get_option( 'analytics', array() );
$this->render_title( ! empty( $analytics_entries ) );
// Render entries stored in the DB.
foreach ( $analytics_entries as $entry_id => $entry ) {
$this->render_entry( $entry_id, $entry['type'], $entry['config'] );
}
// Empty form for adding more entries.
$this->render_entry();
$this->render_scripts();
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,222 @@
<?php
/**
* Class AMP_Audio_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Audio_Sanitizer
*
* Converts <audio> tags to <amp-audio>
*/
class AMP_Audio_Sanitizer extends AMP_Base_Sanitizer {
use AMP_Noscript_Fallback;
/**
* Tag.
*
* @var string HTML audio tag to identify and replace with AMP version.
* @since 0.2
*/
public static $tag = 'audio';
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'add_noscript_fallback' => true,
);
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array(
'audio' => array( 'amp-audio' ),
);
}
/**
* Sanitize the <audio> elements from the HTML contained in this instance's DOMDocument.
*
* @since 0.2
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
if ( $this->args['add_noscript_fallback'] ) {
$this->initialize_noscript_allowed_attributes( self::$tag );
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
// Skip element if already inside of an AMP element as a noscript fallback.
if ( $this->is_inside_amp_noscript( $node ) ) {
continue;
}
$old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
// For amp-audio, the default width and height are inferred from browser.
$sources = array();
$new_attributes = $this->filter_attributes( $old_attributes );
if ( ! empty( $new_attributes['src'] ) ) {
$sources[] = $new_attributes['src'];
}
/**
* Original node.
*
* @var DOMElement $old_node
*/
$old_node = $node->cloneNode( false );
// Gather all child nodes and supply empty video dimensions from sources.
$fallback = null;
$child_nodes = array();
while ( $node->firstChild ) {
$child_node = $node->removeChild( $node->firstChild );
if ( $child_node instanceof DOMElement && 'source' === $child_node->nodeName && $child_node->hasAttribute( 'src' ) ) {
$src = $this->maybe_enforce_https_src( $child_node->getAttribute( 'src' ), true );
if ( ! $src ) {
// @todo $this->remove_invalid_child( $child_node ), but this will require refactoring the while loop since it uses firstChild.
continue; // Skip adding source.
}
$sources[] = $src;
$child_node->setAttribute( 'src', $src );
$new_attributes = $this->filter_attributes( $new_attributes );
}
if ( ! $fallback && $child_node instanceof DOMElement && ! ( 'source' === $child_node->nodeName || 'track' === $child_node->nodeName ) ) {
$fallback = $child_node;
$fallback->setAttribute( 'fallback', '' );
}
$child_nodes[] = $child_node;
}
/*
* Add fallback for audio shortcode which is not present by default since wp_mediaelement_fallback()
* is not called when wp_audio_shortcode_library is filtered from mediaelement to amp.
*/
if ( ! $fallback && ! empty( $sources ) ) {
$fallback = $this->dom->createElement( 'a' );
$fallback->setAttribute( 'href', $sources[0] );
$fallback->setAttribute( 'fallback', '' );
$fallback->appendChild( $this->dom->createTextNode( $sources[0] ) );
$child_nodes[] = $fallback;
}
/*
* Audio in WordPress is responsive with 100% width, so this infers fixed-layout.
* In AMP, the amp-audio's default height is inferred from the browser.
*/
$new_attributes['width'] = 'auto';
// @todo Make sure poster and artwork attributes are HTTPS.
$new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-audio', $new_attributes );
foreach ( $child_nodes as $child_node ) {
$new_node->appendChild( $child_node );
if ( ! ( $child_node instanceof DOMElement ) || ! $child_node->hasAttribute( 'fallback' ) ) {
$old_node->appendChild( $child_node->cloneNode( true ) );
}
}
// Make sure the updated src and poster are applied to the original.
foreach ( array( 'src', 'poster', 'artwork' ) as $attr_name ) {
if ( $new_node->hasAttribute( $attr_name ) ) {
$old_node->setAttribute( $attr_name, $new_node->getAttribute( $attr_name ) );
}
}
/*
* If the node has at least one valid source, replace the old node with it.
* Otherwise, just remove the node.
*
* @todo Add a fallback handler.
* See: https://github.com/ampproject/amphtml/issues/2261
*/
if ( empty( $sources ) ) {
$this->remove_invalid_child( $node );
} else {
$node->parentNode->replaceChild( $new_node, $node );
if ( $this->args['add_noscript_fallback'] ) {
// Preserve original node in noscript for no-JS environments.
$this->append_old_node_noscript( $new_node, $old_node, $this->dom );
}
}
$this->did_convert_elements = true;
}
}
/**
* "Filter" HTML attributes for <amp-audio> elements.
*
* @since 0.2
*
* @param string[] $attributes {
* Attributes.
*
* @type string $src Audio URL - Empty if HTTPS required per $this->args['require_https_src']
* @type int $width <audio> attribute - Set to numeric value if px or %
* @type int $height <audio> attribute - Set to numeric value if px or %
* @type string $class <audio> attribute - Pass along if found
* @type bool $loop <audio> attribute - Convert 'false' to empty string ''
* @type bool $muted <audio> attribute - Convert 'false' to empty string ''
* @type bool $autoplay <audio> attribute - Convert 'false' to empty string ''
* }
* @return array Returns HTML attributes; removes any not specifically declared above from input.
*/
private function filter_attributes( $attributes ) {
$out = array();
foreach ( $attributes as $name => $value ) {
switch ( $name ) {
case 'src':
$out[ $name ] = $this->maybe_enforce_https_src( $value );
break;
case 'width':
case 'height':
$out[ $name ] = $this->sanitize_dimension( $value, $name );
break;
case 'class':
$out[ $name ] = $value;
break;
case 'loop':
case 'muted':
case 'autoplay':
if ( 'false' !== $value ) {
$out[ $name ] = '';
}
break;
case 'data-amp-layout':
$out['layout'] = $value;
break;
case 'data-amp-noloading':
$out['noloading'] = $value;
break;
default:
$out[ $name ] = $value;
}
}
return $out;
}
}

View File

@ -0,0 +1,587 @@
<?php
/**
* Class AMP_Base_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Base_Sanitizer
*/
abstract class AMP_Base_Sanitizer {
/**
* Value used with the height attribute in an $attributes parameter is empty.
*
* @since 0.3.3
*
* @const int
*/
const FALLBACK_HEIGHT = 400;
/**
* Value for <amp-image-lightbox> ID.
*
* @since 1.0
*
* @const string
*/
const AMP_IMAGE_LIGHTBOX_ID = 'amp-image-lightbox';
/**
* Placeholder for default args, to be set in child classes.
*
* @since 0.2
*
* @var array
*/
protected $DEFAULT_ARGS = array();
/**
* DOM.
*
* @var DOMDocument A standard PHP representation of an HTML document in object form.
*
* @since 0.2
*/
protected $dom;
/**
* Array of flags used to control sanitization.
*
* @var array {
* @type int $content_max_width
* @type bool $add_placeholder
* @type bool $use_document_element
* @type bool $require_https_src
* @type string[] $amp_allowed_tags
* @type string[] $amp_globally_allowed_attributes
* @type string[] $amp_layout_allowed_attributes
* @type array $amp_allowed_tags
* @type array $amp_globally_allowed_attributes
* @type array $amp_layout_allowed_attributes
* @type array $amp_bind_placeholder_prefix
* @type bool $allow_dirty_styles
* @type bool $allow_dirty_scripts
* @type bool $should_locate_sources
* @type callable $validation_error_callback
* }
*/
protected $args;
/**
* Flag to be set in child class' sanitize() method indicating if the
* HTML contained in the DOMDocument has been sanitized yet or not.
*
* @since 0.2
*
* @var bool
*/
protected $did_convert_elements = false;
/**
* The root element used for sanitization. Either html or body.
*
* @var DOMElement
*/
protected $root_element;
/**
* Keep track of nodes that should not be removed to prevent duplicated validation errors since sanitization is rejected.
*
* @var array
*/
private $should_not_removed_nodes = array();
/**
* AMP_Base_Sanitizer constructor.
*
* @since 0.2
*
* @param DOMDocument $dom Represents the HTML document to sanitize.
* @param array $args {
* Args.
*
* @type int $content_max_width
* @type bool $add_placeholder
* @type bool $require_https_src
* @type string[] $amp_allowed_tags
* @type string[] $amp_globally_allowed_attributes
* @type string[] $amp_layout_allowed_attributes
* }
*/
public function __construct( $dom, $args = array() ) {
$this->dom = $dom;
$this->args = array_merge( $this->DEFAULT_ARGS, $args );
if ( ! empty( $this->args['use_document_element'] ) ) {
$this->root_element = $this->dom->documentElement;
} else {
$this->root_element = $this->dom->getElementsByTagName( 'body' )->item( 0 );
}
}
/**
* Add filters to manipulate output during output buffering before the DOM is constructed.
*
* Add actions and filters before the page is rendered so that the sanitizer can fix issues during output buffering.
* This provides an alternative to manipulating the DOM in the sanitize method. This is a static function because
* it is invoked before the class is instantiated, as the DOM is not available yet. This method is only called
* when 'amp' theme support is present. It is conceptually similar to the AMP_Base_Embed_Handler class's register_embed
* method.
*
* @since 1.0
* @see \AMP_Base_Embed_Handler::register_embed()
*
* @param array $args Args.
*/
public static function add_buffering_hooks( $args = array() ) {}
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array();
}
/**
* Run logic before any sanitizers are run.
*
* After the sanitizers are instantiated but before calling sanitize on each of them, this
* method is called with list of all the instantiated sanitizers.
*
* @param AMP_Base_Sanitizer[] $sanitizers Sanitizers.
*/
public function init( $sanitizers ) {}
/**
* Sanitize the HTML contained in the DOMDocument received by the constructor
*/
abstract public function sanitize();
/**
* Return array of values that would be valid as an HTML `script` element.
*
* Array keys are AMP element names and array values are their respective
* Javascript URLs from https://cdn.ampproject.org
*
* @since 0.2
*
* @return string[] Returns component name as array key and JavaScript URL as array value,
* respectively. Will return an empty array if sanitization has yet to be run
* or if it did not find any HTML elements to convert to AMP equivalents.
*/
public function get_scripts() {
return array();
}
/**
* Return array of values that would be valid as an HTML `style` attribute.
*
* @since 0.4
* @deprecated As of 1.0, use get_stylesheets().
*
* @return array[][] Mapping of CSS selectors to arrays of properties.
*/
public function get_styles() {
return array();
}
/**
* Get stylesheets.
*
* @since 0.7
* @returns array Values are the CSS stylesheets. Keys are MD5 hashes of the stylesheets.
*/
public function get_stylesheets() {
$stylesheets = array();
foreach ( $this->get_styles() as $selector => $properties ) {
$stylesheet = sprintf( '%s { %s }', $selector, join( '; ', $properties ) . ';' );
$stylesheets[ md5( $stylesheet ) ] = $stylesheet;
}
return $stylesheets;
}
/**
* Get HTML body as DOMElement from DOMDocument received by the constructor.
*
* @deprecated Just reference $root_element instead.
* @return DOMElement The body element.
*/
protected function get_body_node() {
return $this->dom->getElementsByTagName( 'body' )->item( 0 );
}
/**
* Sanitizes a CSS dimension specifier while being sensitive to dimension context.
*
* @param string $value A valid CSS dimension specifier; e.g. 50, 50px, 50%.
* @param string $dimension 'width' or ignored. 'width' only affects $values ending in '%'.
*
* @return float|int|string Returns a numeric dimension value, or an empty string.
*/
public function sanitize_dimension( $value, $dimension ) {
// Allows 0 to be used as valid dimension.
if ( null === $value ) {
return '';
}
// Accepts both integers and floats & prevents negative values.
if ( is_numeric( $value ) ) {
return max( 0, floatval( $value ) );
}
if ( AMP_String_Utils::endswith( $value, 'px' ) ) {
return absint( $value );
}
if ( AMP_String_Utils::endswith( $value, '%' ) ) {
if ( 'width' === $dimension && isset( $this->args['content_max_width'] ) ) {
$percentage = absint( $value ) / 100;
return round( $percentage * $this->args['content_max_width'] );
}
}
return '';
}
/**
* Sets the layout, and possibly the 'height' and 'width' attributes.
*
* @param string[] $attributes {
* Attributes.
*
* @type int $height
* @type int $width
* @type string $sizes
* @type string $class
* @type string $layout
* }
* @return array Attributes.
*/
public function set_layout( $attributes ) {
if ( isset( $attributes['layout'] ) && ( 'fill' === $attributes['layout'] || 'flex-item' !== $attributes['layout'] ) ) {
return $attributes;
}
if ( empty( $attributes['height'] ) ) {
unset( $attributes['width'] );
$attributes['height'] = self::FALLBACK_HEIGHT;
}
if ( empty( $attributes['width'] ) ) {
$attributes['layout'] = 'fixed-height';
}
return $attributes;
}
/**
* Adds or appends key and value to list of attributes
*
* Adds key and value to list of attributes, or if the key already exists in the array
* it concatenates to existing attribute separator by a space or other supplied separator.
*
* @param string[] $attributes {
* Attributes.
*
* @type int $height
* @type int $width
* @type string $sizes
* @type string $class
* @type string $layout
* }
* @param string $key Valid associative array index to add.
* @param string $value Value to add or append to array indexed at the key.
* @param string $separator Optional; defaults to space but some other separator if needed.
*/
public function add_or_append_attribute( &$attributes, $key, $value, $separator = ' ' ) {
if ( isset( $attributes[ $key ] ) ) {
$attributes[ $key ] = trim( $attributes[ $key ] . $separator . $value );
} else {
$attributes[ $key ] = $value;
}
}
/**
* Decide if we should remove a src attribute if https is required.
*
* If not required, the implementing class may want to try and force https instead.
*
* @param string $src URL to convert to HTTPS if forced, or made empty if $args['require_https_src'].
* @param boolean $force_https Force setting of HTTPS if true.
* @return string URL which may have been updated with HTTPS, or may have been made empty.
*/
public function maybe_enforce_https_src( $src, $force_https = false ) {
$protocol = strtok( $src, ':' ); // @todo What about relative URLs? This should use wp_parse_url( $src, PHP_URL_SCHEME )
if ( 'https' !== $protocol ) {
// Check if https is required.
if ( isset( $this->args['require_https_src'] ) && true === $this->args['require_https_src'] ) {
// Remove the src. Let the implementing class decide what do from here.
$src = '';
} elseif ( ( ! isset( $this->args['require_https_src'] ) || false === $this->args['require_https_src'] )
&& true === $force_https ) {
// Don't remove the src, but force https instead.
$src = set_url_scheme( $src, 'https' );
}
}
return $src;
}
/**
* Removes an invalid child of a node.
*
* Also, calls the mutation callback for it.
* This tracks all the nodes that were removed.
*
* @since 0.7
*
* @param DOMNode|DOMElement $node The node to remove.
* @param array $validation_error Validation error details.
* @return bool Whether the node should have been removed, that is, that the node was sanitized for validity.
*/
public function remove_invalid_child( $node, $validation_error = array() ) {
// Prevent double-reporting nodes that are rejected for sanitization.
if ( isset( $this->should_not_removed_nodes[ $node->nodeName ] ) && in_array( $node, $this->should_not_removed_nodes[ $node->nodeName ], true ) ) {
return false;
}
$should_remove = $this->should_sanitize_validation_error( $validation_error, compact( 'node' ) );
if ( $should_remove ) {
$node->parentNode->removeChild( $node );
} else {
$this->should_not_removed_nodes[ $node->nodeName ][] = $node;
}
return $should_remove;
}
/**
* Removes an invalid attribute of a node.
*
* Also, calls the mutation callback for it.
* This tracks all the attributes that were removed.
*
* @since 0.7
*
* @param DOMElement $element The node for which to remove the attribute.
* @param DOMAttr|string $attribute The attribute to remove from the element.
* @param array $validation_error Validation error details.
* @return bool Whether the node should have been removed, that is, that the node was sanitized for validity.
*/
public function remove_invalid_attribute( $element, $attribute, $validation_error = array() ) {
if ( is_string( $attribute ) ) {
$node = $element->getAttributeNode( $attribute );
} else {
$node = $attribute;
}
$should_remove = $this->should_sanitize_validation_error( $validation_error, compact( 'node' ) );
if ( $should_remove ) {
$element->removeAttributeNode( $node );
}
return $should_remove;
}
/**
* Check whether or not sanitization should occur in response to validation error.
*
* @since 1.0
*
* @param array $validation_error Validation error.
* @param array $data Data including the node.
* @return bool Whether to sanitize.
*/
public function should_sanitize_validation_error( $validation_error, $data = array() ) {
if ( empty( $this->args['validation_error_callback'] ) || ! is_callable( $this->args['validation_error_callback'] ) ) {
return true;
}
$validation_error = $this->prepare_validation_error( $validation_error, $data );
return false !== call_user_func( $this->args['validation_error_callback'], $validation_error, $data );
}
/**
* Prepare validation error.
*
* @param array $error {
* Error.
*
* @type string $code Error code.
* }
* @param array $data {
* Data.
*
* @type DOMElement|DOMNode $node The removed node.
* }
* @return array Error.
*/
public function prepare_validation_error( array $error = array(), array $data = array() ) {
$node = null;
$matches = null;
if ( isset( $data['node'] ) && $data['node'] instanceof DOMNode ) {
$node = $data['node'];
$error['node_name'] = $node->nodeName;
if ( $node->parentNode ) {
$error['parent_name'] = $node->parentNode->nodeName;
}
}
if ( $node instanceof DOMElement ) {
if ( ! isset( $error['code'] ) ) {
$error['code'] = AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE;
}
if ( ! isset( $error['type'] ) ) {
$error['type'] = 'script' === $node->nodeName ? AMP_Validation_Error_Taxonomy::JS_ERROR_TYPE : AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE;
}
if ( ! isset( $error['node_attributes'] ) ) {
$error['node_attributes'] = array();
foreach ( $node->attributes as $attribute ) {
$error['node_attributes'][ $attribute->nodeName ] = $attribute->nodeValue;
}
}
// Capture script contents.
if ( 'script' === $node->nodeName && ! $node->hasAttribute( 'src' ) ) {
$error['text'] = $node->textContent;
}
// Suppress 'ver' param from enqueued scripts and styles.
if ( 'script' === $node->nodeName && isset( $error['node_attributes']['src'] ) && false !== strpos( $error['node_attributes']['src'], 'ver=' ) ) {
$error['node_attributes']['src'] = add_query_arg( 'ver', '__normalized__', $error['node_attributes']['src'] );
} elseif ( 'link' === $node->nodeName && isset( $error['node_attributes']['href'] ) && false !== strpos( $error['node_attributes']['href'], 'ver=' ) ) {
$error['node_attributes']['href'] = add_query_arg( 'ver', '__normalized__', $error['node_attributes']['href'] );
}
} elseif ( $node instanceof DOMAttr ) {
if ( ! isset( $error['code'] ) ) {
$error['code'] = AMP_Validation_Error_Taxonomy::INVALID_ATTRIBUTE_CODE;
}
if ( ! isset( $error['type'] ) ) {
// If this is an attribute that begins with on, like onclick, it should be a js_error.
$error['type'] = preg_match( '/^on\w+/', $node->nodeName ) ? AMP_Validation_Error_Taxonomy::JS_ERROR_TYPE : AMP_Validation_Error_Taxonomy::HTML_ATTRIBUTE_ERROR_TYPE;
}
if ( ! isset( $error['element_attributes'] ) ) {
$error['element_attributes'] = array();
if ( $node->parentNode && $node->parentNode->hasAttributes() ) {
foreach ( $node->parentNode->attributes as $attribute ) {
$error['element_attributes'][ $attribute->nodeName ] = $attribute->nodeValue;
}
}
}
}
return $error;
}
/**
* Get data-amp-* values from the parent node 'figure' added by editor block.
*
* @param DOMElement $node Base node.
* @return array AMP data array.
*/
public function get_data_amp_attributes( $node ) {
$attributes = array();
// Editor blocks add 'figure' as the parent node for images. If this node has data-amp-layout then we should add this as the layout attribute.
$parent_node = $node->parentNode;
if ( 'figure' === $parent_node->tagName ) {
$parent_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $parent_node );
if ( isset( $parent_attributes['data-amp-layout'] ) ) {
$attributes['layout'] = $parent_attributes['data-amp-layout'];
}
if ( isset( $parent_attributes['data-amp-noloading'] ) && true === filter_var( $parent_attributes['data-amp-noloading'], FILTER_VALIDATE_BOOLEAN ) ) {
$attributes['noloading'] = $parent_attributes['data-amp-noloading'];
}
}
return $attributes;
}
/**
* Set AMP attributes.
*
* @param array $attributes Array of attributes.
* @param array $amp_data Array of AMP attributes.
* @return array Updated attributes.
*/
public function filter_data_amp_attributes( $attributes, $amp_data ) {
if ( isset( $amp_data['layout'] ) ) {
$attributes['data-amp-layout'] = $amp_data['layout'];
}
if ( isset( $amp_data['noloading'] ) ) {
$attributes['data-amp-noloading'] = '';
}
return $attributes;
}
/**
* Set attributes to node's parent element according to layout.
*
* @param DOMElement $node Node.
* @param array $new_attributes Attributes array.
* @param string $layout Layout.
* @return array New attributes.
*/
public function filter_attachment_layout_attributes( $node, $new_attributes, $layout ) {
// The width has to be unset / auto in case of fixed-height.
if ( 'fixed-height' === $layout ) {
if ( ! isset( $new_attributes['height'] ) ) {
$new_attributes['height'] = self::FALLBACK_HEIGHT;
}
$new_attributes['width'] = 'auto';
$node->parentNode->setAttribute( 'style', 'height: ' . $new_attributes['height'] . 'px; width: auto;' );
// The parent element should have width/height set and position set in case of 'fill'.
} elseif ( 'fill' === $layout ) {
if ( ! isset( $new_attributes['height'] ) ) {
$new_attributes['height'] = self::FALLBACK_HEIGHT;
}
$node->parentNode->setAttribute( 'style', 'position:relative; width: 100%; height: ' . $new_attributes['height'] . 'px;' );
unset( $new_attributes['width'] );
unset( $new_attributes['height'] );
} elseif ( 'responsive' === $layout ) {
$node->parentNode->setAttribute( 'style', 'position:relative; width: 100%; height: auto' );
} elseif ( 'fixed' === $layout ) {
if ( ! isset( $new_attributes['height'] ) ) {
$new_attributes['height'] = self::FALLBACK_HEIGHT;
}
}
return $new_attributes;
}
/**
* Add <amp-image-lightbox> element to body tag if it doesn't exist yet.
*/
public function maybe_add_amp_image_lightbox_node() {
$nodes = $this->dom->getElementById( self::AMP_IMAGE_LIGHTBOX_ID );
if ( null !== $nodes ) {
return;
}
$nodes = $this->dom->getElementsByTagName( 'body' );
if ( ! $nodes->length ) {
return;
}
$body_node = $nodes->item( 0 );
$amp_image_lightbox = AMP_DOM_Utils::create_node(
$this->dom,
'amp-image-lightbox',
array(
'id' => self::AMP_IMAGE_LIGHTBOX_ID,
'layout' => 'nodisplay',
'data-close-button-aria-label' => __( 'Close', 'amp' ),
)
);
$body_node->appendChild( $amp_image_lightbox );
}
}

View File

@ -0,0 +1,310 @@
<?php
/**
* Class AMP_Blacklist_Sanitizer
*
* @package AMP
*/
/**
* Strips blacklisted tags and attributes from content.
*
* See following for blacklist:
* https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#html-tags
*
* @since 0.5 This has been replaced by AMP_Tag_And_Attribute_Sanitizer but is kept around for back-compat.
* @deprecated
*/
class AMP_Blacklist_Sanitizer extends AMP_Base_Sanitizer {
const PATTERN_REL_WP_ATTACHMENT = '#wp-att-([\d]+)#';
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'add_blacklisted_protocols' => array(),
'add_blacklisted_tags' => array(),
'add_blacklisted_attributes' => array(),
);
/**
* Sanitize.
*/
public function sanitize() {
_deprecated_function( __METHOD__, '0.7', 'AMP_Tag_And_Attribute_Sanitizer::sanitize' );
$blacklisted_tags = $this->get_blacklisted_tags();
$blacklisted_attributes = $this->get_blacklisted_attributes();
$blacklisted_protocols = $this->get_blacklisted_protocols();
$body = $this->root_element;
$this->strip_tags( $body, $blacklisted_tags );
$this->strip_attributes_recursive( $body, $blacklisted_attributes, $blacklisted_protocols );
}
/**
* Strip attributes recursively.
*
* @param DOMNode $node DOM Node.
* @param array $bad_attributes Bad attributes.
* @param array $bad_protocols Bad protocols.
*/
private function strip_attributes_recursive( $node, $bad_attributes, $bad_protocols ) {
if ( XML_ELEMENT_NODE !== $node->nodeType ) {
return;
}
$node_name = $node->nodeName;
// Some nodes may contain valid content but are themselves invalid.
// Remove the node but preserve the children.
if ( 'font' === $node_name ) {
$this->replace_node_with_children( $node, $bad_attributes, $bad_protocols );
return;
} elseif ( 'a' === $node_name && false === $this->validate_a_node( $node ) ) {
$this->replace_node_with_children( $node, $bad_attributes, $bad_protocols );
return;
}
if ( $node->hasAttributes() ) {
$length = $node->attributes->length;
for ( $i = $length - 1; $i >= 0; $i-- ) {
$attribute = $node->attributes->item( $i );
$attribute_name = strtolower( $attribute->name );
if ( in_array( $attribute_name, $bad_attributes, true ) ) {
$this->remove_invalid_attribute( $node, $attribute_name );
continue;
}
// The on* attributes (like onclick) are a special case.
if ( 0 === stripos( $attribute_name, 'on' ) && 'on' !== $attribute_name ) {
$this->remove_invalid_attribute( $node, $attribute_name );
continue;
} elseif ( 'a' === $node_name ) {
$this->sanitize_a_attribute( $node, $attribute );
}
}
}
$length = $node->childNodes->length;
for ( $i = $length - 1; $i >= 0; $i-- ) {
$child_node = $node->childNodes->item( $i );
$this->strip_attributes_recursive( $child_node, $bad_attributes, $bad_protocols );
}
}
/**
* Strip tags.
*
* @param DOMElement $node Node.
* @param string[] $tag_names Tag names.
*/
private function strip_tags( $node, $tag_names ) {
foreach ( $tag_names as $tag_name ) {
$elements = $node->getElementsByTagName( $tag_name );
$length = $elements->length;
if ( 0 === $length ) {
continue;
}
for ( $i = $length - 1; $i >= 0; $i-- ) {
$element = $elements->item( $i );
$parent_node = $element->parentNode;
$this->remove_invalid_child( $element );
if ( 'body' !== $parent_node->nodeName && AMP_DOM_Utils::is_node_empty( $parent_node ) ) {
$this->remove_invalid_child( $parent_node );
}
}
}
}
/**
* Sanitize attribute.
*
* @param DOMElement $node Node.
* @param DOMAttr $attribute Attribute.
*/
private function sanitize_a_attribute( $node, $attribute ) {
$attribute_name = strtolower( $attribute->name );
if ( 'rel' === $attribute_name ) {
$old_value = $attribute->value;
$new_value = trim( preg_replace( self::PATTERN_REL_WP_ATTACHMENT, '', $old_value ) );
if ( empty( $new_value ) ) {
$this->remove_invalid_attribute( $node, $attribute_name );
} elseif ( $old_value !== $new_value ) {
$node->setAttribute( $attribute_name, $new_value );
}
} elseif ( 'rev' === $attribute_name ) {
// rev removed from HTML5 spec, which was used by Jetpack Markdown.
$this->remove_invalid_attribute( $node, $attribute_name );
} elseif ( 'target' === $attribute_name ) {
// _blank is the only allowed value and it must be lowercase.
// replace _new with _blank and others should simply be removed.
$old_value = strtolower( $attribute->value );
if ( '_blank' === $old_value || '_new' === $old_value ) {
// _new is not allowed; swap with _blank
$node->setAttribute( $attribute_name, '_blank' );
} else {
// Only _blank is allowed.
$this->remove_invalid_attribute( $node, $attribute_name );
}
}
}
/**
* Validate node.
*
* @param DOMElement $node Node.
* @return bool
*/
private function validate_a_node( $node ) {
// Get the href attribute.
$href = $node->getAttribute( 'href' );
if ( empty( $href ) ) {
/*
* If no href, check that a is an anchor or not.
* We don't need to validate anchors any further.
*/
return $node->hasAttribute( 'name' ) || $node->hasAttribute( 'id' );
}
// If this is an anchor link, just return true.
if ( 0 === strpos( $href, '#' ) ) {
return true;
}
// If the href starts with a '/', append the home_url to it for validation purposes.
if ( 0 === stripos( $href, '/' ) ) {
$href = untrailingslashit( get_home_url() ) . $href;
}
$valid_protocols = array( 'http', 'https', 'mailto', 'sms', 'tel', 'viber', 'whatsapp' );
$special_protocols = array( 'tel', 'sms' ); // These ones don't valid with `filter_var+FILTER_VALIDATE_URL`.
$protocol = strtok( $href, ':' );
if ( false === filter_var( $href, FILTER_VALIDATE_URL )
&& ! in_array( $protocol, $special_protocols, true ) ) {
return false;
}
if ( ! in_array( $protocol, $valid_protocols, true ) ) {
return false;
}
return true;
}
/**
* Replace node with children.
*
* @param DOMElement $node Node.
* @param array $bad_attributes Bad attributes.
* @param array $bad_protocols Bad protocols.
*/
private function replace_node_with_children( $node, $bad_attributes, $bad_protocols ) {
// If the node has children and also has a parent node,
// clone and re-add all the children just before current node.
if ( $node->hasChildNodes() && $node->parentNode ) {
foreach ( $node->childNodes as $child_node ) {
$new_child = $child_node->cloneNode( true );
$this->strip_attributes_recursive( $new_child, $bad_attributes, $bad_protocols );
$node->parentNode->insertBefore( $new_child, $node );
}
}
// Remove the node from the parent, if defined.
if ( $node->parentNode ) {
$this->remove_invalid_child( $node );
}
}
/**
* Merge defaults with args.
*
* @param string $key Key.
* @param array $values Values.
* @return array Merged args.
*/
private function merge_defaults_with_args( $key, $values ) {
// Merge default values with user specified args.
if ( ! empty( $this->args[ $key ] )
&& is_array( $this->args[ $key ] ) ) {
$values = array_merge( $values, $this->args[ $key ] );
}
return $values;
}
/**
* Get blacklisted protocols.
*
* @return array Protocols.
*/
private function get_blacklisted_protocols() {
return $this->merge_defaults_with_args(
'add_blacklisted_protocols',
array(
'javascript',
)
);
}
/**
* Get blacklisted tags.
*
* @return array Tags.
*/
private function get_blacklisted_tags() {
return $this->merge_defaults_with_args(
'add_blacklisted_tags',
array(
'script',
'noscript',
'style',
'frame',
'frameset',
'object',
'param',
'applet',
'form',
'label',
'input',
'textarea',
'select',
'option',
'link',
'picture',
// Sanitizers run after embed handlers, so if anything wasn't matched, it needs to be removed.
'embed',
'embedvideo',
// Other weird ones.
'comments-count',
)
);
}
/**
* Get blacklisted attributes.
*
* @return array Attributes.
*/
private function get_blacklisted_attributes() {
return $this->merge_defaults_with_args(
'add_blacklisted_attributes',
array(
'style',
'size',
'clear',
'align',
'valign',
)
);
}
}

View File

@ -0,0 +1,137 @@
<?php
/**
* Class AMP_Block_Sanitizer.
*
* @package AMP
*/
/**
* Class AMP_Block_Sanitizer
*
* Modifies elements created as blocks to match the blocks' AMP-specific configuration.
*/
class AMP_Block_Sanitizer extends AMP_Base_Sanitizer {
/**
* Tag.
*
* @var string Figure tag to identify wrapper around AMP elements.
* @since 1.0
*/
public static $tag = 'figure';
/**
* Sanitize the AMP elements contained by <figure> element where necessary.
*
* @since 0.2
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
// We are only looking for <figure> elements which have wp-block-embed as class.
$class = (string) $node->getAttribute( 'class' );
if ( false === strpos( $class, 'wp-block-embed' ) ) {
continue;
}
// Remove classes like wp-embed-aspect-16-9 since responsive layout is handled by AMP's layout system.
$node->setAttribute( 'class', preg_replace( '/(?<=^|\s)wp-embed-aspect-\d+-\d+(?=\s|$)/', '', $class ) );
// We're looking for <figure> elements that have one child node only.
if ( 1 !== count( $node->childNodes ) ) {
continue;
}
$attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
// We are looking for <figure> elements with layout attribute only.
if (
! isset( $attributes['data-amp-layout'] ) &&
! isset( $attributes['data-amp-noloading'] ) &&
! isset( $attributes['data-amp-lightbox'] )
) {
continue;
}
$amp_el_found = false;
foreach ( $node->childNodes as $child_node ) {
// We are looking for child elements which start with 'amp-'.
if ( 0 !== strpos( $child_node->tagName, 'amp-' ) ) {
continue;
}
$amp_el_found = true;
$this->set_attributes( $child_node, $node, $attributes );
}
if ( false === $amp_el_found ) {
continue;
}
$this->did_convert_elements = true;
}
}
/**
* Sets necessary attributes to both parent and AMP element node.
*
* @param DOMNode $node AMP element node.
* @param DOMNode $parent_node <figure> node.
* @param array $attributes Current attributes of the AMP element.
*/
protected function set_attributes( $node, $parent_node, $attributes ) {
if ( isset( $attributes['data-amp-layout'] ) ) {
$node->setAttribute( 'layout', $attributes['data-amp-layout'] );
}
if ( isset( $attributes['data-amp-noloading'] ) && true === filter_var( $attributes['data-amp-noloading'], FILTER_VALIDATE_BOOLEAN ) ) {
$node->setAttribute( 'noloading', '' );
}
$layout = $node->getAttribute( 'layout' );
// The width has to be unset / auto in case of fixed-height.
if ( 'fixed-height' === $layout ) {
if ( ! isset( $attributes['height'] ) ) {
$node->setAttribute( 'height', self::FALLBACK_HEIGHT );
}
$node->setAttribute( 'width', 'auto' );
$height = $node->getAttribute( 'height' );
if ( is_numeric( $height ) ) {
$height .= 'px';
}
$parent_node->setAttribute( 'style', "height: $height; width: auto;" );
// The parent element should have width/height set and position set in case of 'fill'.
} elseif ( 'fill' === $layout ) {
if ( ! isset( $attributes['height'] ) ) {
$attributes['height'] = self::FALLBACK_HEIGHT;
}
$parent_node->setAttribute( 'style', 'position:relative; width: 100%; height: ' . $attributes['height'] . 'px;' );
$node->removeAttribute( 'width' );
$node->removeAttribute( 'height' );
} elseif ( 'responsive' === $layout ) {
$parent_node->setAttribute( 'style', 'position:relative; width: 100%; height: auto' );
} elseif ( 'fixed' === $layout ) {
if ( ! isset( $attributes['height'] ) ) {
$node->setAttribute( 'height', self::FALLBACK_HEIGHT );
}
}
// Set the fallback layout in case needed.
$attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
$attributes = $this->set_layout( $attributes );
if ( $layout !== $attributes['layout'] ) {
$node->setAttribute( 'layout', $attributes['layout'] );
}
}
}

View File

@ -0,0 +1,218 @@
<?php
/**
* Class AMP_Comments_Sanitizer.
*
* @package AMP
*/
/**
* Class AMP_Comments_Sanitizer
*
* Strips and corrects attributes in forms.
*/
class AMP_Comments_Sanitizer extends AMP_Base_Sanitizer {
/**
* Default args.
*
* @since 1.1
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'comment_live_list' => false,
);
/**
* Pre-process the comment form and comment list for AMP.
*
* @since 0.7
*/
public function sanitize() {
foreach ( $this->dom->getElementsByTagName( 'form' ) as $comment_form ) {
/**
* Comment form.
*
* @var DOMElement $comment_form
*/
$action = $comment_form->getAttribute( 'action-xhr' );
if ( ! $action ) {
$action = $comment_form->getAttribute( 'action' );
}
$action_path = wp_parse_url( $action, PHP_URL_PATH );
if ( preg_match( '#/wp-comments-post\.php$#', $action_path ) ) {
$this->process_comment_form( $comment_form );
}
}
if ( ! empty( $this->args['comments_live_list'] ) ) {
$xpath = new DOMXPath( $this->dom );
$comments = $xpath->query( '//amp-live-list/*[ @items ]/*[ starts-with( @id, "comment-" ) ]' );
foreach ( $comments as $comment ) {
$this->add_amp_live_list_comment_attributes( $comment );
}
}
}
/**
* Comment form.
*
* @since 0.7
*
* @param DOMElement $comment_form Comment form.
*/
protected function process_comment_form( $comment_form ) {
/**
* Element.
*
* @var DOMElement $element
*/
/**
* Named input elements.
*
* @var DOMElement[][] $form_fields
*/
$form_fields = array();
foreach ( $comment_form->getElementsByTagName( 'input' ) as $element ) {
$name = $element->getAttribute( 'name' );
if ( $name ) {
$form_fields[ $name ][] = $element;
}
}
foreach ( $comment_form->getElementsByTagName( 'textarea' ) as $element ) {
$name = $element->getAttribute( 'name' );
if ( $name ) {
$form_fields[ $name ][] = $element;
}
}
if ( empty( $form_fields['comment_post_ID'] ) ) {
return;
}
$post_id = (int) $form_fields['comment_post_ID'][0]->getAttribute( 'value' );
$state_id = AMP_Theme_Support::get_comment_form_state_id( $post_id );
$form_state = array(
'values' => array(),
'submitting' => false,
'replyToName' => '',
);
if ( ! empty( $form_fields['comment_parent'] ) ) {
$comment_id = (int) $form_fields['comment_parent'][0]->getAttribute( 'value' );
if ( $comment_id ) {
$reply_comment = get_comment( $comment_id );
if ( $reply_comment ) {
$form_state['replyToName'] = $reply_comment->comment_author;
}
}
}
$amp_bind_attr_format = AMP_DOM_Utils::get_amp_bind_placeholder_prefix() . '%s';
foreach ( $form_fields as $name => $form_field ) {
foreach ( $form_field as $element ) {
// @todo Radio and checkbox inputs are not supported yet.
if ( in_array( strtolower( $element->getAttribute( 'type' ) ), array( 'checkbox', 'radio' ), true ) ) {
continue;
}
$element->setAttribute( sprintf( $amp_bind_attr_format, 'disabled' ), "$state_id.submitting" );
if ( 'textarea' === strtolower( $element->nodeName ) ) {
$form_state['values'][ $name ] = $element->textContent;
$element->setAttribute( sprintf( $amp_bind_attr_format, 'text' ), "$state_id.values.$name" );
} else {
$form_state['values'][ $name ] = $element->hasAttribute( 'value' ) ? $element->getAttribute( 'value' ) : '';
$element->setAttribute( sprintf( $amp_bind_attr_format, 'value' ), "$state_id.values.$name" );
}
// Update the state in response to changing the input.
$element->setAttribute(
'on',
sprintf(
'change:AMP.setState( { %s: { values: { %s: event.value } } } )',
$state_id,
wp_json_encode( $name )
)
);
}
}
// Add amp-state to the document.
$amp_state = $this->dom->createElement( 'amp-state' );
$amp_state->setAttribute( 'id', $state_id );
$script = $this->dom->createElement( 'script' );
$script->setAttribute( 'type', 'application/json' );
$amp_state->appendChild( $script );
$script->appendChild( $this->dom->createTextNode( wp_json_encode( $form_state ) ) );
$comment_form->insertBefore( $amp_state, $comment_form->firstChild );
// Update state when submitting form.
$form_reset_state = $form_state;
unset(
$form_reset_state['values']['author'],
$form_reset_state['values']['email'],
$form_reset_state['values']['url']
);
$on = array(
// Disable the form when submitting.
sprintf(
'submit:AMP.setState( { %s: { submitting: true } } )',
wp_json_encode( $state_id )
),
// Re-enable the form fields when the submission fails.
sprintf(
'submit-error:AMP.setState( { %s: { submitting: false } } )',
wp_json_encode( $state_id )
),
// Reset the form to its initial state (with enabled form fields), except for the author, email, and url.
sprintf(
'submit-success:AMP.setState( { %s: %s } )',
$state_id,
wp_json_encode( $form_reset_state )
),
);
$comment_form->setAttribute( 'on', implode( ';', $on ) );
}
/**
* Add attributes to comment elements when comments are being presented in amp-live-list, when comments_live_list theme support flag is present.
*
* @since 1.1
*
* @param DOMElement $comment_element Comment element.
*/
protected function add_amp_live_list_comment_attributes( $comment_element ) {
$comment_id = (int) str_replace( 'comment-', '', $comment_element->getAttribute( 'id' ) );
if ( ! $comment_id ) {
return;
}
$comment_object = get_comment( $comment_id );
// Skip if the comment is not valid or the comment has a parent, since in that case it is not relevant for amp-live-list.
if ( ! ( $comment_object instanceof WP_Comment ) || $comment_object->comment_parent ) {
return;
}
$comment_element->setAttribute( 'data-sort-time', strtotime( $comment_object->comment_date ) );
$update_time = strtotime( $comment_object->comment_date );
// Ensure the top-level data-update-time reflects the max time of the comments in the thread.
$children = $comment_object->get_children(
array(
'format' => 'flat',
'hierarchical' => 'flat',
'orderby' => 'none',
)
);
foreach ( $children as $child_comment ) {
$update_time = max( strtotime( $child_comment->comment_date ), $update_time );
}
$comment_element->setAttribute( 'data-update-time', $update_time );
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Class AMP_Embed_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Embed_Sanitizer
*
* Calls sanitize_raw_embeds method on embed handlers.
*/
class AMP_Embed_Sanitizer extends AMP_Base_Sanitizer {
/**
* Embed handlers.
*
* @var AMP_Base_Embed_Handler[] AMP_Base_Embed_Handler[]
*/
private $embed_handlers = array();
/**
* AMP_Embed_Sanitizer constructor.
*
* @param DOMDocument $dom DOM.
* @param array $args Args.
*/
public function __construct( $dom, $args = array() ) {
parent::__construct( $dom, $args );
if ( ! empty( $this->args['embed_handlers'] ) ) {
$this->embed_handlers = $this->args['embed_handlers'];
}
}
/**
* Checks if each embed_handler has sanitize_raw_method and calls it.
*/
public function sanitize() {
foreach ( $this->embed_handlers as $embed_handler ) {
if ( method_exists( $embed_handler, 'sanitize_raw_embeds' ) ) {
$embed_handler->sanitize_raw_embeds( $this->dom );
}
}
}
}

View File

@ -0,0 +1,146 @@
<?php
/**
* Class AMP_Form_Sanitizer.
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Form_Sanitizer
*
* Strips and corrects attributes in forms.
*
* @since 0.7
*/
class AMP_Form_Sanitizer extends AMP_Base_Sanitizer {
/**
* Tag.
*
* @var string HTML <form> tag to identify and process.
*
* @since 0.7
*/
public static $tag = 'form';
/**
* Sanitize the <form> elements from the HTML contained in this instance's DOMDocument.
*
* @link https://www.ampproject.org/docs/reference/components/amp-form
* @since 0.7
*/
public function sanitize() {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
// In HTML, the default method is 'get'.
$method = 'get';
if ( $node->getAttribute( 'method' ) ) {
$method = strtolower( $node->getAttribute( 'method' ) );
} else {
$node->setAttribute( 'method', $method );
}
/*
* In HTML, the default action is just the current URL that the page is served from.
* The action "specifies a server endpoint to handle the form input. The value must be an
* https URL and must not be a link to a CDN".
*/
if ( ! $node->getAttribute( 'action' ) ) {
$action_url = esc_url_raw( '//' . $_SERVER['HTTP_HOST'] . wp_unslash( $_SERVER['REQUEST_URI'] ) );
} else {
$action_url = $node->getAttribute( 'action' );
// Check if action_url is a relative path and add the host to it.
if ( ! preg_match( '#^(https?:)?//#', $action_url ) ) {
$action_url = esc_url_raw( '//' . $_SERVER['HTTP_HOST'] . $action_url );
}
}
$xhr_action = $node->getAttribute( 'action-xhr' );
// Make HTTP URLs protocol-less, since HTTPS is required for forms.
if ( 'http://' === strtolower( substr( $action_url, 0, 7 ) ) ) {
$action_url = substr( $action_url, 5 );
}
/*
* According to the AMP spec:
* For GET submissions, provide at least one of action or action-xhr.
* This attribute is required for method=GET. For method=POST, the
* action attribute is invalid, use action-xhr instead.
*/
if ( 'get' === $method ) {
if ( $action_url !== $node->getAttribute( 'action' ) ) {
$node->setAttribute( 'action', $action_url );
}
} elseif ( 'post' === $method ) {
$node->removeAttribute( 'action' );
if ( ! $xhr_action ) {
// record that action was converted tp action-xhr.
$action_url = add_query_arg( '_wp_amp_action_xhr_converted', 1, $action_url );
$node->setAttribute( 'action-xhr', $action_url );
// Append error handler if not found.
$this->ensure_submit_error_element( $node );
} elseif ( 'http://' === substr( $xhr_action, 0, 7 ) ) {
$node->setAttribute( 'action-xhr', substr( $xhr_action, 5 ) );
}
}
/*
* The target "indicates where to display the form response after submitting the form.
* The value must be _blank or _top". The _self and _parent values are treated
* as synonymous with _top, and anything else is treated like _blank.
*/
$target = $node->getAttribute( 'target' );
if ( '_top' !== $target ) {
if ( ! $target || in_array( $target, array( '_self', '_parent' ), true ) ) {
$node->setAttribute( 'target', '_top' );
} elseif ( '_blank' !== $target ) {
$node->setAttribute( 'target', '_blank' );
}
}
}
}
/**
* Checks if the form has an error handler else create one if not.
*
* @link https://www.ampproject.org/docs/reference/components/amp-form#success/error-response-rendering
* @since 0.7
*
* @param DOMElement $form The form node to check.
*/
public function ensure_submit_error_element( $form ) {
$templates = $form->getElementsByTagName( 'template' );
for ( $i = $templates->length - 1; $i >= 0; $i-- ) {
if ( $templates->item( $i )->parentNode->hasAttribute( 'submit-error' ) ) {
return; // Found error template, do nothing.
}
}
$div = $this->dom->createElement( 'div' );
$template = $this->dom->createElement( 'template' );
$mustache = $this->dom->createTextNode( '{{{error}}}' );
$div->setAttribute( 'submit-error', '' );
$template->setAttribute( 'type', 'amp-mustache' );
$template->appendChild( $mustache );
$div->appendChild( $template );
$form->appendChild( $div );
}
}

View File

@ -0,0 +1,211 @@
<?php
/**
* Class AMP_Gallery_Block_Sanitizer.
*
* @package AMP
*/
/**
* Class AMP_Gallery_Block_Sanitizer
*
* Modifies gallery block to match the block's AMP-specific configuration.
*/
class AMP_Gallery_Block_Sanitizer extends AMP_Base_Sanitizer {
/**
* Value used for width of amp-carousel.
*
* @since 1.0
*
* @const int
*/
const FALLBACK_WIDTH = 600;
/**
* Value used for height of amp-carousel.
*
* @since 1.0
*
* @const int
*/
const FALLBACK_HEIGHT = 480;
/**
* Tag.
*
* @since 1.0
*
* @var string Ul tag to identify wrapper around gallery block.
*/
public static $tag = 'ul';
/**
* Expected class of the wrapper around the gallery block.
*
* @since 1.0
*
* @var string
*/
public static $class = 'wp-block-gallery';
/**
* Array of flags used to control sanitization.
*
* @var array {
* @type int $content_max_width Max width of content.
* @type bool $carousel_required Whether carousels are required. This is used when amp theme support is not present, for back-compat.
* }
*/
protected $args;
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'carousel_required' => false,
);
/**
* Sanitize the gallery block contained by <ul> element where necessary.
*
* @since 0.2
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
// We're looking for <ul> elements that have at least one child and the proper class.
if ( 0 === count( $node->childNodes ) || false === strpos( $node->getAttribute( 'class' ), self::$class ) ) {
continue;
}
$attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
$is_amp_lightbox = isset( $attributes['data-amp-lightbox'] ) && true === filter_var( $attributes['data-amp-lightbox'], FILTER_VALIDATE_BOOLEAN );
$is_amp_carousel = ! empty( $this->args['carousel_required'] ) || ( isset( $attributes['data-amp-carousel'] ) && true === filter_var( $attributes['data-amp-carousel'], FILTER_VALIDATE_BOOLEAN ) );
// We are only looking for <ul> elements which have amp-carousel / amp-lightbox true.
if ( ! $is_amp_carousel && ! $is_amp_lightbox ) {
continue;
}
// If lightbox is set, we should add lightbox feature to the gallery images.
if ( $is_amp_lightbox ) {
$this->add_lightbox_attributes_to_image_nodes( $node );
$this->maybe_add_amp_image_lightbox_node();
}
// If amp-carousel is not set, nothing else to do here.
if ( ! $is_amp_carousel ) {
continue;
}
$images = array();
// If it's not AMP lightbox, look for links first.
if ( ! $is_amp_lightbox ) {
foreach ( $node->getElementsByTagName( 'a' ) as $element ) {
$images[] = $element;
}
}
// If not linking to anything then look for <amp-img>.
if ( empty( $images ) ) {
foreach ( $node->getElementsByTagName( 'amp-img' ) as $element ) {
$images[] = $element;
}
}
// Skip if no images found.
if ( empty( $images ) ) {
continue;
}
$amp_carousel = AMP_DOM_Utils::create_node(
$this->dom,
'amp-carousel',
array(
'height' => $this->get_carousel_height( $node ),
'type' => 'slides',
'layout' => 'fixed-height',
)
);
foreach ( $images as $image ) {
$amp_carousel->appendChild( $image );
}
$node->parentNode->replaceChild( $amp_carousel, $node );
}
$this->did_convert_elements = true;
}
/**
* Get carousel height by containing images.
*
* @param DOMElement $element The UL element.
* @return int Height.
*/
protected function get_carousel_height( $element ) {
$images = $element->getElementsByTagName( 'amp-img' );
$num_images = $images->length;
$max_height = 0;
$max_width = 0;
if ( 0 === $num_images ) {
return self::FALLBACK_HEIGHT;
}
foreach ( $images as $image ) {
/**
* Image.
*
* @var DOMElement $image
*/
$image_height = $image->getAttribute( 'height' );
if ( is_numeric( $image_height ) ) {
$max_height = max( $max_height, $image_height );
}
$image_width = $image->getAttribute( 'height' );
if ( is_numeric( $image_width ) ) {
$max_width = max( $max_width, $image_width );
}
}
if ( ! empty( $this->args['content_max_width'] ) && $max_height > 0 && $max_width > $this->args['content_max_width'] ) {
$max_height = ( $max_width * $this->args['content_max_width'] ) / $max_height;
}
return ! $max_height ? self::FALLBACK_HEIGHT : $max_height;
}
/**
* Set lightbox related attributes to <amp-img> within gallery.
*
* @param DOMElement $element The UL element.
*/
protected function add_lightbox_attributes_to_image_nodes( $element ) {
$images = $element->getElementsByTagName( 'amp-img' );
$num_images = $images->length;
if ( 0 === $num_images ) {
return;
}
$attributes = array(
'data-amp-lightbox' => '',
'on' => 'tap:' . self::AMP_IMAGE_LIGHTBOX_ID,
'role' => 'button',
'tabindex' => 0,
);
for ( $j = $num_images - 1; $j >= 0; $j-- ) {
$image_node = $images->item( $j );
foreach ( $attributes as $att => $value ) {
$image_node->setAttribute( $att, $value );
}
}
}
}

View File

@ -0,0 +1,218 @@
<?php
/**
* Class AMP_Iframe_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Iframe_Sanitizer
*
* Converts <iframe> tags to <amp-iframe>
*/
class AMP_Iframe_Sanitizer extends AMP_Base_Sanitizer {
use AMP_Noscript_Fallback;
/**
* Value used for height attribute when $attributes['height'] is empty.
*
* @since 0.2
*
* @const int
*/
const FALLBACK_HEIGHT = 400;
/**
* Default values for sandboxing IFrame.
*
* @since 0.2
*
* @const int
*/
const SANDBOX_DEFAULTS = 'allow-scripts allow-same-origin';
/**
* Tag.
*
* @var string HTML <iframe> tag to identify and replace with AMP version.
*
* @since 0.2
*/
public static $tag = 'iframe';
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'add_placeholder' => false,
'add_noscript_fallback' => true,
);
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array(
'iframe' => array(
'amp-iframe',
),
);
}
/**
* Sanitize the <iframe> elements from the HTML contained in this instance's DOMDocument.
*
* @since 0.2
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
if ( $this->args['add_noscript_fallback'] ) {
$this->initialize_noscript_allowed_attributes( self::$tag );
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
// Skip element if already inside of an AMP element as a noscript fallback.
if ( $this->is_inside_amp_noscript( $node ) ) {
continue;
}
$normalized_attributes = $this->normalize_attributes( AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node ) );
/**
* If the src doesn't exist, remove the node. Either it never
* existed or was invalidated while filtering attributes above.
*
* @todo: add a filter to allow for a fallback element in this instance.
* @see: https://github.com/ampproject/amphtml/issues/2261
*/
if ( empty( $normalized_attributes['src'] ) ) {
$this->remove_invalid_child( $node );
continue;
}
$this->did_convert_elements = true;
$normalized_attributes = $this->set_layout( $normalized_attributes );
if ( empty( $normalized_attributes['layout'] ) && ! empty( $normalized_attributes['width'] ) && ! empty( $normalized_attributes['height'] ) ) {
$normalized_attributes['layout'] = 'intrinsic';
$this->add_or_append_attribute( $normalized_attributes, 'class', 'amp-wp-enforced-sizes' );
}
$new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-iframe', $normalized_attributes );
if ( true === $this->args['add_placeholder'] ) {
$placeholder_node = $this->build_placeholder( $normalized_attributes );
$new_node->appendChild( $placeholder_node );
}
$node->parentNode->replaceChild( $new_node, $node );
if ( $this->args['add_noscript_fallback'] ) {
$node->setAttribute( 'src', $normalized_attributes['src'] );
// Preserve original node in noscript for no-JS environments.
$this->append_old_node_noscript( $new_node, $node, $this->dom );
}
}
}
/**
* Normalize HTML attributes for <amp-iframe> elements.
*
* @param string[] $attributes {
* Attributes.
*
* @type string $src IFrame URL - Empty if HTTPS required per $this->args['require_https_src']
* @type int $width <iframe> width attribute - Set to numeric value if px or %
* @type int $height <iframe> width attribute - Set to numeric value if px or %
* @type string $sandbox <iframe> `sandbox` attribute - Pass along if found; default to value of self::SANDBOX_DEFAULTS
* @type string $class <iframe> `class` attribute - Pass along if found
* @type string $sizes <iframe> `sizes` attribute - Pass along if found
* @type string $id <iframe> `id` attribute - Pass along if found
* @type int $frameborder <iframe> `frameborder` attribute - Filter to '0' or '1'; default to '0'
* @type bool $allowfullscreen <iframe> `allowfullscreen` attribute - Convert 'false' to empty string ''
* @type bool $allowtransparency <iframe> `allowtransparency` attribute - Convert 'false' to empty string ''
* }
* @return array Returns HTML attributes; normalizes src, dimensions, frameborder, sandox, allowtransparency and allowfullscreen
*/
private function normalize_attributes( $attributes ) {
$out = array();
foreach ( $attributes as $name => $value ) {
switch ( $name ) {
case 'src':
$out[ $name ] = $this->maybe_enforce_https_src( $value, true );
break;
case 'width':
case 'height':
$out[ $name ] = $this->sanitize_dimension( $value, $name );
break;
case 'frameborder':
if ( '0' !== $value && '1' !== $value ) {
$value = '0';
}
$out[ $name ] = $value;
break;
case 'allowfullscreen':
case 'allowtransparency':
if ( 'false' !== $value ) {
$out[ $name ] = '';
}
break;
default:
$out[ $name ] = $value;
break;
}
}
if ( ! isset( $out['sandbox'] ) ) {
$out['sandbox'] = self::SANDBOX_DEFAULTS;
}
return $out;
}
/**
* Builds a DOMElement to use as a placeholder for an <iframe>.
*
* Important: The element returned must not be block-level (e.g. div) as the PHP DOM parser
* will move it out from inside any containing paragraph. So this is why a span is used.
*
* @since 0.2
*
* @param string[] $parent_attributes {
* Attributes.
*
* @type string $placeholder AMP HTML <amp-iframe> `placeholder` attribute; default to 'amp-wp-iframe-placeholder'
* @type string $class AMP HTML <amp-iframe> `class` attribute; default to 'amp-wp-iframe-placeholder'
* }
* @return DOMElement|false
*/
private function build_placeholder( $parent_attributes ) {
$placeholder_node = AMP_DOM_Utils::create_node(
$this->dom,
'span',
array(
'placeholder' => '',
'class' => 'amp-wp-iframe-placeholder',
)
);
return $placeholder_node;
}
}

View File

@ -0,0 +1,357 @@
<?php
/**
* Class AMP_Img_Sanitizer.
*
* @package AMP
*/
/**
* Class AMP_Img_Sanitizer
*
* Converts <img> tags to <amp-img> or <amp-anim>
*/
class AMP_Img_Sanitizer extends AMP_Base_Sanitizer {
use AMP_Noscript_Fallback;
/**
* Value used for width attribute when $attributes['width'] is empty.
*
* @since 0.2
*
* @const int
*/
const FALLBACK_WIDTH = 600;
/**
* Value used for height attribute when $attributes['height'] is empty.
*
* @since 0.2
*
* @const int
*/
const FALLBACK_HEIGHT = 400;
/**
* Tag.
*
* @var string HTML <img> tag to identify and replace with AMP version.
*
* @since 0.2
*/
public static $tag = 'img';
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'add_noscript_fallback' => true,
);
/**
* Animation extension.
*
* @var string
*/
private static $anim_extension = '.gif';
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array(
'img' => array(
'amp-img',
'amp-anim',
),
);
}
/**
* Sanitize the <img> elements from the HTML contained in this instance's DOMDocument.
*
* @since 0.2
*/
public function sanitize() {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $this->dom->getElementsByTagName( self::$tag );
$need_dimensions = array();
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
if ( $this->args['add_noscript_fallback'] ) {
$this->initialize_noscript_allowed_attributes( self::$tag );
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
// Skip element if already inside of an AMP element as a noscript fallback.
if ( $this->is_inside_amp_noscript( $node ) ) {
continue;
}
if ( ! $node->hasAttribute( 'src' ) || '' === trim( $node->getAttribute( 'src' ) ) ) {
$this->remove_invalid_child( $node );
continue;
}
// Determine which images need their dimensions determined/extracted.
if ( ! is_numeric( $node->getAttribute( 'width' ) ) || ! is_numeric( $node->getAttribute( 'height' ) ) ) {
$need_dimensions[ $node->getAttribute( 'src' ) ][] = $node;
} else {
$this->adjust_and_replace_node( $node );
}
}
$this->determine_dimensions( $need_dimensions );
$this->adjust_and_replace_nodes_in_array_map( $need_dimensions );
/*
* Opt-in to amp-img-auto-sizes experiment.
* This is needed because the sizes attribute is removed from all img elements converted to amp-img
* in order to prevent the undesirable setting of the width. This $meta tag can be removed once the
* experiment ends (and the feature has been fully launched).
* See <https://github.com/ampproject/amphtml/issues/21371> and <https://github.com/ampproject/amp-wp/pull/2036>.
*/
$head = $this->dom->getElementsByTagName( 'head' )->item( 0 );
if ( $head ) {
$meta = $this->dom->createElement( 'meta' );
$meta->setAttribute( 'name', 'amp-experiments-opt-in' );
$meta->setAttribute( 'content', 'amp-img-auto-sizes' );
$head->insertBefore( $meta, $head->firstChild );
}
}
/**
* "Filter" HTML attributes for <amp-anim> elements.
*
* @since 0.2
*
* @param string[] $attributes {
* Attributes.
*
* @type string $src Image URL - Pass along if found
* @type string $alt <img> `alt` attribute - Pass along if found
* @type string $class <img> `class` attribute - Pass along if found
* @type string $srcset <img> `srcset` attribute - Pass along if found
* @type string $sizes <img> `sizes` attribute - Pass along if found
* @type string $on <img> `on` attribute - Pass along if found
* @type string $attribution <img> `attribution` attribute - Pass along if found
* @type int $width <img> width attribute - Set to numeric value if px or %
* @type int $height <img> width attribute - Set to numeric value if px or %
* }
* @return array Returns HTML attributes; removes any not specifically declared above from input.
*/
private function filter_attributes( $attributes ) {
$out = array();
foreach ( $attributes as $name => $value ) {
switch ( $name ) {
case 'width':
case 'height':
$out[ $name ] = $this->sanitize_dimension( $value, $name );
break;
case 'data-amp-layout':
$out['layout'] = $value;
break;
case 'data-amp-noloading':
$out['noloading'] = $value;
break;
default:
$out[ $name ] = $value;
break;
}
}
return $out;
}
/**
* Determine width and height attribute values for images without them.
*
* Attempt to determine actual dimensions, otherwise set reasonable defaults.
*
* @param DOMElement[][] $need_dimensions Map <img> @src URLs to node for images with missing dimensions.
*/
private function determine_dimensions( $need_dimensions ) {
$dimensions_by_url = AMP_Image_Dimension_Extractor::extract( array_keys( $need_dimensions ) );
foreach ( $dimensions_by_url as $url => $dimensions ) {
foreach ( $need_dimensions[ $url ] as $node ) {
if ( ! $node instanceof DOMElement ) {
continue;
}
$class = $node->getAttribute( 'class' );
if ( ! $class ) {
$class = '';
}
if ( ! $dimensions ) {
$class .= ' amp-wp-unknown-size';
}
$width = isset( $this->args['content_max_width'] ) ? $this->args['content_max_width'] : self::FALLBACK_WIDTH;
$height = self::FALLBACK_HEIGHT;
if ( isset( $dimensions['width'] ) ) {
$width = $dimensions['width'];
}
if ( isset( $dimensions['height'] ) ) {
$height = $dimensions['height'];
}
if ( ! is_numeric( $node->getAttribute( 'width' ) ) ) {
// Let width have the right aspect ratio based on the height attribute.
if ( is_numeric( $node->getAttribute( 'height' ) ) && isset( $dimensions['height'] ) && isset( $dimensions['width'] ) ) {
$width = ( floatval( $node->getAttribute( 'height' ) ) * $dimensions['width'] ) / $dimensions['height'];
}
$node->setAttribute( 'width', $width );
if ( ! isset( $dimensions['width'] ) ) {
$class .= ' amp-wp-unknown-width';
}
}
if ( ! is_numeric( $node->getAttribute( 'height' ) ) ) {
// Let height have the right aspect ratio based on the width attribute.
if ( is_numeric( $node->getAttribute( 'width' ) ) && isset( $dimensions['width'] ) && isset( $dimensions['height'] ) ) {
$height = ( floatval( $node->getAttribute( 'width' ) ) * $dimensions['height'] ) / $dimensions['width'];
}
$node->setAttribute( 'height', $height );
if ( ! isset( $dimensions['height'] ) ) {
$class .= ' amp-wp-unknown-height';
}
}
$node->setAttribute( 'class', trim( $class ) );
}
}
}
/**
* Now that all images have width and height attributes, make final tweaks and replace original image nodes
*
* @param DOMNodeList[] $node_lists Img DOM nodes (now with width and height attributes).
*/
private function adjust_and_replace_nodes_in_array_map( $node_lists ) {
foreach ( $node_lists as $node_list ) {
foreach ( $node_list as $node ) {
$this->adjust_and_replace_node( $node );
}
}
}
/**
* Make final modifications to DOMNode
*
* @param DOMElement $node The img element to adjust and replace.
*/
private function adjust_and_replace_node( $node ) {
$amp_data = $this->get_data_amp_attributes( $node );
$old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
$old_attributes = $this->filter_data_amp_attributes( $old_attributes, $amp_data );
$old_attributes = $this->maybe_add_lightbox_attributes( $old_attributes, $node );
$new_attributes = $this->filter_attributes( $old_attributes );
$layout = isset( $amp_data['layout'] ) ? $amp_data['layout'] : false;
$new_attributes = $this->filter_attachment_layout_attributes( $node, $new_attributes, $layout );
$this->add_or_append_attribute( $new_attributes, 'class', 'amp-wp-enforced-sizes' );
if ( empty( $new_attributes['layout'] ) && ! empty( $new_attributes['height'] ) && ! empty( $new_attributes['width'] ) ) {
// Use responsive images when a theme supports wide and full-bleed images.
if ( ! empty( $this->args['align_wide_support'] ) && $node->parentNode && 'figure' === $node->parentNode->nodeName && preg_match( '/(^|\s)(alignwide|alignfull)(\s|$)/', $node->parentNode->getAttribute( 'class' ) ) ) {
$new_attributes['layout'] = 'responsive';
} else {
$new_attributes['layout'] = 'intrinsic';
}
}
// Remove sizes attribute since it causes headaches in AMP and because AMP will generate it for us. See <https://github.com/ampproject/amphtml/issues/21371>.
unset( $new_attributes['sizes'] );
if ( $this->is_gif_url( $new_attributes['src'] ) ) {
$this->did_convert_elements = true;
$new_tag = 'amp-anim';
} else {
$new_tag = 'amp-img';
}
$img_node = AMP_DOM_Utils::create_node( $this->dom, $new_tag, $new_attributes );
$node->parentNode->replaceChild( $img_node, $node );
$can_include_noscript = (
$this->args['add_noscript_fallback']
&&
( $node->hasAttribute( 'src' ) && ! preg_match( '/^http:/', $node->getAttribute( 'src' ) ) )
&&
( ! $node->hasAttribute( 'srcset' ) || ! preg_match( '/http:/', $node->getAttribute( 'srcset' ) ) )
);
if ( $can_include_noscript ) {
// Preserve original node in noscript for no-JS environments.
$this->append_old_node_noscript( $img_node, $node, $this->dom );
}
}
/**
* Set lightbox attributes.
*
* @param array $attributes Array of attributes.
* @param DomNode $node Array of AMP attributes.
* @return array Updated attributes.
*/
private function maybe_add_lightbox_attributes( $attributes, $node ) {
$parent_node = $node->parentNode;
if ( ! ( $parent_node instanceof DOMElement ) || 'figure' !== $parent_node->tagName ) {
return $attributes;
}
$parent_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $parent_node );
if ( isset( $parent_attributes['data-amp-lightbox'] ) && true === filter_var( $parent_attributes['data-amp-lightbox'], FILTER_VALIDATE_BOOLEAN ) ) {
$attributes['data-amp-lightbox'] = '';
$attributes['on'] = 'tap:' . self::AMP_IMAGE_LIGHTBOX_ID;
$attributes['role'] = 'button';
$attributes['tabindex'] = 0;
$this->maybe_add_amp_image_lightbox_node();
}
return $attributes;
}
/**
* Determines is a URL is considered a GIF URL
*
* @since 0.2
*
* @param string $url URL to inspect for GIF vs. JPEG or PNG.
*
* @return bool Returns true if $url ends in `.gif`
*/
private function is_gif_url( $url ) {
$ext = self::$anim_extension;
$path = wp_parse_url( $url, PHP_URL_PATH );
return substr( $path, -strlen( $ext ) ) === $ext;
}
}

Some files were not shown because too many files have changed in this diff Show More