This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,355 @@
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, see <https://www.gnu.org/licenses/>.
===================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
<https://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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, see <https://www.gnu.org/licenses/>.
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 Moe Ghoul>, 1 April 1989
Moe Ghoul, 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.
@@ -0,0 +1,46 @@
# Security Policy
Full details of the Automattic Security Policy can be found on [automattic.com](https://automattic.com/security/).
## Supported Versions
Generally, only the latest version of Jetpack and its associated plugins have continued support. If a critical vulnerability is found in the current version of a plugin, we may opt to backport any patches to previous versions.
## Reporting a Vulnerability
Our HackerOne program covers the below plugin software, as well as a variety of related projects and infrastructure:
* [Jetpack](https://jetpack.com/)
* Jetpack Backup
* Jetpack Boost
* Jetpack Protect
* Jetpack Search
* Jetpack Social
* Jetpack VideoPress
**For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.**
Our most critical targets are:
* Jetpack and the Jetpack composer packages (all within this repo)
* Jetpack.com -- the primary marketing site.
* cloud.jetpack.com -- a management site.
* wordpress.com -- the shared management site for both Jetpack and WordPress.com sites.
For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic).
_Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._
## Guidelines
We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines:
* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
* Pen-testing Production:
* Please **setup a local environment** instead whenever possible. Most of our code is open source (see above).
* If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC.
* **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels.
* To be eligible for a bounty, all of these guidelines must be followed.
* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability.
We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties.
@@ -0,0 +1,535 @@
<?php
/**
* Jetpack Boost Abilities Registration
*
* Registers Jetpack Boost abilities with the WordPress Abilities API so AI
* agents can read module state, toggle modules, fetch the latest speed score,
* and clear the page cache through the standard `wp-abilities/v1` REST surface.
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Abilities;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
use Automattic\Jetpack\WP_Abilities\Registrar;
use Automattic\Jetpack_Boost\Modules\Features_Index;
use Automattic\Jetpack_Boost\Modules\Module;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
/**
* Registers Jetpack Boost abilities with the WordPress Abilities API.
*
* Surface (4 abilities, all under the `jetpack-boost/` namespace):
*
* - get-modules — filtered read of every Boost module + submodule.
* - set-module-status — declarative toggle, idempotent.
* - get-speed-score — latest mobile/desktop scores from history.
* - clear-page-cache — flush the Boost page cache for the home URL.
*
* @since 4.6.0
*/
class Boost_Abilities extends Registrar {
/**
* @inheritDoc
*/
public static function get_category_slug(): string {
return 'jetpack-boost';
}
/**
* @inheritDoc
*/
public static function get_category_definition(): array {
return array(
// "Jetpack Boost" is a product name and is not translated.
'label' => 'Jetpack Boost',
'description' => __( 'Abilities for inspecting and managing Jetpack Boost performance modules, speed scores, and page cache.', 'jetpack-boost' ),
);
}
/**
* @inheritDoc
*/
public static function get_abilities(): array {
$module_object_schema = array(
'type' => 'object',
'properties' => array(
'slug' => array( 'type' => 'string' ),
'active' => array( 'type' => 'boolean' ),
'available' => array( 'type' => 'boolean' ),
'optimizing' => array( 'type' => 'boolean' ),
),
);
return array(
'jetpack-boost/get-modules' => array(
'label' => __( 'Get Boost modules', 'jetpack-boost' ),
'description' => __( 'List Jetpack Boost performance modules and submodules. Returns an array of { slug, active, available, optimizing }. Slugs use underscores (e.g. "critical_css", "page_cache", "image_cdn"). Pass slug to fetch a single module (returns a 0- or 1-element array; unknown slugs yield an empty array, never an error). Pass status to filter by active/inactive/available/optimizing. Use the returned slugs as input to jetpack-boost/set-module-status.', 'jetpack-boost' ),
'input_schema' => array(
'type' => 'object',
'default' => array(),
'properties' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Return a single module by its slug (e.g. "critical_css", "page_cache"). Unknown slugs yield an empty array.', 'jetpack-boost' ),
'minLength' => 1,
),
'status' => array(
'type' => 'string',
'description' => __( 'Filter by lifecycle state. "active" = enabled by the user. "inactive" = available but disabled. "available" = currently loadable on this site (regardless of enabled state). "optimizing" = active and currently serving optimized output.', 'jetpack-boost' ),
'enum' => array( 'active', 'inactive', 'available', 'optimizing' ),
),
'search' => array(
'type' => 'string',
'description' => __( 'Case-insensitive substring match against the module slug.', 'jetpack-boost' ),
'minLength' => 1,
),
),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'array',
'items' => $module_object_schema,
),
'execute_callback' => array( __CLASS__, 'get_modules' ),
'permission_callback' => array( __CLASS__, 'can_view_modules' ),
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive' => false,
'idempotent' => true,
),
'show_in_rest' => true,
'mcp' => array(
'public' => true,
'type' => 'tool',
),
),
),
'jetpack-boost/set-module-status' => array(
'label' => __( 'Set Boost module status', 'jetpack-boost' ),
'description' => __( 'Enable or disable a single Jetpack Boost module by slug. Required: { slug, active }. Returns { slug, active, changed }. Idempotent: setting a module to its current state returns changed=false. Slugs use underscores (e.g. "critical_css", "page_cache"). Unknown slugs return jetpack_boost_invalid_slug; modules not loadable on this site return jetpack_boost_module_unavailable; always-on modules cannot be disabled and return jetpack_boost_module_always_on — call jetpack-boost/get-modules to enumerate available slugs. Toggling a parent module also drives submodule lifecycle.', 'jetpack-boost' ),
'input_schema' => array(
'type' => 'object',
'required' => array( 'slug', 'active' ),
'properties' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Module slug to toggle (e.g. "critical_css", "page_cache").', 'jetpack-boost' ),
'minLength' => 1,
),
'active' => array(
'type' => 'boolean',
'description' => __( 'Desired state: true to enable, false to disable.', 'jetpack-boost' ),
),
),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'slug' => array( 'type' => 'string' ),
'active' => array( 'type' => 'boolean' ),
'changed' => array( 'type' => 'boolean' ),
),
),
'execute_callback' => array( __CLASS__, 'set_module_status' ),
'permission_callback' => array( __CLASS__, 'can_manage_modules' ),
'meta' => array(
'annotations' => array(
'readonly' => false,
'destructive' => false,
'idempotent' => true,
),
'show_in_rest' => true,
'mcp' => array(
'public' => true,
'type' => 'tool',
),
),
),
'jetpack-boost/get-speed-score' => array(
'label' => __( 'Get latest speed score', 'jetpack-boost' ),
'description' => __( 'Return the most recent Jetpack Boost speed score for the home URL. Returns { mobile, desktop, timestamp, is_stale, has_history }. mobile/desktop are integers 0-100 (Google PageSpeed scale) or null when no score has been recorded. timestamp is a Unix epoch in seconds. is_stale=true means the latest score is older than 24 hours or invalidated by a site change; agents should request a refresh from the Boost UI before quoting it. has_history=false means no scores have been recorded yet.', 'jetpack-boost' ),
'input_schema' => array(
'type' => 'object',
'default' => array(),
'properties' => array(),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'mobile' => array( 'type' => array( 'integer', 'null' ) ),
'desktop' => array( 'type' => array( 'integer', 'null' ) ),
'timestamp' => array( 'type' => array( 'integer', 'null' ) ),
'is_stale' => array( 'type' => 'boolean' ),
'has_history' => array( 'type' => 'boolean' ),
),
),
'execute_callback' => array( __CLASS__, 'get_speed_score' ),
'permission_callback' => array( __CLASS__, 'can_view_modules' ),
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive' => false,
'idempotent' => true,
),
'show_in_rest' => true,
'mcp' => array(
'public' => true,
'type' => 'tool',
),
),
),
'jetpack-boost/clear-page-cache' => array(
'label' => __( 'Clear page cache', 'jetpack-boost' ),
'description' => __( 'Clear every cached page under the site home URL. Idempotent: re-running on an empty cache is a no-op. Returns { cleared, message }. cleared=true means the clear request was dispatched against an active page_cache module; it does not promise that cached files actually existed (the underlying API does not surface a count). Requires the page_cache module to be active; if it is not, returns jetpack_boost_page_cache_inactive — enable it first via jetpack-boost/set-module-status with slug="page_cache".', 'jetpack-boost' ),
'input_schema' => array(
'type' => 'object',
'default' => array(),
'properties' => array(),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'cleared' => array( 'type' => 'boolean' ),
'message' => array( 'type' => 'string' ),
),
),
'execute_callback' => array( __CLASS__, 'clear_page_cache' ),
'permission_callback' => array( __CLASS__, 'can_manage_modules' ),
'meta' => array(
'annotations' => array(
'readonly' => false,
'destructive' => false,
'idempotent' => true,
),
'show_in_rest' => true,
'mcp' => array(
'public' => true,
'type' => 'tool',
),
),
),
);
}
/**
* Permission check for read-only abilities.
*
* Boost has no domain-specific capability; every existing Boost surface
* gates on `manage_options`. We mirror that here so abilities are no more
* (or less) permissive than the REST and admin surfaces.
*
* @since 4.6.0
*/
public static function can_view_modules(): bool {
return is_user_logged_in() && current_user_can( 'manage_options' );
}
/**
* Permission check for write abilities.
*
* @since 4.6.0
*/
public static function can_manage_modules(): bool {
return is_user_logged_in() && current_user_can( 'manage_options' );
}
/**
* Cache for `build_module_index()`. Module construction touches options for
* every feature, so we memoise per-request — both `get_modules()` and
* `set_module_status()` consume the same map.
*
* @var array<string, Module>|null
*/
private static $module_index_cache = null;
/**
* Build a `Module` instance for every feature + submodule, keyed by slug.
*
* @return array<string, Module>
*/
private static function build_module_index(): array {
if ( null !== self::$module_index_cache ) {
return self::$module_index_cache;
}
$index = array();
foreach ( Features_Index::get_all_features() as $feature_class ) {
$module = new Module( new $feature_class() );
$index[ $module->get_slug() ] = $module;
}
self::$module_index_cache = $index;
return $index;
}
private static function render_module( Module $module ): array {
return array(
'slug' => $module->get_slug(),
'active' => $module->is_available() && $module->is_enabled(),
'available' => $module->is_available(),
'optimizing' => $module->is_optimizing(),
);
}
/**
* Execute: filtered read of Boost modules.
*
* @since 4.6.0
*
* @param array|null $input Input matching the ability's input_schema.
* @return array|\WP_Error
*/
public static function get_modules( $input = null ) {
$input = is_array( $input ) ? $input : array();
$index = self::build_module_index();
// Single-slug short-circuit — return 0- or 1-element array, same shape as the list case.
// A non-string slug is invalid input (not "unknown"); rejecting it prevents the bad-shape
// fall-through where slug:123 would silently return every module.
if ( isset( $input['slug'] ) ) {
if ( ! is_string( $input['slug'] ) ) {
return new \WP_Error(
'jetpack_boost_invalid_slug',
__( 'The slug parameter must be a string.', 'jetpack-boost' )
);
}
if ( '' !== $input['slug'] ) {
return isset( $index[ $input['slug'] ] )
? array( self::render_module( $index[ $input['slug'] ] ) )
: array();
}
}
$status_filter = isset( $input['status'] ) && is_string( $input['status'] ) && '' !== $input['status']
? $input['status']
: null;
$search_filter = isset( $input['search'] ) && is_string( $input['search'] ) && '' !== $input['search']
? $input['search']
: null;
$out = array();
foreach ( $index as $slug => $module ) {
$rendered = self::render_module( $module );
if ( null !== $status_filter ) {
$matches = false;
switch ( $status_filter ) {
case 'active':
$matches = $rendered['active'];
break;
case 'inactive':
$matches = $rendered['available'] && ! $rendered['active'];
break;
case 'available':
$matches = $rendered['available'];
break;
case 'optimizing':
$matches = $rendered['optimizing'];
break;
}
if ( ! $matches ) {
continue;
}
}
// Boost slugs are ASCII snake_case; stripos is sufficient and avoids ext-mbstring.
if ( null !== $search_filter && false === stripos( $slug, $search_filter ) ) {
continue;
}
$out[] = $rendered;
}
// Deterministic ordering — agents diff results across calls.
usort(
$out,
static function ( $a, $b ) {
return strcmp( $a['slug'], $b['slug'] );
}
);
return $out;
}
/**
* Execute: declarative module toggle. Idempotent.
*
* @since 4.6.0
*
* @param array|null $input Input matching the ability's input_schema.
* @return array|\WP_Error
*/
public static function set_module_status( $input = null ) {
$input = is_array( $input ) ? $input : array();
// Required-id validation: not empty(), so "0" remains a legal slug if a future module ever uses it.
if ( ! isset( $input['slug'] ) || ! is_string( $input['slug'] ) || '' === $input['slug'] ) {
return new \WP_Error(
'jetpack_boost_missing_slug',
__( 'A module slug is required. Call jetpack-boost/get-modules to enumerate available slugs.', 'jetpack-boost' )
);
}
if ( ! array_key_exists( 'active', $input ) ) {
return new \WP_Error(
'jetpack_boost_missing_active',
__( 'A desired active state (boolean) is required.', 'jetpack-boost' )
);
}
if ( ! is_bool( $input['active'] ) ) {
return new \WP_Error(
'jetpack_boost_invalid_active',
__( 'The active parameter must be a boolean. Strings like "true" / "false" are not accepted.', 'jetpack-boost' )
);
}
$slug = $input['slug'];
$desired = $input['active'];
$index = self::build_module_index();
if ( ! isset( $index[ $slug ] ) ) {
return new \WP_Error(
'jetpack_boost_invalid_slug',
__( 'Unknown Boost module slug. Call jetpack-boost/get-modules to enumerate available slugs.', 'jetpack-boost' )
);
}
$module = $index[ $slug ];
if ( ! $module->is_available() ) {
return new \WP_Error(
'jetpack_boost_module_unavailable',
__( 'This module is not available on this site (e.g. requires a connection or a paid plan).', 'jetpack-boost' )
);
}
// Always-on modules ignore the persisted option at runtime. Writing here would leave
// on-disk state diverged from runtime state with no rollback, so refuse the write up front.
if ( $module->is_always_on() ) {
if ( $desired ) {
return array(
'slug' => $slug,
'active' => true,
'changed' => false,
);
}
return new \WP_Error(
'jetpack_boost_module_always_on',
__( 'This module is always on and cannot be disabled.', 'jetpack-boost' )
);
}
$current = $module->is_enabled();
if ( $desired === $current ) {
return array(
'slug' => $slug,
'active' => $current,
'changed' => false,
);
}
if ( ! $module->update( $desired ) ) {
return new \WP_Error(
'jetpack_boost_module_update_failed',
__( 'Failed to persist the module status.', 'jetpack-boost' )
);
}
/**
* Fires when a module is enabled or disabled through the abilities surface.
*
* Mirrors the action emitted by `Modules_State_Entry::set()` so submodule
* lifecycle handlers fire identically regardless of caller.
*
* @param string $module_slug The module slug.
* @param bool $is_active The new state.
*/
do_action( 'jetpack_boost_module_status_updated', $slug, $desired );
return array(
'slug' => $slug,
'active' => $desired,
'changed' => true,
);
}
/**
* Execute: latest speed score for the home URL.
*
* @since 4.6.0
*
* @param array|null $input Unused; ability has no inputs.
* @return array
*/
public static function get_speed_score( $input = null ) {
unset( $input );
$history = new Speed_Score_History( home_url() );
$latest = $history->latest();
if ( null === $latest ) {
return array(
'mobile' => null,
'desktop' => null,
'timestamp' => null,
'is_stale' => false,
'has_history' => false,
);
}
// `scores` may be stored as either an associative array or stdClass depending on
// the API response payload. Normalise to array before reading.
$scores = isset( $latest['scores'] ) ? (array) $latest['scores'] : array();
$mobile = isset( $scores['mobile'] ) && is_numeric( $scores['mobile'] ) ? (int) $scores['mobile'] : null;
$desktop = isset( $scores['desktop'] ) && is_numeric( $scores['desktop'] ) ? (int) $scores['desktop'] : null;
return array(
'mobile' => $mobile,
'desktop' => $desktop,
'timestamp' => isset( $latest['timestamp'] ) ? (int) $latest['timestamp'] : null,
'is_stale' => $history->is_stale(),
'has_history' => true,
);
}
/**
* Execute: clear the Boost page cache for the home URL.
*
* @since 4.6.0
*
* @param array|null $input Unused; ability has no inputs.
* @return array|\WP_Error
*/
public static function clear_page_cache( $input = null ) {
unset( $input );
$page_cache = new Module( new Page_Cache() );
if ( ! $page_cache->is_available() || ! $page_cache->is_enabled() ) {
return new \WP_Error(
'jetpack_boost_page_cache_inactive',
__( 'The page_cache module is not active. Enable it first via jetpack-boost/set-module-status with slug="page_cache" and active=true.', 'jetpack-boost' )
);
}
$cache = new Boost_Cache();
// Boost_Cache::delete_recursive() returns void — no useful success/no-op signal
// to surface to the agent. Reporting `cleared: true` after a successful module-active
// gate is the most honest answer we can give.
$cache->delete_recursive( home_url() );
return array(
'cleared' => true,
'message' => __( 'Page cache cleared.', 'jetpack-boost' ),
);
}
}
@@ -0,0 +1,143 @@
<?php
/**
* The admin-specific functionality of the plugin.
*
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Admin;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
use Automattic\Jetpack_Boost\Lib\Analytics;
use Automattic\Jetpack_Boost\Lib\Environment_Change_Detector;
use Automattic\Jetpack_Boost\Lib\Premium_Features;
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
class Admin {
/**
* Menu slug.
*/
const MENU_SLUG = 'jetpack-boost';
public function init( Modules_Setup $modules ) {
Environment_Change_Detector::init();
// Initiate speed scores.
new Speed_Score( $modules->get_ready_active_optimization_modules(), 'boost-plugin' );
add_action( 'init', array( new Analytics(), 'init' ) );
add_filter( 'plugin_action_links_' . JETPACK_BOOST_PLUGIN_BASE, array( $this, 'plugin_page_settings_link' ) );
add_action( 'admin_menu', array( $this, 'handle_admin_menu' ), 1 ); // Akismet uses 4, so we use 1 to ensure both menus are added when only they exist.
}
public function handle_admin_menu() {
/**
* Filters the number of problems shown in the Boost sidebar menu
*
* @param int $count the number of problems shown.
*
* @since 1.0.0
*/
$total_problems = apply_filters( 'jetpack_boost_total_problem_count', 0 );
$menu_label = 'Boost'; // "Boost" is a product name, do not translate.
if ( $total_problems ) {
$menu_label .= sprintf( ' <span class="menu-counter count-%d"><span class="count">%d</span></span>', $total_problems, $total_problems );
}
$page_suffix = Admin_Menu::add_menu(
__( 'Jetpack Boost - Settings', 'jetpack-boost' ),
$menu_label,
'manage_options',
JETPACK_BOOST_SLUG,
array( $this, 'render_settings' ),
2
);
add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
}
/**
* Enqueue scripts and styles for the admin page.
*/
public function admin_init() {
// Clear premium features cache when the plugin settings page is loaded.
Premium_Features::clear_cache();
add_action( 'admin_enqueue_scripts', array( My_Jetpack_Initializer::class, 'enqueue_scripts' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
}
/**
* Register the JavaScript for the admin area.
*
* @since 1.0.0
*/
public function enqueue_scripts() {
/**
* Filters the internal path to the distributed assets used by the plugin
*
* @param string $path the path to the assets
*/
$internal_path = apply_filters( 'jetpack_boost_asset_internal_path', 'app/assets/dist/' );
$admin_js_handle = 'jetpack-boost-admin';
$admin_js_dependencies = array(
'wp-i18n',
'wp-components',
'my_jetpack_main_app',
);
Assets::register_script(
$admin_js_handle,
$internal_path . 'jetpack-boost.js',
JETPACK_BOOST_PATH,
array(
'dependencies' => $admin_js_dependencies,
'in_footer' => true,
'textdomain' => 'jetpack-boost',
'css_path' => $internal_path . 'jetpack-boost.css',
)
);
wp_localize_script(
$admin_js_handle,
'Jetpack_Boost',
( new Config() )->constants()
);
Assets::enqueue_script( $admin_js_handle );
}
/**
* Get settings link.
*
* @param array $links the array of links.
*/
public function plugin_page_settings_link( $links ) {
$settings_link = '<a href="' . admin_url( 'admin.php?page=jetpack-boost' ) . '">' . esc_html__( 'Settings', 'jetpack-boost' ) . '</a>';
array_unshift( $links, $settings_link );
return $links;
}
/**
* Generate the settings page.
*/
public function render_settings() {
wp_localize_script(
'jetpack-boost-admin',
'wpApiSettings',
array(
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
)
);
?>
<div id="jb-admin-settings"></div>
<?php
}
}
@@ -0,0 +1,126 @@
<?php
namespace Automattic\Jetpack_Boost\Admin;
use Automattic\Jetpack\Boost\App\Contracts\Is_Dev_Feature;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack_Boost\Lib\Cache_Compatibility;
use Automattic\Jetpack_Boost\Modules\Features_Index;
/**
* Handle the configuration constants.
*
* This is a global state of Jetpack Boost and passed on to the front-end.
*/
class Config {
public function constants() {
/**
* Filters the internal path to the distributed assets used by the plugin
*
* @param string $path the path to the assets
*
* @since 1.0.0
*/
$internal_path = apply_filters( 'jetpack_boost_asset_internal_path', 'app/assets/dist/' );
$constants = array(
'version' => JETPACK_BOOST_VERSION,
'pluginDirUrl' => untrailingslashit( JETPACK_BOOST_PLUGINS_DIR_URL ),
'assetPath' => plugins_url( $internal_path, JETPACK_BOOST_PATH ),
'canResizeImages' => wp_image_editor_supports( array( 'methods' => array( 'resize' ) ) ),
'site' => array(
'url' => get_home_url(),
'domain' => ( new Status() )->get_site_suffix(),
'online' => self::is_website_public(),
'host' => $this->get_hosting_provider(),
'hasCache' => Cache_Compatibility::has_cache(),
),
'api' => array(
'namespace' => JETPACK_BOOST_REST_NAMESPACE,
'prefix' => JETPACK_BOOST_REST_PREFIX,
),
'postTypes' => (object) $this->get_custom_post_types(),
'developmentFeatures' => self::get_development_features(),
);
/**
* Filters the constants so each module can define extra ones
*
* @param array $constant The array of constants used by the plugin
*
* @since 1.0.0
*/
return apply_filters( 'jetpack_boost_js_constants', $constants );
}
/**
* Get a list of features that are marked as development features.
*
* @return array<string, bool> Slugs of features and their status.
*/
private static function get_development_features() {
$features = Features_Index::get_all_features();
$development_features = array();
foreach ( $features as $feature ) {
if ( is_subclass_of( $feature, Is_Dev_Feature::class ) ) {
$development_features[] = ( new $feature() )->get_slug();
}
}
return $development_features;
}
/**
* Retrieves custom post types.
*
* @return array Associative array of custom post types
* with their labels as keys and names as values.
*/
private static function get_custom_post_types() {
$post_types = get_post_types(
array(
'public' => true,
'_builtin' => false,
),
false
);
unset( $post_types['attachment'] );
$post_types = array_filter( $post_types, 'is_post_type_viewable' );
return wp_list_pluck( $post_types, 'label', 'name' );
}
/**
* Retrieves the hosting provider.
* We're only interested in 'atomic' or 'woa' for now.
*
* @since 3.10.0
*
* @return string The hosting provider.
*/
public static function get_hosting_provider() {
$host = new Host();
if ( $host->is_woa_site() ) {
return 'woa';
}
if ( $host->is_atomic_platform() ) {
return 'atomic';
}
return 'other';
}
/**
* Checks if the website is publicly accessible.
*
* @return bool True if the website is public, false otherwise.
*/
public static function is_website_public() {
return ! ( new Status() )->is_offline_mode() && ! ( new Status() )->is_private_site();
}
}
@@ -0,0 +1,106 @@
<?php
/**
* Admin notice base class. Override this to implement each admin notice Jetpack Boost may show.
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Admin;
use Automattic\Jetpack\Boost_Core\Lib\Transient;
/**
* Admin notice for letting users know they need to regenerate their Critical CSS.
*/
class Regenerate_Admin_Notice {
private static $dismissal_key = 'dismiss-critical-css-notice';
public static function enable() {
Transient::set( 'regenerate_admin_notice', true );
}
public static function dismiss() {
Transient::delete( 'regenerate_admin_notice' );
}
public static function is_enabled() {
return Transient::get( 'regenerate_admin_notice', false );
}
/**
* Helper method to generate a dismissal link for this message.
*/
private static function get_dismiss_url() {
return add_query_arg(
array(
self::$dismissal_key => '',
'nonce' => wp_create_nonce( 'jb_dismiss_notice' ),
)
);
}
public static function maybe_handle_dismissal() {
if ( ! is_admin()
|| ! current_user_can( 'manage_options' )
|| ! isset( $_GET[ self::$dismissal_key ] ) || ! isset( $_GET['nonce'] )
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'jb_dismiss_notice' )
) {
return;
}
// Dismiss the notice that shows up for major changes.
static::dismiss();
wp_safe_redirect( remove_query_arg( array( self::$dismissal_key, 'nonce' ) ) );
}
public static function init() {
add_action( 'admin_notices', array( static::class, 'maybe_render' ) );
if ( static::is_enabled() ) {
static::maybe_handle_dismissal();
}
}
public static function maybe_render() {
// We're not actually using the GET parameter here, it's only used to find out what page we're on.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$on_settings_page = is_admin() && isset( $_GET['page'] ) && Admin::MENU_SLUG === $_GET['page'];
if ( $on_settings_page || ! current_user_can( 'manage_options' ) ) {
return;
}
if ( static::is_enabled() ) {
static::render();
}
}
public static function render() {
?>
<div id="jetpack-boost-notice-critical-css-regenerate" class="notice notice-warning is-dismissible">
<h3>
<?php esc_html_e( 'Jetpack Boost - Action Required', 'jetpack-boost' ); ?>
</h3>
<p>
<?php esc_html_e( 'The Critical CSS generated by Jetpack Boost was cleared due to a change in the site theme.', 'jetpack-boost' ); ?>
</p>
<p>
<?php esc_html_e( 'Please head to the Jetpack Boost dashboard to regenerate your Critical CSS.', 'jetpack-boost' ); ?>
</p>
<p>
<a class='button button-primary' href="<?php echo esc_url( admin_url( 'admin.php?page=' . Admin::MENU_SLUG ) ); ?>">
<strong>
<?php esc_html_e( 'Go to Jetpack Boost', 'jetpack-boost' ); ?>
</strong>
</a>
<a class="jb-dismiss-notice" href="<?php echo esc_url( static::get_dismiss_url() ); ?>">
<strong>
<?php esc_html_e( 'Dismiss notice', 'jetpack-boost' ); ?>
</strong>
</a>
</p>
</div>
<?php
}
}
@@ -0,0 +1,40 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 ); // Exit if accessed directly
}
?>
<main class="jp-plugin-deactivation__dialog__content">
<h1><?php esc_html_e( 'Are you sure you want to deactivate?', 'jetpack-boost' ); ?></h1>
<p class="big"><?php esc_html_e( 'Before you go...', 'jetpack-boost' ); ?></p>
<p><?php esc_html_e( "Thank you for trying Jetpack Boost. Before you go, we'd really love your feedback on our plugin.", 'jetpack-boost' ); ?></p>
<p><?php esc_html_e( "Just temporarily deactivating or don't fancy giving feedback? No problem.", 'jetpack-boost' ); ?></p>
</main>
<footer class="jp-plugin-deactivation__dialog__actions">
<button
type="button"
class="jp-plugin-deactivation__button"
data-jp-plugin-deactivation-action="close"
><?php esc_html_e( 'Cancel', 'jetpack-boost' ); ?></button>
<button
type="button"
class="jp-plugin-deactivation__button jp-plugin-deactivation__button--outline jp-plugin-deactivation__button--destructive"
data-jp-plugin-deactivation-action="deactivate"
><?php esc_html_e( 'Just Deactivate', 'jetpack-boost' ); ?></button>
<!-- Using an anchor instead of a button for the Deactivate & Give Feedback may trigger browser's popup blocker as it is navigating to two URLs at once. -->
<button
type="button"
class="jp-plugin-deactivation__button jp-plugin-deactivation__button--destructive"
data-jp-plugin-deactivation-action="deactivate"
onclick="window.open( 'https://jetpack.com/redirect/?source=jetpack-boost-deactivation-feedback', '_blank' )"
><?php esc_html_e( 'Deactivate & Give Feedback', 'jetpack-boost' ); ?></button>
</footer>
<style>
#jp-plugin-deactivation-jetpack-boost p.big {
font-size: 1.7em;
}
#jp-plugin-deactivation-jetpack-boost footer {
text-align: center;
}
</style>
@@ -0,0 +1,6 @@
<?php
/**
* Empty file.
*/
// Silence is golden.
Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

@@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_2115_8307" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="5" width="18" height="14">
<path d="M8.75686 15.9001L4.57746 11.7L3.18433 13.1L8.75686 18.7001L20.698 6.70005L19.3049 5.30005L8.75686 15.9001Z" fill="white"/>
</mask>
<g mask="url(#mask0_2115_8307)">
<rect width="23.8823" height="24" fill="#069E08"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 446 B

@@ -0,0 +1,41 @@
<svg width="363" height="600" viewBox="0 0 363 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_2115_8320" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="363" height="666">
<path d="M0 0H359C361.209 0 363 1.79086 363 4V661.075C363 665.235 357.44 666.631 355.475 662.965L0 0Z" fill="#F9F9F6"/>
</mask>
<g mask="url(#mask0_2115_8320)">
<g filter="url(#filter0_f_2115_8320)">
<ellipse cx="290.5" cy="288.048" rx="151.5" ry="143.048" fill="#069E08" fill-opacity="0.15"/>
</g>
<g filter="url(#filter1_f_2115_8320)">
<ellipse cx="256.5" cy="132.5" rx="131.5" ry="124.5" fill="#F5E6B3"/>
</g>
<g opacity="0.7" filter="url(#filter2_f_2115_8320)">
<ellipse cx="173" cy="-64" rx="213" ry="201" fill="#CED9F2"/>
</g>
<g opacity="0.9" filter="url(#filter3_f_2115_8320)">
<ellipse cx="4.5" cy="67" rx="100.5" ry="95" fill="white"/>
</g>
</g>
<defs>
<filter id="filter0_f_2115_8320" x="55" y="61" width="471" height="454.096" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="42" result="effect1_foregroundBlur_2115_8320"/>
</filter>
<filter id="filter1_f_2115_8320" x="-29" y="-146" width="571" height="557" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="77" result="effect1_foregroundBlur_2115_8320"/>
</filter>
<filter id="filter2_f_2115_8320" x="-134" y="-359" width="614" height="590" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="47" result="effect1_foregroundBlur_2115_8320"/>
</filter>
<filter id="filter3_f_2115_8320" x="-190" y="-122" width="389" height="378" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="47" result="effect1_foregroundBlur_2115_8320"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,17 @@
<svg width="380" height="183" viewBox="0 0 380 183" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.75" filter="url(#filter0_f_5953_11924)">
<path d="M360 -60C360 7.93102 304.931 63 237 63C169.069 63 114 7.93102 114 -60C114 -127.931 169.069 -183 237 -183C304.931 -183 360 -127.931 360 -60Z" fill="url(#paint0_linear_5953_11924)"/>
</g>
<defs>
<filter id="filter0_f_5953_11924" x="-6" y="-303" width="486" height="486" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="60" result="effect1_foregroundBlur_5953_11924"/>
</filter>
<linearGradient id="paint0_linear_5953_11924" x1="360" y1="-60" x2="114" y2="-60" gradientUnits="userSpaceOnUse">
<stop stop-color="#48FF50"/>
<stop offset="0.471846" stop-color="#108642"/>
<stop offset="1" stop-color="#2E38FA"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 990 B

@@ -0,0 +1 @@
<?php return array('dependencies' => array('jetpack-connection', 'jetpack-script-data', 'react', 'react-dom', 'react-jsx-runtime', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '91f80d95ecdf13345634');
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,52 @@
/*! react */
/*! uplot */
/*! uplot-wrappers-common */
/*!*************************!*\
!*** ./common/index.ts ***!
\*************************/
/*!*******************************!*\
!*** ./react/uplot-react.tsx ***!
\*******************************/
/*!**************************************************************************************!*\
!*** external {"amd":"react","commonjs":"react","commonjs2":"react","root":"React"} ***!
\**************************************************************************************/
/*!**************************************************************************************!*\
!*** external {"amd":"uplot","commonjs":"uplot","commonjs2":"uplot","root":"uPlot"} ***!
\**************************************************************************************/
/**
* @license React
* use-sync-external-store-shim.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* use-sync-external-store-shim/with-selector.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* react-router v7.15.1
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

@@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_2115_8307" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="5" width="18" height="14">
<path d="M8.75686 15.9001L4.57746 11.7L3.18433 13.1L8.75686 18.7001L20.698 6.70005L19.3049 5.30005L8.75686 15.9001Z" fill="white"/>
</mask>
<g mask="url(#mask0_2115_8307)">
<rect width="23.8823" height="24" fill="#069E08"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 446 B

@@ -0,0 +1,41 @@
<svg width="363" height="600" viewBox="0 0 363 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_2115_8320" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="363" height="666">
<path d="M0 0H359C361.209 0 363 1.79086 363 4V661.075C363 665.235 357.44 666.631 355.475 662.965L0 0Z" fill="#F9F9F6"/>
</mask>
<g mask="url(#mask0_2115_8320)">
<g filter="url(#filter0_f_2115_8320)">
<ellipse cx="290.5" cy="288.048" rx="151.5" ry="143.048" fill="#069E08" fill-opacity="0.15"/>
</g>
<g filter="url(#filter1_f_2115_8320)">
<ellipse cx="256.5" cy="132.5" rx="131.5" ry="124.5" fill="#F5E6B3"/>
</g>
<g opacity="0.7" filter="url(#filter2_f_2115_8320)">
<ellipse cx="173" cy="-64" rx="213" ry="201" fill="#CED9F2"/>
</g>
<g opacity="0.9" filter="url(#filter3_f_2115_8320)">
<ellipse cx="4.5" cy="67" rx="100.5" ry="95" fill="white"/>
</g>
</g>
<defs>
<filter id="filter0_f_2115_8320" x="55" y="61" width="471" height="454.096" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="42" result="effect1_foregroundBlur_2115_8320"/>
</filter>
<filter id="filter1_f_2115_8320" x="-29" y="-146" width="571" height="557" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="77" result="effect1_foregroundBlur_2115_8320"/>
</filter>
<filter id="filter2_f_2115_8320" x="-134" y="-359" width="614" height="590" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="47" result="effect1_foregroundBlur_2115_8320"/>
</filter>
<filter id="filter3_f_2115_8320" x="-190" y="-122" width="389" height="378" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="47" result="effect1_foregroundBlur_2115_8320"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,8 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_2997_990" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="5" y="5" width="15" height="14">
<path d="M19.5 6.41L18.09 5L12.5 10.59L6.91 5L5.5 6.41L11.09 12L5.5 17.59L6.91 19L12.5 13.41L18.09 19L19.5 17.59L13.91 12L19.5 6.41Z" fill="white"/>
</mask>
<g mask="url(#mask0_2997_990)">
<rect x="0.5" width="24" height="24" fill="#E65054"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 464 B

@@ -0,0 +1,14 @@
<svg width="513" height="400" viewBox="0 0 513 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_f_2285_9948)">
<path opacity="0.7" d="M505.462 -126.766C530.541 -119.966 549.288 -98.8259 572.421 -86.2516C600.401 -71.0427 638.717 -72.4006 656.759 -44.5276C674.393 -17.2838 665.913 20.8197 663.238 54.0552C660.751 84.9436 650.487 113.365 641.675 142.841C632.059 175.007 630.926 212.795 608.725 236.396C586.471 260.054 552.462 263.253 522.033 270.029C492.682 276.566 463.078 280.134 433.434 275.432C402.994 270.605 372.263 262.269 347.494 242.349C322.667 222.382 304.977 193.366 292.881 162.392C281.191 132.458 280.938 99.6507 279.082 67.1702C277.181 33.9036 268.485 -1.53342 281.799 -31.5256C295.088 -61.4623 324.768 -77.8611 350.985 -94.8026C374.635 -110.085 400.357 -119.998 427.357 -125.585C453.264 -130.946 479.871 -133.705 505.462 -126.766Z" fill="#F5E6B3"/>
<path opacity="0.7" d="M274.015 156.173C295.086 169.132 307.167 193.384 321.975 214.247C336.016 234.031 352.197 252.418 359.227 276.233C366.356 300.379 368.273 326.743 362.435 351.298C356.74 375.254 340.991 394.185 326.63 413.345C312.893 431.674 295.965 445.599 279.86 461.47C259.756 481.283 245.911 513.006 219.385 519.036C193.402 524.943 169.073 503.68 144.71 492.194C120.547 480.803 90.952 475.33 76.2142 451.541C61.3709 427.582 68.762 395.334 66.8348 366.495C65.2206 342.34 64.3226 318.928 65.737 294.759C67.2819 268.358 62.3709 239.32 75.5755 217.13C88.7931 194.918 115.438 188.59 136.618 175.644C157.993 162.579 177.206 143.602 201.505 140.158C226.271 136.649 252.261 142.795 274.015 156.173Z" fill="#CED9F2"/>
<path d="M549.377 206.234C594.702 223.625 610.9 283.303 639.95 325.028C666.696 363.443 704.795 395.077 713.74 442.307C722.685 489.54 697.844 534.7 688.591 581.863C678.121 635.231 687.426 696.612 655.585 738.726C623.071 781.732 567.997 793.298 518.428 806.93C467.16 821.029 413.471 836.097 362.916 819.213C311.973 802.2 266.336 763.635 239.174 713.667C213.549 666.525 223.51 608.177 218.559 553.647C214.133 504.904 202.281 457.338 211.548 409.411C221.467 358.118 235.699 302.891 273.35 270.285C310.684 237.954 363.969 247.851 410.604 237.03C457.576 226.13 504.076 188.853 549.377 206.234Z" fill="#069E08" fill-opacity="0.11"/>
</g>
<defs>
<filter id="filter0_f_2285_9948" x="-19.4263" y="-215.362" width="819.537" height="1126.51" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="42.2297" result="effect1_foregroundBlur_2285_9948"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,17 @@
<svg class="jitm-jp-logo" viewBox="0 0 176 32" xmlns="http://www.w3.org/2000/svg">
<path d="M16 32C24.8365 32 32 24.8362 32 15.9994C32 7.16259 24.8365 0 16 0C7.16351 0 0 7.16377 0 16.0006C0 24.8374 7.16351 32 16 32Z" fill="#069E08"/>
<path d="m16.794 13.313v15.511l8-15.511h-8z" fill="#fff"/>
<path d="m15.176 18.657v-15.482l-7.9692 15.482h7.9692z" fill="#fff"/>
<path d="m41.711 27.015c-0.4629-0.7159-0.8936-1.4307-1.3254-2.1152 2.2813-1.4004 3.0517-2.5196 3.0517-4.636v-12.227h-2.6821v-2.3325h5.7039v13.938c0 3.547-1.0169 5.5378-4.7481 7.3729z"/>
<path d="m65.603 18.677c0 1.1819 0.8326 1.3063 1.3876 1.3063 0.5551 0 1.3566-0.1871 1.9727-0.3731v2.178c-0.8637 0.2801-1.7573 0.4974-2.9907 0.4974-1.4798 0-3.206-0.5602-3.206-3.174v-6.4095h-1.5719v-2.2093h1.5719v-3.2657h2.8364v3.2669h3.5756v2.2093h-3.5756v5.9737z"/>
<path d="m71.523 28.104v-17.641h2.7132v1.0576c1.079-0.8402 2.2813-1.369 3.7611-1.369 2.5588 0 4.5937 1.8049 4.5937 5.6935 0 3.8585-2.2191 6.4095-5.8881 6.4095-0.8936 0-1.603-0.1243-2.3435-0.2801v6.098h-2.8364v0.0314zm5.7338-15.587c-0.8326 0-1.8805 0.4045-2.8675 1.2761v6.0051c0.6161 0.1243 1.2645 0.2173 2.127 0.2173 2.0038 0 3.145-1.2761 3.145-3.9515 0-2.458-0.8326-3.547-2.4045-3.547z"/>
<path d="m93.748 22.007h-2.651v-1.2761h-0.0622c-0.9247 0.716-2.066 1.4935-3.7611 1.4935-1.4798 0-3.0828-1.089-3.0828-3.2983 0-2.9555 2.4966-3.5157 4.254-3.7644l2.4966-0.3416v-0.3417c0-1.5562-0.6161-2.0536-2.066-2.0536-0.7094 0-2.3734 0.2173-3.73 0.7775l-0.2464-2.3023c1.2334-0.4358 2.9285-0.7461 4.3473-0.7461 2.7741 0 4.5626 1.1204 4.5626 4.4489v7.4042h-0.061zm-2.8364-5.382-2.3435 0.373c-0.7094 0.093-1.4487 0.5288-1.4487 1.5864 0 0.9332 0.524 1.462 1.2944 1.462 0.8326 0 1.7262-0.4974 2.4966-1.0576v-2.3638h0.0012z"/>
<path d="m105.46 21.633c-1.171 0.4044-2.219 0.6531-3.546 0.6531-4.2537 0-5.95-2.458-5.95-6.0352 0-3.7643 2.3435-6.098 6.135-6.098 1.417 0 2.281 0.2487 3.237 0.5602v2.4266c-0.833-0.3115-2.035-0.6531-3.206-0.6531-1.727 0-3.2064 0.9332-3.2064 3.6085 0 2.9555 1.4794 3.8585 3.3604 3.8585 0.894 0 1.88-0.1871 3.206-0.7159v2.3953h-0.03z"/>
<path d="m110.83 15.442c0.246-0.2801 0.432-0.5602 4.008-4.9474h3.698l-4.624 5.4762 5.055 6.0666h-3.699l-4.408-5.4762v5.4762h-2.835v-16.333h2.836v9.7379h-0.031z"/>
<path d="m59.066 21.632c-1.4798 0.4672-2.7443 0.6531-4.2241 0.6531-3.6378 0-5.888-1.8351-5.888-6.1294 0-3.1425 1.9116-6.005 5.5794-6.005 3.6379 0 4.9023 2.551 4.9023 4.9776 0 0.8089-0.0622 1.2447-0.0921 1.7107h-7.3367c0.0622 2.5197 1.4798 3.1112 3.6067 3.1112 1.1712 0 2.2191-0.2801 3.4226-0.7159v2.3953h0.0299v0.0024zm-2.5888-6.6896c0-1.4005-0.4629-2.6138-1.9726-2.6138-1.4176 0-2.2813 1.0262-2.4667 2.6138h4.4393z"/>
<path d="m131.97 10.894c0 0.6585-0.146 1.2617-0.439 1.8106-0.293 0.549-0.777 1.0038-1.452 1.3679v0.0454c0.375 0.1284 0.718 0.2933 1.03 0.4936 0.311 0.2003 0.577 0.4383 0.799 0.7149 0.22 0.2767 0.394 0.5888 0.518 0.9363 0.124 0.3486 0.185 0.7382 0.185 1.1687 0 0.3708-0.058 0.7681-0.174 1.192-0.117 0.4238-0.321 0.8344-0.614 1.2317s-0.683 0.7626-1.17 1.0946c-0.487 0.3331-1.103 0.5976-1.846 0.7946-0.188 0.0454-0.392 0.0874-0.614 0.1251-0.221 0.0376-0.467 0.0697-0.737 0.0962-0.27 0.0266-0.571 0.0454-0.9 0.0565-0.331 0.0111-0.702 0.0166-1.114 0.0166h-4.378v-15.184h5.234c1.117 0 2.018 0.114 2.701 0.34087 0.51 0.16601 0.953 0.38403 1.328 0.65297 0.375 0.26893 0.685 0.56774 0.929 0.89644 0.243 0.3287 0.423 0.67731 0.54 1.0436 0.116 0.36628 0.174 0.73598 0.174 1.1056zm-5.661 2.2135c0.749 0 1.309-0.0753 1.677-0.2269 0.413-0.1738 0.707-0.4139 0.883-0.7205 0.176-0.3065 0.265-0.6529 0.265-1.0381 0-0.4006-0.101-0.7581-0.304-1.0724-0.203-0.31431-0.548-0.54672-1.035-0.69834-0.203-0.06087-0.439-0.10403-0.709-0.13059s-0.604-0.03984-1.002-0.03984h-2.262v3.9267h2.487zm-2.487 2.3263v4.2786h1.801c0.479 0 0.892-0.0133 1.238-0.0399 0.344-0.0265 0.641-0.0697 0.889-0.1306 0.383-0.083 0.701-0.1948 0.957-0.3353 0.255-0.1395 0.461-0.2988 0.619-0.477 0.158-0.1771 0.27-0.3741 0.338-0.5899s0.101-0.4438 0.101-0.6861c0-0.4239-0.116-0.7947-0.349-1.1123s-0.596-0.5556-1.092-0.7149c-0.217-0.0687-0.473-0.1174-0.765-0.1472-0.293-0.0299-0.667-0.0454-1.125-0.0454h-2.612z" fill="#646970"/>
<path d="m145.21 16.368c0 0.8697-0.13 1.6634-0.391 2.3813-0.261 0.7178-0.635 1.3318-1.121 1.8432-0.487 0.5113-1.075 0.9065-1.765 1.1845-0.689 0.2791-1.459 0.4175-2.31 0.4175-0.873 0-1.657-0.1396-2.351-0.4175-0.694-0.278-1.281-0.6732-1.764-1.1845-0.484-0.5114-0.854-1.1254-1.109-1.8432-0.257-0.7168-0.386-1.5116-0.386-2.3813s0.13-1.6634 0.391-2.3813c0.261-0.7167 0.635-1.3319 1.121-1.8432 0.487-0.5113 1.076-0.9065 1.765-1.1845 0.689-0.2791 1.46-0.4175 2.31-0.4175 0.874 0 1.657 0.1396 2.351 0.4175 0.694 0.2791 1.282 0.6732 1.765 1.1845s0.852 1.1254 1.108 1.8432c0.257 0.7179 0.386 1.5116 0.386 2.3813zm-2.816 0c0-0.5872-0.07-1.1064-0.207-1.5574-0.138-0.4499-0.33-0.8284-0.575-1.1331-0.246-0.3048-0.541-0.5337-0.886-0.6866-0.345-0.153-0.721-0.2289-1.127-0.2289-0.399 0-0.768 0.0759-1.108 0.2289-0.341 0.1529-0.635 0.3818-0.879 0.6866-0.246 0.3059-0.438 0.6832-0.575 1.1331-0.138 0.4499-0.208 0.9691-0.208 1.5574 0 0.5872 0.07 1.1063 0.208 1.5574 0.137 0.4499 0.331 0.8261 0.58 1.1275 0.249 0.3015 0.544 0.5303 0.885 0.6866s0.714 0.2345 1.121 0.2345c0.406 0 0.778-0.0782 1.115-0.2345 0.338-0.1563 0.628-0.3851 0.874-0.6866 0.245-0.3014 0.437-0.6776 0.575-1.1275 0.137-0.4511 0.207-0.9702 0.207-1.5574z" fill="#646970"/>
<path d="m157.45 16.368c0 0.8697-0.126 1.6634-0.379 2.3813-0.253 0.7178-0.615 1.3318-1.086 1.8432-0.471 0.5113-1.042 0.9065-1.71 1.1845-0.667 0.2791-1.413 0.4175-2.238 0.4175-0.846 0-1.605-0.1396-2.277-0.4175-0.672-0.278-1.241-0.6732-1.709-1.1845-0.468-0.5114-0.827-1.1254-1.074-1.8432-0.249-0.7168-0.374-1.5116-0.374-2.3813s0.126-1.6634 0.379-2.3813c0.252-0.7167 0.615-1.3319 1.086-1.8432s1.041-0.9065 1.709-1.1845c0.668-0.2791 1.414-0.4175 2.239-0.4175 0.846 0 1.605 0.1396 2.277 0.4175 0.672 0.2791 1.241 0.6732 1.709 1.1845s0.826 1.1254 1.074 1.8432c0.249 0.7179 0.374 1.5116 0.374 2.3813zm-2.729 0c0-0.5872-0.068-1.1064-0.201-1.5574-0.134-0.4499-0.319-0.8284-0.557-1.1331-0.238-0.3048-0.524-0.5337-0.858-0.6866-0.335-0.153-0.699-0.2289-1.092-0.2289-0.386 0-0.744 0.0759-1.074 0.2289-0.33 0.1529-0.614 0.3818-0.851 0.6866-0.238 0.3059-0.424 0.6832-0.557 1.1331-0.134 0.4499-0.201 0.9691-0.201 1.5574 0 0.5872 0.067 1.1063 0.201 1.5574 0.133 0.4499 0.321 0.8261 0.562 1.1275 0.241 0.3015 0.527 0.5303 0.857 0.6866s0.692 0.2345 1.086 0.2345c0.393 0 0.754-0.0782 1.081-0.2345s0.608-0.3851 0.846-0.6866c0.238-0.3014 0.423-0.6776 0.557-1.1275 0.135-0.4511 0.201-0.9702 0.201-1.5574z" fill="#646970"/>
<path d="m164.24 18.692c0-0.2367-0.067-0.4309-0.199-0.5839-0.133-0.1529-0.309-0.2802-0.529-0.384-0.219-0.1027-0.471-0.192-0.756-0.269-0.283-0.0759-0.581-0.1529-0.892-0.2288-0.394-0.1072-0.772-0.2311-1.136-0.3718-0.364-0.1406-0.684-0.3259-0.96-0.5548-0.277-0.2288-0.497-0.5168-0.66-0.864-0.163-0.3471-0.245-0.7724-0.245-1.2759 0-0.6184 0.11-1.1542 0.33-1.6086 0.219-0.4543 0.517-0.8294 0.892-1.1274 0.375-0.2981 0.81-0.5191 1.307-0.6642s1.021-0.2177 1.574-0.2177c0.674 0 1.307 0.0558 1.898 0.1663s1.125 0.2534 1.603 0.4287v2.2102c-0.251-0.0837-0.514-0.1618-0.791-0.2344-0.277-0.0725-0.557-0.135-0.841-0.1886s-0.567-0.0971-0.847-0.1318c-0.28-0.0346-0.545-0.0513-0.796-0.0513-0.318 0-0.587 0.0301-0.807 0.0915-0.219 0.0614-0.398 0.1429-0.534 0.2456-0.137 0.1027-0.235 0.221-0.296 0.355s-0.091 0.2724-0.091 0.4175c0 0.2523 0.066 0.4577 0.198 0.6184 0.133 0.1608 0.316 0.2903 0.551 0.3896 0.235 0.0994 0.481 0.1808 0.74 0.2467 0.257 0.0648 0.507 0.1284 0.75 0.1887 0.379 0.0915 0.757 0.202 1.136 0.3315s0.72 0.3114 1.023 0.5436c0.303 0.2333 0.551 0.5347 0.745 0.9042 0.193 0.3706 0.29 0.8417 0.29 1.4133 0 0.6262-0.118 1.1709-0.353 1.6364s-0.563 0.8529-0.983 1.1621-0.925 0.5381-1.512 0.6865c-0.588 0.1485-1.233 0.2233-1.938 0.2233-0.697 0-1.324-0.0558-1.881-0.1663-0.556-0.1106-1.017-0.2501-1.381-0.4175v-2.1857c0.591 0.221 1.136 0.3717 1.637 0.4521 0.5 0.0803 0.967 0.1205 1.398 0.1205 0.334 0 0.644-0.0245 0.932-0.0748 0.289-0.0491 0.536-0.1272 0.745-0.2344s0.374-0.2422 0.495-0.4063c0.124-0.1641 0.184-0.3617 0.184-0.5905z" fill="#646970"/>
<path d="m176 21.896c-0.302 0.0916-0.661 0.1641-1.079 0.2177-0.417 0.0536-0.808 0.0804-1.169 0.0804-0.92 0-1.671-0.1507-2.256-0.4521-0.584-0.3014-0.996-0.7344-1.237-1.2992-0.173-0.4041-0.26-0.9532-0.26-1.6486v-5.745h-2.046v-2.2323h2.046v-3.1008h2.678v3.1019h3.165v2.2323h-3.165v5.3912c0 0.4275 0.064 0.7445 0.192 0.9498 0.226 0.3516 0.674 0.5269 1.345 0.5269 0.308 0 0.616-0.0246 0.921-0.0748 0.305-0.0491 0.593-0.1128 0.864-0.1887v2.2413h1e-3z" fill="#646970"/>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="#008710"
xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_3234_4810" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="2" y="1"
width="13" height="13">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M13.724 3.724L12.276 2.276C11.7553 1.75533 10.9113 1.75533 10.3907 2.276L9.33333 3.33333L12.6667 6.66666L13.724 5.60933C14.2447 5.08866 14.2447 4.24466 13.724 3.724ZM8.66667 4L12 7.33333L5.662 13.6713C5.20467 13.214 5.20267 12.4753 5.654 12.0147L5.652 12.0127C5.19133 12.464 4.45267 12.462 3.99533 12.0047C3.544 11.5533 3.538 10.83 3.97133 10.368L3.966 10.3627C3.504 10.7953 2.78 10.7893 2.32933 10.3387L8.66667 4ZM2 14V12C3.10467 12 4 12.8953 4 14H2Z"
fill="white" />
</mask>
<g mask="url(#mask0_3234_4810)">
<rect width="16" height="16" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,463 @@
<?php
/**
* The file that defines the core plugin class
*
* A class definition that includes attributes and functions used across both the
* public-facing side of the site and the admin area.
*
* @link https://automattic.com
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost;
use Automattic\Jetpack\Boost_Core\Lib\Transient;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
use Automattic\Jetpack\Config as Jetpack_Config;
use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
use Automattic\Jetpack\Plugin_Deactivation\Deactivation_Handler;
use Automattic\Jetpack_Boost\Admin\Admin;
use Automattic\Jetpack_Boost\Admin\Regenerate_Admin_Notice;
use Automattic\Jetpack_Boost\Data_Sync\Getting_Started_Entry;
use Automattic\Jetpack_Boost\Lib\Analytics;
use Automattic\Jetpack_Boost\Lib\CLI;
use Automattic\Jetpack_Boost\Lib\Connection;
use Automattic\Jetpack_Boost\Lib\Cornerstone\Cornerstone_Pages;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_Storage;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Generator;
use Automattic\Jetpack_Boost\Lib\Setup;
use Automattic\Jetpack_Boost\Lib\Site_Health;
use Automattic\Jetpack_Boost\Lib\Status;
use Automattic\Jetpack_Boost\Lib\Super_Cache_Tracking;
use Automattic\Jetpack_Boost\Modules\Module;
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
use Automattic\Jetpack_Boost\Modules\Optimizations\Lcp\LCP_State;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Cache_Preload;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache_Setup;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Settings;
use Automattic\Jetpack_Boost\REST_API\Endpoints\List_Cornerstone_Pages;
use Automattic\Jetpack_Boost\REST_API\Endpoints\List_LCP_Analysis;
use Automattic\Jetpack_Boost\REST_API\Endpoints\List_Site_Urls;
use Automattic\Jetpack_Boost\REST_API\Endpoints\List_Source_Providers;
use Automattic\Jetpack_Boost\REST_API\REST_API;
/**
* The core plugin class.
*
* This is used to define internationalization, admin-specific hooks, and
* public-facing site hooks.
*
* Also maintains the unique identifier of this plugin as well as the current
* version of the plugin.
*
* @since 1.0.0
* @author Automattic <support@jetpack.com>
*
* @phan-constructor-used-for-side-effects
*/
class Jetpack_Boost {
/**
* The unique identifier of this plugin.
*
* @since 1.0.0
* @var string The string used to uniquely identify this plugin.
*/
private $plugin_name;
/**
* The current version of the plugin.
*
* @since 1.0.0
* @var string The current version of the plugin.
*/
private $version;
/**
* The Jetpack Boost Connection manager instance.
*
* @since 1.0.0
* @access public
* @var Connection The Jetpack Boost Connection manager instance.
*/
public $connection;
/**
* Define the core functionality of the plugin.
*
* Set the plugin name and the plugin version that can be used throughout the plugin.
* Load the dependencies, define the locale, and set the hooks for the admin area and
* the public-facing side of the site.
*
* @since 1.0.0
*/
public function __construct() {
$this->version = JETPACK_BOOST_VERSION;
$this->plugin_name = 'jetpack-boost';
$this->connection = new Connection();
$this->connection->init();
$this->register_deactivation_hook();
if ( defined( 'WP_CLI' ) && WP_CLI ) {
$cli_instance = new CLI( $this );
\WP_CLI::add_command( 'jetpack-boost', $cli_instance );
}
$modules_setup = new Modules_Setup();
Setup::add( $modules_setup );
$cornerstone_pages = new Cornerstone_Pages();
Setup::add( $cornerstone_pages );
// Initialize Jetpack packages (connection, sync, identity crisis).
$this->init_jetpack_config();
// Initialize the Admin experience.
$this->init_admin( $modules_setup );
add_action( 'admin_init', array( $this, 'schedule_version_change' ) );
add_action( 'init', array( $this, 'init_textdomain' ) );
add_action( 'init', array( $this, 'setup_cron_schedules' ) );
add_action( 'jetpack_boost_environment_changed', array( $this, 'handle_environment_change' ), 10, 2 );
add_action( 'jetpack_boost_handle_version_change_cron', array( $this, 'handle_version_change' ) );
add_action( 'jetpack_boost_general_cleanup', array( Transient::class, 'delete_expired' ) );
// Fired when plugin ready.
do_action( 'jetpack_boost_loaded', $this );
My_Jetpack_Initializer::init();
Deactivation_Handler::init( $this->plugin_name, __DIR__ . '/admin/deactivation-dialog.php' );
// Register the core Image CDN hooks.
Image_CDN_Core::setup();
// Setup Site Health panel functionality.
Site_Health::init();
Super_Cache_Tracking::setup();
// Boost abilities (WP Abilities API). Gated by `jetpack_wp_abilities_enabled`;
// priority 20 leaves room for default-priority filter registrations to land first.
add_action( 'plugins_loaded', array( '\Automattic\Jetpack_Boost\Abilities\Boost_Abilities', 'init' ), 20 );
}
/**
* Register deactivation hook.
*/
private function register_deactivation_hook() {
$plugin_file = trailingslashit( dirname( __DIR__ ) ) . 'jetpack-boost.php';
register_deactivation_hook( $plugin_file, array( $this, 'deactivate' ) );
}
public function schedule_version_change() {
$version = get_option( 'jetpack_boost_version' );
if ( $version === JETPACK_BOOST_VERSION ) {
return;
}
update_option( 'jetpack_boost_version', JETPACK_BOOST_VERSION );
// Schedule the cron event to handle the version change. This ensures the previous version's handle is always flushed.
if ( ! wp_next_scheduled( 'jetpack_boost_handle_version_change_cron' ) ) {
wp_schedule_single_event( time() + 2, 'jetpack_boost_handle_version_change_cron' );
}
}
public function handle_version_change() {
// Remove this option to prevent the notice from showing up.
delete_site_option( 'jetpack_boost_static_minification' );
// Add upgrade check for Cornerstone Pages.
$pages = jetpack_boost_ds_get( 'cornerstone_pages_list' );
if ( is_array( $pages ) && in_array( home_url( '' ), $pages, true ) ) {
// Remove homepage (empty string) from the cornerstone pages list.
$pages = array_filter(
$pages,
function ( $page ) {
return $page !== home_url( '' );
}
);
jetpack_boost_ds_set( 'cornerstone_pages_list', $pages );
}
if ( jetpack_boost_minify_is_enabled() ) {
// We need to clear Minify scheduled events to ensure the latest scheduled jobs are only scheduled irrespective of scheduled arguments.
jetpack_boost_minify_clear_scheduled_events();
jetpack_boost_minify_activation();
}
$page_cache = new Module( new Page_Cache() );
if ( $page_cache->is_enabled() ) {
// Schedule the cronjob to preload the cache for Cornerstone Pages.
( new Cache_Preload() )->schedule_cornerstone_cronjob();
}
// Setup a cleanup job
if ( ! wp_next_scheduled( 'jetpack_boost_general_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'jetpack_boost_general_cleanup' );
}
}
/**
* Adds the custom cron intervals to the schedules list.
*/
public function setup_cron_schedules() {
add_filter( 'cron_schedules', array( $this, 'custom_cron_intervals' ) );
}
/**
* Adds custom cron intervals used by Boost.
*
* @param array $schedules The existing cron schedules.
* @return array The modified cron schedules.
*
* @since 3.12.0
*/
public function custom_cron_intervals( $schedules ) {
// The "twicehourly" name maintains the same pattern as the default "twicedaily" name.
if ( ! isset( $schedules['twicehourly'] ) ) {
$schedules['twicehourly'] = array(
'interval' => 30 * MINUTE_IN_SECONDS,
'display' => __( 'Twice Hourly', 'jetpack-boost' ),
);
}
return $schedules;
}
/**
* Add query args used by Boost to a list of allowed query args.
*
* @param array $allowed_query_args The list of allowed query args.
*
* @return array The modified list of allowed query args.
*/
public static function whitelist_query_args( $allowed_query_args ) {
$allowed_query_args[] = Generator::GENERATE_QUERY_ACTION;
$allowed_query_args[] = Module::DISABLE_MODULE_QUERY_VAR;
return $allowed_query_args;
}
/**
* Plugin activation handler.
*/
public static function activate() {
// Make sure user sees the "Get Started" when first time opening.
( new Getting_Started_Entry() )->set( true );
Analytics::record_user_event( 'activate_plugin' );
$page_cache_status = new Status( Page_Cache::get_slug() );
if ( $page_cache_status->get() && Boost_Cache_Settings::get_instance()->get_enabled() ) {
Page_Cache_Setup::run_setup();
}
$modules_setup = new Modules_Setup();
// Setup a cleanup job
if ( ! wp_next_scheduled( 'jetpack_boost_general_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'jetpack_boost_general_cleanup' );
}
/*
* Check what modules are already active (from a previous activation for example).
* If there are active modules, we need to ensure each module-related event is triggered again.
*/
$active_modules = $modules_setup->get_status();
if (
! empty( $active_modules )
&& ( new Connection() )->is_connected()
) {
foreach ( $active_modules as $module => $status ) {
$modules_setup->on_module_status_update( $module, true );
}
}
}
/**
* Plugin deactivation handler. Clear cache, and reset admin notices.
*/
public function deactivate() {
do_action( 'jetpack_boost_deactivate' );
wp_clear_scheduled_hook( 'jetpack_boost_general_cleanup' );
// Tell Minify JS/CSS to clean up.
jetpack_boost_page_optimize_deactivate();
Regenerate_Admin_Notice::dismiss();
Analytics::record_user_event( 'deactivate_plugin' );
Page_Cache_Setup::deactivate();
// Clean up Image Size Analysis data.
$this->cleanup_image_size_analysis_data();
}
/**
* Clean up Image Size Analysis data from the database.
*
* @since 4.3.0
*/
private function cleanup_image_size_analysis_data() {
global $wpdb;
// Delete all post meta entries for Image Size Analysis fixes.
//phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete(
$wpdb->postmeta,
array( 'meta_key' => '_jb_image_fixes' ),
array( '%s' )
);
}
/**
* Initialize the admin experience.
*/
public function init_admin( $modules_setup ) {
REST_API::register( List_Site_Urls::class );
REST_API::register( List_Source_Providers::class );
REST_API::register( List_Cornerstone_Pages::class );
REST_API::register( List_LCP_Analysis::class );
( new Admin() )->init( $modules_setup );
}
/**
* Initialize Jetpack packages via the Config class.
*
* Consolidates connection, sync, and identity crisis initialization
* into a single Config instance, matching the pattern used by other
* standalone Jetpack plugins (Protect, Social, VideoPress, etc.).
*/
private function init_jetpack_config() {
$jetpack_config = new Jetpack_Config();
/**
* Filter that fakes the connection to WordPress.com. Useful for testing.
*
* @param bool $connection Return true to fake the connection.
*
* @since 1.0.0
*/
if ( ! apply_filters( 'jetpack_boost_connection_bypass', false ) ) {
$jetpack_config->ensure(
'connection',
array(
'slug' => JETPACK_BOOST_SLUG,
'name' => 'Jetpack Boost',
'url_info' => '',
)
);
}
$jetpack_config->ensure(
'sync',
array(
'jetpack_sync_callable_whitelist' => array(
'boost_modules' => array( new Modules_Setup(), 'get_status' ),
'boost_sub_modules_state' => array( new Modules_Setup(), 'get_all_sub_modules_state' ),
'boost_latest_scores' => array( new Speed_Score_History( get_home_url() ), 'latest' ),
'boost_latest_no_boost_scores' => array( new Speed_Score_History( add_query_arg( Module::DISABLE_MODULE_QUERY_VAR, 'all', get_home_url() ) ), 'latest' ),
'critical_css_state' => array( new Critical_CSS_State(), 'get' ),
'lcp_state' => array( new LCP_State(), 'get' ),
),
)
);
$jetpack_config->ensure( 'identity_crisis' );
}
/**
* Loads the textdomain.
*/
public function init_textdomain() {
load_plugin_textdomain(
'jetpack-boost',
false,
JETPACK_BOOST_DIR_PATH . '/languages/'
);
}
/**
* The name of the plugin used to uniquely identify it within the context of
* WordPress and to define internationalization functionality.
*
* @return string The name of the plugin.
* @since 1.0.0
*/
public function get_plugin_name() {
return $this->plugin_name;
}
/**
* Retrieve the version number of the plugin.
*
* @return string The version number of the plugin.
* @since 1.0.0
*/
public function get_version() {
return $this->version;
}
/**
* Handle an environment change to set the correct status to the Critical CSS request.
* This is done here so even if the Critical CSS module is switched off we can
* still capture the change of environment event and flag Critical CSS for a rebuild.
*/
public function handle_environment_change( $is_major_change, $change_type ) {
if ( $is_major_change ) {
Regenerate_Admin_Notice::enable();
}
jetpack_boost_ds_set( 'critical_css_suggest_regenerate', $change_type );
}
/**
* Plugin uninstallation handler. Delete all settings and cache.
*/
public function uninstall() {
global $wpdb;
// When uninstalling, make sure all deactivation cleanups have run as well.
$this->deactivate();
// Delete all Jetpack Boost options.
//phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$option_names = $wpdb->get_col(
"
SELECT `option_name`
FROM `$wpdb->options`
WHERE `option_name` LIKE 'jetpack_boost_%';
"
);
//phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
foreach ( $option_names as $option_name ) {
delete_option( $option_name );
}
// Delete the last run options for the network-wide cron jobs.
delete_site_option( 'jetpack_boost_404_tester_last_run' );
delete_site_option( 'jetpack_boost_minify_cron_cache_cleanup_last_run' );
// Delete stored Critical CSS.
( new Critical_CSS_Storage() )->clear();
// Delete all transients created by boost.
Transient::delete_bulk();
// Clear getting started value
( new Getting_Started_Entry() )->set( false );
Page_Cache_Setup::uninstall();
}
}
@@ -0,0 +1,17 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
/**
* Modules can implement this interface to indicate that they change the HTML output for the site visitor and they may do it after activation.
* E.g. Critical CSS needs to be generated first.
*/
interface Changes_Output_After_Activation {
/**
* Get the action name(s) that should be used to indicate that the module is ready and changing the page output.
*
* @return string[] The names of action hooks which will be triggered to indicate when the module changes the page output.
*/
public static function get_change_output_action_names();
}
@@ -0,0 +1,8 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
/**
* Modules can implement this interface to indicate that they change the HTML output for the site visitor.
*/
interface Changes_Output_On_Activation {}
@@ -0,0 +1,17 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
/**
* Every plugin feature that's large enough
* to need setup also needs a slug
*/
interface Feature extends Has_Setup, Has_Slug {
/**
* Whether the feature is available for use.
* Use this to check for feature flags, etc.
*
* @return bool
*/
public static function is_available();
}
@@ -0,0 +1,6 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
interface Has_Activate {
public static function activate();
}
@@ -0,0 +1,15 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
use Automattic\Jetpack\WP_JS_Data_Sync\Data_Sync;
interface Has_Data_Sync {
/**
* Registers data sync for a modules that is wrapping a Feature within Features_Index.
*
* @param Data_Sync $instance The data sync instance.
*
* @return void
*/
public function register_data_sync( Data_Sync $instance );
}
@@ -0,0 +1,6 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
interface Has_Deactivate {
public static function deactivate();
}
@@ -0,0 +1,19 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
/**
* A class that has a setup step that's supposed to be executed only once.
*/
interface Has_Setup {
/**
* This class has a setup method that should be
* run only once per the request lifecycle.
*
* This is a good place to attach hooks
* or perform other tasks that need
* to be performed once.
*/
public function setup();
}
@@ -0,0 +1,11 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
interface Has_Slug {
/**
* Get the module slug.
*
* Returns a unique identifier for this module within Jetpack Boost.
*/
public static function get_slug();
}
@@ -0,0 +1,8 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
/**
* Modules can implement this interface to indicate that they are always on if available.
*/
interface Is_Always_On {}
@@ -0,0 +1,10 @@
<?php
namespace Automattic\Jetpack\Boost\App\Contracts;
/**
* Indicate that a feature is under development.
*
* If present, the feature will be only visible with JETPACK_BOOST_DEVELOPMENT_FEATURES defined as `true` or in Jurassic Ninja.
*/
interface Is_Dev_Feature {}
@@ -0,0 +1,11 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
interface Needs_To_Be_Ready {
/**
* Check if the module is ready and already optimizing.
* This is useful for modules that need preparation before they can start serving the optimized output. E.g. Critical CSS.
*/
public function is_ready();
}
@@ -0,0 +1,9 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
/**
* Modules with this interface will not work if the website is not publicly available.
*/
interface Needs_Website_To_Be_Public {
}
@@ -0,0 +1,9 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
/**
* Features can implement this interface to indicate that they are an optimization feature.
*/
interface Optimization {
}
@@ -0,0 +1,15 @@
<?php
namespace Automattic\Jetpack_Boost\Contracts;
interface Sub_Feature extends Feature {
/**
* Get the parent features that are required for this feature to work.
*
* At least one parent feature must be enabled for this feature to work.
*
* @return class-string<Feature>[] The parent features.
*/
public static function get_parent_features(): array;
}
@@ -0,0 +1,75 @@
<?php
namespace Automattic\Jetpack_Boost\Data_Sync;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
use Automattic\Jetpack_Boost\Lib\Environment_Change_Detector;
class Cornerstone_Pages_Entry implements Entry_Can_Get, Entry_Can_Set {
private $option_key;
public function __construct( $option_key ) {
$this->option_key = 'jetpack_boost_ds_' . $option_key;
}
public function get( $fallback_value = array() ) {
$urls = get_option( $this->option_key, array() );
if ( empty( $urls ) && ! empty( $fallback_value ) ) {
$urls = $fallback_value;
$this->set( $urls );
}
/**
* Filters the list of cornerstone pages. This list only includes the custom pages.
* If you want the full list, use `jetpack_boost_cornerstone_pages_list_complete` instead.
*
* @since 3.7.0
*
* @param array $urls An array of absolute URLs.
*/
return apply_filters( 'jetpack_boost_cornerstone_pages_list', array_map( array( $this, 'transform_to_absolute' ), $urls ) );
}
public function set( $value ) {
$value = $this->sanitize_value( $value );
$updated = update_option( $this->option_key, $value );
if ( $updated ) {
( new Environment_Change_Detector() )->handle_cornerstone_pages_list_update();
}
}
private function sanitize_value( $value ) {
if ( is_array( $value ) ) {
$value = array_values( array_unique( array_map( 'untrailingslashit', array_map( array( $this, 'transform_to_relative' ), $value ) ) ) );
} else {
$value = array();
}
return $value;
}
private function transform_to_relative( $url ) {
$url = trim( $url );
// Remove the home_url from the beginning of the URL if it exists.
if ( strpos( $url, home_url() ) === 0 ) {
$url = substr( $url, strlen( home_url() ) );
}
// Ensure the URL starts with a slash.
if ( $url !== '' ) {
$url = ltrim( $url, '/' );
$url = '/' . $url;
}
return $url;
}
private function transform_to_absolute( $url ) {
return home_url( $url );
}
}
@@ -0,0 +1,12 @@
<?php
namespace Automattic\Jetpack_Boost\Data_Sync;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Generator;
class Critical_CSS_Meta_Entry implements Entry_Can_Get {
public function get( $_fallback = false ) {
$generator = new Generator();
return $generator->get_generation_metadata();
}
}
@@ -0,0 +1,52 @@
<?php
namespace Automattic\Jetpack_Boost\Data_Sync;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
use Automattic\Jetpack_Boost\Lib\Connection;
use Automattic\Jetpack_Boost\Lib\Premium_Features;
class Getting_Started_Entry implements Entry_Can_Get, Entry_Can_Set {
private $option_key = 'jb_get_started';
/**
* Determines if the user should be shown the Getting Started page.
*/
public function get( $fallback = false ) {
// No point in showing the page if the site is offline, it's probably localhost.
if ( ( new Status() )->is_offline_mode() ) {
return false;
}
// No need to show the page if the site is private.
if ( ( new Status() )->is_private_site() ) {
return false;
}
// If there is no connection, the page must be shown to give them a chance to connect by choosing a plan.
if ( ! ( new Connection() )->is_connected() ) {
return true;
}
// If the site already has premium plan, there is no need to show the page.
if ( Premium_Features::has_any() ) {
return false;
}
// For all other cases, the page should be shown only if the flag is set. It indicates that it's a new site.
if ( $fallback !== false ) {
return \get_option( $this->option_key, $fallback );
}
return \get_option( $this->option_key );
}
public function set( $value ) {
if ( $value === true ) {
update_option( $this->option_key, $value, false );
} else {
delete_option( $this->option_key );
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace Automattic\Jetpack_Boost\Data_Sync;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Merge;
class Mergeable_Array_Entry implements Entry_Can_Get, Entry_Can_Merge {
private $option_key;
public function __construct( $option_key ) {
$this->option_key = $option_key;
}
public function get( $fallback_value = false ) {
// WordPress looks at argument count to figure out if a fallback value was used.
// Only provide the fallback value if it's not the default ( false ).
if ( $fallback_value !== false ) {
return get_option( $this->option_key, $fallback_value );
}
return get_option( $this->option_key );
}
public function set( $value ) {
update_option( $this->option_key, $value, false );
}
public function merge( $value, $partial_value ) {
return array_merge( $value, $partial_value );
}
}
@@ -0,0 +1,78 @@
<?php
namespace Automattic\Jetpack_Boost\Data_Sync;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
/**
* The Minify_Excludes_State_Entry class represents a single state entry for Jetpack Boost Data Sync.
*
* This class implements the Entry_Can_Get and Entry_Can_Set interfaces, allowing it to retrieve and set
* the current state of the Minify Excludes option.
*
* @package Automattic\Jetpack_Boost\Data_Sync
*/
class Minify_Excludes_State_Entry implements Entry_Can_Get, Entry_Can_Set {
/**
* The option key used to store the Minify Excludes option.
*
* @var string
*/
private $option_key;
/**
* Constructs a new instance of the Minify_Excludes_State_Entry class.
*
* @param string $option_key The option key used to store the Minify Excludes option.
*/
public function __construct( $option_key ) {
$this->option_key = 'jetpack_boost_ds_' . $option_key;
}
/**
* Retrieves the value of the specified option.
*
* If the option does not exist, it returns the provided fallback value
* or null if no fallback value is provided.
*
* @param mixed $fallback_value Optional. The value to return if the option does not exist.
* Default is false.
* @return mixed The value of the option, or the fallback value if the option does not exist.
*/
public function get( $fallback_value = false ) {
if ( $fallback_value !== false ) {
return get_option( $this->option_key, $fallback_value );
}
return get_option( $this->option_key );
}
/**
* Sets the value of the Minify Excludes option.
*
* @param mixed $value The new value of the Minify Excludes option.
*/
public function set( $value ) {
$value = $this->sanitize_value( $value );
update_option( $this->option_key, $value );
}
/**
* Sanitizes the given value, ensuring that it is a comma-separated list of unique, trimmed strings.
*
* @param mixed $value The value to sanitize.
*
* @return string The sanitized value, as a comma-separated list of unique, trimmed strings.
*/
private function sanitize_value( $value ) {
if ( is_array( $value ) ) {
$value = array_values( array_unique( array_filter( array_map( 'trim', $value ) ) ) );
} else {
$value = array();
}
return $value;
}
}
@@ -0,0 +1,60 @@
<?php
namespace Automattic\Jetpack_Boost\Data_Sync;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Merge;
use Automattic\Jetpack_Boost\Modules\Module;
class Modules_State_Entry implements Entry_Can_Get, Entry_Can_Merge {
protected $modules = array();
public function __construct( $features ) {
foreach ( $features as $feature ) {
$instance = new Module( new $feature() );
$this->modules[ $instance->get_slug() ] = $instance;
}
}
public function get( $_fallback = false ) {
$state = array();
/*
* Module states are stored in their individual wp_options records.
* We're combining the states of all modules into a single record and attaching the availability of the module.
*/
foreach ( $this->modules as $module ) {
$state[ $module->get_slug() ] = array(
'active' => $module->is_available() && $module->is_enabled(),
'available' => $module->is_available(),
);
}
return $state;
}
public function set( $value ) {
foreach ( $value as $module_slug => $module_state ) {
if ( ! isset( $this->modules[ $module_slug ] ) ) {
continue;
}
$updated = $this->modules[ $module_slug ]->update( $module_state['active'] );
if ( $updated ) {
/**
* Fires when a module is enabled or disabled.
*
* @param string $module The module slug.
* @param bool $status The new status.
* @since 1.5.2
*/
do_action( 'jetpack_boost_module_status_updated', $module_slug, $module_state['active'] );
}
}
}
public function merge( $value, $partial_value ) {
return array_merge( $value, $partial_value );
}
}
@@ -0,0 +1,53 @@
<?php
namespace Automattic\Jetpack_Boost\Data_Sync;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_Graph_History_Request;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Lazy_Entry;
class Performance_History_Entry implements Lazy_Entry, Entry_Can_Get, Entry_Can_Set {
private $start_date;
private $end_date;
public function __construct() {
// Default to the last 30 days
$this->start_date = ( time() - 60 * 60 * 24 * 30 ) * 1000;
$this->end_date = time() * 1000;
}
public function get( $_fallback = false ) {
$request = new Speed_Score_Graph_History_Request( $this->start_date, $this->end_date, array() );
$result = $request->execute();
if ( is_wp_error( $result ) || empty( $result['data'] ) ) {
return array(
'startDate' => $this->start_date,
'endDate' => $this->end_date,
'periods' => array(),
'annotations' => array(),
);
}
$annotations = $result['data']['annotations'] ?? array();
// Sanitize the annotations
foreach ( $annotations as $key => $annotation ) {
$annotations[ $key ] = array(
'timestamp' => $annotation['timestamp'],
'text' => wp_kses_post( $annotation['text'] ),
);
}
return array(
'startDate' => $result['data']['_meta']['start'],
'endDate' => $result['data']['_meta']['end'],
'periods' => $result['data']['periods'],
'annotations' => $annotations,
);
}
public function set( $value ) {
$this->start_date = $value['startDate'];
$this->end_date = $value['endDate'];
}
}
@@ -0,0 +1,85 @@
<?php
/**
* Simple wrapper for Tracks library
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Tracking;
use Jetpack_Options;
use Jetpack_Tracks_Client;
/**
* Class Analytics
*/
class Analytics {
/**
* Initialize tracking.
*/
public function init() {
$tracks = self::get_tracking();
// For tracking events via js/ajax.
add_action( 'admin_enqueue_scripts', array( $tracks, 'enqueue_tracks_scripts' ) );
}
/**
* Get the tracking and manager objects for Boost.
*/
public static function get_tracking() {
return new Tracking( 'jetpack_boost', new Manager( 'jetpack-boost' ) );
}
/**
* Record a user event.
*
* @param string $slug The event slug.
* @param array $data Optional event data.
*/
public static function record_user_event( $slug, $data = array() ) {
if ( ! isset( $data['boost_version'] ) && defined( 'JETPACK_BOOST_VERSION' ) ) {
$data['boost_version'] = JETPACK_BOOST_VERSION;
}
return self::get_tracking()->record_user_event( $slug, $data );
}
public static function init_tracks_scripts() {
$tracks = self::get_tracking();
$tracks::register_tracks_functions_scripts();
wp_enqueue_script( 'jp-tracks' );
wp_add_inline_script(
'jp-tracks',
sprintf(
'window.jpTracksContext = window.jpTracksContext || {}; window.jpTracksContext.blog_id = %s;',
absint( \Jetpack_Options::get_option( 'id', 0 ) )
),
'before'
);
}
public static function get_tracking_data() {
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$user = wp_get_current_user();
$user_data = array(
'userid' => $user->ID,
'username' => $user->user_login,
);
$blog_id = get_current_blog_id();
} else {
$user_data = Jetpack_Tracks_Client::get_connected_user_tracks_identity();
$blog_id = Jetpack_Options::get_option( 'id', 0 );
}
return array(
'userData' => $user_data,
'blogId' => $blog_id,
);
}
}
@@ -0,0 +1,52 @@
<?php
/**
* Asset Manager for Jetpack Boost.
*
* @link https://automattic.com
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
/**
* Class Assets
*/
class Assets {
/**
* Given a minified path, and a non-minified path, will return
* a minified or non-minified file URL based on whether SCRIPT_DEBUG is set and truthy.
*
* Both `$min_base` and `$non_min_base` are expected to be relative to the
* root Jetpack directory.
*
* @param string $min_path minified path.
* @param string $non_min_path non-minified path.
*
* @return string The URL to the file
* @since 1.0.0
*/
public static function get_file_url_for_environment( $min_path, $non_min_path ) {
/**
* Filters the internal path to the distributed assets used by the plugin
*
* @param string $path the path to the assets
*
* @since 1.0.0
*/
$internal_path = apply_filters( 'jetpack_boost_asset_internal_path', 'app/assets/dist/' );
$url = plugins_url( $internal_path . trim( $non_min_path, '/' ), JETPACK_BOOST_PATH );
/**
* Filters the URL for a file passed through the get_file_url_for_environment function.
*
* @param string $url The URL to the file.
* @param string $min_path The minified path.
* @param string $non_min_path The non-minified path.
*
* @since 1.0.0
*/
return apply_filters( 'jetpack_boost_asset_url', $url, $min_path, $non_min_path );
}
}
@@ -0,0 +1,107 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS;
use Automattic\Jetpack_Boost\Modules\Optimizations\Critical_CSS\Critical_CSS;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
/**
* Class Boost_Health
*
* Represents the health of the Jetpack Boost plugin.
*/
class Boost_Health {
/**
* @var array List of issues affecting the health of the plugin.
*/
private $issues = array();
/**
* Boost_Health constructor.
*
* Initializes the Boost_Health object and checks for any health issues.
*/
public function __construct() {
if ( self::critical_css_needs_regeneration() ) {
$this->issues[] = __( 'Outdated Critical CSS', 'jetpack-boost' );
}
if ( self::critical_css_has_errors() ) {
$this->issues[] = __( 'Failed to generate Critical CSS', 'jetpack-boost' );
}
if ( self::cache_engine_not_loading() ) {
$this->issues[] = __( 'Cache engine is not loading', 'jetpack-boost' );
}
}
/**
* Get the total number of issues affecting the health of the plugin.
*
* @return int Total number of issues.
*/
public function get_total_issues() {
return count( $this->issues );
}
/**
* Get all the issues affecting the health of the plugin.
*
* @return array List of issues.
*/
public function get_all_issues() {
return $this->issues;
}
private static function is_critical_css_enabled() {
return ( new Status( Critical_CSS::get_slug() ) )->get();
}
/**
* Check if Critical CSS needs regeneration.
*
* @return bool True if regeneration is needed, false otherwise.
*/
public static function critical_css_needs_regeneration() {
if ( Cloud_CSS::is_available() || ! self::is_critical_css_enabled() ) {
return false;
}
$suggest_regenerate = jetpack_boost_ds_get( 'critical_css_suggest_regenerate' );
return in_array( $suggest_regenerate, Environment_Change_Detector::get_available_env_change_statuses(), true );
}
/**
* Check if Critical CSS generation has errors.
*
* @return bool True if errors are present, false otherwise.
*/
public static function critical_css_has_errors() {
if ( ! self::is_critical_css_enabled() ) {
return false;
}
return ( new Critical_CSS_State() )->has_errors();
}
/**
* Check if the cache engine is not loading.
*
* @return bool True if the cache engine is not loading, false otherwise.
*/
public static function cache_engine_not_loading() {
if ( ! ( new Status( Page_Cache::get_slug() ) )->get() ) {
return false;
}
if ( Boost_Cache::is_loaded() ) {
return false;
}
return true;
}
}
@@ -0,0 +1,51 @@
<?php
/**
* Compatibility class for caching plugins.
*/
namespace Automattic\Jetpack_Boost\Lib;
/**
* Compatibility class for caching plugins.
*/
class Cache_Compatibility {
/**
* Checks if the site has a caching plugin.
*
* Supports:
* - Endurance Page Cache
*
* @since 4.0.0
*
* @return bool True if the site has a caching plugin, false otherwise.
*/
public static function has_cache() {
/**
* Filters whether the site has a caching plugin.
* Useful for testing.
*
* @since 4.0.0
*
* @param bool $has_cache True if the site has a caching plugin, false otherwise.
*/
$has_cache = apply_filters( 'jetpack_boost_compatibility_has_cache', false );
if ( $has_cache ) {
return true;
}
/*
* Disable Page Cache on sites that run Newfold's caching service.
* Their cache is considered enabled when the endurance_cache_level option is set to 2 or 3.
*
* @link https://github.com/bluehost/endurance-page-cache/blob/59fe9993d2cb8a03d1df6da8325f73ad0851ba0a/endurance-page-cache.php#L343
*/
if (
class_exists( '\\Endurance_Page_Cache' )
&& 2 <= (int) get_option( 'endurance_cache_level' )
) {
return true;
}
return false;
}
}
@@ -0,0 +1,219 @@
<?php
/**
* CLI commands for Jetpack Boost.
*
* @link https://automattic.com
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack_Boost\Data_Sync\Getting_Started_Entry;
use Automattic\Jetpack_Boost\Modules\Module;
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Garbage_Collection;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache_Setup;
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Settings;
/**
* Control your local Jetpack Boost installation.
*/
class CLI {
/**
* Jetpack Boost plugin.
*
* @var \Automattic\Jetpack_Boost\Jetpack_Boost $jetpack_boost
*/
private $jetpack_boost;
const MAKE_E2E_TESTS_WORK_MODULES = array( 'critical_css', 'speculation_rules', 'render_blocking_js', 'page_cache', 'lcp', 'minify_js', 'minify_css', 'image_cdn', 'image_guide' );
/**
* CLI constructor.
*
* @param \Automattic\Jetpack_Boost\Jetpack_Boost $jetpack_boost Jetpack Boost plugin.
*/
public function __construct( $jetpack_boost ) {
$this->jetpack_boost = $jetpack_boost;
}
/**
* Manage Jetpack Boost Modules
*
* ## OPTIONS
*
* <activate|deactivate>
* : The action to take.
* ---
* options:
* - activate
* - deactivate
* ---
*
* [<module_slug>]
* : The slug of the module to perform an action on.
*
* ## EXAMPLES
*
* wp jetpack-boost module activate critical_css
* wp jetpack-boost module deactivate critical_css
*
* @param array $args Command arguments.
*/
public function module( $args ) {
$action = $args[0] ?? null;
$module_slug = $args[1] ?? null;
if ( $module_slug === null ) {
/* translators: Placeholder is list of available modules. */
\WP_CLI::error( sprintf( __( 'Please specify a valid module. It should be one of %s', 'jetpack-boost' ), wp_json_encode( self::MAKE_E2E_TESTS_WORK_MODULES, JSON_UNESCAPED_SLASHES ) ) );
}
if ( ! in_array( $module_slug, self::MAKE_E2E_TESTS_WORK_MODULES, true ) ) {
\WP_CLI::error(
/* translators: %1$s refers to the module slug like 'critical-css', %2$s is the list of available modules. */
sprintf( __( "The '%1\$s' module slug is invalid. It should be one of %2\$s", 'jetpack-boost' ), $module_slug, wp_json_encode( self::MAKE_E2E_TESTS_WORK_MODULES, JSON_UNESCAPED_SLASHES ) )
);
}
switch ( $action ) {
case 'activate':
$this->set_module_status( $module_slug, true );
break;
case 'deactivate':
$this->set_module_status( $module_slug, false );
break;
}
}
public function getting_started( $args ) {
$status = $args[0] ?? null;
if ( ! in_array( $status, array( 'true', 'false' ), true ) ) {
\WP_CLI::error(
/* translators: %s refers to the module slug like 'critical-css' */
sprintf( __( "The '%s' status is invalid", 'jetpack-boost' ), $status )
);
}
( new Getting_Started_Entry() )->set( 'true' === $status );
\WP_CLI::success(
/* translators: %s refers to 'true' or 'false' */
sprintf( __( 'Getting started is set to %s', 'jetpack-boost' ), $status )
);
}
/**
* Set a module status.
*
* @param string $module_slug Module slug.
* @param string $status Module status.
*/
private function set_module_status( $module_slug, $status ) {
$modules = ( new Modules_Setup() )->get_available_modules_and_submodules();
$module = $modules[ $module_slug ] ?? false;
if ( ! ( $module instanceof Module ) ) {
\WP_CLI::error(
/* translators: %s refers to the module slug like 'critical-css' */
sprintf( __( "The '%s' module slug is invalid", 'jetpack-boost' ), $module_slug )
);
}
$module->update( $status );
if ( $module_slug === 'page_cache' && $status ) {
$setup_result = Page_Cache_Setup::run_setup();
if ( is_wp_error( $setup_result ) ) {
\WP_CLI::error(
sprintf(
/* translators: %s refers to the error code */
__( 'Setup: %s', 'jetpack-boost' ),
$setup_result->get_error_code()
)
);
}
Garbage_Collection::activate();
Boost_Cache_Settings::get_instance()->set( array( 'enabled' => true ) );
}
$status_label = $status ? __( 'activated', 'jetpack-boost' ) : __( 'deactivated', 'jetpack-boost' );
/* translators: The %1$s refers to the module slug, %2$s refers to the module state (either activated or deactivated)*/
\WP_CLI::success(
sprintf( __( "'%1\$s' has been %2\$s.", 'jetpack-boost' ), $module_slug, $status_label )
);
}
/**
* Manage Jetpack Boost connection
*
* ## OPTIONS
*
* <activate|deactivate|status>
* : The action to take.
* ---
* options:
* - activate
* - deactivate
* - status
* ---
*
* ## EXAMPLES
*
* wp jetpack-boost connection activate
* wp jetpack-boost connection deactivate
* wp jetpack-boost connection status
*
* @param array $args Command arguments.
*/
public function connection( $args ) {
$action = $args[0] ?? null;
switch ( $action ) {
case 'activate':
$result = $this->jetpack_boost->connection->register();
if ( true === $result ) {
\WP_CLI::success( __( 'Boost is connected to WP.com', 'jetpack-boost' ) );
} else {
\WP_CLI::error( __( 'Boost could not be connected to WP.com', 'jetpack-boost' ) );
}
break;
case 'deactivate':
require_once ABSPATH . '/wp-admin/includes/plugin.php';
if ( is_plugin_active_for_network( JETPACK_BOOST_PATH ) ) {
$this->jetpack_boost->connection->deactivate_disconnect_network();
} else {
$this->jetpack_boost->connection->disconnect();
}
\WP_CLI::success( __( 'Boost is disconnected from WP.com', 'jetpack-boost' ) );
break;
case 'status':
$is_connected = $this->jetpack_boost->connection->is_connected();
if ( $is_connected ) {
\WP_CLI::line( 'connected' );
} else {
\WP_CLI::line( 'disconnected' );
}
break;
}
}
/**
* Reset Jetpack Boost
*
* ## EXAMPLE
*
* wp jetpack-boost reset
*/
public function reset() {
$this->jetpack_boost->deactivate();
$this->jetpack_boost->uninstall();
\WP_CLI::success( 'Reset successfully' );
}
}
@@ -0,0 +1,77 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
/**
* A collection of WordPress options that's stored as a single ar
*/
class Collection {
private $key;
/**
* Collections imply that they may carry more data than regular options,
* This might unnecessarily slow down sites.
* Disable autoloading by default.
*
* @see autoload() to enable autoloading.
*
* @var bool
*/
private $autoload = false;
/**
* @param string $key Collection key.
*/
public function __construct( $key ) {
$this->key = $key;
}
/**
* Allow autoloading collections
*/
public function autoload() {
$this->autoload = true;
return $this;
}
/**
* Get the whole collection
*
* @return array
*/
public function get() {
$result = get_option( $this->key, array() );
if ( is_array( $result ) ) {
return $result;
}
return array();
}
/**
* Append a single item to the collection
*
* @param mixed $item
*
* @return bool
*/
public function append( $item ) {
$items = $this->get();
if ( ! in_array( $item, $items, true ) ) {
$items[] = $item;
return update_option( $this->key, $items, $this->autoload );
}
return false;
}
/**
* Delete the whole collection
*
* @return bool
*/
public function delete() {
return delete_option( $this->key );
}
}
@@ -0,0 +1,274 @@
<?php
/**
* Jetpack connection client.
*
* @link https://automattic.com
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Terms_Of_Service;
/**
* Class Connection
*
* Manages the Jetpack connection on behalf of Jetpack Boost.
*/
class Connection {
/**
* Jetpack Connection Manager.
*
* @var Manager $manager The connection manager.
*/
private $manager;
public function __construct() {
$this->manager = new Manager( 'jetpack-boost' );
}
public function init() {
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
$this->initialize_deactivate_disconnect();
}
/**
* Initialize the plugin deactivation hook.
*/
public function initialize_deactivate_disconnect() {
require_once ABSPATH . '/wp-admin/includes/plugin.php';
if ( is_plugin_active_for_network( JETPACK_BOOST_PATH ) ) {
register_deactivation_hook( JETPACK_BOOST_PATH, array( $this, 'deactivate_disconnect_network' ) );
} else {
register_deactivation_hook( JETPACK_BOOST_PATH, array( $this, 'deactivate_disconnect' ) );
}
}
/**
* Deactivate the connection on plugin disconnect.
*/
public function deactivate_disconnect() {
$this->manager->remove_connection();
}
/**
* Deactivate the connection on plugin disconnect for network-activated plugins.
*/
public function deactivate_disconnect_network() {
if ( ! is_network_admin() ) {
return;
}
foreach ( get_sites() as $s ) {
switch_to_blog( (int) $s->blog_id );
$active_plugins = get_option( 'active_plugins' );
/*
* If this plugin was activated in the subsite individually
* we do not want to call disconnect. Plugins activated
* individually (before network activation) stay activated
* when the network deactivation occurs
*/
if ( ! in_array( JETPACK_BOOST_PATH, $active_plugins, true ) ) {
$this->deactivate_disconnect();
}
restore_current_blog();
}
}
/**
* Connection Lifecycle methods.
*/
/**
* Get the WordPress.com blog ID of this site, if it's connected
*/
public static function wpcom_blog_id() {
return defined( 'IS_WPCOM' ) && IS_WPCOM ? get_current_blog_id() : (int) \Jetpack_Options::get_option( 'id' );
}
/**
* True if the site is connected to WP.com.
*
* @return boolean
*/
public function is_connected() {
/**
* Filter that fakes the connection to WordPress.com. Useful for testing.
*
* @param bool $connection Return true to fake the connection.
*
* @since 1.0.0
*/
if ( true === apply_filters( 'jetpack_boost_connection_bypass', false ) ) {
return true;
}
return $this->manager->is_connected();
}
/**
* Register site using connection manager.
*
* @return true|\WP_Error The error object.
*/
public function register() {
if ( $this->is_connected() ) {
Analytics::record_user_event( 'using_existing_connection' );
return true;
}
$result = $this->manager->register();
if ( ! is_wp_error( $result ) ) {
Analytics::record_user_event( 'established_connection' );
Premium_Features::clear_cache();
}
return $result;
}
/**
* Disconnect from Jetpack account.
*
* @return bool
*/
public function disconnect() {
// @todo implement check for Jetpack::validate_sync_error_idc_option() so we don't disconnect production site from staging etc.
Analytics::record_user_event( 'disconnect_site' );
$this->manager->remove_connection();
return true;
}
/**
* REST endpoint methods.
*/
public function register_rest_routes() {
register_rest_route(
JETPACK_BOOST_REST_NAMESPACE,
JETPACK_BOOST_REST_PREFIX . '/connection',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_connection_endpoint' ),
'permission_callback' => array( $this, 'can_manage_connection' ),
)
);
register_rest_route(
JETPACK_BOOST_REST_NAMESPACE,
JETPACK_BOOST_REST_PREFIX . '/connection',
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'create_connection_endpoint' ),
'permission_callback' => array( $this, 'can_manage_connection' ),
)
);
}
/**
* Register site using connection manager.
*
* @param \WP_REST_Request $request The request object.
*
* @return \WP_REST_Response|\WP_Error
*/
public function create_connection_endpoint( \WP_REST_Request $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
( new Terms_Of_Service() )->agree();
$response = $this->register();
// Clear premium features cache to force a refresh.
Premium_Features::clear_cache();
if ( is_wp_error( $response ) ) {
return $response;
}
do_action( 'jetpack_boost_connection_established' );
return rest_ensure_response( $this->get_connection_api_response() );
}
/**
* Fetch connection info.
*
* @param \WP_REST_Request $request The request object.
*
* @return \WP_REST_Response|\WP_Error
*/
public function get_connection_endpoint( \WP_REST_Request $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return rest_ensure_response( $this->get_connection_api_response() );
}
/**
* Connection state sent to client on initialization and after updates.
*
* @return array
*/
public function get_connection_api_response() {
/**
* Filter that fakes the connection to WordPress.com. Useful for testing.
*
* @param bool $connection Return true to fake the connection.
*
* @since 1.0.0
*/
$force_connected = apply_filters( 'jetpack_boost_connection_bypass', false );
$response = array(
'connected' => $force_connected || $this->is_connected(),
'wpcomBlogId' => ( $force_connected || $this->is_connected() ) ? self::wpcom_blog_id() : null,
'userConnected' => $this->manager->is_user_connected(),
);
return $response;
}
/**
* Can user manage the connection?
*
* @return boolean | \WP_Error
*/
public function can_manage_connection() {
if ( current_user_can( 'manage_options' ) ) {
return true;
}
$user_permissions_error_msg = __(
'You do not have the correct user permissions to perform this action.
Please contact your site admin if you think this is a mistake.',
'jetpack-boost'
);
return new \WP_Error(
'invalid_user_permission_jetpack_connect',
$user_permissions_error_msg,
array( 'status' => self::rest_authorization_required_code() )
);
}
/**
* Contextual HTTP error code for authorization failure.
*
* Taken from rest_authorization_required_code() in WP-API plugin until is added to core.
*
* @see https://github.com/WP-API/WP-API/commit/7ba0ae6fe4f605d5ffe4ee85b1cd5f9fb46900a6
*
* @since 4.3.0
*
* @return int
*/
public static function rest_authorization_required_code() {
return is_user_logged_in() ? 403 : 401;
}
}
@@ -0,0 +1,34 @@
<?php
/**
* Implement debug helper methods.
*
* @link https://automattic.com
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
/**
* Class Debug
*/
class Debug {
/**
* Returns whether the debug mode has been triggered.
*/
public static function is_debug_mode() {
$script_debug = defined( 'SCRIPT_DEBUG' ) && \SCRIPT_DEBUG;
$manual_debug_mode = filter_input( INPUT_GET, 'jetpack-boost-debug' );
$debug = $script_debug || $manual_debug_mode;
/**
* Filter debug status on/off
*
* @param bool $debug_status enable or disable debug mode.
*
* @since 1.0.0
*/
return apply_filters( 'jetpack_boost_debug', $debug );
}
}
@@ -0,0 +1,133 @@
<?php
/**
* Environment Change Detector class.
*
* @link https://automattic.com
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack_Boost\Lib\Cornerstone\Cornerstone_Utils;
/**
* Class Environment_Change_Detector
*/
class Environment_Change_Detector {
const ENV_CHANGE_LEGACY = '1';
const ENV_CHANGE_PAGE_SAVED = 'page_saved';
const ENV_CHANGE_POST_SAVED = 'post_saved';
const ENV_CHANGE_SWITCHED_THEME = 'switched_theme';
const ENV_CHANGE_PLUGIN_CHANGE = 'plugin_change';
const ENV_CHANGE_CORNERSTONE_PAGE_SAVED = 'cornerstone_page_saved';
const ENV_CHANGE_CORNERSTONE_PAGES_LIST_UPDATED = 'cornerstone_pages_list_updated';
/**
* Initialize the change detection hooks.
*/
public static function init() {
$object = new self();
$object->register_hooks();
}
public function register_hooks() {
add_action( 'after_switch_theme', array( $this, 'handle_theme_change' ) );
add_action( 'save_post', array( $this, 'handle_post_change' ), 10, 2 );
add_action( 'activated_plugin', array( $this, 'handle_plugin_change' ) );
add_action( 'deactivated_plugin', array( $this, 'handle_plugin_change' ) );
}
public function handle_post_change( $post_id, $post ) {
// Ignore changes to any post which is not published.
if ( 'publish' !== $post->post_status ) {
return;
}
// Ignore changes to post types which do not affect the front-end UI
if ( ! $this->is_post_type_invalidating( $post->post_type ) ) {
return;
}
$this->do_action( false, $this->get_post_change_type( $post ) );
}
public function handle_theme_change() {
$this->do_action( true, $this::ENV_CHANGE_SWITCHED_THEME );
}
public function handle_plugin_change() {
$this->do_action( false, $this::ENV_CHANGE_PLUGIN_CHANGE );
}
public function handle_cornerstone_pages_list_update() {
$this->do_action( false, $this::ENV_CHANGE_CORNERSTONE_PAGES_LIST_UPDATED );
}
/**
* Fire the environment change action.
*
* @param bool $is_major_change Whether the change is such that we should stop serving existing critical CSS immediately unless refreshed.
* @param string $change_type The change type.
*/
public function do_action( $is_major_change, $change_type ) {
do_action_deprecated(
'jetpack_boost_critical_css_environment_changed',
array( $is_major_change, $change_type ),
'3.20.0',
'jetpack_boost_environment_changed'
);
do_action( 'jetpack_boost_environment_changed', $is_major_change, $change_type );
}
public static function get_available_env_change_statuses() {
return array(
self::ENV_CHANGE_PAGE_SAVED,
self::ENV_CHANGE_POST_SAVED,
self::ENV_CHANGE_SWITCHED_THEME,
self::ENV_CHANGE_PLUGIN_CHANGE,
self::ENV_CHANGE_CORNERSTONE_PAGE_SAVED,
self::ENV_CHANGE_CORNERSTONE_PAGES_LIST_UPDATED,
);
}
/**
* Given a post_type, return true if this post type affects the front end of
* the site - i.e.: should cause cached optimizations to be invalidated.
*
* @param string $post_type The post type to check
* @return bool True if this post type affects the front end of the site.
*/
private function is_post_type_invalidating( $post_type ) {
// Special cases: items which are not viewable, but affect the UI.
if ( in_array( $post_type, array( 'wp_template', 'wp_template_part' ), true ) ) {
return true;
}
if ( is_post_type_viewable( $post_type ) ) {
return true;
}
}
/**
* Get the type of change for a specific post.
*
* @param \WP_Post $post The post object.
* @return string The change type.
*/
private function get_post_change_type( $post ) {
if ( Cornerstone_Utils::is_cornerstone_page( $post->ID ) ) {
return $this::ENV_CHANGE_CORNERSTONE_PAGE_SAVED;
}
if ( 'page' === $post->post_type ) {
$change_type = $this::ENV_CHANGE_PAGE_SAVED;
} else {
$change_type = $this::ENV_CHANGE_POST_SAVED;
}
return $change_type;
}
}
@@ -0,0 +1,747 @@
<?php
/**
* Heuristic structural validator for minified JavaScript.
*
* @link https://automattic.com
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
/**
* Answers one narrow question: does a chunk of minified JS look structurally
* broken/truncated? It is NOT a full JS validator (PHP has none built in) -- it
* is a single left-to-right lexer pass that tracks string/template/regex/comment
* state so brackets inside those are ignored, counts bracket nesting in code, and
* reports "broken" when, at EOF, a string/template/regex/block-comment is still
* open or brackets are unbalanced. That is exactly the signature of the truncation
* corruption the bundled MatthiasMullie minifier produces on modern JS.
*
* It deliberately fails safe: a "broken" verdict only causes the caller to skip
* re-minification for that bundle, so ambiguity is cheap to get wrong.
*
* Known blind spot: corruption that stays perfectly balanced (e.g. semantic-only
* damage that still parses) returns "intact". Those do not crash the page and are
* not what this guard targets.
*
* Known (harmless) false positives: a `/` after `}` is read as a regex, not
* division, because telling a block `}` apart from an object-literal `}` needs a
* full parser; so valid object-literal division such as `({}/2)` is reported
* "broken". This generalizes to any object-literal `}` followed by `/` anywhere
* in a source file (`f({}/2)`, `[{}/2]`, `var o={a:1}/2`), and because one
* occurrence skips re-minification of that whole file's contribution to the
* bundle, the cost is more than a single statement's worth of compression. It is
* still fail-safe
* (it only skips re-minification, never corrupts output), so the heuristic stays
* simple rather than guess at block-vs-expression context; the new fallback
* observability hook lets the real-world frequency be measured.
*/
class Js_Structure_Scanner {
/**
* Lexer states.
*/
private const ST_CODE = 0;
private const ST_STRING = 1; // Inside a '...' or "..." string.
private const ST_TEMPLATE = 2; // Inside a `...` template literal.
private const ST_REGEX = 3; // Inside a /.../ regex literal.
private const ST_LINE_COMMENT = 4; // Inside a // comment.
private const ST_BLOCK_COMMENT = 5; // Inside a /* */ comment.
/**
* Upper bound (in bytes) on output we will fully scan per call.
*
* The scan only runs on a cache miss, but on hosts where the concatenation
* cache is not writable the _jb_static path re-minifies (and would re-scan)
* on every request, so the per-call cost must stay bounded. This is set high
* enough to cover realistic modern bundles (Tailwind/SPA/plugin-heavy output
* routinely runs a few MB) so the truncation signature is actually caught,
* rather than silently bypassed for anything above an aggressive cap.
*
* For the rare bundle larger than this, the full lexer scan is skipped, but a
* cheap gross-truncation backstop still runs when the original input is known
* (see looks_broken()).
*/
private const MAX_SCAN_BYTES = 8388608; // 8 MB.
/**
* Above MAX_SCAN_BYTES we cannot afford the full scan, but a minified output
* that is dramatically smaller than its input is the truncation signature by
* itself. Real-world minification shrinks JS by ~20-40%; an output below this
* fraction of the original is treated as truncated. The threshold is
* deliberately conservative so it only trips on gross truncation, never on
* ordinary (even aggressive) minification.
*
* Blind spot: a size-preserving break above the cap (e.g. an unterminated
* template at EOF, which drops only a byte or two) leaves the ratio normal and
* is not caught -- and because no fallback fires, the observability hook does not
* surface it either. Accepted because minification runs per source file, so the
* cap only engages for a pathological multi-MB single source file, a vanishingly
* rare input for this corruption.
*/
private const TRUNCATION_RATIO = 0.5;
/**
* Upper bound on simultaneous open brackets / template-interpolation frames.
*
* $stack and $frames grow with structural nesting depth, which is bounded by
* input bytes but not proportional to them: a small file of runaway `${`/`[`/`(`
* openers can amplify into hundreds of MB of frame state and trip the PHP memory
* limit. That fatal is NOT a \Throwable, so Minify::js()'s try/catch cannot catch
* it -- it would white-screen the page, the exact failure this scanner exists to
* prevent. Nesting this deep never occurs in legitimate output (real code nests a
* few levels), so reaching the cap is itself a corruption/abuse signature: the
* scan stops and returns "broken", routing the caller to the fail-safe original
* bytes.
*
* Kept to a few thousand rather than higher: the check runs after each push, so
* the cap also bounds peak frame state, and each interp frame is a small array.
* A higher cap (e.g. 100k) lets ~150 KB of runaway `${` openers build ~22 MB of
* state before tripping -- enough to OOM-fatal a low-memory shared host (Boost's
* core audience) before the cap engages, reintroducing the white-screen. At a few
* thousand the peak stays well under 1 MB while still sitting orders of magnitude
* above any real nesting.
*/
private const MAX_NESTING_DEPTH = 2000;
/**
* JS being scanned and its length.
*
* @var string
*/
private $js;
/**
* @var int
*/
private $len;
/**
* Current scan offset.
*
* @var int
*/
private $pos = 0;
/**
* Current lexer state (one of the ST_* constants).
*
* @var int
*/
private $state = self::ST_CODE;
/**
* The quote character that opened the current string (' or ").
*
* @var string
*/
private $string_quote = '';
/**
* Stack of currently open brackets ('{', '(', '[').
*
* Coordinates with $frames: an `interp` frame snapshots count($stack) at
* push time, and closes_interpolation() uses that snapshot to tell a '}'
* that ends `${ ... }` apart from one that closes an ordinary block.
*
* @var string[]
*/
private $stack = array();
/**
* Template / interpolation frames.
*
* Each frame is one of:
* - array( 'type' => 'template' ): inside the body of a `...` literal.
* - array( 'type' => 'interp', 'depth' => int ): inside `${ ... }`; the
* 'depth' field captures count($stack) at the moment the frame was
* pushed, so closes_interpolation() can pair this '}' with the matching
* '${' rather than a sibling code block. See scan_template() and
* closes_interpolation().
*
* @var array[]
*/
private $frames = array();
/**
* True once an extra or mismatched closing bracket has been seen.
*
* @var bool
*/
private $unmatched = false;
/**
* True while inside a regex [...] character class.
*
* @var bool
*/
private $re_class = false;
/**
* Last significant char seen in code state.
*
* @var string
*/
private $prev = '';
/**
* The significant char before $prev.
*
* @var string
*/
private $prev_prev = '';
/**
* Last identifier/keyword seen in code state.
*
* @var string
*/
private $prev_word = '';
/**
* Whether $prev_word was a member access (immediately after a '.').
*
* @var bool
*/
private $prev_dot = false;
/**
* Whether the given minified JS looks structurally broken/truncated.
*
* @since 4.6.0
*
* @param string $js Minified JS to inspect.
* @param string|null $original_js The pre-minification input, when available. Used
* only for the gross-truncation backstop on inputs
* too large to scan in full (see MAX_SCAN_BYTES).
*
* @return bool True if it looks broken; false if it looks intact.
*/
public static function looks_broken( $js, $original_js = null ) {
$js = (string) $js;
// Empty output is handled by the caller.
if ( '' === $js ) {
return false;
}
// Output larger than the scan budget is not lexed in full. Fall back to a
// cheap size-delta check against the original: a minified output far smaller
// than its input is the truncation signature even without a full scan. With
// no original to compare against, assume intact.
$len = strlen( $js );
if ( $len > self::MAX_SCAN_BYTES ) {
if ( null === $original_js ) {
return false;
}
$original_len = strlen( (string) $original_js );
return $original_len > 0 && $len < ( $original_len * self::TRUNCATION_RATIO );
}
return ( new self( $js ) )->run();
}
/**
* @param string $js Minified JS to inspect.
*/
private function __construct( $js ) {
$this->js = $js;
$this->len = strlen( $js );
}
/**
* Run the scan and return the verdict.
*
* @return bool
*/
private function run() {
while ( $this->pos < $this->len ) {
switch ( $this->state ) {
case self::ST_CODE:
$broken = $this->scan_code();
break;
case self::ST_STRING:
$broken = $this->scan_string();
break;
case self::ST_TEMPLATE:
$broken = $this->scan_template();
break;
case self::ST_REGEX:
$broken = $this->scan_regex();
break;
case self::ST_LINE_COMMENT:
$broken = $this->scan_line_comment();
break;
case self::ST_BLOCK_COMMENT:
$broken = $this->scan_block_comment();
break;
default:
return true; // Unknown state: fail safe.
}
if ( $broken ) {
return true;
}
// Structural nesting this deep never occurs in legitimate output; stop
// before unbounded $stack/$frames growth can exhaust memory (an
// uncatchable fatal). The depth itself is a corruption signature, so the
// fail-safe verdict is "broken".
if ( count( $this->stack ) > self::MAX_NESTING_DEPTH
|| count( $this->frames ) > self::MAX_NESTING_DEPTH ) {
return true;
}
}
return $this->is_broken_at_eof();
}
/**
* Scan one step while in code state.
*
* @return bool True to short-circuit as broken.
*/
private function scan_code() {
$c = $this->js[ $this->pos ];
// `true`/`false` shortened to !0/!1 before a member access (e.g.
// true.toString() -> !0.toString()) is invalid: `0.` is a numeric literal,
// so the following identifier is a syntax error. This stays bracket-balanced,
// so check it explicitly. Excludes the exponent case: `0.e5` / `0.e+5` is a
// valid numeric literal, so `!0.e5` is valid and must not be flagged.
if ( '!' === $c
&& ( '0' === $this->peek( 1 ) || '1' === $this->peek( 1 ) )
&& '.' === $this->peek( 2 )
&& self::is_ident_start( $this->peek( 3 ) )
&& ! $this->is_exponent_at( 3 ) ) {
return true;
}
if ( '/' === $c ) {
$next = $this->peek();
if ( '/' === $next ) {
$this->state = self::ST_LINE_COMMENT;
$this->pos += 2;
return false;
}
if ( '*' === $next ) {
$this->state = self::ST_BLOCK_COMMENT;
$this->pos += 2;
return false;
}
if ( $this->regex_allowed_here() ) {
$this->state = self::ST_REGEX;
$this->re_class = false;
}
$this->record_prev( '/' );
++$this->pos;
return false;
}
if ( "'" === $c || '"' === $c ) {
$this->state = self::ST_STRING;
$this->string_quote = $c;
++$this->pos;
return false;
}
if ( '`' === $c ) {
$this->frames[] = array( 'type' => 'template' );
$this->state = self::ST_TEMPLATE;
++$this->pos;
return false;
}
if ( '{' === $c || '(' === $c || '[' === $c ) {
$this->stack[] = $c;
$this->record_prev( $c );
++$this->pos;
return false;
}
if ( '}' === $c || ')' === $c || ']' === $c ) {
// A '}' may close a template interpolation (${ ... }) rather than a
// code block: the interpolation pushed its own '{' on the stack, so this
// '}' closes the interpolation when that '{' is the current stack top.
if ( '}' === $c && $this->closes_interpolation() ) {
array_pop( $this->frames );
array_pop( $this->stack ); // The '{' from ${.
$this->state = self::ST_TEMPLATE;
$this->record_prev( '`' );
++$this->pos;
return false;
}
$expected = ( '}' === $c ) ? '{' : ( ( ')' === $c ) ? '(' : '[' );
if ( empty( $this->stack ) || end( $this->stack ) !== $expected ) {
$this->unmatched = true;
} else {
array_pop( $this->stack );
}
$this->record_prev( $c );
++$this->pos;
return false;
}
if ( ctype_space( $c ) ) {
++$this->pos;
return false;
}
if ( self::is_ident_char( $c ) ) {
$start = $this->pos;
while ( $this->pos < $this->len && self::is_ident_char( $this->js[ $this->pos ] ) ) {
++$this->pos;
}
$this->prev_dot = ( '.' === $this->prev );
$this->prev_prev = $this->prev;
$this->prev = $this->js[ $this->pos - 1 ];
$this->prev_word = substr( $this->js, $start, $this->pos - $start );
return false;
}
// Any other significant char (operators like + - * . etc.).
$this->record_prev( $c );
++$this->pos;
return false;
}
/**
* Scan one step while inside a '...' or "..." string.
*
* @return bool True to short-circuit as broken (unterminated string).
*/
private function scan_string() {
$c = $this->js[ $this->pos ];
if ( '\\' === $c ) {
// Escape sequence, including a line continuation (`\` then a line
// terminator). A CRLF continuation is three bytes (`\` + CR + LF), so
// consume the trailing LF too -- otherwise it is left at the new position
// and trips the raw-newline check below, falsely flagging a valid
// continuation as broken. A lone CR or LF after `\` is the normal 2-byte
// skip.
if ( "\r" === $this->peek() && "\n" === $this->peek( 2 ) ) {
$this->pos += 3;
} else {
$this->pos += 2;
}
return false;
}
// A raw LF/CR inside a string literal is a syntax error (a real newline must
// be escaped or a line continuation), so it never appears in valid minified
// output -- only in truncated/corrupted output where the closing quote was
// lost and a later quote happened to re-open the state. Checking ASCII LF/CR
// is zero-false-positive and keeps the byte-oriented lexer simple.
if ( "\n" === $c || "\r" === $c ) {
return true; // Unterminated string literal.
}
if ( $c === $this->string_quote ) {
$this->return_to_code( $c );
}
++$this->pos;
return false;
}
/**
* Scan one step while inside a `...` template literal.
*
* @return bool
*/
private function scan_template() {
$c = $this->js[ $this->pos ];
if ( '\\' === $c ) {
$this->pos += 2;
return false;
}
if ( '`' === $c ) {
$top = end( $this->frames );
if ( $top && 'template' === $top['type'] ) {
array_pop( $this->frames );
}
$this->return_to_code( '`' );
++$this->pos;
return false;
}
if ( '$' === $c && '{' === $this->peek() ) {
$this->frames[] = array(
'type' => 'interp',
'depth' => count( $this->stack ),
);
$this->stack[] = '{'; // The '{' from ${.
$this->return_to_code( '{' );
$this->pos += 2;
return false;
}
++$this->pos;
return false;
}
/**
* Scan one step while inside a /.../ regex literal.
*
* @return bool True to short-circuit as broken (unterminated regex).
*/
private function scan_regex() {
$c = $this->js[ $this->pos ];
if ( '\\' === $c ) {
// Unlike a string (where `\<LF>` is a valid line continuation), a line
// terminator cannot be escaped inside a regex literal -- a backslash
// immediately before a raw LF/CR is the truncation signature. Check the
// escaped byte before skipping over the pair.
$escaped = $this->peek();
if ( "\n" === $escaped || "\r" === $escaped ) {
return true; // Unterminated regex literal.
}
$this->pos += 2;
return false;
}
// A raw line terminator (LF or CR) is invalid anywhere in a regex literal,
// including inside a [...] character class, so this check sits above the
// re_class branch.
if ( "\n" === $c || "\r" === $c ) {
return true; // Unterminated regex literal.
}
if ( $this->re_class ) {
if ( ']' === $c ) {
$this->re_class = false;
}
++$this->pos;
return false;
}
if ( '[' === $c ) {
$this->re_class = true;
++$this->pos;
return false;
}
if ( '/' === $c ) {
$this->return_to_code( '/' );
++$this->pos;
while ( $this->pos < $this->len && ctype_alpha( $this->js[ $this->pos ] ) ) {
++$this->pos; // Skip regex flags.
}
return false;
}
++$this->pos;
return false;
}
/**
* Scan one step while inside a // line comment.
*
* @return bool
*/
private function scan_line_comment() {
// CR, LF, and CRLF all end a line comment in JS; a bare CR must close it too,
// otherwise code after a CR-only line ending is swallowed and broken input
// can read as intact. (CRLF closes on the CR; the trailing LF is then a
// no-op space in code state.)
$c = $this->js[ $this->pos ];
if ( "\n" === $c || "\r" === $c ) {
$this->state = self::ST_CODE;
}
++$this->pos;
return false;
}
/**
* Scan one step while inside a block comment.
*
* @return bool
*/
private function scan_block_comment() {
if ( '*' === $this->js[ $this->pos ] && '/' === $this->peek() ) {
$this->state = self::ST_CODE;
$this->pos += 2;
return false;
}
++$this->pos;
return false;
}
/**
* Verdict once the whole input has been consumed.
*
* @return bool
*/
private function is_broken_at_eof() {
// A line comment running to EOF is valid (e.g. a trailing
// `//# sourceMappingURL=...` with no final newline). Every other open state
// at EOF is a genuinely unterminated construct.
if ( self::ST_LINE_COMMENT === $this->state ) {
$this->state = self::ST_CODE;
}
if ( self::ST_CODE !== $this->state ) {
return true; // Unterminated string/template/regex/block-comment.
}
if ( ! empty( $this->stack ) ) {
return true; // Unbalanced brackets.
}
if ( ! empty( $this->frames ) ) {
return true; // Unterminated template/interpolation.
}
return $this->unmatched; // Saw an extra/mismatched closing bracket.
}
/**
* Whether the current '}' closes a template interpolation rather than a block.
*
* @return bool
*/
private function closes_interpolation() {
$top = end( $this->frames );
return $top && 'interp' === $top['type'] && ( $top['depth'] + 1 ) === count( $this->stack );
}
/**
* Whether a `/` at the current position begins a regex literal (vs division).
*
* JavaScript cannot decide this locally; lexers approximate it from the
* previous significant token: after a value (identifier, number, `)`, `]`,
* postfix ++/--) `/` is division; after an operator/keyword that expects an
* operand next, `/` begins a regex.
*
* @return bool
*/
private function regex_allowed_here() {
$prev = $this->prev;
if ( '' === $prev ) {
return true; // Start of input.
}
// Postfix ++ / -- yields a value, so a following `/` is division.
if ( ( '+' === $prev && '+' === $this->prev_prev ) || ( '-' === $prev && '-' === $this->prev_prev ) ) {
return false;
}
// Punctuation/operators after which a regex literal is legal. Includes the
// arithmetic/comparison binary operators so real minified expressions like
// `x+"|"+/\d{1,2}/.source` (regex after a binary +) are not misread.
static $puncts = array(
'(' => 1,
',' => 1,
'=' => 1,
':' => 1,
'[' => 1,
'!' => 1,
'&' => 1,
'|' => 1,
'?' => 1,
'{' => 1,
';' => 1,
'}' => 1,
'+' => 1,
'-' => 1,
'*' => 1,
'%' => 1,
'<' => 1,
'>' => 1,
'~' => 1,
'^' => 1,
);
if ( isset( $puncts[ $prev ] ) ) {
return true;
}
// After a word: only the keywords below allow a regex, and only when the
// word is not a property access (e.g. `a.return/b` is division).
if ( self::is_ident_char( $prev ) ) {
static $kw = array(
'return' => 1,
'throw' => 1,
'typeof' => 1,
'in' => 1,
'of' => 1,
'new' => 1,
'do' => 1,
'else' => 1,
'void' => 1,
'delete' => 1,
'instanceof' => 1,
'case' => 1,
'yield' => 1,
'await' => 1,
);
return ! $this->prev_dot && '' !== $this->prev_word && isset( $kw[ $this->prev_word ] );
}
// Closing bracket ) ], a '.', etc. -> division.
return false;
}
/**
* Record the most recent significant token (without changing state).
*
* @param string $char The token char.
*/
private function record_prev( $char ) {
$this->prev_prev = $this->prev;
$this->prev = $char;
$this->prev_word = '';
$this->prev_dot = false;
}
/**
* Return to code state, recording $char as the most recent token.
*
* @param string $char The token char (the closing delimiter / interpolation brace).
*/
private function return_to_code( $char ) {
$this->state = self::ST_CODE;
$this->record_prev( $char );
}
/**
* Look ahead from the current position.
*
* @param int $offset How far ahead to peek (default the next char).
*
* @return string The char, or '' if out of range.
*/
private function peek( $offset = 1 ) {
$i = $this->pos + $offset;
return ( $i < $this->len ) ? $this->js[ $i ] : '';
}
/**
* Whether $c can appear within a JS identifier.
*
* @param string $c Single character.
* @return bool
*/
private static function is_ident_char( $c ) {
return '' !== $c && ( ctype_alnum( $c ) || '_' === $c || '$' === $c );
}
/**
* Whether $c can start a JS identifier.
*
* @param string $c Single character.
* @return bool
*/
private static function is_ident_start( $c ) {
return '' !== $c && ( ctype_alpha( $c ) || '_' === $c || '$' === $c );
}
/**
* Whether the chars at $offset begin a valid exponent part (the `e5` of the
* numeric literal `0.e5`). Lets the !0/!1 member-access check tell a real,
* broken member access (`!0.toString()`) apart from a valid exponent
* literal (`!0.e5`), which is not a member access at all.
*
* @param int $offset Offset from the current position.
* @return bool
*/
private function is_exponent_at( $offset ) {
$c = $this->peek( $offset );
if ( 'e' !== $c && 'E' !== $c ) {
return false;
}
$next = $this->peek( $offset + 1 );
if ( '+' === $next || '-' === $next ) {
$next = $this->peek( $offset + 2 );
}
return '' !== $next && ctype_digit( $next );
}
}
@@ -0,0 +1,171 @@
<?php
/**
* Implement the minify class.
*
* @link https://automattic.com
* @since 0.2
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
use MatthiasMullie\Minify\CSS as CSSMinifier;
use MatthiasMullie\Minify\JS as JSMinifier;
/**
* Class Minify
*/
class Minify {
/**
* Reasons passed to the jetpack_boost_js_minify_fallback action. Public so hook
* consumers can compare against Minify::FALLBACK_* instead of bare strings; the
* string values are the stable wire format and must not change.
*
* @since 4.6.0
*/
public const FALLBACK_EXCEPTION = 'exception';
public const FALLBACK_ERROR = 'error';
public const FALLBACK_SCAN_ERROR = 'scan_error';
public const FALLBACK_EMPTY_OUTPUT = 'empty_output';
public const FALLBACK_LOOKS_BROKEN = 'looks_broken';
/**
* Strips whitespace from JavaScript scripts.
*
* @param string $js Input JS string.
*
* @return string String with whitespace stripped.
*/
public static function js( $js ) {
try {
$minifier = new JSMinifier( $js );
$minified_js = $minifier->minify();
} catch ( \Exception $e ) {
// Ordinary failure (e.g. a PCRE backtrack-limit hit on a huge bundle):
// serve the original input rather than nothing.
return self::fallback_js( $js, self::FALLBACK_EXCEPTION, $e );
} catch ( \Error $e ) {
// \Error subclasses (e.g. \TypeError, \ParseError) signal a genuine bug
// rather than unsupported syntax. We still fall back -- a performance
// optimization must never white-screen the page -- but report a distinct
// reason so the condition is distinguishable from ordinary unsupported
// syntax via the hook. (A true memory-limit fatal is not a \Throwable and
// cannot be caught here at all; the scanner instead bounds its own nesting
// depth so it never provokes one -- see Js_Structure_Scanner.)
return self::fallback_js( $js, self::FALLBACK_ERROR, $e );
}
// The bundled MatthiasMullie minifier is regex-based and ES5-era: it can
// SILENTLY corrupt modern JS without throwing. The classic case is a `//`
// inside a (nested) template literal being eaten as a line comment to
// end-of-line, which drops the closing backtick and everything after it,
// truncating the bundle -> "Unexpected end of input" in the browser. The
// try/catch above never fires for this because nothing is thrown.
//
// As a safety net, structurally validate the output: if it looks broken,
// fall back to the original (still concatenated, just not re-minified)
// bytes. A slightly larger working bundle beats a smaller broken one.
if ( '' === (string) $minified_js && '' !== (string) $js ) {
return self::fallback_js( $js, self::FALLBACK_EMPTY_OUTPUT );
}
// The structural scan runs outside the minifier try/catch above, so guard it
// too: any \Throwable from the scan itself must degrade to the original rather
// than white-screen the page. It reports the distinct 'scan_error' reason so a
// scanner fault is never conflated with a minifier-level 'error' in the hook's
// (released, immutable) reason contract. (Memory exhaustion is a fatal, not a
// \Throwable, so it cannot be caught -- the scanner instead bounds its own
// nesting depth to avoid provoking one.) Pass the original input so the scanner
// can apply its gross-truncation backstop to bundles too large to scan in full.
// See Js_Structure_Scanner.
try {
$looks_broken = Js_Structure_Scanner::looks_broken( $minified_js, $js );
} catch ( \Throwable $e ) {
return self::fallback_js( $js, self::FALLBACK_SCAN_ERROR, $e );
}
if ( $looks_broken ) {
return self::fallback_js( $js, self::FALLBACK_LOOKS_BROKEN );
}
return $minified_js;
}
/**
* Serve the original (un-re-minified) JS when minification is declined, and
* surface the reason so the safety net is observable in production.
*
* Observability policy: the jetpack_boost_js_minify_fallback action is the
* always-on surface and fires on every fallback (including the abnormal
* 'error' arm), so monitoring should hook it. The error_log calls are a debug
* aid only and are gated behind WP_DEBUG: a recurring fallback on a
* no-writable-cache host can fire every request, and unconditional logging
* would flood the log without telling an operator anything the hook cannot.
*
* @param string $js The original JS that will be served.
* @param string $reason One of the FALLBACK_* constants.
* @param \Throwable|null $error The throwable, when the fallback was triggered by one.
*
* @return string The original JS, unchanged.
*/
private static function fallback_js( $js, $reason, $error = null ) {
// js() has no type hint, so a caller can pass a non-string (the \Error-arm
// test does exactly that); measure length defensively rather than assume.
$bytes = is_string( $js ) ? strlen( $js ) : 0;
$is_debug = defined( 'WP_DEBUG' ) && WP_DEBUG;
try {
/**
* Fires when Minify::js() declines its minified output and serves the
* original JS instead. This is the always-on observability surface for
* the minify safety net: it fires on every fallback, including the
* abnormal 'error' / 'scan_error' arms (e.g. \TypeError / \ParseError),
* so register a callback here to monitor how often -- and why -- it
* engages. Callbacks must not throw; a throwing callback is swallowed so
* it cannot break minification.
*
* @since 4.6.0
*
* @param string $reason Why the fallback fired: one of the Minify::FALLBACK_* values ('exception', 'error', 'scan_error', 'empty_output', 'looks_broken'). 'error' is a \Throwable raised by the minifier itself; 'scan_error' is a \Throwable raised by the structural scanner -- both carry it in $error.
* @param int $bytes Length of the original JS being served, in bytes.
* @param \Throwable|null $error The throwable when triggered by one, otherwise null. Its message may embed an internal filesystem path (e.g. an IOException), so consumers should not surface it unsanitized.
*/
do_action( 'jetpack_boost_js_minify_fallback', $reason, $bytes, $error );
} catch ( \Throwable $hook_error ) {
// A misbehaving hook callback must never turn a handled fallback into a
// fatal error -- degrading gracefully to the original bundle is the whole
// point of this method. Surface the hook failure only under WP_DEBUG.
if ( $is_debug ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'Jetpack Boost: jetpack_boost_js_minify_fallback hook threw: ' . $hook_error->getMessage() );
}
}
if ( $is_debug ) {
$detail = $error instanceof \Throwable
? sprintf( ' (%s: %s)', get_class( $error ), $error->getMessage() )
: '';
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( sprintf( 'Jetpack Boost: Minify::js() fell back to original JS [reason=%s, bytes=%d]%s', $reason, $bytes, $detail ) );
}
return $js;
}
/**
* Minifies the supplied CSS code, returning its minified form.
*/
public static function css( $css ) {
try {
$minifier = new CSSMinifier( $css );
$minified_css = $minifier->minify();
} catch ( \Throwable $e ) {
// Unlike js(), CSS uses a single \Throwable catch: there is no structural
// validator for CSS output, and CSS minification has not shown the
// silent-corruption failure mode that motivated splitting js()'s catch.
return $css;
}
return $minified_css;
}
}
@@ -0,0 +1,19 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack\My_Jetpack\Products;
class My_Jetpack {
public static function get_product() {
$product = Products::get_product_class( 'boost' );
if ( ! $product ) {
return array();
}
return array(
'tiers' => $product::get_tiers(),
'features_by_tier' => $product::get_features_by_tier(),
);
}
}
@@ -0,0 +1,49 @@
<?php
/**
* Implement nonce helper methods.
*
* @link https://automattic.com
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
/**
* Class Nonce
*/
class Nonce {
/**
* This is a light clone of wp_create_nonce and wp_verify_nonce which skips the UID and cookie token parts,
* so it can be used in anonymous HTTP callbacks. It is therefore not as secure, so be careful.
*
* @param string $action The action.
*/
public static function create( $action ) {
return substr( wp_hash( wp_nonce_tick() . '|' . $action, 'nonce' ), -12, 10 );
}
/**
* Verify the nonce.
*
* @param string $nonce The nonce.
* @param string $action The action.
*/
public static function verify( $nonce, $action ) {
$i = wp_nonce_tick();
// Current nonce.
$expected = substr( wp_hash( $i . '|' . $action, 'nonce' ), -12, 10 );
if ( hash_equals( $expected, $nonce ) ) {
return 1;
}
// Nonce generated 12-24 hours ago.
$expected = substr( wp_hash( ( $i - 1 ) . '|' . $action, 'nonce' ), -12, 10 );
if ( hash_equals( $expected, $nonce ) ) {
return 2;
}
return false;
}
}
@@ -0,0 +1,146 @@
<?php
/**
* Uses standard output buffering implemented to PHP
* to allow for manipulating the output stream.
*
* The implementation allows for seamless text search and manipulation by
* always taking into account two subsequent chunks of data.
*
* Example sequence of chunks sent to output (chunk size 4):
*
* ABCD EFGH IJKL MNOP QRST
*
* A standard output buffer callback handler would always receive only one
* of those chunks, e.g. 'ABCD' and would be unable to match strings on
* seams of individual chunks, e.g. 'DEF', because 'D' appears in chunk #1,
* whereas 'EF' in chunk #2.
*
* This class solves this issue by utilizing a sliding window of size 2 chunks.
* That means the callback receives:
*
* ABCDEFGH EFEGHIJKL IJKLMNOP MNOPQRST
*
* That allows for more advanced string manipulation even across chunk seams.
* It is assumed any string searches are much shorter than a chunk size.
*
* @link https://automattic.com
* @since 0.2.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
/**
* Class Output_Filter
*/
class Output_Filter {
/**
* Output chunk size.
*/
const CHUNK_SIZE = 4096;
/**
* List of callbacks.
*
* @var array
*/
private $callbacks = array();
/**
* One chunk always remains in the buffer to allow for cross-seam matching.
*
* @var string
*/
private $buffered_chunk = '';
/**
* Whether we allow the callbacks to filter incoming chunks of output.
*
* @var boolean
*/
private $is_filtering = false;
/**
* Add an output filtering callback.
*
* @param callable $callback Output filtering callback.
*
* @return void
*/
public function add_callback( $callback ) {
$this->callbacks[] = $callback;
if ( 1 === count( $this->callbacks ) ) {
// Start filtering output now that we have some callbacks.
$this->is_filtering = true;
ob_start(
array( $this, 'tick' ),
self::CHUNK_SIZE
);
}
}
/**
* Processing a full output buffer.
*
* @param string $buffer Output buffer.
* @param int $phase Bitmask of PHP_OUTPUT_HANDLER_* constants.
*
* @return string Buffer data to be flushed to browser.
*/
public function tick( $buffer, $phase ) {
// Bail early if not filtering.
if ( ! $this->is_filtering ) {
return $buffer;
}
// Check if this the first or last buffer. Use the $phase bitmask to figure it out.
// $phase can contain multiple PHP_OUTPUT_HANDLER_* constants.
// e.g.: PHP_OUTPUT_HANDLER_END = 8 (binary 1000), PHP_OUTPUT_HANDLER_START = 1 (binary 0001). Both = 9 (binary 1001).
// Use bitwise AND to read individual flags from $phase.
$is_first_chunk = ( $phase & PHP_OUTPUT_HANDLER_START ) > 0;
$is_last_chunk = ( $phase & PHP_OUTPUT_HANDLER_END ) > 0;
// Don't handle the first chunk (unless it's also the last) - we want to output
// one chunk behind the latest to allow for cross-seam matching.
if ( $is_first_chunk && ! $is_last_chunk ) {
$this->buffered_chunk = $buffer;
return '';
}
$buffer_start = $this->buffered_chunk;
$buffer_end = $buffer;
foreach ( $this->callbacks as $callback ) {
list( $buffer_start, $buffer_end ) = call_user_func( $callback, $buffer_start, $buffer_end );
}
$this->buffered_chunk = $buffer_end;
$joint_buffer = $buffer_start . $buffer_end;
// If the second part of the buffer is the last chunk,
// merge the buffer back together to ensure whole output.
if ( $is_last_chunk ) {
// If more buffer chunks arrive, don't apply callbacks to them.
$this->is_filtering = false;
// Join remaining buffers and allow plugin to append anything to them.
/**
* Filter the Critical CSS output buffer
*
* @param string $joint_buffer The entire output buffer
* @param string $buffer_start The top half of the buffer
* @param string $buffer_end The bottom half of the buffer
*
* @since 1.0.0
*/
return apply_filters( 'jetpack_boost_output_filtering_last_buffer', $joint_buffer, $buffer_start, $buffer_end );
}
// Send the first part of the whole buffer to the browser only,
// because buffer_end will be manipulated in the next tick.
return $buffer_start;
}
}
@@ -0,0 +1,73 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack\Boost_Core\Lib\Boost_API;
use Automattic\Jetpack\Boost_Core\Lib\Transient;
class Premium_Features {
const CLOUD_CSS = 'cloud-critical-css';
const PERFORMANCE_HISTORY = 'performance-history';
const IMAGE_CDN_LIAR = 'image-cdn-liar';
const IMAGE_CDN_QUALITY = 'image-cdn-quality';
const PRIORITY_SUPPORT = 'support';
const PAGE_CACHE = 'page-cache';
const CORNERSTONE_TEN_PAGES = 'cornerstone-10-pages';
const TRANSIENT_KEY = 'premium_features';
public static function has_feature( $feature ) {
$features = self::get_features();
if ( is_array( $features ) ) {
return in_array( $feature, $features, true );
}
return false;
}
public static function get_features() {
$available_features = Transient::get( self::TRANSIENT_KEY, false );
$all_features = array(
self::CLOUD_CSS,
self::IMAGE_CDN_LIAR,
self::IMAGE_CDN_QUALITY,
self::PERFORMANCE_HISTORY,
self::PRIORITY_SUPPORT,
self::CORNERSTONE_TEN_PAGES,
);
if ( ! is_array( $available_features ) ) {
$available_features = Boost_API::get( 'features' );
if ( ! is_array( $available_features ) ) {
$available_features = array();
}
Transient::set( self::TRANSIENT_KEY, $available_features, 3 * DAY_IN_SECONDS );
}
$features = array();
// Prepare a list of features after applying jetpack_boost_has_feature_* filter for each feature.
foreach ( $all_features as $feature ) {
/**
* Filter the availability of a feature
*
* @param bool $has_feature if the feature is available
*
* @since 1.0.0
*/
if ( apply_filters( "jetpack_boost_has_feature_{$feature}", in_array( $feature, $available_features, true ) ) ) {
$features[] = $feature;
}
}
return $features;
}
public static function has_any() {
return count( self::get_features() ) > 0;
}
public static function clear_cache() {
Transient::delete( self::TRANSIENT_KEY );
}
}
@@ -0,0 +1,33 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack\My_Jetpack\Products\Boost;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Status;
class Premium_Pricing {
/**
* Get an object containing the yearly pricing information for Jetpack Boost.
*
* Used by Jetpack_Boost js constants and data sync.
*/
public static function get_yearly_pricing() {
$yearly_pricing = Wpcom_Products::get_product_pricing( Boost::UPGRADED_TIER_PRODUCT_SLUG );
if ( empty( $yearly_pricing ) ) {
// In offline mode, we don't have access to the pricing data and it's not an error.
if ( ! ( new Status() )->is_offline_mode() ) {
Analytics::record_user_event( 'upgrade_price_missing', array( 'error_message' => 'Missing pricing information on benefits interstitial page.' ) );
}
return null;
}
return array(
'priceBefore' => $yearly_pricing['full_price'],
'priceAfter' => $yearly_pricing['discount_price'],
'currencyCode' => $yearly_pricing['currency_code'],
'isIntroductoryOffer' => $yearly_pricing['is_introductory_offer'] === true,
);
}
}
@@ -0,0 +1,54 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack_Boost\Contracts\Has_Setup;
class Setup {
protected static $instances = array();
/**
* This method takes a `Has_Setup` instance and registers the setup action.
* In addition, it will keep track of all the instances passed to it,
*
* This is useful if a plugin needs the instance
* to modify the behavior at a certain hook that
* Jetpack Boost is using.
*
* The use case would be something like this:
* ```
* $instance = my_get_instance_method( Setup::get_instances() );
* remove_action( 'wp_footer', array( $instance, 'foobar' ) );
* ```
*
* @param Has_Setup $instance
*
* @return void
*/
public static function add( Has_Setup $instance ) {
$instance->setup();
self::$instances[] = $instance;
}
public static function get_instances() {
return self::$instances;
}
/**
* @template T
* @param class-string<T> $class_name
* @return T|null
*/
public static function get_instance_of( $class_name ) {
foreach ( self::get_instances() as $instance ) {
if ( $instance instanceof $class_name ) {
// @phan-suppress-next-line PhanTypeMismatchReturn -- Phan isn't inferring the type correctly from the `instanceof $class_name` like it's supposed to.
return $instance;
}
}
return null;
}
}
@@ -0,0 +1,101 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
/**
* Site_Health.
*
* Displays performance issues in WordPress site health page.
*/
class Site_Health {
/**
* Initialize hooks
*
* @access public
* @return void
*/
public static function init() {
if ( ! has_filter( 'site_status_tests', array( __CLASS__, 'add_check' ) ) ) {
add_filter( 'site_status_tests', array( __CLASS__, 'add_check' ), 99 );
}
}
/**
* Add site-health page tests.
*
* @param array $checks Core checks.
*
* @access public
* @return array
*/
public static function add_check( $checks ) {
$checks['direct']['jetpack_boost_checks'] = array(
'label' => __( 'Jetpack Boost checks', 'jetpack-boost' ),
'test' => array( __CLASS__, 'do_checks' ),
);
return $checks;
}
/**
* Do site-health page checks
*
* @access public
* @return array
*/
public static function do_checks() {
$health = new Boost_Health();
$total_issues = $health->get_total_issues();
$issues = $health->get_all_issues();
/**
* Default, no issues found
*/
$result = array(
'label' => __( 'No issues found', 'jetpack-boost' ),
'status' => 'good',
'badge' => array(
'label' => __( 'Performance', 'jetpack-boost' ),
'color' => 'gray',
),
'description' => sprintf(
'<p>%s</p>',
__( 'Jetpack Boost did not find any known performance issues with your site.', 'jetpack-boost' )
),
'actions' => '',
'test' => 'jetpack_boost_checks',
);
/**
* If issues found.
*/
if ( $total_issues ) {
$result['status'] = 'critical';
/* translators: $d is the number of performance issues found. */
$result['label'] = sprintf( _n( 'Your site is affected by %d performance issue', 'Your site is affected by %d performance issues', $total_issues, 'jetpack-boost' ), $total_issues );
$result['description'] = __( 'Jetpack Boost detected the following performance issues with your site:', 'jetpack-boost' );
foreach ( $issues as $issue ) {
$result['description'] .= '<p>';
$result['description'] .= "<span class='dashicons dashicons-warning' style='color: crimson;'></span> &nbsp";
$result['description'] .= wp_kses( $issue, array( 'a' => array( 'href' => array() ) ) ); // Only allow a href HTML tags.
$result['description'] .= '</p>';
}
$result['description'] .= '<p>';
$result['description'] .= sprintf(
wp_kses(
/* translators: Link to Jetpack Boost. */
__( 'Visit <a href="%s">Boost settings page</a> for more information.', 'jetpack-boost' ),
array(
'a' => array( 'href' => array() ),
)
),
esc_url( admin_url( 'admin.php?page=jetpack-boost' ) )
);
$result['description'] .= '</p>';
}
return $result;
}
}
@@ -0,0 +1,158 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
class Site_Urls {
public static function get( $limit = 100 ) {
$core_urls = self::get_wp_core_urls();
$post_urls = self::cleanup_post_urls(
self::get_post_urls( $limit ),
wp_list_pluck(
$core_urls,
'url'
)
);
$urls = array_slice(
array_merge(
$core_urls,
$post_urls
),
0,
$limit
);
/**
* Filters the list of site URLs used by the Image Size Analysis.
*
* @since 3.7.0
*
* @param array $urls An array of URLs.
* @param int $limit The maximum number of URLs that should be returned.
*/
return apply_filters( 'jetpack_boost_site_urls', $urls, $limit );
}
private static function get_wp_core_urls() {
$urls = array();
$front_page = get_option( 'page_on_front' );
if ( ! empty( $front_page ) ) {
$urls['core_front_page'] = array(
'url' => get_permalink( $front_page ),
'modified' => get_post_modified_time( 'Y-m-d H:i:s', false, $front_page ),
'group' => 'core_front_page',
);
} else {
$urls['core_front_page'] = array(
'url' => home_url( '/' ),
'modified' => current_time( 'Y-m-d H:i:s' ),
'group' => 'core_front_page',
);
}
$posts_page = get_option( 'page_for_posts' );
if ( ! empty( $posts_page ) ) {
$urls['core_posts_page'] = array(
'url' => get_permalink( $posts_page ),
'modified' => get_post_modified_time( 'Y-m-d H:i:s', false, $posts_page ),
'group' => 'other',
);
}
return $urls;
}
private static function get_post_urls( $limit ) {
global $wpdb;
$public_post_types = self::get_public_post_types();
$post_types_placeholders = implode(
',',
array_fill(
0,
count( $public_post_types ),
'%s'
)
);
$prepare_values = $public_post_types;
$prepare_values[] = $limit;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$results = $wpdb->get_results(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT ID, post_modified FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_type IN ({$post_types_placeholders}) ORDER BY post_modified DESC LIMIT 0, %d",
$prepare_values
)
);
$urls = array();
foreach ( $results as $result ) {
$urls[ 'post_id_' . $result->ID ] = array(
'url' => get_permalink( $result->ID ),
'modified' => get_post_modified_time( 'Y-m-d H:i:s', false, $result ),
'group' => self::get_post_group( $result ),
);
}
return $urls;
}
/**
* Removes duplicate URLs from the $post_urls list
* based on the additional URLs.
*
* @param array $post_urls List of URLs to cleanup.
* @param array $additional_urls List of URLs to lookup while cleaning.
*
* @return array
*/
private static function cleanup_post_urls( $post_urls, $additional_urls ) {
$clean = array();
foreach ( $post_urls as $key => $item ) {
if ( in_array( $item['url'], $additional_urls, true ) ) {
continue;
}
$clean[ $key ] = $item;
}
return $clean;
}
private static function get_public_post_types() {
$post_types = get_post_types(
array(
'public' => true,
)
);
unset( $post_types['attachment'] );
return array_values(
array_filter(
$post_types,
'is_post_type_viewable'
)
);
}
/**
* Returns the group for the post.
*
* @param \WP_Post $p Post object.
*
* @return string
*/
private static function get_post_group( $p ) {
$post_type = get_post_type( $p->ID );
if ( 'post' === $post_type || 'page' === $post_type ) {
return 'singular_' . $post_type;
}
return 'other';
}
}
@@ -0,0 +1,103 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS;
use Automattic\Jetpack_Boost\Modules\Optimizations\Critical_CSS\Critical_CSS;
class Status implements Entry_Can_Get, Entry_Can_Set {
/**
* Slug of the optimization module which is currently being toggled
*
* @var string $slug
*/
protected $slug;
/**
* A map of modules whose status are synced.
*
* @var array[] $status_sync_map
*/
protected $status_sync_map;
/**
* @var string $option_name
*/
protected $option_name;
public function __construct( $slug ) {
$this->slug = $slug;
$module_slug = str_replace( '_', '-', $this->slug );
$this->option_name = 'jetpack_boost_status_' . $module_slug;
$this->status_sync_map = array(
Cloud_CSS::get_slug() => array(
Critical_CSS::get_slug(),
),
);
}
public function get( $_fallback = false ) {
return get_option( $this->option_name, false );
}
public function set( $value ) {
return update_option( $this->option_name, $value );
}
/**
* Called when the module is toggled.
*
* Called by Modules and triggered by the `jetpack_ds_set` action.
*/
public function on_update( $new_status ) {
$this->update_mapped_modules( $new_status );
$this->track_module_status( (bool) $new_status );
}
/**
* Update modules which are to follow the status of the current module.
*
* For example: critical-css module status should be synced with cloud-css module.
*
* @param mixed $new_status
* @return void
*/
protected function update_mapped_modules( $new_status ) {
if ( ! isset( $this->status_sync_map[ $this->slug ] ) ) {
return;
}
$modules_instance = Setup::get_instance_of( Modules_Setup::class );
// The moduleInstance will be there. But check just in case.
if ( $modules_instance !== null ) {
// Remove the action temporarily to avoid infinite loop.
remove_action( 'jetpack_boost_module_status_updated', array( $modules_instance, 'on_module_status_update' ) );
}
foreach ( $this->status_sync_map[ $this->slug ] as $mapped_module_slug ) {
$mapped_status = new Status( $mapped_module_slug );
$mapped_status->set( $new_status );
}
// The moduleInstance will be there. But check just in case.
if ( $modules_instance !== null ) {
add_action( 'jetpack_boost_module_status_updated', array( $modules_instance, 'on_module_status_update' ), 10, 2 );
}
}
protected function track_module_status( $status ) {
Analytics::record_user_event(
'set_module_status',
array(
'module' => $this->slug,
'status' => $status,
)
);
}
}
@@ -0,0 +1,286 @@
<?php
/**
* Cache for Jetpack Boost that uses CPTs to store the data.
*
* @link https://automattic.com
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib;
/**
* Class Storage_Post_type
*/
class Storage_Post_Type {
/**
* The name.
*
* @var string
*/
private $name;
/**
* Storage_Post_type constructor.
*
* @param string $name The name.
*/
public function __construct( $name ) {
$this->name = sanitize_title( $name );
$this->init();
}
/**
* Get the post type slug.
*/
public function post_type_slug() {
return 'jb_store_' . $this->name;
}
/**
* Static initialization.
*/
private function init() {
// Check if post type already registered.
if ( post_type_exists( $this->post_type_slug() ) ) {
return;
}
register_post_type(
$this->post_type_slug(),
array(
'description' => 'Cache entries for the Jetpack Boost plugin.',
'public' => false,
'show_in_rest' => true,
'rewrite' => false,
'can_export' => false,
'delete_with_user' => false,
)
);
}
/**
* Sets the cache entry using a CPT.
*
* @param string $key Cache key name.
* @param mixed $value Cache value.
* @param int $expiry Cache expiration in seconds.
*
* @return void
*/
public function set( $key, $value, $expiry = 0 ) {
$data_post_data = array(
'post_type' => $this->post_type_slug(),
'post_title' => $key,
'post_name' => $key,
'post_status' => 'publish',
);
$data_post = $this->get_post_by_name( $key );
$expiry_timestamp = 0;
if ( $expiry ) {
$expiry_timestamp = time() + $expiry;
}
$value = array(
'data' => $value,
'expiry' => $expiry_timestamp,
);
$data_post_data['post_content'] = base64_encode( maybe_serialize( $value ) ); // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
// Update an existing data post if we have one or create a new one.
if ( $data_post ) {
$data_post_data['ID'] = $data_post->ID;
wp_update_post( $data_post_data );
} else {
wp_insert_post( $data_post_data );
}
delete_transient( $this->post_type_slug() . '_' . $key );
}
/**
* Gets a cache entry using a CPT.
*
* @param string $key Cache key name.
* @param mixed $default Default value.
*
* @return mixed
*/
public function get( $key, $default ) {
$cached = get_transient( $this->post_type_slug() . '_' . $key );
if ( $cached ) {
return $cached;
}
$data_post = $this->get_post_by_name( $key );
if ( ! $data_post ) {
return $default;
}
/**
* Array(
* 'data' => mixed,
* 'expiry' => int,
* )
*/
// phpcs:disable
$value = maybe_unserialize( base64_decode( $data_post->post_content ) );
// phpcs:enable
if ( isset( $value['expiry'] ) && intval( $value['expiry'] ) > 0 ) {
if ( time() > intval( $value['expiry'] ) ) {
// The cache entry expired. Clear it.
$this->delete( $key );
return $default;
}
}
if ( ! isset( $value['data'] ) ) {
return $default;
}
set_transient( $this->post_type_slug() . '_' . $key, $value['data'], HOUR_IN_SECONDS );
return $value['data'];
}
/**
* Delete a cache entry from a CPT.
*
* @param string $key Cache key name.
*
* @return void
*/
public function delete( $key ) {
$data_post = $this->get_post_by_name( $key );
// Delete the post.
if ( $data_post ) {
wp_delete_post( $data_post->ID, true );
}
}
/**
* Returns a single WP post based on the `post_name` property.
*
* Note: `post_name` is indexed in the DB.
*
* @see https://codex.wordpress.org/Database_Description#Indexes_6
*
* @param string $post_name Post name.
*
* @return bool|\WP_Post
*/
public function get_post_by_name( $post_name ) {
$post_query = new \WP_Query(
array(
'name' => $post_name,
'post_type' => $this->post_type_slug(),
'post_status' => 'publish',
'posts_per_page' => 1,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
if ( ! $post_query->have_posts() ) {
return false;
}
if ( ! $post_query->posts[0] instanceof \WP_Post ) {
return false;
}
return $post_query->posts[0];
}
/**
* Clear all data stored in post types. On systems which support it, this
* will use wp_cache_flush_group and a db query to efficiently flush the
* cache. Otherwise, it will fall back to deleting each item.
*/
public function clear() {
if (
function_exists( 'wp_cache_flush_group' ) &&
function_exists( 'wp_cache_supports' ) &&
wp_cache_supports( 'flush_group' )
) {
$this->clear_bulk();
} else {
$this->clear_manually();
}
}
/**
* Clear all data stored in post types using wp_cache_flush_group and a db
* query. This is more efficient than deleting each item individually.
* Make sure that wp_cache_supports( 'flush_group' ) returns true before
* calling this method.
*/
private function clear_bulk() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete(
$wpdb->posts,
array( 'post_type' => $this->post_type_slug() ),
array( '%s' )
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
$wpdb->prepare(
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
'_transient_' . $this->post_type_slug() . '_%'
)
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
$wpdb->prepare(
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
'_transient_timeout_' . $this->post_type_slug() . '_%'
)
);
wp_cache_flush_group( $this->post_type_slug() );
wp_cache_flush_group( 'options' );
}
/**
* Clear all data stored in post types by deleting each item individually.
* This is less efficient than using wp_cache_flush_group and a db query,
* but works on all systems.
*/
private function clear_manually() {
$posts = get_posts(
array(
'post_type' => $this->post_type_slug(),
'posts_per_page' => -1,
)
);
$keys = array();
foreach ( $posts as $post ) {
wp_delete_post( $post->ID, true );
wp_cache_delete( $post->post_name, $this->post_type_slug() );
$keys[] = '_transient_' . $this->post_type_slug() . '_' . $post->post_name;
$keys[] = '_transient_timeout_' . $this->post_type_slug() . '_' . $post->post_name;
}
if ( empty( $keys ) ) {
return;
}
foreach ( $posts as $post ) {
$key = $this->post_type_slug() . '_' . $post->post_name;
delete_transient( $key );
}
}
}
@@ -0,0 +1,143 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
class Super_Cache_Config_Compatibility {
public static function is_compatible() {
if ( ! self::include_supercache_config() ) {
return true;
}
if ( self::is_mobile_enabled() ) {
return false;
}
if ( self::is_late_init_enabled() ) {
return false;
}
if ( self::is_rejected_cookies_configured() ) {
return false;
}
if ( self::is_cache_restrictions_configured() ) {
return false;
}
if ( self::is_preload_enabled() ) {
return false;
}
if ( self::is_no_cache_for_get_enabled() ) {
return false;
}
if ( self::is_save_headers_enabled() ) {
return false;
}
if ( self::is_make_known_anon_enabled() ) {
return false;
}
if ( self::is_dynamic_cache_enabled() ) {
return false;
}
if ( self::is_clear_on_post_edit_enabled() ) {
return false;
}
if ( self::is_front_page_checks_enabled() ) {
return false;
}
if ( self::is_extra_pages_enabled() ) {
return false;
}
if ( self::is_extra_acceptable_files_enabled() ) {
return false;
}
if ( self::is_extra_rejected_uris_enabled() ) {
return false;
}
if ( self::is_extra_rejected_user_agents_enabled() ) {
return false;
}
return true;
}
private static function include_supercache_config() {
return include WPCACHECONFIGPATH . '/wp-cache-config.php';
}
private static function is_mobile_enabled() {
return ! empty( $GLOBALS['wp_cache_mobile_enabled'] );
}
private static function is_late_init_enabled() {
return ! empty( $GLOBALS['wp_super_cache_late_init'] );
}
private static function is_rejected_cookies_configured() {
return ! empty( $GLOBALS['wpsc_rejected_cookies'] );
}
private static function is_cache_restrictions_configured() {
return isset( $GLOBALS['wp_cache_not_logged_in'] ) && $GLOBALS['wp_cache_not_logged_in'] !== 2;
}
private static function is_preload_enabled() {
return ! empty( $GLOBALS['wp_cache_preload_on'] );
}
private static function is_no_cache_for_get_enabled() {
return ! empty( $GLOBALS['wp_cache_no_cache_for_get'] );
}
private static function is_save_headers_enabled() {
return ! empty( $GLOBALS['wpsc_save_headers'] );
}
private static function is_make_known_anon_enabled() {
return ! empty( $GLOBALS['wp_cache_make_known_anon'] );
}
private static function is_dynamic_cache_enabled() {
return ! empty( $GLOBALS['wp_cache_mfunc_enabled'] );
}
private static function is_clear_on_post_edit_enabled() {
return ! empty( $GLOBALS['wp_cache_clear_on_post_edit'] );
}
private static function is_front_page_checks_enabled() {
return ! empty( $GLOBALS['wp_cache_front_page_checks'] );
}
private static function is_extra_pages_enabled() {
return is_array( $GLOBALS['wp_cache_pages'] ) && array_sum( $GLOBALS['wp_cache_pages'] );
}
private static function is_extra_acceptable_files_enabled() {
$default_cache_acceptable_files = array( 'wp-comments-popup.php', 'wp-links-opml.php', 'wp-locations.php' );
return self::is_array_value_changed( $default_cache_acceptable_files, $GLOBALS['cache_acceptable_files'] );
}
private static function is_extra_rejected_uris_enabled() {
$default_cache_rejected_uri = array( 'wp-.*\\.php', 'index\\.php' );
return self::is_array_value_changed( $default_cache_rejected_uri, $GLOBALS['cache_rejected_uri'] );
}
private static function is_extra_rejected_user_agents_enabled() {
return self::is_array_value_changed( array( '' ), $GLOBALS['cache_rejected_user_agent'] );
}
private static function is_array_value_changed( $default, $current ) {
return is_array( $current ) && array_diff( $current, $default );
}
}
@@ -0,0 +1,16 @@
<?php
namespace Automattic\Jetpack_Boost\Lib;
class Super_Cache_Tracking {
public static function setup() {
add_action( 'jb_cache_moved_to_wpsc', array( __CLASS__, 'track_move_to_wpsc' ) );
}
/**
* Track when site owner moves cache to WPSC.
*/
public static function track_move_to_wpsc() {
Analytics::record_user_event( 'moved_cache_to_wpsc' );
}
}
@@ -0,0 +1,151 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Cornerstone;
use Automattic\Jetpack\Schema\Schema;
use Automattic\Jetpack_Boost\Contracts\Has_Setup;
use Automattic\Jetpack_Boost\Data_Sync\Cornerstone_Pages_Entry;
use Automattic\Jetpack_Boost\Lib\Premium_Features;
class Cornerstone_Pages implements Has_Setup {
const PREMIUM_MAX_PAGES = 10;
const FREE_MAX_PAGES = 1;
public function setup() {
$this->register_ds_stores();
add_filter( 'jetpack_boost_critical_css_providers', array( $this, 'remove_ccss_front_page_provider' ), 10, 2 );
add_filter( 'display_post_states', array( $this, 'add_display_post_states' ), 10, 2 );
add_action( 'init', array( $this, 'set_default_pages' ), 0 );
}
/**
* Set the default pages for the first time Boost is running on the website.
*/
public function set_default_pages() {
// Since the DS store always returns an empty array, we can't know if the user
// wants an empty list of pages or this is the first time Boost is running on the website
// and we need to actually load the default pages.
$raw_pages = \get_option( 'jetpack_boost_ds_cornerstone_pages_list' );
if ( $raw_pages === false ) {
jetpack_boost_ds_set( 'cornerstone_pages_list', $this->default_pages() );
}
}
private function register_ds_stores() {
$schema = Schema::as_array( Schema::as_string() )->fallback( array() );
jetpack_boost_register_option( 'cornerstone_pages_list', $schema, new Cornerstone_Pages_Entry( 'cornerstone_pages_list' ) );
jetpack_boost_register_readonly_option( 'cornerstone_pages_properties', array( $this, 'get_properties' ) );
}
public function remove_ccss_front_page_provider( $providers ) {
$filtered_providers = array();
foreach ( $providers as $provider ) {
if ( $provider['key'] !== 'core_front_page' ) {
$filtered_providers[] = $provider;
}
}
return $filtered_providers;
}
private function default_pages() {
$max_pages = $this->get_max_pages();
$yoast_cornerstone_pages = $this->get_yoast_cornerstone_pages();
$woocommerce_pages = $this->get_woocommerce_pages();
$urls = array_unique( array_merge( $woocommerce_pages, $yoast_cornerstone_pages ) );
$urls = array_map( 'untrailingslashit', $urls );
// An empty string represents the home page.
// Remove it if it's in the list since the home page is a predefined page.
$urls = array_filter( $urls );
return array_slice( $urls, 0, $max_pages );
}
private function get_yoast_cornerstone_pages() {
$urls = array();
if ( ! function_exists( 'wpseo_init' ) ) {
return $urls;
}
$max_pages = $this->get_max_pages();
$yoast_cornerstone_content = get_posts(
array(
'meta_key' => '_yoast_wpseo_is_cornerstone',
'meta_value' => '1',
'post_type' => 'any',
'posts_per_page' => $max_pages,
)
);
foreach ( $yoast_cornerstone_content as $post ) {
$permalink = get_permalink( $post->ID );
if ( $permalink ) {
$relative_permalink = $this->make_relative_url( $permalink );
$urls[] = $relative_permalink;
}
}
return $urls;
}
private function get_woocommerce_pages() {
$urls = array();
if ( ! function_exists( 'wc_get_page_id' ) ) {
return $urls;
}
$shop_page_id = \wc_get_page_id( 'shop' );
if ( $shop_page_id ) {
$url = get_permalink( $shop_page_id );
if ( $url ) {
$relative_url = $this->make_relative_url( $url );
if ( $relative_url ) {
$urls[] = $relative_url;
}
}
}
return $urls;
}
private function make_relative_url( $url ) {
if ( is_string( $url ) && strpos( $url, home_url() ) === 0 ) {
$url = substr( $url, strlen( home_url() ) );
}
return $url;
}
public function get_properties() {
$properties = array(
'max_pages' => $this->get_max_pages(),
'max_pages_premium' => static::PREMIUM_MAX_PAGES,
'default_pages' => array(),
'predefined_pages' => Cornerstone_Utils::get_predefined_list(),
);
// We need this to ensure we don't include the home page in the default pages since an empty array is returned when no default pages are found.
$default_pages = $this->default_pages();
if ( ! empty( $default_pages ) ) {
$properties['default_pages'] = array_map( 'home_url', $default_pages );
}
return $properties;
}
public function add_display_post_states( $post_states, $post ) {
if ( Cornerstone_Utils::is_cornerstone_page( $post->ID ) ) {
$post_states[] = __( 'Cornerstone Page', 'jetpack-boost' );
}
return $post_states;
}
private function get_max_pages() {
return Premium_Features::has_feature( Premium_Features::CORNERSTONE_TEN_PAGES ) ? static::PREMIUM_MAX_PAGES : static::FREE_MAX_PAGES;
}
}
@@ -0,0 +1,147 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Cornerstone;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Cornerstone_Provider;
class Cornerstone_Utils {
/**
* Get the list of cornerstone pages.
*
* @return string[] The relative URLs of all the cornerstone pages.
*/
public static function get_list() {
/**
* Filters the list of cornerstone pages. This list includes the predefined and custom pages.
* If you want to change the list of custom pages, use `jetpack_boost_cornerstone_pages_list` instead.
*
* @since 4.4.0-beta1
*
* @param string[] $urls The absolute URLs of all the cornerstone pages.
*/
return apply_filters( 'jetpack_boost_cornerstone_pages_list_complete', array_merge( self::get_predefined_list(), self::get_custom_list() ) );
}
/**
* Gets the list of Cornerstone Pages that the user has added to the custom list.
*
* @return string[] The absolute URLs of the cornerstone pages.
*
* @since 4.2.0
*/
public static function get_custom_list() {
$pages = jetpack_boost_ds_get( 'cornerstone_pages_list' );
// Bail early if no pages are found.
if ( empty( $pages ) ) {
return array();
}
return self::maybe_trailing_slash_urls( $pages );
}
/**
* Gets the list of Cornerstone Pages that the user cannot remove.
*
* @return string[] The absolute URLs of the cornerstone pages.
*
* @since 4.2.0
*/
public static function get_predefined_list() {
return self::maybe_trailing_slash_urls( array( home_url() ) );
}
/**
* Checks if a URL is a cornerstone page.
*
* @param string $url The URL to check.
* @return bool True if the URL is a cornerstone page, false otherwise.
*/
public static function is_cornerstone_page_by_url( $url ) {
$cornerstone_pages = self::get_list();
if ( empty( $cornerstone_pages ) ) {
return false;
}
$cornerstone_pages = array_map( 'untrailingslashit', $cornerstone_pages );
return in_array( self::sanitize_url( $url ), $cornerstone_pages, true );
}
/**
* Sanitize a URL to make it a compatible cornerstone page URL.
*
* @param string $url The URL to sanitize.
* @return string The sanitized URL.
*/
public static function sanitize_url( $url ) {
return untrailingslashit( $url );
}
/**
* Get the provider key for a given URL.
*
* @param string $url The URL to get the provider key for.
* @return string The provider key.
*/
public static function get_provider_key( $url ) {
return Cornerstone_Provider::get_provider_key( self::sanitize_url( $url ) );
}
/**
* Prepare provider data for a given URL.
* This is usually sent to the Cloud API.
*
* @param string $url The URL to prepare provider data for.
* @return array The provider data.
*/
public static function prepare_provider_data( $url ) {
return array(
'key' => self::get_provider_key( $url ),
// Send the URL verbatim (do not sanitize/untrailingslashit it). The cloud analyzer
// fetches this exact URL, so it must keep the site's canonical trailing-slash form to
// avoid edge/WAF 301s or 403s that break analysis. The `key` is intentionally still
// stripped so storage/retrieval stays slash-agnostic. Do not "simplify" url back to
// self::sanitize_url( $url ).
'url' => $url,
);
}
/**
* Checks if the current page is a cornerstone page.
*
* @return bool True if the current page is a cornerstone page, false otherwise.
*
* @since 3.13.1
*/
public static function is_current_page_cornerstone() {
return self::is_cornerstone_page_by_url( Cornerstone_Provider::get_request_url() );
}
/**
* Check if a post ID is a cornerstone page.
*
* @param int $post_id The ID of the post to check.
* @return bool True if the post is a cornerstone page, false otherwise.
*/
public static function is_cornerstone_page( $post_id ) {
return self::is_cornerstone_page_by_url( get_permalink( $post_id ) );
}
/**
* Adds trailing slashes to URLs if the current permalink structure requires it.
*
* @param string[] $urls The URLs to process.
* @return string[] The processed URLs.
*/
public static function maybe_trailing_slash_urls( $urls ) {
$permalink_structure = \get_option( 'permalink_structure' );
// If permalink structure ends with slash, add trailing slashes.
if ( $permalink_structure && substr( $permalink_structure, -1 ) === '/' ) {
$urls = array_map( 'trailingslashit', $urls );
}
return $urls;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
class Admin_Bar_Compatibility {
/**
* Enforces the admin bar stylesheet to load late and synchronously
* when the admin bar is present on the page.
*/
public static function init() {
// Force the Admin Bar to render in the footer.
remove_action( 'wp_body_open', 'wp_admin_bar_render', '0' );
add_filter( 'jetpack_boost_async_style', array( __CLASS__, 'enable_asynchronous_admin_bar' ), 10, 2 );
add_action( 'wp_before_admin_bar_render', array( __CLASS__, 'force_admin_bar_stylesheet' ) );
add_action( 'wp_head', array( __CLASS__, 'dequeue_admin_bar' ), 0 );
}
/**
* Load the admin bar CSS synchronously.
*
* @param bool $is_async Whether admin bar is async.
* @param string $handle Asset handle.
*
* @return bool
*/
public static function enable_asynchronous_admin_bar( $is_async, $handle ) {
if ( 'admin-bar' === $handle ) {
$is_async = false;
}
return $is_async;
}
/**
* Dequeue the admin bar stylesheet, so that it's not printed early.
*
* @see wp_head
*/
public static function dequeue_admin_bar() {
wp_dequeue_style( 'admin-bar' );
}
/**
* Force the admin bar stylesheet to print right before the admin bar markup.
*
* @see wp_before_admin_bar_render
*/
public static function force_admin_bar_stylesheet() {
wp_print_styles( 'admin-bar' );
}
}
@@ -0,0 +1,84 @@
<?php
/**
* Critical CSS Invalidator
*
* Reset critical CSS when existing critical css values are stale.
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
use Automattic\Jetpack_Boost\Admin\Regenerate_Admin_Notice;
use Automattic\Jetpack_Boost\Lib\Boost_Health;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Source_Providers;
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS;
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS_Followup;
/**
* Handler for invalidating Critical CSS; both Cloud and Local. Watches relevant
* hooks and clears data when necessary. Also sends out its own action
* (after_critical_css_invalidate) so that the Cloud or Local implementations of
* Critical CSS can respond to the invalidation.
*/
class Critical_CSS_Invalidator {
public static function init() {
add_action( 'jetpack_boost_deactivate', array( __CLASS__, 'reset_data' ) );
add_action( 'jetpack_boost_environment_changed', array( __CLASS__, 'handle_environment_change' ) );
add_filter( 'jetpack_boost_total_problem_count', array( __CLASS__, 'update_boost_problem_count' ) );
}
/**
* Reset Critical CSS data.
* For Cloud CSS, we need to make sure there are providers in the state,
* otherwise Cloud CSS cannot be stored after it is generated by the cloud.
*/
public static function reset_data() {
$storage = new Critical_CSS_Storage();
$storage->clear();
$state = new Critical_CSS_State();
$state->clear();
if ( self::is_cloud_css() ) {
$state->prepare_for_generation( ( new Source_Providers() )->get_provider_sources() );
$state->save();
// Clear the regenerate flag so things are nice and tidy.
jetpack_boost_ds_delete( 'critical_css_suggest_regenerate' );
// Also clear the admin notice flag, so the notice doesn't show up in case
// the user is reverted to free Boost.
Regenerate_Admin_Notice::dismiss();
}
Cloud_CSS_Followup::unschedule();
}
/**
* Respond to environment changes; deciding whether or not to clear Critical CSS data.
*/
public static function handle_environment_change( $is_major_change ) {
if ( $is_major_change ) {
self::reset_data();
/**
* Indicate that all existing critical CSS has been invalidated.
*/
do_action( 'jetpack_boost_critical_css_invalidated' );
}
}
public static function update_boost_problem_count( $count ) {
$css_needs_regeneration = Boost_Health::critical_css_needs_regeneration();
if ( $css_needs_regeneration ) {
++$count;
}
return $count;
}
public static function is_cloud_css() {
$optimizations = ( new Modules_Setup() )->get_status();
return isset( $optimizations[ Cloud_CSS::get_slug() ] ) && $optimizations[ Cloud_CSS::get_slug() ];
}
}
@@ -0,0 +1,248 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
use WP_Error;
class Critical_CSS_State {
const GENERATION_STATES = array(
'not_generated' => 'not_generated',
'pending' => 'pending',
'generated' => 'generated',
'error' => 'error',
);
const PROVIDER_STATES = array(
'pending' => 'pending',
'success' => 'success',
'error' => 'error',
);
public $state;
public function __construct() {
$this->state = jetpack_boost_ds_get( 'critical_css_state' );
}
public function clear() {
jetpack_boost_ds_delete( 'critical_css_state' );
}
public function save() {
$this->state['updated'] = microtime( true );
jetpack_boost_ds_set( 'critical_css_state', $this->state );
if ( $this->is_generated() ) {
/**
* Fires when critical CSS has successfully been generated.
*/
do_action( 'jetpack_boost_critical_css_generated' );
}
}
public function set_error( $message ) {
if ( empty( $message ) ) {
error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
'Critical CSS: set_error() called with empty message'
);
return $this;
}
$this->state['status_error'] = $message;
$this->state['status'] = self::GENERATION_STATES['error'];
return $this;
}
public function set_provider_error_dismissed( $provider_key, $error_type, $dismissed ) {
if ( empty( $this->state['providers'] ) ) {
return new WP_Error( 'invalid_provider_key', 'No providers exist' );
}
$provider_index = array_search( $provider_key, array_column( $this->state['providers'], 'key' ), true );
if ( $provider_index === false ) {
return new WP_Error( 'invalid_provider_key', 'Invalid provider key' );
}
if ( ! isset( $this->state['providers'][ $provider_index ]['dismissed_errors'] ) ) {
$this->state['providers'][ $provider_index ]['dismissed_errors'] = array();
}
if ( $dismissed ) {
if ( ! in_array( $error_type, $this->state['providers'][ $provider_index ]['dismissed_errors'], true ) ) {
$this->state['providers'][ $provider_index ]['dismissed_errors'][] = $error_type;
}
} else {
$key = array_search( $error_type, $this->state['providers'][ $provider_index ]['dismissed_errors'], true );
if ( $key !== false ) {
unset( $this->state['providers'][ $provider_index ]['dismissed_errors'][ $key ] );
$this->state['providers'][ $provider_index ]['dismissed_errors'] = array_values( $this->state['providers'][ $provider_index ]['dismissed_errors'] );
}
}
return true;
}
/**
* Update a provider's state. The provider must already exist in the state to be updated.
*
* @param string $provider_key The provider key.
* @param array $state An array to overlay over the current state.
* @return bool|WP_Error True on success, WP_Error on failure.
*/
private function update_provider_state( $provider_key, $state ) {
if ( empty( $this->state['providers'] ) ) {
return new WP_Error( 'invalid_provider_key', 'No providers exist' );
}
$provider_index = array_search( $provider_key, array_column( $this->state['providers'], 'key' ), true );
if ( $provider_index === false ) {
return new WP_Error( 'invalid_provider_key', 'Invalid provider key' );
}
$this->state['providers'][ $provider_index ] = array_merge(
$this->state['providers'][ $provider_index ],
$state
);
$this->maybe_set_generated();
return true;
}
/**
* Set a provider's state to error.
*
* @param string $provider_key The provider key.
* @param array $errors A list of errors to store with this provider.
* @return bool|WP_Error True on success, WP_Error on failure.
*/
public function set_provider_errors( $provider_key, $errors ) {
return $this->update_provider_state(
$provider_key,
array(
'status' => self::PROVIDER_STATES['error'],
'errors' => $errors,
)
);
}
/**
* Set a provider's state to success.
*
* @param string $provider_key The provider key.
* @return bool|WP_Error True on success, WP_Error on failure.
*/
public function set_provider_success( $provider_key ) {
return $this->update_provider_state(
$provider_key,
array(
'status' => self::PROVIDER_STATES['success'],
)
);
}
/**
* Set the state to generated if all providers are done. Should be called wherever
* a provider's state is updated.
*/
private function maybe_set_generated() {
if ( empty( $this->state['providers'] ) ) {
return;
}
$provider_states = array_column( $this->state['providers'], 'status' );
$is_done = ! in_array( self::GENERATION_STATES['pending'], $provider_states, true );
if ( $is_done ) {
$this->state['status'] = self::GENERATION_STATES['generated'];
}
}
public function has_errors() {
// Check if any of the providers have errors as well.
$any_provider_has_error = in_array(
'error',
array_unique(
wp_list_pluck(
$this->state['providers'],
'status'
)
),
true
);
return self::GENERATION_STATES['error'] === $this->state['status'] || $any_provider_has_error;
}
public function get_error_message() {
return $this->state['status_error'] ?? null;
}
public function is_generated() {
return self::GENERATION_STATES['generated'] === $this->state['status'];
}
public function is_requesting() {
return self::GENERATION_STATES['pending'] === $this->state['status'];
}
public function prepare_request() {
$this->state = array(
'status' => self::GENERATION_STATES['pending'],
'providers' => array(),
'created' => microtime( true ),
'updated' => microtime( true ),
);
return $this;
}
public function set_pending_providers( $providers ) {
foreach ( $providers as $key => $provider ) {
$providers[ $key ]['status'] = self::PROVIDER_STATES['pending'];
}
$this->state['providers'] = $providers;
return $this;
}
/**
* Add providers to the state, sets their status to pending
* and sets the generation status to pending.
*
* @param array $providers The providers to include in the state and set as pending.
* @return $this
*/
public function prepare_for_generation( $providers ) {
$this->set_pending_providers( $providers );
$this->state['status'] = self::GENERATION_STATES['pending'];
return $this;
}
/**
* Get fresh state
*/
public function get() {
$this->state = jetpack_boost_ds_get( 'critical_css_state' );
return $this->state;
}
public function has_pending_provider( $needles = array() ) {
if ( empty( $this->state['providers'] ) ) {
return false;
}
$providers = $this->state['providers'];
foreach ( $providers as $provider ) {
if (
! empty( $provider['key'] )
&& ! empty( $provider['status'] )
&& self::PROVIDER_STATES['pending'] === $provider['status']
&& in_array( $provider['key'], $needles, true )
) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,75 @@
<?php
/**
* Critical CSS storage.
*
* @link https://automattic.com
* @since 1.0.0
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
use Automattic\Jetpack_Boost\Lib\Storage_Post_Type;
/**
* Critical CSS Storage class
*/
class Critical_CSS_Storage {
/**
* Storage post type.
*
* @var Storage_Post_Type
*/
protected $storage;
/**
* Critical_CSS_Storage constructor.
*/
public function __construct() {
$this->storage = new Storage_Post_Type( 'css' );
}
/**
* Store Critical CSS for a specific provider.
*
* @param string $key Provider key.
* @param string $value Critical CSS.
*/
public function store_css( $key, $value ) {
$this->storage->set(
$key,
array(
'css' => $value,
)
);
}
/**
* Clear the whole Critical CSS storage.
*/
public function clear() {
$this->storage->clear();
}
/**
* Get Critical CSS for specific provider keys.
*
* @param array $provider_keys Provider keys.
*
* @return array|false
*/
public function get_css( $provider_keys ) {
foreach ( $provider_keys as $key ) {
$data = $this->storage->get( $key, false );
if ( $data && $data['css'] ) {
return array(
'key' => $key,
'css' => $data['css'],
);
}
}
return false;
}
}
@@ -0,0 +1,188 @@
<?php
/**
* Class that's responsible for rendering
* Critical CSS on the site front-end.
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
class Display_Critical_CSS {
/**
* @var string The Critical CSS to display.
*/
protected $css;
/**
* @param string $css
*/
public function __construct( $css ) {
$this->css = $css;
}
/**
* Converts existing screen CSS to be asynchronously loaded.
*
* @param string $html The link tag for the enqueued style.
* @param string $handle The style's registered handle.
* @param string $href The stylesheet's source URL.
* @param string $media The stylesheet's media attribute.
*
* @return string
* @see style_loader_tag
*/
public function asynchronize_stylesheets(
$html,
$handle,
$href,
$media
) {
// If there is no critical CSS, do not alter the stylesheet loading.
if ( ! $this->css ) {
return $html;
}
$supported_loading_methods = array( 'async', 'deferred' );
/**
* Loading method for stylesheets.
*
* Filter the loading method for each stylesheet for the screen with following values:
* async - Stylesheets are loaded asynchronously.
* Styles are applied once the stylesheet is loaded completely without render blocking.
* deferred - Loading of stylesheets are deferred until the window load event.
* Styles from all the stylesheets are applied at once after the page load.
*
* Stylesheet loading behaviour is not altered for any other value such as false or 'default'.
* Stylesheet loading is instant and the process blocks the page rendering.
* Eg: add_filter( 'jetpack_boost_async_style', '__return_false' );
*
* @param string $handle The style's registered handle.
* @param string $media The stylesheet's media attribute.
*
* @see onload_flip_stylesheets for how stylesheets loading is deferred.
*
* @todo Retrieve settings from database, either via auto-configuration or UI option.
*/
$method = apply_filters( 'jetpack_boost_async_style', 'async', $handle, $media );
// If the loading method is not supported, do not alter the stylesheet loading.
if ( ! in_array( $method, $supported_loading_methods, true ) ) {
return $html;
}
// Update the stylesheet markup for supported loading methods using WordPress HTML API.
$processor = new \WP_HTML_Tag_Processor( $html );
if ( ! $processor->next_tag( 'link' ) ) {
return $html;
}
// Only process if this is a stylesheet link tag.
if ( 'stylesheet' !== $processor->get_attribute( 'rel' ) ) {
return $html;
}
// Set the new attributes based on the selected method.
$processor->set_attribute( 'media', 'not all' );
$processor->set_attribute( 'data-media', $media );
if ( 'async' === $method ) {
$processor->set_attribute( 'onload', "this.media=this.dataset.media; delete this.dataset.media; this.removeAttribute( 'onload' );" );
}
// Prepend the original HTML stylesheet tag within the noscript tag
// to support the rendering of the stylesheet when JavaScript is disabled.
return '<noscript>' . $html . '</noscript>' . $processor->get_updated_html();
}
/**
* Prints the critical CSS to the page.
*/
public function display_critical_css() {
$critical_css = $this->css;
if ( ! $critical_css ) {
return false;
}
echo '<style id="jetpack-boost-critical-css">';
// Ensure the CSS cannot terminate the style element early.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::neutralize_style_closing_tags( $critical_css );
echo '</style>';
}
/**
* Neutralize any closing </style tag so CSS can be printed inside a <style>
* element without breaking out of it.
*
* This is NOT a general-purpose CSS sanitizer: it does exactly one thing,
* which is to stop the CSS from terminating the surrounding <style> element.
* It deliberately avoids wp_strip_all_tags(), which corrupts valid CSS values
* that contain markup - e.g. `background-image: url("data:image/svg+xml,<svg ...></svg>")`.
*
* Per the HTML rawtext tokenizer, a <style> element can only be terminated by
* the literal sequence `</style` (case-insensitive). Escaping its forward slash
* to `<\/style` defeats the tokenizer - `</` must be immediately followed by the
* tag name, and `<\` is treated as literal text - so the markup stays inert. The
* replacement string contains no `</style` substring, so a single left-to-right
* pass cannot reconstruct the sequence (including from nested input like
* `<</style/style`), and the transform is idempotent.
*
* Inside CSS strings and url() tokens `\/` is a valid escape for `/`, so
* legitimate quoted values keep their meaning. (A literal `</style` outside a
* quoted string does not occur in well-formed CSS; the security guarantee takes
* priority there regardless.)
*
* @param string $css CSS to neutralize.
* @return string CSS that cannot terminate the surrounding <style> element.
*/
public static function neutralize_style_closing_tags( $css ) {
return str_ireplace( '</style', '<\/style', $css );
}
/**
* Add a small piece of JavaScript to the footer, which on load flips all
* linked stylesheets from media="not all" to "all", and switches the
* Critical CSS <style> block to media="not all" to deactivate it.
*/
public function onload_flip_stylesheets() {
/*
Unminified version of footer script.
?>
<script>
window.addEventListener( 'load', function() {
// Flip all media="not all" links to media="all".
document.querySelectorAll( 'link' ).forEach(
function( link ) {
if ( link.media === 'not all' && link.dataset.media ) {
link.media = link.dataset.media;
delete link.dataset.media;
}
}
);
// Turn off Critical CSS style block with media="not all".
var element = document.getElementById( 'jetpack-boost-critical-css' );
if ( element ) {
element.media = 'not all';
}
} );
</script>
<?php
*/
// Minified version of footer script. See above comment for unminified version.
?>
<script>window.addEventListener( 'load', function() {
document.querySelectorAll( 'link' ).forEach( function( e ) {'not all' === e.media && e.dataset.media && ( e.media = e.dataset.media, delete e.dataset.media );} );
var e = document.getElementById( 'jetpack-boost-critical-css' );
e && ( e.media = 'not all' );
} );</script>
<?php
}
}
@@ -0,0 +1,66 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
use Automattic\Jetpack_Boost\Modules\Optimizations\Critical_CSS\CSS_Proxy;
class Generator {
const GENERATE_QUERY_ACTION = 'jb-generate-critical-css';
public static function init() {
$generator = new static();
if ( static::is_generating_critical_css() ) {
add_action( 'wp_head', array( $generator, 'display_generate_meta' ), 0 );
$generator->force_logged_out_render();
}
}
/**
* Force the current page to render as viewed by a logged out user. Useful when generating
* Critical CSS.
*/
private function force_logged_out_render() {
$current_user_id = get_current_user_id();
if ( 0 !== $current_user_id ) {
// Force current user to 0 to ensure page is rendered as a non-logged-in user.
wp_set_current_user( 0 );
// Turn off display of admin bar.
add_filter( 'show_admin_bar', '__return_false', PHP_INT_MAX );
}
}
/**
* Return true if page is loaded to generate critical CSS
*
* phpcs:disable WordPress.Security.NonceVerification.Recommended
*/
public static function is_generating_critical_css() {
return isset( $_GET[ self::GENERATE_QUERY_ACTION ] );
}
/**
* Get a Critical CSS status block, adding in local generation nonces (if applicable).
* i.e.: Call this method to supply enough Critical CSS status to kick off local generation,
* such as in response to a request-generate API call or during page initialization.
*/
public function get_generation_metadata() {
$status = array();
// Add a user-bound nonce to use when proxying CSS for Critical CSS generation.
$status['proxy_nonce'] = wp_create_nonce( CSS_Proxy::NONCE_ACTION );
return $status;
}
/**
* Renders a <meta> tag used to verify this is a valid page to generate Critical CSS with.
*/
public function display_generate_meta() {
?>
<meta name="<?php echo esc_attr( self::GENERATE_QUERY_ACTION ); ?>" content="true"/>
<?php
}
}
@@ -0,0 +1,56 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
use Automattic\Jetpack_Boost\Admin\Regenerate_Admin_Notice;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Source_Providers;
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS;
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS_Followup;
class Regenerate {
/** @var Critical_CSS_State */
private $state;
public function is_cloud_css() {
$optimizations = ( new Modules_Setup() )->get_status();
return isset( $optimizations[ Cloud_CSS::get_slug() ] ) && $optimizations[ Cloud_CSS::get_slug() ];
}
public function start() {
// Get Critical CSS Source URLs
$source_providers = new Source_Providers();
$providers = $source_providers->get_provider_sources();
// Store those URLs in the Critical CSS State
$this->state = new Critical_CSS_State();
$this->state->prepare_request()
->set_pending_providers( $providers )
->save();
// Get the data
$data = $this->state->get();
if ( $this->is_cloud_css() ) {
// If this is a cloud CSS request, we need to trigger the generation
// of the CSS and return the URL to the CSS file.
$cloud_css = new Cloud_CSS();
$cloud_css->regenerate_cloud_css( Cloud_CSS::REGENERATE_REASON_USER_REQUEST, $cloud_css->get_all_providers() );
Cloud_CSS_Followup::schedule();
}
// Clear previous Critical CSS From storage
$storage = new Critical_CSS_Storage();
$storage->clear();
// Dismiss admin notices
Regenerate_Admin_Notice::dismiss();
jetpack_boost_ds_delete( 'critical_css_suggest_regenerate' );
return $data;
}
public function get_state() {
return $this->state;
}
}
@@ -0,0 +1,31 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Regenerate;
/**
* Critical CSS Action: request regeneration.
*/
class Regenerate_CSS implements Data_Sync_Action {
/**
* Handles the action logic.
*
* @param mixed $_data JSON Data passed to the action.
* @param \WP_REST_Request $_request The request object.
*/
public function handle( $_data, $_request ) {
$regenerate = new Regenerate();
$regenerate->start();
$state = $regenerate->get_state();
return array(
'success' => ! $state->has_errors(),
'state' => $state->get(),
'errors' => $state->get_error_message(),
);
}
}
@@ -0,0 +1,55 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_Storage;
/**
* Critical CSS Action: Set CSS for a provider.
*/
class Set_Provider_CSS implements Data_Sync_Action {
/**
* Handles the action logic.
*
* @param mixed $data JSON Data passed to the action.
* @param \WP_REST_Request $_request The request object.
*/
public function handle( $data, $_request ) {
$state = new Critical_CSS_State();
if ( empty( $data['key'] ) || empty( $data['css'] ) ) {
return array(
'success' => false,
'state' => $state->get(),
'error' => 'Invalid data',
);
}
$provider_key = sanitize_key( $data['key'] );
$css = $this->unmask_content( $data['css'] );
$storage = new Critical_CSS_Storage();
$storage->store_css( $provider_key, $css );
$state->set_provider_success( $provider_key );
$state->save();
return array(
'success' => true,
'state' => $state->get(),
);
}
/**
* Unmasks the content.
*
* @param string $content The content to unmask.
* @return string The unmasked content.
*/
private function unmask_content( $content ) {
return str_replace( '__JB_XMLNS__', 'xmlns', $content );
}
}
@@ -0,0 +1,43 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
/**
* Critical CSS Action: Update whether or not to show a provider which is in an error state.
*/
class Set_Provider_Error_Dismissed implements Data_Sync_Action {
/**
* Handles the action logic.
*
* @param mixed $data JSON Data passed to the action.
* @param \WP_REST_Request $_request The request object.
*/
public function handle( $data, $_request ) {
$state = new Critical_CSS_State();
foreach ( $data as $item ) {
if ( empty( $item['provider'] ) ) {
return array(
'success' => false,
'state' => $state->get(),
'error' => 'Invalid data',
);
}
$provider_key = sanitize_key( $item['provider'] );
$dismissed = ! empty( $item['dismissed'] );
$state->set_provider_error_dismissed( $provider_key, $item['error_type'], $dismissed );
}
$state->save();
return array(
'success' => true,
'state' => $state->get(),
);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions;
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
/**
* Critical CSS Action: Store errors for a provider.
*/
class Set_Provider_Errors implements Data_Sync_Action {
/**
* Handles the action logic.
*
* @param mixed $data JSON Data passed to the action.
* @param \WP_REST_Request $_request The request object.
*/
public function handle( $data, $_request ) {
$state = new Critical_CSS_State();
if ( empty( $data['key'] ) || empty( $data['errors'] ) ) {
return array(
'success' => false,
'state' => $state->get(),
'error' => 'Invalid data',
);
}
$provider_key = sanitize_key( $data['key'] );
$errors = $data['errors'];
$state->set_provider_errors( $provider_key, $errors );
$state->save();
return array(
'success' => true,
'state' => $state->get(),
);
}
}
@@ -0,0 +1,115 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync;
use Automattic\Jetpack\Schema\Schema;
/**
* Registers data sync for both the Critical CSS module and the Cloud CSS module. Both of these modules cannot be available at the same time.
*/
class Data_Sync_Schema {
/**
* Represents a set of errors that can be stored for a single Provider Key in a Critical CSS state block.
*/
public static function critical_css_provider_error() {
return Schema::as_array(
Schema::as_assoc_array(
array(
'url' => Schema::as_string(),
'message' => Schema::as_string(),
'type' => Schema::as_string(),
'meta' => Schema::any_json_data()->nullable(),
)
)->fallback(
array(
'url' => '',
'message' => '',
'type' => '',
)
)
);
}
public static function critical_css_state() {
return Schema::as_assoc_array(
array(
'providers' => Schema::as_array(
Schema::as_assoc_array(
array(
'key' => Schema::as_string(),
'label' => Schema::as_string(),
'urls' => Schema::as_array( Schema::as_string() ),
'success_ratio' => Schema::as_float(),
'status' => Schema::enum( array( 'success', 'pending', 'error', 'validation-error' ) )->fallback( 'validation-error' ),
'dismissed_errors' => Schema::as_array( Schema::as_string() )->nullable(),
'errors' => self::critical_css_provider_error()->nullable(),
)
)
)->nullable(),
'status' => Schema::enum( array( 'not_generated', 'generated', 'pending', 'error' ) )->fallback( 'not_generated' ),
'created' => Schema::as_float()->nullable(),
'updated' => Schema::as_float()->nullable(),
'status_error' => Schema::as_string()->nullable(),
)
)->fallback(
array(
'providers' => array(),
'status' => 'not_generated',
'created' => null,
'updated' => null,
)
);
}
public static function critical_css_meta() {
return Schema::as_assoc_array(
array(
'proxy_nonce' => Schema::as_string()->nullable(),
)
);
}
public static function critical_css_suggest_regenerate() {
return Schema::enum(
array(
'1', // Old versions of Boost stored a boolean in the DB.
'page_saved',
'post_saved',
'switched_theme',
'plugin_change',
'cornerstone_page_saved',
'cornerstone_pages_list_updated',
)
)->nullable();
}
public static function critical_css_set_provider() {
return Schema::as_assoc_array(
array(
'key' => Schema::as_string(),
'css' => Schema::as_string(),
)
);
}
public static function critical_css_set_provider_errors() {
return Schema::as_assoc_array(
array(
'key' => Schema::as_string(),
'errors' => self::critical_css_provider_error(),
)
);
}
public static function critical_css_set_provider_errors_dismissed() {
return Schema::as_array(
Schema::as_assoc_array(
array(
'provider' => Schema::as_string(),
'error_type' => Schema::as_string(),
'dismissed' => Schema::as_boolean(),
)
)
);
}
}
@@ -0,0 +1,211 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_Storage;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Archive_Provider;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Cornerstone_Provider;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Post_ID_Provider;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Provider;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Singular_Post_Provider;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Taxonomy_Provider;
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\WP_Core_Provider;
class Source_Providers {
/**
* Variable used to cache the CSS string during the page request.
* This is here because `get_critical_css` is called multiple
* times in `style_loader_tag` hook (on each CSS file).
*
* @var null|false|string
*/
protected $request_cached_css;
/**
* Stores the Critical CSS key used for rendering the current page if any.
*
* @var null|string
*/
protected $current_critical_css_key;
/**
* List of all the Critical CSS Types.
*
* The order is important because searching for critical CSS will stop as soon as a value is found.
* So finding Critical CSS by post ID is attempted before searching for a common Singular Post critical CSS.
*
* @var Provider[]
*/
protected $providers = array(
Cornerstone_Provider::class,
Post_ID_Provider::class,
WP_Core_Provider::class,
Singular_Post_Provider::class,
Archive_Provider::class,
Taxonomy_Provider::class,
);
public function get_providers() {
return $this->providers;
}
/**
* Returns the Provider which controls a given key.
*/
public function get_provider_for_key( $key ) {
foreach ( $this->providers as $provider ) {
if ( $provider::owns_key( $key ) ) {
return $provider;
}
}
return null;
}
/**
* Get all critical CSS storage keys that are available for the current request.
* Caches the result.
*
* @return array
*/
public function get_current_request_css_keys() {
static $keys = null;
if ( null !== $keys ) {
return $keys;
}
$keys = array();
foreach ( $this->providers as $provider ) {
$provider_keys = $provider::get_current_storage_keys();
if ( empty( $provider_keys ) ) {
continue;
}
$keys = array_merge( $keys, $provider_keys );
}
return $keys;
}
/**
* Get critical CSS for the current request.
*
* @return string|false
*/
public function get_current_request_css() {
if ( null !== $this->request_cached_css ) {
return $this->request_cached_css;
}
$storage = new Critical_CSS_Storage();
$data = $storage->get_css( $this->get_current_request_css_keys() );
if ( false === $data ) {
return false;
}
$this->request_cached_css = $data['css'];
$this->current_critical_css_key = $data['key'];
return $this->request_cached_css;
}
public function get_current_critical_css_key() {
return $this->current_critical_css_key;
}
/**
* Get providers sources.
*
* @return array
*/
public function get_provider_sources( $context_posts = array() ) {
$sources = array();
$flat_core_and_cornerstone_urls = array();
$wp_core_provider_urls = WP_Core_Provider::get_critical_source_urls( $context_posts );
foreach ( $wp_core_provider_urls as $urls ) {
$flat_core_and_cornerstone_urls = array_merge( $flat_core_and_cornerstone_urls, $urls );
}
$cornerstone_provider_urls = Cornerstone_Provider::get_critical_source_urls( $context_posts );
foreach ( $cornerstone_provider_urls as $urls ) {
$flat_core_and_cornerstone_urls = array_merge( $flat_core_and_cornerstone_urls, $urls );
}
$flat_core_and_cornerstone_urls = array_values( array_unique( $flat_core_and_cornerstone_urls ) );
foreach ( $this->get_providers() as $provider ) {
$provider_name = $provider::get_provider_name();
// For each provider,
// Gather a list of URLs that are going to be used as Critical CSS source.
foreach ( $provider::get_critical_source_urls( $context_posts ) as $group => $urls ) {
if ( empty( $urls ) ) {
continue;
}
// This removes core and cornerstone URLs from the list of URLs,
// so they don't belong to two separate groups.
if ( ! in_array( $provider, array( WP_Core_Provider::class, Cornerstone_Provider::class ), true ) ) {
$urls = array_values( array_diff( $urls, $flat_core_and_cornerstone_urls ) );
}
if ( empty( $urls ) ) {
continue;
}
$urls = $this->make_absolute_urls( $urls );
$key = $provider_name . '_' . $group;
// For each provider
// Track the state and errors in a state array.
$sources[] = array(
'key' => $key,
'label' => $provider::describe_key( $key ),
/**
* Filters the URLs used by Critical CSS for each provider.
*
* @param array $urls The list of URLs to be used to generate critical CSS
* @param string $provider The provider name.
* @since 1.0.0
*/
'urls' => apply_filters( 'jetpack_boost_critical_css_urls', $urls, $provider ),
'success_ratio' => $provider::get_success_ratio(),
);
}
}
/**
* Filters the list of Critical CSS source providers.
*
* @param array $sources The list of Critical CSS source providers.
* @since 3.6.0
*/
return apply_filters( 'jetpack_boost_critical_css_providers', $sources );
}
/**
* Make URLs absolute.
*
* @param array $urls The list of URLs to make absolute.
*
* @return array
*/
private function make_absolute_urls( $urls ) {
$absolute_urls = array();
foreach ( $urls as $url ) {
if ( class_exists( '\WP_Http' ) && method_exists( '\WP_Http', 'make_absolute_url' ) ) {
$absolute_urls[] = \WP_Http::make_absolute_url( $url, home_url() );
continue;
}
if ( stripos( $url, home_url() ) === 0 ) {
$absolute_urls[] = $url;
} else {
$absolute_urls[] = home_url( $url );
}
}
return $absolute_urls;
}
}
@@ -0,0 +1,128 @@
<?php
/**
* Archive provider class.
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
/**
* Class Archive_Provider
*
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
*/
class Archive_Provider extends Provider {
/**
* Provider name.
*
* @var string
*/
protected static $name = 'archive';
/** @inheritdoc */
public static function get_critical_source_urls( $context_posts = array() ) {
$links = array();
$context_post_types = wp_list_pluck( $context_posts, 'post_type' );
$post_types = self::get_post_types();
if ( ! empty( $context_post_types ) ) {
$post_types = array_intersect( $post_types, $context_post_types );
}
foreach ( $post_types as $post_type ) {
$link = get_post_type_archive_link( $post_type );
if ( ! empty( $link ) ) {
$links[ $post_type ][] = $link;
}
}
return $links;
}
/** @inheritdoc */
public static function get_current_storage_keys() {
if ( ! is_archive() ) {
return array();
}
// For example: "archive_post".
return array( self::$name . '_' . get_post_type() );
}
/** @inheritdoc */
public static function get_keys() {
return self::get_post_types();
}
/** @inheritdoc */
public static function get_edit_url( $_provider_key ) {
return null;
}
/** @inheritdoc */
public static function describe_key( $provider_key ) {
$post_type = substr( $provider_key, strlen( static::$name ) + 1 );
switch ( $post_type ) {
case 'post':
return __( 'Post archive view', 'jetpack-boost' );
case 'page':
return __( 'Page archive view', 'jetpack-boost' );
default:
return __( 'Archive page for custom post type', 'jetpack-boost' );
}
}
/**
* Get post types that need Critical CSS.
*
* @return mixed|void
*/
public static function get_post_types() {
$post_types = get_post_types(
array(
'public' => true,
'has_archive' => true,
),
'objects'
);
unset( $post_types['attachment'] );
$post_types = array_filter( $post_types, 'is_post_type_viewable' );
$provider_post_types = array();
// Generate a name => name array for backwards compatibility.
foreach ( $post_types as $post_type ) {
$provider_post_types[ $post_type->name ] = $post_type->name;
}
/**
* Filters the post types used for Critical CSS
*
* @param array $post_types The array of post types to be used
*
* @since 1.0.0
*/
return apply_filters(
'jetpack_boost_critical_css_post_types_archives',
apply_filters_deprecated(
'jetpack_boost_critical_css_post_types',
array(
$provider_post_types,
),
'3.4.0',
'jetpack_boost_critical_css_post_types_archives'
),
$post_types
);
}
/** @inheritdoc */
public static function get_success_ratio() {
return 1;
}
}
@@ -0,0 +1,129 @@
<?php
/**
* Provider for the cornerstone pages
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
use Automattic\Jetpack_Boost\Lib\Cornerstone\Cornerstone_Utils;
/**
* Class Cornerstone_Provider
*
* @package Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers
*/
class Cornerstone_Provider extends Provider {
/**
* @var string
*/
protected static $name = 'cornerstone';
/**
* Get the providers for cornerstone pages.
*
* @param array $_context_posts Context posts, not used. Cornerstone pages are always available.
* @return array
*/
public static function get_critical_source_urls( $_context_posts = array() ) {
$urls = Cornerstone_Utils::get_list();
$groups = array();
foreach ( $urls as $url ) {
$groups[ self::get_hash_for_url( $url ) ] = array( $url );
}
return $groups;
}
public static function get_provider_key( $url ) {
return self::$name . '_' . self::get_hash_for_url( $url );
}
/**
* Get the current storage keys for cornerstone pages.
*
* @return array
*/
public static function get_current_storage_keys() {
$current_url = self::get_request_url();
return array( self::get_provider_key( $current_url ) );
}
public static function get_request_url() {
global $wp;
// If pretty parmalinks are enabled, use the request. Otherwise, use the query vars.
if ( get_option( 'permalink_structure' ) ) {
return home_url( $wp->request );
}
return add_query_arg( $wp->query_vars, home_url() );
}
/**
* Get the keys for cornerstone pages.
*
* @return array
*/
public static function get_keys() {
$urls = Cornerstone_Utils::get_list();
return array_map( array( __CLASS__, 'get_hash_for_url' ), $urls );
}
/**
* @inheritdoc
*/
public static function get_hash_for_url( $url ) {
// Remove the home_url from the beginning of the URL.
$home_url = home_url();
if ( stripos( $url, $home_url ) === 0 ) {
$url = substr( $url, strlen( $home_url ) );
}
$url = ltrim( $url, '/' );
$url = untrailingslashit( $url );
$hash = hash( 'md5', $url );
return substr( $hash, 0, 8 );
}
/**
* @inheritdoc
*/
public static function describe_key( $_key ) {
return __( 'Cornerstone page', 'jetpack-boost' );
}
/**
* @inheritdoc
*/
public static function get_edit_url( $key ) {
$hash = substr( $key, strlen( self::$name ) + 1 );
$source_urls = self::get_critical_source_urls();
if ( ! isset( $source_urls[ $hash ] ) ) {
return null;
}
$post_id = url_to_postid( $source_urls[ $hash ][0] );
if ( ! $post_id ) {
return null;
}
return get_edit_post_link( $post_id );
}
/**
* @inheritdoc
*/
public static function get_success_ratio() {
return 1;
}
}
@@ -0,0 +1,162 @@
<?php
/**
* The Post ID provider class.
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
/**
* Class Post_ID_Provider
*
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
*/
class Post_ID_Provider extends Provider {
/**
* Post Ids storage key.
*
* @var string
*/
const STORAGE_KEY = 'jetpack_boost_critical_css_post_ids';
/**
* Provider name.
*
* @var string
*/
protected static $name = 'post_id';
/** @inheritdoc */
public static function get_critical_source_urls( $context_posts = array() ) {
$results = array();
$query = self::get_posts();
$context_post_ids = wp_list_pluck( $context_posts, 'ID' );
if ( false === $query ) {
return array();
}
foreach ( $query->posts as $post ) {
if ( empty( $context_post_ids ) || in_array( $post->ID, $context_post_ids, true ) ) {
$url = get_permalink( $post );
if ( ! empty( $url ) ) {
$results[ $post->ID ] = array( $url );
}
}
}
return $results;
}
/** @inheritdoc */
public static function get_current_storage_keys() {
if ( ! is_singular() ) {
return array();
}
// For example: "post_id_123".
return array( self::$name . '_' . get_the_ID() );
}
/** @inheritdoc */
public static function describe_key( $provider_key ) {
$post_id = (int) substr( $provider_key, strlen( self::$name ) + 1 );
$post = get_post( $post_id );
if ( ! $post ) {
/* translators: %d is the id of a post which cannot be found. */
return sprintf( __( 'Post %d', 'jetpack-boost' ), $post_id );
}
return $post->post_title;
}
/** @inheritdoc */
public static function get_edit_url( $provider_key ) {
$post_id = (int) substr( $provider_key, strlen( self::$name ) + 1 );
return get_edit_post_link( $post_id, 'link' );
}
/**
* Returns a key that can be used to identify the current page, if any exists.
*
* @return string|null
*/
public static function get_current_page_key() {
$keys = static::get_current_storage_keys();
if ( count( $keys ) > 0 ) {
return $keys[0];
} else {
return null;
}
}
/**
* Get post ids.
*
* @return array
*/
public static function get_post_ids() {
// Store the IDs somewhere.
return get_option( self::STORAGE_KEY, array() );
}
/**
* Add post id to storage.
*
* @param int $post_id Post Id.
*
* @return bool
*/
public static function add_post_id( $post_id ) {
$post_ids = static::get_post_ids();
if ( in_array( $post_id, $post_ids, true ) ) {
return false;
}
$post_ids[] = (int) $post_id;
return update_option( self::STORAGE_KEY, $post_ids );
}
/** @inheritdoc */
public static function get_keys() {
return self::get_post_ids();
}
/**
* Create a new WP_Query to gather sample posts.
*
* @return false|\WP_Query
*/
public static function get_posts() {
$ids = self::get_post_ids();
if ( ! $ids ) {
return false;
}
return new \WP_Query(
array(
'post__in' => $ids,
'posts_per_page' => count( $ids ),
'post_status' => array( 'publish' ),
'post_type' => 'any',
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
)
);
}
/** @inheritdoc */
public static function get_success_ratio() {
return 1;
}
}
@@ -0,0 +1,88 @@
<?php
/**
* Abstract Critical CSS provider class.
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
/**
* Class Provider
*
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
*/
abstract class Provider {
/**
* The name of the provider.
*
* @since 1.0.0
* @access protected
* @var string $name The name of the provider
*/
protected static $name;
/**
* Each provider must return a list of URLs to generate CSS from.
*
* @param \WP_Post[] $context_posts The posts to generate CSS from.
* @return array
*/
abstract public static function get_critical_source_urls( $context_posts = array() );
/**
* What key should this provider look for during the current request?
* Used in the front-end to determine where the CSS
* might be stored for the current request.
*
* @return array
*/
abstract public static function get_current_storage_keys();
/**
* Returns a list of all keys that this provider can provide, regardless
* of the current URL.
*/
abstract public static function get_keys();
/**
* Get a human-displayable string describing the given provider key.
*
* @param string $provider_key the key to describe.
*/
abstract public static function describe_key( $provider_key );
/**
* Get the URL of the edit page for the given provider key.
*
* @param string $provider_key the key to edit.
*/
abstract public static function get_edit_url( $provider_key );
/**
* Returns true if the key looks like it belongs to this provider.
*
* @param string $key The key.
*/
public static function owns_key( $key ) {
return strncmp( static::$name, $key, strlen( static::$name ) ) === 0;
}
/**
* Get the name of the provider.
*
* @return string
*/
public static function get_provider_name() {
return static::$name;
}
/**
* Returns the ratio of valid urls from the provider source urls
* for the Critical CSS generation to be considered successful.
*
* @return float|int
*/
abstract public static function get_success_ratio();
}
@@ -0,0 +1,178 @@
<?php
/**
* Critical CSS Provider for singular posts.
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
/**
* Class Singular_Post_Provider
*
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
*/
class Singular_Post_Provider extends Provider {
/**
* Provider name.
*
* @var string
*/
protected static $name = 'singular';
/**
* Max number of posts to query.
*
* @var integer
*/
const MAX_URLS = 10;
/**
* Minimum number of posts to have Critical CSS generated in order for the whole process to be successful.
*
* @var integer
*/
const MIN_SUCCESS_URLS = 5;
/** @inheritdoc */
public static function get_critical_source_urls( $context_posts = array() ) {
$links = array();
$context_post_types = wp_list_pluck( $context_posts, 'post_type' );
$post_types = self::get_post_types();
if ( ! empty( $context_post_types ) ) {
$post_types = array_intersect( $post_types, $context_post_types );
}
foreach ( $post_types as $post_type ) {
$query = self::post_type_query( $post_type );
foreach ( $query->posts as $post ) {
$url = get_permalink( $post );
if ( ! empty( $url ) ) {
$links[ $post_type ][] = $url;
}
}
}
return $links;
}
/** @inheritdoc */
public static function get_current_storage_keys() {
if ( ! is_singular() ) {
return array();
}
// For example: "singular_post".
return array( self::$name . '_' . get_post_type() );
}
/** @inheritdoc */
public static function get_keys() {
return array_keys( self::get_post_types() );
}
/** @inheritdoc */
public static function get_edit_url( $_provider_key ) {
return null;
}
/** @inheritdoc */
public static function describe_key( $provider_key ) {
$post_type = substr( $provider_key, strlen( static::$name ) + 1 );
switch ( $post_type ) {
case 'post':
return __( 'Single post view', 'jetpack-boost' );
case 'page':
return __( 'Single page view', 'jetpack-boost' );
case 'product':
return __( 'Single product view', 'jetpack-boost' );
default:
return __( 'Custom post type', 'jetpack-boost' );
}
}
/**
* Get post types that need Critical CSS.
*
* @return mixed|void
*/
public static function get_post_types() {
$post_types = get_post_types(
array(
'public' => true,
),
'objects'
);
unset( $post_types['attachment'] );
$post_types = array_filter( $post_types, 'is_post_type_viewable' );
$provider_post_types = array();
// Generate a name => name array for backwards compatibility.
foreach ( $post_types as $post_type ) {
$provider_post_types[ $post_type->name ] = $post_type->name;
}
/**
* Filters the post types used for Critical CSS
*
* @param array $post_types The array of post types to be used
*
* @since 1.0.0
*/
return apply_filters(
'jetpack_boost_critical_css_post_types_singular',
apply_filters_deprecated(
'jetpack_boost_critical_css_post_types',
array(
$provider_post_types,
),
'3.4.0',
'jetpack_boost_critical_css_post_types_singular'
),
$post_types
);
}
/**
* Create a new WP_Query to gather sample posts.
*
* @param string $post_type post type.
*
* @return \WP_Query
*/
public static function post_type_query( $post_type ) {
/**
* Filters the WP_Query parameters used to gather sample posts
*
* @param array $args The arguments that will be used by WP_Query
*
* @since 1.0.0
*/
$args = apply_filters(
'jetpack_boost_critical_css_post_type_query',
array(
'orderby' => 'ID',
'post_type' => $post_type,
'posts_per_page' => static::MAX_URLS,
'post_status' => array( 'publish' ),
'no_found_rows' => true,
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
)
);
return new \WP_Query( $args );
}
/** @inheritdoc */
public static function get_success_ratio() {
return static::MIN_SUCCESS_URLS / static::MAX_URLS;
}
}
@@ -0,0 +1,171 @@
<?php
/**
* Provides taxonomy support for critical CSS
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
/**
* Class Taxonomy_Provider
*
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
*/
class Taxonomy_Provider extends Provider {
/**
* Provider name.
*
* @var string
*/
protected static $name = 'taxonomy';
/**
* Max number of posts to query.
*
* @var integer
*/
const MAX_URLS = 10;
/**
* Minimum number of posts to have Critical CSS generated in order for the whole process to be successful.
*
* @var integer
*/
const MIN_SUCCESS_URLS = 5;
/** @inheritdoc */
public static function get_critical_source_urls( $context_posts = array() ) {
$results = array();
$taxonomies = self::get_available_taxonomies();
if ( ! empty( $context_posts ) ) {
$context_post_types = array_unique( wp_list_pluck( $context_posts, 'post_type' ) );
$context_taxonomies = get_object_taxonomies( $context_post_types, 'names' );
$taxonomies = array_intersect( $taxonomies, $context_taxonomies );
}
foreach ( $taxonomies as $taxonomy ) {
$terms = self::get_terms( $taxonomy );
if ( ! $terms ) {
continue;
}
foreach ( $terms as $term ) {
$url = get_term_link( $term, $taxonomy );
if ( ! is_wp_error( $url ) && ! empty( $url ) ) {
$results[ $taxonomy ][] = $url;
}
}
}
return $results;
}
/** @inheritdoc */
public static function get_current_storage_keys() {
if ( ! is_category() && ! is_tax() ) {
return array();
}
if ( ! get_queried_object() ) {
return array();
}
// For example: "taxonomy_category".
return array( self::$name . '_' . get_queried_object()->taxonomy );
}
/** @inheritdoc */
public static function get_keys() {
return array_keys(
array_filter(
self::get_available_taxonomies(),
function ( $taxonomy ) {
return ! empty( Taxonomy_Provider::get_terms( $taxonomy ) );
}
)
);
}
/** @inheritdoc */
public static function describe_key( $provider_key ) {
$taxonomy = substr( $provider_key, strlen( static::$name ) + 1 );
switch ( $taxonomy ) {
case 'category':
return __( 'Category view', 'jetpack-boost' );
default:
return __( 'View for custom taxonomy', 'jetpack-boost' );
}
}
/** @inheritdoc */
public static function get_edit_url( $_provider_key ) {
return null;
}
/**
* Which taxonomies should Critical CSS be generated for.
*
* @return array
*/
public static function get_available_taxonomies() {
$taxonomies = get_taxonomies(
array(
'public' => true,
'show_in_rest' => true,
),
'objects'
);
$taxonomies = array_filter( $taxonomies, 'is_taxonomy_viewable' );
$provider_taxonomies = array();
// Generate a name => name array for backwards compatibility.
foreach ( $taxonomies as $taxonomy ) {
$provider_taxonomies[ $taxonomy->name ] = $taxonomy->name;
}
return $provider_taxonomies;
}
/**
* Get a couple sample terms for a taxonomy.
*
* @param string $taxonomy Taxonomy.
*
* @return array
*/
public static function get_terms( $taxonomy ) {
/**
* Filters the WP_Term_Query args to get a sample of terms for a taxonomy
*
* @param array $args The arguments that will be used by WP_Term_Query
*
* @since 1.0.0
*/
$args = apply_filters(
'jetpack_boost_critical_css_terms_query',
array(
'fields' => 'ids',
'taxonomy' => $taxonomy,
'orderby' => 'term_order',
'number' => static::MAX_URLS,
'hide_empty' => true,
'hierarchical' => false,
'update_term_meta_cache' => false,
)
);
return ( new \WP_Term_Query( $args ) )->terms;
}
/** @inheritdoc */
public static function get_success_ratio() {
return static::MIN_SUCCESS_URLS / static::MAX_URLS;
}
}
@@ -0,0 +1,118 @@
<?php
/**
* Provides core support for critical CSS
*
* @package automattic/jetpack-boost
*/
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
/**
* Class WP_Core_Provider.
*
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
*/
class WP_Core_Provider extends Provider {
/**
* Provider name.
*
* @var string
*/
protected static $name = 'core';
/** @inheritdoc */
public static function get_critical_source_urls( $context_posts = array() ) {
$urls = array();
$front_page = (int) get_option( 'page_on_front' );
$posts_page = (int) get_option( 'page_for_posts' );
if ( ! empty( $front_page ) && empty( $context_posts ) ) {
$permalink = get_permalink( $front_page );
if ( ! empty( $permalink ) ) {
$urls['front_page'] = array( $permalink );
}
}
$context_post_types = wp_list_pluck( $context_posts, 'post_type' );
$context_post_ids = wp_list_pluck( $context_posts, 'ID' );
// The blog page is only in context if the context posts include a 'post' post_type.
// Or, if the blog page itself is in context.
if ( empty( $context_post_types ) || in_array( 'post', $context_post_types, true ) || in_array( $posts_page, $context_post_ids, true ) ) {
if ( ! empty( $posts_page ) ) {
$permalink = get_permalink( $posts_page );
if ( ! empty( $permalink ) ) {
$urls['posts_page'] = array( $permalink );
}
}
}
if ( ! $front_page && ! isset( $urls['posts_page'] ) ) {
$urls['posts_page'] = array( home_url( '/' ) );
}
return $urls;
}
/** @inheritdoc */
public static function get_keys() {
$keys = array( 'posts_page' );
if ( ! empty( get_option( 'page_on_front' ) ) ) {
$keys[] = 'front_page';
}
return $keys;
}
/** @inheritdoc */
public static function get_current_storage_keys() {
if ( is_home() ) {
$key = 'posts_page';
} elseif ( is_front_page() ) {
$key = 'front_page';
}
if ( ! isset( $key ) ) {
return array();
}
// For example: "core_posts_page".
return array( self::$name . '_' . $key );
}
/** @inheritdoc */
public static function get_edit_url( $provider_key ) {
if ( $provider_key === 'core_front_page' ) {
$front_page_id = get_option( 'page_on_front' );
if ( ! empty( $front_page_id ) ) {
return get_edit_post_link( $front_page_id, 'link' );
}
}
return null;
}
/** @inheritdoc */
public static function describe_key( $provider_key ) {
$page = substr( $provider_key, strlen( static::$name ) + 1 );
switch ( $page ) {
case 'posts_page':
return __( 'Posts page', 'jetpack-boost' );
case 'front_page':
return __( 'Front page', 'jetpack-boost' );
default:
return $provider_key;
}
}
/** @inheritdoc */
public static function get_success_ratio() {
return 1;
}
}
@@ -0,0 +1,171 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Minify;
/**
* Takes care of cleaning up options created during concatenation.
*
* @since 4.1.2
*/
class Cleanup_Stored_Paths {
/**
* The maximum number of options to process in a single batch.
*
* @var int
*/
private $max_options_to_process = 50;
/**
* The key of the option that stores the ID of the last processed option.
*
* @var string
*/
private $last_processed_option_key = 'jetpack_boost_cleanup_concat_paths_last_processed_option_id';
/**
* Whether to schedule a followup cleanup.
*
* @var bool
*/
private $should_schedule_followup = false;
/**
* Schedules the start of the cleanup.
*/
public static function setup_schedule() {
if ( false === wp_next_scheduled( 'jetpack_boost_minify_cron_cleanup_concat_paths' ) ) {
wp_schedule_event( time(), 'daily', 'jetpack_boost_minify_cron_cleanup_concat_paths' );
}
}
/**
* Hooks the callbacks for the cleanup.
*/
public static function add_cleanup_actions() {
add_action( 'jetpack_boost_minify_cron_cleanup_concat_paths', array( __CLASS__, 'run_cleanup' ) );
add_action( 'jetpack_boost_minify_cron_cleanup_concat_paths_followup', array( __CLASS__, 'run_cleanup' ) );
}
/**
* Clears the cleanup schedules.
*/
public static function clear_schedules() {
wp_unschedule_hook( 'jetpack_boost_minify_cron_cleanup_concat_paths' );
wp_unschedule_hook( 'jetpack_boost_minify_cron_cleanup_concat_paths_followup' );
}
/**
* Runs the cleanup for the first batch,
* and if there are more entries to process,
* schedules the cleanup for the next batch.
*/
public static function run_cleanup() {
$cleanup = new Cleanup_Stored_Paths();
$can_continue = $cleanup->cleanup_stored_paths_batch();
if ( ! $can_continue ) {
return;
}
if ( ! $cleanup->should_schedule_followup ) {
return;
}
// 'batch' arg is necessary, to tell WP that this is a unique event
// and allow it to be run within 10 minutes of the last.
// See https://developer.wordpress.org/reference/functions/wp_schedule_single_event/#description
if ( ! wp_next_scheduled( 'jetpack_boost_minify_cron_cleanup_concat_paths_followup', array( 'batch' ) ) ) {
wp_schedule_single_event( time(), 'jetpack_boost_minify_cron_cleanup_concat_paths_followup', array( 'batch' ) );
}
}
/**
* Cleans up expired stored paths.
*
* @return bool True if there are more entries to process, false if not.
*/
public function cleanup_stored_paths_batch() {
$stored_paths = $this->get_stored_paths();
if ( ! $stored_paths ) {
// Cleanup after the cleanup.
delete_option( $this->last_processed_option_key );
return false;
}
// Used to tell the cleanup to skip the entries that were checked in the previous run.
// Avoids processing the same entries over and over again.
$last_processed_option_id = false;
foreach ( $stored_paths as $option ) {
$value = maybe_unserialize( $option['option_value'] );
if ( ! is_array( $value ) ) {
continue;
}
if ( $value['expire'] <= time() ) {
$this->delete_static_file_by_hash( str_replace( 'jb_transient_concat_paths_', '', $option['option_name'] ) );
delete_option( $option['option_name'] );
}
$last_processed_option_id = $option['option_id'];
}
update_option( $this->last_processed_option_key, $last_processed_option_id, false );
return true;
}
/**
* Deletes the static files by hash.
*
* @param string $hash The hash of the file.
* @return void
*/
private function delete_static_file_by_hash( $hash ) {
// Since we don't have a way to know if this file is JS or CSS,
// we delete both.
$js_file_path = jetpack_boost_get_minify_file_path( $hash . '.min.js' );
if ( file_exists( $js_file_path ) ) {
wp_delete_file( $js_file_path );
}
$css_file_path = jetpack_boost_get_minify_file_path( $hash . '.min.css' );
if ( file_exists( $css_file_path ) ) {
wp_delete_file( $css_file_path );
}
}
/**
* Gets the stored paths to process, and skips the ones that were checked in the previous run.
* Also sets a flag that determines if there should be a followup cleanup or not.
*
* @return array The stored paths.
*/
private function get_stored_paths() {
$last_processed_option_id = get_option( $this->last_processed_option_key );
global $wpdb;
$query = "SELECT * FROM {$wpdb->options} WHERE option_name LIKE 'jb_transient_concat_paths_%'";
if ( $last_processed_option_id ) {
$query .= $wpdb->prepare( ' AND option_id > %d', $last_processed_option_id );
}
// Add 1 to use it as a flag to know if there are more entries to process.
$max_options_to_process_offset = $this->max_options_to_process + 1;
$query .= $wpdb->prepare( ' ORDER BY option_id ASC LIMIT %d', $max_options_to_process_offset );
$stored_paths = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
// If the number of stored paths is equal to the offset of max options to process,
// it means that there are more entries to process, so a followup cleanup is needed.
if ( count( $stored_paths ) === $max_options_to_process_offset ) {
$this->should_schedule_followup = true;
// Since 1 was added to the limit, it needs to be removed from the list.
array_pop( $stored_paths );
}
return $stored_paths;
}
}
@@ -0,0 +1,273 @@
<?php
namespace Automattic\Jetpack_Boost\Lib\Minify;
use WP_Styles;
// Disable complaints about enqueuing stylesheets, as this class alters the way enqueuing them works.
// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
/**
* Replacement for, and subclass of WP_Styles - used to control the way that styles are enqueued and output.
*/
class Concatenate_CSS extends WP_Styles {
private $dependency_path_mapping;
private $old_styles;
public $allow_gzip_compression;
public function __construct( $styles ) {
if ( empty( $styles ) || ! ( $styles instanceof WP_Styles ) ) {
$this->old_styles = new WP_Styles();
} else {
$this->old_styles = $styles;
}
// Unset all the object properties except our private copy of the styles object.
// We have to unset everything so that the overload methods talk to $this->old_styles->whatever
// instead of $this->whatever.
foreach ( array_keys( get_object_vars( $this ) ) as $key ) {
if ( 'old_styles' === $key ) {
continue;
}
unset( $this->$key );
}
$this->dependency_path_mapping = new Dependency_Path_Mapping(
/**
* Filter the URL of the site the plugin will be concatenating CSS or JS on
*
* @param string $url URL of the page with CSS or JS to concatonate.
*
* @since 1.0.0
*/
apply_filters( 'page_optimize_site_url', $this->base_url )
);
}
public function do_items( $handles = false, $group = false ) {
$handles = false === $handles ? $this->queue : (array) $handles;
$stylesheets = array();
/**
* Filter the URL of the site the plugin will be concatenating CSS or JS on
*
* @param string $url URL of the page with CSS or JS to concatonate.
*
* @since 1.0.0
*/
$siteurl = apply_filters( 'page_optimize_site_url', $this->base_url );
$this->all_deps( $handles );
$stylesheet_group_index = 0;
// Merge CSS into a single file
$concat_group = 'concat';
// Concat group on top (first array element gets processed earlier)
$stylesheets[ $concat_group ] = array();
foreach ( $this->to_do as $key => $handle ) {
$obj = $this->registered[ $handle ];
/**
* Filter the style source URL
*
* @param string $url URL of the page with CSS or JS to concatonate.
* @param string $handle handle to CSS file.
*
* @since 1.0.0
*/
$obj->src = apply_filters( 'style_loader_src', $obj->src, $obj->handle );
// Core is kind of broken and returns "true" for src of "colors" handle
// http://core.trac.wordpress.org/attachment/ticket/16827/colors-hacked-fixed.diff
// http://core.trac.wordpress.org/ticket/20729
$css_url = $obj->src;
if ( 'colors' === $obj->handle && true === $css_url ) {
$css_url = wp_style_loader_src( $css_url, $obj->handle );
}
$css_url = jetpack_boost_enqueued_to_absolute_url( $css_url );
$css_url_parsed = wp_parse_url( $css_url );
$extra = $obj->extra;
// Don't concat by default
$do_concat = false;
// Only try to concat static css files
if ( str_contains( $css_url_parsed['path'], '.css' ) ) {
$do_concat = true;
} elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
printf( "\n<!-- No Concat CSS %s => Maybe Not Static File %s -->\n", esc_html( $handle ), esc_html( $obj->src ) );
}
// Don't try to concat styles which are loaded conditionally (like IE stuff)
if ( isset( $extra['conditional'] ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
printf( "\n<!-- No Concat CSS %s => Has Conditional -->\n", esc_html( $handle ) );
}
$do_concat = false;
}
// Don't concat rtl stuff for now until concat supports it correctly
if ( $do_concat && 'rtl' === $this->text_direction && ! empty( $extra['rtl'] ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
printf( "\n<!-- No Concat CSS %s => Is RTL -->\n", esc_html( $handle ) );
}
$do_concat = false;
}
// Don't try to concat externally hosted scripts
$is_internal_uri = $this->dependency_path_mapping->is_internal_uri( $css_url );
if ( $do_concat && ! $is_internal_uri ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
printf( "\n<!-- No Concat CSS %s => External URL: %s -->\n", esc_html( $handle ), esc_url( $css_url ) );
}
$do_concat = false;
}
if ( $do_concat ) {
// Resolve paths and concat styles that exist in the filesystem
$css_realpath = $this->dependency_path_mapping->dependency_src_to_fs_path( $css_url );
if ( false === $css_realpath ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
printf( "\n<!-- No Concat CSS %s => Invalid Path %s -->\n", esc_html( $handle ), esc_html( $css_realpath ) );
}
$do_concat = false;
}
}
// Skip concating CSS from exclusion list
$exclude_list = jetpack_boost_page_optimize_css_exclude_list();
foreach ( $exclude_list as $exclude ) {
if ( $do_concat && $handle === $exclude ) {
$do_concat = false;
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
printf( "\n<!-- No Concat CSS %s => Excluded option -->\n", esc_html( $handle ) );
}
}
}
/**
* Filter that allows plugins to disable concatenation of certain stylesheets.
*
* @param bool $do_concat if true, then perform concatenation
* @param string $handle handle to CSS file
*
* @since 1.0.0
*/
if ( $do_concat && ! apply_filters( 'css_do_concat', $do_concat, $handle ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
printf( "\n<!-- No Concat CSS %s => Filtered `false` -->\n", esc_html( $handle ) );
}
}
/**
* Filter that allows plugins to disable concatenation of certain stylesheets.
*
* @param bool $do_concat if true, then perform concatenation
* @param string $handle handle to CSS file
*
* @since 1.0.0
*/
$do_concat = apply_filters( 'css_do_concat', $do_concat, $handle );
if ( true === $do_concat ) {
$media = $obj->args;
if ( empty( $media ) ) {
$media = 'all';
}
$stylesheets[ $concat_group ][ $media ][ $handle ] = $css_url_parsed['path'];
$this->done[] = $handle;
} else {
++$stylesheet_group_index;
$stylesheets[ $stylesheet_group_index ]['noconcat'][] = $handle;
++$stylesheet_group_index;
}
unset( $this->to_do[ $key ] );
}
foreach ( $stylesheets as $_idx => $stylesheets_group ) {
foreach ( $stylesheets_group as $media => $css ) {
$href = '';
if ( 'noconcat' === $media ) {
foreach ( $css as $handle ) {
if ( $this->do_item( $handle, $group ) ) {
$this->done[] = $handle;
}
}
continue;
} elseif ( count( $css ) > 0 ) {
// Split the CSS into groups of max files, we are also chunking the handles to match the CSS groups and depending on order to be maintained.
$css_groups = array_chunk( $css, jetpack_boost_minify_concat_max_files(), true );
foreach ( $css_groups as $css_group ) {
$file_name = jetpack_boost_page_optimize_generate_concat_path( $css_group, $this->dependency_path_mapping );
if ( get_site_option( 'jetpack_boost_static_minification' ) ) {
$href = jetpack_boost_get_minify_url( $file_name . '.min.css' );
} else {
$href = $siteurl . jetpack_boost_get_static_prefix() . '??' . $file_name;
}
$this->print_style_tag( $href, array_keys( $css_group ), $media );
}
}
}
}
return $this->done;
}
private function print_style_tag( $href, $handles, $media ) {
$css_id = sanitize_title_with_dashes( $media ) . '-css-' . md5( $href );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
$style_tag = "<link data-handles='" . esc_attr( implode( ',', $handles ) ) . "' rel='stylesheet' id='$css_id' href='$href' type='text/css' media='$media' />";
} else {
$style_tag = "<link rel='stylesheet' id='$css_id' href='$href' type='text/css' media='$media' />";
}
/**
* Filter the style loader HTML tag for page optimize.
*
* @param string $style_tag style loader tag
* @param array $handles handles of CSS files
* @param string $href link to CSS file
* @param string $media media attribute of the link.
*
* @since 1.0.0
*/
$style_tag = apply_filters( 'page_optimize_style_loader_tag', $style_tag, $handles, $href, $media );
/**
* Filter the stylesheet tag. For example: making it deferred when using Critical CSS.
*
* @param string $style_tag stylesheet tag
* @param array $handles handles of CSS files
* @param string $href link to CSS file
* @param string $media media attribute of the link.
*
* @since 1.0.0
*/
$style_tag = apply_filters( 'style_loader_tag', $style_tag, implode( ',', $handles ), $href, $media );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $style_tag . "\n";
array_map( array( $this, 'print_inline_style' ), $handles );
}
public function __isset( $key ) {
return isset( $this->old_styles->$key );
}
public function __unset( $key ) {
unset( $this->old_styles->$key );
}
public function &__get( $key ) {
return $this->old_styles->$key;
}
public function __set( $key, $value ) {
$this->old_styles->$key = $value;
}
}

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