Файловый менеджер - Редактировать - /home/bean7936/perfect-community.com/442aa3/inc.tar
Назад
Dependencies/ActionScheduler/action-scheduler.php 0000644 00000005717 15174671745 0016203 0 ustar 00 <?php /** * Plugin Name: Action Scheduler * Plugin URI: https://actionscheduler.org * Description: A robust scheduling library for use in WordPress plugins. * Author: Automattic * Author URI: https://automattic.com/ * Version: 3.9.0 * License: GPLv3 * Requires at least: 6.5 * Tested up to: 6.7 * Requires PHP: 7.1 * * Copyright 2019 Automattic, Inc. (https://automattic.com/contact/) * * 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 3 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/>. * * @package ActionScheduler */ if ( ! function_exists( 'action_scheduler_register_3_dot_9_dot_0' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION. if ( ! class_exists( 'ActionScheduler_Versions', false ) ) { require_once __DIR__ . '/classes/ActionScheduler_Versions.php'; add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); } add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_9_dot_0', 0, 0 ); // WRCS: DEFINED_VERSION. // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace /** * Registers this version of Action Scheduler. */ function action_scheduler_register_3_dot_9_dot_0() { // WRCS: DEFINED_VERSION. $versions = ActionScheduler_Versions::instance(); $versions->register( '3.9.0', 'action_scheduler_initialize_3_dot_9_dot_0' ); // WRCS: DEFINED_VERSION. } // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace /** * Initializes this version of Action Scheduler. */ function action_scheduler_initialize_3_dot_9_dot_0() { // WRCS: DEFINED_VERSION. // A final safety check is required even here, because historic versions of Action Scheduler // followed a different pattern (in some unusual cases, we could reach this point and the // ActionScheduler class is already defined—so we need to guard against that). if ( ! class_exists( 'ActionScheduler', false ) ) { require_once __DIR__ . '/classes/abstracts/ActionScheduler.php'; ActionScheduler::init( __FILE__ ); } } // Support usage in themes - load this version if no plugin has loaded a version yet. if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) && ! class_exists( 'ActionScheduler', false ) ) { action_scheduler_initialize_3_dot_9_dot_0(); // WRCS: DEFINED_VERSION. do_action( 'action_scheduler_pre_theme_init' ); ActionScheduler_Versions::initialize_latest_version(); } } Dependencies/ActionScheduler/lib/cron-expression/CronExpression_YearField.php 0000644 00000001651 15174671745 0023554 0 ustar 00 <?php /** * Year field. Allows: * , / - * * @author Michael Dowling <mtdowling@gmail.com> */ class CronExpression_YearField extends CronExpression_AbstractField { /** * {@inheritdoc} */ public function isSatisfiedBy(DateTime $date, $value) { return $this->isSatisfied($date->format('Y'), $value); } /** * {@inheritdoc} */ public function increment(DateTime $date, $invert = false) { if ($invert) { $date->modify('-1 year'); $date->setDate($date->format('Y'), 12, 31); $date->setTime(23, 59, 0); } else { $date->modify('+1 year'); $date->setDate($date->format('Y'), 1, 1); $date->setTime(0, 0, 0); } return $this; } /** * {@inheritdoc} */ public function validate($value) { return (bool) preg_match('/[\*,\/\-0-9]+/', $value); } } Dependencies/ActionScheduler/lib/cron-expression/CronExpression_MonthField.php 0000644 00000002567 15174671745 0023750 0 ustar 00 <?php /** * Month field. Allows: * , / - * * @author Michael Dowling <mtdowling@gmail.com> */ class CronExpression_MonthField extends CronExpression_AbstractField { /** * {@inheritdoc} */ public function isSatisfiedBy(DateTime $date, $value) { // Convert text month values to integers $value = str_ireplace( array( 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC' ), range(1, 12), $value ); return $this->isSatisfied($date->format('m'), $value); } /** * {@inheritdoc} */ public function increment(DateTime $date, $invert = false) { if ($invert) { // $date->modify('last day of previous month'); // remove for php 5.2 compat $date->modify('previous month'); $date->modify($date->format('Y-m-t')); $date->setTime(23, 59); } else { //$date->modify('first day of next month'); // remove for php 5.2 compat $date->modify('next month'); $date->modify($date->format('Y-m-01')); $date->setTime(0, 0); } return $this; } /** * {@inheritdoc} */ public function validate($value) { return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value); } } Dependencies/ActionScheduler/lib/cron-expression/LICENSE 0000644 00000002112 15174671745 0017134 0 ustar 00 Copyright (c) 2011 Michael Dowling <mtdowling@gmail.com> and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldInterface.php 0000644 00000002162 15174671745 0024552 0 ustar 00 <?php /** * CRON field interface * * @author Michael Dowling <mtdowling@gmail.com> */ interface CronExpression_FieldInterface { /** * Check if the respective value of a DateTime field satisfies a CRON exp * * @param DateTime $date DateTime object to check * @param string $value CRON expression to test against * * @return bool Returns TRUE if satisfied, FALSE otherwise */ public function isSatisfiedBy(DateTime $date, $value); /** * When a CRON expression is not satisfied, this method is used to increment * or decrement a DateTime object by the unit of the cron field * * @param DateTime $date DateTime object to change * @param bool $invert (optional) Set to TRUE to decrement * * @return CronExpression_FieldInterface */ public function increment(DateTime $date, $invert = false); /** * Validates a CRON expression for a given field * * @param string $value CRON expression value to validate * * @return bool Returns TRUE if valid, FALSE otherwise */ public function validate($value); } Dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfWeekField.php 0000644 00000007521 15174671745 0024474 0 ustar 00 <?php /** * Day of week field. Allows: * / , - ? L # * * Days of the week can be represented as a number 0-7 (0|7 = Sunday) * or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT. * * 'L' stands for "last". It allows you to specify constructs such as * "the last Friday" of a given month. * * '#' is allowed for the day-of-week field, and must be followed by a * number between one and five. It allows you to specify constructs such as * "the second Friday" of a given month. * * @author Michael Dowling <mtdowling@gmail.com> */ class CronExpression_DayOfWeekField extends CronExpression_AbstractField { /** * {@inheritdoc} */ public function isSatisfiedBy(DateTime $date, $value) { if ($value == '?') { return true; } // Convert text day of the week values to integers $value = str_ireplace( array('SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'), range(0, 6), $value ); $currentYear = $date->format('Y'); $currentMonth = $date->format('m'); $lastDayOfMonth = $date->format('t'); // Find out if this is the last specific weekday of the month if (strpos($value, 'L')) { $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L'))); $tdate = clone $date; $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); while ($tdate->format('w') != $weekday) { $tdate->setDate($currentYear, $currentMonth, --$lastDayOfMonth); } return $date->format('j') == $lastDayOfMonth; } // Handle # hash tokens if (strpos($value, '#')) { list($weekday, $nth) = explode('#', $value); // Validate the hash fields if ($weekday < 1 || $weekday > 5) { throw new InvalidArgumentException("Weekday must be a value between 1 and 5. {$weekday} given"); } if ($nth > 5) { throw new InvalidArgumentException('There are never more than 5 of a given weekday in a month'); } // The current weekday must match the targeted weekday to proceed if ($date->format('N') != $weekday) { return false; } $tdate = clone $date; $tdate->setDate($currentYear, $currentMonth, 1); $dayCount = 0; $currentDay = 1; while ($currentDay < $lastDayOfMonth + 1) { if ($tdate->format('N') == $weekday) { if (++$dayCount >= $nth) { break; } } $tdate->setDate($currentYear, $currentMonth, ++$currentDay); } return $date->format('j') == $currentDay; } // Handle day of the week values if (strpos($value, '-')) { $parts = explode('-', $value); if ($parts[0] == '7') { $parts[0] = '0'; } elseif ($parts[1] == '0') { $parts[1] = '7'; } $value = implode('-', $parts); } // Test to see which Sunday to use -- 0 == 7 == Sunday $format = in_array(7, str_split($value)) ? 'N' : 'w'; $fieldValue = $date->format($format); return $this->isSatisfied($fieldValue, $value); } /** * {@inheritdoc} */ public function increment(DateTime $date, $invert = false) { if ($invert) { $date->modify('-1 day'); $date->setTime(23, 59, 0); } else { $date->modify('+1 day'); $date->setTime(0, 0, 0); } return $this; } /** * {@inheritdoc} */ public function validate($value) { return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value); } } Dependencies/ActionScheduler/lib/cron-expression/CronExpression_DayOfMonthField.php 0000644 00000007014 15174671745 0024663 0 ustar 00 <?php /** * Day of month field. Allows: * , / - ? L W * * 'L' stands for "last" and specifies the last day of the month. * * The 'W' character is used to specify the weekday (Monday-Friday) nearest the * given day. As an example, if you were to specify "15W" as the value for the * day-of-month field, the meaning is: "the nearest weekday to the 15th of the * month". So if the 15th is a Saturday, the trigger will fire on Friday the * 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If * the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you * specify "1W" as the value for day-of-month, and the 1st is a Saturday, the * trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary * of a month's days. The 'W' character can only be specified when the * day-of-month is a single day, not a range or list of days. * * @author Michael Dowling <mtdowling@gmail.com> */ class CronExpression_DayOfMonthField extends CronExpression_AbstractField { /** * Get the nearest day of the week for a given day in a month * * @param int $currentYear Current year * @param int $currentMonth Current month * @param int $targetDay Target day of the month * * @return DateTime Returns the nearest date */ private static function getNearestWeekday($currentYear, $currentMonth, $targetDay) { $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT); $target = new DateTime("$currentYear-$currentMonth-$tday"); $currentWeekday = (int) $target->format('N'); if ($currentWeekday < 6) { return $target; } $lastDayOfMonth = $target->format('t'); foreach (array(-1, 1, -2, 2) as $i) { $adjusted = $targetDay + $i; if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { $target->setDate($currentYear, $currentMonth, $adjusted); if ($target->format('N') < 6 && $target->format('m') == $currentMonth) { return $target; } } } } /** * {@inheritdoc} */ public function isSatisfiedBy(DateTime $date, $value) { // ? states that the field value is to be skipped if ($value == '?') { return true; } $fieldValue = $date->format('d'); // Check to see if this is the last day of the month if ($value == 'L') { return $fieldValue == $date->format('t'); } // Check to see if this is the nearest weekday to a particular value if (strpos($value, 'W')) { // Parse the target day $targetDay = substr($value, 0, strpos($value, 'W')); // Find out if the current day is the nearest day of the week return $date->format('j') == self::getNearestWeekday( $date->format('Y'), $date->format('m'), $targetDay )->format('j'); } return $this->isSatisfied($date->format('d'), $value); } /** * {@inheritdoc} */ public function increment(DateTime $date, $invert = false) { if ($invert) { $date->modify('previous day'); $date->setTime(23, 59); } else { $date->modify('next day'); $date->setTime(0, 0); } return $this; } /** * {@inheritdoc} */ public function validate($value) { return (bool) preg_match('/[\*,\/\-\?LW0-9A-Za-z]+/', $value); } } Dependencies/ActionScheduler/lib/cron-expression/CronExpression_FieldFactory.php 0000644 00000003321 15174671745 0024257 0 ustar 00 <?php /** * CRON field factory implementing a flyweight factory * * @author Michael Dowling <mtdowling@gmail.com> * @link http://en.wikipedia.org/wiki/Cron */ class CronExpression_FieldFactory { /** * @var array Cache of instantiated fields */ private $fields = array(); /** * Get an instance of a field object for a cron expression position * * @param int $position CRON expression position value to retrieve * * @return CronExpression_FieldInterface * @throws InvalidArgumentException if a position is not valid */ public function getField($position) { if (!isset($this->fields[$position])) { switch ($position) { case 0: $this->fields[$position] = new CronExpression_MinutesField(); break; case 1: $this->fields[$position] = new CronExpression_HoursField(); break; case 2: $this->fields[$position] = new CronExpression_DayOfMonthField(); break; case 3: $this->fields[$position] = new CronExpression_MonthField(); break; case 4: $this->fields[$position] = new CronExpression_DayOfWeekField(); break; case 5: $this->fields[$position] = new CronExpression_YearField(); break; default: throw new InvalidArgumentException( $position . ' is not a valid position' ); } } return $this->fields[$position]; } } Dependencies/ActionScheduler/lib/cron-expression/CronExpression_HoursField.php 0000644 00000002205 15174671745 0023750 0 ustar 00 <?php /** * Hours field. Allows: * , / - * * @author Michael Dowling <mtdowling@gmail.com> */ class CronExpression_HoursField extends CronExpression_AbstractField { /** * {@inheritdoc} */ public function isSatisfiedBy(DateTime $date, $value) { return $this->isSatisfied($date->format('H'), $value); } /** * {@inheritdoc} */ public function increment(DateTime $date, $invert = false) { // Change timezone to UTC temporarily. This will // allow us to go back or forwards and hour even // if DST will be changed between the hours. $timezone = $date->getTimezone(); $date->setTimezone(new DateTimeZone('UTC')); if ($invert) { $date->modify('-1 hour'); $date->setTime($date->format('H'), 59); } else { $date->modify('+1 hour'); $date->setTime($date->format('H'), 0); } $date->setTimezone($timezone); return $this; } /** * {@inheritdoc} */ public function validate($value) { return (bool) preg_match('/[\*,\/\-0-9]+/', $value); } } Dependencies/ActionScheduler/lib/cron-expression/CronExpression_MinutesField.php 0000644 00000001371 15174671745 0024277 0 ustar 00 <?php /** * Minutes field. Allows: * , / - * * @author Michael Dowling <mtdowling@gmail.com> */ class CronExpression_MinutesField extends CronExpression_AbstractField { /** * {@inheritdoc} */ public function isSatisfiedBy(DateTime $date, $value) { return $this->isSatisfied($date->format('i'), $value); } /** * {@inheritdoc} */ public function increment(DateTime $date, $invert = false) { if ($invert) { $date->modify('-1 minute'); } else { $date->modify('+1 minute'); } return $this; } /** * {@inheritdoc} */ public function validate($value) { return (bool) preg_match('/[\*,\/\-0-9]+/', $value); } } Dependencies/ActionScheduler/lib/cron-expression/README.md 0000644 00000006277 15174671745 0017426 0 ustar 00 PHP Cron Expression Parser ========================== [](https://packagist.org/packages/mtdowling/cron-expression) [](https://packagist.org/packages/mtdowling/cron-expression) [](http://travis-ci.org/mtdowling/cron-expression) The PHP cron expression parser can parse a CRON expression, determine if it is due to run, calculate the next run date of the expression, and calculate the previous run date of the expression. You can calculate dates far into the future or past by skipping n number of matching dates. The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9), lists (e.g. 1,2,3), W to find the nearest weekday for a given day of the month, L to find the last day of the month, L to find the last given weekday of a month, and hash (#) to find the nth weekday of a given month. Credits ========== Created by Micheal Dowling. Ported to PHP 5.2 by Flightless, Inc. Based on version 1.0.3: https://github.com/mtdowling/cron-expression/tree/v1.0.3 Installing ========== Add the following to your project's composer.json: ```javascript { "require": { "mtdowling/cron-expression": "1.0.*" } } ``` Usage ===== ```php <?php require_once '/vendor/autoload.php'; // Works with predefined scheduling definitions $cron = Cron\CronExpression::factory('@daily'); $cron->isDue(); echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s'); // Works with complex expressions $cron = Cron\CronExpression::factory('3-59/15 2,6-12 */15 1 2-5'); echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); // Calculate a run date two iterations into the future $cron = Cron\CronExpression::factory('@daily'); echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s'); // Calculate a run date relative to a specific time $cron = Cron\CronExpression::factory('@monthly'); echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s'); ``` CRON Expressions ================ A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows: * * * * * * - - - - - - | | | | | | | | | | | + year [optional] | | | | +----- day of week (0 - 7) (Sunday=0 or 7) | | | +---------- month (1 - 12) | | +--------------- day of month (1 - 31) | +-------------------- hour (0 - 23) +------------------------- min (0 - 59) Requirements ============ - PHP 5.3+ - PHPUnit is required to run the unit tests - Composer is required to run the unit tests CHANGELOG ========= 1.0.3 (2013-11-23) ------------------ * Only set default timezone if the given $currentTime is not a DateTime instance (#34) * Fixes issue #28 where PHP increments of ranges were failing due to PHP casting hyphens to 0 * Now supports expressions with any number of extra spaces, tabs, or newlines * Using static instead of self in `CronExpression::factory` Dependencies/ActionScheduler/lib/cron-expression/CronExpression.php 0000644 00000026536 15174671745 0021641 0 ustar 00 <?php /** * CRON expression parser that can determine whether or not a CRON expression is * due to run, the next run date and previous run date of a CRON expression. * The determinations made by this class are accurate if checked run once per * minute (seconds are dropped from date time comparisons). * * Schedule parts must map to: * minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week * [1-7|MON-SUN], and an optional year. * * @author Michael Dowling <mtdowling@gmail.com> * @link http://en.wikipedia.org/wiki/Cron */ class CronExpression { const MINUTE = 0; const HOUR = 1; const DAY = 2; const MONTH = 3; const WEEKDAY = 4; const YEAR = 5; /** * @var array CRON expression parts */ private $cronParts; /** * @var CronExpression_FieldFactory CRON field factory */ private $fieldFactory; /** * @var array Order in which to test of cron parts */ private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE); /** * Factory method to create a new CronExpression. * * @param string $expression The CRON expression to create. There are * several special predefined values which can be used to substitute the * CRON expression: * * @yearly, @annually) - Run once a year, midnight, Jan. 1 - 0 0 1 1 * * @monthly - Run once a month, midnight, first of month - 0 0 1 * * * @weekly - Run once a week, midnight on Sun - 0 0 * * 0 * @daily - Run once a day, midnight - 0 0 * * * * @hourly - Run once an hour, first minute - 0 * * * * * *@param CronExpression_FieldFactory $fieldFactory (optional) Field factory to use * * @return CronExpression */ public static function factory($expression, CronExpression_FieldFactory $fieldFactory = null) { $mappings = array( '@yearly' => '0 0 1 1 *', '@annually' => '0 0 1 1 *', '@monthly' => '0 0 1 * *', '@weekly' => '0 0 * * 0', '@daily' => '0 0 * * *', '@hourly' => '0 * * * *' ); if (isset($mappings[$expression])) { $expression = $mappings[$expression]; } return new self($expression, $fieldFactory ? $fieldFactory : new CronExpression_FieldFactory()); } /** * Parse a CRON expression * * @param string $expression CRON expression (e.g. '8 * * * *') * @param CronExpression_FieldFactory $fieldFactory Factory to create cron fields */ public function __construct($expression, CronExpression_FieldFactory $fieldFactory) { $this->fieldFactory = $fieldFactory; $this->setExpression($expression); } /** * Set or change the CRON expression * * @param string $value CRON expression (e.g. 8 * * * *) * * @return CronExpression * @throws InvalidArgumentException if not a valid CRON expression */ public function setExpression($value) { $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); if (count($this->cronParts) < 5) { throw new InvalidArgumentException( $value . ' is not a valid CRON expression' ); } foreach ($this->cronParts as $position => $part) { $this->setPart($position, $part); } return $this; } /** * Set part of the CRON expression * * @param int $position The position of the CRON expression to set * @param string $value The value to set * * @return CronExpression * @throws InvalidArgumentException if the value is not valid for the part */ public function setPart($position, $value) { if (!$this->fieldFactory->getField($position)->validate($value)) { throw new InvalidArgumentException( 'Invalid CRON field value ' . $value . ' as position ' . $position ); } $this->cronParts[$position] = $value; return $this; } /** * Get a next run date relative to the current date or a specific date * * @param string|DateTime $currentTime (optional) Relative calculation date * @param int $nth (optional) Number of matches to skip before returning a * matching next run date. 0, the default, will return the current * date and time if the next run date falls on the current date and * time. Setting this value to 1 will skip the first match and go to * the second match. Setting this value to 2 will skip the first 2 * matches and so on. * @param bool $allowCurrentDate (optional) Set to TRUE to return the * current date if it matches the cron expression * * @return DateTime * @throws RuntimeException on too many iterations */ public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) { return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate); } /** * Get a previous run date relative to the current date or a specific date * * @param string|DateTime $currentTime (optional) Relative calculation date * @param int $nth (optional) Number of matches to skip before returning * @param bool $allowCurrentDate (optional) Set to TRUE to return the * current date if it matches the cron expression * * @return DateTime * @throws RuntimeException on too many iterations * @see CronExpression::getNextRunDate */ public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) { return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate); } /** * Get multiple run dates starting at the current date or a specific date * * @param int $total Set the total number of dates to calculate * @param string|DateTime $currentTime (optional) Relative calculation date * @param bool $invert (optional) Set to TRUE to retrieve previous dates * @param bool $allowCurrentDate (optional) Set to TRUE to return the * current date if it matches the cron expression * * @return array Returns an array of run dates */ public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false) { $matches = array(); for ($i = 0; $i < max(0, $total); $i++) { $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate); } return $matches; } /** * Get all or part of the CRON expression * * @param string $part (optional) Specify the part to retrieve or NULL to * get the full cron schedule string. * * @return string|null Returns the CRON expression, a part of the * CRON expression, or NULL if the part was specified but not found */ public function getExpression($part = null) { if (null === $part) { return implode(' ', $this->cronParts); } elseif (array_key_exists($part, $this->cronParts)) { return $this->cronParts[$part]; } return null; } /** * Helper method to output the full expression. * * @return string Full CRON expression */ public function __toString() { return $this->getExpression(); } /** * Determine if the cron is due to run based on the current date or a * specific date. This method assumes that the current number of * seconds are irrelevant, and should be called once per minute. * * @param string|DateTime $currentTime (optional) Relative calculation date * * @return bool Returns TRUE if the cron is due to run or FALSE if not */ public function isDue($currentTime = 'now') { if ('now' === $currentTime) { $currentDate = date('Y-m-d H:i'); $currentTime = strtotime($currentDate); } elseif ($currentTime instanceof DateTime) { $currentDate = $currentTime->format('Y-m-d H:i'); $currentTime = strtotime($currentDate); } else { $currentTime = new DateTime($currentTime); $currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0); $currentDate = $currentTime->format('Y-m-d H:i'); $currentTime = (int)($currentTime->getTimestamp()); } return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime; } /** * Get the next or previous run date of the expression relative to a date * * @param string|DateTime $currentTime (optional) Relative calculation date * @param int $nth (optional) Number of matches to skip before returning * @param bool $invert (optional) Set to TRUE to go backwards in time * @param bool $allowCurrentDate (optional) Set to TRUE to return the * current date if it matches the cron expression * * @return DateTime * @throws RuntimeException on too many iterations */ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false) { if ($currentTime instanceof DateTime) { $currentDate = $currentTime; } else { $currentDate = new DateTime($currentTime ? $currentTime : 'now'); $currentDate->setTimezone(new DateTimeZone(date_default_timezone_get())); } $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); $nextRun = clone $currentDate; $nth = (int) $nth; // Set a hard limit to bail on an impossible date for ($i = 0; $i < 1000; $i++) { foreach (self::$order as $position) { $part = $this->getExpression($position); if (null === $part) { continue; } $satisfied = false; // Get the field object used to validate this part $field = $this->fieldFactory->getField($position); // Check if this is singular or a list if (strpos($part, ',') === false) { $satisfied = $field->isSatisfiedBy($nextRun, $part); } else { foreach (array_map('trim', explode(',', $part)) as $listPart) { if ($field->isSatisfiedBy($nextRun, $listPart)) { $satisfied = true; break; } } } // If the field is not satisfied, then start over if (!$satisfied) { $field->increment($nextRun, $invert); continue 2; } } // Skip this match if needed if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { $this->fieldFactory->getField(0)->increment($nextRun, $invert); continue; } return $nextRun; } // @codeCoverageIgnoreStart throw new RuntimeException('Impossible CRON expression'); // @codeCoverageIgnoreEnd } } Dependencies/ActionScheduler/lib/cron-expression/CronExpression_AbstractField.php 0000644 00000005020 15174671745 0024411 0 ustar 00 <?php /** * Abstract CRON expression field * * @author Michael Dowling <mtdowling@gmail.com> */ abstract class CronExpression_AbstractField implements CronExpression_FieldInterface { /** * Check to see if a field is satisfied by a value * * @param string $dateValue Date value to check * @param string $value Value to test * * @return bool */ public function isSatisfied($dateValue, $value) { if ($this->isIncrementsOfRanges($value)) { return $this->isInIncrementsOfRanges($dateValue, $value); } elseif ($this->isRange($value)) { return $this->isInRange($dateValue, $value); } return $value == '*' || $dateValue == $value; } /** * Check if a value is a range * * @param string $value Value to test * * @return bool */ public function isRange($value) { return strpos($value, '-') !== false; } /** * Check if a value is an increments of ranges * * @param string $value Value to test * * @return bool */ public function isIncrementsOfRanges($value) { return strpos($value, '/') !== false; } /** * Test if a value is within a range * * @param string $dateValue Set date value * @param string $value Value to test * * @return bool */ public function isInRange($dateValue, $value) { $parts = array_map('trim', explode('-', $value, 2)); return $dateValue >= $parts[0] && $dateValue <= $parts[1]; } /** * Test if a value is within an increments of ranges (offset[-to]/step size) * * @param string $dateValue Set date value * @param string $value Value to test * * @return bool */ public function isInIncrementsOfRanges($dateValue, $value) { $parts = array_map('trim', explode('/', $value, 2)); $stepSize = isset($parts[1]) ? $parts[1] : 0; if ($parts[0] == '*' || $parts[0] === '0') { return (int) $dateValue % $stepSize == 0; } $range = explode('-', $parts[0], 2); $offset = $range[0]; $to = isset($range[1]) ? $range[1] : $dateValue; // Ensure that the date value is within the range if ($dateValue < $offset || $dateValue > $to) { return false; } for ($i = $offset; $i <= $to; $i+= $stepSize) { if ($i == $dateValue) { return true; } } return false; } } Dependencies/ActionScheduler/lib/WP_Async_Request.php 0000644 00000007206 15174671745 0016706 0 ustar 00 <?php /** * WP Async Request * * @package WP-Background-Processing */ /* Library URI: https://github.com/deliciousbrains/wp-background-processing/blob/fbbc56f2480910d7959972ec9ec0819a13c6150a/classes/wp-async-request.php Author: Delicious Brains Inc. Author URI: https://deliciousbrains.com/ License: GNU General Public License v2.0 License URI: https://github.com/deliciousbrains/wp-background-processing/commit/126d7945dd3d39f39cb6488ca08fe1fb66cb351a */ if ( ! class_exists( 'WP_Async_Request' ) ) { /** * Abstract WP_Async_Request class. * * @abstract */ abstract class WP_Async_Request { /** * Prefix * * (default value: 'wp') * * @var string */ protected $prefix = 'wp'; /** * Action * * (default value: 'async_request') * * @var string */ protected $action = 'async_request'; /** * Identifier * * @var mixed */ protected $identifier; /** * Data * * (default value: array()) * * @var array */ protected $data = array(); /** * Initiate new async request */ public function __construct() { $this->identifier = $this->prefix . '_' . $this->action; add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); } /** * Set data used during the request * * @param array $data Data. * * @return $this */ public function data( $data ) { $this->data = $data; return $this; } /** * Dispatch the async request * * @return array|WP_Error */ public function dispatch() { $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); $args = $this->get_post_args(); return wp_remote_post( esc_url_raw( $url ), $args ); } /** * Get query args * * @return array */ protected function get_query_args() { if ( property_exists( $this, 'query_args' ) ) { return $this->query_args; } $args = array( 'action' => $this->identifier, 'nonce' => wp_create_nonce( $this->identifier ), ); /** * Filters the post arguments used during an async request. * * @param array $url */ return apply_filters( $this->identifier . '_query_args', $args ); } /** * Get query URL * * @return string */ protected function get_query_url() { if ( property_exists( $this, 'query_url' ) ) { return $this->query_url; } $url = admin_url( 'admin-ajax.php' ); /** * Filters the post arguments used during an async request. * * @param string $url */ return apply_filters( $this->identifier . '_query_url', $url ); } /** * Get post args * * @return array */ protected function get_post_args() { if ( property_exists( $this, 'post_args' ) ) { return $this->post_args; } $args = array( 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, 'cookies' => $_COOKIE, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), ); /** * Filters the post arguments used during an async request. * * @param array $args */ return apply_filters( $this->identifier . '_post_args', $args ); } /** * Maybe handle * * Check for correct nonce and pass to handler. */ public function maybe_handle() { // Don't lock up other requests while processing. session_write_close(); check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); wp_die(); } /** * Handle * * Override this method to perform any actions required * during the async request. */ abstract protected function handle(); } } Dependencies/ActionScheduler/license.txt 0000644 00000104515 15174671745 0014420 0 ustar 00 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 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 GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. 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 them 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 prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. 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. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey 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; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If 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 convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU 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 that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. 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. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 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. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. 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 state 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 3 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 does terminal interaction, make it output a short notice like this when it starts in an interactive mode: <program> Copyright (C) <year> <name of author> This program 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, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>. The GNU 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. But first, please read <https://www.gnu.org/licenses/why-not-lgpl.html>. Dependencies/ActionScheduler/classes/schema/ActionScheduler_LoggerSchema.php 0000644 00000005672 15174671745 0023343 0 ustar 00 <?php /** * Class ActionScheduler_LoggerSchema * * @codeCoverageIgnore * * Creates a custom table for storing action logs */ class ActionScheduler_LoggerSchema extends ActionScheduler_Abstract_Schema { const LOG_TABLE = 'actionscheduler_logs'; /** * Schema version. * * Increment this value to trigger a schema update. * * @var int */ protected $schema_version = 3; /** * Construct. */ public function __construct() { $this->tables = array( self::LOG_TABLE, ); } /** * Performs additional setup work required to support this schema. */ public function init() { add_action( 'action_scheduler_before_schema_update', array( $this, 'update_schema_3_0' ), 10, 2 ); } /** * Get table definition. * * @param string $table Table name. */ protected function get_table_definition( $table ) { global $wpdb; $table_name = $wpdb->$table; $charset_collate = $wpdb->get_charset_collate(); switch ( $table ) { case self::LOG_TABLE: $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; return "CREATE TABLE $table_name ( log_id bigint(20) unsigned NOT NULL auto_increment, action_id bigint(20) unsigned NOT NULL, message text NOT NULL, log_date_gmt datetime NULL default '{$default_date}', log_date_local datetime NULL default '{$default_date}', PRIMARY KEY (log_id), KEY action_id (action_id), KEY log_date_gmt (log_date_gmt) ) $charset_collate"; default: return ''; } } /** * Update the logs table schema, allowing datetime fields to be NULL. * * This is needed because the NOT NULL constraint causes a conflict with some versions of MySQL * configured with sql_mode=NO_ZERO_DATE, which can for instance lead to tables not being created. * * Most other schema updates happen via ActionScheduler_Abstract_Schema::update_table(), however * that method relies on dbDelta() and this change is not possible when using that function. * * @param string $table Name of table being updated. * @param string $db_version The existing schema version of the table. */ public function update_schema_3_0( $table, $db_version ) { global $wpdb; if ( 'actionscheduler_logs' !== $table || version_compare( $db_version, '3', '>=' ) ) { return; } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $table_name = $wpdb->prefix . 'actionscheduler_logs'; $table_list = $wpdb->get_col( "SHOW TABLES LIKE '{$table_name}'" ); $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; if ( ! empty( $table_list ) ) { $query = " ALTER TABLE {$table_name} MODIFY COLUMN log_date_gmt datetime NULL default '{$default_date}', MODIFY COLUMN log_date_local datetime NULL default '{$default_date}' "; $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } Dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php 0000644 00000011773 15174671745 0023217 0 ustar 00 <?php /** * Class ActionScheduler_StoreSchema * * @codeCoverageIgnore * * Creates custom tables for storing scheduled actions */ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema { const ACTIONS_TABLE = 'actionscheduler_actions'; const CLAIMS_TABLE = 'actionscheduler_claims'; const GROUPS_TABLE = 'actionscheduler_groups'; const DEFAULT_DATE = '0000-00-00 00:00:00'; /** * Schema version. * * Increment this value to trigger a schema update. * * @var int */ protected $schema_version = 7; /** * Construct. */ public function __construct() { $this->tables = array( self::ACTIONS_TABLE, self::CLAIMS_TABLE, self::GROUPS_TABLE, ); } /** * Performs additional setup work required to support this schema. */ public function init() { add_action( 'action_scheduler_before_schema_update', array( $this, 'update_schema_5_0' ), 10, 2 ); } /** * Get table definition. * * @param string $table Table name. */ protected function get_table_definition( $table ) { global $wpdb; $table_name = $wpdb->$table; $charset_collate = $wpdb->get_charset_collate(); $default_date = self::DEFAULT_DATE; // phpcs:ignore Squiz.PHP.CommentedOutCode $max_index_length = 191; // @see wp_get_db_schema() $hook_status_scheduled_date_gmt_max_index_length = $max_index_length - 20 - 8; // - status, - scheduled_date_gmt switch ( $table ) { case self::ACTIONS_TABLE: return "CREATE TABLE {$table_name} ( action_id bigint(20) unsigned NOT NULL auto_increment, hook varchar(191) NOT NULL, status varchar(20) NOT NULL, scheduled_date_gmt datetime NULL default '{$default_date}', scheduled_date_local datetime NULL default '{$default_date}', priority tinyint unsigned NOT NULL default '10', args varchar($max_index_length), schedule longtext, group_id bigint(20) unsigned NOT NULL default '0', attempts int(11) NOT NULL default '0', last_attempt_gmt datetime NULL default '{$default_date}', last_attempt_local datetime NULL default '{$default_date}', claim_id bigint(20) unsigned NOT NULL default '0', extended_args varchar(8000) DEFAULT NULL, PRIMARY KEY (action_id), KEY hook_status_scheduled_date_gmt (hook($hook_status_scheduled_date_gmt_max_index_length), status, scheduled_date_gmt), KEY status_scheduled_date_gmt (status, scheduled_date_gmt), KEY scheduled_date_gmt (scheduled_date_gmt), KEY args (args($max_index_length)), KEY group_id (group_id), KEY last_attempt_gmt (last_attempt_gmt), KEY `claim_id_status_scheduled_date_gmt` (`claim_id`, `status`, `scheduled_date_gmt`) ) $charset_collate"; case self::CLAIMS_TABLE: return "CREATE TABLE {$table_name} ( claim_id bigint(20) unsigned NOT NULL auto_increment, date_created_gmt datetime NULL default '{$default_date}', PRIMARY KEY (claim_id), KEY date_created_gmt (date_created_gmt) ) $charset_collate"; case self::GROUPS_TABLE: return "CREATE TABLE {$table_name} ( group_id bigint(20) unsigned NOT NULL auto_increment, slug varchar(255) NOT NULL, PRIMARY KEY (group_id), KEY slug (slug($max_index_length)) ) $charset_collate"; default: return ''; } } /** * Update the actions table schema, allowing datetime fields to be NULL. * * This is needed because the NOT NULL constraint causes a conflict with some versions of MySQL * configured with sql_mode=NO_ZERO_DATE, which can for instance lead to tables not being created. * * Most other schema updates happen via ActionScheduler_Abstract_Schema::update_table(), however * that method relies on dbDelta() and this change is not possible when using that function. * * @param string $table Name of table being updated. * @param string $db_version The existing schema version of the table. */ public function update_schema_5_0( $table, $db_version ) { global $wpdb; if ( 'actionscheduler_actions' !== $table || version_compare( $db_version, '5', '>=' ) ) { return; } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $table_name = $wpdb->prefix . 'actionscheduler_actions'; $table_list = $wpdb->get_col( "SHOW TABLES LIKE '{$table_name}'" ); $default_date = self::DEFAULT_DATE; if ( ! empty( $table_list ) ) { $query = " ALTER TABLE {$table_name} MODIFY COLUMN scheduled_date_gmt datetime NULL default '{$default_date}', MODIFY COLUMN scheduled_date_local datetime NULL default '{$default_date}', MODIFY COLUMN last_attempt_gmt datetime NULL default '{$default_date}', MODIFY COLUMN last_attempt_local datetime NULL default '{$default_date}' "; $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } Dependencies/ActionScheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php 0000644 00000004163 15174671745 0024521 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * ActionScheduler_AsyncRequest_QueueRunner class. */ class ActionScheduler_AsyncRequest_QueueRunner extends WP_Async_Request { /** * Data store for querying actions * * @var ActionScheduler_Store */ protected $store; /** * Prefix for ajax hooks * * @var string */ protected $prefix = 'as'; /** * Action for ajax hooks * * @var string */ protected $action = 'async_request_queue_runner'; /** * Initiate new async request. * * @param ActionScheduler_Store $store Store object. */ public function __construct( ActionScheduler_Store $store ) { parent::__construct(); $this->store = $store; } /** * Handle async requests * * Run a queue, and maybe dispatch another async request to run another queue * if there are still pending actions after completing a queue in this request. */ protected function handle() { do_action( 'action_scheduler_run_queue', 'Async Request' ); // run a queue in the same way as WP Cron, but declare the Async Request context. $sleep_seconds = $this->get_sleep_seconds(); if ( $sleep_seconds ) { sleep( $sleep_seconds ); } $this->maybe_dispatch(); } /** * If the async request runner is needed and allowed to run, dispatch a request. */ public function maybe_dispatch() { if ( ! $this->allow() ) { return; } $this->dispatch(); ActionScheduler_QueueRunner::instance()->unhook_dispatch_async_request(); } /** * Only allow async requests when needed. * * Also allow 3rd party code to disable running actions via async requests. */ protected function allow() { if ( ! has_action( 'action_scheduler_run_queue' ) || ActionScheduler::runner()->has_maximum_concurrent_batches() || ! $this->store->has_pending_actions_due() ) { $allow = false; } else { $allow = true; } return apply_filters( 'action_scheduler_allow_async_request_runner', $allow ); } /** * Chaining async requests can crash MySQL. A brief sleep call in PHP prevents that. */ protected function get_sleep_seconds() { return apply_filters( 'action_scheduler_async_request_sleep_seconds', 5, $this ); } } Dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php 0000644 00000022770 15174671745 0022017 0 ustar 00 <?php /** * Class ActionScheduler_QueueRunner */ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner { const WP_CRON_HOOK = 'action_scheduler_run_queue'; const WP_CRON_SCHEDULE = 'every_minute'; /** * ActionScheduler_AsyncRequest_QueueRunner instance. * * @var ActionScheduler_AsyncRequest_QueueRunner */ protected $async_request; /** * ActionScheduler_QueueRunner instance. * * @var ActionScheduler_QueueRunner */ private static $runner = null; /** * Number of processed actions. * * @var int */ private $processed_actions_count = 0; /** * Get instance. * * @return ActionScheduler_QueueRunner * @codeCoverageIgnore */ public static function instance() { if ( empty( self::$runner ) ) { $class = apply_filters( 'action_scheduler_queue_runner_class', 'ActionScheduler_QueueRunner' ); self::$runner = new $class(); } return self::$runner; } /** * ActionScheduler_QueueRunner constructor. * * @param ActionScheduler_Store|null $store Store object. * @param ActionScheduler_FatalErrorMonitor|null $monitor Monitor object. * @param ActionScheduler_QueueCleaner|null $cleaner Cleaner object. * @param ActionScheduler_AsyncRequest_QueueRunner|null $async_request Async request runner object. */ public function __construct( ?ActionScheduler_Store $store = null, ?ActionScheduler_FatalErrorMonitor $monitor = null, ?ActionScheduler_QueueCleaner $cleaner = null, ?ActionScheduler_AsyncRequest_QueueRunner $async_request = null ) { parent::__construct( $store, $monitor, $cleaner ); if ( is_null( $async_request ) ) { $async_request = new ActionScheduler_AsyncRequest_QueueRunner( $this->store ); } $this->async_request = $async_request; } /** * Initialize. * * @codeCoverageIgnore */ public function init() { add_filter( 'cron_schedules', array( self::instance(), 'add_wp_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.CronSchedulesInterval // Check for and remove any WP Cron hook scheduled by Action Scheduler < 3.0.0, which didn't include the $context param. $next_timestamp = wp_next_scheduled( self::WP_CRON_HOOK ); if ( $next_timestamp ) { wp_unschedule_event( $next_timestamp, self::WP_CRON_HOOK ); } $cron_context = array( 'WP Cron' ); if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) { $schedule = apply_filters( 'action_scheduler_run_schedule', self::WP_CRON_SCHEDULE ); wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK, $cron_context ); } add_action( self::WP_CRON_HOOK, array( self::instance(), 'run' ) ); $this->hook_dispatch_async_request(); } /** * Hook check for dispatching an async request. */ public function hook_dispatch_async_request() { add_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); } /** * Unhook check for dispatching an async request. */ public function unhook_dispatch_async_request() { remove_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); } /** * Check if we should dispatch an async request to process actions. * * This method is attached to 'shutdown', so is called frequently. To avoid slowing down * the site, it mitigates the work performed in each request by: * 1. checking if it's in the admin context and then * 2. haven't run on the 'shutdown' hook within the lock time (60 seconds by default) * 3. haven't exceeded the number of allowed batches. * * The order of these checks is important, because they run from a check on a value: * 1. in memory - is_admin() maps to $GLOBALS or the WP_ADMIN constant * 2. in memory - transients use autoloaded options by default * 3. from a database query - has_maximum_concurrent_batches() run the query * $this->store->get_claim_count() to find the current number of claims in the DB. * * If all of these conditions are met, then we request an async runner check whether it * should dispatch a request to process pending actions. */ public function maybe_dispatch_async_request() { // Only start an async queue at most once every 60 seconds. if ( is_admin() && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) && ActionScheduler::lock()->set( 'async-request-runner' ) ) { $this->async_request->maybe_dispatch(); } } /** * Process actions in the queue. Attached to self::WP_CRON_HOOK i.e. 'action_scheduler_run_queue' * * The $context param of this method defaults to 'WP Cron', because prior to Action Scheduler 3.0.0 * that was the only context in which this method was run, and the self::WP_CRON_HOOK hook had no context * passed along with it. New code calling this method directly, or by triggering the self::WP_CRON_HOOK, * should set a context as the first parameter. For an example of this, refer to the code seen in * * @see ActionScheduler_AsyncRequest_QueueRunner::handle() * * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * @return int The number of actions processed. */ public function run( $context = 'WP Cron' ) { ActionScheduler_Compatibility::raise_memory_limit(); ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() ); do_action( 'action_scheduler_before_process_queue' ); $this->run_cleanup(); $this->processed_actions_count = 0; if ( false === $this->has_maximum_concurrent_batches() ) { do { $batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 ); $processed_actions_in_batch = $this->do_batch( $batch_size, $context ); $this->processed_actions_count += $processed_actions_in_batch; } while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $this->processed_actions_count ) ); // keep going until we run out of actions, time, or memory. } do_action( 'action_scheduler_after_process_queue' ); return $this->processed_actions_count; } /** * Process a batch of actions pending in the queue. * * Actions are processed by claiming a set of pending actions then processing each one until either the batch * size is completed, or memory or time limits are reached, defined by @see $this->batch_limits_exceeded(). * * @param int $size The maximum number of actions to process in the batch. * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * @return int The number of actions processed. */ protected function do_batch( $size = 100, $context = '' ) { $claim = $this->store->stake_claim( $size ); $this->monitor->attach( $claim ); $processed_actions = 0; foreach ( $claim->get_actions() as $action_id ) { // bail if we lost the claim. if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $claim->get_id() ), true ) ) { break; } $this->process_action( $action_id, $context ); $processed_actions++; if ( $this->batch_limits_exceeded( $processed_actions + $this->processed_actions_count ) ) { break; } } $this->store->release_claim( $claim ); $this->monitor->detach(); $this->clear_caches(); return $processed_actions; } /** * Flush the cache if possible (intended for use after a batch of actions has been processed). * * This is useful because running large batches can eat up memory and because invalid data can accrue in the * runtime cache, which may lead to unexpected results. */ protected function clear_caches() { /* * Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object * cache, so we will always prefer this method (as compared to calling wp_cache_flush()) when it is available. * * However, this function was only introduced in WordPress 6.0. Additionally, the preferred way of detecting if * it is supported changed in WordPress 6.1 so we use two different methods to decide if we should utilize it. */ $flushing_runtime_cache_explicitly_supported = function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' ); $flushing_runtime_cache_implicitly_supported = ! function_exists( 'wp_cache_supports' ) && function_exists( 'wp_cache_flush_runtime' ); if ( $flushing_runtime_cache_explicitly_supported || $flushing_runtime_cache_implicitly_supported ) { wp_cache_flush_runtime(); } elseif ( ! wp_using_ext_object_cache() /** * When an external object cache is in use, and when wp_cache_flush_runtime() is not available, then * normally the cache will not be flushed after processing a batch of actions (to avoid a performance * penalty for other processes). * * This filter makes it possible to override this behavior and always flush the cache, even if an external * object cache is in use. * * @since 1.0 * * @param bool $flush_cache If the cache should be flushed. */ || apply_filters( 'action_scheduler_queue_runner_flush_cache', false ) ) { wp_cache_flush(); } } /** * Add schedule to WP cron. * * @param array<string, array<string, int|string>> $schedules Schedules. * @return array<string, array<string, int|string>> */ public function add_wp_cron_schedule( $schedules ) { $schedules['every_minute'] = array( 'interval' => 60, // in seconds. 'display' => __( 'Every minute', 'action-scheduler' ), ); return $schedules; } } Dependencies/ActionScheduler/classes/ActionScheduler_AdminView.php 0000644 00000022415 15174671745 0021420 0 ustar 00 <?php /** * Class ActionScheduler_AdminView * * @codeCoverageIgnore */ class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated { /** * Instance. * * @var null|self */ private static $admin_view = null; /** * Screen ID. * * @var string */ private static $screen_id = 'tools_page_action-scheduler'; /** * ActionScheduler_ListTable instance. * * @var ActionScheduler_ListTable */ protected $list_table; /** * Get instance. * * @return ActionScheduler_AdminView * @codeCoverageIgnore */ public static function instance() { if ( empty( self::$admin_view ) ) { $class = apply_filters( 'action_scheduler_admin_view_class', 'ActionScheduler_AdminView' ); self::$admin_view = new $class(); } return self::$admin_view; } /** * Initialize. * * @codeCoverageIgnore */ public function init() { if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) { if ( class_exists( 'WooCommerce' ) ) { add_action( 'woocommerce_admin_status_content_action-scheduler', array( $this, 'render_admin_ui' ) ); add_action( 'woocommerce_system_status_report', array( $this, 'system_status_report' ) ); add_filter( 'woocommerce_admin_status_tabs', array( $this, 'register_system_status_tab' ) ); } add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_notices', array( $this, 'maybe_check_pastdue_actions' ) ); add_action( 'current_screen', array( $this, 'add_help_tabs' ) ); } } /** * Print system status report. */ public function system_status_report() { $table = new ActionScheduler_wcSystemStatus( ActionScheduler::store() ); $table->render(); } /** * Registers action-scheduler into WooCommerce > System status. * * @param array $tabs An associative array of tab key => label. * @return array $tabs An associative array of tab key => label, including Action Scheduler's tabs */ public function register_system_status_tab( array $tabs ) { $tabs['action-scheduler'] = __( 'Scheduled Actions', 'action-scheduler' ); return $tabs; } /** * Include Action Scheduler's administration under the Tools menu. * * A menu under the Tools menu is important for backward compatibility (as that's * where it started), and also provides more convenient access than the WooCommerce * System Status page, and for sites where WooCommerce isn't active. */ public function register_menu() { $hook_suffix = add_submenu_page( 'tools.php', __( 'Scheduled Actions', 'action-scheduler' ), __( 'Scheduled Actions', 'action-scheduler' ), 'manage_options', 'action-scheduler', array( $this, 'render_admin_ui' ) ); add_action( 'load-' . $hook_suffix, array( $this, 'process_admin_ui' ) ); } /** * Triggers processing of any pending actions. */ public function process_admin_ui() { $this->get_list_table(); } /** * Renders the Admin UI */ public function render_admin_ui() { $table = $this->get_list_table(); $table->display_page(); } /** * Get the admin UI object and process any requested actions. * * @return ActionScheduler_ListTable */ protected function get_list_table() { if ( null === $this->list_table ) { $this->list_table = new ActionScheduler_ListTable( ActionScheduler::store(), ActionScheduler::logger(), ActionScheduler::runner() ); $this->list_table->process_actions(); } return $this->list_table; } /** * Action: admin_notices * * Maybe check past-due actions, and print notice. * * @uses $this->check_pastdue_actions() */ public function maybe_check_pastdue_actions() { // Filter to prevent checking actions (ex: inappropriate user). if ( ! apply_filters( 'action_scheduler_check_pastdue_actions', current_user_can( 'manage_options' ) ) ) { return; } // Get last check transient. $last_check = get_transient( 'action_scheduler_last_pastdue_actions_check' ); // If transient exists, we're within interval, so bail. if ( ! empty( $last_check ) ) { return; } // Perform the check. $this->check_pastdue_actions(); } /** * Check past-due actions, and print notice. */ protected function check_pastdue_actions() { // Set thresholds. $threshold_seconds = (int) apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS ); $threshold_min = (int) apply_filters( 'action_scheduler_pastdue_actions_min', 1 ); // Set fallback value for past-due actions count. $num_pastdue_actions = 0; // Allow third-parties to preempt the default check logic. $check = apply_filters( 'action_scheduler_pastdue_actions_check_pre', null ); // If no third-party preempted and there are no past-due actions, return early. if ( ! is_null( $check ) ) { return; } // Scheduled actions query arguments. $query_args = array( 'date' => as_get_datetime_object( time() - $threshold_seconds ), 'status' => ActionScheduler_Store::STATUS_PENDING, 'per_page' => $threshold_min, ); // If no third-party preempted, run default check. if ( is_null( $check ) ) { $store = ActionScheduler_Store::instance(); $num_pastdue_actions = (int) $store->query_actions( $query_args, 'count' ); // Check if past-due actions count is greater than or equal to threshold. $check = ( $num_pastdue_actions >= $threshold_min ); $check = (bool) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $num_pastdue_actions, $threshold_seconds, $threshold_min ); } // If check failed, set transient and abort. if ( ! boolval( $check ) ) { $interval = apply_filters( 'action_scheduler_pastdue_actions_check_interval', round( $threshold_seconds / 4 ), $threshold_seconds ); set_transient( 'action_scheduler_last_pastdue_actions_check', time(), $interval ); return; } $actions_url = add_query_arg( array( 'page' => 'action-scheduler', 'status' => 'past-due', 'order' => 'asc', ), admin_url( 'tools.php' ) ); // Print notice. echo '<div class="notice notice-warning"><p>'; printf( wp_kses( // translators: 1) is the number of affected actions, 2) is a link to an admin screen. _n( '<strong>Action Scheduler:</strong> %1$d <a href="%2$s">past-due action</a> found; something may be wrong. <a href="https://actionscheduler.org/faq/#my-site-has-past-due-actions-what-can-i-do" target="_blank">Read documentation »</a>', '<strong>Action Scheduler:</strong> %1$d <a href="%2$s">past-due actions</a> found; something may be wrong. <a href="https://actionscheduler.org/faq/#my-site-has-past-due-actions-what-can-i-do" target="_blank">Read documentation »</a>', $num_pastdue_actions, 'action-scheduler' ), array( 'strong' => array(), 'a' => array( 'href' => true, 'target' => true, ), ) ), absint( $num_pastdue_actions ), esc_attr( esc_url( $actions_url ) ) ); echo '</p></div>'; // Facilitate third-parties to evaluate and print notices. do_action( 'action_scheduler_pastdue_actions_extra_notices', $query_args ); } /** * Provide more information about the screen and its data in the help tab. */ public function add_help_tabs() { $screen = get_current_screen(); if ( ! $screen || self::$screen_id !== $screen->id ) { return; } $as_version = ActionScheduler_Versions::instance()->latest_version(); $screen->add_help_tab( array( 'id' => 'action_scheduler_about', 'title' => __( 'About', 'action-scheduler' ), 'content' => // translators: %s is the Action Scheduler version. '<h2>' . sprintf( __( 'About Action Scheduler %s', 'action-scheduler' ), $as_version ) . '</h2>' . '<p>' . __( 'Action Scheduler is a scalable, traceable job queue for background processing large sets of actions. Action Scheduler works by triggering an action hook to run at some time in the future. Scheduled actions can also be scheduled to run on a recurring schedule.', 'action-scheduler' ) . '</p>', ) ); $screen->add_help_tab( array( 'id' => 'action_scheduler_columns', 'title' => __( 'Columns', 'action-scheduler' ), 'content' => '<h2>' . __( 'Scheduled Action Columns', 'action-scheduler' ) . '</h2>' . '<ul>' . sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Hook', 'action-scheduler' ), __( 'Name of the action hook that will be triggered.', 'action-scheduler' ) ) . sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Status', 'action-scheduler' ), __( 'Action statuses are Pending, Complete, Canceled, Failed', 'action-scheduler' ) ) . sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Arguments', 'action-scheduler' ), __( 'Optional data array passed to the action hook.', 'action-scheduler' ) ) . sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Group', 'action-scheduler' ), __( 'Optional action group.', 'action-scheduler' ) ) . sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Recurrence', 'action-scheduler' ), __( 'The action\'s schedule frequency.', 'action-scheduler' ) ) . sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Scheduled', 'action-scheduler' ), __( 'The date/time the action is/was scheduled to run.', 'action-scheduler' ) ) . sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Log', 'action-scheduler' ), __( 'Activity log for the action.', 'action-scheduler' ) ) . '</ul>', ) ); } } Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php0000644 00000003502 15174671745 0030364 0 ustar 00 <?php /** * Class ActionScheduler_wpPostStore_PostStatusRegistrar * * @codeCoverageIgnore */ class ActionScheduler_wpPostStore_PostStatusRegistrar { /** * Registrar. */ public function register() { register_post_status( ActionScheduler_Store::STATUS_RUNNING, array_merge( $this->post_status_args(), $this->post_status_running_labels() ) ); register_post_status( ActionScheduler_Store::STATUS_FAILED, array_merge( $this->post_status_args(), $this->post_status_failed_labels() ) ); } /** * Build the args array for the post type definition * * @return array */ protected function post_status_args() { $args = array( 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, ); return apply_filters( 'action_scheduler_post_status_args', $args ); } /** * Build the args array for the post type definition * * @return array */ protected function post_status_failed_labels() { $labels = array( 'label' => _x( 'Failed', 'post', 'action-scheduler' ), /* translators: %s: count */ 'label_count' => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'action-scheduler' ), ); return apply_filters( 'action_scheduler_post_status_failed_labels', $labels ); } /** * Build the args array for the post type definition * * @return array */ protected function post_status_running_labels() { $labels = array( 'label' => _x( 'In-Progress', 'post', 'action-scheduler' ), /* translators: %s: count */ 'label_count' => _n_noop( 'In-Progress <span class="count">(%s)</span>', 'In-Progress <span class="count">(%s)</span>', 'action-scheduler' ), ); return apply_filters( 'action_scheduler_post_status_running_labels', $labels ); } } Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_HybridStore.php 0000644 00000030557 15174671745 0024227 0 ustar 00 <?php use ActionScheduler_Store as Store; use Action_Scheduler\Migration\Runner; use Action_Scheduler\Migration\Config; use Action_Scheduler\Migration\Controller; /** * Class ActionScheduler_HybridStore * * A wrapper around multiple stores that fetches data from both. * * @since 3.0.0 */ class ActionScheduler_HybridStore extends Store { const DEMARKATION_OPTION = 'action_scheduler_hybrid_store_demarkation'; /** * Primary store instance. * * @var ActionScheduler_Store */ private $primary_store; /** * Secondary store instance. * * @var ActionScheduler_Store */ private $secondary_store; /** * Runner instance. * * @var Action_Scheduler\Migration\Runner */ private $migration_runner; /** * The dividing line between IDs of actions created * by the primary and secondary stores. * * @var int * * Methods that accept an action ID will compare the ID against * this to determine which store will contain that ID. In almost * all cases, the ID should come from the primary store, but if * client code is bypassing the API functions and fetching IDs * from elsewhere, then there is a chance that an unmigrated ID * might be requested. */ private $demarkation_id = 0; /** * ActionScheduler_HybridStore constructor. * * @param Config|null $config Migration config object. */ public function __construct( ?Config $config = null ) { $this->demarkation_id = (int) get_option( self::DEMARKATION_OPTION, 0 ); if ( empty( $config ) ) { $config = Controller::instance()->get_migration_config_object(); } $this->primary_store = $config->get_destination_store(); $this->secondary_store = $config->get_source_store(); $this->migration_runner = new Runner( $config ); } /** * Initialize the table data store tables. * * @codeCoverageIgnore */ public function init() { add_action( 'action_scheduler/created_table', array( $this, 'set_autoincrement' ), 10, 2 ); $this->primary_store->init(); $this->secondary_store->init(); remove_action( 'action_scheduler/created_table', array( $this, 'set_autoincrement' ), 10 ); } /** * When the actions table is created, set its autoincrement * value to be one higher than the posts table to ensure that * there are no ID collisions. * * @param string $table_name Table name. * @param string $table_suffix Suffix of table name. * * @return void * @codeCoverageIgnore */ public function set_autoincrement( $table_name, $table_suffix ) { if ( ActionScheduler_StoreSchema::ACTIONS_TABLE === $table_suffix ) { if ( empty( $this->demarkation_id ) ) { $this->demarkation_id = $this->set_demarkation_id(); } /** * Global. * * @var \wpdb $wpdb */ global $wpdb; /** * A default date of '0000-00-00 00:00:00' is invalid in MySQL 5.7 when configured with * sql_mode including both STRICT_TRANS_TABLES and NO_ZERO_DATE. */ $default_date = new DateTime( 'tomorrow' ); $null_action = new ActionScheduler_NullAction(); $date_gmt = $this->get_scheduled_date_string( $null_action, $default_date ); $date_local = $this->get_scheduled_date_string_local( $null_action, $default_date ); $row_count = $wpdb->insert( $wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE}, array( 'action_id' => $this->demarkation_id, 'hook' => '', 'status' => '', 'scheduled_date_gmt' => $date_gmt, 'scheduled_date_local' => $date_local, 'last_attempt_gmt' => $date_gmt, 'last_attempt_local' => $date_local, ) ); if ( $row_count > 0 ) { $wpdb->delete( $wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE}, array( 'action_id' => $this->demarkation_id ) ); } } } /** * Store the demarkation id in WP options. * * @param int $id The ID to set as the demarkation point between the two stores * Leave null to use the next ID from the WP posts table. * * @return int The new ID. * * @codeCoverageIgnore */ private function set_demarkation_id( $id = null ) { if ( empty( $id ) ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $id = (int) $wpdb->get_var( "SELECT MAX(ID) FROM $wpdb->posts" ); $id++; } update_option( self::DEMARKATION_OPTION, $id ); return $id; } /** * Find the first matching action from the secondary store. * If it exists, migrate it to the primary store immediately. * After it migrates, the secondary store will logically contain * the next matching action, so return the result thence. * * @param string $hook Action's hook. * @param array $params Action's arguments. * * @return string */ public function find_action( $hook, $params = array() ) { $found_unmigrated_action = $this->secondary_store->find_action( $hook, $params ); if ( ! empty( $found_unmigrated_action ) ) { $this->migrate( array( $found_unmigrated_action ) ); } return $this->primary_store->find_action( $hook, $params ); } /** * Find actions matching the query in the secondary source first. * If any are found, migrate them immediately. Then the secondary * store will contain the canonical results. * * @param array $query Query arguments. * @param string $query_type Whether to select or count the results. Default, select. * * @return int[] */ public function query_actions( $query = array(), $query_type = 'select' ) { $found_unmigrated_actions = $this->secondary_store->query_actions( $query, 'select' ); if ( ! empty( $found_unmigrated_actions ) ) { $this->migrate( $found_unmigrated_actions ); } return $this->primary_store->query_actions( $query, $query_type ); } /** * Get a count of all actions in the store, grouped by status * * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status. */ public function action_counts() { $unmigrated_actions_count = $this->secondary_store->action_counts(); $migrated_actions_count = $this->primary_store->action_counts(); $actions_count_by_status = array(); foreach ( $this->get_status_labels() as $status_key => $status_label ) { $count = 0; if ( isset( $unmigrated_actions_count[ $status_key ] ) ) { $count += $unmigrated_actions_count[ $status_key ]; } if ( isset( $migrated_actions_count[ $status_key ] ) ) { $count += $migrated_actions_count[ $status_key ]; } $actions_count_by_status[ $status_key ] = $count; } $actions_count_by_status = array_filter( $actions_count_by_status ); return $actions_count_by_status; } /** * If any actions would have been claimed by the secondary store, * migrate them immediately, then ask the primary store for the * canonical claim. * * @param int $max_actions Maximum number of actions to claim. * @param null|DateTime $before_date Latest timestamp of actions to claim. * @param string[] $hooks Hook of actions to claim. * @param string $group Group of actions to claim. * * @return ActionScheduler_ActionClaim */ public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ) { $claim = $this->secondary_store->stake_claim( $max_actions, $before_date, $hooks, $group ); $claimed_actions = $claim->get_actions(); if ( ! empty( $claimed_actions ) ) { $this->migrate( $claimed_actions ); } $this->secondary_store->release_claim( $claim ); return $this->primary_store->stake_claim( $max_actions, $before_date, $hooks, $group ); } /** * Migrate a list of actions to the table data store. * * @param array $action_ids List of action IDs. */ private function migrate( $action_ids ) { $this->migration_runner->migrate_actions( $action_ids ); } /** * Save an action to the primary store. * * @param ActionScheduler_Action $action Action object to be saved. * @param DateTime|null $date Optional. Schedule date. Default null. * * @return int The action ID */ public function save_action( ActionScheduler_Action $action, ?DateTime $date = null ) { return $this->primary_store->save_action( $action, $date ); } /** * Retrieve an existing action whether migrated or not. * * @param int $action_id Action ID. */ public function fetch_action( $action_id ) { $store = $this->get_store_from_action_id( $action_id, true ); if ( $store ) { return $store->fetch_action( $action_id ); } else { return new ActionScheduler_NullAction(); } } /** * Cancel an existing action whether migrated or not. * * @param int $action_id Action ID. */ public function cancel_action( $action_id ) { $store = $this->get_store_from_action_id( $action_id ); if ( $store ) { $store->cancel_action( $action_id ); } } /** * Delete an existing action whether migrated or not. * * @param int $action_id Action ID. */ public function delete_action( $action_id ) { $store = $this->get_store_from_action_id( $action_id ); if ( $store ) { $store->delete_action( $action_id ); } } /** * Get the schedule date an existing action whether migrated or not. * * @param int $action_id Action ID. */ public function get_date( $action_id ) { $store = $this->get_store_from_action_id( $action_id ); if ( $store ) { return $store->get_date( $action_id ); } else { return null; } } /** * Mark an existing action as failed whether migrated or not. * * @param int $action_id Action ID. */ public function mark_failure( $action_id ) { $store = $this->get_store_from_action_id( $action_id ); if ( $store ) { $store->mark_failure( $action_id ); } } /** * Log the execution of an existing action whether migrated or not. * * @param int $action_id Action ID. */ public function log_execution( $action_id ) { $store = $this->get_store_from_action_id( $action_id ); if ( $store ) { $store->log_execution( $action_id ); } } /** * Mark an existing action complete whether migrated or not. * * @param int $action_id Action ID. */ public function mark_complete( $action_id ) { $store = $this->get_store_from_action_id( $action_id ); if ( $store ) { $store->mark_complete( $action_id ); } } /** * Get an existing action status whether migrated or not. * * @param int $action_id Action ID. */ public function get_status( $action_id ) { $store = $this->get_store_from_action_id( $action_id ); if ( $store ) { return $store->get_status( $action_id ); } return null; } /** * Return which store an action is stored in. * * @param int $action_id ID of the action. * @param bool $primary_first Optional flag indicating search the primary store first. * @return ActionScheduler_Store */ protected function get_store_from_action_id( $action_id, $primary_first = false ) { if ( $primary_first ) { $stores = array( $this->primary_store, $this->secondary_store, ); } elseif ( $action_id < $this->demarkation_id ) { $stores = array( $this->secondary_store, $this->primary_store, ); } else { $stores = array( $this->primary_store, ); } foreach ( $stores as $store ) { $action = $store->fetch_action( $action_id ); if ( ! is_a( $action, 'ActionScheduler_NullAction' ) ) { return $store; } } return null; } /** * * * * * * * * * * * * * * * * * * * * * * * * * * * * All claim-related functions should operate solely * on the primary store. * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /** * Get the claim count from the table data store. */ public function get_claim_count() { return $this->primary_store->get_claim_count(); } /** * Retrieve the claim ID for an action from the table data store. * * @param int $action_id Action ID. */ public function get_claim_id( $action_id ) { return $this->primary_store->get_claim_id( $action_id ); } /** * Release a claim in the table data store. * * @param ActionScheduler_ActionClaim $claim Claim object. */ public function release_claim( ActionScheduler_ActionClaim $claim ) { $this->primary_store->release_claim( $claim ); } /** * Release claims on an action in the table data store. * * @param int $action_id Action ID. */ public function unclaim_action( $action_id ) { $this->primary_store->unclaim_action( $action_id ); } /** * Retrieve a list of action IDs by claim. * * @param int $claim_id Claim ID. */ public function find_actions_by_claim_id( $claim_id ) { return $this->primary_store->find_actions_by_claim_id( $claim_id ); } } Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php 0000644 00000001372 15174671745 0030054 0 ustar 00 <?php /** * Class ActionScheduler_wpPostStore_TaxonomyRegistrar * * @codeCoverageIgnore */ class ActionScheduler_wpPostStore_TaxonomyRegistrar { /** * Registrar. */ public function register() { register_taxonomy( ActionScheduler_wpPostStore::GROUP_TAXONOMY, ActionScheduler_wpPostStore::POST_TYPE, $this->taxonomy_args() ); } /** * Get taxonomy arguments. */ protected function taxonomy_args() { $args = array( 'label' => __( 'Action Group', 'action-scheduler' ), 'public' => false, 'hierarchical' => false, 'show_admin_column' => true, 'query_var' => false, 'rewrite' => false, ); $args = apply_filters( 'action_scheduler_taxonomy_args', $args ); return $args; } } Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php 0000644 00000010620 15174671745 0023403 0 ustar 00 <?php /** * Class ActionScheduler_DBLogger * * Action logs data table data store. * * @since 3.0.0 */ class ActionScheduler_DBLogger extends ActionScheduler_Logger { /** * Add a record to an action log. * * @param int $action_id Action ID. * @param string $message Message to be saved in the log entry. * @param DateTime|null $date Timestamp of the log entry. * * @return int The log entry ID. */ public function log( $action_id, $message, ?DateTime $date = null ) { if ( empty( $date ) ) { $date = as_get_datetime_object(); } else { $date = clone $date; } $date_gmt = $date->format( 'Y-m-d H:i:s' ); ActionScheduler_TimezoneHelper::set_local_timezone( $date ); $date_local = $date->format( 'Y-m-d H:i:s' ); /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort global $wpdb; $wpdb->insert( $wpdb->actionscheduler_logs, array( 'action_id' => $action_id, 'message' => $message, 'log_date_gmt' => $date_gmt, 'log_date_local' => $date_local, ), array( '%d', '%s', '%s', '%s' ) ); return $wpdb->insert_id; } /** * Retrieve an action log entry. * * @param int $entry_id Log entry ID. * * @return ActionScheduler_LogEntry */ public function get_entry( $entry_id ) { /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort global $wpdb; $entry = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE log_id=%d", $entry_id ) ); return $this->create_entry_from_db_record( $entry ); } /** * Create an action log entry from a database record. * * @param object $record Log entry database record object. * * @return ActionScheduler_LogEntry */ private function create_entry_from_db_record( $record ) { if ( empty( $record ) ) { return new ActionScheduler_NullLogEntry(); } if ( is_null( $record->log_date_gmt ) ) { $date = as_get_datetime_object( ActionScheduler_StoreSchema::DEFAULT_DATE ); } else { $date = as_get_datetime_object( $record->log_date_gmt ); } return new ActionScheduler_LogEntry( $record->action_id, $record->message, $date ); } /** * Retrieve an action's log entries from the database. * * @param int $action_id Action ID. * * @return ActionScheduler_LogEntry[] */ public function get_logs( $action_id ) { /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort global $wpdb; $records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE action_id=%d", $action_id ) ); return array_map( array( $this, 'create_entry_from_db_record' ), $records ); } /** * Initialize the data store. * * @codeCoverageIgnore */ public function init() { $table_maker = new ActionScheduler_LoggerSchema(); $table_maker->init(); $table_maker->register_tables(); parent::init(); add_action( 'action_scheduler_deleted_action', array( $this, 'clear_deleted_action_logs' ), 10, 1 ); } /** * Delete the action logs for an action. * * @param int $action_id Action ID. */ public function clear_deleted_action_logs( $action_id ) { /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort global $wpdb; $wpdb->delete( $wpdb->actionscheduler_logs, array( 'action_id' => $action_id ), array( '%d' ) ); } /** * Bulk add cancel action log entries. * * @param array $action_ids List of action ID. */ public function bulk_log_cancel_actions( $action_ids ) { if ( empty( $action_ids ) ) { return; } /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort global $wpdb; $date = as_get_datetime_object(); $date_gmt = $date->format( 'Y-m-d H:i:s' ); ActionScheduler_TimezoneHelper::set_local_timezone( $date ); $date_local = $date->format( 'Y-m-d H:i:s' ); $message = __( 'action canceled', 'action-scheduler' ); $format = '(%d, ' . $wpdb->prepare( '%s, %s, %s', $message, $date_gmt, $date_local ) . ')'; $sql_query = "INSERT {$wpdb->actionscheduler_logs} (action_id, message, log_date_gmt, log_date_local) VALUES "; $value_rows = array(); foreach ( $action_ids as $action_id ) { $value_rows[] = $wpdb->prepare( $format, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } $sql_query .= implode( ',', $value_rows ); $wpdb->query( $sql_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php 0000644 00000003711 15174671745 0030024 0 ustar 00 <?php /** * Class ActionScheduler_wpPostStore_PostTypeRegistrar * * @codeCoverageIgnore */ class ActionScheduler_wpPostStore_PostTypeRegistrar { /** * Registrar. */ public function register() { register_post_type( ActionScheduler_wpPostStore::POST_TYPE, $this->post_type_args() ); } /** * Build the args array for the post type definition * * @return array */ protected function post_type_args() { $args = array( 'label' => __( 'Scheduled Actions', 'action-scheduler' ), 'description' => __( 'Scheduled actions are hooks triggered on a certain date and time.', 'action-scheduler' ), 'public' => false, 'map_meta_cap' => true, 'hierarchical' => false, 'supports' => array( 'title', 'editor', 'comments' ), 'rewrite' => false, 'query_var' => false, 'can_export' => true, 'ep_mask' => EP_NONE, 'labels' => array( 'name' => __( 'Scheduled Actions', 'action-scheduler' ), 'singular_name' => __( 'Scheduled Action', 'action-scheduler' ), 'menu_name' => _x( 'Scheduled Actions', 'Admin menu name', 'action-scheduler' ), 'add_new' => __( 'Add', 'action-scheduler' ), 'add_new_item' => __( 'Add New Scheduled Action', 'action-scheduler' ), 'edit' => __( 'Edit', 'action-scheduler' ), 'edit_item' => __( 'Edit Scheduled Action', 'action-scheduler' ), 'new_item' => __( 'New Scheduled Action', 'action-scheduler' ), 'view' => __( 'View Action', 'action-scheduler' ), 'view_item' => __( 'View Action', 'action-scheduler' ), 'search_items' => __( 'Search Scheduled Actions', 'action-scheduler' ), 'not_found' => __( 'No actions found', 'action-scheduler' ), 'not_found_in_trash' => __( 'No actions found in trash', 'action-scheduler' ), ), ); $args = apply_filters( 'action_scheduler_post_type_args', $args ); return $args; } } Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php 0000644 00000114765 15174671745 0023277 0 ustar 00 <?php /** * Class ActionScheduler_DBStore * * Action data table data store. * * @since 3.0.0 */ class ActionScheduler_DBStore extends ActionScheduler_Store { /** * Used to share information about the before_date property of claims internally. * * This is used in preference to passing the same information as a method param * for backwards-compatibility reasons. * * @var DateTime|null */ private $claim_before_date = null; /** * Maximum length of args. * * @var int */ protected static $max_args_length = 8000; /** * Maximum length of index. * * @var int */ protected static $max_index_length = 191; /** * List of claim filters. * * @var array */ protected $claim_filters = array( 'group' => '', 'hooks' => '', 'exclude-groups' => '', ); /** * Initialize the data store * * @codeCoverageIgnore */ public function init() { $table_maker = new ActionScheduler_StoreSchema(); $table_maker->init(); $table_maker->register_tables(); } /** * Save an action, checks if this is a unique action before actually saving. * * @param ActionScheduler_Action $action Action object. * @param DateTime|null $scheduled_date Optional schedule date. Default null. * * @return int Action ID. * @throws RuntimeException Throws exception when saving the action fails. */ public function save_unique_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) { return $this->save_action_to_db( $action, $scheduled_date, true ); } /** * Save an action. Can save duplicate action as well, prefer using `save_unique_action` instead. * * @param ActionScheduler_Action $action Action object. * @param DateTime|null $scheduled_date Optional schedule date. Default null. * * @return int Action ID. * @throws RuntimeException Throws exception when saving the action fails. */ public function save_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) { return $this->save_action_to_db( $action, $scheduled_date, false ); } /** * Save an action. * * @param ActionScheduler_Action $action Action object. * @param ?DateTime $date Optional schedule date. Default null. * @param bool $unique Whether the action should be unique. * * @return int Action ID. * @throws \RuntimeException Throws exception when saving the action fails. */ private function save_action_to_db( ActionScheduler_Action $action, ?DateTime $date = null, $unique = false ) { global $wpdb; try { $this->validate_action( $action ); $data = array( 'hook' => $action->get_hook(), 'status' => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ), 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ), 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ), 'schedule' => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize 'group_id' => current( $this->get_group_ids( $action->get_group() ) ), 'priority' => $action->get_priority(), ); $args = wp_json_encode( $action->get_args() ); if ( strlen( $args ) <= static::$max_index_length ) { $data['args'] = $args; } else { $data['args'] = $this->hash_args( $args ); $data['extended_args'] = $args; } $insert_sql = $this->build_insert_sql( $data, $unique ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_sql should be already prepared. $wpdb->query( $insert_sql ); $action_id = $wpdb->insert_id; if ( is_wp_error( $action_id ) ) { throw new \RuntimeException( $action_id->get_error_message() ); } elseif ( empty( $action_id ) ) { if ( $unique ) { return 0; } throw new \RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'action-scheduler' ) ); } do_action( 'action_scheduler_stored_action', $action_id ); return $action_id; } catch ( \Exception $e ) { /* translators: %s: error message */ throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); } } /** * Helper function to build insert query. * * @param array $data Row data for action. * @param bool $unique Whether the action should be unique. * * @return string Insert query. */ private function build_insert_sql( array $data, $unique ) { global $wpdb; $columns = array_keys( $data ); $values = array_values( $data ); $placeholders = array_map( array( $this, 'get_placeholder_for_column' ), $columns ); $table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions'; $column_sql = '`' . implode( '`, `', $columns ) . '`'; $placeholder_sql = implode( ', ', $placeholders ); $where_clause = $this->build_where_clause_for_insert( $data, $table_name, $unique ); // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $column_sql and $where_clause are already prepared. $placeholder_sql is hardcoded. $insert_query = $wpdb->prepare( " INSERT INTO $table_name ( $column_sql ) SELECT $placeholder_sql FROM DUAL WHERE ( $where_clause ) IS NULL", $values ); // phpcs:enable return $insert_query; } /** * Helper method to build where clause for action insert statement. * * @param array $data Row data for action. * @param string $table_name Action table name. * @param bool $unique Where action should be unique. * * @return string Where clause to be used with insert. */ private function build_where_clause_for_insert( $data, $table_name, $unique ) { global $wpdb; if ( ! $unique ) { return 'SELECT NULL FROM DUAL'; } $pending_statuses = array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_RUNNING, ); $pending_status_placeholders = implode( ', ', array_fill( 0, count( $pending_statuses ), '%s' ) ); // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $pending_status_placeholders is hardcoded. $where_clause = $wpdb->prepare( " SELECT action_id FROM $table_name WHERE status IN ( $pending_status_placeholders ) AND hook = %s AND `group_id` = %d ", array_merge( $pending_statuses, array( $data['hook'], $data['group_id'], ) ) ); // phpcs:enable return "$where_clause" . ' LIMIT 1'; } /** * Helper method to get $wpdb->prepare placeholder for a given column name. * * @param string $column_name Name of column in actions table. * * @return string Placeholder to use for given column. */ private function get_placeholder_for_column( $column_name ) { $string_columns = array( 'hook', 'status', 'scheduled_date_gmt', 'scheduled_date_local', 'args', 'schedule', 'last_attempt_gmt', 'last_attempt_local', 'extended_args', ); return in_array( $column_name, $string_columns, true ) ? '%s' : '%d'; } /** * Generate a hash from json_encoded $args using MD5 as this isn't for security. * * @param string $args JSON encoded action args. * @return string */ protected function hash_args( $args ) { return md5( $args ); } /** * Get action args query param value from action args. * * @param array $args Action args. * @return string */ protected function get_args_for_query( $args ) { $encoded = wp_json_encode( $args ); if ( strlen( $encoded ) <= static::$max_index_length ) { return $encoded; } return $this->hash_args( $encoded ); } /** * Get a group's ID based on its name/slug. * * @param string|array $slugs The string name of a group, or names for several groups. * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. * * @return array The group IDs, if they exist or were successfully created. May be empty. */ protected function get_group_ids( $slugs, $create_if_not_exists = true ) { $slugs = (array) $slugs; $group_ids = array(); if ( empty( $slugs ) ) { return array(); } /** * Global. * * @var \wpdb $wpdb */ global $wpdb; foreach ( $slugs as $slug ) { $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); if ( empty( $group_id ) && $create_if_not_exists ) { $group_id = $this->create_group( $slug ); } if ( $group_id ) { $group_ids[] = $group_id; } } return $group_ids; } /** * Create an action group. * * @param string $slug Group slug. * * @return int Group ID. */ protected function create_group( $slug ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $wpdb->insert( $wpdb->actionscheduler_groups, array( 'slug' => $slug ) ); return (int) $wpdb->insert_id; } /** * Retrieve an action. * * @param int $action_id Action ID. * * @return ActionScheduler_Action */ public function fetch_action( $action_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $data = $wpdb->get_row( $wpdb->prepare( "SELECT a.*, g.slug AS `group` FROM {$wpdb->actionscheduler_actions} a LEFT JOIN {$wpdb->actionscheduler_groups} g ON a.group_id=g.group_id WHERE a.action_id=%d", $action_id ) ); if ( empty( $data ) ) { return $this->get_null_action(); } if ( ! empty( $data->extended_args ) ) { $data->args = $data->extended_args; unset( $data->extended_args ); } // Convert NULL dates to zero dates. $date_fields = array( 'scheduled_date_gmt', 'scheduled_date_local', 'last_attempt_gmt', 'last_attempt_gmt', ); foreach ( $date_fields as $date_field ) { if ( is_null( $data->$date_field ) ) { $data->$date_field = ActionScheduler_StoreSchema::DEFAULT_DATE; } } try { $action = $this->make_action_from_db_record( $data ); } catch ( ActionScheduler_InvalidActionException $exception ) { do_action( 'action_scheduler_failed_fetch_action', $action_id, $exception ); return $this->get_null_action(); } return $action; } /** * Create a null action. * * @return ActionScheduler_NullAction */ protected function get_null_action() { return new ActionScheduler_NullAction(); } /** * Create an action from a database record. * * @param object $data Action database record. * * @return ActionScheduler_Action|ActionScheduler_CanceledAction|ActionScheduler_FinishedAction */ protected function make_action_from_db_record( $data ) { $hook = $data->hook; $args = json_decode( $data->args, true ); $schedule = unserialize( $data->schedule ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize $this->validate_args( $args, $data->action_id ); $this->validate_schedule( $schedule, $data->action_id ); if ( empty( $schedule ) ) { $schedule = new ActionScheduler_NullSchedule(); } $group = $data->group ? $data->group : ''; return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group, $data->priority ); } /** * Returns the SQL statement to query (or count) actions. * * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status. * * @param array $query Filtering options. * @param string $select_or_count Whether the SQL should select and return the IDs or just the row count. * * @return string SQL statement already properly escaped. * @throws \InvalidArgumentException If the query is invalid. * @throws \RuntimeException When "unknown partial args matching value". */ protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) { if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) { throw new InvalidArgumentException( __( 'Invalid value for select or count parameter. Cannot query actions.', 'action-scheduler' ) ); } $query = wp_parse_args( $query, array( 'hook' => '', 'args' => null, 'partial_args_matching' => 'off', // can be 'like' or 'json'. 'date' => null, 'date_compare' => '<=', 'modified' => null, 'modified_compare' => '<=', 'group' => '', 'status' => '', 'claimed' => null, 'per_page' => 5, 'offset' => 0, 'orderby' => 'date', 'order' => 'ASC', ) ); /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $db_server_info = is_callable( array( $wpdb, 'db_server_info' ) ) ? $wpdb->db_server_info() : $wpdb->db_version(); if ( false !== strpos( $db_server_info, 'MariaDB' ) ) { $supports_json = version_compare( PHP_VERSION_ID >= 80016 ? $wpdb->db_version() : preg_replace( '/[^0-9.].*/', '', str_replace( '5.5.5-', '', $db_server_info ) ), '10.2', '>=' ); } else { $supports_json = version_compare( $wpdb->db_version(), '5.7', '>=' ); } $sql = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id'; $sql .= " FROM {$wpdb->actionscheduler_actions} a"; $sql_params = array(); if ( ! empty( $query['group'] ) || 'group' === $query['orderby'] ) { $sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id"; } $sql .= ' WHERE 1=1'; if ( ! empty( $query['group'] ) ) { $sql .= ' AND g.slug=%s'; $sql_params[] = $query['group']; } if ( ! empty( $query['hook'] ) ) { $sql .= ' AND a.hook=%s'; $sql_params[] = $query['hook']; } if ( ! is_null( $query['args'] ) ) { switch ( $query['partial_args_matching'] ) { case 'json': if ( ! $supports_json ) { throw new \RuntimeException( __( 'JSON partial matching not supported in your environment. Please check your MySQL/MariaDB version.', 'action-scheduler' ) ); } $supported_types = array( 'integer' => '%d', 'boolean' => '%s', 'double' => '%f', 'string' => '%s', ); foreach ( $query['args'] as $key => $value ) { $value_type = gettype( $value ); if ( 'boolean' === $value_type ) { $value = $value ? 'true' : 'false'; } $placeholder = isset( $supported_types[ $value_type ] ) ? $supported_types[ $value_type ] : false; if ( ! $placeholder ) { throw new \RuntimeException( sprintf( /* translators: %s: provided value type */ __( 'The value type for the JSON partial matching is not supported. Must be either integer, boolean, double or string. %s type provided.', 'action-scheduler' ), $value_type ) ); } $sql .= ' AND JSON_EXTRACT(a.args, %s)=' . $placeholder; $sql_params[] = '$.' . $key; $sql_params[] = $value; } break; case 'like': foreach ( $query['args'] as $key => $value ) { $sql .= ' AND a.args LIKE %s'; $json_partial = $wpdb->esc_like( trim( wp_json_encode( array( $key => $value ) ), '{}' ) ); $sql_params[] = "%{$json_partial}%"; } break; case 'off': $sql .= ' AND a.args=%s'; $sql_params[] = $this->get_args_for_query( $query['args'] ); break; default: throw new \RuntimeException( __( 'Unknown partial args matching value.', 'action-scheduler' ) ); } } if ( $query['status'] ) { $statuses = (array) $query['status']; $placeholders = array_fill( 0, count( $statuses ), '%s' ); $sql .= ' AND a.status IN (' . join( ', ', $placeholders ) . ')'; $sql_params = array_merge( $sql_params, array_values( $statuses ) ); } if ( $query['date'] instanceof \DateTime ) { $date = clone $query['date']; $date->setTimezone( new \DateTimeZone( 'UTC' ) ); $date_string = $date->format( 'Y-m-d H:i:s' ); $comparator = $this->validate_sql_comparator( $query['date_compare'] ); $sql .= " AND a.scheduled_date_gmt $comparator %s"; $sql_params[] = $date_string; } if ( $query['modified'] instanceof \DateTime ) { $modified = clone $query['modified']; $modified->setTimezone( new \DateTimeZone( 'UTC' ) ); $date_string = $modified->format( 'Y-m-d H:i:s' ); $comparator = $this->validate_sql_comparator( $query['modified_compare'] ); $sql .= " AND a.last_attempt_gmt $comparator %s"; $sql_params[] = $date_string; } if ( true === $query['claimed'] ) { $sql .= ' AND a.claim_id != 0'; } elseif ( false === $query['claimed'] ) { $sql .= ' AND a.claim_id = 0'; } elseif ( ! is_null( $query['claimed'] ) ) { $sql .= ' AND a.claim_id = %d'; $sql_params[] = $query['claimed']; } if ( ! empty( $query['search'] ) ) { $sql .= ' AND (a.hook LIKE %s OR (a.extended_args IS NULL AND a.args LIKE %s) OR a.extended_args LIKE %s'; for ( $i = 0; $i < 3; $i++ ) { $sql_params[] = sprintf( '%%%s%%', $query['search'] ); } $search_claim_id = (int) $query['search']; if ( $search_claim_id ) { $sql .= ' OR a.claim_id = %d'; $sql_params[] = $search_claim_id; } $sql .= ')'; } if ( 'select' === $select_or_count ) { if ( 'ASC' === strtoupper( $query['order'] ) ) { $order = 'ASC'; } else { $order = 'DESC'; } switch ( $query['orderby'] ) { case 'hook': $sql .= " ORDER BY a.hook $order"; break; case 'group': $sql .= " ORDER BY g.slug $order"; break; case 'modified': $sql .= " ORDER BY a.last_attempt_gmt $order"; break; case 'none': break; case 'action_id': $sql .= " ORDER BY a.action_id $order"; break; case 'date': default: $sql .= " ORDER BY a.scheduled_date_gmt $order"; break; } if ( $query['per_page'] > 0 ) { $sql .= ' LIMIT %d, %d'; $sql_params[] = $query['offset']; $sql_params[] = $query['per_page']; } } if ( ! empty( $sql_params ) ) { $sql = $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } return $sql; } /** * Query for action count or list of action IDs. * * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status. * * @see ActionScheduler_Store::query_actions for $query arg usage. * * @param array $query Query filtering options. * @param string $query_type Whether to select or count the results. Defaults to select. * * @return string|array|null The IDs of actions matching the query. Null on failure. */ public function query_actions( $query = array(), $query_type = 'select' ) { /** * Global. * * @var wpdb $wpdb */ global $wpdb; $sql = $this->get_query_actions_sql( $query, $query_type ); return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoSql, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * Get a count of all actions in the store, grouped by status. * * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status. */ public function action_counts() { global $wpdb; $sql = "SELECT a.status, count(a.status) as 'count'"; $sql .= " FROM {$wpdb->actionscheduler_actions} a"; $sql .= ' GROUP BY a.status'; $actions_count_by_status = array(); $action_stati_and_labels = $this->get_status_labels(); foreach ( $wpdb->get_results( $sql ) as $action_data ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared // Ignore any actions with invalid status. if ( array_key_exists( $action_data->status, $action_stati_and_labels ) ) { $actions_count_by_status[ $action_data->status ] = $action_data->count; } } return $actions_count_by_status; } /** * Cancel an action. * * @param int $action_id Action ID. * * @return void * @throws \InvalidArgumentException If the action update failed. */ public function cancel_action( $action_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $updated = $wpdb->update( $wpdb->actionscheduler_actions, array( 'status' => self::STATUS_CANCELED ), array( 'action_id' => $action_id ), array( '%s' ), array( '%d' ) ); if ( false === $updated ) { /* translators: %s: action ID */ throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to cancel this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } do_action( 'action_scheduler_canceled_action', $action_id ); } /** * Cancel pending actions by hook. * * @since 3.0.0 * * @param string $hook Hook name. * * @return void */ public function cancel_actions_by_hook( $hook ) { $this->bulk_cancel_actions( array( 'hook' => $hook ) ); } /** * Cancel pending actions by group. * * @param string $group Group slug. * * @return void */ public function cancel_actions_by_group( $group ) { $this->bulk_cancel_actions( array( 'group' => $group ) ); } /** * Bulk cancel actions. * * @since 3.0.0 * * @param array $query_args Query parameters. */ protected function bulk_cancel_actions( $query_args ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; if ( ! is_array( $query_args ) ) { return; } // Don't cancel actions that are already canceled. if ( isset( $query_args['status'] ) && self::STATUS_CANCELED === $query_args['status'] ) { return; } $action_ids = true; $query_args = wp_parse_args( $query_args, array( 'per_page' => 1000, 'status' => self::STATUS_PENDING, 'orderby' => 'none', ) ); while ( $action_ids ) { $action_ids = $this->query_actions( $query_args ); if ( empty( $action_ids ) ) { break; } $format = array_fill( 0, count( $action_ids ), '%d' ); $query_in = '(' . implode( ',', $format ) . ')'; $parameters = $action_ids; array_unshift( $parameters, self::STATUS_CANCELED ); $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->actionscheduler_actions} SET status = %s WHERE action_id IN {$query_in}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $parameters ) ); do_action( 'action_scheduler_bulk_cancel_actions', $action_ids ); } } /** * Delete an action. * * @param int $action_id Action ID. * @throws \InvalidArgumentException If the action deletion failed. */ public function delete_action( $action_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $deleted = $wpdb->delete( $wpdb->actionscheduler_actions, array( 'action_id' => $action_id ), array( '%d' ) ); if ( empty( $deleted ) ) { /* translators: %s is the action ID */ throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to delete this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } do_action( 'action_scheduler_deleted_action', $action_id ); } /** * Get the schedule date for an action. * * @param string $action_id Action ID. * * @return \DateTime The local date the action is scheduled to run, or the date that it ran. */ public function get_date( $action_id ) { $date = $this->get_date_gmt( $action_id ); ActionScheduler_TimezoneHelper::set_local_timezone( $date ); return $date; } /** * Get the GMT schedule date for an action. * * @param int $action_id Action ID. * * @throws \InvalidArgumentException If action cannot be identified. * @return \DateTime The GMT date the action is scheduled to run, or the date that it ran. */ protected function get_date_gmt( $action_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) ); if ( empty( $record ) ) { /* translators: %s is the action ID */ throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to determine the date of this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } if ( self::STATUS_PENDING === $record->status ) { return as_get_datetime_object( $record->scheduled_date_gmt ); } else { return as_get_datetime_object( $record->last_attempt_gmt ); } } /** * Stake a claim on actions. * * @param int $max_actions Maximum number of action to include in claim. * @param DateTime|null $before_date Jobs must be schedule before this date. Defaults to now. * @param array $hooks Hooks to filter for. * @param string $group Group to filter for. * * @return ActionScheduler_ActionClaim */ public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ) { $claim_id = $this->generate_claim_id(); $this->claim_before_date = $before_date; $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group ); $action_ids = $this->find_actions_by_claim_id( $claim_id ); $this->claim_before_date = null; return new ActionScheduler_ActionClaim( $claim_id, $action_ids ); } /** * Generate a new action claim. * * @return int Claim ID. */ protected function generate_claim_id() { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $now = as_get_datetime_object(); $wpdb->insert( $wpdb->actionscheduler_claims, array( 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ) ); return $wpdb->insert_id; } /** * Set a claim filter. * * @param string $filter_name Claim filter name. * @param mixed $filter_values Values to filter. * @return void */ public function set_claim_filter( $filter_name, $filter_values ) { if ( isset( $this->claim_filters[ $filter_name ] ) ) { $this->claim_filters[ $filter_name ] = $filter_values; } } /** * Get the claim filter value. * * @param string $filter_name Claim filter name. * @return mixed */ public function get_claim_filter( $filter_name ) { if ( isset( $this->claim_filters[ $filter_name ] ) ) { return $this->claim_filters[ $filter_name ]; } return ''; } /** * Mark actions claimed. * * @param string $claim_id Claim Id. * @param int $limit Number of action to include in claim. * @param DateTime|null $before_date Should use UTC timezone. * @param array $hooks Hooks to filter for. * @param string $group Group to filter for. * * @return int The number of actions that were claimed. * @throws \InvalidArgumentException Throws InvalidArgumentException if group doesn't exist. * @throws \RuntimeException Throws RuntimeException if unable to claim action. */ protected function claim_actions( $claim_id, $limit, ?DateTime $before_date = null, $hooks = array(), $group = '' ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $now = as_get_datetime_object(); $date = is_null( $before_date ) ? $now : clone $before_date; // can't use $wpdb->update() because of the <= condition. $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s"; $params = array( $claim_id, $now->format( 'Y-m-d H:i:s' ), current_time( 'mysql' ), ); // Set claim filters. if ( ! empty( $hooks ) ) { $this->set_claim_filter( 'hooks', $hooks ); } else { $hooks = $this->get_claim_filter( 'hooks' ); } if ( ! empty( $group ) ) { $this->set_claim_filter( 'group', $group ); } else { $group = $this->get_claim_filter( 'group' ); } $where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s'; $params[] = $date->format( 'Y-m-d H:i:s' ); $params[] = self::STATUS_PENDING; if ( ! empty( $hooks ) ) { $placeholders = array_fill( 0, count( $hooks ), '%s' ); $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')'; $params = array_merge( $params, array_values( $hooks ) ); } $group_operator = 'IN'; if ( empty( $group ) ) { $group = $this->get_claim_filter( 'exclude-groups' ); $group_operator = 'NOT IN'; } if ( ! empty( $group ) ) { $group_ids = $this->get_group_ids( $group, false ); // throw exception if no matching group(s) found, this matches ActionScheduler_wpPostStore's behaviour. if ( empty( $group_ids ) ) { throw new InvalidArgumentException( sprintf( /* translators: %s: group name(s) */ _n( 'The group "%s" does not exist.', 'The groups "%s" do not exist.', is_array( $group ) ? count( $group ) : 1, 'action-scheduler' ), $group ) ); } $id_list = implode( ',', array_map( 'intval', $group_ids ) ); $where .= " AND group_id {$group_operator} ( $id_list )"; } /** * Sets the order-by clause used in the action claim query. * * @since 3.4.0 * @since 3.8.3 Made $claim_id and $hooks available. * * @param string $order_by_sql * @param string $claim_id Claim Id. * @param array $hooks Hooks to filter for. */ $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC', $claim_id, $hooks ); $params[] = $limit; $sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders $rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( false === $rows_affected ) { $error = empty( $wpdb->last_error ) ? _x( 'unknown', 'database error', 'action-scheduler' ) : $wpdb->last_error; throw new \RuntimeException( sprintf( /* translators: %s database error. */ __( 'Unable to claim actions. Database error: %s.', 'action-scheduler' ), $error ) ); } return (int) $rows_affected; } /** * Get the number of active claims. * * @return int */ public function get_claim_count() { global $wpdb; $sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)"; $sql = $wpdb->prepare( $sql, array( self::STATUS_PENDING, self::STATUS_RUNNING ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * Return an action's claim ID, as stored in the claim_id column. * * @param string $action_id Action ID. * @return mixed */ public function get_claim_id( $action_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * Retrieve the action IDs of action in a claim. * * @param int $claim_id Claim ID. * @return int[] */ public function find_actions_by_claim_id( $claim_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $action_ids = array(); $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object(); $cut_off = $before_date->format( 'Y-m-d H:i:s' ); $sql = $wpdb->prepare( "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC", $claim_id ); // Verify that the scheduled date for each action is within the expected bounds (in some unusual // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify). foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( $claimed_action->scheduled_date_gmt <= $cut_off ) { $action_ids[] = absint( $claimed_action->action_id ); } } return $action_ids; } /** * Release actions from a claim and delete the claim. * * @param ActionScheduler_ActionClaim $claim Claim object. * @throws \RuntimeException When unable to release actions from claim. */ public function release_claim( ActionScheduler_ActionClaim $claim ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; /** * Deadlock warning: This function modifies actions to release them from claims that have been processed. Earlier, we used to it in a atomic query, i.e. we would update all actions belonging to a particular claim_id with claim_id = 0. * While this was functionally correct, it would cause deadlock, since this update query will hold a lock on the claim_id_.. index on the action table. * This allowed the possibility of a race condition, where the claimer query is also running at the same time, then the claimer query will also try to acquire a lock on the claim_id_.. index, and in this case if claim release query has already progressed to the point of acquiring the lock, but have not updated yet, it would cause a deadlock. * * We resolve this by getting all the actions_id that we want to release claim from in a separate query, and then releasing the claim on each of them. This way, our lock is acquired on the action_id index instead of the claim_id index. Note that the lock on claim_id will still be acquired, but it will only when we actually make the update, rather than when we select the actions. */ $action_ids = $wpdb->get_col( $wpdb->prepare( "SELECT action_id FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", $claim->get_id() ) ); $row_updates = 0; if ( count( $action_ids ) > 0 ) { $action_id_string = implode( ',', array_map( 'absint', $action_ids ) ); $row_updates = $wpdb->query( "UPDATE {$wpdb->actionscheduler_actions} SET claim_id = 0 WHERE action_id IN ({$action_id_string})" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } $wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) ); if ( $row_updates < count( $action_ids ) ) { throw new RuntimeException( sprintf( // translators: %d is an id. __( 'Unable to release actions from claim id %d.', 'action-scheduler' ), $claim->get_id() ) ); } } /** * Remove the claim from an action. * * @param int $action_id Action ID. * * @return void */ public function unclaim_action( $action_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $wpdb->update( $wpdb->actionscheduler_actions, array( 'claim_id' => 0 ), array( 'action_id' => $action_id ), array( '%s' ), array( '%d' ) ); } /** * Mark an action as failed. * * @param int $action_id Action ID. * @throws \InvalidArgumentException Throw an exception if action was not updated. */ public function mark_failure( $action_id ) { /** * Global. * @var \wpdb $wpdb */ global $wpdb; $updated = $wpdb->update( $wpdb->actionscheduler_actions, array( 'status' => self::STATUS_FAILED ), array( 'action_id' => $action_id ), array( '%s' ), array( '%d' ) ); if ( empty( $updated ) ) { /* translators: %s is the action ID */ throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having failed. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } } /** * Add execution message to action log. * * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress'). * * @param int $action_id Action ID. * * @return void */ public function log_execution( $action_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d"; $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $status_updated = $wpdb->query( $sql ); if ( ! $status_updated ) { throw new Exception( sprintf( /* translators: 1: action ID. 2: status slug. */ __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ), $action_id, self::STATUS_RUNNING ) ); } } /** * Mark an action as complete. * * @param int $action_id Action ID. * * @return void * @throws \InvalidArgumentException Throw an exception if action was not updated. */ public function mark_complete( $action_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $updated = $wpdb->update( $wpdb->actionscheduler_actions, array( 'status' => self::STATUS_COMPLETE, 'last_attempt_gmt' => current_time( 'mysql', true ), 'last_attempt_local' => current_time( 'mysql' ), ), array( 'action_id' => $action_id ), array( '%s' ), array( '%d' ) ); if ( empty( $updated ) ) { /* translators: %s is the action ID */ throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having completed. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } /** * Fires after a scheduled action has been completed. * * @since 3.4.2 * * @param int $action_id Action ID. */ do_action( 'action_scheduler_completed_action', $action_id ); } /** * Get an action's status. * * @param int $action_id Action ID. * * @return string * @throws \InvalidArgumentException Throw an exception if not status was found for action_id. * @throws \RuntimeException Throw an exception if action status could not be retrieved. */ public function get_status( $action_id ) { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $sql = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $status = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( is_null( $status ) ) { throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) ); } elseif ( empty( $status ) ) { throw new \RuntimeException( __( 'Unknown status found for action.', 'action-scheduler' ) ); } else { return $status; } } } Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php 0000644 00000017033 15174671745 0025074 0 ustar 00 <?php /** * Class ActionScheduler_wpCommentLogger */ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger { const AGENT = 'ActionScheduler'; const TYPE = 'action_log'; /** * Create log entry. * * @param string $action_id Action ID. * @param string $message Action log's message. * @param DateTime|null $date Action log's timestamp. * * @return string The log entry ID */ public function log( $action_id, $message, ?DateTime $date = null ) { if ( empty( $date ) ) { $date = as_get_datetime_object(); } else { $date = as_get_datetime_object( clone $date ); } $comment_id = $this->create_wp_comment( $action_id, $message, $date ); return $comment_id; } /** * Create comment. * * @param int $action_id Action ID. * @param string $message Action log's message. * @param DateTime $date Action log entry's timestamp. */ protected function create_wp_comment( $action_id, $message, DateTime $date ) { $comment_date_gmt = $date->format( 'Y-m-d H:i:s' ); ActionScheduler_TimezoneHelper::set_local_timezone( $date ); $comment_data = array( 'comment_post_ID' => $action_id, 'comment_date' => $date->format( 'Y-m-d H:i:s' ), 'comment_date_gmt' => $comment_date_gmt, 'comment_author' => self::AGENT, 'comment_content' => $message, 'comment_agent' => self::AGENT, 'comment_type' => self::TYPE, ); return wp_insert_comment( $comment_data ); } /** * Get single log entry for action. * * @param string $entry_id Entry ID. * * @return ActionScheduler_LogEntry */ public function get_entry( $entry_id ) { $comment = $this->get_comment( $entry_id ); if ( empty( $comment ) || self::TYPE !== $comment->comment_type ) { return new ActionScheduler_NullLogEntry(); } $date = as_get_datetime_object( $comment->comment_date_gmt ); ActionScheduler_TimezoneHelper::set_local_timezone( $date ); return new ActionScheduler_LogEntry( $comment->comment_post_ID, $comment->comment_content, $date ); } /** * Get action's logs. * * @param string $action_id Action ID. * * @return ActionScheduler_LogEntry[] */ public function get_logs( $action_id ) { $status = 'all'; $logs = array(); if ( get_post_status( $action_id ) === 'trash' ) { $status = 'post-trashed'; } $comments = get_comments( array( 'post_id' => $action_id, 'orderby' => 'comment_date_gmt', 'order' => 'ASC', 'type' => self::TYPE, 'status' => $status, ) ); foreach ( $comments as $c ) { $entry = $this->get_entry( $c ); if ( ! empty( $entry ) ) { $logs[] = $entry; } } return $logs; } /** * Get comment. * * @param int $comment_id Comment ID. */ protected function get_comment( $comment_id ) { return get_comment( $comment_id ); } /** * Filter comment queries. * * @param WP_Comment_Query $query Comment query object. */ public function filter_comment_queries( $query ) { foreach ( array( 'ID', 'parent', 'post_author', 'post_name', 'post_parent', 'type', 'post_type', 'post_id', 'post_ID' ) as $key ) { if ( ! empty( $query->query_vars[ $key ] ) ) { return; // don't slow down queries that wouldn't include action_log comments anyway. } } $query->query_vars['action_log_filter'] = true; add_filter( 'comments_clauses', array( $this, 'filter_comment_query_clauses' ), 10, 2 ); } /** * Filter comment queries. * * @param array $clauses Query's clauses. * @param WP_Comment_Query $query Query object. * * @return array */ public function filter_comment_query_clauses( $clauses, $query ) { if ( ! empty( $query->query_vars['action_log_filter'] ) ) { $clauses['where'] .= $this->get_where_clause(); } return $clauses; } /** * Make sure Action Scheduler logs are excluded from comment feeds, which use WP_Query, not * the WP_Comment_Query class handled by @see self::filter_comment_queries(). * * @param string $where Query's `where` clause. * @param WP_Query $query Query object. * * @return string */ public function filter_comment_feed( $where, $query ) { if ( is_comment_feed() ) { $where .= $this->get_where_clause(); } return $where; } /** * Return a SQL clause to exclude Action Scheduler comments. * * @return string */ protected function get_where_clause() { global $wpdb; return sprintf( " AND {$wpdb->comments}.comment_type != '%s'", self::TYPE ); } /** * Remove action log entries from wp_count_comments() * * @param array $stats Comment count. * @param int $post_id Post ID. * * @return object */ public function filter_comment_count( $stats, $post_id ) { global $wpdb; if ( 0 === $post_id ) { $stats = $this->get_comment_count(); } return $stats; } /** * Retrieve the comment counts from our cache, or the database if the cached version isn't set. * * @return object */ protected function get_comment_count() { global $wpdb; $stats = get_transient( 'as_comment_count' ); if ( ! $stats ) { $stats = array(); $count = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} WHERE comment_type NOT IN('order_note','action_log') GROUP BY comment_approved", ARRAY_A ); $total = 0; $stats = array(); $approved = array( '0' => 'moderated', '1' => 'approved', 'spam' => 'spam', 'trash' => 'trash', 'post-trashed' => 'post-trashed', ); foreach ( (array) $count as $row ) { // Don't count post-trashed toward totals. if ( 'post-trashed' !== $row['comment_approved'] && 'trash' !== $row['comment_approved'] ) { $total += $row['num_comments']; } if ( isset( $approved[ $row['comment_approved'] ] ) ) { $stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments']; } } $stats['total_comments'] = $total; $stats['all'] = $total; foreach ( $approved as $key ) { if ( empty( $stats[ $key ] ) ) { $stats[ $key ] = 0; } } $stats = (object) $stats; set_transient( 'as_comment_count', $stats ); } return $stats; } /** * Delete comment count cache whenever there is new comment or the status of a comment changes. Cache * will be regenerated next time ActionScheduler_wpCommentLogger::filter_comment_count() is called. */ public function delete_comment_count_cache() { delete_transient( 'as_comment_count' ); } /** * Initialize. * * @codeCoverageIgnore */ public function init() { add_action( 'action_scheduler_before_process_queue', array( $this, 'disable_comment_counting' ), 10, 0 ); add_action( 'action_scheduler_after_process_queue', array( $this, 'enable_comment_counting' ), 10, 0 ); parent::init(); add_action( 'pre_get_comments', array( $this, 'filter_comment_queries' ), 10, 1 ); add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs. add_action( 'comment_feed_where', array( $this, 'filter_comment_feed' ), 10, 2 ); // Delete comments count cache whenever there is a new comment or a comment status changes. add_action( 'wp_insert_comment', array( $this, 'delete_comment_count_cache' ) ); add_action( 'wp_set_comment_status', array( $this, 'delete_comment_count_cache' ) ); } /** * Defer comment counting. */ public function disable_comment_counting() { wp_defer_comment_counting( true ); } /** * Enable comment counting. */ public function enable_comment_counting() { wp_defer_comment_counting( false ); } } Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php 0000644 00000106256 15174671745 0024302 0 ustar 00 <?php /** * Class ActionScheduler_wpPostStore */ class ActionScheduler_wpPostStore extends ActionScheduler_Store { const POST_TYPE = 'scheduled-action'; const GROUP_TAXONOMY = 'action-group'; const SCHEDULE_META_KEY = '_action_manager_schedule'; const DEPENDENCIES_MET = 'as-post-store-dependencies-met'; /** * Used to share information about the before_date property of claims internally. * * This is used in preference to passing the same information as a method param * for backwards-compatibility reasons. * * @var DateTime|null */ private $claim_before_date = null; /** * Local Timezone. * * @var DateTimeZone */ protected $local_timezone = null; /** * Save action. * * @param ActionScheduler_Action $action Scheduled Action. * @param DateTime|null $scheduled_date Scheduled Date. * * @throws RuntimeException Throws an exception if the action could not be saved. * @return int */ public function save_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) { try { $this->validate_action( $action ); $post_array = $this->create_post_array( $action, $scheduled_date ); $post_id = $this->save_post_array( $post_array ); $this->save_post_schedule( $post_id, $action->get_schedule() ); $this->save_action_group( $post_id, $action->get_group() ); do_action( 'action_scheduler_stored_action', $post_id ); return $post_id; } catch ( Exception $e ) { /* translators: %s: action error message */ throw new RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); } } /** * Create post array. * * @param ActionScheduler_Action $action Scheduled Action. * @param DateTime|null $scheduled_date Scheduled Date. * * @return array Returns an array of post data. */ protected function create_post_array( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) { $post = array( 'post_type' => self::POST_TYPE, 'post_title' => $action->get_hook(), 'post_content' => wp_json_encode( $action->get_args() ), 'post_status' => ( $action->is_finished() ? 'publish' : 'pending' ), 'post_date_gmt' => $this->get_scheduled_date_string( $action, $scheduled_date ), 'post_date' => $this->get_scheduled_date_string_local( $action, $scheduled_date ), ); return $post; } /** * Save post array. * * @param array $post_array Post array. * @return int Returns the post ID. * @throws RuntimeException Throws an exception if the action could not be saved. */ protected function save_post_array( $post_array ) { add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); if ( $has_kses ) { // Prevent KSES from corrupting JSON in post_content. kses_remove_filters(); } $post_id = wp_insert_post( $post_array ); if ( $has_kses ) { kses_init_filters(); } remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 ); remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); if ( is_wp_error( $post_id ) || empty( $post_id ) ) { throw new RuntimeException( __( 'Unable to save action.', 'action-scheduler' ) ); } return $post_id; } /** * Filter insert post data. * * @param array $postdata Post data to filter. * * @return array */ public function filter_insert_post_data( $postdata ) { if ( self::POST_TYPE === $postdata['post_type'] ) { $postdata['post_author'] = 0; if ( 'future' === $postdata['post_status'] ) { $postdata['post_status'] = 'publish'; } } return $postdata; } /** * Create a (probably unique) post name for scheduled actions in a more performant manner than wp_unique_post_slug(). * * When an action's post status is transitioned to something other than 'draft', 'pending' or 'auto-draft, like 'publish' * or 'failed' or 'trash', WordPress will find a unique slug (stored in post_name column) using the wp_unique_post_slug() * function. This is done to ensure URL uniqueness. The approach taken by wp_unique_post_slug() is to iterate over existing * post_name values that match, and append a number 1 greater than the largest. This makes sense when manually creating a * post from the Edit Post screen. It becomes a bottleneck when automatically processing thousands of actions, with a * database containing thousands of related post_name values. * * WordPress 5.1 introduces the 'pre_wp_unique_post_slug' filter for plugins to address this issue. * * We can short-circuit WordPress's wp_unique_post_slug() approach using the 'pre_wp_unique_post_slug' filter. This * method is available to be used as a callback on that filter. It provides a more scalable approach to generating a * post_name/slug that is probably unique. Because Action Scheduler never actually uses the post_name field, or an * action's slug, being probably unique is good enough. * * For more backstory on this issue, see: * - https://github.com/woocommerce/action-scheduler/issues/44 and * - https://core.trac.wordpress.org/ticket/21112 * * @param string $override_slug Short-circuit return value. * @param string $slug The desired slug (post_name). * @param int $post_ID Post ID. * @param string $post_status The post status. * @param string $post_type Post type. * @return string */ public function set_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) { if ( self::POST_TYPE === $post_type ) { $override_slug = uniqid( self::POST_TYPE . '-', true ) . '-' . wp_generate_password( 32, false ); } return $override_slug; } /** * Save post schedule. * * @param int $post_id Post ID of the scheduled action. * @param string $schedule Schedule to save. * * @return void */ protected function save_post_schedule( $post_id, $schedule ) { update_post_meta( $post_id, self::SCHEDULE_META_KEY, $schedule ); } /** * Save action group. * * @param int $post_id Post ID. * @param string $group Group to save. * @return void */ protected function save_action_group( $post_id, $group ) { if ( empty( $group ) ) { wp_set_object_terms( $post_id, array(), self::GROUP_TAXONOMY, false ); } else { wp_set_object_terms( $post_id, array( $group ), self::GROUP_TAXONOMY, false ); } } /** * Fetch actions. * * @param int $action_id Action ID. * @return object */ public function fetch_action( $action_id ) { $post = $this->get_post( $action_id ); if ( empty( $post ) || self::POST_TYPE !== $post->post_type ) { return $this->get_null_action(); } try { $action = $this->make_action_from_post( $post ); } catch ( ActionScheduler_InvalidActionException $exception ) { do_action( 'action_scheduler_failed_fetch_action', $post->ID, $exception ); return $this->get_null_action(); } return $action; } /** * Get post. * * @param string $action_id - Action ID. * @return WP_Post|null */ protected function get_post( $action_id ) { if ( empty( $action_id ) ) { return null; } return get_post( $action_id ); } /** * Get NULL action. * * @return ActionScheduler_NullAction */ protected function get_null_action() { return new ActionScheduler_NullAction(); } /** * Make action from post. * * @param WP_Post $post Post object. * @return WP_Post */ protected function make_action_from_post( $post ) { $hook = $post->post_title; $args = json_decode( $post->post_content, true ); $this->validate_args( $args, $post->ID ); $schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true ); $this->validate_schedule( $schedule, $post->ID ); $group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array( 'fields' => 'names' ) ); $group = empty( $group ) ? '' : reset( $group ); return ActionScheduler::factory()->get_stored_action( $this->get_action_status_by_post_status( $post->post_status ), $hook, $args, $schedule, $group ); } /** * Get action status by post status. * * @param string $post_status Post status. * * @throws InvalidArgumentException Throw InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels(). * @return string */ protected function get_action_status_by_post_status( $post_status ) { switch ( $post_status ) { case 'publish': $action_status = self::STATUS_COMPLETE; break; case 'trash': $action_status = self::STATUS_CANCELED; break; default: if ( ! array_key_exists( $post_status, $this->get_status_labels() ) ) { throw new InvalidArgumentException( sprintf( 'Invalid post status: "%s". No matching action status available.', $post_status ) ); } $action_status = $post_status; break; } return $action_status; } /** * Get post status by action status. * * @param string $action_status Action status. * * @throws InvalidArgumentException Throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels(). * @return string */ protected function get_post_status_by_action_status( $action_status ) { switch ( $action_status ) { case self::STATUS_COMPLETE: $post_status = 'publish'; break; case self::STATUS_CANCELED: $post_status = 'trash'; break; default: if ( ! array_key_exists( $action_status, $this->get_status_labels() ) ) { throw new InvalidArgumentException( sprintf( 'Invalid action status: "%s".', $action_status ) ); } $post_status = $action_status; break; } return $post_status; } /** * Returns the SQL statement to query (or count) actions. * * @param array $query - Filtering options. * @param string $select_or_count - Whether the SQL should select and return the IDs or just the row count. * * @throws InvalidArgumentException - Throw InvalidArgumentException if $select_or_count not count or select. * @return string SQL statement. The returned SQL is already properly escaped. */ protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) { if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) { throw new InvalidArgumentException( __( 'Invalid schedule. Cannot save action.', 'action-scheduler' ) ); } $query = wp_parse_args( $query, array( 'hook' => '', 'args' => null, 'date' => null, 'date_compare' => '<=', 'modified' => null, 'modified_compare' => '<=', 'group' => '', 'status' => '', 'claimed' => null, 'per_page' => 5, 'offset' => 0, 'orderby' => 'date', 'order' => 'ASC', 'search' => '', ) ); /** * Global wpdb object. * * @var wpdb $wpdb */ global $wpdb; $sql = ( 'count' === $select_or_count ) ? 'SELECT count(p.ID)' : 'SELECT p.ID '; $sql .= "FROM {$wpdb->posts} p"; $sql_params = array(); if ( empty( $query['group'] ) && 'group' === $query['orderby'] ) { $sql .= " LEFT JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID"; $sql .= " LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id"; $sql .= " LEFT JOIN {$wpdb->terms} t ON tt.term_id=t.term_id"; } elseif ( ! empty( $query['group'] ) ) { $sql .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID"; $sql .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id"; $sql .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id"; $sql .= ' AND t.slug=%s'; $sql_params[] = $query['group']; } $sql .= ' WHERE post_type=%s'; $sql_params[] = self::POST_TYPE; if ( $query['hook'] ) { $sql .= ' AND p.post_title=%s'; $sql_params[] = $query['hook']; } if ( ! is_null( $query['args'] ) ) { $sql .= ' AND p.post_content=%s'; $sql_params[] = wp_json_encode( $query['args'] ); } if ( $query['status'] ) { $post_statuses = array_map( array( $this, 'get_post_status_by_action_status' ), (array) $query['status'] ); $placeholders = array_fill( 0, count( $post_statuses ), '%s' ); $sql .= ' AND p.post_status IN (' . join( ', ', $placeholders ) . ')'; $sql_params = array_merge( $sql_params, array_values( $post_statuses ) ); } if ( $query['date'] instanceof DateTime ) { $date = clone $query['date']; $date->setTimezone( new DateTimeZone( 'UTC' ) ); $date_string = $date->format( 'Y-m-d H:i:s' ); $comparator = $this->validate_sql_comparator( $query['date_compare'] ); $sql .= " AND p.post_date_gmt $comparator %s"; $sql_params[] = $date_string; } if ( $query['modified'] instanceof DateTime ) { $modified = clone $query['modified']; $modified->setTimezone( new DateTimeZone( 'UTC' ) ); $date_string = $modified->format( 'Y-m-d H:i:s' ); $comparator = $this->validate_sql_comparator( $query['modified_compare'] ); $sql .= " AND p.post_modified_gmt $comparator %s"; $sql_params[] = $date_string; } if ( true === $query['claimed'] ) { $sql .= " AND p.post_password != ''"; } elseif ( false === $query['claimed'] ) { $sql .= " AND p.post_password = ''"; } elseif ( ! is_null( $query['claimed'] ) ) { $sql .= ' AND p.post_password = %s'; $sql_params[] = $query['claimed']; } if ( ! empty( $query['search'] ) ) { $sql .= ' AND (p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_password LIKE %s)'; for ( $i = 0; $i < 3; $i++ ) { $sql_params[] = sprintf( '%%%s%%', $query['search'] ); } } if ( 'select' === $select_or_count ) { switch ( $query['orderby'] ) { case 'hook': $orderby = 'p.post_title'; break; case 'group': $orderby = 't.name'; break; case 'status': $orderby = 'p.post_status'; break; case 'modified': $orderby = 'p.post_modified'; break; case 'claim_id': $orderby = 'p.post_password'; break; case 'schedule': case 'date': default: $orderby = 'p.post_date_gmt'; break; } if ( 'ASC' === strtoupper( $query['order'] ) ) { $order = 'ASC'; } else { $order = 'DESC'; } $sql .= " ORDER BY $orderby $order"; if ( $query['per_page'] > 0 ) { $sql .= ' LIMIT %d, %d'; $sql_params[] = $query['offset']; $sql_params[] = $query['per_page']; } } return $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * Query for action count or list of action IDs. * * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status. * * @see ActionScheduler_Store::query_actions for $query arg usage. * * @param array $query Query filtering options. * @param string $query_type Whether to select or count the results. Defaults to select. * * @return string|array|null The IDs of actions matching the query. Null on failure. */ public function query_actions( $query = array(), $query_type = 'select' ) { /** * Global $wpdb object. * * @var wpdb $wpdb */ global $wpdb; $sql = $this->get_query_actions_sql( $query, $query_type ); return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared } /** * Get a count of all actions in the store, grouped by status * * @return array */ public function action_counts() { $action_counts_by_status = array(); $action_stati_and_labels = $this->get_status_labels(); $posts_count_by_status = (array) wp_count_posts( self::POST_TYPE, 'readable' ); foreach ( $posts_count_by_status as $post_status_name => $count ) { try { $action_status_name = $this->get_action_status_by_post_status( $post_status_name ); } catch ( Exception $e ) { // Ignore any post statuses that aren't for actions. continue; } if ( array_key_exists( $action_status_name, $action_stati_and_labels ) ) { $action_counts_by_status[ $action_status_name ] = $count; } } return $action_counts_by_status; } /** * Cancel action. * * @param int $action_id Action ID. * * @throws InvalidArgumentException If $action_id is not identified. */ public function cancel_action( $action_id ) { $post = get_post( $action_id ); if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { /* translators: %s is the action ID */ throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to cancel this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } do_action( 'action_scheduler_canceled_action', $action_id ); add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); wp_trash_post( $action_id ); remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); } /** * Delete action. * * @param int $action_id Action ID. * @return void * @throws InvalidArgumentException If action is not identified. */ public function delete_action( $action_id ) { $post = get_post( $action_id ); if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { /* translators: %s is the action ID */ throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to delete this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } do_action( 'action_scheduler_deleted_action', $action_id ); wp_delete_post( $action_id, true ); } /** * Get date for claim id. * * @param int $action_id Action ID. * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran. */ public function get_date( $action_id ) { $next = $this->get_date_gmt( $action_id ); return ActionScheduler_TimezoneHelper::set_local_timezone( $next ); } /** * Get Date GMT. * * @param int $action_id Action ID. * * @throws InvalidArgumentException If $action_id is not identified. * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran. */ public function get_date_gmt( $action_id ) { $post = get_post( $action_id ); if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { /* translators: %s is the action ID */ throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to determine the date of this action. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } if ( 'publish' === $post->post_status ) { return as_get_datetime_object( $post->post_modified_gmt ); } else { return as_get_datetime_object( $post->post_date_gmt ); } } /** * Stake claim. * * @param int $max_actions Maximum number of actions. * @param DateTime|null $before_date Jobs must be schedule before this date. Defaults to now. * @param array $hooks Claim only actions with a hook or hooks. * @param string $group Claim only actions in the given group. * * @return ActionScheduler_ActionClaim * @throws RuntimeException When there is an error staking a claim. * @throws InvalidArgumentException When the given group is not valid. */ public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ) { $this->claim_before_date = $before_date; $claim_id = $this->generate_claim_id(); $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group ); $action_ids = $this->find_actions_by_claim_id( $claim_id ); $this->claim_before_date = null; return new ActionScheduler_ActionClaim( $claim_id, $action_ids ); } /** * Get claim count. * * @return int */ public function get_claim_count() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT post_password) FROM {$wpdb->posts} WHERE post_password != '' AND post_type = %s AND post_status IN ('in-progress','pending')", array( self::POST_TYPE ) ) ); } /** * Generate claim id. * * @return string */ protected function generate_claim_id() { $claim_id = md5( microtime( true ) . wp_rand( 0, 1000 ) ); return substr( $claim_id, 0, 20 ); // to fit in db field with 20 char limit. } /** * Claim actions. * * @param string $claim_id Claim ID. * @param int $limit Limit. * @param DateTime|null $before_date Should use UTC timezone. * @param array $hooks Claim only actions with a hook or hooks. * @param string $group Claim only actions in the given group. * * @return int The number of actions that were claimed. * @throws RuntimeException When there is a database error. */ protected function claim_actions( $claim_id, $limit, ?DateTime $before_date = null, $hooks = array(), $group = '' ) { // Set up initial variables. $date = null === $before_date ? as_get_datetime_object() : clone $before_date; $limit_ids = ! empty( $group ); $ids = $limit_ids ? $this->get_actions_by_group( $group, $limit, $date ) : array(); // If limiting by IDs and no posts found, then return early since we have nothing to update. if ( $limit_ids && 0 === count( $ids ) ) { return 0; } /** * Global wpdb object. * * @var wpdb $wpdb */ global $wpdb; /* * Build up custom query to update the affected posts. Parameters are built as a separate array * to make it easier to identify where they are in the query. * * We can't use $wpdb->update() here because of the "ID IN ..." clause. */ $update = "UPDATE {$wpdb->posts} SET post_password = %s, post_modified_gmt = %s, post_modified = %s"; $params = array( $claim_id, current_time( 'mysql', true ), current_time( 'mysql' ), ); // Build initial WHERE clause. $where = "WHERE post_type = %s AND post_status = %s AND post_password = ''"; $params[] = self::POST_TYPE; $params[] = ActionScheduler_Store::STATUS_PENDING; if ( ! empty( $hooks ) ) { $placeholders = array_fill( 0, count( $hooks ), '%s' ); $where .= ' AND post_title IN (' . join( ', ', $placeholders ) . ')'; $params = array_merge( $params, array_values( $hooks ) ); } /* * Add the IDs to the WHERE clause. IDs not escaped because they came directly from a prior DB query. * * If we're not limiting by IDs, then include the post_date_gmt clause. */ if ( $limit_ids ) { $where .= ' AND ID IN (' . join( ',', $ids ) . ')'; } else { $where .= ' AND post_date_gmt <= %s'; $params[] = $date->format( 'Y-m-d H:i:s' ); } // Add the ORDER BY clause and,ms limit. $order = 'ORDER BY menu_order ASC, post_date_gmt ASC, ID ASC LIMIT %d'; $params[] = $limit; // Run the query and gather results. $rows_affected = $wpdb->query( $wpdb->prepare( "{$update} {$where} {$order}", $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare if ( false === $rows_affected ) { throw new RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); } return (int) $rows_affected; } /** * Get IDs of actions within a certain group and up to a certain date/time. * * @param string $group The group to use in finding actions. * @param int $limit The number of actions to retrieve. * @param DateTime $date DateTime object representing cutoff time for actions. Actions retrieved will be * up to and including this DateTime. * * @return array IDs of actions in the appropriate group and before the appropriate time. * @throws InvalidArgumentException When the group does not exist. */ protected function get_actions_by_group( $group, $limit, DateTime $date ) { // Ensure the group exists before continuing. if ( ! term_exists( $group, self::GROUP_TAXONOMY ) ) { /* translators: %s is the group name */ throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) ); } // Set up a query for post IDs to use later. $query = new WP_Query(); $query_args = array( 'fields' => 'ids', 'post_type' => self::POST_TYPE, 'post_status' => ActionScheduler_Store::STATUS_PENDING, 'has_password' => false, 'posts_per_page' => $limit * 3, 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters 'no_found_rows' => true, 'orderby' => array( 'menu_order' => 'ASC', 'date' => 'ASC', 'ID' => 'ASC', ), 'date_query' => array( 'column' => 'post_date_gmt', 'before' => $date->format( 'Y-m-d H:i' ), 'inclusive' => true, ), 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery array( 'taxonomy' => self::GROUP_TAXONOMY, 'field' => 'slug', 'terms' => $group, 'include_children' => false, ), ), ); return $query->query( $query_args ); } /** * Find actions by claim ID. * * @param string $claim_id Claim ID. * @return array */ public function find_actions_by_claim_id( $claim_id ) { /** * Global wpdb object. * * @var wpdb $wpdb */ global $wpdb; $action_ids = array(); $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object(); $cut_off = $before_date->format( 'Y-m-d H:i:s' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $results = $wpdb->get_results( $wpdb->prepare( "SELECT ID, post_date_gmt FROM {$wpdb->posts} WHERE post_type = %s AND post_password = %s", array( self::POST_TYPE, $claim_id, ) ) ); // Verify that the scheduled date for each action is within the expected bounds (in some unusual // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify). foreach ( $results as $claimed_action ) { if ( $claimed_action->post_date_gmt <= $cut_off ) { $action_ids[] = absint( $claimed_action->ID ); } } return $action_ids; } /** * Release claim. * * @param ActionScheduler_ActionClaim $claim Claim object to release. * @return void * @throws RuntimeException When the claim is not unlocked. */ public function release_claim( ActionScheduler_ActionClaim $claim ) { $action_ids = $this->find_actions_by_claim_id( $claim->get_id() ); if ( empty( $action_ids ) ) { return; // nothing to do. } $action_id_string = implode( ',', array_map( 'intval', $action_ids ) ); /** * Global wpdb object. * * @var wpdb $wpdb */ global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $result = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID IN ($action_id_string) AND post_password = %s", //phpcs:ignore array( $claim->get_id(), ) ) ); if ( false === $result ) { /* translators: %s: claim ID */ throw new RuntimeException( sprintf( __( 'Unable to unlock claim %s. Database error.', 'action-scheduler' ), $claim->get_id() ) ); } } /** * Unclaim action. * * @param string $action_id Action ID. * @throws RuntimeException When unable to unlock claim on action ID. */ public function unclaim_action( $action_id ) { /** * Global wpdb object. * * @var wpdb $wpdb */ global $wpdb; //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $result = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID = %d AND post_type = %s", $action_id, self::POST_TYPE ) ); if ( false === $result ) { /* translators: %s: action ID */ throw new RuntimeException( sprintf( __( 'Unable to unlock claim on action %s. Database error.', 'action-scheduler' ), $action_id ) ); } } /** * Mark failure on action. * * @param int $action_id Action ID. * * @return void * @throws RuntimeException When unable to mark failure on action ID. */ public function mark_failure( $action_id ) { /** * Global wpdb object. * * @var wpdb $wpdb */ global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $result = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_status = %s WHERE ID = %d AND post_type = %s", self::STATUS_FAILED, $action_id, self::POST_TYPE ) ); if ( false === $result ) { /* translators: %s: action ID */ throw new RuntimeException( sprintf( __( 'Unable to mark failure on action %s. Database error.', 'action-scheduler' ), $action_id ) ); } } /** * Return an action's claim ID, as stored in the post password column * * @param int $action_id Action ID. * @return mixed */ public function get_claim_id( $action_id ) { return $this->get_post_column( $action_id, 'post_password' ); } /** * Return an action's status, as stored in the post status column * * @param int $action_id Action ID. * * @return mixed * @throws InvalidArgumentException When the action ID is invalid. */ public function get_status( $action_id ) { $status = $this->get_post_column( $action_id, 'post_status' ); if ( null === $status ) { throw new InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) ); } return $this->get_action_status_by_post_status( $status ); } /** * Get post column * * @param string $action_id Action ID. * @param string $column_name Column Name. * * @return string|null */ private function get_post_column( $action_id, $column_name ) { /** * Global wpdb object. * * @var wpdb $wpdb */ global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_var( $wpdb->prepare( "SELECT {$column_name} FROM {$wpdb->posts} WHERE ID=%d AND post_type=%s", // phpcs:ignore $action_id, self::POST_TYPE ) ); } /** * Log Execution. * * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress'). * * @param string $action_id Action ID. */ public function log_execution( $action_id ) { /** * Global wpdb object. * * @var wpdb $wpdb */ global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $status_updated = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s", self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id, self::POST_TYPE ) ); if ( ! $status_updated ) { throw new Exception( sprintf( /* translators: 1: action ID. 2: status slug. */ __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ), $action_id, self::STATUS_RUNNING ) ); } } /** * Record that an action was completed. * * @param string $action_id ID of the completed action. * * @throws InvalidArgumentException When the action ID is invalid. * @throws RuntimeException When there was an error executing the action. */ public function mark_complete( $action_id ) { $post = get_post( $action_id ); if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { /* translators: %s is the action ID */ throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having completed. It may may have been deleted by another process.', 'action-scheduler' ), $action_id ) ); } add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); $result = wp_update_post( array( 'ID' => $action_id, 'post_status' => 'publish', ), true ); remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 ); remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); if ( is_wp_error( $result ) ) { throw new RuntimeException( $result->get_error_message() ); } /** * Fires after a scheduled action has been completed. * * @since 3.4.2 * * @param int $action_id Action ID. */ do_action( 'action_scheduler_completed_action', $action_id ); } /** * Mark action as migrated when there is an error deleting the action. * * @param int $action_id Action ID. */ public function mark_migrated( $action_id ) { wp_update_post( array( 'ID' => $action_id, 'post_status' => 'migrated', ) ); } /** * Determine whether the post store can be migrated. * * @param [type] $setting - Setting value. * @return bool */ public function migration_dependencies_met( $setting ) { global $wpdb; $dependencies_met = get_transient( self::DEPENDENCIES_MET ); if ( empty( $dependencies_met ) ) { $maximum_args_length = apply_filters( 'action_scheduler_maximum_args_length', 191 ); $found_action = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND CHAR_LENGTH(post_content) > %d LIMIT 1", $maximum_args_length, self::POST_TYPE ) ); $dependencies_met = $found_action ? 'no' : 'yes'; set_transient( self::DEPENDENCIES_MET, $dependencies_met, DAY_IN_SECONDS ); } return 'yes' === $dependencies_met ? $setting : false; } /** * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4. * * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However, * as we prepare to move to custom tables, and can use an indexed VARCHAR column instead, we want to warn * developers of this impending requirement. * * @param ActionScheduler_Action $action Action object. */ protected function validate_action( ActionScheduler_Action $action ) { try { parent::validate_action( $action ); } catch ( Exception $e ) { /* translators: %s is the error message */ $message = sprintf( __( '%s Support for strings longer than this will be removed in a future version.', 'action-scheduler' ), $e->getMessage() ); _doing_it_wrong( 'ActionScheduler_Action::$args', esc_html( $message ), '2.1.0' ); } } /** * (@codeCoverageIgnore) */ public function init() { add_filter( 'action_scheduler_migration_dependencies_met', array( $this, 'migration_dependencies_met' ) ); $post_type_registrar = new ActionScheduler_wpPostStore_PostTypeRegistrar(); $post_type_registrar->register(); $post_status_registrar = new ActionScheduler_wpPostStore_PostStatusRegistrar(); $post_status_registrar->register(); $taxonomy_registrar = new ActionScheduler_wpPostStore_TaxonomyRegistrar(); $taxonomy_registrar->register(); } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php 0000644 00000024726 15174671745 0021452 0 ustar 00 <?php use Action_Scheduler\WP_CLI\Migration_Command; use Action_Scheduler\Migration\Controller; /** * Class ActionScheduler * * @codeCoverageIgnore */ abstract class ActionScheduler { /** * Plugin file path. * * @var string */ private static $plugin_file = ''; /** * ActionScheduler_ActionFactory instance. * * @var ActionScheduler_ActionFactory */ private static $factory = null; /** * Data store is initialized. * * @var bool */ private static $data_store_initialized = false; /** * Factory. */ public static function factory() { if ( ! isset( self::$factory ) ) { self::$factory = new ActionScheduler_ActionFactory(); } return self::$factory; } /** * Get Store instance. */ public static function store() { return ActionScheduler_Store::instance(); } /** * Get Lock instance. */ public static function lock() { return ActionScheduler_Lock::instance(); } /** * Get Logger instance. */ public static function logger() { return ActionScheduler_Logger::instance(); } /** * Get QueueRunner instance. */ public static function runner() { return ActionScheduler_QueueRunner::instance(); } /** * Get AdminView instance. */ public static function admin_view() { return ActionScheduler_AdminView::instance(); } /** * Get the absolute system path to the plugin directory, or a file therein * * @static * @param string $path Path relative to plugin directory. * @return string */ public static function plugin_path( $path ) { $base = dirname( self::$plugin_file ); if ( $path ) { return trailingslashit( $base ) . $path; } else { return untrailingslashit( $base ); } } /** * Get the absolute URL to the plugin directory, or a file therein * * @static * @param string $path Path relative to plugin directory. * @return string */ public static function plugin_url( $path ) { return plugins_url( $path, self::$plugin_file ); } /** * Autoload. * * @param string $class Class name. */ public static function autoload( $class ) { $d = DIRECTORY_SEPARATOR; $classes_dir = self::plugin_path( 'classes' . $d ); $separator = strrpos( $class, '\\' ); if ( false !== $separator ) { if ( 0 !== strpos( $class, 'Action_Scheduler' ) ) { return; } $class = substr( $class, $separator + 1 ); } if ( 'Deprecated' === substr( $class, -10 ) ) { $dir = self::plugin_path( 'deprecated' . $d ); } elseif ( self::is_class_abstract( $class ) ) { $dir = $classes_dir . 'abstracts' . $d; } elseif ( self::is_class_migration( $class ) ) { $dir = $classes_dir . 'migration' . $d; } elseif ( 'Schedule' === substr( $class, -8 ) ) { $dir = $classes_dir . 'schedules' . $d; } elseif ( 'Action' === substr( $class, -6 ) ) { $dir = $classes_dir . 'actions' . $d; } elseif ( 'Schema' === substr( $class, -6 ) ) { $dir = $classes_dir . 'schema' . $d; } elseif ( strpos( $class, 'ActionScheduler' ) === 0 ) { $segments = explode( '_', $class ); $type = isset( $segments[1] ) ? $segments[1] : ''; switch ( $type ) { case 'WPCLI': $dir = $classes_dir . 'WP_CLI' . $d; break; case 'DBLogger': case 'DBStore': case 'HybridStore': case 'wpPostStore': case 'wpCommentLogger': $dir = $classes_dir . 'data-stores' . $d; break; default: $dir = $classes_dir; break; } } elseif ( self::is_class_cli( $class ) ) { $dir = $classes_dir . 'WP_CLI' . $d; } elseif ( strpos( $class, 'CronExpression' ) === 0 ) { $dir = self::plugin_path( 'lib' . $d . 'cron-expression' . $d ); } elseif ( strpos( $class, 'WP_Async_Request' ) === 0 ) { $dir = self::plugin_path( 'lib' . $d ); } else { return; } if ( file_exists( $dir . "{$class}.php" ) ) { include $dir . "{$class}.php"; return; } } /** * Initialize the plugin * * @static * @param string $plugin_file Plugin file path. */ public static function init( $plugin_file ) { self::$plugin_file = $plugin_file; spl_autoload_register( array( __CLASS__, 'autoload' ) ); /** * Fires in the early stages of Action Scheduler init hook. */ do_action( 'action_scheduler_pre_init' ); require_once self::plugin_path( 'functions.php' ); ActionScheduler_DataController::init(); $store = self::store(); $logger = self::logger(); $runner = self::runner(); $admin_view = self::admin_view(); // Ensure initialization on plugin activation. if ( ! did_action( 'init' ) ) { // phpcs:ignore Squiz.PHP.CommentedOutCode add_action( 'init', array( $admin_view, 'init' ), 0, 0 ); // run before $store::init(). add_action( 'init', array( $store, 'init' ), 1, 0 ); add_action( 'init', array( $logger, 'init' ), 1, 0 ); add_action( 'init', array( $runner, 'init' ), 1, 0 ); add_action( 'init', /** * Runs after the active store's init() method has been called. * * It would probably be preferable to have $store->init() (or it's parent method) set this itself, * once it has initialized, however that would cause problems in cases where a custom data store is in * use and it has not yet been updated to follow that same logic. */ function () { self::$data_store_initialized = true; /** * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point. * * @since 3.5.5 */ do_action( 'action_scheduler_init' ); }, 1 ); } else { $admin_view->init(); $store->init(); $logger->init(); $runner->init(); self::$data_store_initialized = true; /** * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point. * * @since 3.5.5 */ do_action( 'action_scheduler_init' ); } if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) { require_once self::plugin_path( 'deprecated/functions.php' ); } if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' ); WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Clean_Command' ); if ( ! ActionScheduler_DataController::is_migration_complete() && Controller::instance()->allow_migration() ) { $command = new Migration_Command(); $command->register(); } } /** * Handle WP comment cleanup after migration. */ if ( is_a( $logger, 'ActionScheduler_DBLogger' ) && ActionScheduler_DataController::is_migration_complete() && ActionScheduler_WPCommentCleaner::has_logs() ) { ActionScheduler_WPCommentCleaner::init(); } add_action( 'action_scheduler/migration_complete', 'ActionScheduler_WPCommentCleaner::maybe_schedule_cleanup' ); } /** * Check whether the AS data store has been initialized. * * @param string $function_name The name of the function being called. Optional. Default `null`. * @return bool */ public static function is_initialized( $function_name = null ) { if ( ! self::$data_store_initialized && ! empty( $function_name ) ) { $message = sprintf( /* translators: %s function name. */ __( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ), esc_attr( $function_name ) ); _doing_it_wrong( esc_html( $function_name ), esc_html( $message ), '3.1.6' ); } return self::$data_store_initialized; } /** * Determine if the class is one of our abstract classes. * * @since 3.0.0 * * @param string $class The class name. * * @return bool */ protected static function is_class_abstract( $class ) { static $abstracts = array( 'ActionScheduler' => true, 'ActionScheduler_Abstract_ListTable' => true, 'ActionScheduler_Abstract_QueueRunner' => true, 'ActionScheduler_Abstract_Schedule' => true, 'ActionScheduler_Abstract_RecurringSchedule' => true, 'ActionScheduler_Lock' => true, 'ActionScheduler_Logger' => true, 'ActionScheduler_Abstract_Schema' => true, 'ActionScheduler_Store' => true, 'ActionScheduler_TimezoneHelper' => true, ); return isset( $abstracts[ $class ] ) && $abstracts[ $class ]; } /** * Determine if the class is one of our migration classes. * * @since 3.0.0 * * @param string $class The class name. * * @return bool */ protected static function is_class_migration( $class ) { static $migration_segments = array( 'ActionMigrator' => true, 'BatchFetcher' => true, 'DBStoreMigrator' => true, 'DryRun' => true, 'LogMigrator' => true, 'Config' => true, 'Controller' => true, 'Runner' => true, 'Scheduler' => true, ); $segments = explode( '_', $class ); $segment = isset( $segments[1] ) ? $segments[1] : $class; return isset( $migration_segments[ $segment ] ) && $migration_segments[ $segment ]; } /** * Determine if the class is one of our WP CLI classes. * * @since 3.0.0 * * @param string $class The class name. * * @return bool */ protected static function is_class_cli( $class ) { static $cli_segments = array( 'QueueRunner' => true, 'Command' => true, 'ProgressBar' => true, ); $segments = explode( '_', $class ); $segment = isset( $segments[1] ) ? $segments[1] : $class; return isset( $cli_segments[ $segment ] ) && $cli_segments[ $segment ]; } /** * Clone. */ final public function __clone() { trigger_error( 'Singleton. No cloning allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } /** * Wakeup. */ final public function __wakeup() { trigger_error( 'Singleton. No serialization allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } /** * Construct. */ final private function __construct() {} /** Deprecated **/ /** * Get DateTime object. * * @param null|string $when Date/time string. * @param string $timezone Timezone string. */ public static function get_datetime_object( $when = null, $timezone = 'UTC' ) { _deprecated_function( __METHOD__, '2.0', 'wcs_add_months()' ); return as_get_datetime_object( $when, $timezone ); } /** * Issue deprecated warning if an Action Scheduler function is called in the shutdown hook. * * @param string $function_name The name of the function being called. * @deprecated 3.1.6. */ public static function check_shutdown_hook( $function_name ) { _deprecated_function( __FUNCTION__, '3.1.6' ); } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php 0000644 00000011415 15174671745 0024453 0 ustar 00 <?php /** * Class ActionScheduler_TimezoneHelper */ abstract class ActionScheduler_TimezoneHelper { /** * DateTimeZone object. * * @var null|DateTimeZone */ private static $local_timezone = null; /** * Set a DateTime's timezone to the WordPress site's timezone, or a UTC offset * if no timezone string is available. * * @since 2.1.0 * * @param DateTime $date Timestamp. * @return ActionScheduler_DateTime */ public static function set_local_timezone( DateTime $date ) { // Accept a DateTime for easier backward compatibility, even though we require methods on ActionScheduler_DateTime. if ( ! is_a( $date, 'ActionScheduler_DateTime' ) ) { $date = as_get_datetime_object( $date->format( 'U' ) ); } if ( get_option( 'timezone_string' ) ) { $date->setTimezone( new DateTimeZone( self::get_local_timezone_string() ) ); } else { $date->setUtcOffset( self::get_local_timezone_offset() ); } return $date; } /** * Helper to retrieve the timezone string for a site until a WP core method exists * (see https://core.trac.wordpress.org/ticket/24730). * * Adapted from wc_timezone_string() and https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155. * * If no timezone string is set, and its not possible to match the UTC offset set for the site to a timezone * string, then an empty string will be returned, and the UTC offset should be used to set a DateTime's * timezone. * * @since 2.1.0 * @param bool $reset Unused. * @return string PHP timezone string for the site or empty if no timezone string is available. */ protected static function get_local_timezone_string( $reset = false ) { // If site timezone string exists, return it. $timezone = get_option( 'timezone_string' ); if ( $timezone ) { return $timezone; } // Get UTC offset, if it isn't set then return UTC. $utc_offset = intval( get_option( 'gmt_offset', 0 ) ); if ( 0 === $utc_offset ) { return 'UTC'; } // Adjust UTC offset from hours to seconds. $utc_offset *= 3600; // Attempt to guess the timezone string from the UTC offset. $timezone = timezone_name_from_abbr( '', $utc_offset ); if ( $timezone ) { return $timezone; } // Last try, guess timezone string manually. foreach ( timezone_abbreviations_list() as $abbr ) { foreach ( $abbr as $city ) { if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- we are actually interested in the runtime timezone. return $city['timezone_id']; } } } // No timezone string. return ''; } /** * Get timezone offset in seconds. * * @since 2.1.0 * @return float */ protected static function get_local_timezone_offset() { $timezone = get_option( 'timezone_string' ); if ( $timezone ) { $timezone_object = new DateTimeZone( $timezone ); return $timezone_object->getOffset( new DateTime( 'now' ) ); } else { return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS; } } /** * Get local timezone. * * @param bool $reset Toggle to discard stored value. * @deprecated 2.1.0 */ public static function get_local_timezone( $reset = false ) { _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); if ( $reset ) { self::$local_timezone = null; } if ( ! isset( self::$local_timezone ) ) { $tzstring = get_option( 'timezone_string' ); if ( empty( $tzstring ) ) { $gmt_offset = absint( get_option( 'gmt_offset' ) ); if ( 0 === $gmt_offset ) { $tzstring = 'UTC'; } else { $gmt_offset *= HOUR_IN_SECONDS; $tzstring = timezone_name_from_abbr( '', $gmt_offset, 1 ); // If there's no timezone string, try again with no DST. if ( false === $tzstring ) { $tzstring = timezone_name_from_abbr( '', $gmt_offset, 0 ); } // Try mapping to the first abbreviation we can find. if ( false === $tzstring ) { $is_dst = date( 'I' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- we are actually interested in the runtime timezone. foreach ( timezone_abbreviations_list() as $abbr ) { foreach ( $abbr as $city ) { if ( $city['dst'] === $is_dst && $city['offset'] === $gmt_offset ) { // If there's no valid timezone ID, keep looking. if ( is_null( $city['timezone_id'] ) ) { continue; } $tzstring = $city['timezone_id']; break 2; } } } } // If we still have no valid string, then fall back to UTC. if ( false === $tzstring ) { $tzstring = 'UTC'; } } } self::$local_timezone = new DateTimeZone( $tzstring ); } return self::$local_timezone; } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php 0000644 00000034071 15174671745 0022620 0 ustar 00 <?php /** * Class ActionScheduler_Store * * @codeCoverageIgnore */ abstract class ActionScheduler_Store extends ActionScheduler_Store_Deprecated { const STATUS_COMPLETE = 'complete'; const STATUS_PENDING = 'pending'; const STATUS_RUNNING = 'in-progress'; const STATUS_FAILED = 'failed'; const STATUS_CANCELED = 'canceled'; const DEFAULT_CLASS = 'ActionScheduler_wpPostStore'; /** * ActionScheduler_Store instance. * * @var ActionScheduler_Store */ private static $store = null; /** * Maximum length of args. * * @var int */ protected static $max_args_length = 191; /** * Save action. * * @param ActionScheduler_Action $action Action to save. * @param null|DateTime $scheduled_date Optional Date of the first instance * to store. Otherwise uses the first date of the action's * schedule. * * @return int The action ID */ abstract public function save_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ); /** * Get action. * * @param string $action_id Action ID. * * @return ActionScheduler_Action */ abstract public function fetch_action( $action_id ); /** * Find an action. * * Note: the query ordering changes based on the passed 'status' value. * * @param string $hook Action hook. * @param array $params Parameters of the action to find. * * @return string|null ID of the next action matching the criteria or NULL if not found. */ public function find_action( $hook, $params = array() ) { $params = wp_parse_args( $params, array( 'args' => null, 'status' => self::STATUS_PENDING, 'group' => '', ) ); // These params are fixed for this method. $params['hook'] = $hook; $params['orderby'] = 'date'; $params['per_page'] = 1; if ( ! empty( $params['status'] ) ) { if ( self::STATUS_PENDING === $params['status'] ) { $params['order'] = 'ASC'; // Find the next action that matches. } else { $params['order'] = 'DESC'; // Find the most recent action that matches. } } $results = $this->query_actions( $params ); return empty( $results ) ? null : $results[0]; } /** * Query for action count or list of action IDs. * * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status. * * @param array $query { * Query filtering options. * * @type string $hook The name of the actions. Optional. * @type string|array $status The status or statuses of the actions. Optional. * @type array $args The args array of the actions. Optional. * @type DateTime $date The scheduled date of the action. Used in UTC timezone. Optional. * @type string $date_compare Operator for selecting by $date param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='. * @type DateTime $modified The last modified date of the action. Used in UTC timezone. Optional. * @type string $modified_compare Operator for comparing $modified param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='. * @type string $group The group the action belongs to. Optional. * @type bool|int $claimed TRUE to find claimed actions, FALSE to find unclaimed actions, an int to find a specific claim ID. Optional. * @type int $per_page Number of results to return. Defaults to 5. * @type int $offset The query pagination offset. Defaults to 0. * @type int $orderby Accepted values are 'hook', 'group', 'modified', 'date' or 'none'. Defaults to 'date'. * @type string $order Accepted values are 'ASC' or 'DESC'. Defaults to 'ASC'. * } * @param string $query_type Whether to select or count the results. Default, select. * * @return string|array|null The IDs of actions matching the query. Null on failure. */ abstract public function query_actions( $query = array(), $query_type = 'select' ); /** * Run query to get a single action ID. * * @since 3.3.0 * * @see ActionScheduler_Store::query_actions for $query arg usage but 'per_page' and 'offset' can't be used. * * @param array $query Query parameters. * * @return int|null */ public function query_action( $query ) { $query['per_page'] = 1; $query['offset'] = 0; $results = $this->query_actions( $query ); if ( empty( $results ) ) { return null; } else { return (int) $results[0]; } } /** * Get a count of all actions in the store, grouped by status * * @return array */ abstract public function action_counts(); /** * Get additional action counts. * * - add past-due actions * * @return array */ public function extra_action_counts() { $extra_actions = array(); $pastdue_action_counts = (int) $this->query_actions( array( 'status' => self::STATUS_PENDING, 'date' => as_get_datetime_object(), ), 'count' ); if ( $pastdue_action_counts ) { $extra_actions['past-due'] = $pastdue_action_counts; } /** * Allows 3rd party code to add extra action counts (used in filters in the list table). * * @since 3.5.0 * @param $extra_actions array Array with format action_count_identifier => action count. */ return apply_filters( 'action_scheduler_extra_action_counts', $extra_actions ); } /** * Cancel action. * * @param string $action_id Action ID. */ abstract public function cancel_action( $action_id ); /** * Delete action. * * @param string $action_id Action ID. */ abstract public function delete_action( $action_id ); /** * Get action's schedule or run timestamp. * * @param string $action_id Action ID. * * @return DateTime The date the action is schedule to run, or the date that it ran. */ abstract public function get_date( $action_id ); /** * Make a claim. * * @param int $max_actions Maximum number of actions to claim. * @param DateTime|null $before_date Claim only actions schedule before the given date. Defaults to now. * @param array $hooks Claim only actions with a hook or hooks. * @param string $group Claim only actions in the given group. * * @return ActionScheduler_ActionClaim */ abstract public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ); /** * Get claim count. * * @return int */ abstract public function get_claim_count(); /** * Release the claim. * * @param ActionScheduler_ActionClaim $claim Claim object. */ abstract public function release_claim( ActionScheduler_ActionClaim $claim ); /** * Un-claim the action. * * @param string $action_id Action ID. */ abstract public function unclaim_action( $action_id ); /** * Mark action as failed. * * @param string $action_id Action ID. */ abstract public function mark_failure( $action_id ); /** * Log action's execution. * * @param string $action_id Actoin ID. */ abstract public function log_execution( $action_id ); /** * Mark action as complete. * * @param string $action_id Action ID. */ abstract public function mark_complete( $action_id ); /** * Get action's status. * * @param string $action_id Action ID. * @return string */ abstract public function get_status( $action_id ); /** * Get action's claim ID. * * @param string $action_id Action ID. * @return mixed */ abstract public function get_claim_id( $action_id ); /** * Find actions by claim ID. * * @param string $claim_id Claim ID. * @return array */ abstract public function find_actions_by_claim_id( $claim_id ); /** * Validate SQL operator. * * @param string $comparison_operator Operator. * @return string */ protected function validate_sql_comparator( $comparison_operator ) { if ( in_array( $comparison_operator, array( '!=', '>', '>=', '<', '<=', '=' ), true ) ) { return $comparison_operator; } return '='; } /** * Get the time MySQL formatted date/time string for an action's (next) scheduled date. * * @param ActionScheduler_Action $action Action. * @param null|DateTime $scheduled_date Action's schedule date (optional). * @return string */ protected function get_scheduled_date_string( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) { $next = is_null( $scheduled_date ) ? $action->get_schedule()->get_date() : $scheduled_date; if ( ! $next ) { $next = date_create(); } $next->setTimezone( new DateTimeZone( 'UTC' ) ); return $next->format( 'Y-m-d H:i:s' ); } /** * Get the time MySQL formatted date/time string for an action's (next) scheduled date. * * @param ActionScheduler_Action|null $action Action. * @param null|DateTime $scheduled_date Action's scheduled date (optional). * @return string */ protected function get_scheduled_date_string_local( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) { $next = is_null( $scheduled_date ) ? $action->get_schedule()->get_date() : $scheduled_date; if ( ! $next ) { $next = date_create(); } ActionScheduler_TimezoneHelper::set_local_timezone( $next ); return $next->format( 'Y-m-d H:i:s' ); } /** * Validate that we could decode action arguments. * * @param mixed $args The decoded arguments. * @param int $action_id The action ID. * * @throws ActionScheduler_InvalidActionException When the decoded arguments are invalid. */ protected function validate_args( $args, $action_id ) { // Ensure we have an array of args. if ( ! is_array( $args ) ) { throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id ); } // Validate JSON decoding if possible. if ( function_exists( 'json_last_error' ) && JSON_ERROR_NONE !== json_last_error() ) { throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id, $args ); } } /** * Validate a ActionScheduler_Schedule object. * * @param mixed $schedule The unserialized ActionScheduler_Schedule object. * @param int $action_id The action ID. * * @throws ActionScheduler_InvalidActionException When the schedule is invalid. */ protected function validate_schedule( $schedule, $action_id ) { if ( empty( $schedule ) || ! is_a( $schedule, 'ActionScheduler_Schedule' ) ) { throw ActionScheduler_InvalidActionException::from_schedule( $action_id, $schedule ); } } /** * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4. * * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However, * with custom tables, we use an indexed VARCHAR column instead. * * @param ActionScheduler_Action $action Action to be validated. * @throws InvalidArgumentException When json encoded args is too long. */ protected function validate_action( ActionScheduler_Action $action ) { if ( strlen( wp_json_encode( $action->get_args() ) ) > static::$max_args_length ) { // translators: %d is a number (maximum length of action arguments). throw new InvalidArgumentException( sprintf( __( 'ActionScheduler_Action::$args too long. To ensure the args column can be indexed, action args should not be more than %d characters when encoded as JSON.', 'action-scheduler' ), static::$max_args_length ) ); } } /** * Cancel pending actions by hook. * * @since 3.0.0 * * @param string $hook Hook name. * * @return void */ public function cancel_actions_by_hook( $hook ) { $action_ids = true; while ( ! empty( $action_ids ) ) { $action_ids = $this->query_actions( array( 'hook' => $hook, 'status' => self::STATUS_PENDING, 'per_page' => 1000, 'orderby' => 'none', ) ); $this->bulk_cancel_actions( $action_ids ); } } /** * Cancel pending actions by group. * * @since 3.0.0 * * @param string $group Group slug. * * @return void */ public function cancel_actions_by_group( $group ) { $action_ids = true; while ( ! empty( $action_ids ) ) { $action_ids = $this->query_actions( array( 'group' => $group, 'status' => self::STATUS_PENDING, 'per_page' => 1000, 'orderby' => 'none', ) ); $this->bulk_cancel_actions( $action_ids ); } } /** * Cancel a set of action IDs. * * @since 3.0.0 * * @param int[] $action_ids List of action IDs. * * @return void */ private function bulk_cancel_actions( $action_ids ) { foreach ( $action_ids as $action_id ) { $this->cancel_action( $action_id ); } do_action( 'action_scheduler_bulk_cancel_actions', $action_ids ); } /** * Get status labels. * * @return array<string, string> */ public function get_status_labels() { return array( self::STATUS_COMPLETE => __( 'Complete', 'action-scheduler' ), self::STATUS_PENDING => __( 'Pending', 'action-scheduler' ), self::STATUS_RUNNING => __( 'In-progress', 'action-scheduler' ), self::STATUS_FAILED => __( 'Failed', 'action-scheduler' ), self::STATUS_CANCELED => __( 'Canceled', 'action-scheduler' ), ); } /** * Check if there are any pending scheduled actions due to run. * * @return string */ public function has_pending_actions_due() { $pending_actions = $this->query_actions( array( 'per_page' => 1, 'date' => as_get_datetime_object(), 'status' => self::STATUS_PENDING, 'orderby' => 'none', ), 'count' ); return ! empty( $pending_actions ); } /** * Callable initialization function optionally overridden in derived classes. */ public function init() {} /** * Callable function to mark an action as migrated optionally overridden in derived classes. * * @param int $action_id Action ID. */ public function mark_migrated( $action_id ) {} /** * Get instance. * * @return ActionScheduler_Store */ public static function instance() { if ( empty( self::$store ) ) { $class = apply_filters( 'action_scheduler_store_class', self::DEFAULT_CLASS ); self::$store = new $class(); } return self::$store; } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php 0000644 00000032710 15174671745 0025623 0 ustar 00 <?php /** * Abstract class with common Queue Cleaner functionality. */ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abstract_QueueRunner_Deprecated { /** * ActionScheduler_QueueCleaner instance. * * @var ActionScheduler_QueueCleaner */ protected $cleaner; /** * ActionScheduler_FatalErrorMonitor instance. * * @var ActionScheduler_FatalErrorMonitor */ protected $monitor; /** * ActionScheduler_Store instance. * * @var ActionScheduler_Store */ protected $store; /** * The created time. * * Represents when the queue runner was constructed and used when calculating how long a PHP request has been running. * For this reason it should be as close as possible to the PHP request start time. * * @var int */ private $created_time; /** * ActionScheduler_Abstract_QueueRunner constructor. * * @param ActionScheduler_Store|null $store Store object. * @param ActionScheduler_FatalErrorMonitor|null $monitor Monitor object. * @param ActionScheduler_QueueCleaner|null $cleaner Cleaner object. */ public function __construct( ?ActionScheduler_Store $store = null, ?ActionScheduler_FatalErrorMonitor $monitor = null, ?ActionScheduler_QueueCleaner $cleaner = null ) { $this->created_time = microtime( true ); $this->store = $store ? $store : ActionScheduler_Store::instance(); $this->monitor = $monitor ? $monitor : new ActionScheduler_FatalErrorMonitor( $this->store ); $this->cleaner = $cleaner ? $cleaner : new ActionScheduler_QueueCleaner( $this->store ); } /** * Process an individual action. * * @param int $action_id The action ID to process. * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * @throws \Exception When error running action. */ public function process_action( $action_id, $context = '' ) { // Temporarily override the error handler while we process the current action. // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler set_error_handler( /** * Temporary error handler which can catch errors and convert them into exceptions. This facilitates more * robust error handling across all supported PHP versions. * * @throws Exception * * @param int $type Error level expressed as an integer. * @param string $message Error message. */ function ( $type, $message ) { throw new Exception( $message ); }, E_USER_ERROR | E_RECOVERABLE_ERROR ); /* * The nested try/catch structure is required because we potentially need to convert thrown errors into * exceptions (and an exception thrown from a catch block cannot be caught by a later catch block in the *same* * structure). */ try { try { $valid_action = false; do_action( 'action_scheduler_before_execute', $action_id, $context ); if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { do_action( 'action_scheduler_execution_ignored', $action_id, $context ); return; } $valid_action = true; do_action( 'action_scheduler_begin_execute', $action_id, $context ); $action = $this->store->fetch_action( $action_id ); $this->store->log_execution( $action_id ); $action->execute(); do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); $this->store->mark_complete( $action_id ); } catch ( Throwable $e ) { // Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for // compatibility with ActionScheduler_Logger. throw new Exception( $e->getMessage(), $e->getCode(), $e ); } } catch ( Exception $e ) { // This catch block exists for compatibility with PHP 5.6. $this->handle_action_error( $action_id, $e, $context, $valid_action ); } finally { restore_error_handler(); } if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) { $this->schedule_next_instance( $action, $action_id ); } } /** * Marks actions as either having failed execution or failed validation, as appropriate. * * @param int $action_id Action ID. * @param Exception $e Exception instance. * @param string $context Execution context. * @param bool $valid_action If the action is valid. * * @return void */ private function handle_action_error( $action_id, $e, $context, $valid_action ) { if ( $valid_action ) { $this->store->mark_failure( $action_id ); /** * Runs when action execution fails. * * @param int $action_id Action ID. * @param Exception $e Exception instance. * @param string $context Execution context. */ do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); } else { /** * Runs when action validation fails. * * @param int $action_id Action ID. * @param Exception $e Exception instance. * @param string $context Execution context. */ do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); } } /** * Schedule the next instance of the action if necessary. * * @param ActionScheduler_Action $action Action. * @param int $action_id Action ID. */ protected function schedule_next_instance( ActionScheduler_Action $action, $action_id ) { // If a recurring action has been consistently failing, we may wish to stop rescheduling it. if ( ActionScheduler_Store::STATUS_FAILED === $this->store->get_status( $action_id ) && $this->recurring_action_is_consistently_failing( $action, $action_id ) ) { ActionScheduler_Logger::instance()->log( $action_id, __( 'This action appears to be consistently failing. A new instance will not be scheduled.', 'action-scheduler' ) ); return; } try { ActionScheduler::factory()->repeat( $action ); } catch ( Exception $e ) { do_action( 'action_scheduler_failed_to_schedule_next_instance', $action_id, $e, $action ); } } /** * Determine if the specified recurring action has been consistently failing. * * @param ActionScheduler_Action $action The recurring action to be rescheduled. * @param int $action_id The ID of the recurring action. * * @return bool */ private function recurring_action_is_consistently_failing( ActionScheduler_Action $action, $action_id ) { /** * Controls the failure threshold for recurring actions. * * Before rescheduling a recurring action, we look at its status. If it failed, we then check if all of the most * recent actions (upto the threshold set by this filter) sharing the same hook have also failed: if they have, * that is considered consistent failure and a new instance of the action will not be scheduled. * * @param int $failure_threshold Number of actions of the same hook to examine for failure. Defaults to 5. */ $consistent_failure_threshold = (int) apply_filters( 'action_scheduler_recurring_action_failure_threshold', 5 ); // This query should find the earliest *failing* action (for the hook we are interested in) within our threshold. $query_args = array( 'hook' => $action->get_hook(), 'status' => ActionScheduler_Store::STATUS_FAILED, 'date' => date_create( 'now', timezone_open( 'UTC' ) )->format( 'Y-m-d H:i:s' ), 'date_compare' => '<', 'per_page' => 1, 'offset' => $consistent_failure_threshold - 1, ); $first_failing_action_id = $this->store->query_actions( $query_args ); // If we didn't retrieve an action ID, then there haven't been enough failures for us to worry about. if ( empty( $first_failing_action_id ) ) { return false; } // Now let's fetch the first action (having the same hook) of *any status* within the same window. unset( $query_args['status'] ); $first_action_id_with_the_same_hook = $this->store->query_actions( $query_args ); /** * If a recurring action is assessed as consistently failing, it will not be rescheduled. This hook provides a * way to observe and optionally override that assessment. * * @param bool $is_consistently_failing If the action is considered to be consistently failing. * @param ActionScheduler_Action $action The action being assessed. */ return (bool) apply_filters( 'action_scheduler_recurring_action_is_consistently_failing', $first_action_id_with_the_same_hook === $first_failing_action_id, $action ); } /** * Run the queue cleaner. */ protected function run_cleanup() { $this->cleaner->clean( 10 * $this->get_time_limit() ); } /** * Get the number of concurrent batches a runner allows. * * @return int */ public function get_allowed_concurrent_batches() { return apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 1 ); } /** * Check if the number of allowed concurrent batches is met or exceeded. * * @return bool */ public function has_maximum_concurrent_batches() { return $this->store->get_claim_count() >= $this->get_allowed_concurrent_batches(); } /** * Get the maximum number of seconds a batch can run for. * * @return int The number of seconds. */ protected function get_time_limit() { $time_limit = 30; // Apply deprecated filter from deprecated get_maximum_execution_time() method. if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) { _deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' ); $time_limit = apply_filters( 'action_scheduler_maximum_execution_time', $time_limit ); } return absint( apply_filters( 'action_scheduler_queue_runner_time_limit', $time_limit ) ); } /** * Get the number of seconds the process has been running. * * @return int The number of seconds. */ protected function get_execution_time() { $execution_time = microtime( true ) - $this->created_time; // Get the CPU time if the hosting environment uses it rather than wall-clock time to calculate a process's execution time. if ( function_exists( 'getrusage' ) && apply_filters( 'action_scheduler_use_cpu_execution_time', defined( 'PANTHEON_ENVIRONMENT' ) ) ) { $resource_usages = getrusage(); if ( isset( $resource_usages['ru_stime.tv_usec'], $resource_usages['ru_stime.tv_usec'] ) ) { $execution_time = $resource_usages['ru_stime.tv_sec'] + ( $resource_usages['ru_stime.tv_usec'] / 1000000 ); } } return $execution_time; } /** * Check if the host's max execution time is (likely) to be exceeded if processing more actions. * * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action. * @return bool */ protected function time_likely_to_be_exceeded( $processed_actions ) { $execution_time = $this->get_execution_time(); $max_execution_time = $this->get_time_limit(); // Safety against division by zero errors. if ( 0 === $processed_actions ) { return $execution_time >= $max_execution_time; } $time_per_action = $execution_time / $processed_actions; $estimated_time = $execution_time + ( $time_per_action * 3 ); $likely_to_be_exceeded = $estimated_time > $max_execution_time; return apply_filters( 'action_scheduler_maximum_execution_time_likely_to_be_exceeded', $likely_to_be_exceeded, $this, $processed_actions, $execution_time, $max_execution_time ); } /** * Get memory limit * * Based on WP_Background_Process::get_memory_limit() * * @return int */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { $memory_limit = '128M'; // Sensible default, and minimum required by WooCommerce. } if ( ! $memory_limit || -1 === $memory_limit || '-1' === $memory_limit ) { // Unlimited, set to 32GB. $memory_limit = '32G'; } return ActionScheduler_Compatibility::convert_hr_to_bytes( $memory_limit ); } /** * Memory exceeded * * Ensures the batch process never exceeds 90% of the maximum WordPress memory. * * Based on WP_Background_Process::memory_exceeded() * * @return bool */ protected function memory_exceeded() { $memory_limit = $this->get_memory_limit() * 0.90; $current_memory = memory_get_usage( true ); $memory_exceeded = $current_memory >= $memory_limit; return apply_filters( 'action_scheduler_memory_exceeded', $memory_exceeded, $this ); } /** * See if the batch limits have been exceeded, which is when memory usage is almost at * the maximum limit, or the time to process more actions will exceed the max time limit. * * Based on WC_Background_Process::batch_limits_exceeded() * * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action. * @return bool */ protected function batch_limits_exceeded( $processed_actions ) { return $this->memory_exceeded() || $this->time_likely_to_be_exceeded( $processed_actions ); } /** * Process actions in the queue. * * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * @return int The number of actions processed. */ abstract public function run( $context = '' ); } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php 0000644 00000003547 15174671745 0025107 0 ustar 00 <?php /** * Class ActionScheduler_Abstract_Schedule */ abstract class ActionScheduler_Abstract_Schedule extends ActionScheduler_Schedule_Deprecated { /** * The date & time the schedule is set to run. * * @var DateTime */ private $scheduled_date = null; /** * Timestamp equivalent of @see $this->scheduled_date * * @var int */ protected $scheduled_timestamp = null; /** * Construct. * * @param DateTime $date The date & time to run the action. */ public function __construct( DateTime $date ) { $this->scheduled_date = $date; } /** * Check if a schedule should recur. * * @return bool */ abstract public function is_recurring(); /** * Calculate when the next instance of this schedule would run based on a given date & time. * * @param DateTime $after Start timestamp. * @return DateTime */ abstract protected function calculate_next( DateTime $after ); /** * Get the next date & time when this schedule should run after a given date & time. * * @param DateTime $after Start timestamp. * @return DateTime|null */ public function get_next( DateTime $after ) { $after = clone $after; if ( $after > $this->scheduled_date ) { $after = $this->calculate_next( $after ); return $after; } return clone $this->scheduled_date; } /** * Get the date & time the schedule is set to run. * * @return DateTime|null */ public function get_date() { return $this->scheduled_date; } /** * For PHP 5.2 compat, because DateTime objects can't be serialized * * @return array */ public function __sleep() { $this->scheduled_timestamp = $this->scheduled_date->getTimestamp(); return array( 'scheduled_timestamp', ); } /** * Wakeup. */ public function __wakeup() { $this->scheduled_date = as_get_datetime_object( $this->scheduled_timestamp ); unset( $this->scheduled_timestamp ); } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php 0000644 00000006346 15174671745 0026770 0 ustar 00 <?php /** * Class ActionScheduler_Abstract_RecurringSchedule */ abstract class ActionScheduler_Abstract_RecurringSchedule extends ActionScheduler_Abstract_Schedule { /** * The date & time the first instance of this schedule was setup to run (which may not be this instance). * * Schedule objects are attached to an action object. Each schedule stores the run date for that * object as the start date - @see $this->start - and logic to calculate the next run date after * that - @see $this->calculate_next(). The $first_date property also keeps a record of when the very * first instance of this chain of schedules ran. * * @var DateTime */ private $first_date = null; /** * Timestamp equivalent of @see $this->first_date * * @var int */ protected $first_timestamp = null; /** * The recurrence between each time an action is run using this schedule. * Used to calculate the start date & time. Can be a number of seconds, in the * case of ActionScheduler_IntervalSchedule, or a cron expression, as in the * case of ActionScheduler_CronSchedule. Or something else. * * @var mixed */ protected $recurrence; /** * Construct. * * @param DateTime $date The date & time to run the action. * @param mixed $recurrence The data used to determine the schedule's recurrence. * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance. */ public function __construct( DateTime $date, $recurrence, ?DateTime $first = null ) { parent::__construct( $date ); $this->first_date = empty( $first ) ? $date : $first; $this->recurrence = $recurrence; } /** * Schedule is recurring. * * @return bool */ public function is_recurring() { return true; } /** * Get the date & time of the first schedule in this recurring series. * * @return DateTime|null */ public function get_first_date() { return clone $this->first_date; } /** * Get the schedule's recurrence. * * @return string */ public function get_recurrence() { return $this->recurrence; } /** * For PHP 5.2 compat, since DateTime objects can't be serialized * * @return array */ public function __sleep() { $sleep_params = parent::__sleep(); $this->first_timestamp = $this->first_date->getTimestamp(); return array_merge( $sleep_params, array( 'first_timestamp', 'recurrence', ) ); } /** * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 * * Prior to Action Scheduler 3.0.0, schedules used different property names to refer * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp * was the same as ActionScheduler_SimpleSchedule::timestamp. This was addressed in * Action Scheduler 3.0.0, where properties and property names were aligned for better * inheritance. To maintain backward compatibility with scheduled serialized and stored * prior to 3.0, we need to correctly map the old property names. */ public function __wakeup() { parent::__wakeup(); if ( $this->first_timestamp > 0 ) { $this->first_date = as_get_datetime_object( $this->first_timestamp ); } else { $this->first_date = $this->get_date(); } } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Logger.php 0000644 00000017525 15174671745 0022750 0 ustar 00 <?php /** * Class ActionScheduler_Logger * * @codeCoverageIgnore */ abstract class ActionScheduler_Logger { /** * Instance. * * @var null|self */ private static $logger = null; /** * Get instance. * * @return ActionScheduler_Logger */ public static function instance() { if ( empty( self::$logger ) ) { $class = apply_filters( 'action_scheduler_logger_class', 'ActionScheduler_wpCommentLogger' ); self::$logger = new $class(); } return self::$logger; } /** * Create log entry. * * @param string $action_id Action ID. * @param string $message Log message. * @param DateTime|null $date Log date. * * @return string The log entry ID */ abstract public function log( $action_id, $message, ?DateTime $date = null ); /** * Get action's log entry. * * @param string $entry_id Entry ID. * * @return ActionScheduler_LogEntry */ abstract public function get_entry( $entry_id ); /** * Get action's logs. * * @param string $action_id Action ID. * * @return ActionScheduler_LogEntry[] */ abstract public function get_logs( $action_id ); /** * Initialize. * * @codeCoverageIgnore */ public function init() { $this->hook_stored_action(); add_action( 'action_scheduler_canceled_action', array( $this, 'log_canceled_action' ), 10, 1 ); add_action( 'action_scheduler_begin_execute', array( $this, 'log_started_action' ), 10, 2 ); add_action( 'action_scheduler_after_execute', array( $this, 'log_completed_action' ), 10, 3 ); add_action( 'action_scheduler_failed_execution', array( $this, 'log_failed_action' ), 10, 3 ); add_action( 'action_scheduler_failed_action', array( $this, 'log_timed_out_action' ), 10, 2 ); add_action( 'action_scheduler_unexpected_shutdown', array( $this, 'log_unexpected_shutdown' ), 10, 2 ); add_action( 'action_scheduler_reset_action', array( $this, 'log_reset_action' ), 10, 1 ); add_action( 'action_scheduler_execution_ignored', array( $this, 'log_ignored_action' ), 10, 2 ); add_action( 'action_scheduler_failed_fetch_action', array( $this, 'log_failed_fetch_action' ), 10, 2 ); add_action( 'action_scheduler_failed_to_schedule_next_instance', array( $this, 'log_failed_schedule_next_instance' ), 10, 2 ); add_action( 'action_scheduler_bulk_cancel_actions', array( $this, 'bulk_log_cancel_actions' ), 10, 1 ); } /** * Register callback for storing action. */ public function hook_stored_action() { add_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) ); } /** * Unhook callback for storing action. */ public function unhook_stored_action() { remove_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) ); } /** * Log action stored. * * @param int $action_id Action ID. */ public function log_stored_action( $action_id ) { $this->log( $action_id, __( 'action created', 'action-scheduler' ) ); } /** * Log action cancellation. * * @param int $action_id Action ID. */ public function log_canceled_action( $action_id ) { $this->log( $action_id, __( 'action canceled', 'action-scheduler' ) ); } /** * Log action start. * * @param int $action_id Action ID. * @param string $context Action execution context. */ public function log_started_action( $action_id, $context = '' ) { if ( ! empty( $context ) ) { /* translators: %s: context */ $message = sprintf( __( 'action started via %s', 'action-scheduler' ), $context ); } else { $message = __( 'action started', 'action-scheduler' ); } $this->log( $action_id, $message ); } /** * Log action completion. * * @param int $action_id Action ID. * @param null|ActionScheduler_Action $action Action. * @param string $context Action execution context. */ public function log_completed_action( $action_id, $action = null, $context = '' ) { if ( ! empty( $context ) ) { /* translators: %s: context */ $message = sprintf( __( 'action complete via %s', 'action-scheduler' ), $context ); } else { $message = __( 'action complete', 'action-scheduler' ); } $this->log( $action_id, $message ); } /** * Log action failure. * * @param int $action_id Action ID. * @param Exception $exception Exception. * @param string $context Action execution context. */ public function log_failed_action( $action_id, Exception $exception, $context = '' ) { if ( ! empty( $context ) ) { /* translators: 1: context 2: exception message */ $message = sprintf( __( 'action failed via %1$s: %2$s', 'action-scheduler' ), $context, $exception->getMessage() ); } else { /* translators: %s: exception message */ $message = sprintf( __( 'action failed: %s', 'action-scheduler' ), $exception->getMessage() ); } $this->log( $action_id, $message ); } /** * Log action timeout. * * @param int $action_id Action ID. * @param string $timeout Timeout. */ public function log_timed_out_action( $action_id, $timeout ) { /* translators: %s: amount of time */ $this->log( $action_id, sprintf( __( 'action marked as failed after %s seconds. Unknown error occurred. Check server, PHP and database error logs to diagnose cause.', 'action-scheduler' ), $timeout ) ); } /** * Log unexpected shutdown. * * @param int $action_id Action ID. * @param mixed[] $error Error. */ public function log_unexpected_shutdown( $action_id, $error ) { if ( ! empty( $error ) ) { /* translators: 1: error message 2: filename 3: line */ $this->log( $action_id, sprintf( __( 'unexpected shutdown: PHP Fatal error %1$s in %2$s on line %3$s', 'action-scheduler' ), $error['message'], $error['file'], $error['line'] ) ); } } /** * Log action reset. * * @param int $action_id Action ID. */ public function log_reset_action( $action_id ) { $this->log( $action_id, __( 'action reset', 'action-scheduler' ) ); } /** * Log ignored action. * * @param int $action_id Action ID. * @param string $context Action execution context. */ public function log_ignored_action( $action_id, $context = '' ) { if ( ! empty( $context ) ) { /* translators: %s: context */ $message = sprintf( __( 'action ignored via %s', 'action-scheduler' ), $context ); } else { $message = __( 'action ignored', 'action-scheduler' ); } $this->log( $action_id, $message ); } /** * Log the failure of fetching the action. * * @param string $action_id Action ID. * @param null|Exception $exception The exception which occurred when fetching the action. NULL by default for backward compatibility. */ public function log_failed_fetch_action( $action_id, ?Exception $exception = null ) { if ( ! is_null( $exception ) ) { /* translators: %s: exception message */ $log_message = sprintf( __( 'There was a failure fetching this action: %s', 'action-scheduler' ), $exception->getMessage() ); } else { $log_message = __( 'There was a failure fetching this action', 'action-scheduler' ); } $this->log( $action_id, $log_message ); } /** * Log the failure of scheduling the action's next instance. * * @param int $action_id Action ID. * @param Exception $exception Exception object. */ public function log_failed_schedule_next_instance( $action_id, Exception $exception ) { /* translators: %s: exception message */ $this->log( $action_id, sprintf( __( 'There was a failure scheduling the next instance of this action: %s', 'action-scheduler' ), $exception->getMessage() ) ); } /** * Bulk add cancel action log entries. * * Implemented here for backward compatibility. Should be implemented in parent loggers * for more performant bulk logging. * * @param array $action_ids List of action ID. */ public function bulk_log_cancel_actions( $action_ids ) { if ( empty( $action_ids ) ) { return; } foreach ( $action_ids as $action_id ) { $this->log_canceled_action( $action_id ); } } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php 0000644 00000061057 15174671745 0025236 0 ustar 00 <?php if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** * Action Scheduler Abstract List Table class * * This abstract class enhances WP_List_Table making it ready to use. * * By extending this class we can focus on describing how our table looks like, * which columns needs to be shown, filter, ordered by and more and forget about the details. * * This class supports: * - Bulk actions * - Search * - Sortable columns * - Automatic translations of the columns * * @codeCoverageIgnore * @since 2.0.0 */ abstract class ActionScheduler_Abstract_ListTable extends WP_List_Table { /** * The table name * * @var string */ protected $table_name; /** * Package name, used to get options from WP_List_Table::get_items_per_page. * * @var string */ protected $package; /** * How many items do we render per page? * * @var int */ protected $items_per_page = 10; /** * Enables search in this table listing. If this array * is empty it means the listing is not searchable. * * @var array */ protected $search_by = array(); /** * Columns to show in the table listing. It is a key => value pair. The * key must much the table column name and the value is the label, which is * automatically translated. * * @var array */ protected $columns = array(); /** * Defines the row-actions. It expects an array where the key * is the column name and the value is an array of actions. * * The array of actions are key => value, where key is the method name * (with the prefix row_action_<key>) and the value is the label * and title. * * @var array */ protected $row_actions = array(); /** * The Primary key of our table * * @var string */ protected $ID = 'ID'; /** * Enables sorting, it expects an array * of columns (the column names are the values) * * @var array */ protected $sort_by = array(); /** * The default sort order * * @var string */ protected $filter_by = array(); /** * The status name => count combinations for this table's items. Used to display status filters. * * @var array */ protected $status_counts = array(); /** * Notices to display when loading the table. Array of arrays of form array( 'class' => {updated|error}, 'message' => 'This is the notice text display.' ). * * @var array */ protected $admin_notices = array(); /** * Localised string displayed in the <h1> element above the able. * * @var string */ protected $table_header; /** * Enables bulk actions. It must be an array where the key is the action name * and the value is the label (which is translated automatically). It is important * to notice that it will check that the method exists (`bulk_$name`) and will throw * an exception if it does not exists. * * This class will automatically check if the current request has a bulk action, will do the * validations and afterwards will execute the bulk method, with two arguments. The first argument * is the array with primary keys, the second argument is a string with a list of the primary keys, * escaped and ready to use (with `IN`). * * @var array */ protected $bulk_actions = array(); /** * Makes translation easier, it basically just wraps * `_x` with some default (the package name). * * @param string $text The new text to translate. * @param string $context The context of the text. * @return string|void The translated text. * * @deprecated 3.0.0 Use `_x()` instead. */ protected function translate( $text, $context = '' ) { return $text; } /** * Reads `$this->bulk_actions` and returns an array that WP_List_Table understands. It * also validates that the bulk method handler exists. It throws an exception because * this is a library meant for developers and missing a bulk method is a development-time error. * * @return array * * @throws RuntimeException Throws RuntimeException when the bulk action does not have a callback method. */ protected function get_bulk_actions() { $actions = array(); foreach ( $this->bulk_actions as $action => $label ) { if ( ! is_callable( array( $this, 'bulk_' . $action ) ) ) { throw new RuntimeException( "The bulk action $action does not have a callback method" ); } $actions[ $action ] = $label; } return $actions; } /** * Checks if the current request has a bulk action. If that is the case it will validate and will * execute the bulk method handler. Regardless if the action is valid or not it will redirect to * the previous page removing the current arguments that makes this request a bulk action. */ protected function process_bulk_action() { global $wpdb; // Detect when a bulk action is being triggered. $action = $this->current_action(); if ( ! $action ) { return; } check_admin_referer( 'bulk-' . $this->_args['plural'] ); $method = 'bulk_' . $action; if ( array_key_exists( $action, $this->bulk_actions ) && is_callable( array( $this, $method ) ) && ! empty( $_GET['ID'] ) && is_array( $_GET['ID'] ) ) { $ids_sql = '(' . implode( ',', array_fill( 0, count( $_GET['ID'] ), '%s' ) ) . ')'; $id = array_map( 'absint', $_GET['ID'] ); $this->$method( $id, $wpdb->prepare( $ids_sql, $id ) ); //phpcs:ignore WordPress.DB.PreparedSQL } if ( isset( $_SERVER['REQUEST_URI'] ) ) { wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce', 'ID', 'action', 'action2' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); exit; } } /** * Default code for deleting entries. * validated already by process_bulk_action() * * @param array $ids ids of the items to delete. * @param string $ids_sql the sql for the ids. * @return void */ protected function bulk_delete( array $ids, $ids_sql ) { $store = ActionScheduler::store(); foreach ( $ids as $action_id ) { $store->delete( $action_id ); } } /** * Prepares the _column_headers property which is used by WP_Table_List at rendering. * It merges the columns and the sortable columns. */ protected function prepare_column_headers() { $this->_column_headers = array( $this->get_columns(), get_hidden_columns( $this->screen ), $this->get_sortable_columns(), ); } /** * Reads $this->sort_by and returns the columns name in a format that WP_Table_List * expects */ public function get_sortable_columns() { $sort_by = array(); foreach ( $this->sort_by as $column ) { $sort_by[ $column ] = array( $column, true ); } return $sort_by; } /** * Returns the columns names for rendering. It adds a checkbox for selecting everything * as the first column */ public function get_columns() { $columns = array_merge( array( 'cb' => '<input type="checkbox" />' ), $this->columns ); return $columns; } /** * Get prepared LIMIT clause for items query * * @global wpdb $wpdb * * @return string Prepared LIMIT clause for items query. */ protected function get_items_query_limit() { global $wpdb; $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page ); return $wpdb->prepare( 'LIMIT %d', $per_page ); } /** * Returns the number of items to offset/skip for this current view. * * @return int */ protected function get_items_offset() { $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page ); $current_page = $this->get_pagenum(); if ( 1 < $current_page ) { $offset = $per_page * ( $current_page - 1 ); } else { $offset = 0; } return $offset; } /** * Get prepared OFFSET clause for items query * * @global wpdb $wpdb * * @return string Prepared OFFSET clause for items query. */ protected function get_items_query_offset() { global $wpdb; return $wpdb->prepare( 'OFFSET %d', $this->get_items_offset() ); } /** * Prepares the ORDER BY sql statement. It uses `$this->sort_by` to know which * columns are sortable. This requests validates the orderby $_GET parameter is a valid * column and sortable. It will also use order (ASC|DESC) using DESC by default. */ protected function get_items_query_order() { if ( empty( $this->sort_by ) ) { return ''; } $orderby = esc_sql( $this->get_request_orderby() ); $order = esc_sql( $this->get_request_order() ); return "ORDER BY {$orderby} {$order}"; } /** * Querystring arguments to persist between form submissions. * * @since 3.7.3 * * @return string[] */ protected function get_request_query_args_to_persist() { return array_merge( $this->sort_by, array( 'page', 'status', 'tab', ) ); } /** * Return the sortable column specified for this request to order the results by, if any. * * @return string */ protected function get_request_orderby() { $valid_sortable_columns = array_values( $this->sort_by ); if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended $orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended } else { $orderby = $valid_sortable_columns[0]; } return $orderby; } /** * Return the sortable column order specified for this request. * * @return string */ protected function get_request_order() { if ( ! empty( $_GET['order'] ) && 'desc' === strtolower( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended $order = 'DESC'; } else { $order = 'ASC'; } return $order; } /** * Return the status filter for this request, if any. * * @return string */ protected function get_request_status() { $status = ( ! empty( $_GET['status'] ) ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended return $status; } /** * Return the search filter for this request, if any. * * @return string */ protected function get_request_search_query() { $search_query = ( ! empty( $_GET['s'] ) ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended return $search_query; } /** * Process and return the columns name. This is meant for using with SQL, this means it * always includes the primary key. * * @return array */ protected function get_table_columns() { $columns = array_keys( $this->columns ); if ( ! in_array( $this->ID, $columns, true ) ) { $columns[] = $this->ID; } return $columns; } /** * Check if the current request is doing a "full text" search. If that is the case * prepares the SQL to search texts using LIKE. * * If the current request does not have any search or if this list table does not support * that feature it will return an empty string. * * @return string */ protected function get_items_query_search() { global $wpdb; if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended return ''; } $search_string = sanitize_text_field( wp_unslash( $_GET['s'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended $filter = array(); foreach ( $this->search_by as $column ) { $wild = '%'; $sql_like = $wild . $wpdb->esc_like( $search_string ) . $wild; $filter[] = $wpdb->prepare( '`' . $column . '` LIKE %s', $sql_like ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.NotPrepared } return implode( ' OR ', $filter ); } /** * Prepares the SQL to filter rows by the options defined at `$this->filter_by`. Before trusting * any data sent by the user it validates that it is a valid option. */ protected function get_items_query_filters() { global $wpdb; if ( ! $this->filter_by || empty( $_GET['filter_by'] ) || ! is_array( $_GET['filter_by'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended return ''; } $filter = array(); foreach ( $this->filter_by as $column => $options ) { if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended continue; } $filter[] = $wpdb->prepare( "`$column` = %s", sanitize_text_field( wp_unslash( $_GET['filter_by'][ $column ] ) ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.InterpolatedNotPrepared } return implode( ' AND ', $filter ); } /** * Prepares the data to feed WP_Table_List. * * This has the core for selecting, sorting and filtering data. To keep the code simple * its logic is split among many methods (get_items_query_*). * * Beside populating the items this function will also count all the records that matches * the filtering criteria and will do fill the pagination variables. */ public function prepare_items() { global $wpdb; $this->process_bulk_action(); $this->process_row_actions(); if ( ! empty( $_REQUEST['_wp_http_referer'] && ! empty( $_SERVER['REQUEST_URI'] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); exit; } $this->prepare_column_headers(); $limit = $this->get_items_query_limit(); $offset = $this->get_items_query_offset(); $order = $this->get_items_query_order(); $where = array_filter( array( $this->get_items_query_search(), $this->get_items_query_filters(), ) ); $columns = '`' . implode( '`, `', $this->get_table_columns() ) . '`'; if ( ! empty( $where ) ) { $where = 'WHERE (' . implode( ') AND (', $where ) . ')'; } else { $where = ''; } $sql = "SELECT $columns FROM {$this->table_name} {$where} {$order} {$limit} {$offset}"; $this->set_items( $wpdb->get_results( $sql, ARRAY_A ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}"; $total_items = $wpdb->get_var( $query_count ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page ); $this->set_pagination_args( array( 'total_items' => $total_items, 'per_page' => $per_page, 'total_pages' => ceil( $total_items / $per_page ), ) ); } /** * Display the table. * * @param string $which The name of the table. */ public function extra_tablenav( $which ) { if ( ! $this->filter_by || 'top' !== $which ) { return; } echo '<div class="alignleft actions">'; foreach ( $this->filter_by as $id => $options ) { $default = ! empty( $_GET['filter_by'][ $id ] ) ? sanitize_text_field( wp_unslash( $_GET['filter_by'][ $id ] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $options[ $default ] ) ) { $default = ''; } echo '<select name="filter_by[' . esc_attr( $id ) . ']" class="first" id="filter-by-' . esc_attr( $id ) . '">'; foreach ( $options as $value => $label ) { echo '<option value="' . esc_attr( $value ) . '" ' . esc_html( $value === $default ? 'selected' : '' ) . '>' . esc_html( $label ) . '</option>'; } echo '</select>'; } submit_button( esc_html__( 'Filter', 'action-scheduler' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) ); echo '</div>'; } /** * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns * are serialized). This can be override in child classes for further data transformation. * * @param array $items Items array. */ protected function set_items( array $items ) { $this->items = array(); foreach ( $items as $item ) { $this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item ); } } /** * Renders the checkbox for each row, this is the first column and it is named ID regardless * of how the primary key is named (to keep the code simpler). The bulk actions will do the proper * name transformation though using `$this->ID`. * * @param array $row The row to render. */ public function column_cb( $row ) { return '<input name="ID[]" type="checkbox" value="' . esc_attr( $row[ $this->ID ] ) . '" />'; } /** * Renders the row-actions. * * This method renders the action menu, it reads the definition from the $row_actions property, * and it checks that the row action method exists before rendering it. * * @param array $row Row to be rendered. * @param string $column_name Column name. * @return string */ protected function maybe_render_actions( $row, $column_name ) { if ( empty( $this->row_actions[ $column_name ] ) ) { return; } $row_id = $row[ $this->ID ]; $actions = '<div class="row-actions">'; $action_count = 0; foreach ( $this->row_actions[ $column_name ] as $action_key => $action ) { $action_count++; if ( ! method_exists( $this, 'row_action_' . $action_key ) ) { continue; } $action_link = ! empty( $action['link'] ) ? $action['link'] : add_query_arg( array( 'row_action' => $action_key, 'row_id' => $row_id, 'nonce' => wp_create_nonce( $action_key . '::' . $row_id ), ) ); $span_class = ! empty( $action['class'] ) ? $action['class'] : $action_key; $separator = ( $action_count < count( $this->row_actions[ $column_name ] ) ) ? ' | ' : ''; $actions .= sprintf( '<span class="%s">', esc_attr( $span_class ) ); $actions .= sprintf( '<a href="%1$s" title="%2$s">%3$s</a>', esc_url( $action_link ), esc_attr( $action['desc'] ), esc_html( $action['name'] ) ); $actions .= sprintf( '%s</span>', $separator ); } $actions .= '</div>'; return $actions; } /** * Process the bulk actions. * * @return void */ protected function process_row_actions() { $parameters = array( 'row_action', 'row_id', 'nonce' ); foreach ( $parameters as $parameter ) { if ( empty( $_REQUEST[ $parameter ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } } $action = sanitize_text_field( wp_unslash( $_REQUEST['row_action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $row_id = sanitize_text_field( wp_unslash( $_REQUEST['row_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $nonce = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $method = 'row_action_' . $action; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( wp_verify_nonce( $nonce, $action . '::' . $row_id ) && method_exists( $this, $method ) ) { $this->$method( sanitize_text_field( wp_unslash( $row_id ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } if ( isset( $_SERVER['REQUEST_URI'] ) ) { wp_safe_redirect( remove_query_arg( array( 'row_id', 'row_action', 'nonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); exit; } } /** * Default column formatting, it will escape everything for security. * * @param array $item The item array. * @param string $column_name Column name to display. * * @return string */ public function column_default( $item, $column_name ) { $column_html = esc_html( $item[ $column_name ] ); $column_html .= $this->maybe_render_actions( $item, $column_name ); return $column_html; } /** * Display the table heading and search query, if any */ protected function display_header() { echo '<h1 class="wp-heading-inline">' . esc_attr( $this->table_header ) . '</h1>'; if ( $this->get_request_search_query() ) { /* translators: %s: search query */ echo '<span class="subtitle">' . esc_attr( sprintf( __( 'Search results for "%s"', 'action-scheduler' ), $this->get_request_search_query() ) ) . '</span>'; } echo '<hr class="wp-header-end">'; } /** * Display the table heading and search query, if any */ protected function display_admin_notices() { foreach ( $this->admin_notices as $notice ) { echo '<div id="message" class="' . esc_attr( $notice['class'] ) . '">'; echo ' <p>' . wp_kses_post( $notice['message'] ) . '</p>'; echo '</div>'; } } /** * Prints the available statuses so the user can click to filter. */ protected function display_filter_by_status() { $status_list_items = array(); $request_status = $this->get_request_status(); // Helper to set 'all' filter when not set on status counts passed in. if ( ! isset( $this->status_counts['all'] ) ) { $all_count = array_sum( $this->status_counts ); if ( isset( $this->status_counts['past-due'] ) ) { $all_count -= $this->status_counts['past-due']; } $this->status_counts = array( 'all' => $all_count ) + $this->status_counts; } // Translated status labels. $status_labels = ActionScheduler_Store::instance()->get_status_labels(); $status_labels['all'] = esc_html_x( 'All', 'status labels', 'action-scheduler' ); $status_labels['past-due'] = esc_html_x( 'Past-due', 'status labels', 'action-scheduler' ); foreach ( $this->status_counts as $status_slug => $count ) { if ( 0 === $count ) { continue; } if ( $status_slug === $request_status || ( empty( $request_status ) && 'all' === $status_slug ) ) { $status_list_item = '<li class="%1$s"><a href="%2$s" class="current">%3$s</a> (%4$d)</li>'; } else { $status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>'; } $status_name = isset( $status_labels[ $status_slug ] ) ? $status_labels[ $status_slug ] : ucfirst( $status_slug ); $status_filter_url = ( 'all' === $status_slug ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_slug ); $status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url ); $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_slug ), esc_url( $status_filter_url ), esc_html( $status_name ), absint( $count ) ); } if ( $status_list_items ) { echo '<ul class="subsubsub">'; echo implode( " | \n", $status_list_items ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '</ul>'; } } /** * Renders the table list, we override the original class to render the table inside a form * and to render any needed HTML (like the search box). By doing so the callee of a function can simple * forget about any extra HTML. */ protected function display_table() { echo '<form id="' . esc_attr( $this->_args['plural'] ) . '-filter" method="get">'; foreach ( $this->get_request_query_args_to_persist() as $arg ) { $arg_value = isset( $_GET[ $arg ] ) ? sanitize_text_field( wp_unslash( $_GET[ $arg ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! $arg_value ) { continue; } echo '<input type="hidden" name="' . esc_attr( $arg ) . '" value="' . esc_attr( $arg_value ) . '" />'; } if ( ! empty( $this->search_by ) ) { echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } parent::display(); echo '</form>'; } /** * Process any pending actions. */ public function process_actions() { $this->process_bulk_action(); $this->process_row_actions(); if ( ! empty( $_REQUEST['_wp_http_referer'] ) && ! empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); exit; } } /** * Render the list table page, including header, notices, status filters and table. */ public function display_page() { $this->prepare_items(); echo '<div class="wrap">'; $this->display_header(); $this->display_admin_notices(); $this->display_filter_by_status(); $this->display_table(); echo '</div>'; } /** * Get the text to display in the search box on the list table. */ protected function get_search_box_placeholder() { return esc_html__( 'Search', 'action-scheduler' ); } /** * Gets the screen per_page option name. * * @return string */ protected function get_per_page_option_name() { return $this->package . '_items_per_page'; } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php 0000644 00000011443 15174671745 0024545 0 ustar 00 <?php /** * Class ActionScheduler_Abstract_Schema * * @package Action_Scheduler * * @codeCoverageIgnore * * Utility class for creating/updating custom tables */ abstract class ActionScheduler_Abstract_Schema { /** * Increment this value in derived class to trigger a schema update. * * @var int */ protected $schema_version = 1; /** * Schema version stored in database. * * @var string */ protected $db_version; /** * Names of tables that will be registered by this class. * * @var array */ protected $tables = array(); /** * Can optionally be used by concrete classes to carry out additional initialization work * as needed. */ public function init() {} /** * Register tables with WordPress, and create them if needed. * * @param bool $force_update Optional. Default false. Use true to always run the schema update. * * @return void */ public function register_tables( $force_update = false ) { global $wpdb; // make WP aware of our tables. foreach ( $this->tables as $table ) { $wpdb->tables[] = $table; $name = $this->get_full_table_name( $table ); $wpdb->$table = $name; } // create the tables. if ( $this->schema_update_required() || $force_update ) { foreach ( $this->tables as $table ) { /** * Allow custom processing before updating a table schema. * * @param string $table Name of table being updated. * @param string $db_version Existing version of the table being updated. */ do_action( 'action_scheduler_before_schema_update', $table, $this->db_version ); $this->update_table( $table ); } $this->mark_schema_update_complete(); } } /** * Get table definition. * * @param string $table The name of the table. * * @return string The CREATE TABLE statement, suitable for passing to dbDelta */ abstract protected function get_table_definition( $table ); /** * Determine if the database schema is out of date * by comparing the integer found in $this->schema_version * with the option set in the WordPress options table * * @return bool */ private function schema_update_required() { $option_name = 'schema-' . static::class; $this->db_version = get_option( $option_name, 0 ); // Check for schema option stored by the Action Scheduler Custom Tables plugin in case site has migrated from that plugin with an older schema. if ( 0 === $this->db_version ) { $plugin_option_name = 'schema-'; switch ( static::class ) { case 'ActionScheduler_StoreSchema': $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker'; break; case 'ActionScheduler_LoggerSchema': $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker'; break; } $this->db_version = get_option( $plugin_option_name, 0 ); delete_option( $plugin_option_name ); } return version_compare( $this->db_version, $this->schema_version, '<' ); } /** * Update the option in WordPress to indicate that * our schema is now up to date * * @return void */ private function mark_schema_update_complete() { $option_name = 'schema-' . static::class; // work around race conditions and ensure that our option updates. $value_to_save = (string) $this->schema_version . '.0.' . time(); update_option( $option_name, $value_to_save ); } /** * Update the schema for the given table * * @param string $table The name of the table to update. * * @return void */ private function update_table( $table ) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $definition = $this->get_table_definition( $table ); if ( $definition ) { $updated = dbDelta( $definition ); foreach ( $updated as $updated_table => $update_description ) { if ( strpos( $update_description, 'Created table' ) === 0 ) { do_action( 'action_scheduler/created_table', $updated_table, $table ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } } } } /** * Get full table name. * * @param string $table Table name. * * @return string The full name of the table, including the * table prefix for the current blog */ protected function get_full_table_name( $table ) { return $GLOBALS['wpdb']->prefix . $table; } /** * Confirms that all of the tables registered by this schema class have been created. * * @return bool */ public function tables_exist() { global $wpdb; $tables_exist = true; foreach ( $this->tables as $table_name ) { $table_name = $wpdb->prefix . $table_name; $pattern = str_replace( '_', '\\_', $table_name ); $existing_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pattern ) ); if ( $existing_table !== $table_name ) { $tables_exist = false; break; } } return $tables_exist; } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_WPCLI_Command.php 0000644 00000004073 15174671745 0024037 0 ustar 00 <?php /** * Abstract for WP-CLI commands. */ abstract class ActionScheduler_WPCLI_Command extends \WP_CLI_Command { const DATE_FORMAT = 'Y-m-d H:i:s O'; /** * Keyed arguments. * * @var string[] */ protected $args; /** * Positional arguments. * * @var array<string, string> */ protected $assoc_args; /** * Construct. * * @param string[] $args Positional arguments. * @param array<string, string> $assoc_args Keyed arguments. * @throws \Exception When loading a CLI command file outside of WP CLI context. */ public function __construct( array $args, array $assoc_args ) { if ( ! defined( 'WP_CLI' ) || ! constant( 'WP_CLI' ) ) { /* translators: %s php class name */ throw new \Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), get_class( $this ) ) ); } $this->args = $args; $this->assoc_args = $assoc_args; } /** * Execute command. */ abstract public function execute(); /** * Get the scheduled date in a human friendly format. * * @see ActionScheduler_ListTable::get_schedule_display_string() * @param ActionScheduler_Schedule $schedule Schedule. * @return string */ protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) { $schedule_display_string = ''; if ( ! $schedule->get_date() ) { return '0000-00-00 00:00:00'; } $next_timestamp = $schedule->get_date()->getTimestamp(); $schedule_display_string .= $schedule->get_date()->format( static::DATE_FORMAT ); return $schedule_display_string; } /** * Transforms arguments with '__' from CSV into expected arrays. * * @see \WP_CLI\CommandWithDBObject::process_csv_arguments_to_arrays() * @link https://github.com/wp-cli/entity-command/blob/c270cc9a2367cb8f5845f26a6b5e203397c91392/src/WP_CLI/CommandWithDBObject.php#L99 * @return void */ protected function process_csv_arguments_to_arrays() { foreach ( $this->assoc_args as $k => $v ) { if ( false !== strpos( $k, '__' ) ) { $this->assoc_args[ $k ] = explode( ',', $v ); } } } } Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php 0000644 00000003437 15174671745 0022416 0 ustar 00 <?php /** * Abstract class for setting a basic lock to throttle some action. * * Class ActionScheduler_Lock */ abstract class ActionScheduler_Lock { /** * Instance. * * @var ActionScheduler_Lock */ private static $locker = null; /** * Duration of lock. * * @var int */ protected static $lock_duration = MINUTE_IN_SECONDS; /** * Check if a lock is set for a given lock type. * * @param string $lock_type A string to identify different lock types. * @return bool */ public function is_locked( $lock_type ) { return ( $this->get_expiration( $lock_type ) >= time() ); } /** * Set a lock. * * To prevent race conditions, implementations should avoid setting the lock if the lock is already held. * * @param string $lock_type A string to identify different lock types. * @return bool */ abstract public function set( $lock_type ); /** * If a lock is set, return the timestamp it was set to expiry. * * @param string $lock_type A string to identify different lock types. * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. */ abstract public function get_expiration( $lock_type ); /** * Get the amount of time to set for a given lock. 60 seconds by default. * * @param string $lock_type A string to identify different lock types. * @return int */ protected function get_duration( $lock_type ) { return apply_filters( 'action_scheduler_lock_duration', self::$lock_duration, $lock_type ); } /** * Get instance. * * @return ActionScheduler_Lock */ public static function instance() { if ( empty( self::$locker ) ) { $class = apply_filters( 'action_scheduler_lock_class', 'ActionScheduler_OptionLock' ); self::$locker = new $class(); } return self::$locker; } } Dependencies/ActionScheduler/classes/ActionScheduler_DateTime.php 0000644 00000004014 15174671745 0021224 0 ustar 00 <?php /** * ActionScheduler DateTime class. * * This is a custom extension to DateTime that */ class ActionScheduler_DateTime extends DateTime { /** * UTC offset. * * Only used when a timezone is not set. When a timezone string is * used, this will be set to 0. * * @var int */ protected $utcOffset = 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase /** * Get the unix timestamp of the current object. * * Missing in PHP 5.2 so just here so it can be supported consistently. * * @return int */ #[\ReturnTypeWillChange] public function getTimestamp() { return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' ); } /** * Set the UTC offset. * * This represents a fixed offset instead of a timezone setting. * * @param string|int $offset UTC offset value. */ public function setUtcOffset( $offset ) { $this->utcOffset = intval( $offset ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } /** * Returns the timezone offset. * * @return int * @link http://php.net/manual/en/datetime.getoffset.php */ #[\ReturnTypeWillChange] public function getOffset() { return $this->utcOffset ? $this->utcOffset : parent::getOffset(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } /** * Set the TimeZone associated with the DateTime * * @param DateTimeZone $timezone Timezone object. * * @return static * @link http://php.net/manual/en/datetime.settimezone.php */ #[\ReturnTypeWillChange] public function setTimezone( $timezone ) { $this->utcOffset = 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase parent::setTimezone( $timezone ); return $this; } /** * Get the timestamp with the WordPress timezone offset added or subtracted. * * @since 3.0.0 * @return int */ public function getOffsetTimestamp() { return $this->getTimestamp() + $this->getOffset(); } } Dependencies/ActionScheduler/classes/schedules/ActionScheduler_NullSchedule.php 0000644 00000001305 15174671745 0024076 0 ustar 00 <?php /** * Class ActionScheduler_NullSchedule */ class ActionScheduler_NullSchedule extends ActionScheduler_SimpleSchedule { /** * DateTime instance. * * @var DateTime|null */ protected $scheduled_date; /** * Make the $date param optional and default to null. * * @param null|DateTime $date The date & time to run the action. */ public function __construct( ?DateTime $date = null ) { $this->scheduled_date = null; } /** * This schedule has no scheduled DateTime, so we need to override the parent __sleep(). * * @return array */ public function __sleep() { return array(); } /** * Wakeup. */ public function __wakeup() { $this->scheduled_date = null; } } Dependencies/ActionScheduler/classes/schedules/ActionScheduler_IntervalSchedule.php 0000644 00000005111 15174671745 0024747 0 ustar 00 <?php /** * Class ActionScheduler_IntervalSchedule */ class ActionScheduler_IntervalSchedule extends ActionScheduler_Abstract_RecurringSchedule implements ActionScheduler_Schedule { /** * Deprecated property @see $this->__wakeup() for details. * * @var null */ private $start_timestamp = null; /** * Deprecated property @see $this->__wakeup() for details. * * @var null */ private $interval_in_seconds = null; /** * Calculate when this schedule should start after a given date & time using * the number of seconds between recurrences. * * @param DateTime $after Timestamp. * @return DateTime */ protected function calculate_next( DateTime $after ) { $after->modify( '+' . (int) $this->get_recurrence() . ' seconds' ); return $after; } /** * Schedule interval in seconds. * * @return int */ public function interval_in_seconds() { _deprecated_function( __METHOD__, '3.0.0', '(int)ActionScheduler_Abstract_RecurringSchedule::get_recurrence()' ); return (int) $this->get_recurrence(); } /** * Serialize interval schedules with data required prior to AS 3.0.0 * * Prior to Action Scheduler 3.0.0, recurring schedules used different property names to * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 * aligned properties and property names for better inheritance. To guard against the * possibility of infinite loops if downgrading to Action Scheduler < 3.0.0, we need to * also store the data with the old property names so if it's unserialized in AS < 3.0, * the schedule doesn't end up with a null/false/0 recurrence. * * @return array */ public function __sleep() { $sleep_params = parent::__sleep(); $this->start_timestamp = $this->scheduled_timestamp; $this->interval_in_seconds = $this->recurrence; return array_merge( $sleep_params, array( 'start_timestamp', 'interval_in_seconds', ) ); } /** * Unserialize interval schedules serialized/stored prior to AS 3.0.0 * * For more background, @see ActionScheduler_Abstract_RecurringSchedule::__wakeup(). */ public function __wakeup() { if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->start_timestamp ) ) { $this->scheduled_timestamp = $this->start_timestamp; unset( $this->start_timestamp ); } if ( is_null( $this->recurrence ) && ! is_null( $this->interval_in_seconds ) ) { $this->recurrence = $this->interval_in_seconds; unset( $this->interval_in_seconds ); } parent::__wakeup(); } } Dependencies/ActionScheduler/classes/schedules/ActionScheduler_Schedule.php 0000644 00000000710 15174671745 0023242 0 ustar 00 <?php /** * Class ActionScheduler_Schedule */ interface ActionScheduler_Schedule { /** * Get the date & time this schedule was created to run, or calculate when it should be run * after a given date & time. * * @param null|DateTime $after Timestamp. * @return DateTime|null */ public function next( ?DateTime $after = null ); /** * Identify the schedule as (not) recurring. * * @return bool */ public function is_recurring(); } Dependencies/ActionScheduler/classes/schedules/ActionScheduler_CanceledSchedule.php 0000644 00000003157 15174671745 0024671 0 ustar 00 <?php /** * Class ActionScheduler_SimpleSchedule */ class ActionScheduler_CanceledSchedule extends ActionScheduler_SimpleSchedule { /** * Deprecated property @see $this->__wakeup() for details. * * @var null */ private $timestamp = null; /** * Calculate when the next instance of this schedule would run based on a given date & time. * * @param DateTime $after Timestamp. * * @return DateTime|null */ public function calculate_next( DateTime $after ) { return null; } /** * Cancelled actions should never have a next schedule, even if get_next() * is called with $after < $this->scheduled_date. * * @param DateTime $after Timestamp. * @return DateTime|null */ public function get_next( DateTime $after ) { return null; } /** * Action is not recurring. * * @return bool */ public function is_recurring() { return false; } /** * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 * * Prior to Action Scheduler 3.0.0, schedules used different property names to refer * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 * aligned properties and property names for better inheritance. To maintain backward * compatibility with schedules serialized and stored prior to 3.0, we need to correctly * map the old property names with matching visibility. */ public function __wakeup() { if ( ! is_null( $this->timestamp ) ) { $this->scheduled_timestamp = $this->timestamp; unset( $this->timestamp ); } parent::__wakeup(); } } Dependencies/ActionScheduler/classes/schedules/ActionScheduler_SimpleSchedule.php 0000644 00000004477 15174671745 0024432 0 ustar 00 <?php /** * Class ActionScheduler_SimpleSchedule */ class ActionScheduler_SimpleSchedule extends ActionScheduler_Abstract_Schedule { /** * Deprecated property @see $this->__wakeup() for details. * * @var null|DateTime */ private $timestamp = null; /** * Calculate when this schedule should start after a given date & time using * the number of seconds between recurrences. * * @param DateTime $after Timestamp. * * @return DateTime|null */ public function calculate_next( DateTime $after ) { return null; } /** * Schedule is not recurring. * * @return bool */ public function is_recurring() { return false; } /** * Serialize schedule with data required prior to AS 3.0.0 * * Prior to Action Scheduler 3.0.0, schedules used different property names to refer * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 * aligned properties and property names for better inheritance. To guard against the * scheduled date for single actions always being seen as "now" if downgrading to * Action Scheduler < 3.0.0, we need to also store the data with the old property names * so if it's unserialized in AS < 3.0, the schedule doesn't end up with a null recurrence. * * @return array */ public function __sleep() { $sleep_params = parent::__sleep(); $this->timestamp = $this->scheduled_timestamp; return array_merge( $sleep_params, array( 'timestamp', ) ); } /** * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 * * Prior to Action Scheduler 3.0.0, schedules used different property names to refer * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 * aligned properties and property names for better inheritance. To maintain backward * compatibility with schedules serialized and stored prior to 3.0, we need to correctly * map the old property names with matching visibility. */ public function __wakeup() { if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->timestamp ) ) { $this->scheduled_timestamp = $this->timestamp; unset( $this->timestamp ); } parent::__wakeup(); } } Dependencies/ActionScheduler/classes/schedules/ActionScheduler_CronSchedule.php 0000644 00000007367 15174671745 0024103 0 ustar 00 <?php /** * Class ActionScheduler_CronSchedule */ class ActionScheduler_CronSchedule extends ActionScheduler_Abstract_RecurringSchedule implements ActionScheduler_Schedule { /** * Deprecated property @see $this->__wakeup() for details. * * @var null */ private $start_timestamp = null; /** * Deprecated property @see $this->__wakeup() for details. * * @var null */ private $cron = null; /** * Wrapper for parent constructor to accept a cron expression string and map it to a CronExpression for this * objects $recurrence property. * * @param DateTime $start The date & time to run the action at or after. If $start aligns with the CronSchedule passed via $recurrence, it will be used. If it does not align, the first matching date after it will be used. * @param CronExpression|string $recurrence The CronExpression used to calculate the schedule's next instance. * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance. */ public function __construct( DateTime $start, $recurrence, ?DateTime $first = null ) { if ( ! is_a( $recurrence, 'CronExpression' ) ) { $recurrence = CronExpression::factory( $recurrence ); } // For backward compatibility, we need to make sure the date is set to the first matching cron date, not whatever date is passed in. Importantly, by passing true as the 3rd param, if $start matches the cron expression, then it will be used. This was previously handled in the now deprecated next() method. $date = $recurrence->getNextRunDate( $start, 0, true ); // parent::__construct() will set this to $date by default, but that may be different to $start now. $first = empty( $first ) ? $start : $first; parent::__construct( $date, $recurrence, $first ); } /** * Calculate when an instance of this schedule would start based on a given * date & time using its the CronExpression. * * @param DateTime $after Timestamp. * @return DateTime */ protected function calculate_next( DateTime $after ) { return $this->recurrence->getNextRunDate( $after, 0, false ); } /** * Get the schedule's recurrence. * * @return string */ public function get_recurrence() { return strval( $this->recurrence ); } /** * Serialize cron schedules with data required prior to AS 3.0.0 * * Prior to Action Scheduler 3.0.0, recurring schedules used different property names to * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 * aligned properties and property names for better inheritance. To guard against the * possibility of infinite loops if downgrading to Action Scheduler < 3.0.0, we need to * also store the data with the old property names so if it's unserialized in AS < 3.0, * the schedule doesn't end up with a null recurrence. * * @return array */ public function __sleep() { $sleep_params = parent::__sleep(); $this->start_timestamp = $this->scheduled_timestamp; $this->cron = $this->recurrence; return array_merge( $sleep_params, array( 'start_timestamp', 'cron', ) ); } /** * Unserialize cron schedules serialized/stored prior to AS 3.0.0 * * For more background, @see ActionScheduler_Abstract_RecurringSchedule::__wakeup(). */ public function __wakeup() { if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->start_timestamp ) ) { $this->scheduled_timestamp = $this->start_timestamp; unset( $this->start_timestamp ); } if ( is_null( $this->recurrence ) && ! is_null( $this->cron ) ) { $this->recurrence = $this->cron; unset( $this->cron ); } parent::__wakeup(); } } Dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php 0000644 00000037620 15174671745 0022306 0 ustar 00 <?php /** * Class ActionScheduler_ActionFactory */ class ActionScheduler_ActionFactory { /** * Return stored actions for given params. * * @param string $status The action's status in the data store. * @param string $hook The hook to trigger when this action runs. * @param array $args Args to pass to callbacks when the hook is triggered. * @param ActionScheduler_Schedule $schedule The action's schedule. * @param string $group A group to put the action in. * phpcs:ignore Squiz.Commenting.FunctionComment.ExtraParamComment * @param int $priority The action priority. * * @return ActionScheduler_Action An instance of the stored action. */ public function get_stored_action( $status, $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) { // The 6th parameter ($priority) is not formally declared in the method signature to maintain compatibility with // third-party subclasses created before this param was added. $priority = func_num_args() >= 6 ? (int) func_get_arg( 5 ) : 10; switch ( $status ) { case ActionScheduler_Store::STATUS_PENDING: $action_class = 'ActionScheduler_Action'; break; case ActionScheduler_Store::STATUS_CANCELED: $action_class = 'ActionScheduler_CanceledAction'; if ( ! is_null( $schedule ) && ! is_a( $schedule, 'ActionScheduler_CanceledSchedule' ) && ! is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) { $schedule = new ActionScheduler_CanceledSchedule( $schedule->get_date() ); } break; default: $action_class = 'ActionScheduler_FinishedAction'; break; } $action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group ); $action = new $action_class( $hook, $args, $schedule, $group ); $action->set_priority( $priority ); /** * Allow 3rd party code to change the instantiated action for a given hook, args, schedule and group. * * @param ActionScheduler_Action $action The instantiated action. * @param string $hook The instantiated action's hook. * @param array $args The instantiated action's args. * @param ActionScheduler_Schedule $schedule The instantiated action's schedule. * @param string $group The instantiated action's group. * @param int $priority The action priority. */ return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group, $priority ); } /** * Enqueue an action to run one time, as soon as possible (rather a specific scheduled time). * * This method creates a new action using the NullSchedule. In practice, this results in an action scheduled to * execute "now". Therefore, it will generally run as soon as possible but is not prioritized ahead of other actions * that are already past-due. * * @param string $hook The hook to trigger when this action runs. * @param array $args Args to pass when the hook is triggered. * @param string $group A group to put the action in. * * @return int The ID of the stored action. */ public function async( $hook, $args = array(), $group = '' ) { return $this->async_unique( $hook, $args, $group, false ); } /** * Same as async, but also supports $unique param. * * @param string $hook The hook to trigger when this action runs. * @param array $args Args to pass when the hook is triggered. * @param string $group A group to put the action in. * @param bool $unique Whether to ensure the action is unique. * * @return int The ID of the stored action. */ public function async_unique( $hook, $args = array(), $group = '', $unique = true ) { $schedule = new ActionScheduler_NullSchedule(); $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); return $unique ? $this->store_unique_action( $action, $unique ) : $this->store( $action ); } /** * Create single action. * * @param string $hook The hook to trigger when this action runs. * @param array $args Args to pass when the hook is triggered. * @param int $when Unix timestamp when the action will run. * @param string $group A group to put the action in. * * @return int The ID of the stored action. */ public function single( $hook, $args = array(), $when = null, $group = '' ) { return $this->single_unique( $hook, $args, $when, $group, false ); } /** * Create single action only if there is no pending or running action with same name and params. * * @param string $hook The hook to trigger when this action runs. * @param array $args Args to pass when the hook is triggered. * @param int $when Unix timestamp when the action will run. * @param string $group A group to put the action in. * @param bool $unique Whether action scheduled should be unique. * * @return int The ID of the stored action. */ public function single_unique( $hook, $args = array(), $when = null, $group = '', $unique = true ) { $date = as_get_datetime_object( $when ); $schedule = new ActionScheduler_SimpleSchedule( $date ); $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); return $unique ? $this->store_unique_action( $action ) : $this->store( $action ); } /** * Create the first instance of an action recurring on a given interval. * * @param string $hook The hook to trigger when this action runs. * @param array $args Args to pass when the hook is triggered. * @param int $first Unix timestamp for the first run. * @param int $interval Seconds between runs. * @param string $group A group to put the action in. * * @return int The ID of the stored action. */ public function recurring( $hook, $args = array(), $first = null, $interval = null, $group = '' ) { return $this->recurring_unique( $hook, $args, $first, $interval, $group, false ); } /** * Create the first instance of an action recurring on a given interval only if there is no pending or running action with same name and params. * * @param string $hook The hook to trigger when this action runs. * @param array $args Args to pass when the hook is triggered. * @param int $first Unix timestamp for the first run. * @param int $interval Seconds between runs. * @param string $group A group to put the action in. * @param bool $unique Whether action scheduled should be unique. * * @return int The ID of the stored action. */ public function recurring_unique( $hook, $args = array(), $first = null, $interval = null, $group = '', $unique = true ) { if ( empty( $interval ) ) { return $this->single_unique( $hook, $args, $first, $group, $unique ); } $date = as_get_datetime_object( $first ); $schedule = new ActionScheduler_IntervalSchedule( $date, $interval ); $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); return $unique ? $this->store_unique_action( $action ) : $this->store( $action ); } /** * Create the first instance of an action recurring on a Cron schedule. * * @param string $hook The hook to trigger when this action runs. * @param array $args Args to pass when the hook is triggered. * @param int $base_timestamp The first instance of the action will be scheduled * to run at a time calculated after this timestamp matching the cron * expression. This can be used to delay the first instance of the action. * @param int $schedule A cron definition string. * @param string $group A group to put the action in. * * @return int The ID of the stored action. */ public function cron( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '' ) { return $this->cron_unique( $hook, $args, $base_timestamp, $schedule, $group, false ); } /** * Create the first instance of an action recurring on a Cron schedule only if there is no pending or running action with same name and params. * * @param string $hook The hook to trigger when this action runs. * @param array $args Args to pass when the hook is triggered. * @param int $base_timestamp The first instance of the action will be scheduled * to run at a time calculated after this timestamp matching the cron * expression. This can be used to delay the first instance of the action. * @param int $schedule A cron definition string. * @param string $group A group to put the action in. * @param bool $unique Whether action scheduled should be unique. * * @return int The ID of the stored action. **/ public function cron_unique( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '', $unique = true ) { if ( empty( $schedule ) ) { return $this->single_unique( $hook, $args, $base_timestamp, $group, $unique ); } $date = as_get_datetime_object( $base_timestamp ); $cron = CronExpression::factory( $schedule ); $schedule = new ActionScheduler_CronSchedule( $date, $cron ); $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); return $unique ? $this->store_unique_action( $action ) : $this->store( $action ); } /** * Create a successive instance of a recurring or cron action. * * Importantly, the action will be rescheduled to run based on the current date/time. * That means when the action is scheduled to run in the past, the next scheduled date * will be pushed forward. For example, if a recurring action set to run every hour * was scheduled to run 5 seconds ago, it will be next scheduled for 1 hour in the * future, which is 1 hour and 5 seconds from when it was last scheduled to run. * * Alternatively, if the action is scheduled to run in the future, and is run early, * likely via manual intervention, then its schedule will change based on the time now. * For example, if a recurring action set to run every day, and is run 12 hours early, * it will run again in 24 hours, not 36 hours. * * This slippage is less of an issue with Cron actions, as the specific run time can * be set for them to run, e.g. 1am each day. In those cases, and entire period would * need to be missed before there was any change is scheduled, e.g. in the case of an * action scheduled for 1am each day, the action would need to run an entire day late. * * @param ActionScheduler_Action $action The existing action. * * @return string The ID of the stored action * @throws InvalidArgumentException If $action is not a recurring action. */ public function repeat( $action ) { $schedule = $action->get_schedule(); $next = $schedule->get_next( as_get_datetime_object() ); if ( is_null( $next ) || ! $schedule->is_recurring() ) { throw new InvalidArgumentException( __( 'Invalid action - must be a recurring action.', 'action-scheduler' ) ); } $schedule_class = get_class( $schedule ); $new_schedule = new $schedule( $next, $schedule->get_recurrence(), $schedule->get_first_date() ); $new_action = new ActionScheduler_Action( $action->get_hook(), $action->get_args(), $new_schedule, $action->get_group() ); $new_action->set_priority( $action->get_priority() ); return $this->store( $new_action ); } /** * Creates a scheduled action. * * This general purpose method can be used in place of specific methods such as async(), * async_unique(), single() or single_unique(), etc. * * @internal Not intended for public use, should not be overridden by subclasses. * * @param array $options { * Describes the action we wish to schedule. * * @type string $type Must be one of 'async', 'cron', 'recurring', or 'single'. * @type string $hook The hook to be executed. * @type array $arguments Arguments to be passed to the callback. * @type string $group The action group. * @type bool $unique If the action should be unique. * @type int $when Timestamp. Indicates when the action, or first instance of the action in the case * of recurring or cron actions, becomes due. * @type int|string $pattern Recurrence pattern. This is either an interval in seconds for recurring actions * or a cron expression for cron actions. * @type int $priority Lower values means higher priority. Should be in the range 0-255. * } * * @return int The action ID. Zero if there was an error scheduling the action. */ public function create( array $options = array() ) { $defaults = array( 'type' => 'single', 'hook' => '', 'arguments' => array(), 'group' => '', 'unique' => false, 'when' => time(), 'pattern' => null, 'priority' => 10, ); $options = array_merge( $defaults, $options ); // Cron/recurring actions without a pattern are treated as single actions (this gives calling code the ability // to use functions like as_schedule_recurring_action() to schedule recurring as well as single actions). if ( ( 'cron' === $options['type'] || 'recurring' === $options['type'] ) && empty( $options['pattern'] ) ) { $options['type'] = 'single'; } switch ( $options['type'] ) { case 'async': $schedule = new ActionScheduler_NullSchedule(); break; case 'cron': $date = as_get_datetime_object( $options['when'] ); $cron = CronExpression::factory( $options['pattern'] ); $schedule = new ActionScheduler_CronSchedule( $date, $cron ); break; case 'recurring': $date = as_get_datetime_object( $options['when'] ); $schedule = new ActionScheduler_IntervalSchedule( $date, $options['pattern'] ); break; case 'single': $date = as_get_datetime_object( $options['when'] ); $schedule = new ActionScheduler_SimpleSchedule( $date ); break; default: // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( "Unknown action type '{$options['type']}' specified when trying to create an action for '{$options['hook']}'." ); return 0; } $action = new ActionScheduler_Action( $options['hook'], $options['arguments'], $schedule, $options['group'] ); $action->set_priority( $options['priority'] ); $action_id = 0; try { $action_id = $options['unique'] ? $this->store_unique_action( $action ) : $this->store( $action ); } catch ( Exception $e ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( sprintf( /* translators: %1$s is the name of the hook to be enqueued, %2$s is the exception message. */ __( 'Caught exception while enqueuing action "%1$s": %2$s', 'action-scheduler' ), $options['hook'], $e->getMessage() ) ); } return $action_id; } /** * Save action to database. * * @param ActionScheduler_Action $action Action object to save. * * @return int The ID of the stored action */ protected function store( ActionScheduler_Action $action ) { $store = ActionScheduler_Store::instance(); return $store->save_action( $action ); } /** * Store action if it's unique. * * @param ActionScheduler_Action $action Action object to store. * * @return int ID of the created action. Will be 0 if action was not created. */ protected function store_unique_action( ActionScheduler_Action $action ) { $store = ActionScheduler_Store::instance(); if ( method_exists( $store, 'save_unique_action' ) ) { return $store->save_unique_action( $action ); } else { /** * Fallback to non-unique action if the store doesn't support unique actions. * We try to save the action as unique, accepting that there might be a race condition. * This is likely still better than giving up on unique actions entirely. */ $existing_action_id = (int) $store->find_action( $action->get_hook(), array( 'args' => $action->get_args(), 'status' => ActionScheduler_Store::STATUS_PENDING, 'group' => $action->get_group(), ) ); if ( $existing_action_id > 0 ) { return 0; } return $store->save_action( $action ); } } } Dependencies/ActionScheduler/classes/ActionScheduler_Exception.php 0000644 00000000317 15174671745 0021470 0 ustar 00 <?php /** * ActionScheduler Exception Interface. * * Facilitates catching Exceptions unique to Action Scheduler. * * @package ActionScheduler * @since 2.1.0 */ interface ActionScheduler_Exception {} Dependencies/ActionScheduler/classes/ActionScheduler_WPCommentCleaner.php 0000644 00000010560 15174671745 0022676 0 ustar 00 <?php /** * Class ActionScheduler_WPCommentCleaner * * @since 3.0.0 */ class ActionScheduler_WPCommentCleaner { /** * Post migration hook used to cleanup the WP comment table. * * @var string */ protected static $cleanup_hook = 'action_scheduler/cleanup_wp_comment_logs'; /** * An instance of the ActionScheduler_wpCommentLogger class to interact with the comments table. * * This instance should only be used as an interface. It should not be initialized. * * @var ActionScheduler_wpCommentLogger */ protected static $wp_comment_logger = null; /** * The key used to store the cached value of whether there are logs in the WP comment table. * * @var string */ protected static $has_logs_option_key = 'as_has_wp_comment_logs'; /** * Initialize the class and attach callbacks. */ public static function init() { if ( empty( self::$wp_comment_logger ) ) { self::$wp_comment_logger = new ActionScheduler_wpCommentLogger(); } add_action( self::$cleanup_hook, array( __CLASS__, 'delete_all_action_comments' ) ); // While there are orphaned logs left in the comments table, we need to attach the callbacks which filter comment counts. add_action( 'pre_get_comments', array( self::$wp_comment_logger, 'filter_comment_queries' ), 10, 1 ); add_action( 'wp_count_comments', array( self::$wp_comment_logger, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs. add_action( 'comment_feed_where', array( self::$wp_comment_logger, 'filter_comment_feed' ), 10, 2 ); // Action Scheduler may be displayed as a Tools screen or WooCommerce > Status administration screen. add_action( 'load-tools_page_action-scheduler', array( __CLASS__, 'register_admin_notice' ) ); add_action( 'load-woocommerce_page_wc-status', array( __CLASS__, 'register_admin_notice' ) ); } /** * Determines if there are log entries in the wp comments table. * * Uses the flag set on migration completion set by @see self::maybe_schedule_cleanup(). * * @return boolean Whether there are scheduled action comments in the comments table. */ public static function has_logs() { return 'yes' === get_option( self::$has_logs_option_key ); } /** * Schedules the WP Post comment table cleanup to run in 6 months if it's not already scheduled. * Attached to the migration complete hook 'action_scheduler/migration_complete'. */ public static function maybe_schedule_cleanup() { $args = array( 'type' => ActionScheduler_wpCommentLogger::TYPE, 'number' => 1, 'fields' => 'ids', ); if ( (bool) get_comments( $args ) ) { update_option( self::$has_logs_option_key, 'yes' ); if ( ! as_next_scheduled_action( self::$cleanup_hook ) ) { as_schedule_single_action( gmdate( 'U' ) + ( 6 * MONTH_IN_SECONDS ), self::$cleanup_hook ); } } } /** * Delete all action comments from the WP Comments table. */ public static function delete_all_action_comments() { global $wpdb; $wpdb->delete( $wpdb->comments, array( 'comment_type' => ActionScheduler_wpCommentLogger::TYPE, 'comment_agent' => ActionScheduler_wpCommentLogger::AGENT, ) ); delete_option( self::$has_logs_option_key ); } /** * Registers admin notices about the orphaned action logs. */ public static function register_admin_notice() { add_action( 'admin_notices', array( __CLASS__, 'print_admin_notice' ) ); } /** * Prints details about the orphaned action logs and includes information on where to learn more. */ public static function print_admin_notice() { $next_cleanup_message = ''; $next_scheduled_cleanup_hook = as_next_scheduled_action( self::$cleanup_hook ); if ( $next_scheduled_cleanup_hook ) { /* translators: %s: date interval */ $next_cleanup_message = sprintf( __( 'This data will be deleted in %s.', 'action-scheduler' ), human_time_diff( gmdate( 'U' ), $next_scheduled_cleanup_hook ) ); } $notice = sprintf( /* translators: 1: next cleanup message 2: github issue URL */ __( 'Action Scheduler has migrated data to custom tables; however, orphaned log entries exist in the WordPress Comments table. %1$s <a href="%2$s">Learn more »</a>', 'action-scheduler' ), $next_cleanup_message, 'https://github.com/woocommerce/action-scheduler/issues/368' ); echo '<div class="notice notice-warning"><p>' . wp_kses_post( $notice ) . '</p></div>'; } } Dependencies/ActionScheduler/classes/ActionScheduler_NullLogEntry.php 0000644 00000000512 15174671745 0022125 0 ustar 00 <?php /** * Class ActionScheduler_NullLogEntry */ class ActionScheduler_NullLogEntry extends ActionScheduler_LogEntry { /** * Construct. * * @param string $action_id Action ID. * @param string $message Log entry. */ public function __construct( $action_id = '', $message = '' ) { // nothing to see here. } } Dependencies/ActionScheduler/classes/ActionScheduler_DataController.php 0000644 00000012445 15174671745 0022454 0 ustar 00 <?php use Action_Scheduler\Migration\Controller; /** * Class ActionScheduler_DataController * * The main plugin/initialization class for the data stores. * * Responsible for hooking everything up with WordPress. * * @package Action_Scheduler * * @since 3.0.0 */ class ActionScheduler_DataController { /** Action data store class name. */ const DATASTORE_CLASS = 'ActionScheduler_DBStore'; /** Logger data store class name. */ const LOGGER_CLASS = 'ActionScheduler_DBLogger'; /** Migration status option name. */ const STATUS_FLAG = 'action_scheduler_migration_status'; /** Migration status option value. */ const STATUS_COMPLETE = 'complete'; /** Migration minimum required PHP version. */ const MIN_PHP_VERSION = '5.5'; /** * Instance. * * @var ActionScheduler_DataController */ private static $instance; /** * Sleep time in seconds. * * @var int */ private static $sleep_time = 0; /** * Tick count required for freeing memory. * * @var int */ private static $free_ticks = 50; /** * Get a flag indicating whether the migration environment dependencies are met. * * @return bool */ public static function dependencies_met() { $php_support = version_compare( PHP_VERSION, self::MIN_PHP_VERSION, '>=' ); return $php_support && apply_filters( 'action_scheduler_migration_dependencies_met', true ); } /** * Get a flag indicating whether the migration is complete. * * @return bool Whether the flag has been set marking the migration as complete */ public static function is_migration_complete() { return get_option( self::STATUS_FLAG ) === self::STATUS_COMPLETE; } /** * Mark the migration as complete. */ public static function mark_migration_complete() { update_option( self::STATUS_FLAG, self::STATUS_COMPLETE ); } /** * Unmark migration when a plugin is de-activated. Will not work in case of silent activation, for example in an update. * We do this to mitigate the bug of lost actions which happens if there was an AS 2.x to AS 3.x migration in the past, but that plugin is now * deactivated and the site was running on AS 2.x again. */ public static function mark_migration_incomplete() { delete_option( self::STATUS_FLAG ); } /** * Set the action store class name. * * @param string $class Classname of the store class. * * @return string */ public static function set_store_class( $class ) { return self::DATASTORE_CLASS; } /** * Set the action logger class name. * * @param string $class Classname of the logger class. * * @return string */ public static function set_logger_class( $class ) { return self::LOGGER_CLASS; } /** * Set the sleep time in seconds. * * @param integer $sleep_time The number of seconds to pause before resuming operation. */ public static function set_sleep_time( $sleep_time ) { self::$sleep_time = (int) $sleep_time; } /** * Set the tick count required for freeing memory. * * @param integer $free_ticks The number of ticks to free memory on. */ public static function set_free_ticks( $free_ticks ) { self::$free_ticks = (int) $free_ticks; } /** * Free memory if conditions are met. * * @param int $ticks Current tick count. */ public static function maybe_free_memory( $ticks ) { if ( self::$free_ticks && 0 === $ticks % self::$free_ticks ) { self::free_memory(); } } /** * Reduce memory footprint by clearing the database query and object caches. */ public static function free_memory() { if ( 0 < self::$sleep_time ) { /* translators: %d: amount of time */ \WP_CLI::warning( sprintf( _n( 'Stopped the insanity for %d second', 'Stopped the insanity for %d seconds', self::$sleep_time, 'action-scheduler' ), self::$sleep_time ) ); sleep( self::$sleep_time ); } \WP_CLI::warning( __( 'Attempting to reduce used memory...', 'action-scheduler' ) ); /** * Globals. * * @var $wpdb \wpdb * @var $wp_object_cache \WP_Object_Cache */ global $wpdb, $wp_object_cache; $wpdb->queries = array(); if ( ! is_a( $wp_object_cache, 'WP_Object_Cache' ) ) { return; } $wp_object_cache->group_ops = array(); $wp_object_cache->stats = array(); $wp_object_cache->memcache_debug = array(); $wp_object_cache->cache = array(); if ( is_callable( array( $wp_object_cache, '__remoteset' ) ) ) { call_user_func( array( $wp_object_cache, '__remoteset' ) ); // important! } } /** * Connect to table datastores if migration is complete. * Otherwise, proceed with the migration if the dependencies have been met. */ public static function init() { if ( self::is_migration_complete() ) { add_filter( 'action_scheduler_store_class', array( 'ActionScheduler_DataController', 'set_store_class' ), 100 ); add_filter( 'action_scheduler_logger_class', array( 'ActionScheduler_DataController', 'set_logger_class' ), 100 ); add_action( 'deactivate_plugin', array( 'ActionScheduler_DataController', 'mark_migration_incomplete' ) ); } elseif ( self::dependencies_met() ) { Controller::init(); } add_action( 'action_scheduler/progress_tick', array( 'ActionScheduler_DataController', 'maybe_free_memory' ) ); } /** * Singleton factory. */ public static function instance() { if ( ! isset( self::$instance ) ) { self::$instance = new static(); } return self::$instance; } } Dependencies/ActionScheduler/classes/ActionScheduler_ActionClaim.php 0000644 00000001214 15174671745 0021712 0 ustar 00 <?php /** * Class ActionScheduler_ActionClaim */ class ActionScheduler_ActionClaim { /** * Claim ID. * * @var string */ private $id = ''; /** * Claimed action IDs. * * @var int[] */ private $action_ids = array(); /** * Construct. * * @param string $id Claim ID. * @param int[] $action_ids Action IDs. */ public function __construct( $id, array $action_ids ) { $this->id = $id; $this->action_ids = $action_ids; } /** * Get claim ID. */ public function get_id() { return $this->id; } /** * Get IDs of claimed actions. */ public function get_actions() { return $this->action_ids; } } Dependencies/ActionScheduler/classes/ActionScheduler_Versions.php 0000644 00000003351 15174671745 0021343 0 ustar 00 <?php /** * Class ActionScheduler_Versions */ class ActionScheduler_Versions { /** * ActionScheduler_Versions instance. * * @var ActionScheduler_Versions */ private static $instance = null; /** * Versions. * * @var array<string, callable> */ private $versions = array(); /** * Register version's callback. * * @param string $version_string Action Scheduler version. * @param callable $initialization_callback Callback to initialize the version. */ public function register( $version_string, $initialization_callback ) { if ( isset( $this->versions[ $version_string ] ) ) { return false; } $this->versions[ $version_string ] = $initialization_callback; return true; } /** * Get all versions. */ public function get_versions() { return $this->versions; } /** * Get latest version registered. */ public function latest_version() { $keys = array_keys( $this->versions ); if ( empty( $keys ) ) { return false; } uasort( $keys, 'version_compare' ); return end( $keys ); } /** * Get callback for latest registered version. */ public function latest_version_callback() { $latest = $this->latest_version(); if ( empty( $latest ) || ! isset( $this->versions[ $latest ] ) ) { return '__return_null'; } return $this->versions[ $latest ]; } /** * Get instance. * * @return ActionScheduler_Versions * @codeCoverageIgnore */ public static function instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Initialize. * * @codeCoverageIgnore */ public static function initialize_latest_version() { $self = self::instance(); call_user_func( $self->latest_version_callback() ); } } Dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php 0000644 00000052166 15174671745 0021426 0 ustar 00 <?php /** * Implements the admin view of the actions. * * @codeCoverageIgnore */ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable { /** * The package name. * * @var string */ protected $package = 'action-scheduler'; /** * Columns to show (name => label). * * @var array */ protected $columns = array(); /** * Actions (name => label). * * @var array */ protected $row_actions = array(); /** * The active data stores * * @var ActionScheduler_Store */ protected $store; /** * A logger to use for getting action logs to display * * @var ActionScheduler_Logger */ protected $logger; /** * A ActionScheduler_QueueRunner runner instance (or child class) * * @var ActionScheduler_QueueRunner */ protected $runner; /** * Bulk actions. The key of the array is the method name of the implementation. * Example: bulk_<key>(array $ids, string $sql_in). * * See the comments in the parent class for further details * * @var array */ protected $bulk_actions = array(); /** * Flag variable to render our notifications, if any, once. * * @var bool */ protected static $did_notification = false; /** * Array of seconds for common time periods, like week or month, alongside an internationalised string representation, i.e. "Day" or "Days" * * @var array */ private static $time_periods; /** * Sets the current data store object into `store->action` and initialises the object. * * @param ActionScheduler_Store $store Store object. * @param ActionScheduler_Logger $logger Logger object. * @param ActionScheduler_QueueRunner $runner Runner object. */ public function __construct( ActionScheduler_Store $store, ActionScheduler_Logger $logger, ActionScheduler_QueueRunner $runner ) { $this->store = $store; $this->logger = $logger; $this->runner = $runner; $this->table_header = __( 'Scheduled Actions', 'action-scheduler' ); $this->bulk_actions = array( 'delete' => __( 'Delete', 'action-scheduler' ), ); $this->columns = array( 'hook' => __( 'Hook', 'action-scheduler' ), 'status' => __( 'Status', 'action-scheduler' ), 'args' => __( 'Arguments', 'action-scheduler' ), 'group' => __( 'Group', 'action-scheduler' ), 'recurrence' => __( 'Recurrence', 'action-scheduler' ), 'schedule' => __( 'Scheduled Date', 'action-scheduler' ), 'log_entries' => __( 'Log', 'action-scheduler' ), ); $this->sort_by = array( 'schedule', 'hook', 'group', ); $this->search_by = array( 'hook', 'args', 'claim_id', ); $request_status = $this->get_request_status(); if ( empty( $request_status ) ) { $this->sort_by[] = 'status'; } elseif ( in_array( $request_status, array( 'in-progress', 'failed' ), true ) ) { $this->columns += array( 'claim_id' => __( 'Claim ID', 'action-scheduler' ) ); $this->sort_by[] = 'claim_id'; } $this->row_actions = array( 'hook' => array( 'run' => array( 'name' => __( 'Run', 'action-scheduler' ), 'desc' => __( 'Process the action now as if it were run as part of a queue', 'action-scheduler' ), ), 'cancel' => array( 'name' => __( 'Cancel', 'action-scheduler' ), 'desc' => __( 'Cancel the action now to avoid it being run in future', 'action-scheduler' ), 'class' => 'cancel trash', ), ), ); self::$time_periods = array( array( 'seconds' => YEAR_IN_SECONDS, /* translators: %s: amount of time */ 'names' => _n_noop( '%s year', '%s years', 'action-scheduler' ), ), array( 'seconds' => MONTH_IN_SECONDS, /* translators: %s: amount of time */ 'names' => _n_noop( '%s month', '%s months', 'action-scheduler' ), ), array( 'seconds' => WEEK_IN_SECONDS, /* translators: %s: amount of time */ 'names' => _n_noop( '%s week', '%s weeks', 'action-scheduler' ), ), array( 'seconds' => DAY_IN_SECONDS, /* translators: %s: amount of time */ 'names' => _n_noop( '%s day', '%s days', 'action-scheduler' ), ), array( 'seconds' => HOUR_IN_SECONDS, /* translators: %s: amount of time */ 'names' => _n_noop( '%s hour', '%s hours', 'action-scheduler' ), ), array( 'seconds' => MINUTE_IN_SECONDS, /* translators: %s: amount of time */ 'names' => _n_noop( '%s minute', '%s minutes', 'action-scheduler' ), ), array( 'seconds' => 1, /* translators: %s: amount of time */ 'names' => _n_noop( '%s second', '%s seconds', 'action-scheduler' ), ), ); parent::__construct( array( 'singular' => 'action-scheduler', 'plural' => 'action-scheduler', 'ajax' => false, ) ); add_screen_option( 'per_page', array( 'default' => $this->items_per_page, ) ); add_filter( 'set_screen_option_' . $this->get_per_page_option_name(), array( $this, 'set_items_per_page_option' ), 10, 3 ); set_screen_options(); } /** * Handles setting the items_per_page option for this screen. * * @param mixed $status Default false (to skip saving the current option). * @param string $option Screen option name. * @param int $value Screen option value. * @return int */ public function set_items_per_page_option( $status, $option, $value ) { return $value; } /** * Convert an interval of seconds into a two part human friendly string. * * The WordPress human_time_diff() function only calculates the time difference to one degree, meaning * even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step * further to display two degrees of accuracy. * * Inspired by the Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/ * * @param int $interval A interval in seconds. * @param int $periods_to_include Depth of time periods to include, e.g. for an interval of 70, and $periods_to_include of 2, both minutes and seconds would be included. With a value of 1, only minutes would be included. * @return string A human friendly string representation of the interval. */ private static function human_interval( $interval, $periods_to_include = 2 ) { if ( $interval <= 0 ) { return __( 'Now!', 'action-scheduler' ); } $output = ''; $num_time_periods = count( self::$time_periods ); for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < $num_time_periods && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) { $periods_in_interval = floor( $seconds_remaining / self::$time_periods[ $time_period_index ]['seconds'] ); if ( $periods_in_interval > 0 ) { if ( ! empty( $output ) ) { $output .= ' '; } $output .= sprintf( translate_nooped_plural( self::$time_periods[ $time_period_index ]['names'], $periods_in_interval, 'action-scheduler' ), $periods_in_interval ); $seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds']; $periods_included++; } } return $output; } /** * Returns the recurrence of an action or 'Non-repeating'. The output is human readable. * * @param ActionScheduler_Action $action Action object. * * @return string */ protected function get_recurrence( $action ) { $schedule = $action->get_schedule(); if ( $schedule->is_recurring() && method_exists( $schedule, 'get_recurrence' ) ) { $recurrence = $schedule->get_recurrence(); if ( is_numeric( $recurrence ) ) { /* translators: %s: time interval */ return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence ) ); } else { return $recurrence; } } return __( 'Non-repeating', 'action-scheduler' ); } /** * Serializes the argument of an action to render it in a human friendly format. * * @param array $row The array representation of the current row of the table. * * @return string */ public function column_args( array $row ) { if ( empty( $row['args'] ) ) { return apply_filters( 'action_scheduler_list_table_column_args', '', $row ); } $row_html = '<ul>'; foreach ( $row['args'] as $key => $value ) { $row_html .= sprintf( '<li><code>%s => %s</code></li>', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export } $row_html .= '</ul>'; return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row ); } /** * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal. * * @param array $row Action array. * @return string */ public function column_log_entries( array $row ) { $log_entries_html = '<ol>'; $timezone = new DateTimezone( 'UTC' ); foreach ( $row['log_entries'] as $log_entry ) { $log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone ); } $log_entries_html .= '</ol>'; return $log_entries_html; } /** * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal. * * @param ActionScheduler_LogEntry $log_entry Log entry object. * @param DateTimezone $timezone Timestamp. * @return string */ protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) { $date = $log_entry->get_date(); $date->setTimezone( $timezone ); return sprintf( '<li><strong>%s</strong><br/>%s</li>', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) ); } /** * Only display row actions for pending actions. * * @param array $row Row to render. * @param string $column_name Current row. * * @return string */ protected function maybe_render_actions( $row, $column_name ) { if ( 'pending' === strtolower( $row['status_name'] ) ) { return parent::maybe_render_actions( $row, $column_name ); } return ''; } /** * Renders admin notifications * * Notifications: * 1. When the maximum number of tasks are being executed simultaneously. * 2. Notifications when a task is manually executed. * 3. Tables are missing. */ public function display_admin_notices() { global $wpdb; if ( ( is_a( $this->store, 'ActionScheduler_HybridStore' ) || is_a( $this->store, 'ActionScheduler_DBStore' ) ) && apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) { $table_list = array( 'actionscheduler_actions', 'actionscheduler_logs', 'actionscheduler_groups', 'actionscheduler_claims', ); $found_tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared foreach ( $table_list as $table_name ) { if ( ! in_array( $wpdb->prefix . $table_name, $found_tables, true ) ) { $this->admin_notices[] = array( 'class' => 'error', 'message' => __( 'It appears one or more database tables were missing. Attempting to re-create the missing table(s).', 'action-scheduler' ), ); $this->recreate_tables(); parent::display_admin_notices(); return; } } } if ( $this->runner->has_maximum_concurrent_batches() ) { $claim_count = $this->store->get_claim_count(); $this->admin_notices[] = array( 'class' => 'updated', 'message' => sprintf( /* translators: %s: amount of claims */ _n( 'Maximum simultaneous queues already in progress (%s queue). No additional queues will begin processing until the current queues are complete.', 'Maximum simultaneous queues already in progress (%s queues). No additional queues will begin processing until the current queues are complete.', $claim_count, 'action-scheduler' ), $claim_count ), ); } elseif ( $this->store->has_pending_actions_due() ) { $async_request_lock_expiration = ActionScheduler::lock()->get_expiration( 'async-request-runner' ); // No lock set or lock expired. if ( false === $async_request_lock_expiration || $async_request_lock_expiration < time() ) { $in_progress_url = add_query_arg( 'status', 'in-progress', remove_query_arg( 'status' ) ); /* translators: %s: process URL */ $async_request_message = sprintf( __( 'A new queue has begun processing. <a href="%s">View actions in-progress »</a>', 'action-scheduler' ), esc_url( $in_progress_url ) ); } else { /* translators: %d: seconds */ $async_request_message = sprintf( __( 'The next queue will begin processing in approximately %d seconds.', 'action-scheduler' ), $async_request_lock_expiration - time() ); } $this->admin_notices[] = array( 'class' => 'notice notice-info', 'message' => $async_request_message, ); } $notification = get_transient( 'action_scheduler_admin_notice' ); if ( is_array( $notification ) ) { delete_transient( 'action_scheduler_admin_notice' ); $action = $this->store->fetch_action( $notification['action_id'] ); $action_hook_html = '<strong><code>' . $action->get_hook() . '</code></strong>'; if ( 1 === absint( $notification['success'] ) ) { $class = 'updated'; switch ( $notification['row_action_type'] ) { case 'run': /* translators: %s: action HTML */ $action_message_html = sprintf( __( 'Successfully executed action: %s', 'action-scheduler' ), $action_hook_html ); break; case 'cancel': /* translators: %s: action HTML */ $action_message_html = sprintf( __( 'Successfully canceled action: %s', 'action-scheduler' ), $action_hook_html ); break; default: /* translators: %s: action HTML */ $action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'action-scheduler' ), $action_hook_html ); break; } } else { $class = 'error'; /* translators: 1: action HTML 2: action ID 3: error message */ $action_message_html = sprintf( __( 'Could not process change for action: "%1$s" (ID: %2$d). Error: %3$s', 'action-scheduler' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) ); } $action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification ); $this->admin_notices[] = array( 'class' => $class, 'message' => $action_message_html, ); } parent::display_admin_notices(); } /** * Prints the scheduled date in a human friendly format. * * @param array $row The array representation of the current row of the table. * * @return string */ public function column_schedule( $row ) { return $this->get_schedule_display_string( $row['schedule'] ); } /** * Get the scheduled date in a human friendly format. * * @param ActionScheduler_Schedule $schedule Action's schedule. * @return string */ protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) { $schedule_display_string = ''; if ( is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) { return __( 'async', 'action-scheduler' ); } if ( ! method_exists( $schedule, 'get_date' ) || ! $schedule->get_date() ) { return '0000-00-00 00:00:00'; } $next_timestamp = $schedule->get_date()->getTimestamp(); $schedule_display_string .= $schedule->get_date()->format( 'Y-m-d H:i:s O' ); $schedule_display_string .= '<br/>'; if ( gmdate( 'U' ) > $next_timestamp ) { /* translators: %s: date interval */ $schedule_display_string .= sprintf( __( ' (%s ago)', 'action-scheduler' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) ); } else { /* translators: %s: date interval */ $schedule_display_string .= sprintf( __( ' (%s)', 'action-scheduler' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) ); } return $schedule_display_string; } /** * Bulk delete. * * Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data * properly validated by the callee and it will delete the actions without any extra validation. * * @param int[] $ids Action IDs. * @param string $ids_sql Inherited and unused. */ protected function bulk_delete( array $ids, $ids_sql ) { foreach ( $ids as $id ) { try { $this->store->delete_action( $id ); } catch ( Exception $e ) { // A possible reason for an exception would include a scenario where the same action is deleted by a // concurrent request. // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( sprintf( /* translators: 1: action ID 2: exception message. */ __( 'Action Scheduler was unable to delete action %1$d. Reason: %2$s', 'action-scheduler' ), $id, $e->getMessage() ) ); } } } /** * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their * parameters are valid. * * @param int $action_id Action ID. */ protected function row_action_cancel( $action_id ) { $this->process_row_action( $action_id, 'cancel' ); } /** * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their * parameters are valid. * * @param int $action_id Action ID. */ protected function row_action_run( $action_id ) { $this->process_row_action( $action_id, 'run' ); } /** * Force the data store schema updates. */ protected function recreate_tables() { if ( is_a( $this->store, 'ActionScheduler_HybridStore' ) ) { $store = $this->store; } else { $store = new ActionScheduler_HybridStore(); } add_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10, 2 ); $store_schema = new ActionScheduler_StoreSchema(); $logger_schema = new ActionScheduler_LoggerSchema(); $store_schema->register_tables( true ); $logger_schema->register_tables( true ); remove_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10 ); } /** * Implements the logic behind processing an action once an action link is clicked on the list table. * * @param int $action_id Action ID. * @param string $row_action_type The type of action to perform on the action. */ protected function process_row_action( $action_id, $row_action_type ) { try { switch ( $row_action_type ) { case 'run': $this->runner->process_action( $action_id, 'Admin List Table' ); break; case 'cancel': $this->store->cancel_action( $action_id ); break; } $success = 1; $error_message = ''; } catch ( Exception $e ) { $success = 0; $error_message = $e->getMessage(); } set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 ); } /** * {@inheritDoc} */ public function prepare_items() { $this->prepare_column_headers(); $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page ); $query = array( 'per_page' => $per_page, 'offset' => $this->get_items_offset(), 'status' => $this->get_request_status(), 'orderby' => $this->get_request_orderby(), 'order' => $this->get_request_order(), 'search' => $this->get_request_search_query(), ); /** * Change query arguments to query for past-due actions. * Past-due actions have the 'pending' status and are in the past. * This is needed because registering 'past-due' as a status is overkill. */ if ( 'past-due' === $this->get_request_status() ) { $query['status'] = ActionScheduler_Store::STATUS_PENDING; $query['date'] = as_get_datetime_object(); } $this->items = array(); $total_items = $this->store->query_actions( $query, 'count' ); $status_labels = $this->store->get_status_labels(); foreach ( $this->store->query_actions( $query ) as $action_id ) { try { $action = $this->store->fetch_action( $action_id ); } catch ( Exception $e ) { continue; } if ( is_a( $action, 'ActionScheduler_NullAction' ) ) { continue; } $this->items[ $action_id ] = array( 'ID' => $action_id, 'hook' => $action->get_hook(), 'status_name' => $this->store->get_status( $action_id ), 'status' => $status_labels[ $this->store->get_status( $action_id ) ], 'args' => $action->get_args(), 'group' => $action->get_group(), 'log_entries' => $this->logger->get_logs( $action_id ), 'claim_id' => $this->store->get_claim_id( $action_id ), 'recurrence' => $this->get_recurrence( $action ), 'schedule' => $action->get_schedule(), ); } $this->set_pagination_args( array( 'total_items' => $total_items, 'per_page' => $per_page, 'total_pages' => ceil( $total_items / $per_page ), ) ); } /** * Prints the available statuses so the user can click to filter. */ protected function display_filter_by_status() { $this->status_counts = $this->store->action_counts() + $this->store->extra_action_counts(); parent::display_filter_by_status(); } /** * Get the text to display in the search box on the list table. */ protected function get_search_box_button_text() { return __( 'Search hook, args and claim ID', 'action-scheduler' ); } /** * {@inheritDoc} */ protected function get_per_page_option_name() { return str_replace( '-', '_', $this->screen->id ) . '_per_page'; } } Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php 0000644 00000007363 15174671745 0024175 0 ustar 00 <?php /** * Commands for Action Scheduler. */ class ActionScheduler_WPCLI_Clean_Command extends WP_CLI_Command { /** * Run the Action Scheduler Queue Cleaner * * ## OPTIONS * * [--batch-size=<size>] * : The maximum number of actions to delete per batch. Defaults to 20. * * [--batches=<size>] * : Limit execution to a number of batches. Defaults to 0, meaning batches will continue all eligible actions are deleted. * * [--status=<status>] * : Only clean actions with the specified status. Defaults to Canceled, Completed. Define multiple statuses as a comma separated string (without spaces), e.g. `--status=complete,failed,canceled` * * [--before=<datestring>] * : Only delete actions with scheduled date older than this. Defaults to 31 days. e.g `--before='7 days ago'`, `--before='02-Feb-2020 20:20:20'` * * [--pause=<seconds>] * : The number of seconds to pause between batches. Default no pause. * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @throws \WP_CLI\ExitException When an error occurs. * * @subcommand clean */ public function clean( $args, $assoc_args ) { // Handle passed arguments. $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 20 ) ); $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); $status = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'status', '' ) ); $status = array_filter( array_map( 'trim', $status ) ); $before = \WP_CLI\Utils\get_flag_value( $assoc_args, 'before', '' ); $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); $batches_completed = 0; $actions_deleted = 0; $unlimited = 0 === $batches; try { $lifespan = as_get_datetime_object( $before ); } catch ( Exception $e ) { $lifespan = null; } try { // Custom queue cleaner instance. $cleaner = new ActionScheduler_QueueCleaner( null, $batch ); // Clean actions for as long as possible. while ( $unlimited || $batches_completed < $batches ) { if ( $sleep && $batches_completed > 0 ) { sleep( $sleep ); } $deleted = count( $cleaner->clean_actions( $status, $lifespan, null, 'CLI' ) ); if ( $deleted <= 0 ) { break; } $actions_deleted += $deleted; $batches_completed++; $this->print_success( $deleted ); } } catch ( Exception $e ) { $this->print_error( $e ); } $this->print_total_batches( $batches_completed ); if ( $batches_completed > 1 ) { $this->print_success( $actions_deleted ); } } /** * Print WP CLI message about how many batches of actions were processed. * * @param int $batches_processed Number of batches processed. */ protected function print_total_batches( int $batches_processed ) { WP_CLI::log( sprintf( /* translators: %d refers to the total number of batches processed */ _n( '%d batch processed.', '%d batches processed.', $batches_processed, 'action-scheduler' ), $batches_processed ) ); } /** * Convert an exception into a WP CLI error. * * @param Exception $e The error object. */ protected function print_error( Exception $e ) { WP_CLI::error( sprintf( /* translators: %s refers to the exception error message */ __( 'There was an error deleting an action: %s', 'action-scheduler' ), $e->getMessage() ) ); } /** * Print a success message with the number of completed actions. * * @param int $actions_deleted Number of deleted actions. */ protected function print_success( int $actions_deleted ) { WP_CLI::success( sprintf( /* translators: %d refers to the total number of actions deleted */ _n( '%d action deleted.', '%d actions deleted.', $actions_deleted, 'action-scheduler' ), $actions_deleted ) ); } } Dependencies/ActionScheduler/classes/WP_CLI/ProgressBar.php 0000644 00000005316 15174671745 0017650 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI; /** * WP_CLI progress bar for Action Scheduler. */ /** * Class ProgressBar * * @package Action_Scheduler\WP_CLI * * @since 3.0.0 * * @codeCoverageIgnore */ class ProgressBar { /** * Current number of ticks. * * @var integer */ protected $total_ticks; /** * Total number of ticks. * * @var integer */ protected $count; /** * Progress bar update interval. * * @var integer */ protected $interval; /** * Progress bar message. * * @var string */ protected $message; /** * Instance. * * @var \cli\progress\Bar */ protected $progress_bar; /** * ProgressBar constructor. * * @param string $message Text to display before the progress bar. * @param integer $count Total number of ticks to be performed. * @param integer $interval Optional. The interval in milliseconds between updates. Default 100. * * @throws \Exception When this is not run within WP CLI. */ public function __construct( $message, $count, $interval = 100 ) { if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) { /* translators: %s php class name */ throw new \Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), __CLASS__ ) ); } $this->total_ticks = 0; $this->message = $message; $this->count = $count; $this->interval = $interval; } /** * Increment the progress bar ticks. */ public function tick() { if ( null === $this->progress_bar ) { $this->setup_progress_bar(); } $this->progress_bar->tick(); $this->total_ticks++; do_action( 'action_scheduler/progress_tick', $this->total_ticks ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } /** * Get the progress bar tick count. * * @return int */ public function current() { return $this->progress_bar ? $this->progress_bar->current() : 0; } /** * Finish the current progress bar. */ public function finish() { if ( null !== $this->progress_bar ) { $this->progress_bar->finish(); } $this->progress_bar = null; } /** * Set the message used when creating the progress bar. * * @param string $message The message to be used when the next progress bar is created. */ public function set_message( $message ) { $this->message = $message; } /** * Set the count for a new progress bar. * * @param integer $count The total number of ticks expected to complete. */ public function set_count( $count ) { $this->count = $count; $this->finish(); } /** * Set up the progress bar. */ protected function setup_progress_bar() { $this->progress_bar = \WP_CLI\Utils\make_progress_bar( $this->message, $this->count, $this->interval ); } } Dependencies/ActionScheduler/classes/WP_CLI/System_Command.php 0000644 00000016135 15174671745 0020342 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping output is not necessary in WP CLI. use ActionScheduler_SystemInformation; use WP_CLI; use function \WP_CLI\Utils\get_flag_value; /** * System info WP-CLI commands for Action Scheduler. */ class System_Command { /** * Data store for querying actions * * @var ActionScheduler_Store */ protected $store; /** * Construct. */ public function __construct() { $this->store = \ActionScheduler::store(); } /** * Print in-use data store class. * * @param array $args Positional args. * @param array $assoc_args Keyed args. * @return void * * @subcommand data-store */ public function datastore( array $args, array $assoc_args ) { echo $this->get_current_datastore(); } /** * Print in-use runner class. * * @param array $args Positional args. * @param array $assoc_args Keyed args. * @return void */ public function runner( array $args, array $assoc_args ) { echo $this->get_current_runner(); } /** * Get system status. * * @param array $args Positional args. * @param array $assoc_args Keyed args. * @return void */ public function status( array $args, array $assoc_args ) { /** * Get runner status. * * @link https://github.com/woocommerce/action-scheduler-disable-default-runner */ $runner_enabled = has_action( 'action_scheduler_run_queue', array( \ActionScheduler::runner(), 'run' ) ); \WP_CLI::line( sprintf( 'Data store: %s', $this->get_current_datastore() ) ); \WP_CLI::line( sprintf( 'Runner: %s%s', $this->get_current_runner(), ( $runner_enabled ? '' : ' (disabled)' ) ) ); \WP_CLI::line( sprintf( 'Version: %s', $this->get_latest_version() ) ); $rows = array(); $action_counts = $this->store->action_counts(); $oldest_and_newest = $this->get_oldest_and_newest( array_keys( $action_counts ) ); foreach ( $action_counts as $status => $count ) { $rows[] = array( 'status' => $status, 'count' => $count, 'oldest' => $oldest_and_newest[ $status ]['oldest'], 'newest' => $oldest_and_newest[ $status ]['newest'], ); } $formatter = new \WP_CLI\Formatter( $assoc_args, array( 'status', 'count', 'oldest', 'newest' ) ); $formatter->display_items( $rows ); } /** * Display the active version, or all registered versions. * * ## OPTIONS * * [--all] * : List all registered versions. * * @param array $args Positional args. * @param array $assoc_args Keyed args. * @return void */ public function version( array $args, array $assoc_args ) { $all = (bool) get_flag_value( $assoc_args, 'all' ); $latest = $this->get_latest_version(); if ( ! $all ) { echo $latest; \WP_CLI::halt( 0 ); } $instance = \ActionScheduler_Versions::instance(); $versions = $instance->get_versions(); $rows = array(); foreach ( $versions as $version => $callback ) { $active = $version === $latest; $rows[ $version ] = array( 'version' => $version, 'callback' => $callback, 'active' => $active ? 'yes' : 'no', ); } uksort( $rows, 'version_compare' ); $formatter = new \WP_CLI\Formatter( $assoc_args, array( 'version', 'callback', 'active' ) ); $formatter->display_items( $rows ); } /** * Display the current source, or all registered sources. * * ## OPTIONS * * [--all] * : List all registered sources. * * [--fullpath] * : List full path of source(s). * * @param array $args Positional args. * @param array $assoc_args Keyed args. * @uses ActionScheduler_SystemInformation::active_source_path() * @uses \WP_CLI\Formatter::display_items() * @uses $this->get_latest_version() * @return void */ public function source( array $args, array $assoc_args ) { $all = (bool) get_flag_value( $assoc_args, 'all' ); $fullpath = (bool) get_flag_value( $assoc_args, 'fullpath' ); $source = ActionScheduler_SystemInformation::active_source_path(); $path = $source; if ( ! $fullpath ) { $path = str_replace( ABSPATH, '', $path ); } if ( ! $all ) { echo $path; \WP_CLI::halt( 0 ); } $sources = ActionScheduler_SystemInformation::get_sources(); if ( empty( $sources ) ) { WP_CLI::log( __( 'Detailed information about registered sources is not currently available.', 'action-scheduler' ) ); return; } $rows = array(); foreach ( $sources as $check_source => $version ) { $active = dirname( $check_source ) === $source; $path = $check_source; if ( ! $fullpath ) { $path = str_replace( ABSPATH, '', $path ); } $rows[ $check_source ] = array( 'source' => $path, 'version' => $version, 'active' => $active ? 'yes' : 'no', ); } ksort( $rows ); \WP_CLI::log( PHP_EOL . 'Please note there can only be one unique registered instance of Action Scheduler per ' . PHP_EOL . 'version number, so this list may not include all the currently present copies of ' . PHP_EOL . 'Action Scheduler.' . PHP_EOL ); $formatter = new \WP_CLI\Formatter( $assoc_args, array( 'source', 'version', 'active' ) ); $formatter->display_items( $rows ); } /** * Get current data store. * * @return string */ protected function get_current_datastore() { return get_class( $this->store ); } /** * Get latest version. * * @param null|\ActionScheduler_Versions $instance Versions. * @return string */ protected function get_latest_version( $instance = null ) { if ( is_null( $instance ) ) { $instance = \ActionScheduler_Versions::instance(); } return $instance->latest_version(); } /** * Get current runner. * * @return string */ protected function get_current_runner() { return get_class( \ActionScheduler::runner() ); } /** * Get oldest and newest scheduled dates for a given set of statuses. * * @param array $status_keys Set of statuses to find oldest & newest action for. * @return array */ protected function get_oldest_and_newest( $status_keys ) { $oldest_and_newest = array(); foreach ( $status_keys as $status ) { $oldest_and_newest[ $status ] = array( 'oldest' => '–', 'newest' => '–', ); if ( 'in-progress' === $status ) { continue; } $oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' ); $oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' ); } return $oldest_and_newest; } /** * Get oldest or newest scheduled date for a given status. * * @param string $status Action status label/name string. * @param string $date_type Oldest or Newest. * @return string */ protected function get_action_status_date( $status, $date_type = 'oldest' ) { $order = 'oldest' === $date_type ? 'ASC' : 'DESC'; $args = array( 'claimed' => false, 'status' => $status, 'per_page' => 1, 'order' => $order, ); $action = $this->store->query_actions( $args ); if ( ! empty( $action ) ) { $date_object = $this->store->get_date( $action[0] ); $action_date = $date_object->format( 'Y-m-d H:i:s O' ); } else { $action_date = '–'; } return $action_date; } } Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php 0000644 00000014331 15174671745 0023764 0 ustar 00 <?php use Action_Scheduler\WP_CLI\ProgressBar; /** * WP CLI Queue runner. * * This class can only be called from within a WP CLI instance. */ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRunner { /** * Claimed actions. * * @var array */ protected $actions; /** * ActionScheduler_ActionClaim instance. * * @var ActionScheduler_ActionClaim */ protected $claim; /** * Progress bar instance. * * @var \cli\progress\Bar */ protected $progress_bar; /** * ActionScheduler_WPCLI_QueueRunner constructor. * * @param ActionScheduler_Store|null $store Store object. * @param ActionScheduler_FatalErrorMonitor|null $monitor Monitor object. * @param ActionScheduler_QueueCleaner|null $cleaner Cleaner object. * * @throws Exception When this is not run within WP CLI. */ public function __construct( ?ActionScheduler_Store $store = null, ?ActionScheduler_FatalErrorMonitor $monitor = null, ?ActionScheduler_QueueCleaner $cleaner = null ) { if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) { /* translators: %s php class name */ throw new Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), __CLASS__ ) ); } parent::__construct( $store, $monitor, $cleaner ); } /** * Set up the Queue before processing. * * @param int $batch_size The batch size to process. * @param array $hooks The hooks being used to filter the actions claimed in this batch. * @param string $group The group of actions to claim with this batch. * @param bool $force Whether to force running even with too many concurrent processes. * * @return int The number of actions that will be run. * @throws \WP_CLI\ExitException When there are too many concurrent batches. */ public function setup( $batch_size, $hooks = array(), $group = '', $force = false ) { $this->run_cleanup(); $this->add_hooks(); // Check to make sure there aren't too many concurrent processes running. if ( $this->has_maximum_concurrent_batches() ) { if ( $force ) { WP_CLI::warning( __( 'There are too many concurrent batches, but the run is forced to continue.', 'action-scheduler' ) ); } else { WP_CLI::error( __( 'There are too many concurrent batches.', 'action-scheduler' ) ); } } // Stake a claim and store it. $this->claim = $this->store->stake_claim( $batch_size, null, $hooks, $group ); $this->monitor->attach( $this->claim ); $this->actions = $this->claim->get_actions(); return count( $this->actions ); } /** * Add our hooks to the appropriate actions. */ protected function add_hooks() { add_action( 'action_scheduler_before_execute', array( $this, 'before_execute' ) ); add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ), 10, 2 ); add_action( 'action_scheduler_failed_execution', array( $this, 'action_failed' ), 10, 2 ); } /** * Set up the WP CLI progress bar. */ protected function setup_progress_bar() { $count = count( $this->actions ); $this->progress_bar = new ProgressBar( /* translators: %d: amount of actions */ sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), $count ), $count ); } /** * Process actions in the queue. * * @param string $context Optional runner context. Default 'WP CLI'. * * @return int The number of actions processed. */ public function run( $context = 'WP CLI' ) { do_action( 'action_scheduler_before_process_queue' ); $this->setup_progress_bar(); foreach ( $this->actions as $action_id ) { // Error if we lost the claim. if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $this->claim->get_id() ), true ) ) { WP_CLI::warning( __( 'The claim has been lost. Aborting current batch.', 'action-scheduler' ) ); break; } $this->process_action( $action_id, $context ); $this->progress_bar->tick(); } $completed = $this->progress_bar->current(); $this->progress_bar->finish(); $this->store->release_claim( $this->claim ); do_action( 'action_scheduler_after_process_queue' ); return $completed; } /** * Handle WP CLI message when the action is starting. * * @param int $action_id Action ID. */ public function before_execute( $action_id ) { /* translators: %s refers to the action ID */ WP_CLI::log( sprintf( __( 'Started processing action %s', 'action-scheduler' ), $action_id ) ); } /** * Handle WP CLI message when the action has completed. * * @param int $action_id ActionID. * @param null|ActionScheduler_Action $action The instance of the action. Default to null for backward compatibility. */ public function after_execute( $action_id, $action = null ) { // backward compatibility. if ( null === $action ) { $action = $this->store->fetch_action( $action_id ); } /* translators: 1: action ID 2: hook name */ WP_CLI::log( sprintf( __( 'Completed processing action %1$s with hook: %2$s', 'action-scheduler' ), $action_id, $action->get_hook() ) ); } /** * Handle WP CLI message when the action has failed. * * @param int $action_id Action ID. * @param Exception $exception Exception. * @throws \WP_CLI\ExitException With failure message. */ public function action_failed( $action_id, $exception ) { WP_CLI::error( /* translators: 1: action ID 2: exception message */ sprintf( __( 'Error processing action %1$s: %2$s', 'action-scheduler' ), $action_id, $exception->getMessage() ), false ); } /** * Sleep and help avoid hitting memory limit * * @param int $sleep_time Amount of seconds to sleep. * @deprecated 3.0.0 */ protected function stop_the_insanity( $sleep_time = 0 ) { _deprecated_function( 'ActionScheduler_WPCLI_QueueRunner::stop_the_insanity', '3.0.0', 'ActionScheduler_DataController::free_memory' ); ActionScheduler_DataController::free_memory(); } /** * Maybe trigger the stop_the_insanity() method to free up memory. */ protected function maybe_stop_the_insanity() { // The value returned by progress_bar->current() might be padded. Remove padding, and convert to int. $current_iteration = intval( trim( $this->progress_bar->current() ) ); if ( 0 === $current_iteration % 50 ) { $this->stop_the_insanity(); } } } Dependencies/ActionScheduler/classes/WP_CLI/Action_Command.php 0000644 00000020224 15174671745 0020265 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI; /** * Action command for Action Scheduler. */ class Action_Command extends \WP_CLI_Command { /** * Cancel the next occurrence or all occurrences of a scheduled action. * * ## OPTIONS * * [<hook>] * : Name of the action hook. * * [--group=<group>] * : The group the job is assigned to. * * [--args=<args>] * : JSON object of arguments assigned to the job. * --- * default: [] * --- * * [--all] * : Cancel all occurrences of a scheduled action. * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @return void */ public function cancel( array $args, array $assoc_args ) { require_once 'Action/Cancel_Command.php'; $command = new Action\Cancel_Command( $args, $assoc_args ); $command->execute(); } /** * Creates a new scheduled action. * * ## OPTIONS * * <hook> * : Name of the action hook. * * <start> * : A unix timestamp representing the date you want the action to start. Also 'async' or 'now' to enqueue an async action. * * [--args=<args>] * : JSON object of arguments to pass to callbacks when the hook triggers. * --- * default: [] * --- * * [--cron=<cron>] * : A cron-like schedule string (https://crontab.guru/). * --- * default: '' * --- * * [--group=<group>] * : The group to assign this job to. * --- * default: '' * --- * * [--interval=<interval>] * : Number of seconds to wait between runs. * --- * default: 0 * --- * * ## EXAMPLES * * wp action-scheduler action create hook_async async * wp action-scheduler action create hook_single 1627147598 * wp action-scheduler action create hook_recurring 1627148188 --interval=5 * wp action-scheduler action create hook_cron 1627147655 --cron='5 4 * * *' * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @return void */ public function create( array $args, array $assoc_args ) { require_once 'Action/Create_Command.php'; $command = new Action\Create_Command( $args, $assoc_args ); $command->execute(); } /** * Delete existing scheduled action(s). * * ## OPTIONS * * <id>... * : One or more IDs of actions to delete. * --- * default: 0 * --- * * ## EXAMPLES * * # Delete the action with id 100 * $ wp action-scheduler action delete 100 * * # Delete the actions with ids 100 and 200 * $ wp action-scheduler action delete 100 200 * * # Delete the first five pending actions in 'action-scheduler' group * $ wp action-scheduler action delete $( wp action-scheduler action list --status=pending --group=action-scheduler --format=ids ) * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @return void */ public function delete( array $args, array $assoc_args ) { require_once 'Action/Delete_Command.php'; $command = new Action\Delete_Command( $args, $assoc_args ); $command->execute(); } /** * Generates some scheduled actions. * * ## OPTIONS * * <hook> * : Name of the action hook. * * <start> * : The Unix timestamp representing the date you want the action to start. * * [--count=<count>] * : Number of actions to create. * --- * default: 1 * --- * * [--interval=<interval>] * : Number of seconds to wait between runs. * --- * default: 0 * --- * * [--args=<args>] * : JSON object of arguments to pass to callbacks when the hook triggers. * --- * default: [] * --- * * [--group=<group>] * : The group to assign this job to. * --- * default: '' * --- * * ## EXAMPLES * * wp action-scheduler action generate test_multiple 1627147598 --count=5 --interval=5 * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @return void */ public function generate( array $args, array $assoc_args ) { require_once 'Action/Generate_Command.php'; $command = new Action\Generate_Command( $args, $assoc_args ); $command->execute(); } /** * Get details about a scheduled action. * * ## OPTIONS * * <id> * : The ID of the action to get. * --- * default: 0 * --- * * [--field=<field>] * : Instead of returning the whole action, returns the value of a single field. * * [--fields=<fields>] * : Limit the output to specific fields (comma-separated). Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @return void */ public function get( array $args, array $assoc_args ) { require_once 'Action/Get_Command.php'; $command = new Action\Get_Command( $args, $assoc_args ); $command->execute(); } /** * Get a list of scheduled actions. * * Display actions based on all arguments supported by * [as_get_scheduled_actions()](https://actionscheduler.org/api/#function-reference--as_get_scheduled_actions). * * ## OPTIONS * * [--<field>=<value>] * : One or more arguments to pass to as_get_scheduled_actions(). * * [--field=<field>] * : Prints the value of a single property for each action. * * [--fields=<fields>] * : Limit the output to specific object properties. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - ids * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each action: * * * id * * hook * * status * * group * * recurring * * scheduled_date * * These fields are optionally available: * * * args * * log_entries * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @return void * * @subcommand list */ public function subcommand_list( array $args, array $assoc_args ) { require_once 'Action/List_Command.php'; $command = new Action\List_Command( $args, $assoc_args ); $command->execute(); } /** * Get logs for a scheduled action. * * ## OPTIONS * * <id> * : The ID of the action to get. * --- * default: 0 * --- * * @param array $args Positional arguments. * @return void */ public function logs( array $args ) { $command = sprintf( 'action-scheduler action get %d --field=log_entries', $args[0] ); WP_CLI::runcommand( $command ); } /** * Get the ID or timestamp of the next scheduled action. * * ## OPTIONS * * <hook> * : The hook of the next scheduled action. * * [--args=<args>] * : JSON object of arguments to search for next scheduled action. * --- * default: [] * --- * * [--group=<group>] * : The group to which the next scheduled action is assigned. * --- * default: '' * --- * * [--raw] * : Display the raw output of as_next_scheduled_action() (timestamp or boolean). * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @return void */ public function next( array $args, array $assoc_args ) { require_once 'Action/Next_Command.php'; $command = new Action\Next_Command( $args, $assoc_args ); $command->execute(); } /** * Run existing scheduled action(s). * * ## OPTIONS * * <id>... * : One or more IDs of actions to run. * --- * default: 0 * --- * * ## EXAMPLES * * # Run the action with id 100 * $ wp action-scheduler action run 100 * * # Run the actions with ids 100 and 200 * $ wp action-scheduler action run 100 200 * * # Run the first five pending actions in 'action-scheduler' group * $ wp action-scheduler action run $( wp action-scheduler action list --status=pending --group=action-scheduler --format=ids ) * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @return void */ public function run( array $args, array $assoc_args ) { require_once 'Action/Run_Command.php'; $command = new Action\Run_Command( $args, $assoc_args ); $command->execute(); } } Dependencies/ActionScheduler/classes/WP_CLI/Migration_Command.php 0000644 00000011670 15174671745 0021006 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI; use Action_Scheduler\Migration\Config; use Action_Scheduler\Migration\Runner; use Action_Scheduler\Migration\Scheduler; use Action_Scheduler\Migration\Controller; use WP_CLI; use WP_CLI_Command; /** * Class Migration_Command * * @package Action_Scheduler\WP_CLI * * @since 3.0.0 * * @codeCoverageIgnore */ class Migration_Command extends WP_CLI_Command { /** * Number of actions migrated. * * @var int */ private $total_processed = 0; /** * Register the command with WP-CLI */ public function register() { if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { return; } WP_CLI::add_command( 'action-scheduler migrate', array( $this, 'migrate' ), array( 'shortdesc' => 'Migrates actions to the DB tables store', 'synopsis' => array( array( 'type' => 'assoc', 'name' => 'batch-size', 'optional' => true, 'default' => 100, 'description' => 'The number of actions to process in each batch', ), array( 'type' => 'assoc', 'name' => 'free-memory-on', 'optional' => true, 'default' => 50, 'description' => 'The number of actions to process between freeing memory. 0 disables freeing memory', ), array( 'type' => 'assoc', 'name' => 'pause', 'optional' => true, 'default' => 0, 'description' => 'The number of seconds to pause when freeing memory', ), array( 'type' => 'flag', 'name' => 'dry-run', 'optional' => true, 'description' => 'Reports on the actions that would have been migrated, but does not change any data', ), ), ) ); } /** * Process the data migration. * * @param array $positional_args Required for WP CLI. Not used in migration. * @param array $assoc_args Optional arguments. * * @return void */ public function migrate( $positional_args, $assoc_args ) { $this->init_logging(); $config = $this->get_migration_config( $assoc_args ); $runner = new Runner( $config ); $runner->init_destination(); $batch_size = isset( $assoc_args['batch-size'] ) ? (int) $assoc_args['batch-size'] : 100; $free_on = isset( $assoc_args['free-memory-on'] ) ? (int) $assoc_args['free-memory-on'] : 50; $sleep = isset( $assoc_args['pause'] ) ? (int) $assoc_args['pause'] : 0; \ActionScheduler_DataController::set_free_ticks( $free_on ); \ActionScheduler_DataController::set_sleep_time( $sleep ); do { $actions_processed = $runner->run( $batch_size ); $this->total_processed += $actions_processed; } while ( $actions_processed > 0 ); if ( ! $config->get_dry_run() ) { // let the scheduler know that there's nothing left to do. $scheduler = new Scheduler(); $scheduler->mark_complete(); } WP_CLI::success( sprintf( '%s complete. %d actions processed.', $config->get_dry_run() ? 'Dry run' : 'Migration', $this->total_processed ) ); } /** * Build the config object used to create the Runner * * @param array $args Optional arguments. * * @return ActionScheduler\Migration\Config */ private function get_migration_config( $args ) { $args = wp_parse_args( $args, array( 'dry-run' => false, ) ); $config = Controller::instance()->get_migration_config_object(); $config->set_dry_run( ! empty( $args['dry-run'] ) ); return $config; } /** * Hook command line logging into migration actions. */ private function init_logging() { add_action( 'action_scheduler/migrate_action_dry_run', function ( $action_id ) { WP_CLI::debug( sprintf( 'Dry-run: migrated action %d', $action_id ) ); } ); add_action( 'action_scheduler/no_action_to_migrate', function ( $action_id ) { WP_CLI::debug( sprintf( 'No action found to migrate for ID %d', $action_id ) ); } ); add_action( 'action_scheduler/migrate_action_failed', function ( $action_id ) { WP_CLI::warning( sprintf( 'Failed migrating action with ID %d', $action_id ) ); } ); add_action( 'action_scheduler/migrate_action_incomplete', function ( $source_id, $destination_id ) { WP_CLI::warning( sprintf( 'Unable to remove source action with ID %d after migrating to new ID %d', $source_id, $destination_id ) ); }, 10, 2 ); add_action( 'action_scheduler/migrated_action', function ( $source_id, $destination_id ) { WP_CLI::debug( sprintf( 'Migrated source action with ID %d to new store with ID %d', $source_id, $destination_id ) ); }, 10, 2 ); add_action( 'action_scheduler/migration_batch_starting', function ( $batch ) { WP_CLI::debug( 'Beginning migration of batch: ' . print_r( $batch, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r } ); add_action( 'action_scheduler/migration_batch_complete', function ( $batch ) { WP_CLI::log( sprintf( 'Completed migration of %d actions', count( $batch ) ) ); } ); } } Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php 0000644 00000015307 15174671745 0025126 0 ustar 00 <?php /** * Commands for Action Scheduler. */ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command { /** * Force tables schema creation for Action Scheduler * * ## OPTIONS * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * * @subcommand fix-schema */ public function fix_schema( $args, $assoc_args ) { $schema_classes = array( ActionScheduler_LoggerSchema::class, ActionScheduler_StoreSchema::class ); foreach ( $schema_classes as $classname ) { if ( is_subclass_of( $classname, ActionScheduler_Abstract_Schema::class ) ) { $obj = new $classname(); $obj->init(); $obj->register_tables( true ); WP_CLI::success( sprintf( /* translators: %s refers to the schema name*/ __( 'Registered schema for %s', 'action-scheduler' ), $classname ) ); } } } /** * Run the Action Scheduler * * ## OPTIONS * * [--batch-size=<size>] * : The maximum number of actions to run. Defaults to 100. * * [--batches=<size>] * : Limit execution to a number of batches. Defaults to 0, meaning batches will continue being executed until all actions are complete. * * [--cleanup-batch-size=<size>] * : The maximum number of actions to clean up. Defaults to the value of --batch-size. * * [--hooks=<hooks>] * : Only run actions with the specified hook. Omitting this option runs actions with any hook. Define multiple hooks as a comma separated string (without spaces), e.g. `--hooks=hook_one,hook_two,hook_three` * * [--group=<group>] * : Only run actions from the specified group. Omitting this option runs actions from all groups. * * [--exclude-groups=<groups>] * : Run actions from all groups except the specified group(s). Define multiple groups as a comma separated string (without spaces), e.g. '--group_a,group_b'. This option is ignored when `--group` is used. * * [--free-memory-on=<count>] * : The number of actions to process between freeing memory. 0 disables freeing memory. Default 50. * * [--pause=<seconds>] * : The number of seconds to pause when freeing memory. Default no pause. * * [--force] * : Whether to force execution despite the maximum number of concurrent processes being exceeded. * * @param array $args Positional arguments. * @param array $assoc_args Keyed arguments. * @throws \WP_CLI\ExitException When an error occurs. * * @subcommand run */ public function run( $args, $assoc_args ) { // Handle passed arguments. $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) ); $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); $clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) ); $hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) ); $hooks = array_filter( array_map( 'trim', $hooks ) ); $group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' ); $exclude_groups = \WP_CLI\Utils\get_flag_value( $assoc_args, 'exclude-groups', '' ); $free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 ); $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ); ActionScheduler_DataController::set_free_ticks( $free_on ); ActionScheduler_DataController::set_sleep_time( $sleep ); $batches_completed = 0; $actions_completed = 0; $unlimited = 0 === $batches; if ( is_callable( array( ActionScheduler::store(), 'set_claim_filter' ) ) ) { $exclude_groups = $this->parse_comma_separated_string( $exclude_groups ); if ( ! empty( $exclude_groups ) ) { ActionScheduler::store()->set_claim_filter( 'exclude-groups', $exclude_groups ); } } try { // Custom queue cleaner instance. $cleaner = new ActionScheduler_QueueCleaner( null, $clean ); // Get the queue runner instance. $runner = new ActionScheduler_WPCLI_QueueRunner( null, null, $cleaner ); // Determine how many tasks will be run in the first batch. $total = $runner->setup( $batch, $hooks, $group, $force ); // Run actions for as long as possible. while ( $total > 0 ) { $this->print_total_actions( $total ); $actions_completed += $runner->run(); $batches_completed++; // Maybe set up tasks for the next batch. $total = ( $unlimited || $batches_completed < $batches ) ? $runner->setup( $batch, $hooks, $group, $force ) : 0; } } catch ( Exception $e ) { $this->print_error( $e ); } $this->print_total_batches( $batches_completed ); $this->print_success( $actions_completed ); } /** * Converts a string of comma-separated values into an array of those same values. * * @param string $string The string of one or more comma separated values. * * @return array */ private function parse_comma_separated_string( $string ): array { return array_filter( str_getcsv( $string ) ); } /** * Print WP CLI message about how many actions are about to be processed. * * @param int $total Number of actions found. */ protected function print_total_actions( $total ) { WP_CLI::log( sprintf( /* translators: %d refers to how many scheduled tasks were found to run */ _n( 'Found %d scheduled task', 'Found %d scheduled tasks', $total, 'action-scheduler' ), $total ) ); } /** * Print WP CLI message about how many batches of actions were processed. * * @param int $batches_completed Number of completed batches. */ protected function print_total_batches( $batches_completed ) { WP_CLI::log( sprintf( /* translators: %d refers to the total number of batches executed */ _n( '%d batch executed.', '%d batches executed.', $batches_completed, 'action-scheduler' ), $batches_completed ) ); } /** * Convert an exception into a WP CLI error. * * @param Exception $e The error object. * * @throws \WP_CLI\ExitException Under some conditions WP CLI may throw an exception. */ protected function print_error( Exception $e ) { WP_CLI::error( sprintf( /* translators: %s refers to the exception error message */ __( 'There was an error running the action scheduler: %s', 'action-scheduler' ), $e->getMessage() ) ); } /** * Print a success message with the number of completed actions. * * @param int $actions_completed Number of completed actions. */ protected function print_success( $actions_completed ) { WP_CLI::success( sprintf( /* translators: %d refers to the total number of tasks completed */ _n( '%d scheduled task completed.', '%d scheduled tasks completed.', $actions_completed, 'action-scheduler' ), $actions_completed ) ); } } Dependencies/ActionScheduler/classes/WP_CLI/Action/Delete_Command.php 0000644 00000005145 15174671745 0021474 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI\Action; /** * WP-CLI command: action-scheduler action delete */ class Delete_Command extends \ActionScheduler_WPCLI_Command { /** * Array of action IDs to delete. * * @var int[] */ protected $action_ids = array(); /** * Number of deleted, failed, and total actions deleted. * * @var array<string, int> */ protected $action_counts = array( 'deleted' => 0, 'failed' => 0, 'total' => 0, ); /** * Construct. * * @param string[] $args Positional arguments. * @param array<string, string> $assoc_args Keyed arguments. */ public function __construct( array $args, array $assoc_args ) { parent::__construct( $args, $assoc_args ); $this->action_ids = array_map( 'absint', $args ); $this->action_counts['total'] = count( $this->action_ids ); add_action( 'action_scheduler_deleted_action', array( $this, 'on_action_deleted' ) ); } /** * Execute. * * @return void */ public function execute() { $store = \ActionScheduler::store(); $progress_bar = \WP_CLI\Utils\make_progress_bar( sprintf( /* translators: %d: number of actions to be deleted */ _n( 'Deleting %d action', 'Deleting %d actions', $this->action_counts['total'], 'action-scheduler' ), number_format_i18n( $this->action_counts['total'] ) ), $this->action_counts['total'] ); foreach ( $this->action_ids as $action_id ) { try { $store->delete_action( $action_id ); } catch ( \Exception $e ) { $this->action_counts['failed']++; \WP_CLI::warning( $e->getMessage() ); } $progress_bar->tick(); } $progress_bar->finish(); /* translators: %1$d: number of actions deleted */ $format = _n( 'Deleted %1$d action', 'Deleted %1$d actions', $this->action_counts['deleted'], 'action-scheduler' ) . ', '; /* translators: %2$d: number of actions deletions failed */ $format .= _n( '%2$d failure.', '%2$d failures.', $this->action_counts['failed'], 'action-scheduler' ); \WP_CLI::success( sprintf( $format, number_format_i18n( $this->action_counts['deleted'] ), number_format_i18n( $this->action_counts['failed'] ) ) ); } /** * Action: action_scheduler_deleted_action * * @param int $action_id Action ID. * @return void */ public function on_action_deleted( $action_id ) { if ( 'action_scheduler_deleted_action' !== current_action() ) { return; } $action_id = absint( $action_id ); if ( ! in_array( $action_id, $this->action_ids, true ) ) { return; } $this->action_counts['deleted']++; \WP_CLI::debug( sprintf( 'Action %d was deleted.', $action_id ) ); } } Dependencies/ActionScheduler/classes/WP_CLI/Action/Create_Command.php 0000644 00000010476 15174671745 0021500 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI\Action; /** * WP-CLI command: action-scheduler action create */ class Create_Command extends \ActionScheduler_WPCLI_Command { const ASYNC_OPTS = array( 'async', 0 ); /** * Execute command. * * @return void */ public function execute() { $hook = $this->args[0]; $schedule_start = $this->args[1]; $callback_args = get_flag_value( $this->assoc_args, 'args', array() ); $group = get_flag_value( $this->assoc_args, 'group', '' ); $interval = absint( get_flag_value( $this->assoc_args, 'interval', 0 ) ); $cron = get_flag_value( $this->assoc_args, 'cron', '' ); $unique = get_flag_value( $this->assoc_args, 'unique', false ); $priority = absint( get_flag_value( $this->assoc_args, 'priority', 10 ) ); if ( ! empty( $callback_args ) ) { $callback_args = json_decode( $callback_args, true ); } $function_args = array( 'start' => $schedule_start, 'cron' => $cron, 'interval' => $interval, 'hook' => $hook, 'callback_args' => $callback_args, 'group' => $group, 'unique' => $unique, 'priority' => $priority, ); try { // Generate schedule start if appropriate. if ( ! in_array( $schedule_start, static::ASYNC_OPTS, true ) ) { $schedule_start = as_get_datetime_object( $schedule_start ); $function_args['start'] = $schedule_start->format( 'U' ); } } catch ( \Exception $e ) { \WP_CLI::error( $e->getMessage() ); } // Default to creating single action. $action_type = 'single'; $function = 'as_schedule_single_action'; if ( ! empty( $interval ) ) { // Creating recurring action. $action_type = 'recurring'; $function = 'as_schedule_recurring_action'; $function_args = array_filter( $function_args, static function( $key ) { return in_array( $key, array( 'start', 'interval', 'hook', 'callback_args', 'group', 'unique', 'priority' ), true ); }, ARRAY_FILTER_USE_KEY ); } elseif ( ! empty( $cron ) ) { // Creating cron action. $action_type = 'cron'; $function = 'as_schedule_cron_action'; $function_args = array_filter( $function_args, static function( $key ) { return in_array( $key, array( 'start', 'cron', 'hook', 'callback_args', 'group', 'unique', 'priority' ), true ); }, ARRAY_FILTER_USE_KEY ); } elseif ( in_array( $function_args['start'], static::ASYNC_OPTS, true ) ) { // Enqueue async action. $action_type = 'async'; $function = 'as_enqueue_async_action'; $function_args = array_filter( $function_args, static function( $key ) { return in_array( $key, array( 'hook', 'callback_args', 'group', 'unique', 'priority' ), true ); }, ARRAY_FILTER_USE_KEY ); } else { // Enqueue single action. $function_args = array_filter( $function_args, static function( $key ) { return in_array( $key, array( 'start', 'hook', 'callback_args', 'group', 'unique', 'priority' ), true ); }, ARRAY_FILTER_USE_KEY ); } $function_args = array_values( $function_args ); try { $action_id = call_user_func_array( $function, $function_args ); } catch ( \Exception $e ) { $this->print_error( $e ); } if ( 0 === $action_id ) { $e = new \Exception( __( 'Unable to create a scheduled action.', 'action-scheduler' ) ); $this->print_error( $e ); } $this->print_success( $action_id, $action_type ); } /** * Print a success message with the action ID. * * @param int $action_id Created action ID. * @param string $action_type Type of action. * * @return void */ protected function print_success( $action_id, $action_type ) { \WP_CLI::success( sprintf( /* translators: %1$s: type of action, %2$d: ID of the created action */ __( '%1$s action (%2$d) scheduled.', 'action-scheduler' ), ucfirst( $action_type ), $action_id ) ); } /** * Convert an exception into a WP CLI error. * * @param \Exception $e The error object. * @throws \WP_CLI\ExitException When an error occurs. * @return void */ protected function print_error( \Exception $e ) { \WP_CLI::error( sprintf( /* translators: %s refers to the exception error message. */ __( 'There was an error creating the scheduled action: %s', 'action-scheduler' ), $e->getMessage() ) ); } } Dependencies/ActionScheduler/classes/WP_CLI/Action/Next_Command.php 0000644 00000003503 15174671745 0021204 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI\Action; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping output is not necessary in WP CLI. use function \WP_CLI\Utils\get_flag_value; /** * WP-CLI command: action-scheduler action next */ class Next_Command extends \ActionScheduler_WPCLI_Command { /** * Execute command. * * @return void */ public function execute() { $hook = $this->args[0]; $group = get_flag_value( $this->assoc_args, 'group', '' ); $callback_args = get_flag_value( $this->assoc_args, 'args', null ); $raw = (bool) get_flag_value( $this->assoc_args, 'raw', false ); if ( ! empty( $callback_args ) ) { $callback_args = json_decode( $callback_args, true ); } if ( $raw ) { \WP_CLI::line( as_next_scheduled_action( $hook, $callback_args, $group ) ); return; } $params = array( 'hook' => $hook, 'orderby' => 'date', 'order' => 'ASC', 'group' => $group, ); if ( is_array( $callback_args ) ) { $params['args'] = $callback_args; } $params['status'] = \ActionScheduler_Store::STATUS_RUNNING; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export \WP_CLI::debug( 'ActionScheduler()::store()->query_action( ' . var_export( $params, true ) . ' )' ); $store = \ActionScheduler::store(); $action_id = $store->query_action( $params ); if ( $action_id ) { echo $action_id; return; } $params['status'] = \ActionScheduler_Store::STATUS_PENDING; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export \WP_CLI::debug( 'ActionScheduler()::store()->query_action( ' . var_export( $params, true ) . ' )' ); $action_id = $store->query_action( $params ); if ( $action_id ) { echo $action_id; return; } \WP_CLI::warning( 'No matching next action.' ); } } Dependencies/ActionScheduler/classes/WP_CLI/Action/Cancel_Command.php 0000644 00000006410 15174671745 0021453 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI\Action; use function \WP_CLI\Utils\get_flag_value; /** * WP-CLI command: action-scheduler action cancel */ class Cancel_Command extends \ActionScheduler_WPCLI_Command { /** * Execute command. * * @return void */ public function execute() { $hook = ''; $group = get_flag_value( $this->assoc_args, 'group', '' ); $callback_args = get_flag_value( $this->assoc_args, 'args', null ); $all = get_flag_value( $this->assoc_args, 'all', false ); if ( ! empty( $this->args[0] ) ) { $hook = $this->args[0]; } if ( ! empty( $callback_args ) ) { $callback_args = json_decode( $callback_args, true ); } if ( $all ) { $this->cancel_all( $hook, $callback_args, $group ); return; } $this->cancel_single( $hook, $callback_args, $group ); } /** * Cancel single action. * * @param string $hook The hook that the job will trigger. * @param array $callback_args Args that would have been passed to the job. * @param string $group The group the job is assigned to. * @return void */ protected function cancel_single( $hook, $callback_args, $group ) { if ( empty( $hook ) ) { \WP_CLI::error( __( 'Please specify hook of action to cancel.', 'action-scheduler' ) ); } try { $result = as_unschedule_action( $hook, $callback_args, $group ); } catch ( \Exception $e ) { $this->print_error( $e, false ); } if ( null === $result ) { $e = new \Exception( __( 'Unable to cancel scheduled action: check the logs.', 'action-scheduler' ) ); $this->print_error( $e, false ); } $this->print_success( false ); } /** * Cancel all actions. * * @param string $hook The hook that the job will trigger. * @param array $callback_args Args that would have been passed to the job. * @param string $group The group the job is assigned to. * @return void */ protected function cancel_all( $hook, $callback_args, $group ) { if ( empty( $hook ) && empty( $group ) ) { \WP_CLI::error( __( 'Please specify hook and/or group of actions to cancel.', 'action-scheduler' ) ); } try { $result = as_unschedule_all_actions( $hook, $callback_args, $group ); } catch ( \Exception $e ) { $this->print_error( $e, $multiple ); } /** * Because as_unschedule_all_actions() does not provide a result, * neither confirm or deny actions cancelled. */ \WP_CLI::success( __( 'Request to cancel scheduled actions completed.', 'action-scheduler' ) ); } /** * Print a success message. * * @return void */ protected function print_success() { \WP_CLI::success( __( 'Scheduled action cancelled.', 'action-scheduler' ) ); } /** * Convert an exception into a WP CLI error. * * @param \Exception $e The error object. * @param bool $multiple Boolean if multiple actions. * @throws \WP_CLI\ExitException When an error occurs. * @return void */ protected function print_error( \Exception $e, $multiple ) { \WP_CLI::error( sprintf( /* translators: %1$s: singular or plural %2$s: refers to the exception error message. */ __( 'There was an error cancelling the %1$s: %2$s', 'action-scheduler' ), $multiple ? __( 'scheduled actions', 'action-scheduler' ) : __( 'scheduled action', 'action-scheduler' ), $e->getMessage() ) ); } } Dependencies/ActionScheduler/classes/WP_CLI/Action/List_Command.php 0000644 00000006067 15174671745 0021211 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI\Action; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping output is not necessary in WP CLI. /** * WP-CLI command: action-scheduler action list */ class List_Command extends \ActionScheduler_WPCLI_Command { const PARAMETERS = array( 'hook', 'args', 'date', 'date_compare', 'modified', 'modified_compare', 'group', 'status', 'claimed', 'per_page', 'offset', 'orderby', 'order', ); /** * Execute command. * * @return void */ public function execute() { $store = \ActionScheduler::store(); $logger = \ActionScheduler::logger(); $fields = array( 'id', 'hook', 'status', 'group', 'recurring', 'scheduled_date', ); $this->process_csv_arguments_to_arrays(); if ( ! empty( $this->assoc_args['fields'] ) ) { $fields = $this->assoc_args['fields']; } $formatter = new \WP_CLI\Formatter( $this->assoc_args, $fields ); $query_args = $this->assoc_args; /** * The `claimed` parameter expects a boolean or integer: * check for string 'false', and set explicitly to `false` boolean. */ if ( array_key_exists( 'claimed', $query_args ) && 'false' === strtolower( $query_args['claimed'] ) ) { $query_args['claimed'] = false; } $return_format = 'OBJECT'; if ( in_array( $formatter->format, array( 'ids', 'count' ), true ) ) { $return_format = '\'ids\''; } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export $params = var_export( $query_args, true ); if ( empty( $query_args ) ) { $params = 'array()'; } \WP_CLI::debug( sprintf( 'as_get_scheduled_actions( %s, %s )', $params, $return_format ) ); if ( ! empty( $query_args['args'] ) ) { $query_args['args'] = json_decode( $query_args['args'], true ); } switch ( $formatter->format ) { case 'ids': $actions = as_get_scheduled_actions( $query_args, 'ids' ); echo implode( ' ', $actions ); break; case 'count': $actions = as_get_scheduled_actions( $query_args, 'ids' ); $formatter->display_items( $actions ); break; default: $actions = as_get_scheduled_actions( $query_args, OBJECT ); $actions_arr = array(); foreach ( $actions as $action_id => $action ) { $action_arr = array( 'id' => $action_id, 'hook' => $action->get_hook(), 'status' => $store->get_status( $action_id ), 'args' => $action->get_args(), 'group' => $action->get_group(), 'recurring' => $action->get_schedule()->is_recurring() ? 'yes' : 'no', 'scheduled_date' => $this->get_schedule_display_string( $action->get_schedule() ), 'log_entries' => array(), ); foreach ( $logger->get_logs( $action_id ) as $log_entry ) { $action_arr['log_entries'][] = array( 'date' => $log_entry->get_date()->format( static::DATE_FORMAT ), 'message' => $log_entry->get_message(), ); } $actions_arr[] = $action_arr; } $formatter->display_items( $actions_arr ); break; } } } Dependencies/ActionScheduler/classes/WP_CLI/Action/Generate_Command.php 0000644 00000006737 15174671745 0022034 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI\Action; use function \WP_CLI\Utils\get_flag_value; /** * WP-CLI command: action-scheduler action generate */ class Generate_Command extends \ActionScheduler_WPCLI_Command { /** * Execute command. * * @return void */ public function execute() { $hook = $this->args[0]; $schedule_start = $this->args[1]; $callback_args = get_flag_value( $this->assoc_args, 'args', array() ); $group = get_flag_value( $this->assoc_args, 'group', '' ); $interval = (int) get_flag_value( $this->assoc_args, 'interval', 0 ); // avoid absint() to support negative intervals $count = absint( get_flag_value( $this->assoc_args, 'count', 1 ) ); if ( ! empty( $callback_args ) ) { $callback_args = json_decode( $callback_args, true ); } $schedule_start = as_get_datetime_object( $schedule_start ); $function_args = array( 'start' => absint( $schedule_start->format( 'U' ) ), 'interval' => $interval, 'count' => $count, 'hook' => $hook, 'callback_args' => $callback_args, 'group' => $group, ); $function_args = array_values( $function_args ); try { $actions_added = $this->generate( ...$function_args ); } catch ( \Exception $e ) { $this->print_error( $e ); } $num_actions_added = count( (array) $actions_added ); $this->print_success( $num_actions_added, 'single' ); } /** * Schedule multiple single actions. * * @param int $schedule_start Starting timestamp of first action. * @param int $interval How long to wait between runs. * @param int $count Limit number of actions to schedule. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @return int[] IDs of actions added. */ protected function generate( $schedule_start, $interval, $count, $hook, array $args = array(), $group = '' ) { $actions_added = array(); $progress_bar = \WP_CLI\Utils\make_progress_bar( sprintf( /* translators: %d is number of actions to create */ _n( 'Creating %d action', 'Creating %d actions', $count, 'action-scheduler' ), number_format_i18n( $count ) ), $count ); for ( $i = 0; $i < $count; $i++ ) { $actions_added[] = as_schedule_single_action( $schedule_start + ( $i * $interval ), $hook, $args, $group ); $progress_bar->tick(); } $progress_bar->finish(); return $actions_added; } /** * Print a success message with the action ID. * * @param int $actions_added Number of actions generated. * @param string $action_type Type of actions scheduled. * @return void */ protected function print_success( $actions_added, $action_type ) { \WP_CLI::success( sprintf( /* translators: %1$d refers to the total number of tasks added, %2$s is the action type */ _n( '%1$d %2$s action scheduled.', '%1$d %2$s actions scheduled.', $actions_added, 'action-scheduler' ), number_format_i18n( $actions_added ), $action_type ) ); } /** * Convert an exception into a WP CLI error. * * @param \Exception $e The error object. * @throws \WP_CLI\ExitException When an error occurs. * @return void */ protected function print_error( \Exception $e ) { \WP_CLI::error( sprintf( /* translators: %s refers to the exception error message. */ __( 'There was an error creating the scheduled action: %s', 'action-scheduler' ), $e->getMessage() ) ); } } Dependencies/ActionScheduler/classes/WP_CLI/Action/Get_Command.php 0000644 00000004240 15174671745 0021004 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI\Action; /** * WP-CLI command: action-scheduler action get */ class Get_Command extends \ActionScheduler_WPCLI_Command { /** * Execute command. * * @return void */ public function execute() { $action_id = $this->args[0]; $store = \ActionScheduler::store(); $logger = \ActionScheduler::logger(); $action = $store->fetch_action( $action_id ); if ( is_a( $action, ActionScheduler_NullAction::class ) ) { /* translators: %d is action ID. */ \WP_CLI::error( sprintf( esc_html__( 'Unable to retrieve action %d.', 'action-scheduler' ), $action_id ) ); } $only_logs = ! empty( $this->assoc_args['field'] ) && 'log_entries' === $this->assoc_args['field']; $only_logs = $only_logs || ( ! empty( $this->assoc_args['fields'] && 'log_entries' === $this->assoc_args['fields'] ) ); $log_entries = array(); foreach ( $logger->get_logs( $action_id ) as $log_entry ) { $log_entries[] = array( 'date' => $log_entry->get_date()->format( static::DATE_FORMAT ), 'message' => $log_entry->get_message(), ); } if ( $only_logs ) { $args = array( 'format' => \WP_CLI\Utils\get_flag_value( $this->assoc_args, 'format', 'table' ), ); $formatter = new \WP_CLI\Formatter( $args, array( 'date', 'message' ) ); $formatter->display_items( $log_entries ); return; } try { $status = $store->get_status( $action_id ); } catch ( \Exception $e ) { \WP_CLI::error( $e->getMessage() ); } $action_arr = array( 'id' => $this->args[0], 'hook' => $action->get_hook(), 'status' => $status, 'args' => $action->get_args(), 'group' => $action->get_group(), 'recurring' => $action->get_schedule()->is_recurring() ? 'yes' : 'no', 'scheduled_date' => $this->get_schedule_display_string( $action->get_schedule() ), 'log_entries' => $log_entries, ); $fields = array_keys( $action_arr ); if ( ! empty( $this->assoc_args['fields'] ) ) { $fields = explode( ',', $this->assoc_args['fields'] ); } $formatter = new \WP_CLI\Formatter( $this->assoc_args, $fields ); $formatter->display_item( $action_arr ); } } Dependencies/ActionScheduler/classes/WP_CLI/Action/Run_Command.php 0000644 00000011161 15174671745 0021031 0 ustar 00 <?php namespace Action_Scheduler\WP_CLI\Action; /** * WP-CLI command: action-scheduler action run */ class Run_Command extends \ActionScheduler_WPCLI_Command { /** * Array of action IDs to execute. * * @var int[] */ protected $action_ids = array(); /** * Number of executed, failed, ignored, invalid, and total actions. * * @var array<string, int> */ protected $action_counts = array( 'executed' => 0, 'failed' => 0, 'ignored' => 0, 'invalid' => 0, 'total' => 0, ); /** * Construct. * * @param string[] $args Positional arguments. * @param array<string, string> $assoc_args Keyed arguments. */ public function __construct( array $args, array $assoc_args ) { parent::__construct( $args, $assoc_args ); $this->action_ids = array_map( 'absint', $args ); $this->action_counts['total'] = count( $this->action_ids ); add_action( 'action_scheduler_execution_ignored', array( $this, 'on_action_ignored' ) ); add_action( 'action_scheduler_after_execute', array( $this, 'on_action_executed' ) ); add_action( 'action_scheduler_failed_execution', array( $this, 'on_action_failed' ), 10, 2 ); add_action( 'action_scheduler_failed_validation', array( $this, 'on_action_invalid' ), 10, 2 ); } /** * Execute. * * @return void */ public function execute() { $runner = \ActionScheduler::runner(); $progress_bar = \WP_CLI\Utils\make_progress_bar( sprintf( /* translators: %d: number of actions */ _n( 'Executing %d action', 'Executing %d actions', $this->action_counts['total'], 'action-scheduler' ), number_format_i18n( $this->action_counts['total'] ) ), $this->action_counts['total'] ); foreach ( $this->action_ids as $action_id ) { $runner->process_action( $action_id, 'Action Scheduler CLI' ); $progress_bar->tick(); } $progress_bar->finish(); foreach ( array( 'ignored', 'invalid', 'failed', ) as $type ) { $count = $this->action_counts[ $type ]; if ( empty( $count ) ) { continue; } /* * translators: * %1$d: count of actions evaluated. * %2$s: type of action evaluated. */ $format = _n( '%1$d action %2$s.', '%1$d actions %2$s.', $count, 'action-scheduler' ); \WP_CLI::warning( sprintf( $format, number_format_i18n( $count ), $type ) ); } \WP_CLI::success( sprintf( /* translators: %d: number of executed actions */ _n( 'Executed %d action.', 'Executed %d actions.', $this->action_counts['executed'], 'action-scheduler' ), number_format_i18n( $this->action_counts['executed'] ) ) ); } /** * Action: action_scheduler_execution_ignored * * @param int $action_id Action ID. * @return void */ public function on_action_ignored( $action_id ) { if ( 'action_scheduler_execution_ignored' !== current_action() ) { return; } $action_id = absint( $action_id ); if ( ! in_array( $action_id, $this->action_ids, true ) ) { return; } $this->action_counts['ignored']++; \WP_CLI::debug( sprintf( 'Action %d was ignored.', $action_id ) ); } /** * Action: action_scheduler_after_execute * * @param int $action_id Action ID. * @return void */ public function on_action_executed( $action_id ) { if ( 'action_scheduler_after_execute' !== current_action() ) { return; } $action_id = absint( $action_id ); if ( ! in_array( $action_id, $this->action_ids, true ) ) { return; } $this->action_counts['executed']++; \WP_CLI::debug( sprintf( 'Action %d was executed.', $action_id ) ); } /** * Action: action_scheduler_failed_execution * * @param int $action_id Action ID. * @param \Exception $e Exception. * @return void */ public function on_action_failed( $action_id, \Exception $e ) { if ( 'action_scheduler_failed_execution' !== current_action() ) { return; } $action_id = absint( $action_id ); if ( ! in_array( $action_id, $this->action_ids, true ) ) { return; } $this->action_counts['failed']++; \WP_CLI::debug( sprintf( 'Action %d failed execution: %s', $action_id, $e->getMessage() ) ); } /** * Action: action_scheduler_failed_validation * * @param int $action_id Action ID. * @param \Exception $e Exception. * @return void */ public function on_action_invalid( $action_id, \Exception $e ) { if ( 'action_scheduler_failed_validation' !== current_action() ) { return; } $action_id = absint( $action_id ); if ( ! in_array( $action_id, $this->action_ids, true ) ) { return; } $this->action_counts['invalid']++; \WP_CLI::debug( sprintf( 'Action %d failed validation: %s', $action_id, $e->getMessage() ) ); } } Dependencies/ActionScheduler/classes/ActionScheduler_wcSystemStatus.php 0000644 00000012270 15174671745 0022555 0 ustar 00 <?php /** * Class ActionScheduler_wcSystemStatus */ class ActionScheduler_wcSystemStatus { /** * The active data stores * * @var ActionScheduler_Store */ protected $store; /** * Constructor method for ActionScheduler_wcSystemStatus. * * @param ActionScheduler_Store $store Active store object. * * @return void */ public function __construct( $store ) { $this->store = $store; } /** * Display action data, including number of actions grouped by status and the oldest & newest action in each status. * * Helpful to identify issues, like a clogged queue. */ public function render() { $action_counts = $this->store->action_counts(); $status_labels = $this->store->get_status_labels(); $oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) ); $this->get_template( $status_labels, $action_counts, $oldest_and_newest ); } /** * Get oldest and newest scheduled dates for a given set of statuses. * * @param array $status_keys Set of statuses to find oldest & newest action for. * @return array */ protected function get_oldest_and_newest( $status_keys ) { $oldest_and_newest = array(); foreach ( $status_keys as $status ) { $oldest_and_newest[ $status ] = array( 'oldest' => '–', 'newest' => '–', ); if ( 'in-progress' === $status ) { continue; } $oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' ); $oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' ); } return $oldest_and_newest; } /** * Get oldest or newest scheduled date for a given status. * * @param string $status Action status label/name string. * @param string $date_type Oldest or Newest. * @return DateTime */ protected function get_action_status_date( $status, $date_type = 'oldest' ) { $order = 'oldest' === $date_type ? 'ASC' : 'DESC'; $action = $this->store->query_actions( array( 'claimed' => false, 'status' => $status, 'per_page' => 1, 'order' => $order, ) ); if ( ! empty( $action ) ) { $date_object = $this->store->get_date( $action[0] ); $action_date = $date_object->format( 'Y-m-d H:i:s O' ); } else { $action_date = '–'; } return $action_date; } /** * Get oldest or newest scheduled date for a given status. * * @param array $status_labels Set of statuses to find oldest & newest action for. * @param array $action_counts Number of actions grouped by status. * @param array $oldest_and_newest Date of the oldest and newest action with each status. */ protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) { $as_version = ActionScheduler_Versions::instance()->latest_version(); $as_datastore = get_class( ActionScheduler_Store::instance() ); ?> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="5" data-export-label="Action Scheduler"><h2><?php esc_html_e( 'Action Scheduler', 'action-scheduler' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows details of Action Scheduler.', 'action-scheduler' ) ); ?></h2></th> </tr> <tr> <td colspan="2" data-export-label="Version"><?php esc_html_e( 'Version:', 'action-scheduler' ); ?></td> <td colspan="3"><?php echo esc_html( $as_version ); ?></td> </tr> <tr> <td colspan="2" data-export-label="Data store"><?php esc_html_e( 'Data store:', 'action-scheduler' ); ?></td> <td colspan="3"><?php echo esc_html( $as_datastore ); ?></td> </tr> <tr> <td><strong><?php esc_html_e( 'Action Status', 'action-scheduler' ); ?></strong></td> <td class="help"> </td> <td><strong><?php esc_html_e( 'Count', 'action-scheduler' ); ?></strong></td> <td><strong><?php esc_html_e( 'Oldest Scheduled Date', 'action-scheduler' ); ?></strong></td> <td><strong><?php esc_html_e( 'Newest Scheduled Date', 'action-scheduler' ); ?></strong></td> </tr> </thead> <tbody> <?php foreach ( $action_counts as $status => $count ) { // WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column. printf( '<tr><td>%1$s</td><td> </td><td>%2$s<span style="display: none;">, Oldest: %3$s, Newest: %4$s</span></td><td>%3$s</td><td>%4$s</td></tr>', esc_html( $status_labels[ $status ] ), esc_html( number_format_i18n( $count ) ), esc_html( $oldest_and_newest[ $status ]['oldest'] ), esc_html( $oldest_and_newest[ $status ]['newest'] ) ); } ?> </tbody> </table> <?php } /** * Is triggered when invoking inaccessible methods in an object context. * * @param string $name Name of method called. * @param array $arguments Parameters to invoke the method with. * * @return mixed * @link https://php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.methods */ public function __call( $name, $arguments ) { switch ( $name ) { case 'print': _deprecated_function( __CLASS__ . '::print()', '2.2.4', __CLASS__ . '::render()' ); return call_user_func_array( array( $this, 'render' ), $arguments ); } return null; } } Dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php 0000644 00000017607 15174671745 0022122 0 ustar 00 <?php /** * Class ActionScheduler_QueueCleaner */ class ActionScheduler_QueueCleaner { /** * The batch size. * * @var int */ protected $batch_size; /** * ActionScheduler_Store instance. * * @var ActionScheduler_Store */ private $store = null; /** * 31 days in seconds. * * @var int */ private $month_in_seconds = 2678400; /** * Default list of statuses purged by the cleaner process. * * @var string[] */ private $default_statuses_to_purge = array( ActionScheduler_Store::STATUS_COMPLETE, ActionScheduler_Store::STATUS_CANCELED, ); /** * ActionScheduler_QueueCleaner constructor. * * @param ActionScheduler_Store|null $store The store instance. * @param int $batch_size The batch size. */ public function __construct( ?ActionScheduler_Store $store = null, $batch_size = 20 ) { $this->store = $store ? $store : ActionScheduler_Store::instance(); $this->batch_size = $batch_size; } /** * Default queue cleaner process used by queue runner. * * @return array */ public function delete_old_actions() { /** * Filter the minimum scheduled date age for action deletion. * * @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted. */ $lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds ); try { $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' ); } catch ( Exception $e ) { _doing_it_wrong( __METHOD__, sprintf( /* Translators: %s is the exception message. */ esc_html__( 'It was not possible to determine a valid cut-off time: %s.', 'action-scheduler' ), esc_html( $e->getMessage() ) ), '3.5.5' ); return array(); } /** * Filter the statuses when cleaning the queue. * * @param string[] $default_statuses_to_purge Action statuses to clean. */ $statuses_to_purge = (array) apply_filters( 'action_scheduler_default_cleaner_statuses', $this->default_statuses_to_purge ); return $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() ); } /** * Delete selected actions limited by status and date. * * @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete. * @param DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago. * @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20. * @param string $context Calling process context. Defaults to `old`. * @return array Actions deleted. */ public function clean_actions( array $statuses_to_purge, DateTime $cutoff_date, $batch_size = null, $context = 'old' ) { $batch_size = ! is_null( $batch_size ) ? $batch_size : $this->batch_size; $cutoff = ! is_null( $cutoff_date ) ? $cutoff_date : as_get_datetime_object( $this->month_in_seconds . ' seconds ago' ); $lifespan = time() - $cutoff->getTimestamp(); if ( empty( $statuses_to_purge ) ) { $statuses_to_purge = $this->default_statuses_to_purge; } $deleted_actions = array(); foreach ( $statuses_to_purge as $status ) { $actions_to_delete = $this->store->query_actions( array( 'status' => $status, 'modified' => $cutoff, 'modified_compare' => '<=', 'per_page' => $batch_size, 'orderby' => 'none', ) ); $deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) ); } return $deleted_actions; } /** * Delete actions. * * @param int[] $actions_to_delete List of action IDs to delete. * @param int $lifespan Minimum scheduled age in seconds of the actions being deleted. * @param string $context Context of the delete request. * @return array Deleted action IDs. */ private function delete_actions( array $actions_to_delete, $lifespan = null, $context = 'old' ) { $deleted_actions = array(); if ( is_null( $lifespan ) ) { $lifespan = $this->month_in_seconds; } foreach ( $actions_to_delete as $action_id ) { try { $this->store->delete_action( $action_id ); $deleted_actions[] = $action_id; } catch ( Exception $e ) { /** * Notify 3rd party code of exceptions when deleting a completed action older than the retention period * * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their * actions. * * @param int $action_id The scheduled actions ID in the data store * @param Exception $e The exception thrown when attempting to delete the action from the data store * @param int $lifespan The retention period, in seconds, for old actions * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch * @since 2.0.0 */ do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) ); } } return $deleted_actions; } /** * Unclaim pending actions that have not been run within a given time limit. * * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed * as a parameter is 10x the time limit used for queue processing. * * @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes). */ public function reset_timeouts( $time_limit = 300 ) { $timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit ); if ( $timeout < 0 ) { return; } $cutoff = as_get_datetime_object( $timeout . ' seconds ago' ); $actions_to_reset = $this->store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_PENDING, 'modified' => $cutoff, 'modified_compare' => '<=', 'claimed' => true, 'per_page' => $this->get_batch_size(), 'orderby' => 'none', ) ); foreach ( $actions_to_reset as $action_id ) { $this->store->unclaim_action( $action_id ); do_action( 'action_scheduler_reset_action', $action_id ); } } /** * Mark actions that have been running for more than a given time limit as failed, based on * the assumption some uncatchable and unloggable fatal error occurred during processing. * * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed * as a parameter is 10x the time limit used for queue processing. * * @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes). */ public function mark_failures( $time_limit = 300 ) { $timeout = apply_filters( 'action_scheduler_failure_period', $time_limit ); if ( $timeout < 0 ) { return; } $cutoff = as_get_datetime_object( $timeout . ' seconds ago' ); $actions_to_reset = $this->store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_RUNNING, 'modified' => $cutoff, 'modified_compare' => '<=', 'per_page' => $this->get_batch_size(), 'orderby' => 'none', ) ); foreach ( $actions_to_reset as $action_id ) { $this->store->mark_failure( $action_id ); do_action( 'action_scheduler_failed_action', $action_id, $timeout ); } } /** * Do all of the cleaning actions. * * @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes). */ public function clean( $time_limit = 300 ) { $this->delete_old_actions(); $this->reset_timeouts( $time_limit ); $this->mark_failures( $time_limit ); } /** * Get the batch size for cleaning the queue. * * @return int */ protected function get_batch_size() { /** * Filter the batch size when cleaning the queue. * * @param int $batch_size The number of actions to clean in one batch. */ return absint( apply_filters( 'action_scheduler_cleanup_batch_size', $this->batch_size ) ); } } Dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php 0000644 00000007754 15174671745 0021627 0 ustar 00 <?php /** * Provide a way to set simple transient locks to block behaviour * for up-to a given duration. * * Class ActionScheduler_OptionLock * * @since 3.0.0 */ class ActionScheduler_OptionLock extends ActionScheduler_Lock { /** * Set a lock using options for a given amount of time (60 seconds by default). * * Using an autoloaded option avoids running database queries or other resource intensive tasks * on frequently triggered hooks, like 'init' or 'shutdown'. * * For example, ActionScheduler_QueueRunner->maybe_dispatch_async_request() uses a lock to avoid * calling ActionScheduler_QueueRunner->has_maximum_concurrent_batches() every time the 'shutdown', * hook is triggered, because that method calls ActionScheduler_QueueRunner->store->get_claim_count() * to find the current number of claims in the database. * * @param string $lock_type A string to identify different lock types. * @bool True if lock value has changed, false if not or if set failed. */ public function set( $lock_type ) { global $wpdb; $lock_key = $this->get_key( $lock_type ); $existing_lock_value = $this->get_existing_lock( $lock_type ); $new_lock_value = $this->new_lock_value( $lock_type ); // The lock may not exist yet, or may have been deleted. if ( empty( $existing_lock_value ) ) { return (bool) $wpdb->insert( $wpdb->options, array( 'option_name' => $lock_key, 'option_value' => $new_lock_value, 'autoload' => 'no', ) ); } if ( $this->get_expiration_from( $existing_lock_value ) >= time() ) { return false; } // Otherwise, try to obtain the lock. return (bool) $wpdb->update( $wpdb->options, array( 'option_value' => $new_lock_value ), array( 'option_name' => $lock_key, 'option_value' => $existing_lock_value, ) ); } /** * If a lock is set, return the timestamp it was set to expiry. * * @param string $lock_type A string to identify different lock types. * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. */ public function get_expiration( $lock_type ) { return $this->get_expiration_from( $this->get_existing_lock( $lock_type ) ); } /** * Given the lock string, derives the lock expiration timestamp (or false if it cannot be determined). * * @param string $lock_value String containing a timestamp, or pipe-separated combination of unique value and timestamp. * * @return false|int */ private function get_expiration_from( $lock_value ) { $lock_string = explode( '|', $lock_value ); // Old style lock? if ( count( $lock_string ) === 1 && is_numeric( $lock_string[0] ) ) { return (int) $lock_string[0]; } // New style lock? if ( count( $lock_string ) === 2 && is_numeric( $lock_string[1] ) ) { return (int) $lock_string[1]; } return false; } /** * Get the key to use for storing the lock in the transient * * @param string $lock_type A string to identify different lock types. * @return string */ protected function get_key( $lock_type ) { return sprintf( 'action_scheduler_lock_%s', $lock_type ); } /** * Supplies the existing lock value, or an empty string if not set. * * @param string $lock_type A string to identify different lock types. * * @return string */ private function get_existing_lock( $lock_type ) { global $wpdb; // Now grab the existing lock value, if there is one. return (string) $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s", $this->get_key( $lock_type ) ) ); } /** * Supplies a lock value consisting of a unique value and the current timestamp, which are separated by a pipe * character. * * Example: (string) "649de012e6b262.09774912|1688068114" * * @param string $lock_type A string to identify different lock types. * * @return string */ private function new_lock_value( $lock_type ) { return uniqid( '', true ) . '|' . ( time() + $this->get_duration( $lock_type ) ); } } Dependencies/ActionScheduler/classes/ActionScheduler_FatalErrorMonitor.php 0000644 00000005005 15174671745 0023142 0 ustar 00 <?php /** * Class ActionScheduler_FatalErrorMonitor */ class ActionScheduler_FatalErrorMonitor { /** * ActionScheduler_ActionClaim instance. * * @var ActionScheduler_ActionClaim */ private $claim = null; /** * ActionScheduler_Store instance. * * @var ActionScheduler_Store */ private $store = null; /** * Current action's ID. * * @var int */ private $action_id = 0; /** * Construct. * * @param ActionScheduler_Store $store Action store. */ public function __construct( ActionScheduler_Store $store ) { $this->store = $store; } /** * Start monitoring. * * @param ActionScheduler_ActionClaim $claim Claimed actions. */ public function attach( ActionScheduler_ActionClaim $claim ) { $this->claim = $claim; add_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) ); add_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0, 1 ); add_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0, 0 ); add_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0, 0 ); add_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0, 0 ); } /** * Stop monitoring. */ public function detach() { $this->claim = null; $this->untrack_action(); remove_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) ); remove_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0 ); remove_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0 ); remove_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0 ); remove_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0 ); } /** * Track specified action. * * @param int $action_id Action ID to track. */ public function track_current_action( $action_id ) { $this->action_id = $action_id; } /** * Un-track action. */ public function untrack_action() { $this->action_id = 0; } /** * Handle unexpected shutdown. */ public function handle_unexpected_shutdown() { $error = error_get_last(); if ( $error ) { if ( in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) { if ( ! empty( $this->action_id ) ) { $this->store->mark_failure( $this->action_id ); do_action( 'action_scheduler_unexpected_shutdown', $this->action_id, $error ); } } $this->store->release_claim( $this->claim ); } } } Dependencies/ActionScheduler/classes/actions/ActionScheduler_NullAction.php 0000644 00000001131 15174671745 0023235 0 ustar 00 <?php /** * Class ActionScheduler_NullAction */ class ActionScheduler_NullAction extends ActionScheduler_Action { /** * Construct. * * @param string $hook Action hook. * @param mixed[] $args Action arguments. * @param null|ActionScheduler_Schedule $schedule Action schedule. */ public function __construct( $hook = '', array $args = array(), ?ActionScheduler_Schedule $schedule = null ) { $this->set_schedule( new ActionScheduler_NullSchedule() ); } /** * Execute action. */ public function execute() { // don't execute. } } Dependencies/ActionScheduler/classes/actions/ActionScheduler_CanceledAction.php 0000644 00000001563 15174671745 0024032 0 ustar 00 <?php /** * Class ActionScheduler_CanceledAction * * Stored action which was canceled and therefore acts like a finished action but should always return a null schedule, * regardless of schedule passed to its constructor. */ class ActionScheduler_CanceledAction extends ActionScheduler_FinishedAction { /** * Construct. * * @param string $hook Action's hook. * @param array $args Action's arguments. * @param null|ActionScheduler_Schedule $schedule Action's schedule. * @param string $group Action's group. */ public function __construct( $hook, array $args = array(), ?ActionScheduler_Schedule $schedule = null, $group = '' ) { parent::__construct( $hook, $args, $schedule, $group ); if ( is_null( $schedule ) ) { $this->set_schedule( new ActionScheduler_NullSchedule() ); } } } Dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php 0000644 00000007510 15174671745 0022411 0 ustar 00 <?php /** * Class ActionScheduler_Action */ class ActionScheduler_Action { /** * Action's hook. * * @var string */ protected $hook = ''; /** * Action's args. * * @var array<string, mixed> */ protected $args = array(); /** * Action's schedule. * * @var ActionScheduler_Schedule */ protected $schedule = null; /** * Action's group. * * @var string */ protected $group = ''; /** * Priorities are conceptually similar to those used for regular WordPress actions. * Like those, a lower priority takes precedence over a higher priority and the default * is 10. * * Unlike regular WordPress actions, the priority of a scheduled action is strictly an * integer and should be kept within the bounds 0-255 (anything outside the bounds will * be brought back into the acceptable range). * * @var int */ protected $priority = 10; /** * Construct. * * @param string $hook Action's hook. * @param mixed[] $args Action's arguments. * @param null|ActionScheduler_Schedule $schedule Action's schedule. * @param string $group Action's group. */ public function __construct( $hook, array $args = array(), ?ActionScheduler_Schedule $schedule = null, $group = '' ) { $schedule = empty( $schedule ) ? new ActionScheduler_NullSchedule() : $schedule; $this->set_hook( $hook ); $this->set_schedule( $schedule ); $this->set_args( $args ); $this->set_group( $group ); } /** * Executes the action. * * If no callbacks are registered, an exception will be thrown and the action will not be * fired. This is useful to help detect cases where the code responsible for setting up * a scheduled action no longer exists. * * @throws Exception If no callbacks are registered for this action. */ public function execute() { $hook = $this->get_hook(); if ( ! has_action( $hook ) ) { throw new Exception( sprintf( /* translators: 1: action hook. */ __( 'Scheduled action for %1$s will not be executed as no callbacks are registered.', 'action-scheduler' ), $hook ) ); } do_action_ref_array( $hook, array_values( $this->get_args() ) ); } /** * Set action's hook. * * @param string $hook Action's hook. */ protected function set_hook( $hook ) { $this->hook = $hook; } /** * Get action's hook. */ public function get_hook() { return $this->hook; } /** * Set action's schedule. * * @param ActionScheduler_Schedule $schedule Action's schedule. */ protected function set_schedule( ActionScheduler_Schedule $schedule ) { $this->schedule = $schedule; } /** * Action's schedule. * * @return ActionScheduler_Schedule */ public function get_schedule() { return $this->schedule; } /** * Set action's args. * * @param mixed[] $args Action's arguments. */ protected function set_args( array $args ) { $this->args = $args; } /** * Get action's args. */ public function get_args() { return $this->args; } /** * Section action's group. * * @param string $group Action's group. */ protected function set_group( $group ) { $this->group = $group; } /** * Action's group. * * @return string */ public function get_group() { return $this->group; } /** * Action has not finished. * * @return bool */ public function is_finished() { return false; } /** * Sets the priority of the action. * * @param int $priority Priority level (lower is higher priority). Should be in the range 0-255. * * @return void */ public function set_priority( $priority ) { if ( $priority < 0 ) { $priority = 0; } elseif ( $priority > 255 ) { $priority = 255; } $this->priority = (int) $priority; } /** * Gets the action priority. * * @return int */ public function get_priority() { return $this->priority; } } Dependencies/ActionScheduler/classes/actions/ActionScheduler_FinishedAction.php 0000644 00000000450 15174671745 0024057 0 ustar 00 <?php /** * Class ActionScheduler_FinishedAction */ class ActionScheduler_FinishedAction extends ActionScheduler_Action { /** * Execute action. */ public function execute() { // don't execute. } /** * Get finished state. */ public function is_finished() { return true; } } Dependencies/ActionScheduler/classes/migration/ActionScheduler_DBStoreMigrator.php 0000644 00000003453 15174671745 0024536 0 ustar 00 <?php /** * Class ActionScheduler_DBStoreMigrator * * A class for direct saving of actions to the table data store during migration. * * @since 3.0.0 */ class ActionScheduler_DBStoreMigrator extends ActionScheduler_DBStore { /** * Save an action with optional last attempt date. * * Normally, saving an action sets its attempted date to 0000-00-00 00:00:00 because when an action is first saved, * it can't have been attempted yet, but migrated completed actions will have an attempted date, so we need to save * that when first saving the action. * * @param ActionScheduler_Action $action Action to migrate. * @param null|DateTime $scheduled_date Optional date of the first instance to store. * @param null|DateTime $last_attempt_date Optional date the action was last attempted. * * @return string The action ID * @throws \RuntimeException When the action is not saved. */ public function save_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null, ?DateTime $last_attempt_date = null ) { try { /** * Global. * * @var \wpdb $wpdb */ global $wpdb; $action_id = parent::save_action( $action, $scheduled_date ); if ( null !== $last_attempt_date ) { $data = array( 'last_attempt_gmt' => $this->get_scheduled_date_string( $action, $last_attempt_date ), 'last_attempt_local' => $this->get_scheduled_date_string_local( $action, $last_attempt_date ), ); $wpdb->update( $wpdb->actionscheduler_actions, $data, array( 'action_id' => $action_id ), array( '%s', '%s' ), array( '%d' ) ); } return $action_id; } catch ( \Exception $e ) { // translators: %s is an error message. throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); } } } Dependencies/ActionScheduler/classes/migration/LogMigrator.php 0000644 00000002441 15174671745 0020615 0 ustar 00 <?php namespace Action_Scheduler\Migration; use ActionScheduler_Logger; /** * Class LogMigrator * * @package Action_Scheduler\Migration * * @since 3.0.0 * * @codeCoverageIgnore */ class LogMigrator { /** * Source logger instance. * * @var ActionScheduler_Logger */ private $source; /** * Destination logger instance. * * @var ActionScheduler_Logger */ private $destination; /** * ActionMigrator constructor. * * @param ActionScheduler_Logger $source_logger Source logger object. * @param ActionScheduler_Logger $destination_logger Destination logger object. */ public function __construct( ActionScheduler_Logger $source_logger, ActionScheduler_Logger $destination_logger ) { $this->source = $source_logger; $this->destination = $destination_logger; } /** * Migrate an action log. * * @param int $source_action_id Source logger object. * @param int $destination_action_id Destination logger object. */ public function migrate( $source_action_id, $destination_action_id ) { $logs = $this->source->get_logs( $source_action_id ); foreach ( $logs as $log ) { if ( absint( $log->get_action_id() ) === absint( $source_action_id ) ) { $this->destination->log( $destination_action_id, $log->get_message(), $log->get_date() ); } } } } Dependencies/ActionScheduler/classes/migration/DryRun_LogMigrator.php 0000644 00000000713 15174671745 0022120 0 ustar 00 <?php namespace Action_Scheduler\Migration; /** * Class DryRun_LogMigrator * * @package Action_Scheduler\Migration * * @codeCoverageIgnore */ class DryRun_LogMigrator extends LogMigrator { /** * Simulate migrating an action log. * * @param int $source_action_id Source logger object. * @param int $destination_action_id Destination logger object. */ public function migrate( $source_action_id, $destination_action_id ) { // no-op. } } Dependencies/ActionScheduler/classes/migration/BatchFetcher.php 0000644 00000003350 15174671745 0020711 0 ustar 00 <?php namespace Action_Scheduler\Migration; use ActionScheduler_Store as Store; /** * Class BatchFetcher * * @package Action_Scheduler\Migration * * @since 3.0.0 * * @codeCoverageIgnore */ class BatchFetcher { /** * Store instance. * * @var ActionScheduler_Store */ private $store; /** * BatchFetcher constructor. * * @param ActionScheduler_Store $source_store Source store object. */ public function __construct( Store $source_store ) { $this->store = $source_store; } /** * Retrieve a list of actions. * * @param int $count The number of actions to retrieve. * * @return int[] A list of action IDs */ public function fetch( $count = 10 ) { foreach ( $this->get_query_strategies( $count ) as $query ) { $action_ids = $this->store->query_actions( $query ); if ( ! empty( $action_ids ) ) { return $action_ids; } } return array(); } /** * Generate a list of prioritized of action search parameters. * * @param int $count Number of actions to find. * * @return array */ private function get_query_strategies( $count ) { $now = as_get_datetime_object(); $args = array( 'date' => $now, 'per_page' => $count, 'offset' => 0, 'orderby' => 'date', 'order' => 'ASC', ); $priorities = array( Store::STATUS_PENDING, Store::STATUS_FAILED, Store::STATUS_CANCELED, Store::STATUS_COMPLETE, Store::STATUS_RUNNING, '', // any other unanticipated status. ); foreach ( $priorities as $status ) { yield wp_parse_args( array( 'status' => $status, 'date_compare' => '<=', ), $args ); yield wp_parse_args( array( 'status' => $status, 'date_compare' => '>=', ), $args ); } } } Dependencies/ActionScheduler/classes/migration/DryRun_ActionMigrator.php 0000644 00000001052 15174671745 0022611 0 ustar 00 <?php namespace Action_Scheduler\Migration; /** * Class DryRun_ActionMigrator * * @package Action_Scheduler\Migration * * @since 3.0.0 * * @codeCoverageIgnore */ class DryRun_ActionMigrator extends ActionMigrator { /** * Simulate migrating an action. * * @param int $source_action_id Action ID. * * @return int */ public function migrate( $source_action_id ) { do_action( 'action_scheduler/migrate_action_dry_run', $source_action_id ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores return 0; } } Dependencies/ActionScheduler/classes/migration/ActionMigrator.php 0000644 00000010253 15174671745 0021311 0 ustar 00 <?php namespace Action_Scheduler\Migration; /** * Class ActionMigrator * * @package Action_Scheduler\Migration * * @since 3.0.0 * * @codeCoverageIgnore */ class ActionMigrator { /** * Source store instance. * * @var ActionScheduler_Store */ private $source; /** * Destination store instance. * * @var ActionScheduler_Store */ private $destination; /** * LogMigrator instance. * * @var LogMigrator */ private $log_migrator; /** * ActionMigrator constructor. * * @param \ActionScheduler_Store $source_store Source store object. * @param \ActionScheduler_Store $destination_store Destination store object. * @param LogMigrator $log_migrator Log migrator object. */ public function __construct( \ActionScheduler_Store $source_store, \ActionScheduler_Store $destination_store, LogMigrator $log_migrator ) { $this->source = $source_store; $this->destination = $destination_store; $this->log_migrator = $log_migrator; } /** * Migrate an action. * * @param int $source_action_id Action ID. * * @return int 0|new action ID * @throws \RuntimeException When unable to delete action from the source store. */ public function migrate( $source_action_id ) { try { $action = $this->source->fetch_action( $source_action_id ); $status = $this->source->get_status( $source_action_id ); } catch ( \Exception $e ) { $action = null; $status = ''; } if ( is_null( $action ) || empty( $status ) || ! $action->get_schedule()->get_date() ) { // null action or empty status means the fetch operation failed or the action didn't exist. // null schedule means it's missing vital data. // delete it and move on. try { $this->source->delete_action( $source_action_id ); } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // nothing to do, it didn't exist in the first place. } do_action( 'action_scheduler/no_action_to_migrate', $source_action_id, $this->source, $this->destination ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores return 0; } try { // Make sure the last attempt date is set correctly for completed and failed actions. $last_attempt_date = ( \ActionScheduler_Store::STATUS_PENDING !== $status ) ? $this->source->get_date( $source_action_id ) : null; $destination_action_id = $this->destination->save_action( $action, null, $last_attempt_date ); } catch ( \Exception $e ) { do_action( 'action_scheduler/migrate_action_failed', $source_action_id, $this->source, $this->destination ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores return 0; // could not save the action in the new store. } try { switch ( $status ) { case \ActionScheduler_Store::STATUS_FAILED: $this->destination->mark_failure( $destination_action_id ); break; case \ActionScheduler_Store::STATUS_CANCELED: $this->destination->cancel_action( $destination_action_id ); break; } $this->log_migrator->migrate( $source_action_id, $destination_action_id ); $this->source->delete_action( $source_action_id ); $test_action = $this->source->fetch_action( $source_action_id ); if ( ! is_a( $test_action, 'ActionScheduler_NullAction' ) ) { // translators: %s is an action ID. throw new \RuntimeException( sprintf( __( 'Unable to remove source migrated action %s', 'action-scheduler' ), $source_action_id ) ); } do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores return $destination_action_id; } catch ( \Exception $e ) { // could not delete from the old store. $this->source->mark_migrated( $source_action_id ); // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores do_action( 'action_scheduler/migrate_action_incomplete', $source_action_id, $destination_action_id, $this->source, $this->destination ); do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); // phpcs:enable return $destination_action_id; } } } Dependencies/ActionScheduler/classes/migration/Runner.php 0000644 00000010174 15174671745 0017642 0 ustar 00 <?php namespace Action_Scheduler\Migration; /** * Class Runner * * @package Action_Scheduler\Migration * * @since 3.0.0 * * @codeCoverageIgnore */ class Runner { /** * Source store instance. * * @var ActionScheduler_Store */ private $source_store; /** * Destination store instance. * * @var ActionScheduler_Store */ private $destination_store; /** * Source logger instance. * * @var ActionScheduler_Logger */ private $source_logger; /** * Destination logger instance. * * @var ActionScheduler_Logger */ private $destination_logger; /** * Batch fetcher instance. * * @var BatchFetcher */ private $batch_fetcher; /** * Action migrator instance. * * @var ActionMigrator */ private $action_migrator; /** * Log migrator instance. * * @var LogMigrator */ private $log_migrator; /** * Progress bar instance. * * @var ProgressBar */ private $progress_bar; /** * Runner constructor. * * @param Config $config Migration configuration object. */ public function __construct( Config $config ) { $this->source_store = $config->get_source_store(); $this->destination_store = $config->get_destination_store(); $this->source_logger = $config->get_source_logger(); $this->destination_logger = $config->get_destination_logger(); $this->batch_fetcher = new BatchFetcher( $this->source_store ); if ( $config->get_dry_run() ) { $this->log_migrator = new DryRun_LogMigrator( $this->source_logger, $this->destination_logger ); $this->action_migrator = new DryRun_ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator ); } else { $this->log_migrator = new LogMigrator( $this->source_logger, $this->destination_logger ); $this->action_migrator = new ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator ); } if ( defined( 'WP_CLI' ) && WP_CLI ) { $this->progress_bar = $config->get_progress_bar(); } } /** * Run migration batch. * * @param int $batch_size Optional batch size. Default 10. * * @return int Size of batch processed. */ public function run( $batch_size = 10 ) { $batch = $this->batch_fetcher->fetch( $batch_size ); $batch_size = count( $batch ); if ( ! $batch_size ) { return 0; } if ( $this->progress_bar ) { /* translators: %d: amount of actions */ $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), $batch_size ) ); $this->progress_bar->set_count( $batch_size ); } $this->migrate_actions( $batch ); return $batch_size; } /** * Migration a batch of actions. * * @param array $action_ids List of action IDs to migrate. */ public function migrate_actions( array $action_ids ) { do_action( 'action_scheduler/migration_batch_starting', $action_ids ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores \ActionScheduler::logger()->unhook_stored_action(); $this->destination_logger->unhook_stored_action(); foreach ( $action_ids as $source_action_id ) { $destination_action_id = $this->action_migrator->migrate( $source_action_id ); if ( $destination_action_id ) { $this->destination_logger->log( $destination_action_id, sprintf( /* translators: 1: source action ID 2: source store class 3: destination action ID 4: destination store class */ __( 'Migrated action with ID %1$d in %2$s to ID %3$d in %4$s', 'action-scheduler' ), $source_action_id, get_class( $this->source_store ), $destination_action_id, get_class( $this->destination_store ) ) ); } if ( $this->progress_bar ) { $this->progress_bar->tick(); } } if ( $this->progress_bar ) { $this->progress_bar->finish(); } \ActionScheduler::logger()->hook_stored_action(); do_action( 'action_scheduler/migration_batch_complete', $action_ids ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } /** * Initialize destination store and logger. */ public function init_destination() { $this->destination_store->init(); $this->destination_logger->init(); } } Dependencies/ActionScheduler/classes/migration/Scheduler.php 0000644 00000006050 15174671745 0020305 0 ustar 00 <?php namespace Action_Scheduler\Migration; /** * Class Scheduler * * @package Action_Scheduler\WP_CLI * * @since 3.0.0 * * @codeCoverageIgnore */ class Scheduler { /** Migration action hook. */ const HOOK = 'action_scheduler/migration_hook'; /** Migration action group. */ const GROUP = 'action-scheduler-migration'; /** * Set up the callback for the scheduled job. */ public function hook() { add_action( self::HOOK, array( $this, 'run_migration' ), 10, 0 ); } /** * Remove the callback for the scheduled job. */ public function unhook() { remove_action( self::HOOK, array( $this, 'run_migration' ), 10 ); } /** * The migration callback. */ public function run_migration() { $migration_runner = $this->get_migration_runner(); $count = $migration_runner->run( $this->get_batch_size() ); if ( 0 === $count ) { $this->mark_complete(); } else { $this->schedule_migration( time() + $this->get_schedule_interval() ); } } /** * Mark the migration complete. */ public function mark_complete() { $this->unschedule_migration(); \ActionScheduler_DataController::mark_migration_complete(); do_action( 'action_scheduler/migration_complete' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } /** * Get a flag indicating whether the migration is scheduled. * * @return bool Whether there is a pending action in the store to handle the migration */ public function is_migration_scheduled() { $next = as_next_scheduled_action( self::HOOK ); return ! empty( $next ); } /** * Schedule the migration. * * @param int $when Optional timestamp to run the next migration batch. Defaults to now. * * @return string The action ID */ public function schedule_migration( $when = 0 ) { $next = as_next_scheduled_action( self::HOOK ); if ( ! empty( $next ) ) { return $next; } if ( empty( $when ) ) { $when = time() + MINUTE_IN_SECONDS; } return as_schedule_single_action( $when, self::HOOK, array(), self::GROUP ); } /** * Remove the scheduled migration action. */ public function unschedule_migration() { as_unschedule_action( self::HOOK, null, self::GROUP ); } /** * Get migration batch schedule interval. * * @return int Seconds between migration runs. Defaults to 0 seconds to allow chaining migration via Async Runners. */ private function get_schedule_interval() { return (int) apply_filters( 'action_scheduler/migration_interval', 0 ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } /** * Get migration batch size. * * @return int Number of actions to migrate in each batch. Defaults to 250. */ private function get_batch_size() { return (int) apply_filters( 'action_scheduler/migration_batch_size', 250 ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } /** * Get migration runner object. * * @return Runner */ private function get_migration_runner() { $config = Controller::instance()->get_migration_config_object(); return new Runner( $config ); } } Dependencies/ActionScheduler/classes/migration/Config.php 0000644 00000010156 15174671745 0017576 0 ustar 00 <?php namespace Action_Scheduler\Migration; use Action_Scheduler\WP_CLI\ProgressBar; use ActionScheduler_Logger as Logger; use ActionScheduler_Store as Store; /** * Class Config * * @package Action_Scheduler\Migration * * @since 3.0.0 * * A config builder for the ActionScheduler\Migration\Runner class */ class Config { /** * Source store instance. * * @var ActionScheduler_Store */ private $source_store; /** * Source logger instance. * * @var ActionScheduler_Logger */ private $source_logger; /** * Destination store instance. * * @var ActionScheduler_Store */ private $destination_store; /** * Destination logger instance. * * @var ActionScheduler_Logger */ private $destination_logger; /** * Progress bar object. * * @var Action_Scheduler\WP_CLI\ProgressBar */ private $progress_bar; /** * Flag indicating a dryrun. * * @var bool */ private $dry_run = false; /** * Config constructor. */ public function __construct() { } /** * Get the configured source store. * * @return ActionScheduler_Store * @throws \RuntimeException When source store is not configured. */ public function get_source_store() { if ( empty( $this->source_store ) ) { throw new \RuntimeException( __( 'Source store must be configured before running a migration', 'action-scheduler' ) ); } return $this->source_store; } /** * Set the configured source store. * * @param ActionScheduler_Store $store Source store object. */ public function set_source_store( Store $store ) { $this->source_store = $store; } /** * Get the configured source logger. * * @return ActionScheduler_Logger * @throws \RuntimeException When source logger is not configured. */ public function get_source_logger() { if ( empty( $this->source_logger ) ) { throw new \RuntimeException( __( 'Source logger must be configured before running a migration', 'action-scheduler' ) ); } return $this->source_logger; } /** * Set the configured source logger. * * @param ActionScheduler_Logger $logger Logger object. */ public function set_source_logger( Logger $logger ) { $this->source_logger = $logger; } /** * Get the configured destination store. * * @return ActionScheduler_Store * @throws \RuntimeException When destination store is not configured. */ public function get_destination_store() { if ( empty( $this->destination_store ) ) { throw new \RuntimeException( __( 'Destination store must be configured before running a migration', 'action-scheduler' ) ); } return $this->destination_store; } /** * Set the configured destination store. * * @param ActionScheduler_Store $store Action store object. */ public function set_destination_store( Store $store ) { $this->destination_store = $store; } /** * Get the configured destination logger. * * @return ActionScheduler_Logger * @throws \RuntimeException When destination logger is not configured. */ public function get_destination_logger() { if ( empty( $this->destination_logger ) ) { throw new \RuntimeException( __( 'Destination logger must be configured before running a migration', 'action-scheduler' ) ); } return $this->destination_logger; } /** * Set the configured destination logger. * * @param ActionScheduler_Logger $logger Logger object. */ public function set_destination_logger( Logger $logger ) { $this->destination_logger = $logger; } /** * Get flag indicating whether it's a dry run. * * @return bool */ public function get_dry_run() { return $this->dry_run; } /** * Set flag indicating whether it's a dry run. * * @param bool $dry_run Dry run toggle. */ public function set_dry_run( $dry_run ) { $this->dry_run = (bool) $dry_run; } /** * Get progress bar object. * * @return ActionScheduler\WPCLI\ProgressBar */ public function get_progress_bar() { return $this->progress_bar; } /** * Set progress bar object. * * @param ActionScheduler\WPCLI\ProgressBar $progress_bar Progress bar object. */ public function set_progress_bar( ProgressBar $progress_bar ) { $this->progress_bar = $progress_bar; } } Dependencies/ActionScheduler/classes/migration/Controller.php 0000644 00000014574 15174671745 0020524 0 ustar 00 <?php namespace Action_Scheduler\Migration; use ActionScheduler_DataController; use ActionScheduler_LoggerSchema; use ActionScheduler_StoreSchema; use Action_Scheduler\WP_CLI\ProgressBar; /** * Class Controller * * The main plugin/initialization class for migration to custom tables. * * @package Action_Scheduler\Migration * * @since 3.0.0 * * @codeCoverageIgnore */ class Controller { /** * Instance. * * @var self */ private static $instance; /** * Scheduler instance. * * @var Action_Scheduler\Migration\Scheduler */ private $migration_scheduler; /** * Class name of the store object. * * @var string */ private $store_classname; /** * Class name of the logger object. * * @var string */ private $logger_classname; /** * Flag to indicate migrating custom store. * * @var bool */ private $migrate_custom_store; /** * Controller constructor. * * @param Scheduler $migration_scheduler Migration scheduler object. */ protected function __construct( Scheduler $migration_scheduler ) { $this->migration_scheduler = $migration_scheduler; $this->store_classname = ''; } /** * Set the action store class name. * * @param string $class Classname of the store class. * * @return string */ public function get_store_class( $class ) { if ( \ActionScheduler_DataController::is_migration_complete() ) { return \ActionScheduler_DataController::DATASTORE_CLASS; } elseif ( \ActionScheduler_Store::DEFAULT_CLASS !== $class ) { $this->store_classname = $class; return $class; } else { return 'ActionScheduler_HybridStore'; } } /** * Set the action logger class name. * * @param string $class Classname of the logger class. * * @return string */ public function get_logger_class( $class ) { \ActionScheduler_Store::instance(); if ( $this->has_custom_datastore() ) { $this->logger_classname = $class; return $class; } else { return \ActionScheduler_DataController::LOGGER_CLASS; } } /** * Get flag indicating whether a custom datastore is in use. * * @return bool */ public function has_custom_datastore() { return (bool) $this->store_classname; } /** * Set up the background migration process. * * @return void */ public function schedule_migration() { $logging_tables = new ActionScheduler_LoggerSchema(); $store_tables = new ActionScheduler_StoreSchema(); /* * In some unusual cases, the expected tables may not have been created. In such cases * we do not schedule a migration as doing so will lead to fatal error conditions. * * In such cases the user will likely visit the Tools > Scheduled Actions screen to * investigate, and will see appropriate messaging (this step also triggers an attempt * to rebuild any missing tables). * * @see https://github.com/woocommerce/action-scheduler/issues/653 */ if ( ActionScheduler_DataController::is_migration_complete() || $this->migration_scheduler->is_migration_scheduled() || ! $store_tables->tables_exist() || ! $logging_tables->tables_exist() ) { return; } $this->migration_scheduler->schedule_migration(); } /** * Get the default migration config object * * @return ActionScheduler\Migration\Config */ public function get_migration_config_object() { static $config = null; if ( ! $config ) { $source_store = $this->store_classname ? new $this->store_classname() : new \ActionScheduler_wpPostStore(); $source_logger = $this->logger_classname ? new $this->logger_classname() : new \ActionScheduler_wpCommentLogger(); $config = new Config(); $config->set_source_store( $source_store ); $config->set_source_logger( $source_logger ); $config->set_destination_store( new \ActionScheduler_DBStoreMigrator() ); $config->set_destination_logger( new \ActionScheduler_DBLogger() ); if ( defined( 'WP_CLI' ) && WP_CLI ) { $config->set_progress_bar( new ProgressBar( '', 0 ) ); } } return apply_filters( 'action_scheduler/migration_config', $config ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } /** * Hook dashboard migration notice. */ public function hook_admin_notices() { if ( ! $this->allow_migration() || \ActionScheduler_DataController::is_migration_complete() ) { return; } add_action( 'admin_notices', array( $this, 'display_migration_notice' ), 10, 0 ); } /** * Show a dashboard notice that migration is in progress. */ public function display_migration_notice() { printf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html__( 'Action Scheduler migration in progress. The list of scheduled actions may be incomplete.', 'action-scheduler' ) ); } /** * Add store classes. Hook migration. */ private function hook() { add_filter( 'action_scheduler_store_class', array( $this, 'get_store_class' ), 100, 1 ); add_filter( 'action_scheduler_logger_class', array( $this, 'get_logger_class' ), 100, 1 ); add_action( 'init', array( $this, 'maybe_hook_migration' ) ); add_action( 'wp_loaded', array( $this, 'schedule_migration' ) ); // Action Scheduler may be displayed as a Tools screen or WooCommerce > Status administration screen. add_action( 'load-tools_page_action-scheduler', array( $this, 'hook_admin_notices' ), 10, 0 ); add_action( 'load-woocommerce_page_wc-status', array( $this, 'hook_admin_notices' ), 10, 0 ); } /** * Possibly hook the migration scheduler action. */ public function maybe_hook_migration() { if ( ! $this->allow_migration() || \ActionScheduler_DataController::is_migration_complete() ) { return; } $this->migration_scheduler->hook(); } /** * Allow datastores to enable migration to AS tables. */ public function allow_migration() { if ( ! \ActionScheduler_DataController::dependencies_met() ) { return false; } if ( null === $this->migrate_custom_store ) { $this->migrate_custom_store = apply_filters( 'action_scheduler_migrate_data_store', false ); } return ( ! $this->has_custom_datastore() ) || $this->migrate_custom_store; } /** * Proceed with the migration if the dependencies have been met. */ public static function init() { if ( \ActionScheduler_DataController::dependencies_met() ) { self::instance()->hook(); } } /** * Singleton factory. */ public static function instance() { if ( ! isset( self::$instance ) ) { self::$instance = new static( new Scheduler() ); } return self::$instance; } } Dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php 0000644 00000007500 15174671745 0022344 0 ustar 00 <?php /** * Class ActionScheduler_Compatibility */ class ActionScheduler_Compatibility { /** * Converts a shorthand byte value to an integer byte value. * * Wrapper for wp_convert_hr_to_bytes(), moved to load.php in WordPress 4.6 from media.php * * @link https://secure.php.net/manual/en/function.ini-get.php * @link https://secure.php.net/manual/en/faq.using.php#faq.using.shorthandbytes * * @param string $value A (PHP ini) byte value, either shorthand or ordinary. * @return int An integer byte value. */ public static function convert_hr_to_bytes( $value ) { if ( function_exists( 'wp_convert_hr_to_bytes' ) ) { return wp_convert_hr_to_bytes( $value ); } $value = strtolower( trim( $value ) ); $bytes = (int) $value; if ( false !== strpos( $value, 'g' ) ) { $bytes *= GB_IN_BYTES; } elseif ( false !== strpos( $value, 'm' ) ) { $bytes *= MB_IN_BYTES; } elseif ( false !== strpos( $value, 'k' ) ) { $bytes *= KB_IN_BYTES; } // Deal with large (float) values which run into the maximum integer size. return min( $bytes, PHP_INT_MAX ); } /** * Attempts to raise the PHP memory limit for memory intensive processes. * * Only allows raising the existing limit and prevents lowering it. * * Wrapper for wp_raise_memory_limit(), added in WordPress v4.6.0 * * @return bool|int|string The limit that was set or false on failure. */ public static function raise_memory_limit() { if ( function_exists( 'wp_raise_memory_limit' ) ) { return wp_raise_memory_limit( 'admin' ); } $current_limit = @ini_get( 'memory_limit' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $current_limit_int = self::convert_hr_to_bytes( $current_limit ); if ( -1 === $current_limit_int ) { return false; } $wp_max_limit = WP_MAX_MEMORY_LIMIT; $wp_max_limit_int = self::convert_hr_to_bytes( $wp_max_limit ); $filtered_limit = apply_filters( 'admin_memory_limit', $wp_max_limit ); $filtered_limit_int = self::convert_hr_to_bytes( $filtered_limit ); // phpcs:disable WordPress.PHP.IniSet.memory_limit_Blacklisted // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged if ( -1 === $filtered_limit_int || ( $filtered_limit_int > $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) { if ( false !== @ini_set( 'memory_limit', $filtered_limit ) ) { return $filtered_limit; } else { return false; } } elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) { if ( false !== @ini_set( 'memory_limit', $wp_max_limit ) ) { return $wp_max_limit; } else { return false; } } // phpcs:enable return false; } /** * Attempts to raise the PHP timeout for time intensive processes. * * Only allows raising the existing limit and prevents lowering it. Wrapper for wc_set_time_limit(), when available. * * @param int $limit The time limit in seconds. */ public static function raise_time_limit( $limit = 0 ) { $limit = (int) $limit; $max_execution_time = (int) ini_get( 'max_execution_time' ); // If the max execution time is already set to zero (unlimited), there is no reason to make a further change. if ( 0 === $max_execution_time ) { return; } // Whichever of $max_execution_time or $limit is higher is the amount by which we raise the time limit. $raise_by = 0 === $limit || $limit > $max_execution_time ? $limit : $max_execution_time; if ( function_exists( 'wc_set_time_limit' ) ) { wc_set_time_limit( $raise_by ); } elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved @set_time_limit( $raise_by ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } } } Dependencies/ActionScheduler/classes/ActionScheduler_LogEntry.php 0000644 00000003626 15174671745 0021303 0 ustar 00 <?php /** * Class ActionScheduler_LogEntry */ class ActionScheduler_LogEntry { /** * Action's ID for log entry. * * @var int $action_id */ protected $action_id = ''; /** * Log entry's message. * * @var string $message */ protected $message = ''; /** * Log entry's date. * * @var Datetime $date */ protected $date; /** * Constructor * * @param mixed $action_id Action ID. * @param string $message Message. * @param Datetime $date Datetime object with the time when this log entry was created. If this parameter is * not provided a new Datetime object (with current time) will be created. */ public function __construct( $action_id, $message, $date = null ) { /* * ActionScheduler_wpCommentLogger::get_entry() previously passed a 3rd param of $comment->comment_type * to ActionScheduler_LogEntry::__construct(), goodness knows why, and the Follow-up Emails plugin * hard-codes loading its own version of ActionScheduler_wpCommentLogger with that out-dated method, * goodness knows why, so we need to guard against that here instead of using a DateTime type declaration * for the constructor's 3rd param of $date and causing a fatal error with older versions of FUE. */ if ( null !== $date && ! is_a( $date, 'DateTime' ) ) { _doing_it_wrong( __METHOD__, 'The third parameter must be a valid DateTime instance, or null.', '2.0.0' ); $date = null; } $this->action_id = $action_id; $this->message = $message; $this->date = $date ? $date : new Datetime(); } /** * Returns the date when this log entry was created * * @return Datetime */ public function get_date() { return $this->date; } /** * Get action ID of log entry. */ public function get_action_id() { return $this->action_id; } /** * Get log entry message. */ public function get_message() { return $this->message; } } Dependencies/ActionScheduler/classes/ActionScheduler_InvalidActionException.php 0000644 00000002717 15174671745 0024143 0 ustar 00 <?php /** * InvalidAction Exception. * * Used for identifying actions that are invalid in some way. * * @package ActionScheduler */ class ActionScheduler_InvalidActionException extends \InvalidArgumentException implements ActionScheduler_Exception { /** * Create a new exception when the action's schedule cannot be fetched. * * @param string $action_id The action ID with bad args. * @param mixed $schedule Passed schedule. * @return static */ public static function from_schedule( $action_id, $schedule ) { $message = sprintf( /* translators: 1: action ID 2: schedule */ __( 'Action [%1$s] has an invalid schedule: %2$s', 'action-scheduler' ), $action_id, var_export( $schedule, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export ); return new static( $message ); } /** * Create a new exception when the action's args cannot be decoded to an array. * * @param string $action_id The action ID with bad args. * @param mixed $args Passed arguments. * @return static */ public static function from_decoding_args( $action_id, $args = array() ) { $message = sprintf( /* translators: 1: action ID 2: arguments */ __( 'Action [%1$s] has invalid arguments. It cannot be JSON decoded to an array. $args = %2$s', 'action-scheduler' ), $action_id, var_export( $args, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export ); return new static( $message ); } } Dependencies/ActionScheduler/classes/ActionScheduler_SystemInformation.php 0000644 00000004701 15174671745 0023225 0 ustar 00 <?php /** * Provides information about active and registered instances of Action Scheduler. */ class ActionScheduler_SystemInformation { /** * Returns information about the plugin or theme which contains the current active version * of Action Scheduler. * * If this cannot be determined, or if Action Scheduler is being loaded via some other * method, then it will return an empty array. Otherwise, if populated, the array will * look like the following: * * [ * 'type' => 'plugin', # or 'theme' * 'name' => 'Name', * ] * * @return array */ public static function active_source(): array { $plugins = get_plugins(); $plugin_files = array_keys( $plugins ); foreach ( $plugin_files as $plugin_file ) { $plugin_path = trailingslashit( WP_PLUGIN_DIR ) . dirname( $plugin_file ); $plugin_file = trailingslashit( WP_PLUGIN_DIR ) . $plugin_file; if ( 0 !== strpos( dirname( __DIR__ ), $plugin_path ) ) { continue; } $plugin_data = get_plugin_data( $plugin_file ); if ( ! is_array( $plugin_data ) || empty( $plugin_data['Name'] ) ) { continue; } return array( 'type' => 'plugin', 'name' => $plugin_data['Name'], ); } $themes = (array) search_theme_directories(); foreach ( $themes as $slug => $data ) { $needle = trailingslashit( $data['theme_root'] ) . $slug . '/'; if ( 0 !== strpos( __FILE__, $needle ) ) { continue; } $theme = wp_get_theme( $slug ); if ( ! is_object( $theme ) || ! is_a( $theme, \WP_Theme::class ) ) { continue; } return array( 'type' => 'theme', // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 'name' => $theme->Name, ); } return array(); } /** * Returns the directory path for the currently active installation of Action Scheduler. * * @return string */ public static function active_source_path(): string { return trailingslashit( dirname( __DIR__ ) ); } /** * Get registered sources. * * It is not always possible to obtain this information. For instance, if earlier versions (<=3.9.0) of * Action Scheduler register themselves first, then the necessary data about registered sources will * not be available. * * @return array<string, string> */ public static function get_sources() { $versions = ActionScheduler_Versions::instance(); return method_exists( $versions, 'get_sources' ) ? $versions->get_sources() : array(); } } Dependencies/ActionScheduler/functions.php 0000644 00000045301 15174671745 0014753 0 ustar 00 <?php /** * General API functions for scheduling actions * * @package ActionScheduler. */ /** * Enqueue an action to run one time, as soon as possible * * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. It will not be scheduled if another pending or running action has the same hook and group parameters. * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * * @return int The action ID. Zero if there was an error scheduling the action. */ function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } /** * Provides an opportunity to short-circuit the default process for enqueuing async * actions. * * Returning a value other than null from the filter will short-circuit the normal * process. The expectation in such a scenario is that callbacks will return an integer * representing the enqueued action ID (enqueued using some alternative process) or else * zero. * * @param int|null $pre_option The value to return instead of the option value. * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. * @param int $priority Action priority. * @param bool $unique Unique action. */ $pre = apply_filters( 'pre_as_enqueue_async_action', null, $hook, $args, $group, $priority, $unique ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } return ActionScheduler::factory()->create( array( 'type' => 'async', 'hook' => $hook, 'arguments' => $args, 'group' => $group, 'unique' => $unique, 'priority' => $priority, ) ); } /** * Schedule an action to run one time * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. It will not be scheduled if another pending or running action has the same hook and group parameters. * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * * @return int The action ID. Zero if there was an error scheduling the action. */ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } /** * Provides an opportunity to short-circuit the default process for enqueuing single * actions. * * Returning a value other than null from the filter will short-circuit the normal * process. The expectation in such a scenario is that callbacks will return an integer * representing the scheduled action ID (scheduled using some alternative process) or else * zero. * * @param int|null $pre_option The value to return instead of the option value. * @param int $timestamp When the action will run. * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. * @param int $priorities Action priority. */ $pre = apply_filters( 'pre_as_schedule_single_action', null, $timestamp, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } return ActionScheduler::factory()->create( array( 'type' => 'single', 'hook' => $hook, 'arguments' => $args, 'when' => $timestamp, 'group' => $group, 'unique' => $unique, 'priority' => $priority, ) ); } /** * Schedule a recurring action * * @param int $timestamp When the first instance of the job will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. It will not be scheduled if another pending or running action has the same hook and group parameters. * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * * @return int The action ID. Zero if there was an error scheduling the action. */ function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } $interval = (int) $interval_in_seconds; // We expect an integer and allow it to be passed using float and string types, but otherwise // should reject unexpected values. // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison if ( ! is_numeric( $interval_in_seconds ) || $interval_in_seconds != $interval ) { _doing_it_wrong( __METHOD__, sprintf( /* translators: 1: provided value 2: provided type. */ esc_html__( 'An integer was expected but "%1$s" (%2$s) was received.', 'action-scheduler' ), esc_html( $interval_in_seconds ), esc_html( gettype( $interval_in_seconds ) ) ), '3.6.0' ); return 0; } /** * Provides an opportunity to short-circuit the default process for enqueuing recurring * actions. * * Returning a value other than null from the filter will short-circuit the normal * process. The expectation in such a scenario is that callbacks will return an integer * representing the scheduled action ID (scheduled using some alternative process) or else * zero. * * @param int|null $pre_option The value to return instead of the option value. * @param int $timestamp When the action will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. * @param int $priority Action priority. */ $pre = apply_filters( 'pre_as_schedule_recurring_action', null, $timestamp, $interval_in_seconds, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } return ActionScheduler::factory()->create( array( 'type' => 'recurring', 'hook' => $hook, 'arguments' => $args, 'when' => $timestamp, 'pattern' => $interval_in_seconds, 'group' => $group, 'unique' => $unique, 'priority' => $priority, ) ); } /** * Schedule an action that recurs on a cron-like schedule. * * @param int $timestamp The first instance of the action will be scheduled * to run at a time calculated after this timestamp matching the cron * expression. This can be used to delay the first instance of the action. * @param string $schedule A cron-link schedule string. * @see http://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ * | | | | | | * | | | | | + year [optional] * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) * | | | +---------- month (1 - 12) * | | +--------------- day of month (1 - 31) * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. It will not be scheduled if another pending or running action has the same hook and group parameters. * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * * @return int The action ID. Zero if there was an error scheduling the action. */ function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } /** * Provides an opportunity to short-circuit the default process for enqueuing cron * actions. * * Returning a value other than null from the filter will short-circuit the normal * process. The expectation in such a scenario is that callbacks will return an integer * representing the scheduled action ID (scheduled using some alternative process) or else * zero. * * @param int|null $pre_option The value to return instead of the option value. * @param int $timestamp When the action will run. * @param string $schedule Cron-like schedule string. * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. * @param int $priority Action priority. */ $pre = apply_filters( 'pre_as_schedule_cron_action', null, $timestamp, $schedule, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } return ActionScheduler::factory()->create( array( 'type' => 'cron', 'hook' => $hook, 'arguments' => $args, 'when' => $timestamp, 'pattern' => $schedule, 'group' => $group, 'unique' => $unique, 'priority' => $priority, ) ); } /** * Cancel the next occurrence of a scheduled action. * * While only the next instance of a recurring or cron action is unscheduled by this method, that will also prevent * all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled in * a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled * only after the former action is run. If the next instance is never run, because it's unscheduled by this function, * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled * by this method also. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to. * * @return int|null The scheduled action ID if a scheduled action was found, or null if no matching action found. */ function as_unschedule_action( $hook, $args = array(), $group = '' ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } $params = array( 'hook' => $hook, 'status' => ActionScheduler_Store::STATUS_PENDING, 'orderby' => 'date', 'order' => 'ASC', 'group' => $group, ); if ( is_array( $args ) ) { $params['args'] = $args; } $action_id = ActionScheduler::store()->query_action( $params ); if ( $action_id ) { try { ActionScheduler::store()->cancel_action( $action_id ); } catch ( Exception $exception ) { ActionScheduler::logger()->log( $action_id, sprintf( /* translators: %1$s is the name of the hook to be cancelled, %2$s is the exception message. */ __( 'Caught exception while cancelling action "%1$s": %2$s', 'action-scheduler' ), $hook, $exception->getMessage() ) ); $action_id = null; } } return $action_id; } /** * Cancel all occurrences of a scheduled action. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to. */ function as_unschedule_all_actions( $hook, $args = array(), $group = '' ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return; } if ( empty( $args ) ) { if ( ! empty( $hook ) && empty( $group ) ) { ActionScheduler_Store::instance()->cancel_actions_by_hook( $hook ); return; } if ( ! empty( $group ) && empty( $hook ) ) { ActionScheduler_Store::instance()->cancel_actions_by_group( $group ); return; } } do { $unscheduled_action = as_unschedule_action( $hook, $args, $group ); } while ( ! empty( $unscheduled_action ) ); } /** * Check if there is an existing action in the queue with a given hook, args and group combination. * * An action in the queue could be pending, in-progress or async. If the is pending for a time in * future, its scheduled date will be returned as a timestamp. If it is currently being run, or an * async action sitting in the queue waiting to be processed, in which case boolean true will be * returned. Or there may be no async, in-progress or pending action for this hook, in which case, * boolean false will be the return value. * * @param string $hook Name of the hook to search for. * @param array $args Arguments of the action to be searched. * @param string $group Group of the action to be searched. * * @return int|bool The timestamp for the next occurrence of a pending scheduled action, true for an async or in-progress action or false if there is no matching action. */ function as_next_scheduled_action( $hook, $args = null, $group = '' ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return false; } $params = array( 'hook' => $hook, 'orderby' => 'date', 'order' => 'ASC', 'group' => $group, ); if ( is_array( $args ) ) { $params['args'] = $args; } $params['status'] = ActionScheduler_Store::STATUS_RUNNING; $action_id = ActionScheduler::store()->query_action( $params ); if ( $action_id ) { return true; } $params['status'] = ActionScheduler_Store::STATUS_PENDING; $action_id = ActionScheduler::store()->query_action( $params ); if ( null === $action_id ) { return false; } $action = ActionScheduler::store()->fetch_action( $action_id ); $scheduled_date = $action->get_schedule()->get_date(); if ( $scheduled_date ) { return (int) $scheduled_date->format( 'U' ); } elseif ( null === $scheduled_date ) { // pending async action with NullSchedule. return true; } return false; } /** * Check if there is a scheduled action in the queue but more efficiently than as_next_scheduled_action(). * * It's recommended to use this function when you need to know whether a specific action is currently scheduled * (pending or in-progress). * * @since 3.3.0 * * @param string $hook The hook of the action. * @param array $args Args that have been passed to the action. Null will matches any args. * @param string $group The group the job is assigned to. * * @return bool True if a matching action is pending or in-progress, false otherwise. */ function as_has_scheduled_action( $hook, $args = null, $group = '' ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return false; } $query_args = array( 'hook' => $hook, 'status' => array( ActionScheduler_Store::STATUS_RUNNING, ActionScheduler_Store::STATUS_PENDING ), 'group' => $group, 'orderby' => 'none', ); if ( null !== $args ) { $query_args['args'] = $args; } $action_id = ActionScheduler::store()->query_action( $query_args ); return null !== $action_id; } /** * Find scheduled actions * * @param array $args Possible arguments, with their default values. * 'hook' => '' - the name of the action that will be triggered. * 'args' => NULL - the args array that will be passed with the action. * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='. * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='. * 'group' => '' - the group the action belongs to. * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING. * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID. * 'per_page' => 5 - Number of results to return. * 'offset' => 0. * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', 'date' or 'none'. * 'order' => 'ASC'. * * @param string $return_format OBJECT, ARRAY_A, or ids. * * @return array */ function as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return array(); } $store = ActionScheduler::store(); foreach ( array( 'date', 'modified' ) as $key ) { if ( isset( $args[ $key ] ) ) { $args[ $key ] = as_get_datetime_object( $args[ $key ] ); } } $ids = $store->query_actions( $args ); if ( 'ids' === $return_format || 'int' === $return_format ) { return $ids; } $actions = array(); foreach ( $ids as $action_id ) { $actions[ $action_id ] = $store->fetch_action( $action_id ); } if ( ARRAY_A === $return_format ) { foreach ( $actions as $action_id => $action_object ) { $actions[ $action_id ] = get_object_vars( $action_object ); } } return $actions; } /** * Helper function to create an instance of DateTime based on a given * string and timezone. By default, will return the current date/time * in the UTC timezone. * * Needed because new DateTime() called without an explicit timezone * will create a date/time in PHP's timezone, but we need to have * assurance that a date/time uses the right timezone (which we almost * always want to be UTC), which means we need to always include the * timezone when instantiating datetimes rather than leaving it up to * the PHP default. * * @param mixed $date_string A date/time string. Valid formats are explained in http://php.net/manual/en/datetime.formats.php. * @param string $timezone A timezone identifier, like UTC or Europe/Lisbon. The list of valid identifiers is available http://php.net/manual/en/timezones.php. * * @return ActionScheduler_DateTime */ function as_get_datetime_object( $date_string = null, $timezone = 'UTC' ) { if ( is_object( $date_string ) && $date_string instanceof DateTime ) { $date = new ActionScheduler_DateTime( $date_string->format( 'Y-m-d H:i:s' ), new DateTimeZone( $timezone ) ); } elseif ( is_numeric( $date_string ) ) { $date = new ActionScheduler_DateTime( '@' . $date_string, new DateTimeZone( $timezone ) ); } else { $date = new ActionScheduler_DateTime( null === $date_string ? 'now' : $date_string, new DateTimeZone( $timezone ) ); } return $date; } Dependencies/ActionScheduler/deprecated/ActionScheduler_Schedule_Deprecated.php 0000644 00000001477 15174671745 0024061 0 ustar 00 <?php /** * Class ActionScheduler_Abstract_Schedule */ abstract class ActionScheduler_Schedule_Deprecated implements ActionScheduler_Schedule { /** * Get the date & time this schedule was created to run, or calculate when it should be run * after a given date & time. * * @param DateTime $after DateTime to calculate against. * * @return DateTime|null */ public function next( DateTime $after = null ) { if ( empty( $after ) ) { $return_value = $this->get_date(); $replacement_method = 'get_date()'; } else { $return_value = $this->get_next( $after ); $replacement_method = 'get_next( $after )'; } _deprecated_function( __METHOD__, '3.0.0', __CLASS__ . '::' . $replacement_method ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return $return_value; } } Dependencies/ActionScheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php 0000644 00000001524 15174671745 0026417 0 ustar 00 <?php /** * Abstract class with common Queue Cleaner functionality. */ abstract class ActionScheduler_Abstract_QueueRunner_Deprecated { /** * Get the maximum number of seconds a batch can run for. * * @deprecated 2.1.1 * @return int The number of seconds. */ protected function get_maximum_execution_time() { _deprecated_function( __METHOD__, '2.1.1', 'ActionScheduler_Abstract_QueueRunner::get_time_limit()' ); $maximum_execution_time = 30; // Apply deprecated filter. if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) { _deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' ); $maximum_execution_time = apply_filters( 'action_scheduler_maximum_execution_time', $maximum_execution_time ); } return absint( $maximum_execution_time ); } } Dependencies/ActionScheduler/deprecated/functions.php 0000644 00000012231 15174671745 0017047 0 ustar 00 <?php /** * Deprecated API functions for scheduling actions * * Functions with the wc prefix were deprecated to avoid confusion with * Action Scheduler being included in WooCommerce core, and it providing * a different set of APIs for working with the action queue. * * @package ActionScheduler */ /** * Schedule an action to run one time. * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * * @return string The job ID */ function wc_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_single_action()' ); return as_schedule_single_action( $timestamp, $hook, $args, $group ); } /** * Schedule a recurring action. * * @param int $timestamp When the first instance of the job will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * * @deprecated 2.1.0 * * @return string The job ID */ function wc_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_recurring_action()' ); return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group ); } /** * Schedule an action that recurs on a cron-like schedule. * * @param int $timestamp The schedule will start on or after this time. * @param string $schedule A cron-link schedule string. * @see http://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ * | | | | | | * | | | | | + year [optional] * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) * | | | +---------- month (1 - 12) * | | +--------------- day of month (1 - 31) * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * * @deprecated 2.1.0 * * @return string The job ID */ function wc_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_cron_action()' ); return as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group ); } /** * Cancel the next occurrence of a job. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group Action's group. * * @deprecated 2.1.0 */ function wc_unschedule_action( $hook, $args = array(), $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_unschedule_action()' ); as_unschedule_action( $hook, $args, $group ); } /** * Get next scheduled action. * * @param string $hook Action's hook. * @param array $args Action's args. * @param string $group Action's group. * * @deprecated 2.1.0 * * @return int|bool The timestamp for the next occurrence, or false if nothing was found */ function wc_next_scheduled_action( $hook, $args = null, $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_next_scheduled_action()' ); return as_next_scheduled_action( $hook, $args, $group ); } /** * Find scheduled actions * * @param array $args Possible arguments, with their default values: * 'hook' => '' - the name of the action that will be triggered * 'args' => NULL - the args array that will be passed with the action * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' * 'group' => '' - the group the action belongs to * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID * 'per_page' => 5 - Number of results to return * 'offset' => 0 * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' * 'order' => 'ASC'. * @param string $return_format OBJECT, ARRAY_A, or ids. * * @deprecated 2.1.0 * * @return array */ function wc_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_get_scheduled_actions()' ); return as_get_scheduled_actions( $args, $return_format ); } Dependencies/ActionScheduler/deprecated/ActionScheduler_Store_Deprecated.php 0000644 00000002043 15174671745 0023407 0 ustar 00 <?php /** * Class ActionScheduler_Store_Deprecated * * @codeCoverageIgnore */ abstract class ActionScheduler_Store_Deprecated { /** * Mark an action that failed to fetch correctly as failed. * * @since 2.2.6 * * @param int $action_id The ID of the action. */ public function mark_failed_fetch_action( $action_id ) { _deprecated_function( __METHOD__, '3.0.0', 'ActionScheduler_Store::mark_failure()' ); self::$store->mark_failure( $action_id ); } /** * Add base hooks * * @since 2.2.6 */ protected static function hook() { _deprecated_function( __METHOD__, '3.0.0' ); } /** * Remove base hooks * * @since 2.2.6 */ protected static function unhook() { _deprecated_function( __METHOD__, '3.0.0' ); } /** * Get the site's local time. * * @deprecated 2.1.0 * @return DateTimeZone */ protected function get_local_timezone() { _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); return ActionScheduler_TimezoneHelper::get_local_timezone(); } } Dependencies/ActionScheduler/deprecated/ActionScheduler_AdminView_Deprecated.php 0000644 00000012532 15174671745 0024202 0 ustar 00 <?php /** * Class ActionScheduler_AdminView_Deprecated * * Store deprecated public functions previously found in the ActionScheduler_AdminView class. * Keeps them out of the way of the main class. * * @codeCoverageIgnore */ class ActionScheduler_AdminView_Deprecated { /** * Adjust parameters for custom post type. * * @param array $args Args. */ public function action_scheduler_post_type_args( $args ) { _deprecated_function( __METHOD__, '2.0.0' ); return $args; } /** * Customise the post status related views displayed on the Scheduled Actions administration screen. * * @param array $views An associative array of views and view labels which can be used to filter the 'scheduled-action' posts displayed on the Scheduled Actions administration screen. * @return array $views An associative array of views and view labels which can be used to filter the 'scheduled-action' posts displayed on the Scheduled Actions administration screen. */ public function list_table_views( $views ) { _deprecated_function( __METHOD__, '2.0.0' ); return $views; } /** * Do not include the "Edit" action for the Scheduled Actions administration screen. * * Hooked to the 'bulk_actions-edit-action-scheduler' filter. * * @param array $actions An associative array of actions which can be performed on the 'scheduled-action' post type. * @return array $actions An associative array of actions which can be performed on the 'scheduled-action' post type. */ public function bulk_actions( $actions ) { _deprecated_function( __METHOD__, '2.0.0' ); return $actions; } /** * Completely customer the columns displayed on the Scheduled Actions administration screen. * * Because we can't filter the content of the default title and date columns, we need to recreate our own * custom columns for displaying those post fields. For the column content, @see self::list_table_column_content(). * * @param array $columns An associative array of columns that are use for the table on the Scheduled Actions administration screen. * @return array $columns An associative array of columns that are use for the table on the Scheduled Actions administration screen. */ public function list_table_columns( $columns ) { _deprecated_function( __METHOD__, '2.0.0' ); return $columns; } /** * Make our custom title & date columns use defaulting title & date sorting. * * @param array $columns An associative array of columns that can be used to sort the table on the Scheduled Actions administration screen. * @return array $columns An associative array of columns that can be used to sort the table on the Scheduled Actions administration screen. */ public static function list_table_sortable_columns( $columns ) { _deprecated_function( __METHOD__, '2.0.0' ); return $columns; } /** * Print the content for our custom columns. * * @param string $column_name The key for the column for which we should output our content. * @param int $post_id The ID of the 'scheduled-action' post for which this row relates. */ public static function list_table_column_content( $column_name, $post_id ) { _deprecated_function( __METHOD__, '2.0.0' ); } /** * Hide the inline "Edit" action for all 'scheduled-action' posts. * * Hooked to the 'post_row_actions' filter. * * @param array $actions An associative array of actions which can be performed on the 'scheduled-action' post type. * @param WP_Post $post The 'scheduled-action' post object. * @return array $actions An associative array of actions which can be performed on the 'scheduled-action' post type. */ public static function row_actions( $actions, $post ) { _deprecated_function( __METHOD__, '2.0.0' ); return $actions; } /** * Run an action when triggered from the Action Scheduler administration screen. * * @codeCoverageIgnore */ public static function maybe_execute_action() { _deprecated_function( __METHOD__, '2.0.0' ); } /** * Convert an interval of seconds into a two part human friendly string. * * The WordPress human_time_diff() function only calculates the time difference to one degree, meaning * even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step * further to display two degrees of accuracy. * * Based on Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/ * * @return void */ public static function admin_notices() { _deprecated_function( __METHOD__, '2.0.0' ); } /** * Filter search queries to allow searching by Claim ID (i.e. post_password). * * @param string $orderby MySQL orderby string. * @param WP_Query $query Instance of a WP_Query object. * @return void */ public function custom_orderby( $orderby, $query ) { _deprecated_function( __METHOD__, '2.0.0' ); } /** * Filter search queries to allow searching by Claim ID (i.e. post_password). * * @param string $search MySQL search string. * @param WP_Query $query Instance of a WP_Query object. * @return void */ public function search_post_password( $search, $query ) { _deprecated_function( __METHOD__, '2.0.0' ); } /** * Change messages when a scheduled action is updated. * * @param array $messages Messages. * @return array */ public function post_updated_messages( $messages ) { _deprecated_function( __METHOD__, '2.0.0' ); return $messages; } } Dependencies/ActionScheduler/changelog.txt 0000644 00000020134 15174671745 0014717 0 ustar 00 *** Changelog *** = 3.9.0 - 2024-11-14 = * Minimum required version of PHP is now 7.1. * Performance improvements for the `as_pending_actions_due()` function. * Existing filter hook `action_scheduler_claim_actions_order_by` enhanced to provide callbacks with additional information. * Improved compatibility with PHP 8.4, specifically by making implicitly nullable parameters explicitly nullable. * A large number of coding standards-enhancements, to help reduce friction when submitting plugins to marketplaces and plugin directories. Special props @crstauf for this effort. * Minor documentation tweaks and improvements. = 3.8.2 - 2024-09-12 = * Add missing parameter to the `pre_as_enqueue_async_action` hook. * Bump minimum PHP version to 7.0. * Bump minimum WordPress version to 6.4. * Make the batch size adjustable during processing. = 3.8.1 - 2024-06-20 = * Fix typos. * Improve the messaging in our unidentified action exceptions. = 3.8.0 - 2024-05-22 = * Documentation - Fixed typos in perf.md. * Update - We now require WordPress 6.3 or higher. * Update - We now require PHP 7.0 or higher. = 3.7.4 - 2024-04-05 = * Give a clear description of how the $unique parameter works. * Preserve the tab field if set. * Tweak - WP 6.5 compatibility. = 3.7.3 - 2024-03-20 = * Do not iterate over all of GET when building form in list table. * Fix a few issues reported by PCP (Plugin Check Plugin). * Try to save actions as unique even when the store doesn't support it. * Tweak - WP 6.4 compatibility. * Update "Tested up to" tag to WordPress 6.5. * update version in package-lock.json. = 3.7.2 - 2024-02-14 = * No longer user variables in `_n()` translation function. = 3.7.1 - 2023-12-13 = * update semver to 5.7.2 because of a security vulnerability in 5.7.1. = 3.7.0 - 2023-11-20 = * Important: starting with this release, Action Scheduler follows an L-2 version policy (WordPress, and consequently PHP). * Add extended indexes for hook_status_scheduled_date_gmt and status_scheduled_date_gmt. * Catch and log exceptions thrown when actions can't be created, e.g. under a corrupt database schema. * Tweak - WP 6.4 compatibility. * Update unit tests for upcoming dependency version policy. * make sure hook action_scheduler_failed_execution can access original exception object. * mention dependency version policy in usage.md. = 3.6.4 - 2023-10-11 = * Performance improvements when bulk cancelling actions. * Dev-related fixes. = 3.6.3 - 2023-09-13 = * Use `_doing_it_wrong` in initialization check. = 3.6.2 - 2023-08-09 = * Add guidance about passing arguments. * Atomic option locking. * Improve bulk delete handling. * Include database error in the exception message. * Tweak - WP 6.3 compatibility. = 3.6.1 - 2023-06-14 = * Document new optional `$priority` arg for various API functions. * Document the new `--exclude-groups` WP CLI option. * Document the new `action_scheduler_init` hook. * Ensure actions within each claim are executed in the expected order. * Fix incorrect text domain. * Remove SHOW TABLES usage when checking if tables exist. = 3.6.0 - 2023-05-10 = * Add $unique parameter to function signatures. * Add a cast-to-int for extra safety before forming new DateTime object. * Add a hook allowing exceptions for consistently failing recurring actions. * Add action priorities. * Add init hook. * Always raise the time limit. * Bump minimatch from 3.0.4 to 3.0.8. * Bump yaml from 2.2.1 to 2.2.2. * Defensive coding relating to gaps in declared schedule types. * Do not process an action if it cannot be set to `in-progress`. * Filter view labels (status names) should be translatable | #919. * Fix WPCLI progress messages. * Improve data-store initialization flow. * Improve error handling across all supported PHP versions. * Improve logic for flushing the runtime cache. * Support exclusion of multiple groups. * Update lint-staged and Node/NPM requirements. * add CLI clean command. * add CLI exclude-group filter. * exclude past-due from list table all filter count. * throwing an exception if as_schedule_recurring_action interval param is not of type integer. = 3.5.4 - 2023-01-17 = * Add pre filters during action registration. * Async scheduling. * Calculate timeouts based on total actions. * Correctly order the parameters for `ActionScheduler_ActionFactory`'s calls to `single_unique`. * Fetch action in memory first before releasing claim to avoid deadlock. * PHP 8.2: declare property to fix creation of dynamic property warning. * PHP 8.2: fix "Using ${var} in strings is deprecated, use {$var} instead". * Prevent `undefined variable` warning for `$num_pastdue_actions`. = 3.5.3 - 2022-11-09 = * Query actions with partial match. = 3.5.2 - 2022-09-16 = * Fix - erroneous 3.5.1 release. = 3.5.1 - 2022-09-13 = * Maintenance on A/S docs. * fix: PHP 8.2 deprecated notice. = 3.5.0 - 2022-08-25 = * Add - The active view link within the "Tools > Scheduled Actions" screen is now clickable. * Add - A warning when there are past-due actions. * Enhancement - Added the ability to schedule unique actions via an atomic operation. * Enhancement - Improvements to cache invalidation when processing batches (when running on WordPress 6.0+). * Enhancement - If a recurring action is found to be consistently failing, it will stop being rescheduled. * Enhancement - Adds a new "Past Due" view to the scheduled actions list table. = 3.4.2 - 2022-06-08 = * Fix - Change the include for better linting. * Fix - update: Added Action scheduler completed action hook. = 3.4.1 - 2022-05-24 = * Fix - Change the include for better linting. * Fix - Fix the documented return type. = 3.4.0 - 2021-10-29 = * Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771 * Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755 * Fix - Avoid triggering autoloaders during the version resolution process (props @olegabr). #731 & #776 * Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). #761 * Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). #768 * Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). #762 * Dev - Improve actions table indices (props @glagonikas). #774 & #777 * Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). #769 & #778 * Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). #763 & #779 * Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). #773 * Dev - PHPCS fixes for ActionScheduler_WpPostStore class (props @ovidiul). #780 = 3.3.0 - 2021-09-15 = * Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645 * Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519 * Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645 * Dev - Now supports queries that use multiple statuses. #649 * Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723 = 3.2.1 - 2021-06-21 = * Fix - Add extra safety/account for different versions of AS and different loading patterns. #714 * Fix - Handle hidden columns (Tools → Scheduled Actions) | #600. = 3.2.0 - 2021-06-03 = * Fix - Add "no ordering" option to as_next_scheduled_action(). * Fix - Add secondary scheduled date checks when claiming actions (DBStore) | #634. * Fix - Add secondary scheduled date checks when claiming actions (wpPostStore) | #634. * Fix - Adds a new index to the action table, reducing the potential for deadlocks (props: @glagonikas). * Fix - Fix unit tests infrastructure and adapt tests to PHP 8. * Fix - Identify in-use data store. * Fix - Improve test_migration_is_scheduled. * Fix - PHP notice on list table. * Fix - Speed up clean up and batch selects. * Fix - Update pending dependencies. * Fix - [PHP 8.0] Only pass action arg values through to do_action_ref_array(). * Fix - [PHP 8] Set the PHP version to 7.1 in composer.json for PHP 8 compatibility. * Fix - add is_initialized() to docs. * Fix - fix file permissions. * Fix - fixes #664 by replacing __ with esc_html__. = 3.1.6 - 2020-05-12 = * Change log starts. Dependencies/ActionScheduler/README.md 0000644 00000005357 15174671745 0013520 0 ustar 00 # Action Scheduler - Job Queue for WordPress [](https://travis-ci.org/woocommerce/action-scheduler) [](https://codecov.io/gh/woocommerce/action-scheduler) Action Scheduler is a scalable, traceable job queue for background processing large sets of actions in WordPress. It's specially designed to be distributed in WordPress plugins. Action Scheduler works by triggering an action hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occasions. Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook. ## Battle-Tested Background Processing Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins. It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, at a sustained rate of over 10,000 / hour without negatively impacting normal site operations. This is all on infrastructure and WordPress sites outside the control of the plugin author. If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help. ## Learn More To learn more about how to Action Scheduler works, and how to use it in your plugin, check out the docs on [ActionScheduler.org](https://actionscheduler.org). There you will find: * [Usage guide](https://actionscheduler.org/usage/): instructions on installing and using Action Scheduler * [WP CLI guide](https://actionscheduler.org/wp-cli/): instructions on running Action Scheduler at scale via WP CLI * [API Reference](https://actionscheduler.org/api/): complete reference guide for all API functions * [Administration Guide](https://actionscheduler.org/admin/): guide to managing scheduled actions via the administration screen * [Guide to Background Processing at Scale](https://actionscheduler.org/perf/): instructions for running Action Scheduler at scale via the default WP Cron queue runner ## Credits Action Scheduler is developed and maintained by [Automattic](http://automattic.com/) with significant early development completed by [Flightless](https://flightless.us/). Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/woocommerce/action-scheduler/pulls) welcome. Dependencies/ActionScheduler/readme.txt 0000644 00000025457 15174671745 0014242 0 ustar 00 === Action Scheduler === Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1 Tags: scheduler, cron Stable tag: 3.9.0 License: GPLv3 Requires at least: 6.5 Tested up to: 6.7 Requires PHP: 7.1 Action Scheduler - Job Queue for WordPress == Description == Action Scheduler is a scalable, traceable job queue for background processing large sets of actions in WordPress. It's specially designed to be distributed in WordPress plugins. Action Scheduler works by triggering an action hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occasions. Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook. ## Battle-Tested Background Processing Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins. It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, at a sustained rate of over 10,000 / hour without negatively impacting normal site operations. This is all on infrastructure and WordPress sites outside the control of the plugin author. If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help. ## Learn More To learn more about how to Action Scheduler works, and how to use it in your plugin, check out the docs on [ActionScheduler.org](https://actionscheduler.org). There you will find: * [Usage guide](https://actionscheduler.org/usage/): instructions on installing and using Action Scheduler * [WP CLI guide](https://actionscheduler.org/wp-cli/): instructions on running Action Scheduler at scale via WP CLI * [API Reference](https://actionscheduler.org/api/): complete reference guide for all API functions * [Administration Guide](https://actionscheduler.org/admin/): guide to managing scheduled actions via the administration screen * [Guide to Background Processing at Scale](https://actionscheduler.org/perf/): instructions for running Action Scheduler at scale via the default WP Cron queue runner ## Credits Action Scheduler is developed and maintained by [Automattic](http://automattic.com/) with significant early development completed by [Flightless](https://flightless.us/). Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/woocommerce/action-scheduler/pulls) welcome. == Changelog == = 3.9.0 - 2024-11-14 = * Minimum required version of PHP is now 7.1. * Performance improvements for the `as_pending_actions_due()` function. * Existing filter hook `action_scheduler_claim_actions_order_by` enhanced to provide callbacks with additional information. * Improved compatibility with PHP 8.4, specifically by making implicitly nullable parameters explicitly nullable. * A large number of coding standards-enhancements, to help reduce friction when submitting plugins to marketplaces and plugin directories. Special props @crstauf for this effort. * Minor documentation tweaks and improvements. = 3.8.2 - 2024-09-12 = * Add missing parameter to the `pre_as_enqueue_async_action` hook. * Bump minimum PHP version to 7.0. * Bump minimum WordPress version to 6.4. * Make the batch size adjustable during processing. = 3.8.1 - 2024-06-20 = * Fix typos. * Improve the messaging in our unidentified action exceptions. = 3.8.0 - 2024-05-22 = * Documentation - Fixed typos in perf.md. * Update - We now require WordPress 6.3 or higher. * Update - We now require PHP 7.0 or higher. = 3.7.4 - 2024-04-05 = * Give a clear description of how the $unique parameter works. * Preserve the tab field if set. * Tweak - WP 6.5 compatibility. = 3.7.3 - 2024-03-20 = * Do not iterate over all of GET when building form in list table. * Fix a few issues reported by PCP (Plugin Check Plugin). * Try to save actions as unique even when the store doesn't support it. * Tweak - WP 6.4 compatibility. * Update "Tested up to" tag to WordPress 6.5. * update version in package-lock.json. = 3.7.2 - 2024-02-14 = * No longer user variables in `_n()` translation function. = 3.7.1 - 2023-12-13 = * update semver to 5.7.2 because of a security vulnerability in 5.7.1. = 3.7.0 - 2023-11-20 = * Important: starting with this release, Action Scheduler follows an L-2 version policy (WordPress, and consequently PHP). * Add extended indexes for hook_status_scheduled_date_gmt and status_scheduled_date_gmt. * Catch and log exceptions thrown when actions can't be created, e.g. under a corrupt database schema. * Tweak - WP 6.4 compatibility. * Update unit tests for upcoming dependency version policy. * make sure hook action_scheduler_failed_execution can access original exception object. * mention dependency version policy in usage.md. = 3.6.4 - 2023-10-11 = * Performance improvements when bulk cancelling actions. * Dev-related fixes. = 3.6.3 - 2023-09-13 = * Use `_doing_it_wrong` in initialization check. = 3.6.2 - 2023-08-09 = * Add guidance about passing arguments. * Atomic option locking. * Improve bulk delete handling. * Include database error in the exception message. * Tweak - WP 6.3 compatibility. = 3.6.1 - 2023-06-14 = * Document new optional `$priority` arg for various API functions. * Document the new `--exclude-groups` WP CLI option. * Document the new `action_scheduler_init` hook. * Ensure actions within each claim are executed in the expected order. * Fix incorrect text domain. * Remove SHOW TABLES usage when checking if tables exist. = 3.6.0 - 2023-05-10 = * Add $unique parameter to function signatures. * Add a cast-to-int for extra safety before forming new DateTime object. * Add a hook allowing exceptions for consistently failing recurring actions. * Add action priorities. * Add init hook. * Always raise the time limit. * Bump minimatch from 3.0.4 to 3.0.8. * Bump yaml from 2.2.1 to 2.2.2. * Defensive coding relating to gaps in declared schedule types. * Do not process an action if it cannot be set to `in-progress`. * Filter view labels (status names) should be translatable | #919. * Fix WPCLI progress messages. * Improve data-store initialization flow. * Improve error handling across all supported PHP versions. * Improve logic for flushing the runtime cache. * Support exclusion of multiple groups. * Update lint-staged and Node/NPM requirements. * add CLI clean command. * add CLI exclude-group filter. * exclude past-due from list table all filter count. * throwing an exception if as_schedule_recurring_action interval param is not of type integer. = 3.5.4 - 2023-01-17 = * Add pre filters during action registration. * Async scheduling. * Calculate timeouts based on total actions. * Correctly order the parameters for `ActionScheduler_ActionFactory`'s calls to `single_unique`. * Fetch action in memory first before releasing claim to avoid deadlock. * PHP 8.2: declare property to fix creation of dynamic property warning. * PHP 8.2: fix "Using ${var} in strings is deprecated, use {$var} instead". * Prevent `undefined variable` warning for `$num_pastdue_actions`. = 3.5.3 - 2022-11-09 = * Query actions with partial match. = 3.5.2 - 2022-09-16 = * Fix - erroneous 3.5.1 release. = 3.5.1 - 2022-09-13 = * Maintenance on A/S docs. * fix: PHP 8.2 deprecated notice. = 3.5.0 - 2022-08-25 = * Add - The active view link within the "Tools > Scheduled Actions" screen is now clickable. * Add - A warning when there are past-due actions. * Enhancement - Added the ability to schedule unique actions via an atomic operation. * Enhancement - Improvements to cache invalidation when processing batches (when running on WordPress 6.0+). * Enhancement - If a recurring action is found to be consistently failing, it will stop being rescheduled. * Enhancement - Adds a new "Past Due" view to the scheduled actions list table. = 3.4.2 - 2022-06-08 = * Fix - Change the include for better linting. * Fix - update: Added Action scheduler completed action hook. = 3.4.1 - 2022-05-24 = * Fix - Change the include for better linting. * Fix - Fix the documented return type. = 3.4.0 - 2021-10-29 = * Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771 * Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755 * Fix - Avoid triggering autoloaders during the version resolution process (props @olegabr). #731 & #776 * Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). #761 * Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). #768 * Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). #762 * Dev - Improve actions table indices (props @glagonikas). #774 & #777 * Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). #769 & #778 * Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). #763 & #779 * Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). #773 * Dev - PHPCS fixes for ActionScheduler_WpPostStore class (props @ovidiul). #780 = 3.3.0 - 2021-09-15 = * Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645 * Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519 * Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645 * Dev - Now supports queries that use multiple statuses. #649 * Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723 = 3.2.1 - 2021-06-21 = * Fix - Add extra safety/account for different versions of AS and different loading patterns. #714 * Fix - Handle hidden columns (Tools → Scheduled Actions) | #600. = 3.2.0 - 2021-06-03 = * Fix - Add "no ordering" option to as_next_scheduled_action(). * Fix - Add secondary scheduled date checks when claiming actions (DBStore) | #634. * Fix - Add secondary scheduled date checks when claiming actions (wpPostStore) | #634. * Fix - Adds a new index to the action table, reducing the potential for deadlocks (props: @glagonikas). * Fix - Fix unit tests infrastructure and adapt tests to PHP 8. * Fix - Identify in-use data store. * Fix - Improve test_migration_is_scheduled. * Fix - PHP notice on list table. * Fix - Speed up clean up and batch selects. * Fix - Update pending dependencies. * Fix - [PHP 8.0] Only pass action arg values through to do_action_ref_array(). * Fix - [PHP 8] Set the PHP version to 7.1 in composer.json for PHP 8 compatibility. * Fix - add is_initialized() to docs. * Fix - fix file permissions. * Fix - fixes #664 by replacing __ with esc_html__. common/partners.php 0000644 00000002335 15174671745 0010427 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); add_action( 'update_option_imagify_settings', 'imagify_maybe_delete_partner_on_option_update', 10, 2 ); /** * After the first API key has been successfully added, make sure the partner ID is deleted. * * @since 1.6.14 * @author Grégory Viguier * * @param mixed $old_value The old option value. * @param mixed $new_value The new option value. */ function imagify_maybe_delete_partner_on_option_update( $old_value, $new_value ) { if ( empty( $old_value['api_key'] ) && ! empty( $new_value['api_key'] ) ) { imagify_delete_partner(); } } add_action( 'update_site_option_imagify_settings', 'imagify_maybe_delete_partner_on_network_option_update', 10, 3 ); /** * After the first API key has been successfully added to the network option, make sure the partner ID is deleted. * * @since 1.6.14 * @author Grégory Viguier * * @param string $option Name of the network option. * @param mixed $new_value The new network option value. * @param mixed $old_value The old network option value. */ function imagify_maybe_delete_partner_on_network_option_update( $option, $new_value, $old_value ) { imagify_maybe_delete_partner_on_option_update( $old_value, $new_value ); } common/attachments.php 0000644 00000004604 15174671745 0011105 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); add_action( 'delete_attachment', 'imagify_trigger_delete_attachment_hook' ); /** * Trigger a common Imagify hook when an attachment is deleted. * * @since 1.9 * @author Grégory Viguier * * @param int $post_id Attachment ID. */ function imagify_trigger_delete_attachment_hook( $post_id ) { $process = imagify_get_optimization_process( $post_id, 'wp' ); if ( ! $process->is_valid() ) { return; } imagify_trigger_delete_media_hook( $process ); } add_action( 'imagify_delete_media', 'imagify_cleanup_after_media_deletion' ); /** * Delete the backup file and the next-gen files when an attachement is deleted. * * @since 1.9 * @author Grégory Viguier * * @param ProcessInterface $process An optimization process. */ function imagify_cleanup_after_media_deletion( $process ) { if ( 'wp' !== $process->get_media()->get_context() ) { return; } /** * The optimization data will be automatically deleted by WP (post metas). * Delete the Nextgen versions and the backup file. */ $process->delete_nextgen_files( false, true ); $process->delete_backup(); } add_filter( 'ext2type', 'imagify_add_avif_type' ); /** * Add the AVIF extension to wp_get_ext_types(). * * @since 1.9 * @author Grégory Viguier * * @param array $ext2type Multi-dimensional array with extensions for a default set of file types. * @return array */ function imagify_add_avif_type( $ext2type ) { if ( ! in_array( 'avif', $ext2type['image'], true ) ) { $ext2type['image'][] = 'avif'; } return $ext2type; } /** * Set WP’s "big images threshold" to Imagify’s resizing value. * * @since 1.9.8 * @since WP 5.3 * @author Grégory Viguier */ add_filter( 'big_image_size_threshold', [ imagify_get_context( 'wp' ), 'get_resizing_threshold' ], IMAGIFY_INT_MAX ); /** * Add filters to manage images formats that will be generated * * @return array */ function imagify_nextgen_images_formats() { $value = get_imagify_option( 'optimization_format' ); $formats = []; if ( 'off' !== $value ) { $formats[ $value ] = $value; } $default = $formats; /** * Filters the array of next gen formats to generate with Imagify * * @since 2.2 * * @param array $formats Array of image formats */ $formats = apply_filters( 'imagify_nextgen_images_formats', $formats ); if ( ! is_array( $formats ) ) { $formats = $default; } return $formats; } classes/class-imagify-files-iterator.php 0000644 00000005564 15174671745 0014424 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class allowing to filter DirectoryIterator, to return only files that Imagify can optimize and folders. * It also allows to remove forbidden folders. * * @since 1.7 * @author Grégory Viguier */ class Imagify_Files_Iterator extends FilterIterator { /** * Class version. * * @var string * @since 1.7 * @author Grégory Viguier */ const VERSION = '1.0.2'; /** * Tell if the iterator will return both folders and images, or only images. * * @var bool * @since 1.7 */ protected $include_folders; /** * Filesystem object. * * @var object Imagify_Filesystem * @since 1.7.1 * @access protected * @author Grégory Viguier */ protected $filesystem; /** * Check whether the current element of the iterator is acceptable. * * @since 1.7 * @access public * @author Grégory Viguier * * @param object $iterator The iterator that is being filtered. * @param bool $include_folders True to return both folders and images. False to return only images. */ public function __construct( $iterator, $include_folders = true ) { parent::__construct( $iterator ); $this->include_folders = (bool) $include_folders; $this->filesystem = Imagify_Filesystem::get_instance(); } /** * Check whether the current element of the iterator is acceptable. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool Returns whether the current element of the iterator is acceptable through this filter. */ public function accept(): bool { static $extensions, $has_extension_method; $file_path = $this->current()->getPathname(); // Prevent triggering an open_basedir restriction error. $file_name = $this->filesystem->file_name( $file_path ); if ( '.' === $file_name || '..' === $file_name ) { return false; } // Forbidden file/folder paths and names. $is_dir = $this->isDir(); if ( $is_dir ) { $file_path = trailingslashit( $file_path ); } if ( Imagify_Files_Scan::is_path_forbidden( $file_path ) ) { return false; } // OK for folders. if ( $this->include_folders && $is_dir ) { return true; } // Only files. if ( ! $this->current()->isFile() ) { return false; } // Only files with the required extension. if ( ! isset( $extensions ) ) { $extensions = array_keys( imagify_get_mime_types() ); $extensions = implode( '|', $extensions ); } if ( ! isset( $has_extension_method ) ) { // This method was introduced in php 5.3.6. $has_extension_method = method_exists( $this->current(), 'getExtension' ); } if ( $has_extension_method ) { $file_extension = strtolower( $this->current()->getExtension() ); } else { $file_extension = strtolower( $this->filesystem->path_info( $file_path, 'extension' ) ); } return preg_match( '@^' . $extensions . '$@', $file_extension ); } } classes/class-imagify-files-db.php 0000644 00000007023 15174671745 0013150 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * DB class that handles files in "custom folders". * * @since 1.7 * @author Grégory Viguier */ class Imagify_Files_DB extends Imagify_Abstract_DB { use InstanceGetterTrait; /** * Class version. * * @var string */ const VERSION = '1.1'; /** * The suffix used in the name of the database table (so, without the wpdb prefix). * * @var string * @since 1.7 * @access protected */ protected $table = 'imagify_files'; /** * The version of our database table. * * @var int * @since 1.7 * @access protected */ protected $table_version = 102; /** * Tell if the table is the same for each site of a Multisite. * * @var bool * @since 1.7 * @access protected */ protected $table_is_global = true; /** * The name of the primary column. * * @var string * @since 1.7 * @access protected */ protected $primary_key = 'file_id'; /** * Get the column placeholders. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public function get_columns() { return [ 'file_id' => '%d', 'folder_id' => '%d', 'file_date' => '%s', 'path' => '%s', 'hash' => '%s', 'mime_type' => '%s', 'modified' => '%d', 'width' => '%d', 'height' => '%d', 'original_size' => '%d', 'optimized_size' => '%d', 'percent' => '%d', 'optimization_level' => '%d', 'status' => '%s', 'error' => '%s', 'data' => '%s', ]; } /** * Default column values. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public function get_column_defaults() { return [ 'file_id' => 0, 'folder_id' => 0, 'file_date' => '0000-00-00 00:00:00', 'path' => '', 'hash' => '', 'mime_type' => '', 'modified' => 0, 'width' => 0, 'height' => 0, 'original_size' => 0, 'optimized_size' => null, 'percent' => null, 'optimization_level' => null, 'status' => null, 'error' => null, 'data' => [], ]; } /** * Get the query to create the table fields. * * For with and height: `smallint(2) unsigned` means 65,535px max. * * @since 1.7 * @access protected * @author Grégory Viguier * * @return string */ protected function get_table_schema() { return " file_id bigint(20) unsigned NOT NULL auto_increment, folder_id bigint(20) unsigned NOT NULL default 0, file_date datetime NOT NULL default '0000-00-00 00:00:00', path varchar(191) NOT NULL default '', hash varchar(32) NOT NULL default '', mime_type varchar(100) NOT NULL default '', modified tinyint(1) unsigned NOT NULL default 0, width smallint(2) unsigned NOT NULL default 0, height smallint(2) unsigned NOT NULL default 0, original_size int(4) unsigned NOT NULL default 0, optimized_size int(4) unsigned default NULL, percent smallint(2) unsigned default NULL, optimization_level tinyint(1) unsigned default NULL, status varchar(20) default NULL, error varchar(255) default NULL, data longtext default NULL, PRIMARY KEY (file_id), UNIQUE KEY path (path), KEY folder_id (folder_id), KEY optimization_level (optimization_level), KEY status (status), KEY modified (modified)"; } } classes/class-imagify-cron-rating.php 0000644 00000003500 15174671745 0013702 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * Class that handles the plugin rating cron. * * @since 1.7 * @author Grégory Viguier */ class Imagify_Cron_Rating extends Imagify_Abstract_Cron { use InstanceGetterTrait; /** * Class version. * * @var string * @since 1.7 */ const VERSION = '1.0'; /** * Cron name. * * @var string * @since 1.7 * @access protected */ protected $event_name = 'imagify_rating_event'; /** * Cron recurrence. * * @var string * @since 1.7 * @access protected */ protected $event_recurrence = 'daily'; /** * Cron time. * * @var string * @since 1.7 * @access protected */ protected $event_time = '15:00'; /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Initiate the event. * * @since 1.7 * @access public * @author Grégory Viguier */ public function schedule_event() { if ( ! wp_next_scheduled( $this->get_event_name() ) && ! get_site_transient( 'do_imagify_rating_cron' ) ) { wp_schedule_event( $this->get_event_timestamp(), $this->get_event_recurrence(), $this->get_event_name() ); } } /** * The event action. * * @since 1.7 * @access public * @author Grégory Viguier */ public function do_event() { // Stop the process if the plugin isn't installed for 3 days. if ( get_site_transient( 'imagify_seen_rating_notice' ) ) { return; } $user = get_imagify_user(); if ( ! is_wp_error( $user ) && isset( $user->image_count ) && (int) $user->image_count > 100 ) { set_site_transient( 'imagify_user_images_count', $user->image_count ); } } } classes/class-imagify-data.php 0000644 00000003367 15174671745 0012403 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * Class that handles the plugin data. * * @since 1.7 */ class Imagify_Data extends Imagify_Abstract_Options { use InstanceGetterTrait; /** * Class version. * * @var string * @since 1.7 */ const VERSION = '1.0'; /** * Suffix used in the name of the option. * * @var string * @since 1.7 * @access protected */ protected $identifier = 'data'; /** * The default values for the Imagify main options. * These are the "zero state" values. * Don't use null as value. * * @var array * @since 1.7 * @access protected */ protected $default_values = [ 'total_size_images_library' => 0.0, 'average_size_images_per_month' => 0.0, 'previous_quota_percent' => 0.0, ]; /** ----------------------------------------------------------------------------------------- */ /** SANITIZATION, VALIDATION ================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Sanitize and validate an option value. Basic casts have been made. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $key The option key. * @param mixed $value The value. * @param mixed $default_value The default value. * @return mixed */ public function sanitize_and_validate_value( $key, $value, $default_value ) { switch ( $key ) { case 'total_size_images_library': case 'average_size_images_per_month': if ( $value <= 0 ) { // Invalid. return 0.0; } return $value; case 'previous_quota_percent': $value = round( $value, 1 ); return min( max( 0, $value ), 100 ); } return false; } } classes/Dependencies/wp-media/event-manager/SubscriberInterface.php 0000644 00000002124 15174671745 0021500 0 ustar 00 <?php namespace Imagify\EventManagement; /** * A Subscriber knows what specific WordPress events it wants to listen to. * * When an EventManager adds a Subscriber, it gets all the WordPress events that * it wants to listen to. It then adds the subscriber as a listener for each of them. * * @author Carl Alexander <contact@carlalexander.ca> */ interface SubscriberInterface { /** * Returns an array of events that this subscriber wants to listen to. * * The array key is the event name. The value can be: * * * The method name * * An array with the method name and priority * * An array with the method name, priority and number of accepted arguments * * For instance: * * * array('hook_name' => 'method_name') * * array('hook_name' => array('method_name', $priority)) * * array('hook_name' => array('method_name', $priority, $accepted_args)) * * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2))) * * @return array */ public static function get_subscribed_events(); } classes/Dependencies/wp-media/event-manager/EventManagerAwareSubscriberInterface.php 0000644 00000000553 15174671745 0024761 0 ustar 00 <?php namespace Imagify\EventManagement; interface EventManagerAwareSubscriberInterface extends SubscriberInterface { /** * Set the WordPress event manager for the subscriber. * * @since 3.1 * @author Remy Perona * * @param EventManager $event_manager EventManager instance. */ public function set_event_manager( EventManager $event_manager ); } classes/Dependencies/wp-media/event-manager/EventManager.php 0000644 00000011455 15174671745 0020137 0 ustar 00 <?php namespace Imagify\EventManagement; /** * The event manager manages events using the WordPress plugin API. * * @since 3.1 * @author Carl Alexander <contact@carlalexander.ca> */ class EventManager { /** * Adds a callback to a specific hook of the WordPress plugin API. * * @uses add_filter() * * @param string $hook_name Name of the hook. * @param callable $callback Callback function. * @param int $priority Priority. * @param int $accepted_args Number of arguments. */ public function add_callback( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { add_filter( $hook_name, $callback, $priority, $accepted_args ); } /** * Add an event subscriber. * * The event manager registers all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param SubscriberInterface $subscriber SubscriberInterface implementation. */ public function add_subscriber( SubscriberInterface $subscriber ) { if ( $subscriber instanceof EventManagerAwareSubscriberInterface ) { $subscriber->set_event_manager( $this ); } $events = $subscriber->get_subscribed_events(); if ( empty( $events ) ) { return; } foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) { $this->add_subscriber_callback( $subscriber, $hook_name, $parameters ); } } /** * Checks the WordPress plugin API to see if the given hook has * the given callback. The priority of the callback will be returned * or false. If no callback is given will return true or false if * there's any callbacks registered to the hook. * * @uses has_filter() * * @param string $hook_name Hook name. * @param mixed $callback Callback. * * @return bool|int */ public function has_callback( $hook_name, $callback = false ) { return has_filter( $hook_name, $callback ); } /** * Removes the given callback from the given hook. The WordPress plugin API only * removes the hook if the callback and priority match a registered hook. * * @uses remove_filter() * * @param string $hook_name Hook name. * @param callable $callback Callback. * @param int $priority Priority. * * @return bool */ public function remove_callback( $hook_name, $callback, $priority = 10 ) { return remove_filter( $hook_name, $callback, $priority ); } /** * Remove an event subscriber. * * The event manager removes all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param SubscriberInterface $subscriber SubscriberInterface implementation. */ public function remove_subscriber( SubscriberInterface $subscriber ) { foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) { $this->remove_subscriber_callback( $subscriber, $hook_name, $parameters ); } } /** * Adds the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param SubscriberInterface $subscriber SubscriberInterface implementation. * @param string $hook_name Hook name. * @param mixed $parameters Parameters, can be a string, an array or a multidimensional array. */ private function add_subscriber_callback( SubscriberInterface $subscriber, $hook_name, $parameters ) { if ( is_string( $parameters ) ) { $this->add_callback( $hook_name, [ $subscriber, $parameters ] ); } elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) { foreach ( $parameters as $parameter ) { $this->add_subscriber_callback( $subscriber, $hook_name, $parameter ); } } elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) { $this->add_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10, isset( $parameters[2] ) ? $parameters[2] : 1 ); } } /** * Removes the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param SubscriberInterface $subscriber SubscriberInterface implementation. * @param string $hook_name Hook name. * @param mixed $parameters Parameters, can be a string, an array or a multidimensional array. */ private function remove_subscriber_callback( SubscriberInterface $subscriber, $hook_name, $parameters ) { if ( is_string( $parameters ) ) { $this->remove_callback( $hook_name, [ $subscriber, $parameters ] ); } elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) { foreach ( $parameters as $parameter ) { $this->remove_subscriber_callback( $subscriber, $hook_name, $parameter ); } } elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) { $this->remove_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10 ); } } } classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-background-process.php 0000644 00000055720 15174671745 0027302 0 ustar 00 <?php /** * WP Background Process * * @package WP-Background-Processing */ /** * Abstract Imagify_WP_Background_Process class. * * @abstract * @extends Imagify_WP_Async_Request */ abstract class Imagify_WP_Background_Process extends Imagify_WP_Async_Request { /** * The default query arg name used for passing the chain ID to new processes. */ const CHAIN_ID_ARG_NAME = 'chain_id'; /** * Unique background process chain ID. * * @var string */ private $chain_id; /** * Action * * (default value: 'background_process') * * @var string * @access protected */ protected $action = 'background_process'; /** * Start time of current process. * * (default value: 0) * * @var int * @access protected */ protected $start_time = 0; /** * Cron_hook_identifier * * @var string * @access protected */ protected $cron_hook_identifier; /** * Cron_interval_identifier * * @var string * @access protected */ protected $cron_interval_identifier; /** * Restrict object instantiation when using unserialize. * * @var bool|array */ protected $allowed_batch_data_classes = true; /** * The status set when process is cancelling. * * @var int */ const STATUS_CANCELLED = 1; /** * The status set when process is paused or pausing. * * @var int; */ const STATUS_PAUSED = 2; /** * Initiate new background process. * * @param bool|array $allowed_batch_data_classes Optional. Array of class Imagify_names that can be unserialized. Default true (any class). */ public function __construct( $allowed_batch_data_classes = true ) { parent::__construct(); if ( empty( $allowed_batch_data_classes ) && false !== $allowed_batch_data_classes ) { $allowed_batch_data_classes = true; } if ( ! is_bool( $allowed_batch_data_classes ) && ! is_array( $allowed_batch_data_classes ) ) { $allowed_batch_data_classes = true; } // If allowed_batch_data_classes property set in subclass, // only apply override if not allowing any class. if ( true === $this->allowed_batch_data_classes || true !== $allowed_batch_data_classes ) { $this->allowed_batch_data_classes = $allowed_batch_data_classes; } $this->cron_hook_identifier = $this->identifier . '_cron'; $this->cron_interval_identifier = $this->identifier . '_cron_interval'; add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); // Ensure dispatch query args included extra data. add_filter( $this->identifier . '_query_args', array( $this, 'filter_dispatch_query_args' ) ); } /** * Schedule the cron healthcheck and dispatch an async request to start processing the queue. * * @access public * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted. */ public function dispatch() { if ( $this->is_processing() ) { // Process already running. return false; } /** * Filter fired before background process dispatches its next process. * * @param bool $cancel Should the dispatch be cancelled? Default false. * @param string $chain_id The background process chain ID. */ $cancel = apply_filters( $this->identifier . '_pre_dispatch', false, $this->get_chain_id() ); if ( $cancel ) { return false; } // Schedule the cron healthcheck. $this->schedule_event(); // Perform remote post. return parent::dispatch(); } /** * Push to the queue. * * Note, save must be called in order to persist queued items to a batch for processing. * * @param mixed $data Data. * * @return $this */ public function push_to_queue( $data ) { $this->data[] = $data; return $this; } /** * Save the queued items for future processing. * * @return $this */ public function save() { $key = $this->generate_key(); if ( ! empty( $this->data ) ) { update_site_option( $key, $this->data ); } // Clean out data so that new data isn't prepended with closed session's data. $this->data = array(); return $this; } /** * Update a batch's queued items. * * @param string $key Key. * @param array $data Data. * * @return $this */ public function update( $key, $data ) { if ( ! empty( $data ) ) { update_site_option( $key, $data ); } return $this; } /** * Delete a batch of queued items. * * @param string $key Key. * * @return $this */ public function delete( $key ) { delete_site_option( $key ); return $this; } /** * Delete entire job queue. */ public function delete_all() { $batches = $this->get_batches(); foreach ( $batches as $batch ) { $this->delete( $batch->key ); } delete_site_option( $this->get_status_key() ); $this->cancelled(); } /** * Cancel job on next batch. */ public function cancel() { update_site_option( $this->get_status_key(), self::STATUS_CANCELLED ); // Just in case the job was paused at the time. $this->dispatch(); } /** * Has the process been cancelled? * * @return bool */ public function is_cancelled() { return $this->get_status() === self::STATUS_CANCELLED; } /** * Called when background process has been cancelled. */ protected function cancelled() { do_action( $this->identifier . '_cancelled', $this->get_chain_id() ); } /** * Pause job on next batch. */ public function pause() { update_site_option( $this->get_status_key(), self::STATUS_PAUSED ); } /** * Has the process been paused? * * @return bool */ public function is_paused() { return $this->get_status() === self::STATUS_PAUSED; } /** * Called when background process has been paused. */ protected function paused() { do_action( $this->identifier . '_paused', $this->get_chain_id() ); } /** * Resume job. */ public function resume() { delete_site_option( $this->get_status_key() ); $this->schedule_event(); $this->dispatch(); $this->resumed(); } /** * Called when background process has been resumed. */ protected function resumed() { do_action( $this->identifier . '_resumed', $this->get_chain_id() ); } /** * Is queued? * * @return bool */ public function is_queued() { return ! $this->is_queue_empty(); } /** * Is the tool currently active, e.g. starting, working, paused or cleaning up? * * @return bool */ public function is_active() { return $this->is_queued() || $this->is_processing() || $this->is_paused() || $this->is_cancelled(); } /** * Generate key for a batch. * * Generates a unique key based on microtime. Queue items are * given a unique key so that they can be merged upon save. * * @param int $length Optional max length to trim key to, defaults to 64 characters. * @param string $key Optional string to append to identifier before hash, defaults to "batch". * * @return string */ protected function generate_key( $length = 64, $key = 'batch' ) { $unique = md5( microtime() . wp_rand() ); $prepend = $this->identifier . '_' . $key . '_'; return substr( $prepend . $unique, 0, $length ); } /** * Get the status key. * * @return string */ protected function get_status_key() { return $this->identifier . '_status'; } /** * Get the status value for the process. * * @return int */ protected function get_status() { global $wpdb; if ( is_multisite() ) { $status = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->sitemeta WHERE meta_key = %s AND site_id = %d LIMIT 1", $this->get_status_key(), get_current_network_id() ) ); } else { $status = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $this->get_status_key() ) ); } return absint( $status ); } /** * Maybe process a batch of queued items. * * Checks whether data exists within the queue and that * the process is not already running. */ public function maybe_handle() { // Don't lock up other requests while processing. session_write_close(); check_ajax_referer( $this->identifier, 'nonce' ); // Background process already running. if ( $this->is_processing() ) { return $this->maybe_wp_die(); } // Cancel requested. if ( $this->is_cancelled() ) { $this->clear_scheduled_event(); $this->delete_all(); return $this->maybe_wp_die(); } // Pause requested. if ( $this->is_paused() ) { $this->clear_scheduled_event(); $this->paused(); return $this->maybe_wp_die(); } // No data to process. if ( $this->is_queue_empty() ) { return $this->maybe_wp_die(); } $this->handle(); return $this->maybe_wp_die(); } /** * Is queue empty? * * @return bool */ protected function is_queue_empty() { return empty( $this->get_batch() ); } /** * Is process running? * * Check whether the current process is already running * in a background process. * * @return bool * * @deprecated 1.1.0 Superseded. * @see is_processing() */ protected function is_process_running() { return $this->is_processing(); } /** * Is the background process currently running? * * @return bool */ public function is_processing() { if ( get_site_transient( $this->identifier . '_process_lock' ) ) { // Process already running. return true; } return false; } /** * Lock process. * * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that * defined in the time_exceeded() method. * * @param bool $reset_start_time Optional, default true. */ public function lock_process( $reset_start_time = true ) { if ( $reset_start_time ) { $this->start_time = time(); // Set start time of current process. } $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); $microtime = microtime(); $locked = set_site_transient( $this->identifier . '_process_lock', $microtime, $lock_duration ); /** * Action to note whether the background process managed to create its lock. * * The lock is used to signify that a process is running a task and no other * process should be allowed to run the same task until the lock is released. * * @param bool $locked Whether the lock was successfully created. * @param string $microtime Microtime string value used for the lock. * @param int $lock_duration Max number of seconds that the lock will live for. * @param string $chain_id Current background process chain ID. */ do_action( $this->identifier . '_process_locked', $locked, $microtime, $lock_duration, $this->get_chain_id() ); } /** * Unlock process. * * Unlock the process so that other instances can spawn. * * @return $this */ protected function unlock_process() { $unlocked = delete_site_transient( $this->identifier . '_process_lock' ); /** * Action to note whether the background process managed to release its lock. * * The lock is used to signify that a process is running a task and no other * process should be allowed to run the same task until the lock is released. * * @param bool $unlocked Whether the lock was released. * @param string $chain_id Current background process chain ID. */ do_action( $this->identifier . '_process_unlocked', $unlocked, $this->get_chain_id() ); return $this; } /** * Get batch. * * @return stdClass Return the first batch of queued items. */ protected function get_batch() { return array_reduce( $this->get_batches( 1 ), static function ( $carry, $batch ) { return $batch; }, array() ); } /** * Get batches. * * @param int $limit Number of batches to return, defaults to all. * * @return array of stdClass */ public function get_batches( $limit = 0 ) { global $wpdb; if ( empty( $limit ) || ! is_int( $limit ) ) { $limit = 0; } $table = $wpdb->options; $column = 'option_name'; $key_column = 'option_id'; $value_column = 'option_value'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; $key_column = 'meta_id'; $value_column = 'meta_value'; } $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; $sql = ' SELECT * FROM ' . $table . ' WHERE ' . $column . ' LIKE %s ORDER BY ' . $key_column . ' ASC '; $args = array( $key ); if ( ! empty( $limit ) ) { $sql .= ' LIMIT %d'; $args[] = $limit; } $items = $wpdb->get_results( $wpdb->prepare( $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $args ) ); $batches = array(); if ( ! empty( $items ) ) { $allowed_classes = $this->allowed_batch_data_classes; $batches = array_map( static function ( $item ) use ( $column, $value_column, $allowed_classes ) { $batch = new stdClass(); $batch->key = $item->{$column}; $batch->data = static::maybe_unserialize( $item->{$value_column}, $allowed_classes ); return $batch; }, $items ); } return $batches; } /** * Handle a dispatched request. * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. */ protected function handle() { $this->lock_process(); /** * Number of seconds to sleep between batches. Defaults to 0 seconds, minimum 0. * * @param int $seconds */ $throttle_seconds = max( 0, apply_filters( $this->identifier . '_seconds_between_batches', apply_filters( $this->prefix . '_seconds_between_batches', 0 ) ) ); do { $batch = $this->get_batch(); foreach ( $batch->data as $key => $value ) { $task = $this->task( $value ); if ( false !== $task ) { $batch->data[ $key ] = $task; } else { unset( $batch->data[ $key ] ); } // Keep the batch up to date while processing it. if ( ! empty( $batch->data ) ) { $this->update( $batch->key, $batch->data ); } // Let the server breathe a little. sleep( $throttle_seconds ); // Batch limits reached, or pause or cancel requested. if ( ! $this->should_continue() ) { break; } } // Delete current batch if fully processed. if ( empty( $batch->data ) ) { $this->delete( $batch->key ); } } while ( ! $this->is_queue_empty() && $this->should_continue() ); $this->unlock_process(); // Start next batch or complete process. if ( ! $this->is_queue_empty() ) { $this->dispatch(); } else { $this->complete(); } return $this->maybe_wp_die(); } /** * Memory exceeded? * * Ensures the batch process never exceeds 90% * of the maximum WordPress memory. * * @return bool */ protected function memory_exceeded() { $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory $current_memory = memory_get_usage( true ); $return = false; if ( $current_memory >= $memory_limit ) { $return = true; } return apply_filters( $this->identifier . '_memory_exceeded', $return ); } /** * Get memory limit in bytes. * * @return int */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { // Sensible default. $memory_limit = '128M'; } if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } return wp_convert_hr_to_bytes( $memory_limit ); } /** * Time limit exceeded? * * Ensures the batch never exceeds a sensible time limit. * A timeout limit of 30s is common on shared hosting. * * @return bool */ protected function time_exceeded() { $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds $return = false; if ( time() >= $finish ) { $return = true; } return apply_filters( $this->identifier . '_time_exceeded', $return ); } /** * Complete processing. * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). */ protected function complete() { delete_site_option( $this->get_status_key() ); // Remove the cron healthcheck job from the cron schedule. $this->clear_scheduled_event(); $this->completed(); } /** * Called when background process has completed. */ protected function completed() { do_action( $this->identifier . '_completed', $this->get_chain_id() ); } /** * Get the cron healthcheck interval in minutes. * * Default is 5 minutes, minimum is 1 minute. * * @return int */ public function get_cron_interval() { $interval = 5; if ( property_exists( $this, 'cron_interval' ) ) { $interval = $this->cron_interval; } $interval = apply_filters( $this->cron_interval_identifier, $interval ); return is_int( $interval ) && 0 < $interval ? $interval : 5; } /** * Schedule the cron healthcheck job. * * @access public * * @param mixed $schedules Schedules. * * @return mixed */ public function schedule_cron_healthcheck( $schedules ) { $interval = $this->get_cron_interval(); if ( 1 === $interval ) { $display = __( 'Every Minute' ); } else { $display = sprintf( __( 'Every %d Minutes' ), $interval ); } // Adds an "Every NNN Minute(s)" schedule to the existing cron schedules. $schedules[ $this->cron_interval_identifier ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, 'display' => $display, ); return $schedules; } /** * Handle cron healthcheck event. * * Restart the background process if not already running * and data exists in the queue. */ public function handle_cron_healthcheck() { if ( $this->is_processing() ) { // Background process already running. exit; } if ( $this->is_queue_empty() ) { // No data to process. $this->clear_scheduled_event(); exit; } $this->dispatch(); } /** * Schedule the cron healthcheck event. */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { wp_schedule_event( time() + ( $this->get_cron_interval() * MINUTE_IN_SECONDS ), $this->cron_interval_identifier, $this->cron_hook_identifier ); } } /** * Clear scheduled cron healthcheck event. */ protected function clear_scheduled_event() { $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); if ( $timestamp ) { wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); } } /** * Cancel the background process. * * Stop processing queue items, clear cron job and delete batch. * * @deprecated 1.1.0 Superseded. * @see cancel() */ public function cancel_process() { $this->cancel(); } /** * Perform task with queued item. * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing * in the next pass through. Or, return false to remove the * item from the queue. * * @param mixed $item Queue item to iterate over. * * @return mixed */ abstract protected function task( $item ); /** * Maybe unserialize data, but not if an object. * * @param mixed $data Data to be unserialized. * @param bool|array $allowed_classes Array of class Imagify_names that can be unserialized. * * @return mixed */ protected static function maybe_unserialize( $data, $allowed_classes ) { if ( is_serialized( $data ) ) { $options = array(); if ( is_bool( $allowed_classes ) || is_array( $allowed_classes ) ) { $options['allowed_classes'] = $allowed_classes; } return @unserialize( $data, $options ); // @phpcs:ignore } return $data; } /** * Should any processing continue? * * @return bool */ public function should_continue() { /** * Filter whether the current background process should continue running the task * if there is data to be processed. * * If the processing time or memory limits have been exceeded, the value will be false. * If pause or cancel have been requested, the value will be false. * * It is very unlikely that you would want to override a false value with true. * * If false is returned here, it does not necessarily mean background processing is * complete. If there is batch data still to be processed and pause or cancel have not * been requested, it simply means this background process should spawn a new process * for the chain to continue processing and then close itself down. * * @param bool $continue Should the current process continue processing the task? * @param string $chain_id The current background process chain's ID. * * @return bool */ return apply_filters( $this->identifier . '_should_continue', ! ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_paused() || $this->is_cancelled() ), $this->get_chain_id() ); } /** * Get the string used to identify this type of background process. * * @return string */ public function get_identifier() { return $this->identifier; } /** * Return the current background process chain's ID. * * If the chain's ID hasn't been set before this function is first used, * and hasn't been passed as a query arg during dispatch, * the chain ID will be generated before being returned. * * @return string */ public function get_chain_id() { // The line below is a custom fix while waiting for the upstream package to be updated. if ( empty( $this->chain_id ) && wp_doing_ajax() && isset( $_REQUEST['action'] ) && $_REQUEST['action'] === $this->identifier ) { check_ajax_referer( $this->identifier, 'nonce' ); if ( ! empty( $_GET[ $this->get_chain_id_arg_name() ] ) ) { $chain_id = sanitize_key( $_GET[ $this->get_chain_id_arg_name() ] ); if ( wp_is_uuid( $chain_id ) ) { $this->chain_id = $chain_id; return $this->chain_id; } } } if ( empty( $this->chain_id ) ) { $this->chain_id = wp_generate_uuid4(); } return $this->chain_id; } /** * Filters the query arguments used during an async request. * * @param array $args Current query args. * * @return array */ public function filter_dispatch_query_args( $args ) { $args[ $this->get_chain_id_arg_name() ] = $this->get_chain_id(); return $args; } /** * Get the query arg name used for passing the chain ID to new processes. * * @return string */ private function get_chain_id_arg_name() { static $chain_id_arg_name; if ( ! empty( $chain_id_arg_name ) ) { return $chain_id_arg_name; } /** * Filter the query arg name used for passing the chain ID to new processes. * * If you encounter problems with using the default query arg name, you can * change it with this filter. * * @param string $chain_id_arg_name Default "chain_id". * * @return string */ $chain_id_arg_name = apply_filters( $this->identifier . '_chain_id_arg_name', self::CHAIN_ID_ARG_NAME ); if ( ! is_string( $chain_id_arg_name ) || empty( $chain_id_arg_name ) ) { $chain_id_arg_name = self::CHAIN_ID_ARG_NAME; } return $chain_id_arg_name; } } classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-async-request.php 0000644 00000007471 15174671745 0026312 0 ustar 00 <?php /** * WP Async Request * * @package WP-Background-Processing */ /** * Abstract Imagify_WP_Async_Request class. * * @abstract */ abstract class Imagify_WP_Async_Request { /** * Prefix * * (default value: 'wp') * * @var string * @access protected */ protected $prefix = 'wp'; /** * Action * * (default value: 'async_request') * * @var string * @access protected */ protected $action = 'async_request'; /** * Identifier * * @var mixed * @access protected */ protected $identifier; /** * Data * * (default value: array()) * * @var array * @access protected */ protected $data = array(); /** * Initiate new async request. */ public function __construct() { $this->identifier = $this->prefix . '_' . $this->action; add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); } /** * Set data used during the request. * * @param array $data Data. * * @return $this */ public function data( $data ) { $this->data = $data; return $this; } /** * Dispatch the async request. * * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted. */ public function dispatch() { $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); $args = $this->get_post_args(); return wp_remote_post( esc_url_raw( $url ), $args ); } /** * Get query args. * * @return array */ protected function get_query_args() { if ( property_exists( $this, 'query_args' ) ) { return $this->query_args; } $args = array( 'action' => $this->identifier, 'nonce' => wp_create_nonce( $this->identifier ), ); /** * Filters the query arguments used during an async request. * * @param array $args */ return apply_filters( $this->identifier . '_query_args', $args ); } /** * Get query URL. * * @return string */ protected function get_query_url() { if ( property_exists( $this, 'query_url' ) ) { return $this->query_url; } $url = admin_url( 'admin-ajax.php' ); /** * Filters the query URL used during an async request. * * @param string $url */ return apply_filters( $this->identifier . '_query_url', $url ); } /** * Get post args. * * @return array */ protected function get_post_args() { if ( property_exists( $this, 'post_args' ) ) { return $this->post_args; } $args = array( 'timeout' => 5, 'blocking' => false, 'body' => $this->data, 'cookies' => $_COOKIE, // Passing cookies ensures request is performed as initiating user. 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // Local requests, fine to pass false. ); /** * Filters the post arguments used during an async request. * * @param array $args */ return apply_filters( $this->identifier . '_post_args', $args ); } /** * Maybe handle a dispatched request. * * Check for correct nonce and pass to handler. * * @return void|mixed */ public function maybe_handle() { // Don't lock up other requests while processing. session_write_close(); check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); return $this->maybe_wp_die(); } /** * Should the process exit with wp_die? * * @param mixed $return What to return if filter says don't die, default is null. * * @return void|mixed */ protected function maybe_wp_die( $return = null ) { /** * Should wp_die be used? * * @return bool */ if ( apply_filters( $this->identifier . '_wp_die', true ) ) { wp_die(); } return $return; } /** * Handle a dispatched request. * * Override this method to perform any actions required * during the async request. */ abstract protected function handle(); } classes/Dependencies/.gitkeep 0000644 00000000000 15174671745 0012227 0 ustar 00 classes/class-imagify-admin-ajax-post.php 0000644 00000113314 15174671745 0014460 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * Class that handles admin ajax/post callbacks. * * @since 1.6.11 */ class Imagify_Admin_Ajax_Post extends Imagify_Admin_Ajax_Post_Deprecated { use InstanceGetterTrait; /** * Class version. * * @var string */ const VERSION = '1.1'; /** * Actions to be triggered on admin ajax and admin post. * * @var array */ protected $ajax_post_actions = [ // WP optimization. 'imagify_manual_optimize', 'imagify_manual_reoptimize', 'imagify_optimize_missing_sizes', 'imagify_generate_nextgen_versions', 'imagify_delete_nextgen_versions', 'imagify_restore', // Custom folders optimization. 'imagify_optimize_file', 'imagify_reoptimize_file', 'imagify_restore_file', 'imagify_refresh_file_modified', ]; /** * Actions to be triggered only on admin ajax. * * @var array */ protected $ajax_only_actions = [ // Settings page. 'imagify_check_backup_dir_is_writable', 'imagify_get_files_tree', // Account. 'imagify_signup', 'imagify_check_api_key_validity', 'imagify_get_prices', 'imagify_check_coupon', 'imagify_get_discount', 'imagify_get_images_counts', 'imagify_update_estimate_sizes', 'imagify_get_user_data', 'imagify_delete_user_data_cache', // Various. 'nopriv_imagify_rpc', ]; /** * Actions to be triggered only on admin post. * * @var array */ protected $post_only_actions = [ // Custom folders optimization. 'imagify_scan_custom_folders', // Various. 'imagify_dismiss_ad', ]; /** * Filesystem object. * * @var Imagify_Filesystem */ protected $filesystem; /** ----------------------------------------------------------------------------------------- */ /** INIT ==================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * The constructor. */ public function __construct() { $this->filesystem = Imagify_Filesystem::get_instance(); } /** * Launch the hooks. * * @since 1.6.11 */ public function init() { $doing_ajax = wp_doing_ajax(); foreach ( $this->ajax_post_actions as $action ) { $action_callback = "{$action}_callback"; if ( $doing_ajax ) { add_action( 'wp_ajax_' . $action, [ $this, $action_callback ] ); } add_action( 'admin_post_' . $action, [ $this, $action_callback ] ); } // Actions triggered only on admin ajax. if ( $doing_ajax ) { foreach ( $this->ajax_only_actions as $action ) { add_action( 'wp_ajax_' . $action, [ $this, $action . '_callback' ] ); } } // Actions triggered on admin post. foreach ( $this->post_only_actions as $action ) { add_action( 'admin_post_' . $action, [ $this, $action . '_callback' ] ); } } /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION PROCESSES ================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Optimize one media. * * @since 1.9 * * @param int $media_id The media ID. * @param string $context The context. * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ protected function optimize_media( $media_id, $context ) { return imagify_get_optimization_process( $media_id, $context )->optimize(); } /** * Re-optimize a media to a different optimization level. * * @since 1.9 * * @param int $media_id The media ID. * @param string $context The context. * @param int $level The optimization level. * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ protected function reoptimize_media( $media_id, $context, $level ) { return imagify_get_optimization_process( $media_id, $context )->reoptimize( $level ); } /** * Optimize all files from a media, whatever this media’s previous optimization status (will be restored if needed). * This is used by the bulk optimization page. * * @since 1.9 * * @param int $media_id The media ID. * @param string $context The context. * @param int $level The optimization level. * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ protected function force_optimize( $media_id, $context, $level ) { $process = imagify_get_optimization_process( $media_id, $context ); $data = $process->get_data(); // Restore before re-optimizing. if ( $data->is_optimized() ) { $result = $process->restore(); if ( is_wp_error( $result ) ) { // Return an error message. return $result; } } return $process->optimize( $level ); } /** * Optimize one or some thumbnails that are not optimized yet. * * @since 1.9 * * @param int $media_id The media ID. * @param string $context The context. * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ protected function optimize_missing_sizes( $media_id, $context ) { return imagify_get_optimization_process( $media_id, $context )->optimize_missing_thumbnails(); } /** * Generate next-gen images if they are missing. * * @since 1.9 * * @param int $media_id The media ID. * @param string $context The context. * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ protected function generate_nextgen_versions( $media_id, $context ) { return imagify_get_optimization_process( $media_id, $context )->generate_nextgen_versions(); } /** * Delete Next gen images for media that are "already_optimize". * * @since 1.9.6 * * @param int $media_id The media ID. * @param string $context The context. * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ protected function delete_nextgen_versions( $media_id, $context ) { $process = imagify_get_optimization_process( $media_id, $context ); if ( ! $process->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $data = $process->get_data(); if ( ! $data->is_already_optimized() ) { return new WP_Error( 'not_already_optimized', __( 'This media does not have the right optimization status.', 'imagify' ) ); } if ( ! $process->has_next_gen() ) { return true; } $data->delete_optimization_data(); $deleted = $process->delete_nextgen_files( false, true ); if ( is_wp_error( $deleted ) ) { return new WP_Error( 'nextgen_not_deleted', __( 'Previous next-gen files could not be deleted.', 'imagify' ) ); } return true; } /** * Restore a media. * * @since 1.9 * * @param int $media_id The media ID. * @param string $context The context. * @return bool|WP_Error True on success. A \WP_Error instance on failure. */ protected function restore_media( $media_id, $context ) { return imagify_get_optimization_process( $media_id, $context )->restore(); } /** ----------------------------------------------------------------------------------------- */ /** WP OPTIMIZATION CALLBACKS =============================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Optimize all thumbnails of a specific image with the manual method. * * @since 1.6.11 */ public function imagify_manual_optimize_callback() { $context = $this->get_context(); $media_id = $this->get_media_id(); if ( ! $media_id || ! $context ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } imagify_check_nonce( 'imagify-optimize-' . $media_id . '-' . $context ); if ( ! imagify_get_context( $context )->current_user_can( 'manual-optimize', $media_id ) ) { imagify_die(); } $result = $this->optimize_media( $media_id, $context ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); if ( is_wp_error( $result ) ) { // Return an error message. $output = $result->get_error_message(); wp_send_json_error( [ 'html' => $output ] ); } wp_send_json_success(); } /** * Optimize all thumbnails of a specific image with a different optimization level. * * @since 1.6.11 */ public function imagify_manual_reoptimize_callback() { $context = $this->get_context(); $media_id = $this->get_media_id(); if ( ! $media_id || ! $context ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } imagify_check_nonce( 'imagify-manual-reoptimize-' . $media_id . '-' . $context ); if ( ! imagify_get_context( $context )->current_user_can( 'manual-optimize', $media_id ) ) { imagify_die(); } $result = $this->reoptimize_media( $media_id, $context, $this->get_optimization_level() ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); if ( is_wp_error( $result ) ) { // Return an error message. $output = $result->get_error_message(); wp_send_json_error( [ 'html' => $output ] ); } wp_send_json_success(); } /** * Optimize one or some thumbnails that are not optimized yet. * * @since 1.6.11 */ public function imagify_optimize_missing_sizes_callback() { $context = $this->get_context(); $media_id = $this->get_media_id(); if ( ! $media_id || ! $context ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } imagify_check_nonce( 'imagify-optimize-missing-sizes-' . $media_id . '-' . $context ); if ( ! imagify_get_context( $context )->current_user_can( 'manual-optimize', $media_id ) ) { imagify_die(); } $result = $this->optimize_missing_sizes( $media_id, $context ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); if ( is_wp_error( $result ) ) { // Return an error message. $output = $result->get_error_message(); wp_send_json_error( [ 'html' => $output ] ); } wp_send_json_success(); } /** * Generate next-gen images if they are missing. * * @since 1.9 */ public function imagify_generate_nextgen_versions_callback() { $context = $this->get_context(); $media_id = $this->get_media_id(); if ( ! $media_id || ! $context ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } imagify_check_nonce( 'imagify-generate-nextgen-versions-' . $media_id . '-' . $context ); if ( ! imagify_get_context( $context )->current_user_can( 'manual-optimize', $media_id ) ) { imagify_die(); } $result = $this->generate_nextgen_versions( $media_id, $context ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); if ( is_wp_error( $result ) ) { // Return an error message. $output = $result->get_error_message(); wp_send_json_error( [ 'html' => $output ] ); } wp_send_json_success(); } /** * Generate next-gen images if they are missing. * * @since 1.9.6 */ public function imagify_delete_nextgen_versions_callback() { $context = $this->get_context(); $media_id = $this->get_media_id(); if ( ! $media_id || ! $context ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } imagify_check_nonce( 'imagify-delete-nextgen-versions-' . $media_id . '-' . $context ); if ( ! imagify_get_context( $context )->current_user_can( 'manual-restore', $media_id ) ) { imagify_die(); } $result = $this->delete_nextgen_versions( $media_id, $context ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); if ( is_wp_error( $result ) ) { // Return an error message. $output = $result->get_error_message(); wp_send_json_error( [ 'html' => $output ] ); } wp_send_json_success(); } /** * Process a restoration to the original attachment. * * @since 1.6.11 */ public function imagify_restore_callback() { $context = $this->get_context(); $media_id = $this->get_media_id(); if ( ! $media_id || ! $context ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } imagify_check_nonce( 'imagify-restore-' . $media_id . '-' . $context ); if ( ! imagify_get_context( $context )->current_user_can( 'manual-restore', $media_id ) ) { imagify_die(); } $result = $this->restore_media( $media_id, $context ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); if ( is_wp_error( $result ) ) { // Return an error message. $output = $result->get_error_message(); wp_send_json_error( [ 'html' => $output ] ); } // Return the optimization button. $output = Imagify_Views::get_instance()->get_template( 'button/optimize', [ 'url' => get_imagify_admin_url( 'optimize', [ 'attachment_id' => $media_id, 'context' => $context, ] ), ] ); wp_send_json_success( [ 'html' => $output ] ); } /** ----------------------------------------------------------------------------------------- */ /** CUSTOM FOLDERS OPTIMIZATION CALLBACKS =================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Optimize a file. * * @since 1.7 */ public function imagify_optimize_file_callback() { imagify_check_nonce( 'imagify_optimize_file' ); $media_id = $this->get_media_id( 'GET', 'id' ); if ( ! $media_id ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manual-optimize', $media_id ) ) { imagify_die(); } $result = $this->optimize_media( $media_id, 'custom-folders' ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); if ( is_wp_error( $result ) ) { // Return an error message. wp_send_json_error( $result->get_error_message() ); } wp_send_json_success(); } /** * Re-optimize a file. * * @since 1.7 */ public function imagify_reoptimize_file_callback() { imagify_check_nonce( 'imagify_reoptimize_file' ); $media_id = $this->get_media_id( 'GET', 'id' ); if ( ! $media_id ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manual-optimize', $media_id ) ) { imagify_die(); } $level = $this->get_optimization_level( 'GET', 'level' ); $result = $this->reoptimize_media( $media_id, 'custom-folders', $level ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); if ( is_wp_error( $result ) ) { // Return an error message. wp_send_json_error( $result->get_error_message() ); } wp_send_json_success(); } /** * Restore a file. * * @since 1.7 */ public function imagify_restore_file_callback() { imagify_check_nonce( 'imagify_restore_file' ); $media_id = $this->get_media_id( 'GET', 'id' ); if ( ! $media_id ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manual-restore', $media_id ) ) { imagify_die(); } $result = $this->restore_media( $media_id, 'custom-folders' ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); if ( is_wp_error( $result ) ) { // Return an error message. wp_send_json_error( $result->get_error_message() ); } $process = imagify_get_optimization_process( $media_id, 'custom-folders' ); $this->file_optimization_output( $process ); } /** * Check if a file has been modified, and update the database accordingly. * * @since 1.7 */ public function imagify_refresh_file_modified_callback() { imagify_check_nonce( 'imagify_refresh_file_modified' ); $media_id = $this->get_media_id( 'GET', 'id' ); if ( ! $media_id ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manual-optimize', $media_id ) ) { imagify_die(); } $process = imagify_get_optimization_process( $media_id, 'custom-folders' ); $result = Imagify_Custom_Folders::refresh_file( $process ); if ( is_wp_error( $result ) ) { // The media is not valid or has been removed from the database. $message = $result->get_error_message(); imagify_maybe_redirect( $message ); wp_send_json_error( [ 'row' => $message, ] ); } imagify_maybe_redirect(); // Return some HTML to the ajax call. $this->file_optimization_output( $process ); } /** * Look for new files in custom folders. * * @since 1.7 */ public function imagify_scan_custom_folders_callback() { imagify_check_nonce( 'imagify_scan_custom_folders' ); if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'optimize' ) ) { imagify_die(); } $folder = (int) filter_input( INPUT_GET, 'folder', FILTER_VALIDATE_INT ); if ( $folder > 0 ) { // A specific custom folder (selected or not). $folders_db = Imagify_Folders_DB::get_instance(); $folders_key = $folders_db->get_primary_key(); $folder = $folders_db->get( $folder ); if ( ! $folder ) { // This should not happen. imagify_maybe_redirect( __( 'This folder is not in the database.', 'imagify' ) ); } $folder['folder_path'] = Imagify_Files_Scan::remove_placeholder( $folder['path'] ); $folders = [ $folder[ $folders_key ] => $folder, ]; Imagify_Custom_Folders::get_files_from_folders( $folders, [ 'add_inactive_folder_files' => true, ] ); imagify_maybe_redirect(); } // All selected custom folders. $folders = Imagify_Custom_Folders::get_folders( [ 'active' => true, ] ); Imagify_Custom_Folders::get_files_from_folders( $folders ); imagify_maybe_redirect(); } /** ----------------------------------------------------------------------------------------- */ /** SETTINGS PAGE CALLBACKS ================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Check if the backup directory is writable. * This is used to display an error message in the plugin's settings page. * * @since 1.6.11 */ public function imagify_check_backup_dir_is_writable_callback() { imagify_check_nonce( 'imagify_check_backup_dir_is_writable' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } wp_send_json_success( [ 'is_writable' => (int) Imagify_Requirements::attachments_backup_dir_is_writable(), ] ); } /** * Get files and folders that are direct children of a given folder. * * @since 1.7 */ public function imagify_get_files_tree_callback() { imagify_check_nonce( 'get-files-tree' ); if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manage' ) ) { imagify_die(); } if ( ! isset( $_POST['folder'] ) || '' === $_POST['folder'] ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } $folder = sanitize_text_field( wp_unslash( $_POST['folder'] ) ); $folder = trailingslashit( $folder ); $folder = realpath( $this->filesystem->get_site_root() . ltrim( $folder, '/' ) ); if ( ! $folder ) { imagify_die( __( 'This folder doesn\'t exist.', 'imagify' ) ); } if ( ! $this->filesystem->is_dir( $folder ) ) { imagify_die( __( 'This file is not a folder.', 'imagify' ) ); } $folder = $this->filesystem->normalize_dir_path( $folder ); if ( Imagify_Files_Scan::is_path_forbidden( $folder ) ) { imagify_die( __( 'This folder is not allowed.', 'imagify' ) ); } // Finally we made all our validations. $selected = ! empty( $_POST['selected'] ) && is_array( $_POST['selected'] ) ? array_flip( array_map( 'sanitize_text_field', wp_unslash( $_POST['selected'] ) ) ) : []; $views = Imagify_Views::get_instance(); $output = ''; if ( $this->filesystem->is_site_root( $folder ) ) { $output .= $views->get_template( 'part-settings-files-tree-row', [ 'relative_path' => '/', // Value #///# Label. 'checkbox_value' => '{{ROOT}}/#///#' . esc_attr__( 'Site\'s root', 'imagify' ), 'checkbox_id' => 'ABSPATH', 'checkbox_selected' => isset( $selected['{{ROOT}}/'] ), 'label' => __( 'Site\'s root', 'imagify' ), 'no_button' => true, ] ); } $dir = new DirectoryIterator( $folder ); $dir = new Imagify_Files_Iterator( $dir ); $images = 0; foreach ( new IteratorIterator( $dir ) as $file ) { if ( ! $file->isDir() ) { ++$images; continue; } $folder_path = trailingslashit( $file->getPathname() ); $relative_path = $this->filesystem->make_path_relative( $folder_path ); $placeholder = Imagify_Files_Scan::add_placeholder( $folder_path ); $output .= $views->get_template( 'part-settings-files-tree-row', [ 'relative_path' => esc_attr( $relative_path ), // Value #///# Label. 'checkbox_value' => esc_attr( $placeholder ) . '#///#' . esc_attr( $relative_path ), 'checkbox_id' => sanitize_html_class( $placeholder ), 'checkbox_selected' => isset( $selected[ $placeholder ] ), 'label' => $this->filesystem->file_name( $folder_path ), ] ); } if ( $images ) { /* translators: %s is a formatted number, dont use %d. */ $output .= '<li class="imagify-number-of-images-in-folder"><em><span class="dashicons dashicons-images-alt"></span> ' . sprintf( _n( '%s Media File', '%s Media Files', $images, 'imagify' ), number_format_i18n( $images ) ) . '</em></li>'; } if ( ! $output ) { $output .= '<li class="imagify-empty-folder"><em>' . __( 'No optimizable files', 'imagify' ) . '</em></li>'; } wp_send_json_success( $output ); } /** ----------------------------------------------------------------------------------------- */ /** IMAGIFY ACCOUNT CALLBACKS =============================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Create a new Imagify account. * * @since 1.6.11 */ public function imagify_signup_callback() { imagify_check_nonce( 'imagify-signup', 'imagifysignupnonce' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } if ( empty( $_GET['email'] ) ) { imagify_die( __( 'Empty email address.', 'imagify' ) ); } $email = sanitize_email( wp_unslash( $_GET['email'] ) ); if ( ! is_email( $email ) ) { imagify_die( __( 'Not a valid email address.', 'imagify' ) ); } $data = [ 'email' => $email, 'password' => wp_generate_password( 12, false ), 'lang' => imagify_get_locale(), ]; $response = add_imagify_user( $data ); if ( is_wp_error( $response ) ) { imagify_die( $response ); } wp_send_json_success(); } /** * Check the API key validity. * * @since 1.6.11 */ public function imagify_check_api_key_validity_callback() { imagify_check_nonce( 'imagify-check-api-key', 'imagifycheckapikeynonce' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } if ( empty( $_GET['api_key'] ) ) { imagify_die( __( 'Empty API key.', 'imagify' ) ); } $api_key = sanitize_key( wp_unslash( $_GET['api_key'] ) ); $response = get_imagify_status( $api_key ); if ( is_wp_error( $response ) ) { imagify_die( $response ); } update_imagify_option( 'api_key', $api_key ); delete_transient( 'imagify_user_cache' ); wp_send_json_success(); } /** * Get pricings from API for Onetime and Plans at the same time. * * @since 1.6.11 */ public function imagify_get_prices_callback() { imagify_check_nonce( 'imagify_get_pricing_' . get_current_user_id(), 'imagifynonce' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } $prices_all = get_imagify_all_prices(); if ( is_wp_error( $prices_all ) ) { imagify_die( $prices_all ); } if ( ! is_object( $prices_all ) ) { imagify_die( __( 'Wrongly formatted response from our server.', 'imagify' ) ); } wp_send_json_success( [ 'monthlies' => $prices_all->Plans, ] ); } /** * Check Coupon code on modal popin. * * @since 1.6.11 */ public function imagify_check_coupon_callback() { imagify_check_nonce( 'imagify_get_pricing_' . get_current_user_id(), 'imagifynonce' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } if ( empty( $_POST['coupon'] ) ) { wp_send_json_success( [ 'success' => false, 'detail' => __( 'Coupon is empty.', 'imagify' ), ] ); } $coupon = sanitize_text_field( wp_unslash( $_POST['coupon'] ) ); $coupon = check_imagify_coupon_code( $coupon ); if ( is_wp_error( $coupon ) ) { imagify_die( $coupon ); } wp_send_json_success( imagify_translate_api_message( $coupon ) ); } /** * Get current discount promotion to display information on payment modal. * * @since 1.6.11 */ public function imagify_get_discount_callback() { imagify_check_nonce( 'imagify_get_pricing_' . get_current_user_id(), 'imagifynonce' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } wp_send_json_success( imagify_translate_api_message( check_imagify_discount() ) ); } /** * Get estimated sizes from the WordPress library. * * @since 1.6.11 */ public function imagify_get_images_counts_callback() { imagify_check_nonce( 'imagify_get_pricing_' . get_current_user_id(), 'imagifynonce' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } $raw_total_size_in_library = imagify_calculate_total_size_images_library() + Imagify_Files_Stats::get_overall_original_size(); $raw_average_per_month = imagify_calculate_average_size_images_per_month() + Imagify_Files_Stats::calculate_average_size_per_month(); Imagify_Data::get_instance()->set( [ 'total_size_images_library' => $raw_total_size_in_library, 'average_size_images_per_month' => $raw_average_per_month, ] ); wp_send_json_success( [ 'total_library_size' => [ 'raw' => $raw_total_size_in_library, 'human' => imagify_size_format( $raw_total_size_in_library ), ], 'average_month_size' => [ 'raw' => $raw_average_per_month, 'human' => imagify_size_format( $raw_average_per_month ), ], ] ); } /** * Estimate sizes and update the options values for them. * * @since 1.6.11 */ public function imagify_update_estimate_sizes_callback() { imagify_check_nonce( 'update_estimate_sizes' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } $raw_total_size_in_library = imagify_calculate_total_size_images_library() + Imagify_Files_Stats::get_overall_original_size(); $raw_average_per_month = imagify_calculate_average_size_images_per_month() + Imagify_Files_Stats::calculate_average_size_per_month(); Imagify_Data::get_instance()->set( [ 'total_size_images_library' => $raw_total_size_in_library, 'average_size_images_per_month' => $raw_average_per_month, ] ); die( 1 ); } /** * Get the Imagify User data. * * @since 1.7 */ public function imagify_get_user_data_callback() { imagify_check_nonce( 'imagify_get_user_data' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } $user = imagify_cache_user(); if ( ! $user || ! $user->id ) { imagify_die( __( 'Couldn\'t get user data.', 'imagify' ) ); } // Remove useless sensitive data. unset( $user->email ); if ( ! $user->get_percent_unconsumed_quota ) { $user->best_plan_title = __( 'Oops, It\'s Over!', 'imagify' ); } elseif ( $user->get_percent_unconsumed_quota <= 20 ) { $user->best_plan_title = __( 'Oops, It\'s almost over!', 'imagify' ); } else { $user->best_plan_title = __( 'Unlock Imagify\'s full potential', 'imagify' ); } wp_send_json_success( $user ); } /** * Delete the Imagify User data cache. * * @since 1.9.5 */ public function imagify_delete_user_data_cache_callback() { imagify_check_nonce( 'imagify_delete_user_data_cache' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } imagify_delete_cached_user(); wp_send_json_success(); } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS CALLBACKS ======================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Bridge between XML-RPC and actions triggered by imagify_do_async_job(). * When XML-RPC is used, a current user is set, but no cookies are set, so they cannot be sent with the request. Instead we stored the user ID in a transient. * * @since 1.6.11 * @see imagify_do_async_job() */ public function nopriv_imagify_rpc_callback() { if ( empty( $_POST['imagify_rpc_action'] ) || empty( $_POST['imagify_rpc_id'] ) ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } $action = sanitize_text_field( wp_unslash( $_POST['imagify_rpc_action'] ) ); if ( 32 !== strlen( $action ) ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } // Not necessary but just in case, whitelist the original action. $actions = array_flip( $this->ajax_only_actions ); unset( $actions['nopriv_imagify_rpc'] ); if ( ! isset( $actions[ $action ] ) ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } // Get the user ID. $rpc_id = sanitize_key( $_POST['imagify_rpc_id'] ); $user_id = absint( get_transient( 'imagify_rpc_' . $rpc_id ) ); $user = $user_id ? get_userdata( $user_id ) : false; delete_transient( 'imagify_rpc_' . $rpc_id ); if ( ! $user || ! $user->exists() ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } // The current user must be set before verifying the nonce. wp_set_current_user( $user_id ); imagify_check_nonce( 'imagify_rpc_' . $rpc_id, 'imagify_rpc_nonce' ); // Trigger the action we originally wanted. $_POST['action'] = $action; unset( $_POST['imagify_rpc_action'], $_POST['imagify_rpc_id'], $_POST['imagify_rpc_nonce'] ); /** This hook is documented in wp-admin/admin-ajax.php. */ do_action( 'wp_ajax_' . $action ); } /** * Store the "closed" status of the ads. * * @since 1.7 */ public function imagify_dismiss_ad_callback() { imagify_check_nonce( 'imagify-dismiss-ad' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } if ( empty( $_GET['ad'] ) ) { imagify_maybe_redirect(); wp_send_json_error(); } $notice = sanitize_text_field( wp_unslash( $_GET['ad'] ) ); if ( ! $notice ) { imagify_maybe_redirect(); wp_send_json_error(); } $user_id = get_current_user_id(); $notices = get_user_meta( $user_id, '_imagify_ignore_ads', true ); $notices = $notices && is_array( $notices ) ? array_flip( $notices ) : []; if ( isset( $notices[ $notice ] ) ) { imagify_maybe_redirect(); wp_send_json_success(); } $notices = array_flip( $notices ); $notices[] = $notice; $notices = array_filter( $notices ); $notices = array_values( $notices ); update_user_meta( $user_id, '_imagify_ignore_ads', $notices ); imagify_maybe_redirect(); wp_send_json_success(); } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS HELPERS ========================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the submitted optimization level. * * @since 1.7 * @since 1.9 Added $method and $parameter parameters. * * @param string $method The method used: 'GET' (default), or 'POST'. * @param string $parameter The name of the parameter to look for. * @return int */ public function get_optimization_level( $method = 'GET', $parameter = 'optimization_level' ) { $method = 'POST' === $method ? INPUT_POST : INPUT_GET; $level = filter_input( $method, $parameter ); if ( ! is_numeric( $level ) || $level < 0 || $level > 2 ) { if ( get_imagify_option( 'lossless' ) ) { return 0; } return get_imagify_option( 'optimization_level' ); } return (int) $level; } /** * Get the submitted context. * * @since 1.9 * * @param string $method The method used: 'GET' (default), or 'POST'. * @param string $parameter The name of the parameter to look for. * @return string */ public function get_context( $method = 'GET', $parameter = 'context' ) { if ( empty( $_POST[ $parameter ] ) && empty( $_GET[ $parameter ] ) ) { // No context. return 'noop'; } $context = 'POST' === $method ? sanitize_text_field( wp_unslash( $_POST[ $parameter ] ) ) : sanitize_text_field( wp_unslash( $_GET[ $parameter ] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended return imagify_sanitize_context( $context ); } /** * Get the submitted media ID. * * @since 1.9 * * @param string $method The method used: 'GET' (default), or 'POST'. * @param string $parameter The name of the parameter to look for. * @return int */ public function get_media_id( $method = 'GET', $parameter = 'attachment_id' ) { $method = 'POST' === $method ? INPUT_POST : INPUT_GET; $media_id = filter_input( $method, $parameter ); if ( ! is_numeric( $media_id ) || $media_id < 0 ) { return 0; } return (int) $media_id; } /** * Get the submitted folder_type. * * @since 1.9 * * @param string $method The method used: 'GET' (default), or 'POST'. * @param string $parameter The name of the parameter to look for. * * @return string */ public function get_folder_type( $method = 'GET', $parameter = 'folder_type' ) { if ( empty( $_POST[ $parameter ] ) && empty( $_GET[ $parameter ] ) ) { // No folder type. return 'noop'; } $folder_type = 'POST' === $method ? sanitize_text_field( wp_unslash( $_POST[ $parameter ] ) ) : sanitize_text_field( wp_unslash( $_GET[ $parameter ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended return $folder_type; } /** * Get the submitted imagify action. * * @since 1.9 * * @param string $method The method used: 'GET' (default), or 'POST'. * @param string $parameter The name of the parameter to look for. * * @return string */ public function get_imagify_action( $method = 'GET', $parameter = 'imagify_action' ) { if ( empty( $_POST[ $parameter ] ) && empty( $_GET[ $parameter ] ) ) { // No action. return 'optimize'; } $action = 'POST' === $method ? sanitize_text_field( wp_unslash( $_POST[ $parameter ] ) ) : sanitize_text_field( wp_unslash( $_GET[ $parameter ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended return $action ? $action : 'optimize'; } /** * Get the Bulk class name depending on a context. * * @since 1.9 * * @param string $context The context name. Default values are 'wp' and 'custom-folders'. * @return string The Bulk class name. */ public function get_bulk_class_name( $context ) { switch ( $context ) { case 'wp': $class_name = '\\Imagify\\Bulk\\WP'; break; case 'custom-folders': $class_name = '\\Imagify\\Bulk\\CustomFolders'; break; default: $class_name = '\\Imagify\\Bulk\\Noop'; } /** * Filter the name of the class to use for bulk process. * * @since 1.9 * * @param int $class_name The class name. * @param string $context The context name. */ $class_name = apply_filters( 'imagify_bulk_class_name', $class_name, $context ); return '\\' . ltrim( $class_name, '\\' ); } /** * Get the Bulk instance depending on a context. * * @since 1.9 * * @param string $context The context name. Default values are 'wp' and 'custom-folders'. * @return BulkInterface The optimization process instance. */ public function get_bulk_instance( $context ) { $class_name = $this->get_bulk_class_name( $context ); return new $class_name(); } /** * Check if the user has a valid account and has quota. Die on failure. * * @since 1.7 */ public function check_can_optimize() { if ( ! Imagify_Requirements::is_api_key_valid() ) { if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { wp_send_json_error( [ 'message' => 'invalid-api-key' ] ); } imagify_die( __( 'Your API key is not valid!', 'imagify' ) ); } if ( Imagify_Requirements::is_over_quota() ) { if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { wp_send_json_error( [ 'message' => 'over-quota' ] ); } imagify_die( __( 'You have used all your credits!', 'imagify' ) ); } } /** * Get a media columns for the "Other Media" page. * * @since 1.9 * * @param object $process A \Imagify\Optimization\Process\CustomFolders object. * @param object $list_table A Imagify_Files_List_Table object. * @return array An array of HTML, keyed by column name. */ public function get_media_columns( $process, $list_table ) { $item = (object) [ 'process' => $process ]; return [ 'folder' => $list_table->get_column( 'folder', $item ), 'optimization' => $list_table->get_column( 'optimization', $item ), 'status' => $list_table->get_column( 'status', $item ), 'optimization_level' => $list_table->get_column( 'optimization_level', $item ), 'actions' => $list_table->get_column( 'actions', $item ), 'title' => $list_table->get_column( 'title', $item ), // This one must remain after the "optimization" column, otherwize the data for the comparison tool won't be up-to-date. ]; } /** * After a file optimization, restore, or whatever, redirect the user or output HTML for ajax. * * @since 1.7 * @since 1.9 Removed parameter $result. * @since 1.9 Added $folder in the returned JSON. * * @param object $process A \Imagify\Optimization\Process\CustomFolders object. */ protected function file_optimization_output( $process ) { $list_table = new Imagify_Files_List_Table( [ 'screen' => 'imagify-files', ] ); wp_send_json_success( [ 'columns' => $this->get_media_columns( $process, $list_table ), ] ); } } classes/class-imagify-requirements.php 0000644 00000023507 15174671745 0014213 0 ustar 00 <?php use Imagify\User\User; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class used to check that Imagify has everything it needs. * * @since 1.7.1 * @author Grégory Viguier */ class Imagify_Requirements { /** * Cache the test results. * * @var object * @access protected * @since 1.7.1 * @author Grégory Viguier */ protected static $supports = []; /** ----------------------------------------------------------------------------------------- */ /** SERVER ================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Test for cURL. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param bool $reset_cache True to get a fresh value. * @return bool */ public static function supports_curl( $reset_cache = false ) { if ( $reset_cache || ! isset( self::$supports['curl'] ) ) { self::$supports['curl'] = function_exists( 'curl_init' ) && function_exists( 'curl_exec' ); } return self::$supports['curl']; } /** * Test for imageMagick and GD. * Similar to _wp_image_editor_choose(), but allows to test for multiple mime types at once. * * @since 1.7.1 * @access public * @see _wp_image_editor_choose() * @author Grégory Viguier * * @param bool $reset_cache True to get a fresh value. * @return bool */ public static function supports_image_editor( $reset_cache = false ) { if ( ! $reset_cache && isset( self::$supports['image_editor'] ) ) { return self::$supports['image_editor']; } require_once ABSPATH . WPINC . '/class-wp-image-editor.php'; require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php'; require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php'; self::$supports['image_editor'] = false; $args = [ 'path' => IMAGIFY_PATH . 'assets/images/imagify-logo.png', 'mime_types' => imagify_get_mime_types( 'image' ), 'methods' => Imagify_Attachment::get_editor_methods(), ]; /** This filter is documented in /wp-includes/media.php. */ $implementations = apply_filters( 'wp_image_editors', [ 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' ] ); foreach ( $implementations as $implementation ) { if ( ! call_user_func( [ $implementation, 'test' ], $args ) ) { continue; } foreach ( $args['mime_types'] as $mime_type ) { if ( ! call_user_func( [ $implementation, 'supports_mime_type' ], $mime_type ) ) { continue 2; } } if ( array_diff( $args['methods'], get_class_methods( $implementation ) ) ) { continue; } self::$supports['image_editor'] = true; break; } return self::$supports['image_editor']; } /** * Test for the uploads directory. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param bool $reset_cache True to get a fresh value. * @return bool */ public static function supports_uploads( $reset_cache = false ) { if ( ! $reset_cache && isset( self::$supports['uploads'] ) ) { return self::$supports['uploads']; } self::$supports['uploads'] = Imagify_Filesystem::get_instance()->get_upload_basedir(); if ( self::$supports['uploads'] ) { self::$supports['uploads'] = Imagify_Filesystem::get_instance()->is_writable( self::$supports['uploads'] ); } return self::$supports['uploads']; } /** * Test if external requests are blocked for Imagify. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param bool $reset_cache True to get a fresh value. * @return bool */ public static function is_imagify_blocked( $reset_cache = false ) { if ( ! $reset_cache && isset( self::$supports['imagify_blocked'] ) ) { return self::$supports['imagify_blocked']; } if ( ! defined( 'WP_HTTP_BLOCK_EXTERNAL' ) || ! WP_HTTP_BLOCK_EXTERNAL ) { self::$supports['imagify_blocked'] = false; return self::$supports['imagify_blocked']; } if ( ! defined( 'WP_ACCESSIBLE_HOSTS' ) ) { self::$supports['imagify_blocked'] = true; return self::$supports['imagify_blocked']; } $accessible_hosts = explode( ',', WP_ACCESSIBLE_HOSTS ); $accessible_hosts = array_map( 'trim', $accessible_hosts ); $accessible_hosts = array_flip( $accessible_hosts ); if ( isset( $accessible_hosts['*.imagify.io'] ) ) { self::$supports['imagify_blocked'] = false; return self::$supports['imagify_blocked']; } if ( isset( $accessible_hosts['imagify.io'], $accessible_hosts['app.imagify.io'], $accessible_hosts['storage.imagify.io'] ) ) { self::$supports['imagify_blocked'] = false; return self::$supports['imagify_blocked']; } self::$supports['imagify_blocked'] = true; return self::$supports['imagify_blocked']; } /** ----------------------------------------------------------------------------------------- */ /** IMAGIFY BACKUP DIRECTORIES ============================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Test for the attachments backup directory. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param bool $reset_cache True to get a fresh value. * @return bool */ public static function attachments_backup_dir_is_writable( $reset_cache = false ) { if ( $reset_cache || ! isset( self::$supports['attachment_backups'] ) ) { self::$supports['attachment_backups'] = imagify_backup_dir_is_writable(); } return self::$supports['attachment_backups']; } /** * Test for the custom folders backup directory. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param bool $reset_cache True to get a fresh value. * @return bool */ public static function custom_folders_backup_dir_is_writable( $reset_cache = false ) { if ( $reset_cache || ! isset( self::$supports['custom_folder_backups'] ) ) { self::$supports['custom_folder_backups'] = Imagify_Custom_Folders::backup_dir_is_writable(); } return self::$supports['custom_folder_backups']; } /** ----------------------------------------------------------------------------------------- */ /** IMAGIFY API ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Determine if the Imagify API is available by checking the API version. * The result is cached for 3 minutes. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param bool $reset_cache True to get a fresh value. * @return bool */ public static function is_api_up( $reset_cache = false ) { if ( ! $reset_cache && isset( self::$supports['api_up'] ) ) { return self::$supports['api_up']; } $transient_name = 'imagify_check_api_version'; $transient_expiration = 3 * MINUTE_IN_SECONDS; $transient_value = $reset_cache ? false : get_site_transient( $transient_name ); if ( false !== $transient_value ) { self::$supports['api_up'] = (bool) $transient_value; return self::$supports['api_up']; } self::$supports['api_up'] = ! is_wp_error( get_imagify_api_version() ); $transient_value = (int) self::$supports['api_up']; set_site_transient( $transient_name, $transient_value, $transient_expiration ); return self::$supports['api_up']; } /** * Test for the Imagify API key validity. * A positive result is cached for 1 year. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param bool $reset_cache True to get a fresh value. * @return bool */ public static function is_api_key_valid( $reset_cache = false ) { if ( $reset_cache ) { self::reset_cache( 'api_key_valid' ); } if ( isset( self::$supports['api_key_valid'] ) ) { return self::$supports['api_key_valid']; } if ( ! Imagify_Options::get_instance()->get( 'api_key' ) ) { self::$supports['api_key_valid'] = false; return self::$supports['api_key_valid']; } if ( get_site_transient( 'imagify_check_licence_1' ) ) { self::$supports['api_key_valid'] = true; return self::$supports['api_key_valid']; } if ( is_wp_error( get_imagify_user() ) ) { self::$supports['api_key_valid'] = false; return self::$supports['api_key_valid']; } self::$supports['api_key_valid'] = true; set_site_transient( 'imagify_check_licence_1', 1, YEAR_IN_SECONDS ); return self::$supports['api_key_valid']; } /** * Test for the Imagify account quota. * * @since 1.7.1 * @since 1.9.9 Return false when the API cannot be reached. * @access public * @author Grégory Viguier * * @param bool $reset_cache True to get a fresh value. * @return bool True when over quota. False otherwise, even when the API cannot be reached. */ public static function is_over_quota( $reset_cache = false ) { if ( ! $reset_cache && isset( self::$supports['over_quota'] ) ) { return self::$supports['over_quota']; } $user = new User(); self::$supports['over_quota'] = $user->get_error() ? false : $user->is_over_quota(); return self::$supports['over_quota']; } /** ----------------------------------------------------------------------------------------- */ /** CLASS CACHE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Reset a test cache. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $cache_key Cache key. */ public static function reset_cache( $cache_key ) { unset( self::$supports[ $cache_key ] ); $transients = [ 'api_up' => 'imagify_check_api_version', 'api_key_valid' => 'imagify_check_licence_1', ]; if ( isset( $transients[ $cache_key ] ) && get_site_transient( $transients[ $cache_key ] ) ) { delete_site_transient( $transients[ $cache_key ] ); } } } classes/class-imagify-folders-db.php 0000644 00000011566 15174671745 0013513 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * DB class that handles files in "custom folders". * * @since 1.7 * @author Grégory Viguier */ class Imagify_Folders_DB extends Imagify_Abstract_DB { use InstanceGetterTrait; /** * Class version. * * @var string */ const VERSION = '1.0.1'; /** * The suffix used in the name of the database table (so, without the wpdb prefix). * * @var string * @since 1.7 * @access protected */ protected $table = 'imagify_folders'; /** * The version of our database table. * * @var int * @since 1.7 * @access protected */ protected $table_version = 100; /** * Tell if the table is the same for each site of a Multisite. * * @var bool * @since 1.7 * @access protected */ protected $table_is_global = true; /** * The name of the primary column. * * @var string * @since 1.7 * @access protected */ protected $primary_key = 'folder_id'; /** * Whitelist of columns. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public function get_columns() { return [ 'folder_id' => '%d', 'path' => '%s', 'active' => '%d', ]; } /** * Default column values. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public function get_column_defaults() { return [ 'folder_id' => 0, 'path' => '', 'active' => 0, ]; } /** * Get the query to create the table fields. * * @since 1.7 * @access protected * @author Grégory Viguier * * @return string */ protected function get_table_schema() { return " folder_id bigint(20) unsigned NOT NULL auto_increment, path varchar(191) NOT NULL default '', active tinyint(1) unsigned NOT NULL default 0, PRIMARY KEY (folder_id), UNIQUE KEY path (path), KEY active (active)"; } /** * Tell if folders are selected in the plugin settings. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public function has_active_folders() { global $wpdb; $column = esc_sql( $this->get_primary_key() ); return (bool) $wpdb->get_var( "SELECT $column FROM $this->table_name WHERE active = 1 LIMIT 1;" ); // WPCS: unprepared SQL ok. } /** * Retrieve active folders (checked in the settings). * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_select A column name. * @return array */ public function get_active_folders_column( $column_select ) { global $wpdb; $column = esc_sql( $column_select ); $result = $wpdb->get_col( "SELECT $column FROM $this->table_name WHERE active = 1;" ); // WPCS: unprepared SQL ok. return $this->cast_col( $result, $column_select ); } /** * Retrieve active folders (checked in the settings) by the specified column / values. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_select A column name. * @param string $column_where A column name. * @param array $column_values An array of values. * @return array */ public function get_active_folders_column_in( $column_select, $column_where, $column_values ) { global $wpdb; $column = esc_sql( $column_select ); $column_where = esc_sql( $column_where ); $column_values = Imagify_DB::prepare_values_list( $column_values ); $result = $wpdb->get_col( "SELECT $column FROM $this->table_name WHERE $column_where IN ( $column_values ) AND active = 1;" ); // WPCS: unprepared SQL ok. return $this->cast_col( $result, $column_select ); } /** * Retrieve active folders (checked in the settings) by the specified column / values. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_select A column name. * @param string $column_where A column name. * @param array $column_values An array of values. * @return array */ public function get_active_folders_column_not_in( $column_select, $column_where, $column_values ) { global $wpdb; $column = esc_sql( $column_select ); $column_where = esc_sql( $column_where ); $column_values = Imagify_DB::prepare_values_list( $column_values ); $result = $wpdb->get_col( "SELECT $column FROM $this->table_name WHERE $column_where NOT IN ( $column_values ) AND active = 1;" ); // WPCS: unprepared SQL ok. return $this->cast_col( $result, $column_select ); } /** * Retrieve not active folders (not checked in the settings). * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_select A column name. * @return array */ public function get_inactive_folders_column( $column_select ) { global $wpdb; $column = esc_sql( $column_select ); $result = $wpdb->get_col( "SELECT $column FROM $this->table_name WHERE active != 1;" ); // WPCS: unprepared SQL ok. return $this->cast_col( $result, $column_select ); } } classes/class-imagify-filesystem.php 0000644 00000072312 15174671745 0013652 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; /** * Class that enhance the WP filesystem class. * * @since 1.7.1 * @author Grégory Viguier */ class Imagify_Filesystem extends WP_Filesystem_Direct { use InstanceGetterTrait; /** * Class version. * * @var string */ const VERSION = '1.2'; /** * Delimiter used for regex patterns. * * @var string * @since 1.8 * @author Grégory Viguier */ const PATTERN_DELIMITER = '@'; /** ----------------------------------------------------------------------------------------- */ /** INSTANCIATION =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Constructor. * * @since 1.7.1 * @access public * @author Grégory Viguier */ public function __construct() { // Define the permission constants if not already done. if ( ! defined( 'FS_CHMOD_DIR' ) ) { define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) ); } if ( ! defined( 'FS_CHMOD_FILE' ) ) { define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) ); } parent::__construct( '' ); } /** ----------------------------------------------------------------------------------------- */ /** CUSTOM TOOLS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the file name. * Replacement for basename(). * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path Path to the file. * @return string|bool The base name of the given path. False on failure. */ public function file_name( $file_path ) { if ( ! $file_path ) { return false; } return wp_basename( $file_path ); } /** * Get the parent directory's path. * Replacement for dirname(). * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path Path to the file. * @return string|bool The directory path with a trailing slash. False on failure. */ public function dir_path( $file_path ) { if ( ! $file_path ) { return false; } $file_path = dirname( $file_path ); return $this->is_root( $file_path ) ? $this->get_root() : trailingslashit( $file_path ); } /** * Get information about a file path. * Replacement for pathinfo(). * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path Path to the file. * @param string $option If present, specifies a specific element to be returned; one of 'dir_path', 'file_name', 'extension' or 'file_base'. * If option is not specified, returns all available elements. * @return array|string|null If the option parameter is not passed, an associative array containing the following elements is returned: 'dir_path' (with trailing slash), 'file_name' (with extension), 'extension' (if any), and 'file_base' (without extension). */ public function path_info( $file_path, $option = null ) { if ( ! $file_path ) { if ( isset( $option ) ) { return ''; } return [ 'dir_path' => '', 'file_name' => '', 'extension' => null, 'file_base' => '', ]; } if ( isset( $option ) ) { $options = [ 'dir_path' => PATHINFO_DIRNAME, 'file_name' => PATHINFO_BASENAME, 'extension' => PATHINFO_EXTENSION, 'file_base' => PATHINFO_FILENAME, ]; if ( ! isset( $options[ $option ] ) ) { return ''; } $output = pathinfo( $file_path, $options[ $option ] ); if ( 'dir_path' !== $option ) { return $output; } return $this->is_root( $output ) ? $this->get_root() : trailingslashit( $output ); } $output = pathinfo( $file_path ); $output['dirname'] = $this->is_root( $output['dirname'] ) ? $this->get_root() : trailingslashit( $output['dirname'] ); $output['extension'] = isset( $output['extension'] ) ? $output['extension'] : null; // '/www/htdocs/inc/lib.inc.php' return [ 'dir_path' => $output['dirname'], // '/www/htdocs/inc/' 'file_name' => $output['basename'], // 'lib.inc.php' 'extension' => $output['extension'], // 'php' 'file_base' => $output['filename'], // 'lib.inc' ]; } /** * Recursive directory creation based on full path. Will attempt to set permissions on folders. * Replacement for recursive mkdir(). * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $path Full path to attempt to create. * @return bool Whether the path was created. True if path already exists. */ public function make_dir( $path ) { /* * Safe mode fails with a trailing slash under certain PHP versions. */ $path = untrailingslashit( wp_normalize_path( $path ) ); if ( $this->is_root( $path ) ) { return $this->is_dir( $this->get_root() ) && $this->is_writable( $this->get_root() ); } if ( $this->exists( $path ) ) { return $this->is_dir( $path ) && $this->is_writable( $path ); } $site_root = $this->get_site_root(); if ( strpos( $path, $site_root ) !== 0 ) { return false; } $bits = preg_replace( '@^' . preg_quote( $site_root, '@' ) . '@i', '', $path ); $bits = explode( '/', trim( $bits, '/' ) ); $path = untrailingslashit( $site_root ); foreach ( $bits as $bit ) { $parent_path = $path; $path .= '/' . $bit; if ( $this->exists( $path ) ) { if ( ! $this->is_dir( $path ) ) { return false; } continue; } if ( ! $this->is_writable( $parent_path ) ) { $this->chmod_dir( $parent_path ); if ( ! $this->is_writable( $parent_path ) ) { return false; } } $this->mkdir( $path ); if ( ! $this->exists( $path ) ) { return false; } $this->touch( trailingslashit( $path ) . 'index.php' ); } return true; } /** * Set a file permissions using FS_CHMOD_FILE. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path Path to the file. * @return bool True on success, false on failure. */ public function chmod_file( $file_path ) { if ( ! $file_path ) { return false; } return $this->chmod( $file_path, FS_CHMOD_FILE ); } /** * Set a directory permissions using FS_CHMOD_DIR. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path Path to the directory. * @return bool True on success, false on failure. */ public function chmod_dir( $file_path ) { if ( ! $file_path ) { return false; } return $this->chmod( $file_path, FS_CHMOD_DIR ); } /** * Get a file mime type. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path A file path (prefered) or a filename. * @return string|bool A mime type. False on failure: the test is limited to mime types supported by Imagify. */ public function get_mime_type( $file_path ) { if ( ! $file_path ) { return false; } $file_type = wp_check_filetype( $file_path, imagify_get_mime_types() ); return $file_type['type']; } /** * Get a file modification date, formated as "mysql". Fallback to current date. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path Path to the file. * @return string The date. */ public function get_date( $file_path ) { static $offset; if ( ! $file_path ) { return current_time( 'mysql' ); } $date = $this->mtime( $file_path ); if ( ! $date ) { return current_time( 'mysql' ); } if ( ! isset( $offset ) ) { $offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS; } return gmdate( 'Y-m-d H:i:s', $date + $offset ); } /** * Tell if a file is symlinked. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path An absolute path. * @return bool */ public function is_symlinked( $file_path ) { static $site_root; static $plugin_paths = []; global $wp_plugin_paths; if ( ! $file_path ) { return false; } $real_path = realpath( $file_path ); if ( ! $real_path ) { return false; } if ( ! isset( $site_root ) ) { $site_root = $this->normalize_path_for_comparison( $this->get_site_root() ); } $lower_file_path = $this->normalize_path_for_comparison( $real_path ); if ( strpos( $lower_file_path, $site_root ) !== 0 ) { return true; } if ( $wp_plugin_paths && is_array( $wp_plugin_paths ) ) { if ( ! $plugin_paths ) { foreach ( $wp_plugin_paths as $dir => $real_dir ) { $dir = $this->normalize_path_for_comparison( $dir ); $plugin_paths[ $dir ] = $this->normalize_path_for_comparison( $real_dir ); } } $lower_file_path = $this->normalize_path_for_comparison( $file_path ); foreach ( $plugin_paths as $dir => $real_dir ) { if ( strpos( $lower_file_path, $dir ) === 0 ) { return true; } } } return false; } /** * Tell if a file is a pdf. * * @since 1.8 * @access public * @author Grégory Viguier * * @param string $file_path Path to the file. * @return bool */ public function is_pdf( $file_path ) { if ( function_exists( 'finfo_fopen' ) ) { $finfo = finfo_open( FILEINFO_MIME ); if ( $finfo ) { $mimetype = finfo_file( $finfo, $file_path ); if ( false !== $mimetype ) { return 'application/pdf' === $mimetype; } } } if ( function_exists( 'mime_content_type' ) ) { $mimetype = mime_content_type( $file_path ); return 'application/pdf' === $mimetype; } return false; } /** ----------------------------------------------------------------------------------------- */ /** CLASS OVERWRITES ======================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Move a file and apply chmod. * If the file failed to be moved once, a 2nd attempt is made after applying chmod. * * @since 1.8 * @access public * @author Grégory Viguier * * @param string $source Path to the file to move. * @param string $destination Path to the destination. * @param bool $overwrite Allow to overwrite existing file at destination. * @return bool True on success, false on failure. */ public function move( $source, $destination, $overwrite = false ) { if ( parent::move( $source, $destination, $overwrite ) ) { return $this->chmod_file( $destination ); } if ( ! $this->chmod_file( $destination ) ) { return false; } if ( parent::move( $source, $destination, $overwrite ) ) { return $this->chmod_file( $destination ); } return false; } /** * Determine if a file or directory is writable. * This function is used to work around certain ACL issues in PHP primarily affecting Windows Servers. * Replacement for is_writable(). * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path Path to the file. * @return bool */ public function is_writable( $file_path ) { if ( ! $file_path ) { return false; } return wp_is_writable( $file_path ); } /** ----------------------------------------------------------------------------------------- */ /** WORK WITH IMAGES ======================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if a file is an image. * * @since 1.8 * @access public * @author Grégory Viguier * * @param string $file_path Path to the file. * @return bool */ public function is_image( $file_path ) { if ( function_exists( 'finfo_fopen' ) ) { $finfo = finfo_open( FILEINFO_MIME ); if ( $finfo ) { $mimetype = finfo_file( $finfo, $file_path ); if ( false !== $mimetype ) { return strpos( $mimetype, 'image/' ) === 0; } } } if ( function_exists( 'exif_imagetype' ) ) { $mimetype = exif_imagetype( $file_path ); return (bool) $mimetype; } if ( function_exists( 'mime_content_type' ) ) { $mimetype = mime_content_type( $file_path ); return strpos( $mimetype, 'image/' ) === 0; } return false; } /** * Get an image data. * Replacement for getimagesize(). * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path Path to the file. * @return array The image data. An empty array on failure. */ public function get_image_size( $file_path ) { if ( ! $file_path ) { return []; } $size = @getimagesize( $file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( ! $size || ! isset( $size[0], $size[1] ) ) { return []; } return [ 0 => (int) $size[0], 1 => (int) $size[1], 'width' => (int) $size[0], 'height' => (int) $size[1], 'type' => (int) $size[2], 'attr' => $size[3], 'channels' => isset( $size['channels'] ) ? (int) $size['channels'] : null, 'bits' => isset( $size['bits'] ) ? (int) $size['bits'] : null, 'mime' => $size['mime'], ]; } /** * Tell if exif_read_data() is available. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @return bool */ public function can_get_exif() { static $callable; if ( ! isset( $callable ) ) { $callable = is_callable( 'exif_read_data' ); } return $callable; } /** * Get the EXIF headers from an image file. * Replacement for exif_read_data(). * * @since 1.7.1 * @access public * @author Grégory Viguier * @see https://secure.php.net/manual/en/function.exif-read-data.php * * @param string $file_path Path to the file. * @param string $sections A comma separated list of sections that need to be present in file to produce a result array. See exif_read_data() documentation for values: FILE, COMPUTED, ANY_TAG, IFD0, THUMBNAIL, COMMENT, EXIF. * @param bool $arrays Specifies whether or not each section becomes an array. The sections COMPUTED, THUMBNAIL, and COMMENT always become arrays as they may contain values whose names conflict with other sections. * @param bool $thumbnail When set to TRUE the thumbnail itself is read. Otherwise, only the tagged data is read. * @return array The EXIF headers. An empty array on failure. */ public function get_image_exif( $file_path, $sections = null, $arrays = false, $thumbnail = false ) { if ( ! $file_path || ! $this->can_get_exif() ) { return []; } $exif = @exif_read_data( $file_path, $sections, $arrays, $thumbnail ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged return is_array( $exif ) ? $exif : []; } /** * Tell if a file is an animated gif. * * @since 1.9.5 * @access public * @source https://www.php.net/manual/en/function.imagecreatefromgif.php#104473 * @author Grégory Viguier * * @param string $file_path Path to the file. * @return bool|null Null if the file cannot be read. */ public function is_animated_gif( $file_path ) { if ( $this->path_info( $file_path, 'extension' ) !== 'gif' ) { // Not a gif file. return false; } $fh = @fopen( $file_path, 'rb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_fopen if ( ! $fh ) { // Could not open the file. return null; } /** * An animated gif contains multiple "frames", with each frame having a header made up of: * - a static 4-byte sequence (\x00\x21\xF9\x04), * - 4 variable bytes, * - a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?). */ $count = 0; // We read through the file til we reach the end of the file, or we've found at least 2 frame headers. while ( ! feof( $fh ) && $count < 2 ) { // Read 100kb at a time. $chunk = fread( $fh, 1024 * 100 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread $count += preg_match_all( '#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches ); } fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose return $count > 1; } /** ----------------------------------------------------------------------------------------- */ /** WORK WITH PATHS ========================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Make an absolute path relative to WordPress' root folder. * Also works for files from registered symlinked plugins. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path An absolute path. * @param string $base A base path to use instead of ABSPATH. * @return string|bool A relative path. Can return the absolute path or false in case of a failure. */ public function make_path_relative( $file_path, $base = '' ) { global $wp_plugin_paths; if ( ! $file_path ) { return false; } $file_path = wp_normalize_path( $file_path ); $base = $base ? $this->normalize_dir_path( $base ) : $this->get_site_root(); $pos = strpos( $file_path, $base ); if ( false === $pos && $wp_plugin_paths && is_array( $wp_plugin_paths ) ) { // The file is probably part of a symlinked plugin. arsort( $wp_plugin_paths ); foreach ( $wp_plugin_paths as $dir => $real_dir ) { if ( strpos( $file_path, $real_dir ) === 0 ) { $file_path = wp_normalize_path( $dir . substr( $file_path, strlen( $real_dir ) ) ); } } $pos = strpos( $file_path, $base ); } if ( false === $pos ) { // We're in trouble. return $file_path; } return substr_replace( $file_path, '', 0, $pos + strlen( $base ) ); } /** * Normalize a directory path. * The path is normalized and a trailing slash is added. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path The file path. * @return string The normalized dir path. */ public function normalize_dir_path( $file_path ) { return wp_normalize_path( trailingslashit( $file_path ) ); } /** * Normalize a file path, aiming for path comparison. * The path is normalized, case-lowered, and a trailing slash is added. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path The file path. * @return string The normalized file path. */ public function normalize_path_for_comparison( $file_path ) { return strtolower( $this->normalize_dir_path( $file_path ) ); } /** ----------------------------------------------------------------------------------------- */ /** SOME WELL KNOWN PATHS AND URLS ========================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if WordPress is installed in its own directory: aka WP's path !== site's path. * * @since 1.8.1 * @access public * @see https://codex.wordpress.org/Giving_WordPress_Its_Own_Directory * @author Grégory Viguier * * @return string */ public function has_wp_its_own_directory() { return $this->get_abspath() !== $this->get_site_root(); } /** * The path to the server's root is not always '/', it can also be '//' or 'C://'. * I am get_root. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @return string The path to the server's root. */ public function get_root() { static $groot; if ( isset( $groot ) ) { return $groot; } $groot = preg_replace( '@^((?:.:)?/+).*@', '$1', $this->get_site_root() ); return $groot; } /** * Tell if a path is the server's root. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $path The path. * @return bool */ public function is_root( $path ) { $path = rtrim( $path, '/\\' ); return '.' === $path || '' === $path || preg_match( '@^.:$@', $path ); } /** * Get the path to the site's root. * This is an improved version of get_home_path() that *should* work in almost every cases. * Because creating a constant like ABSPATH was too simple. * * @since 1.8.1 * @access public * @see get_home_path() * @author Grégory Viguier * * @return string */ public function get_site_root() { static $root_path; if ( isset( $root_path ) ) { return $root_path; } /** * Filter the path to the site's root. * * @since 1.8.1 * @author Grégory Viguier * * @param string $root_path Path to the site's root. Default is null. */ $root_path = apply_filters( 'imagify_site_root', null ); if ( is_string( $root_path ) ) { $root_path = trailingslashit( wp_normalize_path( $root_path ) ); return $root_path; } $home = set_url_scheme( untrailingslashit( get_option( 'home' ) ), 'http' ); $siteurl = set_url_scheme( untrailingslashit( get_option( 'siteurl' ) ), 'http' ); if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) { $wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */ $pos = strripos( str_replace( '\\', '/', ABSPATH ), trailingslashit( $wp_path_rel_to_home ) ); $root_path = substr( ABSPATH, 0, $pos ); $root_path = trailingslashit( wp_normalize_path( $root_path ) ); return $root_path; } if ( ! defined( 'PATH_CURRENT_SITE' ) || ! is_multisite() || is_main_site() ) { $root_path = $this->get_abspath(); return $root_path; } if ( empty( $_SERVER['DOCUMENT_ROOT'] ) ) { return $root_path; } /** * For a multisite in its own directory, get_home_path() returns the expected path only for the main site. * * Friend, each time an attempt is made to improve this method, and especially this part, please increment the following counter. * Improvement attempts: 3. */ $document_root = realpath( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // `realpath()` is needed for those cases where $_SERVER['DOCUMENT_ROOT'] is totally different from ABSPATH. $document_root = trailingslashit( str_replace( '\\', '/', $document_root ) ); $path_current_site = trim( str_replace( '\\', '/', PATH_CURRENT_SITE ), '/' ); $root_path = trailingslashit( wp_normalize_path( $document_root . $path_current_site ) ); return $root_path; } /** * Get the URL of the site's root. It corresponds to the main site's home page URL. * * @since 1.8.1 * @access public * @author Grégory Viguier * * @return string */ public function get_site_root_url() { static $root_url; if ( isset( $root_url ) ) { return $root_url; } if ( ! is_multisite() || is_main_site() ) { $root_url = home_url( '/' ); return $root_url; } $current_network = false; if ( function_exists( 'get_network' ) ) { $current_network = get_network(); } elseif ( function_exists( 'get_current_site' ) ) { $current_network = get_current_site(); } if ( ! $current_network ) { $root_url = home_url( '/' ); return $root_url; } $root_url = is_ssl() ? 'https' : 'http'; $root_url = set_url_scheme( 'http://' . $current_network->domain . $current_network->path, $root_url ); $root_url = trailingslashit( $root_url ); return $root_url; } /** * Tell if a path is the site's root. * * @since 1.8.1 * @access public * @author Grégory Viguier * * @param string $path The path. * @return bool */ public function is_site_root( $path ) { return $this->normalize_dir_path( $path ) === $this->get_site_root(); } /** * Get a clean value of ABSPATH. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @return string The path to WordPress' root folder. */ public function get_abspath() { static $abspath; if ( isset( $abspath ) ) { return $abspath; } $abspath = wp_normalize_path( ABSPATH ); // Make sure ABSPATH is not messed up: it could be defined as a relative path for example (yeah, I know, but we've seen it). $test_file = wp_normalize_path( IMAGIFY_FILE ); $pos = strpos( $test_file, $abspath ); if ( $pos > 0 ) { // ABSPATH has a wrong value. $abspath = substr( $test_file, 0, $pos ) . $abspath; } elseif ( false === $pos && class_exists( 'ReflectionClass' ) ) { // Imagify is symlinked (dude, you look for trouble). $reflector = new ReflectionClass( 'WP' ); $test_file = $reflector->getFileName(); $pos = strpos( $test_file, $abspath ); if ( 0 < $pos ) { // ABSPATH has a wrong value. $abspath = substr( $test_file, 0, $pos ) . $abspath; } } $abspath = trailingslashit( $abspath ); if ( '/' !== substr( $abspath, 0, 1 ) && ':' !== substr( $abspath, 1, 1 ) ) { $abspath = '/' . $abspath; } return $abspath; } /** * Tell if a path is WP's root (ABSPATH). * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $path The path. * @return bool */ public function is_abspath( $path ) { return $this->normalize_dir_path( $path ) === $this->get_abspath(); } /** * Get the upload basedir. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param bool $bypass_error True to return the path even if there is an error. This is used when we want to display this path in a message for example. * @return string|bool The path. False on failure. */ public function get_upload_basedir( $bypass_error = false ) { static $upload_basedir; static $upload_basedir_or_error; if ( isset( $upload_basedir ) ) { return $bypass_error ? $upload_basedir : $upload_basedir_or_error; } $uploads = wp_upload_dir(); $upload_basedir = $this->normalize_dir_path( $uploads['basedir'] ); if ( false !== $uploads['error'] ) { $upload_basedir_or_error = false; } else { $upload_basedir_or_error = $upload_basedir; } return $bypass_error ? $upload_basedir : $upload_basedir_or_error; } /** * Get the upload baseurl. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @return string|bool The URL. False on failure. */ public function get_upload_baseurl() { static $upload_baseurl; if ( isset( $upload_baseurl ) ) { return $upload_baseurl; } $uploads = wp_upload_dir(); if ( false !== $uploads['error'] ) { $upload_baseurl = false; return $upload_baseurl; } $upload_baseurl = trailingslashit( $uploads['baseurl'] ); return $upload_baseurl; } /** * Get the path to the uploads base directory of the main site. * * @since 1.8 * @access public * @author Grégory Viguier * * @return string */ public function get_main_upload_basedir() { static $basedir; if ( isset( $basedir ) ) { return $basedir; } $basedir = get_imagify_upload_basedir( true ); if ( is_multisite() ) { $pattern = '/' . $this->get_multisite_uploads_subdir_pattern() . '$'; $basedir = preg_replace( self::PATTERN_DELIMITER . $pattern . self::PATTERN_DELIMITER, '/', $basedir ); } return $basedir; } /** * Get the URL of the uploads base directory of the main site. * * @since 1.8 * @access public * @author Grégory Viguier * * @return string */ public function get_main_upload_baseurl() { static $baseurl; if ( isset( $baseurl ) ) { return $baseurl; } $baseurl = get_imagify_upload_baseurl( true ); if ( is_multisite() ) { $pattern = '/' . $this->get_multisite_uploads_subdir_pattern() . '$'; $baseurl = preg_replace( self::PATTERN_DELIMITER . $pattern . self::PATTERN_DELIMITER, '/', $baseurl ); } return $baseurl; } /** * Get the regex pattern used to match the uploads subdir on multisite in a file path. * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`. * Paths tested against these patterns are lower-cased. * * @since 1.8 * @access public * @see _wp_upload_dir() * @author Grégory Viguier * * @return string */ public function get_multisite_uploads_subdir_pattern() { static $pattern; if ( isset( $pattern ) ) { return $pattern; } $pattern = ''; if ( ! is_multisite() ) { return $pattern; } if ( ! get_site_option( 'ms_files_rewriting' ) ) { if ( defined( 'MULTISITE' ) ) { $pattern = 'sites/\d+/'; } else { $pattern = '\d+/'; } } elseif ( defined( 'UPLOADS' ) ) { $site_id = (string) get_current_blog_id(); $path = $this->get_upload_basedir( true ); // Something like `/absolute/path/to/wp-content/blogs.dir/3/files/`, also for site 1. $path = strrev( $path ); if ( preg_match( self::PATTERN_DELIMITER . '^.*' . strrev( $site_id ) . '[^/]*/' . self::PATTERN_DELIMITER . 'U', $path, $matches ) ) { $pattern = end( $matches ); $pattern = ltrim( strtolower( strrev( $pattern ) ), '/' ); $pattern = str_replace( $site_id, '\d+', $pattern ); } } /** * Filter the regex pattern used to match the uploads subdir on multisite in a file path. * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`. * Important: lowercase, no heading slash, mandatory trailing slash. * * @since 1.8 * @author Grégory Viguier * * @param string $pattern The regex pattern. */ $pattern = apply_filters( 'imagify_multisite_uploads_subdir_pattern', $pattern ); return $pattern; } } classes/class-imagify-requirements-check.php 0000644 00000022723 15174671745 0015265 0 ustar 00 <?php /** * Class to check if the current WordPress and PHP versions meet our requirements. * * @since 1.9 * @source Based on class WP_Rocket_Requirements_Check from WP Rocket plugin. * @author Grégory Viguier * @author Remy Perona */ class Imagify_Requirements_Check { /** * Plugin Name. * * @var string * @since 1.9 * @access private * @author Grégory Viguier */ private $plugin_name; /** * Plugin filepath. * * @var string * @since 1.9 * @access private * @author Grégory Viguier */ private $plugin_file; /** * Plugin version. * * @var string * @since 1.9 * @access private * @author Grégory Viguier */ private $plugin_version; /** * Last plugin version handling the current version of WP. * * @var string * @since 1.9 * @access private * @author Grégory Viguier */ private $wp_last_version; /** * Last plugin version handling the current version of PHP. * * @var string * @since 1.9 * @access private * @author Grégory Viguier */ private $php_last_version; /** * Required WordPress version. * * @var string * @since 1.9 * @access private * @author Grégory Viguier */ private $wp_version; /** * Required PHP version. * * @var string * @since 1.9 * @access private * @author Grégory Viguier */ private $php_version; /** * Constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $args { * Arguments to populate the class properties. * * @type string $plugin_name Plugin name. * @type string $plugin_file Plugin filepath. * @type string $plugin_version Plugin version. * @type string $wp_last_version Last plugin version handling the current version of WP. * @type string $php_last_version Last plugin version handling the current version of PHP. * @type string $wp_version Required WordPress version. * @type string $php_version Required PHP version. * } */ public function __construct( $args ) { foreach ( [ 'plugin_name', 'plugin_file', 'plugin_version', 'wp_last_version', 'php_last_version', 'wp_version', 'php_version' ] as $setting ) { if ( isset( $args[ $setting ] ) ) { $this->$setting = $args[ $setting ]; } } if ( empty( $this->wp_last_version ) ) { $this->wp_last_version = '1.6.14.2'; } if ( empty( $this->php_last_version ) ) { $this->php_last_version = '1.8.4.1'; } } /** * Check if all requirements are ok, if not, display a notice and the rollback. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function check() { if ( ! $this->php_passes() || ! $this->wp_passes() ) { add_action( 'admin_notices', [ $this, 'print_notice' ] ); add_action( 'admin_post_imagify_rollback', [ $this, 'rollback' ] ); return false; } return true; } /** * Check if the current PHP version is equal or superior to the required PHP version. * * @since 1.9 * @access private * @author Grégory Viguier * * @return bool */ private function php_passes() { return version_compare( PHP_VERSION, $this->php_version ) >= 0; } /** * Check if the current WordPress version is equal or superior to the required PHP version. * * @since 1.9 * @access private * @author Grégory Viguier * * @return bool */ private function wp_passes() { global $wp_version; return version_compare( $wp_version, $this->wp_version ) >= 0; } /** * Get the last version of the plugin that can run with the current WP and PHP versions. * * @since 1.9 * @access private * @author Grégory Viguier * * @return string */ private function get_last_version() { $last_version = ''; if ( ! $this->php_passes() ) { $last_version = $this->php_last_version; } if ( ! $this->wp_passes() ) { $last_version = ! $last_version || version_compare( $last_version, $this->wp_last_version ) > 0 ? $this->wp_last_version : $last_version; } return $last_version; } /** * Tell if the current user can rollback. * * @since 1.9 * @access private * @author Grégory Viguier * * @return bool */ private function current_user_can() { $describer = 'manage'; $capacity = $this->is_active_for_network() ? 'manage_network_options' : 'manage_options'; // This filter is documented in classes/Context/AbstractContext.php. $capacity = (string) apply_filters( 'imagify_capacity', $capacity, $describer, 'wp' ); $user_can = current_user_can( $capacity ); // This filter is documented in classes/Context/AbstractContext.php. $user_can = (bool) apply_filters( 'imagify_current_user_can', $user_can, $capacity, $describer, null, 'wp' ); return $user_can; } /** * Tell if Imagify is activated on the network. * * @since 1.9 * @access private * @author Grégory Viguier * * return bool True if Imagify is activated on the network. */ private function is_active_for_network() { if ( ! is_multisite() ) { return false; } if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return is_plugin_active_for_network( plugin_basename( $this->plugin_file ) ); } /** * Warn if PHP version is less than 5.4 and offers to rollback. * * @since 1.9 */ public function print_notice() { if ( ! $this->current_user_can() ) { return; } $message = []; $required = []; $rollback_url = wp_nonce_url( admin_url( 'admin-post.php?action=imagify_rollback' ), 'imagify_rollback' ); if ( ! $this->php_passes() ) { /* translators: %1$s = Plugin name, %2$s = PHP version required. */ $message[] = sprintf( esc_html__( 'To use this %1$s version, please ask your web host how to upgrade your server to PHP %2$s or higher.', 'imagify' ), $this->plugin_name, $this->php_version ); $required[] = 'PHP ' . $this->php_version; } if ( ! $this->wp_passes() ) { /* translators: %1$s = Plugin name, %2$s = WordPress version required. */ $message[] = sprintf( esc_html__( 'To use this %1$s version, please upgrade WordPress to version %2$s or higher.', 'imagify' ), $this->plugin_name, $this->wp_version ); $required[] = 'WordPress ' . $this->wp_version; } $message = '<p>' . implode( '<br/>', $message ) . "</p>\n"; $required = wp_sprintf_l( '%l', $required ); /* translators: %1$s = Plugin name, %2$s = Plugin version, $3$s is something like "PHP 5.4" or "PHP 5.4 and WordPress 4.0". */ $message = '<p>' . sprintf( esc_html__( 'To function properly, %1$s %2$s requires at least %3$s.', 'imagify' ), '<strong>' . $this->plugin_name . '</strong>', $this->plugin_version, $required ) . "</p>\n" . $message; $message .= '<p>' . esc_html__( 'If you are not able to upgrade, you can rollback to the previous version by using the button below.', 'imagify' ) . "</p>\n"; /* translators: %s = Previous plugin version. */ $message .= '<p class="submit"><a href="' . esc_url( $rollback_url ) . '" class="button">' . sprintf( __( 'Re-install version %s', 'imagify' ), $this->get_last_version() ) . '</a></p>'; echo '<div class="notice notice-error">' . $message . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Do the rollback. * * @since 1.9 */ public function rollback() { check_ajax_referer( 'imagify_rollback' ); if ( ! $this->current_user_can() ) { wp_die(); } $plugin_transient = get_site_transient( 'update_plugins' ); $plugin_basename = plugin_basename( $this->plugin_file ); $plugin_folder = dirname( $plugin_basename ); $last_version = $this->get_last_version(); $package_filename = $plugin_folder . '.' . $last_version . '.zip'; $plugin_transient->checked[ $plugin_basename ] = $last_version; if ( ! empty( $plugin_transient->response[ $plugin_basename ] ) ) { $tmp_obj = $plugin_transient->response[ $plugin_basename ]; } elseif ( ! empty( $plugin_transient->no_update[ $plugin_basename ] ) ) { $tmp_obj = $plugin_transient->no_update[ $plugin_basename ]; } else { $tmp_obj = (object) [ 'id' => 'w.org/plugins/' . $plugin_folder, 'slug' => $plugin_folder, 'plugin' => $plugin_basename, 'new_version' => $last_version, 'url' => 'https://wordpress.org/plugins/' . $plugin_folder . '/', 'package' => 'https://downloads.wordpress.org/plugin/' . $package_filename, 'icons' => [], 'banners' => [], 'banners_rtl' => [], ]; } $tmp_obj->new_version = $last_version; $tmp_obj->package = preg_replace( '@/[^/]+$@', '/' . $package_filename, $tmp_obj->package ); $plugin_transient->response[ $plugin_basename ] = $tmp_obj; unset( $plugin_transient->no_update[ $plugin_basename ] ); set_site_transient( 'update_plugins', $plugin_transient ); require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; /* translators: %s is the plugin name. */ $title = sprintf( __( '%s Update Rollback', 'imagify' ), $this->plugin_name ); $nonce = 'upgrade-plugin_' . $plugin_basename; $url = 'update.php?action=upgrade-plugin&plugin=' . rawurlencode( $plugin_basename ); $upgrader_skin = new Plugin_Upgrader_Skin( compact( 'title', 'nonce', 'url', 'plugin' ) ); $upgrader = new Plugin_Upgrader( $upgrader_skin ); $upgrader->upgrade( $plugin_basename ); wp_die( '', // translators: %s is the plugin name. sprintf( esc_html__( '%s Update Rollback', 'imagify' ), esc_html( $this->plugin_name ) ), [ 'response' => 200 ] ); } } classes/class-imagify-abstract-options.php 0000644 00000033651 15174671745 0014765 0 ustar 00 <?php /** * Abstract class to handle a part of the plugin options. * * @since 1.7 */ abstract class Imagify_Abstract_Options { /** * Class version. * * @var string * @since 1.7 */ const VERSION = '1.0'; /** * Suffix used in the name of the option. * * @var string * @since 1.7 * @access protected */ protected $identifier; /** * The default values for the Imagify main options. * These are the "zero state" values. * Don't use null as value. * * @var array * @since 1.7 * @access protected */ protected $default_values; /** * The Imagify main option values used when they are set the first time or reset. * Values identical to default values are not listed. * * @var array * @since 1.7 * @access protected */ protected $reset_values = []; /** * Tell if the option should be autoloaded by WP. * Possible values are 'yes' and 'no'. * * @var string * @since 1.7 * @access protected */ protected $autoload = 'yes'; /** * Tell if the option should be a network option. * * @var bool * @since 1.7 * @access protected */ protected $network_option = false; /** * Identifier used in the hook names. * * @var string * @since 1.7 * @access private */ private $hook_identifier; /** * The constructor. * * @since 1.7 * @author Grégory Viguier * @access protected */ protected function __construct() { $this->hook_identifier = rtrim( strtolower( str_replace( 'Imagify_', '', get_class( $this ) ) ), 's' ); if ( ! is_string( $this->autoload ) ) { $this->autoload = $this->autoload ? 'yes' : 'no'; } $this->default_values = array_merge( [ 'version' => '', ], $this->default_values ); } /** * Launch the hooks. * * @since 1.7 * @author Grégory Viguier * @access public */ public function init() { add_filter( 'sanitize_option_' . $this->get_option_name(), [ $this, 'sanitize_and_validate_on_update' ], 50 ); } /** ----------------------------------------------------------------------------------------- */ /** GET/SET/DELETE OPTION(S) ================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get an Imagify option. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $key The option name. * @return mixed The option value. */ public function get( $key ) { $default_values = $this->get_default_values(); if ( ! isset( $default_values[ $key ] ) ) { return null; } $default = $default_values[ $key ]; /** * Pre-filter any Imagify option before read. * * @since 1.0 * * @param mixed $value Value to return instead of the option value. Default null to skip it. * @param mixed $default The default value. */ $value = apply_filters( 'pre_get_imagify_' . $this->get_hook_identifier() . '_' . $key, null, $default ); if ( isset( $value ) ) { return $value; } // Get all values. $values = $this->get_all(); // Sanitize and validate the value. $value = $this->sanitize_and_validate( $key, $values[ $key ], $default ); /** * Filter any Imagify option after read. * * @since 1.0 * * @param mixed $value Value of the option. * @param mixed $default The default value. Default false. */ return apply_filters( 'get_imagify_' . $this->get_hook_identifier() . '_' . $key, $value, $default ); } /** * Get all options (no cast, no sanitization, no validation). * * @since 1.7 * @author Grégory Viguier * @access public * * @return array The options. */ public function get_all() { $values = $this->get_raw(); if ( ! $values ) { return $this->get_reset_values(); } return imagify_merge_intersect( $values, $this->get_default_values() ); } /** * Set one or multiple options. * * @since 1.7 * @author Grégory Viguier * @access public * * @param array $values An array of option name / option value pairs. */ public function set( $values ) { $args = func_get_args(); if ( isset( $args[1] ) && is_string( $args[0] ) ) { $values = [ $args[0] => $args[1] ]; } if ( ! is_array( $values ) ) { // PABKAC. return; } $values = array_merge( $this->get_all(), $values ); $values = array_intersect_key( $values, $this->get_default_values() ); $this->set_raw( $values ); } /** * Delete one or multiple options. * * @since 1.7 * @author Grégory Viguier * @access public * * @param array|string $keys An array of option names or a single option name. */ public function delete( $keys ) { $values = $this->get_raw(); if ( ! $values ) { if ( false !== $values ) { $this->delete_raw(); } return; } $keys = array_flip( (array) $keys ); $values = array_diff_key( $values, $keys ); $this->set_raw( $values ); } /** * Checks if the option with the given name exists or not. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $key The option name. * @return bool */ public function has( $key ) { return null !== $this->get( $key ); } /** ----------------------------------------------------------------------------------------- */ /** GET / UPDATE / DELETE RAW VALUES ======================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the name of the option that stores the settings. * * @since 1.7 * @author Grégory Viguier * @access public * * @return string */ public function get_option_name() { return IMAGIFY_SLUG . '_' . $this->identifier; } /** * Get the identifier used in the hook names. * * @since 1.7 * @author Grégory Viguier * @access public * * @return string */ public function get_hook_identifier() { return $this->hook_identifier; } /** * Tell if the option is autoloaded. * * @since 1.7 * @author Grégory Viguier * @access public * * @return bool */ public function is_autoloaded() { return 'yes' === $this->autoload; } /** * Tell if the option is a network option. * * @since 1.7 * @author Grégory Viguier * @access public * * @return bool */ public function is_network_option() { return (bool) $this->network_option; } /** * Get the raw value of all Imagify options. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array|bool The options. False if not set yet. An empty array if invalid. */ public function get_raw() { $values = $this->is_network_option() ? get_site_option( $this->get_option_name() ) : get_option( $this->get_option_name() ); if ( false !== $values && ! is_array( $values ) ) { return []; } return $values; } /** * Update the Imagify options. * * @since 1.7 * @author Grégory Viguier * @access public * * @param array $values An array of option name / option value pairs. */ public function set_raw( $values ) { if ( ! $values ) { // The option is empty: delete it. $this->delete_raw(); } elseif ( $this->is_network_option() ) { // Network option. update_site_option( $this->get_option_name(), $values ); } elseif ( false === get_option( $this->get_option_name() ) ) { // Compat' with WP < 4.2 + autoload: the option doesn't exist in the database. add_option( $this->get_option_name(), $values, '', $this->autoload ); } else { // Update the current value. update_option( $this->get_option_name(), $values, $this->autoload ); } } /** * Delete all Imagify options. * * @since 1.7 * @author Grégory Viguier * @access public */ public function delete_raw() { $this->is_network_option() ? delete_site_option( $this->get_option_name() ) : delete_option( $this->get_option_name() ); } /** ----------------------------------------------------------------------------------------- */ /** DEFAULT + RESET VALUES ================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get default option values. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array */ public function get_default_values() { $default_values = $this->default_values; if ( ! empty( $default_values['cached'] ) ) { unset( $default_values['cached'] ); return $default_values; } /** * Allow to add more default option values. * * @since 1.7 * @author Grégory Viguier * * @param array $new_values New default option values. * @param array $default_values Plugin default option values. */ $new_values = apply_filters( 'imagify_default_' . $this->get_hook_identifier() . '_values', [], $default_values ); $new_values = is_array( $new_values ) ? $new_values : []; if ( $new_values ) { // Don't allow new values to overwrite the plugin values. $new_values = array_diff_key( $new_values, $default_values ); } if ( $new_values ) { $default_values = array_merge( $default_values, $new_values ); $this->default_values = $default_values; } $this->default_values['cached'] = 1; return $default_values; } /** * Get the values used when the option is empty. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array */ public function get_reset_values() { $reset_values = $this->reset_values; if ( ! empty( $reset_values['cached'] ) ) { unset( $reset_values['cached'] ); return $reset_values; } $default_values = $this->get_default_values(); $reset_values = array_merge( $default_values, $reset_values ); /** * Allow to filter the "reset" option values. * * @since 1.7 * @author Grégory Viguier * * @param array $reset_values Plugin reset option values. */ $new_values = apply_filters( 'imagify_reset_' . $this->get_hook_identifier() . '_values', $reset_values ); if ( $new_values && is_array( $new_values ) ) { $reset_values = array_merge( $reset_values, $new_values ); } $this->reset_values = $reset_values; $this->reset_values['cached'] = 1; return $reset_values; } /** ----------------------------------------------------------------------------------------- */ /** SANITIZATION, VALIDATION ================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Sanitize and validate an option value. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $key The option key. * @param mixed $value The value. * @param mixed $default_value The default value. * @return mixed */ public function sanitize_and_validate( $key, $value, $default_value = null ) { if ( ! isset( $default_value ) ) { $default_values = $this->get_default_values(); $default_value = $default_values[ $key ]; } // Cast the value. $value = self::cast( $value, $default_value ); if ( $value === $default_value ) { return $value; } // Version. if ( 'version' === $key ) { return sanitize_text_field( $value ); } return $this->sanitize_and_validate_value( $key, $value, $default_value ); } /** * Sanitize and validate an option value. Basic casts have been made. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $key The option key. * @param mixed $value The value. * @param mixed $default_value The default value. * @return mixed */ abstract public function sanitize_and_validate_value( $key, $value, $default_value ); /** * Sanitize and validate Imagify's options before storing them. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $values The option value. * @return array */ public function sanitize_and_validate_on_update( $values ) { $values = is_array( $values ) ? $values : []; $default_values = $this->get_default_values(); if ( $values ) { foreach ( $default_values as $key => $default ) { if ( isset( $values[ $key ] ) ) { $values[ $key ] = $this->sanitize_and_validate( $key, $values[ $key ], $default ); } } } $values = array_intersect_key( $values, $default_values ); // Version. if ( empty( $values['version'] ) ) { $values['version'] = IMAGIFY_VERSION; } return $this->validate_values_on_update( $values ); } /** * Validate Imagify's options before storing them. Basic sanitization and validation have been made, row by row. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $values The option value. * @return array */ public function validate_values_on_update( $values ) { return $values; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Cast a value, depending on its default value type. * * @since 1.7 * @author Grégory Viguier * @access public * * @param mixed $value The value to cast. * @param mixed $default_value The default value. * @return mixed */ public static function cast( $value, $default_value ) { if ( is_array( $default_value ) ) { return is_array( $value ) ? $value : []; } if ( is_int( $default_value ) ) { return (int) $value; } if ( is_bool( $default_value ) ) { return (bool) $value; } if ( is_float( $default_value ) ) { return round( (float) $value, 3 ); } return $value; } /** * Cast a float like 3.000 into an integer. * * @since 1.7 * @author Grégory Viguier * @access public * * @param float $value The value. * @return float|int */ public static function maybe_cast_float_as_int( $value ) { return ( $value / (int) $value ) === (float) 1 ? (int) $value : $value; } } classes/class-imagify-views.php 0000644 00000046542 15174671745 0012631 0 ustar 00 <?php use Imagify\User\User; use Imagify\Dependencies\WPMedia\PluginFamily\Model\PluginFamily; use Imagify\Traits\InstanceGetterTrait; /** * Class that handles templates and menus. * * @since 1.7 */ class Imagify_Views { use InstanceGetterTrait; /** * Class version. * * @var string * @since 1.7 */ const VERSION = '1.1'; /** * Slug used for the settings page URL. * * @var string * @since 1.7 */ protected $slug_settings; /** * Slug used for the bulk optimization page URL. * * @var string * @since 1.7 */ protected $slug_bulk; /** * Slug used for the "custom folders" page URL. * * @var string * @since 1.7 */ protected $slug_files; /** * A list of JS templates to print at the end of the page. * * @var array * @since 1.9 */ protected $templates_in_footer = []; /** * Stores the "custom folders" files list instance. * * @var Imagify_Files_List_Table * @since 1.7 */ protected $list_table; /** * Filesystem object. * * @var Imagify_Filesystem * @since 1.7.1 */ protected $filesystem; /** * Imagify admin bar menu. * * @var bool */ private $admin_menu_is_present = false; /** ----------------------------------------------------------------------------------------- */ /** INSTANCE/INIT =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * The constructor. * * @since 1.7 */ protected function __construct() { $this->slug_settings = IMAGIFY_SLUG; $this->slug_bulk = IMAGIFY_SLUG . '-bulk-optimization'; $this->slug_files = IMAGIFY_SLUG . '-files'; $this->filesystem = Imagify_Filesystem::get_instance(); } /** * Launch the hooks. * * @since 1.7 */ public function init() { // Menu items. add_action( 'admin_menu', [ $this, 'add_site_menus' ] ); if ( imagify_is_active_for_network() ) { add_action( 'network_admin_menu', [ $this, 'add_network_menus' ] ); } // Save the "per page" option value from the files list screen. add_filter( 'set-screen-option', [ 'Imagify_Files_List_Table', 'save_screen_options' ], 10, 3 ); // JS templates in footer. add_action( 'admin_print_footer_scripts', [ $this, 'print_js_templates' ] ); add_action( 'admin_footer', [ $this, 'print_modal_payment' ] ); add_action( 'wp_before_admin_bar_render', [ $this, 'maybe_print_modal_payment' ] ); } /** ----------------------------------------------------------------------------------------- */ /** MENU ITEMS ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Add sub-menus for all sites. * * @since 1.7 */ public function add_site_menus() { $wp_context = imagify_get_context( 'wp' ); // Sub-menu item: bulk optimization. add_media_page( __( 'Bulk Optimization', 'imagify' ), __( 'Bulk Optimization', 'imagify' ), $wp_context->get_capacity( 'bulk-optimize' ), $this->get_bulk_page_slug(), [ $this, 'display_bulk_page' ] ); if ( imagify_is_active_for_network() ) { return; } /** * Plugin is not network activated. */ if ( imagify_can_optimize_custom_folders() ) { // Sub-menu item: custom folders list. $cf_context = imagify_get_context( 'custom-folders' ); $screen_id = add_media_page( __( 'Other Media optimized by Imagify', 'imagify' ), __( 'Other Media', 'imagify' ), $cf_context->get_capacity( 'optimize' ), $this->get_files_page_slug(), [ $this, 'display_files_list' ] ); if ( $screen_id ) { // Load the data for this page. add_action( 'load-' . $screen_id, [ $this, 'load_files_list' ] ); } } // Sub-menu item: settings. add_options_page( 'Imagify', 'Imagify', $wp_context->get_capacity( 'manage' ), $this->get_settings_page_slug(), [ $this, 'display_settings_page' ] ); } /** * Add menu and sub-menus in the network admin when Imagify is network-activated. * * @since 1.7 */ public function add_network_menus() { global $submenu; $wp_context = imagify_get_context( 'wp' ); if ( ! imagify_can_optimize_custom_folders() ) { // Main item: settings (edge case). add_menu_page( 'Imagify', 'Imagify', $wp_context->get_capacity( 'manage' ), $this->get_settings_page_slug(), [ $this, 'display_settings_page' ] ); return; } $cf_context = imagify_get_context( 'custom-folders' ); // Main item: bulk optimization (custom folders). add_menu_page( __( 'Bulk Optimization', 'imagify' ), 'Imagify', $cf_context->current_user_can( 'bulk-optimize' ), $this->get_bulk_page_slug(), [ $this, 'display_bulk_page' ] ); // Sub-menu item: custom folders list. $screen_id = add_submenu_page( $this->get_bulk_page_slug(), __( 'Other Media optimized by Imagify', 'imagify' ), __( 'Other Media', 'imagify' ), $cf_context->current_user_can( 'bulk-optimize' ), $this->get_files_page_slug(), [ $this, 'display_files_list' ] ); // Sub-menu item: settings. add_submenu_page( $this->get_bulk_page_slug(), 'Imagify', __( 'Settings', 'imagify' ), $wp_context->get_capacity( 'manage' ), $this->get_settings_page_slug(), [ $this, 'display_settings_page' ] ); // Change the sub-menu label. if ( ! empty( $submenu[ $this->get_bulk_page_slug() ] ) ) { $submenu[ $this->get_bulk_page_slug() ][0][0] = __( 'Bulk Optimization', 'imagify' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } if ( $screen_id ) { // On the "Other Media optimized by Imagify" page, load the data. add_action( 'load-' . $screen_id, [ $this, 'load_files_list' ] ); } } /** ----------------------------------------------------------------------------------------- */ /** MAIN PAGE TEMPLATES ===================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * The main settings page. * * @since 1.7 */ public function display_settings_page() { $plugin_family = new PluginFamily(); $plugins_array = $plugin_family->get_filtered_plugins( 'imagify/imagify' ); $data = [ 'hide_plugin_family' => wpm_apply_filters_typed( 'boolean', 'imagify_hide_plugin_family', false ), 'plugin_family' => $plugins_array['uncategorized'], ]; $this->print_template( 'page-settings', $data ); } /** * The bulk optimization page. * * @since 1.7 */ public function display_bulk_page() { $types = []; $data = [ // Limits. 'unoptimized_attachment_limit' => 0, // What to optimize. 'icon' => 'images-alt2', 'title' => __( 'Optimize your media files', 'imagify' ), 'groups' => [], ]; if ( imagify_is_screen( 'bulk' ) ) { if ( ! is_network_admin() ) { /** * Library: in each site. */ $types['library|wp'] = 1; } if ( imagify_can_optimize_custom_folders() && ( ( imagify_is_active_for_network() && is_network_admin() ) || ! imagify_is_active_for_network() ) ) { /** * Custom folders: in network admin only if network activated, in each site otherwise. */ $types['custom-folders|custom-folders'] = 1; } } /** * Filter the types to display in the bulk optimization page. * * @since 1.7.1 * @author Grégory Viguier * * @param array $types The folder types displayed on the page. If a folder type is "library", the context should be suffixed after a pipe character. They are passed as array keys. */ $types = apply_filters( 'imagify_bulk_page_types', $types ); $types = array_filter( (array) $types ); if ( isset( $types['library|wp'] ) ) { // Limits. $data['unoptimized_attachment_limit'] += imagify_get_unoptimized_attachment_limit(); // Group. $data['groups']['library'] = [ /** * The group_id corresponds to the file names like 'part-bulk-optimization-results-row-{$group_id}'. * It is also used in get_imagify_localize_script_translations(). */ 'group_id' => 'library', 'context' => 'wp', 'title' => __( 'Media Library', 'imagify' ), /* translators: 1 is the opening of a link, 2 is the closing of this link. */ 'footer' => sprintf( __( 'You can also re-optimize your media files from your %1$sMedia Library%2$s screen.', 'imagify' ), '<a href="' . esc_url( admin_url( 'upload.php' ) ) . '">', '</a>' ), ]; } if ( isset( $types['custom-folders|custom-folders'] ) ) { if ( ! Imagify_Folders_DB::get_instance()->has_items() ) { $data['no-custom-folders'] = true; } elseif ( Imagify_Folders_DB::get_instance()->has_active_folders() ) { // Group. $data['groups']['custom-folders'] = [ 'group_id' => 'custom-folders', 'context' => 'custom-folders', 'title' => __( 'Custom folders', 'imagify' ), /* translators: 1 is the opening of a link, 2 is the closing of this link. */ 'footer' => sprintf( __( 'You can re-optimize your media files more finely directly in the %1$smedia management%2$s.', 'imagify' ), '<a href="' . esc_url( get_imagify_admin_url( 'files-list' ) ) . '">', '</a>' ), ]; } } // Add generic stats. $data = array_merge( $data, imagify_get_bulk_stats( $types, [ 'fullset' => true, ] ) ); /** * Filter the data to use on the bulk optimization page. * * @since 1.7 * @since 1.7.1 Added the $types parameter. * @author Grégory Viguier * * @param array $data The data to use. * @param array $types The folder types displayed on the page. They are passed as array keys. */ $data = apply_filters( 'imagify_bulk_page_data', $data, $types ); $this->print_template( 'page-bulk', $data ); } /** * The page displaying the "custom folders" files. * * @since 1.7 */ public function display_files_list() { $this->print_template( 'page-files-list' ); } /** * Initiate the "custom folders" list table data. * * @since 1.7 */ public function load_files_list() { // Instantiate the list. $this->list_table = new Imagify_Files_List_Table( [ 'screen' => 'imagify-files', ] ); // Query the Items. $this->list_table->prepare_items(); } /** ----------------------------------------------------------------------------------------- */ /** GETTERS ================================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the settings page slug. * * @since 1.7 * * @return string */ public function get_settings_page_slug() { return $this->slug_settings; } /** * Get the bulk optimization page slug. * * @since 1.7 * * @return string */ public function get_bulk_page_slug() { return $this->slug_bulk; } /** * Get the "custom folders" files page slug. * * @since 1.7 * * @return string */ public function get_files_page_slug() { return $this->slug_files; } /** ----------------------------------------------------------------------------------------- */ /** PAGE TESTS ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if we’re displaying the settings page. * * @since 1.9 * * @return bool */ public function is_settings_page() { global $pagenow; if ( ! isset( $_GET['page'] ) ) { return false; } $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended if ( $this->get_settings_page_slug() !== $page ) { return false; } if ( imagify_is_active_for_network() ) { return 'admin.php' === $pagenow; } return 'options-general.php' === $pagenow; } /** * Tell if we’re displaying the bulk optimization page. * * @since 1.9 * * @return bool */ public function is_bulk_page() { global $pagenow; if ( ! isset( $_GET['page'] ) ) { return false; } $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended return 'upload.php' === $pagenow && $this->get_bulk_page_slug() === $page; } /** * Tell if we’re displaying the custom files list page. * * @since 1.9 * * @return bool */ public function is_files_page() { global $pagenow; if ( ! isset( $_GET['page'] ) ) { return false; } $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended return 'upload.php' === $pagenow && $this->get_files_page_slug() === $page; } /** * Tell if we’re displaying the WP media library page. * * @since 1.9 * * @return bool */ public function is_wp_library_page() { global $pagenow; return 'upload.php' === $pagenow && ! isset( $_GET['page'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } /** * Tell if we’re displaying a media page. * * @since 1.9 * * @return bool */ public function is_media_page() { global $pagenow, $typenow; return 'post.php' === $pagenow && 'attachment' === $typenow; } /** ----------------------------------------------------------------------------------------- */ /** QUOTA =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the remaining quota in percent. * * @since 1.8.1 * * @return int */ public function get_quota_percent() { static $quota; if ( isset( $quota ) ) { return $quota; } $user = new User(); $quota = $user->get_percent_unconsumed_quota(); return $quota; } /** * Get the HTML class used for the quota (to change the color when out of quota for example). * * @since 1.8.1 * * @return string */ public function get_quota_class() { static $class; if ( isset( $class ) ) { return $class; } $quota = $this->get_quota_percent(); $class = 'imagify-bar-'; if ( $quota <= 20 ) { $class .= 'negative'; } elseif ( $quota <= 50 ) { $class .= 'neutral'; } else { $class .= 'positive'; } return $class; } /** * Get the HTML tag used for the quota (the weather-like icon). * * @since 1.8.1 * * @return string */ public function get_quota_icon() { static $icon; if ( isset( $icon ) ) { return $icon; } $quota = $this->get_quota_percent(); if ( $quota <= 20 ) { $icon = '<img src="' . IMAGIFY_ASSETS_IMG_URL . 'stormy.svg" width="40" height="63" alt="" />'; } elseif ( $quota <= 50 ) { $icon = '<img src="' . IMAGIFY_ASSETS_IMG_URL . 'cloudy-sun.svg" width="63" height="64" alt="" />'; } else { $icon = '<img src="' . IMAGIFY_ASSETS_IMG_URL . 'sun.svg" width="63" height="64" alt="" />'; } return $icon; } /** ----------------------------------------------------------------------------------------- */ /** GENERIC TEMPLATE TOOLS ================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get a template contents. * * @since 1.7 * * @param string $template The template name. * @param mixed $data Some data to pass to the template. * @return string|bool The page contents. False if the template doesn't exist. */ public function get_template( $template, $data = [] ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $path = str_replace( '_', '-', $template ); $path = IMAGIFY_PATH . 'views/' . $template . '.php'; if ( ! $this->filesystem->exists( $path ) ) { return false; } ob_start(); include $path; $contents = ob_get_clean(); return trim( (string) $contents ); } /** * Print a template. * * @since 1.7 * * @param string $template The template name. * @param mixed $data Some data to pass to the template. */ public function print_template( $template, $data = [] ) { echo $this->get_template( $template, $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Add a template to the list of JS templates to print at the end of the page. * * @since 1.7 * * @param string $template The template name. */ public function print_js_template_in_footer( $template ) { if ( isset( $this->templates_in_footer[ $template ] ) ) { return; } if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { return; } switch ( $template ) { case 'button/processing': $data = [ 'label' => '{{ data.label }}' ]; break; default: $data = []; } $this->templates_in_footer[ $template ] = $data; } /** * Print the JS templates that have been added to the "queue". * * @since 1.9 */ public function print_js_templates() { if ( ! $this->templates_in_footer ) { return; } foreach ( $this->templates_in_footer as $template => $data ) { $template_id = str_replace( [ '/', '_' ], '-', $template ); echo '<script type="text/html" id="tmpl-imagify-' . esc_attr( $template_id ) . '">'; $this->print_template( $template, $data ); echo '</script>'; } } /** * Get imagify user info * * @return bool */ private function get_user_info(): bool { $user = new User(); $unconsumed_quota = $user->get_percent_unconsumed_quota(); return ( ! $user->is_infinite() && $unconsumed_quota <= 20 ) || ( $user->is_free() && $unconsumed_quota > 20 ); } /** * Start print the payment modal process. */ public function maybe_print_modal_payment() { if ( $this->get_user_info() ) { global $wp_admin_bar; $this->admin_menu_is_present = $wp_admin_bar && $wp_admin_bar->get_node( 'imagify' ); return; } $this->admin_menu_is_present = false; } /** * Print the payment modal. * * @return void */ public function print_modal_payment() { if ( is_admin_bar_showing() && $this->admin_menu_is_present ) { $this->print_template( 'modal-payment', [ 'attachments_number' => $this->get_attachments_number_modal(), ] ); } } /** * Get the number of attachments to display in the payment modal. * * @return int */ private function get_attachments_number_modal() { $transient = get_transient( 'imagify_attachments_number_modal' ); if ( false !== $transient ) { return $transient; } $attachments_number = imagify_count_attachments() + Imagify_Files_Stats::count_all_files(); set_transient( 'imagify_attachments_number_modal', $attachments_number, 1 * DAY_IN_SECONDS ); return $attachments_number; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Create HTML attributes from an array. * * @since 1.9 * * @param array $attributes A list of attribute pairs. * @return string HTML attributes. */ public function build_attributes( $attributes ) { if ( ! $attributes || ! is_array( $attributes ) ) { return ''; } $out = ''; foreach ( $attributes as $attribute => $value ) { if ( '' === $value ) { continue; } $out .= ' ' . $attribute . '="' . esc_attr( $value ) . '"'; } return $out; } } classes/class-imagify-abstract-background-process.php 0000644 00000007644 15174671745 0017070 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * Class handling background processes. * * @since 1.8.1 */ abstract class Imagify_Abstract_Background_Process extends Imagify_WP_Background_Process { use InstanceGetterTrait; /** * Prefix used to build the global process identifier. * * @var string * @since 1.8.1 */ protected $prefix = 'imagify'; /** * URL to query on. * * @var string */ protected $query_url = ''; /** * Set to true to automatically displatch at the end of the page. * * @var bool * @since 1.9 * @see $this->save() * @see $this->maybe_save_and_dispatch() */ protected $auto_dispatch = false; /** * Init: launch a hook that will clear the scheduled events and empty the queue when the plugin is disabled. * This is only a precaution in case something went wrong. * * @since 1.8.1 */ public function init() { $this->query_url = admin_url( 'admin-ajax.php' ); /** * Filter the URL to use for background processes. * * @since 1.9.5 * * @param string $query_url An URL. * @param object $this This class instance. */ $this->query_url = apply_filters( 'imagify_background_process_url', $this->query_url, $this ); if ( ! $this->query_url || ! is_string( $this->query_url ) || ! preg_match( '@^https?://@', $this->query_url ) ) { $this->query_url = admin_url( 'admin-ajax.php' ); } // Deactivation hook. if ( did_action( static::get_deactivation_hook_name() ) ) { $this->cancel_process(); } else { add_action( static::get_deactivation_hook_name(), [ $this, 'cancel_process' ] ); } // Automatically save and dispatch at the end of the page if the queue is not empty. add_action( 'shutdown', [ $this, 'maybe_save_and_dispatch' ], 666 ); // Evil magic number. } /** ----------------------------------------------------------------------------------------- */ /** OVERRIDES =============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Cancel Process. * Stop processing queue items, clear cronjob and delete batch. * This is a copy of the parent's method, in case an older version of WP_Background_Process is loaded instead of this one (an old version without this method). * * @since 1.8.1 */ public function cancel_process() { if ( method_exists( $this, 'cancel_process' ) ) { parent::cancel_process(); return; } if ( ! $this->is_queue_empty() ) { $batch = $this->get_batch(); $this->delete( $batch->key ); wp_clear_scheduled_hook( $this->get_event_name() ); } } /** * Save the queen. No, I meant the queue. * Also empty the queue to avoid to create several batches with the same items. * * @since 1.9 * * @return $this */ public function save() { if ( empty( $this->data ) ) { return $this; } parent::save(); $this->auto_dispatch = true; $this->data = []; return $this; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Save and dispatch if the queue is not empty. * * @since 1.9 */ public function maybe_save_and_dispatch() { $this->save(); if ( $this->auto_dispatch ) { $this->dispatch(); } } /** * Get the cron name. * * @since 1.8.1 * * @return string */ public function get_event_name() { return $this->cron_hook_identifier; } /** * Get the deactivation hook name. * * @since 1.8.1 * * @return string */ public static function get_deactivation_hook_name() { static $deactivation_hook; if ( ! isset( $deactivation_hook ) ) { $deactivation_hook = 'deactivate_' . plugin_basename( IMAGIFY_FILE ); } return $deactivation_hook; } } classes/class-imagify-files-recursive-iterator.php 0000644 00000004750 15174671745 0016425 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class allowing to filter RecursiveDirectoryIterator, to return only files that Imagify can optimize. * It also allows to remove forbidden folders. * * @since 1.7 * @author Grégory Viguier */ class Imagify_Files_Recursive_Iterator extends RecursiveFilterIterator { /** * Class version. * * @var string * @since 1.7 * @author Grégory Viguier */ const VERSION = '1.0.2'; /** * Filesystem object. * * @var object Imagify_Filesystem * @since 1.7.1 * @access protected * @author Grégory Viguier */ protected $filesystem; /** * Check whether the current element of the iterator is acceptable. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param object $iterator The iterator that is being filtered. */ public function __construct( $iterator ) { parent::__construct( $iterator ); $this->filesystem = Imagify_Filesystem::get_instance(); } /** * Check whether the current element of the iterator is acceptable. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool Returns whether the current element of the iterator is acceptable through this filter. */ public function accept(): bool { static $extensions, $has_extension_method; $file_path = $this->current()->getPathname(); // Prevent triggering an open_basedir restriction error. $file_name = $this->filesystem->file_name( $file_path ); if ( '.' === $file_name || '..' === $file_name ) { return false; } if ( $this->current()->isDir() ) { $file_path = trailingslashit( $file_path ); } if ( Imagify_Files_Scan::is_path_forbidden( $file_path ) ) { return false; } // OK for folders. if ( $this->hasChildren() ) { return true; } // Only files. if ( ! $this->current()->isFile() ) { return false; } // Only files with the required extension. if ( ! isset( $extensions ) ) { $extensions = array_keys( imagify_get_mime_types() ); $extensions = implode( '|', $extensions ); } if ( ! isset( $has_extension_method ) ) { // This method was introduced in php 5.3.6. $has_extension_method = method_exists( $this->current(), 'getExtension' ); } if ( $has_extension_method ) { $file_extension = strtolower( $this->current()->getExtension() ); } else { $file_extension = strtolower( $this->filesystem->path_info( $file_path, 'extension' ) ); } return preg_match( '@^' . $extensions . '$@', $file_extension ); } } classes/class-imagify-assets.php 0000644 00000053170 15174671745 0012771 0 ustar 00 <?php use Imagify\Notices\Notices; use Imagify\Traits\InstanceGetterTrait; /** * Class that handles stylesheets and JavaScripts. * * @since 1.6.10 */ class Imagify_Assets extends Imagify_Assets_Deprecated { use InstanceGetterTrait; /** * Class version. * * @var string */ const VERSION = '1.0.4'; /** * Prefix used for stylesheet handles. * * @var string */ const CSS_PREFIX = 'imagify-'; /** * Prefix used for script handles. * * @var string */ const JS_PREFIX = 'imagify-'; /** * An array containing our registered styles. * * @var array */ protected $styles = []; /** * An array containing our registered scripts. * * @var array */ protected $scripts = []; /** * Current handle. * * @var string */ protected $current_handle; /** * Current handle type. * * @var string 'css' or 'js'. */ protected $current_handle_type; /** * Array of scripts that should be localized when they are enqueued. * * @var array */ protected $deferred_localizations = []; /** * A "random" script version to use when debug is on. * * @var int */ protected static $version; /** * The constructor. * * @return void */ protected function __construct() { if ( ! isset( self::$version ) ) { self::$version = time(); } } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC METHODS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Launch the hooks. * * @since 1.6.10 */ public function init() { if ( ! is_admin() ) { add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles_and_scripts_frontend' ] ); return; } add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles_and_scripts' ], IMAGIFY_INT_MAX ); add_action( 'wp_enqueue_media', [ $this, 'enqueue_media_modal' ] ); } /** * Enqueue stylesheets and scripts for the frontend. * * @since 1.6.10 */ public function enqueue_styles_and_scripts_frontend() { if ( ! $this->is_admin_bar_item_showing() ) { return; } $this->register_style( 'admin-bar' ); $this->register_script( 'admin-bar', 'admin-bar', [ 'jquery' ] ); $this->enqueue_assets( 'admin-bar' )->localize( 'imagifyAdminBar' ); } /** * Register stylesheets and scripts for the administration area. * * @since 1.6.10 */ public function register_styles_and_scripts() { static $done = false; if ( $done ) { return; } $done = true; /** * 3rd Party Styles. */ $this->register_style( 'sweetalert-core', 'sweetalert2', [], '4.6.6' ); /** * Imagify Styles. */ $this->register_style( 'sweetalert', 'sweetalert-custom', [ 'sweetalert-core' ] ); $this->register_style( 'admin-bar' ); $this->register_style( 'admin' ); $this->register_style( 'notices', 'notices', [ 'admin' ] ); // Needs SweetAlert on some cases. $this->register_style( 'twentytwenty', 'twentytwenty', [ 'admin' ] ); $this->register_style( 'pricing-modal', 'pricing-modal', [ 'admin' ] ); $this->register_style( 'bulk', 'bulk', [ 'sweetalert', 'admin' ] ); $this->register_style( 'options', 'options', [ 'sweetalert', 'admin' ] ); $this->register_style( 'files-list', 'files-list', [ 'admin' ] ); /** * 3rd Party Scripts. */ $this->register_script( 'promise-polyfill', 'es6-promise.auto', [], '4.1.1' ); $this->register_script( 'sweetalert', 'sweetalert2', [ 'promise-polyfill' ], '4.6.6' )->localize( 'imagifySwal' ); $this->register_bud_script( 'runtime', 'runtime' ); $this->register_bud_script( 'chart', 'chart', [ 'runtime' ], '4.4.0' ); $this->register_script( 'event-move', 'jquery.event.move', [ 'jquery' ], '2.0.1' ); /** * Imagify Scripts. */ $this->register_script( 'admin-bar', 'admin-bar', [ 'jquery' ] )->defer_localization( 'imagifyAdminBar' ); $this->register_script( 'admin', 'admin', [ 'jquery' ] ); $this->register_script( 'notices', 'notices', [ 'jquery', 'admin' ] )->defer_localization( 'imagifyNotices' ); // Needs SweetAlert on some cases. $this->register_script( 'twentytwenty', 'jquery.twentytwenty', [ 'jquery', 'event-move', 'chart', 'admin' ] )->defer_localization( 'imagifyTTT' ); $this->register_script( 'beat', 'beat', [ 'jquery' ] )->localize( 'imagifybeatSettings' ); $this->register_script( 'media-modal', 'media-modal', [ 'jquery', 'beat', 'underscore', 'chart', 'admin' ] )->localize( 'imagifyModal' ); $this->register_script( 'pricing-modal', 'pricing-modal', [ 'jquery', 'admin' ] )->defer_localization( 'imagifyPricingModal' ); $this->register_script( 'library', 'library', [ 'jquery', 'media-modal' ] )->defer_localization( 'imagifyLibrary' ); $this->register_script( 'async', 'imagify-gulp' ); $this->register_bud_script( 'bulk', 'bulk', [ 'jquery', 'beat', 'underscore', 'chart', 'sweetalert', 'async', 'admin' ] )->defer_localization( 'imagifyBulk' ); $this->register_script( 'options', 'options', [ 'jquery', 'beat', 'sweetalert', 'underscore', 'admin' ] )->defer_localization( 'imagifyOptions' ); $this->register_script( 'files-list', 'files-list', [ 'jquery', 'beat', 'underscore', 'chart', 'admin' ] )->defer_localization( 'imagifyFiles' ); } /** * Enqueue stylesheets and scripts for the administration area. * * @since 1.6.10 */ public function enqueue_styles_and_scripts() { static $done = false; if ( $done ) { return; } $done = true; /* * Register stylesheets and scripts. */ $this->register_styles_and_scripts(); /** * Admin bar. */ if ( $this->is_admin_bar_item_showing() ) { $this->enqueue_assets( 'admin-bar' ); } /** * Notices. */ $notices = Notices::get_instance(); if ( $notices->has_notices() ) { if ( $notices->display_welcome_steps() || $notices->display_wrong_api_key() ) { // This is where we display things about the API key. $this->enqueue_assets( 'sweetalert' ); } $this->enqueue_assets( 'notices' ); } /** * Loaded in the library and attachment edition. */ if ( imagify_is_screen( 'library' ) || imagify_is_screen( 'attachment' ) ) { $this->enqueue_assets( 'twentytwenty' ); } /** * Loaded in the library. */ if ( imagify_is_screen( 'library' ) ) { $this->enqueue_style( 'admin' )->enqueue_script( 'library' ); } /** * Loaded in the bulk optimization page. */ if ( imagify_is_screen( 'bulk' ) ) { $this->enqueue_assets( 'bulk' ); } /* * Loaded in the settings page. */ if ( imagify_is_screen( 'imagify-settings' ) ) { $this->enqueue_assets( [ 'sweetalert', 'notices', 'twentytwenty', 'options' ] ); } /* * Loaded in the files list page. */ if ( imagify_is_screen( 'files-list' ) ) { $this->enqueue_assets( [ 'files-list', 'twentytwenty' ] ); } $this->enqueue_assets( 'pricing-modal' ); /** * Triggered after Imagify CSS and JS have been enqueued. * * @since 1.6.10 */ do_action( 'imagify_assets_enqueued' ); } /** * Enqueue stylesheets and scripts for the media modal. * * @since 1.6.10 */ public function enqueue_media_modal() { static $done = false; if ( $done ) { return; } $done = true; /* * Register stylesheets and scripts. */ $this->register_styles_and_scripts(); $this->enqueue_style( 'admin' )->enqueue_script( 'media-modal' ); // When the optimization buttons are displayed in the media modal, they are fetched through ajax, so they can’t print the "processing" button template in the footer. Imagify_Views::get_instance()->print_js_template_in_footer( 'button/processing' ); /** * Triggered after Imagify CSS and JS have been enqueued for the media modal. * * @since 1.6.10 */ do_action( 'imagify_media_modal_assets_enqueued' ); } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC TOOLS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Register a style. * * @since 1.6.10 * * @param string $handle Name of the stylesheet. Should be unique. * @param string|null $file_name The file name, without the extension. If null, $handle is used. * @param array $dependencies An array of registered stylesheet handles this stylesheet depends on. * @param string|null $version String specifying stylesheet version number. If set to null, the plugin version is used. If SCRIPT_DEBUG is true, a random string is used. * @return object This class instance. */ public function register_style( $handle, $file_name = null, $dependencies = [], $version = null ) { // If we register it, it's one of our styles. $this->styles[ $handle ] = 1; $this->current_handle = $handle; $this->current_handle_type = 'css'; $file_name = $file_name ? $file_name : $handle; $version = $version ? $version : IMAGIFY_VERSION; $version = $this->is_debug() ? self::$version : $version; $extension = $this->is_debug() ? '.css' : '.min.css'; $handle = self::CSS_PREFIX . $handle; $dependencies = $this->prefix_dependencies( $dependencies, 'css' ); wp_register_style( $handle, IMAGIFY_URL . 'assets/css/' . $file_name . $extension, $dependencies, $version ); return $this; } /** * Enqueue a style. * * @since 1.6.10 * * @param string|array $handles Name of the stylesheet. Should be unique. Can be an array to enqueue several stylesheets. * @return object This class instance. */ public function enqueue_style( $handles ) { $handles = (array) $handles; foreach ( $handles as $handle ) { $this->current_handle = $handle; $this->current_handle_type = 'css'; if ( ! empty( $this->styles[ $handle ] ) ) { // If we registered it, it's one of our styles. $handle = self::CSS_PREFIX . $handle; } wp_enqueue_style( $handle ); } return $this; } /** * Dequeue a style. * * @since 1.6.10 * * @param string|array $handles Name of the stylesheet. Should be unique. Can be an array to dequeue several stylesheets. * @return object This class instance. */ public function dequeue_style( $handles ) { $handles = (array) $handles; foreach ( $handles as $handle ) { $this->current_handle = $handle; $this->current_handle_type = 'css'; if ( ! empty( $this->styles[ $handle ] ) ) { // If we registered it, it's one of our styles. $handle = self::CSS_PREFIX . $handle; } wp_dequeue_style( $handle ); } return $this; } /** * Register a script. * * @since 1.6.10 * * @param string $handle Name of the script. Should be unique. * @param string|null $file_name The file name, without the extension. If null, $handle is used. * @param array $dependencies An array of registered script handles this script depends on. * @param string|null $version String specifying script version number. If set to null, the plugin version is used. If SCRIPT_DEBUG is true, a random string is used. * @return object This class instance. */ public function register_script( $handle, $file_name = null, $dependencies = [], $version = null ) { // If we register it, it's one of our scripts. $this->scripts[ $handle ] = 1; // Set the current handler and handler type. $this->current_handle = $handle; $this->current_handle_type = 'js'; $file_name = $file_name ? $file_name : $handle; $version = $version ? $version : IMAGIFY_VERSION; $version = $this->is_debug() ? self::$version : $version; $extension = $this->is_debug() ? '.js' : '.min.js'; $handle = self::JS_PREFIX . $handle; $dependencies = $this->prefix_dependencies( $dependencies ); wp_register_script( $handle, IMAGIFY_URL . 'assets/js/' . $file_name . $extension, $dependencies, $version, true ); return $this; } /** * Register a script. * * @since 1.6.10 * * @param string $handle Name of the script. Should be unique. * @param string|null $file_name The file name, without the extension. If null, $handle is used. * @param array $dependencies An array of registered script handles this script depends on. * @param string|null $version String specifying script version number. If set to null, the plugin version is used. If SCRIPT_DEBUG is true, a random string is used. * @return object This class instance. */ public function register_bud_script( $handle, $file_name = null, $dependencies = [], $version = null ) { // If we register it, it's one of our scripts. $this->scripts[ $handle ] = 1; // Set the current handler and handler type. $this->current_handle = $handle; $this->current_handle_type = 'js'; $file_name = $file_name ? $file_name : $handle; $version = $version ? $version : IMAGIFY_VERSION; $version = $this->is_debug() ? self::$version : $version; $extension = '.js'; $handle = self::JS_PREFIX . $handle; $dependencies = $this->prefix_dependencies( $dependencies ); wp_register_script( $handle, IMAGIFY_URL . 'assets/admin/js/' . $file_name . $extension, $dependencies, $version, true ); return $this; } /** * Enqueue a script. * * @since 1.6.10 * * @param string|array $handles Name of the script. Should be unique. Can be an array to enqueue several scripts. * @return object This class instance. */ public function enqueue_script( $handles ) { $handles = (array) $handles; foreach ( $handles as $handle ) { // Enqueue the corresponding style. if ( ! empty( $this->styles[ $handle ] ) ) { $this->enqueue_style( $handle ); } $this->current_handle = $handle; $this->current_handle_type = 'js'; if ( ! empty( $this->scripts[ $handle ] ) ) { // If we registered it, it's one of our scripts. $handle = self::JS_PREFIX . $handle; } wp_enqueue_script( $handle ); // Deferred localization. if ( ! empty( $this->deferred_localizations[ $this->current_handle ] ) ) { array_map( [ $this, 'localize' ], $this->deferred_localizations[ $this->current_handle ] ); unset( $this->deferred_localizations[ $this->current_handle ] ); } } return $this; } /** * Dequeue a script. * * @since 1.6.10 * * @param string|array $handles Name of the script. Should be unique. Can be an array to dequeue several scripts. * @return object This class instance. */ public function dequeue_script( $handles ) { $handles = (array) $handles; foreach ( $handles as $handle ) { // Enqueue the corresponding style. if ( ! empty( $this->styles[ $handle ] ) ) { $this->dequeue_style( $handle ); } $this->current_handle = $handle; $this->current_handle_type = 'js'; if ( ! empty( $this->scripts[ $handle ] ) ) { // If we registered it, it's one of our scripts. $handle = self::JS_PREFIX . $handle; } wp_dequeue_script( $handle ); } return $this; } /** * Localize a script. * * @since 1.6.10 * * @param string $handle Name of the script. Should be unique. * @param string $object_name Name for the JavaScript object. Passed directly, so it should be qualified JS variable. Example: '/[a-zA-Z0-9_]+/'. * @param string|array|null $l10n The data itself. The data can be either a single or multi-dimensional array. If null, $handle is used. * @return object This class instance. */ public function localize_script( $handle, $object_name, $l10n = null ) { $this->current_handle = $handle; $this->current_handle_type = 'js'; if ( ! isset( $l10n ) ) { $l10n = $handle; } if ( is_string( $l10n ) ) { $l10n = $this->get_localization_data( $l10n ); } if ( ! $l10n ) { return $this; } if ( ! empty( $this->scripts[ $handle ] ) ) { // If we registered it, it's one of our scripts. $handle = self::JS_PREFIX . $handle; } wp_localize_script( $handle, $object_name, $l10n ); return $this; } /** * Enqueue a style and a script that have the same handle. * * @since 1.6.10 * * @param string|array $handles Name of the script. Should be unique. Can be an array to enqueue several scripts. * @return object This class instance. */ public function enqueue_assets( $handles ) { $handles = (array) $handles; foreach ( $handles as $handle ) { $this->enqueue_script( $handle ); } return $this; } /** * Dequeue a style and a script that have the same handle. * * @since 1.6.10 * * @param string|array $handles Name of the script. Should be unique. Can be an array to dequeue several scripts. * @return object This class instance. */ public function dequeue_assets( $handles ) { $handles = (array) $handles; foreach ( $handles as $handle ) { $this->dequeue_style( $handle ); $this->dequeue_script( $handle ); } return $this; } /** * Enqueue the current script or style. * * @since 1.6.10 * * @return object This class instance. */ public function enqueue() { if ( 'js' === $this->current_handle_type ) { $this->enqueue_script( $this->current_handle ); } elseif ( 'css' === $this->current_handle_type ) { $this->enqueue_style( $this->current_handle ); } return $this; } /** * Localize the current script. * * @since 1.6.10 * * @param string $object_name Name for the JavaScript object. Passed directly, so it should be qualified JS variable. Example: '/[a-zA-Z0-9_]+/'. * @param string|array|null $l10n The data itself. The data can be either a single or multi-dimensional array. If null, $handle is used. * @return object This class instance. */ public function localize( $object_name, $l10n = null ) { return $this->localize_script( $this->current_handle, $object_name, $l10n ); } /** * Localize the current script when it is enqueued with `$this->enqueue()` or `$this->enqueue_script()`. This should be used right after `$this->register_script()`. * Be careful, it won't work if the script is enqueued because it's a dependency. * This is handy to not forget to localize the script later. It also prevents to localize the script right away, and maybe execute all localizations while the script is not enqueued (so we localize for nothing). * * @since 1.6.10 * * @param string $object_name Name for the JavaScript object. Passed directly, so it should be qualified JS variable. Example: '/[a-zA-Z0-9_]+/'. * @return object This class instance. */ public function defer_localization( $object_name ) { if ( ! isset( $this->deferred_localizations[ $this->current_handle ] ) ) { $this->deferred_localizations[ $this->current_handle ] = []; } $this->deferred_localizations[ $this->current_handle ][ $object_name ] = $object_name; return $this; } /** * Remove a deferred localization. * * @since 1.6.10 * * @param string $handle Name of the script. Should be unique. * @param string $object_name Name for the JavaScript object. Passed directly, so it should be qualified JS variable. Example: '/[a-zA-Z0-9_]+/'. * @return object This class instance. */ public function remove_deferred_localization( $handle, $object_name = null ) { if ( empty( $this->deferred_localizations[ $handle ] ) ) { return $this; } if ( $object_name ) { unset( $this->deferred_localizations[ $handle ][ $object_name ] ); } else { unset( $this->deferred_localizations[ $handle ] ); } return $this; } /** * Get all translations we can use with wp_localize_script(). * * @since 1.6.10 * * @param string $context The translation context. * @param array $more_data More data to merge. * @return array $translations The translations. */ public function get_localization_data( $context, $more_data = [] ) { $data = get_imagify_localize_script_translations( $context ); if ( $more_data ) { return array_merge( $data, $more_data ); } return $data; } /** ----------------------------------------------------------------------------------------- */ /** INTERNAL TOOLS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Prefix the dependencies if they are ours. * * @since 1.6.10 * * @param array $dependencies An array of registered script handles this script depends on. * @param string $type Type of dependency: css or js. * @return array */ protected function prefix_dependencies( $dependencies, $type = 'js' ) { if ( ! $dependencies ) { return []; } if ( 'js' === $type ) { $prefix = self::JS_PREFIX; $scripts = $this->scripts; } else { $prefix = self::CSS_PREFIX; $scripts = $this->styles; } $depts = []; foreach ( $dependencies as $dept ) { if ( ! empty( $scripts[ $dept ] ) ) { $depts[] = $prefix . $dept; } else { $depts[] = $dept; } } return $depts; } /** * Tell if debug is on. * * @since 1.6.10 * * @return bool */ protected function is_debug() { return ( defined( 'IMAGIFY_DEBUG' ) && IMAGIFY_DEBUG ) || ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ); } /** * Tell if the admin bar item is displaying. * * @since 1.6.10 * * @return bool */ protected function is_admin_bar_item_showing() { if ( defined( 'IMAGIFY_HIDDEN_ACCOUNT' ) && IMAGIFY_HIDDEN_ACCOUNT ) { return false; } return get_imagify_option( 'api_key' ) && is_admin_bar_showing() && imagify_get_context( 'wp' )->current_user_can( 'manage' ) && get_imagify_option( 'admin_bar_menu' ); } } classes/class-imagify-settings.php 0000644 00000073745 15174671745 0013341 0 ustar 00 <?php use Imagify\Notices\Notices; use Imagify\Traits\InstanceGetterTrait; /** * Class that handles the plugin settings. * * @since 1.7 */ class Imagify_Settings { use InstanceGetterTrait; /** * Class version. * * @since 1.7 * @var string */ const VERSION = '1.0.1'; /** * The settings group. * * @since 1.7 * @var string */ protected $settings_group; /** * The option name. * * @since 1.7 * @var string */ protected $option_name; /** * The options instance. * * @since 1.7 * @var object */ protected $options; /** * The constructor. * * @since 1.7 */ protected function __construct() { $this->options = Imagify_Options::get_instance(); $this->option_name = $this->options->get_option_name(); $this->settings_group = IMAGIFY_SLUG; } /** * Launch the hooks. * * @since 1.7 */ public function init() { add_filter( 'sanitize_option_' . $this->option_name, [ $this, 'populate_values_on_save' ], 5 ); add_action( 'admin_init', [ $this, 'register' ] ); add_filter( 'option_page_capability_' . $this->settings_group, [ $this, 'get_capability' ] ); if ( imagify_is_active_for_network() ) { add_filter( 'pre_update_site_option_' . $this->option_name, [ $this, 'maybe_set_redirection' ], 10, 2 ); add_action( 'update_site_option_' . $this->option_name, [ $this, 'after_save_network_options' ], 10, 3 ); add_action( 'admin_post_update', [ $this, 'update_site_option_on_network' ] ); } else { add_filter( 'pre_update_option_' . $this->option_name, [ $this, 'maybe_set_redirection' ], 10, 2 ); add_action( 'update_option_' . $this->option_name, [ $this, 'after_save_options' ], 10, 2 ); } } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS HELPERS ========================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the name of the settings group. * * @since 1.7 * @return string */ public function get_settings_group() { return $this->settings_group; } /** * Get the URL to use as form action. * * @since 1.7 * @return string */ public function get_form_action() { return imagify_is_active_for_network() ? admin_url( 'admin-post.php' ) : admin_url( 'options.php' ); } /** * Tell if we're submitting the settings form. * * @since 1.7 * @return bool */ public function is_form_submit() { if ( ! isset( $_POST['option_page'], $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return false; } return sanitize_text_field( wp_unslash( $_POST['option_page'] ) ) === $this->settings_group && sanitize_text_field( wp_unslash( $_POST['action'] ) ) === 'update'; // phpcs:ignore WordPress.Security.NonceVerification.Missing } /** ----------------------------------------------------------------------------------------- */ /** ON FORM SUBMIT ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * On form submit, handle some specific values. * This must be hooked before Imagify_Options::sanitize_and_validate_on_update(). * * @since 1.7 * * @param array $values The option values. * * @return array */ public function populate_values_on_save( $values ) { if ( ! $this->is_form_submit() ) { return $values; } $values = is_array( $values ) ? $values : []; /** * Disabled thumbnail sizes. */ $values = $this->populate_disallowed_sizes( $values ); /** * Custom folders. */ $values = $this->populate_custom_folders( $values ); /** * Filter settings when saved via the settings page. * * @since 1.9 * * @param array $values The option values. */ $values = apply_filters( 'imagify_settings_on_save', $values ); return (array) $values; } /** * On form submit, handle disallowed thumbnail sizes. * * @since 1.7 * * @param array $values The option values. * * @return array */ protected function populate_disallowed_sizes( $values ) { $values['disallowed-sizes'] = []; if ( isset( $values['disallowed-sizes-reversed'] ) && is_array( $values['disallowed-sizes-reversed'] ) ) { $checked = ! empty( $values['disallowed-sizes-checked'] ) && is_array( $values['disallowed-sizes-checked'] ) ? array_flip( $values['disallowed-sizes-checked'] ) : []; if ( ! empty( $values['disallowed-sizes-reversed'] ) ) { foreach ( $values['disallowed-sizes-reversed'] as $size_key ) { if ( ! isset( $checked[ $size_key ] ) ) { // The checkbox is not checked: the size is disabled. $values['disallowed-sizes'][ $size_key ] = 1; } } } } unset( $values['disallowed-sizes-reversed'], $values['disallowed-sizes-checked'] ); return $values; } /** * On form submit, handle the custom folders. * * @since 1.7 * * @param array $values The option values. * * @return array */ protected function populate_custom_folders( $values ) { if ( ! imagify_can_optimize_custom_folders() ) { // The databases are not ready or the user has not the permission. unset( $values['custom_folders'] ); return $values; } if ( ! isset( $values['custom_folders'] ) ) { // No selected folders: set them all inactive. Imagify_Custom_Folders::deactivate_all_folders(); // Remove files that are in inactive folders and are not optimized. Imagify_Custom_Folders::remove_unoptimized_files_from_inactive_folders(); // Remove empty inactive folders. Imagify_Custom_Folders::remove_empty_inactive_folders(); return $values; } if ( ! is_array( $values['custom_folders'] ) ) { // Invalid value. unset( $values['custom_folders'] ); return $values; } $selected = array_filter( $values['custom_folders'] ); unset( $values['custom_folders'] ); if ( ! $selected ) { // No selected folders: set them all inactive. Imagify_Custom_Folders::deactivate_all_folders(); // Remove files that are in inactive folders and are not optimized. Imagify_Custom_Folders::remove_unoptimized_files_from_inactive_folders(); // Remove empty inactive folders. Imagify_Custom_Folders::remove_empty_inactive_folders(); return $values; } // Normalize the paths, remove duplicates, and remove sub-paths. $selected = array_map( 'sanitize_text_field', $selected ); $selected = array_map( 'wp_normalize_path', $selected ); $selected = array_map( 'trailingslashit', $selected ); $selected = array_flip( array_flip( $selected ) ); $selected = Imagify_Custom_Folders::remove_sub_paths( $selected ); // Remove the active status from the folders that are not selected. Imagify_Custom_Folders::deactivate_not_selected_folders( $selected ); // Add the active status to the folders that are selected (and already in the DB). $selected = Imagify_Custom_Folders::activate_selected_folders( $selected ); // If we still have paths here, they need to be added to the DB with an active status. Imagify_Custom_Folders::insert_folders( $selected ); // Remove files that are in inactive folders and are not optimized. Imagify_Custom_Folders::remove_unoptimized_files_from_inactive_folders(); // Reassign files to active folders. Imagify_Custom_Folders::reassign_inactive_files(); // Remove empty inactive folders. Imagify_Custom_Folders::remove_empty_inactive_folders(); return $values; } /** ----------------------------------------------------------------------------------------- */ /** SETTINGS API ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Add Imagify' settings to the settings API whitelist. * * @since 1.7 */ public function register() { register_setting( $this->settings_group, $this->option_name ); } /** * Set the user capacity needed to save Imagify's main options from the settings page. * * @since 1.7 */ public function get_capability() { return imagify_get_context( 'wp' )->get_capacity( 'manage' ); } /** * If the user clicked the "Save & Go to Bulk Optimizer" button, set a redirection to the bulk optimizer. * We use this hook because it can be triggered even if the option value hasn't changed. * * @since 1.7 * * @param mixed $value The new, unserialized option value. * @param mixed $old_value The old option value. * * @return mixed The option value. */ public function maybe_set_redirection( $value, $old_value ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( isset( $_POST['submit-goto-bulk'] ) ) { // WPCS: CSRF ok. $_REQUEST['_wp_http_referer'] = esc_url_raw( get_admin_url( get_current_blog_id(), 'upload.php?page=imagify-bulk-optimization' ) ); } return $value; } /** * Used to launch some actions after saving the network options. * * @since 1.7 * * @param string $option Name of the network option. * @param mixed $value Current value of the network option. * @param mixed $old_value Old value of the network option. */ public function after_save_network_options( $option, $value, $old_value ) { $this->after_save_options( $old_value, $value ); } /** * Used to launch some actions after saving the options. * * @since 1.7 * * @param mixed $old_value The old option value. * @param mixed $value The new option value. */ public function after_save_options( $old_value, $value ) { $old_key = isset( $old_value['api_key'] ) ? $old_value['api_key'] : ''; $new_key = isset( $value['api_key'] ) ? $value['api_key'] : ''; if ( $old_key === $new_key ) { return; } delete_transient( 'imagify_user_cache' ); // Handle API key validation cache and notices. if ( Imagify_Requirements::is_api_key_valid( true ) ) { Notices::dismiss_notice( 'wrong-api-key' ); } else { Notices::renew_notice( 'wrong-api-key' ); } } /** * `options.php` does not handle network options. Let's use `admin-post.php` for multisite installations. * * @since 1.9.11 deprecate 'whitelist_options' filter. * @since 1.7 * * @return void */ public function update_site_option_on_network() { global $wp_version; if ( empty( $_POST['option_page'] ) || $_POST['option_page'] !== $this->settings_group ) { // WPCS: CSRF ok. return; } /** This filter is documented in /wp-admin/options.php. */ $capability = apply_filters( 'option_page_capability_' . $this->settings_group, 'manage_network_options' ); if ( ! current_user_can( $capability ) ) { imagify_die(); return; } if ( ! imagify_check_nonce( $this->settings_group . '-options' ) ) { return; } if ( version_compare( $wp_version, '5.5', '>=' ) ) { $allowed_options = apply_filters_deprecated( 'whitelist_options', [ [] ], '5.5.0', 'allowed_options', __( 'Please consider writing more inclusive code.' ) ); } else { $allowed_options = apply_filters( 'whitelist_options', [] ); } $allowed_options = apply_filters( 'allowed_options', $allowed_options ); if ( ! isset( $allowed_options[ $this->settings_group ] ) ) { imagify_die( __( '<strong>ERROR</strong>: options page not found.' ) ); return; } $options = $allowed_options[ $this->settings_group ]; if ( $options ) { foreach ( $options as $option ) { $option = trim( $option ); $value = null; if ( isset( $_POST[ $option ] ) ) { $value = wp_unslash( $_POST[ $option ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! is_array( $value ) ) { $value = trim( $value ); } $value = wp_unslash( $value ); } update_site_option( $option, $value ); } } /** * Redirect back to the settings page that was submitted. */ imagify_maybe_redirect( false, [ 'settings-updated' => 'true' ] ); } /** ----------------------------------------------------------------------------------------- */ /** FIELDS ================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Display a single checkbox. * * @since 1.7 * * @param array $args Arguments: * {option_name} string The option name. E.g. 'disallowed-sizes'. Mandatory. * {label} string The label to use. * {info} string Text to display in an "Info box" after the field. A 'aria-describedby' attribute will automatically be created. * {attributes} array A list of HTML attributes, as 'attribute' => 'value'. * {current_value} int|bool USE ONLY WHEN DEALING WITH DATA THAT IS NOT SAVED IN THE PLUGIN OPTIONS. If not provided, the field will automatically get the value from the options. */ public function field_checkbox( $args ) { $args = array_merge( [ 'option_name' => '', 'label' => '', 'info' => '', 'attributes' => [], // To not use the plugin settings: use an integer. 'current_value' => null, ], $args ); if ( ! $args['option_name'] || ! $args['label'] ) { return; } if ( is_numeric( $args['current_value'] ) || is_bool( $args['current_value'] ) ) { // We don't use the plugin settings. $current_value = (int) (bool) $args['current_value']; } else { // This is a normal plugin setting. $current_value = $this->options->get( $args['option_name'] ); } $option_name_class = sanitize_html_class( $args['option_name'] ); $attributes = [ 'name' => $this->option_name . '[' . $args['option_name'] . ']', 'id' => 'imagify_' . $option_name_class, ]; if ( $args['info'] && empty( $attributes['aria-describedby'] ) ) { $attributes['aria-describedby'] = 'describe-' . $option_name_class; } $attributes = array_merge( $attributes, $args['attributes'] ); $args['attributes'] = self::build_attributes( $attributes ); ?> <input type="checkbox" value="1" <?php checked( $current_value, 1 ); ?> <?php echo $args['attributes']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> /> <!-- Empty onclick attribute to make clickable labels on iTruc & Mac --> <label for="<?php echo esc_attr( $attributes['id'] ); ?>" onclick=""> <?php echo esc_html( $args['label'] ); ?> </label> <?php if ( ! $args['info'] ) { return; } ?> <span id="<?php echo esc_attr( $attributes['aria-describedby'] ); ?>" class="imagify-info"> <span class="dashicons dashicons-info"></span> <?php echo esc_html( $args['info'] ); ?> </span> <?php } /** * Display a checkbox group. * * @since 1.7 * * @param array $args Arguments: * {option_name} string The option name. E.g. 'disallowed-sizes'. Mandatory. * {legend} string Label to use for the <legend> tag. * {values} array List of values to display, in the form of 'value' => 'Label'. Mandatory. * {disabled_values} array Values to be disabled. Values are the array keys. * {reverse_check} bool If true, the values that will be stored in the option are the ones that are unchecked. It requires special treatment when saving (detect what values are unchecked). * {attributes} array A list of HTML attributes, as 'attribute' => 'value'. * {current_values} array USE ONLY WHEN DEALING WITH DATA THAT IS NOT SAVED IN THE PLUGIN OPTIONS. If not provided, the field will automatically get the value from the options. */ public function field_checkbox_list( $args ) { $args = array_merge( [ 'option_name' => '', 'legend' => '', 'values' => [], 'disabled_values' => [], 'reverse_check' => false, 'attributes' => [], // To not use the plugin settings: use an array. 'current_values' => false, ], $args ); if ( ! $args['option_name'] || ! $args['values'] ) { return; } if ( is_array( $args['current_values'] ) ) { // We don't use the plugin settings. $current_values = $args['current_values']; } else { // This is a normal plugin setting. $current_values = $this->options->get( $args['option_name'] ); } $option_name_class = sanitize_html_class( $args['option_name'] ); $attributes = array_merge( [ 'name' => $this->option_name . '[' . $args['option_name'] . ( $args['reverse_check'] ? '-checked' : '' ) . '][]', 'id' => 'imagify_' . $option_name_class . '_%s', 'class' => 'imagify-row-check', ], $args['attributes'] ); $id_attribute = $attributes['id']; unset( $attributes['id'] ); $args['attributes'] = self::build_attributes( $attributes ); $current_values = array_diff_key( $current_values, $args['disabled_values'] ); $nb_of_values = count( $args['values'] ); $display_check_all = $nb_of_values > 3; $nb_of_checked = 0; ?> <fieldset class="imagify-check-group <?php echo $nb_of_values > 5 ? ' imagify-is-scrollable' : ''; ?>"> <?php if ( $args['legend'] ) { ?> <legend class="screen-reader-text"> <?php echo esc_html( $args['legend'] ); ?> </legend> <?php } foreach ( $args['values'] as $value => $label ) { $input_id = sprintf( $id_attribute, sanitize_html_class( $value ) ); $disabled = isset( $args['disabled_values'][ $value ] ); if ( $args['reverse_check'] ) { $checked = ! $disabled && ! isset( $current_values[ $value ] ); } else { $checked = ! $disabled && isset( $current_values[ $value ] ); } $nb_of_checked = $checked ? $nb_of_checked + 1 : $nb_of_checked; if ( $args['reverse_check'] ) { echo '<input type="hidden" name="' . esc_attr( $this->option_name . '[' . $args['option_name'] ) . '-reversed][]" value="' . esc_attr( $value ) . '" />'; } ?> <p> <input type="checkbox" value="<?php echo esc_attr( $value ); ?>" id="<?php echo esc_attr( $input_id ); ?>" <?php echo $args['attributes']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <?php checked( $checked ); ?> <?php disabled( $disabled ); ?> /> <label for="<?php echo esc_attr( $input_id ); ?>" onclick=""> <?php echo esc_html( $label ); ?> </label> </p> <?php } ?> </fieldset> <?php if ( $display_check_all ) { if ( $args['reverse_check'] ) { $all_checked = ! array_intersect_key( $args['values'], $current_values ); } else { $all_checked = ! array_diff_key( $args['values'], $current_values ); } ?> <p class="hide-if-no-js imagify-select-all-buttons"> <button type="button" class="imagify-link-like imagify-select-all <?php echo $all_checked ? ' imagify-is-inactive" aria-disabled="true' : ''; ?>" data-action="select"> <?php esc_html_e( 'Select All', 'imagify' ); ?> </button> <span class="imagify-pipe"></span> <button type="button" class="imagify-link-like imagify-select-all <?php echo $nb_of_checked ? '' : ' imagify-is-inactive" aria-disabled="true'; ?> " data-action="unselect"> <?php esc_html_e( 'Unselect All', 'imagify' ); ?> </button> </p> <?php } } /** * Display a radio list group. * * @since 1.9 * * @param array $args { * Arguments. * * @type string $option_name The option name. E.g. 'disallowed-sizes'. Mandatory. * @type string $legend Label to use for the <legend> tag. * @type string $info Text to display in an "Info box" after the field. A 'aria-describedby' attribute will automatically be created. * @type array $values List of values to display, in the form of 'value' => 'Label'. Mandatory. * @type array $attributes A list of HTML attributes, as 'attribute' => 'value'. * @type array $current_value USE ONLY WHEN DEALING WITH DATA THAT IS NOT SAVED IN THE PLUGIN OPTIONS. If not provided, the field will automatically get the value from the options. * } */ public function field_radio_list( $args ) { $args = array_merge( [ 'option_name' => '', 'legend' => '', 'info' => '', 'values' => [], 'attributes' => [], // To not use the plugin settings: use an array. 'current_value' => false, ], $args ); if ( ! $args['option_name'] || ! $args['values'] ) { return; } if ( is_array( $args['current_value'] ) ) { // We don't use the plugin settings. $current_value = $args['current_value']; } else { // This is a normal plugin setting. $current_value = $this->options->get( $args['option_name'] ); } $option_name_class = sanitize_html_class( $args['option_name'] ); $attributes = array_merge( [ 'name' => $this->option_name . '[' . $args['option_name'] . ']', 'id' => 'imagify_' . $option_name_class . '_%s', 'class' => 'imagify-row-radio', ], $args['attributes'] ); $id_attribute = $attributes['id']; unset( $attributes['id'] ); $args['attributes'] = self::build_attributes( $attributes ); ?> <fieldset class="imagify-radio-group"> <?php if ( $args['legend'] ) { ?> <legend class="screen-reader-text"> <?php echo esc_html( $args['legend'] ); ?> </legend> <?php } foreach ( $args['values'] as $value => $label ) { $input_id = sprintf( $id_attribute, sanitize_html_class( $value ) ); ?> <input type="radio" value="<?php echo esc_attr( $value ); ?>" id="<?php echo esc_attr( $input_id ); ?>" <?php echo $args['attributes']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <?php checked( $current_value, $value ); ?> /> <label for="<?php echo esc_attr( $input_id ); ?>" onclick=""> <?php echo esc_html( $label ); ?> </label> <br/> <?php } ?> </fieldset> <?php if ( ! $args['info'] ) { return; } ?> <span id="<?php echo esc_attr( $attributes['aria-describedby'] ); ?>" class="imagify-info"> <span class="dashicons dashicons-info"></span> <?php echo esc_html( $args['info'] ); ?> </span> <?php } /** * Display styled radio list group. * * @param array $args Arguments: * {option_name} string The option name. E.g. 'disallowed-sizes'. Mandatory. * {values} array List of values to display, in the form of 'value' => 'Label'. Mandatory. * {attributes} array A list of HTML attributes, as 'attribute' => 'value'. * {current_value} int|bool USE ONLY WHEN DEALING WITH DATA THAT IS NOT SAVED IN THE PLUGIN OPTIONS. If not provided, the field will automatically get the value from the options. * * @return void */ public function field_inline_radio_list( $args ) { $args = array_merge( [ 'option_name' => '', 'values' => [], 'info' => '', 'attributes' => [], 'current_value' => false, ], $args ); if ( ! $args['option_name'] || ! $args['values'] ) { return; } if ( is_numeric( $args['current_value'] ) || is_string( $args['current_value'] ) ) { $current_value = $args['current_value']; } else { $current_value = $this->options->get( $args['option_name'] ); } $option_name_class = sanitize_html_class( $args['option_name'] ); $attributes = array_merge( [ 'name' => $this->option_name . '[' . $args['option_name'] . ']', 'id' => 'imagify_' . $option_name_class . '_%s', 'class' => 'imagify-row-radio', ], $args['attributes'] ); $id_attribute = $attributes['id']; unset( $attributes['id'] ); $args['attributes'] = self::build_attributes( $attributes ); ?> <div class="imagify-setting-optim-level"> <p class="imagify-inline-options imagify-inline-options-<?php echo esc_attr( $args['info_class'] ); ?>"> <?php foreach ( $args['values'] as $value => $label ) { $input_id = sprintf( $id_attribute, sanitize_html_class( $value ) ); ?> <input type="radio" value="<?php echo esc_attr( $value ); ?>" id="<?php echo esc_attr( $input_id ); ?>"<?php echo $args['attributes']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <?php checked( $current_value, $value ); ?> /> <label for="<?php echo esc_attr( $input_id ); ?>" onclick=""><?php echo esc_html( $label ); ?></label> <?php } ?> </p> <span id="<?php echo esc_attr( $attributes['aria-describedby'] ); ?>" class="imagify-<?php echo esc_attr( $args['info_class'] ); ?>"> <span class="dashicons dashicons-info"></span> <?php echo esc_html( $args['info'] ); ?> </span> </div> <?php } /** * Display a text box. * * @since 1.9.3 * * @param array $args Arguments: * {option_name} string The option name. E.g. 'disallowed-sizes'. Mandatory. * {label} string The label to use. * {info} string Text to display in an "Info box" after the field. A 'aria-describedby' attribute will automatically be created. * {attributes} array A list of HTML attributes, as 'attribute' => 'value'. * {current_value} int|bool USE ONLY WHEN DEALING WITH DATA THAT IS NOT SAVED IN THE PLUGIN OPTIONS. If not provided, the field will automatically get the value from the options. */ public function field_text_box( $args ) { $args = array_merge( [ 'option_name' => '', 'label' => '', 'info' => '', 'attributes' => [], // To not use the plugin settings. 'current_value' => null, ], $args ); if ( ! $args['option_name'] || ! $args['label'] ) { return; } if ( is_numeric( $args['current_value'] ) || is_string( $args['current_value'] ) ) { // We don't use the plugin settings. $current_value = $args['current_value']; } else { // This is a normal plugin setting. $current_value = $this->options->get( $args['option_name'] ); } $option_name_class = sanitize_html_class( $args['option_name'] ); $attributes = [ 'name' => $this->option_name . '[' . $args['option_name'] . ']', 'id' => 'imagify_' . $option_name_class, ]; if ( $args['info'] && empty( $attributes['aria-describedby'] ) ) { $attributes['aria-describedby'] = 'describe-' . $option_name_class; } $attributes = array_merge( $attributes, $args['attributes'] ); $args['attributes'] = self::build_attributes( $attributes ); ?> <!-- Empty onclick attribute to make clickable labels on iTruc & Mac --> <label for="<?php echo esc_attr( $attributes['id'] ); ?>" onclick=""> <?php echo esc_html( $args['label'] ); ?> </label> <input type="text" value="<?php echo esc_attr( $current_value ); ?>" <?php echo $args['attributes']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> /> <?php if ( ! $args['info'] ) { return; } ?> <span id="<?php echo esc_attr( $attributes['aria-describedby'] ); ?>" class="imagify-info"> <span class="dashicons dashicons-info"></span> <?php echo esc_html( $args['info'] ); ?> </span> <?php } /** * Display a simple hidden input. * * @since 1.9.3 * * @param array $args Arguments: * {option_name} string The option name. E.g. 'disallowed-sizes'. Mandatory. * {attributes} array A list of HTML attributes, as 'attribute' => 'value'. * {current_value} int|bool USE ONLY WHEN DEALING WITH DATA THAT IS NOT SAVED IN THE PLUGIN OPTIONS. If not provided, the field will automatically get the value from the options. */ public function field_hidden( $args ) { $args = array_merge( [ 'option_name' => '', 'attributes' => [], // To not use the plugin settings. 'current_value' => null, ], $args ); if ( ! $args['option_name'] ) { return; } if ( is_numeric( $args['current_value'] ) || is_string( $args['current_value'] ) ) { // We don't use the plugin settings. $current_value = $args['current_value']; } else { // This is a normal plugin setting. $current_value = $this->options->get( $args['option_name'] ); } $option_name_class = sanitize_html_class( $args['option_name'] ); $attributes = [ 'name' => $this->option_name . '[' . $args['option_name'] . ']', 'id' => 'imagify_' . $option_name_class, ]; $attributes = array_merge( $attributes, $args['attributes'] ); $args['attributes'] = self::build_attributes( $attributes ); ?> <input type="hidden" value="<?php echo esc_attr( $current_value ); ?>" <?php echo $args['attributes']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> /> <?php } /** ----------------------------------------------------------------------------------------- */ /** FIELD VALUES ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the thumbnail sizes. * * @since 1.7 * @return array A list of thumbnail sizes in the form of 'medium' => 'medium - 300 × 300'. */ public static function get_thumbnail_sizes() { static $sizes; if ( isset( $sizes ) ) { return $sizes; } $sizes = get_imagify_thumbnail_sizes(); foreach ( $sizes as $size_key => $size_data ) { $sizes[ $size_key ] = sprintf( '%s - %d × %d', esc_html( stripslashes( $size_data['name'] ) ), $size_data['width'], $size_data['height'] ); } return $sizes; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Create HTML attributes from an array. * * @since 1.7 * * @param array $attributes A list of attribute pairs. * * @return string HTML attributes. */ public static function build_attributes( $attributes ) { if ( ! $attributes || ! is_array( $attributes ) ) { return ''; } $out = ''; foreach ( $attributes as $attribute => $value ) { $out .= ' ' . $attribute . '="' . esc_attr( $value ) . '"'; } return $out; } } classes/class-imagify-options.php 0000644 00000013220 15174671745 0013152 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * Class that handles the plugin options. * * @since 1.7 */ class Imagify_Options extends Imagify_Abstract_Options { use InstanceGetterTrait; /** * Suffix used in the name of the option. * * @var string * @since 1.7 */ protected $identifier = 'settings'; /** * The default values for the Imagify main options. * These are the "zero state" values. * Don't use null as value. * * @var array * @since 1.7 */ protected $default_values = [ 'api_key' => '', 'optimization_level' => 2, 'lossless' => 0, 'auto_optimize' => 0, 'backup' => 0, 'resize_larger' => 0, 'resize_larger_w' => 0, 'display_nextgen' => 0, 'display_nextgen_method' => 'picture', 'display_webp' => 0, 'display_webp_method' => 'picture', 'cdn_url' => '', 'disallowed-sizes' => [], 'admin_bar_menu' => 1, 'partner_links' => 0, 'convert_to_avif' => 0, 'convert_to_webp' => 0, 'optimization_format' => 'webp', ]; /** * The Imagify main option values used when they are set the first time or reset. * Values identical to default values are not listed. * * @var array * @since 1.7 */ protected $reset_values = [ 'optimization_level' => 2, 'auto_optimize' => 1, 'backup' => 1, 'admin_bar_menu' => 1, 'partner_links' => 1, ]; /** * The constructor. * Side note: $this->hook_identifier value is "option". * * @since 1.7 */ protected function __construct() { if ( defined( 'IMAGIFY_API_KEY' ) && IMAGIFY_API_KEY ) { $this->default_values['api_key'] = (string) IMAGIFY_API_KEY; } if ( function_exists( 'wp_get_original_image_path' ) ) { $this->reset_values['resize_larger'] = 1; $filter_cb = [ imagify_get_context( 'wp' ), 'get_resizing_threshold' ]; $filtered = has_filter( 'big_image_size_threshold', $filter_cb ); if ( $filtered ) { remove_filter( 'big_image_size_threshold', $filter_cb, IMAGIFY_INT_MAX ); } /** This filter is documented in wp-admin/includes/image.php */ $this->reset_values['resize_larger_w'] = (int) apply_filters( 'big_image_size_threshold', 2560, [ 0, 0 ], '', 0 ); $this->reset_values['resize_larger_w'] = $this->sanitize_and_validate_value( 'resize_larger_w', $this->reset_values['resize_larger_w'], $this->default_values['resize_larger_w'] ); if ( $filtered ) { add_filter( 'big_image_size_threshold', $filter_cb, IMAGIFY_INT_MAX ); } } $this->network_option = imagify_is_active_for_network(); parent::__construct(); } /** * Sanitize and validate an option value. Basic casts have been made. * * @since 1.7 * * @param string $key The option key. * @param mixed $value The value. * @param mixed $default_value The default value. * @return mixed */ public function sanitize_and_validate_value( $key, $value, $default_value ) { static $max_sizes; switch ( $key ) { case 'api_key': if ( defined( 'IMAGIFY_API_KEY' ) && IMAGIFY_API_KEY ) { return (string) IMAGIFY_API_KEY; } return $value ? sanitize_key( $value ) : ''; case 'optimization_level': if ( $value < 0 || $value > 2 ) { // For an invalid value, return the "reset" value. $reset_values = $this->get_reset_values(); return $reset_values[ $key ]; } return $value; case 'optimization_format': if ( ! in_array( $value, [ 'off', 'webp', 'avif' ], true ) ) { // For an invalid value, return the "reset" value. $reset_values = $this->get_reset_values(); return $reset_values[ $key ]; } return $value; case 'auto_optimize': case 'backup': case 'lossless': case 'resize_larger': case 'convert_to_webp': case 'display_nextgen': case 'display_webp': case 'admin_bar_menu': case 'partner_links': case 'convert_to_avif': return empty( $value ) ? 0 : 1; case 'resize_larger_w': if ( $value <= 0 ) { // Invalid. return $default_value; } if ( ! isset( $max_sizes ) ) { $max_sizes = get_imagify_max_intermediate_image_size(); } if ( $value < $max_sizes['width'] ) { // Invalid. return $max_sizes['width']; } return $value; case 'disallowed-sizes': if ( ! $value ) { return $default_value; } $value = array_keys( $value ); $value = array_map( 'sanitize_text_field', $value ); return array_fill_keys( $value, 1 ); case 'display_nextgen_method': case 'display_webp_method': $values = [ 'picture' => 1, 'rewrite' => 1, ]; if ( isset( $values[ $value ] ) ) { return $value; } // For an invalid value, return the "reset" value. $reset_values = $this->get_reset_values(); return $reset_values[ $key ]; case 'cdn_url': $cdn_source = apply_filters( 'imagify_cdn_source_url', $value ); if ( 'option' !== $cdn_source['source'] ) { /** * If the URL is defined via constant or filter, unset the option. * This is useful when the CDN is disabled: there is no need to do anything then. */ return ''; } return $cdn_source['url']; } return false; } /** * Validate Imagify's options before storing them. Basic sanitization and validation have been made, row by row. * * @since 1.7 * * @param string $values The option value. * @return array */ public function validate_values_on_update( $values ) { // The max width for the "Resize larger images" option can't be 0. if ( empty( $values['resize_larger_w'] ) ) { unset( $values['resize_larger'], $values['resize_larger_w'] ); } return $values; } } classes/class-imagify-auto-optimization.php 0000644 00000046656 15174671745 0015176 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * Class that handles the auto-optimization process. * This occurs when a new image is uploaded, and when an optimized image is worked with (resized, etc). * The process will work only if wp_generate_attachment_metadata() and wp_update_attachment_metadata() are used. * * @since 1.8.4 */ class Imagify_Auto_Optimization extends Imagify_Auto_Optimization_Deprecated { use InstanceGetterTrait; /** * An array containing all the "steps" an attachment is going through. * This is used to decide the behavior of the automatic optimization. * * @var array { * An array of arrays with attachment ID as keys. * Each array can contain the following: * * @type $upload int Set to 1 if the attachment is a new upload. * @type $generate int Set to 1 when going though wp_generate_attachment_metadata(). * @type $update int Set to 1 when going though wp_update_attachment_metadata(). * } * @since 1.8.4 * @since 1.9.10 Private. * @since 1.9.10 Items are arrays instead of 1s. */ private $attachments = []; /** * Tell if we’re using WP 5.3+. * * @var bool * @since 1.9.10 */ private $is_wp_53; /** * The ID of the attachment that failed to be uploaded. * * @var int * @since 1.9.8 */ protected $upload_failure_id = 0; /** * Used to prevent an auto-optimization locally. * * @var array * @since 1.8.4 */ private static $prevented = []; /** * Used to prevent an auto-optimization internally. * * @var array * @since 1.9.8 */ private static $prevented_internally = []; /** * Init. * * @since 1.8.4 */ public function init() { global $wp_version; $priority = IMAGIFY_INT_MAX - 30; $this->is_wp_53 = version_compare( $wp_version, '5.3-alpha1' ) >= 0; // Automatic optimization tunel. add_action( 'add_attachment', [ $this, 'store_upload_ids' ], $priority ); add_filter( 'wp_generate_attachment_metadata', [ $this, 'maybe_store_generate_step' ], $priority, 2 ); add_filter( 'wp_update_attachment_metadata', [ $this, 'store_ids_to_optimize' ], $priority, 2 ); if ( $this->is_wp_53 ) { // WP 5.3+. add_action( 'imagify_after_auto_optimization_init', [ $this, 'do_auto_optimization' ], $priority, 2 ); // Upload failure recovering. add_action( 'wp_ajax_media-create-image-subsizes', [ $this, 'prevent_auto_optimization_when_recovering_from_upload_failure' ], -5 ); // Before WP’s hook (priority 1). } else { add_action( 'updated_post_meta', [ $this, 'do_auto_optimization_after_meta_update' ], $priority, 4 ); add_action( 'added_post_meta', [ $this, 'do_auto_optimization_after_meta_update' ], $priority, 4 ); } add_action( 'deleted_post_meta', [ $this, 'unset_optimization' ], $priority, 3 ); // Prevent to re-optimize when updating the image width and height (when resizing the full image). add_action( 'imagify_before_update_wp_media_data_dimensions', [ __CLASS__, 'prevent_optimization' ], 5 ); add_action( 'imagify_after_update_wp_media_data_dimensions', [ __CLASS__, 'allow_optimization' ], 5 ); } /** * Remove the hooks. * * @since 1.8.4 */ public function remove_hooks() { $priority = IMAGIFY_INT_MAX - 30; // Automatic optimization tunel. remove_action( 'add_attachment', [ $this, 'store_upload_ids' ], $priority ); remove_filter( 'wp_generate_attachment_metadata', [ $this, 'maybe_store_generate_step' ], $priority ); remove_filter( 'wp_update_attachment_metadata', [ $this, 'store_ids_to_optimize' ], $priority ); if ( $this->is_wp_53 ) { // WP 5.3+. remove_action( 'imagify_after_auto_optimization_init', [ $this, 'do_auto_optimization' ], $priority ); // Upload failure recovering. remove_action( 'wp_ajax_media-create-image-subsizes', [ $this, 'prevent_auto_optimization_when_recovering_from_upload_failure' ], -5 ); } else { remove_action( 'updated_post_meta', [ $this, 'do_auto_optimization_after_meta_update' ], $priority ); remove_action( 'added_post_meta', [ $this, 'do_auto_optimization_after_meta_update' ], $priority ); } remove_action( 'deleted_post_meta', [ $this, 'unset_optimization' ], $priority ); // Prevent to re-optimize when updating the image width and height (when resizing the full image). remove_action( 'imagify_before_update_wp_media_data_dimensions', [ __CLASS__, 'prevent_optimization' ], 5 ); remove_action( 'imagify_after_update_wp_media_data_dimensions', [ __CLASS__, 'allow_optimization' ], 5 ); } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Store the "upload step" when an attachment has just been uploaded. * * @since 1.8.4 * @see $this->store_ids_to_optimize() * * @param int $attachment_id Current attachment ID. */ public function store_upload_ids( $attachment_id ) { if ( ! self::is_optimization_prevented( $attachment_id ) && imagify_is_attachment_mime_type_supported( $attachment_id ) ) { $this->set_step( $attachment_id, 'upload' ); } } /** * Store the "generate step" when wp_generate_attachment_metadata() is used. * * @since 1.9.10 * * @param array $metadata An array of attachment meta data. * @param int $attachment_id Current attachment ID. * @return array */ public function maybe_store_generate_step( $metadata, $attachment_id ) { if ( self::is_optimization_prevented( $attachment_id ) ) { return $metadata; } if ( empty( $metadata ) || ! imagify_is_attachment_mime_type_supported( $attachment_id ) ) { $this->unset_steps( $attachment_id ); return $metadata; } $this->set_step( $attachment_id, 'generate' ); return $metadata; } /** * After the attachment meta data has been generated (partially, since WP 5.3), init the auto-optimization. * Two cases are possible to trigger the optimization: * - It's a new upload and auto-optimization is enabled. * - It's not a new upload (it is regenerated) and the attachment is already optimized. * * @since 1.8.4 * * @param array $metadata An array of attachment meta data. * @param int $attachment_id Current attachment ID. * @return array */ public function store_ids_to_optimize( $metadata, $attachment_id ) { static $auto_optimize; if ( self::is_optimization_prevented( $attachment_id ) ) { return $metadata; } if ( empty( $metadata ) || ! imagify_is_attachment_mime_type_supported( $attachment_id ) ) { $this->unset_steps( $attachment_id ); return $metadata; } if ( ! $this->has_step( $attachment_id, 'generate' ) ) { return $metadata; } $is_new_upload = $this->has_step( $attachment_id, 'upload' ); if ( $is_new_upload ) { // It's a new upload. if ( ! isset( $auto_optimize ) ) { $auto_optimize = get_imagify_option( 'auto_optimize' ); } if ( ! $auto_optimize ) { /** * Fires when a new attachment is uploaded but auto-optimization is disabled. * * @since 1.8.4 * * @param int $attachment_id Attachment ID. * @param array $metadata An array of attachment meta data. */ do_action( 'imagify_new_attachment_auto_optimization_disabled', $attachment_id, $metadata ); return $metadata; } /** * Allow to prevent automatic optimization for a specific attachment. * * @since 1.6.12 * * @param bool $optimize True to optimize, false otherwise. * @param int $attachment_id Attachment ID. * @param array $metadata An array of attachment meta data. */ $optimize = apply_filters( 'imagify_auto_optimize_attachment', true, $attachment_id, $metadata ); if ( ! $optimize ) { return $metadata; } /** * It's a new upload and auto-optimization is enabled. */ } if ( ! $is_new_upload ) { // An existing attachment being regenerated (or something). $process = imagify_get_optimization_process( $attachment_id, 'wp' ); if ( ! $process->is_valid() ) { // Uh? return $metadata; } if ( ! $process->get_data()->get_optimization_status() ) { /** * Fires when an attachment is updated but not optimized yet. * * @since 1.8.4 * * @param int $attachment_id Attachment ID. * @param array $metadata An array of attachment meta data. */ do_action( 'imagify_not_optimized_attachment_updated', $attachment_id, $metadata ); return $metadata; } /** * Allow to prevent automatic reoptimization for a specific attachment. * * @since 1.8.4 * * @param bool $optimize True to optimize, false otherwise. * @param int $attachment_id Attachment ID. * @param array $metadata An array of attachment meta data. */ $optimize = apply_filters( 'imagify_auto_optimize_optimized_attachment', true, $attachment_id, $metadata ); if ( ! $optimize ) { return $metadata; } /** * The attachment already exists and was already optimized. */ } // Ready for the next step. $this->set_step( $attachment_id, 'update' ); /** * Triggered after a media auto-optimization init. * * @since 1.9.8 * * @param int $attachment_id The media ID. * @param bool $is_new_upload True if it's a new upload. False otherwize. */ do_action( 'imagify_after_auto_optimization_init', $attachment_id, $is_new_upload ); return $metadata; } /** * Launch auto optimization immediately after the post meta '_wp_attachment_metadata' is added or updated. * * @since 1.9 * @since 1.9 Previously named do_auto_optimization(). * * @param int $meta_id ID of the metadata entry. * @param int $attachment_id Current attachment ID. * @param string $meta_key Meta key. * @param mixed $metadata Meta value. */ public function do_auto_optimization_after_meta_update( $meta_id, $attachment_id, $meta_key, $metadata ) { if ( '_wp_attachment_metadata' !== $meta_key ) { return; } if ( self::is_optimization_prevented( $attachment_id ) ) { return; } if ( ! $this->has_step( $attachment_id, 'update' ) ) { return; } $this->do_auto_optimization( $attachment_id, $this->has_step( $attachment_id, 'upload' ) ); } /** * Launch auto optimization immediately after the post meta '_wp_attachment_metadata' is added or updated. * * @since 1.8.4 * @since 1.9.8 Changed signature. * * @param int $attachment_id The media ID. * @param bool $is_new_upload True if it's a new upload. False otherwize. */ public function do_auto_optimization( $attachment_id, $is_new_upload ) { $this->unset_steps( $attachment_id ); $process = imagify_get_optimization_process( $attachment_id, 'wp' ); /** * Fires before an attachment auto-optimization is triggered. * * @since 1.8.4 * * @param int $attachment_id The attachment ID. * @param bool $is_new_upload True if it's a new upload. False otherwize. */ do_action_deprecated( 'imagify_before_auto_optimization_launch', [ $attachment_id, $is_new_upload ], '1.9', 'imagify_before_auto_optimization' ); /** * Triggered before a media is auto-optimized. * * @since 1.8.4 * * @param int $attachment_id The media ID. * @param bool $is_new_upload True if it's a new upload. False otherwize. */ do_action( 'imagify_before_auto_optimization', $attachment_id, $is_new_upload ); if ( $is_new_upload ) { /** * It's a new upload. */ // Optimize. $process->optimize( null, [ 'is_new_upload' => 1 ] ); } else { /** * The media has already been optimized (or at least it has been tried). */ $process_data = $process->get_data(); // Get the optimization level before deleting the optimization data. $optimization_level = $process_data->get_optimization_level(); // Some specifics for the image editor. if ( isset( $_POST['action'], $_POST['do'], $_POST['postid'] ) && 'image-editor' === $_POST['action'] && (int) $_POST['postid'] === $attachment_id ) { // WPCS: CSRF ok. check_ajax_referer( 'image_editor-' . $attachment_id ); if ( ! current_user_can( 'edit_post', $attachment_id ) ) { imagify_die(); } // Restore the backup file. $result = $process->restore(); if ( is_wp_error( $result ) ) { // Restoration failed, there is no good way to handle this case. $process_data->delete_optimization_data(); } } else { // Remove old optimization data. $process_data->delete_optimization_data(); } // Optimize. $process->optimize( $optimization_level ); } /** * Triggered after a media auto-optimization is launched. * * @since 1.8.4 * * @param int $attachment_id The media ID. * @param bool $is_new_upload True if it's a new upload. False otherwize. */ do_action( 'imagify_after_auto_optimization', $attachment_id, $is_new_upload ); } /** * Remove the attachment ID from the $attachments property if the post meta '_wp_attachment_metadata' is deleted. * * @since 1.8.4 * * @param int $meta_ids An array of deleted metadata entry IDs. * @param int $attachment_id Current attachment ID. * @param string $meta_key Meta key. */ public function unset_optimization( $meta_ids, $attachment_id, $meta_key ) { if ( '_wp_attachment_metadata' !== $meta_key ) { return; } $this->unset_steps( $attachment_id ); } /** ----------------------------------------------------------------------------------------- */ /** HOOKS FOR WP 5.3+’S UPLOAD FAILURE RECOVERING =========================================== */ /** ----------------------------------------------------------------------------------------- */ /** * With WP 5.3+, prevent auto-optimization when WP tries to create thumbnails after an upload error, because it triggers wp_update_attachment_metadata() for each thumbnail size. * * @since 1.9.8 * @see wp_ajax_media_create_image_subsizes() * @see wp_update_image_subsizes() */ public function prevent_auto_optimization_when_recovering_from_upload_failure() { if ( ! check_ajax_referer( 'media-form', false, false ) ) { return; } if ( ! current_user_can( 'upload_files' ) ) { return; } if ( ! imagify_get_context( 'wp' )->current_user_can( 'auto-optimize' ) ) { return; } $attachment_id = ! empty( $_POST['attachment_id'] ) ? (int) $_POST['attachment_id'] : 0; if ( empty( $attachment_id ) ) { return; } if ( ! imagify_is_attachment_mime_type_supported( $attachment_id ) ) { return; } $this->upload_failure_id = $attachment_id; // Auto-optimization will be done on shutdown. ob_start( [ $this, 'maybe_do_auto_optimization_after_recovering_from_upload_failure' ] ); } /** * Maybe launch auto-optimization after recovering from an upload failure, when all thumbnails are created. * * @since 1.9.8 * @see wp_ajax_media_create_image_subsizes() * * @param string $content Buffer’s content. * @return string Buffer’s content. */ public function maybe_do_auto_optimization_after_recovering_from_upload_failure( $content ) { if ( empty( $content ) ) { return $content; } if ( empty( $this->upload_failure_id ) ) { // Uh? return $content; } if ( ! get_post( $this->upload_failure_id ) ) { return $content; } $json = json_decode( $content ); if ( empty( $json->success ) ) { return $content; } $attachment_id = $this->upload_failure_id; $metadata = wp_get_attachment_metadata( $attachment_id ); // Launch the process. $this->upload_failure_id = 0; $this->set_step( $attachment_id, 'generate' ); $this->store_ids_to_optimize( $metadata, $attachment_id ); return $content; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Set a "step" for an attachment. * * @since 1.9.10 * @see $this->attachments * * @param int $attachment_id Current attachment ID. * @param string $step The step to add. */ public function set_step( $attachment_id, $step ) { if ( empty( $this->attachments[ $attachment_id ] ) ) { $this->attachments[ $attachment_id ] = []; } $this->attachments[ $attachment_id ][ $step ] = 1; } /** * Unset a "step" for an attachment. * * @since 1.9.10 * @see $this->attachments * * @param int $attachment_id Current attachment ID. * @param string $step The step to add. */ public function unset_step( $attachment_id, $step ) { unset( $this->attachments[ $attachment_id ][ $step ] ); if ( empty( $this->attachments[ $attachment_id ] ) ) { $this->unset_steps( $attachment_id ); } } /** * Unset all "steps" for an attachment. * * @since 1.9.10 * @see $this->attachments * * @param int $attachment_id Current attachment ID. */ public function unset_steps( $attachment_id ) { unset( $this->attachments[ $attachment_id ] ); } /** * Tell if a "step" for an attachment exists. * * @since 1.9.10 * @see $this->attachments * * @param int $attachment_id Current attachment ID. * @param string $step The step to add. * @return bool */ public function has_step( $attachment_id, $step ) { return ! empty( $this->attachments[ $attachment_id ][ $step ] ); } /** * Prevent an auto-optimization locally. * How to use it: * Imagify_Auto_Optimization::prevent_optimization( $attachment_id ); * wp_update_attachment_metadata( $attachment_id ); * Imagify_Auto_Optimization::allow_optimization( $attachment_id ); * * @since 1.8.4 * @since 1.9.8 Prevents/Allows can stack. * * @param int $attachment_id Current attachment ID. */ public static function prevent_optimization( $attachment_id ) { if ( ! isset( self::$prevented[ $attachment_id ] ) ) { self::$prevented[ $attachment_id ] = 1; } else { ++self::$prevented[ $attachment_id ]; } } /** * Allow an auto-optimization locally. * How to use it: * Imagify_Auto_Optimization::prevent_optimization( $attachment_id ); * wp_update_attachment_metadata( $attachment_id ); * Imagify_Auto_Optimization::allow_optimization( $attachment_id ); * * @since 1.8.4 * @since 1.9.8 Prevents/Allows can stack. * * @param int $attachment_id Current attachment ID. */ public static function allow_optimization( $attachment_id ) { if ( ! isset( self::$prevented[ $attachment_id ] ) ) { return; } --self::$prevented[ $attachment_id ]; if ( self::$prevented[ $attachment_id ] <= 0 ) { unset( self::$prevented[ $attachment_id ] ); } } /** * Tell if an auto-optimization is prevented locally. * * @since 1.8.4 * * @param int $attachment_id Current attachment ID. * @return bool */ public static function is_optimization_prevented( $attachment_id ) { return ! empty( self::$prevented[ $attachment_id ] ) || ! empty( self::$prevented_internally[ $attachment_id ] ); } /** * Prevent an auto-optimization internally. * * @since 1.9.8 * * @param int $attachment_id Current attachment ID. */ protected static function prevent_optimization_internally( $attachment_id ) { self::$prevented_internally[ $attachment_id ] = 1; } /** * Allow an auto-optimization internally. * * @since 1.9.8 * * @param int $attachment_id Current attachment ID. */ protected static function allow_optimization_internally( $attachment_id ) { unset( self::$prevented_internally[ $attachment_id ] ); } } classes/class-imagify.php 0000644 00000043450 15174671745 0011471 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * Imagify.io API for WordPress. */ class Imagify { use InstanceGetterTrait; /** * The Imagify API endpoint. * * @var string */ const API_ENDPOINT = IMAGIFY_APP_API_URL; /** * The Imagify API key. * * @var string */ private $api_key = ''; /** * Random key used to store the API key in the request args. * * @var string */ private $secure_key = ''; /** * HTTP headers. Each http call must fill it (even if it's with an empty array). * * @var array */ private $headers = []; /** * All (default) HTTP headers. They must not be modified once the class is instanciated, or it will affect any following HTTP calls. * * @var array */ private $all_headers = []; /** * Filesystem object. * * @var object Imagify_Filesystem * @since 1.7.1 * @author Grégory Viguier */ protected $filesystem; /** * Use data fetched from the API. * * @var \stdClass|\WP_Error * @since 1.9.9 * @author Grégory Viguier */ protected static $user; /** * The constructor. */ protected function __construct() { if ( ! class_exists( 'Imagify_Filesystem' ) ) { // Dirty patch used when updating from 1.7. include_once IMAGIFY_PATH . 'inc/classes/class-imagify-filesystem.php'; } $this->api_key = get_imagify_option( 'api_key' ); $this->secure_key = $this->generate_secure_key(); $this->filesystem = Imagify_Filesystem::get_instance(); $this->all_headers['Accept'] = 'Accept: application/json'; $this->all_headers['Content-Type'] = 'Content-Type: application/json'; $this->all_headers['Authorization'] = 'Authorization: token ' . $this->api_key; } /** * Get your Imagify account infos. * * @since 1.6.5 * * @return object */ public function get_user() { if ( empty( $this->api_key ) ) { return new WP_Error( 'api_key_missing', __( 'API key required.', 'imagify' ) ); } global $wp_current_filter; if ( isset( static::$user ) ) { return static::$user; } if ( in_array( 'upgrader_post_install', (array) $wp_current_filter, true ) ) { // Dirty patch used when updating from 1.7. static::$user = new WP_Error(); return static::$user; } $this->headers = $this->all_headers; static::$user = $this->http_call( 'users/me/', [ 'timeout' => 10 ] ); if ( is_wp_error( static::$user ) ) { return static::$user; } $maybe_missing = [ 'account_type' => 'free', 'quota' => 0, 'extra_quota' => 0, 'extra_quota_consumed' => 0, 'consumed_current_month_quota' => 0, ]; foreach ( $maybe_missing as $name => $value ) { if ( ! isset( static::$user->$name ) ) { static::$user->$name = $value; } } return static::$user; } /** * Create a user on your Imagify account. * * @since 1.6.5 * * @param array $data All user data. * @return object */ public function create_user( $data ) { $this->headers = []; $data = array_merge( $data, [ 'from_plugin' => true, 'partner' => imagify_get_partner(), ] ); if ( ! $data['partner'] ) { unset( $data['partner'] ); } $response = $this->http_call( 'users/', [ 'method' => 'POST', 'post_data' => $data, ] ); if ( ! is_wp_error( $response ) && isset( $data['partner'] ) ) { imagify_delete_partner(); } return $response; } /** * Update an existing user on your Imagify account. * * @since 1.6.5 * * @param string $data All user data. * @return object */ public function update_user( $data ) { $this->headers = $this->all_headers; return $this->http_call( 'users/me/', [ 'method' => 'PUT', 'post_data' => $data, 'timeout' => 10, ] ); } /** * Check your Imagify API key status. * * @since 1.6.5 * * @param string $data The license key. * @return object */ public function get_status( $data ) { static $status = []; if ( isset( $status[ $data ] ) ) { return $status[ $data ]; } $this->headers = [ 'Authorization' => 'Authorization: token ' . $data, ]; $uri = 'status/'; $partner = imagify_get_partner(); if ( $partner ) { $uri .= '?partner=' . $partner; } $status[ $data ] = $this->http_call( $uri, [ 'timeout' => 10 ] ); return $status[ $data ]; } /** * Get the Imagify API version. * * @since 1.6.5 * * @return object */ public function get_api_version() { static $api_version; if ( ! isset( $api_version ) ) { $this->headers = [ 'Authorization' => $this->all_headers['Authorization'], ]; $api_version = $this->http_call( 'version/', [ 'timeout' => 5 ] ); } return $api_version; } /** * Get Public Info. * * @since 1.6.5 * * @return object */ public function get_public_info() { $this->headers = $this->all_headers; return $this->http_call( 'public-info' ); } /** * Optimize an image from its binary content. * * @since 1.6.5 * @since 1.6.7 $data['image'] can contain the file path (prefered) or the result of `curl_file_create()`. * * @param string $data All options. * @return object */ public function upload_image( $data ) { $this->headers = [ 'Authorization' => $this->all_headers['Authorization'], ]; return $this->http_call( 'upload/', [ 'method' => 'POST', 'post_data' => $data, ] ); } /** * Optimize an image from its URL. * * @since 1.6.5 * * @param string $data All options. Details here: --. * @return object */ public function fetch_image( $data ) { $this->headers = $this->all_headers; return $this->http_call( 'fetch/', [ 'method' => 'POST', 'post_data' => wp_json_encode( $data ), ] ); } /** * Get prices for plans. * * @since 1.6.5 * * @return object */ public function get_plans_prices() { $this->headers = $this->all_headers; return $this->http_call( 'pricing/plan/' ); } /** * Get all prices (Plans included). * * @since 1.6.5 * * @return object */ public function get_all_prices() { $this->headers = $this->all_headers; return $this->http_call( 'pricing/all/' ); } /** * Get coupon code data. * * @since 1.6.5 * * @param string $coupon A coupon code. * @return object */ public function check_coupon_code( $coupon ) { $this->headers = $this->all_headers; return $this->http_call( 'coupons/' . $coupon . '/' ); } /** * Get information about current discount. * * @since 1.6.5 * * @return object */ public function check_discount() { $this->headers = $this->all_headers; return $this->http_call( 'pricing/discount/' ); } /** * Make an HTTP call using curl. * * @since 1.6.5 * @since 1.6.7 Use `wp_remote_request()` when possible (when we don't need to send an image). * * @param string $url The URL to call. * @param array $args The request args. * @return object */ private function http_call( $url, $args = [] ) { $args = array_merge( [ 'method' => 'GET', 'post_data' => null, 'timeout' => 45, ], $args ); $endpoint = trim( $url, '/' ); /** * Filter the timeout value for any request to the API. * * @since 1.6.7 * @author Grégory Viguier * * @param int $timeout Timeout value in seconds. * @param string $endpoint The targetted endpoint. It's basically URI without heading nor trailing slash. */ $args['timeout'] = apply_filters( 'imagify_api_http_request_timeout', $args['timeout'], $endpoint ); // We need to send an image: we must use cURL directly. if ( isset( $args['post_data']['image'] ) ) { return $this->curl_http_call( $url, $args ); } $args = array_merge( [ 'headers' => [], 'body' => $args['post_data'], 'sslverify' => apply_filters( 'https_ssl_verify', false ), ], $args ); unset( $args['post_data'] ); if ( $this->headers ) { foreach ( $this->headers as $name => $value ) { $value = explode( ':', $value, 2 ); $value = end( $value ); $args['headers'][ $name ] = trim( $value ); } } if ( ! empty( $args['headers']['Authorization'] ) ) { // Make sure our API has not overwritten by some other plugin. $args[ $this->secure_key ] = preg_replace( '/^token /', '', $args['headers']['Authorization'] ); if ( ! has_filter( 'http_request_args', [ $this, 'force_api_key_header' ] ) ) { add_filter( 'http_request_args', [ $this, 'force_api_key_header' ], IMAGIFY_INT_MAX + 25, 2 ); } } $response = wp_remote_request( self::API_ENDPOINT . $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $http_code = wp_remote_retrieve_response_code( $response ); $response = wp_remote_retrieve_body( $response ); return $this->handle_response( $response, $http_code ); } /** * Make an HTTP call using curl. * * @since 1.6.7 * @throws Exception When curl_init() fails. * @author Grégory Viguier * * @param string $url The URL to call. * @param array $args The request arguments. * @return object */ private function curl_http_call( $url, $args = [] ) { // Check if curl is available. if ( ! Imagify_Requirements::supports_curl() ) { return new WP_Error( 'curl', 'cURL isn\'t installed on the server.' ); } /** * Allows to mock Imagify calls to the API. * * @param stdClass|null $response Response from the call. * @param string $url URL from the call. * @param array $args Arguments from the call. */ $response = apply_filters( 'pre_imagify_request', null, $url, $args ); if ( $response ) { return $response; } try { $url = self::API_ENDPOINT . $url; $ch = curl_init(); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init if ( false === $ch ) { throw new Exception( 'Could not initialize a new cURL handle' ); } if ( isset( $args['post_data']['image'] ) && is_string( $args['post_data']['image'] ) && $this->filesystem->exists( $args['post_data']['image'] ) ) { $args['post_data']['image'] = curl_file_create( $args['post_data']['image'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_file_create } // Handle proxies. $proxy = new WP_HTTP_Proxy(); if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt if ( $proxy->use_authentication() ) { curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt } } if ( 'POST' === $args['method'] ) { curl_setopt( $ch, CURLOPT_POST, true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_POSTFIELDS, $args['post_data'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt } elseif ( 'PUT' === $args['method'] ) { curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'PUT' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_POSTFIELDS, $args['post_data'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt } if ( defined( 'CURLOPT_PROTOCOLS' ) ) { curl_setopt( $ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt } $user_agent = apply_filters( 'http_headers_useragent', 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ), $url ); curl_setopt( $ch, CURLOPT_URL, $url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_HEADER, false ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_HTTPHEADER, $this->headers ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_TIMEOUT, $args['timeout'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $args['timeout'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_USERAGENT, $user_agent ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt @curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, false ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt /** * Tell which http version to use with cURL during image optimization. * * @since 1.8.4.1 * @since 1.9.9 Default value is `false`. * @author Grégory Viguier * * @param $use_version_1_0 bool True to use version 1.0. False for 1.1. Default is false. */ if ( apply_filters( 'imagify_curl_http_version_1_0', false ) ) { curl_setopt( $ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt } else { curl_setopt( $ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt } $response = curl_exec( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec $error = curl_error( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_error $http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo if ( is_resource( $ch ) ) { curl_close( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close } else { unset( $ch ); } } catch ( Exception $e ) { $args['headers'] = $this->headers; /** * Fires after a failed curl request. * * @since 1.6.9 * @author Grégory Viguier * * @param string $url The requested URL. * @param array $args The request arguments. * @param object $e The raised Exception. */ do_action( 'imagify_curl_http_response', $url, $args, $e ); return new WP_Error( 'curl', 'An error occurred (' . $e->getMessage() . ')' ); } $args['headers'] = $this->headers; /** * Fires after a successful curl request. * * @since 1.6.9 * @author Grégory Viguier * * @param string $url The requested URL. * @param array $args The request arguments. * @param string $response The request response. * @param int $http_code The request HTTP code. * @param string $error An error message. */ do_action( 'imagify_curl_http_response', $url, $args, $response, $http_code, $error ); return $this->handle_response( $response, $http_code, $error ); } /** * Handle the request response and maybe trigger an error. * * @since 1.6.7 * @author Grégory Viguier * * @param string $response The request response. * @param int $http_code The request HTTP code. * @param string $error An error message. * @return object */ private function handle_response( $response, $http_code, $error = '' ) { $response = json_decode( $response ); if ( 401 === $http_code ) { // Reset the API validity cache if the API key is not valid. Imagify_Requirements::reset_cache( 'api_key_valid' ); } if ( 200 !== $http_code && ! empty( $response->code ) ) { if ( ! empty( $response->detail ) ) { return new WP_Error( 'error ' . $http_code, $response->detail ); } if ( ! empty( $response->image ) ) { $error = (array) $response->image; $error = reset( $error ); return new WP_Error( 'error ' . $http_code, $error ); } } if ( 413 === $http_code ) { return new WP_Error( 'error ' . $http_code, 'Your image is too big to be uploaded on our server.' ); } if ( 200 !== $http_code ) { $error = trim( (string) $error ); $error = '' !== $error ? ' - ' . htmlentities( $error ) : ''; return new WP_Error( 'error ' . $http_code, "Our server returned an error ({$http_code}{$error})" ); } if ( ! is_object( $response ) ) { return new WP_Error( 'invalid response', 'Our server returned an invalid response.', $response ); } return $response; } /** * Generate a random key. * Similar to wp_generate_password() but without filter. * * @since 1.8.4 * @see wp_generate_password() * @author Grégory Viguier * * @return string */ private function generate_secure_key() { $length = wp_rand( 12, 20 ); $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|'; $password = ''; for ( $i = 0; $i < $length; $i++ ) { $password .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 ); } return $password; } /** * Filter the arguments used in an HTTP request, to make sure our API key has not been overwritten by some other plugin. * * @since 1.8.4 * @author Grégory Viguier * * @param array $args An array of HTTP request arguments. * @param string $url The request URL. * @return array */ public function force_api_key_header( $args, $url ) { if ( strpos( $url, self::API_ENDPOINT ) === false ) { return $args; } if ( ! empty( $args['headers']['Authorization'] ) || ! empty( $args[ $this->secure_key ] ) ) { if ( ! empty( $args[ $this->secure_key ] ) ) { $args['headers']['Authorization'] = 'token ' . $args[ $this->secure_key ]; } else { $args['headers']['Authorization'] = 'token ' . $this->api_key; } } return $args; } } classes/class-imagify-custom-folders.php 0000644 00000126202 15174671745 0014432 0 ustar 00 <?php /** * Class that regroups things about "custom folders". * * @since 1.7 * @author Grégory Viguier */ class Imagify_Custom_Folders { /** * Class version. * * @var string */ const VERSION = '1.1'; /** ----------------------------------------------------------------------------------------- */ /** BACKUP FOLDER =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the path to the backups directory (custom folders). * * @since 1.7 * @access public * @author Grégory Viguier * * @return string Path to the backups directory. */ public static function get_backup_dir_path() { static $backup_dir; if ( isset( $backup_dir ) ) { return $backup_dir; } $filesystem = imagify_get_filesystem(); $backup_dir = $filesystem->get_site_root() . 'imagify-backup/'; /** * Filter the backup directory path (custom folders). * * @since 1.7 * @author Grégory Viguier * * @param string $backup_dir The backup directory path. */ $backup_dir = apply_filters( 'imagify_files_backup_directory', $backup_dir ); $backup_dir = $filesystem->normalize_dir_path( $backup_dir ); return $backup_dir; } /** * Tell if the folder containing the backups is writable (custom folders). * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public static function backup_dir_is_writable() { return imagify_get_filesystem()->make_dir( self::get_backup_dir_path() ); } /** * Get the backup path of a specific file (custom folders). * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $file_path The file path. * @return string|bool The backup path. False on failure. */ public static function get_file_backup_path( $file_path ) { $file_path = wp_normalize_path( (string) $file_path ); $site_root = imagify_get_filesystem()->get_site_root(); $backup_dir = self::get_backup_dir_path(); if ( ! $file_path ) { return false; } return preg_replace( '@^' . preg_quote( $site_root, '@' ) . '@', $backup_dir, $file_path ); } /** * Add index.php files recursively to a given directory and all its subdirectories. * * @since 1.9.11 * * @param string $backup_dir (optional) Path to the directory where we will start adding indexes. * Defaults to custom-folders backup dir. * * @return void */ public static function add_indexes( $backup_dir = '' ) { $filesystem = Imagify_Filesystem::get_instance(); if ( empty( $backup_dir ) ) { $backup_dir = self::get_backup_dir_path(); } if ( ! $filesystem->is_writable( $backup_dir ) ) { return; } try { $directory = new RecursiveDirectoryIterator( $backup_dir ); $iterator = new RecursiveIteratorIterator( $directory ); foreach ( $iterator as $fileinfo ) { if ( '.' !== $fileinfo->getFilename() ) { continue; } $path = trailingslashit( $fileinfo->getRealPath() ); if ( ! $filesystem->is_file( $path . 'index.html' ) && ! $filesystem->is_file( $path . 'index.php' ) ) { $filesystem->touch( $path . 'index.php' ); } } } catch ( Exception $e ) { return; } } /** ----------------------------------------------------------------------------------------- */ /** SINGLE FILE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Insert a file into the DB. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $args An array of arguments to pass to Imagify_Files_DB::insert(). Required values are 'folder_id' and ( 'path' or 'file_path'). * @return int The file ID on success. 0 on failure. */ public static function insert_file( $args = [] ) { if ( empty( $args['folder_id'] ) ) { return 0; } if ( empty( $args['path'] ) ) { if ( empty( $args['file_path'] ) ) { return 0; } $args['path'] = Imagify_Files_Scan::add_placeholder( $args['file_path'] ); } if ( empty( $args['file_path'] ) ) { $args['file_path'] = Imagify_Files_Scan::remove_placeholder( $args['path'] ); } $filesystem = imagify_get_filesystem(); if ( ! $filesystem->is_readable( $args['file_path'] ) ) { return 0; } if ( empty( $args['file_date'] ) || '0000-00-00 00:00:00' === $args['file_date'] ) { $args['file_date'] = $filesystem->get_date( $args['file_path'] ); } if ( empty( $args['mime_type'] ) ) { $args['mime_type'] = $filesystem->get_mime_type( $args['file_path'] ); } if ( ( empty( $args['width'] ) || empty( $args['height'] ) ) && strpos( $args['mime_type'], 'image/' ) === 0 ) { $file_size = $filesystem->get_image_size( $args['file_path'] ); $args['width'] = $file_size ? $file_size['width'] : 0; $args['height'] = $file_size ? $file_size['height'] : 0; } if ( empty( $args['hash'] ) ) { $args['hash'] = md5_file( $args['file_path'] ); } if ( empty( $args['original_size'] ) ) { $args['original_size'] = (int) $filesystem->size( $args['file_path'] ); } $files_db = Imagify_Files_DB::get_instance(); $primary_key = $files_db->get_primary_key(); unset( $args[ $primary_key ] ); return $files_db->insert( $args ); } /** * Delete a custom file. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $args An array of arguments. * At least: 'file_id'. At best: 'file_id', 'file_path' (or 'path' for the placeholder), and 'backup_path'. */ public static function delete_file( $args = [] ) { $args = array_merge( [ 'file_id' => 0, 'file_path' => '', 'path' => '', 'backup_path' => '', 'process' => false, ], $args ); $filesystem = imagify_get_filesystem(); // Fill the blanks. if ( $args['process'] && $args['process'] instanceof \Imagify\Optimization\Process\ProcessInterface ) { $process = $args['process']; } else { $process = imagify_get_optimization_process( $args['file_id'], 'custom-folders' ); } if ( ! $process->is_valid() ) { // You fucked up! return; } if ( ! $args['file_path'] && $args['path'] ) { $args['file_path'] = Imagify_Files_Scan::remove_placeholder( $args['path'] ); } if ( ! $args['file_path'] && $args['file_id'] ) { $args['file_path'] = $process->get_media()->get_fullsize_path(); } if ( ! $args['backup_path'] && $args['file_path'] ) { $args['backup_path'] = self::get_file_backup_path( $args['file_path'] ); } if ( ! $args['backup_path'] && $args['file_id'] ) { $args['backup_path'] = $process->get_media()->get_raw_backup_path(); } // Trigger a common hook. imagify_trigger_delete_media_hook( $process ); // The file. if ( $args['file_path'] && $filesystem->exists( $args['file_path'] ) ) { $filesystem->delete( $args['file_path'] ); } // The backup file. if ( $args['backup_path'] && $filesystem->exists( $args['backup_path'] ) ) { $filesystem->delete( $args['backup_path'] ); } // WebP. $mime_type = $filesystem->get_mime_type( $args['file_path'] ); $is_image = $mime_type && strpos( $mime_type, 'image/' ) === 0; $webp_path = $is_image ? imagify_path_to_webp( $args['file_path'] ) : false; if ( $webp_path && $filesystem->is_writable( $webp_path ) ) { $filesystem->delete( $webp_path ); } // In the database. $process->get_media()->delete_row(); } /** * Check if a file has been modified, and update the database accordingly. * * @since 1.7 * @access public * @author Grégory Viguier * * @param ProcessInterface $process A \Imagify\Optimization\Process\ProcessInterface object. * @param bool $is_folder_active Tell if the folder is active. * @return int|bool|object The file ID if modified. False if not modified. A WP_Error object if the entry has been removed from the database. * The entry is removed from the database if: * - The file doesn't exist anymore. * - Or if its folder is not active and: the file has been modified, or the file is not optimized by Imagify, or the file is orphan (its folder is not in the database anymore). */ public static function refresh_file( $process, $is_folder_active = null ) { global $wpdb; if ( ! $process->is_valid() ) { return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $filesystem = imagify_get_filesystem(); $media = $process->get_media(); $file_path = $media->get_fullsize_path(); $mime_type = $filesystem->get_mime_type( $file_path ); $is_image = $mime_type && strpos( $mime_type, 'image/' ) === 0; $webp_path = $is_image ? imagify_path_to_webp( $file_path ) : false; $has_webp = $webp_path && $filesystem->is_writable( $webp_path ); $modified = false; if ( ! $file_path || ! $filesystem->exists( $file_path ) ) { /** * The file doesn't exist anymore. */ // Delete the backup file. $process->delete_backup(); // Get the folder ID before removing the row. $folder_id = $media->get_row(); $folder_id = $folder_id['folder_id']; // Remove the entry from the database. $media->delete_row(); // Remove the corresponding folder if inactive and have no files left. self::remove_empty_inactive_folders( $folder_id ); // Delete the WebP version. if ( $has_webp ) { $filesystem->delete( $webp_path ); } return new WP_Error( 'no-file', __( 'The file was missing or its path could not be retrieved from the database. The entry has been deleted from the database.', 'imagify' ) ); } /** * The file still exists. */ $old_data = $media->get_row(); $new_data = []; // Folder ID. if ( $old_data['folder_id'] ) { $folder = wp_cache_get( 'custom_folder_' . $old_data['folder_id'], 'imagify' ); if ( false === $folder ) { // The folder is not in the cache. $folder = Imagify_Folders_DB::get_instance()->get( $old_data['folder_id'] ); $folder = $folder ? $folder : 0; } if ( ! $folder ) { // The folder is not in the database anymore. $old_data['folder_id'] = 0; $new_data['folder_id'] = 0; } } else { $folder = 0; } // Hash + modified. $current_hash = md5_file( $file_path ); if ( ! $old_data['hash'] ) { $new_data['modified'] = 0; } else { $new_data['modified'] = (int) ! hash_equals( $old_data['hash'], $current_hash ); } // The file is modified or is not optimized. if ( $new_data['modified'] || ! $process->get_data()->is_optimized() ) { if ( ! isset( $is_folder_active ) ) { $is_folder_active = $folder && $folder['active']; } // Its folder is not active: remove the entry from the database and delete the backup. if ( ! $is_folder_active ) { // Delete the backup file. $process->delete_backup(); // Remove the entry from the database. $media->delete_row(); // Remove the corresponding folder if inactive and have no files left. if ( $old_data['folder_id'] ) { self::remove_empty_inactive_folders( $old_data['folder_id'] ); } // Delete the WebP version. if ( $has_webp ) { $filesystem->delete( $webp_path ); } return new WP_Error( 'folder-not-active', __( 'The file has been modified or was not optimized: its folder not being selected in the settings, the entry has been deleted from the database.', 'imagify' ) ); } } $new_data['hash'] = $current_hash; // The file is modified. if ( $new_data['modified'] ) { // Delete all optimization data and update file data. $modified = true; $mime_type = ! empty( $old_data['mime_type'] ) ? $old_data['mime_type'] : $filesystem->get_mime_type( $file_path ); if ( $is_image ) { $size = $filesystem->get_image_size( $file_path ); // Delete the WebP version. if ( $has_webp ) { $filesystem->delete( $webp_path ); } } else { $size = false; } $new_data = array_merge( $new_data, [ 'file_date' => $filesystem->get_date( $file_path ), 'width' => $size ? $size['width'] : 0, 'height' => $size ? $size['height'] : 0, 'original_size' => $filesystem->size( $file_path ), 'optimized_size' => null, 'percent' => null, 'optimization_level' => null, 'status' => null, 'error' => null, 'data' => [], ] ); // Delete the backup of the previous file. $process->delete_backup(); } else { // Update file data to make sure nothing is missing. $backup_path = $media->get_backup_path(); $path = $backup_path ? $backup_path : $file_path; $mime_type = ! empty( $old_data['mime_type'] ) ? $old_data['mime_type'] : $filesystem->get_mime_type( $path ); $file_date = ! empty( $old_data['file_date'] ) && '0000-00-00 00:00:00' !== $old_data['file_date'] ? $old_data['file_date'] : $filesystem->get_date( $path ); if ( $is_image ) { $size = $filesystem->get_image_size( $path ); } else { $size = false; } $new_data = array_merge( $new_data, [ 'file_date' => $file_date, 'width' => $size ? $size['width'] : 0, 'height' => $size ? $size['height'] : 0, 'original_size' => $filesystem->size( $path ), ] ); // WebP. $webp_size = 'full' . $process::WEBP_SUFFIX; if ( $has_webp && empty( $old_data['data'][ $webp_size ]['success'] ) ) { $webp_file_size = $filesystem->size( $webp_path ); $old_data['data'][ $webp_size ] = [ 'success' => true, 'original_size' => $new_data['original_size'], 'optimized_size' => $webp_file_size, 'percent' => round( ( ( $new_data['original_size'] - $webp_file_size ) / $new_data['original_size'] ) * 100, 2 ), ]; } } // Save the new data. $old_data = array_intersect_key( $old_data, $new_data ); ksort( $old_data ); ksort( $new_data ); if ( $old_data !== $new_data ) { $media->update_row( $new_data ); } return $modified ? $media->get_id() : false; } /** ----------------------------------------------------------------------------------------- */ /** FOLDERS AND FILES ======================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get folders from the DB. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $args A list of arguments to tell more precisely what to fetch: * - bool $active True to fetch only "active" folders (checked in the settings). False to fetch only folders that are not "active". * @return array An array of arrays containing the following values: * - int $folder_id The folder ID. * - string $path The folder path, with placeholder. * - int $active 1 if the folder should be optimized. 0 otherwize. * - string $folder_path The real absolute folder path. * Example: * Array( * [7] => Array( * [folder_id] => 7 * [path] => {{ROOT}}/custom-path/ * [active] => 1 * [folder_path] => /absolute/path/to/custom-path/ * ) * [13] => Array( * [folder_id] => 13 * [path] => {{CONTENT}}/another-custom-path/ * [active] => 1 * [folder_path] => /absolute/path/to/wp-content/another-custom-path/ * ) * ) */ public static function get_folders( $args = [] ) { global $wpdb; $folders_db = Imagify_Folders_DB::get_instance(); $folders_table = $folders_db->get_table_name(); $primary_key = $folders_db->get_primary_key(); $where_active = ''; if ( isset( $args['active'] ) ) { if ( $args['active'] ) { $args['active'] = true; $where_active = 'WHERE active = 1'; } else { $args['active'] = false; $where_active = 'WHERE active = 0'; } } // Get the folders from the DB. $results = $wpdb->get_results( "SELECT * FROM $folders_table $where_active;", ARRAY_A ); // WPCS: unprepared SQL ok. if ( ! $results || ! is_array( $results ) ) { return []; } // Cast results, add absolute paths. $folders = []; foreach ( $results as $row_fields ) { // Cast the row. $row_fields = $folders_db->cast_row( $row_fields ); // Add the absolute path. $row_fields['folder_path'] = Imagify_Files_Scan::remove_placeholder( $row_fields['path'] ); // Add the row to the list. $folders[ $row_fields[ $primary_key ] ] = $row_fields; } return $folders; } /** * Get files belonging to the given folders. * Files are scanned from the folders, then: * - If a file doesn't exist in the DB, it is added (maybe, depending on arguments provided). * - If a file is in the DB, but with a wrong folder_id, it is fixed. * - If a file doesn't exist, it is removed from the database and its backup is deleted. * * @since 1.7 * @access public * @see Imagify_Custom_Folders::get_folders() * @author Grégory Viguier * * @param array $folders An array of arrays containing at least the keys 'folder_path' and 'active'. See Imagify_Custom_Folders::get_folders() for the format. * @param array $args A list of arguments to tell more precisely what to fetch: * - int $optimization_level If set with an integer, only files that needs to be optimized to this level will be returned (the status is also checked). * - bool $return_only_old_files True to return only files that have not been newly inserted. * - bool $add_inactive_folder_files When true: if a file is not in the database and its folder is not "active", it is added to the DB. Default false: new files are not added to the database if the folder is not active. * @return array A list of files in the following format: * Array( * [_2] => Array( * [file_id] => 2 * [folder_id] => 7 * [path] => {{ROOT}}/custom-path/image-1.jpg * [optimization_level] => null * [status] => null * [file_path] => /absolute/path/to/custom-path/image-1.jpg * ), * [_3] => Array( * [file_id] => 3 * [folder_id] => 7 * [path] => {{ROOT}}/custom-path/image-2.jpg * [optimization_level] => 2 * [status] => success * [file_path] => /absolute/path/to/custom-path/image-2.jpg * ), * [_6] => Array( * [file_id] => 6 * [folder_id] => 13 * [path] => {{CONTENT}}/another-custom-path/image-1.jpg * [optimization_level] => 0 * [status] => error * [file_path] => /absolute/path/to/wp-content/another-custom-path/image-1.jpg * ), * ) * The fields 'optimization_level' and 'status' are set only if the argument 'optimization_level' was set. */ public static function get_files_from_folders( $folders, $args = [] ) { global $wpdb; if ( ! $folders ) { return []; } $filesystem = imagify_get_filesystem(); $files_db = Imagify_Files_DB::get_instance(); $files_table = $files_db->get_table_name(); $files_key = $files_db->get_primary_key(); $files_key_esc = esc_sql( $files_key ); $optimization = isset( $args['optimization_level'] ) && is_numeric( $args['optimization_level'] ); $no_new_files = ! empty( $args['return_only_old_files'] ); $add_inactive_folder_files = ! empty( $args['add_inactive_folder_files'] ); /** * Scan folders for files. $files_from_scan will be in the following format: * Array( * [7] => Array( * [/absolute/path/to/custom-path/image-1.jpg] => 0 * [/absolute/path/to/custom-path/image-2.jpg] => 1 * ) * [13] => Array( * [/absolute/path/to/wp-content/another-custom-path/image-1.jpg] => 0 * [/absolute/path/to/wp-content/another-custom-path/image-2.jpg] => 1 * [/absolute/path/to/wp-content/another-custom-path/image-3.jpg] => 2 * ) * ) */ $files_from_scan = []; foreach ( $folders as $folder_id => $folder ) { $files_from_scan[ $folder_id ] = Imagify_Files_Scan::get_files_from_folder( $folder['folder_path'] ); if ( is_wp_error( $files_from_scan[ $folder_id ] ) ) { unset( $files_from_scan[ $folder_id ] ); } } $files_from_scan = array_map( 'array_flip', $files_from_scan ); /** * Get the files from DB. $files_from_db will be in the same format as the function output. */ $already_optimized = []; $folder_ids = array_keys( $folders ); $files_from_db = array_fill_keys( $folder_ids, [] ); $folder_ids = Imagify_DB::prepare_values_list( $folder_ids ); $select_fields = "$files_key_esc, folder_id, path" . ( $optimization ? ', optimization_level, status' : '' ); if ( $optimization ) { $orderby = " CASE status WHEN 'already_optimized' THEN 3 WHEN 'error' THEN 2 ELSE 1 END ASC, $files_key_esc DESC"; } else { $orderby = "folder_id, $files_key_esc"; } $results = $wpdb->get_results( "SELECT $select_fields FROM $files_table WHERE folder_id IN ( $folder_ids ) ORDER BY $orderby;", ARRAY_A ); // WPCS: unprepared SQL ok. if ( $results ) { $wpdb->flush(); foreach ( $results as $i => $row_fields ) { // Cast the row. $row_fields = $files_db->cast_row( $row_fields ); // Add the absolute path. $row_fields['file_path'] = Imagify_Files_Scan::remove_placeholder( $row_fields['path'] ); // Remove the file from the scan. unset( $files_from_scan[ $row_fields['folder_id'] ][ $row_fields['file_path'] ] ); if ( $optimization ) { if ( 'error' !== $row_fields['status'] && $row_fields['optimization_level'] === $args['optimization_level'] ) { // Try the same level only if the status is an error. continue; } if ( 'already_optimized' === $row_fields['status'] && $row_fields['optimization_level'] >= $args['optimization_level'] ) { // If the image is already compressed, optimize only if the requested level is higher. continue; } if ( 'success' === $row_fields['status'] && $args['optimization_level'] !== $row_fields['optimization_level'] ) { $file_backup_path = self::get_file_backup_path( $row_fields['file_path'] ); if ( ! $file_backup_path || ! $filesystem->exists( $file_backup_path ) ) { // Don't try to re-optimize if there is no backup file. continue; } } } if ( ! $filesystem->exists( $row_fields['file_path'] ) ) { // If the file doesn't exist: remove all traces of it and bail out. self::delete_file( [ 'file_id' => $row_fields[ $files_key ], 'file_path' => $row_fields['file_path'], ] ); continue; } if ( $optimization && 'already_optimized' === $row_fields['status'] ) { $already_optimized[ '_' . $row_fields[ $files_key ] ] = 1; } // Add the row to the list. $files_from_db[ $row_fields['folder_id'] ][ '_' . $row_fields[ $files_key ] ] = $row_fields; } } unset( $results ); $files_from_scan = array_filter( $files_from_scan ); // Make sure files from the scan are not already in the DB with another folder (shouldn't be possible, but, you know...). if ( $files_from_scan ) { $folders_by_placeholder = []; foreach ( $files_from_scan as $folder_id => $folder_files ) { foreach ( $folder_files as $file_path => $i ) { $placeholder = Imagify_Files_Scan::add_placeholder( $file_path ); $folders_by_placeholder[ $placeholder ] = $folder_id; $files_from_scan[ $folder_id ][ $file_path ] = $placeholder; } } $placeholders = Imagify_DB::prepare_values_list( array_keys( $folders_by_placeholder ) ); $select_fields = "$files_key_esc, folder_id, path" . ( $optimization ? ', optimization_level, status' : '' ); $results = $wpdb->get_results( "SELECT $select_fields FROM $files_table WHERE path IN ( $placeholders ) ORDER BY folder_id, $files_key_esc;", ARRAY_A ); // WPCS: unprepared SQL ok. if ( $results ) { // Damn... $wpdb->flush(); foreach ( $results as $i => $row_fields ) { // Cast the row. $row_fields = $files_db->cast_row( $row_fields ); $old_folder_id = $row_fields['folder_id']; // Add the absolute path. $row_fields['file_path'] = Imagify_Files_Scan::remove_placeholder( $row_fields['path'] ); // Set the new folder ID. $row_fields['folder_id'] = $folders_by_placeholder[ $row_fields['path'] ]; // Remove the file from everywhere. unset( $files_from_db[ $old_folder_id ][ '_' . $row_fields[ $files_key ] ], $files_from_scan[ $old_folder_id ][ $row_fields['file_path'] ], $files_from_scan[ $row_fields['folder_id'] ][ $row_fields['file_path'] ] ); if ( $optimization ) { if ( 'error' !== $row_fields['status'] && $row_fields['optimization_level'] === $args['optimization_level'] ) { // Try the same level only if the status is an error. continue; } if ( 'already_optimized' === $row_fields['status'] && $row_fields['optimization_level'] >= $args['optimization_level'] ) { // If the image is already compressed, optimize only if the requested level is higher. continue; } if ( 'success' === $row_fields['status'] && $args['optimization_level'] !== $row_fields['optimization_level'] ) { $file_backup_path = self::get_file_backup_path( $row_fields['file_path'] ); if ( ! $file_backup_path || ! $filesystem->exists( $file_backup_path ) ) { // Don't try to re-optimize if there is no backup file. continue; } } } if ( ! $filesystem->exists( $row_fields['file_path'] ) ) { // If the file doesn't exist: remove all traces of it and bail out. self::delete_file( [ 'file_id' => $row_fields[ $files_key ], 'file_path' => $row_fields['file_path'], ] ); continue; } // Set the correct folder ID in the DB. $success = $files_db->update( $row_fields[ $files_key ], [ 'folder_id' => $row_fields['folder_id'], ] ); if ( $success ) { if ( $optimization && 'already_optimized' === $row_fields['status'] ) { $already_optimized[ '_' . $row_fields[ $files_key ] ] = 1; } $files_from_db[ $row_fields['folder_id'] ][ '_' . $row_fields[ $files_key ] ] = $row_fields; } } } unset( $results, $folders_by_placeholder ); } $files_from_scan = array_filter( $files_from_scan ); // Insert the remaining files into the DB. if ( $files_from_scan ) { foreach ( $files_from_scan as $folder_id => $placeholders ) { // Don't add the file to the DB if its folder is not "active". if ( ! $add_inactive_folder_files && empty( $folders[ $folder_id ]['active'] ) ) { unset( $files_from_scan[ $folder_id ] ); continue; } foreach ( $placeholders as $file_path => $placeholder ) { $file_id = self::insert_file( [ 'folder_id' => $folder_id, 'path' => $placeholder, 'file_path' => $file_path, ] ); if ( $file_id && ! $no_new_files ) { $files_from_db[ $folder_id ][ '_' . $file_id ] = [ 'file_id' => $file_id, 'folder_id' => $folder_id, 'path' => $placeholder, 'optimization_level' => null, 'status' => null, 'file_path' => $file_path, ]; } } unset( $files_from_scan[ $folder_id ] ); } } $files_from_db = array_filter( $files_from_db ); if ( ! $files_from_db ) { return []; } $files_from_db = call_user_func_array( 'array_merge', array_values( $files_from_db ) ); if ( $already_optimized ) { // Put the files already optimized at the end of the list. $already_optimized = array_intersect_key( $files_from_db, $already_optimized ); $files_from_db = array_diff_key( $files_from_db, $already_optimized ); $files_from_db = array_merge( $files_from_db, $already_optimized ); } return $files_from_db; } /** * Check if files inside the given folders have been modified, and update the database accordingly. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $folders A list of folders. See Imagify_Custom_Folders::get_folders() for the format. */ public static function synchronize_files_from_folders( $folders ) { global $wpdb; /** * Get the files from DB, and from the folder. */ $files = self::get_files_from_folders( $folders, [ 'return_only_old_files' => true, ] ); if ( ! $files ) { // This folder doesn't have (new) images. return; } $files_db = Imagify_Files_DB::get_instance(); $files_table = $files_db->get_table_name(); $files_key = $files_db->get_primary_key(); $files_key_esc = esc_sql( $files_key ); $file_ids = wp_list_pluck( $files, $files_key ); $file_ids = Imagify_DB::prepare_values_list( $file_ids ); $results = $wpdb->get_results( "SELECT * FROM $files_table WHERE $files_key IN ( $file_ids ) ORDER BY $files_key_esc;", ARRAY_A ); // WPCS: unprepared SQL ok. if ( ! $results ) { return; } // Caching the folders will prevent unecessary SQL queries in Imagify_Custom_Folders::refresh_file(). foreach ( $folders as $folder_id => $folder ) { wp_cache_set( 'custom_folder_' . $folder_id, $folder, 'imagify' ); } // Finally, refresh the files data. foreach ( $results as $file ) { $file = $files_db->cast_row( $file ); $folder_id = $file['folder_id']; $process = imagify_get_optimization_process( $file, 'custom-folders' ); self::refresh_file( $process, $folders[ $folder_id ]['active'] ); } foreach ( $folders as $folder_id => $folder ) { wp_cache_delete( 'custom_folder_' . $folder_id, 'imagify' ); } } /** ----------------------------------------------------------------------------------------- */ /** WHEN SAVING SELECTED FOLDERS ============================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Dectivate all active folders. * * @since 1.7 * @access public * @author Grégory Viguier */ public static function deactivate_all_folders() { self::deactivate_not_selected_folders(); } /** * Dectivate folders that are not selected. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array|object|string $selected_paths A list of "placeholdered" paths corresponding to the selected folders. */ public static function deactivate_not_selected_folders( $selected_paths = [] ) { global $wpdb; $folders_table = Imagify_Folders_DB::get_instance()->get_table_name(); if ( $selected_paths ) { if ( is_array( $selected_paths ) || is_object( $selected_paths ) ) { $selected_paths = Imagify_DB::prepare_values_list( $selected_paths ); } $selected_paths_clause = "AND path NOT IN ( $selected_paths )"; } else { $selected_paths_clause = ''; } // Remove the active status from the folders that are not selected. $wpdb->query( "UPDATE $folders_table SET active = 0 WHERE active != 0 $selected_paths_clause" ); // WPCS: unprepared SQL ok. } /** * Activate folders that are selected. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array|object $selected_paths A list of "placeholdered" paths corresponding to the selected folders. * @return array An array of paths of folders that are not in the DB. */ public static function activate_selected_folders( $selected_paths ) { global $wpdb; if ( ! $selected_paths ) { return $selected_paths; } $folders_db = Imagify_Folders_DB::get_instance(); $folders_table = $folders_db->get_table_name(); $folders_key = $folders_db->get_primary_key(); $selected_paths = (array) $selected_paths; $selected_in = Imagify_DB::prepare_values_list( $selected_paths ); // Get folders that already are in the DB. $folders = $wpdb->get_results( "SELECT * FROM $folders_table WHERE path IN ( $selected_in );", ARRAY_A ); // WPCS: unprepared SQL ok. if ( ! $folders ) { return $selected_paths; } $selected_paths = array_flip( $selected_paths ); foreach ( $folders as $folder ) { $folder = $folders_db->cast_row( $folder ); if ( Imagify_Files_Scan::placeholder_path_exists( $folder['path'] ) ) { if ( ! $folder['active'] ) { // Add the active status only if not already set and if the folder exists. $folders_db->update( $folder[ $folders_key ], [ 'active' => 1, ] ); } } else { // Remove the active status if the folder does not exist. $folders_db->update( $folder[ $folders_key ], [ 'active' => 0, ] ); } // Remove the path from the selected list, so the remaining will be created. unset( $selected_paths[ $folder['path'] ] ); } // Paths of folders that are not in the DB. return array_flip( $selected_paths ); } /** * Insert folders into the database. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $folders An array of "placeholdered" paths. * @return array An array of folder IDs. */ public static function insert_folders( $folders ) { if ( ! $folders ) { return []; } $folder_ids = []; $filesystem = imagify_get_filesystem(); $folders_db = Imagify_Folders_DB::get_instance(); foreach ( $folders as $placeholder ) { $full_path = Imagify_Files_Scan::remove_placeholder( $placeholder ); $full_path = realpath( $full_path ); if ( ! $full_path || ! $filesystem->is_readable( $full_path ) || ! $filesystem->is_dir( $full_path ) ) { continue; } if ( Imagify_Files_Scan::is_path_forbidden( trailingslashit( $full_path ) ) ) { continue; } $folder_ids[] = $folders_db->insert( [ 'path' => $placeholder, 'active' => 1, ] ); } return array_filter( $folder_ids ); } /** * Remove files that are in inactive folders and are not optimized. * * @since 1.7 * @access public * @author Grégory Viguier */ public static function remove_unoptimized_files_from_inactive_folders() { global $wpdb; $folders_db = Imagify_Folders_DB::get_instance(); $folders_key = $folders_db->get_primary_key(); $files_table = Imagify_Files_DB::get_instance()->get_table_name(); $folder_ids = $folders_db->get_active_folders_column( $folders_key ); if ( $folder_ids ) { $folder_ids = Imagify_DB::prepare_values_list( $folder_ids ); $wpdb->query( "DELETE FROM $files_table WHERE folder_id NOT IN ( $folder_ids ) AND ( status != 'success' OR status IS NULL )" ); // WPCS: unprepared SQL ok. } else { $wpdb->query( "DELETE FROM $files_table WHERE status != 'success' OR status IS NULL" ); // WPCS: unprepared SQL ok. } } /** * Reassign inactive files to active folders. * Example: * - Consider the file "/a/b/c/d/file.png". * - The folder "/a/b/c/", previously active, becomes inactive. * - The folder "/a/b/", previously inactive, becomes active. * - The file is reassigned to the folder "/a/b/". * * @since 1.7 * @access public * @author Grégory Viguier */ public static function reassign_inactive_files() { global $wpdb; $folders_db = Imagify_Folders_DB::get_instance(); $folders_table = $folders_db->get_table_name(); $folders_key = $folders_db->get_primary_key(); $folders_key_esc = esc_sql( $folders_key ); $files_db = Imagify_Files_DB::get_instance(); $files_table = $files_db->get_table_name(); $files_key = $files_db->get_primary_key(); $files_key_esc = esc_sql( $files_key ); // All active folders. $active_folders = $wpdb->get_results( "SELECT $folders_key_esc, path FROM $folders_table WHERE active = 1;", ARRAY_A ); // WPCS: unprepared SQL ok. if ( ! $active_folders ) { return; } $active_folder_ids = []; $has_site_root = false; foreach ( $active_folders as $i => $active_folder ) { $active_folders[ $i ] = $folders_db->cast_row( $active_folder ); $active_folder_ids[] = $active_folders[ $i ][ $folders_key ]; if ( '{{ROOT}}/' === $active_folders[ $i ]['path'] ) { $has_site_root = true; break; } } // Files not in active folders. $active_folder_ids = Imagify_DB::prepare_values_list( $active_folder_ids ); $inactive_files = $wpdb->get_results( "SELECT $files_key_esc, path FROM $files_table WHERE folder_id NOT IN ( $active_folder_ids )", ARRAY_A ); // WPCS: unprepared SQL ok. if ( ! $inactive_files ) { return; } $filesystem = imagify_get_filesystem(); $file_ids_by_folder = []; $active_folders = self::sort_folders( $active_folders, true ); foreach ( $inactive_files as $inactive_file ) { $inactive_file = $files_db->cast_row( $inactive_file ); $inactive_file['full_path'] = Imagify_Files_Scan::remove_placeholder( $inactive_file['path'] ); if ( $has_site_root ) { $inactive_file['dirname'] = $filesystem->dir_path( $inactive_file['full_path'] ); } foreach ( $active_folders as $active_folder ) { $folder_id = $active_folder[ $folders_key ]; if ( strpos( $inactive_file['full_path'], $active_folder['full_path'] ) !== 0 ) { // The file is not in this folder. continue; } if ( ! isset( $file_ids_by_folder[ $folder_id ] ) ) { $file_ids_by_folder[ $folder_id ] = []; } if ( '{{ROOT}}/' === $active_folder['path'] ) { // For the site's root: only direct childs. if ( $inactive_file['dirname'] === $active_folder['full_path'] ) { // This file is in the site's root folder. $file_ids_by_folder[ $folder_id ][] = $inactive_file[ $files_key ]; } break; } // This file is not in the site's root, but still a grand-child of this folder. $file_ids_by_folder[ $folder_id ][] = $inactive_file[ $files_key ]; break; } } $file_ids_by_folder = array_filter( $file_ids_by_folder ); if ( ! $file_ids_by_folder ) { return; } // Set the new folder ID. foreach ( $file_ids_by_folder as $folder_id => $file_ids ) { $file_ids = Imagify_DB::prepare_values_list( $file_ids ); $wpdb->query( "UPDATE $files_table SET folder_id = $folder_id WHERE $files_key_esc IN ( $file_ids )" ); // WPCS: unprepared SQL ok. } } /** * Remove the given folders from the DB if they are inactive and have no files. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $folder_ids An array of folder IDs. * @return int Number of removed folders. */ public static function remove_empty_inactive_folders( $folder_ids = null ) { global $wpdb; $folders_db = Imagify_Folders_DB::get_instance(); $folders_table = $folders_db->get_table_name(); $folders_key = $folders_db->get_primary_key(); $folders_key_esc = esc_sql( $folders_key ); $files_table = Imagify_Files_DB::get_instance()->get_table_name(); $folder_ids = array_filter( (array) $folder_ids ); if ( $folder_ids ) { $folder_ids = $folders_db->cast_col( $folder_ids, $folders_key ); $folder_ids = Imagify_DB::prepare_values_list( $folder_ids ); $in_clause = "folders.$folders_key_esc IN ( $folder_ids )"; } else { $in_clause = '1=1'; } // Within the range of given folder IDs, filter the ones that are inactive and have no files. $results = $wpdb->get_col( // WPCS: unprepared SQL ok. " SELECT folders.$folders_key_esc FROM $folders_table AS folders LEFT JOIN $files_table AS files ON folders.$folders_key_esc = files.folder_id WHERE $in_clause AND folders.active != 1 AND files.folder_id IS NULL" ); if ( ! $results ) { return 0; } $results = $folders_db->cast_col( $results, $folders_key ); $results = Imagify_DB::prepare_values_list( $results ); // Remove inactive folders with no files. $wpdb->query( "DELETE FROM $folders_table WHERE $folders_key_esc IN ( $results )" ); // WPCS: unprepared SQL ok. return (int) $wpdb->rows_affected; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Sort folders by full path. * The row "full_path" is added to each folder. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $folders An array of folders with at least a "path" row. * @param bool $reverse Reverse the order. * @return array */ public static function sort_folders( $folders, $reverse = false ) { if ( ! $folders ) { return []; } $keyed_folders = []; $keyed_paths = []; foreach ( $folders as $folder ) { $folder = (array) $folder; $folder['full_path'] = Imagify_Files_Scan::remove_placeholder( $folder['path'] ); $keyed_folders[ $folder['path'] ] = $folder; $keyed_paths[ $folder['path'] ] = $folder['full_path']; } natcasesort( $keyed_paths ); if ( $reverse ) { $keyed_paths = array_reverse( $keyed_paths, true ); } $keyed_folders = array_merge( $keyed_paths, $keyed_folders ); return array_values( $keyed_folders ); } /** * Remove sub-paths: if 'a/b/' and 'a/b/c/' are in the array, we keep only the "parent" 'a/b/'. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $placeholders A list of "placeholdered" paths. * @return array */ public static function remove_sub_paths( $placeholders ) { sort( $placeholders ); foreach ( $placeholders as $i => $placeholder_path ) { if ( '{{ROOT}}/' === $placeholder_path ) { continue; } if ( ! isset( $prev_path ) ) { $prev_path = strtolower( Imagify_Files_Scan::remove_placeholder( $placeholder_path ) ); continue; } $placeholder_path = strtolower( Imagify_Files_Scan::remove_placeholder( $placeholder_path ) ); if ( strpos( $placeholder_path, $prev_path ) === 0 ) { unset( $placeholders[ $i ] ); } else { $prev_path = $placeholder_path; } } return $placeholders; } } classes/class-imagify-files-stats.php 0000644 00000027404 15174671745 0013726 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Class handling stats related to "custom folders optimization". * * @since 1.7 * @author Grégory Viguier */ class Imagify_Files_Stats { /** * Class version. * * @var string * @since 1.7 * @author Grégory Viguier */ const VERSION = '1.0'; /** ----------------------------------------------------------------------------------------- */ /** COUNT FILES ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Count number of images in custom folders. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The number of images. */ public static function count_all_files() { /** * Filter the number of images in custom folders. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_files', false ); if ( false !== $pre_count ) { return (int) $pre_count; } return self::count_files( 'all' ); } /** * Count number of images in custom folders with an error. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The number of images. */ public static function count_error_files() { /** * Filter the number of images in custom folders with an error. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_error_files', false ); if ( false !== $pre_count ) { return (int) $pre_count; } return self::count_files( 'error' ); } /** * Count number of images successfully optimized by Imagify in custom folders. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The number of images. */ public static function count_success_files() { /** * Filter the number of images successfully optimized by Imagify in custom folders. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_success_files', false ); if ( false !== $pre_count ) { return (int) $pre_count; } return self::count_files( 'success' ); } /** * Count number of optimized images in custom folders. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The number of images. */ public static function count_optimized_files() { /** * Filter the number of optimized images in custom folders. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_optimized_files', false ); if ( false !== $pre_count ) { return (int) $pre_count; } return self::count_files( 'optimized' ); } /** * Count number of unoptimized images in custom folders. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The number of images. */ public static function count_unoptimized_files() { /** * Filter the number of unoptimized images in custom folders. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_unoptimized_files', false ); if ( false !== $pre_count ) { return (int) $pre_count; } return self::count_files( 'unoptimized' ); } /** * Count number of images without status in custom folders. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The number of images. */ public static function count_no_status_files() { /** * Filter the number of images without status in custom folders. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_no_status_files', false ); if ( false !== $pre_count ) { return (int) $pre_count; } return self::count_files( 'none' ); } /** * Count number of images in custom folders. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $status The status of these folders: all, success, already_optimized, optimized, error, none, unoptimized. * "none" if for files without status. * "optimized" regroups "success" and "already_optimized". * "unoptimized" regroups "error" and "none". * @return int The number of images. */ public static function count_files( $status = 'all' ) { global $wpdb; static $count = []; $status = self::validate_status( $status ); if ( isset( $count[ $status ] ) ) { return $count[ $status ]; } $files_db = Imagify_Files_DB::get_instance(); if ( ! $files_db->can_operate() ) { $count[ $status ] = 0; return $count[ $status ]; } switch ( $status ) { case 'all': $status = ''; break; case 'none': $status = 'status IS NULL'; break; case 'optimized': $status = "status IN ('success','already_optimized')"; break; case 'unoptimized': $status = "( status = 'error' OR status IS NULL )"; break; default: // success, already_optimized or error. $status = "status = '$status'"; } $table_name = $files_db->get_table_name(); $status = $status ? "WHERE $status" : ''; $count[ $status ] = (int) $wpdb->get_var( // WPCS: unprepared SQL ok. "SELECT COUNT( file_id ) FROM $table_name $status" ); return $count[ $status ]; } /** ----------------------------------------------------------------------------------------- */ /** PERCENTS ================================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Count percent of optimized images in custom folders. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The percent of optimized images. */ public static function percent_optimized_files() { /** * Filter the percent of optimized images in custom folders. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $percent Default is false. Provide an integer. */ $percent = apply_filters( 'imagify_percent_optimized_files', false ); if ( false !== $percent ) { return (int) $percent; } $total_files = self::count_all_files(); $total_optimized_files = self::count_optimized_files(); if ( ! $total_files || ! $total_optimized_files ) { return 0; } return min( round( 100 * $total_optimized_files / $total_files ), 100 ); } /** ----------------------------------------------------------------------------------------- */ /** GET FILE SIZES ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Sum up all optimized sizes of all successfully optimized files. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The sizes sum in bytes. */ public static function get_optimized_size() { /** * Filter the optimized sizes of all successfully optimized files. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $pre_size Default is false. Provide an integer. */ $pre_size = apply_filters( 'imagify_get_optimized_files_size', false ); if ( false !== $pre_size ) { return (int) $pre_size; } return self::get_size( 'optimized' ); } /** * Sum up all original sizes of all successfully optimized files. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The sizes sum in bytes. */ public static function get_original_size() { /** * Filter the original sizes of all successfully optimized files. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $pre_size Default is false. Provide an integer. */ $pre_size = apply_filters( 'imagify_get_original_files_size', false ); if ( false !== $pre_size ) { return (int) $pre_size; } return self::get_size( 'original' ); } /** * Sum up all (optimized|original) sizes of all successfully optimized files. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $type "optimized" or "original". * @return int The sizes sum in bytes. */ public static function get_size( $type = null ) { global $wpdb; static $sizes = []; $type = 'optimized' === $type ? 'optimized_size' : 'original_size'; if ( isset( $sizes[ $type ] ) ) { return $sizes[ $type ]; } $files_db = Imagify_Files_DB::get_instance(); if ( ! $files_db->can_operate() ) { $sizes[ $type ] = 0; return $sizes[ $type ]; } $table_name = $files_db->get_table_name(); $sizes[ $type ] = (int) $wpdb->get_var( // WPCS: unprepared SQL ok. "SELECT SUM( $type ) FROM $table_name WHERE status = 'success'" ); return $sizes[ $type ]; } /** * Sum up all original sizes. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The sizes sum in bytes. */ public static function get_overall_original_size() { global $wpdb; static $size; if ( isset( $size ) ) { return $size; } $files_db = Imagify_Files_DB::get_instance(); if ( ! $files_db->can_operate() ) { $size = 0; return $size; } $table_name = $files_db->get_table_name(); $sql = $wpdb->get_var( "SELECT SUM( original_size ) FROM $table_name" ); // WPCS: unprepared SQL ok. $size = is_null( $sql ) ? 0 : round( $sql ); return $size; } /** * Calculate the average size of the images uploaded per month. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int The current average size of images uploaded per month in bytes. */ public static function calculate_average_size_per_month() { global $wpdb; static $average; if ( isset( $average ) ) { return $average; } $files_db = Imagify_Files_DB::get_instance(); if ( ! $files_db->can_operate() ) { $average = 0; return $average; } $table_name = $files_db->get_table_name(); $sql = $wpdb->get_var( "SELECT AVG( size ) AS average_size_per_month FROM ( SELECT SUM( original_size ) AS size FROM $table_name GROUP BY YEAR( file_date ), MONTH( file_date ) ) AS size_per_month" ); // WPCS: unprepared SQL ok. $average = is_null( $sql ) ? 0 : round( $sql ); return $average; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Validate a status. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $status The status of these folders: all, success, already_optimized, optimized, error, none, unoptimized. * "none" if for files without status. * "optimized" regroups "success" and "already_optimized". * "unoptimized" regroups "error" and "none". * @return string Fallback to 'all' if the status is not valid. */ public static function validate_status( $status = 'all' ) { $statuses = [ 'all' => 1, 'success' => 1, 'already_optimized' => 1, 'error' => 1, 'none' => 1, 'optimized' => 1, 'unoptimized' => 1, ]; return isset( $statuses[ $status ] ) ? $status : 'all'; } } classes/class-imagify-files-scan.php 0000644 00000052115 15174671745 0013511 0 ustar 00 <?php /** * Class handling everything that is related to "custom folders optimization". * * @since 1.7 * @author Grégory Viguier */ class Imagify_Files_Scan { /** * Class version. * * @var string * @since 1.7 * @author Grégory Viguier */ const VERSION = '1.1.1'; /** * Get files (optimizable by Imagify) recursively from a specific folder. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $folder An absolute path to a folder. * @return array|object An array of absolute paths. A WP_Error object on error. */ public static function get_files_from_folder( $folder ) { $filesystem = imagify_get_filesystem(); // Formate and validate the folder path. if ( ! is_string( $folder ) ) { return new WP_Error( 'invalid_folder', __( 'Invalid folder.', 'imagify' ) ); } $folder = realpath( $folder ); if ( ! $folder ) { return new WP_Error( 'folder_not_exists', __( 'This folder does not exist.', 'imagify' ) ); } if ( ! $filesystem->is_dir( $folder ) ) { return new WP_Error( 'not_a_folder', __( 'This file is not a folder.', 'imagify' ) ); } if ( self::is_path_forbidden( trailingslashit( $folder ) ) ) { return new WP_Error( 'folder_forbidden', __( 'This folder is not allowed.', 'imagify' ) ); } // Finally we made all our validations. if ( $filesystem->is_site_root( $folder ) ) { // For the site's root, we don't look in sub-folders. $dir = new DirectoryIterator( $folder ); $dir = new Imagify_Files_Iterator( $dir, false ); $images = []; foreach ( new IteratorIterator( $dir ) as $file ) { $images[] = $file->getPathname(); } return $images; } /** * 4096 stands for FilesystemIterator::SKIP_DOTS, which was introduced in php 5.3.0. * 8192 stands for FilesystemIterator::UNIX_PATHS, which was introduced in php 5.3.0. */ $dir = new RecursiveDirectoryIterator( $folder, 4096 | 8192 ); $dir = new Imagify_Files_Recursive_Iterator( $dir ); $images = new RecursiveIteratorIterator( $dir ); $images = array_keys( iterator_to_array( $images ) ); return $images; } /** ----------------------------------------------------------------------------------------- */ /** FORBIDDEN FOLDERS AND FILES ============================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if a path is autorized. * When testing a folder, the path MUST have a trailing slash. * * @since 1.7.1 * @since 1.8 The path must have a trailing slash if for a folder. * @access public * @author Grégory Viguier * * @param string $file_path A file or folder absolute path. * @return bool */ public static function is_path_autorized( $file_path ) { return ! self::is_path_forbidden( $file_path ); } /** * Tell if a path is forbidden. * When testing a folder, the path MUST have a trailing slash. * * @since 1.7 * @since 1.8 The path must have a trailing slash if for a folder. * @access public * @author Grégory Viguier * * @param string $file_path A file or folder absolute path. * @return bool */ public static function is_path_forbidden( $file_path ) { static $folders; $filesystem = imagify_get_filesystem(); if ( self::is_filename_forbidden( $filesystem->file_name( $file_path ) ) ) { return true; } if ( $filesystem->is_symlinked( $file_path ) ) { // Files outside the site's folder are forbidden. return true; } if ( ! isset( $folders ) ) { $folders = self::get_forbidden_folders(); $folders = array_map( 'strtolower', $folders ); $folders = array_flip( $folders ); } $file_path = self::normalize_path_for_comparison( $file_path ); if ( isset( $folders[ $file_path ] ) ) { return true; } $delim = Imagify_Filesystem::PATTERN_DELIMITER; foreach ( self::get_forbidden_folder_patterns() as $pattern ) { if ( preg_match( $delim . '^' . $pattern . $delim, $file_path ) ) { return true; } } foreach ( $folders as $folder => $i ) { if ( strpos( $file_path, $folder ) === 0 ) { return true; } } return false; } /** * Get the list of folders where Imagify won't look for files to optimize. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array A list of absolute paths. */ public static function get_forbidden_folders() { static $folders; if ( isset( $folders ) ) { return $folders; } $filesystem = imagify_get_filesystem(); $site_root = $filesystem->get_site_root(); $abspath = $filesystem->get_abspath(); $folders = [ // Server. $site_root . 'cgi-bin', // `cgi-bin` // WordPress. $abspath . 'wp-admin', $abspath . WPINC, WP_CONTENT_DIR . '/mu-plugins', // MU plugins. WP_CONTENT_DIR . '/upgrade', // Upgrade. // Plugins. WP_CONTENT_DIR . '/bps-backup', // BulletProof Security. self::get_ewww_tools_path(), // EWWW: /wp-content/ewww. WP_CONTENT_DIR . '/ngg', // NextGen Gallery. WP_CONTENT_DIR . '/ngg_styles', // NextGen Gallery. WP_CONTENT_DIR . '/w3tc-config', // W3 Total Cache. WP_CONTENT_DIR . '/wfcache', // WP Fastest Cache. WP_CONTENT_DIR . '/wp-rocket-config', // WP Rocket. Imagify_Custom_Folders::get_backup_dir_path(), // Imagify "Custom folders" backup: /imagify-backup. IMAGIFY_PATH, // Imagify plugin: /wp-content/plugins/imagify. self::get_shortpixel_path(), // ShortPixel: /wp-content/uploads/ShortpixelBackups. ]; if ( ! is_multisite() ) { $uploads_dir = $filesystem->get_upload_basedir( true ); $ngg_galleries = self::get_ngg_galleries_path(); if ( $ngg_galleries ) { $folders[] = $ngg_galleries; // NextGen Gallery: /wp-content/gallery. } $folders[] = $uploads_dir . 'formidable'; // Formidable Forms: /wp-content/uploads/formidable. $folders[] = get_imagify_backup_dir_path( true ); // Imagify Media Library backup: /wp-content/uploads/backup. $folders[] = self::get_wc_logs_path(); // WooCommerce Logs: /wp-content/uploads/wc-logs. $folders[] = $uploads_dir . 'woocommerce_uploads'; // WooCommerce uploads: /wp-content/uploads/woocommerce_uploads. } $folders = array_map( [ $filesystem, 'normalize_dir_path' ], $folders ); /** * Add folders to the list of forbidden ones. * * @since 1.7 * @author Grégory Viguier * * @param array $added_folders List of absolute paths. * @param array $folders List of folders already forbidden. */ $added_folders = apply_filters( 'imagify_add_forbidden_folders', [], $folders ); $added_folders = array_filter( (array) $added_folders ); $added_folders = array_filter( $added_folders, 'is_string' ); if ( ! $added_folders ) { return $folders; } $added_folders = array_map( [ $filesystem, 'normalize_dir_path' ], $added_folders ); $folders = array_merge( $folders, $added_folders ); $folders = array_flip( array_flip( $folders ) ); return $folders; } /** * Get the list of folder patterns where Imagify won't look for files to optimize. This is meant for paths that are dynamic. * `^` will be prepended to each pattern (aka, the pattern must match an absolute path). * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`. * Paths tested against these patterns are lower-cased. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array A list of regex patterns. */ public static function get_forbidden_folder_patterns() { static $folders; if ( isset( $folders ) ) { return $folders; } $folders = []; // Media Library: /wp\-content/uploads/(sites/\d+/)?\d{4}/\d{2}/. $folders[] = self::get_media_library_pattern(); if ( is_multisite() ) { /** * On multisite we can't exclude Imagify's library backup folders, or any other folder located in the uploads folders (created by other plugins): there are too many ways it can fail. * Only exception we're aware of so far is NextGen Gallery, because it provides a clear pattern to use. */ $ngg_galleries = self::get_ngg_galleries_multisite_pattern(); if ( $ngg_galleries ) { // NextGen Gallery: /wp\-content/uploads/sites/\d+/nggallery/. $folders[] = $ngg_galleries; } } /** * Add folder patterns to the list of forbidden ones. * Don't forget to use `Imagify_Files_Scan::normalize_path_for_regex( $path )`! * * @since 1.7 * @author Grégory Viguier * * @param array $added_folders List of patterns. * @param array $folders List of patterns already forbidden. */ $added_folders = apply_filters( 'imagify_add_forbidden_folder_patterns', [], $folders ); $added_folders = array_filter( (array) $added_folders ); $added_folders = array_filter( $added_folders, 'is_string' ); if ( ! $added_folders ) { return $folders; } $folders = array_merge( $folders, $added_folders ); $folders = array_flip( array_flip( $folders ) ); return $folders; } /** * Tell if a file/folder name is forbidden. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $file_name A file or folder name. * @return bool */ public static function is_filename_forbidden( $file_name ) { static $file_names; if ( ! isset( $file_names ) ) { $file_names = array_flip( self::get_forbidden_file_names() ); } return isset( $file_names[ strtolower( $file_name ) ] ); } /** * Get the list of file names that Imagify won't optimize. * It can contain folder names. Names are case-lowered. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array A list of file names */ public static function get_forbidden_file_names() { static $file_names; if ( isset( $file_names ) ) { return $file_names; } $file_names = [ '.', '..', '.DS_Store', '.git', '.svn', 'backup', 'backups', 'cache', 'lang', 'langs', 'languages', 'node_modules', 'Thumbs.db', ]; $file_names = array_map( 'strtolower', $file_names ); /** * Add file names to the list of forbidden ones. * * @since 1.7 * @author Grégory Viguier * * @param array $added_file_names List of file names. * @param array $file_names List of file names already forbidden. */ $added_file_names = apply_filters( 'imagify_add_forbidden_file_names', [], $file_names ); if ( ! $added_file_names || ! is_array( $added_file_names ) ) { return $file_names; } $added_file_names = array_filter( $added_file_names, 'is_string' ); $added_file_names = array_map( 'strtolower', $added_file_names ); $file_names = array_merge( $file_names, $added_file_names ); $file_names = array_flip( array_flip( $file_names ) ); return $file_names; } /** ----------------------------------------------------------------------------------------- */ /** PLACEHOLDERS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Add a placeholder to a path. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $file_path An absolute path. * @return string A "placeholdered" path. */ public static function add_placeholder( $file_path ) { $file_path = wp_normalize_path( $file_path ); $locations = self::get_placeholder_paths(); foreach ( $locations as $placeholder => $location_path ) { if ( strpos( $file_path, $location_path ) === 0 ) { return preg_replace( '@^' . preg_quote( $location_path, '@' ) . '@', $placeholder, $file_path ); } } // Should not happen. return $file_path; } /** * Change a path with a placeholder into a real path or URL. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $file_path A path with a placeholder. * @param string $type What to return: 'path' or 'url'. * @return string An absolute path or a URL. */ public static function remove_placeholder( $file_path, $type = 'path' ) { if ( 'path' === $type ) { $locations = self::get_placeholder_paths(); } else { $locations = self::get_placeholder_urls(); } foreach ( $locations as $placeholder => $location_path ) { if ( strpos( $file_path, $placeholder ) === 0 ) { return preg_replace( '@^' . preg_quote( $placeholder, '@' ) . '@', $location_path, $file_path ); } } // Should not happen. return $file_path; } /** * Get array of pairs of placeholder => corresponding path. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public static function get_placeholder_paths() { static $replacements; if ( isset( $replacements ) ) { return $replacements; } $filesystem = imagify_get_filesystem(); $replacements = [ '{{PLUGINS}}/' => WP_PLUGIN_DIR, '{{MU_PLUGINS}}/' => WPMU_PLUGIN_DIR, '{{THEMES}}/' => WP_CONTENT_DIR . '/themes', '{{UPLOADS}}/' => $filesystem->get_main_upload_basedir(), '{{CONTENT}}/' => WP_CONTENT_DIR, '{{ROOT}}/' => $filesystem->get_site_root(), ]; $replacements = array_map( [ $filesystem, 'normalize_dir_path' ], $replacements ); return $replacements; } /** * Get array of pairs of placeholder => corresponding URL. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public static function get_placeholder_urls() { static $replacements; if ( isset( $replacements ) ) { return $replacements; } $filesystem = imagify_get_filesystem(); $replacements = [ '{{PLUGINS}}/' => plugins_url( '/' ), '{{MU_PLUGINS}}/' => plugins_url( '/', WPMU_PLUGIN_DIR . '/.' ), '{{THEMES}}/' => content_url( 'themes/' ), '{{UPLOADS}}/' => $filesystem->get_main_upload_baseurl(), '{{CONTENT}}/' => content_url( '/' ), '{{ROOT}}/' => $filesystem->get_site_root_url(), ]; return $replacements; } /** * A file_exists() for paths with a placeholder. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $file_path The file path. * @return bool */ public static function placeholder_path_exists( $file_path ) { return imagify_get_filesystem()->is_readable( self::remove_placeholder( $file_path ) ); } /** ----------------------------------------------------------------------------------------- */ /** PATHS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the path to NextGen galleries on monosites. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string|bool An absolute path. False if it can't be retrieved. */ public static function get_ngg_galleries_path() { $galleries_path = get_site_option( 'ngg_options' ); if ( empty( $galleries_path['gallerypath'] ) ) { return false; } $filesystem = imagify_get_filesystem(); $galleries_path = $filesystem->normalize_dir_path( $galleries_path['gallerypath'] ); $galleries_path = trim( $galleries_path, '/' ); // Something like `wp-content/gallery`. $ngg_root = defined( 'NGG_GALLERY_ROOT_TYPE' ) ? NGG_GALLERY_ROOT_TYPE : 'site'; if ( $galleries_path && 'content' === $ngg_root ) { $ngg_root = $filesystem->normalize_dir_path( WP_CONTENT_DIR ); $ngg_root = trim( $ngg_root, '/' ); // Something like `abs-path/to/wp-content`. $exploded_root = explode( '/', $ngg_root ); $exploded_galleries = explode( '/', $galleries_path ); $first_gallery_dirname = reset( $exploded_galleries ); $last_root_dirname = end( $exploded_root ); if ( $last_root_dirname === $first_gallery_dirname ) { array_shift( $exploded_galleries ); $galleries_path = implode( '/', $exploded_galleries ); } } if ( 'content' === $ngg_root ) { $ngg_root = $filesystem->normalize_dir_path( WP_CONTENT_DIR ); } else { $ngg_root = $filesystem->get_abspath(); } if ( strpos( $galleries_path, $ngg_root ) !== 0 ) { $galleries_path = $ngg_root . $galleries_path; } return $galleries_path . '/'; } /** * Get the path to WooCommerce logs on monosites. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string An absolute path. */ public static function get_wc_logs_path() { if ( defined( 'WC_LOG_DIR' ) ) { return WC_LOG_DIR; } return imagify_get_filesystem()->get_upload_basedir( true ) . 'wc-logs/'; } /** * Get the path to EWWW optimization tools. * It is the same for all sites on multisite. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string An absolute path. */ public static function get_ewww_tools_path() { if ( defined( 'EWWW_IMAGE_OPTIMIZER_TOOL_PATH' ) ) { return EWWW_IMAGE_OPTIMIZER_TOOL_PATH; } return WP_CONTENT_DIR . '/ewww/'; } /** * Get the path to ShortPixel backup folder. * It is the same for all sites on multisite (and yes, you'll get a surprise if your upload base dir -aka uploads/sites/12/- is not 2 folders deeper than theuploads folder). * * @since 1.8 * @access public * @author Grégory Viguier * * @return string An absolute path. */ public static function get_shortpixel_path() { if ( defined( 'SHORTPIXEL_BACKUP_FOLDER' ) ) { return trailingslashit( SHORTPIXEL_BACKUP_FOLDER ); } $filesystem = imagify_get_filesystem(); $path = $filesystem->get_upload_basedir( true ); $path = is_main_site() ? $path : $filesystem->dir_path( $filesystem->dir_path( $path ) ); return $path . 'ShortpixelBackups/'; } /** ----------------------------------------------------------------------------------------- */ /** REGEX PATTERNS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the regex pattern used to match the paths to the media library. * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`. * Paths tested against these patterns are lower-cased. * * @since 1.8 * @access public * @author Grégory Viguier * * @return string Something like `/wp\-content/uploads/(sites/\d+/)?\d{4}/\d{2}/`. */ public static function get_media_library_pattern() { $filesystem = imagify_get_filesystem(); $uploads_dir = self::normalize_path_for_regex( $filesystem->get_main_upload_basedir() ); if ( ! is_multisite() ) { if ( get_option( 'uploads_use_yearmonth_folders' ) ) { // In year/month folders. return $uploads_dir . '\d{4}/\d{2}/'; } // Not in year/month folders. return $uploads_dir . '[^/]+$'; } $pattern = $filesystem->get_multisite_uploads_subdir_pattern(); if ( get_option( 'uploads_use_yearmonth_folders' ) ) { // In year/month folders. return $uploads_dir . '(' . $pattern . ')?\d{4}/\d{2}/'; } // Not in year/month folders. return $uploads_dir . '(' . $pattern . ')?[^/]+$'; } /** * Get the regex pattern used to match the paths to NextGen galleries on multisite. * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`. * Paths tested against these patterns are lower-cased. * * @since 1.8 * @access public * @author Grégory Viguier * * @return string|bool Something like `/wp-content/uploads/sites/\d+/nggallery/`. False if it can't be retrieved. */ public static function get_ngg_galleries_multisite_pattern() { $galleries_path = self::get_ngg_galleries_path(); // Something like `wp-content/uploads/sites/%BLOG_ID%/nggallery/`. if ( ! $galleries_path ) { return false; } $galleries_path = self::normalize_path_for_regex( $galleries_path ); $galleries_path = str_replace( [ '%blog_name%', '%blog_id%' ], [ '.+', '\d+' ], $galleries_path ); return $galleries_path; } /** ----------------------------------------------------------------------------------------- */ /** NORMALIZATION TOOLS ===================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Normalize a file path, aiming for path comparison. * The path is normalized and case-lowered. * * @since 1.7 * @since 1.8 No trailing slash anymore, because it can be used for files. * @access public * @author Grégory Viguier * * @param string $file_path The file path. * @return string The normalized file path. */ public static function normalize_path_for_comparison( $file_path ) { return strtolower( wp_normalize_path( $file_path ) ); } /** * Normalize a file path, aiming for use in a regex pattern. * The path is normalized, case-lowered, and escaped. * * @since 1.8 * @access public * @author Grégory Viguier * * @param string $file_path The file path. * @return string The normalized file path. */ public static function normalize_path_for_regex( $file_path ) { return preg_quote( imagify_get_filesystem()->normalize_path_for_comparison( $file_path ), Imagify_Filesystem::PATTERN_DELIMITER ); } } classes/class-imagify-abstract-db.php 0000644 00000047462 15174671745 0013664 0 ustar 00 <?php /** * Imagify DB base class. * * @since 1.5 * @source https://gist.github.com/pippinsplugins/e220a7f0f0f2fbe64608 */ abstract class Imagify_Abstract_DB extends Imagify_Abstract_DB_Deprecated implements \Imagify\DB\DBInterface { /** * Class version. * * @var string */ const VERSION = '1.3'; /** * Suffix used in the name of the options that store the table versions. * * @var string * @since 1.7 */ const TABLE_VERSION_OPTION_SUFFIX = '_db_version'; /** * The suffix used in the name of the database table (so, without the wpdb prefix). * * @var string * @since 1.7 * @access protected */ protected $table; /** * The version of our database table. * * @var int * @since 1.5 * @since 1.7 Not public anymore, now an integer. * @access protected */ protected $table_version; /** * Tell if the table is the same for each site of a Multisite. * * @var bool * @since 1.7 * @access protected */ protected $table_is_global; /** * The name of the primary column. * * @var string * @since 1.5 * @since 1.7 Not public anymore. * @access protected */ protected $primary_key; /** * The name of our database table. * * @var string * @since 1.5 * @since 1.7 Not public anymore. * @access protected */ protected $table_name = ''; /** * Tell if the table has been created. * * @var bool * @since 1.7 * @access protected */ protected $table_created = false; /** * Stores the list of columns that must be (un)serialized. * * @var array * @since 1.7 * @access protected */ protected $to_serialize; /** * Get things started. * * @since 1.5 * @access protected */ protected function __construct() { global $wpdb; $prefix = $this->table_is_global ? $wpdb->base_prefix : $wpdb->prefix; $this->table_name = $prefix . $this->table; if ( ! $this->table_is_up_to_date() ) { /** * The option doesn't exist or is not up-to-date: we must upgrade the table before declaring it ready. * See self::maybe_upgrade_table() for the upgrade. */ return; } $this->set_table_ready(); } /** * Init: * - Launch hooks. * * @since 1.7 * @access public * @author Grégory Viguier */ public function init() { add_action( 'admin_init', [ $this, 'maybe_upgrade_table' ] ); } /** * Tell if we can work with the tables. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public function can_operate() { return $this->table_created; } /** ----------------------------------------------------------------------------------------- */ /** TABLE SPECIFICS ========================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the column placeholders. * * @since 1.5 * @access public * * @return array */ abstract public function get_columns(); /** * Default column values. * * @since 1.5 * @access public * * @return array */ abstract public function get_column_defaults(); /** * Get the query to create the table fields. * * @since 1.7 * @access protected * @author Grégory Viguier * * @return string */ abstract protected function get_table_schema(); /** ----------------------------------------------------------------------------------------- */ /** QUERIES ================================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the table is empty or not. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool True if the table contains at least one row. */ public function has_items() { global $wpdb; $column = esc_sql( $this->primary_key ); return (bool) $wpdb->get_var( "SELECT $column FROM $this->table_name LIMIT 1;" ); // WPCS: unprepared SQL ok. } /** * Retrieve a row by the primary key. * * @since 1.5 * @access public * * @param string $row_id A primary key. * @return array */ public function get( $row_id ) { if ( $row_id <= 0 ) { return []; } return $this->get_by( $this->primary_key, $row_id ); } /** * Retrieve a row by a specific column / value. * * @since 1.5 * @access public * * @param string $column_where A column name. * @param mixed $column_value A value. * @return array */ public function get_by( $column_where, $column_value ) { global $wpdb; $placeholder = $this->get_placeholder( $column_where ); $column_where = esc_sql( $column_where ); $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->table_name WHERE $column_where = $placeholder LIMIT 1;", $column_value ), ARRAY_A ); // WPCS: unprepared SQL ok, PreparedSQLPlaceholders replacement count ok. return (array) $this->cast_row( $result ); } /** * Retrieve a row by the specified column / values. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_where A column name. * @param array $column_values An array of values. * @return array */ public function get_in( $column_where, $column_values ) { global $wpdb; $column_where = esc_sql( $column_where ); $column_values = Imagify_DB::prepare_values_list( $column_values ); $result = $wpdb->get_row( "SELECT * FROM $this->table_name WHERE $column_where IN ( $column_values ) LIMIT 1;", ARRAY_A ); // WPCS: unprepared SQL ok. return (array) $this->cast_row( $result ); } /** * Retrieve a var by the primary key. * Previously named get_column(). * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_select A column name. * @param string $row_id A primary key. * @return mixed */ public function get_var( $column_select, $row_id ) { if ( $row_id <= 0 ) { return false; } return $this->get_var_by( $column_select, $this->primary_key, $row_id ); } /** * Retrieve a var by the specified column / value. * Previously named get_column_by(). * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_select A column name. * @param string $column_where A column name. * @param string $column_value A value. * @return mixed */ public function get_var_by( $column_select, $column_where, $column_value ) { global $wpdb; $placeholder = $this->get_placeholder( $column_where ); $column = esc_sql( $column_select ); $column_where = esc_sql( $column_where ); $result = $wpdb->get_var( $wpdb->prepare( "SELECT $column FROM $this->table_name WHERE $column_where = $placeholder LIMIT 1;", $column_value ) ); // WPCS: unprepared SQL ok, PreparedSQLPlaceholders replacement count ok. return $this->cast( $result, $column_select ); } /** * Retrieve a var by the specified column / values. * Previously named get_column_in(). * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_select A column name. * @param string $column_where A column name. * @param array $column_values An array of values. * @return mixed */ public function get_var_in( $column_select, $column_where, $column_values ) { global $wpdb; $column = esc_sql( $column_select ); $column_where = esc_sql( $column_where ); $column_values = Imagify_DB::prepare_values_list( $column_values ); $result = $wpdb->get_var( "SELECT $column FROM $this->table_name WHERE $column_where IN ( $column_values ) LIMIT 1;" ); // WPCS: unprepared SQL ok. return $this->cast( $result, $column_select ); } /** * Retrieve values by the specified column / values. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_select A column name. * @param string $column_where A column name. * @param array $column_values An array of values. * @return array */ public function get_column_in( $column_select, $column_where, $column_values ) { global $wpdb; $column = esc_sql( $column_select ); $column_where = esc_sql( $column_where ); $column_values = Imagify_DB::prepare_values_list( $column_values ); $result = $wpdb->get_col( "SELECT $column FROM $this->table_name WHERE $column_where IN ( $column_values );" ); // WPCS: unprepared SQL ok. return $this->cast_col( $result, $column_select ); } /** * Retrieve values by the specified column / values. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $column_select A column name. * @param string $column_where A column name. * @param array $column_values An array of values. * @return array */ public function get_column_not_in( $column_select, $column_where, $column_values ) { global $wpdb; $column = esc_sql( $column_select ); $column_where = esc_sql( $column_where ); $column_values = Imagify_DB::prepare_values_list( $column_values ); $result = $wpdb->get_col( "SELECT $column FROM $this->table_name WHERE $column_where NOT IN ( $column_values );" ); // WPCS: unprepared SQL ok. return $this->cast_col( $result, $column_select ); } /** * Insert a new row. * * @since 1.5 * @access public * * @param string $data New data. * @return int The ID. */ public function insert( $data ) { global $wpdb; // Initialise column format array. $column_formats = $this->get_columns(); // Set default values. $data = wp_parse_args( $data, $this->get_column_defaults() ); // Force fields to lower case. $data = array_change_key_case( $data ); // White list columns. $data = array_intersect_key( $data, $column_formats ); // Maybe serialize some values. $data = $this->serialize_columns( $data ); // Reorder $column_formats to match the order of columns given in $data. $column_formats = array_merge( $data, $column_formats ); $wpdb->insert( $this->table_name, $data, $column_formats ); return (int) $wpdb->insert_id; } /** * Update a row. * * @since 1.5 * @access public * * @param int $row_id A primary key. * @param array $data New data. * @param string $where A column name. * @return bool */ public function update( $row_id, $data = [], $where = '' ) { global $wpdb; if ( $row_id <= 0 ) { return false; } if ( ! $this->get( $row_id ) ) { $this->insert( $data ); return true; } if ( empty( $where ) ) { $where = $this->primary_key; } // Initialise column format array. $column_formats = $this->get_columns(); // Force fields to lower case. $data = array_change_key_case( $data ); // White list columns. $data = array_intersect_key( $data, $column_formats ); // Maybe serialize some values. $data = $this->serialize_columns( $data ); // Reorder $column_formats to match the order of columns given in $data. $column_formats = array_merge( $data, $column_formats ); return (bool) $wpdb->update( $this->table_name, $data, [ $where => $row_id ], $column_formats, $this->get_placeholder( $where ) ); } /** * Delete a row identified by the primary key. * * @since 1.5 * @access public * * @param string $row_id A primary key. * @return bool */ public function delete( $row_id = 0 ) { global $wpdb; if ( $row_id <= 0 ) { return false; } $placeholder = $this->get_placeholder( $this->primary_key ); return (bool) $wpdb->query( $wpdb->prepare( "DELETE FROM $this->table_name WHERE $this->primary_key = $placeholder", $row_id ) ); // WPCS: unprepared SQL ok, PreparedSQLPlaceholders replacement count ok. } /** ----------------------------------------------------------------------------------------- */ /** TABLE CREATION ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Maybe create/upgrade the table in the database. * * @since 1.7 * @access public * @author Grégory Viguier */ public function maybe_upgrade_table() { global $wpdb; if ( $this->table_is_up_to_date() ) { // The table has the right version. $this->set_table_ready(); return; } // Create the table. $this->create_table(); } /** * Create/Upgrade the table in the database. * * @since 1.7 * @access public * @author Grégory Viguier */ public function create_table() { if ( ! Imagify_DB::create_table( $this->get_table_name(), $this->get_table_schema() ) ) { // Failure. $this->set_table_not_ready(); $this->delete_db_version(); return; } // Table successfully created/upgraded. $this->set_table_ready(); $this->update_db_version(); } /** * Set various properties to tell the table is ready to be used. * * @since 1.7 * @access public * @author Grégory Viguier */ protected function set_table_ready() { global $wpdb; $this->table_created = true; $wpdb->{$this->table} = $this->table_name; if ( $this->table_is_global ) { $wpdb->global_tables[] = $this->table; } else { $wpdb->tables[] = $this->table; } } /** * Unset various properties to tell the table is NOT ready to be used. * * @since 1.7 * @access public * @author Grégory Viguier */ protected function set_table_not_ready() { global $wpdb; $this->table_created = false; unset( $wpdb->{$this->table} ); if ( $this->table_is_global ) { $wpdb->global_tables = array_diff( $wpdb->global_tables, [ $this->table ] ); } else { $wpdb->tables = array_diff( $wpdb->tables, [ $this->table ] ); } } /** ----------------------------------------------------------------------------------------- */ /** TABLE VERSION =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the table version. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int */ public function get_table_version() { return $this->table_version; } /** * Tell if the table is up-to-date (we don't "downgrade" the tables). * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public function table_is_up_to_date() { return $this->get_db_version() >= $this->get_table_version(); } /** * Get the table version stored in DB. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int|bool The version. False if not set yet. */ public function get_db_version() { $option_name = $this->table . self::TABLE_VERSION_OPTION_SUFFIX; if ( $this->table_is_global && is_multisite() ) { return get_site_option( $option_name ); } return get_option( $option_name ); } /** * Update the table version stored in DB. * * @since 1.7 * @access protected * @author Grégory Viguier */ protected function update_db_version() { $option_name = $this->table . self::TABLE_VERSION_OPTION_SUFFIX; if ( $this->table_is_global && is_multisite() ) { update_site_option( $option_name, $this->get_table_version() ); } else { update_option( $option_name, $this->get_table_version() ); } } /** * Delete the table version stored in DB. * * @since 1.7 * @access protected * @author Grégory Viguier */ protected function delete_db_version() { $option_name = $this->table . self::TABLE_VERSION_OPTION_SUFFIX; if ( $this->table_is_global && is_multisite() ) { delete_site_option( $option_name ); } else { delete_option( $option_name ); } } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the table name. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string */ public function get_table_name() { return $this->table_name; } /** * Tell if the table is the same for each site of a Multisite. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public function is_table_global() { return $this->table_is_global; } /** * Get the primary column name. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string */ public function get_primary_key() { return $this->primary_key; } /** * Get the formats related to the given columns. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $columns An array of column names (as keys). * @return array */ public function get_column_formats( $columns ) { if ( ! is_array( $columns ) ) { $columns = array_flip( (array) $columns ); } // White list columns. return array_intersect_key( $this->get_columns(), $columns ); } /** * Get the placeholder corresponding to the given key. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $key The key. * @return string */ public function get_placeholder( $key ) { $columns = $this->get_columns(); return isset( $columns[ $key ] ) ? $columns[ $key ] : '%s'; } /** * Tell if the column value must be (un)serialized. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $key The key. * @return bool */ public function is_column_serialized( $key ) { $columns = $this->get_column_defaults(); return isset( $columns[ $key ] ) && is_array( $columns[ $key ] ); } /** * Cast a value. * * @since 1.7 * @access public * @author Grégory Viguier * * @param mixed $value The value to cast. * @param string $key The corresponding key. * @return mixed */ public function cast( $value, $key ) { if ( null === $value || is_bool( $value ) ) { return $value; } $placeholder = $this->get_placeholder( $key ); if ( '%d' === $placeholder ) { return (int) $value; } if ( '%f' === $placeholder ) { return (float) $value; } if ( $value && $this->is_column_serialized( $key ) ) { return maybe_unserialize( $value ); } return $value; } /** * Cast a column. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $values The values to cast. * @param string $column The corresponding column name. * @return array */ public function cast_col( $values, $column ) { if ( ! $values ) { return $values; } foreach ( $values as $i => $value ) { $values[ $i ] = $this->cast( $value, $column ); } return $values; } /** * Cast a row. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array|object $row_fields A row from the DB. * @return array|object */ public function cast_row( $row_fields ) { if ( ! $row_fields ) { return $row_fields; } if ( is_array( $row_fields ) ) { foreach ( $row_fields as $field => $value ) { $row_fields[ $field ] = $this->cast( $value, $field ); } } elseif ( is_object( $row_fields ) ) { foreach ( $row_fields as $field => $value ) { $row_fields->$field = $this->cast( $value, $field ); } } return $row_fields; } /** * Serialize columns that need to be. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $data An array of values. * @return array */ public function serialize_columns( $data ) { if ( ! isset( $this->to_serialize ) ) { $this->to_serialize = array_filter( $this->get_column_defaults(), 'is_array' ); } if ( ! $this->to_serialize ) { return $data; } $serialized_data = array_intersect_key( $data, $this->to_serialize ); if ( ! $serialized_data ) { return $data; } $serialized_data = array_map( function ( $value ) { // Try not to store empty serialized arrays. return [] === $value ? null : maybe_serialize( $value ); }, $serialized_data ); return array_merge( $data, $serialized_data ); } } classes/class-imagify-abstract-cron.php 0000644 00000015076 15174671745 0014234 0 ustar 00 <?php /** * Basis class that handles events. * * @since 1.7 * @author Grégory Viguier */ abstract class Imagify_Abstract_Cron { /** * Class version. * * @var string * @since 1.7 */ const VERSION = '1.0'; /** * Cron name. * * @var string * @since 1.7 * @access protected */ protected $event_name = ''; /** * Cron recurrence. * * @var string * @since 1.7 * @access protected */ protected $event_recurrence = ''; /** * Cron time. * * @var string * @since 1.7 * @access protected */ protected $event_time = ''; /** ----------------------------------------------------------------------------------------- */ /** INIT ==================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Launch hooks. * * @since 1.7 * @access public * @author Grégory Viguier */ public function init() { add_action( 'init', [ $this, 'schedule_event' ] ); add_action( $this->get_event_name(), [ $this, 'do_event' ] ); add_filter( 'cron_schedules', [ $this, 'maybe_add_recurrence' ] ); if ( did_action( static::get_deactivation_hook_name() ) ) { $this->unschedule_event(); } else { add_action( static::get_deactivation_hook_name(), [ $this, 'unschedule_event' ] ); } } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Initiate the event. * * @since 1.7 * @access public * @author Grégory Viguier */ public function schedule_event() { if ( ! wp_next_scheduled( $this->get_event_name() ) ) { wp_schedule_event( $this->get_event_timestamp(), $this->get_event_recurrence(), $this->get_event_name() ); } } /** * The event action. * * @since 1.7 * @access public * @author Grégory Viguier */ abstract public function do_event(); /** * Unschedule the event at plugin or submodule deactivation. * * @since 1.7 * @access public * @author Grégory Viguier */ public function unschedule_event() { wp_clear_scheduled_hook( $this->get_event_name() ); } /** * Add the event recurrence schedule. * * @since 1.7 * @access public * @see wp_get_schedules() * @author Grégory Viguier * * @param array $schedules An array of non-default cron schedules. Default empty. * * @return array */ public function maybe_add_recurrence( $schedules ) { $default_schedules = [ 'hourly' => 1, 'twicedaily' => 1, 'daily' => 1, ]; $event_recurrence = $this->get_event_recurrence(); if ( ! empty( $schedules[ $event_recurrence ] ) || ! empty( $default_schedules[ $event_recurrence ] ) ) { return $schedules; } $recurrences = [ 'weekly' => [ 'interval' => WEEK_IN_SECONDS, 'display' => __( 'Once Weekly', 'imagify' ), ], ]; if ( method_exists( $this, 'get_event_recurrence_attributes' ) ) { $recurrences[ $event_recurrence ] = $this->get_event_recurrence_attributes(); } if ( ! empty( $recurrences[ $event_recurrence ] ) ) { $schedules[ $event_recurrence ] = $recurrences[ $event_recurrence ]; } return $schedules; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the cron name. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string */ public function get_event_name() { return $this->event_name; } /** * Get the cron recurrence. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string */ public function get_event_recurrence() { /** * Filter the recurrence of the event. * * @since 1.7 * @author Grégory Viguier * * @param string $event_recurrence The recurrence. * @param string $event_name Name of the event this recurrence is used for. */ return apply_filters( 'imagify_event_recurrence', $this->event_recurrence, $this->get_event_name() ); } /** * Get the time to schedule the event. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string */ public function get_event_time() { /** * Filter the time at which the event is triggered (WordPress time). * * @since 1.7 * @author Grégory Viguier * * @param string $event_time A 24H formated time: `hour:minute`. * @param string $event_name Name of the event this time is used for. */ return apply_filters( 'imagify_event_time', $this->event_time, $this->get_event_name() ); } /** * Get the timestamp to schedule the event. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int Timestamp. */ public function get_event_timestamp() { return self::get_next_timestamp( $this->get_event_time() ); } /** * Get the timestamp of the next (event) date for the given hour. * * @since 1.7 * @author Grégory Viguier * @source secupress_get_next_cron_timestamp() * * @param string $event_time Time when the event callback should be triggered (WordPress time), formated like `hh:mn` (hour:minute). * * @return int Timestamp. */ public static function get_next_timestamp( $event_time = '00:00' ) { $current_time_int = (int) gmdate( 'Gis' ); $event_time_int = (int) str_replace( ':', '', $event_time . '00' ); $event_time = explode( ':', $event_time ); $event_hour = (int) $event_time[0]; $event_minute = (int) $event_time[1]; $offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS; if ( $event_time_int <= $current_time_int ) { // The event time is passed, we need to schedule the event tomorrow. return mktime( $event_hour, $event_minute, 0, (int) gmdate( 'n' ), (int) gmdate( 'j' ) + 1 ) - $offset; } // We haven't passed the event time yet, schedule the event today. return mktime( $event_hour, $event_minute, 0 ) - $offset; } /** * Get the deactivation hook name. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public static function get_deactivation_hook_name() { static $deactivation_hook; if ( ! isset( $deactivation_hook ) ) { $deactivation_hook = 'deactivate_' . plugin_basename( IMAGIFY_FILE ); } return $deactivation_hook; } } classes/class-imagify-db.php 0000644 00000055237 15174671745 0012062 0 ustar 00 <?php /** * Imagify DB class. It reunites tools to work with the DB. * * @since 1.6.13 * @author Grégory Viguier */ class Imagify_DB { /** * Class version. * * @var string */ const VERSION = '1.0.1'; /** * Some hosts limit the number of JOINs in SQL queries, but we need them. * * @since 1.6.13 * @access public * @author Grégory Viguier */ public static function unlimit_joins() { global $wpdb; static $done = false; if ( $done ) { return; } $done = true; $query = 'SET SQL_BIG_SELECTS=1'; /** * Filter the SQL query allowing to remove the limit on JOINs. * * @since 1.6.13 * @author Grégory Viguier * * @param string|bool $query The query. False to prevent any query. */ $query = apply_filters( 'imagify_db_unlimit_joins_query', $query ); if ( $query && is_string( $query ) ) { $wpdb->query( $query ); // WPCS: unprepared SQL ok. } } /** * Change an array of values into a comma separated list, ready to be used in a `IN ()` clause. * * @since 1.6.13 * @access public * @author Grégory Viguier * * @param array $values An array of values. * @return string A comma separated list of values. */ public static function prepare_values_list( $values ) { $values = esc_sql( (array) $values ); $values = array_map( [ __CLASS__, 'quote_string' ], $values ); return implode( ',', $values ); } /** * Wrap a value in quotes, unless it's an integer. * * @since 1.6.13 * @access public * @author Grégory Viguier * * @param int|string $value A value. * @return int|string */ public static function quote_string( $value ) { return is_numeric( $value ) ? $value : "'" . addcslashes( $value, "'" ) . "'"; } /** * First half of escaping for LIKE special characters % and _ before preparing for MySQL. * Use this only before wpdb::prepare() or esc_sql(). Reversing the order is very bad for security. * * Example Prepared Statement: * $wild = '%'; * $find = 'only 43% of planets'; * $like = $wild . $wpdb->esc_like( $find ) . $wild; * $sql = $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_content LIKE %s", $like ); * * Example Escape Chain: * $sql = esc_sql( $wpdb->esc_like( $input ) ); * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $text The raw text to be escaped. The input typed by the user should have no extra or deleted slashes. * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare() or real_escape next. */ public static function esc_like( $text ) { global $wpdb; if ( method_exists( $wpdb, 'esc_like' ) ) { // Introduced in WP 4.0.0. return $wpdb->esc_like( $text ); } return addcslashes( $text, '_%\\' ); } /** * Get Imagify mime types, ready to be used in a `IN ()` clause. * * @since 1.6.13 * @since 1.9 Added $type parameter. * @access public * @author Grégory Viguier * * @param string $type One of 'image', 'not-image'. Any other value will return all mime types. * @return string A comma separated list of mime types. */ public static function get_mime_types( $type = null ) { static $mime_types = []; if ( empty( $type ) ) { $type = 'all'; } if ( ! isset( $mime_types[ $type ] ) ) { $mime_types[ $type ] = self::prepare_values_list( imagify_get_mime_types( $type ) ); } return $mime_types[ $type ]; } /** * Get post statuses related to attachments, ready to be used in a `IN ()` clause. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string A comma separated list of post statuses. */ public static function get_post_statuses() { static $statuses; if ( ! isset( $statuses ) ) { $statuses = self::prepare_values_list( imagify_get_post_statuses() ); } return $statuses; } /** * Get the SQL JOIN clause to use to get only attachments that have the required WP metadata. * It returns an empty string if the database has no attachments without the required metadada. * It also triggers Imagify_DB::unlimit_joins(). * * @param string $id_field An ID field to match the metadata ID against in the JOIN clause. * Default is the posts table `ID` field, using the `p` alias: `p.ID`. * In case of "false" value or PEBKAC, fallback to the same field without alias. * @param bool $matching Set to false to get a query to fetch metas NOT matching the file extensions. * @param bool $test Test if the site has attachments without required metadata before returning the query. False to bypass the test and get the query anyway. * @param string $special_join_conditions Special conditions to apply on the join. * * @return string * @author Grégory Viguier * * @since 1.7 * @access public */ public static function get_required_wp_metadata_join_clause( $id_field = 'p.ID', $matching = true, $test = true, $special_join_conditions = '' ) { global $wpdb; if ( $test && ! imagify_has_attachments_without_required_metadata() ) { return ''; } self::unlimit_joins(); $clause = ''; if ( ! $id_field || ! is_string( $id_field ) ) { $id_field = "$wpdb->posts.ID"; } $join = $matching ? 'INNER' : 'LEFT'; $first = true; foreach ( self::get_required_wp_metadata_aliases() as $meta_name => $alias ) { if ( $first ) { $first = false; $clause .= " $join JOIN $wpdb->postmeta AS $alias ON ( $id_field = $alias.post_id AND $alias.meta_key = '$meta_name' $special_join_conditions )"; continue; } $clause .= " $join JOIN $wpdb->postmeta AS $alias ON ( $id_field = $alias.post_id AND $alias.meta_key = '$meta_name' )"; } return $clause; } /** * Get the Sub query(exists) clause to use to get only attachments that have the required WP metadata. * It returns an empty string if the database has no attachments without the required metadada. * * @param string $id_field An ID field to match the metadata ID against in the WHERE clause. * Default is the posts table `ID` field, using the `p` alias: `p.ID`. * @param bool $test Test if the site has attachments without required metadata before returning the query. False to bypass the test and get the query anyway. * * @return string */ public static function get_required_wp_metadata_exist_clause( $id_field = 'p.ID', $test = true ) { global $wpdb; if ( $test && ! imagify_has_attachments_without_required_metadata() ) { return ''; } self::unlimit_joins(); $clause = ''; if ( ! $id_field || ! is_string( $id_field ) ) { $id_field = "$wpdb->posts.ID"; } $additional_clause = self::get_required_exist_wp_metadata_where_clause( [ 'matching' => false, 'test' => false, ] ); $first = true; foreach ( self::get_required_wp_metadata_aliases() as $meta_name => $alias ) { if ( $first ) { $first = false; $clause .= " EXISTS( SELECT 1 FROM $wpdb->postmeta AS $alias WHERE $alias.post_id = $id_field AND $alias.meta_key = '$meta_name' $additional_clause ) "; continue; } $clause .= " OR NOT EXISTS ( SELECT 1 FROM $wpdb->postmeta AS $alias WHERE $alias.post_id = $id_field AND $alias.meta_key = '$meta_name')"; } return "AND( $clause )"; } /** * Get the SQL part to be used in a WHERE clause, to get only attachments that have (in)valid '_wp_attached_file' and '_wp_attachment_metadata' metadatas. * It returns an empty string if the database has no attachments without the required metadada. * * @since 1.7 * @since 1.7.1.2 Use a single $arg parameter instead of 3. New $prepared parameter. * @access public * @author Grégory Viguier * * @param array $args { * Optional. An array of arguments. * * string $aliases The aliases to use for the meta values. * bool $matching Set to false to get a query to fetch invalid metas. * bool $test Test if the site has attachments without required metadata before returning the query. False to bypass the test and get the query anyway. * bool $prepared Set to true if the query will be prepared with using $wpdb->prepare(). * }. * @return string A query. */ public static function get_required_wp_metadata_where_clause( $args = [] ) { static $query = []; $args = imagify_merge_intersect( $args, [ 'aliases' => [], 'matching' => true, 'test' => true, 'prepared' => false, ] ); list( $aliases, $matching, $test, $prepared ) = array_values( $args ); if ( $test && ! imagify_has_attachments_without_required_metadata() ) { return ''; } if ( $aliases && is_string( $aliases ) ) { $aliases = [ '_wp_attached_file' => $aliases, ]; } elseif ( ! is_array( $aliases ) ) { $aliases = []; } $aliases = imagify_merge_intersect( $aliases, self::get_required_wp_metadata_aliases() ); $key = implode( '|', $aliases ) . '|' . (int) $matching; if ( isset( $query[ $key ] ) ) { return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ]; } unset( $args['prepared'] ); $alias_1 = $aliases['_wp_attached_file']; $alias_2 = $aliases['_wp_attachment_metadata']; $extensions = self::get_extensions_where_clause( $args ); if ( $matching ) { $query[ $key ] = "AND $alias_1.meta_value NOT LIKE '%://%' AND $alias_1.meta_value NOT LIKE '_:\\\\\%' AND $extensions"; } else { $query[ $key ] = "AND ( $alias_2.meta_value IS NULL OR $alias_1.meta_value IS NULL OR $alias_1.meta_value LIKE '%://%' OR $alias_1.meta_value LIKE '_:\\\\\%' AND $extensions )"; } return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ]; } /** * Get the SQL part to be used in a WHERE clause, to get only attachments that have (in)valid '_wp_attached_file' and '_wp_attachment_metadata' metadatas. * It returns an empty string if the database has no attachments without the required metadada. * * @param array $args { * Optional. An array of arguments. * * string $aliases The aliases to use for the meta values. * bool $matching Set to false to get a query to fetch invalid metas. * bool $test Test if the site has attachments without required metadata before returning the query. False to bypass the test and get the query anyway. * bool $prepared Set to true if the query will be prepared with using $wpdb->prepare(). * }. * @return string A query. */ public static function get_required_exist_wp_metadata_where_clause( $args = [] ) { static $query = []; $args = imagify_merge_intersect( $args, [ 'aliases' => [], 'matching' => true, 'test' => true, 'prepared' => false, ] ); list( $aliases, $matching, $test, $prepared ) = array_values( $args ); if ( $test && ! imagify_has_attachments_without_required_metadata() ) { return ''; } if ( $aliases && is_string( $aliases ) ) { $aliases = [ '_wp_attached_file' => $aliases, ]; } elseif ( ! is_array( $aliases ) ) { $aliases = []; } $aliases = imagify_merge_intersect( $aliases, self::get_required_wp_metadata_aliases() ); $key = implode( '|', $aliases ) . '|' . (int) $matching; if ( isset( $query[ $key ] ) ) { return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ]; } unset( $args['prepared'] ); $alias_1 = $aliases['_wp_attached_file']; $extensions = self::get_extensions_where_clause( $args ); if ( $matching ) { $query[ $key ] = "AND $alias_1.meta_value NOT LIKE '%://%' AND $alias_1.meta_value NOT LIKE '_:\\\\\%' OR NOT ( $extensions )"; } else { $query[ $key ] = "AND ( $alias_1.meta_value LIKE '%://%' OR $alias_1.meta_value LIKE '_:\\\\\%' OR NOT ( $extensions ) )"; } return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ]; } /** * Prepare query arguments. * * @param array $args { * Optional. An array of arguments. * * string $aliases The aliases to use for the meta values. * bool $matching Set to false to get a query to fetch invalid metas. * bool $test Test if the site has attachments without required metadata before returning the query. False to bypass the test and get the query anyway. * bool $prepared Set to true if the query will be prepared with using $wpdb->prepare(). * }. * * @return array */ private function prepare_query_args( $args ) { return imagify_merge_intersect( $args, [ 'aliases' => [], 'matching' => true, 'test' => true, 'prepared' => false, ] ); } /** * Generate query. * * @param bool $matching Matching. * @param string $alias Query alias. * @param string $regex Query Regex. * * @return string */ private function generate_query( $matching, $alias, $regex ) { if ( $matching ) { return "REVERSE (LOWER( $alias.meta_value )) REGEXP '$regex'"; } return "REVERSE (LOWER( $alias.meta_value )) NOT REGEXP '$regex'"; } /** * Get the SQL part to be used in a WHERE clause, to get only attachments that have a valid file extensions. * It returns an empty string if the database has no attachments without the required metadada. * * @since 1.7 * @since 1.7.1.2 Use a single $arg parameter instead of 3. New $prepared parameter. * @access public * @author Grégory Viguier * * @param array $args { * Optional. An array of arguments. * * string $alias The alias to use for the meta value. * bool $matching Set to false to get a query to fetch metas NOT matching the file extensions. * bool $test Test if the site has attachments without required metadata before returning the query. False to bypass the test and get the query anyway. * bool $prepared Set to true if the query will be prepared with using $wpdb->prepare(). * }. * @return string A query. */ public static function get_extensions_where_clause( $args = false ) { static $extensions; static $query = []; $instance = new self(); $args = $instance->prepare_query_args( $args ); list( $alias, $matching, $test, $prepared ) = array_values( $args ); if ( $test && ! imagify_has_attachments_without_required_metadata() ) { return ''; } if ( ! isset( $extensions ) ) { $extensions = array_keys( imagify_get_mime_types() ); $extensions = implode( '|', $extensions ); $extensions = explode( '|', $extensions ); $extensions = array_map( function ( $ex ) { return strrev( $ex ); }, $extensions ); } if ( ! $alias ) { $alias = self::get_required_wp_metadata_aliases(); $alias = $alias['_wp_attached_file']; } $key = $alias . '|' . (int) $matching; if ( isset( $query[ $key ] ) ) { return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ]; } $regex = '^' . implode( '\..*|^', $extensions ) . '\..*'; $query[ $key ] = $instance->generate_query( $matching, $alias, $regex ); return $prepared ? str_replace( '%', '%%', $query[ $key ] ) : $query[ $key ]; } /** * Get the aliases used for the metas in self::get_required_wp_metadata_join_clause(), self::get_required_wp_metadata_where_clause(), and self::get_extensions_where_clause(). * * @since 1.7 * @access public * @author Grégory Viguier * * @return array An array with the meta name as key and its alias as value. */ public static function get_required_wp_metadata_aliases() { return [ '_wp_attached_file' => 'imrwpmt1', '_wp_attachment_metadata' => 'imrwpmt2', ]; } /** * Combine two arrays with some specific keys. * We use this function to combine the result of 2 SQL queries. * * @since 1.6.13 * @access public * @author Grégory Viguier * * @param array $keys An array of keys. * @param array $values An array of arrays like array( 'id' => id, 'value' => value ). * @param int $keep_keys_order Set to true to return an array ordered like $keys instead of $values. * @return array The combined arrays. */ public static function combine_query_results( $keys, $values, $keep_keys_order = false ) { if ( ! $keys || ! $values ) { return []; } $result = []; $keys = array_flip( $keys ); foreach ( $values as $v ) { if ( isset( $keys[ $v['id'] ] ) ) { $result[ $v['id'] ] = $v['value']; } } if ( $keep_keys_order ) { $keys = array_intersect_key( $keys, $result ); return array_replace( $keys, $result ); } return $result; } /** * A helper to retrieve all values from one or several post metas, given a list of post IDs. * The $wpdb cache is flushed to save memory. * * @since 1.6.13 * @access public * @author Grégory Viguier * * @param array $metas An array of meta names like: * array( * 'key1' => 'meta_name_1', * 'key2' => 'meta_name_2', * 'key3' => 'meta_name_3', * ) * If a key contains 'data', the results will be unserialized. * @param array $ids An array of post IDs. * @return array An array of arrays of results like: * array( * 'key1' => array( post_id_1 => 'result_1', post_id_2 => 'result_2', post_id_3 => 'result_3' ), * 'key2' => array( post_id_1 => 'result_4', post_id_3 => 'result_5' ), * 'key3' => array( post_id_1 => 'result_6', post_id_2 => 'result_7' ), * ) */ public static function get_metas( $metas, $ids ) { global $wpdb; if ( ! $ids ) { return array_fill_keys( array_keys( $metas ), [] ); } $sql_ids = implode( ',', $ids ); foreach ( $metas as $result_name => $meta_name ) { $metas[ $result_name ] = $wpdb->get_results( // WPCS: unprepared SQL ok. "SELECT pm.post_id as id, pm.meta_value as value FROM $wpdb->postmeta as pm WHERE pm.meta_key = '$meta_name' AND pm.post_id IN ( $sql_ids ) ORDER BY pm.post_id DESC", ARRAY_A ); $wpdb->flush(); $metas[ $result_name ] = self::combine_query_results( $ids, $metas[ $result_name ], true ); if ( strpos( $result_name, 'data' ) !== false ) { $metas[ $result_name ] = array_map( 'maybe_unserialize', $metas[ $result_name ] ); } } return $metas; } /** * Create/Upgrade the table in the database. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $table_name The (prefixed) table name. * @param string $schema_query Query representing the table schema. * @return bool True on success. False otherwise. */ public static function create_table( $table_name, $schema_query ) { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $wpdb->hide_errors(); $schema_query = trim( $schema_query ); $charset_collate = $wpdb->get_charset_collate(); dbDelta( "CREATE TABLE $table_name ($schema_query) $charset_collate;" ); return empty( $wpdb->last_error ) && self::table_exists( $table_name ); } /** * Tell if the given table exists. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $table_name Full name of the table (with DB prefix). * @return bool */ public static function table_exists( $table_name ) { global $wpdb; $escaped_table = self::esc_like( $table_name ); $result = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $escaped_table ) ); return $result === $table_name; } /** * Cache transients used for optimization process locks. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $context The context. * @param array $media_ids The media IDs. */ public static function cache_process_locks( $context, $media_ids ) { global $wpdb; if ( ! $context || ! $media_ids || wp_using_ext_object_cache() ) { return; } // Sanitize the IDs. $media_ids = array_filter( $media_ids ); $media_ids = array_unique( $media_ids ); if ( ! $media_ids ) { return; } $context_instance = imagify_get_context( $context ); $context = $context_instance->get_name(); $process_class_name = imagify_get_optimization_process_class_name( $context ); $transient_name = sprintf( $process_class_name::LOCK_NAME, $context, '%' ); $is_network_wide = $context_instance->is_network_wide(); // Do 1 DB query per context (and cache results) before doing 1 get_transient() (2 DB queries) per media ID. $prefix = $is_network_wide ? '_site_transient_' : '_transient_'; if ( $is_network_wide && is_multisite() ) { $network_id = function_exists( 'get_current_network_id' ) ? get_current_network_id() : (int) $wpdb->siteid; $cache_prefix = "$network_id:"; $notoptions_key = "$network_id:notoptions"; $cache_group = 'site-options'; $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key as name, meta_value as value FROM $wpdb->sitemeta WHERE ( meta_key LIKE %s OR meta_key LIKE %s ) AND site_id = %d", $prefix . $transient_name, $prefix . 'timeout_' . $transient_name, $network_id ), OBJECT_K ); // WPCS: unprepared SQL ok. } else { $cache_prefix = ''; $notoptions_key = 'notoptions'; $cache_group = 'options'; $results = $wpdb->get_results( $wpdb->prepare( "SELECT option_name as name, option_value as value FROM $wpdb->options WHERE ( option_name LIKE %s OR option_name LIKE %s )", $prefix . $transient_name, $prefix . 'timeout_' . $transient_name ), OBJECT_K ); // WPCS: unprepared SQL ok. } $not_exist = []; foreach ( [ '', 'timeout_' ] as $maybe_timeout ) { foreach ( $media_ids as $id ) { $option_name = $prefix . $maybe_timeout . str_replace( '%', $id, $transient_name ); if ( isset( $results[ $option_name ] ) ) { // Cache the value. $value = $results[ $option_name ]->value; $value = maybe_unserialize( $value ); wp_cache_set( "$cache_prefix$option_name", $value, $cache_group ); } else { // No value. $not_exist[ $option_name ] = true; } } } if ( ! $not_exist ) { return; } // Cache the options that don't exist in the DB. $notoptions = wp_cache_get( $notoptions_key, $cache_group ); $notoptions = is_array( $notoptions ) ? $notoptions : []; $notoptions = array_merge( $notoptions, $not_exist ); wp_cache_set( $notoptions_key, $notoptions, $cache_group ); } } classes/class-imagify-cron-sync-files.php 0000644 00000004051 15174671745 0014474 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * Class that scans the custom folders to keep files in sync in the database. * * @since 1.7 */ class Imagify_Cron_Sync_Files extends Imagify_Abstract_Cron { use InstanceGetterTrait; /** * Class version. * * @var string * @since 1.7 */ const VERSION = '1.0'; /** * Cron name. * * @var string * @since 1.7 */ protected $event_name = 'imagify_sync_files'; /** * Cron recurrence. * * @var string * @since 1.7 */ protected $event_recurrence = 'daily'; /** * Cron time. * * @var string * @since 1.7 */ protected $event_time = '02:00'; /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * The event action. * * @since 1.7 */ public function do_event() { $folders_db = Imagify_Folders_DB::get_instance(); $files_db = Imagify_Files_DB::get_instance(); if ( ! $folders_db->can_operate() || ! $files_db->can_operate() ) { return; } if ( ! Imagify_Requirements::is_api_key_valid() ) { return; } if ( Imagify_Requirements::is_over_quota() ) { return; } $this->set_no_time_limit(); /** * Get the folders from DB. */ $folders = Imagify_Custom_Folders::get_folders(); if ( ! $folders ) { return; } Imagify_Custom_Folders::synchronize_files_from_folders( $folders ); } /** * Attempts to set no limit to the PHP timeout for time intensive processes. * * @return void */ protected function set_no_time_limit() { if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved ) { @set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } } } classes/class-imagify-cron-library-size.php 0000644 00000002517 15174671745 0015041 0 ustar 00 <?php use Imagify\Traits\InstanceGetterTrait; /** * Class that handles the cron that calculate and cache the library size. * * @since 1.7 * @author Grégory Viguier */ class Imagify_Cron_Library_Size extends Imagify_Abstract_Cron { use InstanceGetterTrait; /** * Class version. * * @var string * @since 1.7 */ const VERSION = '1.0'; /** * Cron name. * * @var string * @since 1.7 * @access protected */ protected $event_name = 'imagify_update_library_size_calculations_event'; /** * Cron recurrence. * * @var string * @since 1.7 * @access protected */ protected $event_recurrence = 'weekly'; /** * Cron time. * * @var string * @since 1.7 * @access protected */ protected $event_time = '04:00'; /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * The event action. * * @since 1.7 * @access public * @author Grégory Viguier */ public function do_event() { imagify_do_async_job( [ 'action' => 'imagify_update_estimate_sizes', '_ajax_nonce' => wp_create_nonce( 'update_estimate_sizes' ), ] ); } } classes/class-imagify-files-list-table.php 0000644 00000100444 15174671745 0014624 0 ustar 00 <?php if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** * Class that display the "custom folders" files. * * @since 1.7 */ class Imagify_Files_List_Table extends WP_List_Table { /** * Class version. * * @var string * @since 1.7 */ const VERSION = '1.1'; /** * Class version. * * @var string * @since 1.7 */ const PER_PAGE_OPTION = 'imagify_files_per_page'; /** * List of the folders containing the listed files. * * @var array * @since 1.7 */ protected $folders = []; /** * Filesystem object. * * @var Imagify_Filesystem * @since 1.7.1 */ protected $filesystem; /** * Views object. * * @var Imagify_Views * @since 1.9 */ protected $views; /** * Constructor. * * @since 1.7 * * @param array $args An associative array of arguments. */ public function __construct( $args = [] ) { parent::__construct( [ 'plural' => 'imagify-files', 'screen' => isset( $args['screen'] ) ? convert_to_screen( $args['screen'] ) : null, ] ); $this->modes = [ 'list' => __( 'List View', 'imagify' ), ]; $this->filesystem = Imagify_Filesystem::get_instance(); $this->views = Imagify_Views::get_instance(); } /** * Prepares the list of items for displaying. * * @since 1.7 */ public function prepare_items() { global $wpdb; add_screen_option( 'per_page', [ 'label' => __( 'Number of files per page', 'imagify' ), 'default' => 20, 'option' => self::PER_PAGE_OPTION, ] ); $files_db = Imagify_Files_DB::get_instance(); $files_table = $files_db->get_table_name(); $files_key = $files_db->get_primary_key(); $files_key_esc = esc_sql( $files_key ); $per_page = $this->get_items_per_page( self::PER_PAGE_OPTION ); // Prepare the query to get items. $page = $this->get_pagenum(); $offset = ( $page - 1 ) * $per_page; $orderbys = $this->get_sortable_columns(); $orderby = 'path'; $order = 'ASC'; $folders = []; $file_ids = []; $where = ''; $sent_orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended $sent_order = isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended $folder_filter = self::get_folder_filter(); $status_filter = self::get_status_filter(); if ( ! empty( $sent_orderby ) && isset( $orderbys[ $sent_orderby ] ) ) { $orderby = $sent_orderby; $order = is_array( $orderbys[ $orderby ] ) ? 'DESC' : 'ASC'; if ( 'optimization' === $orderby ) { $orderby = 'percent'; } } if ( $sent_order ) { $order = 'ASC' === strtoupper( $sent_order ) ? 'ASC' : 'DESC'; } if ( $folder_filter ) { // Display only files from a specific custom folder. $where = "WHERE folder_id = $folder_filter"; } if ( $status_filter ) { // Display files optimized, not optimized, or with error. $where .= $where ? ' AND ' : 'WHERE '; switch ( $status_filter ) { case 'optimized': $where .= "( status = 'success' OR status = 'already_optimized' )"; break; case 'unoptimized': $where .= 'status IS NULL'; break; case 'errors': $where .= "status = 'error'"; break; } } // Pagination. $this->set_pagination_args( [ 'total_items' => (int) $wpdb->get_var( "SELECT COUNT($files_key_esc) FROM $files_table $where" ), // WPCS: unprepared SQL ok. 'per_page' => $per_page, ] ); // Get items. $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $files_table $where ORDER BY $orderby $order LIMIT %d, %d", $offset, $per_page ) ); // WPCS: unprepared SQL ok. if ( ! $this->items ) { return; } // Prepare items. foreach ( $this->items as $i => $item ) { // Cast values. $item = $files_db->cast_row( $item ); // Store the folders used by the items to get their data later in 1 query. $folders[ $item->folder_id ] = $item->folder_id; // Store the item IDs to store transients later in 1 query. $file_ids[ $item->$files_key ] = $item->$files_key; // Use Imagify objects + add related folder ID and path (set later). $this->items[ $i ] = (object) [ 'process' => imagify_get_optimization_process( $item, 'custom-folders' ), 'folder_id' => $item->folder_id, 'folder_path' => false, 'is_folder_active' => true, ]; if ( ! $this->items[ $i ]->process->is_valid() ) { unset( $this->items[ $i ] ); } } $folders = array_filter( $folders ); // Cache transient values. Imagify_DB::cache_process_locks( 'custom-folders', $file_ids ); if ( ! $folders ) { return; } // Get folders data. $folders_db = Imagify_Folders_DB::get_instance(); $folders_table = $folders_db->get_table_name(); $folders_key_esc = esc_sql( $folders_db->get_primary_key() ); $folders = Imagify_DB::prepare_values_list( $folders ); $folders = $wpdb->get_results( "SELECT * FROM $folders_table WHERE $folders_key_esc IN ( $folders )" ); // WPCS: unprepared SQL ok. if ( ! $folders ) { return; } // Cast folders data and store data into a property. foreach ( $folders as $folder ) { $folder = $folders_db->cast_row( $folder ); $this->folders[ $folder->folder_id ] = $folder; } // Set folders path to each item. foreach ( $this->items as $i => $item ) { if ( $item->folder_id && isset( $this->folders[ $item->folder_id ] ) ) { $item->folder_path = $this->folders[ $item->folder_id ]->path; $item->is_folder_active = (bool) $this->folders[ $item->folder_id ]->active; } } // Button templates. Imagify_Views::get_instance()->print_js_template_in_footer( 'button/processing' ); } /** * Message to be displayed when there are no items. * * @since 1.7 */ public function no_items() { if ( self::get_status_filter() ) { // Filter by status. switch ( self::get_status_filter() ) { case 'optimized': /* translators: 1 is a link tag start, 2 is the link tag end. */ printf( esc_html__( 'No optimized files. Have you tried the %1$sbulk optimization%2$s yet?', 'imagify' ), '<a href="' . esc_url( get_imagify_admin_url( 'files-bulk-optimization' ) ) . '">', '</a>' ); return; case 'unoptimized': esc_html_e( 'No unoptimized files, hurray!', 'imagify' ); return; case 'errors': esc_html_e( 'No errors, hurray!', 'imagify' ); return; } } $args = [ 'action' => 'imagify_scan_custom_folders', '_wpnonce' => wp_create_nonce( 'imagify_scan_custom_folders' ), '_wp_http_referer' => get_imagify_admin_url( 'files-list' ), ]; if ( self::get_folder_filter() ) { // A specific custom folder (selected or not). $args['folder'] = self::get_folder_filter(); $args['_wp_http_referer'] = rawurlencode( add_query_arg( 'folder-filter', self::get_folder_filter(), $args['_wp_http_referer'] ) ); printf( /* translators: 1 and 2 are link tag starts, 3 is a link tag end. */ esc_html__( 'No files yet. Do you want to %1$sscan this folder%3$s for new files or launch a %2$sbulk optimization%3$s directly?', 'imagify' ), '<a href="' . esc_url( add_query_arg( $args, admin_url( 'admin-post.php' ) ) ) . '">', '<a href="' . esc_url( get_imagify_admin_url( 'files-bulk-optimization' ) ) . '">', '</a>' ); return; } if ( Imagify_Folders_DB::get_instance()->has_active_folders() ) { // All selected custom folders. $args['_wp_http_referer'] = rawurlencode( $args['_wp_http_referer'] ); printf( /* translators: 1 and 2 are link tag starts, 3 is a link tag end. */ esc_html__( 'No files yet. Do you want to %1$sscan your selected folders%3$s for new files or launch a %2$sbulk optimization%3$s directly?', 'imagify' ), '<a href="' . esc_url( add_query_arg( $args, admin_url( 'admin-post.php' ) ) ) . '">', '<a href="' . esc_url( get_imagify_admin_url( 'files-bulk-optimization' ) ) . '">', '</a>' ); return; } // Nothing selected in the settings. printf( /* translators: 1 is a link tag start, 2 is the link tag end. */ esc_html__( 'To see things appear here, you must select folders in the settings page first :)', 'imagify' ), '<a href="' . esc_url( get_imagify_admin_url() ) . '">', '</a>' ); } /** * Display views. * * @since 1.7 */ public function views() { global $wpdb; // Get all folders. $folders_table = Imagify_Folders_DB::get_instance()->get_table_name(); $folders = $wpdb->get_results( "SELECT folder_id, path FROM $folders_table" ); // WPCS: unprepared SQL ok. if ( ! $folders ) { return; } $files_db = Imagify_Files_DB::get_instance(); $files_table = $files_db->get_table_name(); $files_key_esc = esc_sql( $files_db->get_primary_key() ); // Filter files by folder. $folder_filters = []; $root_id = 0; $counts = $wpdb->get_results( "SELECT folder_id, COUNT( $files_key_esc ) AS count FROM $files_table GROUP BY folder_id", OBJECT_K ); // WPCS: unprepared SQL ok. foreach ( $folders as $folder ) { if ( '{{ROOT}}/' === $folder->path ) { $root_id = $folder->folder_id; $folder_filters[ $folder->folder_id ] = '/'; } else { $folder_filters[ $folder->folder_id ] = '/' . trim( $this->filesystem->make_path_relative( Imagify_Files_Scan::remove_placeholder( $folder->path ) ), '/' ); } } natcasesort( $folder_filters ); if ( $root_id ) { $folder_filters[ $root_id ] = __( 'Site\'s root', 'imagify' ); } foreach ( $folder_filters as $folder_id => $label ) { $folder_filters[ $folder_id ] .= ' (' . ( isset( $counts[ $folder_id ] ) ? (int) $counts[ $folder_id ]->count : 0 ) . ')'; } // Filter files by status. $counts = $wpdb->get_results( "SELECT status, COUNT( $files_key_esc ) AS count FROM $files_table GROUP BY status", OBJECT_K ); // WPCS: unprepared SQL ok. $status_filters = [ 'optimized' => 0, 'unoptimized' => 0, 'errors' => 0, ]; if ( isset( $counts['success'] ) ) { $status_filters['optimized'] += $counts['success']->count; } if ( isset( $counts['already_optimized'] ) ) { $status_filters['optimized'] += $counts['already_optimized']->count; } if ( isset( $counts[''] ) ) { $status_filters['unoptimized'] += $counts['']->count; } if ( isset( $counts['error'] ) ) { $status_filters['errors'] += $counts['error']->count; } $status_filters = [ '' => __( 'All Media Files', 'imagify' ), 'optimized' => _x( 'Optimized', 'Media Files', 'imagify' ) . ' (' . $status_filters['optimized'] . ')', 'unoptimized' => _x( 'Unoptimized', 'Media Files', 'imagify' ) . ' (' . $status_filters['unoptimized'] . ')', 'errors' => _x( 'Errors', 'Media Files', 'imagify' ) . ' (' . $status_filters['errors'] . ')', ]; // Get submitted values. $folder_filter = self::get_folder_filter(); $status_filter = self::get_status_filter(); // Display the filters. if ( method_exists( $this->screen, 'render_screen_reader_content' ) ) { // Introduced in WP 4.4. $this->screen->render_screen_reader_content( 'heading_views' ); } ?> <div class="wp-filter"> <div class="filter-items"> <label for="folder-filter" class="screen-reader-text"><?php esc_html_e( 'Filter by folder', 'imagify' ); ?></label> <select class="folder-filters" name="folder-filter" id="folder-filter"> <?php printf( '<option value="%s"%s>%s</option>', '', selected( $folder_filter, 0, false ), esc_html__( 'All Folders', 'imagify' ) ); foreach ( $folder_filters as $folder_id => $label ) { printf( '<option value="%d"%s>%s</option>', esc_attr( $folder_id ), selected( $folder_filter, $folder_id, false ), esc_html( $label ) ); } ?> </select> <label for="status-filter" class="screen-reader-text"><?php esc_html_e( 'Filter by status', 'imagify' ); ?></label> <select class="folder-filters" name="status-filter" id="status-filter"> <?php foreach ( $status_filters as $status => $label ) { printf( '<option value="%s"%s>%s</option>', esc_attr( $status ), selected( $status_filter, $status, false ), esc_html( $label ) ); } ?> </select> <?php submit_button( _x( 'Filter', 'verb', 'imagify' ), '', 'filter_action', false, [ 'id' => 'folders-query-submit' ] ); ?> <?php $this->extra_tablenav( 'bar' ); ?> </div> </div> <?php } /** * Get an associative array ( option_name => option_title ) with the list of bulk actions available on this table. * * @since 1.7 * * @return array */ public function get_bulk_actions() { return [ 'imagify-bulk-refresh-status' => __( 'Refresh status', 'imagify' ), ]; } /** * Get a list of columns. The format is: * 'internal-name' => 'Title' * * @since 1.7 * * @return array */ public function get_columns() { return [ 'cb' => '<input type="checkbox" />', 'title' => __( 'File', 'imagify' ), 'folder' => __( 'Folder', 'imagify' ), 'optimization' => __( 'Optimization', 'imagify' ), 'status' => __( 'Status', 'imagify' ), 'optimization_level' => __( 'Optimization Level', 'imagify' ), 'actions' => __( 'Actions', 'imagify' ), ]; } /** * Get a list of sortable columns. The format is: * 'internal-name' => 'orderby' * or * 'internal-name' => array( 'orderby', true ) * * The second format will make the initial sorting order be descending. * * @since 1.7 * * @return array */ public function get_sortable_columns() { return [ 'folder' => 'folder', 'optimization' => [ 'optimization', true ], 'status' => 'status', 'optimization_level' => [ 'optimization_level', true ], ]; } /** * Get a column contents. * * @since 1.7 * * @param string $column The column "name": "cb", "title", "optimization_level", etc. * @param object $item The current item. It must contain at least a $process property. * @return string HTML contents, */ public function get_column( $column, $item ) { if ( ! method_exists( $this, 'column_' . $column ) ) { return ''; } ob_start(); call_user_func( [ $this, 'column_' . $column ], $item ); return ob_get_clean(); } /** * Handles the checkbox column output. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ public function column_cb( $item ) { $media_id = $item->process->get_media()->get_id(); ?> <label class="screen-reader-text" for="cb-select-<?php echo esc_attr( $media_id ); ?>"><?php echo esc_html_x( 'Select file', 'checkbox label', 'imagify' ); ?></label> <input id="cb-select-<?php echo esc_attr( $media_id ); ?>" type="checkbox" name="bulk_select[]" value="<?php echo esc_attr( $media_id ); ?>" /> <?php } /** * Handles the title column output. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ public function column_title( $item ) { $item = $this->maybe_set_item_folder( $item ); $media = $item->process->get_media(); $url = $media->get_fullsize_url(); $base = ! empty( $item->folder_path ) ? Imagify_Files_Scan::remove_placeholder( $item->folder_path ) : ''; $title = $this->filesystem->make_path_relative( $media->get_fullsize_path(), $base ); list( $mime ) = explode( '/', $media->get_mime_type() ); if ( $media->is_image() ) { $dimensions = $media->get_dimensions(); $orientation = $dimensions['width'] > $dimensions['height'] ? ' landscape' : ' portrait'; $orientation = $dimensions['width'] && $dimensions['height'] ? $orientation : ''; if ( ! wp_doing_ajax() && $item->process->get_data()->get_optimized_size( false, 0, false ) > 100000 ) { // LazyLoad. $image_tag = '<img src="' . esc_url( IMAGIFY_ASSETS_IMG_URL . 'lazyload.png' ) . '" data-lazy-src="' . esc_url( $url ) . '" alt="" />'; $image_tag .= '<noscript><img src="' . esc_url( $url ) . '" alt="" /></noscript>'; } else { $image_tag = '<img src="' . esc_url( $url ) . '" class="hide-if-no-js" alt="" />'; } } else { $orientation = ''; $image_tag = '<img src="' . esc_url( wp_mime_type_icon( $mime ) ) . '" class="hide-if-no-js" alt="" />'; } ?> <strong class="has-media-icon"> <a href="<?php echo esc_url( $url ); ?>" target="_blank"> <span class="media-icon <?php echo sanitize_html_class( $mime . '-icon' ); ?><?php echo esc_attr( $orientation ); ?>"> <span class="centered"> <?php echo $image_tag; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </span> </span> <?php echo esc_html( $title ); ?> </a> </strong> <p class="filename"> <?php $this->comparison_tool_button( $item ); ?> </p> <?php } /** * Handles the parent folder column output. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ public function column_folder( $item ) { $item = $this->maybe_set_item_folder( $item ); if ( empty( $item->folder_path ) ) { return; } $format = '%s'; $filter = self::get_folder_filter(); if ( $filter !== $item->folder_id ) { $format = '<a href="' . esc_url( add_query_arg( 'folder-filter', $item->folder_id, get_imagify_admin_url( 'files-list' ) ) ) . '">%s</a>'; } if ( '{{ROOT}}/' === $item->folder_path ) { // It's the site's root. printf( $format, esc_html__( 'Site\'s root', 'imagify' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { printf( $format, '<code>/' . trim( $this->filesystem->make_path_relative( Imagify_Files_Scan::remove_placeholder( $item->folder_path ) ), '/' ) . '</code>' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } if ( ! $item->is_folder_active ) { echo '<br/>'; esc_html_e( 'This folder is not selected for bulk optimization.', 'imagify' ); } } /** * Handles the optimization data column output. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ public function column_optimization( $item ) { $data = $item->process->get_data(); $media_id = $item->process->get_media()->get_id(); ?> <ul class="imagify-datas-list"> <li class="imagify-data-item"> <span class="data"><?php esc_html_e( 'Original Filesize:', 'imagify' ); ?></span> <strong class="data-value original"><?php echo esc_html( $data->get_original_size() ); ?></strong> </li> <?php if ( $data->is_optimized() ) { ?> <li class="imagify-data-item"> <span class="data"><?php esc_html_e( 'New Filesize:', 'imagify' ); ?></span> <strong class="data-value big optimized"><?php echo esc_html( $data->get_optimized_size() ); ?></strong> </li> <li class="imagify-data-item"> <span class="data"><?php esc_html_e( 'Original Saving:', 'imagify' ); ?></span> <strong class="data-value"> <span class="imagify-chart"> <span class="imagify-chart-container"> <canvas class="imagify-consumption-chart imagify-consumption-chart-<?php echo esc_attr( $media_id ); ?>" width="15" height="15"></canvas> <?php if ( wp_doing_ajax() ) { ?> <script type="text/javascript">jQuery( window ).trigger( "canvasprinted.imagify", [ ".imagify-consumption-chart-<?php echo esc_attr( $media_id ); ?>" ] ); </script> <?php } ?> </span> </span> <span class="imagify-chart-value"><?php echo esc_html( $data->get_saving_percent() ); ?></span>% </strong> </li> <?php if ( $item->process->get_media()->is_image() ) { $has_nextgen = $item->process->has_next_gen() ? __( 'Yes', 'imagify' ) : __( 'No', 'imagify' ); ?> <li class="imagify-data-item"> <span class="data"><?php esc_html_e( 'Next-Gen generated:', 'imagify' ); ?></span> <strong class="data-value"><?php echo esc_html( $has_nextgen ); ?></strong> </li> <?php } } ?> </ul> <?php } /** * Handles the status column output. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ public function column_status( $item ) { $data = $item->process->get_data(); $row = $data->get_row(); $status = $data->get_optimization_status(); $messages = []; if ( ! $status ) { // File is not optimized. $messages[] = '<strong class="imagify-status-not-optimized">' . esc_html_x( 'Not optimized', 'Media File', 'imagify' ) . '</strong>'; } elseif ( ! empty( $row['error'] ) ) { // Error or already optimized. $messages[] = '<span class="imagify-status-' . $status . '">' . esc_html( imagify_translate_api_message( $row['error'] ) ) . '</span>'; } if ( empty( $row['modified'] ) && ! $messages ) { // No need to display this if we already have another message to display. $messages[] = '<em class="imagify-status-no-changes">' . esc_html__( 'No changes found', 'imagify' ) . '</em>'; } elseif ( ! empty( $row['modified'] ) ) { // The file has changed or is missing. $messages[] = '<strong class="imagify-status-changed">' . esc_html__( 'The file has changed', 'imagify' ) . '</strong>'; } echo implode( '<br/>', $messages ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->refresh_status_button( $item ); } /** * Handles the optimization level column output. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ public function column_optimization_level( $item ) { $data = $item->process->get_data(); if ( ! $data->is_error() ) { echo imagify_get_optimization_level_label( $data->get_optimization_level(), '%ICON% %s' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Handles the actions column output. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ public function column_actions( $item ) { static $done = false; if ( ! Imagify_Requirements::is_api_key_valid() ) { // Stop the process if the API key isn't valid. if ( ! $done ) { // No need to display this on every row. $done = true; esc_html_e( 'Invalid API key', 'imagify' ); echo '<br/><a href="' . esc_url( get_imagify_admin_url() ) . '">' . esc_html__( 'Check your Settings', 'imagify' ) . '</a>'; } return; } if ( $item->process->is_locked() ) { Imagify_Views::get_instance()->print_template( 'button-processing', [ 'label' => __( 'Optimizing...', 'imagify' ), ] ); return; } $this->optimize_button( $item ); $this->retry_button( $item ); $this->reoptimize_buttons( $item ); $this->generate_nextgen_versions_button( $item ); $this->delete_nextgen_versions_button( $item ); $this->restore_button( $item ); } /** * Prints a button to optimize the file. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ protected function optimize_button( $item ) { if ( $item->process->get_data()->get_optimization_status() ) { // Already optimized. return; } $media = $item->process->get_media(); $class = $media->has_backup() ? ' file-has-backup' : ''; $url = get_imagify_admin_url( 'optimize-file', [ 'attachment_id' => $media->get_id(), ] ); echo $this->views->get_template( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'button/optimize', [ 'url' => $url, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'atts' => [ 'class' => 'button-primary button-imagify-optimize' . esc_attr( $class ), ], ] ); } /** * Prints a button to retry to optimize the file. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ protected function retry_button( $item ) { $data = $item->process->get_data(); if ( ! $data->is_already_optimized() && ! $data->is_error() ) { // Not optimized or successfully optimized. return; } $media = $item->process->get_media(); $class = $media->has_backup() ? ' file-has-backup' : ''; $url = get_imagify_admin_url( 'optimize-file', [ 'attachment_id' => $media->get_id(), ] ); echo $this->views->get_template( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'button/retry-optimize', [ 'url' => $url, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'atts' => [ 'class' => 'button button-imagify-optimize' . esc_attr( $class ), ], ] ); echo '<br/>'; } /** * Prints buttons to re-optimize the file to other levels. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ protected function reoptimize_buttons( $item ) { $data = $item->process->get_data(); if ( ! $data->get_optimization_status() ) { // Not optimized yet. return; } $is_already_optimized = $data->is_already_optimized(); $media = $item->process->get_media(); $can_reoptimize = $is_already_optimized || $media->has_backup(); // Don't display anything if there is no backup or the image has been optimized. if ( ! $can_reoptimize ) { return; } $media_level = $data->get_optimization_level(); $data = []; $url_args = [ 'attachment_id' => $media->get_id() ]; if ( $media_level < 1 ) { $url_args['optimization_level'] = 2; $data['optimization_level'] = 2; $data['url'] = get_imagify_admin_url( 'reoptimize-file', $url_args ); echo $this->views->get_template( 'button/re-optimize', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } elseif ( $media_level > 0 ) { $url_args['optimization_level'] = 0; $data['optimization_level'] = 0; $data['url'] = get_imagify_admin_url( 'reoptimize-file', $url_args ); echo $this->views->get_template( 'button/re-optimize', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Prints a button to generate Next gen versions if they are missing. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ protected function generate_nextgen_versions_button( $item ) { $button = get_imagify_attachment_generate_nextgen_versions_link( $item->process ); if ( $button ) { echo $button . '<br/>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Prints a button to delete next-gen versions when the status is "already_optimized". * * @since 1.9.6 * * @param object $item The current item. It must contain at least a $process property. */ protected function delete_nextgen_versions_button( $item ) { $button = get_imagify_attachment_delete_nextgen_versions_link( $item->process ); if ( $button ) { echo $button . '<br/>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Prints a button to restore the file. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ protected function restore_button( $item ) { $data = $item->process->get_data(); $media = $item->process->get_media(); if ( ! $data->is_optimized() || ! $media->has_backup() ) { return; } $url = get_imagify_admin_url( 'restore-file', [ 'attachment_id' => $media->get_id(), ] ); echo $this->views->get_template( 'button/restore', [ 'url' => $url ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Prints a button to check if the file has been modified or not. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ protected function refresh_status_button( $item ) { $url = get_imagify_admin_url( 'refresh-file-modified', [ 'attachment_id' => $item->process->get_media()->get_id(), ] ); echo '<br/>'; echo $this->views->get_template( 'button/refresh-status', [ 'url' => $url ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Prints a button for the comparison tool (before / after optimization). * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ protected function comparison_tool_button( $item ) { $data = $item->process->get_data(); $media = $item->process->get_media(); if ( ! $data->is_optimized() || ! $media->has_backup() || ! $media->is_image() ) { return; } $file_path = $media->get_fullsize_path(); if ( ! $file_path ) { return; } $dimensions = $media->get_dimensions(); if ( $dimensions['width'] < 360 ) { return; } $backup_url = $media->get_backup_url(); echo $this->views->get_template( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'button/compare-images', [ 'url' => $backup_url, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'backup_url' => $backup_url, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'original_url' => $media->get_fullsize_url(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'media_id' => $media->get_id(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'width' => $dimensions['width'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'height' => $dimensions['height'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ] ); if ( wp_doing_ajax() ) { ?> <script type="text/javascript">jQuery( window ).trigger( 'comparisonprinted.imagify', [ <?php echo esc_js( $media->get_id() ); ?> ] ); </script> <?php } } /** * Add the folder_id and folder_path properties to the $item if not set yet. * It may happen if the $item doesn't come from the prepare() method. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. * @return object The current item. */ protected function maybe_set_item_folder( $item ) { if ( isset( $item->folder_path ) ) { return $item; } $item->folder_id = 0; $item->folder_path = false; $row = $item->process->get_data()->get_row(); if ( empty( $row['folder_id'] ) ) { return $item; } $folder = Imagify_Folders_DB::get_instance()->get( $row['folder_id'] ); if ( ! $folder ) { return $item; } $item->folder_id = $folder['folder_id']; $item->folder_path = $folder['path']; $item->is_folder_active = (bool) $folder['active']; return $item; } /** * Get the name of the default primary column. * * @since 1.7 * * @return string Name of the default primary column, in this case, 'title'. */ protected function get_default_primary_column_name() { return 'title'; } /** * Get a list of CSS classes for the WP_List_Table table tag. * * @since 1.7 * * @return array List of CSS classes for the table tag. */ protected function get_table_classes() { return [ 'widefat', 'fixed', 'striped', 'media', $this->_args['plural'] ]; } /** * Allow to save the screen options when submitted by the user. * * @since 1.7 * * @param bool|int $status Screen option value. Default false to skip. * @param string $option The option name. * @param int $value The number of rows to use. * @return int|bool */ public static function save_screen_options( $status, $option, $value ) { if ( self::PER_PAGE_OPTION === $option ) { return (int) $value; } return $status; } /** * Get the requested folder filter. * * @since 1.7 * * @return string */ public static function get_folder_filter() { static $filter; if ( ! isset( $filter ) ) { $filter = filter_input( INPUT_GET, 'folder-filter', FILTER_VALIDATE_INT ); $filter = max( 0, $filter ); } return $filter; } /** * Get the requested status filter. * * @since 1.7 * * @return string */ public static function get_status_filter() { static $filter; if ( isset( $filter ) ) { return $filter; } $values = [ 'optimized' => 1, 'unoptimized' => 1, 'errors' => 1, ]; $filter = isset( $_GET['status-filter'] ) ? trim( sanitize_text_field( wp_unslash( $_GET['status-filter'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended $filter = isset( $values[ $filter ] ) ? $filter : ''; return $filter; } } admin/upgrader.php 0000644 00000024324 15174671745 0010204 0 ustar 00 <?php use WP_Rocket\Logger\Logger; defined( 'ABSPATH' ) || exit; /** * Tell WP what to do when admin is loaded aka upgrader * * @since 1.0 */ function rocket_upgrader() { // Grab some infos. $actual_version = (string) get_rocket_option( 'version', '' ); // You can hook the upgrader to trigger any action when WP Rocket is upgraded. // first install. if ( ! $actual_version ) { do_action( 'wp_rocket_first_install' ); } // already installed but got updated. elseif ( WP_ROCKET_VERSION !== $actual_version ) { do_action( 'wp_rocket_upgrade', WP_ROCKET_VERSION, $actual_version ); } // If any upgrade has been done, we flush and update version number. if ( did_action( 'wp_rocket_first_install' ) || did_action( 'wp_rocket_upgrade' ) ) { flush_rocket_htaccess(); $options = get_option( WP_ROCKET_SLUG ); // do not use get_rocket_option() here. $options['version'] = WP_ROCKET_VERSION; $options['previous_version'] = $actual_version; $keys = rocket_check_key(); if ( is_array( $keys ) ) { $options = array_merge( $keys, $options ); } update_option( WP_ROCKET_SLUG, $options ); } $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'wprocket' === $page && did_action( 'wp_rocket_upgrade' ) ) { wp_safe_redirect( esc_url_raw( admin_url( 'options-general.php?page=wprocket' ) ) ); exit; } if ( ! rocket_valid_key() && current_user_can( 'rocket_manage_options' ) && 'wprocket' === $page ) { add_action( 'admin_notices', 'rocket_need_api_key' ); } } add_action( 'admin_init', 'rocket_upgrader' ); /** * Keeps this function up to date at each version * * @since 1.0 */ function rocket_first_install() { // Generate an random key for cache dir of user. $secret_cache_key = create_rocket_uniqid(); // Generate an random key for minify md5 filename. $minify_css_key = create_rocket_uniqid(); $minify_js_key = create_rocket_uniqid(); // Create Option. add_option( rocket_get_constant( 'WP_ROCKET_SLUG' ), /** * Filters the default rocket options array * * @since 2.8 * * @param array Array of default rocket options */ apply_filters( 'rocket_first_install_options', [ 'secret_cache_key' => $secret_cache_key, 'cache_mobile' => 1, 'do_caching_mobile_files' => 1, 'cache_webp' => 0, 'cache_logged_user' => 0, 'cache_ssl' => 1, 'emoji' => 1, 'cache_reject_uri' => [], 'cache_reject_cookies' => [], 'cache_reject_ua' => [], 'cache_query_strings' => [], 'cache_purge_pages' => [], 'purge_cron_interval' => 10, 'purge_cron_unit' => 'HOUR_IN_SECONDS', 'exclude_css' => [], 'exclude_js' => [], 'exclude_inline_js' => [], 'defer_all_js' => 0, 'async_css' => 0, 'critical_css' => '', 'lazyload' => 0, 'lazyload_iframes' => 0, 'lazyload_youtube' => 0, 'minify_css' => 1, 'minify_css_key' => $minify_css_key, 'minify_js' => 1, 'minify_js_key' => $minify_js_key, 'minify_concatenate_js' => 0, 'minify_google_fonts' => 1, 'manual_preload' => 1, 'dns_prefetch' => 0, 'preload_fonts' => [], 'database_revisions' => 0, 'database_auto_drafts' => 0, 'database_trashed_posts' => 0, 'database_spam_comments' => 0, 'database_trashed_comments' => 0, 'database_all_transients' => 0, 'database_optimize_tables' => 0, 'schedule_automatic_cleanup' => 0, 'automatic_cleanup_frequency' => 'daily', 'cdn' => 0, 'cdn_cnames' => [], 'cdn_zone' => [], 'cdn_reject_files' => [], 'do_cloudflare' => 0, 'cloudflare_email' => '', 'cloudflare_api_key' => '', 'cloudflare_zone_id' => '', 'cloudflare_devmode' => 0, 'cloudflare_protocol_rewrite' => 0, 'cloudflare_auto_settings' => 0, 'cloudflare_old_settings' => '', 'control_heartbeat' => 1, 'heartbeat_site_behavior' => 'reduce_periodicity', 'heartbeat_admin_behavior' => 'reduce_periodicity', 'heartbeat_editor_behavior' => 'reduce_periodicity', 'varnish_auto_purge' => 0, 'analytics_enabled' => 0, 'sucury_waf_cache_sync' => 0, 'sucury_waf_api_key' => '', ] ) ); rocket_dismiss_box( 'rocket_warning_plugin_modification' ); } add_action( 'wp_rocket_first_install', 'rocket_first_install' ); /** * What to do when Rocket is updated, depending on versions * * @since 1.0 * * @param string $wp_rocket_version Latest WP Rocket version. * @param string $actual_version Installed WP Rocket version. */ function rocket_new_upgrade( $wp_rocket_version, $actual_version ) { if ( version_compare( $actual_version, '2.4.1', '<' ) ) { delete_transient( 'rocket_ask_for_update' ); } if ( version_compare( $actual_version, '2.8', '<' ) ) { $options = get_option( WP_ROCKET_SLUG ); $options['manual_preload'] = 1; $options['automatic_preload'] = 1; $options['sitemap_preload_url_crawl'] = '500000'; update_option( WP_ROCKET_SLUG, $options ); } // Deactivate CloudFlare completely if PHP Version is lower than 5.4. if ( version_compare( $actual_version, '2.8.16', '<' ) ) { $options = get_option( WP_ROCKET_SLUG ); $options['do_cloudflare'] = 0; $options['cloudflare_email'] = ''; $options['cloudflare_api_key'] = ''; $options['cloudflare_devmode'] = 0; $options['cloudflare_protocol_rewrite'] = 0; $options['cloudflare_auto_settings'] = 0; $options['cloudflare_old_settings'] = ''; update_option( WP_ROCKET_SLUG, $options ); } // Disable minification options if they're active in Autoptimize. if ( version_compare( $actual_version, '2.9.5', '<' ) ) { if ( is_plugin_active( 'autoptimize/autoptimize.php' ) ) { if ( 'on' === get_option( 'autoptimize_css' ) ) { update_rocket_option( 'minify_css', 0 ); } if ( 'on' === get_option( 'autoptimize_js' ) ) { update_rocket_option( 'minify_js', 0 ); } } } // Delete old transients. if ( version_compare( $actual_version, '2.9.7', '<' ) ) { delete_transient( 'rocket_check_licence_30' ); delete_transient( 'rocket_check_licence_1' ); } if ( version_compare( $actual_version, '2.11', '<' ) ) { rocket_clean_minify(); } if ( version_compare( $actual_version, '3.2', '<' ) ) { // Default Heartbeat settings. $options = get_option( WP_ROCKET_SLUG, [] ); $options['heartbeat_site_behavior'] = 'reduce_periodicity'; $options['heartbeat_admin_behavior'] = 'reduce_periodicity'; $options['heartbeat_editor_behavior'] = 'reduce_periodicity'; if ( ! empty( $options['automatic_preload'] ) || ! empty( $options['sitemap_preload'] ) ) { $options['manual_preload'] = 1; } update_option( WP_ROCKET_SLUG, $options ); rocket_generate_config_file(); // Create a .htaccess file in the log folder. $handler = Logger::get_stream_handler(); if ( method_exists( $handler, 'create_htaccess_file' ) ) { try { $success = $handler->create_htaccess_file(); } catch ( \Exception $e ) { $success = false; } if ( ! $success ) { Logger::delete_log_file(); } } } if ( version_compare( $actual_version, '3.2.0.1', '<' ) ) { wp_safe_remote_get( esc_url( home_url() ) ); } if ( version_compare( $actual_version, '3.12.6', '<' ) ) { do_action( 'rocket_preload_unlock_all_urls' ); } if ( version_compare( $actual_version, '3.3.6', '<' ) ) { delete_site_transient( 'update_wprocket' ); delete_site_transient( 'update_wprocket_response' ); if ( get_rocket_option( 'do_cloudflare' ) && get_rocket_option( 'cloudflare_auto_settings' ) ) { if ( function_exists( 'set_rocket_cloudflare_browser_cache_ttl' ) ) { set_rocket_cloudflare_browser_cache_ttl( '31536000' ); } } } if ( rocket_is_ssl_website() ) { if ( 1 !== (int) get_rocket_option( 'cache_ssl' ) ) { update_rocket_option( 'cache_ssl', 1 ); rocket_generate_config_file(); } } if ( version_compare( $actual_version, '3.4', '<' ) ) { wp_clear_scheduled_hook( 'rocket_purge_time_event' ); } if ( version_compare( $actual_version, '3.6', '<' ) ) { rocket_clean_cache_busting(); rocket_clean_domain(); } if ( version_compare( $actual_version, '3.7', '<' ) ) { rocket_clean_minify( 'css' ); rocket_generate_advanced_cache_file(); } if ( version_compare( $actual_version, '3.8.1', '<' ) ) { $options = get_option( rocket_get_constant( 'WP_ROCKET_SLUG' ) ); unset( $options['dequeue_jquery_migrate'] ); update_option( rocket_get_constant( 'WP_ROCKET_SLUG' ), $options ); } if ( version_compare( $actual_version, '3.9', '<' ) ) { $busting_path = rocket_get_constant( 'WP_ROCKET_CACHE_BUSTING_PATH' ); rocket_rrmdir( $busting_path . 'facebook-tracking' ); rocket_rrmdir( $busting_path . 'google-tracking' ); wp_clear_scheduled_hook( 'rocket_facebook_tracking_cache_update' ); wp_clear_scheduled_hook( 'rocket_google_tracking_cache_update' ); } if ( version_compare( $actual_version, '3.10', '<' ) ) { $options = get_option( rocket_get_constant( 'WP_ROCKET_SLUG' ) ); if ( isset( $options['async_css'] ) && $options['async_css'] && isset( $options['remove_unused_css'] ) && $options['remove_unused_css'] ) { $options['async_css'] = 0; $cache_path = rocket_get_constant( 'WP_ROCKET_CACHE_ROOT_PATH' ); rocket_rrmdir( $cache_path . 'used-css' ); update_option( rocket_get_constant( 'WP_ROCKET_SLUG' ), $options ); } } if ( version_compare( $actual_version, '3.11.1', '<' ) ) { rocket_generate_config_file(); } if ( version_compare( $actual_version, '3.12.4', '<' ) ) { delete_transient( 'wp_rocket_pricing' ); } } add_action( 'wp_rocket_upgrade', 'rocket_new_upgrade', 10, 2 ); admin/media.php 0000644 00000005446 15174671745 0007456 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; add_filter( 'attachment_fields_to_edit', '_imagify_attachment_fields_to_edit', IMAGIFY_INT_MAX, 2 ); /** * Add "Imagify" column in the Media Uploader * * @since 1.2 * @author Jonathan Buttigieg * * @param array $form_fields An array of attachment form fields. * @param object $post The WP_Post attachment object. * @return array */ function _imagify_attachment_fields_to_edit( $form_fields, $post ) { global $pagenow; if ( 'post.php' === $pagenow ) { return $form_fields; } if ( ! imagify_get_context( 'wp' )->current_user_can( 'manual-optimize', $post->ID ) ) { return $form_fields; } $process = imagify_get_optimization_process( $post->ID, 'wp' ); $form_fields['imagify'] = [ 'label' => 'Imagify', 'input' => 'html', 'html' => get_imagify_media_column_content( $process ), 'show_in_edit' => true, 'show_in_modal' => true, ]; return $form_fields; } add_filter( 'media_row_actions', '_imagify_add_actions_to_media_list_row', IMAGIFY_INT_MAX, 2 ); /** * Add "Compare Original VS Optimized" link to the media row action * * @since 1.4.3 * @author Geoffrey Crofte * @param array $actions An array of action links for each attachment. * Default 'Edit', 'Delete Permanently', 'View'. * @param object $post WP_Post object for the current attachment. * @return array */ function _imagify_add_actions_to_media_list_row( $actions, $post ) { if ( ! imagify_get_context( 'wp' )->current_user_can( 'manual-optimize', $post->ID ) ) { return $actions; } $process = imagify_get_optimization_process( $post->ID, 'wp' ); if ( ! $process->is_valid() ) { return $actions; } $media = $process->get_media(); // If this media is not an image, do nothing. if ( ! $media->is_supported() || ! $media->is_image() ) { return $actions; } $data = $process->get_data(); // If Imagify license not valid, or image is not optimized, do nothing. if ( ! Imagify_Requirements::is_api_key_valid() || ! $data->is_optimized() ) { return $actions; } // If no backup, do nothing. if ( ! $media->has_backup() ) { return $actions; } $dimensions = $media->get_dimensions(); // If full image is too small. See get_imagify_localize_script_translations(). if ( $dimensions['width'] < 360 ) { return $actions; } // Else, add action link for comparison (JS triggered). $actions['imagify-compare'] = Imagify_Views::get_instance()->get_template( 'button/compare-images', [ 'url' => get_edit_post_link( $media->get_id() ) . '#imagify-compare', 'backup_url' => $media->get_backup_url(), 'original_url' => $media->get_fullsize_url(), 'media_id' => $media->get_id(), 'width' => $dimensions['width'], 'height' => $dimensions['height'], ] ); return $actions; } admin/upload.php 0000644 00000004670 15174671745 0007661 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; add_filter( 'manage_media_columns', '_imagify_manage_media_columns' ); /** * Add "Imagify" column in upload.php. * * @since 1.0 * @author Jonathan Buttigieg * * @param array $columns An array of columns displayed in the Media list table. * @return array */ function _imagify_manage_media_columns( $columns ) { if ( imagify_get_context( 'wp' )->current_user_can( 'optimize' ) ) { $columns['imagify_optimized_file'] = __( 'Imagify', 'imagify' ); } return $columns; } add_action( 'manage_media_custom_column', '_imagify_manage_media_custom_column', 10, 2 ); /** * Add content to the "Imagify" columns in upload.php. * * @since 1.0 * @author Jonathan Buttigieg * * @param string $column_name Name of the custom column. * @param int $attachment_id Attachment ID. */ function _imagify_manage_media_custom_column( $column_name, $attachment_id ) { if ( 'imagify_optimized_file' !== $column_name ) { return; } $process = imagify_get_optimization_process( $attachment_id, 'wp' ); echo get_imagify_media_column_content( $process ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } add_filter( 'request', '_imagify_sort_attachments_by_status' ); /** * Modify the query based on the imagify-status variable in $_GET. * * @since 1.0 * @author Jonathan Buttigieg * * @param array $vars The array of requested query variables. * @return array */ function _imagify_sort_attachments_by_status( $vars ) { if ( empty( $_GET['imagify-status'] ) || ! Imagify_Views::get_instance()->is_wp_library_page() ) { return $vars; } $status = sanitize_text_field( wp_unslash( $_GET['imagify-status'] ) ); $meta_key = '_imagify_status'; $meta_compare = '='; $relation = []; switch ( $status ) { case 'unoptimized': $meta_key = '_imagify_data'; $meta_compare = 'NOT EXISTS'; break; case 'optimized': $status = 'success'; $relation = [ 'key' => $meta_key, 'value' => 'already_optimized', 'compare' => $meta_compare, ]; break; case 'errors': $status = 'error'; break; default: return $vars; } $vars = array_merge( $vars, [ 'meta_query' => [ 'relation' => 'or', [ 'key' => $meta_key, 'value' => $status, 'compare' => $meta_compare, ], $relation, ], ] ); if ( ! key_exists( 'post_mime_type', $vars ) ) { $vars['post_mime_type'] = imagify_get_mime_types(); } return $vars; } admin/custom-folders.php 0000644 00000004706 15174671745 0011343 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); add_filter( 'upgrader_post_install', 'imagify_sync_theme_plugin_files_on_update', IMAGIFY_INT_MAX, 3 ); /** * Sync files right after a theme or plugin has been updated. * * @since 1.7 * @author Grégory Viguier * * @param bool $response Installation response. * @param array $hook_extra Extra arguments passed to hooked filters. * @param array $result Installation result data. * @return bool */ function imagify_sync_theme_plugin_files_on_update( $response, $hook_extra, $result ) { global $wpdb; if ( ( empty( $hook_extra['plugin'] ) && empty( $hook_extra['theme'] ) ) || empty( $result['destination'] ) ) { return $response; } $folders_to_sync = get_site_transient( 'imagify_themes_plugins_to_sync' ); $folders_to_sync = is_array( $folders_to_sync ) ? $folders_to_sync : []; $folders_to_sync[] = $result['destination']; set_site_transient( 'imagify_themes_plugins_to_sync', $folders_to_sync, DAY_IN_SECONDS ); return $response; } add_action( 'admin_init', 'imagify_sync_theme_plugin_files_after_update' ); /** * Sync files after some themes or plugins have been updated. * * @since 1.7.1.2 * @author Grégory Viguier */ function imagify_sync_theme_plugin_files_after_update() { global $wpdb; $folders_to_sync = get_site_transient( 'imagify_themes_plugins_to_sync' ); if ( ! $folders_to_sync || ! is_array( $folders_to_sync ) ) { return; } delete_site_transient( 'imagify_themes_plugins_to_sync' ); $folders_db = Imagify_Folders_DB::get_instance(); $files_db = Imagify_Files_DB::get_instance(); if ( ! $folders_db->can_operate() || ! $files_db->can_operate() ) { return; } foreach ( $folders_to_sync as $folder_path ) { $folder_path = trailingslashit( $folder_path ); if ( Imagify_Files_Scan::is_path_forbidden( $folder_path ) ) { // This theme or plugin must not be optimized. continue; } // Get the related folder. $placeholder = Imagify_Files_Scan::add_placeholder( $folder_path ); $folder = Imagify_Folders_DB::get_instance()->get_in( 'path', $placeholder ); if ( ! $folder ) { // This theme or plugin is not in the database. continue; } // Sync the folder files. Imagify_Custom_Folders::synchronize_files_from_folders( [ $folder['folder_id'] => [ 'folder_id' => $folder['folder_id'], 'path' => $placeholder, 'active' => $folder['active'], 'folder_path' => $folder_path, ], ] ); } } admin/meta-boxes.php 0000644 00000005537 15174671745 0010444 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); add_action( 'attachment_submitbox_misc_actions', '_imagify_attachment_submitbox_misc_actions', IMAGIFY_INT_MAX ); /** * Add a "Optimize It" button or the Imagify optimization data in the attachment submit area. * * @since 1.0 */ function _imagify_attachment_submitbox_misc_actions() { global $post; if ( ! imagify_get_context( 'wp' )->current_user_can( 'manual-optimize', $post->ID ) ) { return; } $process = imagify_get_optimization_process( $post->ID, 'wp' ); if ( ! $process->is_valid() ) { return; } $media = $process->get_media(); if ( ! $media->is_supported() ) { return; } if ( ! $media->has_required_media_data() ) { return; } $data = $process->get_data(); $views = Imagify_Views::get_instance(); if ( ! Imagify_Requirements::is_api_key_valid() && ! $data->is_optimized() ) { ?> <div class="misc-pub-section misc-pub-imagify"><h4><?php esc_html_e( 'Imagify', 'imagify' ); ?></h4></div> <div class="misc-pub-section misc-pub-imagify"> <?php esc_html_e( 'Invalid API key', 'imagify' ); ?> <br/> <a href="<?php echo esc_url( get_imagify_admin_url() ); ?>"> <?php esc_html_e( 'Check your Settings', 'imagify' ); ?> </a> </div> <?php } else { $is_locked = $process->is_locked(); if ( $is_locked ) { switch ( $is_locked ) { case 'optimizing': $lock_label = __( 'Optimizing...', 'imagify' ); break; case 'restoring': $lock_label = __( 'Restoring...', 'imagify' ); break; default: $lock_label = __( 'Processing...', 'imagify' ); } ?> <div class="misc-pub-section misc-pub-imagify"> <?php $views->print_template( 'button/processing', [ 'label' => $lock_label ] ); ?> </div> <?php } elseif ( $data->is_optimized() || $data->is_already_optimized() || $data->is_error() ) { ?> <div class="misc-pub-section misc-pub-imagify"><h4><?php esc_html_e( 'Imagify', 'imagify' ); ?></h4></div> <div class="misc-pub-section misc-pub-imagify imagify-data-item"> <?php echo get_imagify_attachment_optimization_text( $process ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </div> <?php } else { $url = get_imagify_admin_url( 'optimize', [ 'attachment_id' => $post->ID ] ); ?> <div class="misc-pub-section misc-pub-imagify"> <a class="button-primary" href="<?php echo esc_url( $url ); ?>"><?php esc_html_e( 'Optimize', 'imagify' ); ?></a> </div> <?php } } if ( $media->has_backup() && $data->is_optimized() ) { ?> <input id="imagify-full-original" type="hidden" value="<?php echo esc_url( $media->get_backup_url() ); ?>"> <input id="imagify-full-original-size" type="hidden" value="<?php echo esc_attr( $data->get_original_size() ); ?>"> <input id="imagify-full-optimized-size" type="hidden" value="<?php echo esc_attr( $data->get_optimized_size() ); ?>"> <?php } } 3rd-party/amazon-s3-and-cloudfront/classes/Main.php 0000644 00000042036 15174671745 0016176 0 ustar 00 <?php namespace Imagify\ThirdParty\AS3CF; use Imagify\Optimization\File; use Imagify\ThirdParty\AS3CF\CDN\WP\AS3 as CDN; use Imagify\Traits\InstanceGetterTrait; /** * Imagify WP Offload S3 class. * * @since 1.9 * @author Grégory Viguier */ class Main extends \Imagify_AS3CF_Deprecated { use InstanceGetterTrait; /** * AS3CF settings. * * @var array * @since 1.9 * @access protected * @author Grégory Viguier */ protected $s3_settings; /** * Filesystem object. * * @var \Imagify_Filesystem * @since 1.9 * @access protected * @author Grégory Viguier */ protected $filesystem; /** * The class constructor. * * @since 1.6.6 * @author Grégory Viguier */ protected function __construct() { $this->filesystem = \Imagify_Filesystem::get_instance(); } /** * Launch the hooks. * * @since 1.6.6 * @author Grégory Viguier */ public function init() { static $done = false; if ( $done ) { return; } $done = true; /** * WebP images to display with a <picture> tag. */ add_action( 'as3cf_init', [ $this, 'store_s3_settings' ] ); add_filter( 'imagify_webp_picture_process_image', [ $this, 'picture_tag_webp_image' ] ); /** * Register CDN. */ add_filter( 'imagify_cdn', [ $this, 'register_cdn' ], 8, 3 ); /** * Optimization process. */ add_filter( 'imagify_before_optimize_size', [ $this, 'maybe_copy_file_from_cdn_before_optimization' ], 8, 6 ); add_action( 'imagify_after_optimize', [ $this, 'maybe_send_media_to_cdn_after_optimization' ], 8, 2 ); /** * Restoration process. */ add_action( 'imagify_after_restore_media', [ $this, 'maybe_send_media_to_cdn_after_restore' ], 8, 4 ); /** * WebP support. */ add_filter( 'as3cf_attachment_file_paths', [ $this, 'add_webp_images_to_attachment' ], 8, 3 ); add_filter( 'mime_types', [ $this, 'add_webp_support' ] ); /** * Redirections. */ add_filter( 'imagify_redirect_to', [ $this, 'redirect_referrer' ] ); /** * Stats. */ add_filter( 'imagify_total_attachment_filesize', [ $this, 'add_stats_for_s3_files' ], 8, 4 ); } /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION PROCESS ==================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * On AS3CF init, store its settings. * * @since 1.9 * @access public * @author Grégory Viguier * * @param \Amazon_S3_And_CloudFront $as3cf AS3CF’s main instance. */ public function store_s3_settings( $as3cf ) { if ( method_exists( $as3cf, 'get_settings' ) ) { $this->store_s3_settings = (array) $as3cf->get_settings(); } } /** * WebP images to display with a <picture> tag. * * @since 1.9 * @see \Imagify\Picture\Display->process_image() * @author Grégory Viguier * * @param array $data An array of data for this image. * @return array */ public function picture_tag_webp_image( $data ) { global $wpdb; if ( ! empty( $data['src']['webp_path'] ) ) { // The file is local. return $data; } $match = $this->is_s3_url( $data['src']['url'] ); if ( ! $match ) { // The file is not on S3. return $data; } // Get the image ID. $post_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value = %s", // We use only year/month + filename, we should not have any subdir between them for the main file. $match['year_month'] . $match['filename'] ) ); if ( $post_id <= 0 ) { // Not in the database. return $data; } $s3_info = get_post_meta( $post_id, 'amazonS3_info', true ); $imagify_data = get_post_meta( $post_id, '_imagify_data', true ); if ( ! $s3_info || ! $imagify_data ) { return $data; } $webp_size_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::WEBP_SUFFIX' ); $webp_size_name = 'full' . $webp_size_suffix; if ( ! empty( $imagify_data['sizes'][ $webp_size_name ]['success'] ) ) { // We have a WebP image. $data['src']['webp_exists'] = true; } if ( empty( $data['srcset'] ) ) { return $data; } $meta_data = get_post_meta( $post_id, '_wp_attachment_metadata', true ); if ( empty( $meta_data['sizes'] ) ) { return $data; } // Ease the search for corresponding file name. $size_files = []; foreach ( $meta_data['sizes'] as $size_name => $size_data ) { $size_files[ $size_data['file'] ] = $size_name; } // Look for a corresponding size name. foreach ( $data['srcset'] as $i => $srcset_data ) { if ( empty( $srcset_data['webp_url'] ) ) { // Not a supported image format. continue; } if ( ! empty( $srcset_data['webp_path'] ) ) { // The file is local. continue; } $match = $this->is_s3_url( $srcset_data['url'] ); if ( ! $match ) { // Not on S3. continue; } // Try with no subdirs. $filename = $match['filename']; if ( isset( $size_files[ $filename ] ) ) { $size_name = $size_files[ $filename ]; } else { // Try with subdirs. $filename = $match['subdirs'] . $match['filename']; if ( isset( $size_files[ $filename ] ) ) { $size_name = $size_files[ $filename ]; } elseif ( preg_match( '@/\d+/$@', $match['subdirs'] ) ) { // Last try: the subdirs may contain the S3 versioning. If not the case, we can still build a pyramid with this code. $filename = preg_replace( '@/\d+/$@', '/', $match['subdirs'] ) . $match['filename']; if ( isset( $size_files[ $filename ] ) ) { $size_name = $size_files[ $filename ]; } else { continue; } } } $webp_size_name = $size_name . $webp_size_suffix; if ( ! empty( $imagify_data['sizes'][ $webp_size_name ]['success'] ) ) { // We have a WebP image. $data['srcset'][ $i ]['webp_exists'] = true; } } return $data; } /** * The CDN to use for this media. * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool|PushCDNInterface $cdn A PushCDNInterface instance. False if no CDN is used. * @param int $media_id The media ID. * @param ContextInterface $context The context object. */ public function register_cdn( $cdn, $media_id, $context ) { if ( 'wp' !== $context->get_name() ) { return $cdn; } if ( $cdn instanceof PushCDNInterface ) { return $cdn; } return new CDN( $media_id ); } /** * Before performing a file optimization, download the file from the CDN if it is missing. * * @since 1.9 * @access public * @author Grégory Viguier * * @param null|\WP_Error $response Null by default. * @param ProcessInterface $process The optimization process instance. * @param File $file The file instance. If $webp is true, $file references the non-webp file. * @param string $thumb_size The media size. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * @param bool $webp The image will be converted to WebP. * @return null|\WP_Error Null. A \WP_Error object on error. */ public function maybe_copy_file_from_cdn_before_optimization( $response, $process, $file, $thumb_size, $optimization_level, $webp ) { if ( is_wp_error( $response ) || 'wp' !== $process->get_media()->get_context() ) { return $response; } $media = $process->get_media(); $cdn = $media->get_cdn(); if ( ! $cdn instanceof CDN ) { return $response; } if ( $this->filesystem->exists( $file->get_path() ) ) { return $response; } // Get files from the CDN. $result = $cdn->get_files_from_cdn( [ $file->get_path() ] ); if ( is_wp_error( $result ) ) { return $result; } return $response; } /** * After performing a media optimization: * - Save some data, * - Upload the files to the CDN, * - Maybe delete them from the server. * * @since 1.9 * @access public * @author Grégory Viguier * * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. */ public function maybe_send_media_to_cdn_after_optimization( $process, $item ) { if ( 'wp' !== $process->get_media()->get_context() ) { return; } $media = $process->get_media(); $cdn = $media->get_cdn(); if ( ! $cdn instanceof CDN ) { return; } $cdn->send_to_cdn( ! empty( $item['data']['is_new_upload'] ) ); } /** * After restoring a media: * - Save some data, * - Upload the files to the CDN, * - Maybe delete WebP files from the CDN. * * @since 1.9 * @access public * @author Grégory Viguier * * @param ProcessInterface $process The optimization process. * @param bool|WP_Error $response The result of the operation: true on success, a WP_Error object on failure. * @param array $files The list of files, before restoring them. * @param array $data The optimization data, before deleting it. */ public function maybe_send_media_to_cdn_after_restore( $process, $response, $files, $data ) { if ( 'wp' !== $process->get_media()->get_context() ) { return; } $media = $process->get_media(); $cdn = $media->get_cdn(); if ( ! $cdn instanceof CDN ) { return; } if ( is_wp_error( $response ) ) { $error_code = $response->get_error_code(); if ( 'copy_failed' === $error_code ) { // No files have been restored. return; } // No thumbnails left? } $cdn->send_to_cdn( false ); // Remove WebP files from CDN. $webp_files = []; if ( $files ) { // Get the paths to the WebP files. foreach ( $files as $size_name => $file ) { $webp_size_name = $size_name . $process::WEBP_SUFFIX; if ( empty( $data['sizes'][ $webp_size_name ]['success'] ) ) { // This size has no WebP version. continue; } if ( 0 === strpos( $file['mime-type'], 'image/' ) ) { $webp_file = new File( $file['path'] ); if ( ! $webp_file->is_webp() ) { $webp_files[] = $webp_file->get_path_to_webp(); } } } } if ( $webp_files ) { $cdn->remove_files_from_cdn( $webp_files ); } } /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION PROCESS ==================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Add the WebP files to the list of files that the CDN must handle. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $paths A list of file paths, keyed by size name. 'file' for the full size. Includes a 'backup' size and a 'thumb' size. * @param int $attachment_id The media ID. * @param array $metadata The attachment meta data. * @return array */ public function add_webp_images_to_attachment( $paths, $attachment_id, $metadata ) { if ( ! $paths ) { // ¯\(°_o)/¯. return $paths; } $process = imagify_get_optimization_process( $attachment_id, 'wp' ); if ( ! $process->is_valid() ) { return $paths; } $media = $process->get_media(); if ( ! $media->is_image() ) { return $paths; } // Use the optimization data (the files may not be on the server). $data = $process->get_data()->get_optimization_data(); if ( empty( $data['sizes'] ) ) { return $paths; } foreach ( $paths as $size_name => $file_path ) { if ( 'thumb' === $size_name || 'backup' === $size_name || $process->is_size_next_gen( $size_name ) ) { continue; } if ( 'file' === $size_name ) { $size_name = 'full'; } $webp_size_name = $size_name . $process::WEBP_SUFFIX; if ( empty( $data['sizes'][ $webp_size_name ]['success'] ) ) { // This size has no WebP version. continue; } $file = new File( $file_path ); if ( ! $file->is_webp() ) { $paths[ $webp_size_name ] = $file->get_path_to_webp(); } } return $paths; } /** * Add WebP format to the list of allowed mime types. * * @since 1.9 * @access public * @see get_allowed_mime_types() * @author Grégory Viguier * * @param array $mime_types A list of mime types. * @return array */ public function add_webp_support( $mime_types ) { $mime_types['webp'] = 'image/webp'; return $mime_types; } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS HOOKS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * After a non-ajax optimization, remove some unnecessary arguments from the referrer used for the redirection. * Those arguments don't break anything, they're just not relevant and display obsolete admin notices. * * @since 1.6.10 * @author Grégory Viguier * * @param string $redirect The URL to redirect to. * @return string */ public function redirect_referrer( $redirect ) { return remove_query_arg( [ 'as3cfpro-action', 'as3cf_id', 'errors', 'count' ], $redirect ); } /** * Provide the file sizes and the number of thumbnails for files that are only on S3. * * @since 1.6.7 * @author Grégory Viguier * * @param bool $size_and_count False by default. * @param int $image_id The attachment ID. * @param array $files An array of file paths with thumbnail sizes as keys. * @param array $image_ids An array of all attachment IDs. * @return bool|array False by default. Provide an array with the keys 'filesize' (containing the total filesize) and 'thumbnails' (containing the number of thumbnails). */ public function add_stats_for_s3_files( $size_and_count, $image_id, $files, $image_ids ) { static $data; if ( is_array( $size_and_count ) ) { return $size_and_count; } if ( $this->filesystem->exists( $files['full'] ) ) { // If the full size is on the server, that probably means all files are on the server too. return $size_and_count; } if ( ! isset( $data ) ) { $data = \Imagify_DB::get_metas( [ // Get the filesizes. 's3_filesize' => 'wpos3_filesize_total', ], $image_ids ); $data = array_map( 'absint', $data['s3_filesize'] ); } if ( empty( $data[ $image_id ] ) ) { // The file is not on S3. return $size_and_count; } // We can't take the disallowed sizes into account here. return [ 'filesize' => (int) $data[ $image_id ], 'thumbnails' => count( $files ) - 1, ]; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if an URL is a S3 one. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $url The URL to test. * @return array|bool { * An array if an S3 URL. False otherwise. * * @type string $key Bucket key. Ex: subdir/wp-content/uploads/2019/02/13142432/foobar-480x510.jpg. * @type string $year_month The uploads year/month folders. Ex: 2019/02/. * @type string $subdirs Sub-directories between year/month folders and the filename. * It can be the S3 versioning folder, any folder added by a plugin, or both. * There is no way to know which one it is. Ex: foo/13142432/. * @type string $filename The file name. Ex: foobar-480x510.jpg. * } */ public function is_s3_url( $url ) { static $uploads_dir; static $domain; /** * Tell if an URL is a S3 one. * * @since 1.9 * @author Grégory Viguier * * @param null|array|bool $is Null by default. Must return an array if an S3 URL, or false if not. * @param string $url The URL to test. */ $is = apply_filters( 'imagify_as3cf_is_s3_url', null, $url ); if ( false === $is ) { return false; } if ( is_array( $is ) ) { return imagify_merge_intersect( $is, [ 'key' => '', 'year_month' => '', 'subdirs' => '', 'filename' => '', ] ); } if ( ! isset( $uploads_dir ) ) { $uploads_dir = wp_parse_url( $this->filesystem->get_upload_baseurl() ); $uploads_dir = trim( $uploads_dir['path'], '/' ) . '/'; } if ( ! isset( $domain ) ) { if ( ! empty( $this->store_s3_settings['cloudfront'] ) ) { $domain = sanitize_text_field( $this->store_s3_settings['cloudfront'] ); $domain = preg_replace( '@^(?:https?:)?//@', '//', $domain ); $domain = preg_quote( $domain, '@' ); } else { $domain = 's3-.+\.amazonaws\.com/[^/]+/'; } } $pattern = '@^(?:https?:)?//' . $domain . '/(?<key>' . $uploads_dir . '(?<year_month>\d{4}/\d{2}/)?(?<subdirs>.+/)?(?<filename>[^/]+))$@i'; if ( ! preg_match( $pattern, $url, $match ) ) { return false; } unset( $match[0] ); return array_merge( [ 'year_month' => '', 'subdirs' => '', ], $match ); } } 3rd-party/amazon-s3-and-cloudfront/classes/CDN/WP/AS3.php 0000644 00000032706 15174671745 0016635 0 ustar 00 <?php namespace Imagify\ThirdParty\AS3CF\CDN\WP; use Imagify\CDN\PushCDNInterface; use Imagify\Context\ContextInterface; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * AS3 CDN for WP context. * * @since 1.9 * @author Grégory Viguier */ class AS3 implements PushCDNInterface { /** * The media ID. * * @var int * @since 1.9 * @access protected * @author Grégory Viguier */ protected $id; /** * Filesystem object. * * @var object Imagify_Filesystem * @since 1.9 * @access protected * @author Grégory Viguier */ protected $filesystem; /** * Tell if we’re playing in WP 5.3’s garden. * * @var bool * @since 1.9.8 * @access protected * @author Grégory Viguier */ protected $is_wp53; /** * Constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int $media_id The media ID. */ public function __construct( $media_id ) { $this->id = (int) $media_id; $this->filesystem = \Imagify_Filesystem::get_instance(); } /** * Tell if the CDN is ready (not necessarily reachable). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_ready() { global $as3cf; static $is; if ( ! isset( $is ) ) { $is = $as3cf && $as3cf->is_plugin_setup(); } return $is; } /** * Tell if the media is on the CDN. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function media_is_on_cdn() { return (bool) $this->get_cdn_info(); } /** * Get files from the CDN. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $file_paths A list of file paths. * @return bool|\WP_Error True on success. A \WP_error object on failure. */ public function get_files_from_cdn( $file_paths ) { global $as3cf; if ( ! $this->is_ready() ) { return new \WP_Error( 'not_ready', __( 'CDN is not set up.', 'imagify' ) ); } $cdn_info = $this->get_cdn_info(); if ( ! $cdn_info ) { // The media is not on the CDN. return new \WP_Error( 'not_on_cdn', __( 'This media could not be found on the CDN.', 'imagify' ) ); } $directory = $this->filesystem->dir_path( $cdn_info['key'] ); $directory = $this->filesystem->is_root( $directory ) ? '' : $directory; $new_method = method_exists( $as3cf->plugin_compat, 'copy_s3_file_to_server' ); $errors = []; foreach ( $file_paths as $file_path ) { $cdn_info['key'] = $directory . $this->filesystem->file_name( $file_path ); // Retrieve file from the CDN. if ( $new_method ) { $as3cf->plugin_compat->copy_s3_file_to_server( $cdn_info, $file_path ); } else { $as3cf->plugin_compat->copy_provider_file_to_server( $cdn_info, $file_path ); } if ( ! $this->filesystem->exists( $file_path ) ) { $errors[] = $file_path; } } if ( $errors ) { $nbr_errors = count( $errors ); $errors_txt = array_map( [ $this->filesystem, 'make_path_relative' ], $errors ); $errors_txt = wp_sprintf_l( '%l', $errors_txt ); return new \WP_Error( 'not_retrieved', sprintf( /* translators: %s is a list of file paths. */ _n( 'The following file could not be retrieved from the CDN: %s.', 'The following files could not be retrieved from the CDN: %s.', $nbr_errors, 'imagify' ), $errors_txt ), $errors ); } return true; } /** * Remove files from the CDN. * Don't use this to empty a folder. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $file_paths A list of file paths. Those paths are not necessary absolute, and can be also file names. * @return bool|\WP_Error True on success. A \WP_error object on failure. */ public function remove_files_from_cdn( $file_paths ) { global $as3cf; if ( ! $this->is_ready() ) { return new \WP_Error( 'not_ready', __( 'CDN is not set up.', 'imagify' ) ); } $cdn_info = $this->get_cdn_info(); if ( ! $cdn_info ) { // The media is not on the CDN. return new \WP_Error( 'not_on_cdn', __( 'This media could not be found on the CDN.', 'imagify' ) ); } $directory = $this->filesystem->dir_path( $cdn_info['key'] ); $directory = $this->filesystem->is_root( $directory ) ? '' : $directory; if ( method_exists( $as3cf, 'get_s3object_region' ) ) { $region = $as3cf->get_s3object_region( $cdn_info ); } else { $region = $as3cf->get_provider_object_region( $cdn_info ); } if ( is_wp_error( $region ) ) { $region = ''; } $to_remove = []; foreach ( $file_paths as $file_path ) { $to_remove[] = [ 'Key' => $directory . $this->filesystem->file_name( $file_path ), ]; } if ( method_exists( $as3cf, 'delete_s3_objects' ) ) { $result = $as3cf->delete_s3_objects( $region, $cdn_info['bucket'], $to_remove, false, false, false ); } else { $result = $as3cf->delete_objects( $region, $cdn_info['bucket'], $to_remove, false, false, false ); } if ( is_wp_error( $result ) ) { return new \WP_Error( 'deletion_failed', __( 'File(s) could not be removed from the CDN.', 'imagify' ) ); } return true; } /** * Send all files from a media to the CDN. * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $is_new_upload Tell if the current media is a new upload. If not, it means it's a media being regenerated, restored, etc. * @return bool|\WP_Error True/False if sent or not. A \WP_error object on failure. */ public function send_to_cdn( $is_new_upload ) { global $as3cf; if ( ! $this->is_ready() ) { return new \WP_Error( 'not_ready', __( 'CDN is not set up.', 'imagify' ) ); } if ( ! $this->can_send_to_cdn( $is_new_upload ) ) { return false; } // Retrieve the missing files from the CDN: we must send all of them at once, even those that have not been modified. $file_paths = \AS3CF_Utils::get_attachment_file_paths( $this->id, false ); if ( $file_paths ) { foreach ( $file_paths as $size => $file_path ) { if ( $this->filesystem->exists( $file_path ) ) { // Keep only the files that don't exist. unset( $file_paths[ $size ] ); } } } if ( $file_paths ) { $result = $this->get_files_from_cdn( $file_paths ); if ( is_wp_error( $result ) ) { return $result; } } $remove_local_files = $this->should_delete_files( $is_new_upload ); if ( ! $is_new_upload ) { if ( $remove_local_files ) { // Force files deletion when not a new media. add_filter( 'as3cf_get_setting', [ $this, 'force_local_file_removal_setting' ], 100, 2 ); add_filter( 'as3cf_setting_remove-local-file', 'imagify_return_true', 100 ); } else { // Force to keep the files when not a new media. add_filter( 'as3cf_get_setting', [ $this, 'force_local_file_keep_setting' ], 100, 2 ); add_filter( 'as3cf_setting_remove-local-file', 'imagify_return_false', 100 ); } } if ( method_exists( $as3cf, 'upload_attachment_to_s3' ) ) { $result = $as3cf->upload_attachment_to_s3( $this->id, null, null, false, $remove_local_files ); } else { $result = $as3cf->upload_attachment( $this->id, null, null, false, $remove_local_files ); } if ( ! $is_new_upload ) { if ( $remove_local_files ) { remove_filter( 'as3cf_get_setting', [ $this, 'force_local_file_removal_setting' ], 100 ); remove_filter( 'as3cf_setting_remove-local-file', 'imagify_return_true', 100 ); } else { remove_filter( 'as3cf_get_setting', [ $this, 'force_local_file_keep_setting' ], 100 ); remove_filter( 'as3cf_setting_remove-local-file', 'imagify_return_false', 100 ); } } if ( is_wp_error( $result ) ) { return $result; } return true; } /** * Get a file URL. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $file_name Name of the file. Leave empty for the full size file. * @return string URL to the file. */ public function get_file_url( $file_name = false ) { $file_url = wp_get_attachment_url( $this->id ); if ( $file_name ) { // It's not the full size. $file_url = $this->filesystem->dir_path( $file_url ) . $file_name; } return $file_url; } /** * Get a file path. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $file_name Name of the file. Leave empty for the full size file. Use 'original' to get the path to the original file. * @return string Path to the file. */ public function get_file_path( $file_name = false ) { if ( ! $file_name ) { // Full size. return get_attached_file( $this->id, true ); } if ( 'original' === $file_name ) { // Original file. if ( $this->is_wp_53() ) { // `wp_get_original_image_path()` may return false. $file_path = wp_get_original_image_path( $this->id ); } else { $file_path = false; } if ( ! $file_path ) { $file_path = get_attached_file( $this->id, true ); } return $file_path; } // Thumbnail. $file_path = get_attached_file( $this->id, true ); $file_path = $this->filesystem->dir_path( $file_path ) . $file_name; return $file_path; } /** ----------------------------------------------------------------------------------------- */ /** INTERNAL TOOLS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Filter the CDN setting 'remove-local-file': this is used to force deletion when the media is not a new one. * Caution to not use $this->should_delete_files() when this filter is used! * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $setting The setting value. * @param string $key The setting name. * @return bool */ public function force_local_file_removal_setting( $setting, $key ) { if ( 'remove-local-file' === $key ) { return true; } return $setting; } /** * Filter the CDN setting 'remove-local-file': this is used to force not-deletion when the media is not a new one. * Caution to not use $this->should_delete_files() when this filter is used! * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $setting The setting value. * @param string $key The setting name. * @return bool */ public function force_local_file_keep_setting( $setting, $key ) { if ( 'remove-local-file' === $key ) { return false; } return $setting; } /** * Tell if a media is stored on the CDN. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return array|bool The CDN info on success. False if the media is not on the CDN. */ protected function get_cdn_info() { global $as3cf; if ( ! $as3cf ) { return false; } if ( method_exists( $as3cf, 'get_attachment_s3_info' ) ) { return $as3cf->get_attachment_s3_info( $this->id ); } return $as3cf->get_attachment_provider_info( $this->id ); } /** * Tell if a media can (and should) be sent to the CDN. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param bool $is_new_upload Tell if the current media is a new upload. If not, it means it's a media being regenerated, restored, etc. * @return bool */ protected function can_send_to_cdn( $is_new_upload ) { global $as3cf; static $can = []; static $cdn_setting; if ( isset( $can[ $this->id ] ) ) { return $can[ $this->id ]; } if ( ! $this->is_ready() ) { $can[ $this->id ] = false; return $can[ $this->id ]; } if ( ! isset( $cdn_setting ) ) { $cdn_setting = $as3cf && $as3cf->get_setting( 'copy-to-s3' ); } // The CDN is set up: test if the media is on it. $can[ $this->id ] = $this->media_is_on_cdn(); if ( $can[ $this->id ] && $is_new_upload ) { // Use the CDN setting to tell if we're allowed to send the files (should be true since it's a new upload and it's already there). $can[ $this->id ] = $cdn_setting; } /** * Tell if a media can (and should) be sent to the CDN. * * @since 1.9 * @author Grégory Viguier * * @param bool $can True if the media can be sent. False otherwize. * @param PushCDNInterface $cdn The CDN instance. * @param bool $cdn_setting CDN setting that tells if a new media can be sent to the CDN. * @param bool $is_new_upload Tell if the current media is a new upload. If not, it means it's a media being regenerated, restored, etc. */ $can[ $this->id ] = (bool) apply_filters( 'imagify_can_send_to_cdn', $can[ $this->id ], $this, $cdn_setting, $is_new_upload ); return $can[ $this->id ]; } /** * Tell if the files should be deleted after optimization. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param bool $is_new_upload Tell if the current media is a new upload. If not, it means it's a media being regenerated, restored, etc. * @return bool */ protected function should_delete_files( $is_new_upload ) { global $as3cf; if ( $is_new_upload ) { return (bool) $as3cf->get_setting( 'remove-local-file' ); } // If the attachment has a 'filesize' metadata, that means the local files are meant to be deleted. return (bool) get_post_meta( $this->id, 'wpos3_filesize_total', true ); } /** * Tell if we’re playing in WP 5.3’s garden. * * @since 1.9.8 * @access protected * @author Grégory Viguier * * @return bool */ protected function is_wp_53() { if ( isset( $this->is_wp53 ) ) { return $this->is_wp53; } $this->is_wp53 = function_exists( 'wp_get_original_image_path' ); return $this->is_wp53; } } 3rd-party/amazon-s3-and-cloudfront/amazon-s3-and-cloudfront.php 0000644 00000003155 15174671745 0020401 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Tell if WP Offload S3 compatibility is loaded. * * @since 1.7 * @author Grégory Viguier * * @return bool */ function imagify_load_as3cf_compat() { if ( function_exists( 'as3cf_init' ) ) { // WP Offload S3 Lite. $version = ! empty( $GLOBALS['aws_meta']['amazon-s3-and-cloudfront']['version'] ) ? $GLOBALS['aws_meta']['amazon-s3-and-cloudfront']['version'] : false; if ( ! $version ) { return false; } if ( ! function_exists( 'amazon_web_services_init' ) && version_compare( $version, '1.3' ) < 0 ) { // Old version, plugin Amazon Web Services is required. return false; } if ( version_compare( $version, '2.3' ) >= 0 ) { // A new version that removes a punlic method. return false; } return true; } if ( function_exists( 'as3cf_pro_init' ) ) { // WP Offload S3 Pro. $version = ! empty( $GLOBALS['aws_meta']['amazon-s3-and-cloudfront-pro']['version'] ) ? $GLOBALS['aws_meta']['amazon-s3-and-cloudfront-pro']['version'] : false; if ( ! $version ) { return false; } if ( ! function_exists( 'amazon_web_services_init' ) && version_compare( $version, '1.6' ) < 0 ) { // Old version, plugin Amazon Web Services is required. return false; } if ( version_compare( $version, '2.3' ) >= 0 ) { // A new version that removes a punlic method. return false; } return true; } return false; } if ( imagify_load_as3cf_compat() ) : class_alias( '\\Imagify\\ThirdParty\\AS3CF\\Main', '\\Imagify_AS3CF' ); add_action( 'imagify_loaded', [ \Imagify\ThirdParty\AS3CF\Main::get_instance(), 'init' ], 1 ); endif; 3rd-party/WooCommerce/class-woocommerce.php 0000644 00000004325 15174671745 0014770 0 ustar 00 <?php namespace Imagify\ThirdParty\WooCommerce; /** * Compatibility for WooCommerce. * * @since 1.10.0 */ class WooCommerce { /** * Initialize compatibility functionality. * * @since 1.10.0 * * @return void */ public function init() { add_action( 'woocommerce_single_product_summary', [ $this, 'variable_products_nextgen_compat' ] ); } /** * Add Variable Products Next-gen images Compatibility. * * @since 1.10.0 * * @return void */ public function variable_products_nextgen_compat() { global $product; if ( ! isset( $product ) || ! $product->is_type( 'variable' ) ) { return; } add_filter( 'imagify_picture_attributes', [ $this, 'remove_wp_post_image_class' ], 10, 2 ); add_filter( 'imagify_picture_source_attributes', [ $this, 'maybe_add_wp_post_image_class_on_picture_internal_tags' ], 10, 2 ); add_filter( 'imagify_picture_img_attributes', [ $this, 'maybe_add_wp_post_image_class_on_picture_internal_tags' ], 10, 2 ); } /** * Remove wp-post-image class from picture tags. * * @since 1.10.0 * * @param array $attributes The picture tag attributes. * * @return array The picture tage attributes with modified or removed 'class'. */ public function remove_wp_post_image_class( $attributes ) { if ( isset( $attributes['class'] ) ) { $attributes['class'] = str_replace( 'wp-post-image', '', $attributes['class'] ); } if ( empty( $attributes['class'] ) ) { unset( $attributes['class'] ); } return $attributes; } /** * Add wp-post-image class to source and image tags internal to a picture tag. * * @since 1.10.0 * * @param array $attributes The source or img tag attributes. * @param array $image The original image tag data. * * @return array Source or image tag attributes with modified 'class'. */ public function maybe_add_wp_post_image_class_on_picture_internal_tags( $attributes, $image ) { if ( ! empty( $image['attributes']['class'] ) && strpos( $image['attributes']['class'], 'wp-post-image' ) !== false ) { $attributes['class'] = isset( $attributes['class'] ) ? $attributes['class'] . ' wp-post-image' : 'wp-post-image'; } return $attributes; } } ( new WooCommerce() )->init(); 3rd-party/screets-lc.php 0000644 00000002005 15174671745 0011164 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( function_exists( 'fn_lc_fix_ssl_upload_url' ) && defined( 'SLC_VERSION' ) && version_compare( SLC_VERSION, '2.2.8' ) < 0 ) : /** * Fixes a bug in Screets Live Chat plugin (prior version 2.2.8), preventing wp_get_upload_dir() to work properly. */ remove_filter( 'upload_dir', 'fn_lc_fix_ssl_upload_url' ); add_filter( 'upload_dir', 'imagify_screets_lc_fix_ssl_upload_url' ); /** * Filters the uploads directory data to force https URLs. * * @since 1.6.7 * @author Grégory Viguier * * @param array $uploads Array of upload directory data with keys of 'path', 'url', 'subdir, 'basedir', 'baseurl', and 'error'. * @return array */ function imagify_screets_lc_fix_ssl_upload_url( $uploads ) { if ( false !== $uploads['error'] || ! is_ssl() ) { return $uploads; } $uploads['url'] = str_replace( 'http://', 'https://', $uploads['url'] ); $uploads['baseurl'] = str_replace( 'http://', 'https://', $uploads['baseurl'] ); return $uploads; } endif; 3rd-party/formidable-pro/classes/Main.php 0000644 00000003050 15174671745 0014344 0 ustar 00 <?php namespace Imagify\ThirdParty\FormidablePro; use Imagify\Traits\InstanceGetterTrait; /** * Compat class for Formidable Forms Pro plugin. * Each call to `new WP_Query()` made by Imagify must have a `'is_imagify' => true` argument. * * @since 1.6.13 * @author Grégory Viguier */ class Main { use InstanceGetterTrait; /** * Class version. * * @var string */ const VERSION = '1.1'; /** * Set to true when the current query comes from Imagify. * * @var int */ protected $is_imagify; /** * Launch the hooks. * * @since 1.6.13 * @author Grégory Viguier */ public function init() { add_action( 'parse_query', [ $this, 'maybe_remove_media_library_filter' ] ); add_action( 'posts_selection', [ $this, 'maybe_put_media_library_filter_back' ] ); } /** * Fires before the 'pre_get_posts' hook. * * @since 1.6.13 * @author Grégory Viguier * * @param object $wp_query The WP_Query instance (passed by reference). */ public function maybe_remove_media_library_filter( $wp_query ) { if ( ! empty( $wp_query->query_vars['is_imagify'] ) && class_exists( 'FrmProFileField' ) ) { $this->is_imagify = true; remove_action( 'pre_get_posts', 'FrmProFileField::filter_media_library', 99 ); } else { $this->is_imagify = false; } } /** * Fires after the 'pre_get_posts' hook. * * @since 1.6.13 * @author Grégory Viguier */ public function maybe_put_media_library_filter_back() { if ( $this->is_imagify ) { add_action( 'pre_get_posts', 'FrmProFileField::filter_media_library', 99 ); } } } 3rd-party/formidable-pro/formidable-pro.php 0000644 00000000437 15174671745 0014733 0 ustar 00 <?php use Imagify\ThirdParty\FormidablePro; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); if ( class_exists( 'FrmProEddController' ) ) : class_alias( '\\Imagify\\ThirdParty\\FormidablePro\\Main', '\\Imagify_Formidable_Pro' ); FormidablePro\Main::get_instance()->init(); endif; 3rd-party/enable-media-replace/classes/Main.php 0000644 00000010120 15174671745 0015352 0 ustar 00 <?php namespace Imagify\ThirdParty\EnableMediaReplace; use Imagify\Traits\InstanceGetterTrait; use Imagify_Enable_Media_Replace_Deprecated; use Imagify_Filesystem; /** * Compat class for Enable Media Replace plugin. * * @since 1.6.9 */ class Main extends Imagify_Enable_Media_Replace_Deprecated { use InstanceGetterTrait; /** * The media ID. * * @var int * @since 1.9 */ protected $media_id; /** * The process instance for the current attachment. * * @var ProcessInterface * @since 1.9 */ protected $process; /** * The path to the old backup file. * * @var string * @since 1.6.9 */ protected $old_backup_path; /** * List of paths to the old next-gen files. * * @var array * @since 1.9.8 */ protected $old_nextgen_paths = []; /** * Launch the hooks before the files and data are replaced. * * @since 1.6.9 * @since 1.9.10 The parameter changed from boolean to array. The method doesn’t return anything. * * @param array $args An array containing the post ID. */ public function init( $args = [] ) { if ( is_array( $args ) && ! empty( $args['post_id'] ) ) { $this->media_id = $args['post_id']; } else { // Backward compatibility. $this->media_id = (int) filter_input( INPUT_POST, 'ID' ); $this->media_id = max( 0, $this->media_id ); if ( ! $this->media_id ) { return; } } // Store the old backup file path. $this->get_process(); if ( ! $this->process ) { $this->media_id = 0; return; } $this->old_backup_path = $this->process->get_media()->get_backup_path(); if ( ! $this->old_backup_path ) { $this->media_id = 0; return; } /** * Keep track of existing next-gen files. * * Whether the user chooses to rename the files or not, we will need to delete the current next-gen files before creating new ones: * - Rename the files: the old ones must be removed, they are useless now. * - Do not rename the files: the thumbnails may still get new names because of the suffix containing the image dimensions, which may differ (for example when thumbnails are scaled, not cropped). * In this last case, the thumbnails with the old dimensions are removed from the drive and from the WP’s post meta, so there is no need of keeping orphan next-gen files that would stay on the drive for ever, even after the attachment is deleted from WP. */ foreach ( $this->process->get_media()->get_media_files() as $media_file ) { foreach ( [ 'avif', 'webp' ] as $format ) { $this->old_nextgen_paths[] = imagify_path_to_nextgen( $media_file['path'], $format ); } } // Delete the old backup file and old next-gen files. add_action( 'imagify_before_auto_optimization', [ $this, 'delete_backup' ] ); add_action( 'imagify_not_optimized_attachment_updated', [ $this, 'delete_backup' ] ); } /** * Delete previous backup file and next-gen files. * This is done after the images have been already replaced by Enable Media Replace. * * @since 1.8.4 * * @param int $media_id The attachment ID. */ public function delete_backup( $media_id ) { if ( ! $this->old_backup_path || ! $this->media_id || $media_id !== $this->media_id ) { return; } $filesystem = Imagify_Filesystem::get_instance(); if ( $filesystem->exists( $this->old_backup_path ) ) { // Delete old backup file. $filesystem->delete( $this->old_backup_path ); $this->old_backup_path = false; } if ( ! empty( $this->old_nextgen_paths ) ) { // Delete old next-gen files. $this->old_nextgen_paths = array_filter( $this->old_nextgen_paths, [ $filesystem, 'exists' ] ); array_map( [ $filesystem, 'delete' ], $this->old_nextgen_paths ); $this->old_nextgen_paths = []; } } /** * Get the optimization process corresponding to the current media. * * @since 1.9 * * @return ProcessInterface|bool False if invalid. */ protected function get_process() { if ( isset( $this->process ) ) { return $this->process; } $this->process = imagify_get_optimization_process( $this->media_id, 'wp' ); if ( ! $this->process->is_valid() ) { $this->process = false; } return $this->process; } } 3rd-party/enable-media-replace/enable-media-replace.php 0000644 00000000613 15174671745 0016753 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); if ( function_exists( 'enable_media_replace' ) || class_exists( '\\EnableMediaReplace\\EnableMediaReplacePlugin' ) ) : class_alias( '\\Imagify\\ThirdParty\\EnableMediaReplace\\Main', '\\Imagify_Enable_Media_Replace' ); add_filter( 'wp_handle_replace', [ \Imagify\ThirdParty\EnableMediaReplace\Main::get_instance(), 'init' ] ); endif; 3rd-party/regenerate-thumbnails/classes/Main.php 0000644 00000026456 15174671745 0015746 0 ustar 00 <?php namespace Imagify\ThirdParty\RegenerateThumbnails; use Imagify_Requirements; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class that handles compatibility with Regenerate Thumbnails plugin. * * @since 1.7.1 * @author Grégory Viguier */ class Main extends \Imagify_Regenerate_Thumbnails_Deprecated { use \Imagify\Traits\InstanceGetterTrait; /** * Class version. * * @var string * @since 1.7.1 * @author Grégory Viguier */ const VERSION = '1.2'; /** * Action used for the ajax callback. * * @var string * @since 1.9 * @author Grégory Viguier */ const HOOK_SUFFIX = 'regenerate_thumbnails'; /** * List of optimization processes. * * @var array An array of ProcessInterface objects. The array keys are the media IDs. * @since 1.7.1 * @access protected * @author Grégory Viguier */ protected $processes = []; /** * Tell if we’re playing in WP 5.3’s garden. * * @var bool * @since 1.9.8 * @access protected * @author Grégory Viguier */ protected $is_wp53; /** ----------------------------------------------------------------------------------------- */ /** INIT ==================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Launch the hooks. * * @since 1.7.1 * @access public * @author Grégory Viguier */ public function init() { if ( ! class_exists( '\RegenerateThumbnails_Regenerator' ) ) { return; } add_filter( 'rest_dispatch_request', [ $this, 'maybe_init_attachment' ], 4, 4 ); add_action( 'imagify_after_' . static::HOOK_SUFFIX, [ $this, 'after_regenerate_thumbnails' ], 8, 2 ); } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Filter the REST dispatch request result, to hook before Regenerate Thumbnails starts its magic. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param bool $dispatch_result Dispatch result, will be used if not empty. * @param WP_REST_Request $request Request used to generate the response. * @param string $route Route matched for the request. * @param array $handler Route handler used for the request. * @return bool */ public function maybe_init_attachment( $dispatch_result, $request, $route = null, $handler = null ) { if ( strpos( $route, static::get_route_prefix() ) === false ) { return $dispatch_result; } $media_id = $request->get_param( 'id' ); if ( ! $this->set_process( $media_id ) ) { return $dispatch_result; } $media_id = $this->get_process( $media_id )->get_media()->get_id(); // The attachment can be regenerated: keep the optimized full-sized file safe, and replace it by the backup file. $this->backup_optimized_file( $media_id ); // Prevent automatic optimization. \Imagify_Auto_Optimization::prevent_optimization( $media_id ); // Launch the needed hook. add_filter( 'wp_generate_attachment_metadata', [ $this, 'launch_async_optimization' ], IMAGIFY_INT_MAX - 30, 2 ); return $dispatch_result; } /** * Auto-optimize after an attachment is regenerated. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param array $metadata An array of attachment meta data, containing the sizes that have been regenerated. * @param int $media_id Current media ID. * @return array */ public function launch_async_optimization( $metadata, $media_id ) { $process = $this->get_process( $media_id ); if ( ! $process ) { return $metadata; } $sizes = isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ? $metadata['sizes'] : []; $media = $process->get_media(); $fullsize_path = $media->get_raw_fullsize_path(); if ( $fullsize_path ) { $original_path = $media->get_original_path(); if ( $original_path && $this->is_wp_53() && $original_path !== $fullsize_path ) { /** * The original file and the full-sized file are not the same: * That means wp_generate_attachment_metadata() recreated the full-sized file, based on the original one. * So it must be optimized again now. */ $sizes['full'] = []; } } if ( ! $sizes ) { // Put the optimized full-sized file back. $this->put_optimized_file_back( $media_id ); return $metadata; } /** * Optimize the sizes that have been regenerated. */ // If the media has next-gen versions, recreate them for the sizes that have been regenerated. $data = $process->get_data(); $optimization_data = $data->get_optimization_data(); if ( ! empty( $optimization_data['sizes'] ) ) { foreach ( $optimization_data['sizes'] as $size_name => $size_data ) { $non_nextgen_size_name = $process->is_size_next_gen( $size_name ); if ( ! $non_nextgen_size_name || ! isset( $sizes[ $non_nextgen_size_name ] ) ) { continue; } // Add the next-gen size. $sizes[ $size_name ] = []; } } $sizes = array_keys( $sizes ); $optimization_level = $data->get_optimization_level(); $optimization_args = [ 'hook_suffix' => static::HOOK_SUFFIX ]; // Delete related optimization data or nothing will be optimized. $data->delete_sizes_optimization_data( $sizes ); $process->optimize_sizes( $sizes, $optimization_level, $optimization_args ); $this->unset_process( $media_id ); return $metadata; } /** * Fires after regenerating the thumbnails. * This puts the full-sized optimized file back. * * @since 1.9 * @access public * @author Grégory Viguier * * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. See $this->task(). */ public function after_regenerate_thumbnails( $process, $item ) { $media_id = $process->get_media()->get_id(); $this->processes[ $media_id ] = $process; $this->put_optimized_file_back( $media_id ); } /** ----------------------------------------------------------------------------------------- */ /** INTERNAL TOOLS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Set an optimization process. * * @since 1.9 * @author Grégory Viguier * @access protected * * @param int $media_id The media ID. * @return bool */ protected function set_process( $media_id ) { if ( ! $media_id || ! Imagify_Requirements::is_api_key_valid() ) { return false; } $process = imagify_get_optimization_process( $media_id, 'wp' ); if ( ! $process->is_valid() || ! $process->get_media()->is_image() || ! $process->get_data()->is_optimized() ) { // Invalid, not animage, or no optimization have been attempted yet. return false; } $this->processes[ $media_id ] = $process; return true; } /** * Unset an optimization process. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param int $media_id The media ID. */ protected function unset_process( $media_id ) { unset( $this->processes[ $media_id ] ); } /** * Unset an optimization process. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param int $media_id The media ID. * @return ProcessInterface|bool An optimization process object. False on failure. */ protected function get_process( $media_id ) { return ! empty( $this->processes[ $media_id ] ) ? $this->processes[ $media_id ] : false; } /** * Backup the optimized full-sized file and replace it by the original backup file. * * @since 1.7.1 * @access protected * @author Grégory Viguier * * @param int $media_id Media ID. */ protected function backup_optimized_file( $media_id ) { $media = $this->get_process( $media_id )->get_media(); $fullsize_path = $media->get_raw_fullsize_path(); if ( ! $fullsize_path ) { // Uh? return; } $original_path = $media->get_original_path(); if ( $original_path && $this->is_wp_53() && $original_path !== $fullsize_path ) { /** * The original file and the full-sized file are not the same: * That means wp_generate_attachment_metadata() will recreate the full-sized file, based on the original one. * Then, the thumbnails will be created from a newly created (unoptimized) file. */ return; } $backup_path = $media->get_backup_path(); if ( ! $backup_path ) { // No backup file, too bad. return; } /** * Replace the optimized full-sized file by the backup, so any optimization will not use an optimized file, but the original one. * The optimized full-sized file is kept and renamed, and will be put back in place at the end of the optimization process. */ $filesystem = \Imagify_Filesystem::get_instance(); if ( $filesystem->exists( $fullsize_path ) ) { $tmp_file_path = static::get_temporary_file_path( $fullsize_path ); $moved = $filesystem->move( $fullsize_path, $tmp_file_path, true ); } $filesystem->copy( $backup_path, $fullsize_path ); } /** * Put the optimized full-sized file back. * * @since 1.7.1 * @since 1.9 Replaced $attachment parameter by $media_id. * @access protected * @author Grégory Viguier * * @param int $media_id Media ID. */ protected function put_optimized_file_back( $media_id ) { $file_path = $this->get_process( $media_id )->get_media()->get_raw_fullsize_path(); $tmp_file_path = static::get_temporary_file_path( $file_path ); $filesystem = \Imagify_Filesystem::get_instance(); if ( $filesystem->exists( $tmp_file_path ) ) { $moved = $filesystem->move( $tmp_file_path, $file_path, true ); } } /** * Tell if we’re playing in WP 5.3’s garden. * * @since 1.9.8 * @access protected * @author Grégory Viguier * * @return bool */ protected function is_wp_53() { if ( isset( $this->is_wp53 ) ) { return $this->is_wp53; } $this->is_wp53 = function_exists( 'wp_get_original_image_path' ); return $this->is_wp53; } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC TOOLS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the beginning of the route used to regenerate thumbnails. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @return string */ public static function get_route_prefix() { static $route; if ( ! isset( $route ) ) { $regen = \RegenerateThumbnails(); if ( ( empty( $regen->rest_api ) || ! is_object( $regen->rest_api ) ) && method_exists( $regen, 'rest_api_init' ) ) { $regen->rest_api_init(); } $route = '/' . trim( $regen->rest_api->namespace, '/' ) . '/regenerate/'; } return $route; } /** * Get the path to the temporary file. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param string $file_path The optimized full-sized file path. * @return string */ public static function get_temporary_file_path( $file_path ) { return $file_path . '_backup'; } } 3rd-party/regenerate-thumbnails/regenerate-thumbnails.php 0000644 00000000562 15174671745 0017700 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); if ( ! class_exists( 'RegenerateThumbnails' ) || ! function_exists( 'RegenerateThumbnails' ) ) { return; } class_alias( '\\Imagify\\ThirdParty\\RegenerateThumbnails\\Main', '\\Imagify_Regenerate_Thumbnails' ); add_action( 'init', [ \Imagify\ThirdParty\RegenerateThumbnails\Main::get_instance(), 'init' ], 20 ); 3rd-party/amp/amp.php 0000644 00000001057 15174671745 0010460 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Compatibility with AMP plugin from WordPress.com VIP. */ if ( function_exists( 'is_amp_endpoint' ) ) : add_filter( 'imagify_allow_picture_tags_for_nextgen', 'imagify_amp_disable_picture_on_endpoint' ); /** * Do not use <picture> tags in AMP pages. * * @since 1.9 * * @param bool $allow True to allow the use of <picture> tags (default). False to prevent their use. * @return bool */ function imagify_amp_disable_picture_on_endpoint( $allow ) { return $allow && ! is_amp_endpoint(); } endif; 3rd-party/yoast-seo.php 0000644 00000001173 15174671745 0011050 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); if ( defined( 'WPSEO_VERSION' ) && is_admin() && ! wp_doing_ajax() ) : add_action( 'wp_print_scripts', '_imagify_dequeue_yoastseo_script' ); /** * Remove Yoast SEO bugged script. * * @since 1.4.1 */ function _imagify_dequeue_yoastseo_script() { if ( ! function_exists( 'get_current_screen' ) ) { return; } $current_screen = get_current_screen(); if ( isset( $current_screen ) && 'post' === $current_screen->base && 'attachment' === $current_screen->post_type ) { wp_dequeue_script( 'yoast-seo' ); wp_deregister_script( 'yoast-seo' ); } } endif; 3rd-party/nextgen-gallery/inc/common/attachments.php 0000644 00000025043 15174671745 0016610 0 ustar 00 <?php use Imagify\Optimization\File; use Imagify\ThirdParty\NGG; defined( 'ABSPATH' ) || exit; add_action( 'ngg_after_new_images_added', '_imagify_ngg_optimize_attachment', IMAGIFY_INT_MAX, 2 ); /** * Auto-optimize when a new attachment is added to the database (NGG plugin's table), except for images imported from the library. * * @since 1.5 * @author Jonathan Buttigieg * * @param int $gallery_id A Gallery ID. * @param array $image_ids An array of Ids or objects. Ids which are sucessfully added. */ function _imagify_ngg_optimize_attachment( $gallery_id, $image_ids ) { if ( ! Imagify_Requirements::is_api_key_valid() || ! get_imagify_option( 'auto_optimize' ) ) { return; } $is_maybe_library_import = ! empty( $_POST['action'] ) && 'import_media_library' === $_POST['action'] && ! empty( $_POST['attachment_ids'] ) && is_array( $_POST['attachment_ids'] ); // WPCS: CSRF ok. if ( $is_maybe_library_import && ! empty( $_POST['nextgen_upload_image_sec'] ) ) { // WPCS: CSRF ok. /** * The images are imported from the library. * In this case, those images are dealt with in _imagify_ngg_media_library_imported_image_data(). */ return; } if ( $is_maybe_library_import && ( ! empty( $_POST['gallery_id'] ) || ! empty( $_POST['gallery_name'] ) ) ) { // WPCS: CSRF ok. /** * Same thing but for NGG 2.0 probably. */ return; } foreach ( $image_ids as $image ) { if ( is_numeric( $image ) ) { $image_id = (int) $image; } elseif ( is_object( $image ) && ! empty( $image->pid ) ) { $image_id = (int) $image->pid; } else { $image_id = 0; } if ( ! $image_id ) { continue; } /** * Allow to prevent automatic optimization for a specific NGG gallery image. * * @since 1.6.12 * @author Grégory Viguier * * @param bool $optimize True to optimize, false otherwise. * @param int $image_id Image ID. * @param int $gallery_id Gallery ID. */ $optimize = apply_filters( 'imagify_auto_optimize_ngg_gallery_image', true, $image_id, $gallery_id ); if ( ! $optimize ) { continue; } $process = imagify_get_optimization_process( $image, 'ngg' ); if ( ! $process->is_valid() ) { continue; } if ( $process->get_data()->get_optimization_status() ) { // Optimization already attempted. continue; } $process->optimize(); } } add_filter( 'ngg_medialibrary_imported_image', '_imagify_ngg_media_library_imported_image_data', 10, 2 ); /** * Import Imagify data from a WordPress image to a new NGG image, and optimize the thumbnails. * * @since 1.5 * @author Jonathan Buttigieg * * @param object $image A NGG image object. * @param object $attachment An attachment object. * @return object */ function _imagify_ngg_media_library_imported_image_data( $image, $attachment ) { $wp_process = imagify_get_optimization_process( $attachment->ID, 'wp' ); if ( ! $wp_process->is_valid() || ! $wp_process->get_media()->is_supported() ) { return $image; } $wp_data = $wp_process->get_data(); if ( ! $wp_data->is_optimized() ) { // The main image is not optimized. return $image; } // Copy the full size data. $wp_full_size_data = $wp_data->get_size_data(); $optimization_level = $wp_data->get_optimization_level(); NGG\DB::get_instance()->update( $image->pid, [ 'pid' => $image->pid, 'optimization_level' => $optimization_level, 'status' => $wp_data->get_optimization_status(), 'data' => [ 'sizes' => [ 'full' => $wp_full_size_data, ], 'stats' => [ 'original_size' => $wp_full_size_data['original_size'], 'optimized_size' => $wp_full_size_data['optimized_size'], 'percent' => $wp_full_size_data['percent'], ], ], ] ); $ngg_process = imagify_get_optimization_process( $image->pid, 'ngg' ); if ( ! $ngg_process->is_valid() ) { // WTF. return $image; } // Copy the backup file (we don't want to backup the optimized file if it can be avoided). $ngg_media = $ngg_process->get_media(); $wp_media = $wp_process->get_media(); $wp_backup_path = $wp_media->get_backup_path(); $filesystem = imagify_get_filesystem(); $backup_copied = false; if ( $wp_backup_path ) { $ngg_backup_path = $ngg_media->get_raw_backup_path(); $backup_copied = $filesystem->copy( $wp_backup_path, $ngg_backup_path, true ); if ( $backup_copied ) { $filesystem->chmod_file( $ngg_backup_path ); } } /** * Next-gen for the full size. * Look for an existing copy locally: * - if it exists, copy it (and its optimization data), * - if not, add it to the optimization queue. */ $add_full_nextgen = $wp_media->is_image(); if ( $add_full_nextgen ) { $formats = [ 'avif' => $wp_process::AVIF_SUFFIX, 'webp' => $wp_process::WEBP_SUFFIX, ]; foreach ( $formats as $extension => $suffix ) { $wp_full_path_nextgen = false; $nextgen_size_name = 'full' . $suffix; $wp_nextgen_data = $wp_data->get_size_data( $nextgen_size_name ); // Get the path to the next-gen image if it exists. $wp_full_path_nextgen = $wp_process->get_fullsize_file()->get_path_to_nextgen( $extension ); if ( $wp_full_path_nextgen && ! $filesystem->exists( $wp_full_path_nextgen ) ) { $wp_full_path_nextgen = false; } if ( $wp_full_path_nextgen ) { // We know we have a next-gen version. Make sure we have the right data. $wp_nextgen_data['success'] = true; if ( empty( $wp_nextgen_data['original_size'] ) ) { // The next-gen data is missing. $full_size_weight = $wp_full_size_data['original_size']; if ( ! $full_size_weight && $wp_backup_path ) { // For some reason we don't have the original file weight, but we can get it from the backup file. $full_size_weight = $filesystem->size( $wp_backup_path ); if ( $full_size_weight ) { $wp_nextgen_data['original_size'] = $full_size_weight; } } } if ( ! empty( $wp_nextgen_data['original_size'] ) && empty( $wp_nextgen_data['optimized_size'] ) ) { // The next-gen file size. $wp_nextgen_data['optimized_size'] = $filesystem->size( $wp_full_path_nextgen ); } if ( empty( $wp_nextgen_data['original_size'] ) || empty( $wp_nextgen_data['optimized_size'] ) ) { // We must have both original and optimized sizes. $wp_nextgen_data = []; } } if ( $wp_full_path_nextgen && $wp_nextgen_data ) { // We have the file and the data. // Copy the file. $ngg_full_file = new File( $ngg_media->get_raw_fullsize_path() ); $ngg_full_path_nextgen = $ngg_full_file->get_path_to_nextgen( $extension ); // Destination. if ( $ngg_full_path_nextgen ) { $copied = $filesystem->copy( $wp_full_path_nextgen, $ngg_full_path_nextgen, true ); if ( $copied ) { // Success. $filesystem->chmod_file( $ngg_full_path_nextgen ); $add_full_nextgen = false; } } if ( ! $add_full_nextgen ) { // The next-gen file has been successfully copied: now, copy the data. $ngg_process->get_data()->update_size_optimization_data( $nextgen_size_name, $wp_nextgen_data ); } } } } // Optimize thumbnails. $sizes = $ngg_media->get_media_files(); unset( $sizes['full'] ); if ( $add_full_nextgen ) { // We could not use a local next-gen copy: ask for a new one. $formats = imagify_nextgen_images_formats(); foreach ( $formats as $format ) { if ( 'webp' === $format ) { $suffix = $wp_process::WEBP_SUFFIX; } elseif ( 'avif' === $format ) { $suffix = $wp_process::AVIF_SUFFIX; } $sizes[ 'full' . $suffix ] = []; } } if ( ! $sizes ) { return $image; } $args = [ 'hook_suffix' => 'optimize_imported_images', ]; $ngg_process->optimize_sizes( array_keys( $sizes ), $optimization_level, $args ); return $image; } add_action( 'ngg_generated_image', 'imagify_ngg_maybe_add_dynamic_thumbnail_to_background_process', IMAGIFY_INT_MAX, 2 ); /** * Add a dynamically generated thumbnail to the background process queue. * Note that this won’t work when images are imported (from WP Library or uploaded), since they are already being processed, and locked. * * @since 1.8 * @since 1.9 Doesn't use the class Imagify_NGG_Dynamic_Thumbnails_Background_Process anymore. * @author Grégory Viguier * * @param object $image A NGG image object. * @param string $size The thumbnail size name. */ function imagify_ngg_maybe_add_dynamic_thumbnail_to_background_process( $image, $size ) { NGG\DynamicThumbnails::get_instance()->push_to_queue( $image, $size ); } add_action( 'ngg_delete_picture', 'imagify_ngg_cleanup_after_media_deletion', 10, 2 ); /** * Delete everything when a NGG image is deleted. * * @since 1.9 * @author Grégory Viguier * * @param int $image_id The image ID. * @param object $image A NGG object. */ function imagify_ngg_cleanup_after_media_deletion( $image_id, $image ) { $process = imagify_get_optimization_process( $image, 'ngg' ); if ( ! $process->is_valid() ) { return; } // Trigger a common hook. imagify_trigger_delete_media_hook( $process ); /** * The backup file has already been deleted by NGG. * Delete the next-gen versions and the optimization data. */ $process->delete_nextgen_files(); $process->get_data()->delete_optimization_data(); } add_filter( 'imagify_crop_thumbnail', 'imagify_ngg_should_crop_thumbnail', 10, 4 ); /** * In case of a dynamic thumbnail, tell if the image must be croped or resized. * * @since 1.9 * @author Grégory Viguier * * @param bool $crop True to crop the thumbnail, false to resize. Null by default. * @param string $size Name of the thumbnail size. * @param array $size_data Data of the thumbnail being processed. Contains at least 'width', 'height', and 'path'. * @param MediaInterface $media The MediaInterface instance corresponding to the image being processed. * @return bool */ function imagify_ngg_should_crop_thumbnail( $crop, $size, $size_data, $media ) { static $data_per_media = []; static $storage_per_media = []; if ( 'ngg' !== $media->get_context() ) { return $crop; } $media_id = $media->get_id(); if ( ! isset( $data_per_media[ $media_id ] ) ) { $image = \nggdb::find_image( $media_id ); if ( ! empty( $image->_ngiw ) ) { $storage_per_media[ $media_id ] = $image->_ngiw->get_storage()->object; } else { $storage_per_media[ $media_id ] = \C_Gallery_Storage::get_instance()->object; } $data_per_media[ $media_id ] = $storage_per_media[ $media_id ]->_image_mapper->find( $media_id ); // stdClass Object. } $params = $storage_per_media[ $media_id ]->get_image_size_params( $data_per_media[ $media_id ], $size ); return ! empty( $params['crop'] ); } 3rd-party/nextgen-gallery/inc/admin/menu.php 0000644 00000001074 15174671745 0015037 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); add_action( 'admin_menu', '_imagify_ngg_bulk_optimization_menu' ); /** * Add submenu in menu "Media" * * @since 1.5 * @author Jonathan Buttigieg */ function _imagify_ngg_bulk_optimization_menu() { if ( ! defined( 'NGGFOLDER' ) ) { return; } $capacity = imagify_get_context( 'ngg' )->get_capacity( 'bulk-optimize' ); add_submenu_page( NGGFOLDER, __( 'Bulk Optimization', 'imagify' ), __( 'Bulk Optimization', 'imagify' ), $capacity, imagify_get_ngg_bulk_screen_slug(), '_imagify_display_bulk_page' ); } 3rd-party/nextgen-gallery/inc/admin/bulk.php 0000644 00000007115 15174671745 0015032 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); add_filter( 'imagify_bulk_page_types', 'imagify_ngg_bulk_page_types' ); /** * Filter the types to display in the bulk optimization page. * * @since 1.7.1 * @author Grégory Viguier * * @param array $types The folder types displayed on the page. If a folder type is "library", the context should be suffixed after a pipe character. They are passed as array keys. * @return array */ function imagify_ngg_bulk_page_types( $types ) { if ( ! empty( $_GET['page'] ) && imagify_get_ngg_bulk_screen_slug() === $_GET['page'] ) { // WPCS: CSRF ok. $types['library|ngg'] = 1; } return $types; } add_filter( 'imagify_bulk_stats', 'imagify_ngg_bulk_stats', 10, 2 ); /** * Filter the generic stats used in the bulk optimization page. * * @since 1.7.1 * @author Grégory Viguier * * @param array $data The data. * @param array $types The folder types. They are passed as array keys. * @return array */ function imagify_ngg_bulk_stats( $data, $types ) { if ( ! isset( $types['library|ngg'] ) ) { return $data; } add_filter( 'imagify_count_saving_data', 'imagify_ngg_count_saving_data', 8 ); $total_saving_data = imagify_count_saving_data(); remove_filter( 'imagify_count_saving_data', 'imagify_ngg_count_saving_data', 8 ); // Global chart. $data['total_attachments'] += imagify_ngg_count_attachments(); $data['unoptimized_attachments'] += imagify_ngg_count_unoptimized_attachments(); $data['optimized_attachments'] += imagify_ngg_count_optimized_attachments(); $data['errors_attachments'] += imagify_ngg_count_error_attachments(); // Stats block. $data['already_optimized_attachments'] += $total_saving_data['count']; $data['original_human'] += $total_saving_data['original_size']; $data['optimized_human'] += $total_saving_data['optimized_size']; return $data; } add_filter( 'imagify_bulk_page_data', 'imagify_ngg_bulk_page_data', 10, 2 ); /** * Filter the data to use on the bulk optimization page. * * @since 1.7 * @since 1.7.1 Added the $types parameter. * @author Grégory Viguier * * @param array $data The data to use. * @param array $types The folder types displayed on the page. They are passed as array keys. * @return array */ function imagify_ngg_bulk_page_data( $data, $types ) { if ( ! isset( $types['library|ngg'] ) ) { return $data; } // Limits. $data['unoptimized_attachment_limit'] += imagify_get_unoptimized_attachment_limit(); // Group. $data['groups']['ngg'] = [ /** * The group_id corresponds to the file names like 'part-bulk-optimization-results-row-{$group_id}'. * It is also used in get_imagify_localize_script_translations(). */ 'group_id' => 'library', 'context' => 'ngg', 'title' => __( 'NextGen Galleries', 'imagify' ), /* translators: 1 is the opening of a link, 2 is the closing of this link. */ 'footer' => sprintf( __( 'You can also re-optimize your images more finely directly in each %1$sgallery%2$s.', 'imagify' ), '<a href="' . esc_url( admin_url( 'admin.php?page=nggallery-manage-gallery' ) ) . '">', '</a>' ), ]; return $data; } add_filter( 'imagify_optimization_errors_url', 'imagify_ngg_optimization_errors_url', 10, 2 ); /** * Provide a URL to a page displaying optimization errors for the NGG context. * * @since 1.9 * @author Grégory Viguier * * @param string $url The URL. * @param string $context The context. * @return string */ function imagify_ngg_optimization_errors_url( $url, $context ) { if ( 'ngg' === $context ) { return admin_url( 'admin.php?page=nggallery-manage-gallery' ); } return $url; } 3rd-party/nextgen-gallery/inc/admin/gallery.php 0000644 00000003356 15174671745 0015537 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; add_filter( 'ngg_manage_images_number_of_columns', '_imagify_ngg_manage_images_number_of_columns' ); /** * Add "Imagify" column in admin.php?page=nggallery-manage-gallery. * * @since 1.5 * @author Jonathan Buttigieg * * @param int $count Number of columns. * @return int Incremented number of columns. */ function _imagify_ngg_manage_images_number_of_columns( $count ) { ++$count; add_filter( 'ngg_manage_images_column_' . $count . '_header', '_imagify_ngg_manage_media_columns' ); add_filter( 'ngg_manage_images_column_' . $count . '_content', '_imagify_ngg_manage_media_custom_column', 10, 2 ); return $count; } /** * Get the column title. * * @since 1.5 * @author Jonathan Buttigieg * * @return string */ function _imagify_ngg_manage_media_columns() { return 'Imagify'; } /** * Get the column content. * * @since 1.5 * @author Jonathan Buttigieg * * @param string $output The column content. * @param object $image An NGG Image object. * @return string */ function _imagify_ngg_manage_media_custom_column( $output, $image ) { $process = imagify_get_optimization_process( $image, 'ngg' ); return get_imagify_media_column_content( $process ); } add_filter( 'imagify_display_missing_thumbnails_link', '_imagify_ngg_hide_missing_thumbnails_link', 10, 3 ); /** * Hide the "Optimize missing thumbnails" link. * * @since 1.6.10 * @author Grégory Viguier * * @param bool $display True to display the link. False to not display it. * @param object $attachment The attachement object. * @param string $context The context. * @return bool */ function _imagify_ngg_hide_missing_thumbnails_link( $display, $attachment, $context ) { return 'ngg' === $context ? false : $display; } 3rd-party/nextgen-gallery/inc/admin/enqueue.php 0000644 00000002365 15174671745 0015546 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; add_action( 'imagify_assets_enqueued', '_imagify_ngg_admin_print_styles' ); /** * Add some CSS and JS for NGG compatibility. * * @since 1.5 * @since 1.6.10 Use the new class Imagify_Assets. * @author Jonathan Buttigieg * @author Grégory Viguier */ function _imagify_ngg_admin_print_styles() { $assets = Imagify_Assets::get_instance(); /** * Manage Gallery Images. */ if ( imagify_is_screen( 'nggallery-manage-images' ) || ( isset( $_GET['gid'] ) && ! empty( $_GET['pid'] ) && imagify_is_screen( 'nggallery-manage-gallery' ) // WPCS: CSRF ok. ) ) { $assets->enqueue_style( 'admin' )->enqueue_script( 'library' ); return; } /** * NGG Bulk Optimization. */ $bulk_screen_id = imagify_get_ngg_bulk_screen_id(); if ( ! imagify_is_screen( $bulk_screen_id ) ) { return; } $assets->remove_deferred_localization( 'bulk', 'imagifyBulk' ); $l10n = $assets->get_localization_data( 'bulk', [ 'bufferSizes' => [ 'ngg' => 4, ], ] ); /** This filter is documented in inc/functions/i18n.php */ $l10n['bufferSizes'] = apply_filters( 'imagify_bulk_buffer_sizes', $l10n['bufferSizes'] ); $assets->enqueue_assets( [ 'pricing-modal', 'bulk' ] )->localize( 'imagifyBulk', $l10n ); } 3rd-party/nextgen-gallery/inc/functions/attachments.php 0000644 00000000726 15174671745 0017331 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Get the backup path of a specific attachement. * * @since 1.6.8 * @author Grégory Viguier * * @param string $file_path The file path. * @return string|bool The backup path. False on failure. */ function get_imagify_ngg_attachment_backup_path( $file_path ) { $file_path = wp_normalize_path( (string) $file_path ); if ( ! $file_path ) { return false; } return $file_path . '_backup'; } 3rd-party/nextgen-gallery/inc/functions/admin-stats.php 0000644 00000012101 15174671745 0017230 0 ustar 00 <?php use Imagify\ThirdParty\NGG\DB; defined( 'ABSPATH' ) || exit; /** * Count number of attachments. * * @since 1.5 * @author Jonathan Buttigieg * * @return int The number of attachments. */ function imagify_ngg_count_attachments() { global $wpdb; static $count; if ( isset( $count ) ) { return $count; } $table_name = $wpdb->prefix . 'ngg_pictures'; $count = (int) $wpdb->get_var( "SELECT COUNT($table_name.pid) FROM $table_name" ); // WPCS: unprepared SQL ok. return $count; } /** * Count number of optimized attachments with an error. * * @since 1.5 * @author Jonathan Buttigieg * * @return int The number of attachments. */ function imagify_ngg_count_error_attachments() { static $count; if ( isset( $count ) ) { return $count; } $ngg_db = DB::get_instance(); $key = $ngg_db->get_primary_key(); $count = (int) $ngg_db->get_var_by( "COUNT($key)", 'status', 'error' ); return $count; } /** * Count number of optimized attachments (by Imagify or an other tool before). * * @since 1.5 * @author Jonathan Buttigieg * * @return int The number of attachments. */ function imagify_ngg_count_optimized_attachments() { static $count; if ( isset( $count ) ) { return $count; } $ngg_db = DB::get_instance(); $key = $ngg_db->get_primary_key(); $count = (int) $ngg_db->get_var_in( "COUNT($key)", 'status', [ 'success', 'already_optimized' ] ); return $count; } /** * Count number of unoptimized attachments. * * @since 1.0 * @author Jonathan Buttigieg * * @return int The number of attachments. */ function imagify_ngg_count_unoptimized_attachments() { return imagify_ngg_count_attachments() - imagify_ngg_count_optimized_attachments() - imagify_ngg_count_error_attachments(); } /** * Count percent of optimized attachments. * * @since 1.0 * @author Jonathan Buttigieg * * @return int The percent of optimized attachments. */ function imagify_ngg_percent_optimized_attachments() { $total_attachments = imagify_ngg_count_attachments(); $total_optimized_attachments = imagify_ngg_count_optimized_attachments(); if ( ! $total_attachments || ! $total_optimized_attachments ) { return 0; } return min( round( 100 * $total_optimized_attachments / $total_attachments ), 100 ); } /** * Count percent, original & optimized size of all images optimized by Imagify. * * @since 1.5 * @since 1.6.7 Revamped to handle huge libraries. * @author Jonathan Buttigieg * * @param bool|array $attachments An array containing the keys 'count', 'original_size', and 'optimized_size', or an array of attachments (back compat', deprecated), or false. * @return array An array containing the keys 'count', 'original_size', and 'optimized_size'. */ function imagify_ngg_count_saving_data( $attachments ) { global $wpdb; if ( is_array( $attachments ) ) { return $attachments; } /** * Filter the query to get all optimized NGG attachments. * 3rd party will be able to override the result. * * @since 1.6.7 * * @param bool|array $attachments An array containing the keys ('count', 'original_size', and 'optimized_size'), or false. */ $attachments = apply_filters( 'imagify_ngg_count_saving_data', false ); if ( is_array( $attachments ) ) { return $attachments; } $original_size = 0; $optimized_size = 0; $count = 0; /** This filter is documented in /inc/functions/admin-stats.php */ $limit = apply_filters( 'imagify_count_saving_data_limit', 15000 ); $limit = absint( $limit ); $offset = 0; $query = " SELECT data FROM $wpdb->ngg_imagify_data WHERE status = 'success' LIMIT %d, %d"; $attachments = $wpdb->get_col( $wpdb->prepare( $query, $offset, $limit ) ); // WPCS: unprepared SQL ok. $wpdb->flush(); while ( $attachments ) { $attachments = array_map( 'maybe_unserialize', $attachments ); foreach ( $attachments as $attachment_data ) { if ( ! $attachment_data ) { continue; } ++$count; $original_data = $attachment_data['sizes']['full']; // Increment the original sizes. $original_size += $original_data['original_size'] ? $original_data['original_size'] : 0; $optimized_size += $original_data['optimized_size'] ? $original_data['optimized_size'] : 0; unset( $attachment_data['sizes']['full'], $original_data ); // Increment the thumbnails sizes. foreach ( $attachment_data['sizes'] as $size_data ) { if ( ! empty( $size_data['success'] ) ) { $original_size += $size_data['original_size'] ? $size_data['original_size'] : 0; $optimized_size += $size_data['optimized_size'] ? $size_data['optimized_size'] : 0; } } unset( $size_data ); } unset( $attachment_data ); if ( count( $attachments ) === $limit ) { // Unless we are really unlucky, we still have attachments to fetch. $offset += $limit; $attachments = $wpdb->get_col( $wpdb->prepare( $query, $offset, $limit ) ); // WPCS: unprepared SQL ok. $wpdb->flush(); } else { // Save one request, don't go back to the beginning of the loop. $attachments = []; } } return [ 'count' => $count, 'original_size' => $original_size, 'optimized_size' => $optimized_size, ]; } 3rd-party/nextgen-gallery/inc/functions/common.php 0000644 00000001576 15174671745 0016312 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Get NGG Bulk Optimization screen ID. * Because WP nonsense, the screen ID depends on the menu title, which is translated. So the screen ID changes depending on the administration locale. * * @since 1.6.13 * @author Grégory Viguier * * @return string */ function imagify_get_ngg_bulk_screen_id() { global $admin_page_hooks; $ngg_menu_slug = defined( 'NGGFOLDER' ) ? plugin_basename( NGGFOLDER ) : 'nextgen-gallery'; $ngg_menu_slug = isset( $admin_page_hooks[ $ngg_menu_slug ] ) ? $admin_page_hooks[ $ngg_menu_slug ] : 'gallery'; return $ngg_menu_slug . '_page_' . imagify_get_ngg_bulk_screen_slug(); } /** * Get NGG Bulk Optimization screen slug. * * @since 1.7 * @author Grégory Viguier * * @return string */ function imagify_get_ngg_bulk_screen_slug() { return IMAGIFY_SLUG . '-ngg-bulk-optimization'; } 3rd-party/nextgen-gallery/classes/Bulk/NGG.php 0000644 00000015345 15174671745 0015205 0 ustar 00 <?php namespace Imagify\ThirdParty\NGG\Bulk; use C_Gallery_Storage; use Imagify\Bulk\AbstractBulk; use Imagify\ThirdParty\NGG\DB; /** * Class to use for bulk for NextGen Gallery. * * @since 1.9 */ class NGG extends AbstractBulk { /** * Context "short name". * * @var string * @since 1.9 */ protected $context = 'ngg'; /** * Get all unoptimized media ids. * * @since 1.9 * * @param int $optimization_level The optimization level. * @return array A list of unoptimized media IDs. */ public function get_unoptimized_media_ids( $optimization_level ) { global $wpdb; $this->set_no_time_limit(); $storage = C_Gallery_Storage::get_instance(); $ngg_table = $wpdb->prefix . 'ngg_pictures'; $data = []; $images = $wpdb->get_results( $wpdb->prepare( // WPCS: unprepared SQL ok. " SELECT DISTINCT picture.pid as id, picture.filename, idata.optimization_level, idata.status, idata.data FROM $ngg_table as picture LEFT JOIN $wpdb->ngg_imagify_data as idata ON picture.pid = idata.pid WHERE idata.pid IS NULL OR idata.optimization_level != %d OR idata.status = 'error' LIMIT %d", $optimization_level, imagify_get_unoptimized_attachment_limit() ), ARRAY_A ); if ( ! $images ) { return []; } foreach ( $images as $image ) { $id = absint( $image['id'] ); $file_path = $storage->get_image_abspath( $id ); if ( ! $file_path || ! $this->filesystem->exists( $file_path ) ) { continue; } $attachment_data = maybe_unserialize( $image['data'] ); $attachment_error = ''; if ( isset( $attachment_data['sizes']['full']['error'] ) ) { $attachment_error = $attachment_data['sizes']['full']['error']; } $attachment_error = trim( $attachment_error ); $attachment_status = $image['status']; $attachment_optimization_level = $image['optimization_level']; $attachment_backup_path = get_imagify_ngg_attachment_backup_path( $file_path ); // Don't try to re-optimize if the optimization level is still the same. if ( $optimization_level === $attachment_optimization_level && is_string( $attachment_error ) ) { continue; } // Don't try to re-optimize if there is no backup file. if ( 'success' === $attachment_status && $optimization_level !== $attachment_optimization_level && ! $this->filesystem->exists( $attachment_backup_path ) ) { continue; } // Don't try to re-optimize images already compressed. if ( 'already_optimized' === $attachment_status && $attachment_optimization_level >= $optimization_level ) { continue; } // Don't try to re-optimize images with an empty error message. if ( 'error' === $attachment_status && empty( $attachment_error ) ) { continue; } $data[] = $id; } return $data; } /** * Get ids of all optimized media without next-gen versions. * * @since 2.2 * * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. * @type array $errors { * @type array $no_file_path A list of media IDs. * @type array $no_backup A list of media IDs. * } * } */ public function get_optimized_media_ids_without_format( $format ) { global $wpdb; $this->set_no_time_limit(); $storage = C_Gallery_Storage::get_instance(); $ngg_table = $wpdb->prefix . 'ngg_pictures'; $data_table = DB::get_instance()->get_table_name(); $suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::WEBP_SUFFIX' ); if ( 'avif' === get_imagify_option( 'optimization_format' ) ) { $suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::AVIF_SUFFIX' ); } $files = $wpdb->get_col( $wpdb->prepare( // WPCS: unprepared SQL ok. " SELECT ngg.pid FROM $ngg_table as ngg INNER JOIN $data_table AS data ON ( ngg.pid = data.pid ) WHERE ( data.status = 'success' OR data.status = 'already_optimized' ) AND data.data NOT LIKE %s ORDER BY ngg.pid DESC", '%' . $wpdb->esc_like( $suffix . '";a:4:{s:7:"success";b:1;' ) . '%' ) ); $wpdb->flush(); unset( $ngg_table, $data_table, $suffix ); $data = [ 'ids' => [], 'errors' => [ 'no_file_path' => [], 'no_backup' => [], ], ]; if ( ! $files ) { return $data; } foreach ( $files as $file_id ) { $file_id = absint( $file_id ); $file_path = $storage->get_image_abspath( $file_id ); if ( ! $file_path ) { // Problem. $data['errors']['no_file_path'][] = $file_id; continue; } $backup_path = get_imagify_ngg_attachment_backup_path( $file_path ); if ( ! $this->filesystem->exists( $backup_path ) ) { // No backup, no next-gen. $data['errors']['no_backup'][] = $file_id; continue; } $data['ids'][] = $file_id; } return $data; } /** * Tell if there are optimized media without next-gen versions. * * @since 2.2 * * @return int The number of media. */ public function has_optimized_media_without_nextgen() { global $wpdb; $ngg_table = $wpdb->prefix . 'ngg_pictures'; $data_table = DB::get_instance()->get_table_name(); $suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::WEBP_SUFFIX' ); if ( 'avif' === get_imagify_option( 'optimization_format' ) ) { $suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::AVIF_SUFFIX' ); } return (int) $wpdb->get_var( $wpdb->prepare( // WPCS: unprepared SQL ok. " SELECT COUNT(ngg.pid) FROM $ngg_table as ngg INNER JOIN $data_table AS data ON ( ngg.pid = data.pid ) WHERE ( data.status = 'success' OR data.status = 'already_optimized' ) AND data.data NOT LIKE %s", '%' . $wpdb->esc_like( $suffix . '";a:4:{s:7:"success";b:1;' ) . '%' ) ); } /** * Get the context data. * * @since 1.9 * * @return array { * The formated data. * * @type string $count-optimized Number of media optimized. * @type string $count-errors Number of media having an optimization error, with a link to the page listing the optimization errors. * @type string $optimized-size Optimized filesize. * @type string $original-size Original filesize. * } */ public function get_context_data() { $total_saving_data = imagify_count_saving_data(); $data = [ 'count-optimized' => imagify_ngg_count_optimized_attachments(), 'count-errors' => imagify_ngg_count_error_attachments(), 'optimized-size' => $total_saving_data['optimized_size'], 'original-size' => $total_saving_data['original_size'], 'errors_url' => get_imagify_admin_url( 'folder-errors', $this->context ), ]; return $this->format_context_data( $data ); } } 3rd-party/nextgen-gallery/classes/Optimization/Process/NGG.php 0000644 00000005536 15174671745 0020415 0 ustar 00 <?php namespace Imagify\ThirdParty\NGG\Optimization\Process; use Imagify\Optimization\File; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Optimization class for NextGen Gallery. * This class constructor accepts: * - A NGG image ID (int). * - A \nggImage object. * - A \nggdb object. * - An anonym object containing a pid property (and everything else). * - A \Imagify\Media\MediaInterface object. * * @since 1.9 * @see Imagify\ThirdParty\NGG\Media\NGG * @author Grégory Viguier */ class NGG extends \Imagify\Optimization\Process\AbstractProcess { /** ----------------------------------------------------------------------------------------- */ /** MISSING THUMBNAILS ====================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the sizes for this media that have not get through optimization. * Since this context doesn't handle this feature, this will always return an empty array, unless an error is triggered. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array|WP_Error A WP_Error object on failure. An empty array on success: this context has no thumbnails. * The tests are kept for consistency. */ public function get_missing_sizes() { // The media must have been optimized once and have a backup. if ( ! $this->is_valid() ) { return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media->is_supported() ) { return new \WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } $data = $this->get_data(); if ( ! $data->is_optimized() ) { return new \WP_Error( 'media_not_optimized', __( 'This media is not optimized yet.', 'imagify' ) ); } if ( ! $media->has_backup() ) { return new \WP_Error( 'no_backup', __( 'This file has no backup file.', 'imagify' ) ); } if ( ! $media->is_image() ) { return new \WP_Error( 'media_not_an_image', __( 'This media is not an image.', 'imagify' ) ); } return []; } /** * Optimize missing thumbnail sizes. * Since this context doesn't handle this feature, this will always return a \WP_Error object. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize_missing_thumbnails() { if ( ! $this->is_valid() ) { return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } if ( ! $this->get_media()->is_supported() ) { return new \WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } return new \WP_Error( 'no_sizes', __( 'No thumbnails seem to be missing.', 'imagify' ) ); } } 3rd-party/nextgen-gallery/classes/Optimization/Data/NGG.php 0000644 00000016536 15174671745 0017652 0 ustar 00 <?php namespace Imagify\ThirdParty\NGG\Optimization\Data; use Imagify\Traits\MediaRowTrait; /** * Optimization data class for the custom folders. * This class constructor accepts: * - A NGG image ID (int). * - A \nggImage object. * - A \nggdb object. * - An anonym object containing a pid property (and everything else). * - A \Imagify\Media\MediaInterface object. * * @since 1.9 * @see Imagify\ThirdParty\NGG\Media\NGG * @author Grégory Viguier */ class NGG extends \Imagify\Optimization\Data\AbstractData { use MediaRowTrait; /** * The attachment SQL DB class. * * @var string * @since 1.9 * @access protected * @author Grégory Viguier */ protected $db_class_name = '\\Imagify\\ThirdParty\\NGG\\DB'; /** * The constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id An ID, or whatever type the "Media" class constructor accepts. */ public function __construct( $id ) { parent::__construct( $id ); if ( ! $this->is_valid() ) { return; } // This is required by MediaRowTrait. $this->id = $this->get_media()->get_id(); } /** * Get the whole media optimization data. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array The data. See parent method for details. */ public function get_optimization_data() { if ( ! $this->is_valid() ) { return $this->default_optimization_data; } $row = array_merge( $this->get_row_db_instance()->get_column_defaults(), $this->get_row() ); $data = $this->default_optimization_data; $data['status'] = $row['status']; $data['level'] = $row['optimization_level']; $data['level'] = is_numeric( $data['level'] ) ? (int) $data['level'] : false; $data['stats'] = [ 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, ]; if ( ! empty( $row['data']['sizes'] ) && is_array( $row['data']['sizes'] ) ) { $data['sizes'] = $row['data']['sizes']; $data['sizes'] = array_filter( $data['sizes'], 'is_array' ); } if ( empty( $data['sizes']['full'] ) ) { if ( 'success' === $row['status'] ) { $data['sizes']['full'] = [ 'success' => true, 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, ]; } elseif ( ! empty( $row['status'] ) ) { $data['sizes']['full'] = [ 'success' => false, 'error' => '', ]; } } if ( empty( $data['sizes'] ) ) { return $data; } foreach ( $data['sizes'] as $size_data ) { // Cast. if ( isset( $size_data['original_size'] ) ) { $size_data['original_size'] = (int) $size_data['original_size']; } if ( isset( $size_data['optimized_size'] ) ) { $size_data['optimized_size'] = (int) $size_data['optimized_size']; } if ( isset( $size_data['percent'] ) ) { $size_data['percent'] = round( $size_data['percent'], 2 ); } // Stats. if ( ! empty( $size_data['original_size'] ) && ! empty( $size_data['optimized_size'] ) ) { $data['stats']['original_size'] += $size_data['original_size']; $data['stats']['optimized_size'] += $size_data['optimized_size']; } } if ( $data['stats']['original_size'] && $data['stats']['optimized_size'] ) { $data['stats']['percent'] = $data['stats']['original_size'] - $data['stats']['optimized_size']; $data['stats']['percent'] = round( $data['stats']['percent'] / $data['stats']['original_size'] * 100, 2 ); } return $data; } /** * Update the optimization data, level, and status for a size. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $size The size name. * @param array $data The optimization data. See parent method for details. */ public function update_size_optimization_data( $size, array $data ) { if ( ! $this->is_valid() ) { return; } $old_data = array_merge( $this->get_reset_data(), $this->get_row() ); $old_data['data']['sizes'] = ! empty( $old_data['data']['sizes'] ) && is_array( $old_data['data']['sizes'] ) ? $old_data['data']['sizes'] : []; if ( 'full' === $size ) { /** * Original file. */ $old_data['optimization_level'] = $data['level']; $old_data['status'] = $data['status']; } if ( ! $data['success'] ) { /** * Error. */ $old_data['data']['sizes'][ $size ] = [ 'success' => false, 'error' => $data['error'], ]; } else { /** * Success. */ $old_data['data']['sizes'][ $size ] = [ 'success' => true, 'original_size' => $data['original_size'], 'optimized_size' => $data['optimized_size'], 'percent' => round( ( $data['original_size'] - $data['optimized_size'] ) / $data['original_size'] * 100, 2 ), ]; } $this->update_row( $old_data ); } /** * Delete the media optimization data, level, and status. * * @since 1.9 * @access public * @author Grégory Viguier */ public function delete_optimization_data() { if ( ! $this->is_valid() ) { return; } $this->delete_row(); } /** * Delete the optimization data for the given sizes. * If all sizes are removed, all optimization data is deleted. * Status and level are not modified nor removed if the "full" size is removed. This leaves the media in a Schrödinger state. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @param array $sizes A list of sizes to remove. */ public function delete_sizes_optimization_data( array $sizes ) { if ( ! $sizes || ! $this->is_valid() ) { return; } $data = array_merge( $this->get_reset_data(), $this->get_row() ); $data['data']['sizes'] = ! empty( $data['data']['sizes'] ) && is_array( $data['data']['sizes'] ) ? $data['data']['sizes'] : []; if ( ! $data['data']['sizes'] ) { return; } $remaining_sizes_data = array_diff_key( $data['data']['sizes'], array_flip( $sizes ) ); if ( ! $remaining_sizes_data ) { // All sizes have been removed: delete everything. $this->delete_optimization_data(); return; } if ( count( $remaining_sizes_data ) === count( $data['data']['sizes'] ) ) { // Nothing has been removed. return; } $data['data']['sizes'] = $remaining_sizes_data; $this->update_row( $data ); } /** * Get default values used to reset optimization data. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return array { * The default values related to the optimization. * * @type string $optimization_level The optimization level. * @type string $status The status: success, already_optimized, error. * @type array $data Data related to the thumbnails. * } */ protected function get_reset_data() { $db_instance = $this->get_row_db_instance(); $primary_key = $db_instance->get_primary_key(); $column_defaults = $db_instance->get_column_defaults(); return array_diff_key( $column_defaults, [ 'data_id' => 0, $primary_key => 0, ] ); } /** * Update the row. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $data The data to update. */ public function update_row( $data ) { if ( ! $this->db_class_name || $this->id <= 0 ) { return; } $primary_key = $this->get_row_db_instance()->get_primary_key(); // This is needed in case the row doesn't exist yet. $data[ $primary_key ] = $this->id; $this->get_row_db_instance()->update( $this->id, $data ); $this->reset_row_cache(); } } 3rd-party/nextgen-gallery/classes/Main.php 0000644 00000005345 15174671745 0014560 0 ustar 00 <?php namespace Imagify\ThirdParty\NGG; use Imagify\Traits\InstanceGetterTrait; /** * Imagify NextGen Gallery class. * * @since 1.5 * @author Jonathan Buttigieg */ class Main { use InstanceGetterTrait; /** * Class version. * * @var string */ const VERSION = '1.1'; /** * Launch the hooks. * * @since 1.6.5 * @access public * @author Grégory Viguier */ public function init() { static $done = false; if ( $done ) { return; } $done = true; add_filter( 'imagify_register_context', [ $this, 'register_context' ] ); add_filter( 'imagify_context_class_name', [ $this, 'add_context_class_name' ], 10, 2 ); add_filter( 'imagify_process_class_name', [ $this, 'add_process_class_name' ], 10, 2 ); add_filter( 'imagify_bulk_class_name', [ $this, 'add_bulk_class_name' ], 10, 2 ); add_action( 'init', [ $this, 'add_mixin' ] ); } /** * Register the context used for NGG. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $contexts An array of context names. * @return array */ public function register_context( $contexts ) { $contexts[] = 'ngg'; return $contexts; } /** * Filter the name of the class to use to define a context. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int $class_name The class name. * @param string $context The context name. * @return string */ public function add_context_class_name( $class_name, $context ) { if ( 'ngg' === $context ) { return '\\Imagify\\ThirdParty\\NGG\\Context\\NGG'; } return $class_name; } /** * Filter the name of the class to use for the optimization. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int $class_name The class name. * @param string $context The context name. * @return string */ public function add_process_class_name( $class_name, $context ) { if ( 'ngg' === $context ) { return '\\Imagify\\ThirdParty\\NGG\\Optimization\\Process\\NGG'; } return $class_name; } /** * Filter the name of the class to use for the bulk optimization. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int $class_name The class name. * @param string $context The context name. * @return string */ public function add_bulk_class_name( $class_name, $context ) { if ( 'ngg' === $context ) { return '\\Imagify\\ThirdParty\\NGG\\Bulk\\NGG'; } return $class_name; } /** * Add custom NGG mixin to override its functions. * * @since 1.5 * @access public * @author Jonathan Buttigieg */ public function add_mixin() { \C_Gallery_Storage::get_instance()->get_wrapped_instance()->add_mixin( '\\Imagify\\ThirdParty\\NGG\\NGGStorage' ); } } 3rd-party/nextgen-gallery/classes/Media/NGG.php 0000644 00000034327 15174671745 0015330 0 ustar 00 <?php namespace Imagify\ThirdParty\NGG\Media; use Imagify\Deprecated\Traits\Media\NGGDeprecatedTrait; /** * Media class for the medias from NextGen Gallery. * * @since 1.9 * @author Grégory Viguier */ class NGG extends \Imagify\Media\AbstractMedia { use NGGDeprecatedTrait; /** * Context (where the media "comes from"). * * @var string * @since 1.9 * @access protected * @author Grégory Viguier */ protected $context = 'ngg'; /** * The image object. * * @var object A \nggImage object. * @since 1.9 * @access protected * @author Grégory Viguier */ protected $image; /** * The storage object used by NGG. * * @var object A \C_Gallery_Storage object (by default). * @since 1.8. * @access protected */ protected $storage; /** * The constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int|\nggImage|\nggdb|\StdClass $id The NGG image ID, \nggImage object, \nggdb object, or an anonym object containing a pid property. */ public function __construct( $id ) { if ( ! static::constructor_accepts( $id ) ) { parent::__construct( 0 ); return; } if ( is_numeric( $id ) ) { $this->image = \nggdb::find_image( (int) $id ); $id = ! empty( $this->image->pid ) ? (int) $this->image->pid : 0; } elseif ( $id instanceof \nggImage ) { $this->image = $id; $id = (int) $id->pid; } elseif ( is_object( $id ) ) { $this->image = \nggdb::find_image( (int) $id->pid ); $id = ! empty( $this->image->pid ) ? (int) $this->image->pid : 0; } else { $id = 0; } if ( ! $id ) { $this->image = null; parent::__construct( 0 ); return; } parent::__construct( $id ); // NGG storage. if ( ! empty( $this->image->_ngiw ) ) { $this->storage = $this->image->_ngiw->get_storage()->object; } else { $this->storage = \C_Gallery_Storage::get_instance()->object; } // Load nggAdmin class. $ngg_admin_functions_path = WP_PLUGIN_DIR . '/' . NGGFOLDER . '/products/photocrati_nextgen/modules/ngglegacy/admin/functions.php'; if ( ! class_exists( 'nggAdmin' ) && $this->filesystem->exists( $ngg_admin_functions_path ) ) { require_once $ngg_admin_functions_path; } } /** * Tell if the given entry can be accepted in the constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id Whatever. * @return bool */ public static function constructor_accepts( $id ) { if ( ! $id ) { return false; } if ( is_numeric( $id ) || $id instanceof \nggImage ) { return true; } return is_object( $id ) && ! empty( $id->pid ) && is_numeric( $id->pid ); } /** ----------------------------------------------------------------------------------------- */ /** NGG SPECIFICS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the NGG image object. * * @since 1.9 * @access public * @author Grégory Viguier * * @return \nggImage */ public function get_ngg_image() { return $this->image; } /** * Get the NGG storage object. * * @since 1.9 * @access public * @author Grégory Viguier * * @return \C_Gallery_Storage */ public function get_ngg_storage() { return $this->storage; } /** ----------------------------------------------------------------------------------------- */ /** ORIGINAL FILE =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the original file path, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_original_path() { if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_path( 'original' ); } return ! empty( $this->image->imagePath ) ? $this->image->imagePath : false; } /** ----------------------------------------------------------------------------------------- */ /** FULL SIZE FILE ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the URL of the media’s full size file. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_fullsize_url() { if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_url(); } return ! empty( $this->image->imageURL ) ? $this->image->imageURL : false; } /** * Get the path to the media’s full size file, even if the file doesn't exist. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_fullsize_path() { if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_path(); } return ! empty( $this->image->imagePath ) ? $this->image->imagePath : false; } /** ----------------------------------------------------------------------------------------- */ /** BACKUP FILE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the backup URL, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_backup_url() { if ( ! $this->is_valid() ) { return false; } return site_url( '/' . $this->filesystem->make_path_relative( $this->get_raw_backup_path() ) ); } /** * Get the backup file path, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_backup_path() { if ( ! $this->is_valid() ) { return false; } return get_imagify_ngg_attachment_backup_path( $this->get_raw_original_path() ); } /** ----------------------------------------------------------------------------------------- */ /** THUMBNAILS ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Create the media thumbnails. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|WP_Error True on success. A \WP_Error instance on failure. */ public function generate_thumbnails() { if ( ! $this->is_valid() ) { return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $image_data = $this->storage->_image_mapper->find( $this->get_id() ); if ( ! $image_data ) { return new \WP_Error( 'no_ngg_image', __( 'Image not found in NextGen Gallery data.', 'imagify' ) ); } if ( empty( $image_data->meta_data['backup'] ) || ! is_array( $image_data->meta_data['backup'] ) ) { $full_path = $this->storage->get_image_abspath( $image_data ); $image_data->meta_data['backup'] = [ 'filename' => $this->filesystem->file_name( $full_path ), // Yes, $full_path. 'width' => $image_data->meta_data['width'], // Original image width. 'height' => $image_data->meta_data['height'], // Original image height. 'generated' => microtime(), ]; } $backup_path = $this->storage->get_image_abspath( $image_data, 'backup' ); $failed = []; foreach ( $this->get_media_files() as $size_name => $size_data ) { if ( 'full' === $size_name ) { continue; } $params = $this->storage->get_image_size_params( $image_data, $size_name ); $thumbnail = @$this->storage->generate_image_clone( // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $backup_path, $this->storage->get_image_abspath( $image_data, $size_name ), $params ); if ( ! $thumbnail ) { // Failed. $failed[] = $size_name; unset( $image_data->meta_data[ $size_name ] ); continue; } $size_meta = [ 'width' => 0, 'height' => 0, 'filename' => \M_I18n::mb_basename( $thumbnail->fileName ), 'generated' => microtime(), ]; $dimensions = $this->filesystem->get_image_size( $thumbnail->fileName ); if ( $dimensions ) { $size_meta['width'] = $dimensions['width']; $size_meta['height'] = $dimensions['height']; } if ( isset( $params['crop_frame'] ) ) { $size_meta['crop_frame'] = $params['crop_frame']; } $image_data->meta_data[ $size_name ] = $size_meta; } // Keep our property up to date. $this->image->_ngiw->_cache['meta_data'] = $image_data->meta_data; $this->image->_ngiw->_orig_image = $image_data; $post_id = $this->storage->_image_mapper->save( $image_data ); if ( ! $post_id ) { return new \WP_Error( 'meta_data_not_saved', __( 'Related NextGen Gallery data could not be saved.', 'imagify' ) ); } if ( $failed ) { return new \WP_Error( 'thumbnail_restore_failed', sprintf( _n( '%n thumbnail could not be restored.', '%n thumbnails could not be restored.', count( $failed ), 'imagify' ), count( $failed ) ), [ 'failed_thumbnails' => $failed ] ); } return true; } /** ----------------------------------------------------------------------------------------- */ /** MEDIA DATA ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the current media has the required data (the data containing the file paths and thumbnails). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function has_required_media_data() { static $sizes; if ( ! $this->is_valid() ) { return false; } if ( ! isset( $sizes ) ) { $sizes = $this->get_media_files(); } return $sizes && ! empty( $this->image->imagePath ); } /** * Get the list of the files of this media, including the full size file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array { * An array with the size names as keys ('full' is used for the full size file), and arrays of data as values: * * @type string $size The size name. * @type string $path Absolute path to the file. * @type int $width The file width. * @type int $height The file height. * @type string $mime-type The file mime type. * @type bool $disabled True if the size is disabled in the plugin’s settings. * } */ public function get_media_files() { if ( ! $this->is_valid() ) { return []; } $fullsize_path = $this->get_raw_fullsize_path(); if ( ! $fullsize_path ) { return []; } $dimensions = $this->get_dimensions(); $all_sizes = [ 'full' => [ 'size' => 'full', 'path' => $fullsize_path, 'width' => $dimensions['width'], 'height' => $dimensions['height'], 'mime-type' => $this->get_mime_type(), 'disabled' => false, ], ]; if ( ! $this->is_image() ) { return $this->filter_media_files( $all_sizes ); } // Remove common values (that have no value for us here, lol). Also remove 'full' and 'backup'. $image_data = array_diff_key( $this->image->meta_data, [ 'full' => 1, 'backup' => 1, 'width' => 1, 'height' => 1, 'md5' => 1, 'aperture' => 1, 'credit' => 1, 'camera' => 1, 'caption' => 1, 'created_timestamp' => 1, 'copyright' => 1, 'focal_length' => 1, 'iso' => 1, 'shutter_speed' => 1, 'flash' => 1, 'title' => 1, 'keywords' => 1, 'saved' => 1, ] ); if ( ! $image_data ) { return $this->filter_media_files( $all_sizes ); } $ngg_data = $this->storage->_image_mapper->find( $this->get_id() ); foreach ( $image_data as $size => $size_data ) { if ( ! isset( $size_data['width'], $size_data['height'], $size_data['filename'], $size_data['generated'] ) ) { continue; } $file_type = (object) wp_check_filetype( $size_data['filename'], $this->get_allowed_mime_types() ); if ( ! $file_type->type ) { continue; } $all_sizes[ $size ] = [ 'size' => $size, 'path' => $this->storage->get_image_abspath( $ngg_data, $size ), 'width' => (int) $size_data['width'], 'height' => (int) $size_data['height'], 'mime-type' => $file_type->type, 'disabled' => false, ]; } return $this->filter_media_files( $all_sizes ); } /** * If the media is an image, get its width and height. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array */ public function get_dimensions() { if ( ! $this->is_image() ) { return [ 'width' => 0, 'height' => 0, ]; } return [ 'width' => ! empty( $this->image->meta_data['width'] ) ? (int) $this->image->meta_data['width'] : 0, 'height' => ! empty( $this->image->meta_data['height'] ) ? (int) $this->image->meta_data['height'] : 0, ]; } /** * Update the media data dimensions. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $dimensions { * An array containing width and height. * * @type int $width The image width. * @type int $height The image height. * } */ protected function update_media_data_dimensions( $dimensions ) { $changed = false; $data = [ 'width' => $dimensions['width'], 'height' => $dimensions['height'], 'md5' => md5_file( $this->get_raw_fullsize_path() ), ]; foreach ( $data as $k => $v ) { if ( ! isset( $this->image->meta_data[ $k ] ) || $this->image->meta_data[ $k ] !== $v ) { $this->image->meta_data[ $k ] = $v; $changed = true; } } if ( $changed ) { \nggdb::update_image_meta( $this->id, $this->image->meta_data ); } } } 3rd-party/nextgen-gallery/classes/NGGStorage.php 0000644 00000006211 15174671745 0015625 0 ustar 00 <?php namespace Imagify\ThirdParty\NGG; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Imagify NextGen Gallery storage class. * * @since 1.5 * @author Jonathan Buttigieg */ class NGGStorage extends \Mixin { /** * Class version. * * @var string */ const VERSION = '1.1'; /** * Delete a gallery AND all the pictures associated to this gallery! * * @since 1.5 * @author Jonathan Buttigieg * * @param int|object $gallery A gallery ID or object. * @return bool Whetther tha gallery was been deleted or not. */ public function delete_gallery( $gallery ) { $gallery_id = is_numeric( $gallery ) ? $gallery : $gallery->{$gallery->id_field}; $images_id = \nggdb::get_ids_from_gallery( $gallery_id ); foreach ( $images_id as $pid ) { $process = imagify_get_optimization_process( $pid, 'ngg' ); if ( $process->is_valid() && $process->get_data()->is_optimized() ) { $process->get_data()->delete_optimization_data(); } } return $this->call_parent( 'delete_gallery', $gallery ); } /** * Generates a specific size for an image. * * @since 1.5 * @author Jonathan Buttigieg * * @param int|object $image An image ID or NGG object. * @param string $size An image size. * @param array $params An array of parameters. * @param bool $skip_defaults Whatever NGG does with default settings. * @return bool|object An object on success. False on failure. */ public function generate_image_size( $image, $size, $params = null, $skip_defaults = false ) { // $image could be an object or an (int) image ID. if ( is_numeric( $image ) ) { $image = $this->object->_image_mapper->find( $image ); } // If a user adds a watermark, rotates or resizes an image, we restore it. // TO DO - waiting for a hook to be able to re-optimize the original size after restoring. if ( isset( $image->pid ) && ( true === $params['watermark'] || ( isset( $params['rotation'] ) || isset( $params['flip'] ) ) || ( ! empty( $params['width'] ) || ! empty( $params['height'] ) ) ) ) { $process = imagify_get_optimization_process( $image->pid, 'ngg' ); if ( $process->is_valid() && $process->get_data()->is_optimized() ) { $process->get_data()->delete_optimization_data(); } } return $this->call_parent( 'generate_image_size', $image, $size, $params, $skip_defaults ); } /** * Recover image from backup. * * @since 1.5 * @author Jonathan Buttigieg * * @param int|object $image An image ID or NGG object. * @return string|bool Result code on success. False on failure. */ public function recover_image( $image ) { // $image could be an object or an (int) image ID. if ( is_numeric( $image ) ) { $image = $this->object->_image_mapper->find( $image ); } if ( ! $image ) { return false; } // Remove Imagify data. if ( isset( $image->pid ) ) { $process = imagify_get_optimization_process( $image->pid, 'ngg' ); if ( $process->is_valid() && $process->get_data()->is_optimized() ) { $process->get_data()->delete_optimization_data(); } } return $this->call_parent( 'recover_image', $image ); } } 3rd-party/nextgen-gallery/classes/DynamicThumbnails.php 0000644 00000005211 15174671745 0017277 0 ustar 00 <?php namespace Imagify\ThirdParty\NGG; use Imagify\Traits\InstanceGetterTrait; /** * Class that handles the optimization of thumbnails dynamically generated. * * @since 1.9 * @author Grégory Viguier */ class DynamicThumbnails { use InstanceGetterTrait; /** * The queue containing the sizes, grouped by image ID. * * @var array * @since 1.9 * @access protected * @author Grégory Viguier */ protected static $sizes = []; /** * A list of NGG image objects, grouped by image ID. * * @var array * @since 1.9 * @access protected * @author Grégory Viguier */ protected static $images = []; /** * Add a dynamically generated thumbnail to the background process queue. * * @since 1.9 * @access public * @author Grégory Viguier * * @param object $image A NGG image object. * @param string $size The thumbnail size name. */ public function push_to_queue( $image, $size ) { static $done = false; if ( empty( $image->pid ) ) { // WUT? return; } if ( empty( static::$sizes[ $image->pid ] ) ) { static::$sizes[ $image->pid ] = []; } static::$sizes[ $image->pid ][] = $size; if ( empty( static::$images[ $image->pid ] ) ) { static::$images[ $image->pid ] = $image; } if ( $done ) { return; } $done = true; add_action( 'shutdown', [ $this, 'optimize' ], 555 ); // Must come before 666 (see Imagify_Abstract_Background_Process->init()). } /** * Launch the optimizations. * * @since 1.9 * @access public * @author Grégory Viguier */ public function optimize() { if ( empty( static::$sizes ) ) { return; } foreach ( static::$sizes as $image_id => $sizes ) { if ( empty( static::$images[ $image_id ] ) ) { continue; } $sizes = array_filter( $sizes ); if ( empty( $sizes ) ) { continue; } $process = imagify_get_optimization_process( static::$images[ $image_id ], 'ngg' ); if ( ! $process->is_valid() || ! $process->get_media()->is_supported() ) { continue; } $data = $process->get_data(); if ( ! $data->is_optimized() ) { // The main image is not optimized. continue; } $sizes = array_unique( $sizes ); foreach ( $sizes as $i => $size ) { $size_status = $data->get_size_data( $size, 'success' ); if ( $size_status ) { // This thumbnail has already been processed. unset( $sizes[ $i ] ); } } if ( empty( $sizes ) ) { continue; } $optimization_level = $process->get_data()->get_optimization_level(); $args = [ 'hook_suffix' => 'optimize_generated_image', ]; $process->optimize_sizes( $sizes, $optimization_level, $args ); } } } 3rd-party/nextgen-gallery/classes/Context/NGG.php 0000644 00000004737 15174671745 0015737 0 ustar 00 <?php namespace Imagify\ThirdParty\NGG\Context; use Imagify\Context\AbstractContext; use Imagify\Traits\InstanceGetterTrait; /** * Context class used for the WP media library. * * @since 1.9 * @author Grégory Viguier */ class NGG extends AbstractContext { use InstanceGetterTrait; /** * Context "short name". * * @var string * @since 1.9 * @author Grégory Viguier */ protected $context = 'ngg'; /** * Type of files this context allows. * * @var string Possible values are: * - 'all' to allow all types. * - 'image' to allow only images. * - 'not-image' to allow only pdf files. * @since 1.9 * @see imagify_get_mime_types() * @author Grégory Viguier */ protected $allowed_mime_types = 'image'; /** * The thumbnail sizes for this context, except the full size. * * @var array { * Data for the currently registered thumbnail sizes. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * } * @since 1.9 * @author Grégory Viguier */ protected $thumbnail_sizes = []; /** * Tell if the optimization process is allowed to backup in this context. * * @var bool * @since 1.9 * @author Grégory Viguier */ protected $can_backup = true; /** * Get images max width for this context. This is used when resizing. * 0 means to not resize. * * @since 1.9.8 * @author Grégory Viguier * * @return int */ public function get_resizing_threshold() { return 0; } /** * Get user capacity to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param string $describer Capacity describer. Possible values are like 'manage', 'bulk-optimize', 'manual-optimize', 'auto-optimize'. * @return string */ public function get_capacity( $describer = 'manage' ) { switch ( $describer ) { case 'manage': $capacity = 'NextGEN Change options'; break; case 'bulk-optimize': $capacity = 'NextGEN Manage others gallery'; break; case 'optimize': case 'restore': case 'manual-optimize': case 'manual-restore': $capacity = 'NextGEN Manage gallery'; break; case 'auto-optimize': $capacity = 'NextGEN Upload images'; break; default: $capacity = $describer; } return $this->filter_capacity( $capacity, $describer ); } } 3rd-party/nextgen-gallery/classes/DB.php 0000644 00000004266 15174671745 0014162 0 ustar 00 <?php namespace Imagify\ThirdParty\NGG; use Imagify\Traits\InstanceGetterTrait; /** * Imagify NextGen Gallery DB class. * * @since 1.5 * @author Jonathan Buttigieg */ class DB extends \Imagify_Abstract_DB { use InstanceGetterTrait; /** * Class version. * * @var string */ const VERSION = '1.1.1'; /** * The suffix used in the name of the database table (so, without the wpdb prefix). * * @var string * @since 1.7 * @access protected */ protected $table = 'ngg_imagify_data'; /** * The version of our database table. * * @var int * @since 1.5 * @since 1.7 Not public anymore, now an integer. * @access protected */ protected $table_version = 100; /** * Tell if the table is the same for each site of a Multisite. * * @var bool * @since 1.7 * @access protected */ protected $table_is_global = false; /** * The name of the primary column. * * @var string * @since 1.5 * @since 1.7 Not public anymore. * @access protected */ protected $primary_key = 'pid'; /** * Whitelist of columns. * * @since 1.5 * @access public * @author Jonathan Buttigieg * * @return array */ public function get_columns() { return [ 'data_id' => '%d', 'pid' => '%d', 'optimization_level' => '%s', 'status' => '%s', 'data' => '%s', ]; } /** * Default column values. * * @since 1.5 * @access public * @author Jonathan Buttigieg * * @return array */ public function get_column_defaults() { return [ 'data_id' => 0, 'pid' => 0, 'optimization_level' => '', 'status' => '', 'data' => [], ]; } /** * Get the query to create the table fields. * * @since 1.7 * @access protected * @author Grégory Viguier * * @return string */ protected function get_table_schema() { return " data_id int(11) unsigned NOT NULL AUTO_INCREMENT, pid int(11) unsigned NOT NULL default 0, optimization_level varchar(1) NOT NULL default '', status varchar(30) NOT NULL default '', data longtext NOT NULL default '', PRIMARY KEY (data_id), KEY pid (pid)"; } } 3rd-party/nextgen-gallery/nextgen-gallery.php 0000644 00000002221 15174671745 0015332 0 ustar 00 <?php use Imagify\ThirdParty\NGG; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); if ( ! class_exists( 'C_NextGEN_Bootstrap' ) || ! class_exists( 'Mixin' ) || ! get_site_option( 'ngg_options' ) ) { return; } class_alias( '\\Imagify\\ThirdParty\\NGG\\Main', '\\Imagify_NGG' ); class_alias( '\\Imagify\\ThirdParty\\NGG\\DB', '\\Imagify_NGG_DB' ); class_alias( '\\Imagify\\ThirdParty\\NGG\\NGGStorage', '\\Imagify_NGG_Storage' ); require IMAGIFY_PATH . 'inc/3rd-party/nextgen-gallery/inc/functions/admin-stats.php'; require IMAGIFY_PATH . 'inc/3rd-party/nextgen-gallery/inc/functions/attachments.php'; require IMAGIFY_PATH . 'inc/3rd-party/nextgen-gallery/inc/functions/common.php'; require IMAGIFY_PATH . 'inc/3rd-party/nextgen-gallery/inc/common/attachments.php'; NGG\Main::get_instance()->init(); NGG\DB::get_instance()->init(); if ( is_admin() ) { require IMAGIFY_PATH . 'inc/3rd-party/nextgen-gallery/inc/admin/enqueue.php'; require IMAGIFY_PATH . 'inc/3rd-party/nextgen-gallery/inc/admin/menu.php'; require IMAGIFY_PATH . 'inc/3rd-party/nextgen-gallery/inc/admin/gallery.php'; require IMAGIFY_PATH . 'inc/3rd-party/nextgen-gallery/inc/admin/bulk.php'; } 3rd-party/wp-cloudflare-super-page-cache.php 0000644 00000001000 15174671745 0014765 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; add_action( 'admin_enqueue_scripts', function ( $hook_suffix ) { $imagify_admin_pages = [ 'media_page_imagify-bulk-optimization', 'settings_page_imagify', 'media_page_imagify-files', 'nextgen-gallery_page_imagify-ngg-bulk-optimization', ]; if ( ! is_admin() || ! class_exists( 'SWCFPC_Backend' ) || ! in_array( $hook_suffix, $imagify_admin_pages, true ) ) { return; } wp_deregister_script( 'swcfpc_sweetalert_js' ); }, 100 ); 3rd-party/hosting/flywheel.php 0000644 00000004100 15174671745 0012410 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Changes the text on the Varnish one-click block. * * @since 3.0 * @author Remy Perona * * @param array $settings Field settings data. * * @return array modified field settings data. */ function rocket_flywheel_varnish_field( $settings ) { $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'Flywheel' ); return $settings; } add_filter( 'rocket_varnish_field_settings', 'rocket_flywheel_varnish_field' ); add_filter( 'rocket_display_input_varnish_auto_purge', '__return_false' ); /** * Allow to purge Varnish on Flywheel websites * * @since 2.6.8 */ add_filter( 'do_rocket_varnish_http_purge', '__return_true' ); add_filter( 'do_rocket_generate_caching_files', '__return_false' ); // Prevent mandatory cookies on hosting with server cache. add_filter( 'rocket_cache_mandatory_cookies', '__return_empty_array', PHP_INT_MAX ); /** * Set up the right Varnish IP for Flywheel * * @since 2.6.8 * @param array $varnish_ip Varnish IP. */ function rocket_varnish_ip_on_flywheel( $varnish_ip ) { $varnish_ip[] = '127.0.0.1'; return $varnish_ip; } add_filter( 'rocket_varnish_ip', 'rocket_varnish_ip_on_flywheel' ); /** * Remove WP Rocket functions on WP core action hooks to prevent triggering a double cache clear. * * @since 3.3.1 * @author Remy Perona * * @return void */ function rocket_flywheel_remove_partial_purge_hooks() { // WP core action hooks rocket_clean_post() gets hooked into. $clean_post_hooks = [ // Disables the refreshing of partial cache when content is edited. 'wp_trash_post', 'delete_post', 'clean_post_cache', 'wp_update_comment_count', ]; // Remove rocket_clean_post() from core action hooks. array_map( function ( $hook ) { remove_action( $hook, 'rocket_clean_post' ); }, $clean_post_hooks ); remove_filter( 'rocket_clean_files', 'rocket_clean_files_users' ); } add_action( 'wp_rocket_loaded', 'rocket_flywheel_remove_partial_purge_hooks' ); 3rd-party/hosting/pressable.php 0000644 00000001272 15174671745 0012560 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( defined( 'IS_PRESSABLE' ) ) : add_filter( 'imagify_site_root', 'imagify_pressable_site_root', IMAGIFY_INT_MAX ); /** * Filter the path to the site's root. * * @since 1.9.4 * @author Grégory Viguier * * @param string|null $root_path Path to the site's root. Default is null. * @return string */ function imagify_pressable_site_root( $root_path ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $upload_basedir = trim( wp_normalize_path( WP_CONTENT_DIR ), '/' ); $upload_basedir = explode( '/', $upload_basedir ); $upload_basedir = reset( $upload_basedir ); return '/' . $upload_basedir . '/'; } endif; 3rd-party/hosting/wpengine.php 0000644 00000001307 15174671745 0012413 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( class_exists( 'WpeCommon' ) ) : add_filter( 'imagify_unoptimized_attachment_limit', '_imagify_wpengine_unoptimized_attachment_limit' ); add_filter( 'imagify_count_saving_data_limit', '_imagify_wpengine_unoptimized_attachment_limit' ); /** * Change the limit for the number of posts: WP Engine limits SQL queries size to 2048 octets (16384 characters). * * @since 1.4.7 * @since 1.6.7 Renamed (and deprecated) _imagify_wengine_unoptimized_attachment_limit() into _imagify_wpengine_unoptimized_attachment_limit(). * @author Jonathan Buttigieg * * @return int */ function _imagify_wpengine_unoptimized_attachment_limit() { return 2500; } endif; 3rd-party/hosting/wordpress-com.php 0000644 00000001401 15174671745 0013376 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); if ( defined( 'WPCOMSH_VERSION' ) ) : add_filter( 'imagify_site_root', 'imagify_wpcom_site_root', IMAGIFY_INT_MAX ); /** * Filter the path to the site's root. * * @since 1.8.4 * @author Grégory Viguier * * @param string|null $root_path Path to the site's root. Default is null. * @return string|null */ function imagify_wpcom_site_root( $root_path ) { $upload_basedir = imagify_get_filesystem()->get_upload_basedir( true ); if ( strpos( $upload_basedir, '/wp-content/' ) === false ) { // Uh oooooh... return $root_path; } $upload_basedir = explode( '/wp-content/', $upload_basedir ); $upload_basedir = reset( $upload_basedir ); return $upload_basedir . '/'; } endif; 3rd-party/hosting/siteground.php 0000644 00000005464 15174671745 0012772 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Returns the current version of the SG Optimizer plugin. * * @since 3.2.3.1 * @author Remy Perona * * @return string version number. */ function rocket_get_sg_optimizer_version() { static $version; if ( isset( $version ) ) { return $version; } $sg_optimizer = get_file_data( WP_PLUGIN_DIR . '/sg-cachepress/sg-cachepress.php', [ 'Version' => 'Version' ] ); $version = $sg_optimizer['Version']; return $version; } /** * Checks if SG Optimizer Supercache is active. * * @since 3.2.3.1 * @author Remy Perona * * @return bool */ function rocket_is_supercacher_active() { if ( ! version_compare( rocket_get_sg_optimizer_version(), '5.0' ) < 0 ) { global $sg_cachepress_environment; return isset( $sg_cachepress_environment ) && $sg_cachepress_environment instanceof SG_CachePress_Environment && $sg_cachepress_environment->cache_is_enabled(); } return (bool) get_option( 'siteground_optimizer_enable_cache', 0 ); } /** * Call the cache server to purge the cache with SuperCacher (SiteGround). * * @since 2.3 * * @return void */ function rocket_clean_supercacher() { if ( ! rocket_is_supercacher_active() ) { return; } if ( ! version_compare( rocket_get_sg_optimizer_version(), '5.0' ) < 0 ) { SiteGround_Optimizer\Supercacher\Supercacher::purge_cache(); } elseif ( isset( $sg_cachepress_supercacher ) && $sg_cachepress_supercacher instanceof SG_CachePress_Supercacher ) { $sg_cachepress_supercacher->purge_cache(); } } /** * Clean WP Rocket cache when cleaning SG cache * * @return void */ function rocket_sg_clear_cache() { if ( empty( $_GET['_wpnonce'] ) ) { return; } if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'sg-cachepress-purge' ) ) { return; } if ( ! current_user_can( 'rocket_purge_cache' ) ) { return; } rocket_clean_domain(); } if ( rocket_is_supercacher_active() ) { add_action( 'admin_post_sg-cachepress-purge', 'rocket_sg_clear_cache', 0 ); add_action( 'rocket_after_clean_domain', 'rocket_clean_supercacher' ); add_filter( 'rocket_display_varnish_options_tab', '__return_false' ); // Prevent mandatory cookies on hosting with server cache. add_filter( 'rocket_cache_mandatory_cookies', '__return_empty_array', PHP_INT_MAX ); /** * Force WP Rocket caching on SG Optimizer versions before 4.0.5. * * @since 3.0.4 * @author Arun Basil Lal * * @link https://github.com/wp-media/wp-rocket/issues/925 */ if ( version_compare( rocket_get_sg_optimizer_version(), '4.0.5' ) < 0 ) { add_filter( 'do_rocket_generate_caching_files', '__return_true', 11 ); } if ( version_compare( rocket_get_sg_optimizer_version(), '5.0' ) < 0 ) { add_action( 'wp_ajax_sg-cachepress-purge', 'rocket_sg_clear_cache', 0 ); } else { add_action( 'wp_ajax_admin_bar_purge_cache', 'rocket_sg_clear_cache', 0 ); } } 3rd-party/3rd-party.php 0000644 00000007527 15174671745 0010763 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( class_exists( 'FlywheelNginxCompat' ) ) { require WP_ROCKET_3RD_PARTY_PATH . 'hosting/flywheel.php'; } if ( defined( 'DB_HOST' ) && strpos( DB_HOST, '.wpserveur.net' ) !== false ) { require WP_ROCKET_3RD_PARTY_PATH . 'hosting/wp-serveur.php'; } if ( rocket_is_plugin_active( 'sg-cachepress/sg-cachepress.php' ) ) { require WP_ROCKET_3RD_PARTY_PATH . 'hosting/siteground.php'; } if ( defined( 'PL_INSTANCE_REF' ) && class_exists( '\Presslabs\Cache\CacheHandler' ) && file_exists( WP_CONTENT_DIR . '/advanced-cache.php' ) ) { require WP_ROCKET_3RD_PARTY_PATH . 'hosting/presslabs.php'; } require WP_ROCKET_3RD_PARTY_PATH . 'hosting/pagely.php'; require WP_ROCKET_3RD_PARTY_PATH . 'hosting/nginx.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/slider/meta-slider.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/slider/soliloquy.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/i18n/polylang.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/aelia-currencyswitcher.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/aelia-prices-by-country.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/aelia-tax-display-by-country.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/woocommerce-multilingual.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/woocommerce-currency-converter-widget.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/edd-software-licencing.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/easy-digital-downloads.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/ithemes-exchange.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/jigoshop.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/wpshop.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/ecommerce/give.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/autoptimize.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/envira-gallery.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/cookies/cookie-notice.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/cookies/uk-cookie-consent.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/cookies/eu-cookie-law.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/cookies/weepie-cookie-allow.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/cookies/gdpr.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/rating/kk-star-ratings.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/rating/wp-postratings.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/wp-print.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/buddypress.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/disqus.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/custom-login.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/mobile/wp-appkit.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/seo/premium-seo-pack.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/wp-rest-api.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/page-builder/thrive-visual-editor.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/page-builder/visual-composer.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/security/secupress.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/security/sf-move-login.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/security/wps-hide-login.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/varnish-http-purge.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/thrive-leads.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/mailchimp.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/advanced-custom-fields.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/wp-offload-s3.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/wp-offload-s3-assets.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/s2member.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/sumome.php'; require WP_ROCKET_3RD_PARTY_PATH . 'plugins/nginx-helper.php'; require WP_ROCKET_3RD_PARTY_PATH . 'themes/studiopress.php'; 3rd-party/wp-real-media-library.php 0000644 00000004136 15174671745 0013215 0 ustar 00 <?php use Imagify\Notices\Notices; defined( 'ABSPATH' ) || exit; if ( defined( 'RML_FILE' ) ) : /** * Dequeue all WP Real Media Library's styles and scripts where we use ours. * * Prevent WP Real Media Library to use its outdated version of SweetAlert where we need ours, and to mess with our CSS styles. * * @since 1.6.13 */ function imagify_wprml_init() { static $done = false; if ( $done ) { return; } $done = true; if ( ! class_exists( '\\MatthiasWeb\\RealMediaLibrary\\general\\Backend' ) ) { return; } $notices = Notices::get_instance(); if ( $notices->has_notices() && ( $notices->display_welcome_steps() || $notices->display_wrong_api_key() ) ) { // We display a notice that uses SweetAlert. imagify_wprml_dequeue(); return; } if ( imagify_is_screen( 'bulk' ) || imagify_is_screen( 'imagify-settings' ) ) { // We display a page that uses SweetAlert. imagify_wprml_dequeue(); return; } if ( function_exists( 'imagify_get_ngg_bulk_screen_id' ) && imagify_is_screen( imagify_get_ngg_bulk_screen_id() ) ) { // We display the NGG Bulk Optimization page. imagify_wprml_dequeue(); } } add_action( 'current_screen', 'imagify_wprml_init' ); /** * Prevent WP Real Media Library to enqueue its styles and scripts. * * @since 1.6.13 */ function imagify_wprml_dequeue() { $instance = \MatthiasWeb\RealMediaLibrary\general\Backend::getInstance(); remove_action( 'admin_enqueue_scripts', [ $instance, 'admin_enqueue_scripts' ], 0 ); remove_action( 'admin_footer', [ $instance, 'admin_footer' ] ); if ( class_exists( '\\MatthiasWeb\\RealMediaLibrary\\general\\FolderShortcode' ) ) { $instance = \MatthiasWeb\RealMediaLibrary\general\FolderShortcode::getInstance(); remove_action( 'admin_head', [ $instance, 'admin_head' ] ); remove_action( 'admin_enqueue_scripts', [ $instance, 'admin_enqueue_scripts' ] ); } if ( class_exists( '\\MatthiasWeb\\RealMediaLibrary\\comp\\PageBuilders' ) ) { $instance = \MatthiasWeb\RealMediaLibrary\comp\PageBuilders::getInstance(); remove_action( 'init', [ $instance, 'init' ] ); } } endif; 3rd-party/wp-rocket/classes/Main.php 0000644 00000003725 15174671745 0013366 0 ustar 00 <?php namespace Imagify\ThirdParty\WPRocket; use Imagify\Traits\InstanceGetterTrait; /** * Compat class for WP Rocket plugin. * * @since 1.9.3 */ class Main { use InstanceGetterTrait; /** * Launch the hooks. * * @since 1.9.3 */ public function init() { add_filter( 'imagify_cdn_source', [ $this, 'set_cdn_source' ] ); } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Provide a custom CDN source. * * @since 1.9.3 * * @param array $source { * An array of arguments. * * @type $name string The name of which provides the URL (plugin name, etc). * @type $url string The CDN URL. * } * @return array */ public function set_cdn_source( $source ) { if ( ! function_exists( 'get_rocket_option' ) ) { return $source; } if ( ! get_rocket_option( 'cdn' ) ) { return $source; } $container = apply_filters( 'rocket_container', null ); if ( is_object( $container ) && method_exists( $container, 'get' ) ) { $cdn = $container->get( 'cdn' ); if ( $cdn && method_exists( $cdn, 'get_cdn_urls' ) ) { $url = $cdn->get_cdn_urls( [ 'all', 'images' ] ); } } if ( ! isset( $url ) && function_exists( 'get_rocket_cdn_cnames' ) ) { $url = get_rocket_cdn_cnames( [ 'all', 'images' ] ); } if ( empty( $url ) ) { return $source; } $url = reset( $url ); if ( ! $url ) { return $source; } if ( ! preg_match( '@^(https?:)?//@i', $url ) ) { $url = '//' . $url; } $scheme = wp_parse_url( \Imagify_Filesystem::get_instance()->get_site_root_url() ); $scheme = ! empty( $scheme['scheme'] ) ? $scheme['scheme'] : null; $url = set_url_scheme( $url, $scheme ); $source['name'] = 'WP Rocket'; $source['url'] = $url; return $source; } } 3rd-party/wp-rocket/wp-rocket.php 0000644 00000000245 15174671745 0012752 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); if ( defined( 'WP_ROCKET_VERSION' ) ) : \Imagify\ThirdParty\WPRocket\Main::get_instance()->init(); endif; functions/partners.php 0000644 00000001460 15174671745 0011145 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Get the partner ID stored in the database. * * @since 1.6.14 * * @return string|bool The partner ID. False otherwise. */ function imagify_get_partner() { if ( class_exists( 'Imagify_Partner' ) ) { return Imagify_Partner::get_stored_partner(); } $partner = get_option( 'imagifyp_id' ); if ( $partner && is_string( $partner ) ) { $partner = preg_replace( '@[^a-z0-9_-]@', '', strtolower( $partner ) ); } return $partner ? $partner : false; } /** * Delete the partner ID stored in the database. * * @since 1.6.14 */ function imagify_delete_partner() { if ( class_exists( 'Imagify_Partner' ) ) { Imagify_Partner::delete_stored_partner(); } elseif ( false !== get_option( 'imagifyp_id' ) ) { delete_option( 'imagifyp_id' ); } } functions/i18n.php 0000644 00000041537 15174671745 0010077 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Get all langs to display in admin bar for WPML * * @since 1.3.0 * * @return array $langlinks List of active languages */ function get_rocket_wpml_langs_for_admin_bar() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals global $sitepress; $langlinks = []; // Making all languages the first option either when it's set or not. $langlinks[] = [ 'code' => 'all', 'current' => 'all' === $sitepress->get_current_language(), 'anchor' => __( 'All languages', 'rocket' ), 'flag' => '<img class="icl_als_iclflag" src="' . ICL_PLUGIN_URL . '/res/img/icon16.png" alt="all" width="16" height="16" />', ]; foreach ( $sitepress->get_active_languages() as $lang ) { // Get flag. $flag = $sitepress->get_flag( $lang['code'] ); $flag_url = ICL_PLUGIN_URL . '/res/flags/' . $flag->flag; if ( $flag->from_template ) { $wp_upload_dir = wp_upload_dir(); $flag_url = $wp_upload_dir['baseurl'] . '/flags/' . $flag->flag; } $langlinks[] = [ 'code' => $lang['code'], 'current' => $lang['code'] === $sitepress->get_current_language(), 'anchor' => $lang['display_name'], 'flag' => '<img class="icl_als_iclflag" src="' . esc_url( $flag_url ) . '" alt="' . esc_attr( $lang['code'] ) . '" width="18" height="12" />', ]; } return $langlinks; } /** * Get all langs to display in admin bar for qTranslate * * @since 2.7 add fork param * @since 1.3.5 * * @param string $fork qTranslate fork name. * @return array $langlinks List of active languages */ function get_rocket_qtranslate_langs_for_admin_bar( $fork = '' ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals global $q_config; $langlinks = []; $currentlang = []; foreach ( $q_config['enabled_languages'] as $lang ) { $langlinks[ $lang ] = [ 'code' => $lang, 'anchor' => $q_config['language_name'][ $lang ], 'flag' => '<img src="' . esc_url( trailingslashit( WP_CONTENT_URL ) . $q_config['flag_location'] . $q_config['flag'][ $lang ] ) . '" alt="' . esc_attr( $q_config['language_name'][ $lang ] ) . '" width="18" height="12" />', ]; } if ( isset( $_GET['lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $lang = sanitize_key( $_GET['lang'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'x' === $fork ) { if ( qtranxf_isEnabled( $lang ) ) { $currentlang[ $lang ] = $langlinks[ $lang ]; unset( $langlinks[ $lang ] ); $langlinks = $currentlang + $langlinks; } } elseif ( qtrans_isEnabled( $lang ) ) { $currentlang[ $lang ] = $langlinks[ $lang ]; unset( $langlinks[ $lang ] ); $langlinks = $currentlang + $langlinks; } } return $langlinks; } /** * Get all langs to display in admin bar for Polylang * * @since 2.2 * * @return array $langlinks List of active languages */ function get_rocket_polylang_langs_for_admin_bar() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals global $polylang; $langlinks = []; $currentlang = []; $langs = []; $img = ''; $pll = function_exists( 'PLL' ) ? PLL() : $polylang; if ( isset( $pll ) ) { $langs = $pll->model->get_languages_list(); if ( ! empty( $langs ) ) { foreach ( $langs as $lang ) { if ( ! empty( $lang->flag ) ) { $img = strpos( $lang->flag, 'img' ) !== false ? $lang->flag . ' ' : $lang->flag; } if ( isset( $pll->curlang->slug ) && $lang->slug === $pll->curlang->slug ) { $currentlang[ $lang->slug ] = [ 'code' => $lang->slug, 'anchor' => $lang->name, 'flag' => $img, ]; } else { $langlinks[ $lang->slug ] = [ 'code' => $lang->slug, 'anchor' => $lang->name, 'flag' => $img, ]; } } } } return $currentlang + $langlinks; } /** * Tell if a translation plugin is activated. * * @since 2.0 * @since 3.2.1 Return an identifier on success instead of true. * * @return string An identifier corresponding to the active plugin. */ function rocket_has_i18n() { global $sitepress, $q_config, $polylang; if ( ! empty( $sitepress ) && is_object( $sitepress ) && method_exists( $sitepress, 'get_active_languages' ) ) { // WPML. return 'wpml'; } if ( ! empty( $polylang ) && function_exists( 'pll_languages_list' ) ) { $languages = pll_languages_list(); if ( empty( $languages ) ) { return ''; } // Polylang, Polylang Pro. return 'polylang'; } if ( ! empty( $q_config ) && is_array( $q_config ) ) { if ( function_exists( 'qtranxf_convertURL' ) ) { // qTranslate-x. return 'qtranslate-x'; } if ( function_exists( 'qtrans_convertURL' ) ) { // qTranslate. return 'qtranslate'; } } $identifier = ''; $default = $identifier; /** * Filters the value of i18n plugin detection * * @param string $identifier An identifier value. */ $identifier = apply_filters( 'rocket_has_i18n', $identifier ); if ( ! is_string( $identifier ) ) { $identifier = $default; } return $identifier; } /** * Get infos of all active languages. * * @since 2.0 * * @return array A list of language codes. */ function get_rocket_i18n_code() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $i18n_plugin = rocket_has_i18n(); if ( ! $i18n_plugin ) { return []; } if ( 'wpml' === $i18n_plugin ) { // WPML. return array_keys( $GLOBALS['sitepress']->get_active_languages() ); } if ( 'qtranslate' === $i18n_plugin || 'qtranslate-x' === $i18n_plugin ) { // qTranslate, qTranslate-x. return ! empty( $GLOBALS['q_config']['enabled_languages'] ) ? $GLOBALS['q_config']['enabled_languages'] : []; } if ( 'polylang' === $i18n_plugin ) { // Polylang, Polylang Pro. return pll_languages_list(); } $codes = []; $default = $codes; /** * Filters the active languages codes list * * @param array $codes Array of languages codes. */ $codes = apply_filters( 'rocket_get_i18n_code', $codes ); if ( ! is_array( $codes ) ) { $codes = $default; } return $codes; } /** * Get all active languages host * * @since 2.6.8 * * @return array $urls List of all active languages host */ function get_rocket_i18n_host() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $langs_host = []; $langs = get_rocket_i18n_uri(); if ( empty( $langs ) ) { return $langs_host; } foreach ( $langs as $lang ) { $langs_host[] = wp_parse_url( $lang, PHP_URL_HOST ); } return $langs_host; } /** * Get all active languages URI. * * @since 2.0 * * @return array $urls List of all active languages URI. */ function get_rocket_i18n_uri() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $i18n_plugin = rocket_has_i18n(); $urls = []; if ( 'wpml' === $i18n_plugin ) { // WPML. foreach ( get_rocket_i18n_code() as $lang ) { $urls[] = $GLOBALS['sitepress']->language_url( $lang ); } } elseif ( 'qtranslate' === $i18n_plugin || 'qtranslate-x' === $i18n_plugin ) { // qTranslate, qTranslate-x. foreach ( get_rocket_i18n_code() as $lang ) { if ( 'qtranslate' === $i18n_plugin ) { $urls[] = qtrans_convertURL( home_url(), $lang, true ); } else { $urls[] = qtranxf_convertURL( home_url(), $lang, true ); } } } elseif ( 'polylang' === $i18n_plugin ) { // Polylang, Polylang Pro. $pll = function_exists( 'PLL' ) ? PLL() : $GLOBALS['polylang']; if ( ! empty( $pll ) && is_object( $pll ) ) { if ( ! defined( 'POLYLANG_VERSION' ) || version_compare( POLYLANG_VERSION, '3.4', '<' ) ) { $urls = wp_list_pluck( $pll->model->get_languages_list(), 'search_url' ); }else { $languages = $pll->model->get_languages_list(); foreach ( $languages as $language ) { $urls[] = $language->get_home_url(); } } } } /** * Filters the value of all active languages URI * * @param array Array of active languages URI. */ $urls = apply_filters( 'rocket_get_i18n_uri', $urls ); if ( ! is_array( $urls ) || empty( $urls ) ) { $urls[] = home_url(); } return $urls; } /** * Get directories paths to preserve languages when purging a domain. * This function is required when the domains of languages (other than the default) are managed by subdirectories. * By default, when you clear the cache of the french website with the domain example.com, all subdirectory like /en/ * and /de/ are deleted. But, if you have a domain for your english and german websites with example.com/en/ and * example.com/de/, you want to keep the /en/ and /de/ directory when the french domain is cleared. * * @since 3.5.5 Normalize paths + micro-optimization by passing in the cache path. * @since 2.0 * * @param string $current_lang The current language code. * @param string $cache_path Optional. WP Rocket's cache path. * * @return array A list of directories path to preserve. */ function get_rocket_i18n_to_preserve( $current_lang, $cache_path = '' ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // Must not be an empty string. if ( empty( $current_lang ) ) { return []; } // Must not be anything else but a string. if ( ! is_string( $current_lang ) ) { return []; } $i18n_plugin = rocket_has_i18n(); if ( ! $i18n_plugin ) { return []; } $langs = get_rocket_i18n_code(); if ( empty( $langs ) ) { return []; } // Remove current lang to the preserve dirs. $langs = array_diff( $langs, [ $current_lang ] ); if ( '' === $cache_path ) { $cache_path = _rocket_get_wp_rocket_cache_path(); } // Stock all URLs of langs to preserve. $langs_to_preserve = []; foreach ( $langs as $lang ) { $parse_url = get_rocket_parse_url( get_rocket_i18n_home_url( $lang ) ); $langs_to_preserve[] = _rocket_normalize_path( "{$cache_path}{$parse_url['host']}(.*)/" . trim( $parse_url['path'], '/' ), true // escape directory separators for regex. ); } /** * Filter directories path to preserve of cache purge. * * @since 2.1 * * @param array $langs_to_preserve List of directories path to preserve. */ return (array) apply_filters( 'rocket_langs_to_preserve', $langs_to_preserve ); } /** * Get all languages subdomains URLs * * @since 2.1 * * @return array $urls List of languages subdomains URLs */ function get_rocket_i18n_subdomains() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $i18n_plugin = rocket_has_i18n(); if ( ! $i18n_plugin ) { return []; } $urls = []; $default = $urls; switch ( $i18n_plugin ) { // WPML. case 'wpml': $option = get_option( 'icl_sitepress_settings' ); if ( 2 === (int) $option['language_negotiation_type'] ) { $urls = get_rocket_i18n_uri(); } break; // qTranslate. case 'qtranslate': if ( 3 === (int) $GLOBALS['q_config']['url_mode'] ) { $urls = get_rocket_i18n_uri(); } break; // qTranslate-x. case 'qtranslate-x': if ( 3 === (int) $GLOBALS['q_config']['url_mode'] || 4 === (int) $GLOBALS['q_config']['url_mode'] ) { $urls = get_rocket_i18n_uri(); } break; // Polylang, Polylang Pro. case 'polylang': $pll = function_exists( 'PLL' ) ? PLL() : $GLOBALS['polylang']; if ( ! empty( $pll ) && is_object( $pll ) && ( 2 === (int) $pll->options['force_lang'] || 3 === (int) $pll->options['force_lang'] ) ) { $urls = get_rocket_i18n_uri(); } break; default: /** * Filters the list of languages subdomains URLs * * @param array $urls Array of languages subdomains URLs. */ $urls = apply_filters( 'rocket_i18n_subdomains', $urls ); if ( ! is_array( $urls ) ) { $urls = $default; } } return $urls; } /** * Get home URL of a specific lang. * * @since 2.2 * * @param string $lang The language code. Default is an empty string. * @return string $url */ function get_rocket_i18n_home_url( $lang = '' ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $i18n_plugin = rocket_has_i18n(); $home_url = home_url(); $default = $home_url; if ( ! $i18n_plugin ) { return $home_url; } switch ( $i18n_plugin ) { // WPML. case 'wpml': $home_url = $GLOBALS['sitepress']->language_url( $lang ); break; // qTranslate. case 'qtranslate': $home_url = qtrans_convertURL( home_url(), $lang, true ); break; // qTranslate-x. case 'qtranslate-x': $home_url = qtranxf_convertURL( home_url(), $lang, true ); break; // Polylang, Polylang Pro. case 'polylang': $pll = function_exists( 'PLL' ) ? PLL() : $GLOBALS['polylang']; if ( ! empty( $pll->options['force_lang'] ) && isset( $pll->links ) ) { $home_url = pll_home_url( $lang ); } break; default: /** * Filters the home URL value for a specific language * * @param string $home_url Home URL. * @param string $lang The language code. */ $home_url = apply_filters( 'rocket_i18n_home_url', $home_url, $lang ); if ( ! is_string( $home_url ) ) { $home_url = $default; } } return $home_url; } /** * Get all translated path of a specific post with ID. * * @since 2.4 * * @param int $post_id Post ID. * @param string $post_type Post Type. * @param string $regex Regex to include at the end. * @return array */ function get_rocket_i18n_translated_post_urls( $post_id, $post_type = 'page', $regex = '' ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $url = get_permalink( $post_id ); $path = wp_parse_url( $url, PHP_URL_PATH ); if ( empty( $path ) ) { return []; } $i18n_plugin = rocket_has_i18n(); $urls = []; $default = $urls; switch ( $i18n_plugin ) { // WPML. case 'wpml': $langs = get_rocket_i18n_code(); if ( $langs ) { foreach ( $langs as $lang ) { $urls[] = wp_parse_url( get_permalink( icl_object_id( $post_id, $post_type, true, $lang ) ), PHP_URL_PATH ) . $regex; } } break; // qTranslate & qTranslate-x. case 'qtranslate': case 'qtranslate-x': $langs = $GLOBALS['q_config']['enabled_languages']; $langs = array_diff( $langs, [ $GLOBALS['q_config']['default_language'] ] ); $urls[] = wp_parse_url( $url, PHP_URL_PATH ) . $regex; if ( $langs ) { foreach ( $langs as $lang ) { if ( 'qtranslate' === $i18n_plugin ) { $urls[] = wp_parse_url( qtrans_convertURL( $url, $lang, true ), PHP_URL_PATH ) . $regex; } elseif ( 'qtranslate-x' === $i18n_plugin ) { $urls[] = wp_parse_url( qtranxf_convertURL( $url, $lang, true ), PHP_URL_PATH ) . $regex; } } } break; // Polylang. case 'polylang': if ( function_exists( 'PLL' ) && is_object( PLL()->model ) ) { $translations = pll_get_post_translations( $post_id ); } elseif ( ! empty( $GLOBALS['polylang']->model ) && is_object( $GLOBALS['polylang']->model ) ) { $translations = $GLOBALS['polylang']->model->get_translations( 'page', $post_id ); } if ( ! empty( $translations ) ) { foreach ( $translations as $translation_post_id ) { $urls[] = wp_parse_url( get_permalink( $translation_post_id ), PHP_URL_PATH ) . $regex; } } break; default: /** * Filters the list of translated URLs for a post ID * * @param array $urls Array of translated URLs. * @param string $url URL to use. * @param string $post_type Post type. * @param string $regex Pattern to include at the end. */ $urls = apply_filters( 'rocket_i18n_translated_post_urls', $urls, $url, $post_type, $regex ); if ( ! is_array( $urls ) ) { $urls = $default; } } if ( trim( $path, '/' ) !== '' ) { $urls[] = $path . $regex; } return array_unique( $urls ); } /** * Returns the home URL, without WPML filters if the plugin is active * * @since 3.2.4 * * @param string $path Path to add to the home URL. * @return string */ function rocket_get_home_url( $path = '' ) { global $wpml_url_filters; static $home_url = []; static $has_wpml; if ( isset( $home_url[ $path ] ) ) { return $home_url[ $path ]; } if ( ! isset( $has_wpml ) ) { $has_wpml = $wpml_url_filters && is_object( $wpml_url_filters ) && method_exists( $wpml_url_filters, 'home_url_filter' ); } if ( $has_wpml ) { remove_filter( 'home_url', [ $wpml_url_filters, 'home_url_filter' ], -10 ); } $home_url[ $path ] = home_url( $path ); if ( $has_wpml ) { add_filter( 'home_url', [ $wpml_url_filters, 'home_url_filter' ], -10, 4 ); } return $home_url[ $path ]; } /** * Gets the current language * * @since 3.3.3 * * @return string */ function rocket_get_current_language() { $i18n_plugin = rocket_has_i18n(); if ( ! $i18n_plugin ) { return ''; } $current_language = ''; $default = $current_language; if ( 'polylang' === $i18n_plugin && function_exists( 'pll_current_language' ) ) { return pll_current_language(); } elseif ( 'wpml' === $i18n_plugin ) { return apply_filters( 'wpml_current_language', null ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Filters the current language value * * @param string $current_language Current language. */ $current_language = apply_filters( 'rocket_i18n_current_language', $current_language ); if ( ! is_string( $current_language ) ) { $current_language = $default; } return $current_language; } functions/admin-ui.php 0000644 00000042123 15174671745 0011013 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Get the optimization data list for a specific media. * * @since 1.0 * @since 1.9 Function signature changed. * @author Jonathan Buttigieg * * @param ProcessInterface $process The optimization process object. * @return string The output to print. */ function get_imagify_attachment_optimization_text( $process ) { if ( ! $process->is_valid() ) { return ''; } $is_media_page = Imagify_Views::get_instance()->is_media_page(); $is_library_page = Imagify_Views::get_instance()->is_wp_library_page(); $output = $is_media_page ? '' : '<ul class="imagify-datas-list" id="imagify_data_sum">'; $output_before = $is_media_page ? '' : '<li class="imagify-data-item">'; $output_after = $is_media_page ? '<br/>' : '</li>'; $reoptimize_link = get_imagify_attachment_reoptimize_link( $process ); $reoptimize_link .= get_imagify_attachment_optimize_missing_thumbnails_link( $process ); $reoptimize_link .= get_imagify_attachment_generate_nextgen_versions_link( $process ); $reoptimize_link .= get_imagify_attachment_delete_nextgen_versions_link( $process ); $reoptimize_output = $reoptimize_link ? $reoptimize_link : ''; $reoptimize_output_before = '<div class="imagify-datas-actions-links">'; $reoptimize_output_after = '</div><!-- .imagify-datas-actions-links -->'; $error = get_imagify_attachment_error_text( $process ); $media = $process->get_media(); if ( $error ) { if ( ! $is_media_page && $reoptimize_link && $media->has_backup() ) { $reoptimize_output .= '<span class="attachment-has-backup hidden"></span>'; } $reoptimize_output = $reoptimize_output_before . $reoptimize_output . $reoptimize_output_after; return $is_media_page ? $output_before . $error . $reoptimize_output . $output_after : $error . $reoptimize_output; } $data = $process->get_data(); $optimized_data = $data->get_optimization_data(); $attachment_id = $media->get_id(); $optimization_level = imagify_get_optimization_level_label( $data->get_optimization_level() ); if ( ! $is_media_page ) { $output .= $output_before . '<span class="data">' . __( 'New Filesize:', 'imagify' ) . '</span> <strong class="big">' . $data->get_optimized_size() . '</strong>' . $output_after; } if ( key_exists( 'message', $optimized_data ) && $optimized_data['message'] ) { $output .= $output_before . '<span class="data">' . __( 'Convert:', 'imagify' ) . '</span> <strong class="big">' . $optimized_data['message'] . '</strong>' . $output_after; } $chart = ''; if ( ! $is_media_page ) { if ( ! $is_library_page ) { // No need to print this on the library page, the event whould be triggered before the handler is attached (the JS file is loaded in the footer). $chart = '<script type="text/javascript">jQuery( window ).trigger( "canvasprinted.imagify", [ ".imagify-consumption-chart-' . $attachment_id . '" ] ); </script>'; } $chart = '<span class="imagify-chart"> <span class="imagify-chart-container"> <canvas class="imagify-consumption-chart imagify-consumption-chart-' . $attachment_id . '" width="15" height="15"></canvas> ' . $chart . ' </span> </span>'; } $output .= $output_before; $output .= '<span class="data">' . __( 'Original Saving:', 'imagify' ) . '</span> '; $output .= '<strong>' . $chart . '<span class="imagify-chart-value">' . $data->get_saving_percent() . '</span>%</strong>'; $output .= $output_after; // More details section. if ( ! $is_media_page ) { // New list. $output .= '</ul>'; $output .= '<p class="imagify-datas-more-action">'; $output .= '<a href="#imagify-view-details-' . $attachment_id . '" data-close="' . __( 'Close details', 'imagify' ) . '" data-open="' . __( 'View details', 'imagify' ) . '">'; $output .= '<span class="the-text">' . __( 'View details', 'imagify' ) . '</span>'; $output .= '<span class="dashicons dashicons-arrow-down-alt2"></span>'; $output .= '</a>'; $output .= '</p>'; $output .= '<ul id="imagify-view-details-' . $attachment_id . '" class="imagify-datas-list imagify-datas-details">'; // Not in metabox. $output .= $output_before . '<span class="data">' . __( 'Original Filesize:', 'imagify' ) . '</span> <strong class="original">' . $data->get_original_size() . '</strong>' . $output_after; } $output .= $output_before . '<span class="data">' . __( 'Level:', 'imagify' ) . '</span> <strong>' . $optimization_level . '</strong>' . $output_after; if ( $media->is_image() ) { $has_nextgen = $process->has_next_gen() ? __( 'Yes', 'imagify' ) : __( 'No', 'imagify' ); if ( $process->has_next_gen() ) { $has_nextgen = $process->is_full_next_gen() ? __( 'Yes', 'imagify' ) : __( 'Partially', 'imagify' ); } $output .= $output_before . '<span class="data">' . __( 'Next-Gen generated:', 'imagify' ) . '</span> <strong class="big">' . esc_html( $has_nextgen ) . '</strong>' . $output_after; $total_optimized_thumbnails = $data->get_optimized_sizes_count(); if ( $total_optimized_thumbnails ) { $output .= $output_before . '<span class="data">' . __( 'Thumbnails Optimized:', 'imagify' ) . '</span> <strong>' . $total_optimized_thumbnails . '</strong>' . $output_after; $output .= $output_before . '<span class="data">' . __( 'Overall Saving:', 'imagify' ) . '</span> <strong>' . $data->get_overall_saving_percent() . '%</strong>' . $output_after; } } // End of list. $output .= $is_media_page ? '' : '</ul>'; // Actions section. $output .= $is_media_page ? $output_before : ''; $output .= $reoptimize_output_before; $output .= $reoptimize_output; if ( $media->has_backup() ) { $url = get_imagify_admin_url( 'restore', [ 'attachment_id' => $attachment_id, 'context' => $media->get_context(), ] ); $output .= Imagify_Views::get_instance()->get_template( 'button/restore', [ 'url' => $url, 'atts' => [ 'class' => $is_media_page ? '' : null, ], ] ); if ( ! $is_library_page ) { $output .= '<input id="imagify-original-src" type="hidden" value="' . esc_url( $media->get_backup_url() ) . '">'; $output .= '<input id="imagify-original-size" type="hidden" value="' . $data->get_original_size() . '">'; $output .= '<input id="imagify-full-src" type="hidden" value="' . esc_url( $media->get_fullsize_url() ) . '">'; if ( $media->is_image() ) { $dimensions = $media->get_dimensions(); $output .= '<input id="imagify-full-width" type="hidden" value="' . $dimensions['width'] . '">'; $output .= '<input id="imagify-full-height" type="hidden" value="' . $dimensions['height'] . '">'; } } } $output .= $reoptimize_output_after; return $output; } /** * Get the error message for a specific attachment. * * @since 1.0 * @since 1.9 Function signature changed. * @author Jonathan Buttigieg * * @param ProcessInterface $process The optimization process object. * @return string The output to print. */ function get_imagify_attachment_error_text( $process ) { if ( ! $process->is_valid() ) { return ''; } $data = $process->get_data()->get_optimization_data(); if ( ! isset( $data['sizes']['full']['success'] ) || $data['sizes']['full']['success'] ) { return ''; } $class = 'button'; $media = $process->get_media(); $url = get_imagify_admin_url( 'optimize', [ 'attachment_id' => $media->get_id(), 'context' => $media->get_context(), ] ); if ( ! Imagify_Views::get_instance()->is_media_page() ) { $class .= ' button-imagify-optimize'; } return Imagify_Views::get_instance()->get_template( 'button/retry-optimize', [ 'url' => $url, 'error' => $data['sizes']['full']['error'], 'atts' => [ 'class' => $class, ], ] ); } /** * Get the re-optimize link for a specific attachment. * * @since 1.0 * @since 1.9 Function signature changed. * @author Jonathan Buttigieg * * @param ProcessInterface $process The optimization process object. * @return string The output to print. */ function get_imagify_attachment_reoptimize_link( $process ) { if ( ! $process->is_valid() ) { return ''; } $data = $process->get_data(); if ( ! $data->get_optimization_status() ) { // Not optimized yet. return ''; } // Stop the process if the API key isn't valid. if ( ! Imagify_Requirements::is_api_key_valid() ) { return ''; } $is_already_optimized = $data->is_already_optimized(); $media = $process->get_media(); $can_reoptimize = $is_already_optimized || $media->has_backup(); // Don't display anything if there is no backup or the image has been optimized. if ( ! $can_reoptimize ) { return ''; } $output = ''; $views = Imagify_Views::get_instance(); $media_level = $data->get_optimization_level(); $data = []; $url_args = [ 'attachment_id' => $media->get_id(), 'context' => $media->get_context(), ]; if ( Imagify_Views::get_instance()->is_media_page() ) { $data['atts'] = [ 'class' => '', ]; } if ( $media_level < 1 ) { $url_args['optimization_level'] = 2; $data['optimization_level'] = 2; $data['url'] = get_imagify_admin_url( 'manual-reoptimize', $url_args ); $output .= $views->get_template( 'button/re-optimize', $data ); } elseif ( $media_level > 0 ) { $url_args['optimization_level'] = 0; $data['optimization_level'] = 0; $data['url'] = get_imagify_admin_url( 'manual-reoptimize', $url_args ); $output .= $views->get_template( 'button/re-optimize', $data ); } return $output; } /** * Get the link to optimize missing thumbnail sizes for a specific attachment. * * @since 1.6.10 * @since 1.9 Function signature changed. * @author Grégory Viguier * * @param ProcessInterface $process The optimization process object. * @return string The output to print. */ function get_imagify_attachment_optimize_missing_thumbnails_link( $process ) { if ( ! $process->is_valid() ) { return ''; } $media = $process->get_media(); if ( ! $media->is_image() || ! Imagify_Requirements::is_api_key_valid() || ! $media->has_backup() ) { return ''; } $context = $media->get_context(); /** * Allow to not display the "Optimize missing thumbnails" link. * * @since 1.6.10 * @since 1.9 The $attachment object is replaced by a $process object. * @author Grégory Viguier * * @param bool $display True to display the link. False to not display it. * @param ProcessInterface $process The optimization process object. * @param string $context The context. */ $display = apply_filters( 'imagify_display_missing_thumbnails_link', true, $process, $context ); // Stop the process if the filter is false. if ( ! $display ) { return ''; } $missing_sizes = $process->get_missing_sizes(); if ( ! $missing_sizes || is_wp_error( $missing_sizes ) ) { return ''; } $url = get_imagify_admin_url( 'optimize-missing-sizes', [ 'attachment_id' => $media->get_id(), 'context' => $context, ] ); return Imagify_Views::get_instance()->get_template( 'button/optimize-missing-sizes', [ 'url' => $url, 'count' => count( $missing_sizes ), ] ); } /** * Get the link to generate next-gen versions if they are missing. * * @since 1.9 * * @param ProcessInterface $process The optimization process object. * * @return string The output to print. */ function get_imagify_attachment_generate_nextgen_versions_link( $process ) { if ( ! $process->is_valid() ) { return ''; } $formats = imagify_nextgen_images_formats(); if ( empty( $formats ) ) { return ''; } $media = $process->get_media(); if ( ! $media->is_image() || ! Imagify_Requirements::is_api_key_valid() || ! $media->has_backup() ) { return ''; } $format = get_imagify_option( 'optimization_format' ); if ( 'avif' === $format && 'image/avif' === $media->get_mime_type() ) { return ''; } elseif ( 'image/webp' === $media->get_mime_type() ) { return ''; } $data = $process->get_data(); if ( ! $data->is_optimized() && ! $data->is_already_optimized() ) { return ''; } if ( $process->has_next_gen() ) { return ''; } $context = $media->get_context(); $display = apply_filters_deprecated( 'imagify_display_generate_webp_versions_link', [ true, $process, $context ], '2.2', 'imagify_display_generate_next_gen_versions_link' ); /** * Allow to not display the "Generate next-gen versions" link. * * @since 1.9 * @author Grégory Viguier * * @param bool $display True to display the link. False to not display it. * @param ProcessInterface $process The optimization process object. * @param string $context The context. */ $display = apply_filters( 'imagify_display_generate_next_gen_versions_link', $display, $process, $context ); // Stop the process if the filter is false. if ( ! $display ) { return ''; } $url = get_imagify_admin_url( 'generate-nextgen-versions', [ 'attachment_id' => $media->get_id(), 'context' => $context, ] ); $output = Imagify_Views::get_instance()->get_template( 'button/generate-webp', [ 'url' => $url, ] ); return $output . '<br/>'; } /** * Get the link to delete next-gen versions when the status is "already_optimized". * * @since 1.9.6 * @author Grégory Viguier * * @param ProcessInterface $process The optimization process object. * @return string The output to print. */ function get_imagify_attachment_delete_nextgen_versions_link( $process ) { if ( ! $process->is_valid() ) { return ''; } $media = $process->get_media(); $context = $media->get_context(); $media_id = $media->get_id(); if ( ! imagify_get_context( $context )->current_user_can( 'manual-restore', $media_id ) ) { imagify_die(); } $data = $process->get_data(); if ( ! $data->is_already_optimized() || ! $process->has_next_gen() ) { return ''; } $class = ''; $url = get_imagify_admin_url( 'delete-nextgen-versions', [ 'attachment_id' => $media_id, 'context' => $context, ] ); if ( ! Imagify_Views::get_instance()->is_media_page() ) { $class .= 'button-imagify-delete-webp'; } return Imagify_Views::get_instance()->get_template( 'button/delete-webp', [ 'url' => $url, 'atts' => [ 'class' => $class, ], ] ); } /** * Get all data to diplay for a specific media. * * @since 1.2 * @since 1.9 Function signature changed. * @author Jonathan Buttigieg * * @param \Imagify\Optimization\Process\ProcessInterface $process The optimization process object. * @param bool $with_container Set to false to not return the HTML container. * * @return string The output to print. */ function get_imagify_media_column_content( $process, $with_container = true ) { if ( ! $process->is_valid() ) { return __( 'This media is not valid.', 'imagify' ); } if ( ! $process->current_user_can( 'manual-optimize' ) ) { return __( 'You are not allowed to optimize this file.', 'imagify' ); } $media = $process->get_media(); // Check if the media is supported. if ( ! $media->is_supported() ) { return __( 'This media is not supported.', 'imagify' ); } // Check if the media has the required WP data. if ( ! $media->has_required_media_data() ) { return __( 'This media lacks the required metadata and cannot be optimized.', 'imagify' ); } $data = $process->get_data(); // Check if the API key is valid. if ( ! Imagify_Requirements::is_api_key_valid() && ! $data->is_optimized() ) { $output = __( 'Invalid API key', 'imagify' ); $output .= '<br/>'; $output .= '<a href="' . esc_url( get_imagify_admin_url() ) . '">' . __( 'Check your Settings', 'imagify' ) . '</a>'; return $output; } $media_id = $media->get_id(); $context = $media->get_context(); $views = Imagify_Views::get_instance(); $is_locked = $process->is_locked(); if ( $is_locked ) { switch ( $is_locked ) { case 'optimizing': $lock_label = __( 'Optimizing...', 'imagify' ); break; case 'restoring': $lock_label = __( 'Restoring...', 'imagify' ); break; default: $lock_label = __( 'Processing...', 'imagify' ); } if ( ! $with_container ) { return $views->get_template( 'button/processing', [ 'label' => $lock_label ] ); } return $views->get_template( 'container/data-actions', [ 'media_id' => $media_id, 'context' => $context, 'content' => $views->get_template( 'button/processing', [ 'label' => $lock_label ] ), ] ); } // Check if the image was optimized. if ( ! $data->get_optimization_status() ) { $output = Imagify_Views::get_instance()->get_template( 'button/optimize', [ 'url' => get_imagify_admin_url( 'manual-optimize', [ 'attachment_id' => $media_id, 'context' => $context, ] ), ] ); if ( $media->has_backup() ) { $output .= '<span class="attachment-has-backup hidden"></span>'; } } else { $output = get_imagify_attachment_optimization_text( $process ); } if ( ! $with_container ) { return $output; } return $views->get_template( 'container/data-actions', [ 'media_id' => $media_id, 'context' => $context, 'content' => $output, ] ); } functions/attachments.php 0000644 00000023667 15174671745 0011637 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Get all mime types which could be optimized by Imagify. * * @since 1.7 * @since 1.8 Added $type parameter. * * @param string $type One of 'image', 'not-image'. Any other value will return all mime types. * @return array The mime types. */ function imagify_get_mime_types( $type = null ) { $mimes = []; if ( 'not-image' !== $type ) { $mimes = [ 'jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', ]; } if ( 'image' !== $type ) { $mimes['pdf'] = 'application/pdf'; } return $mimes; } /** * Tell if an attachment has a supported mime type. * Was previously Imagify_AS3CF::is_mime_type_supported() since 1.6.6. * * @since 1.6.8 * @author Grégory Viguier * * @param int $attachment_id The attachment ID. * @return bool */ function imagify_is_attachment_mime_type_supported( $attachment_id ) { static $is = [ false ]; $attachment_id = absint( $attachment_id ); if ( isset( $is[ $attachment_id ] ) ) { return $is[ $attachment_id ]; } $mime_types = imagify_get_mime_types(); $mime_types = array_flip( $mime_types ); $mime_type = (string) get_post_mime_type( $attachment_id ); $is[ $attachment_id ] = isset( $mime_types[ $mime_type ] ); return $is[ $attachment_id ]; } /** * Get post statuses related to attachments. * * @since 1.7 * @author Grégory Viguier * * @return array */ function imagify_get_post_statuses() { static $statuses; if ( isset( $statuses ) ) { return $statuses; } $statuses = [ 'inherit' => 'inherit', 'private' => 'private', ]; $custom_statuses = get_post_stati( [ 'public' => true ] ); unset( $custom_statuses['publish'] ); if ( $custom_statuses ) { $statuses = array_merge( $statuses, $custom_statuses ); } /** * Filter the post statuses Imagify is allowed to optimize. * * @since 1.7 * @author Grégory Viguier * * @param array $statuses An array of post statuses. Kays and values are set. */ $statuses = apply_filters( 'imagify_post_statuses', $statuses ); return $statuses; } /** * Tell if the site has attachments (only the ones Imagify would optimize) without the required WP metadata. * * @param bool $reset Reset the static method to null when set to true, defaulted to false. * @since 1.7 * @author Grégory Viguier * * @return bool */ function imagify_has_attachments_without_required_metadata( $reset = false ) { global $wpdb; static $has; if ( $reset ) { $has = null; } if ( isset( $has ) ) { return $has; } $mime_types = Imagify_DB::get_mime_types(); $statuses = Imagify_DB::get_post_statuses(); $exist_data = Imagify_DB::get_required_wp_metadata_exist_clause( 'p.ID', false ); $has = (bool) $wpdb->get_var( // WPCS: unprepared SQL ok. " SELECT p.ID FROM $wpdb->posts AS p WHERE p.post_mime_type IN ( $mime_types ) AND p.post_type = 'attachment' AND p.post_status IN ( $statuses ) $exist_data LIMIT 1" ); return $has; } /** * Get the path to the backups directory. * * @since 1.6.8 * @author Grégory Viguier * * @param bool $bypass_error True to return the path even if there is an error. This is used when we want to display this path in a message for example. * @return string|bool Path to the backups directory. False on failure. */ function get_imagify_backup_dir_path( $bypass_error = false ) { static $backup_dir; if ( isset( $backup_dir ) ) { return $backup_dir; } $upload_basedir = get_imagify_upload_basedir( $bypass_error ); if ( ! $upload_basedir ) { return false; } $backup_dir = $upload_basedir . 'backup/'; /** * Filter the backup directory path. * * @since 1.0 * * @param string $backup_dir The backup directory path. */ $backup_dir = apply_filters( 'imagify_backup_directory', $backup_dir ); $backup_dir = imagify_get_filesystem()->normalize_dir_path( $backup_dir ); return $backup_dir; } /** * Tell if the folder containing the backups is writable. * * @since 1.6.8 * @author Grégory Viguier * * @return bool */ function imagify_backup_dir_is_writable() { return imagify_get_filesystem()->make_dir( get_imagify_backup_dir_path() ); } /** * Get the backup path of a specific attachement. * * @since 1.0 * * @param string $file_path The file path. * @return string|bool The backup path. False on failure. */ function get_imagify_attachment_backup_path( $file_path ) { $file_path = wp_normalize_path( (string) $file_path ); $upload_basedir = get_imagify_upload_basedir(); $backup_dir = get_imagify_backup_dir_path(); if ( ! $file_path || ! $upload_basedir ) { return false; } return preg_replace( '@^' . preg_quote( $upload_basedir, '@' ) . '@', $backup_dir, $file_path ); } /** * Retrieve file path for an attachment based on filename. * * @since 1.4.5 * * @param int $file_path The file path. * @return string|false The file path to where the attached file should be, false otherwise. */ function get_imagify_attached_file( $file_path ) { $file_path = wp_normalize_path( (string) $file_path ); $upload_basedir = get_imagify_upload_basedir(); if ( ! $file_path || ! $upload_basedir ) { return false; } // The file path is absolute. if ( strpos( $file_path, '/' ) === 0 || preg_match( '|^.:\\\|', $file_path ) ) { return false; } // Prepend upload dir. return $upload_basedir . $file_path; } /** * Retrieve the URL for an attachment based on file path. * * @since 1.4.5 * * @param string $file_path A relative or absolute file path. * @return string|bool File URL, otherwise false. */ function get_imagify_attachment_url( $file_path ) { $file_path = wp_normalize_path( (string) $file_path ); $upload_basedir = get_imagify_upload_basedir(); if ( ! $file_path || ! $upload_basedir ) { return false; } $upload_baseurl = get_imagify_upload_baseurl(); // Check that the upload base exists in the (absolute) file location. if ( 0 === strpos( $file_path, $upload_basedir ) ) { // Replace file location with url location. return preg_replace( '@^' . preg_quote( $upload_basedir, '@' ) . '@', $upload_baseurl, $file_path ); } if ( false !== strpos( '/' . $file_path, '/wp-content/uploads/' ) ) { // Get the directory name relative to the basedir (back compat for pre-2.7 uploads). return trailingslashit( $upload_baseurl . _wp_get_attachment_relative_path( $file_path ) ) . imagify_get_filesystem()->file_name( $file_path ); } // It's a newly-uploaded file, therefore $file is relative to the basedir. return $upload_baseurl . $file_path; } /** * Get size information for all currently registered thumbnail sizes. * * @since 1.5.10 * @since 1.6.10 For consistency, revamped the function like WP does with wp_generate_attachment_metadata(). * Removed the filter, added crop value to each size. * @author Grégory Viguier * * @return array { * Data for the currently registered thumbnail sizes. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * } */ function get_imagify_thumbnail_sizes() { // All image size names. $intermediate_image_sizes = get_intermediate_image_sizes(); $intermediate_image_sizes = array_flip( $intermediate_image_sizes ); // Additional image size attributes. $additional_image_sizes = wp_get_additional_image_sizes(); // Create the full array with sizes and crop info. foreach ( $intermediate_image_sizes as $size_name => $s ) { $intermediate_image_sizes[ $size_name ] = [ 'width' => '', 'height' => '', 'crop' => false, 'name' => $size_name, ]; if ( isset( $additional_image_sizes[ $size_name ]['width'] ) ) { // For theme-added sizes. $intermediate_image_sizes[ $size_name ]['width'] = (int) $additional_image_sizes[ $size_name ]['width']; } else { // For default sizes set in options. $intermediate_image_sizes[ $size_name ]['width'] = (int) get_option( "{$size_name}_size_w" ); } if ( isset( $additional_image_sizes[ $size_name ]['height'] ) ) { // For theme-added sizes. $intermediate_image_sizes[ $size_name ]['height'] = (int) $additional_image_sizes[ $size_name ]['height']; } else { // For default sizes set in options. $intermediate_image_sizes[ $size_name ]['height'] = (int) get_option( "{$size_name}_size_h" ); } if ( isset( $additional_image_sizes[ $size_name ]['crop'] ) ) { // For theme-added sizes. $intermediate_image_sizes[ $size_name ]['crop'] = (int) $additional_image_sizes[ $size_name ]['crop']; } else { // For default sizes set in options. $intermediate_image_sizes[ $size_name ]['crop'] = (int) get_option( "{$size_name}_crop" ); } } return $intermediate_image_sizes; } /** * A simple helper to get the upload basedir. * * @since 1.6.7 * @since 1.6.8 Added the $bypass_error parameter. * @author Grégory Viguier * * @param bool $bypass_error True to return the path even if there is an error. This is used when we want to display this path in a message for example. * @return string|bool The path. False on failure. */ function get_imagify_upload_basedir( $bypass_error = false ) { return imagify_get_filesystem()->get_upload_basedir( $bypass_error ); } /** * A simple helper to get the upload baseurl. * * @since 1.6.7 * @author Grégory Viguier * * @return string|bool The URL. False on failure. */ function get_imagify_upload_baseurl() { return imagify_get_filesystem()->get_upload_baseurl(); } /** * Get the maximal number of unoptimized attachments to fetch. * * @since 1.6.14 * @author Grégory Viguier * * @return int */ function imagify_get_unoptimized_attachment_limit() { /** * Filter the unoptimized attachments limit query. * * @since 1.4.4 * * @param int $limit The limit (-1 for unlimited). */ $limit = (int) apply_filters( 'imagify_unoptimized_attachment_limit', 10000 ); return -1 === $limit ? PHP_INT_MAX : abs( $limit ); } functions/media.php 0000644 00000000777 15174671745 0010400 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Trigger a hook that should happen before a media is deleted. * * @since 1.9 * @author Grégory Viguier * * @param ProcessInterface $process An optimization process. */ function imagify_trigger_delete_media_hook( $process ) { /** * Triggered bifore a media is deleted. * * @since 1.9 * @author Grégory Viguier * * @param ProcessInterface $process An optimization process. */ do_action( 'imagify_delete_media', $process ); } functions/api.php 0000644 00000006655 15174671745 0010073 0 ustar 00 <?php /** * Get an URL with one of CNAMES added in options * * @since 2.1 * * @param string $url The URL to parse. * @param array $zone (default: array( 'all' )). Deprecated. * @return string */ function get_rocket_cdn_url( $url, $zone = [ 'all' ] ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals, Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $container = apply_filters( 'rocket_container', '' ); $cdn = $container->get( 'cdn' ); return $cdn->rewrite_url( $url ); } /** * Wrapper of get_rocket_cdn_url() and print result * * @since 2.1 * * @param string $url The URL to parse. * @param array $zone (default: array( 'all' )). Deprecated. */ function rocket_cdn_url( $url, $zone = [ 'all' ] ) { echo esc_url( get_rocket_cdn_url( $url, $zone ) ); } /** * Get all CNAMES. * * @since 2.1 * @since 3.0 Don't check for WP Rocket CDN option activated to be able to use the function on Hosting with CDN auto-enabled. * * @param string $zone List of zones. Default is 'all'. * @return array List of CNAMES */ function get_rocket_cdn_cnames( $zone = 'all' ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $hosts = []; $cnames = get_rocket_option( 'cdn_cnames', [] ); if ( $cnames ) { $cnames_zone = get_rocket_option( 'cdn_zone', [] ); $zone = (array) $zone; foreach ( $cnames as $k => $_urls ) { if ( ! in_array( $cnames_zone[ $k ], $zone, true ) ) { continue; } $_urls = explode( ',', $_urls ); $_urls = array_map( 'trim', $_urls ); foreach ( $_urls as $url ) { $hosts[] = $url; } } } /** * Filter all CNAMES. * * @since 2.7 * * @param array $hosts List of CNAMES. * @param array $zone Array of CDN zones. */ $hosts = (array) apply_filters( 'rocket_cdn_cnames', $hosts, $zone ); $hosts = array_filter( $hosts ); $hosts = array_flip( array_flip( $hosts ) ); $hosts = array_values( $hosts ); return $hosts; } /** * Check if the current URL is for a live site (not local, not staging). * * @since 3.5 * @author Remy Perona * * @return bool True if live, false otherwise. */ function rocket_is_live_site() { if ( rocket_get_constant( 'WP_ROCKET_DEBUG' ) ) { return true; } $host = wp_parse_url( home_url(), PHP_URL_HOST ); if ( ! $host ) { return false; } // Check for local development sites. $local_tlds = [ '127.0.0.1', 'localhost', '.local', '.test', '.docksal', '.docksal.site', '.dev.cc', '.lndo.site', ]; foreach ( $local_tlds as $local_tld ) { if ( $host === $local_tld ) { return false; } // Check the TLD. if ( substr( $host, -strlen( $local_tld ) ) === $local_tld ) { return false; } } $default_staging = []; /** * Get the list of staging domains from SaaS * * @param array $default_staging default result in case there isn't. */ $staging = apply_filters( 'rocket_staging_list', $default_staging ); if ( ! is_array( $staging ) ) { $staging = $default_staging; } foreach ( $staging as $partial_host ) { if ( strpos( $host, $partial_host ) ) { return false; } } return true; } /** * Checks if importing * * @return bool */ function rocket_is_importing() { /** * Filter use to determine if we are currently importing data into the WordPress. * Bails out if this filter returns true. * * @param boolean Tells if we are importing or not. */ return (bool) apply_filters( 'rocket_is_importing', rocket_get_constant( 'WP_IMPORTING' ) ); } functions/admin.php 0000644 00000042505 15174671745 0010404 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * This warning is displayed when the API KEY isn't already set or not valid * * @since 1.0 */ function rocket_need_api_key() { $message = ''; $errors = (array) get_transient( 'rocket_check_key_errors' ); foreach ( $errors as $error ) { $message .= '<p>' . $error . '</p>'; } ?> <div class="notice notice-error"> <p><strong><?php echo esc_html( WP_ROCKET_PLUGIN_NAME ); ?></strong> <?php echo esc_html( _n( 'There seems to be an issue validating your license. Please see the error message below.', 'There seems to be an issue validating your license. You can see the error messages below.', count( $errors ), 'rocket' ) ); ?> </p> <?php echo wp_kses_post( $message ); ?> </div> <?php } /** * Renew all boxes for everyone if $uid is missing * * @since 1.1.10 * @modified 2.1 : * - Better usage of delete_user_meta into delete_metadata * * @param (int|null) $uid : a User id, can be null, null = all users. * @param (string|array) $keep_this : which box have to be kept. * @return void */ function rocket_renew_all_boxes( $uid = null, $keep_this = [] ) { // Delete a user meta for 1 user or all at a time. delete_metadata( 'user', $uid, 'rocket_boxes', '', ! $uid ); // $keep_this works only for the current user. if ( ! empty( $keep_this ) && null !== $uid ) { if ( ! is_array( $keep_this ) ) { $keep_this = (array) $keep_this; } foreach ( $keep_this as $kt ) { rocket_dismiss_box( $kt ); } } } /** * Renew a dismissed error box admin side * * @since 1.1.10 * * @param string $function function name. * @param int $uid User ID. * @return void */ function rocket_renew_box( $function, $uid = 0 ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.functionFound global $current_user; $uid = 0 === $uid ? $current_user->ID : $uid; $actual = get_user_meta( $uid, 'rocket_boxes', true ); if ( $actual && false !== array_search( $function, $actual, true ) ) { unset( $actual[ array_search( $function, $actual, true ) ] ); update_user_meta( $uid, 'rocket_boxes', $actual ); } } /** * Dismiss one box. * * @since 1.3.0 * @since 3.6 Doesn’t die anymore. * * @param string $function Function (box) name. */ function rocket_dismiss_box( $function ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.functionFound $actual = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); $actual = array_merge( (array) $actual, [ $function ] ); $actual = array_filter( $actual ); $actual = array_unique( $actual ); update_user_meta( get_current_user_id(), 'rocket_boxes', $actual ); delete_transient( $function ); } /** * Create a unique id for some Rocket options and functions * * @since 2.1 */ function create_rocket_uniqid() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals return str_replace( '.', '', uniqid( '', true ) ); } /** * Gets names of all active plugins. * * @since 2.11 Only get the name * @since 2.6 * * @return array An array of active plugins names. */ function rocket_get_active_plugins() { $plugins = []; $active_plugins = array_intersect_key( get_plugins(), array_flip( array_filter( array_keys( get_plugins() ), 'is_plugin_active' ) ) ); foreach ( $active_plugins as $plugin ) { $plugins[] = $plugin['Name']; } return $plugins; } /** * Check if the whole website is on the SSL protocol * * @since 3.3.6 Use the superglobal $_SERVER values to detect SSL. * @since 2.7 */ function rocket_is_ssl_website() { if ( isset( $_SERVER['HTTPS'] ) ) { $https = sanitize_text_field( wp_unslash( $_SERVER['HTTPS'] ) ); if ( 'on' === strtolower( $https ) ) { return true; } if ( '1' === (string) $https ) { return true; } } elseif ( isset( $_SERVER['SERVER_PORT'] ) && '443' === (string) sanitize_text_field( wp_unslash( $_SERVER['SERVER_PORT'] ) ) ) { return true; } return false; } /** * Get the WP Rocket documentation URL * * @since 2.7 */ function get_rocket_documentation_url() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $langs = [ 'fr_FR' => 'fr.', ]; $lang = get_locale(); $prefix = isset( $langs[ $lang ] ) ? $langs[ $lang ] : ''; $url = "https://{$prefix}docs.wp-rocket.me/?utm_source=wp_plugin&utm_medium=wp_rocket"; return $url; } /** * Get WP Rocket FAQ URL * * @since 2.10 * @author Remy Perona * * @return string URL in the correct language */ function get_rocket_faq_url() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $langs = [ 'de' => 1, 'es' => 1, 'fr' => 1, 'it' => 1, ]; $locale = explode( '_', get_locale() ); $lang = isset( $langs[ $locale[0] ] ) ? $locale[0] . '/' : ''; $url = WP_ROCKET_WEB_MAIN . "{$lang}faq/?utm_source=wp_plugin&utm_medium=wp_rocket"; return $url; } /** * Get the Activation Link for a given plugin * * @since 2.7.3 * @author Geoffrey Crofte * * @param string $plugin the given plugin folder/file.php (e.i. "imagify/imagify.php"). * @return string URL to activate the plugin */ function rocket_get_plugin_activation_link( $plugin ) { $activation_url = wp_nonce_url( 'plugins.php?action=activate&plugin=' . $plugin . '&plugin_status=all&paged=1&s', 'activate-plugin_' . $plugin ); return $activation_url; } /** * Check if a given plugin is installed but not necessarily activated * Note: get_plugins( $folder ) from WP Core doesn't work * * @since 2.7.3 * @author Geoffrey Crofte * * @param string $plugin a plugin folder/file.php (e.i. "imagify/imagify.php"). * @return bool True if installed, false otherwise */ function rocket_is_plugin_installed( $plugin ) { $installed_plugins = get_plugins(); return isset( $installed_plugins[ $plugin ] ); } /** * When Woocommerce, EDD, iThemes Exchange, Jigoshop & WP-Shop options are saved or deleted, * we update .htaccess & config file to get the right checkout page to exclude to the cache. * * @since 2.9.3 Support for SF Move Login moved to 3rd party file * @since 2.6 Add support with SF Move Login & WPS Hide Login to exclude login pages * @since 2.4 * * @param array $old_value An array of previous settings values. * @param array $value An array of submitted settings values. */ function rocket_after_update_single_options( $old_value, $value ) { if ( $old_value !== $value ) { // Update .htaccess file rules. flush_rocket_htaccess(); // Update config file. rocket_generate_config_file(); } } /** * We need to regenerate the config file + htaccess depending on some plugins * * @since 2.9.3 Support for SF Move Login moved to 3rd party file * @since 2.6.5 Add support with SF Move Login & WPS Hide Login * * @param array $old_value An array of previous settings values. * @param array $value An array of submitted settings values. */ function rocket_after_update_array_options( $old_value, $value ) { $options = [ 'purchase_page', 'jigoshop_cart_page_id', 'jigoshop_checkout_page_id', 'jigoshop_myaccount_page_id', ]; foreach ( $options as $val ) { if ( ( ! isset( $old_value[ $val ] ) && isset( $value[ $val ] ) ) || ( isset( $old_value[ $val ], $value[ $val ] ) && $old_value[ $val ] !== $value[ $val ] ) ) { // Update .htaccess file rules. flush_rocket_htaccess(); // Update config file. rocket_generate_config_file(); break; } } } /** * Check if a mobile plugin is active * * @since 2.10 * @author Remy Perona * * @return bool true if a mobile plugin in the list is active, false otherwise. **/ function rocket_is_mobile_plugin_active() { return \WP_Rocket\Subscriber\Third_Party\Plugins\Mobile_Subscriber::is_mobile_plugin_active(); } /** * Allow upload of JSON file. * * @since 2.10.7 * @author Remy Perona * * @param array $wp_get_mime_types Array of allowed mime types. * @return array Updated array of allowed mime types */ function rocket_allow_json_mime_type( $wp_get_mime_types ) { $wp_get_mime_types['json'] = 'application/json'; return $wp_get_mime_types; } /** * Forces the correct file type for JSON file if the WP checks is incorrect * * @since 3.2.3.1 * @author Gregory Viguier * * @param array $wp_check_filetype_and_ext File data array containing 'ext', 'type', and * 'proper_filename' keys. * @param string $file Full path to the file. * @param string $filename The name of the file (may differ from $file due to * $file being in a tmp directory). * @param array $mimes Key is the file extension with value as the mime type. * @return array */ function rocket_check_json_filetype( $wp_check_filetype_and_ext, $file, $filename, $mimes ) { if ( ! empty( $wp_check_filetype_and_ext['ext'] ) && ! empty( $wp_check_filetype_and_ext['type'] ) ) { return $wp_check_filetype_and_ext; } $wp_filetype = wp_check_filetype( $filename, $mimes ); if ( 'json' !== $wp_filetype['ext'] ) { return $wp_check_filetype_and_ext; } if ( empty( $wp_filetype['type'] ) ) { // In case some other filter messed it up. $wp_filetype['type'] = 'application/json'; } if ( ! extension_loaded( 'fileinfo' ) ) { return $wp_check_filetype_and_ext; } $finfo = finfo_open( FILEINFO_MIME_TYPE ); $real_mime = finfo_file( $finfo, $file ); finfo_close( $finfo ); if ( 'text/plain' !== $real_mime ) { return $wp_check_filetype_and_ext; } $wp_check_filetype_and_ext = array_merge( $wp_check_filetype_and_ext, $wp_filetype ); return $wp_check_filetype_and_ext; } /** * Lists Data collected for analytics * * @since 2.11 * @author Caspar Hübinger * * @return string HTML list table */ function rocket_data_collection_preview_table() { $data = rocket_analytics_data(); if ( ! $data ) { return; } $html = '<table class="wp-rocket-data-table widefat striped">'; $html .= '<tbody>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'Server type:', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', $data['web_server'] ); $html .= '</td>'; $html .= '</tr>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'PHP version number:', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', $data['php_version'] ); $html .= '</td>'; $html .= '</tr>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'WordPress version number:', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', $data['wordpress_version'] ); $html .= '</td>'; $html .= '</tr>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'WordPress multisite:', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', $data['multisite'] ? 'true' : 'false' ); $html .= '</td>'; $html .= '</tr>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'Current theme:', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', $data['current_theme'] ); $html .= '</td>'; $html .= '</tr>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'Current site language:', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', $data['locale'] ); $html .= '</td>'; $html .= '</tr>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'Active plugins:', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', __( 'Plugin names of all active plugins', 'rocket' ) ); $html .= '</td>'; $html .= '</tr>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'Anonymized WP Rocket settings:', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', __( 'Which WP Rocket settings are active', 'rocket' ) ); $html .= '</td>'; $html .= '</tr>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'Anonymized WP Rocket statistics:', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', __( 'How WP Rocket features function and perform.', 'rocket' ) ); $html .= '</td>'; $html .= '</tr>'; $html .= '<tr>'; $html .= '<td class="column-primary">'; $html .= sprintf( '<strong>%s</strong>', __( 'WP Rocket license type', 'rocket' ) ); $html .= '</td>'; $html .= '<td>'; $html .= sprintf( '<em>%s</em>', $data['license_type'] ); $html .= '</td>'; $html .= '</tr>'; $html .= '</tbody>'; $html .= '</table>'; return $html; } /** * Adds error message after settings import and redirects. * * @since 3.0 * @author Remy Perona * * @param string $message Message to display in the error notice. * @param string $status Status of the error. * @return void */ function rocket_settings_import_redirect( $message, $status ) { add_settings_error( 'general', 'settings_updated', $message, $status ); set_transient( 'settings_errors', get_settings_errors(), 30 ); $goback = add_query_arg( 'settings-updated', 'true', wp_get_referer() ); wp_safe_redirect( esc_url_raw( $goback ) ); die(); } /** * Check if WPR options should be displayed. * * @return bool */ function rocket_can_display_options() { $disallowed_post_status = [ 'draft', 'trash', 'private', 'future', 'pending', ]; $post_status = get_post_status(); if ( in_array( $post_status, $disallowed_post_status, true ) ) { return false; } if ( function_exists( 'get_current_screen' ) && is_object( get_current_screen() ) && 'add' === get_current_screen()->action ) { return false; } return true; } /** * Create a hash from wp rocket options. * * @param array $value options. * * @return string */ function rocket_create_options_hash( $value ) { $removed = [ 'cache_mobile' => true, 'purge_cron_interval' => true, 'purge_cron_unit' => true, 'database_revisions' => true, 'database_auto_drafts' => true, 'database_trashed_posts' => true, 'database_spam_comments' => true, 'database_trashed_comments' => true, 'database_all_transients' => true, 'database_optimize_tables' => true, 'schedule_automatic_cleanup' => true, 'automatic_cleanup_frequency' => true, 'do_cloudflare' => true, 'cloudflare_email' => true, 'cloudflare_api_key' => true, 'cloudflare_zone_id' => true, 'cloudflare_devmode' => true, 'cloudflare_auto_settings' => true, 'cloudflare_old_settings' => true, 'heartbeat_admin_behavior' => true, 'heartbeat_editor_behavior' => true, 'varnish_auto_purge' => true, 'analytics_enabled' => true, 'sucury_waf_cache_sync' => true, 'sucury_waf_api_key' => true, 'manual_preload' => true, 'preload_excluded_uri' => true, 'cache_reject_uri' => true, 'version' => true, ]; // Create 2 arrays to compare. $value_diff = array_diff_key( $value, $removed ); ksort( $value_diff ); return md5( wp_json_encode( $value_diff ) ); } /** * This function returns the license type for a customer. * * @param object $customer_data customer data as an object. * @return string the type of the license the user has. */ function rocket_get_license_type( $customer_data ) { if ( false === $customer_data || ! isset( $customer_data->licence_account ) ) { return __( 'Unavailable', 'rocket' ); } // The licence name directly from User endpoint. if ( ! empty( $customer_data->licence->name ) ) { return esc_html( $customer_data->licence->name ); } // Fallback to licence account. if ( 1 <= $customer_data->licence_account && $customer_data->licence_account < 3 ) { return 'Single'; } elseif ( -1 === (int) $customer_data->licence_account ) { return 'Infinite'; } return 'Plus'; } /** * Fires callback attached to a deprecated filter hook. * * @param string $new_hook The hook that should have been used. * @param array $args Array of additional function arguments to be passed. * @param string $version The version of WPR that deprecated the hook. * @param string $old_hook Name of the original filter hook. * * @return mixed The filtered value after all hooked functions are applied to it. */ function rocket_apply_filter_and_deprecated( string $new_hook, array $args, string $version, string $old_hook ) { $filtered_value = apply_filters_deprecated( $old_hook, $args, $version, $new_hook ); $args[0] = $filtered_value; return apply_filters_ref_array( $new_hook, $args ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Fires callback attached to a deprecated action hook. * * @param string $new_hook The hook that should have been used. * @param array $args Array of additional function arguments to be passed. * @param string $version The version of WPR that deprecated the hook. * @param string $old_hook The name of the action hook. * * @return void */ function rocket_do_action_and_deprecated( string $new_hook, array $args, string $version, string $old_hook ): void { do_action_deprecated( $old_hook, $args, $version, $new_hook ); do_action_ref_array( $new_hook, $args ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } functions/admin-stats.php 0000644 00000062464 15174671745 0011546 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Count number of attachments. * * @since 1.0 * @author Jonathan Buttigieg * * @return int The number of attachments. */ function imagify_count_attachments() { global $wpdb; static $count; /** * Filter the number of attachments. * 3rd party will be able to override the result. * * @since 1.5 * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_attachments', false ); if ( false !== $pre_count ) { return (int) $pre_count; } if ( isset( $count ) ) { return $count; } $mime_types = Imagify_DB::get_mime_types(); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = ''; $nodata_where = ''; if ( ! imagify_has_attachments_without_required_metadata() ) { $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause( 'p.ID', true, true, "AND p.post_mime_type IN ( $mime_types ) AND p.post_type = 'attachment' AND p.post_status IN ( $statuses )" ); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause(); } $count = (int) $wpdb->get_var( // WPCS: unprepared SQL ok. " SELECT COUNT( p.ID ) FROM $wpdb->posts AS p $nodata_join WHERE p.post_mime_type IN ( $mime_types ) AND p.post_type = 'attachment' AND p.post_status IN ( $statuses ) $nodata_where" ); if ( $count > imagify_get_unoptimized_attachment_limit() ) { set_transient( 'imagify_large_library', 1 ); } elseif ( get_transient( 'imagify_large_library' ) ) { // In case the number is decreasing under our limit. delete_transient( 'imagify_large_library' ); } return $count; } /** * Count number of optimized attachments with an error. * * @since 1.0 * @author Jonathan Buttigieg * * @return int The number of attachments. */ function imagify_count_error_attachments() { global $wpdb; static $count; /** * Filter the number of optimized attachments with an error. * 3rd party will be able to override the result. * * @since 1.5 * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_error_attachments', false ); if ( false !== $pre_count ) { return (int) $pre_count; } if ( isset( $count ) ) { return $count; } $mime_types = Imagify_DB::get_mime_types(); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = ''; $nodata_where = ''; if ( ! imagify_has_attachments_without_required_metadata() ) { $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause(); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause(); } $count = (int) $wpdb->get_var( // WPCS: unprepared SQL ok. " SELECT COUNT(*) FROM ( SELECT p.ID FROM $wpdb->posts AS p $nodata_join INNER JOIN $wpdb->postmeta AS mt1 ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' ) WHERE p.post_mime_type IN ( $mime_types ) AND p.post_type = 'attachment' AND p.post_status IN ( $statuses ) AND mt1.meta_value = 'error' $nodata_where GROUP BY p.ID ) AS imagify_count_error" ); return $count; } /** * Count number of optimized attachments (by Imagify or an other tool before). * * @since 1.0 * @author Jonathan Buttigieg * * @return int The number of attachments. */ function imagify_count_optimized_attachments() { global $wpdb; static $count; /** * Filter the number of optimized attachments. * 3rd party will be able to override the result. * * @since 1.5 * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_optimized_attachments', false ); if ( false !== $pre_count ) { return (int) $pre_count; } if ( isset( $count ) ) { return $count; } $mime_types = Imagify_DB::get_mime_types(); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = ''; $nodata_where = ''; if ( ! imagify_has_attachments_without_required_metadata() ) { $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause(); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause(); } $count = (int) $wpdb->get_var( // WPCS: unprepared SQL ok. " SELECT COUNT( DISTINCT p.ID ) FROM $wpdb->posts AS p $nodata_join INNER JOIN $wpdb->postmeta AS mt1 ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' ) WHERE p.post_mime_type IN ( $mime_types ) AND p.post_type = 'attachment' AND p.post_status IN ( $statuses ) AND mt1.meta_value IN ( 'success', 'already_optimized' ) $nodata_where" ); return $count; } /** * Count number of unoptimized attachments. * * @since 1.0 * @author Jonathan Buttigieg * * @return int The number of attachments. */ function imagify_count_unoptimized_attachments() { /** * Filter the number of unoptimized attachments. * 3rd party will be able to override the result. * * @since 1.5 * * @param int|bool $pre_count Default is false. Provide an integer. */ $pre_count = apply_filters( 'imagify_count_unoptimized_attachments', false ); if ( false !== $pre_count ) { return (int) $pre_count; } return imagify_count_attachments() - imagify_count_optimized_attachments() - imagify_count_error_attachments(); } /** * Count percent of optimized attachments. * * @since 1.0 * @author Jonathan Buttigieg * * @return int The percent of optimized attachments. */ function imagify_percent_optimized_attachments() { /** * Filter the percent of optimized attachments. * 3rd party will be able to override the result. * * @since 1.5 * * @param int|bool $percent Default is false. Provide an integer. */ $percent = apply_filters( 'imagify_percent_optimized_attachments', false ); if ( false !== $percent ) { return (int) $percent; } $total_attachments = imagify_count_attachments(); $total_optimized_attachments = imagify_count_optimized_attachments(); if ( ! $total_attachments || ! $total_optimized_attachments ) { return 0; } return min( round( 100 * $total_optimized_attachments / $total_attachments ), 100 ); } /** * Count percent, original & optimized size of all images optimized by Imagify. * * @since 1.0 * @since 1.6.7 Revamped to handle huge libraries. * @author Jonathan Buttigieg * * @param string $key What data to return. Choices are between 'count', 'original_size', 'optimized_size', and 'percent'. If left empty, the whole array is returned. * @return array|int An array containing the optimization data. A single data if $key is provided. */ function imagify_count_saving_data( $key = '' ) { global $wpdb; /** * Filter the query to get all optimized attachments. * 3rd party will be able to override the result. * * @since 1.5 * @since 1.6.7 This filter should return an array containing the following keys: 'count', 'original_size', and 'optimized_size'. * * @param bool|array $attachments An array containing the keys ('count', 'original_size', and 'optimized_size'), or an array of attachments (back compat', deprecated), or false. */ $attachments = apply_filters( 'imagify_count_saving_data', false ); $original_size = 0; $optimized_size = 0; $count = 0; if ( is_array( $attachments ) ) { /** * Bypass. */ if ( isset( $attachments['count'], $attachments['original_size'], $attachments['optimized_size'] ) ) { /** * We have the results we need. */ $attachments['percent'] = $attachments['optimized_size'] && $attachments['original_size'] ? ceil( ( ( $attachments['original_size'] - $attachments['optimized_size'] ) / $attachments['original_size'] ) * 100 ) : 0; return $attachments; } /** * Back compat'. * The following shouldn't be used. Sites with a huge library won't like it. */ $attachments = array_map( 'maybe_unserialize', (array) $attachments ); if ( $attachments ) { foreach ( $attachments as $attachment_data ) { if ( ! $attachment_data ) { continue; } ++$count; $original_data = $attachment_data['sizes']['full']; // Increment the original sizes. $original_size += $original_data['original_size'] ? $original_data['original_size'] : 0; $optimized_size += $original_data['optimized_size'] ? $original_data['optimized_size'] : 0; unset( $attachment_data['sizes']['full'] ); // Increment the thumbnails sizes. if ( $attachment_data['sizes'] ) { foreach ( $attachment_data['sizes'] as $size_data ) { if ( ! empty( $size_data['success'] ) ) { $original_size += $size_data['original_size'] ? $size_data['original_size'] : 0; $optimized_size += $size_data['optimized_size'] ? $size_data['optimized_size'] : 0; } } } } } } else { /** * Filter the chunk size of the requests fetching the data. * 15,000 seems to be a good balance between memory used, speed, and number of DB hits. * * @param int $limit The maximum number of elements per chunk. */ $limit = apply_filters( 'imagify_count_saving_data_limit', 15000 ); $limit = absint( $limit ); $mime_types = Imagify_DB::get_mime_types(); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = ''; $nodata_where = ''; if ( ! imagify_has_attachments_without_required_metadata() ) { $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause(); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause(); } $attachment_ids = $wpdb->get_col( // WPCS: unprepared SQL ok. " SELECT p.ID FROM $wpdb->posts AS p $nodata_join INNER JOIN $wpdb->postmeta AS mt1 ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' ) WHERE p.post_mime_type IN ( $mime_types ) AND p.post_type = 'attachment' AND p.post_status IN ( $statuses ) AND mt1.meta_value = 'success' $nodata_where ORDER BY CAST( p.ID AS UNSIGNED )" ); $wpdb->flush(); $attachment_ids = array_map( 'absint', array_unique( $attachment_ids ) ); $attachment_ids = array_chunk( $attachment_ids, $limit ); while ( $attachment_ids ) { $limit_ids = array_shift( $attachment_ids ); $limit_ids = implode( ',', $limit_ids ); $attachments = $wpdb->get_col( // WPCS: unprepared SQL ok. " SELECT meta_value FROM $wpdb->postmeta WHERE post_id IN ( $limit_ids ) AND meta_key = '_imagify_data'" ); $wpdb->flush(); unset( $limit_ids ); if ( ! $attachments ) { continue; } $attachments = array_map( 'maybe_unserialize', $attachments ); foreach ( $attachments as $attachment_data ) { if ( ! $attachment_data ) { continue; } if ( empty( $attachment_data['sizes']['full']['success'] ) ) { /** * - Case where this attachment has multiple '_imagify_status' metas, and is fetched (in the above query) as a "success" while the '_imagify_data' says otherwise. * - Case where this meta has no "full" entry. * Don't ask how it's possible, I don't know ¯\(°_o)/¯ */ continue; } $original_data = $attachment_data['sizes']['full']; ++$count; // Increment the original sizes. $original_size += ! empty( $original_data['original_size'] ) ? $original_data['original_size'] : 0; $optimized_size += ! empty( $original_data['optimized_size'] ) ? $original_data['optimized_size'] : 0; unset( $attachment_data['sizes']['full'], $original_data ); // Increment the thumbnails sizes. if ( $attachment_data['sizes'] ) { foreach ( $attachment_data['sizes'] as $size_data ) { if ( ! empty( $size_data['success'] ) ) { $original_size += ! empty( $size_data['original_size'] ) ? $size_data['original_size'] : 0; $optimized_size += ! empty( $size_data['optimized_size'] ) ? $size_data['optimized_size'] : 0; } } } unset( $size_data ); } unset( $attachments, $attachment_data ); } } $data = [ 'count' => $count, 'original_size' => $original_size, 'optimized_size' => $optimized_size, 'percent' => $original_size && $optimized_size ? ceil( ( ( $original_size - $optimized_size ) / $original_size ) * 100 ) : 0, ]; if ( ! empty( $key ) ) { return isset( $data[ $key ] ) ? $data[ $key ] : 0; } return $data; } /** * Returns the estimated total size of the images not optimized. * * We estimate the total size of the images in the library by getting the latest 250 images and their thumbnails * add up their filesizes, and doing some maths to get the total size. * * @since 1.6 * @author Remy Perona * * @return int The current estimated total size of images not optimized. */ function imagify_calculate_total_size_images_library() { global $wpdb; $mime_types = Imagify_DB::get_mime_types(); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause(); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause(); $image_ids = $wpdb->get_col( // WPCS: unprepared SQL ok. " SELECT p.ID FROM $wpdb->posts AS p $nodata_join WHERE p.post_mime_type IN ( $mime_types ) AND p.post_type = 'attachment' AND p.post_status IN ( $statuses ) $nodata_where LIMIT 250 " ); if ( ! $image_ids ) { return 0; } $count_latest_images = count( $image_ids ); $count_total_images = imagify_count_attachments(); return imagify_calculate_total_image_size( $image_ids, $count_latest_images, $count_total_images ); } /** * Returns the estimated average size of the images uploaded per month. * * We estimate the average size of the images uploaded in the library per month by getting the latest 250 images and their thumbnails * for the 3 latest months, add up their filesizes, and doing some maths to get the total average size. * * @since 1.6 * @since 1.7 Use wpdb instead of WP_Query. * @author Remy Perona * * @return int The current estimated average size of images uploaded per month. */ function imagify_calculate_average_size_images_per_month() { global $wpdb; $mime_types = Imagify_DB::get_mime_types(); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause( "$wpdb->posts.ID" ); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause(); $limit = ' LIMIT 0, 250'; $query = " SELECT $wpdb->posts.ID FROM $wpdb->posts $nodata_join WHERE $wpdb->posts.post_mime_type IN ( $mime_types ) AND $wpdb->posts.post_type = 'attachment' AND $wpdb->posts.post_status IN ( $statuses ) $nodata_where %date_query%"; // Queries per month. $date_query = new WP_Date_Query( [ [ 'before' => 'now', 'after' => '1 month ago', ], ] ); $partial_images_uploaded_last_month = $wpdb->get_col( str_replace( '%date_query%', $date_query->get_sql(), $query . $limit ) ); // WPCS: unprepared SQL ok. $date_query = new WP_Date_Query( [ [ 'before' => '1 month ago', 'after' => '2 months ago', ], ] ); $partial_images_uploaded_two_months_ago = $wpdb->get_col( str_replace( '%date_query%', $date_query->get_sql(), $query . $limit ) ); // WPCS: unprepared SQL ok. $date_query = new WP_Date_Query( [ [ 'before' => '2 month ago', 'after' => '3 months ago', ], ] ); $partial_images_uploaded_three_months_ago = $wpdb->get_col( str_replace( '%date_query%', $date_query->get_sql(), $query . $limit ) ); // WPCS: unprepared SQL ok. // Total for the 3 months. $partial_images_uploaded_id = array_merge( $partial_images_uploaded_last_month, $partial_images_uploaded_two_months_ago, $partial_images_uploaded_three_months_ago ); if ( ! $partial_images_uploaded_id ) { return 0; } // Total for the 3 months, without the "250" limit. $date_query = new WP_Date_Query( [ [ 'before' => 'now', 'after' => '3 month ago', ], ] ); $images_uploaded_id = $wpdb->get_col( str_replace( '%date_query%', $date_query->get_sql(), $query ) ); // WPCS: unprepared SQL ok. if ( ! $images_uploaded_id ) { return 0; } // Number of image attachments uploaded for the 3 latest months, limited to 250 per month. $partial_total_images_uploaded = count( $partial_images_uploaded_id ); // Total number of image attachments uploaded for the 3 latest months. $total_images_uploaded = count( $images_uploaded_id ); return imagify_calculate_total_image_size( $partial_images_uploaded_id, $partial_total_images_uploaded, $total_images_uploaded ) / 3; } /** * Returns the estimated total size of images. * * @since 1.6 * @author Remy Perona * * @param array $image_ids Array of image IDs. * @param int $partial_total_images The number of image attachments we're doing the calculation with. * @param int $total_images The total number of image attachments. * @return int The estimated total size of images. */ function imagify_calculate_total_image_size( $image_ids, $partial_total_images, $total_images ) { global $wpdb; $image_ids = array_filter( array_map( 'absint', $image_ids ) ); if ( ! $image_ids ) { return 0; } $results = Imagify_DB::get_metas( [ // Get attachments filename. 'filenames' => '_wp_attached_file', // Get attachments data. 'data' => '_wp_attachment_metadata', // Get Imagify data. 'imagify_data' => '_imagify_data', // Get attachments status. 'statuses' => '_imagify_status', ], $image_ids ); // Number of image attachments we're doing the calculation with. In case array_filter() removed results. $partial_total_images = count( $image_ids ); // Total size of unoptimized size. $partial_size_images = 0; // Total number of thumbnails. $partial_total_intermediate_images = 0; $filesystem = imagify_get_filesystem(); $is_active_for_network = imagify_is_active_for_network(); $disallowed_sizes = get_imagify_option( 'disallowed-sizes' ); foreach ( $image_ids as $i => $image_id ) { $attachment_status = isset( $results['statuses'][ $image_id ] ) ? $results['statuses'][ $image_id ] : false; if ( 'success' === $attachment_status ) { /** * The image files have been optimized. */ // Original size. $partial_size_images += isset( $results['imagify_data'][ $image_id ]['stats']['original_size'] ) ? $results['imagify_data'][ $image_id ]['stats']['original_size'] : 0; // Number of thumbnails. $partial_total_intermediate_images += count( $results['imagify_data'][ $image_id ]['sizes'] ); unset( $image_ids[ $i ], $results['filenames'][ $image_id ], $results['data'][ $image_id ], $results['imagify_data'][ $image_id ], $results['statuses'][ $image_id ] ); continue; } /** * The image files are not optimized. */ // Create an array containing all this attachment files. $files = [ 'full' => get_imagify_attached_file( $results['filenames'][ $image_id ] ), ]; $sizes = isset( $results['data'][ $image_id ]['sizes'] ) ? $results['data'][ $image_id ]['sizes'] : []; if ( $sizes && is_array( $sizes ) ) { if ( ! $is_active_for_network ) { $sizes = array_diff_key( $sizes, $disallowed_sizes ); } if ( $sizes ) { $full_dirname = $filesystem->dir_path( $files['full'] ); foreach ( $sizes as $size_key => $size_data ) { $files[ $size_key ] = $full_dirname . '/' . $size_data['file']; } } } /** * Allow to provide all files size and the number of thumbnails. * * @since 1.6.7 * @author Grégory Viguier * * @param bool $size_and_count False by default. * @param int $image_id The attachment ID. * @param array $files An array of file paths with thumbnail sizes as keys. * @param array $image_ids An array of all attachment IDs. * @return bool|array False by default. Provide an array with the keys 'filesize' (containing the total filesize) and 'thumbnails' (containing the number of thumbnails). */ $size_and_count = apply_filters( 'imagify_total_attachment_filesize', false, $image_id, $files, $image_ids ); if ( is_array( $size_and_count ) ) { $partial_size_images += $size_and_count['filesize']; $partial_total_intermediate_images += $size_and_count['thumbnails']; } else { foreach ( $files as $file ) { if ( $filesystem->exists( $file ) ) { $partial_size_images += $filesystem->size( $file ); } } unset( $files['full'] ); $partial_total_intermediate_images += count( $files ); } unset( $image_ids[ $i ], $results['filenames'][ $image_id ], $results['data'][ $image_id ], $results['imagify_data'][ $image_id ], $results['statuses'][ $image_id ] ); } // Number of thumbnails per attachment = Number of thumbnails / Number of attachments. $intermediate_images_per_image = $partial_total_intermediate_images / $partial_total_images; /** * Note: Number of attachments ($partial_total_images) === Number of full sizes. * Average image size = Size of the images / ( Number of full sizes + Number of thumbnails ). * Average image size = Size of the images / Number of images. */ $average_size_images = $partial_size_images / ( $partial_total_images + $partial_total_intermediate_images ); /** * Note: Total number of attachments ($total_images) === Total number of full sizes. * Total images size = Average image size * ( Total number of full sizes + ( Number of thumbnails per attachment * Total number of attachments ) ). * Total images size = Average image size * ( Total number of full sizes + Total number of thumbnails ). */ $total_size_images = $average_size_images * ( $total_images + ( $intermediate_images_per_image * $total_images ) ); return $total_size_images; } /** * Get all generic stats to be used in the bulk optimization page. * * @since 1.7.1 * @author Grégory Viguier * * @param array $types The folder types. If a folder type is "library", the context should be suffixed after a pipe character. They are passed as array keys. * @param array $args { * Optional. An array of arguments. * * @type bool $fullset True to return the full set of data. False to return only the main data. * @type bool $formatting Some of the data is returned formatted. * } * @return array */ function imagify_get_bulk_stats( $types, $args = [] ) { $types = $types && is_array( $types ) ? $types : []; $args = array_merge( [ 'fullset' => false, 'formatting' => true, ], (array) $args ); $data = [ // Global chart. 'total_attachments' => 0, 'unoptimized_attachments' => 0, 'optimized_attachments' => 0, 'errors_attachments' => 0, // Stats block. 'already_optimized_attachments' => 0, 'original_human' => 0, 'optimized_human' => 0, ]; if ( isset( $types['library|wp'] ) ) { /** * Library. */ $saving_data = imagify_count_saving_data(); // Global chart. $data['total_attachments'] += imagify_count_attachments(); $data['unoptimized_attachments'] += imagify_count_unoptimized_attachments(); $data['optimized_attachments'] += imagify_count_optimized_attachments(); $data['errors_attachments'] += imagify_count_error_attachments(); // Stats block. $data['already_optimized_attachments'] += $saving_data['count']; $data['original_human'] += $saving_data['original_size']; $data['optimized_human'] += $saving_data['optimized_size']; } if ( isset( $types['custom-folders|custom-folders'] ) ) { /** * Custom folders. */ // Global chart. $data['total_attachments'] += Imagify_Files_Stats::count_all_files(); $data['unoptimized_attachments'] += Imagify_Files_Stats::count_no_status_files(); $data['optimized_attachments'] += Imagify_Files_Stats::count_optimized_files(); $data['errors_attachments'] += Imagify_Files_Stats::count_error_files(); // Stats block. $data['already_optimized_attachments'] += Imagify_Files_Stats::count_success_files(); $data['original_human'] += Imagify_Files_Stats::get_original_size(); $data['optimized_human'] += Imagify_Files_Stats::get_optimized_size(); } /** * Full set of data. */ if ( $args['fullset'] ) { // User account. $views = Imagify_Views::get_instance(); $data['unconsumed_quota'] = $views->get_quota_percent(); $data['quota_class'] = $views->get_quota_class(); $data['quota_icon'] = $views->get_quota_icon(); } /** * Filter the generic stats used in the bulk optimization page. * * @since 1.7.1 * @author Grégory Viguier * * @param array $data The data. * @param array $types The folder types. They are passed as array keys. * @param array $args { * Optional. An array of arguments. * * @type bool $fullset True to return the full set of data. False to return only the main data. * @type bool $formatting Some of the data is returned formatted. * } */ $data = apply_filters( 'imagify_bulk_stats', $data, $types, $args ); /** * Percentages. */ if ( $data['total_attachments'] && $data['optimized_attachments'] ) { $data['optimized_attachments_percent'] = round( 100 * $data['optimized_attachments'] / $data['total_attachments'] ); } else { $data['optimized_attachments_percent'] = 0; } if ( $data['original_human'] && $data['optimized_human'] ) { $data['optimized_percent'] = ceil( 100 - ( 100 * $data['optimized_human'] / $data['original_human'] ) ); } else { $data['optimized_percent'] = 0; } /** * Formating. */ if ( $args['formatting'] ) { $data['already_optimized_attachments'] = number_format_i18n( $data['already_optimized_attachments'] ); $data['original_human'] = imagify_size_format( $data['original_human'], 1 ); $data['optimized_human'] = imagify_size_format( $data['optimized_human'], 1 ); } return $data; } functions/process.php 0000644 00000003772 15174671745 0010775 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Run an async job to optimize images in background. * * @param array $body Contains the usual $_POST. * * @since 1.4 */ function imagify_do_async_job( $body ) { $args = [ 'timeout' => 0.01, 'blocking' => false, 'body' => $body, 'cookies' => isset( $_COOKIE ) && is_array( $_COOKIE ) ? $_COOKIE : [], 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), ]; /** * Filter the arguments used to launch an async job. * * @since 1.6.6 * @author Grégory Viguier * * @param array $args An array of arguments passed to wp_remote_post(). */ $args = apply_filters( 'imagify_do_async_job_args', $args ); /** * It can be a XML-RPC request. The problem is that XML-RPC doesn't use cookies. */ if ( defined( 'XMLRPC_REQUEST' ) && get_current_user_id() ) { /** * In old WP versions, the field "option_name" in the wp_options table was limited to 64 characters. * From 64, remove 19 characters for "_transient_timeout_" = 45. * Then remove 12 characters for "imagify_rpc_" (transient name) = 33. * Luckily, a md5 is 32 characters long. */ $rpc_id = md5( maybe_serialize( $body ) ); // Send the request to our RPC bridge instead. $args['body']['imagify_rpc_action'] = $args['body']['action']; $args['body']['action'] = 'imagify_rpc'; $args['body']['imagify_rpc_id'] = $rpc_id; $args['body']['imagify_rpc_nonce'] = wp_create_nonce( 'imagify_rpc_' . $rpc_id ); // Since we can't send cookies to keep the user logged in, store the user ID in a transient. set_transient( 'imagify_rpc_' . $rpc_id, get_current_user_id(), 30 ); } $url = admin_url( 'admin-ajax.php' ); /** * Filter the URL to use for async jobs. * * @since 1.9.5 * @author Grégory Viguier * * @param string $url An URL. * @param array $args An array of arguments passed to wp_remote_post(). */ $url = apply_filters( 'imagify_async_job_url', $url, $args ); wp_remote_post( $url, $args ); } functions/common.php 0000644 00000037713 15174671745 0010611 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Get the list of the names of the Imagify context currently in use. * * @since 1.9 * @author Grégory Viguier * * @return array An array of strings. */ function imagify_get_context_names() { static $contexts; if ( isset( $contexts ) ) { return $contexts; } /** * Register new contexts. * * @since 1.9 * @author Grégory Viguier * * @param array $contexts An array of context names. */ $contexts = (array) apply_filters( 'imagify_register_context', [] ); $contexts = array_filter( $contexts, function ( $context ) { return $context && is_string( $context ); } ); $contexts = array_merge( [ 'wp', 'custom-folders' ], $contexts ); sort( $contexts ); return $contexts; } /** * Sanitize an optimization context. * * @since 1.6.11 * @author Grégory Viguier * * @param string $context The context. * @return string */ function imagify_sanitize_context( $context ) { return sanitize_key( $context ); } /** * Get the Imagify context instance. * * @since 1.9 * @author Grégory Viguier * * @param string $context The context name. Default values are 'wp' and 'custom-folders'. * @return \Imagify\Context\ContextInterface The context instance. */ function imagify_get_context( $context ) { $class_name = imagify_get_context_class_name( $context ); return $class_name::get_instance(); } /** * Get the Imagify context class name. * * @since 1.9 * @author Grégory Viguier * * @param string $context The context name. Default values are 'wp' and 'custom-folders'. * @return string The context class name. */ function imagify_get_context_class_name( $context ) { $context = imagify_sanitize_context( $context ); switch ( $context ) { case 'wp': $class_name = '\\Imagify\\Context\\WP'; break; case 'custom-folders': $class_name = '\\Imagify\\Context\\CustomFolders'; break; default: $class_name = '\\Imagify\\Context\\Noop'; } /** * Filter the name of the class to use to define a context. * * @since 1.9 * @author Grégory Viguier * * @param int $class_name The class name. * @param string $context The context name. */ $class_name = apply_filters( 'imagify_context_class_name', $class_name, $context ); return '\\' . ltrim( $class_name, '\\' ); } /** * Get the Imagify process instance depending on a context. * * @since 1.9 * @author Grégory Viguier * * @param int $media_id The media ID. * @param string $context The context name. Default values are 'wp' and 'custom-folders'. * @return \Imagify\Optimization\Process\ProcessInterface The optimization process instance. */ function imagify_get_optimization_process( $media_id, $context ) { $class_name = imagify_get_optimization_process_class_name( $context ); return new $class_name( $media_id ); } /** * Get the Imagify process class name depending on a context. * * @since 1.9 * @author Grégory Viguier * * @param string $context The context name. Default values are 'wp' and 'custom-folders'. * @return string The optimization process class name. */ function imagify_get_optimization_process_class_name( $context ) { $context = imagify_sanitize_context( $context ); switch ( $context ) { case 'wp': $class_name = '\\Imagify\\Optimization\\Process\\WP'; break; case 'custom-folders': $class_name = '\\Imagify\\Optimization\\Process\\CustomFolders'; break; default: $class_name = '\\Imagify\\Optimization\\Process\\Noop'; } /** * Filter the name of the class to use for the optimization. * * @since 1.9 * @author Grégory Viguier * * @param int $class_name The class name. * @param string $context The context name. */ $class_name = apply_filters( 'imagify_process_class_name', $class_name, $context ); return '\\' . ltrim( $class_name, '\\' ); } /** * Get WP Direct filesystem object. Also define chmod constants if not done yet. * * @since 1.6.5 * @author Grégory Viguier * * @return object A Imagify_Filesystem object. */ function imagify_get_filesystem() { return Imagify_Filesystem::get_instance(); } /** * Convert a path (or URL) to its WebP version. * To keep the function simple: * - Not tested if it's an image. * - File existance is not tested. * - If an URL is given, make sure it doesn't contain query args. * * @since 1.9 * @author Grégory Viguier * * @param string $path A file path or URL. * @return string */ function imagify_path_to_webp( $path ) { return $path . '.webp'; } /** * Convert a path (or URL) to its next-gen version. * To keep the function simple: * - Not tested if it's an image. * - File existance is not tested. * - If an URL is given, make sure it doesn't contain query args. * * @since 2.2 * * @param string $path A file path or URL. * @param string $format format we are targeting. * @return string */ function imagify_path_to_nextgen( $path, string $format ) { switch ( $format ) { case 'webp': $path = $path . '.webp'; break; case 'avif': $path = $path . '.avif'; break; } return $path; } /** * Tell if the current user can optimize custom folders. * * @since 1.7 * @author Grégory Viguier * * @return bool */ function imagify_can_optimize_custom_folders() { static $can; if ( isset( $can ) ) { return $can; } // Check if the DB tables are ready. if ( ! Imagify_Folders_DB::get_instance()->can_operate() || ! Imagify_Files_DB::get_instance()->can_operate() ) { $can = false; return $can; } // Check for user capacity. $can = imagify_get_context( 'custom-folders' )->current_user_can( 'optimize' ); return $can; } /** * Simple helper to get some external URLs, like to the documentation. * * @since 1.6.12 * @author Grégory Viguier * * @param string $target What we want. * @param array $query_args An array of query arguments. * @return string The URL. */ function imagify_get_external_url( $target, $query_args = [] ) { $site_url = IMAGIFY_SITE_DOMAIN . '/'; $app_url = IMAGIFY_APP_DOMAIN . '/#/'; switch ( $target ) { case 'plugin': /* translators: Plugin URI of the plugin/theme */ $url = __( 'https://wordpress.org/plugins/imagify/', 'imagify' ); break; case 'rate': $url = 'https://wordpress.org/support/view/plugin-reviews/imagify?rate=5#postform'; break; case 'contact': $lang = imagify_get_current_lang_in( 'fr' ); $paths = [ 'en' => 'contact', 'fr' => 'fr/contact', ]; $url = $site_url . $paths[ $lang ] . '/'; break; case 'documentation': $url = $site_url . 'documentation/'; break; case 'documentation-imagick-gd': $url = $site_url . 'documentation/solve-imagemagick-gd-required/'; break; case 'register': $partner = imagify_get_partner(); if ( $partner ) { $query_args['partner'] = $partner; } $url = $app_url . 'register'; break; case 'subscription': $url = $app_url . 'subscription'; break; case 'get-api-key': $url = $app_url . 'api'; break; case 'payment': // Don't remove the trailing slash. $url = $app_url . 'plugin/'; break; default: return ''; } if ( $query_args ) { $url = add_query_arg( $query_args, $url ); } return $url; } /** * Get the current lang ('fr', 'en', 'de'...), limited to a given list. * * @since 1.6.14 * @author Grégory Viguier * * @param array $langs An array of langs, like array( 'de', 'es', 'fr', 'it' ). * @return string The current lang. Default is 'en'. */ function imagify_get_current_lang_in( $langs ) { static $locale; if ( ! isset( $locale ) ) { $locale = imagify_get_locale(); $locale = explode( '_', strtolower( $locale . '_' ) ); // Trailing underscore is to make sure $locale[1] is set. } foreach ( (array) $langs as $lang ) { if ( $lang === $locale[0] || $lang === $locale[1] ) { return $lang; } } return 'en'; } /** * Get the current locale. * * @since 1.6.14 * @author Grégory Viguier * * @return string The current locale. */ function imagify_get_locale() { $locale = function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale(); /** * Filter the locale used by Imagify. * * @since 1.6.14 * @author Grégory Viguier * * @param string $locale The current locale. */ return apply_filters( 'imagify_locale', $locale ); } /** * Get the label corresponding to the given optimization label. * * @since 1.7 * @author Grégory Viguier * * @param int|bool $level Optimization level (between 0 and 2). False if no level. * @param string $format Format to display the label. Use %ICON% for the icon and %s for the label. * @return string The label. */ function imagify_get_optimization_level_label( $level, $format = '%s' ) { if ( ! is_numeric( $level ) ) { return ''; } if ( strpos( $format, '%ICON%' ) !== false ) { $icon = '<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><g fill="#40B1D0" fill-rule="evenodd">'; switch ( $level ) { case 2: case 1: $icon .= '<polygon points="11.6054688 11.6054688 8.7890625 11.6054688 8.7890625 0.39453125 11.6054688 0.39453125"/><polygon points="7.39453125 11.6054688 4.60546875 11.6054688 4.60546875 3.89453125 7.39453125 3.89453125"/><polygon points="3.2109375 11.6054688 0.39453125 11.6054688 0.39453125 6 3.2109375 6"/>'; break; case 0: $icon .= '<polygon fill="#CCD1D6" points="11.6054688 11.6054688 8.7890625 11.6054688 8.7890625 0.39453125 11.6054688 0.39453125"/><polygon fill="#CCD1D6" points="7.39453125 11.6054688 4.60546875 11.6054688 4.60546875 3.89453125 7.39453125 3.89453125"/><polygon points="3.2109375 11.6054688 0.39453125 11.6054688 0.39453125 6 3.2109375 6"/>'; } $icon .= '</g></svg>'; $format = str_replace( '%ICON%', $icon, $format ); } switch ( $level ) { case 2: case 1: return sprintf( $format, __( 'Smart', 'imagify' ) ); case 0: return sprintf( $format, __( 'Lossless', 'imagify' ) ); } return ''; } /** * `array_merge()` + `array_intersect_key()`. * * @since 1.7 * @author Grégory Viguier * * @param array $values The array we're interested in. * @param array $default_values The array we use as boundaries. * @return array */ function imagify_merge_intersect( $values, $default_values ) { $values = array_merge( $default_values, (array) $values ); return array_intersect_key( $values, $default_values ); } /** * Returns true. * Useful for returning true to filters easily. * Similar to WP's __return_true() function, it allows to remove it from a filter without removing another one added by another plugin. * * @since 1.9 * @author Grégory Viguier * * @return bool True. */ function imagify_return_true() { return true; } /** * Returns false. * Useful for returning false to filters easily. * Similar to WP's __return_false() function, it allows to remove it from a filter without removing another one added by another plugin. * * @since 1.9 * @author Grégory Viguier * * @return bool False. */ function imagify_return_false() { return false; } /** * Marks a class as deprecated and informs when it has been used. * Similar to _deprecated_constructor(), but with different strings. * The current behavior is to trigger a user error if `WP_DEBUG` is true. * * @since 1.9 * @author Grégory Viguier * * @param string $class_name The class containing the deprecated constructor. * @param string $version The version of WordPress that deprecated the function. * @param string $replacement Optional. The function that should have been called. Default null. * @param string $parent_class Optional. The parent class calling the deprecated constructor. Default empty string. */ function imagify_deprecated_class( $class_name, $version, $replacement = null, $parent_class = '' ) { /** * Fires when a deprecated class is called. * * @since 1.9 * @author Grégory Viguier * * @param string $class_name The class containing the deprecated constructor. * @param string $version The version of WordPress that deprecated the function. * @param string $replacement Optional. The function that should have been called. * @param string $parent_class The parent class calling the deprecated constructor. */ do_action( 'imagify_deprecated_class_run', $class_name, $version, $replacement, $parent_class ); if ( ! WP_DEBUG ) { return; } /** * Filters whether to trigger an error for deprecated classes. * * `WP_DEBUG` must be true in addition to the filter evaluating to true. * * @since 1.9 * @author Grégory Viguier * * @param bool $trigger Whether to trigger the error for deprecated classes. Default true. */ if ( ! apply_filters( 'imagify_deprecated_class_trigger_error', true ) ) { return; } if ( function_exists( '__' ) ) { if ( ! empty( $parent_class ) ) { /** * With parent class. */ if ( ! empty( $replacement ) ) { /** * With replacement. */ call_user_func( 'trigger_error', sprintf( /* translators: 1: PHP class name, 2: PHP parent class name, 3: version number, 4: replacement class name. */ __( 'The called class %1$s extending %2$s is <strong>deprecated</strong> since version %3$s! Use %4$s instead.', 'imagify' ), '<code>' . $class_name . '</code>', '<code>' . $parent_class . '</code>', '<strong>' . $version . '</strong>', '<code>' . $replacement . '</code>' ) ); return; } /** * Without replacement. */ call_user_func( 'trigger_error', sprintf( /* translators: 1: PHP class name, 2: PHP parent class name, 3: version number. */ __( 'The called class %1$s extending %2$s is <strong>deprecated</strong> since version %3$s!', 'imagify' ), '<code>' . $class_name . '</code>', '<code>' . $parent_class . '</code>', '<strong>' . $version . '</strong>' ) ); return; } /** * Without parent class. */ if ( ! empty( $replacement ) ) { /** * With replacement. */ call_user_func( 'trigger_error', sprintf( /* translators: 1: PHP class name, 2: version number, 3: replacement class name. */ __( 'The called class %1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.', 'imagify' ), '<code>' . $class_name . '</code>', '<strong>' . $version . '</strong>', '<code>' . $replacement . '</code>' ) ); return; } /** * Without replacement. */ call_user_func( 'trigger_error', sprintf( /* translators: 1: PHP class name, 2: version number. */ __( 'The called class %1$s is <strong>deprecated</strong> since version %2$s!', 'imagify' ), '<code>' . $class_name . '</code>', '<strong>' . $version . '</strong>' ) ); return; } if ( ! empty( $parent_class ) ) { /** * With parent class. */ if ( ! empty( $replacement ) ) { /** * With replacement. */ call_user_func( 'trigger_error', sprintf( 'The called class %1$s extending %2$s is <strong>deprecated</strong> since version %3$s! Use %4$s instead.', '<code>' . $class_name . '</code>', '<code>' . $parent_class . '</code>', '<strong>' . $version . '</strong>', '<code>' . $replacement . '</code>' ) ); return; } /** * Without replacement. */ call_user_func( 'trigger_error', sprintf( 'The called class %1$s extending %2$s is <strong>deprecated</strong> since version %3$s!', '<code>' . $class_name . '</code>', '<code>' . $parent_class . '</code>', '<strong>' . $version . '</strong>' ) ); return; } /** * Without parent class. */ if ( ! empty( $replacement ) ) { /** * With replacement. */ call_user_func( 'trigger_error', sprintf( 'The called class %1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.', '<code>' . $class_name . '</code>', '<strong>' . $version . '</strong>', '<code>' . $replacement . '</code>' ) ); return; } /** * Without replacement. */ call_user_func( 'trigger_error', sprintf( 'The called class %1$s is <strong>deprecated</strong> since version %2$s!', '<code>' . $class_name . '</code>', '<strong>' . $version . '</strong>' ) ); } functions/formatting.php 0000644 00000035425 15174671745 0011471 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Get relative url * Clean URL file to get only the equivalent of REQUEST_URI * ex: rocket_clean_exclude_file( 'http://www.geekpress.fr/referencement-wordpress/') return /referencement-wordpress/ * * @since 1.3.5 Redo the function * @since 1.0 * * @param string $file URL we want to parse. * @return bool|string false if $file is empty or false, relative path otherwise */ function rocket_clean_exclude_file( $file ) { if ( ! $file ) { return false; } return wp_parse_url( $file, PHP_URL_PATH ); } /** * Clean Never Cache URL(s) bad wildcards * * @since 3.4.2 * @author Soponar Cristina * * @param string $path URL which needs to be cleaned. * @return string Cleaned URL */ function rocket_clean_wildcards( $path ) { if ( ! $path ) { return ''; } $path_components = explode( '/', $path ); $arr = [ '.*' => '(.*)', '*' => '(.*)', '(*)' => '(.*)', '(.*)' => '(.*)', ]; foreach ( $path_components as &$path_component ) { $path_component = strtr( $path_component, $arr ); } $path = implode( '/', $path_components ); return $path; } /** * Used with array_filter to remove files without .css extension * * @since 1.0 * * @param string $file filepath to sanitize. * @return bool\string false if not a css file, filepath otherwise */ function rocket_sanitize_css( $file ) { $file = preg_replace( '#\?.*$#', '', $file ); $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ); return ( 'css' === $ext || 'php' === $ext ) ? trim( $file ) : false; } /** * Used with array_filter to remove files without .js extension * * @since 1.0 * * @param string $file filepath to sanitize. * @return bool\string false if not a js file, filepath otherwise */ function rocket_sanitize_js( $file ) { $file = preg_replace( '#\?.*$#', '', $file ); $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ); return ( 'js' === $ext || 'php' === $ext ) ? trim( $file ) : false; } /** * Sanitize and validate JS files to exclude from the minification. * * @since 3.3.7 * @author Remy Perona * @author Grégory Viguier * * @param string $file filepath to sanitize. * @return string */ function rocket_validate_js( $file ) { if ( rocket_is_internal_file( $file ) ) { $file = trim( $file ); $file = rocket_clean_exclude_file( $file ); $file = rocket_sanitize_js( $file ); return $file; } return rocket_remove_url_protocol( esc_url_raw( strtok( $file, '?' ) ) ); } /** * Sanitize and validate CSS files to exclude from the minification. * * @since 3.7 * * @param string $file filepath to sanitize. * @return string */ function rocket_validate_css( $file ) { if ( rocket_is_internal_file( $file ) ) { return rocket_sanitize_css( rocket_clean_exclude_file( trim( $file ) ) ); } return rocket_remove_url_protocol( esc_url_raw( strtok( $file, '?' ) ) ); } /** * Check if the passed value is an internal URL (default domain or CDN/Multilingual). * * @since 3.3.7 * * @param string $file string to test. * @return bool */ function rocket_is_internal_file( $file ) { $file_host = wp_parse_url( rocket_add_url_protocol( $file ), PHP_URL_HOST ); if ( empty( $file_host ) ) { return false; } /** * Filters the allowed hosts for optimization * * @since 3.4 * * @param array $hosts Allowed hosts. * @param array $zones Zones to check available hosts. */ $hosts = apply_filters( 'rocket_cdn_hosts', [], [ 'all', 'css_and_js', 'css', 'js' ] ); $hosts[] = wp_parse_url( content_url(), PHP_URL_HOST ); $langs = get_rocket_i18n_uri(); // Get host for all langs. if ( ! empty( $langs ) ) { foreach ( $langs as $lang ) { $hosts[] = wp_parse_url( $lang, PHP_URL_HOST ); } } $hosts = array_unique( $hosts ); if ( empty( $hosts ) ) { return false; } return in_array( $file_host, $hosts, true ); } /** * Sanitize a setting value meant for a textarea. * * @since 3.3.7 * * @param string $field The field’s name. Can be one of the following: * 'exclude_css', 'exclude_inline_js', 'exclude_js', 'cache_reject_uri', * 'cache_reject_ua', 'cache_purge_pages', 'cdn_reject_files'. * @param array|string $value The value to sanitize. * @return array|null */ function rocket_sanitize_textarea_field( $field, $value ) { $fields = [ 'cache_purge_pages' => [ 'esc_url', 'rocket_clean_exclude_file', 'rocket_clean_wildcards' ], // Pattern. 'cache_reject_cookies' => [ 'rocket_sanitize_key' ], 'cache_reject_ua' => [ 'rocket_sanitize_ua', 'rocket_clean_wildcards' ], // Pattern. 'cache_reject_uri' => [ 'esc_url', 'rocket_clean_exclude_file', 'rocket_clean_wildcards' ], // Pattern. 'cache_query_strings' => [ 'rocket_sanitize_key' ], 'cdn_reject_files' => [ 'rocket_clean_exclude_file', 'rocket_clean_wildcards' ], // Pattern. 'exclude_css' => [ 'rocket_validate_css', 'rocket_clean_wildcards' ], // Pattern. 'exclude_inline_js' => [ 'sanitize_text_field' ], 'exclude_defer_js' => [ 'sanitize_text_field' ], 'exclude_js' => [ 'rocket_validate_js', 'rocket_clean_wildcards' ], // Pattern. 'exclude_lazyload' => [ 'sanitize_text_field' ], 'delay_js_exclusions' => [ 'sanitize_text_field', 'rocket_clean_wildcards' ], 'remove_unused_css_safelist' => [ 'sanitize_text_field', 'rocket_clean_wildcards' ], 'preload_excluded_uri' => [ 'sanitize_text_field', 'rocket_clean_wildcards' ], ]; if ( ! isset( $fields[ $field ] ) ) { return null; } $sanitizations = $fields[ $field ]; if ( ! is_array( $value ) ) { $value = explode( "\n", $value ); } $value = array_map( 'trim', $value ); $value = array_filter( $value ); if ( ! $value ) { return []; } // Sanitize. foreach ( $sanitizations as $sanitization ) { $value = array_filter( array_map( $sanitization, $value ) ); } return array_unique( $value ); } /** * Used with array_filter to remove files without .xml extension * * @since 2.8 * @author Remy Perona * * @param string $file filepath to sanitize. * @return string|boolean filename or false if not xml */ function rocket_sanitize_xml( $file ) { $file = preg_replace( '#\?.*$#', '', $file ); $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ); return ( 'xml' === $ext ) ? trim( $file ) : false; } /** * Sanitizes a string key like the sanitize_key() WordPress function without forcing lowercase. * * @since 2.7 * * @param string $key Key string to sanitize. * @return string */ function rocket_sanitize_key( $key ) { $key = preg_replace( '/[^a-z0-9_\-]/i', '', $key ); return $key; } /** * Used to sanitize values of the "Never send cache pages for these user agents" option. * * @since 2.6.4 * * @param string $user_agent User Agent string. * @return string */ function rocket_sanitize_ua( $user_agent ) { $user_agent = preg_replace( '/[^a-z0-9._\(\)\*\-\/\s\x5c]/i', '', $user_agent ); return $user_agent; } /** * Get an url without HTTP protocol * * @since 1.3.0 * * @param string $url The URL to parse. * @param bool $no_dots (default: false). * @return string $url The URL without protocol */ function rocket_remove_url_protocol( $url, $no_dots = false ) { $url = preg_replace( '#^(https?:)?\/\/#im', '', $url ); /** This filter is documented in inc/front/htaccess.php */ if ( apply_filters( 'rocket_url_no_dots', $no_dots ) ) { $url = str_replace( '.', '_', $url ); } return $url; } /** * Add HTTP protocol to an url that does not have it. * * @since 2.2.1 * * @param string $url The URL to parse. * * @return string $url The URL with protocol. */ function rocket_add_url_protocol( $url ) { // Bail out if the URL starts with http:// or https://. if ( strpos( $url, 'http://' ) !== false || strpos( $url, 'https://' ) !== false ) { return $url; } if ( substr( $url, 0, 2 ) !== '//' ) { $url = '//' . $url; } return set_url_scheme( $url ); } /** * Set the scheme for a internal URL * * @since 2.6 * * @param string $url Absolute url that includes a scheme. * @return string $url URL with a scheme. */ function rocket_set_internal_url_scheme( $url ) { $tmp_url = set_url_scheme( $url ); if ( rocket_extract_url_component( $tmp_url, PHP_URL_HOST ) === rocket_extract_url_component( home_url(), PHP_URL_HOST ) ) { $url = $tmp_url; } return $url; } /** * Get the domain of an URL without subdomain * (ex: rocket_get_domain( 'http://www.geekpress.fr' ) return geekpress.fr * * @source : http://stackoverflow.com/a/15498686 * @since 2.7.3 undeprecated & updated * @since 1.0 * * @param string $url URL to parse. * @return string|bool Domain or false */ function rocket_get_domain( $url ) { // Add URL protocol if the $url doesn't have one to prevent issue with parse_url. $url = rocket_add_url_protocol( trim( $url ) ); $url_array = wp_parse_url( $url ); $host = $url_array['host']; /** * Filters the tld max range for edge cases * * @since 2.7.3 * * @param string Max range number */ $match = '/(?P<domain>[a-z0-9][a-z0-9\-]{1,63}\.[a-z\.]{2,' . apply_filters( 'rocket_get_domain_preg', '6' ) . '})$/i'; if ( preg_match( $match, $host, $regs ) ) { return $regs['domain']; } return false; } /** * Extract and return host, path, query and scheme of an URL * * @since 2.11.5 Supports UTF-8 URLs * @since 2.1 Add $query variable * @since 2.0 * * @param string $url The URL to parse. * @return array Components of an URL */ function get_rocket_parse_url( $url ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals if ( ! is_string( $url ) ) { return; } $encoded_url = preg_replace_callback( '%[^:/@?&=#]+%usD', function ( $matches ) { return rawurlencode( $matches[0] ); }, $url ); $url = wp_parse_url( $encoded_url ); $host = isset( $url['host'] ) ? strtolower( urldecode( $url['host'] ) ) : ''; $path = isset( $url['path'] ) ? urldecode( $url['path'] ) : ''; $scheme = isset( $url['scheme'] ) ? urldecode( $url['scheme'] ) : ''; $query = isset( $url['query'] ) ? urldecode( $url['query'] ) : ''; $fragment = isset( $url['fragment'] ) ? urldecode( $url['fragment'] ) : ''; /** * Filter components of an URL * * @since 2.2 * * @param array Components of an URL */ return (array) apply_filters( 'rocket_parse_url', [ 'host' => $host, 'path' => $path, 'scheme' => $scheme, 'query' => $query, 'fragment' => $fragment, ] ); } /** * Extract a component from an URL. * * @since 2.11 * @author Remy Perona * * @param string $url URL to parse and extract component of. * @param string $component URL component to extract using constant as in parse_url(). * @return string extracted component */ function rocket_extract_url_component( $url, $component ) { return _get_component_from_parsed_url_array( wp_parse_url( $url ), $component ); } /** * Returns realpath to file (used for relative path with /../ in it or not-yet existing file) * * @since 2.11 * @author Remy Perona * * @param string $file File to determine realpath for. * @return string Resolved file path */ function rocket_realpath( $file ) { $wrapper = null; // Strip the protocol. if ( rocket_is_stream( $file ) ) { list( $wrapper, $file ) = explode( '://', $file, 2 ); } $path = []; foreach ( explode( '/', $file ) as $part ) { if ( '' === $part || '.' === $part ) { continue; } if ( '..' !== $part ) { array_push( $path, $part ); } elseif ( count( $path ) > 0 ) { array_pop( $path ); } } $file = join( '/', $path ); if ( null !== $wrapper ) { return $wrapper . '://' . $file; } $prefix = 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) ? '' : '/'; return $prefix . $file; } /** * Converts an URL to an absolute path. * * @since 2.11.7 * @author Remy Perona * * @param string $url URL to convert. * @param array $zones Zones to check available hosts. * @return string|bool */ function rocket_url_to_path( $url, array $zones = [ 'all' ] ) { $wp_content_dir = rocket_get_constant( 'WP_CONTENT_DIR' ); $root_dir = trailingslashit( dirname( $wp_content_dir ) ); $content_url_host = wp_parse_url( content_url(), PHP_URL_HOST ); if ( null !== $content_url_host ) { $root_url = str_replace( wp_basename( $wp_content_dir ), '', content_url() ); } else { $root_url = site_url( '/' ); } $url_host = wp_parse_url( $url, PHP_URL_HOST ); // relative path. if ( null === $url_host ) { $subdir_levels = substr_count( preg_replace( '/https?:\/\//', '', site_url() ), '/' ); $url = trailingslashit( site_url() . str_repeat( '/..', $subdir_levels ) ) . ltrim( $url, '/' ); } /** * Filters the URL before converting it to a path * * @since 3.5.3 * @author Remy Perona * * @param string $url URL of the asset. * @param array $zones CDN zones corresponding to the current assets type. */ $url = apply_filters( 'rocket_asset_url', $url, $zones ); $url = rawurldecode( $url ); $root_url = preg_replace( '/^https?:/', '', $root_url ); $url = preg_replace( '/^https?:/', '', $url ); $file = str_replace( $root_url, $root_dir, $url ); $file = rocket_realpath( $file ); /** * Filters the absolute path to the asset file * * @since 3.3 * @author Remy Perona * * @param string $file Absolute path to the file. * @param string $url URL of the asset. */ $file = apply_filters( 'rocket_url_to_path', $file, $url ); if ( ! rocket_direct_filesystem()->is_readable( $file ) ) { return false; } return $file; } /** * Simple helper to get some external URLs. * * @since 2.10.10 * @author Grégory Viguier * * @param string $target What we want. * @param array $query_args An array of query arguments. * @return string The URL. */ function rocket_get_external_url( $target, $query_args = [] ) { $site_url = WP_ROCKET_WEB_MAIN; switch ( $target ) { case 'support': $locale = function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale(); $paths = [ 'default' => 'support', 'fr_FR' => 'fr/support', 'fr_CA' => 'fr/support', 'it_IT' => 'it/supporto', 'de_DE' => 'de/support', 'es_ES' => 'es/soporte', ]; $url = isset( $paths[ $locale ] ) ? $paths[ $locale ] : $paths['default']; $url = $site_url . $url . '/'; break; case 'account': $locale = function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale(); $paths = [ 'default' => 'account', 'fr_FR' => 'fr/compte', 'fr_CA' => 'fr/compte', 'it_IT' => 'it/account/', 'de_DE' => 'de/konto/', 'es_ES' => 'es/cuenta/', ]; $url = isset( $paths[ $locale ] ) ? $paths[ $locale ] : $paths['default']; $url = $site_url . $url . '/'; break; default: $url = $site_url; } if ( $query_args ) { $url = add_query_arg( $query_args, $url ); } return $url; } functions/options.php 0000644 00000052112 15174671745 0011002 0 ustar 00 <?php use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Logger\Logger; defined( 'ABSPATH' ) || exit; /** * A wrapper to easily get rocket option * * @since 3.0 Use the new options classes * @since 1.3.0 * * @param string $option The option name. * @param mixed $default (default: false) The default value of option. * @return mixed The option value */ function get_rocket_option( $option, $default = false ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $options_api = new Options( 'wp_rocket_' ); $options = new Options_Data( $options_api->get( 'settings', [] ) ); return $options->get( $option, $default ); } /** * Export settings into JSON. * * @return array */ function rocket_export_options() { $site_name = get_rocket_parse_url( get_home_url() ); $site_name = $site_name['host'] . $site_name['path']; $filename = sprintf( 'wp-rocket-settings-%s-%s-%s.json', $site_name, date( 'Y-m-d' ), uniqid() ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date return [ $filename, wp_json_encode( get_option( WP_ROCKET_SLUG ), JSON_PRETTY_PRINT ) ]; // do not use get_rocket_option() here. } /** * Update a WP Rocket option. * * @since 3.0 Use the new options classes * @since 2.7 * * @param string $key The option name. * @param mixed $value The value of the option. * @return void */ function update_rocket_option( $key, $value ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $options_api = new Options( 'wp_rocket_' ); $options = new Options_Data( $options_api->get( 'settings', [] ) ); $options->set( $key, $value ); $options_api->set( 'settings', $options->get_options() ); } /** * Check whether the plugin is active by checking the active_plugins list. * * @since 1.3.0 * * @source wp-admin/includes/plugin.php * * @param string $plugin Plugin folder/main file. * * @return boolean true when plugin is active; else false. */ function rocket_is_plugin_active( $plugin ) { return ( in_array( $plugin, (array) get_option( 'active_plugins', [] ), true ) || rocket_is_plugin_active_for_network( $plugin ) ); } /** * Check whether the plugin is active for the entire network. * * @since 1.3.0 * * @source wp-admin/includes/plugin.php * * @param string $plugin Plugin folder/main file. * * @return bool true if multisite and plugin is active for network; else, false. */ function rocket_is_plugin_active_for_network( $plugin ) { if ( ! is_multisite() ) { return false; } $plugins = get_site_option( 'active_sitewide_plugins' ); return isset( $plugins[ $plugin ] ); } /** * Is we need to exclude some specifics options on a post. * * @since 2.5 * * @param string $option The option name (lazyload, css, js, cdn). * @return bool True if the option is deactivated */ function is_rocket_post_excluded_option( $option ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals global $post; if ( ! is_object( $post ) ) { return false; } if ( is_home() ) { $post_id = get_queried_object_id(); } if ( is_singular() && isset( $post ) ) { $post_id = $post->ID; } return ( isset( $post_id ) ) ? get_post_meta( $post_id, '_rocket_exclude_' . $option, true ) : false; } /** * Check if we need to cache the mobile version of the website (if available) * * @since 1.0 * * @return bool True if option is activated */ function is_rocket_cache_mobile() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals return get_rocket_option( 'cache_mobile', false ); } /** * Check if we need to generate a different caching file for mobile (if available) * * @since 2.7 * * @return bool True if option is activated and if mobile caching is enabled */ function is_rocket_generate_caching_mobile_files() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals return get_rocket_option( 'cache_mobile', false ) && get_rocket_option( 'do_caching_mobile_files', false ); } /** * Get the domain names to DNS prefetch from WP Rocket options * * @since 2.8.9 * @author Remy Perona * * return Array An array of domain names to DNS prefetch */ function rocket_get_dns_prefetch_domains() { $domains = (array) get_rocket_option( 'dns_prefetch', [] ); /** * Filter list of domains to prefetch DNS * * @since 1.1.0 * * @param array $domains List of domains to prefetch DNS */ return apply_filters( 'rocket_dns_prefetch', $domains ); } /** * Gets the parameters ignored during caching * * These parameters are ignored when checking the query string during caching to allow serving the default cache when they are present * * @since 3.4 * * @return array */ function rocket_get_ignored_parameters() { /** * Filters the ignored parameters * * @since 3.4 * * @param array $params An array of ignored parameters as array keys. */ return apply_filters( 'rocket_cache_ignored_parameters', [] ); } /** * Get all uri we don't cache. * * @since 3.3.2 Exclude embedded URLs * @since 2.6 Using json_get_url_prefix() to auto-exclude the WordPress REST API. * @since 2.4.1 Auto-exclude WordPress REST API. * @since 2.0 * * @param bool $force Force the static uris to be reverted to null. * @param bool $show_safe_content show sensitive uris. * @return string A pipe separated list of rejected uri. */ function get_rocket_cache_reject_uri( $force = false, $show_safe_content = true ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals static $uris; global $wp_rewrite; if ( $force ) { $uris = null; } if ( $uris ) { return $uris; } $uris = (array) get_rocket_option( 'cache_reject_uri', [] ); $home_root = rocket_get_home_dirname(); $home_root_escaped = preg_quote( $home_root, '/' ); // The site is not at the domain root, it's in a folder. $home_root_len = strlen( $home_root ); if ( '' !== $home_root && $uris ) { foreach ( $uris as $i => $uri ) { /** * Since these URIs can be regex patterns like `/homeroot(/.+)/`, we can't simply search for the string `/homeroot/` (nor `/homeroot`). * So this pattern searches for `/homeroot/` and `/homeroot(/`. */ if ( ! preg_match( '/' . $home_root_escaped . '\(?\//', $uri ) ) { // Reject URIs located outside site's folder. unset( $uris[ $i ] ); continue; } // Remove the home directory. $uris[ $i ] = substr( $uri, $home_root_len ); } } // Exclude feeds. $uris[] = '/(?:.+/)?' . $wp_rewrite->feed_base . '(?:/(?:.+/?)?)?$'; // Exclude embedded URLs. $uris[] = '/(?:.+/)?embed/'; /** * Filter the rejected uri * * @since 2.1 * * @param array $uris List of rejected uri * @param bool $show_safe_content show sensitive uris. */ $uris = apply_filters( 'rocket_cache_reject_uri', $uris, $show_safe_content ); $uris = array_filter( $uris ); if ( ! $uris ) { return ''; } $uris = array_map( function ( $uri ) { // Sanitize URIs and remove single quote from them to avoid syntax errors in .htaccess and php config file. return str_replace( "'", '', esc_url_raw( $uri ) ); }, $uris ); if ( '' !== $home_root ) { foreach ( $uris as $i => $uri ) { if ( preg_match( '/' . $home_root_escaped . '\(?\//', $uri ) ) { // Remove the home directory from the new URIs. $uris[ $i ] = substr( $uri, $home_root_len ); } } } $uris = implode( '|', $uris ); if ( '' !== $home_root ) { // Add the home directory back. $uris = $home_root . '(' . $uris . ')'; } return $uris; } /** * Get all cookie names we don't cache. * * @since 2.0 * * @return string A pipe separated list of rejected cookies. */ function get_rocket_cache_reject_cookies() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $logged_in_cookie = explode( COOKIEHASH, LOGGED_IN_COOKIE ); $logged_in_cookie = array_map( 'preg_quote', $logged_in_cookie ); $logged_in_cookie = implode( '.+', $logged_in_cookie ); $cookies = get_rocket_option( 'cache_reject_cookies', [] ); $cookies[] = $logged_in_cookie; $cookies[] = 'wp-postpass_'; $cookies[] = 'wptouch_switch_toggle'; $cookies[] = 'comment_author_'; $cookies[] = 'comment_author_email_'; /** * Filter the rejected cookies. * * @since 2.1 * * @param array $cookies List of rejected cookies. */ $cookies = (array) apply_filters( 'rocket_cache_reject_cookies', $cookies ); $cookies = array_filter( $cookies ); $cookies = array_flip( array_flip( $cookies ) ); return implode( '|', $cookies ); } /** * Get list of mandatory cookies to be able to cache pages. * * @since 2.7 * * @return string A pipe separated list of mandatory cookies. */ function get_rocket_cache_mandatory_cookies() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $cookies = []; /** * Filter list of mandatory cookies. * * @since 2.7 * * @param array $cookies List of mandatory cookies. */ $cookies = (array) apply_filters( 'rocket_cache_mandatory_cookies', $cookies ); $cookies = array_filter( $cookies ); $cookies = array_flip( array_flip( $cookies ) ); return implode( '|', $cookies ); } /** * Get list of dynamic cookies. * * @since 2.7 * * @return array List of dynamic cookies. */ function get_rocket_cache_dynamic_cookies() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $cookies = []; /** * Filter list of dynamic cookies. * * @since 2.7 * * @param array $cookies List of dynamic cookies. */ $cookies = (array) apply_filters( 'rocket_cache_dynamic_cookies', $cookies ); $cookies = array_filter( $cookies ); $cookies = array_unique( $cookies ); return $cookies; } /** * Get all User-Agent we don't allow to get cache files. * * @since 2.3.5 * * @return string A pipe separated list of rejected User-Agent. */ function get_rocket_cache_reject_ua() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $ua = get_rocket_option( 'cache_reject_ua', [] ); $ua[] = 'facebookexternalhit'; $ua[] = 'WhatsApp'; /** * Filter the rejected User-Agent * * @since 2.3.5 * * @param array $ua List of rejected User-Agent. */ $ua = (array) apply_filters( 'rocket_cache_reject_ua', $ua ); $ua = array_filter( $ua ); $ua = array_flip( array_flip( $ua ) ); $ua = implode( '|', $ua ); return str_replace( [ ' ', '\\\\ ' ], '\\ ', $ua ); } /** * Get all query strings which can be cached. * * @since 2.3 * * @return array List of query strings which can be cached. */ function get_rocket_cache_query_string() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $query_strings = get_rocket_option( 'cache_query_strings', [] ); /** * Filter query strings which can be cached. * * @since 2.3 * * @param array $query_strings List of query strings which can be cached. */ $query_strings = (array) apply_filters( 'rocket_cache_query_strings', $query_strings ); $query_strings = array_filter( $query_strings ); $query_strings = array_flip( array_flip( $query_strings ) ); return $query_strings; } /** * Determine if the key is valid * * @since 2.9 use hash_equals() to compare the hash values * @since 1.0 * * @return bool true if everything is ok, false otherwise */ function rocket_valid_key() { $rocket_secret_key = (string) get_rocket_option( 'secret_key', '' ); if ( ! $rocket_secret_key ) { return false; } $valid_details = 8 === strlen( (string) get_rocket_option( 'consumer_key', '' ) ) && hash_equals( $rocket_secret_key, hash( 'crc32', get_rocket_option( 'consumer_email', '' ) ) ); if ( ! $valid_details ) { set_transient( 'rocket_check_key_errors', [ __( 'The provided license data are not valid.', 'rocket' ) . ' <br>' . // Translators: %1$s = opening link tag, %2$s = closing link tag. sprintf( __( 'To resolve, please %1$scontact support%2$s.', 'rocket' ), '<a href="https://wp-rocket.me/support/" rel="noopener noreferrer" target=_"blank">', '</a>' ), ] ); return $valid_details; } return $valid_details; } /** * Determine if the key is valid. * * @since 2.9.7 Remove arguments ($type & $data). * @since 2.9.7 Stop to auto-check the validation each 1 & 30 days. * @since 2.2 The function do the live check and update the option. * * @return bool|array */ function rocket_check_key() { // Recheck the license. $return = rocket_valid_key(); if ( $return ) { rocket_delete_licence_data_file(); return $return; } Logger::info( 'LICENSE VALIDATION PROCESS STARTED.', [ 'license validation process' ] ); $response = wp_remote_get( 'https://api.wp-rocket.me/valid_key.php', [ 'timeout' => 30, ] ); if ( is_wp_error( $response ) ) { Logger::error( 'License validation failed.', [ 'license validation process', 'request_error' => $response->get_error_messages(), ] ); set_transient( 'rocket_check_key_errors', $response->get_error_messages() ); return $return; } $body = wp_remote_retrieve_body( $response ); $json = json_decode( $body ); if ( null === $json ) { if ( '' === $body ) { Logger::error( 'License validation failed. No body available in response.', [ 'license validation process' ] ); // Translators: %1$s = opening em tag, %2$s = closing em tag, %3$s = opening link tag, %4$s closing link tag. $message = __( 'License validation failed. Our server could not resolve the request from your website.', 'rocket' ) . '<br>' . sprintf( __( 'Try clicking %1$sValidate License%2$s below. If the error persists, follow %3$sthis guide%4$s.', 'rocket' ), '<em>', '</em>', '<a href="https://docs.wp-rocket.me/article/100-resolving-problems-with-license-validation#general">', '</a>' ); set_transient( 'rocket_check_key_errors', [ $message ] ); return $return; } Logger::error( 'License validation failed.', [ 'license validation process', 'response_body' => $body, ] ); if ( 'NULLED' === $body ) { // Translators: %1$s = opening link tag, %2$s = closing link tag. $message = __( 'License validation failed. You may be using a nulled version of the plugin. Please do the following:', 'rocket' ) . '<ul><li>' . sprintf( __( 'Login to your WP Rocket %1$saccount%2$s', 'rocket' ), '<a href="https://wp-rocket.me/account/" rel="noopener noreferrer" target=_"blank">', '</a>' ) . '</li><li>' . __( 'Download the zip file', 'rocket' ) . '<li></li>' . __( 'Reinstall', 'rocket' ) . '</li></ul>' . sprintf( __( 'If you do not have a WP Rocket account, please %1$spurchase a license%2$s.', 'rocket' ), '<a href="https://wp-rocket.me/" rel="noopener noreferrer" target="_blank">', '</a>' ); set_transient( 'rocket_check_key_errors', [ $message ] ); return $return; } if ( 'BAD_USER' === $body ) { // Translators: %1$s = opening link tag, %2$s = closing link tag. $message = __( 'License validation failed. This user account does not exist in our database.', 'rocket' ) . '<br>' . sprintf( __( 'To resolve, please contact support.', 'rocket' ), '<a href="https://wp-rocket.me/support/" rel="noopener noreferrer" target=_"blank">', '</a>' ); set_transient( 'rocket_check_key_errors', [ $message ] ); return $return; } if ( 'USER_BLOCKED' === $body ) { // Translators: %1$s = opening link tag, %2$s = closing link tag. $message = __( 'License validation failed. This user account is blocked.', 'rocket' ) . '<br>' . sprintf( __( 'Please see %1$sthis guide%2$s for more info.', 'rocket' ), '<a href="https://docs.wp-rocket.me/article/100-resolving-problems-with-license-validation#errors" rel="noopener noreferrer" target=_"blank">', '</a>' ); set_transient( 'rocket_check_key_errors', [ $message ] ); return $return; } // Translators: %1$s = opening em tag, %2$s = closing em tag, %3$s = opening link tag, %4$s closing link tag. $message = __( 'License validation failed. Our server could not resolve the request from your website.', 'rocket' ) . '<br>' . sprintf( __( 'Try clicking %1$sSave Changes%2$s below. If the error persists, follow %3$sthis guide%4$s.', 'rocket' ), '<em>', '</em>', '<a href="https://docs.wp-rocket.me/article/100-resolving-problems-with-license-validation#general" rel="noopener noreferrer" target=_"blank">', '</a>' ); set_transient( 'rocket_check_key_errors', [ $message ] ); return $return; } $rocket_options = []; $rocket_options['consumer_key'] = $json->data->consumer_key; $rocket_options['consumer_email'] = $json->data->consumer_email; if ( ! $json->success ) { $messages = [ // Translators: %1$s = opening link tag, %2$s = closing link tag. 'BAD_LICENSE' => __( 'Your license is not valid.', 'rocket' ) . '<br>' . sprintf( __( 'Make sure you have an active %1$sWP Rocket license%2$s.', 'rocket' ), '<a href="https://wp-rocket.me/" rel="noopener noreferrer" target="_blank">', '</a>' ), // Translators: %1$s = opening link tag, %2$s = closing link tag, %3$s = opening link tag. 'BAD_NUMBER' => __( 'You have added as many sites as your current license allows.', 'rocket' ) . '<br>' . sprintf( __( 'Upgrade your %1$saccount%2$s or %3$stransfer your license%2$s to this domain.', 'rocket' ), '<a href="https://wp-rocket.me/account/" rel="noopener noreferrer" target=_"blank">', '</a>', '<a href="https://docs.wp-rocket.me/article/28-transfering-your-license-to-another-site" rel="noopener noreferrer" target=_"blank">' ), // Translators: %1$s = opening link tag, %2$s = closing link tag. 'BAD_SITE' => __( 'This website is not allowed.', 'rocket' ) . '<br>' . sprintf( __( 'Please %1$scontact support%2$s.', 'rocket' ), '<a href="https://wp-rocket.me/support/" rel="noopener noreferrer" target=_"blank">', '</a>' ), // Translators: %1$s = opening link tag, %2$s = closing link tag. 'BAD_KEY' => __( 'This license key is not recognized.', 'rocket' ) . '<ul><li>' . sprintf( __( 'Login to your WP Rocket %1$saccount%2$s', 'rocket' ), '<a href="https://wp-rocket.me/account/" rel="noopener noreferrer" target=_"blank">', '</a>' ) . '</li><li>' . __( 'Download the zip file', 'rocket' ) . '<li></li>' . __( 'Reinstall', 'rocket' ) . '</li></ul>' . sprintf( __( 'If the issue persists, please %1$scontact support%2$s.', 'rocket' ), '<a href="https://wp-rocket.me/support/" rel="noopener noreferrer" target=_"blank">', '</a>' ), ]; $rocket_options['secret_key'] = ''; // Translators: %s = error message returned. set_transient( 'rocket_check_key_errors', [ sprintf( __( 'License validation failed: %s', 'rocket' ), $messages[ $json->data->reason ] ) ] ); Logger::error( 'License validation failed.', [ 'license validation process', 'response_error' => $json->data->reason, ] ); set_transient( rocket_get_constant( 'WP_ROCKET_SLUG' ), $rocket_options ); return $rocket_options; } $rocket_options['secret_key'] = $json->data->secret_key; if ( ! get_rocket_option( 'license' ) ) { $rocket_options['license'] = '1'; } Logger::info( 'License validation successful.', [ 'license validation process' ] ); set_transient( rocket_get_constant( 'WP_ROCKET_SLUG' ), $rocket_options ); delete_transient( 'rocket_check_key_errors' ); rocket_delete_licence_data_file(); update_option( 'wp_rocket_no_licence', 0 ); return $rocket_options; } /** * Deletes the licence-data.php file if it exists * * @since 3.5 * @author Remy Perona * * @return void */ function rocket_delete_licence_data_file() { if ( is_multisite() ) { return; } $rocket_path = rocket_get_constant( 'WP_ROCKET_PATH' ); if ( ! rocket_direct_filesystem()->exists( $rocket_path . 'licence-data.php' ) ) { return; } rocket_direct_filesystem()->delete( $rocket_path . 'licence-data.php' ); } /** * Is WP a MultiSite and a subfolder install? * * @since 3.1.1 * @author Grégory Viguier * * @return bool */ function rocket_is_subfolder_install() { global $wpdb; static $subfolder_install; if ( isset( $subfolder_install ) ) { return $subfolder_install; } if ( is_multisite() ) { $subfolder_install = ! is_subdomain_install(); } elseif ( ! is_null( $wpdb->sitemeta ) ) { $subfolder_install = ! $wpdb->get_var( "SELECT meta_value FROM $wpdb->sitemeta WHERE site_id = 1 AND meta_key = 'subdomain_install'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } else { $subfolder_install = false; } return $subfolder_install; } /** * Get the name of the "home directory", in case the home URL is not at the domain's root. * It can be seen like the `RewriteBase` from the .htaccess file, but without the trailing slash. * * @since 3.1.1 * @author Grégory Viguier * * @return string */ function rocket_get_home_dirname() { static $home_root; if ( isset( $home_root ) ) { return $home_root; } $home_root = wp_parse_url( rocket_get_main_home_url() ); if ( ! empty( $home_root['path'] ) ) { $home_root = '/' . trim( $home_root['path'], '/' ); $home_root = rtrim( $home_root, '/' ); } else { $home_root = ''; } return $home_root; } /** * Get the URL of the site's root. It corresponds to the main site's home page URL. * * @since 3.1.1 * @author Grégory Viguier * * @return string */ function rocket_get_main_home_url() { static $root_url; if ( isset( $root_url ) ) { return $root_url; } if ( ! is_multisite() || is_main_site() ) { $root_url = rocket_get_home_url( '/' ); return $root_url; } $current_network = get_network(); if ( $current_network ) { $root_url = set_url_scheme( 'https://' . $current_network->domain . $current_network->path ); $root_url = trailingslashit( $root_url ); } else { $root_url = rocket_get_home_url( '/' ); } return $root_url; } deprecated/deprecated.php 0000644 00000321371 15174671745 0011505 0 ustar 00 <?php use WP_Rocket\Dependencies\Minify; defined( 'ABSPATH' ) || exit; /** * Deprecated functions come here to die. */ if ( ! function_exists( 'rocket_replace_domain_mapping_siteurl' ) ) : /** * Get Domain Mapping host based on original URL * * @since 2.2 * @deprecated 2.6.5 * * @param string $url Original URL. */ function rocket_replace_domain_mapping_siteurl( $url = null ) { _deprecated_function( __FUNCTION__, '2.6.5' ); return false; } endif; if ( ! function_exists( 'rocket_sanitize_cookie' ) ) : /** * Used to sanitize values of the "Don't cache pages that use the following cookies" option. * * @since 2.6.4 * @deprecated 2.7 * @deprecated Use rocket_sanitize_key() * * @param string $cookie Cookie value to sanitize. */ function rocket_sanitize_cookie( $cookie ) { _deprecated_function( __FUNCTION__, '2.7', 'rocket_sanitize_key()' ); return rocket_sanitize_key( $cookie ); } endif; if ( ! function_exists( 'set_rocket_cloudflare_async' ) ) : /** * Used to set the CloudFlare Rocket Loader value * * @since 2.5 * @deprecated 2.8.16 * @deprecated Use set_rocket_cloudflare_rocket_loader() * * @param string $cf_rocket_loader Value for the Rocket Loader. */ function set_rocket_cloudflare_async( $cf_rocket_loader ) { _deprecated_function( __FUNCTION__, '2.8.16', 'set_rocket_cloudflare_rocket_loader()' ); } endif; if ( ! function_exists( 'set_rocket_cloudflare_cache_lvl' ) ) : /** * Used to set the CloudFlare cache level * * @since 2.5 * @deprecated 2.8.16 * @deprecated Use set_rocket_cloudflare_cache_level() * * @param string $cf_cache_level Value for the cache level. */ function set_rocket_cloudflare_cache_lvl( $cf_cache_level ) { _deprecated_function( __FUNCTION__, '2.8.16', 'set_rocket_cloudflare_cache_level()' ); } endif; if ( ! function_exists( 'rocket_delete_script_wp_version' ) ) : /** * Used to remove version query string in CSS/JS URL * * @since 1.1.6 * @deprecated 2.9 * @deprecated Use rocket_browser_cache_busting() * * @param string $src Source URL for the JS/CSS. */ function rocket_delete_script_wp_version( $src ) { _deprecated_function( __FUNCTION__, '2.9', 'rocket_browser_cache_busting()' ); return rocket_browser_cache_busting( $src ); } endif; if ( ! function_exists( 'rocket_exclude_deferred_js' ) ) : /** * Used to remove deferred JS files from the buffer * * @since 1.1.0 * @deprecated 2.10 * @deprecated Use rocket_insert_deferred_js() * * @param string $buffer HTML code. */ function rocket_exclude_deferred_js( $buffer ) { _deprecated_function( __FUNCTION__, '2.10', 'rocket_insert_deferred_js()' ); return rocket_insert_deferred_js( $buffer ); } endif; if ( ! function_exists( 'is_rocket_cache_feed' ) ) : /** * Check if we need to cache the feeds of the website * * @since 2.7 * @deprecated 2.10 * * @return bool True if option is activated */ function is_rocket_cache_feed() { _deprecated_function( __FUNCTION__, '2.10' ); return get_rocket_option( 'cache_feed', false ); } endif; if ( ! function_exists( 'rocket_exclude_js_buddypress' ) ) : /** * Excludes BuddyPress's plupload from JS minification * * Exclude it to prevent an error after minification/concatenation * preventing the image upload from working correctly * * @since 2.8.10 * @deprecated 2.10.7 * @author Remy Perona * * @param Array $excluded_handle An array of JS handles enqueued in WordPress. * @return Array the updated array of handles */ function rocket_exclude_js_buddypress( $excluded_handle ) { _deprecated_function( __FUNCTION__, '2.10.7' ); return $excluded_handle; } endif; if ( ! function_exists( 'get_rocket_logins_exclude_pages' ) ) : /** * Get hide login pages to automatically exclude them to the cache. * * @since 2.6 * @deprecated 2.11 * * @return array $urls */ function get_rocket_logins_exclude_pages() { _deprecated_function( __FUNCTION__, '2.11' ); return array(); } endif; if ( ! function_exists( 'get_rocket_ecommerce_exclude_pages' ) ) : /** * Get cart & checkout path with their translations to automatically exclude them to the cache. * * @since 2.4 * @deprecated 2.11 * * @return array $urls */ function get_rocket_ecommerce_exclude_pages() { _deprecated_function( __FUNCTION__, '2.11' ); return array(); } endif; /** * Get list of JS files to deferred. * * @since 2.6 * @deprecated 2.11 * * @return array List of JS files. */ function get_rocket_deferred_js_files() { _deprecated_function( __FUNCTION__, '2.11' ); /** * Filter list of Deferred JavaScript files * * @since 1.1.0 * * @param array List of Deferred JavaScript files */ $deferred_js_files = apply_filters( 'rocket_minify_deferred_js', get_rocket_option( 'deferred_js_files', array() ) ); return $deferred_js_files; } /** * Add defer attribute to script that should be deferred * * @since 2.10 Use defer attribute instead of labJS * @since 1.1.0 * @deprecated 2.11 * * @param string $buffer HTML content in the buffer. * @return string Updated HTML content */ function rocket_insert_deferred_js( $buffer ) { _deprecated_function( __FUNCTION__, '2.11', 'rocket_defer_js()' ); if ( get_rocket_option( 'defer_all_js' ) ) { return $buffer; } // Get all JS files with this regex. preg_match_all( '#<script.*src=[\'|"]([^\'|"]+\.js?.+)[\'|"].*></script>#iU', $buffer, $tags_match ); if ( ! isset( $tags_match[0] ) ) { return $buffer; } foreach ( $tags_match[0] as $i => $tag ) { // Strip query args. $url = strtok( $tags_match[1][ $i ], '?' ); $deferred_js_files = array_flip( get_rocket_deferred_js_files() ); // Check if this file should be deferred. if ( isset( $deferred_js_files[ $url ] ) ) { $deferred_tag = str_replace( '></script>', ' defer></script>', $tag ); $buffer = str_replace( $tag, $deferred_tag, $buffer ); } } return $buffer; } /** * Used to display the deferred module on settings form * * @since 1.1.0 * @deprecated 2.11 */ function rocket_defered_module() { _deprecated_function( __FUNCTION__, '2.11' ); ?> <fieldset> <legend class="screen-reader-text"><span><?php _e( '<strong>JS</strong> files with Deferred Loading JavaScript', 'rocket' ); ?></span></legend> <div id="rkt-drop-deferred" class="rkt-module rkt-module-drop"> <?php $deferred_js_files = get_rocket_option( 'deferred_js_files' ); if ( $deferred_js_files ) { foreach ( $deferred_js_files as $k => $_url ) { ?> <p class="rkt-module-drag"> <span class="dashicons dashicons-sort rkt-module-move hide-if-no-js"></span> <input style="width: 32em" type="text" placeholder="http://" class="deferred_js regular-text" name="wp_rocket_settings[deferred_js_files][<?php echo $k; ?>]" value="<?php echo esc_url( $_url ); ?>" /> <span class="dashicons dashicons-no rkt-module-remove hide-if-no-js"></span> </p> <!-- .rkt-module-drag --> <?php } } else { // If no files yet, use this template inside #rkt-drop-deferred. ?> <p class="rkt-module-drag"> <span class="dashicons dashicons-sort rkt-module-move hide-if-no-js"></span> <input style="width: 32em" type="text" placeholder="http://" class="deferred_js regular-text" name="wp_rocket_settings[deferred_js_files][0]" value="" /> </p> <!-- .rkt-module-drag --> <?php } ?> </div> <!-- .rkt-drop-deferred --> <div class="rkt-module-model hide-if-js"> <p class="rkt-module-drag"> <span class="dashicons dashicons-sort rkt-module-move hide-if-no-js"></span> <input style="width: 32em" type="text" placeholder="http://" class="deferred_js regular-text" name="wp_rocket_settings[deferred_js_files][]" value="" /> <span class="dashicons dashicons-no rkt-module-remove hide-if-no-js"></span> </p> <!-- .rkt-module-drag --> </div> <!-- .rkt-model-deferred--> <p><a href="javascript:void(0)" class="rkt-module-clone hide-if-no-js button-secondary"><?php _e( 'Add URL', 'rocket' ); ?></a></p> <?php } /** * Check if minify cache file exist and create it if not * * @since 2.10 Use wp_safe_remote_get() instead of curl * @since 2.1 * @deprecated 2.11 * * @param string $url The minified URL with Minify Library. * @param string $pretty_url The minified URL cache file. * @return bool True if successfully saved the minify cache file, false otherwise */ function rocket_fetch_and_cache_minify( $url, $pretty_url ) { _deprecated_function( __FUNCTION__, '2.11', 'rocket_minify()' ); return false; } /** * Minify a file and return the URL * * @since 2.10 * @deprecated 2.11 * * @param string $file File to minify. * @param bool $force_pretty_url (default: true). * @param string $pretty_filename (default: null) The new filename if $force_pretty_url set to true. * @return string URL of the minified file */ function get_rocket_minify_file( $file, $force_pretty_url = true, $pretty_filename = null ) { _deprecated_function( __FUNCTION__, '2.11', 'get_rocket_minify_url()' ); return $file; } /** * Get tag of a group of files or JS minified CSS * * @since 2.1 * @deprecated 2.11 * * @param array $files List of files to minify (CSS or JS). * @param bool $force_pretty_url (default: true). * @param string $pretty_filename (default: null) The new filename if $force_pretty_url set to true. * @return string $tags HTML tags for the minified CSS/JS files */ function get_rocket_minify_files( $files, $force_pretty_url = true, $pretty_filename = null ) { _deprecated_function( __FUNCTION__, '2.11', 'get_rocket_minify_url()' ); return false; } /** * Used to minify and concat CSS files * * @since 1.1.0 Fix Bug with externals URLs like //ajax.google.com * @since 1.0.2 Remove the filter, remove the array_chunk, add an automatic way to cut strings to 255c max * @since 1.0 * @deprecated 2.11 * * @param string $buffer HTML content. * @return string Updated HTML content */ function rocket_minify_css( $buffer ) { _deprecated_function( __FUNCTION__, '2.11', 'rocket_minify_files()' ); return rocket_minify_files( $buffer, 'css' ); } /** * Used to minify and concat JavaScript files * * @since 1.1.0 Fix Bug with externals URLs like //ajax.google.com * @since 1.0.2 Remove the filter, remove the array_chunk, add an automatic way to cut strings to 255c max * @since 1.0 * @deprecated 2.11 * * @param string $buffer HTML content. * @return string Updated HTML content */ function rocket_minify_js( $buffer ) { _deprecated_function( __FUNCTION__, '2.11', 'rocket_minify_files()' ); return rocket_minify_files( $buffer, 'js' ); } /** * Minify CSS/JS files without concatenation. * * @since 2.10 * @deprecated 2.11 * @author Remy Perona * * @param string $buffer HTML code to parse. * @param string $extension css or js. * @return string Updated HTML code */ function rocket_minify_only( $buffer, $extension ) { _deprecated_function( __FUNCTION__, '2.11', 'rocket_minify_files()' ); return rocket_minify_files( $buffer, $extension ); } /** * Get all CSS files to exclude to the minification. * * @since 2.6 * @deprecated 2.11 * * @return array List of excluded CSS files. */ function get_rocket_exclude_css() { _deprecated_function( __FUNCTION__, '2.11', 'get_rocket_exclude_files()' ); return get_rocket_exclude_files( 'css' ); } /** * Get all JS files to exclude to the minification. * * @since 2.6 * @deprecated 2.11 * * @return array List of excluded JS files. */ function get_rocket_exclude_js() { _deprecated_function( __FUNCTION__, '2.11', 'get_rocket_exclude_files()' ); return get_rocket_exclude_files( 'js' ); } /** * Get all JS files to move in the footer during the minification. * * @since 2.6 * @deprecated 2.11 * * @return array List of JS files. */ function get_rocket_minify_js_in_footer() { _deprecated_function( __FUNCTION__, '2.11' ); return array(); } /** * Extract all enqueued CSS files which should be exclude to the minification * * @since 2.6 * @deprecated 2.11 */ function rocket_extract_excluded_css_files() { _deprecated_function( __FUNCTION__, '2.11' ); return false; } /** * Extract all enqueued JS files which should be exclude to the minification * * @since 2.6.1 * @deprecated 2.11 */ function rocket_extract_excluded_js_files() { _deprecated_function( __FUNCTION__, '2.11' ); return false; } /** * Extract all enqueued JS files which should be insert in the footer * * @since 2.10 * @since 2.6 * @deprecated 2.11 */ function rocket_extract_js_files_from_footer() { _deprecated_function( __FUNCTION__, '2.11' ); return false; } /** * Insert JS minify files in footer * * @since 2.2 */ function rocket_insert_minify_js_in_footer() { _deprecated_function( __FUNCTION__, '2.11' ); return false; } /** * Compatibility with WordPress multisite with subfolders websites * * @since 2.6.5 * @deprecated 2.11 * * @param string $url minified file URL. * @return string Updated minified file URL */ function rocket_fix_minify_multisite_path_issue( $url ) { _deprecated_function( __FUNCTION__, '2.11' ); return $url; } /** * Force the minification to create only 1 file. * * @param int $length maximum URL length. * @param string $ext file extension. * @return int Updated length */ function rocket_force_minify_combine_all( $length, $ext ) { _deprecated_function( __FUNCTION__, '2.11' ); if ( 'css' === $ext && get_rocket_option( 'minify_css_combine_all', false ) ) { $length = PHP_INT_MAX; } if ( 'js' === $ext && get_rocket_option( 'minify_js_combine_all', false ) ) { $length = PHP_INT_MAX; } return $length; } /** * Add some CSS to display the dismiss cross * * @since 1.1.10 * @deprecated 2.11 */ function rocket_admin_print_styles() { _deprecated_function( __FUNCTION__, '2.11' ); } /** * Optimizes the database depending on the option * * @since 2.8 * @deprecated 2.11 * @see Rocket_Background_Database_Optimisation->task() * @author Remy Perona * * @param string $type Type of optimization to perform. */ function rocket_database_optimize( $type ) { _deprecated_function( __FUNCTION__, '2.11', 'Rocket_Background_Database_Optimisation->task()' ); } /** * Launches the database optimization from admin * * @since 2.8 * @deprecated 2.11 * @see Rocket_Database_Optimisation->optimize() * @author Remy Perona */ function rocket_optimize_database() { _deprecated_function( __FUNCTION__, '2.11', 'Rocket_Database_Optimisation->optimize()' ); } /** * Count the number of items concerned by the database cleanup * * @since 2.8 * @deprecated 2.11 * @see Rocket_Database_Optimisation->count_cleanup_items() * @author Remy Perona * * @param string $type Item type to count. * @return int Number of items for this type */ function rocket_database_count_cleanup_items( $type ) { _deprecated_function( __FUNCTION__, '2.11', 'Rocket_Database_Optimisation->count_cleanup_items()' ); return 0; } /** * Planning database optimization cron * If the task is not programmed, it is automatically triggered * * @since 2.8 * @deprecated 2.11 * @see Rocket_Database_Optimisation->database_optimization_scheduled() * @author Remy Perona */ function rocket_database_optimization_scheduled() { _deprecated_function( __FUNCTION__, '2.11', 'Rocket_Database_Optimisation->database_optimization_scheduled()' ); } /** * Performs the database optimization * * @since 2.8 * @deprecated 2.11 * @see Rocket_Database_Optimisation->process_handler() * @author Remy Perona */ function do_rocket_database_optimization() { _deprecated_function( __FUNCTION__, '2.11', 'Rocket_Database_Optimisation->process_handler()' ); } if ( ! function_exists( 'rocket_define_donotminify_constants' ) ) { /** * Declare and set value to DONOTMINIFYCSS & DONOTMINIFYJS constant * * @since 2.6.2 * @deprecated 2.11 * @see rocket_define_donotoptimize_constant() * * @param bool $value true or false. */ function rocket_define_donotminify_constants( $value ) { _deprecated_function( __FUNCTION__, '2.11', 'rocket_define_donotoptimize_constant' ); if ( ! defined( 'DONOTMINIFYCSS' ) ) { define( 'DONOTMINIFYCSS', (bool) $value ); } if ( ! defined( 'DONOTMINIFYJS' ) ) { define( 'DONOTMINIFYJS', (bool) $value ); } } } if ( ! function_exists( 'rocket_define_donotasync_css_constant' ) ) { /** * Declare and set value to DONOTMASYNCCSS constant * * @since 2.10 * @deprecated 2.11 * @see rocket_define_donotoptimize_constant() * @author Remy Perona * * @param bool $value true or false. */ function rocket_define_donotasync_css_constant( $value ) { _deprecated_function( __FUNCTION__, '2.11', 'rocket_define_donotoptimize_constant' ); if ( ! defined( 'DONOTASYNCCSS' ) ) { define( 'DONOTASYNCCSS', (bool) $value ); } } } /** * Defer loading of CSS files * * @since 2.10 * @deprecated 2.11 * @see Rocket_Critical_CSS->async_css() * @author Remy Perona * * @param string $buffer HTML code. * @return string Updated HTML code */ function rocket_async_css( $buffer ) { _deprecated_function( __FUNCTION__, '2.11', 'Rocket_Critical_CSS->async_css()' ); return $buffer; } /** * Insert critical CSS in the <head> * * @since 2.10 * @deprecated 2.11 * @see Rocket_Critical_CSS->insert_critical_css() * @author Remy Perona */ function rocket_insert_critical_css() { _deprecated_function( __FUNCTION__, '2.11', 'Rocket_Critical_CSS->insert_critical_css()' ); } /** * Insert loadCSS script in <head> * * @since 2.10 * @deprecated 2.11 * @see WP_Rocket\Engine\CriticalPath\CriticalCSSSubscriber->insert_load_css() * @author Remy Perona */ function rocket_insert_load_css() { _deprecated_function( __FUNCTION__, '2.11', 'WP_Rocket\Engine\CriticalPath\CriticalCSSSubscriber->insert_load_css()' ); } if ( ! function_exists( 'rocket_lazyload_async_script' ) ) { /** * Add tags to the lazyload script to async and prevent concatenation * * @since 2.11 * @deprecated 2.11.2 * @author Remy Perona * * @param string $tag HTML for the script. * @param string $handle Handle for the script. * * @return string Updated HTML */ function rocket_lazyload_async_script( $tag, $handle ) { _deprecated_function( __FUNCTION__, '2.11.2' ); return $tag; } } if ( ! function_exists( 'is_rocket_cdn_on_ssl' ) ) { /** * Check if we need to disable CDN on SSL pages * * @since 2.5 * @deprecated 3.0 * * @return bool True if option is activated */ function is_rocket_cdn_on_ssl() { _deprecated_function( __FUNCTION__, '3.0' ); return is_ssl() && get_rocket_option( 'cdn_ssl', 0 ) ? false : true; } } if ( ! function_exists( 'is_rocket_cache_ssl' ) ) { /** * Check if we need to cache SSL requests of the website (if available) * * @since 1.0 * @deprecated 3.0 * * @return bool True if option is activated */ function is_rocket_cache_ssl() { _deprecated_function( __FUNCTION__, '3.0' ); return false; } } if ( ! function_exists( 'rocket_reset_white_label_values_action' ) ) { /** * Reset White Label values to WP Rocket default values * * @since 2.1 * @deprecated 3.0 */ function rocket_reset_white_label_values_action() { _deprecated_function( __FUNCTION__, '3.0' ); if ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], 'rocket_resetwl' ) ) { rocket_reset_white_label_values( true ); } wp_safe_redirect( add_query_arg( 'page', 'wprocket', remove_query_arg( 'page', wp_get_referer() ) ) ); die(); } } if ( ! function_exists( 'rocket_white_label' ) ) { /** * White Label the plugin, if you need to * * @since 2.1 * @deprecated 3.0 * * @param array $plugins An array of plugins installed. * @return array Updated array of plugins installed */ function rocket_white_label( $plugins ) { _deprecated_function( __FUNCTION__, '3.0' ); $white_label_description = get_rocket_option( 'wl_description' ); // We change the plugin's header. $plugins['wp-rocket/wp-rocket.php'] = array( 'Name' => get_rocket_option( 'wl_plugin_name' ), 'PluginURI' => get_rocket_option( 'wl_plugin_URI' ), 'Version' => isset( $plugins['wp-rocket/wp-rocket.php']['Version'] ) ? $plugins['wp-rocket/wp-rocket.php']['Version'] : '', 'Description' => reset( ( $white_label_description ) ), 'Author' => get_rocket_option( 'wl_author' ), 'AuthorURI' => get_rocket_option( 'wl_author_URI' ), 'TextDomain' => isset( $plugins['wp-rocket/wp-rocket.php']['TextDomain'] ) ? $plugins['wp-rocket/wp-rocket.php']['TextDomain'] : '', 'DomainPath' => isset( $plugins['wp-rocket/wp-rocket.php']['DomainPath'] ) ? $plugins['wp-rocket/wp-rocket.php']['DomainPath'] : '', ); // if white label, remove our names from contributors. if ( rocket_is_white_label() ) { remove_filter( 'plugin_row_meta', 'rocket_plugin_row_meta', 10, 2 ); } return $plugins; } } if ( ! function_exists( 'rocket_is_white_label' ) ) { /** * Is this version White Labeled? * * @since 2.1 * @deprecated 3.0 */ function rocket_is_white_label() { _deprecated_function( __FUNCTION__, '3.0' ); $options = ''; $names = array( 'wl_plugin_name', 'wl_plugin_URI', 'wl_description', 'wl_author', 'wl_author_URI', ); foreach ( $names as $value ) { $option = get_rocket_option( $value ); $options .= ! is_array( $option ) ? $option : reset( ( $option ) ); } return '7ddca92d3d48d4da715a90ebcb3ec1f0' !== md5( $options ); } } if ( ! function_exists( 'rocket_reset_white_label_values' ) ) { /** * Reset white label options * * @since 2.1 * @deprecated 3.0 * * @param bool $hack_post true if we need to modify the $_POST content, false otherwise. * @return void */ function rocket_reset_white_label_values( $hack_post ) { _deprecated_function( __FUNCTION__, '3.0' ); // White Label default values - !!! DO NOT TRANSLATE !!! $options = get_option( WP_ROCKET_SLUG ); $options['wl_plugin_name'] = 'WP Rocket'; $options['wl_plugin_slug'] = 'wprocket'; $options['wl_plugin_URI'] = 'https://wp-rocket.me'; $options['wl_description'] = array( 'The best WordPress performance plugin.' ); $options['wl_author'] = 'WP Media'; $options['wl_author_URI'] = 'https://wp-media.me'; if ( $hack_post ) { // hack $_POST to force refresh of files, sorry. $_POST['page'] = 'wprocket'; } update_option( WP_ROCKET_SLUG, $options ); } } if ( ! function_exists( 'rocket_check_no_empty_name' ) ) { /** * When you're doing an update, the constant does not contain yet your option or any value, reset and redirect! * * @since 2.1 * @deprecated 3.0 */ function rocket_check_no_empty_name() { _deprecated_function( __FUNCTION__, '3.0' ); $wl_plugin_name = trim( get_rocket_option( 'wl_plugin_name' ) ); if ( empty( $wl_plugin_name ) ) { wp_safe_redirect( $_SERVER['REQUEST_URI'] ); die(); } } } if ( ! function_exists( 'rocket_correct_capability_for_options_page' ) ) { /** * Fix the capability for our capacity filter hook * * @since 2.6 * @deprecated 3.0 * @see WP_Rocket\Engine\Settings\Page->required_capability() * * @param string $capability Capacity to access WP Rocket options. * @return string Updated capacity */ function rocket_correct_capability_for_options_page( $capability ) { _deprecated_function( __FUNCTION__, '3.0', 'WP_Rocket\Engine\Settings\Page->required_compatibility()' ); /** This filter is documented in inc/admin-bar.php */ return apply_filters( 'rocket_capacity', 'manage_options' ); } } if ( ! function_exists( 'rocket_admin_menu' ) ) { /** * Add submenu in menu "Settings" * * @since 1.0 * @deprecated 3.0 * @see WP_Rocket\Engine\Settings\Page->add_admin_page() */ function rocket_admin_menu() { _deprecated_function( __FUNCTION__, '3.0', 'WP_Rocket\Engine\Settings\Page->add_admin_page()' ); add_options_page( WP_ROCKET_PLUGIN_NAME, WP_ROCKET_PLUGIN_NAME, apply_filters( 'rocket_capacity', 'manage_options' ), WP_ROCKET_PLUGIN_SLUG, 'rocket_display_options' ); } } if ( ! function_exists( 'rocket_include' ) ) { /** * Used to include a file in any tab * * @since 2.2 * @deprecated 3.0 * * @param array $args An array of arguments to include the file. */ function rocket_include( $args ) { _deprecated_function( __FUNCTION__, '3.0' ); include_once dirname( __FILE__ ) . '/' . str_replace( '..', '', $args['file'] ) . '.inc.php'; } } if ( ! function_exists( 'rocket_register_setting' ) ) { /** * Tell to WordPress to be confident with our setting, we are clean! * * @since 1.0 * @deprecated 3.0 * @see WP_Rocket\Engine\Settings\Page->configure() */ function rocket_register_setting() { _deprecated_function( __FUNCTION__, '3.0', 'WP_Rocket\Engine\Settings\Page->configure()' ); register_setting( 'wp_rocket', WP_ROCKET_SLUG, 'rocket_settings_callback' ); } } if ( ! function_exists( 'rocket_settings_callback' ) ) { /** * Used to clean and sanitize the settings fields * * @since 1.0 * * @param array $inputs An array of values submitted by the settings form. */ function rocket_settings_callback( $inputs ) { _deprecated_function( __FUNCTION__, '3.0', 'WP_Rocket\Engine\Admin\Settings\Settings->sanitize_callback()' ); if ( isset( $_GET['action'] ) && 'purge_cache' === $_GET['action'] ) { return $inputs; } /* * Option : Minification CSS & JS */ $inputs['minify_css'] = ! empty( $inputs['minify_css'] ) ? 1 : 0; $inputs['minify_js'] = ! empty( $inputs['minify_js'] ) ? 1 : 0; // Option: mobile cache. if ( rocket_is_mobile_plugin_active() ) { $inputs['cache_mobile'] = 1; $inputs['do_caching_mobile_files'] = 1; } if ( empty( $inputs['lazyload_iframes'] ) ) { $inputs['lazyload_youtube'] = 0; } /* * Option : Purge delay */ $inputs['purge_cron_interval'] = isset( $inputs['purge_cron_interval'] ) ? (int) $inputs['purge_cron_interval'] : get_rocket_option( 'purge_cron_interval' ); $inputs['purge_cron_unit'] = isset( $inputs['purge_cron_unit'] ) ? $inputs['purge_cron_unit'] : get_rocket_option( 'purge_cron_unit' ); if ( $inputs['purge_cron_interval'] < 10 && 'MINUTE_IN_SECONDS' === $inputs['purge_cron_unit'] ) { $inputs['purge_cron_interval'] = 10; } /* * Option : Remove query strings */ $inputs['remove_query_strings'] = ! empty( $inputs['remove_query_strings'] ) ? 1 : 0; /* * Option : Prefetch DNS requests */ if ( ! empty( $inputs['dns_prefetch'] ) ) { if ( ! is_array( $inputs['dns_prefetch'] ) ) { $inputs['dns_prefetch'] = explode( "\n", $inputs['dns_prefetch'] ); } $inputs['dns_prefetch'] = array_map( 'trim', $inputs['dns_prefetch'] ); $inputs['dns_prefetch'] = array_map( 'esc_url', $inputs['dns_prefetch'] ); $inputs['dns_prefetch'] = (array) array_filter( $inputs['dns_prefetch'] ); $inputs['dns_prefetch'] = array_unique( $inputs['dns_prefetch'] ); } else { $inputs['dns_prefetch'] = array(); } /* * Option : Empty the cache of the following pages when updating an article */ if ( ! empty( $inputs['cache_purge_pages'] ) ) { if ( ! is_array( $inputs['cache_purge_pages'] ) ) { $inputs['cache_purge_pages'] = explode( "\n", $inputs['cache_purge_pages'] ); } $inputs['cache_purge_pages'] = array_map( 'trim', $inputs['cache_purge_pages'] ); $inputs['cache_purge_pages'] = array_map( 'esc_url', $inputs['cache_purge_pages'] ); $inputs['cache_purge_pages'] = array_map( 'rocket_clean_exclude_file', $inputs['cache_purge_pages'] ); $inputs['cache_purge_pages'] = (array) array_filter( $inputs['cache_purge_pages'] ); $inputs['cache_purge_pages'] = array_unique( $inputs['cache_purge_pages'] ); } else { $inputs['cache_purge_pages'] = array(); } /* * Option : Never cache the following pages */ if ( ! empty( $inputs['cache_reject_uri'] ) ) { if ( ! is_array( $inputs['cache_reject_uri'] ) ) { $inputs['cache_reject_uri'] = explode( "\n", $inputs['cache_reject_uri'] ); } $inputs['cache_reject_uri'] = array_map( 'trim', $inputs['cache_reject_uri'] ); $inputs['cache_reject_uri'] = array_map( 'esc_url', $inputs['cache_reject_uri'] ); $inputs['cache_reject_uri'] = array_map( 'rocket_clean_exclude_file', $inputs['cache_reject_uri'] ); $inputs['cache_reject_uri'] = (array) array_filter( $inputs['cache_reject_uri'] ); $inputs['cache_reject_uri'] = array_unique( $inputs['cache_reject_uri'] ); } else { $inputs['cache_reject_uri'] = array(); } /* * Option : Don't cache pages that use the following cookies */ if ( ! empty( $inputs['cache_reject_cookies'] ) ) { if ( ! is_array( $inputs['cache_reject_cookies'] ) ) { $inputs['cache_reject_cookies'] = explode( "\n", $inputs['cache_reject_cookies'] ); } $inputs['cache_reject_cookies'] = array_map( 'trim', $inputs['cache_reject_cookies'] ); $inputs['cache_reject_cookies'] = array_map( 'rocket_sanitize_key', $inputs['cache_reject_cookies'] ); $inputs['cache_reject_cookies'] = (array) array_filter( $inputs['cache_reject_cookies'] ); $inputs['cache_reject_cookies'] = array_unique( $inputs['cache_reject_cookies'] ); } else { $inputs['cache_reject_cookies'] = array(); } /* * Option : Cache pages that use the following query strings (GET parameters) */ if ( ! empty( $inputs['cache_query_strings'] ) ) { if ( ! is_array( $inputs['cache_query_strings'] ) ) { $inputs['cache_query_strings'] = explode( "\n", $inputs['cache_query_strings'] ); } $inputs['cache_query_strings'] = array_map( 'trim', $inputs['cache_query_strings'] ); $inputs['cache_query_strings'] = array_map( 'rocket_sanitize_key', $inputs['cache_query_strings'] ); $inputs['cache_query_strings'] = (array) array_filter( $inputs['cache_query_strings'] ); $inputs['cache_query_strings'] = array_unique( $inputs['cache_query_strings'] ); } else { $inputs['cache_query_strings'] = array(); } /* * Option : Never send cache pages for these user agents */ if ( ! empty( $inputs['cache_reject_ua'] ) ) { if ( ! is_array( $inputs['cache_reject_ua'] ) ) { $inputs['cache_reject_ua'] = explode( "\n", $inputs['cache_reject_ua'] ); } $inputs['cache_reject_ua'] = array_map( 'trim', $inputs['cache_reject_ua'] ); $inputs['cache_reject_ua'] = array_map( 'rocket_sanitize_ua', $inputs['cache_reject_ua'] ); $inputs['cache_reject_ua'] = (array) array_filter( $inputs['cache_reject_ua'] ); $inputs['cache_reject_ua'] = array_unique( $inputs['cache_reject_ua'] ); } else { $inputs['cache_reject_ua'] = array(); } /* * Option : CSS files to exclude of the minification */ if ( ! empty( $inputs['exclude_css'] ) ) { if ( ! is_array( $inputs['exclude_css'] ) ) { $inputs['exclude_css'] = explode( "\n", $inputs['exclude_css'] ); } $inputs['exclude_css'] = array_map( 'trim', $inputs['exclude_css'] ); $inputs['exclude_css'] = array_map( 'rocket_clean_exclude_file', $inputs['exclude_css'] ); $inputs['exclude_css'] = array_map( 'rocket_sanitize_css', $inputs['exclude_css'] ); $inputs['exclude_css'] = (array) array_filter( $inputs['exclude_css'] ); $inputs['exclude_css'] = array_unique( $inputs['exclude_css'] ); } else { $inputs['exclude_css'] = array(); } /* * Option : JS files to exclude of the minification */ if ( ! empty( $inputs['exclude_js'] ) ) { if ( ! is_array( $inputs['exclude_js'] ) ) { $inputs['exclude_js'] = explode( "\n", $inputs['exclude_js'] ); } $inputs['exclude_js'] = array_map( 'trim', $inputs['exclude_js'] ); $inputs['exclude_js'] = array_map( 'rocket_clean_exclude_file', $inputs['exclude_js'] ); $inputs['exclude_js'] = array_map( 'rocket_sanitize_js', $inputs['exclude_js'] ); $inputs['exclude_js'] = (array) array_filter( $inputs['exclude_js'] ); $inputs['exclude_js'] = array_unique( $inputs['exclude_js'] ); } else { $inputs['exclude_js'] = array(); } // Option: Async CSS. $inputs['async_css'] = ! empty( $inputs['async_css'] ) ? 1 : 0; // Option: Critical CSS. $inputs['critical_css'] = ! empty( $inputs['critical_css'] ) ? str_replace( array( '<style>', '</style>' ), '', wp_kses( $inputs['critical_css'], array( "\'", '\"' ) ) ) : ''; /* * Option : JS files to exclude from defer JS */ if ( ! empty( $inputs['exclude_defer_js'] ) ) { if ( ! is_array( $inputs['exclude_defer_js'] ) ) { $inputs['exclude_defer_js'] = explode( "\n", $inputs['exclude_defer_js'] ); } $inputs['exclude_defer_js'] = array_map( 'trim', $inputs['exclude_defer_js'] ); $inputs['exclude_defer_js'] = array_unique( $inputs['exclude_defer_js'] ); $inputs['exclude_defer_js'] = array_map( 'rocket_sanitize_js', $inputs['exclude_defer_js'] ); $inputs['exclude_defer_js'] = array_filter( $inputs['exclude_defer_js'] ); } else { $inputs['exclude_defer_js'] = array(); } /** * Database options */ $inputs['database_revisions'] = ! empty( $inputs['database_revisions'] ) ? 1 : 0; $inputs['database_auto_drafts'] = ! empty( $inputs['database_auto_drafts'] ) ? 1 : 0; $inputs['database_trashed_posts'] = ! empty( $inputs['database_trashed_posts'] ) ? 1 : 0; $inputs['database_spam_comments'] = ! empty( $inputs['database_spam_comments'] ) ? 1 : 0; $inputs['database_trashed_comments'] = ! empty( $inputs['database_trashed_comments'] ) ? 1 : 0; $inputs['database_expired_transients'] = ! empty( $inputs['database_expired_transients'] ) ? 1 : 0; $inputs['database_all_transients'] = ! empty( $inputs['database_all_transients'] ) ? 1 : 0; $inputs['database_optimize_tables'] = ! empty( $inputs['database_optimize_tables'] ) ? 1 : 0; $inputs['schedule_automatic_cleanup'] = ! empty( $inputs['schedule_automatic_cleanup'] ) ? 1 : 0; $inputs['automatic_cleanup_frequency'] = ! empty( $inputs['automatic_cleanup_frequency'] ) ? $inputs['automatic_cleanup_frequency'] : ''; if ( 1 !== $inputs['schedule_automatic_cleanup'] && ( 'daily' !== $inputs['automatic_cleanup_frequency'] || 'weekly' !== $inputs['automatic_cleanup_frequency'] || 'monthly' !== $inputs['automatic_cleanup_frequency'] ) ) { unset( $inputs['automatic_cleanup_frequency'] ); } /** * Options: Activate bot preload */ $inputs['manual_preload'] = ! empty( $inputs['manual_preload'] ) ? 1 : 0; $inputs['automatic_preload'] = ! empty( $inputs['automatic_preload'] ) ? 1 : 0; /* * Option: activate sitemap preload */ $inputs['sitemap_preload'] = ! empty( $inputs['sitemap_preload'] ) ? 1 : 0; /* * Option : XML sitemaps URLs */ if ( ! empty( $inputs['sitemaps'] ) ) { if ( ! is_array( $inputs['sitemaps'] ) ) { $inputs['sitemaps'] = explode( "\n", $inputs['sitemaps'] ); } $inputs['sitemaps'] = array_map( 'trim', $inputs['sitemaps'] ); $inputs['sitemaps'] = array_map( 'rocket_sanitize_xml', $inputs['sitemaps'] ); $inputs['sitemaps'] = (array) array_filter( $inputs['sitemaps'] ); $inputs['sitemaps'] = array_unique( $inputs['sitemaps'] ); } else { $inputs['sitemaps'] = array(); } /* * Option : CloudFlare Domain */ if ( ! empty( $inputs['cloudflare_domain'] ) ) { $inputs['cloudflare_domain'] = rocket_get_domain( $inputs['cloudflare_domain'] ); } else { $inputs['cloudflare_domain'] = ''; } $inputs['cloudflare_devmode'] = ( isset( $inputs['cloudflare_devmode'] ) && is_numeric( $inputs['cloudflare_devmode'] ) ) ? (int) $inputs['cloudflare_devmode'] : 0; $inputs['cloudflare_auto_settings'] = ( isset( $inputs['cloudflare_auto_settings'] ) && is_numeric( $inputs['cloudflare_auto_settings'] ) ) ? (int) $inputs['cloudflare_auto_settings'] : 0; /* * Option : CloudFlare */ if ( defined( 'WP_ROCKET_CF_API_KEY' ) ) { $inputs['cloudflare_api_key'] = get_rocket_option( 'cloudflare_api_key' ); } /* * Option : CDN */ $inputs['cdn_cnames'] = isset( $inputs['cdn_cnames'] ) ? array_unique( array_filter( $inputs['cdn_cnames'] ) ) : array(); if ( ! $inputs['cdn_cnames'] ) { $inputs['cdn_zone'] = array(); } else { $total_cdn_cnames = max( array_keys( $inputs['cdn_cnames'] ) ); for ( $i = 0; $i <= $total_cdn_cnames; $i++ ) { if ( ! isset( $inputs['cdn_cnames'][ $i ] ) ) { unset( $inputs['cdn_zone'][ $i ] ); } else { $inputs['cdn_zone'][ $i ] = isset( $inputs['cdn_zone'][ $i ] ) ? $inputs['cdn_zone'][ $i ] : 'all'; } } $inputs['cdn_cnames'] = array_values( $inputs['cdn_cnames'] ); $inputs['cdn_cnames'] = array_map( 'untrailingslashit', $inputs['cdn_cnames'] ); ksort( $inputs['cdn_zone'] ); $inputs['cdn_zone'] = array_values( $inputs['cdn_zone'] ); } /* * Option : Files to exclude of the CDN process */ if ( ! empty( $inputs['cdn_reject_files'] ) ) { if ( ! is_array( $inputs['cdn_reject_files'] ) ) { $inputs['cdn_reject_files'] = explode( "\n", $inputs['cdn_reject_files'] ); } $inputs['cdn_reject_files'] = array_map( 'trim', $inputs['cdn_reject_files'] ); $inputs['cdn_reject_files'] = array_map( 'rocket_clean_exclude_file', $inputs['cdn_reject_files'] ); $inputs['cdn_reject_files'] = (array) array_filter( $inputs['cdn_reject_files'] ); $inputs['cdn_reject_files'] = array_unique( $inputs['cdn_reject_files'] ); } else { $inputs['cdn_reject_files'] = array(); } /* * Option: Support */ $fake_options = array( 'support_summary', 'support_description', 'support_documentation_validation', ); foreach ( $fake_options as $option ) { if ( isset( $inputs[ $option ] ) ) { unset( $inputs[ $option ] ); } } if ( isset( $_FILES['import'] ) && 0 !== $_FILES['import']['size'] && $settings = rocket_handle_settings_import( $_FILES['import'], 'wp-rocket', $inputs ) ) { $inputs = $settings; } if ( ! rocket_valid_key() ) { $checked = rocket_check_key(); } if ( isset( $checked ) && is_array( $checked ) ) { $inputs['consumer_key'] = $checked['consumer_key']; $inputs['consumer_email'] = $checked['consumer_email']; $inputs['secret_key'] = $checked['secret_key']; } if ( rocket_valid_key() && ! empty( $inputs['secret_key'] ) && ! isset( $inputs['ignore'] ) ) { unset( $inputs['ignore'] ); add_settings_error( 'general', 'settings_updated', __( 'Settings saved.', 'rocket' ), 'updated' ); } return apply_filters( 'rocket_inputs_sanitize', $inputs ); } } if ( ! function_exists( 'rocket_import_upload_form' ) ) { /** * Outputs the form used by the importers to accept the data to be imported * * @since 2.2 * @deprecated 3.0 * * @see WP_Rocket\Admin\Render->render_import_form(); */ function rocket_import_upload_form() { _deprecated_function( __FUNCTION__, '3.0', 'WP_Rocket\Admin\Render->render_import_form()' ); /** * Filter the maximum allowed upload size for import files. * * @since (WordPress) 2.3.0 * * @see wp_max_upload_size() * * @param int $max_upload_size Allowed upload size. Default 1 MB. */ $bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); // Filter from WP Core. $size = size_format( $bytes ); $upload_dir = wp_upload_dir(); if ( ! empty( $upload_dir['error'] ) ) { ?> <div class="error"><p><?php _e( 'Before you can upload your import file, you will need to fix the following error:', 'rocket' ); ?></p> <p><strong><?php echo $upload_dir['error']; ?></strong></p></div> <?php } else { ?> <p> <input type="file" id="upload" name="import" size="25" /> <br /> <label for="upload"> <?php // translators: %s is the maximum upload size set on the current server. echo apply_filters( 'rocket_help', sprintf( __( 'Choose a file from your computer (maximum size: %s)', 'rocket' ), $size ), 'upload', 'help' ); ?> </label> <input type="hidden" name="max_file_size" value="<?php echo $bytes; ?>" /> </p> <?php submit_button( __( 'Upload file and import settings', 'rocket' ), 'button', 'import' ); } } } if ( ! function_exists( 'rocket_field' ) ) { /** * Used to display fields on settings form * * @since 1.0 * @deprecated 3.0 * * @param array $args An array of arguments to populate the settings fields. */ function rocket_field( $args ) { _deprecated_function( __FUNCTION__, '3.0' ); if ( ! is_array( reset( $args ) ) ) { $args = array( $args ); } $full = $args; foreach ( $full as $args ) { if ( ! is_array( $args ) ) { continue; } if ( isset( $args['display'] ) && ! $args['display'] ) { continue; } $args['label_for'] = isset( $args['label_for'] ) ? $args['label_for'] : ''; $args['name'] = isset( $args['name'] ) ? $args['name'] : $args['label_for']; $parent = isset( $args['parent'] ) ? 'data-parent="' . sanitize_html_class( $args['parent'] ) . '"' : null; $placeholder = isset( $args['placeholder'] ) ? 'placeholder="' . $args['placeholder'] . '" ' : ''; $class = isset( $args['class'] ) ? sanitize_html_class( $args['class'] ) : sanitize_html_class( $args['name'] ); $class .= ( $parent ) ? ' has-parent' : null; $label = isset( $args['label'] ) ? $args['label'] : ''; $default = isset( $args['default'] ) ? $args['default'] : ''; $readonly = isset( $args['readonly'] ) && $args['readonly'] ? ' readonly="readonly" disabled="disabled"' : ''; $cols = isset( $args['cols'] ) ? (int) $args['cols'] : 50; $rows = isset( $args['rows'] ) ? (int) $args['rows'] : 5; if ( ! isset( $args['fieldset'] ) || 'start' === $args['fieldset'] ) { printf( '<fieldset class="fieldname-%1$s fieldtype-%2$s %3$s">', sanitize_html_class( $args['name'] ), sanitize_html_class( $args['type'] ), isset( $args['parent'] ) ? 'fieldparent-' . sanitize_html_class( $args['parent'] ) : '' ); } switch ( $args['type'] ) { case 'number': case 'email': case 'text': $value = get_rocket_option( $args['name'] ); if ( false === $value ) { $value = $default; } $value = esc_attr( $value ); $number_options = 'number' === $args['type'] ? ' min="0" class="small-text"' : ''; $autocomplete = in_array( $args['name'], array( 'consumer_key', 'consumer_email' ), true ) ? ' autocomplete="off"' : ''; $disabled = ( 'consumer_key' === $args['name'] && defined( 'WP_ROCKET_KEY' ) ) || ( 'consumer_email' === $args['name'] && defined( 'WP_ROCKET_EMAIL' ) ) ? ' disabled="disabled"' : $readonly; ?> <legend class="screen-reader-text"><span><?php echo $args['label_screen']; ?></span></legend> <label><input<?php echo $autocomplete . $disabled; ?> type="<?php echo $args['type']; ?>"<?php echo $number_options; ?> id="<?php echo $args['label_for']; ?>" name="wp_rocket_settings[<?php echo $args['name']; ?>]" value="<?php echo $value; ?>" <?php echo $placeholder; ?><?php echo $readonly; ?>/> <?php echo $label; ?></label> <?php break; case 'cloudflare_api_key': $value = get_rocket_option( $args['name'] ); if ( 'cloudflare_api_key' === $args['name'] && defined( 'WP_ROCKET_CF_API_KEY' ) ) { $value = WP_ROCKET_CF_API_KEY; } $value = esc_attr( $value ); $disabled = ( 'cloudflare_api_key' === $args['name'] && defined( 'WP_ROCKET_CF_API_KEY' ) ) ? ' disabled="disabled"' : $readonly; $cf_valid_credentials = false; if ( function_exists( 'rocket_cloudflare_valid_auth' ) ) { $cf_valid_credentials = ( is_wp_error( rocket_cloudflare_valid_auth() ) ) ? false : true; } ?> <legend class="screen-reader-text"><span><?php echo $args['label_screen']; ?></span></legend> <label> <input<?php echo $disabled; ?> type="text" id="<?php echo $args['label_for']; ?>" name="wp_rocket_settings[<?php echo $args['name']; ?>]" value="<?php echo $value; ?>" <?php echo $placeholder; ?><?php echo $readonly; ?>/> <?php echo $label; ?> <?php if ( $cf_valid_credentials ) { ?> <span id="rocket-check-cloudflare-api-container" class="rocket-cloudflare-api-valid"> <span class="dashicons dashicons-yes" aria-hidden="true"></span> <?php _e( 'Your Cloudflare credentials are valid.', 'rocket' ); ?> </span> <?php } elseif ( ! $cf_valid_credentials && $value ) { ?> <span id="rocket-check-cloudflare-api-container"> <span class="dashicons dashicons-no" aria-hidden="true"></span> <?php _e( 'Your Cloudflare credentials are invalid!', 'rocket' ); ?> </span> <?php } ?> </label> <?php break; case 'textarea': $t_temp = get_rocket_option( $args['name'], '' ); if ( is_array( $t_temp ) ) { $t_temp = implode( "\n", $t_temp ); } $value = ! empty( $t_temp ) ? esc_textarea( $t_temp ) : ''; if ( ! $value ) { $value = $default; } ?> <legend class="screen-reader-text"><span><?php echo $args['label_screen']; ?></span></legend> <label><textarea id="<?php echo $args['label_for']; ?>" name="wp_rocket_settings[<?php echo $args['name']; ?>]" cols="<?php echo $cols; ?>" rows="<?php echo $rows; ?>" class="<?php echo $class; ?>" <?php echo $readonly; echo $placeholder; echo $parent; ?> ><?php echo esc_html( $value ); ?></textarea> </label> <?php break; case 'checkbox': if ( isset( $args['label_screen'] ) ) { ?> <legend class="screen-reader-text"><span><?php echo $args['label_screen']; ?></span></legend> <?php } ?> <label><input type="checkbox" id="<?php echo $args['name']; ?>" class="<?php echo $class; ?>" name="wp_rocket_settings[<?php echo $args['name']; ?>]" value="1"<?php echo $readonly; ?> <?php checked( get_rocket_option( $args['name'], $default ), 1 ); ?> <?php echo $parent; ?>/> <?php echo $args['label']; ?> </label> <?php break; case 'select': ?> <legend class="screen-reader-text"><span><?php echo $args['label_screen']; ?></span></legend> <label> <select id="<?php echo $args['name']; ?>" class="<?php echo $class; ?>" name="wp_rocket_settings[<?php echo $args['name']; ?>]"<?php echo $readonly; ?> <?php echo $parent; ?>> <?php foreach ( $args['options'] as $val => $title ) { ?> <option value="<?php echo $val; ?>" <?php selected( get_rocket_option( $args['name'] ), $val ); ?>><?php echo $title; ?></option> <?php } ?> </select> <?php echo $label; ?> </label> <?php break; case 'submit_optimize': ?> <input type="submit" name="wp_rocket_settings[submit_optimize]" id="rocket_submit_optimize" class="button button-primary" value="<?php _e( 'Save and optimize', 'rocket' ); ?>"> <a href="<?php echo wp_nonce_url( admin_url( 'admin-post.php?action=rocket_optimize_database' ), 'rocket_optimize_database' ); ?>" class="button button-secondary"><?php _e( 'Optimize', 'rocket' ); ?></a> <?php break; case 'repeater': $fields = new WP_Rocket_Repeater_Field( $args ); $fields->render(); break; case 'helper_description': $description = isset( $args['description'] ) ? sprintf( '<p class="description help %1$s" %2$s><span class="dashicons dashicons-info" aria-hidden="true"></span><strong class="screen-reader-text">%3$s</strong> %4$s</p>', $class, $parent, _x( 'Note:', 'screen-reader-text', 'rocket' ), $args['description'] ) : ''; echo apply_filters( 'rocket_help', $description, $args['name'], 'description' ); break; case 'helper_performance': $description = isset( $args['description'] ) ? sprintf( '<p class="description help tip--perf %1$s" %2$s><span class="dashicons dashicons-performance" aria-hidden="true"></span><strong class="screen-reader-text">%3$s</strong> <strong>%4$s</strong></p>', $class, $parent, _x( 'Performance tip:', 'screen-reader-text', 'rocket' ), $args['description'] ) : ''; echo apply_filters( 'rocket_help', $description, $args['name'], 'description' ); break; case 'helper_detection': $description = isset( $args['description'] ) ? sprintf( '<p class="description help tip--detect %1$s" %2$s><span class="dashicons dashicons-visibility" aria-hidden="true"></span><strong class="screen-reader-text">%3$s</strong> %4$s</p>', $class, $parent, _x( 'Third-party feature detected:', 'screen-reader-text', 'rocket' ), $args['description'] ) : ''; echo apply_filters( 'rocket_help', $description, $args['name'], 'description' ); break; case 'helper_help': $description = isset( $args['description'] ) ? sprintf( '<p class="description help tip--use %1$s" %2$s>%3$s</p>', $class, $parent, $args['description'] ) : ''; echo apply_filters( 'rocket_help', $description, $args['name'], 'help' ); break; case 'helper_warning': $description = isset( $args['description'] ) ? sprintf( '<p class="description warning file-error %1$s" %2$s><span class="dashicons dashicons-warning" aria-hidden="true"></span><strong class="screen-reader-text">%3$s</strong> %4$s</p>', $class, $parent, _x( 'Warning:', 'screen-reader-text', 'rocket' ), $args['description'] ) : ''; echo apply_filters( 'rocket_help', $description, $args['name'], 'warning' ); break; case 'helper_panel_description': $description = isset( $args['description'] ) ? sprintf( '<div class="rocket-panel-description"><p class="%1$s" %2$s>%3$s</p></div>', $class, $parent, $args['description'] ) : ''; echo $description; break; case 'rocket_export_form': ?> <a href="<?php echo wp_nonce_url( admin_url( 'admin-post.php?action=rocket_export' ), 'rocket_export' ); ?>" id="export" class="button button-secondary rocketicon"><?php _ex( 'Download settings', 'button text', 'rocket' ); ?></a> <?php break; case 'rocket_import_upload_form': rocket_import_upload_form( 'rocket_importer' ); break; default: 'Type manquant ou incorrect'; // ne pas traduire. } if ( ! isset( $args['fieldset'] ) || 'end' === $args['fieldset'] ) { echo '</fieldset>'; } } } } if ( ! function_exists( 'rocket_cnames_module' ) ) { /** * Used to display the CNAMES module on settings form * * @since 2.1 */ function rocket_cnames_module() { _deprecated_function( __FUNCTION__, '3.0' ); ?> <legend class="screen-reader-text"><span><?php _e( 'Replace site\'s hostname with:', 'rocket' ); ?></span></legend> <div id="rkt-cnames" class="rkt-module"> <?php $cnames = get_rocket_option( 'cdn_cnames' ); $cnames_zone = get_rocket_option( 'cdn_zone' ); if ( $cnames ) { foreach ( $cnames as $k => $_url ) { ?> <p> <input style="width: 32em" type="text" placeholder="http://" class="regular-text" name="wp_rocket_settings[cdn_cnames][<?php echo $k; ?>]" value="<?php echo esc_attr( $_url ); ?>" /> <label> <?php _e( 'reserved for', 'rocket' ); ?> <select name="wp_rocket_settings[cdn_zone][<?php echo $k; ?>]"> <option value="all" <?php selected( $cnames_zone[ $k ], 'all' ); ?>><?php _e( 'All files', 'rocket' ); ?></option> <?php /** * Controls the inclusion of images option for the CDN dropdown * * @since 2.10.7 * @author Remy Perona * * @param bool $allow true to add the option, false otherwise. */ if ( apply_filters( 'rocket_allow_cdn_images', true ) ) : ?> <option value="images" <?php selected( $cnames_zone[ $k ], 'images' ); ?>><?php _e( 'Images', 'rocket' ); ?></option> <?php endif; ?> <option value="css_and_js" <?php selected( $cnames_zone[ $k ], 'css_and_js' ); ?>>CSS & JavaScript</option> <option value="js" <?php selected( $cnames_zone[ $k ], 'js' ); ?>>JavaScript</option> <option value="css" <?php selected( $cnames_zone[ $k ], 'css' ); ?>>CSS</option> </select> </label> <span class="dashicons dashicons-no rkt-module-remove hide-if-no-js"></span> </p> <?php } } else { // If no files yet, use this template inside #rkt-cnames. ?> <p> <input style="width: 32em" type="text" placeholder="http://" class="regular-text" name="wp_rocket_settings[cdn_cnames][]" value="" /> <label> <?php _e( 'reserved for', 'rocket' ); ?> <select name="wp_rocket_settings[cdn_zone][]"> <option value="all"><?php _e( 'All files', 'rocket' ); ?></option> <?php // this filter is defined in inc/admin/options.php. if ( apply_filters( 'rocket_allow_cdn_images', true ) ) : ?> <option value="images"><?php _e( 'Images', 'rocket' ); ?></option> <?php endif; ?> <option value="css_and_js">CSS & JavaScript</option> <option value="js">JavaScript</option> <option value="css">CSS</option> </select> </label> </p> <?php } ?> </div> <?php // Clone Template. ?> <div class="rkt-module-model hide-if-js"> <p> <input style="width: 32em" type="text" placeholder="http://" class="regular-text" name="wp_rocket_settings[cdn_cnames][]" value="" /> <label> <?php _e( 'reserved for', 'rocket' ); ?> <select name="wp_rocket_settings[cdn_zone][]"> <option value="all"><?php _e( 'All files', 'rocket' ); ?></option> <?php // this filter is defined in inc/admin/options.php. if ( apply_filters( 'rocket_allow_cdn_images', true ) ) : ?> <option value="images"><?php _e( 'Images', 'rocket' ); ?></option> <?php endif; ?> <option value="css_and_js">CSS & JavaScript</option> <option value="js">JavaScript</option> <option value="css">CSS</option> </select> </label> <span class="dashicons dashicons-no rkt-module-remove hide-if-no-js"></span> </p> </div> <p><a href="javascript:void(0)" class="rkt-module-clone hide-if-no-js button-secondary"><?php _e( 'Add CNAME', 'rocket' ); ?></a></p> </fieldset> <?php } } if ( ! function_exists( 'rocket_button' ) ) { /** * Used to display buttons on settings form, tools tab * * @since 1.1.0 * @deprecated 3.0 * * @param array $args An array of arguments to populate the button attributes. */ function rocket_button( $args ) { _deprecated_function( __FUNCTION__, '3.0' ); $button = $args['button']; $desc = isset( $args['helper_description'] ) ? $args['helper_description'] : null; $help = isset( $args['helper_help'] ) ? $args['helper_help'] : null; $warning = isset( $args['helper_warning'] ) ? $args['helper_warning'] : null; $id = isset( $button['button_id'] ) ? sanitize_html_class( $button['button_id'] ) : null; $class = sanitize_html_class( strip_tags( $button['button_label'] ) ); $button_style = isset( $button['style'] ) ? 'button-' . sanitize_html_class( $button['style'] ) : 'button-secondary'; if ( ! empty( $help ) ) { $help = '<p class="description help ' . $class . '">' . $help['description'] . '</p>'; } if ( ! empty( $desc ) ) { $desc = sprintf( '<p class="description help %1$s"><span class="dashicons dashicons-info" aria-hidden="true"></span><strong class="screen-reader-text">%2$s</strong> %3$s</p>', $class, _x( 'Note:', 'screen-reader-text', 'rocket' ), $desc['description'] ); } if ( ! empty( $warning ) ) { $warning = sprintf( '<p class="description warning file-error %1$s"><span class="dashicons dashicons-warning" aria-hidden="true"></span><strong class="screen-reader-text">%2$s</strong> %3$s</p>', $class, _x( 'Warning:', 'screen-reader-text', 'rocket' ), $warning['description'] ); } ?> <fieldset class="fieldname-<?php echo $class; ?> fieldtype-button"> <?php if ( isset( $button['url'] ) ) { echo '<a href="' . esc_url( $button['url'] ) . '" id="' . $id . '" class="' . $button_style . ' rocketicon rocketicon-' . $class . '">' . wp_kses_post( $button['button_label'] ) . '</a>'; } else { echo '<button id="' . $id . '" class="' . $button_style . ' rocketicon rocketicon-' . $class . '">' . wp_kses_post( $button['button_label'] ) . '</button>'; } ?> <?php echo apply_filters( 'rocket_help', $desc, sanitize_key( strip_tags( $button['button_label'] ) ), 'description' ); ?> <?php echo apply_filters( 'rocket_help', $help, sanitize_key( strip_tags( $button['button_label'] ) ), 'help' ); ?> <?php echo apply_filters( 'rocket_help', $warning, sanitize_key( strip_tags( $button['button_label'] ) ), 'warning' ); ?> </fieldset> <?php } /** * Used to display videos buttons on settings form * * @since 2.2 * * @param array $args An array of arguments to populate the video attributes. */ function rocket_video( $args ) { $desc = '<p class="description desc ' . sanitize_html_class( $args['name'] ) . '">' . $args['description'] . '</p>'; ?> <fieldset class="fieldname-<?php echo $args['name']; ?> fieldtype-button"> <a href="<?php echo esc_url( $args['url'] ); ?>" class="button-secondary fancybox rocketicon rocketicon-video"><?php _e( 'Watch the video', 'rocket' ); ?></a> <?php echo apply_filters( 'rocket_help', $desc, $args['name'], 'description' ); ?> </fieldset> <?php } } if ( ! function_exists( 'rocket_display_options' ) ) { /** * The main settings page constructor using the required functions from WP * * @since 1.1.0 Add tabs, tools tab and change options severity * @since 1.0 * @deprecated 3.0 */ function rocket_display_options() { _deprecated_function( __FUNCTION__, '3.0' ); $modules = array( 'api-key', 'basic', 'advanced', 'optimization', 'database', 'preload', 'cloudflare', 'cdn', 'varnish', 'tools', 'support', ); foreach ( $modules as $module ) { require WP_ROCKET_ADMIN_UI_MODULES_PATH . $module . '.php'; } $heading_tag = version_compare( $GLOBALS['wp_version'], '4.3' ) >= 0 ? 'h1' : 'h2'; ?> <div class="wrap"> <<?php echo $heading_tag; ?>><?php echo WP_ROCKET_PLUGIN_NAME; ?> <small><sup><?php echo WP_ROCKET_VERSION; ?></sup></small></<?php echo $heading_tag; ?>> <form action="options.php" id="rocket_options" method="post" enctype="multipart/form-data"> <?php settings_fields( 'wp_rocket' ); rocket_hidden_fields( array( 'consumer_key', 'consumer_email', 'secret_key', 'license', 'secret_cache_key', 'minify_css_key', 'minify_js_key', 'version', 'cloudflare_old_settings', 'cloudflare_zone_id', 'sitemap_preload_url_crawl', ) ); submit_button(); ?> <h2 class="nav-tab-wrapper hide-if-no-js"> <?php if ( rocket_valid_key() ) { ?> <a href="#tab_basic" class="nav-tab"><?php _e( 'Basic', 'rocket' ); ?></a> <a href="#tab_optimization" class="nav-tab"><?php _e( 'Static Files', 'rocket' ); ?></a> <a href="#tab_cdn" class="nav-tab"><?php _e( 'CDN', 'rocket' ); ?></a> <a href="#tab_advanced" class="nav-tab"><?php _e( 'Advanced', 'rocket' ); ?></a> <a href="#tab_database" class="nav-tab"><?php _e( 'Database', 'rocket' ); ?></a> <a href="#tab_preload" class="nav-tab"><?php _e( 'Preload', 'rocket' ); ?></a> <?php if ( get_rocket_option( 'do_cloudflare' ) ) { ?> <a href="#tab_cloudflare" class="nav-tab">Cloudflare</a> <?php } /** This filter is documented in inc/admin/ui/modules/vanrish.php */ if ( apply_filters( 'rocket_display_varnish_options_tab', true ) ) { ?> <a href="#tab_varnish" class="nav-tab">Varnish</a> <?php } ?> <a href="#tab_tools" class="nav-tab"><?php _e( 'Tools', 'rocket' ); ?></a> <a href="#tab_support" class="nav-tab"><?php _e( 'Support', 'rocket' ); ?></a> <?php } else { ?> <a href="#tab_apikey" class="nav-tab"><?php _e( 'License', 'rocket' ); ?></a> <?php } ?> <?php do_action( 'rocket_tab', rocket_valid_key() ); ?> </h2> <div id="rockettabs"> <?php if ( rocket_valid_key() ) { ?> <div class="rkt-tab" id="tab_basic"><?php do_settings_sections( 'rocket_basic' ); ?></div> <div class="rkt-tab" id="tab_optimization"><?php do_settings_sections( 'rocket_optimization' ); ?></div> <div class="rkt-tab" id="tab_cdn"><?php do_settings_sections( 'rocket_cdn' ); ?></div> <div class="rkt-tab" id="tab_advanced"><?php do_settings_sections( 'rocket_advanced' ); ?></div> <div class="rkt-tab" id="tab_database"><?php do_settings_sections( 'rocket_database' ); ?></div> <div class="rkt-tab" id="tab_preload"><?php do_settings_sections( 'rocket_preload' ); ?></div> <div class="rkt-tab" id="tab_cloudflare" <?php echo get_rocket_option( 'do_cloudflare' ) ? '' : 'style="display:none"'; ?>><?php do_settings_sections( 'rocket_cloudflare' ); ?></div> <?php /** This filter is documented in inc/admin/ui/modules/vanrish.php */ if ( apply_filters( 'rocket_display_varnish_options_tab', true ) ) { ?> <div class="rkt-tab" id="tab_varnish"><?php do_settings_sections( 'rocket_varnish' ); ?></div> <?php } ?> <div class="rkt-tab" id="tab_tools"><?php do_settings_sections( 'rocket_tools' ); ?></div> <div class="rkt-tab rkt-tab-txt" id="tab_support"><?php do_settings_sections( 'rocket_support' ); ?></div> <?php } else { ?> <div class="rkt-tab" id="tab_apikey"><?php do_settings_sections( 'rocket_apikey' ); ?></div> <?php } ?> <?php do_action( 'rocket_tab_content', rocket_valid_key() ); ?> </div> <?php submit_button(); ?> </form> <?php } } if ( ! function_exists( 'rocket_hidden_fields' ) ) { /** * Function used to print all hidden fields from rocket to avoid the loss of these. * * @since 2.1 * @deprecated 3.0 * * @param array $fields An array of fields to add to WP Rocket settings. */ function rocket_hidden_fields( $fields ) { _deprecated_function( __FUNCTION__, '3.0', 'WP_Rocket\Admin\Render->render_hidden_fields()' ); if ( ! is_array( $fields ) ) { return; } foreach ( $fields as $field ) { echo '<input type="hidden" name="wp_rocket_settings[' . $field . ']" value="' . esc_attr( get_rocket_option( $field ) ) . '" />'; } } } if ( ! function_exists( 'wp_ajax_rocket_new_ticket_support' ) ) { /** * Open a ticket support. * * @since 2.6 * @deprecated 3.0 */ function wp_ajax_rocket_new_ticket_support() { _deprecated_function( __FUNCTION__, '3.0' ); // rocket_capability is a typo (should have been rocket_capacity). if ( ! isset( $_POST['_wpnonce'], $_POST['summary'], $_POST['description'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'wp_rocket-options' ) || ! current_user_can( apply_filters_deprecated( 'rocket_capability', array( 'manage_options' ), '2.8.9', 'rocket_capacity' ) ) || ! current_user_can( apply_filters( 'rocket_capacity', 'manage_options' ) ) ) { return; } $response = wp_remote_post( WP_ROCKET_WEB_API . 'support/new-ticket.php', array( 'timeout' => 10, 'body' => array( 'data' => array( 'user_email' => defined( 'WP_ROCKET_EMAIL' ) ? sanitize_email( WP_ROCKET_EMAIL ) : '', 'user_key' => defined( 'WP_ROCKET_KEY' ) ? sanitize_key( WP_ROCKET_KEY ) : '', 'user_website' => home_url(), 'wp_version' => $GLOBALS['wp_version'], 'wp_active_plugins' => rocket_get_active_plugins(), 'wp_rocket_version' => WP_ROCKET_VERSION, 'wp_rocket_options' => get_option( WP_ROCKET_SLUG ), 'support_summary' => $_POST['summary'], 'support_description' => $_POST['description'], ), ), ) ); if ( ! is_wp_error( $response ) ) { wp_send_json( wp_remote_retrieve_body( $response ) ); } else { wp_send_json( array( 'msg' => 'BAD_SERVER', ) ); } } } if ( ! function_exists( 'wp_ajax_rocket_helpscout_live_search' ) ) { /** * Documentation suggestions based on the summary input from the new ticket support form. * * @since 2.6 * @deprecated 3.0 */ function wp_ajax_rocket_helpscout_live_search() { _deprecated_function( __FUNCTION__, '3.0' ); if ( current_user_can( apply_filters( 'rocket_capability', 'manage_options' ) ) ) { $query = filter_input( INPUT_POST, 'query' ); $response = wp_remote_post( WP_ROCKET_WEB_MAIN . 'tools/wp-rocket/helpscout/livesearch.php', array( 'timeout' => 10, 'body' => array( 'query' => esc_html( wp_strip_all_tags( $query, true ) ), 'lang' => get_locale(), ), ) ); if ( ! is_wp_error( $response ) ) { wp_send_json( wp_remote_retrieve_body( $response ) ); } } } } if ( ! function_exists( 'rocket_php_warning' ) ) { /** * Warns if PHP version is less than 5.3 and offers to rollback. * * @since 2.11 * @deprecated 3.0 * @see WP_Rocket_Requirements_check::notice(); * @author Remy Perona */ function rocket_php_warning() { _deprecated_function( __FUNCTION__, '3.0', 'WP_Rocket_Requirements_check::notice()' ); if ( version_compare( PHP_VERSION, '5.3' ) >= 0 ) { return; } /** This filter is documented in inc/admin-bar.php */ if ( ! current_user_can( apply_filters( 'rocket_capacity', 'manage_options' ) ) ) { return; } // Translators: %1$s = Plugin name, %2$s = Plugin version, %3$s = PHP version required. echo '<div class="notice notice-error"><p>' . sprintf( __( '%1$s %2$s requires at least PHP %3$s to function properly. To use this version, please ask your web host how to upgrade your server to PHP %3$s or higher. If you are not able to upgrade, you can rollback to the previous version by using the button below.', 'rocket' ), WP_ROCKET_PLUGIN_NAME, WP_ROCKET_VERSION, '5.3' ) . '</p> <p><a href="' . wp_nonce_url( admin_url( 'admin-post.php?action=rocket_rollback' ), 'rocket_rollback' ) . '" class="button">' . // Translators: %s = Previous plugin version. sprintf( __( 'Re-install version %s', 'rocket' ), WP_ROCKET_LASTVERSION ) . '</a></p></div>'; } } if ( ! function_exists( 'rocket_get_home_path' ) ) { /** * Get the absolute filesystem path to the root of the WordPress installation. * * @since 2.11.7 copy function get_home_path() from WP core. * @since 2.11.5 * @deprecated 3.0 * * @author Chris Williams * * @return string Full filesystem path to the root of the WordPress installation. */ function rocket_get_home_path() { _deprecated_function( __FUNCTION__, '3.0' ); $home = set_url_scheme( get_option( 'home' ), 'http' ); $siteurl = set_url_scheme( get_option( 'siteurl' ), 'http' ); $home_path = ABSPATH; if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) { $wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */ $pos = strripos( str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ), trailingslashit( $wp_path_rel_to_home ) ); $home_path = substr( $_SERVER['SCRIPT_FILENAME'], 0, $pos ); $home_path = trailingslashit( $home_path ); } return str_replace( '\\', '/', $home_path ); } } if ( ! function_exists( 'rocket_clean_cache_after_woocommerce_save_product_variation' ) ) { /** * Clean product cache on variation update * * @since 2.9 * @deprecated 3.1 * @see WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::clean_cache_after_woocommerce_save_product_variation() * @author Remy Perona * * @param int $variation_id ID of the variation. */ function rocket_clean_cache_after_woocommerce_save_product_variation( $variation_id ) { _deprecated_function( __FUNCTION__, '3.1', 'WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::clean_cache_after_woocommerce_save_product_variation()' ); $product_id = wp_get_post_parent_id( $variation_id ); if ( $product_id ) { rocket_clean_post( $product_id ); } } } if ( ! function_exists( 'rocket_cache_v_query_string' ) ) { /** * Automatically cache v query string when WC geolocation with cache compatibility option is active * * @since 2.8.6 * @deprecated 3.1 * @see WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::cache_geolocation_query_string() * @author Rémy Perona * * @param array $query_strings list of query strings to cache. * @return array Updated list of query strings to cache */ function rocket_cache_v_query_string( $query_strings ) { _deprecated_function( __FUNCTION__, '3.1', 'WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::cache_geolocation_query_string()' ); if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) { $query_strings[] = 'v'; } return $query_strings; } } if ( ! function_exists( 'rocket_exclude_woocommerce_pages' ) ) { /** * Exclude WooCommerce cart, checkout and account pages from caching * * @since 2.11 Moved to 3rd party * @since 2.4 * @deprecated 3.1 * @see WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::exclude_pages() * * @param array $urls An array of excluded pages. * @return array Updated array of excluded pages */ function rocket_exclude_woocommerce_pages( $urls ) { _deprecated_function( __FUNCTION__, '3.1', 'WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::exclude_pages()' ); if ( function_exists( 'WC' ) && function_exists( 'wc_get_page_id' ) ) { if ( wc_get_page_id( 'checkout' ) && wc_get_page_id( 'checkout' ) !== -1 && wc_get_page_id( 'checkout' ) !== (int) get_option( 'page_on_front' ) ) { $checkout_urls = get_rocket_i18n_translated_post_urls( wc_get_page_id( 'checkout' ), 'page', '(.*)' ); $urls = array_merge( $urls, $checkout_urls ); } if ( wc_get_page_id( 'cart' ) && wc_get_page_id( 'cart' ) !== -1 && wc_get_page_id( 'cart' ) !== (int) get_option( 'page_on_front' ) ) { $cart_urls = get_rocket_i18n_translated_post_urls( wc_get_page_id( 'cart' ) ); $urls = array_merge( $urls, $cart_urls ); } if ( wc_get_page_id( 'myaccount' ) && wc_get_page_id( 'myaccount' ) !== -1 && wc_get_page_id( 'myaccount' ) !== (int) get_option( 'page_on_front' ) ) { $cart_urls = get_rocket_i18n_translated_post_urls( wc_get_page_id( 'myaccount' ), 'page', '(.*)' ); $urls = array_merge( $urls, $cart_urls ); } } return $urls; } } if ( ! function_exists( 'rocket_activate_woocommerce' ) ) { /** * Add query string to exclusion when activating the plugin * * @since 2.8.6 * @deprecated 3.1 * @see WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::activate_woocommerce() * @author Rémy Perona */ function rocket_activate_woocommerce() { _deprecated_function( __FUNCTION__, '3.1', 'WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::activate_woocommerce()' ); add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_woocommerce_pages' ); add_filter( 'rocket_cache_query_strings', 'rocket_cache_v_query_string' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } } if ( ! function_exists( 'rocket_deactivate_woocommerce' ) ) { /** * Remove query string from exclusion when deactivating the plugin * * @since 2.8.6 * @deprecated 3.1 * @see WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::deactivate_woocommerce() * @author Rémy Perona */ function rocket_deactivate_woocommerce() { _deprecated_function( __FUNCTION__, '3.1', 'WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::deactivate_woocommerce()' ); remove_filter( 'rocket_cache_reject_uri', 'rocket_exclude_woocommerce_pages' ); remove_filter( 'rocket_cache_query_strings', 'rocket_cache_v_query_string' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } } if ( ! function_exists( 'rocket_exclude_wc_rest_api' ) ) { /** * Exclude WooCommerce REST API URL from cache * * @since 2.6.5 * @deprecated 3.1 * @see WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::exclude_wc_rest_api() * * @param array $uri URLs to exclude from cache. * @return array Updated list of URLs to exclude from cache */ function rocket_exclude_wc_rest_api( $uri ) { _deprecated_function( __FUNCTION__, '3.1', 'WP_Rocket\Third_Party\Plugins\Ecommerce\WooCommerce::exclude_wc_rest_api()' ); /** * By default, don't cache the WooCommerce REST API. * * @since 2.6.5 * * @param bool false will force to cache the WooCommerce REST API */ $rocket_cache_reject_wc_rest_api = apply_filters( 'rocket_cache_reject_wc_rest_api', true ); // Exclude WooCommerce REST API. if ( $rocket_cache_reject_wc_rest_api ) { $uri[] = rocket_clean_exclude_file( home_url( '/wc-api/v(.*)' ) ); } return $uri; } } if ( ! function_exists( 'rocket_minify_process' ) ) { /** * Launch WP Rocket minification process (HTML, CSS and JavaScript) * * @since 2.10 New process for minification without concatenation * @since 1.3.0 This process is called via the new filter rocket_buffer * @since 1.1.6 Minify inline CSS and JavaScript * @since 1.0 * @deprecated 3.1 * * @param string $buffer HTML content. * @return string Modified HTML content */ function rocket_minify_process( $buffer ) { _deprecated_function( __FUNCTION__, '3.1' ); $enable_css = get_rocket_option( 'minify_css' ); $enable_js = get_rocket_option( 'minify_js' ); $enable_google_fonts = get_rocket_option( 'minify_google_fonts' ); if ( $enable_css || $enable_js || $enable_google_fonts ) { list( $buffer, $conditionals ) = rocket_extract_ie_conditionals( $buffer ); } // Minify JavaScript. if ( $enable_js && ( ! defined( 'DONOTROCKETOPTIMIZE' ) || ! DONOTROCKETOPTIMIZE ) && ( ! defined( 'DONOTMINIFYJS' ) || ! DONOTMINIFYJS ) && ! is_rocket_post_excluded_option( 'minify_js' ) ) { $buffer = rocket_minify_files( $buffer, 'js' ); } // Minify CSS. if ( $enable_css && ( ! defined( 'DONOTROCKETOPTIMIZE' ) || ! DONOTROCKETOPTIMIZE ) && ( ! defined( 'DONOTMINIFYCSS' ) || ! DONOTMINIFYCSS ) && ! is_rocket_post_excluded_option( 'minify_css' ) ) { $buffer = rocket_minify_files( $buffer, 'css' ); } // Concatenate Google Fonts. if ( $enable_google_fonts ) { $buffer = rocket_concatenate_google_fonts( $buffer ); } if ( $enable_css || $enable_js || $enable_google_fonts ) { $buffer = rocket_inject_ie_conditionals( $buffer, $conditionals ); } return $buffer; } } if ( ! function_exists( 'rocket_minify_html' ) ) { /** * Minifies inline HTML * * @since 2.10 Do the HTML minification independently and hook it later to prevent conflicts * @since 1.1.12 * @deprecated 3.1 * * @param string $buffer HTML content. * @return string Updated HTML content */ function rocket_minify_html( $buffer ) { _deprecated_function( __FUNCTION__, '3.1' ); if ( ! get_rocket_option( 'minify_html' ) || is_rocket_post_excluded_option( 'minify_html' ) ) { return $buffer; } $html_options = array( 'cssMinifier' => 'rocket_minify_inline_css', ); /** * Filter options of minify inline HTML * * @since 1.1.12 * * @param array $html_options Options of minify inline HTML. */ $html_options = apply_filters( 'rocket_minify_html_options', $html_options ); return Minify_HTML::minify( $buffer, $html_options ); } } if ( ! function_exists( 'rocket_fix_ssl_minify' ) ) { /** * Fix issue with SSL and minification * * @since 2.3 * @deprecated 3.1 * * @param string $url An url to filter to set the scheme to https if needed. * @return string Updated URL */ function rocket_fix_ssl_minify( $url ) { _deprecated_function( __FUNCTION__, '3.1' ); if ( is_ssl() && false === strpos( $url, 'https://' ) && ! in_array( rocket_extract_url_component( $url, PHP_URL_HOST ), get_rocket_cnames_host( array( 'all', 'css_js', 'css', 'js' ) ), true ) ) { $url = str_replace( 'http://', 'https://', $url ); } return $url; } } if ( ! function_exists( 'rocket_minify_i18n_multidomain' ) ) { /** * Compatibility with multilingual plugins & multidomain configuration * * @since 2.6.13 Regression Fix: Apply CDN on minified CSS and JS files by checking the CNAME host * @since 2.6.8 * @deprecated 3.1 * * @param string $url Minified file URL. * @return string Updated minified file URL */ function rocket_minify_i18n_multidomain( $url ) { _deprecated_function( __FUNCTION__, '3.1' ); if ( ! rocket_has_i18n() ) { return $url; } $url_host = rocket_extract_url_component( $url, PHP_URL_HOST ); $zone = array( 'all', 'css_and_js' ); $current_filter = current_filter(); // Add only CSS zone. if ( 'rocket_css_url' === $current_filter ) { $zone[] = 'css'; } // Add only JS zone. if ( 'rocket_js_url' === $current_filter ) { $zone[] = 'js'; } $cnames = get_rocket_cdn_cnames( $zone ); $cnames = array_map( 'rocket_remove_url_protocol' , $cnames ); if ( $url_host !== $_SERVER['HTTP_HOST'] && in_array( $_SERVER['HTTP_HOST'], get_rocket_i18n_host(), true ) && ! in_array( $url_host, $cnames, true ) ) { $url = str_replace( $url_host, $_SERVER['HTTP_HOST'], $url ); } return $url; } } if ( ! function_exists( 'rocket_get_js_enqueued_in_head' ) ) { /** * Get all src for JS files already enqueued in head * * @since 2.10 * @deprecated 3.1 * @author Remy Perona */ function rocket_get_js_enqueued_in_head() { _deprecated_function( __FUNCTION__, '3.1' ); global $wp_scripts, $rocket_js_enqueued_in_head; if ( ! (bool) $wp_scripts->done ) { return; } foreach ( $wp_scripts->done as $handle ) { if ( ! empty( $wp_scripts->registered[ $handle ]->src ) ) { $rocket_js_enqueued_in_head[] = str_replace( '#', '\#', rocket_clean_exclude_file( $wp_scripts->registered[ $handle ]->src ) ); } } } } if ( ! function_exists( 'get_rocket_exclude_files' ) ) { /** * Get all files to exclude from minification/concatenation. * * @since 2.11 * @deprecated 3.1 * @author Remy Perona * * @param string $extension Type of files to exclude. * @return array Array of excluded files. */ function get_rocket_exclude_files( $extension ) { _deprecated_function( __FUNCTION__, '3.1' ); if ( 'css' === $extension ) { $excluded_files = get_rocket_option( 'exclude_css', array() ); /** * Filters CSS files to exclude from minification/concatenation. * * @since 2.6 * * @param array $excluded_files List of excluded CSS files. */ $excluded_files = apply_filters( 'rocket_exclude_css', $excluded_files ); } elseif ( 'js' === $extension ) { global $wp_scripts; $excluded_files = get_rocket_option( 'exclude_js', array() ); if ( get_rocket_option( 'defer_all_js', 0 ) && get_rocket_option( 'defer_all_js_safe', 0 ) ) { $excluded_files[] = rocket_clean_exclude_file( site_url( $wp_scripts->registered['jquery-core']->src ) ); } /** * Filter JS files to exclude from minification/concatenation. * * @since 2.6 * * @param array $js_files List of excluded JS files. */ $excluded_files = apply_filters( 'rocket_exclude_js', $excluded_files ); } return $excluded_files; } } if ( ! function_exists( 'rocket_concatenate_google_fonts' ) ) { /** * Concatenates Google Fonts tags (http://fonts.googleapis.com/css?...) * * @since 2.3 * @deprecated 3.1 * * @param string $buffer HTML content. * @return string Modified HTML content */ function rocket_concatenate_google_fonts( $buffer ) { _deprecated_function( __FUNCTION__, '3.1' ); // Get all Google Fonts CSS files. $buffer_without_comments = preg_replace( '/<!--(.*)-->/Uis', '', $buffer ); preg_match_all( '/<link(?:\s+(?:(?!href\s*=\s*)[^>])+)?(?:\s+href\s*=\s*([\'"])((?:https?:)?\/\/fonts\.googleapis\.com\/css(?:(?!\1).)+)\1)(?:\s+[^>]*)?>/iU', $buffer_without_comments, $matches ); if ( ! $matches[2] || 1 === count( $matches ) ) { return $buffer; } $fonts = array(); $subsets = array(); foreach ( $matches[2] as $k => $font ) { // Get fonts name. $font = str_replace( array( '%7C', '%7c' ), '|', $font ); $font = explode( 'family=', $font ); $font = ( isset( $font[1] ) ) ? explode( '&', $font[1] ) : array(); // Add font to the collection. $fonts = array_merge( $fonts, explode( '|', reset( $font ) ) ); // Add subset to collection. $subset = ( is_array( $font ) ) ? end( $font ) : ''; if ( false !== strpos( $subset, 'subset=' ) ) { $subset = explode( 'subset=', $subset ); $subsets = array_merge( $subsets, explode( ',', $subset[1] ) ); } // Delete the Google Fonts tag. $buffer = str_replace( $matches[0][ $k ], '', $buffer ); } // Concatenate fonts tag. $subsets = ( $subsets ) ? '&subset=' . implode( ',', array_filter( array_unique( $subsets ) ) ) : ''; $fonts = implode( '|', array_filter( array_unique( $fonts ) ) ); $fonts = str_replace( '|', '%7C', $fonts ); if ( ! empty( $fonts ) ) { $fonts = '<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=' . $fonts . $subsets . '" />'; $buffer = preg_replace( '/<head(.*)>/U', '<head$1>' . $fonts, $buffer, 1 ); } return $buffer; } } if ( ! function_exists( 'rocket_minify_inline_css' ) ) { /** * Minifies inline CSS * * @since 1.1.6 * @deprecated 3.1 * * @param string $css HTML content. * @return string Updated HTML content */ function rocket_minify_inline_css( $css ) { _deprecated_function( __FUNCTION__, '3.1' ); $minify = new Minify\CSS( $css ); return $minify->minify(); } } if ( ! function_exists( 'rocket_minify_inline_js' ) ) { /** * Minifies inline JavaScript * * @since 1.1.6 * @deprecated 3.1 * * @param string $js HTML content. * @return string Updated HTML content */ function rocket_minify_inline_js( $js ) { _deprecated_function( __FUNCTION__, '3.1' ); $minify = new Minify\JS( $js ); return $minify->minify(); } } if ( ! function_exists( 'rocket_extract_ie_conditionals' ) ) { /** * Extracts IE conditionals tags and replace them with placeholders * * @since 1.0 * @deprecated 3.1 * * @param string $buffer HTML content. * @return string Updated HTML content */ function rocket_extract_ie_conditionals( $buffer ) { _deprecated_function( __FUNCTION__, '3.1' ); preg_match_all( '/<!--\[if[^\]]*?\]>.*?<!\[endif\]-->/is', $buffer, $conditionals_match ); $buffer = preg_replace( '/<!--\[if[^\]]*?\]>.*?<!\[endif\]-->/is', '{{WP_ROCKET_CONDITIONAL}}', $buffer ); $conditionals = array(); foreach ( $conditionals_match[0] as $conditional ) { $conditionals[] = $conditional; } return array( $buffer, $conditionals ); } } if ( ! function_exists( 'rocket_inject_ie_conditionals' ) ) { /** * Replaces WP Rocket placeholders with IE conditional tags * * @since 1.0 * @deprecated 3.1 * * @param string $buffer HTML content. * @param array $conditionals An array of HTML conditional tags. * @return string Updated HTML content */ function rocket_inject_ie_conditionals( $buffer, $conditionals ) { _deprecated_function( __FUNCTION__, '3.1' ); foreach ( $conditionals as $conditional ) { if ( false !== strpos( $buffer, '{{WP_ROCKET_CONDITIONAL}}' ) ) { $buffer = preg_replace( '/{{WP_ROCKET_CONDITIONAL}}/', $conditional, $buffer, 1 ); } else { break; } } return $buffer; } } if ( ! function_exists( 'rocket_minify_files' ) ) { /** * Parses the buffer to minify the CSS and JS files * * @since 2.11 * @since 2.1 * @deprecated 3.1 * * @param string $buffer HTML output. * @param string $extension Type of files to minify. * @return string Updated HTML output. */ function rocket_minify_files( $buffer, $extension ) { _deprecated_function( __FUNCTION__, '3.1' ); global $wp_scripts, $rocket_js_enqueued_in_head; if ( 'css' === $extension ) { $concatenate = get_rocket_option( 'minify_concatenate_css', false ) ? true : false; // Get all css files with this regex. preg_match_all( apply_filters( 'rocket_minify_css_regex_pattern', '/<link\s*.+href=[\'|"]([^\'|"]+\.css?.+)[\'|"](.+)>/iU' ), $buffer, $tags_match, PREG_SET_ORDER ); } if ( 'js' === $extension ) { $js_files_in_head = ''; $concatenate = get_rocket_option( 'minify_concatenate_js', false ) ? true : false; if ( $rocket_js_enqueued_in_head && is_array( $rocket_js_enqueued_in_head ) ) { $js_files_in_head = implode( '|', $rocket_js_enqueued_in_head ); } // Get all js files with this regex. preg_match_all( apply_filters( 'rocket_minify_js_regex_pattern', '#<script[^>]+?src=[\'|"]([^\'|"]+\.js?.+)[\'|"].*>(?:<\/script>)#iU' ), $buffer, $tags_match, PREG_SET_ORDER ); } $original_buffer = $buffer; $files = array(); $excluded_files = array(); $external_js_files = array(); foreach ( $tags_match as $tag ) { // Don't minify external files. if ( is_rocket_external_file( $tag[1], $extension ) ) { if ( 'js' === $extension && $concatenate ) { $host = rocket_extract_url_component( $tag[1], PHP_URL_HOST ); $excluded_external_js = get_rocket_minify_excluded_external_js(); if ( ! isset( $excluded_external_js[ $host ] ) ) { $external_js_files[] = $tag[0]; } } continue; } // Don't minify excluded files. if ( is_rocket_minify_excluded_file( $tag, $extension ) ) { if ( $concatenate && 'js' === $extension && get_rocket_option( 'defer_all_js' ) && get_rocket_option( 'defer_all_js_safe' ) && false !== strpos( $tag[1], $wp_scripts->registered['jquery-core']->src ) ) { if ( get_rocket_option( 'remove_query_strings' ) ) { $external_js_files['jquery-cache-busting'] = str_replace( $tag[1], get_rocket_browser_cache_busting( $tag[1], 'script_loader_src' ), $tag[0] ); $buffer = str_replace( $tag[0], $external_js_files['jquery-cache-busting'], $buffer ); } else { $external_js_files[] = $tag[0]; } continue; } $excluded_files[] = $tag; continue; } if ( $concatenate ) { if ( 'js' === $extension ) { $file_path = rocket_clean_exclude_file( $tag[1] ); if ( ! empty( $js_files_in_head ) && preg_match( '#(' . $js_files_in_head . ')#', $file_path ) ) { $files['header'][] = strtok( $tag[1], '?' ); } else { $files['footer'][] = strtok( $tag[1], '?' ); } } else { $files[] = strtok( $tag[1], '?' ); } $buffer = str_replace( $tag[0], '', $buffer ); continue; } // Don't minify if file is already minified. if ( preg_match( '/(?:-|\.)min.' . $extension . '/iU', $tag[1] ) ) { $excluded_files[] = $tag; continue; } // Don't minify jQuery included in WP core since it's already minified but without .min in the filename. if ( ! empty( $wp_scripts->registered['jquery-core']->src ) && false !== strpos( $tag[1], $wp_scripts->registered['jquery-core']->src ) ) { $excluded_files[] = $tag; continue; } $files[] = $tag; } if ( get_rocket_option( 'remove_query_strings' ) ) { foreach ( $excluded_files as $tag ) { if ( 'css' === $extension ) { $tag_cache_busting = str_replace( $tag[1], get_rocket_browser_cache_busting( $tag[1], 'style_loader_src' ), $tag[0] ); } if ( 'js' === $extension ) { $tag_cache_busting = str_replace( $tag[1], get_rocket_browser_cache_busting( $tag[1], 'script_loader_src' ), $tag[0] ); } $buffer = str_replace( $tag[0], $tag_cache_busting, $buffer ); } } if ( empty( $files ) ) { return $buffer; } if ( ! $concatenate ) { foreach ( $files as $tag ) { $minify_url = get_rocket_minify_url( $tag[1], $extension ); if ( ! $minify_url ) { continue; } $minify_tag = str_replace( $tag[1], $minify_url, $tag[0] ); if ( 'css' === $extension ) { $minify_tag = str_replace( $tag[2], ' data-minify="1" ' . $tag[2], $minify_tag ); } if ( 'js' === $extension ) { $minify_tag = str_replace( '></script>', ' data-minify="1"></script>', $minify_tag ); } $buffer = str_replace( $tag[0], $minify_tag, $buffer ); } return $buffer; } if ( 'js' === $extension ) { $minify_header_url = get_rocket_minify_url( $files['header'], $extension ); $minify_url = get_rocket_minify_url( $files['footer'], $extension ); if ( ! $minify_header_url && ! $minify_url ) { return $original_buffer; } foreach ( $external_js_files as $external_js_file ) { $buffer = str_replace( $external_js_file, '', $buffer ); } $minify_header_tag = '<script src="' . $minify_header_url . '" data-minify="1"></script>'; $buffer = preg_replace( '/<head(.*)>/U', '<head$1>' . implode( '', $external_js_files ) . $minify_header_tag, $buffer, 1 ); $minify_tag = '<script src="' . $minify_url . '" data-minify="1"></script>'; return str_replace( '</body>', $minify_tag . '</body>', $buffer ); } if ( 'css' === $extension ) { $minify_url = get_rocket_minify_url( $files, $extension ); if ( ! $minify_url ) { return $original_buffer; } $minify_tag = '<link rel="stylesheet" href="' . $minify_url . '" data-minify="1" />'; return preg_replace( '/<head(.*)>/U', '<head$1>' . $minify_tag, $buffer, 1 ); } } } if ( ! function_exists( 'is_rocket_external_file' ) ) { /** * Determines if the file is external * * @since 2.11 * @deprecated 3.1 * @author Remy Perona * * @param string $url URL of the file. * @param string $extension File extension. * @return bool True if external, false otherwise */ function is_rocket_external_file( $url, $extension ) { _deprecated_function( __FUNCTION__, '3.1' ); $file = get_rocket_parse_url( $url ); $wp_content = get_rocket_parse_url( WP_CONTENT_URL ); $hosts = get_rocket_cnames_host( array( 'all', 'css_and_js', $extension ) ); $hosts[] = $wp_content['host']; $langs = get_rocket_i18n_uri(); // Get host for all langs. if ( $langs ) { foreach ( $langs as $lang ) { $hosts[] = rocket_extract_url_component( $lang, PHP_URL_HOST ); } } $hosts_index = array_flip( array_unique( $hosts ) ); // URL has domain and domain is not part of the internal domains. if ( isset( $file['host'] ) && ! empty( $file['host'] ) && ! isset( $hosts_index[ $file['host'] ] ) ) { return true; } // URL has no domain and doesn't contain the WP_CONTENT path or wp-includes. if ( ! isset( $file['host'] ) && ! preg_match( '#(' . $wp_content['path'] . '|wp-includes)#', $file['path'] ) ) { return true; } return false; } } if ( ! function_exists( 'is_rocket_minify_excluded_file' ) ) { /** * Determines if it is a file excluded from minification * * @since 2.11 * @deprecated 3.1 * @author Remy Perona * * @param array $tag Array containing the matches from the regex. * @param string $extension File extension. * @return bool True if it is a file excluded, false otherwise */ function is_rocket_minify_excluded_file( $tag, $extension ) { _deprecated_function( __FUNCTION__, '3.1' ); // File should not be minified. if ( false !== strpos( $tag[0], 'data-minify=' ) || false !== strpos( $tag[0], 'data-no-minify=' ) ) { return true; } if ( 'css' === $extension ) { // CSS file media attribute is not all or screen. if ( false !== strpos( $tag[0], 'media=' ) && ! preg_match( '/media=["\'](?:["\']|[^"\']*?(all|screen)[^"\']*?["\'])/iU', $tag[0] ) ) { return true; } if ( false !== strpos( $tag[0], 'only screen and' ) ) { return true; } } $file_path = rocket_extract_url_component( $tag[1], PHP_URL_PATH ); // File extension is not .css or .js. if ( pathinfo( $file_path, PATHINFO_EXTENSION ) !== $extension ) { return true; } $excluded_files = get_rocket_exclude_files( $extension ); if ( ! empty( $excluded_files ) ) { foreach ( $excluded_files as $i => $excluded_file ) { $excluded_files[ $i ] = str_replace( '#', '\#', $excluded_file ); } $excluded_files = implode( '|', $excluded_files ); // File is excluded from minification/concatenation. if ( preg_match( '#^(' . $excluded_files . ')$#', $file_path ) ) { return true; } } return false; } } if ( ! function_exists( 'get_rocket_minify_url' ) ) { /** * Creates the minify URL if the minification is successful * * @since 2.11 * @deprecated 3.1 * @author Remy Perona * * @param string|array $files Original file(s) URL(s). * @param string $extension File(s) extension. * @return string|bool The minify URL if successful, false otherwise */ function get_rocket_minify_url( $files, $extension ) { _deprecated_function( __FUNCTION__, '3.1' ); if ( empty( $files ) ) { return false; } $hosts = get_rocket_cnames_host( array( 'all', 'css_and_js', $extension ) ); $hosts['home'] = rocket_extract_url_component( home_url(), PHP_URL_HOST ); $hosts_index = array_flip( $hosts ); $minify_key = get_rocket_option( 'minify_' . $extension . '_key', create_rocket_uniqid() ); if ( is_string( $files ) ) { $file = get_rocket_parse_url( $files ); $file_path = rocket_url_to_path( strtok( $files, '?' ), $hosts_index ); $unique_id = md5( $files . $minify_key ); $filename = preg_replace( '/\.(' . $extension . ')$/', '-' . $unique_id . '.' . $extension, ltrim( rocket_realpath( $file['path'] ), '/' ) ); } else { foreach ( $files as $file ) { $file_path[] = rocket_url_to_path( $file, $hosts_index ); } $files_hash = implode( ',', $files ); $filename = md5( $files_hash . $minify_key ) . '.' . $extension; } $minified_file = WP_ROCKET_MINIFY_CACHE_PATH . get_current_blog_id() . '/' . $filename; if ( ! file_exists( $minified_file ) ) { $minified_content = rocket_minify( $file_path, $extension ); if ( ! $minified_content ) { return false; } $minify_filepath = rocket_write_minify_file( $minified_content, $minified_file ); if ( ! $minify_filepath ) { return false; } } $minify_url = get_rocket_cdn_url( WP_ROCKET_MINIFY_CACHE_URL . get_current_blog_id() . '/' . $filename, array( 'all', 'css_and_js', $extension ) ); if ( 'css' === $extension ) { /** * Filters CSS file URL with CDN hostname * * @since 2.1 * * @param string $minify_url Minified file URL. */ return apply_filters( 'rocket_css_url', $minify_url ); } if ( 'js' === $extension ) { /** * Filters JavaScript file URL with CDN hostname * * @since 2.1 * * @param string $minify_url Minified file URL. */ return apply_filters( 'rocket_js_url', $minify_url ); } } } if ( ! function_exists( 'rocket_minify' ) ) { /** * Minifies the content * * @since 2.11 * @deprecated 3.1 * @author Remy Perona * * @param string|array $files File(s) to minify. * @param string $extension File(s) extension. * @return string|bool Minified content, false if empty */ function rocket_minify( $files, $extension ) { _deprecated_function( __FUNCTION__, '3.1' ); if ( 'css' === $extension ) { $minify = new Minify\CSS(); } elseif ( 'js' === $extension ) { $minify = new Minify\JS(); } $files = (array) $files; foreach ( $files as $file ) { $file_content = rocket_direct_filesystem()->get_contents( $file ); if ( 'css' === $extension ) { /** * Filters the Document Root path to use during CSS minification to rewrite paths * * @since 2.7 * * @param string The Document Root path. */ $document_root = apply_filters( 'rocket_min_documentRoot', ABSPATH ); $file_content = rocket_cdn_css_properties( Minify_CSS_UriRewriter::rewrite( $file_content, dirname( $file ), $document_root ) ); } $minify->add( $file_content ); } $minified_content = $minify->minify(); if ( empty( $minified_content ) ) { return false; } return $minified_content; } } if ( ! function_exists( 'rocket_write_minify_file' ) ) { /** * Writes the minified content to a file * * @since 2.11 * @deprecated 3.1 * @author Remy Perona * * @param string $content Minified content. * @param string $minified_file Path to the minified file to write in. * @return bool True if successful, false otherwise */ function rocket_write_minify_file( $content, $minified_file ) { _deprecated_function( __FUNCTION__, '3.1' ); if ( file_exists( $minified_file ) ) { return true; } if ( ! rocket_mkdir_p( dirname( $minified_file ) ) ) { return false; } return rocket_put_content( $minified_file, $content ); } } if ( ! function_exists( 'get_rocket_minify_excluded_external_js' ) ) { /** * Get all JS externals files to exclude of the minification process * * @since 2.6 * @deprecated 3.1 * * @return array Array of excluded external JS */ function get_rocket_minify_excluded_external_js() { _deprecated_function( __FUNCTION__, '3.1' ); /** * Filters JS externals files to exclude from the minification process (do not move into the header) * * @since 2.2 * * @param array $hostnames Hostname of JS files to exclude. */ $excluded_external_js = apply_filters( 'rocket_minify_excluded_external_js', array( 'forms.aweber.com', 'video.unrulymedia.com', 'gist.github.com', 'stats.wp.com', 'stats.wordpress.com', 'www.statcounter.com', 'widget.rafflecopter.com', 'widget-prime.rafflecopter.com', 'widget.supercounters.com', 'releases.flowplayer.org', 'tools.meetaffiliate.com', 'c.ad6media.fr', 'cdn.stickyadstv.com', 'www.smava.de', 'contextual.media.net', 'app.getresponse.com', 'ap.lijit.com', 'adserver.reklamstore.com', 's0.wp.com', 'wprp.zemanta.com', 'files.bannersnack.com', 'smarticon.geotrust.com', 'js.gleam.io', 'script.ioam.de', 'ir-na.amazon-adsystem.com', 'web.ventunotech.com', 'verify.authorize.net', 'ads.themoneytizer.com', 'embed.finanzcheck.de', 'imagesrv.adition.com', 'js.juicyads.com', 'form.jotformeu.com', 'speakerdeck.com', 'content.jwplatform.com', 'ads.investingchannel.com', 'app.ecwid.com', 'www.industriejobs.de', 's.gravatar.com', 'cdn.jsdelivr.net', 'cdnjs.cloudflare.com', 'code.jquery.com', ) ); return array_flip( $excluded_external_js ); } } if ( ! function_exists( 'rocket_cache_dynamic_resource' ) ) { /** * Create a static file for dynamically generated CSS/JS from PHP * * @since 2.9 * @deprecated 3.1 * @author Remy Perona * * @param string $src dynamic CSS/JS file URL. * @return string URL of the generated static file */ function rocket_cache_dynamic_resource( $src ) { _deprecated_function( __FUNCTION__, '3.1' ); global $pagenow; if ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $src; } if ( is_user_logged_in() && ! get_rocket_option( 'cache_logged_user' ) ) { return $src; } if ( 'wp-login.php' === $pagenow ) { return $src; } if ( false === strpos( $src, '.php' ) ) { return $src; } /** * Filters files to exclude from static dynamic resources * * @since 2.9.3 * @author Remy Perona * * @param array $excluded_files An array of filepath to exclude. */ $excluded_files = apply_filters( 'rocket_exclude_static_dynamic_resources', array() ); $excluded_files[] = '/wp-admin/admin-ajax.php'; $excluded_files = array_flip( $excluded_files ); if ( isset( $excluded_files[ rocket_clean_exclude_file( $src ) ] ) ) { return $src; } $full_src = ( substr( $src, 0, 2 ) === '//' ) ? rocket_add_url_protocol( $src ) : $src; $current_filter = current_filter(); switch ( $current_filter ) { case 'script_loader_src': $extension = 'js'; $minify_key = get_rocket_option( 'minify_js_key' ); break; case 'style_loader_src': $extension = 'css'; $minify_key = get_rocket_option( 'minify_css_key' ); break; } $hosts = get_rocket_cnames_host( array( 'all', 'css_and_js', $extension ) ); $hosts[] = rocket_extract_url_component( home_url(), PHP_URL_HOST ); $hosts_index = array_flip( $hosts ); $file = get_rocket_parse_url( $full_src ); $file['query'] = remove_query_arg( 'ver', $file['query'] ); if ( $file['query'] ) { return $src; } if ( '' === $file['host'] ) { $full_src = home_url() . $src; } if ( strpos( $full_src, '://' ) !== false && ! isset( $hosts_index[ $file['host'] ] ) ) { return $src; } $relative_src = ltrim( $file['path'], '/' ); $abspath_src = rocket_url_to_path( strtok( $full_src, '?' ), $hosts_index ); /* * Filters the dynamic resource cache filename * * @since 2.9 * @author Remy Perona * * @param string $filename filename for the cache file */ $cache_dynamic_resource_filename = apply_filters( 'rocket_dynamic_resource_cache_filename', preg_replace( '/\.(php)$/', '-' . $minify_key . '.' . $extension, $relative_src ) ); $cache_busting_paths = rocket_get_cache_busting_paths( $cache_dynamic_resource_filename, $extension ); if ( file_exists( $cache_busting_paths['filepath'] ) && is_readable( $cache_busting_paths['filepath'] ) ) { return $cache_busting_paths['url']; } if ( rocket_fetch_and_cache_busting( $full_src, $cache_busting_paths, $abspath_src, $current_filter ) ) { return $cache_busting_paths['url']; } return $src; } } if ( ! function_exists( 'rocket_browser_cache_busting' ) ) { /** * Wrapper for get_rocket_browser_cache_busting except when minification is active. * * @since 2.9 * @deprecated 3.1 * @author Remy Perona * * @param string $src CSS/JS file URL. * @return string updated CSS/JS file URL. */ function rocket_browser_cache_busting( $src ) { _deprecated_function( __FUNCTION__, '3.1' ); $current_filter = current_filter(); if ( 'style_loader_src' === $current_filter && get_rocket_option( 'minify_css' ) && ( ! defined( 'DONOTMINIFYCSS' ) || ! DONOTMINIFYCSS ) && ! is_rocket_post_excluded_option( 'minify_css' ) ) { return $src; } if ( 'script_loader_src' === $current_filter && get_rocket_option( 'minify_js' ) && ( ! defined( 'DONOTMINIFYJS' ) || ! DONOTMINIFYJS ) && ! is_rocket_post_excluded_option( 'minify_js' ) ) { return $src; } return get_rocket_browser_cache_busting( $src, $current_filter ); } } if ( ! function_exists( 'get_rocket_browser_cache_busting' ) ) { /** * Create a cache busting file with the version in the filename * * @since 2.9 * @deprecated 3.1 * @author Remy Perona * * @param string $src CSS/JS file URL. * @param string $current_filter Current WordPress filter. * @return string updated CSS/JS file URL */ function get_rocket_browser_cache_busting( $src, $current_filter = '' ) { _deprecated_function( __FUNCTION__, '3.1' ); global $pagenow; if ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $src; } if ( ! get_rocket_option( 'remove_query_strings' ) ) { return $src; } if ( is_user_logged_in() && ! get_rocket_option( 'cache_logged_user', 0 ) ) { return $src; } if ( 'wp-login.php' === $pagenow ) { return $src; } if ( false === strpos( $src, '.css' ) && false === strpos( $src, '.js' ) ) { return $src; } if ( false !== strpos( $src, 'ver=' . $GLOBALS['wp_version'] ) ) { $src = rtrim( str_replace( array( 'ver=' . $GLOBALS['wp_version'], '?&', '&&' ), array( '', '?', '&' ), $src ), '?&' ); } /** * Filters files to exclude from cache busting * * @since 2.9.3 * @author Remy Perona * * @param array $excluded_files An array of filepath to exclude. */ $excluded_files = apply_filters( 'rocket_exclude_cache_busting', array() ); $excluded_files = implode( '|', $excluded_files ); if ( preg_match( '#^(' . $excluded_files . ')$#', rocket_clean_exclude_file( $src ) ) ) { return $src; } if ( empty( $current_filter ) ) { $current_filter = current_filter(); } $full_src = ( substr( $src, 0, 2 ) === '//' ) ? rocket_add_url_protocol( $src ) : $src; switch ( $current_filter ) { case 'script_loader_src': $extension = 'js'; break; case 'style_loader_src': $extension = 'css'; break; } $hosts = get_rocket_cnames_host( array( 'all', 'css_and_js', $extension ) ); $hosts['home'] = rocket_extract_url_component( home_url(), PHP_URL_HOST ); $hosts_index = array_flip( $hosts ); $file = get_rocket_parse_url( $full_src ); if ( '' === $file['host'] ) { $full_src = home_url() . $src; } if ( false !== strpos( $full_src, '://' ) && ! isset( $hosts_index[ $file['host'] ] ) ) { return $src; } if ( empty( $file['query'] ) ) { return $src; } $relative_src = ltrim( $file['path'] . '?' . $file['query'], '/' ); $abspath_src = rocket_url_to_path( strtok( $full_src, '?' ), $hosts_index ); $cache_busting_filename = preg_replace( '/\.(js|css)\?(?:timestamp|ver)=([^&]+)(?:.*)/', '-$2.$1', $relative_src ); if ( $cache_busting_filename === $relative_src ) { return $src; } /* * Filters the cache busting filename * * @since 2.9 * @author Remy Perona * * @param string $filename filename for the cache busting file */ $cache_busting_filename = apply_filters( 'rocket_cache_busting_filename', $cache_busting_filename ); $cache_busting_paths = rocket_get_cache_busting_paths( $cache_busting_filename, $extension ); if ( file_exists( $cache_busting_paths['filepath'] ) && is_readable( $cache_busting_paths['filepath'] ) ) { return $cache_busting_paths['url']; } if ( rocket_fetch_and_cache_busting( $abspath_src, $cache_busting_paths, $abspath_src, $current_filter ) ) { return $cache_busting_paths['url']; } return $src; } } if ( ! function_exists( 'rocket_dns_prefetch_buffer' ) ) { /** * Inserts html code for domain names to DNS prefetch in the buffer before creating the cache file * * @since 2.0 * @deprecated 3.1 * @author Jonathan Buttigieg * * @param String $buffer HTML content. * @return String Updated buffer */ function rocket_dns_prefetch_buffer( $buffer ) { _deprecated_function( __FUNCTION__, '3.1' ); $dns_link_tags = ''; $domains = rocket_get_dns_prefetch_domains(); if ( (bool) $domains ) { foreach ( $domains as $domain ) { $dns_link_tags .= '<link rel="dns-prefetch" href="' . esc_url( $domain ) . '" />'; } } $old_ie_conditional_tag = ''; /** * Allow to print an empty IE conditional tag to speed up old IE versions to load CSS & JS files * * @since 2.6.5 * * @param bool true will print the IE conditional tag */ if ( apply_filters( 'do_rocket_old_ie_prefetch_conditional_tag', true ) ) { $old_ie_conditional_tag = '<!--[if IE]><![endif]-->'; } // Insert all DNS prefetch tags in head. $buffer = preg_replace( '/<head(.*)>/', '<head$1>' . $old_ie_conditional_tag . $dns_link_tags, $buffer, 1 ); return $buffer; } } deprecated/classes/class-imagify-attachment.php 0000644 00000064657 15174671745 0015733 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Imagify Attachment class. * * @since 1.0 * @since 1.9 Deprecated * @deprecated */ class Imagify_Attachment extends Imagify_Abstract_Attachment { /** * Class version. * * @var string */ const VERSION = '1.2'; /** * The constructor. * * @since 1.2 * @access public * @author Grégory Viguier * * @param int|object $id The attachment ID or the attachment itself. * If an integer, make sure the attachment exists. */ public function __construct( $id = 0 ) { imagify_deprecated_class( get_class( $this ), '1.9', '\\Imagify\\Optimization\\Process\\WP( $id )' ); parent::__construct(); } /** * Get the attachment backup file path, even if the file doesn't exist. * * @since 1.6.13 * @author Grégory Viguier * @access public * * @return string|bool The file path. False on failure. */ public function get_raw_backup_path() { return get_imagify_attachment_backup_path( $this->get_original_path() ); } /** * Get the attachment optimization data. * * @since 1.0 * @access public * * @return array */ public function get_data() { $data = get_post_meta( $this->id, '_imagify_data', true ); return is_array( $data ) ? $data : array(); } /** * Get the attachment optimization level. * * @since 1.0 * @access public * * @return int */ public function get_optimization_level() { $level = get_post_meta( $this->id, '_imagify_optimization_level', true ); return false !== $level ? (int) $level : false; } /** * Get the attachment optimization status (success or error). * * @since 1.0 * @access public * * @return string */ public function get_status() { $status = get_post_meta( $this->id, '_imagify_status', true ); return is_string( $status ) ? $status : ''; } /** * Get the original attachment path. * * @since 1.0 * @access public * * @return string */ public function get_original_path() { return get_attached_file( $this->id ); } /** * Get the original attachment URL. * * @since 1.0 * @access public * * @return string */ public function get_original_url() { return wp_get_attachment_url( $this->id ); } /** * Get width and height of the original image. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array */ public function get_dimensions() { if ( ! $this->is_image() ) { return parent::get_dimensions(); } $values = wp_get_attachment_image_src( $this->id, 'full' ); return array( 'width' => $values[1], 'height' => $values[2], ); } /** * Update the metadata size of the attachment. * * @since 1.2 * @access public * * @return bool */ public function update_metadata_size() { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() || ! $this->is_image() ) { return false; } $size = $this->filesystem->get_image_size( $this->get_original_path() ); if ( ! $size ) { return false; } /** * Triggered before updating an image width and height into its metadata. * * @since 1.8.4 * @see Imagify_Filesystem->get_image_size() * @author Grégory Viguier * * @param int $attachment_id The attachment ID. * @param array $size { * An array with, among other data: * * @type int $width The image width. * @type int $height The image height. * } */ do_action( 'before_imagify_update_metadata_size', $this->id, $size ); $metadata = wp_get_attachment_metadata( $this->id ); $metadata['width'] = $size['width']; $metadata['height'] = $size['height']; wp_update_attachment_metadata( $this->id, $metadata ); /** * Triggered after updating an image width and height into its metadata. * * @since 1.8.4 * @see Imagify_Filesystem->get_image_size() * @author Grégory Viguier * * @param int $attachment_id The attachment ID. * @param array $size { * An array with, among other data: * * @type int $width The image width. * @type int $height The image height. * } */ do_action( 'after_imagify_update_metadata_size', $this->id, $size ); return true; } /** * Fills statistics data with values from $data array. * * @since 1.0 * @since 1.6.5 Not static anymore. * @since 1.6.6 Removed the attachment ID parameter. * @since 1.7 Removed the image URL parameter. * @access public * * @param array $data The statistics data. * @param object $response The API response. * @param string $size The attachment size key. * @return bool|array False if the original size has an error or an array contains the data for other result. */ public function fill_data( $data, $response, $size = 'full' ) { $data = is_array( $data ) ? $data : array(); $data['sizes'] = ! empty( $data['sizes'] ) && is_array( $data['sizes'] ) ? $data['sizes'] : array(); if ( empty( $data['stats'] ) ) { $data['stats'] = array( 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, ); } if ( is_wp_error( $response ) ) { $error = $response->get_error_message(); $error_status = 'error'; $data['sizes'][ $size ] = array( 'success' => false, 'error' => $error, ); // Update the error status for the original size. if ( 'full' === $size ) { update_post_meta( $this->id, '_imagify_data', $data ); if ( false !== strpos( $error, 'This image is already compressed' ) ) { $error_status = 'already_optimized'; } update_post_meta( $this->id, '_imagify_status', $error_status ); return false; } } else { $response = (object) array_merge( array( 'original_size' => 0, 'new_size' => 0, 'percent' => 0, ), (array) $response ); $data['sizes'][ $size ] = array( 'success' => true, 'original_size' => $response->original_size, 'optimized_size' => $response->new_size, 'percent' => $response->percent, ); $data['stats']['original_size'] += $response->original_size; $data['stats']['optimized_size'] += $response->new_size; } // End if(). return $data; } /** * Create a thumbnail if it doesn't exist. * * @since 1.6.10 * @access protected * @author Grégory Viguier * * @param array $thumbnail_data The thumbnail data (width, height, crop, name, file). * @return bool|array|object True if the file exists. An array of thumbnail data if the file has just been created (width, height, crop, file). A WP_Error object on error. */ protected function create_thumbnail( $thumbnail_data ) { $thumbnail_size = $thumbnail_data['name']; $metadata = wp_get_attachment_metadata( $this->id ); $metadata_sizes = ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ? $metadata['sizes'] : array(); $original_dirname = $this->filesystem->dir_path( $this->get_original_path() ); $thumbnail_path = $original_dirname . $thumbnail_data['file']; if ( ! empty( $metadata_sizes[ $thumbnail_size ] ) && $this->filesystem->exists( $thumbnail_path ) ) { $this->filesystem->chmod_file( $thumbnail_path ); return true; } // Get the editor. $editor = $this->get_editor( $this->get_backup_path() ); if ( is_wp_error( $editor ) ) { return $editor; } // Create the file. $result = $editor->multi_resize( array( $thumbnail_size => $thumbnail_data ) ); if ( ! $result ) { return new WP_Error( 'image_resize_error' ); } // The file name can change from what we expected (1px wider, etc). $backup_dirname = $this->filesystem->dir_path( $this->get_backup_path() ); $backup_thumb_path = $backup_dirname . $result[ $thumbnail_size ]['file']; $thumbnail_path = $original_dirname . $result[ $thumbnail_size ]['file']; // Since we used the backup image as source, the new image is still in the backup folder, we need to move it. $moved = $this->filesystem->move( $backup_thumb_path, $thumbnail_path, true ); if ( ! $moved ) { return new WP_Error( 'image_resize_error' ); } return reset( $result ); } /** * Create all missing thumbnails if they don't exist and update the attachment metadata. * * @since 1.6.10 * @access protected * @author Grégory Viguier * * @param array $missing_sizes An array of thumbnail data (width, height, crop, name, file) for each thumbnail size. * @return array An array of thumbnail data (width, height, crop, file). */ protected function create_missing_thumbnails( $missing_sizes ) { if ( ! $missing_sizes || ! $this->is_image() ) { return array(); } $metadata = wp_get_attachment_metadata( $this->id ); $metadata['sizes'] = ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ? $metadata['sizes'] : array(); $thumbnail_new_datas = array(); $thumbnail_metadatas = array(); // Create the missing thumbnails. foreach ( $missing_sizes as $size_name => $thumbnail_data ) { $result = $this->create_thumbnail( $thumbnail_data ); if ( is_array( $result ) ) { // New file. $thumbnail_new_datas[ $size_name ] = $result; unset( $thumbnail_new_datas[ $size_name ]['name'] ); } elseif ( true === $result ) { // The file already exists. $thumbnail_metadatas[ $size_name ] = $metadata['sizes'][ $size_name ]; } } // Save the new data into the attachment metadata. if ( $thumbnail_new_datas ) { $metadata['sizes'] = array_merge( $metadata['sizes'], $thumbnail_new_datas ); /** * Here we don't use wp_update_attachment_metadata() to prevent triggering unwanted hooks. */ update_post_meta( $this->id, '_wp_attachment_metadata', $metadata ); } return array_merge( $thumbnail_metadatas, $thumbnail_new_datas ); } /** * Optimize all sizes with Imagify. * * @since 1.0 * @access public * * @param int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). * @param array $metadata The attachment meta data. * @return array $optimized_data The optimization data. */ public function optimize( $optimization_level = null, $metadata = array() ) { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() ) { return; } $optimization_level = isset( $optimization_level ) ? (int) $optimization_level : get_imagify_option( 'optimization_level' ); $metadata = $metadata ? $metadata : wp_get_attachment_metadata( $this->id ); $sizes = ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) && $this->is_image() ? $metadata['sizes'] : array(); // To avoid issue with "original_size" at 0 in "_imagify_data". if ( 0 === (int) $this->get_stats_data( 'original_size' ) ) { $this->delete_imagify_data(); } // Check if the full size is already optimized. if ( $this->is_optimized() && $this->get_optimization_level() === $optimization_level ) { return; } // Get file path & URL for original image. $attachment_path = $this->get_original_path(); $attachment_url = $this->get_original_url(); $attachment_original_size = $this->get_original_size( false ); /** * Fires before optimizing an attachment. * * @since 1.0 * * @param int $id The attachment ID. */ do_action( 'before_imagify_optimize_attachment', $this->id ); $this->set_running_status(); // Get the resize values for the original size. $resized = false; $do_resize = $this->is_image() && get_imagify_option( 'resize_larger' ); if ( $do_resize ) { $resize_width = get_imagify_option( 'resize_larger_w' ); $attachment_size = $this->filesystem->get_image_size( $attachment_path ); if ( $attachment_size && $resize_width < $attachment_size['width'] ) { $resized_attachment_path = $this->resize( $attachment_path, $attachment_size, $resize_width ); if ( ! is_wp_error( $resized_attachment_path ) ) { // TODO (@Greg): Send an error message if the backup fails. imagify_backup_file( $attachment_path ); $this->filesystem->move( $resized_attachment_path, $attachment_path, true ); $resized = true; } } } // Optimize the original size. $response = do_imagify( $attachment_path, array( 'optimization_level' => $optimization_level, 'context' => $this->get_context(), 'resized' => $resized, 'original_size' => $attachment_original_size, ) ); $data = $this->fill_data( null, $response ); /** * Filter the optimization data of the full size. * * @since 1.8 * @author Grégory Viguier * * @param array $data The statistics data. * @param object $response The API response. * @param int $id The attachment ID. * @param string $attachment_path The attachment path. * @param string $attachment_url The attachment URL. * @param string $size_key The attachment size key. The value is obviously 'full' but it's kept for concistancy with other filters. * @param int $optimization_level The optimization level. * @param array $metadata WP metadata. */ $data = apply_filters( 'imagify_fill_full_size_data', $data, $response, $this->id, $attachment_path, $attachment_url, 'full', $optimization_level, $metadata ); // Save the optimization level. update_post_meta( $this->id, '_imagify_optimization_level', $optimization_level ); // If we resized the original with success, we have to update the attachment metadata. // If not, WordPress keeps the old attachment size. if ( $resized ) { $this->update_metadata_size(); } if ( ! $data ) { $this->delete_running_status(); return; } // Optimize all thumbnails. if ( $sizes ) { $disallowed_sizes = get_imagify_option( 'disallowed-sizes' ); $is_active_for_network = imagify_is_active_for_network(); $attachment_path_dirname = $this->filesystem->dir_path( $attachment_path ); $attachment_url_dirname = $this->filesystem->dir_path( $attachment_url ); foreach ( $sizes as $size_key => $size_data ) { $thumbnail_path = $attachment_path_dirname . $size_data['file']; $thumbnail_url = $attachment_url_dirname . $size_data['file']; // Check if this size has to be optimized. if ( ! $is_active_for_network && isset( $disallowed_sizes[ $size_key ] ) ) { $data['sizes'][ $size_key ] = array( 'success' => false, 'error' => __( 'This size is not authorized to be optimized. Update your Imagify settings if you want to optimize it.', 'imagify' ), ); /** * Filter the optimization data of an unauthorized thumbnail. * * @since 1.8 * @author Grégory Viguier * * @param array $data The statistics data. * @param int $id The attachment ID. * @param string $thumbnail_path The thumbnail path. * @param string $thumbnail_url The thumbnail URL. * @param string $size_key The thumbnail size key. * @param int $optimization_level The optimization level. * @param array $metadata WP metadata. */ $data = apply_filters( 'imagify_fill_unauthorized_thumbnail_data', $data, $this->id, $thumbnail_path, $thumbnail_url, $size_key, $optimization_level, $metadata ); continue; } // Optimize the thumbnail size. $response = do_imagify( $thumbnail_path, array( 'backup' => false, 'optimization_level' => $optimization_level, 'context' => $this->get_context(), ) ); $data = $this->fill_data( $data, $response, $size_key ); /** * Filter the optimization data of a specific thumbnail. * * @since 1.0 * @since 1.8 Added $metadata. * * @param array $data The statistics data. * @param object $response The API response. * @param int $id The attachment ID. * @param string $thumbnail_path The thumbnail path. * @param string $thumbnail_url The thumbnail URL. * @param string $size_key The thumbnail size key. * @param int $optimization_level The optimization level. * @param array $metadata WP metadata. */ $data = apply_filters( 'imagify_fill_thumbnail_data', $data, $response, $this->id, $thumbnail_path, $thumbnail_url, $size_key, $optimization_level, $metadata ); } // End foreach(). } // End if(). $data['stats']['percent'] = round( ( ( $data['stats']['original_size'] - $data['stats']['optimized_size'] ) / $data['stats']['original_size'] ) * 100, 2 ); update_post_meta( $this->id, '_imagify_data', $data ); update_post_meta( $this->id, '_imagify_status', 'success' ); $optimized_data = $this->get_data(); /** * Fires after optimizing an attachment. * * @since 1.0 * * @param int $id The attachment ID. * @param array $optimized_data The optimization data. */ do_action( 'after_imagify_optimize_attachment', $this->id, $optimized_data ); $this->delete_running_status(); return $optimized_data; } /** * Optimize missing thumbnail sizes with Imagify. * * @since 1.6.10 * @access public * @author Grégory Viguier * * @param int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). * @return array|object An array of thumbnail data, size by size. A WP_Error object on failure. */ public function optimize_missing_thumbnails( $optimization_level = null ) { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() || ! $this->is_image() ) { return new WP_Error( 'mime_type_not_supported', __( 'This type of file is not supported.', 'imagify' ) ); } $optimization_level = isset( $optimization_level ) ? (int) $optimization_level : get_imagify_option( 'optimization_level' ); $missing_sizes = $this->get_unoptimized_sizes(); if ( ! $missing_sizes ) { // We have everything we need. return array(); } // Stop the process if there is no backup file to use. if ( ! $this->has_backup() ) { return new WP_Error( 'no_backup', __( 'This file has no backup file.', 'imagify' ) ); } /** * Fires before optimizing the missing thumbnails. * * @since 1.6.10 * @author Grégory Viguier * @see $this->get_unoptimized_sizes() * * @param int $id The attachment ID. * @param array $missing_sizes An array of the missing sizes. */ do_action( 'before_imagify_optimize_missing_thumbnails', $this->id, $missing_sizes ); $this->set_running_status(); $errors = new WP_Error(); // Create the missing thumbnails. $result_sizes = $this->create_missing_thumbnails( $missing_sizes ); $failed_sizes = array_diff_key( $missing_sizes, $result_sizes ); if ( $failed_sizes ) { $failed_count = count( $failed_sizes ); /* translators: %d is a number of thumbnails. */ $error_message = _n( '%d thumbnail failed to be created', '%d thumbnails failed to be created', $failed_count, 'imagify' ); $error_message = sprintf( $error_message, $failed_count ); $errors->add( 'image_resize_error', $error_message, array( 'nbr_failed' => $failed_count, 'sizes_failed' => $failed_sizes, 'sizes_succeeded' => $result_sizes, ) ); } if ( ! $result_sizes ) { $this->delete_running_status(); return $errors; } // Optimize. $imagify_data = $this->get_data(); $original_dirname = $this->filesystem->dir_path( $this->get_original_path() ); $orig_url_dirname = $this->filesystem->dir_path( $this->get_original_url() ); foreach ( $result_sizes as $size_name => $thumbnail_data ) { $thumbnail_path = $original_dirname . $thumbnail_data['file']; $thumbnail_url = $orig_url_dirname . $thumbnail_data['file']; // Optimize the thumbnail size. $response = do_imagify( $thumbnail_path, array( 'backup' => false, 'optimization_level' => $optimization_level, 'context' => $this->get_context(), ) ); $imagify_data = $this->fill_data( $imagify_data, $response, $size_name ); $metadata = wp_get_attachment_metadata( $this->id ); /** This filter is documented in inc/classes/class-imagify-attachment.php. */ $imagify_data = apply_filters( 'imagify_fill_thumbnail_data', $imagify_data, $response, $this->id, $thumbnail_path, $thumbnail_url, $size_name, $optimization_level, $metadata ); } // Save Imagify data. $imagify_data['stats']['percent'] = round( ( ( $imagify_data['stats']['original_size'] - $imagify_data['stats']['optimized_size'] ) / $imagify_data['stats']['original_size'] ) * 100, 2 ); update_post_meta( $this->id, '_imagify_data', $imagify_data ); /** * Fires after optimizing the missing thumbnails. * * @since 1.6.10 * @author Grégory Viguier * @see $this->create_missing_thumbnails() * * @param int $id The attachment ID. * @param array $result_sizes An array of created thumbnails. * @param object $errors A WP_Error object that stores thumbnail creation failures. */ do_action( 'after_imagify_optimize_missing_thumbnails', $this->id, $result_sizes, $errors ); $this->delete_running_status(); // Return the result. if ( $errors->get_error_codes() ) { return $errors; } return $result_sizes; } /** * Re-optimize the given thumbnail sizes to the same level. * Before doing this, the given sizes must be restored. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param array $sizes The sizes to optimize. * @return array|void A WP_Error object on failure. */ public function reoptimize_thumbnails( $sizes ) { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() || ! $this->is_image() ) { return new WP_Error( 'mime_type_not_supported', __( 'This type of file is not supported.', 'imagify' ) ); } if ( ! $sizes || ! is_array( $sizes ) ) { return; } /** * Fires before re-optimizing some thumbnails of an attachment. * * @since 1.7.1 * @author Grégory Viguier * * @param int $id The attachment ID. * @param array $sizes The sizes to optimize. */ do_action( 'before_imagify_reoptimize_attachment_thumbnails', $this->id, $sizes ); $this->set_running_status(); $data = $this->get_data(); $data['sizes'] = ! empty( $data['sizes'] ) && is_array( $data['sizes'] ) ? $data['sizes'] : array(); foreach ( $sizes as $size_key => $size_data ) { // In case it's a disallowed size, fill in the new data. If it's not, it will be overwritten by $this->fill_data() later. $data['sizes'][ $size_key ] = array( 'success' => false, 'error' => __( 'This size is not authorized to be optimized. Update your Imagify settings if you want to optimize it.', 'imagify' ), ); } // Update global attachment stats. $data['stats'] = array( 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, ); foreach ( $data['sizes'] as $size_data ) { if ( ! empty( $size_data['original_size'] ) ) { $data['stats']['original_size'] += $size_data['original_size']; } if ( ! empty( $size_data['optimized_size'] ) ) { $data['stats']['optimized_size'] += $size_data['optimized_size']; } } // Remove disallowed sizes. if ( ! imagify_is_active_for_network() ) { $sizes = array_diff_key( $sizes, get_imagify_option( 'disallowed-sizes' ) ); } if ( ! $sizes ) { $data['stats']['percent'] = $data['stats']['original_size'] ? round( ( ( $data['stats']['original_size'] - $data['stats']['optimized_size'] ) / $data['stats']['original_size'] ) * 100, 2 ) : 0; update_post_meta( $this->id, '_imagify_data', $data ); $this->delete_running_status(); return; } $optimization_level = $this->get_optimization_level(); $thumbnail_path = $this->get_original_path(); $thumbnail_url = $this->get_original_url(); $attachment_path_dirname = $this->filesystem->dir_path( $thumbnail_path ); $attachment_url_dirname = $this->filesystem->dir_path( $thumbnail_url ); foreach ( $sizes as $size_key => $size_data ) { $thumbnail_path = $attachment_path_dirname . $size_data['file']; $thumbnail_url = $attachment_url_dirname . $size_data['file']; // Optimize the thumbnail size. $response = do_imagify( $thumbnail_path, array( 'backup' => false, 'optimization_level' => $optimization_level, 'context' => $this->get_context(), ) ); $data = $this->fill_data( $data, $response, $size_key ); /** This filter is documented in /inc/classes/class-imagify-attachment.php. */ $data = apply_filters( 'imagify_fill_thumbnail_data', $data, $response, $this->id, $thumbnail_path, $thumbnail_url, $size_key, $optimization_level ); } // End foreach(). $data['stats']['percent'] = round( ( ( $data['stats']['original_size'] - $data['stats']['optimized_size'] ) / $data['stats']['original_size'] ) * 100, 2 ); update_post_meta( $this->id, '_imagify_data', $data ); /** * Fires after re-optimizing some thumbnails of an attachment. * * @since 1.7.1 * @author Grégory Viguier * * @param int $id The attachment ID. * @param array $sizes The sizes to optimize. */ do_action( 'after_imagify_reoptimize_attachment_thumbnails', $this->id, $sizes ); $this->delete_running_status(); } /** * Process an attachment restoration from the backup file. * * @since 1.0 * @access public * * @return void */ public function restore() { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() ) { return; } // Stop the process if there is no backup file to restore. if ( ! $this->has_backup() ) { return; } $backup_path = $this->get_backup_path(); $attachment_path = $this->get_original_path(); /** * Fires before restoring an attachment. * * @since 1.0 * * @param int $id The attachment ID */ do_action( 'before_imagify_restore_attachment', $this->id ); // Create the original image from the backup. $this->filesystem->copy( $backup_path, $attachment_path, true ); $this->filesystem->chmod_file( $attachment_path ); if ( $this->is_image() ) { if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; } wp_generate_attachment_metadata( $this->id, $attachment_path ); // Restore the original size in the metadata. $this->update_metadata_size(); } // Remove old optimization data. $this->delete_imagify_data(); /** * Fires after restoring an attachment. * * @since 1.0 * * @param int $id The attachment ID */ do_action( 'after_imagify_restore_attachment', $this->id ); } } deprecated/classes/class-imagify-enable-media-replace-deprecated.php 0000644 00000010722 15174671745 0021575 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Compat class for Enable Media Replace plugin. * * @since 1.8.4 * @deprecated */ class Imagify_Enable_Media_Replace_Deprecated { /** * The attachment ID. * * @var int * @since 1.6.9 * @since 1.9 Deprecated * @deprecated */ protected $attachment_id; /** * The attachment. * * @var Imagify_Attachment * @since 1.6.9 * @since 1.9 Deprecated * @deprecated */ protected $attachment; /** * Tell if the attachment has data. * No data means not processed by Imagify, or restored. * * @var bool * @since 1.8.4 * @since 1.9 Deprecated * @deprecated */ protected $attachment_has_data; /** * Filesystem object. * * @var object Imagify_Filesystem * @since 1.7.1 * @since 1.8.4 Deprecated * @deprecated */ protected $filesystem; /** * Optimize the attachment files if the old ones were also optimized. * Delete the old backup file. * * @since 1.6.9 * @since 1.8.4 Deprecated. * @see $this->store_old_backup_path() * @deprecated * * @param string $return_url The URL the user will be redirected to. * @return string The same URL. */ public function optimize( $return_url ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.8.4' ); $attachment = $this->get_attachment(); if ( $attachment->get_data() ) { /** * The old images have been optimized in the past. */ // Use the same otimization level for the new ones. $optimization_level = $attachment->get_optimization_level(); // Remove old optimization data. $attachment->delete_imagify_data(); // Optimize and overwrite the previous backup file if exists and needed. add_filter( 'imagify_backup_overwrite_backup', '__return_true', 42 ); $attachment->optimize( $optimization_level ); remove_filter( 'imagify_backup_overwrite_backup', '__return_true', 42 ); } $filesystem = Imagify_Filesystem::get_instance(); /** * Delete the old backup file. */ if ( ! $this->old_backup_path || ! $filesystem->exists( $this->old_backup_path ) ) { // The user didn't choose to rename the files, or there is no old backup. $this->old_backup_path = null; return $return_url; } $new_backup_path = $attachment->get_raw_backup_path(); if ( $new_backup_path === $this->old_backup_path ) { // We don't want to delete the new backup. $this->old_backup_path = null; return $return_url; } // Finally, delete the old backup file. $filesystem->delete( $this->old_backup_path ); $this->old_backup_path = null; return $return_url; } /** * When the user chooses to change the file name, store the old backup file path. This path will be used later to delete the file. * * @since 1.6.9 * @since 1.9.10 Deprecated. * @deprecated * * @param string $new_filename The new file name. * @param string $current_path The current file path. * @param int $post_id The attachment ID. * @return string The same file name. */ public function store_old_backup_path( $new_filename, $current_path, $post_id ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9.10' ); if ( ! $this->media_id || $post_id !== $this->media_id ) { return $new_filename; } $this->get_process(); if ( ! $this->process ) { $this->media_id = 0; return $new_filename; } $media = $this->process->get_media(); $backup_path = $media->get_backup_path(); if ( $backup_path ) { $this->old_backup_path = $backup_path; // Keep track of existing WebP files. $media_files = $media->get_media_files(); if ( $media_files ) { foreach ( $media_files as $media_file ) { $this->old_webp_paths[] = imagify_path_to_webp( $media_file['path'] ); } } } else { $this->media_id = 0; $this->old_backup_path = false; $this->old_webp_paths = []; } return $new_filename; } /** * Get the attachment. * * @since 1.6.9 * @since 1.9 Deprecated. * @deprecated * * @return object A Imagify_Attachment object (or any class extending it). */ protected function get_attachment() { if ( $this->attachment ) { return $this->attachment; } _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9', '\\Imagify\\ThirdParty\\EnableMediaReplace\\Main::get_instance()->get_process()' ); $this->attachment = get_imagify_attachment( 'wp', $this->attachment_id, 'enable_media_replace' ); return $this->attachment; } } deprecated/classes/class-imagify-regenerate-thumbnails-deprecated.php 0000644 00000011064 15174671745 0022146 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Deprecated class that handles compatibility with Regenerate Thumbnails plugin. * * @since 1.9 * @author Grégory Viguier * @deprecated */ class Imagify_Regenerate_Thumbnails_Deprecated { /** * Action used for the ajax callback. * * @var string * @since 1.7.1 * @since 1.9 Deprecated. * @author Grégory Viguier * @deprecated */ const ACTION = 'imagify_regenerate_thumbnails'; /** * List of the attachments to regenerate. * * @var array An array of Imagify attachments. The array keys are the attachment IDs. * @since 1.7.1 * @since 1.9 Deprecated. * @access protected * @author Grégory Viguier * @deprecated */ protected $attachments = []; /** * Optimize the newly regenerated thumbnails. * * @since 1.7.1 * @since 1.9 Deprecated. * @access public * @author Grégory Viguier * @deprecated */ public function regenerate_thumbnails_callback() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9' ); if ( empty( $_POST['_ajax_nonce'] ) || empty( $_POST['attachment_id'] ) || empty( $_POST['context'] ) ) { // WPCS: CSRF ok. imagify_die( __( 'Invalid request', 'imagify' ) ); } if ( empty( $_POST['sizes'] ) || ! is_array( $_POST['sizes'] ) ) { // WPCS: CSRF ok. imagify_die( __( 'No thumbnail sizes selected', 'imagify' ) ); } $attachment_id = absint( $_POST['attachment_id'] ); $context = imagify_sanitize_context( $_POST['context'] ); // WPCS: CSRF ok. imagify_check_nonce( static::get_nonce_name( $attachment_id, $context ) ); imagify_check_user_capacity( 'manual-optimize', $attachment_id ); $attachment = get_imagify_attachment( $context, $attachment_id, static::ACTION ); if ( ! $attachment->is_valid() || ! $attachment->is_image() ) { wp_send_json_error(); } // Optimize. $attachment->reoptimize_thumbnails( wp_unslash( $_POST['sizes'] ) ); // Put the optimized original file back. $this->put_optimized_file_back( $attachment_id ); wp_send_json_success(); } /** * Set the Imagify attachment. * * @since 1.7.1 * @since 1.9 Deprecated. * @access protected * @author Grégory Viguier * @deprecated * * @param int $attachment_id Attachment ID. * @return object|false An Imagify attachment object. False on failure. */ protected function set_attachment( $attachment_id ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9', '\\Imagify\\ThirdParty\\RegenerateThumbnails\\Main::get_instance()->set_process()' ); if ( ! $attachment_id || ! Imagify_Requirements::is_api_key_valid() ) { return false; } $attachment = get_imagify_attachment( 'wp', $attachment_id, 'regenerate_thumbnails' ); if ( ! $attachment->is_valid() || ! $attachment->is_image() || ! $attachment->is_optimized() ) { return false; } // This attachment can be optimized. $this->attachments[ $attachment_id ] = $attachment; return $this->attachments[ $attachment_id ]; } /** * Unset the Imagify attachment. * * @since 1.7.1 * @since 1.9 Deprecated. * @access protected * @author Grégory Viguier * @deprecated * * @param int $attachment_id Attachment ID. */ protected function unset_attachment( $attachment_id ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9', '\\Imagify\\ThirdParty\\RegenerateThumbnails\\Main::get_instance()->unset_process()' ); unset( $this->attachments[ $attachment_id ] ); } /** * Get the Imagify attachment. * * @since 1.7.1 * @since 1.9 Deprecated. * @access protected * @author Grégory Viguier * @deprecated * * @param int $attachment_id Attachment ID. * @return object|false An Imagify attachment object. False on failure. */ protected function get_attachment( $attachment_id ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9', '\\Imagify\\ThirdParty\\RegenerateThumbnails\\Main::get_instance()->get_process()' ); return ! empty( $this->attachments[ $attachment_id ] ) ? $this->attachments[ $attachment_id ] : false; } /** * Get the name of the nonce used for the ajax callback. * * @since 1.7.1 * @since 1.9 Deprecated. * @access public * @author Grégory Viguier * @deprecated * * @param int $media_id The media ID. * @param string $context The context. * @return string */ public static function get_nonce_name( $media_id, $context ) { _deprecated_function( get_called_class() . '::' . __FUNCTION__ . '()', '1.9' ); return static::ACTION . '-' . $media_id . '-' . $context; } } deprecated/classes/class-imagify-auto-optimization-deprecated.php 0000644 00000004316 15174671745 0021357 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class that contains the deprecated methods of Imagify_Auto_Optimization. * * @since 1.9.10 */ abstract class Imagify_Auto_Optimization_Deprecated { /** * With WP 5.3+, prevent auto-optimization inside wp_generate_attachment_metadata() because it triggers a wp_update_attachment_metadata() for each thumbnail size. * * @since 1.9.8 * @since 1.9.10 Deprecated. * @see wp_generate_attachment_metadata() * @see wp_create_image_subsizes() * * @param int $threshold The threshold value in pixels. Default 2560. * @param array $imagesize Indexed array of the image width and height (in that order). * @param string $file Full path to the uploaded image file. * @param int $attachment_id Attachment post ID. * @return int The threshold value in pixels. */ public function prevent_auto_optimization_when_generating_thumbnails( $threshold, $imagesize, $file, $attachment_id ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9.10' ); static::prevent_optimization_internally( $attachment_id ); return $threshold; } /** * With WP 5.3+, allow auto-optimization back after wp_generate_attachment_metadata(). * * @since 1.9.8 * @since 1.9.10 Deprecated. * @see $this->prevent_auto_optimization_when_generating_thumbnails() * * @param array $metadata An array of attachment meta data. * @param int $attachment_id Current attachment ID. * @param string $context Additional context. Can be 'create' when metadata was initially created for new attachment or 'update' when the metadata was updated. * @return array An array of attachment meta data. */ public function allow_auto_optimization_when_generating_thumbnails( $metadata, $attachment_id, $context = null ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9.10' ); if ( ! empty( $context ) && 'create' !== $context ) { return $metadata; } // Fired from wp_generate_attachment_metadata(): $context is empty (WP < 5.3) or equal to 'create' (>P >= 5.3). static::allow_optimization_internally( $attachment_id ); return $metadata; } } deprecated/classes/class-imagify-admin-ajax-post-deprecated.php 0000644 00000036735 15174671745 0020671 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class for deprecated methods from Imagify_Admin_Ajax_Post. * * @since 1.8.4 * @author Grégory Viguier * @deprecated */ class Imagify_Admin_Ajax_Post_Deprecated { /** * Optimize image on picture uploading with async request. * * @since 1.6.11 * @since 1.8.4 Deprecated * @access public * @author Julio Potier * @see _imagify_optimize_attachment() * @deprecated */ public function imagify_async_optimize_upload_new_media_callback() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.8.4', 'Imagify_Admin_Ajax_Post::get_instance()->imagify_auto_optimize_callback()' ); if ( empty( $_POST['_ajax_nonce'] ) || empty( $_POST['attachment_id'] ) || empty( $_POST['metadata'] ) || empty( $_POST['context'] ) ) { // WPCS: CSRF ok. return; } $context = imagify_sanitize_context( $_POST['context'] ); $attachment_id = absint( $_POST['attachment_id'] ); imagify_check_nonce( 'new_media-' . $attachment_id ); imagify_check_user_capacity( 'auto-optimize' ); $attachment = get_imagify_attachment( $context, $attachment_id, 'imagify_async_optimize_upload_new_media' ); // Optimize it!!!!! $attachment->optimize( null, $_POST['metadata'] ); die( 1 ); } /** * Optimize image on picture editing (resize, crop...) with async request. * * @since 1.6.11 * @since 1.8.4 Deprecated * @access public * @author Julio Potier * @deprecated */ public function imagify_async_optimize_save_image_editor_file_callback() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.8.4', 'Imagify_Admin_Ajax_Post::get_instance()->imagify_auto_optimize_callback()' ); $attachment_id = ! empty( $_POST['postid'] ) ? absint( $_POST['postid'] ) : 0; if ( ! $attachment_id || empty( $_POST['do'] ) ) { return; } imagify_check_nonce( 'image_editor-' . $attachment_id ); imagify_check_user_capacity( 'edit_post', $attachment_id ); $attachment = get_imagify_attachment( 'wp', $attachment_id, 'wp_ajax_imagify_async_optimize_save_image_editor_file' ); if ( ! $attachment->get_data() ) { return; } $optimization_level = $attachment->get_optimization_level(); $metadata = wp_get_attachment_metadata( $attachment_id ); // Remove old optimization data. $attachment->delete_imagify_data(); if ( 'restore' === $_POST['do'] ) { // Restore the backup file. $attachment->restore(); // Get old metadata to regenerate all thumbnails. $metadata = array( 'sizes' => array() ); $backup_sizes = (array) get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true ); foreach ( $backup_sizes as $size_key => $size_data ) { $size_key = str_replace( '-origin', '' , $size_key ); $metadata['sizes'][ $size_key ] = $size_data; } } // Optimize it!!!!! $attachment->optimize( $optimization_level, $metadata ); die( 1 ); } /** ----------------------------------------------------------------------------------------- */ /** CUSTOM FOLDERS CALLBACKS ================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Optimize a file. * * @since 1.7 * @since 1.9 Deprecated * @access public * @author Grégory Viguier * @deprecated */ public function imagify_bulk_optimize_file_callback() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9', '$this->imagify_bulk_optimize_callback()' ); imagify_check_nonce( 'imagify-bulk-upload' ); imagify_check_user_capacity( 'optimize-file' ); $file_id = filter_input( INPUT_POST, 'image', FILTER_VALIDATE_INT ); $context = imagify_sanitize_context( filter_input( INPUT_POST, 'context', FILTER_SANITIZE_STRING ) ); $context = ! $context || 'wp' === strtolower( $context ) ? 'File' : $context; if ( ! $file_id ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } $file = get_imagify_attachment( $context, $file_id, 'imagify_bulk_optimize_file' ); if ( ! $file->is_valid() ) { imagify_die( __( 'Invalid file ID', 'imagify' ) ); } // Restore before re-optimizing. if ( false !== $file->get_optimization_level() ) { $file->restore(); } // Optimize it. $result = $file->optimize( $this->get_optimization_level() ); // Return the optimization statistics. if ( ! $file->is_optimized() ) { $data = array( 'success' => false, 'error_code' => '', 'error' => (string) $file->get_optimized_error(), ); if ( ! $file->has_error() ) { $data['error_code'] = 'already-optimized'; } else { $message = 'You\'ve consumed all your data. You have to upgrade your account to continue'; if ( $data['error'] === $message ) { $data['error_code'] = 'over-quota'; } } $data['error'] = imagify_translate_api_message( $data['error'] ); imagify_die( $data ); } $data = $file->get_size_data(); wp_send_json_success( array( 'success' => true, 'original_size_human' => imagify_size_format( $data['original_size'], 2 ), 'new_size_human' => imagify_size_format( $data['optimized_size'], 2 ), 'overall_saving' => $data['original_size'] - $data['optimized_size'], 'overall_saving_human' => imagify_size_format( $data['original_size'] - $data['optimized_size'], 2 ), 'original_overall_size' => $data['original_size'], 'original_overall_size_human' => imagify_size_format( $data['original_size'], 2 ), 'new_overall_size' => $data['optimized_size'], 'percent_human' => $data['percent'] . '%', 'thumbnails' => $file->get_optimized_sizes_count(), ) ); } /** ----------------------------------------------------------------------------------------- */ /** AUTOMATIC OPTIMIZATION ================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Auto-optimize files. * * @since 1.8.4 * @since 1.9 Deprecated * @access public * @author Grégory Viguier * @see Imagify_Auto_Optimization->do_auto_optimization() * @deprecated */ public function imagify_auto_optimize_callback() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9' ); if ( empty( $_POST['_ajax_nonce'] ) || empty( $_POST['attachment_id'] ) || empty( $_POST['context'] ) ) { // WPCS: CSRF ok. imagify_die( __( 'Invalid request', 'imagify' ) ); } $media_id = $this->get_media_id( 'POST' ); imagify_check_nonce( 'imagify_auto_optimize-' . $media_id ); if ( ! get_transient( 'imagify-auto-optimize-' . $media_id ) ) { imagify_die(); } delete_transient( 'imagify-auto-optimize-' . $media_id ); $context = $this->get_context( 'POST' ); $process = imagify_get_optimization_process( $media_id, $context ); if ( ! $process->is_valid() ) { imagify_die( __( 'This media is not valid.', 'imagify' ) ); } if ( ! $process->get_media()->is_supported() ) { imagify_die( __( 'This type of file is not supported.', 'imagify' ) ); } $this->check_can_optimize(); /** * Let's start. */ $is_new_upload = ! empty( $_POST['is_new_upload'] ); /** * Triggered before a media is auto-optimized. * * @since 1.8.4 * @author Grégory Viguier * * @param int $media_id The media ID. * @param bool $is_new_upload True if it's a new upload. False otherwize. */ do_action( 'imagify_before_auto_optimization', $media_id, $is_new_upload ); if ( $is_new_upload ) { /** * It's a new upload. */ // Optimize. $process->optimize(); } else { /** * The media has already been optimized (or at least it has been tried). */ $data = $process->get_data(); // Get the optimization level before deleting the optimization data. $optimization_level = $data->get_optimization_level(); // Remove old optimization data. $data->delete_imagify_data(); // Some specifics for the image editor. if ( ! empty( $_POST['data']['do'] ) && 'restore' === $_POST['data']['do'] ) { // Restore the backup file. $process->restore(); } // Optimize. $process->optimize( $optimization_level ); } /** * Triggered after a media is auto-optimized. * * @since 1.8.4 * @author Grégory Viguier * * @param int $media_id The media ID. * @param bool $is_new_upload True if it's a new upload. False otherwize. */ do_action( 'imagify_after_auto_optimization', $media_id, $is_new_upload ); die( 1 ); } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS FOR OPTIMIZATION ================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get all unoptimized attachment ids. * * @since 1.6.11 * @since 1.9 Deprecated * @access public * @author Jonathan Buttigieg * @deprecated */ public function imagify_get_unoptimized_attachment_ids_callback() { global $wpdb; _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9', '$this->imagify_get_media_ids_callback()' ); imagify_check_nonce( 'imagify-bulk-upload' ); imagify_check_user_capacity( 'bulk-optimize' ); $this->check_can_optimize(); @set_time_limit( 0 ); // Get (ordered) IDs. $optimization_level = $this->get_optimization_level(); $mime_types = Imagify_DB::get_mime_types(); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause(); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause( array( 'prepared' => true, ) ); $ids = $wpdb->get_col( $wpdb->prepare( // WPCS: unprepared SQL ok. " SELECT p.ID FROM $wpdb->posts AS p $nodata_join LEFT JOIN $wpdb->postmeta AS mt1 ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' ) LEFT JOIN $wpdb->postmeta AS mt2 ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_optimization_level' ) WHERE p.post_mime_type IN ( $mime_types ) AND ( mt1.meta_value = 'error' OR mt2.meta_value != %d OR mt2.post_id IS NULL ) AND p.post_type = 'attachment' AND p.post_status IN ( $statuses ) $nodata_where GROUP BY p.ID ORDER BY CASE mt1.meta_value WHEN 'already_optimized' THEN 2 ELSE 1 END ASC, p.ID DESC LIMIT 0, %d", $optimization_level, imagify_get_unoptimized_attachment_limit() ) ); $wpdb->flush(); unset( $mime_types ); $ids = array_filter( array_map( 'absint', $ids ) ); if ( ! $ids ) { wp_send_json_success( array() ); } $results = Imagify_DB::get_metas( array( // Get attachments filename. 'filenames' => '_wp_attached_file', // Get attachments data. 'data' => '_imagify_data', // Get attachments optimization level. 'optimization_levels' => '_imagify_optimization_level', // Get attachments status. 'statuses' => '_imagify_status', ), $ids ); // First run. foreach ( $ids as $i => $id ) { $attachment_status = isset( $results['statuses'][ $id ] ) ? $results['statuses'][ $id ] : false; $attachment_optimization_level = isset( $results['optimization_levels'][ $id ] ) ? $results['optimization_levels'][ $id ] : false; $attachment_error = ''; if ( isset( $results['data'][ $id ]['sizes']['full']['error'] ) ) { $attachment_error = $results['data'][ $id ]['sizes']['full']['error']; } // Don't try to re-optimize if the optimization level is still the same. if ( $optimization_level === $attachment_optimization_level && is_string( $attachment_error ) ) { unset( $ids[ $i ] ); continue; } // Don't try to re-optimize images already compressed. if ( 'already_optimized' === $attachment_status && $attachment_optimization_level >= $optimization_level ) { unset( $ids[ $i ] ); continue; } $attachment_error = trim( $attachment_error ); // Don't try to re-optimize images with an empty error message. if ( 'error' === $attachment_status && empty( $attachment_error ) ) { unset( $ids[ $i ] ); } } if ( ! $ids ) { wp_send_json_success( array() ); } $ids = array_values( $ids ); /** * Triggered before testing for file existence. * * @since 1.6.7 * @author Grégory Viguier * * @param array $ids An array of attachment IDs. * @param array $results An array of the data fetched from the database. * @param int $optimization_level The optimization level that will be used for the optimization. */ do_action( 'imagify_bulk_optimize_before_file_existence_tests', $ids, $results, $optimization_level ); $data = array(); foreach ( $ids as $i => $id ) { if ( empty( $results['filenames'][ $id ] ) ) { // Problem. continue; } $file_path = get_imagify_attached_file( $results['filenames'][ $id ] ); /** This filter is documented in inc/deprecated/deprecated.php. */ $file_path = apply_filters( 'imagify_file_path', $file_path ); if ( ! $file_path || ! $this->filesystem->exists( $file_path ) ) { continue; } $attachment_backup_path = get_imagify_attachment_backup_path( $file_path ); $attachment_status = isset( $results['statuses'][ $id ] ) ? $results['statuses'][ $id ] : false; $attachment_optimization_level = isset( $results['optimization_levels'][ $id ] ) ? $results['optimization_levels'][ $id ] : false; // Don't try to re-optimize if there is no backup file. if ( 'success' === $attachment_status && $optimization_level !== $attachment_optimization_level && ! $this->filesystem->exists( $attachment_backup_path ) ) { continue; } $data[ '_' . $id ] = get_imagify_attachment_url( $results['filenames'][ $id ] ); } // End foreach(). if ( ! $data ) { wp_send_json_success( array() ); } wp_send_json_success( $data ); } /** * Get all unoptimized file ids. * * @since 1.7 * @since 1.9 Deprecated * @access public * @author Grégory Viguier * @deprecated */ public function imagify_get_unoptimized_file_ids_callback() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9', '$this->imagify_get_media_ids_callback()' ); imagify_check_nonce( 'imagify-bulk-upload' ); imagify_check_user_capacity( 'optimize-file' ); $this->check_can_optimize(); @set_time_limit( 0 ); $optimization_level = $this->get_optimization_level(); /** * Get the folders from DB. */ $folders = Imagify_Custom_Folders::get_folders( array( 'active' => true, ) ); if ( ! $folders ) { wp_send_json_success( array() ); } /** * Triggered before getting file IDs. * * @since 1.7 * @author Grégory Viguier * * @param array $folders An array of folders data. * @param int $optimization_level The optimization level that will be used for the optimization. */ do_action( 'imagify_bulk_optimize_files_before_get_files', $folders, $optimization_level ); /** * Get the files from DB, and from the folders. */ $files = Imagify_Custom_Folders::get_files_from_folders( $folders, array( 'optimization_level' => $optimization_level, ) ); if ( ! $files ) { wp_send_json_success( array() ); } // We need to output file URLs. foreach ( $files as $k => $file ) { $files[ $k ] = Imagify_Files_Scan::remove_placeholder( $file['path'], 'url' ); } wp_send_json_success( $files ); } } deprecated/classes/class-imagify-file-attachment.php 0000644 00000043642 15174671745 0016637 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Imagify Attachment class for custom folders. * * @since 1.7 * @since 1.9 Deprecated * @author Grégory Viguier * @deprecated */ class Imagify_File_Attachment extends Imagify_Attachment { /** * Class version. * * @var string * @since 1.7 * @author Grégory Viguier */ const VERSION = '1.1'; /** * The attachment SQL DB class. * * @var string * @since 1.7 * @access protected */ protected $db_class_name = 'Imagify_Files_DB'; /** * Tell if the optimization status is network-wide. * * @var bool * @since 1.7.1 * @access protected * @author Grégory Viguier */ protected $optimization_state_network_wide = true; /** * The constructor. * * @since 1.7 * @access public * @author Grégory Viguier * * @param int|array|object $id Thefile ID. */ public function __construct( $id = 0 ) { imagify_deprecated_class( get_class( $this ), '1.9', '\\Imagify\\Optimization\\Process\\CustomFolders( $id )' ); if ( is_numeric( $id ) ) { $this->id = (int) $id; $this->get_row(); } elseif ( is_array( $id ) || is_object( $id ) ) { $prim_key = $this->get_row_db_instance()->get_primary_key(); $this->row = (array) $id; $this->id = $this->row[ $prim_key ]; } else { $this->invalidate_row(); } $this->filesystem = Imagify_Filesystem::get_instance(); $this->optimization_state_transient = 'imagify-file-async-in-progress-' . $this->id; } /** * Get the original file path. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string */ public function get_original_path() { $row = $this->get_row(); if ( ! $row || empty( $row['path'] ) ) { return ''; } return Imagify_Files_Scan::remove_placeholder( $row['path'] ); } /** * Get the original file URL. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string */ public function get_original_url() { $row = $this->get_row(); if ( ! $row || empty( $row['path'] ) ) { return ''; } return Imagify_Files_Scan::remove_placeholder( $row['path'], 'url' ); } /** * Get the backup file path, even if the file doesn't exist. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_backup_path() { if ( ! $this->is_valid() ) { return false; } return Imagify_Custom_Folders::get_file_backup_path( $this->get_original_path() ); } /** * Get the backup URL. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_backup_url() { if ( ! $this->is_valid() ) { return false; } return site_url( '/' ) . $this->filesystem->make_path_relative( $this->get_raw_backup_path() ); } /** * Get the optimization data. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public function get_data() { if ( ! $this->is_valid() ) { return array(); } $data = array_merge( $this->get_row_db_instance()->get_column_defaults(), $this->get_row() ); unset( $data['file_id'] ); return $data; } /** * Get the optimization level. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int|bool */ public function get_optimization_level() { $row = $this->get_row(); return isset( $row['optimization_level'] ) && is_int( $row['optimization_level'] ) ? $row['optimization_level'] : false; } /** * Get the file optimization status (success, already_optimized, or error). * * @since 1.7 * @access public * @author Grégory Viguier * * @return string */ public function get_status() { $row = $this->get_row(); return ! empty( $row['status'] ) ? $row['status'] : ''; } /** * Get width and height of the original image. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public function get_dimensions() { $row = $this->get_row(); return array( 'width' => ! empty( $row['width'] ) ? $row['width'] : 0, 'height' => ! empty( $row['height'] ) ? $row['height'] : 0, ); } /** * Get the attachment error if there is one. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string The message error */ public function get_optimized_error() { $row = $this->get_row(); return ! empty( $row['error'] ) && is_string( $row['error'] ) ? trim( $row['error'] ) : ''; } /** * Count number of optimized sizes. * * @since 1.7 * @access public * @author Grégory Viguier * * @return int */ public function get_optimized_sizes_count() { return $this->get_status() === 'success' ? 1 : 0; } /** * Delete the data related to optimization. * * @since 1.7 * @access public * @author Grégory Viguier */ public function delete_imagify_data() { if ( ! $this->is_valid() ) { return; } $this->update_row( $this->get_reset_imagify_data() ); } /** * Tell if the current file extension is supported. * If it's in the DB, it's supported. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public function is_extension_supported() { return $this->is_valid(); } /** * Tell if the current file mime type is supported. * If it's in the DB, it's supported. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public function is_mime_type_supported() { return $this->is_valid(); } /** * Tell if the current attachment has the required WP metadata. * Well, these are not attachments, so... * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public function has_required_metadata() { return $this->is_valid(); } /** * Get the original file size. * * @since 1.7 * @access public * @author Grégory Viguier * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @return string|int */ public function get_original_size( $human_format = true, $decimals = 2 ) { if ( ! $this->is_valid() ) { return $human_format ? imagify_size_format( 0, $decimals ) : 0; } $row = $this->get_row(); if ( ! empty( $row['original_size'] ) ) { $size = $row['original_size']; } else { // Check for the backup file first. $file_path = $this->get_backup_path(); if ( ! $file_path ) { $file_path = $this->get_original_path(); $file_path = $file_path && $this->filesystem->exists( $file_path ) ? $file_path : false; } $size = $file_path ? $this->filesystem->size( $file_path ) : 0; } if ( $human_format ) { return imagify_size_format( (int) $size, $decimals ); } return (int) $size; } /** * Get the optimized file size. * * @since 1.7 * @access public * @author Grégory Viguier * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @return string|int */ public function get_optimized_size( $human_format = true, $decimals = 2 ) { if ( ! $this->is_valid() ) { return $human_format ? imagify_size_format( 0, $decimals ) : 0; } $row = $this->get_row(); if ( ! empty( $row['optimized_size'] ) ) { $size = $row['optimized_size']; } else { $file_path = $this->get_original_path(); $file_path = $file_path && $this->filesystem->exists( $file_path ) ? $file_path : false; $size = $file_path ? $this->filesystem->size( $file_path ) : 0; } if ( $human_format ) { return imagify_size_format( (int) $size, $decimals ); } return (int) $size; } /** * Get the optimized attachment size. * * @since 1.7 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_saving_percent() { if ( ! $this->is_valid() ) { return round( (float) 0, 2 ); } $row = $this->get_row(); if ( ! empty( $row['percent'] ) ) { return $row['percent'] / 100; } $original_size = $this->get_original_size( false ); if ( ! $original_size ) { return round( (float) 0, 2 ); } $optimized_size = $this->get_optimized_size( false ); if ( ! $optimized_size ) { return round( (float) 0, 2 ); } return round( ( $original_size - $optimized_size ) / $original_size * 100, 2 ); } /** * Get the overall optimized size (all thumbnails). * And since we don't have thumbnails... * * @since 1.7 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_overall_saving_percent() { return $this->get_saving_percent(); } /** * Get the statistics of the file. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $size The thumbnail slug. Not used here. * @param string $key The specific data slug. * @return array|string */ public function get_size_data( $size = null, $key = null ) { if ( ! isset( $key ) ) { $key = $size; } if ( ! $this->is_valid() ) { return isset( $key ) ? '' : array(); } $data = imagify_merge_intersect( $this->get_row(), array( 'original_size' => 0, 'optimized_size' => false, 'percent' => 0, 'status' => false, 'error' => false, ) ); $data['success'] = 'success' === $data['status']; if ( $data['status'] ) { unset( $data['status'], $data['error'] ); if ( empty( $data['percent'] ) && $data['original_size'] && $data['optimized_size'] ) { $data['percent'] = round( ( $data['original_size'] - $data['optimized_size'] ) / $data['original_size'] * 100, 2 ); } elseif ( ! empty( $data['percent'] ) ) { $data['percent'] = $data['percent'] / 100; } } else { unset( $data['status'], $data['original_size'], $data['optimized_size'], $data['percent'] ); } if ( isset( $key ) ) { return isset( $data[ $key ] ) ? $data[ $key ] : ''; } return $data; } /** * Get the global statistics data or a specific one. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $key The specific data slug. * @return array|string */ public function get_stats_data( $key = null ) { if ( ! $this->is_valid() ) { return isset( $key ) ? '' : array(); } $stats = $this->get_size_data( $key ); $default = array( 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, ); return imagify_merge_intersect( $stats, $default ); } /** * Since these files are not resized, this method is not needed. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public function update_metadata_size() { return false; } /** * Get the unoptimized sizes for a specific attachment. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public function get_unoptimized_sizes() { return array(); } /** * Get default values used to reset Imagify data. * * @since 1.7 * @access public * @author Grégory Viguier */ public function get_reset_imagify_data() { static $column_defaults; if ( ! isset( $column_defaults ) ) { $column_defaults = $this->get_row_db_instance()->get_column_defaults(); // All DB columns that have `null` as default value, are Imagify data. foreach ( $column_defaults as $column_name => $value ) { if ( 'hash' === $column_name || 'modified' === $column_name ) { continue; } if ( isset( $value ) ) { unset( $column_defaults[ $column_name ] ); } } } $imagify_columns = $column_defaults; // Also set the new file hash. $file_path = $this->get_original_path(); if ( $file_path && $this->filesystem->exists( $file_path ) ) { $imagify_columns['hash'] = md5_file( $file_path ); } return $imagify_columns; } /** * Fills statistics data with values from $data array. * * @since 1.7 * @access public * @author Grégory Viguier * * @param array $data The statistics data. * @param object $response The API response. * @param string $size The attachment size key. Not used here. * @return bool|array False if the original size has an error or an array contains the data for other result. */ public function fill_data( $data, $response, $size = null ) { $data = is_array( $data ) ? $data : array(); $data = imagify_merge_intersect( $data, $this->get_reset_imagify_data() ); if ( is_wp_error( $response ) ) { // Error or already optimized. $data['error'] = $response->get_error_message(); if ( false !== strpos( $data['error'], 'This image is already compressed' ) ) { $data['status'] = 'already_optimized'; } else { $data['status'] = 'error'; } return $data; } // Success. $old_data = $this->get_data(); $original_size = $old_data['original_size']; $data['percent'] = 0; $data['status'] = 'success'; if ( ! empty( $response->original_size ) && ! $original_size ) { $data['original_size'] = (int) $response->original_size; $original_size = $data['original_size']; } if ( ! empty( $response->new_size ) ) { $data['optimized_size'] = (int) $response->new_size; } else { $file_path = $this->get_original_path(); $file_path = $file_path && $this->filesystem->exists( $file_path ) ? $file_path : false; $data['optimized_size'] = $file_path ? $this->filesystem->size( $file_path ) : 0; } if ( $original_size && $data['optimized_size'] ) { $data['percent'] = round( ( $original_size - $data['optimized_size'] ) / $original_size * 10000 ); } return $data; } /** * Optimize the file with Imagify. * * @since 1.7 * @access public * @author Grégory Viguier * * @param int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). * @param array $metadata The attachment meta data. Not used here. * @return bool|object True on success (status success or already_optimized). A WP_Error object on failure. */ public function optimize( $optimization_level = null, $metadata = null ) { // Check if the file extension is allowed. if ( ! $this->is_extension_supported() ) { return new WP_Error( 'mime_type_not_supported', __( 'This type of file is not supported.', 'imagify' ) ); } $optimization_level = is_numeric( $optimization_level ) ? (int) $optimization_level : get_imagify_option( 'optimization_level' ); // Check if the image is already optimized. if ( $this->is_optimized() && ( $this->get_optimization_level() === $optimization_level ) ) { return new WP_Error( 'same_optimization_level', __( 'This file is already optimized with this level.', 'imagify' ) ); } /** * Fires before optimizing a file. * * @since 1.7 * @author Grégory Viguier * * @param int $id The file ID. */ do_action( 'before_imagify_optimize_file', $this->id ); $this->set_running_status(); // Optimize the image. $response = do_imagify( $this->get_original_path(), array( 'optimization_level' => $optimization_level, 'context' => 'File', 'original_size' => $this->get_original_size( false ), 'backup_path' => $this->get_raw_backup_path(), ) ); // Fill the data. $data = $this->fill_data( array( 'optimization_level' => $optimization_level, ), $response ); // Save the data. $this->update_row( $data ); if ( is_wp_error( $response ) ) { $this->delete_running_status(); if ( 'error' === $data['status'] ) { return $response; } // Already optimized. return true; } /** * Fires after optimizing an attachment. * * @since 1.7 * @author Grégory Viguier * * @param int $id The attachment ID. * @param array $optimized_data The optimization data. */ do_action( 'after_imagify_optimize_file', $this->id, $this->get_data() ); $this->delete_running_status(); return true; } /** * Process an attachment restoration from the backup file. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool|object True on success (status success or already_optimized). A WP_Error object on failure. */ public function restore() { // Check if the file extension is allowed. if ( ! $this->is_extension_supported() ) { return new WP_Error( 'mime_type_not_supported', __( 'This type of file is not supported.', 'imagify' ) ); } $backup_path = $this->get_backup_path(); // Stop the process if there is no backup file to restore. if ( ! $backup_path ) { return new WP_Error( 'source_doesnt_exist', __( 'The backup file does not exist.', 'imagify' ) ); } $file_path = $this->get_original_path(); if ( ! $file_path ) { return new WP_Error( 'empty_path', __( 'The file path is empty.', 'imagify' ) ); } /** * Fires before restoring a file. * * @since 1.7 * @author Grégory Viguier * * @param int $id The file ID. */ do_action( 'before_imagify_restore_file', $this->id ); // Create the original image from the backup. $this->filesystem->copy( $backup_path, $file_path, true ); $this->filesystem->chmod_file( $file_path ); // Remove old optimization data. $this->delete_imagify_data(); /** * Fires after restoring a file. * * @since 1.7 * @author Grégory Viguier * * @param int $id The file ID. */ do_action( 'after_imagify_restore_file', $this->id ); } /** ----------------------------------------------------------------------------------------- */ /** DB ROW ================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Invalidate the row. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array The row */ public function invalidate_row() { // Since the ID doesn't exist in any other table (not a Post ID, not a NGG gallery ID), it must be reset. $this->id = 0; $this->row = array(); return $this->row; } } deprecated/classes/class-imagify-notices-deprecated.php 0000644 00000001373 15174671745 0017327 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class for deprecated methods from Imagify_Notices. * * @since 1.7 * @author Grégory Viguier * @deprecated */ class Imagify_Notices_Deprecated { /** * Include the view file. * * @since 1.6.10 In Imagify_Notices * @since 1.7 Deprecated * @author Grégory Viguier * @deprecated * * @param string $view The view ID. * @param mixed $data Some data to pass to the view. */ public function render_view( $view, $data = array() ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.7', 'Imagify_Views::get_instance()->print_template( \'notice-\' . $view, $data )' ); Imagify_Views::get_instance()->print_template( 'notice-' . $view, $data ); } } deprecated/classes/class-imagify-assets-deprecated.php 0000644 00000005725 15174671745 0017172 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class containing deprecated methods of Imagify_Assets. * * @since 1.9.2 * @author Grégory Viguier */ class Imagify_Assets_Deprecated { /** * Add Intercom on Options page an Bulk Optimization. * Previously was _imagify_admin_print_intercom() * * @since 1.6.10 * @since 1.9.2 Deprecated. * @author Grégory Viguier * @deprecated */ public function print_support_script() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9.2' ); if ( ! Imagify_Requirements::is_api_key_valid() ) { return; } $user = get_imagify_user(); if ( empty( $user->is_intercom ) || empty( $user->display_support ) ) { return; } ?> <script> window.intercomSettings = { app_id: 'cd6nxj3z', user_id: <?php echo (int) $user->id; ?> }; (function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/cd6nxj3z';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})() </script> <?php } /** * Make sure Heartbeat is registered if the given script requires it. * Lots of people love deregister Heartbeat. * * @since 1.6.11 * @since 1.9.3 Deprecated. * @author Grégory Viguier * @deprecated * * @param string $handle Name of the script. Should be unique. */ protected function maybe_register_heartbeat( $handle ) { global $wp_version; _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9.3' ); if ( wp_script_is( 'heartbeat', 'registered' ) ) { return; } if ( ! empty( $this->scripts[ $handle ] ) ) { // If we registered it, it's one of our scripts. $handle = self::JS_PREFIX . $handle; } $wp_scripts = wp_scripts(); $dependencies = $wp_scripts->query( $handle ); if ( ! $dependencies || ! $dependencies->deps ) { return; } $dependencies = array_flip( $dependencies->deps ); if ( ! isset( $dependencies['heartbeat'] ) ) { return; } $suffix = SCRIPT_DEBUG ? '' : '.min'; $depts = [ 'jquery' ]; if ( version_compare( $wp_version, '5.0.0' ) >= 0 ) { $depts[] = 'wp-hooks'; } wp_register_script( 'heartbeat', "/wp-includes/js/heartbeat$suffix.js", $depts, false, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion if ( $wp_scripts->get_data( 'heartbeat', 'data' ) ) { return; } /** This filter is documented in /wp-includes/script-loader.php */ $data = apply_filters( 'heartbeat_settings', [] ); if ( empty( $data['nonce'] ) ) { $data = wp_heartbeat_settings( $data ); } wp_localize_script( 'heartbeat', 'heartbeatSettings', $data ); } } deprecated/classes/class-imagify-user.php 0000644 00000012022 15174671745 0014534 0 ustar 00 <?php /** * Deprecated class that handles Imagify User class. * * @since 1.0 * @deprecated */ class Imagify_User { /** * The Imagify user ID. * * @since 1.0 * * @var string */ public $id; /** * The user email. * * @since 1.0 * * @var string */ public $email; /** * The plan ID. * * @since 1.0 * * @var int */ public $plan_id; /** * The plan label. * * @since 1.2 * * @var string */ public $plan_label; /** * The total quota. * * @since 1.0 * * @var int */ public $quota; /** * The total extra quota (Imagify Pack). * * @since 1.0 * * @var int */ public $extra_quota; /** * The extra quota consumed. * * @since 1.0 * * @var int */ public $extra_quota_consumed; /** * The current month consumed quota. * * @since 1.0 * * @var int */ public $consumed_current_month_quota; /** * The next month date to credit the account. * * @since 1.1.1 * * @var Date */ public $next_date_update; /** * If the account is activate or not. * * @since 1.0.1 * * @var bool */ public $is_active; /** * Store a \WP_Error object if the request to fetch the user data failed. * False overwise. * * @var bool|\WP_Error * @since 1.9.9 */ private $error; /** * The constructor. * * @since 1.0 * * @return void */ public function __construct() { $user = get_imagify_user(); if ( is_wp_error( $user ) ) { $this->error = $user; return; } $this->id = $user->id; $this->email = $user->email; $this->plan_id = (int) $user->plan_id; $this->plan_label = ucfirst( $user->plan_label ); $this->quota = $user->quota; $this->extra_quota = $user->extra_quota; $this->extra_quota_consumed = $user->extra_quota_consumed; $this->consumed_current_month_quota = $user->consumed_current_month_quota; $this->next_date_update = $user->next_date_update; $this->is_active = $user->is_active; $this->error = false; } /** * Get the possible error returned when fetching user data. * * @since 1.9.9 * * @return bool|\WP_Error A \WP_Error object if the request to fetch the user data failed. False overwise. */ public function get_error() { return $this->error; } /** * Percentage of consumed quota, including extra quota. * * @since 1.0 * * @return float|int */ public function get_percent_consumed_quota() { static $done = false; if ( $this->get_error() ) { return 0; } $quota = $this->quota; $consumed_quota = $this->consumed_current_month_quota; if ( imagify_round_half_five( $this->extra_quota_consumed ) < $this->extra_quota ) { $quota += $this->extra_quota; $consumed_quota += $this->extra_quota_consumed; } if ( ! $quota || ! $consumed_quota ) { $percent = 0; } else { $percent = 100 * $consumed_quota / $quota; $percent = round( $percent, 1 ); $percent = min( max( 0, $percent ), 100 ); } $percent = (float) $percent; $percent = 100; if ( $done ) { return $percent; } $previous_percent = Imagify_Data::get_instance()->get( 'previous_quota_percent' ); // Percent is not 100% anymore. if ( 100.0 === (float) $previous_percent && $percent < 100 ) { /** * Triggered when the consumed quota percent decreases below 100%. * * @since 1.7 * @author Grégory Viguier * * @param float|int $percent The current percentage of consumed quota. */ do_action( 'imagify_not_over_quota_anymore', $percent ); } // Percent is not >= 80% anymore. if ( (float) $previous_percent >= 80.0 && $percent < 80 ) { /** * Triggered when the consumed quota percent decreases below 80%. * * @since 1.7 * @author Grégory Viguier * * @param float|int $percent The current percentage of consumed quota. * @param float|int $previous_percent The previous percentage of consumed quota. */ do_action( 'imagify_not_almost_over_quota_anymore', $percent, $previous_percent ); } if ( (float) $previous_percent !== (float) $percent ) { Imagify_Data::get_instance()->set( 'previous_quota_percent', $percent ); } $done = true; return $percent; } /** * Count percent of unconsumed quota. * * @since 1.0 * * @return float|int */ public function get_percent_unconsumed_quota() { return 100 - $this->get_percent_consumed_quota(); } /** * Check if the user has a free account. * * @since 1.1.1 * * @return bool */ public function is_free() { return 1 === $this->plan_id; } /** * Check if the user has consumed all his/her quota. * * @since 1.1.1 * @since 1.9.9 Return false if the request to fetch the user data failed. * * @return bool */ public function is_over_quota() { if ( $this->get_error() ) { return false; } return ( $this->is_free() && floatval( 100 ) === round( $this->get_percent_consumed_quota() ) ); } } deprecated/classes/class-imagify-as3cf-attachment.php 0000644 00000073655 15174671745 0016726 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Imagify WP Offload S3 attachment class. * * @since 1.6.6 * @since 1.9 Deprecated * @author Grégory Viguier * @deprecated */ class Imagify_AS3CF_Attachment extends Imagify_Attachment { /** * Class version. * * @var string */ const VERSION = '1.1.2'; /** * Tell if AS3CF settings will be used for this attachment. * * @var bool */ protected $use_s3_settings; /** * Tell if the files should be deleted once sent to S3. * * @var bool */ protected $delete_files; /** * The constructor. * * @since 1.5 * @author Jonathan Buttigieg * * @param int|object $id An image attachment ID or a NGG object. */ public function __construct( $id ) { imagify_deprecated_class( get_class( $this ), '1.9' ); parent::__construct( $id ); } /** ----------------------------------------------------------------------------------------- */ /** ATTACHMENT PATHS AND URLS =============================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the original attachment path. * * @since 1.6.6 * @author Grégory Viguier * * @return string|bool Path to the file if it exists or has been successfully retrieved from S3. False on failure. */ public function get_original_path() { return $this->get_thumbnail_path(); } /** * Get a thumbnail path. * * @since 1.6.6 * @author Grégory Viguier * * @param string $size_file The basename of the file. If not provided, the path to the main file is returned. * @return string|bool Path to the file if it exists or has been successfully retrieved from S3. False on failure. */ public function get_thumbnail_path( $size_file = false ) { if ( ! $this->is_valid() ) { return ''; } $file_path = get_attached_file( $this->id, true ); if ( $size_file ) { // It's not the full size. $file_path = $this->filesystem->dir_path( $file_path ) . $size_file; } return $file_path; } /** * Get the original attachment URL. * * @since 1.6.6 * @author Grégory Viguier * * @return string|bool The main file URL. False on failure. */ public function get_original_url() { return $this->get_thumbnail_url(); } /** * Get a thumbnail URL. * * @since 1.6.6 * @author Grégory Viguier * * @param string $size_file The basename of the file. If not provided, the main file's URL is returned. * @return string|bool The file URL. False on failure. */ public function get_thumbnail_url( $size_file = false ) { if ( ! $this->is_extension_supported() ) { return false; } $file_url = wp_get_attachment_url( $this->id ); if ( $size_file ) { // It's not the full size. $file_url = $this->filesystem->dir_path( $file_url ) . $size_file; } return $file_url; } /** ----------------------------------------------------------------------------------------- */ /** THE PUBLIC STUFF ======================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Optimize all sizes with Imagify. * * @since 1.6.6 * @author Grégory Viguier * * @param int $optimization_level The optimization level (2 = ultra, 1 = aggressive, 0 = normal). * @param array $metadata The attachment meta data, containing the sizes. Provide only for a new attachment. * @return array|bool The optimization data. False on failure. */ public function optimize( $optimization_level = null, $metadata = array() ) { $metadata_changed = false; /** * Make some sanity tests first. */ // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() ) { return false; } // To avoid issue with "original_size" at 0 in "_imagify_data". if ( 0 === (int) $this->get_stats_data( 'original_size' ) ) { $this->delete_imagify_data(); } $optimization_level = isset( $optimization_level ) ? (int) $optimization_level : get_imagify_option( 'optimization_level' ); // Check if the full size is already optimized with this level. if ( $this->is_optimized() && $this->get_optimization_level() === $optimization_level ) { return false; } // Get file path of the full size. $attachment_path = $this->get_original_path(); $attachment_url = $this->get_original_url(); if ( ! $attachment_path ) { // We're in deep sh**. return false; } if ( ! $this->filesystem->exists( $attachment_path ) && ! $this->get_file_from_s3( $attachment_path ) ) { // The file doesn't exist and couldn't be retrieved from S3. return false; } /** * Start the process. */ $this->set_running_status(); /** This hook is documented in /inc/classes/class-imagify-attachment.php. */ do_action( 'before_imagify_optimize_attachment', $this->id ); $metadata = $this->set_deletion_status( $metadata ); // Store the paths of the files that may be deleted once optimized and sent to S3. $to_delete = array(); $filesize_total = 0; // Maybe resize (and backup) the image. $resized = $this->is_image() && $this->maybe_resize( $attachment_path ); if ( $resized ) { $size = $this->filesystem->get_image_size( $attachment_path ); if ( $size ) { $metadata['width'] = $size['width']; $metadata['height'] = $size['height']; $metadata_changed = true; } } // Optimize the full size. $response = do_imagify( $attachment_path, array( 'optimization_level' => $optimization_level, 'context' => 'wp', 'resized' => $resized, 'original_size' => $this->get_original_size( false ), ) ); $data = $this->fill_data( null, $response ); if ( $this->delete_files ) { $to_delete[] = $attachment_path; // This is used by AS3CF. $bytes = $this->filesystem->size( $attachment_path ); if ( false !== $bytes ) { $metadata_changed = true; $filesize_total += $bytes; $metadata['filesize'] = $bytes; } else { $metadata['filesize'] = 0; } } /** This filter is documented in /inc/classes/class-imagify-attachment.php. */ $data = apply_filters( 'imagify_fill_full_size_data', $data, $response, $this->id, $attachment_path, $attachment_url, 'full', $optimization_level, $metadata ); // Save the optimization level. update_post_meta( $this->id, '_imagify_optimization_level', $optimization_level ); if ( ! $data ) { // The optimization failed. $metadata = $metadata_changed ? $metadata : false; $this->cleanup( $metadata, $to_delete ); return false; } // Optimize all thumbnails. if ( ! empty( $metadata['sizes'] ) ) { $disallowed_sizes = get_imagify_option( 'disallowed-sizes' ); $is_active_for_network = imagify_is_active_for_network(); foreach ( $metadata['sizes'] as $size_key => $size_data ) { $thumbnail_path = $this->get_thumbnail_path( $size_data['file'] ); $thumbnail_url = $this->get_thumbnail_url( $size_data['file'] ); if ( $this->delete_files ) { $to_delete[] = $thumbnail_path; // Even if this size must not be optimized ($disallowed_sizes), we must fetch the file from S3 to get its size, and not to trigger a `WP_Error` in `upload_attachment_to_s3()`. if ( ! $this->filesystem->exists( $thumbnail_path ) && ! $this->get_file_from_s3( $thumbnail_path ) ) { // Doesn't exist and couldn't be retrieved from S3. $data['sizes'][ $size_key ] = array( 'success' => false, 'error' => __( 'This size could not be retrieved from Amazon S3.', 'imagify' ), ); continue; } // This is used by AS3CF. $bytes = $this->filesystem->size( $thumbnail_path ); if ( false !== $bytes ) { $filesize_total += $bytes; } } // Check if this size has to be optimized. if ( ! $is_active_for_network && isset( $disallowed_sizes[ $size_key ] ) && $this->is_image() ) { $data['sizes'][ $size_key ] = array( 'success' => false, 'error' => __( 'This size is not authorized to be optimized. Update your Imagify settings if you want to optimize it.', 'imagify' ), ); /** This filter is documented in /inc/classes/class-imagify-attachment.php. */ $data = apply_filters( 'imagify_fill_unauthorized_thumbnail_data', $data, $this->id, $thumbnail_path, $thumbnail_url, $size_key, $optimization_level, $metadata ); continue; } if ( ! $this->delete_files && ! $this->filesystem->exists( $thumbnail_path ) && ! $this->get_file_from_s3( $thumbnail_path ) ) { // Doesn't exist and couldn't be retrieved from S3. $data['sizes'][ $size_key ] = array( 'success' => false, 'error' => __( 'This size could not be retrieved from Amazon S3.', 'imagify' ), ); continue; } if ( ! $this->is_image() ) { continue; } // Optimize the thumbnail size. $response = do_imagify( $thumbnail_path, array( 'backup' => false, 'optimization_level' => $optimization_level, 'context' => 'wp', ) ); $data = $this->fill_data( $data, $response, $size_key ); /** This filter is documented in /inc/classes/class-imagify-attachment.php. */ $data = apply_filters( 'imagify_fill_thumbnail_data', $data, $response, $this->id, $thumbnail_path, $thumbnail_url, $size_key, $optimization_level, $metadata ); } // End foreach(). } // End if(). $data['stats']['percent'] = round( ( ( $data['stats']['original_size'] - $data['stats']['optimized_size'] ) / $data['stats']['original_size'] ) * 100, 2 ); update_post_meta( $this->id, '_imagify_data', $data ); update_post_meta( $this->id, '_imagify_status', 'success' ); if ( $this->delete_files && $filesize_total ) { // Add the total file size for all image sizes. This is a meta used by AS3CF. update_post_meta( $this->id, 'wpos3_filesize_total', $filesize_total ); } $optimized_data = $this->get_data(); /** This hook is documented in /inc/classes/class-imagify-attachment.php. */ do_action( 'after_imagify_optimize_attachment', $this->id, $optimized_data ); $sent = $this->maybe_send_attachment_to_s3( $metadata, $attachment_path ); // Update metadata only if they changed. $metadata = $metadata_changed ? $metadata : false; // Delete files only if they have been uploaded to S3. $to_delete = $sent ? $to_delete : array(); $this->cleanup( $metadata, $to_delete ); return $optimized_data; } /** * Optimize missing thumbnail sizes with Imagify. * * @since 1.6.10 * @access public * @author Grégory Viguier * * @param int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). * @return array|object An array of thumbnail data, size by size. A WP_Error object on failure. */ public function optimize_missing_thumbnails( $optimization_level = null ) { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() || ! $this->is_image() ) { return new WP_Error( 'mime_type_not_supported', __( 'This type of file is not supported.', 'imagify' ) ); } /** * Create missing thumbnails and optimize them. */ $result = parent::optimize_missing_thumbnails( $optimization_level ); $result_sizes = array(); $this->set_running_status(); if ( is_array( $result ) ) { // All good. $result_sizes = $result; } elseif ( is_wp_error( $result ) ) { // Some thumbnails could not be created. Lets see if some were. $result_sizes = $result->get_error_data( 'image_resize_error' ); $result_sizes = ! empty( $result_sizes['sizes_succeeded'] ) ? $result_sizes['sizes_succeeded'] : array(); } if ( ! $result_sizes ) { // No thumbnails created. $this->delete_running_status(); return $result; } /** * Fetch all images from S3 if they're not on the server. * S3 Offload needs ALL images (so it can update its metas), we can't just send some of them without entering Hell -_-'. */ $metadata = $this->set_deletion_status(); if ( ! $this->can_send_to_s3() ) { // The other thumbnails are not on S3, so we don't need to send the new ones. $this->delete_running_status(); return $result; } /** * The main file. */ $attachment_path = $this->get_original_path(); $to_delete = array(); $to_skip = array(); $filesize_total = 0; $metadata_changed = false; if ( ! $attachment_path ) { // WAT?! if ( ! is_wp_error( $result ) ) { $result = new WP_Error( 'no_attachment_path', __( 'Files could not be sent to Amazon S3.', 'imagify' ), array( 'sizes_succeeded' => $result_sizes, ) ); } else { $result->add( 'no_attachment_path', __( 'Files could not be sent to Amazon S3.', 'imagify' ) ); } $this->delete_running_status(); return $result; } if ( ! $this->filesystem->exists( $attachment_path ) && ! $this->get_file_from_s3( $attachment_path ) ) { // The file doesn't exist and couldn't be retrieved from S3. if ( ! is_wp_error( $result ) ) { $result = new WP_Error( 'main_file_not_on_s3', __( 'The main image could not be retrieved from Amazon S3.', 'imagify' ), array( 'sizes_succeeded' => $result_sizes, ) ); } else { $result->add( 'main_file_not_on_s3', __( 'The main image could not be retrieved from Amazon S3.', 'imagify' ) ); } $this->delete_running_status(); return $result; } // Files that must not be retrieved from S3. foreach ( $result_sizes as $size_key => $size_data ) { $to_skip[] = $this->get_thumbnail_path( $size_data['file'] ); } // Store the paths of the files that may be deleted once sent to S3. if ( $this->delete_files ) { $to_delete[] = $attachment_path; $to_delete = array_merge( $to_delete, $to_skip ); // This is used by AS3CF. $bytes = $this->filesystem->size( $attachment_path ); if ( false !== $bytes ) { $metadata_changed = true; $filesize_total += $bytes; $metadata['filesize'] = $bytes; } elseif ( ! isset( $metadata['filesize'] ) ) { $metadata_changed = true; $metadata['filesize'] = 0; } } /** * The thumbnails. */ if ( ! empty( $metadata['sizes'] ) ) { $to_skip = array_flip( $to_skip ); foreach ( $metadata['sizes'] as $size_key => $size_data ) { $thumbnail_path = $this->get_thumbnail_path( $size_data['file'] ); if ( isset( $to_skip[ $thumbnail_path ] ) ) { continue; } if ( ! $this->filesystem->exists( $thumbnail_path ) && ! $this->get_file_from_s3( $thumbnail_path ) ) { // The file doesn't exist and couldn't be retrieved from S3. if ( ! is_wp_error( $result ) ) { $result = new WP_Error( 'thumbnail_not_on_s3', __( 'This size could not be retrieved from Amazon S3.', 'imagify' ), array( 'sizes_succeeded' => $result_sizes, 'size' => $size_key, ) ); } else { $result->add( 'thumbnail_not_on_s3', __( 'This size could not be retrieved from Amazon S3.', 'imagify' ), array( 'size' => $size_key, ) ); } $this->delete_running_status(); return $result; } if ( $this->delete_files ) { $to_delete[] = $thumbnail_path; // This is used by AS3CF. $bytes = $this->filesystem->size( $thumbnail_path ); if ( false !== $bytes ) { $filesize_total += $bytes; } } } // End foreach(). } // End if(). if ( $this->delete_files && $filesize_total ) { // Add the total file size for all image sizes. This is a meta used by AS3CF. update_post_meta( $this->id, 'wpos3_filesize_total', $filesize_total ); } $sent = $this->maybe_send_attachment_to_s3( $metadata, $attachment_path ); // Update metadata only if they changed. $metadata = $metadata_changed ? $metadata : false; // Delete files only if they have been uploaded to S3. $to_delete = $sent ? $to_delete : array(); $this->cleanup( $metadata, $to_delete ); return $result; } /** * Re-optimize the given thumbnail sizes to the same level. * Not supported yet in this context. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param array $sizes The sizes to optimize. * @return array|void A WP_Error object on failure. */ public function reoptimize_thumbnails( $sizes ) {} /** * Process an attachment restoration from the backup file. * * @since 1.6.6 * @author Grégory Viguier * * @return array A list of files sent to S3. */ public function restore() { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() ) { return false; } // Stop the process if there is no backup file to restore. if ( ! $this->has_backup() ) { return false; } /** This hook is documented in /inc/classes/class-imagify-attachment.php. */ do_action( 'before_imagify_restore_attachment', $this->id ); $backup_path = $this->get_backup_path(); $attachment_path = $this->get_original_path(); if ( ! $attachment_path ) { return false; } // Create the original image from the backup. $this->filesystem->copy( $backup_path, $attachment_path, true ); $this->filesystem->chmod_file( $attachment_path ); if ( ! $this->filesystem->exists( $attachment_path ) ) { return false; } $this->set_deletion_status(); // Remove old optimization data. $this->delete_imagify_data(); if ( ! $this->is_image() ) { // We need to delete the thumbnails for pdf, or new ones will be generated with new unique file names. $metadata = wp_get_attachment_metadata( $this->id ); if ( ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) { foreach ( $metadata['sizes'] as $size_key => $size_data ) { $thumbnail_path = $this->get_thumbnail_path( $size_data['file'] ); if ( $this->filesystem->exists( $thumbnail_path ) ) { $this->filesystem->delete( $thumbnail_path ); } } } } if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; } // Generate new thumbnails and new metadata. $metadata = wp_generate_attachment_metadata( $this->id, $attachment_path ); // Send to S3. $sent = $this->maybe_send_attachment_to_s3( $metadata, $attachment_path ); // Files restored (and maybe to delete). $files = array(); // If the files must be deleted, we need to store the file sizes. $filesize_total = 0; if ( $sent ) { $files[] = $attachment_path; } if ( $this->delete_files ) { // This is used by AS3CF. $bytes = $this->filesystem->size( $attachment_path ); if ( false !== $bytes ) { $filesize_total += $bytes; $metadata['filesize'] = $bytes; } else { $metadata['filesize'] = 0; } } if ( ! empty( $metadata['sizes'] ) && ( $sent || $this->delete_files ) ) { foreach ( $metadata['sizes'] as $size_key => $size_data ) { $thumbnail_path = $this->get_thumbnail_path( $size_data['file'] ); if ( $sent ) { $files[] = $thumbnail_path; } if ( $this->delete_files ) { // This is used by AS3CF. $bytes = $this->filesystem->size( $thumbnail_path ); if ( false !== $bytes ) { $filesize_total += $bytes; } } } } if ( $this->delete_files && $filesize_total ) { // Add the total file size for all image sizes. This is a meta used by AS3CF. update_post_meta( $this->id, 'wpos3_filesize_total', $filesize_total ); } /** This hook is documented in /inc/classes/class-imagify-attachment.php. */ do_action( 'after_imagify_restore_attachment', $this->id ); $to_delete = $this->delete_files ? $files : array(); $this->cleanup( $metadata, $to_delete ); return $files; } /** ----------------------------------------------------------------------------------------- */ /** INTERNAL UTILITIES ====================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Cleanup after optimization or a restore: * - Maybe update metadata. * - Maybe delete local files. * - Delete the "optimization status" transient. * * @since 1.6.6 * @author Grégory Viguier * * @param array $new_metadata New attachment metadata to be stored. * @param array $files_to_remove Files to delete. */ protected function cleanup( $new_metadata, $files_to_remove ) { if ( $new_metadata ) { /** * Filter the metadata stored after optimization or a restore. * * @since 1.6.6 * @author Grégory Viguier * * @param array $new_metadata New attachment metadata to be stored. * @param int $id The attachment ID. */ $new_metadata = apply_filters( 'imagify_as3cf_attachment_cleanup_metadata', $new_metadata, $this->id ); /** * Update the attachment meta that contains the file sizes. * Here we don't use wp_update_attachment_metadata() to prevent triggering unwanted hooks. */ update_post_meta( $this->id, '_wp_attachment_metadata', $new_metadata ); } if ( $files_to_remove ) { $attachment_path = $this->get_original_path(); /** This filter is documented in /amazon-s3-and-cloudfront/classes/amazon-s3-and-cloudfront.php. */ $files_to_remove = (array) apply_filters( 'as3cf_upload_attachment_local_files_to_remove', $files_to_remove, $this->id, $attachment_path ); $files_to_remove = array_filter( $files_to_remove ); if ( $files_to_remove ) { $files_to_remove = array_unique( $files_to_remove ); /** * Delete the local files. */ array_map( array( $this, 'maybe_delete_file' ), $files_to_remove ); } } /** * Delete the "optimization status" transient. */ $this->delete_running_status(); } /** * Maybe resize (and backup) an image. * * @since 1.6.6 * @author Grégory Viguier * * @param string $attachment_path The file path. * @return bool True on success. False on failure. */ protected function maybe_resize( $attachment_path ) { if ( ! $this->is_image() ) { return false; } $do_resize = get_imagify_option( 'resize_larger' ); $resize_width = get_imagify_option( 'resize_larger_w' ); $attachment_size = $this->filesystem->get_image_size( $attachment_path ); if ( ! $do_resize || ! $attachment_size || $resize_width >= $attachment_size['width'] ) { return false; } $resized_attachment_path = $this->resize( $attachment_path, $attachment_size, $resize_width ); if ( is_wp_error( $resized_attachment_path ) ) { return false; } $backuped = imagify_backup_file( $attachment_path ); if ( is_wp_error( $backuped ) ) { return false; } return $this->filesystem->move( $resized_attachment_path, $attachment_path, true ); } /** * Tell if the files must be deleted after being optimized or restored. * It sets the 2 properties $this->use_s3_settings and $this->delete_files. * * @since 1.6.6 * @author Grégory Viguier * * @param array $metadata Attachment metadata. Provide, only if it comes from a 'wp_generate_attachment_metadata' or 'wp_update_attachment_metadata' hook. * @return array Attachment metadata. If not provided as argument, new values are fetched. */ protected function set_deletion_status( $metadata = false ) { global $as3cf; if ( $metadata ) { /** * Metadata is provided: we were in a 'wp_generate_attachment_metadata' or 'wp_update_attachment_metadata' hook. * This means we'll follow AS3CF settings to know if the local files must be sent to S3 and/or deleted. */ $this->use_s3_settings = true; $this->delete_files = $as3cf && $as3cf->get_setting( 'remove-local-file' ) && $this->can_send_to_s3(); return $metadata; } /** * Metadata is not provided: we were not in a 'wp_generate_attachment_metadata' or 'wp_update_attachment_metadata' hook. * So, we fetch the current meta value. * This also means we won't follow AS3CF settings to know if the local files must be sent to S3 and/or deleted. * In that case we'll send the files to S3 if they already are there, and delete them if they is a 'filesize' entry in the metadata. */ $metadata = wp_get_attachment_metadata( $this->id, true ); $this->use_s3_settings = false; $this->delete_files = isset( $metadata['filesize'] ) && $this->can_send_to_s3(); return $metadata; } /** ----------------------------------------------------------------------------------------- */ /** S3 UTILITIES ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if AS3CF is set up. * * @since 1.6.6 * @author Grégory Viguier * * @return bool */ public function is_s3_setup() { global $as3cf; static $is; if ( ! isset( $is ) ) { $is = $as3cf && $as3cf->is_plugin_setup(); } return $is; } /** * Tell if an attachment is stored on S3. * * @since 1.6.6 * @author Grégory Viguier * * @return array|bool The S3 info on success. False if the attachment is not on S3. */ public function get_s3_info() { global $as3cf; if ( ! $as3cf ) { return false; } if ( method_exists( $as3cf, 'get_attachment_s3_info' ) ) { return $as3cf->get_attachment_s3_info( $this->id ); } return $as3cf->get_attachment_provider_info( $this->id ); } /** * Get a file from S3. * * @since 1.6.6 * @author Grégory Viguier * * @param string $file_path The file path. * @return string|bool The file path on success, false on failure. */ protected function get_file_from_s3( $file_path ) { global $as3cf; if ( ! $this->is_extension_supported() ) { return false; } if ( ! $this->is_s3_setup() ) { return false; } $s3_object = $this->get_s3_info(); if ( ! $s3_object ) { // The attachment is not on S3. return false; } $directory = $this->filesystem->dir_path( $s3_object['key'] ); $directory = $this->filesystem->is_root( $directory ) ? '' : $directory; $s3_object['key'] = $directory . $this->filesystem->file_name( $file_path ); // Retrieve file from S3. if ( method_exists( $as3cf->plugin_compat, 'copy_s3_file_to_server' ) ) { $as3cf->plugin_compat->copy_s3_file_to_server( $s3_object, $file_path ); } else { $as3cf->plugin_compat->copy_provider_file_to_server( $s3_object, $file_path ); } return $this->filesystem->exists( $file_path ) ? $file_path : false; } /** * Maybe send the attachment to S3. * * @since 1.6.6 * @author Grégory Viguier * * @param array $metadata The attachment metadata. * @param string $attachment_path The attachment path. * @param bool $remove_local_files True to let AS3CF delete the local files (if set in the settings). We usually don't want that, we do it by ourselves. * @return bool True on success. False otherwize. */ protected function maybe_send_attachment_to_s3( $metadata = null, $attachment_path = null, $remove_local_files = false ) { global $as3cf; if ( ! $this->can_send_to_s3() ) { return false; } $s3_object = $this->get_s3_info(); if ( ! $s3_object ) { return false; } $full_file_path = $this->get_original_path(); if ( ! $full_file_path ) { // This is bad. return false; } if ( method_exists( $as3cf, 'upload_attachment_to_s3' ) ) { $s3_data = $as3cf->upload_attachment_to_s3( $this->id, $metadata, $attachment_path, false, $remove_local_files ); } else { $s3_data = $as3cf->upload_attachment( $this->id, $metadata, $attachment_path, false, $remove_local_files ); } return ! is_wp_error( $s3_data ); } /** * Tell if an attachment can be sent to S3. * * @since 1.6.6 * @author Grégory Viguier * * @return bool */ protected function can_send_to_s3() { global $as3cf; static $can = array(); static $copy_to_s3; if ( isset( $can[ $this->id ] ) ) { return $can[ $this->id ]; } if ( ! isset( $copy_to_s3 ) ) { $copy_to_s3 = $as3cf && $as3cf->get_setting( 'copy-to-s3' ); } $is_s3_setup = $this->is_s3_setup(); $s3_object = $this->get_s3_info(); // S3 is set up and the attachment is on S3. $can[ $this->id ] = $is_s3_setup && $s3_object; if ( $can[ $this->id ] && ! empty( $this->use_s3_settings ) ) { // Use AS3CF setting to tell if we're allowed to send the files. $can[ $this->id ] = $copy_to_s3; } /** * Filter the result of Imagify_AS3CF_Attachment::can_send_to_s3(). * * @since 1.6.6 * @author Grégory Viguier * * @param bool $can True if the attachment can be sent. False otherwize. * @param int $id The attachment ID. * @param array $s3_object The S3 infos. * @param bool $is_s3_setup AS3CF is set up or not. * @param bool $copy_to_s3 AS3CF setting that tells if a "new" attachment can be sent. * @param bool $use_s3_settings Tell if we must use AS3CF setting in this case. */ $can[ $this->id ] = (bool) apply_filters( 'imagify_can_send_to_s3', $can[ $this->id ], $this->id, $s3_object, $is_s3_setup, $copy_to_s3, $this->use_s3_settings ); return $can[ $this->id ]; } /** * Maybe delete the local file. * * @since 1.6.6 * @author Grégory Viguier * * @param string $file_path The file path. * @return bool True if deleted or doesn't exist. False on failure or if the file is not supposed to be deleted. */ protected function maybe_delete_file( $file_path ) { if ( ! $this->file_should_be_deleted( $file_path ) ) { return false; } if ( ! $this->filesystem->exists( $file_path ) ) { return true; } return $this->filesystem->delete( $file_path, false, 'f' ); } /** * Tell if a file should be deleted. * * @since 1.6.6 * @author Grégory Viguier * * @param string $file_path The file path. * @return bool True to delete, false to keep. */ protected function file_should_be_deleted( $file_path ) { if ( ! $file_path || ! $this->delete_files ) { // We keep the file. return false; } /** This hook is documented in /amazon-s3-and-cloudfront/classes/amazon-s3-and-cloudfront.php. */ $preserve = apply_filters( 'as3cf_preserve_file_from_local_removal', false, $file_path ); return false === $preserve; } } deprecated/classes/class-imagify-ngg-attachment.php 0000644 00000065073 15174671745 0016475 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Imagify NextGen Gallery attachment class. * * @since 1.5 * @since 1.9 Deprecated * @author Jonathan Buttigieg * @deprecated */ class Imagify_NGG_Attachment extends Imagify_Attachment { /** * Class version. * * @var string */ const VERSION = '1.4'; /** * The attachment SQL DB class. * * @var string * @since 1.7 * @access protected */ protected $db_class_name = '\Imagify\ThirdParty\NGG\DB'; /** * The image object. * * @var object A nggImage object. * @since 1.5 * @since 1.7 Not public anymore. * @access protected */ protected $image; /** * The storage object used by NGG. * * @var object A C_Gallery_Storage object (by default). * @since 1.8. * @access protected */ protected $storage; /** * Tell if the file mime type can be optimized by Imagify. * * @var bool * @since 1.6.9 * @since 1.7 Not public anymore. * @access protected * @see $this->is_mime_type_supported() */ protected $is_mime_type_supported; /** * The constructor. * * @since 1.5 * @author Jonathan Buttigieg * * @param int|object $id An image attachment ID or a NGG object. */ public function __construct( $id ) { imagify_deprecated_class( get_class( $this ), '1.9', '\\Imagify\\ThirdParty\\NGG\\Optimization\\Process\\NGG( $id )' ); if ( is_object( $id ) ) { if ( $id instanceof nggImage ) { $this->image = $id; $this->id = (int) $id->pid; } else { $this->image = nggdb::find_image( (int) $id->pid ); $this->id = ! empty( $this->image->pid ) ? (int) $this->image->pid : 0; } } else { $this->image = nggdb::find_image( absint( $id ) ); $this->id = ! empty( $this->image->pid ) ? (int) $this->image->pid : 0; } $this->get_row(); if ( ! empty( $this->image->_ngiw ) ) { $this->storage = $this->image->_ngiw->get_storage()->object; } else { $this->storage = C_Gallery_Storage::get_instance()->object; } $this->filesystem = Imagify_Filesystem::get_instance(); $this->optimization_state_transient = 'imagify-ngg-async-in-progress-' . $this->id; // Load nggAdmin class. $ngg_admin_functions_path = WP_PLUGIN_DIR . '/' . NGGFOLDER . '/products/photocrati_nextgen/modules/ngglegacy/admin/functions.php'; if ( ! class_exists( 'nggAdmin' ) && $this->filesystem->exists( $ngg_admin_functions_path ) ) { require_once $ngg_admin_functions_path; } } /** * Get the original attachment path. * * @since 1.5 * @author Jonathan Buttigieg * * @access public * @return string */ public function get_original_path() { if ( ! $this->is_valid() ) { return ''; } return $this->image->imagePath; } /** * Get the original attachment URL. * * @since 1.5 * @author Jonathan Buttigieg * * @access public * @return string */ public function get_original_url() { if ( ! $this->is_valid() ) { return ''; } return $this->image->imageURL; } /** * Get the attachment backup file path, even if the file doesn't exist. * * @since 1.6.13 * @author Grégory Viguier * @access public * * @return string|bool The file path. False on failure. */ public function get_raw_backup_path() { if ( ! $this->is_valid() ) { return false; } return get_imagify_ngg_attachment_backup_path( $this->get_original_path() ); } /** * Get the attachment backup URL. * * @since 1.6.8 * @author Grégory Viguier * * @return string|false */ public function get_backup_url() { if ( ! $this->is_valid() ) { return false; } return site_url( '/' ) . $this->filesystem->make_path_relative( $this->get_raw_backup_path() ); } /** * Get the attachment optimization data. * * @since 1.5 * @author Jonathan Buttigieg * * @access public * @return array */ public function get_data() { $row = $this->get_row(); return isset( $row['data'] ) ? $row['data'] : array(); } /** * Get the attachment optimization level. * * @since 1.5 * @author Jonathan Buttigieg * * @access public * @return int|bool */ public function get_optimization_level() { $row = $this->get_row(); return isset( $row['optimization_level'] ) ? (int) $row['optimization_level'] : false; } /** * Get the attachment optimization status (success or error). * * @since 1.5 * @author Jonathan Buttigieg * * @access public * @return string|bool */ public function get_status() { $row = $this->get_row(); return isset( $row['status'] ) ? $row['status'] : false; } /** * Delete the data related to optimization. * * @since 1.7 * @access public * @author Grégory Viguier */ public function delete_imagify_data() { if ( ! $this->get_row() ) { return; } $this->delete_row(); } /** * Get width and height of the original image. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public function get_dimensions() { return array( 'width' => ! empty( $this->image->meta_data['width'] ) ? (int) $this->image->meta_data['width'] : 0, 'height' => ! empty( $this->image->meta_data['height'] ) ? (int) $this->image->meta_data['height'] : 0, ); } /** * Get the file mime type + file extension (if the file is supported). * * @since 1.8 * @access public * @see wp_check_filetype() * @author Grégory Viguier * * @return object */ public function get_file_type() { if ( isset( $this->file_type ) ) { return $this->file_type; } if ( ! $this->is_valid() ) { $this->file_type = (object) array( 'ext' => '', 'type' => '', ); return $this->file_type; } $this->file_type = (object) wp_check_filetype( $this->get_original_path(), imagify_get_mime_types( 'image' ) ); return $this->file_type; } /** * Tell if the current attachment has the required WP metadata. * * @since 1.6.12 * @author Grégory Viguier * * @return bool */ public function has_required_metadata() { static $sizes; if ( ! isset( $sizes ) ) { $sizes = $this->get_image_sizes(); } return $sizes && $this->get_original_path(); } /** * Update the metadata size of the attachment. * * @since 1.5 * * @access public * @return void */ public function update_metadata_size() { $size = $this->filesystem->get_image_size( $this->get_original_path() ); if ( ! $size ) { return; } $this->image->meta_data['width'] = $size['width']; $this->image->meta_data['height'] = $size['height']; $this->image->meta_data['full']['width'] = $size['width']; $this->image->meta_data['full']['height'] = $size['height']; nggdb::update_image_meta( $this->id, $this->image->meta_data ); } /** * Fills statistics data with values from $data array. * * @since 1.5 * @since 1.6.5 Not static anymore. * @since 1.6.6 Removed the attachment ID parameter. * @since 1.7 Removed the image URL parameter. * @author Jonathan Buttigieg * @access public * * @param array $data The statistics data. * @param object $response The API response. * @param string $size The attachment size key. * @return bool|array False if the original size has an error or an array contains the data for other result. */ public function fill_data( $data, $response, $size = 'full' ) { $data = is_array( $data ) ? $data : array(); $data['sizes'] = ! empty( $data['sizes'] ) && is_array( $data['sizes'] ) ? $data['sizes'] : array(); if ( empty( $data['stats'] ) ) { $data['stats'] = array( 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, ); } if ( is_wp_error( $response ) ) { // Error or already optimized. $error = $response->get_error_message(); $error_status = 'error'; $data['sizes'][ $size ] = array( 'success' => false, 'error' => $error, ); // Update the error status for the original size. if ( 'full' === $size ) { if ( false !== strpos( $error, 'This image is already compressed' ) ) { $error_status = 'already_optimized'; } $this->update_row( array( // The pid column is needed in case the row doesn't exist yet. 'pid' => $this->id, 'status' => $error_status, 'data' => $data, ) ); return false; } return $data; } // Success. $old_data = $this->get_data(); $original_size = ! empty( $old_data['sizes'][ $size ]['original_size'] ) ? (int) $old_data['sizes'][ $size ]['original_size'] : 0; $response = (object) array_merge( array( 'original_size' => 0, 'new_size' => 0, 'percent' => 0, ), (array) $response ); if ( ! empty( $response->original_size ) && ! $original_size ) { $original_size = (int) $response->original_size; } if ( ! empty( $response->new_size ) ) { $optimized_size = (int) $response->new_size; } else { $file_path = $this->get_original_path(); $file_path = $file_path && $this->filesystem->exists( $file_path ) ? $file_path : false; $optimized_size = $file_path ? $this->filesystem->size( $file_path ) : 0; } if ( $original_size && $optimized_size ) { $percent = round( ( $original_size - $optimized_size ) / $original_size * 100, 2 ); } elseif ( ! empty( $response->percent ) ) { $percent = round( $response->percent, 2 ); } else { $percent = 0; } $data['sizes'][ $size ] = array( 'success' => true, 'original_size' => $original_size, 'optimized_size' => $optimized_size, 'percent' => $percent, ); $data['stats']['original_size'] += $original_size; $data['stats']['optimized_size'] += $optimized_size; $data['stats']['percent'] = round( ( ( $data['stats']['original_size'] - $data['stats']['optimized_size'] ) / $data['stats']['original_size'] ) * 100, 2 ); return $data; } /** * Optimize all sizes with Imagify. * * @since 1.5 * @author Jonathan Buttigieg * * @access public * @param int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). * @param array $metadata The attachment meta data (not used here). * @return array $data The optimization data. */ public function optimize( $optimization_level = null, $metadata = array() ) { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() ) { return; } $optimization_level = isset( $optimization_level ) ? (int) $optimization_level : get_imagify_option( 'optimization_level' ); // To avoid issue with "original_size" at 0 in "_imagify_data". if ( 0 === (int) $this->get_stats_data( 'original_size' ) ) { $this->delete_imagify_data(); } // Check if the full size is already optimized. if ( $this->is_optimized() && $this->get_optimization_level() === $optimization_level ) { return; } // Get file path for original image. $attachment_path = $this->get_original_path(); $attachment_url = $this->get_original_url(); /** * Fires before optimizing an attachment. * * @since 1.5 * @author Jonathan Buttigieg * * @param int $id The image ID */ do_action( 'before_imagify_ngg_optimize_attachment', $this->id ); $this->set_running_status(); // Optimize the original size. $response = do_imagify( $attachment_path, array( 'optimization_level' => $optimization_level, 'context' => 'NGG', 'keep_exif' => true, 'original_size' => $this->get_original_size( false ), 'backup_path' => $this->get_raw_backup_path(), ) ); $data = $this->fill_data( null, $response ); /** * Filter the optimization data of the full size. * * @since 1.8 * @author Grégory Viguier * * @param array $data The statistics data. * @param object $response The API response. * @param int $id The attachment ID. * @param string $attachment_path The attachment path. * @param string $attachment_url The attachment URL. * @param string $size_key The attachment size key. The value is obviously 'full' but it's kept for concistancy with other filters. * @param int $optimization_level The optimization level. */ $data = apply_filters( 'imagify_fill_ngg_full_size_data', $data, $response, $this->id, $attachment_path, $attachment_url, 'full', $optimization_level ); // Save the optimization level. $this->update_row( array( // The pid column is needed in case the row doesn't exist yet. 'pid' => $this->id, 'optimization_level' => $optimization_level, ) ); if ( ! $data ) { // Error or already optimized. $this->delete_running_status(); return; } // Optimize thumbnails. $data = $this->optimize_thumbnails( $optimization_level, $data ); // Save the status to success. $this->update_row( array( 'status' => 'success', ) ); /** * Update NGG meta data. */ $image_data = $this->storage->_image_mapper->find( $this->id ); if ( ! $image_data ) { $this->delete_running_status(); return $data; } $dimensions = $this->filesystem->get_image_size( $attachment_path ); $md5 = md5_file( $attachment_path ); if ( ( $dimensions || $md5 ) && ( empty( $image_data->meta_data['full'] ) || ! is_array( $image_data->meta_data['full'] ) ) ) { $image_data->meta_data['full'] = array( 'width' => 0, 'height' => 0, 'md5' => '', ); } if ( $dimensions ) { $image_data->meta_data['width'] = $dimensions['width']; $image_data->meta_data['height'] = $dimensions['height']; $image_data->meta_data['full']['width'] = $dimensions['width']; $image_data->meta_data['full']['height'] = $dimensions['height']; } if ( $md5 ) { $image_data->meta_data['md5'] = $md5; $image_data->meta_data['full']['md5'] = $md5; } /** * Fires after optimizing an attachment. * * @since 1.5 * * @param int $id The attachment ID. * @param array $data The optimization data. */ do_action( 'after_imagify_ngg_optimize_attachment', $this->id, $data ); $this->delete_running_status(); return $data; } /** * Optimize all thumbnails of an image. * * @since 1.5 * @author Jonathan Buttigieg * * @access public * @param int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). * @param array $data The optimization data. * @return array $data The optimization data. */ public function optimize_thumbnails( $optimization_level = null, $data = array() ) { $sizes = $this->get_image_sizes(); $data = $data ? $data : $this->get_data(); // Stop if the original image has an error. if ( $this->has_error() ) { return $data; } $optimization_level = isset( $optimization_level ) ? (int) $optimization_level : get_imagify_option( 'optimization_level' ); /** * Fires before optimizing all thumbnails. * * @since 1.5 * @author Jonathan Buttigieg * * @param int $id The image ID. */ do_action( 'before_imagify_ngg_optimize_thumbnails', $this->id ); if ( $sizes ) { $image_data = $this->storage->_image_mapper->find( $this->id ); foreach ( $sizes as $size_key ) { if ( 'full' === $size_key || isset( $data['sizes'][ $size_key ]['success'] ) ) { continue; } $thumbnail_path = $this->storage->get_image_abspath( $image_data, $size_key ); $thumbnail_url = $this->storage->get_image_url( $image_data, $size_key ); // Optimize the thumbnail size. $response = do_imagify( $thumbnail_path, array( 'optimization_level' => $optimization_level, 'context' => 'NGG', 'keep_exif' => true, 'backup' => false, ) ); $data = $this->fill_data( $data, $response, $size_key ); /** * Filter the optimization data of a specific thumbnail. * * @since 1.5 * @author Jonathan Buttigieg * * @param array $data The statistics data. * @param object $response The API response. * @param int $id The image ID. * @param string $thumbnail_path The image path. * @param string $thumbnail_url The image URL. * @param string $size_key The image size key. * @param bool $is_aggressive The optimization level. * @return array $data The new optimization data. */ $data = apply_filters( 'imagify_fill_ngg_thumbnail_data', $data, $response, $this->id, $thumbnail_path, $thumbnail_url, $size_key, $optimization_level ); } $this->update_row( array( 'data' => $data, ) ); } // End if(). /** * Fires after optimizing all thumbnails. * * @since 1.5 * @author Jonathan Buttigieg * * @param int $id The image ID. * @param array $data The optimization data. */ do_action( 'after_imagify_ngg_optimize_thumbnails', $this->id, $data ); return $data; } /** * Optimize one size. * * @since 1.8 * @access public * @author Grégory Viguier * * @param string $size The thumbnail size. */ public function optimize_new_thumbnail( $size ) { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() ) { return; } if ( ! $this->is_optimized() ) { // The main image is not optimized. return; } $data = $this->get_data(); if ( isset( $data['sizes'][ $size ]['success'] ) ) { // This thumbnail has already been processed. return; } $sizes = $this->get_image_sizes(); $sizes = array_flip( $sizes ); if ( ! isset( $sizes[ $size ] ) ) { // This size doesn't exist. return; } /** * Fires before optimizing a thumbnail. * * @since 1.8 * @author Grégory Viguier * * @param int $id The image ID. */ do_action( 'before_imagify_ngg_optimize_new_thumbnail', $this->id ); $this->set_running_status(); $image_data = $this->storage->_image_mapper->find( $this->id ); $thumbnail_path = $this->storage->get_image_abspath( $image_data, $size ); $thumbnail_url = $this->storage->get_image_url( $image_data, $size ); $optimization_level = $this->get_optimization_level(); // Optimize the thumbnail size. $response = do_imagify( $thumbnail_path, array( 'optimization_level' => $optimization_level, 'context' => 'NGG', 'keep_exif' => true, 'backup' => false, ) ); $data = $this->fill_data( $data, $response, $size ); /** This filter is documented in inc/3rd-party/nextgen-gallery/inc/classes/class-imagify-ngg-attachment.php. */ $data = apply_filters( 'imagify_fill_ngg_thumbnail_data', $data, $response, $this->id, $thumbnail_path, $thumbnail_url, $size, $optimization_level ); $this->update_row( array( 'data' => $data, ) ); /** * Fires after optimizing a thumbnail. * * @since 1.8 * @author Grégory Viguier * * @param int $id The image ID. * @param array $data The optimization data. */ do_action( 'after_imagify_ngg_optimize_new_thumbnail', $this->id, $data ); $this->delete_running_status(); } /** * Re-optimize the given thumbnail sizes to the same level. * This is not used in this context. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param array $sizes The sizes to optimize. * @return array|void A WP_Error object on failure. */ public function reoptimize_thumbnails( $sizes ) {} /** * Process an attachment restoration from the backup file. * * @since 1.5 * @since 1.6.9 Doesn't use NGG's recover_image() anymore, these are fundamentally not the same things. This also prevents alt text, description, and tags deletion. * @since 1.6.9 Return true or a WP_Error object. * @author Jonathan Buttigieg * * @access public * @return bool|object True on success, a WP_Error object on error. */ public function restore() { // Check if the attachment extension is allowed. if ( ! $this->is_extension_supported() ) { return new WP_Error( 'mime_not_type_supported', __( 'Mime type not supported.', 'imagify' ) ); } // Stop the process if there is no backup file to restore. if ( ! $this->has_backup() ) { return new WP_Error( 'no_backup', __( 'Backup image not found.', 'imagify' ) ); } $image_data = $this->storage->_image_mapper->find( $this->id ); if ( ! $image_data ) { return new WP_Error( 'no_image', __( 'Image not found in NextGen Gallery data.', 'imagify' ) ); } /** * Make some more tests before restoring the backup. */ $full_abspath = $this->storage->get_image_abspath( $image_data ); $backup_abspath = $this->storage->get_image_abspath( $image_data, 'backup' ); if ( $backup_abspath === $full_abspath ) { return new WP_Error( 'same_path', __( 'Image path and backup path are identical.', 'imagify' ) ); } if ( ! $this->filesystem->is_writable( $full_abspath ) || ! $this->filesystem->is_writable( $this->filesystem->dir_path( $full_abspath ) ) ) { return new WP_Error( 'destination_not_writable', __( 'The image to replace is not writable.', 'imagify' ) ); } /** * Fires before restoring an attachment. * * @since 1.5 * @author Jonathan Buttigieg * * @param int $id The attachment ID. */ do_action( 'before_imagify_ngg_restore_attachment', $this->id ); if ( ! $this->filesystem->copy( $backup_abspath, $full_abspath, true, FS_CHMOD_FILE ) ) { return new WP_Error( 'copy_failed', __( 'Restoration failed.', 'imagify' ) ); } /** * Remove Imagify data. */ $this->delete_row(); /** * Fill in the NGG meta data. */ // 1- Meta data for the backup file. $dimensions = $this->filesystem->get_image_size( $backup_abspath ); $backup_data = array( 'backup' => array( 'filename' => $this->filesystem->file_name( $full_abspath ), // Yes, $full_abspath. 'width' => 0, 'height' => 0, 'generated' => microtime(), ), ); if ( $dimensions ) { $backup_data['backup']['width'] = $dimensions['width']; $backup_data['backup']['height'] = $dimensions['height']; } // 2- Meta data for the full sized image. $full_data = array( 'width' => 0, 'height' => 0, 'md5' => '', 'full' => array( 'width' => 0, 'height' => 0, 'md5' => '', ), ); $dimensions = $this->filesystem->get_image_size( $full_abspath ); if ( $dimensions ) { $full_data['width'] = $dimensions['width']; $full_data['height'] = $dimensions['height']; $full_data['full']['width'] = $dimensions['width']; $full_data['full']['height'] = $dimensions['height']; } $md5 = md5_file( $full_abspath ); if ( $md5 ) { $full_data['md5'] = $md5; $full_data['full']['md5'] = $md5; } // 3- Thumbnails meta data. $thumbnails_data = array(); // 4- Common meta data. require_once NGGALLERY_ABSPATH . '/lib/meta.php'; $meta_obj = new nggMeta( $image_data ); $common_data = $meta_obj->get_common_meta(); if ( $common_data ) { unset( $common_data['width'], $common_data['height'] ); } else { $common_data = array( 'aperture' => 0, 'credit' => '', 'camera' => '', 'caption' => '', 'created_timestamp' => 0, 'copyright' => '', 'focal_length' => 0, 'iso' => 0, 'shutter_speed' => 0, 'flash' => 0, 'title' => '', 'keywords' => '', ); if ( ! empty( $image_data->meta_data ) && is_array( $image_data->meta_data ) ) { $image_data->meta_data = array_merge( $common_data, $image_data->meta_data ); $common_data = array_intersect_key( $image_data->meta_data, $common_data ); } } $common_data['saved'] = true; /** * Re-create non-fullsize image sizes and add related data. */ $failed = array(); foreach ( $this->get_image_sizes() as $named_size ) { if ( 'full' === $named_size ) { continue; } $params = $this->storage->get_image_size_params( $image_data, $named_size ); $thumbnail = $this->storage->generate_image_clone( $backup_abspath, $this->storage->get_image_abspath( $image_data, $named_size ), $params ); if ( ! $thumbnail ) { // Failed. $failed[] = $named_size; continue; } $size_meta = array( 'width' => 0, 'height' => 0, 'filename' => M_I18n::mb_basename( $thumbnail->fileName ), 'generated' => microtime(), ); $dimensions = $this->filesystem->get_image_size( $thumbnail->fileName ); if ( $dimensions ) { $size_meta['width'] = $dimensions['width']; $size_meta['height'] = $dimensions['height']; } if ( isset( $params['crop_frame'] ) ) { $size_meta['crop_frame'] = $params['crop_frame']; } $thumbnails_data[ $named_size ] = $size_meta; } // End foreach(). do_action( 'ngg_recovered_image', $image_data ); /** * Save the meta data. */ $image_data->meta_data = array_merge( $backup_data, $full_data, $thumbnails_data, $common_data ); // Keep our property up to date. $this->image->_ngiw->_cache['meta_data'] = $image_data->meta_data; $this->image->_ngiw->_orig_image = $image_data; $post_id = $this->storage->_image_mapper->save( $image_data ); if ( ! $post_id ) { return new WP_Error( 'meta_data_not_saved', __( 'Related data could not be saved.', 'imagify' ) ); } if ( $failed ) { return new WP_Error( 'thumbnail_restore_failed', sprintf( _n( '%n thumbnail could not be restored.', '%n thumbnails could not be restored.', count( $failed ), 'imagify' ), count( $failed ) ), array( 'failed_thumbnails' => $failed ) ); } /** * Fires after restoring an attachment. * * @since 1.5 * @author Jonathan Buttigieg * * @param int $id The attachment ID. */ do_action( 'after_imagify_ngg_restore_attachment', $this->id ); return true; } /** * Get the image sizes. * * @since 1.8 * @access public * @author Grégory Viguier * * @return array */ public function get_image_sizes() { $sizes = array( 'full', ); // Remove common values (that have no value for us here, lol). $image_data = array_diff_key( $this->image->meta_data, array( 'backup' => 1, 'width' => 1, 'height' => 1, 'md5' => 1, 'full' => 1, 'aperture' => 1, 'credit' => 1, 'camera' => 1, 'caption' => 1, 'created_timestamp' => 1, 'copyright' => 1, 'focal_length' => 1, 'iso' => 1, 'shutter_speed' => 1, 'flash' => 1, 'title' => 1, 'keywords' => 1, 'saved' => 1, ) ); if ( ! $image_data ) { return $sizes; } foreach ( $image_data as $size_name => $size_data ) { if ( isset( $size_data['width'], $size_data['height'], $size_data['filename'], $size_data['generated'] ) ) { $sizes[] = $size_name; } } return $sizes; } } deprecated/classes/class-imagify-as3cf-deprecated.php 0000644 00000023007 15174671745 0016660 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class for deprecated methods from Imagify_AS3CF. * * @since 1.7 * @author Grégory Viguier * @deprecated */ class Imagify_AS3CF_Deprecated { /** * Class version. * * @var string * @since 1.0 * @since 1.9 Deprecated * @author Grégory Viguier * @deprecated */ const VERSION = '1.2'; /** * Context used with get_imagify_attachment(). * It matches the class name Imagify_AS3CF_Attachment. * * @var string * @since 1.6.6 * @since 1.9 Deprecated * @author Grégory Viguier * @deprecated */ const CONTEXT = 'AS3CF'; /** ----------------------------------------------------------------------------------------- */ /** VARIOUS HOOKS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Filter the context used for the optimization (and other stuff). * That way, we'll use the class Imagify_AS3CF_Attachment everywhere (instead of Imagify_Attachment), and make all the manual optimizations fine. * * @since 1.6.6 * @since 1.9 Deprecated * @author Grégory Viguier * @deprecated * * @param string $context The context to determine the class name. * @param int $attachment_id The attachment ID. * @return string The new context. */ public function optimize_attachment_context( $context, $attachment_id ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9' ); if ( self::CONTEXT === $context || ( 'wp' === $context && imagify_is_attachment_mime_type_supported( $attachment_id ) ) ) { return self::CONTEXT; } return $context; } /** * When getting all unoptimized attachment ids before performing a bulk optimization, download the missing files from S3. * * @since 1.6.7 * @since 1.9 Deprecated * @author Grégory Viguier * @deprecated * * @param array $ids An array of attachment IDs. * @param array $results An array of the data fetched from the database. * @param int $optimization_level The optimization level that will be used for the optimization. */ public function maybe_copy_files_from_s3( $ids, $results, $optimization_level ) { global $wpdb, $as3cf; _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9' ); if ( ! $as3cf || ! $as3cf->is_plugin_setup() ) { return; } // Remove from the list files that exist. $ids = array_flip( $ids ); foreach ( $ids as $id => $i ) { if ( empty( $results['filenames'][ $id ] ) ) { // Problem. unset( $ids[ $id ] ); continue; } $file_path = get_imagify_attached_file( $results['filenames'][ $id ] ); /** This filter is documented in inc/deprecated/deprecated.php. */ $file_path = apply_filters( 'imagify_file_path', $file_path, $id, 'as3cf_maybe_copy_files_from_s3' ); if ( ! $file_path || $this->filesystem->exists( $file_path ) ) { // The file exists, no need to retrieve it from S3. unset( $ids[ $id ] ); } else { $ids[ $id ] = $file_path; } } if ( ! $ids ) { // All files are already on the server. return; } // Determine which files are on S3. $ids = array_flip( $ids ); $sql_ids = implode( ',', $ids ); $s3_data = $wpdb->get_results( // WPCS: unprepared SQL ok. "SELECT pm.post_id as id, pm.meta_value as value FROM $wpdb->postmeta as pm WHERE pm.meta_key = 'amazonS3_info' AND pm.post_id IN ( $sql_ids ) ORDER BY pm.post_id DESC", ARRAY_A ); $wpdb->flush(); if ( ! $s3_data ) { return; } unset( $sql_ids ); $s3_data = Imagify_DB::combine_query_results( $ids, $s3_data, true ); // Retrieve the missing files from S3. $ids = array_flip( $ids ); foreach ( $s3_data as $id => $s3_object ) { $s3_object = maybe_unserialize( $s3_object ); $file_path = $ids[ $id ]; $attachment_backup_path = get_imagify_attachment_backup_path( $file_path ); $attachment_status = isset( $results['statuses'][ $id ] ) ? $results['statuses'][ $id ] : false; $attachment_optimization_level = isset( $results['optimization_levels'][ $id ] ) ? $results['optimization_levels'][ $id ] : false; // Don't try to re-optimize if there is no backup file. if ( 'success' === $attachment_status && $optimization_level !== $attachment_optimization_level && ! $this->filesystem->exists( $attachment_backup_path ) ) { unset( $s3_data[ $id ], $ids[ $id ] ); continue; } $directory = $this->filesystem->dir_path( $s3_object['key'] ); $directory = $this->filesystem->is_root( $directory ) ? '' : $directory; $s3_object['key'] = $directory . $this->filesystem->file_name( $file_path ); // Retrieve file from S3. if ( method_exists( $as3cf->plugin_compat, 'copy_s3_file_to_server' ) ) { $as3cf->plugin_compat->copy_s3_file_to_server( $s3_object, $file_path ); } else { $as3cf->plugin_compat->copy_provider_file_to_server( $s3_object, $file_path ); } unset( $s3_data[ $id ], $ids[ $id ] ); } } /** ----------------------------------------------------------------------------------------- */ /** AUTOMATIC OPTIMIZATION: OPTIMIZE AFTER S3 HAS DONE ITS WORK ============================= */ /** ----------------------------------------------------------------------------------------- */ /** * Filter the generated attachment meta data. * This is used when a new attachment has just been uploaded (or not, when wp_generate_attachment_metadata() is used). * We use it to tell the difference later in wp_update_attachment_metadata(). * * @since 1.6.6 * @since 1.8.4 Deprecated * @author Grégory Viguier * @see $this->do_async_job() * @deprecated * * @param array $metadata An array of attachment meta data. * @param int $attachment_id Current attachment ID. * @return array */ public function store_upload_ids( $metadata, $attachment_id ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.8.4' ); if ( imagify_is_attachment_mime_type_supported( $attachment_id ) ) { $this->uploads[ $attachment_id ] = 1; } return $metadata; } /** * After an image (maybe) being sent to S3, launch an async optimization. * * @since 1.6.6 * @since 1.8.4 Deprecated * @author Grégory Viguier * @see $this->store_upload_ids() * @deprecated * * @param array $metadata An array of attachment meta data. * @param int $attachment_id Current attachment ID. * @return array */ public function do_async_job( $metadata, $attachment_id ) { static $auto_optimize; _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.8.4' ); $is_new_upload = ! empty( $this->uploads[ $attachment_id ] ); unset( $this->uploads[ $attachment_id ] ); if ( ! $metadata || ! imagify_is_attachment_mime_type_supported( $attachment_id ) ) { return $metadata; } if ( ! isset( $auto_optimize ) ) { $auto_optimize = Imagify_Requirements::is_api_key_valid() && get_imagify_option( 'auto_optimize' ); } if ( $is_new_upload ) { // It's a new upload. if ( ! $auto_optimize ) { // Auto-optimization is disabled. return $metadata; } /** This filter is documented in inc/common/attachments.php. */ $optimize = apply_filters( 'imagify_auto_optimize_attachment', true, $attachment_id, $metadata ); if ( ! $optimize ) { return $metadata; } } if ( ! $is_new_upload ) { $attachment = get_imagify_attachment( self::CONTEXT, $attachment_id, 'as3cf_async_job' ); if ( ! $attachment->get_data() ) { // It's not a new upload and the attachment is not optimized yet. return $metadata; } } $data = array(); // Some specifics for the image editor. if ( isset( $_POST['action'], $_POST['do'], $_POST['postid'] ) && 'image-editor' === $_POST['action'] && (int) $_POST['postid'] === $attachment_id ) { // WPCS: CSRF ok. check_ajax_referer( 'image_editor-' . $_POST['postid'] ); $data = $_POST; } imagify_do_async_job( array( 'action' => 'imagify_async_optimize_as3cf', '_ajax_nonce' => wp_create_nonce( 'imagify_async_optimize_as3cf' ), 'post_id' => $attachment_id, 'metadata' => $metadata, 'data' => $data, ) ); return $metadata; } /** * Once an image has been sent to S3, optimize it and send it again. * * @since 1.6.6 * @since 1.8.4 Deprecated * @author Grégory Viguier * @deprecated */ public function optimize() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.8.4' ); check_ajax_referer( 'imagify_async_optimize_as3cf' ); if ( empty( $_POST['post_id'] ) || ! imagify_current_user_can( 'auto-optimize' ) ) { die(); } $attachment_id = absint( $_POST['post_id'] ); if ( ! $attachment_id || empty( $_POST['metadata'] ) || ! is_array( $_POST['metadata'] ) || empty( $_POST['metadata']['sizes'] ) ) { die(); } if ( ! imagify_is_attachment_mime_type_supported( $attachment_id ) ) { die(); } $optimization_level = null; $attachment = get_imagify_attachment( self::CONTEXT, $attachment_id, 'as3cf_optimize' ); // Some specifics for the image editor. if ( ! empty( $_POST['data']['do'] ) ) { $optimization_level = $attachment->get_optimization_level(); // Remove old optimization data. $attachment->delete_imagify_data(); if ( 'restore' === $_POST['data']['do'] ) { // Restore the backup file. $attachment->restore(); } } // Optimize it. $attachment->optimize( $optimization_level, $_POST['metadata'] ); } } deprecated/classes/class-imagify-ngg-dynamic-thumbnails-background-process.php 0000644 00000006715 15174671745 0023724 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class that handles background processing of thumbnails dynamically generated. * * @since 1.8 * @since 1.9 Deprecated * @author Grégory Viguier * @deprecated */ class Imagify_NGG_Dynamic_Thumbnails_Background_Process extends Imagify_Abstract_Background_Process { /** * Class version. * * @var string * @since 1.8 * @author Grégory Viguier */ const VERSION = '1.1'; /** * Action. * * @var string * @since 1.8 * @access protected * @author Grégory Viguier */ protected $action = 'ngg_dynamic_thumbnails'; /** * The single instance of the class. * * @var object * @since 1.8 * @access protected * @author Grégory Viguier */ protected static $_instance; /** * Get the main Instance. * * @since 1.8 * @access public * @author Grégory Viguier * * @return object Main instance. */ public static function get_instance() { if ( ! isset( self::$_instance ) ) { self::$_instance = new self(); } return self::$_instance; } /** * Initiate new background process. * * @since 1.9 * @access public * @author Grégory Viguier */ public function __construct() { imagify_deprecated_class( get_class( $this ), '1.9', '\\Imagify\\ThirdParty\\NGG\\DynamicThumbnails()' ); parent::__construct(); } /** ----------------------------------------------------------------------------------------- */ /** BACKGROUND PROCESS ====================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Push to queue. * * @since 1.8 * @access public * @author Grégory Viguier * * @param array $data { * The data to push in queue. * * @type int $id The image ID. Required. * @type string $size The thumbnail size. Required. * } * @return object Class instance. */ public function push_to_queue( $data ) { $key = $data['id'] . '|' . $data['size']; $this->data[ $key ] = $data; return $this; } /** * Dispatch. * * @since 1.8 * @access public * @author Grégory Viguier * * @return array|WP_Error */ public function dispatch() { if ( ! empty( $this->data ) ) { return parent::dispatch(); } } /** * Tell if a task is already in the queue. * * @since 1.8 * @access public * @author Grégory Viguier * * @param array $data { * The data to test against the queue. * * @type int $id The image ID. Required. * @type string $size The thumbnail size. Required. * } * @return bool */ public function is_in_queue( $data ) { $key = $data['id'] . '|' . $data['size']; return isset( $this->data[ $key ] ); } /** * Task: optimize the thumbnail. * * @since 1.8 * @access public * @author Grégory Viguier * * @param array $item { * The data to test against the queue. * * @type int $id The image ID. Required. * @type string $size The thumbnail size. Required. * } * @return bool False to remove the item from the queue. */ protected function task( $item ) { $attachment_id = absint( $item['id'] ); $size = sanitize_text_field( $item['size'] ); if ( ! $attachment_id || ! $size ) { return false; } $attachment = get_imagify_attachment( 'NGG', $attachment_id, 'ngg_optimize_dynamic_thumbnail' ); $attachment->optimize_new_thumbnail( $size ); return false; } } deprecated/classes/class-imagify-abstract-attachment.php 0000644 00000066322 15174671745 0017523 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Imagify Attachment base class. * * @since 1.0 * @since 1.9 Deprecated * @deprecated */ abstract class Imagify_Abstract_Attachment { /** * Class version. * * @var string */ const VERSION = '1.3.2'; /** * The attachment ID. * * @var int * @since 1.0 * @access public */ public $id = 0; /** * Context. * * @var string * @since 1.7.1 * @access protected * @author Grégory Viguier */ protected $context; /** * The attachment SQL DB class. * * @var string * @since 1.7 * @access protected */ protected $db_class_name = ''; /** * The attachment SQL data row. * * @var array * @since 1.7 * @access protected */ protected $row; /** * Tell if the file is an image. * * @var bool * @since 1.8 * @access protected * @see $this->is_image() */ protected $is_image; /** * Tell if the file is a pdf. * * @var bool * @since 1.8 * @access protected * @see $this->is_pdf() */ protected $is_pdf; /** * Stores the file extension (even if the extension is not supported by Imagify). * * @var string|null * @since 1.8 * @access protected * @see $this->get_extension() */ protected $extension = false; /** * Stores the file mime type + file extension (if the file is supported). * * @var array * @since 1.8 * @access protected * @see $this->get_file_type() */ protected $file_type; /** * Filesystem object. * * @var object Imagify_Filesystem * @since 1.7.1 * @access protected * @author Grégory Viguier */ protected $filesystem; /** * The editor instances used to resize files. * * @var array An array of image editor objects (WP_Image_Editor_Imagick, WP_Image_Editor_GD). * @since 1.7.1 * @access protected * @author Grégory Viguier */ protected $editors = array(); /** * The name of the transient that tells if optimization is processing. * * @var string * @since 1.7.1 * @access protected * @author Grégory Viguier */ protected $optimization_state_transient; /** * Tell if the optimization status is network-wide. * * @var bool * @since 1.7.1 * @access protected * @author Grégory Viguier */ protected $optimization_state_network_wide = false; /** * The constructor. * * @since 1.0 * @access public * * @param int|object $id The attachment ID or the attachment itself. * If an integer, make sure the attachment exists. */ public function __construct( $id = 0 ) { global $post; if ( $id ) { if ( $id instanceof WP_Post ) { $this->id = $id->ID; } elseif ( is_numeric( $id ) ) { $this->id = $id; } } elseif ( $post && $id instanceof WP_Post ) { $this->id = $post->ID; } $this->id = (int) $this->id; $this->filesystem = Imagify_Filesystem::get_instance(); $this->optimization_state_transient = 'wp' !== $this->get_context() ? strtolower( $this->get_context() ) . '-' : ''; $this->optimization_state_transient = 'imagify-' . $this->optimization_state_transient . 'async-in-progress-' . $this->id; } /** * Get the attachment context. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @return string */ public function get_context() { if ( $this->context ) { return $this->context; } $this->context = str_replace( array( 'Imagify_', 'Attachment' ), '', get_class( $this ) ); $this->context = trim( $this->context, '_' ); $this->context = $this->context ? $this->context : 'wp'; return $this->context; } /** * Tell if the current attachment is valid. * * @since 1.7 * @author Grégory Viguier * @access public * * @return bool */ public function is_valid() { return $this->id > 0; } /** * Get the attachment ID. * * @since 1.7 * @author Grégory Viguier * @access public * * @return int */ public function get_id() { return $this->id; } /** * Get the original attachment path. * * @since 1.0 * @access public * * @return string */ abstract public function get_original_path(); /** * Get the original attachment URL. * * @since 1.0 * @access public * * @return string */ abstract public function get_original_url(); /** * Get the attachment backup file path, even if the file doesn't exist. * * @since 1.6.13 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ abstract public function get_raw_backup_path(); /** * Get the attachment backup file path. * * @since 1.0 * @access public * * @return string|false The file path. False if it doesn't exist. */ public function get_backup_path() { if ( ! $this->is_valid() ) { return false; } $backup_path = $this->get_raw_backup_path(); if ( $backup_path && $this->filesystem->exists( $backup_path ) ) { return $backup_path; } return false; } /** * Get the attachment backup URL. * * @since 1.4 * @access public * * @return string|false */ public function get_backup_url() { if ( ! $this->is_valid() ) { return false; } return get_imagify_attachment_url( $this->get_raw_backup_path() ); } /** * Get the attachment optimization data. * * @since 1.0 * @access public * * @return array */ abstract public function get_data(); /** * Get the attachment optimization level. * * @since 1.0 * @access public * * @return int */ abstract public function get_optimization_level(); /** * Get the attachment optimization status (success or error). * * @since 1.0 * @access public * * @return string */ abstract public function get_status(); /** * Get the attachment error if there is one. * * @since 1.1.5 * @access public * * @return string The message error */ public function get_optimized_error() { $error = $this->get_size_data( 'full', 'error' ); if ( is_string( $error ) ) { return trim( $error ); } return ''; } /** * Count number of optimized sizes. * * @since 1.0 * @access public * * @return int */ public function get_optimized_sizes_count() { $data = $this->get_data(); $sizes = ! empty( $data['sizes'] ) && is_array( $data['sizes'] ) ? $data['sizes'] : array(); $count = 0; unset( $sizes['full'] ); if ( ! $sizes ) { return 0; } foreach ( $sizes as $size ) { if ( ! empty( $size['success'] ) ) { $count++; } } return $count; } /** * Delete the 3 metas used by Imagify. * * @since 1.6.6 * @access public * @author Grégory Viguier */ public function delete_imagify_data() { if ( ! $this->is_valid() ) { return; } delete_post_meta( $this->id, '_imagify_data' ); delete_post_meta( $this->id, '_imagify_status' ); delete_post_meta( $this->id, '_imagify_optimization_level' ); } /** * Get width and height of the original image. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array */ public function get_dimensions() { return array( 'width' => 0, 'height' => 0, ); } /** * Tell if the current item refers to an image, based on file extension. * * @since 1.8 * @access public * @author Grégory Viguier * * @return bool Returns false in case it's an image but not in a supported format (bmp for example). */ public function is_image() { if ( isset( $this->is_image ) ) { return $this->is_image; } $this->is_image = strpos( (string) $this->get_mime_type(), 'image/' ) === 0; return $this->is_image; } /** * Tell if the current item refers to a pdf, based on file extension. * * @since 1.8 * @access public * @author Grégory Viguier * * @return bool */ public function is_pdf() { if ( isset( $this->is_pdf ) ) { return $this->is_pdf; } $this->is_pdf = 'application/pdf' === $this->get_mime_type(); return $this->is_pdf; } /** * Get the file mime type. * * @since 1.8 * @access public * @author Grégory Viguier * * @return bool */ public function get_mime_type() { return $this->get_file_type()->type; } /** * Get the file mime type + file extension (if the file is supported). * * @since 1.8 * @access public * @see wp_check_filetype() * @author Grégory Viguier * * @return object */ public function get_file_type() { if ( isset( $this->file_type ) ) { return $this->file_type; } if ( ! $this->is_valid() ) { $this->file_type = (object) array( 'ext' => '', 'type' => '', ); return $this->file_type; } $path = $this->get_original_path(); if ( ! $path ) { $this->file_type = (object) array( 'ext' => '', 'type' => '', ); return $this->file_type; } $this->file_type = (object) wp_check_filetype( $path, imagify_get_mime_types() ); return $this->file_type; } /** * Get the attachment extension. * * @since 1.0 * @access public * * @return string|null */ public function get_extension() { if ( false !== $this->extension ) { return $this->extension; } if ( ! $this->is_valid() ) { $this->extension = null; return $this->extension; } $this->extension = $this->filesystem->path_info( $this->get_original_path(), 'extension' ); return $this->extension; } /** * Tell if the current file extension is supported. * * @since 1.7 * @access public * @author Grégory Viguier * * @return bool */ public function is_extension_supported() { return (bool) $this->get_file_type()->ext; } /** * Tell if the current file mime type is supported. * * @since 1.6.9 * @since 1.8 Does the same has this->is_extension_supported(). * @access public * @author Grégory Viguier * * @return bool */ public function is_mime_type_supported() { return (bool) $this->get_mime_type(); } /** * Tell if the current attachment has the required WP metadata. * * @since 1.6.12 * @access public * @author Grégory Viguier * * @return bool */ public function has_required_metadata() { if ( ! $this->is_valid() ) { return false; } return imagify_attachment_has_required_metadata( $this->id ); } /** * Get the attachment optimization level label. * * @since 1.2 * @since 1.7 Added $format parameter. * @access public * * @param string $format Format to display the label. Use %ICON% for the icon and %s for the label. * @return string */ public function get_optimization_level_label( $format = '%s' ) { return imagify_get_optimization_level_label( $this->get_optimization_level(), $format ); } /** * Get the original attachment size. * * @since 1.0 * @access public * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @return string|int */ public function get_original_size( $human_format = true, $decimals = 2 ) { if ( ! $this->is_valid() ) { return $human_format ? imagify_size_format( 0, $decimals ) : 0; } $size = $this->get_size_data( 'full', 'original_size' ); if ( ! $size ) { // Check for the backup file first. $filepath = $this->get_backup_path(); if ( ! $filepath ) { $filepath = $this->get_original_path(); $filepath = $filepath && $this->filesystem->exists( $filepath ) ? $filepath : false; } $size = $filepath ? $this->filesystem->size( $filepath ) : 0; } if ( $human_format ) { return imagify_size_format( (int) $size, $decimals ); } return (int) $size; } /** * Get the optimized attachment size. * * @since 1.7 * @access public * @author Grégory Viguier * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @return string|int */ public function get_optimized_size( $human_format = true, $decimals = 2 ) { if ( ! $this->is_valid() ) { return $human_format ? imagify_size_format( 0, $decimals ) : 0; } $size = $this->get_size_data( 'full', 'optimized_size' ); if ( ! $size ) { $filepath = $this->get_original_path(); $filepath = $filepath && $this->filesystem->exists( $filepath ) ? $filepath : false; $size = $filepath ? $this->filesystem->size( $filepath ) : 0; } if ( $human_format ) { return imagify_size_format( (int) $size, $decimals ); } return (int) $size; } /** * Get the optimized attachment size. * * @since 1.7 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_saving_percent() { if ( ! $this->is_valid() ) { return round( (float) 0, 2 ); } $percent = $this->get_size_data( 'full', 'percent' ); $percent = $percent ? $percent : (float) 0; return round( $percent, 2 ); } /** * Get the overall optimized size (all thumbnails). * * @since 1.7 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_overall_saving_percent() { if ( ! $this->is_valid() ) { return round( (float) 0, 2 ); } $percent = $this->get_data(); $percent = ! empty( $percent['stats']['percent'] ) ? $percent['stats']['percent'] : (float) 0; return round( $percent, 2 ); } /** * Get the statistics of a specific size. * * @since 1.0 * @access public * * @param string $size The thumbnail slug. * @param string $key The specific data slug. * @return array|string */ public function get_size_data( $size = 'full', $key = '' ) { $data = $this->get_data(); $stats = array(); if ( isset( $data['sizes'][ $size ] ) ) { $stats = $data['sizes'][ $size ]; } if ( isset( $stats[ $key ] ) ) { $stats = $stats[ $key ]; } return $stats; } /** * Get the global statistics data or a specific one. * * @since 1.0 * @access public * * @param string $key The specific data slug. * @return array|string */ public function get_stats_data( $key = '' ) { $data = $this->get_data(); $stats = ''; if ( isset( $data['stats'] ) ) { $stats = $data['stats']; } if ( isset( $stats[ $key ] ) ) { $stats = $stats[ $key ]; } return $stats; } /** * Check if the attachment is already optimized (before Imagify). * * @since 1.1.6 * @access public * * @return bool True if the attachment is optimized. */ public function is_already_optimized() { return 'already_optimized' === $this->get_status(); } /** * Check if the attachment is optimized. * * @since 1.0 * @access public * * @return bool True if the attachment is optimized. */ public function is_optimized() { return 'success' === $this->get_status(); } /** * Check if the attachment exceeding the limit size (> 5mo). * * @since 1.0 * @access public * * @return bool True if the attachment is skipped. */ public function is_exceeded() { $filepath = $this->get_original_path(); $size = 0; if ( $filepath && $this->filesystem->exists( $filepath ) ) { $size = $this->filesystem->size( $filepath ); } return $size > IMAGIFY_MAX_BYTES; } /** * Check if the attachment has a backup of the original size. * * @since 1.0 * @access public * * @return bool True if the attachment has a backup. */ public function has_backup() { return (bool) $this->get_backup_path(); } /** * Check if the attachment has an error. * * @since 1.0 * @access public * * @return bool True if the attachment has an error. */ public function has_error() { return 'error' === $this->get_status(); } /** * Get an image editor instance (WP_Image_Editor_Imagick, WP_Image_Editor_GD). * * @since 1.7.1 * @access protected * @author Grégory Viguier * * @param string $path A file path. * @return object An image editor instance (WP_Image_Editor_Imagick, WP_Image_Editor_GD). A WP_Error object on error. */ protected function get_editor( $path ) { if ( isset( $this->editors[ $path ] ) ) { return $this->editors[ $path ]; } $this->editors[ $path ] = wp_get_image_editor( $path, array( 'methods' => self::get_editor_methods(), ) ); return $this->editors[ $path ]; } /** * Get the image editor methods we will use. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @return array */ public static function get_editor_methods() { static $methods; if ( isset( $methods ) ) { return $methods; } $methods = array( 'resize', 'multi_resize', 'generate_filename', 'save', ); if ( Imagify_Filesystem::get_instance()->can_get_exif() ) { $methods[] = 'rotate'; } return $methods; } /** * Update the metadata size of the attachment * * @since 1.2 * @access public * * @return void */ abstract public function update_metadata_size(); /** * Delete the backup file. * * @since 1.0 * @access public * * @return void */ public function delete_backup() { $backup_path = $this->get_backup_path(); if ( $backup_path ) { $this->filesystem->delete( $backup_path ); } } /** * Get the registered sizes. * * @since 1.6.10 * @access public * @author Grégory Viguier * * @return array Data for the registered thumbnail sizes. */ public static function get_registered_sizes() { static $registered_sizes; if ( ! isset( $registered_sizes ) ) { $registered_sizes = get_imagify_thumbnail_sizes(); } return $registered_sizes; } /** * Get the unoptimized sizes for a specific attachment. * * @since 1.6.10 * @access public * @author Grégory Viguier * * @return array Data for the unoptimized thumbnail sizes. * Each size data has a "file" key containing the name the thumbnail "should" have. */ public function get_unoptimized_sizes() { // The attachment must have been optimized once and have a backup. if ( ! $this->is_valid() || ! $this->is_optimized() || ! $this->has_backup() || ! $this->is_image() ) { return array(); } $registered_sizes = self::get_registered_sizes(); $attachment_sizes = $this->get_data(); $attachment_sizes = ! empty( $attachment_sizes['sizes'] ) ? $attachment_sizes['sizes'] : array(); $missing_sizes = array_diff_key( $registered_sizes, $attachment_sizes ); if ( ! $missing_sizes ) { // We have everything we need. return array(); } // Get full size dimensions. $orig = wp_get_attachment_metadata( $this->id ); $orig_f = ! empty( $orig['file'] ) ? $orig['file'] : ''; $orig_w = ! empty( $orig['width'] ) ? (int) $orig['width'] : 0; $orig_h = ! empty( $orig['height'] ) ? (int) $orig['height'] : 0; if ( ! $orig_f || ! $orig_w || ! $orig_h ) { return array(); } $orig_f = $this->filesystem->path_info( $orig_f ); $orig_f = $orig_f['file_base'] . '-{%suffix%}.' . $orig_f['extension']; // Test if the missing sizes are needed. $disallowed_sizes = get_imagify_option( 'disallowed-sizes' ); $is_active_for_network = imagify_is_active_for_network(); foreach ( $missing_sizes as $size_name => $size_data ) { $duplicate = ( $orig_w === $size_data['width'] ) && ( $orig_h === $size_data['height'] ); if ( $duplicate ) { // Same dimensions as the full size. unset( $missing_sizes[ $size_name ] ); continue; } if ( ! $is_active_for_network && isset( $disallowed_sizes[ $size_name ] ) ) { // This size must be optimized. unset( $missing_sizes[ $size_name ] ); continue; } $resize_result = image_resize_dimensions( $orig_w, $orig_h, $size_data['width'], $size_data['height'], $size_data['crop'] ); if ( ! $resize_result ) { // This size is not needed. unset( $missing_sizes[ $size_name ] ); continue; } // Provide what should be the file name. list( , , , , $dst_w, $dst_h ) = $resize_result; $missing_sizes[ $size_name ]['file'] = str_replace( '{%suffix%}', "{$dst_w}x{$dst_h}", $orig_f ); } return $missing_sizes; } /** * Fills statistics data with values from $data array. * * @since 1.0 * @since 1.6.5 Not static anymore. * @since 1.6.6 Removed the attachment ID parameter. * @since 1.7 Removed the image URL parameter. * @access public * * @param array $data The statistics data. * @param object $response The API response. * @param string $size The attachment size key. * @return bool|array False if the original size has an error or an array contains the data for other result. */ abstract public function fill_data( $data, $response, $size = 'full' ); /** * Optimize all sizes with Imagify. * * @since 1.0 * @access public * * @param int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). * @param array $metadata The attachment meta data. * @return array $optimized_data The optimization data. */ abstract public function optimize( $optimization_level = null, $metadata = array() ); /** * Optimize missing sizes with Imagify. * * @since 1.6.10 * @access public * @author Grégory Viguier * * @param int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). * @return array|object An array of thumbnail data, size by size. A WP_Error object on failure. */ abstract public function optimize_missing_thumbnails( $optimization_level = null ); /** * Re-optimize the given thumbnail sizes to the same level. * Before doing this, the given sizes must be restored. * * @since 1.7.1 * @access public * @author Grégory Viguier * * @param array $sizes The sizes to optimize. * @return array|void A WP_Error object on failure. */ abstract public function reoptimize_thumbnails( $sizes ); /** * Process an attachment restoration from the backup file. * * @since 1.0 * @access public * * @return void */ abstract public function restore(); /** * Resize an image if bigger than the maximum width defined in the settings. * * @since 1.5.7 * @since 1.7.1 Keys for width and height in $attachment_sizes are now 'width' and 'height' instead of 0 and 1. * @access public * @author Remy Perona * * @param string $attachment_path Path to the image. * @param array $attachment_sizes Array of original image dimensions. * @param int $max_width Maximum width defined in the settings. * @return string Path the the resized image or the original image if the resize failed. */ public function resize( $attachment_path, $attachment_sizes, $max_width ) { if ( ! $this->is_valid() || ! $this->is_image() ) { return ''; } $editor = $this->get_editor( $attachment_path ); if ( is_wp_error( $editor ) ) { return $editor; } $new_sizes = wp_constrain_dimensions( $attachment_sizes['width'], $attachment_sizes['height'], $max_width ); $image_type = strtolower( (string) $this->filesystem->path_info( $attachment_path, 'extension' ) ); // Try to correct for auto-rotation if the info is available. if ( $this->filesystem->can_get_exif() && ( 'jpg' === $image_type || 'jpe' === $image_type || 'jpeg' === $image_type ) ) { $exif = $this->filesystem->get_image_exif( $attachment_path ); $orientation = isset( $exif['Orientation'] ) ? (int) $exif['Orientation'] : 1; switch ( $orientation ) { case 3: $editor->rotate( 180 ); break; case 6: $editor->rotate( -90 ); break; case 8: $editor->rotate( 90 ); } } // Prevent removal of the exif data when resizing (only works with Imagick). add_filter( 'image_strip_meta', '__return_false', 789 ); $resized = $editor->resize( $new_sizes[0], $new_sizes[1], false ); // Remove the filter when we're done to prevent any conflict. remove_filter( 'image_strip_meta', '__return_false', 789 ); if ( is_wp_error( $resized ) ) { return $resized; } $resized_image_path = $editor->generate_filename( 'imagifyresized' ); $resized_image_saved = $editor->save( $resized_image_path ); if ( is_wp_error( $resized_image_saved ) ) { return $resized_image_saved; } return $resized_image_path; } /** ----------------------------------------------------------------------------------------- */ /** WORKING STATUS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the file is currently being optimized (or restored, etc). * * @since 1.7.1 * @author Grégory Viguier * @access public * * @return bool */ public function is_running() { $callback = $this->optimization_state_network_wide ? 'get_site_transient' : 'get_transient'; return false !== call_user_func( $callback, $this->optimization_state_transient ); } /** * Set the running status to "running" for 10 minutes. * * @since 1.7.1 * @author Grégory Viguier * @access public */ public function set_running_status() { $callback = $this->optimization_state_network_wide ? 'set_site_transient' : 'set_transient'; call_user_func( $callback, $this->optimization_state_transient, true, 10 * MINUTE_IN_SECONDS ); } /** * Unset the running status. * * @since 1.7.1 * @author Grégory Viguier * @access public */ public function delete_running_status() { $callback = $this->optimization_state_network_wide ? 'delete_site_transient' : 'delete_transient'; call_user_func( $callback, $this->optimization_state_transient ); } /** ----------------------------------------------------------------------------------------- */ /** DB ROW ================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the data row. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array */ public function get_row() { if ( isset( $this->row ) ) { return $this->row; } if ( ! $this->db_class_name || ! $this->is_valid() ) { return $this->invalidate_row(); } $this->row = $this->get_row_db_instance()->get( $this->id ); if ( ! $this->row ) { return $this->invalidate_row(); } return $this->row; } /** * Update the data row. * * @since 1.7 * @author Grégory Viguier * @access public * * @param array $data The data to update. */ public function update_row( $data ) { if ( ! $this->db_class_name || ! $this->is_valid() ) { return; } $this->get_row_db_instance()->update( $this->id, $data ); $this->reset_row_cache(); } /** * Delete the data row. * * @since 1.7 * @author Grégory Viguier * @access public */ public function delete_row() { if ( ! $this->db_class_name || ! $this->is_valid() ) { return; } $this->get_row_db_instance()->delete( $this->id ); $this->invalidate_row(); } /** * Shorthand to get the DB table instance. * * @since 1.7.1 * @author Grégory Viguier * @access public * * @return object The DB table instance. */ public function get_row_db_instance() { return call_user_func( array( $this->db_class_name, 'get_instance' ) ); } /** * Invalidate the row. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array The row */ public function invalidate_row() { $this->row = array(); return $this->row; } /** * Reset the row cache. * * @since 1.7 * @author Grégory Viguier * @access public * * @return null The row. */ public function reset_row_cache() { $this->row = null; return $this->row; } } deprecated/classes/class-imagify-abstract-db-deprecated.php 0000644 00000002307 15174671745 0020047 0 ustar 00 <?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class for deprecated methods from Imagify_Abstract_DB. * * @since 1.7 * @author Grégory Viguier * @deprecated */ class Imagify_Abstract_DB_Deprecated { /** * Check if the given table exists. * * @since 1.5 In Imagify_Abstract_DB. * @since 1.7 Deprecated. * @access public * @deprecated * * @param string $table The table name. * @return bool True if the table name exists. */ public function table_exists( $table ) { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.7.0', 'Imagify_DB::table_exists( $table )' ); return Imagify_DB::table_exists( $table ); } /** * Main Instance. * Ensures only one instance of class is loaded or can be loaded. * Well, actually it ensures nothing since it's not a full singleton pattern. * * @since 1.5 In Imagify_NGG_DB. * @since 1.7 Deprecated. * @access public * @author Jonathan Buttigieg * @deprecated * * @return object Main instance. */ public static function instance() { _deprecated_function( 'Imagify_Abstract_DB::instance()', '1.6.5', 'Imagify_Abstract_DB::get_instance()' ); return self::get_instance(); } } deprecated/3rd-party.php 0000644 00000025337 15174671745 0011235 0 ustar 00 <?php use Imagify\User\User; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); if ( class_exists( 'C_NextGEN_Bootstrap' ) && class_exists( 'Mixin' ) && get_site_option( 'ngg_options' ) ) : /** * Create the Imagify table needed for NGG compatibility. * * @since 1.5 * @since 1.7 Deprecated. * @author Jonathan Buttigieg * @deprecated */ function _imagify_create_ngg_table() { _deprecated_function( __FUNCTION__ . '()', '1.7', '\\Imagify\\ThirdParty\\NGG\\DB::get_instance()->maybe_upgrade_table()' ); \Imagify\ThirdParty\NGG\DB::get_instance()->maybe_upgrade_table(); } /** * Update all Imagify stats for NGG Bulk Optimization. * * @since 1.5 * @since 1.7 Deprecated. * @author Jonathan Buttigieg * @deprecated */ function _imagify_ngg_update_bulk_stats() { _deprecated_function( __FUNCTION__ . '()', '1.7', 'imagify_ngg_bulk_page_data()' ); if ( empty( $_GET['page'] ) || imagify_get_ngg_bulk_screen_slug() !== $_GET['page'] ) { // WPCS: CSRF ok. return; } add_filter( 'imagify_count_attachments' , 'imagify_ngg_count_attachments' ); add_filter( 'imagify_count_optimized_attachments' , 'imagify_ngg_count_optimized_attachments' ); add_filter( 'imagify_count_error_attachments' , 'imagify_ngg_count_error_attachments' ); add_filter( 'imagify_count_unoptimized_attachments' , 'imagify_ngg_count_unoptimized_attachments' ); add_filter( 'imagify_percent_optimized_attachments' , 'imagify_ngg_percent_optimized_attachments' ); add_filter( 'imagify_count_saving_data' , 'imagify_ngg_count_saving_data', 8 ); } /** * Prepare the data that goes back with the Heartbeat API. * * @since 1.5 * @since 1.7.1 Deprecated. * @deprecated * * @param array $response The Heartbeat response. * @param array $data The $_POST data sent. * @return array */ function _imagify_ngg_heartbeat_received( $response, $data ) { _deprecated_function( __FUNCTION__ . '()', '1.7.1' ); if ( ! isset( $data['imagify_heartbeat'] ) || 'update_ngg_bulk_data' !== $data['imagify_heartbeat'] ) { return $response; } add_filter( 'imagify_count_saving_data', 'imagify_ngg_count_saving_data', 8 ); $saving_data = imagify_count_saving_data(); $user = new User(); $response['imagify_bulk_data'] = array( // User account. 'unconsumed_quota' => is_wp_error( $user ) ? 0 : $user->get_percent_unconsumed_quota(), // Global chart. 'optimized_attachments_percent' => imagify_ngg_percent_optimized_attachments(), 'unoptimized_attachments' => imagify_ngg_count_unoptimized_attachments(), 'optimized_attachments' => imagify_ngg_count_optimized_attachments(), 'errors_attachments' => imagify_ngg_count_error_attachments(), // Stats block. 'already_optimized_attachments' => number_format_i18n( $saving_data['count'] ), 'original_human' => imagify_size_format( $saving_data['original_size'], 1 ), 'optimized_human' => imagify_size_format( $saving_data['optimized_size'], 1 ), 'optimized_percent' => $saving_data['percent'], ); return $response; } /** * Filter the current user capability to operate Imagify. * * @since 1.6.11 * @since 1.9 Deprecated. * @see imagify_get_capacity() * @author Grégory Viguier * @deprecated * * @param bool $user_can Tell if the current user has the required capacity to operate Imagify. * @param string $capacity The user capacity. * @param string $describer Capacity describer. See imagify_get_capacity() for possible values. Can also be a "real" user capacity. * @param int $post_id A post ID (a gallery ID for NGG). * @return bool */ function imagify_ngg_current_user_can( $user_can, $capacity, $describer, $post_id ) { static $user_can_per_gallery = array(); _deprecated_function( __FUNCTION__ . '()', '1.9' ); if ( ! $user_can || ! $post_id || 'NextGEN Manage gallery' !== $capacity ) { return $user_can; } $image = nggdb::find_image( $post_id ); if ( isset( $user_can_per_gallery[ $image->galleryid ] ) ) { return $user_can_per_gallery[ $image->galleryid ]; } $gallery_mapper = C_Gallery_Mapper::get_instance(); $gallery = $gallery_mapper->find( $image->galleryid, false ); if ( get_current_user_id() === $gallery->author || current_user_can( 'NextGEN Manage others gallery' ) ) { // The user created this gallery or can edit others galleries. $user_can_per_gallery[ $image->galleryid ] = true; return $user_can_per_gallery[ $image->galleryid ]; } // The user can't edit this gallery. $user_can_per_gallery[ $image->galleryid ] = false; return $user_can_per_gallery[ $image->galleryid ]; } /** * Get user capacity to operate Imagify within NGG galleries. * It is meant to be used to filter 'imagify_capacity'. * * @since 1.6.11 * @since 1.9 Deprecated. * @see imagify_get_capacity() * @author Grégory Viguier * @deprecated * * @param string $capacity The user capacity. * @param string $describer Capacity describer. See imagify_get_capacity() for possible values. Can also be a "real" user capacity. * @return string */ function imagify_get_ngg_capacity( $capacity = 'edit_post', $describer = 'manual-optimize' ) { if ( 'manual-optimize' === $describer ) { return 'NextGEN Manage gallery'; } return $capacity; } /** * Dispatch the optimization process. * * @since 1.8 * @since 1.9 Deprecated. * @author Grégory Viguier * @deprecated */ function imagify_ngg_dispatch_dynamic_thumbnail_background_process() { _deprecated_function( __FUNCTION__ . '()', '1.9' ); Imagify_NGG_Dynamic_Thumbnails_Background_Process::get_instance()->save()->dispatch(); } /** * On manual optimization, manual re-optimization, and manual restoration, filter the user capacity to operate Imagify within NGG. * * @since 1.6.11 * @since 1.9 Deprecated. * @author Grégory Viguier * @deprecated */ function _do_admin_post_imagify_ngg_user_capacity() { _deprecated_function( __FUNCTION__ . '()', '1.9' ); if ( ! empty( $_GET['context'] ) && 'NGG' === $_GET['context'] ) { // WPCS: CSRF ok. add_filter( 'imagify_capacity', 'imagify_get_ngg_capacity', 10, 2 ); } } /** * Get all unoptimized attachment ids. * * @since 1.0 * @since 1.9 Deprecated * @author Jonathan Buttigieg * @deprecated */ function _do_wp_ajax_imagify_ngg_get_unoptimized_attachment_ids() { _deprecated_function( __FUNCTION__ . '()', '1.9', '\\Imagify\\ThirdParty\\NGG\\AdminAjaxPost::get_instance()->get_media_ids()' ); \Imagify\ThirdParty\NGG\AdminAjaxPost::get_instance()->get_media_ids(); } /** * Provide custom folder type data. * * @since 1.7 * @since 1.9 Deprecated * @author Grégory Viguier * @deprecated * * @param array $data An array with keys corresponding to cell classes, and values formatted with HTML. * @param string $context A context. * @return array */ function imagify_ngg_get_folder_type_data( $data, $context ) { _deprecated_function( __FUNCTION__ . '()', '1.9' ); if ( 'ngg' !== $context ) { return $data; } // Already filtered in imagify_ngg_bulk_page_data(). $total_saving_data = imagify_count_saving_data(); return [ 'images-optimized' => imagify_ngg_count_optimized_attachments(), 'errors' => imagify_ngg_count_error_attachments(), 'optimized' => $total_saving_data['optimized_size'], 'original' => $total_saving_data['original_size'], 'errors_url' => admin_url( 'admin.php?page=nggallery-manage-gallery' ), ]; } endif; if ( function_exists( 'wr2x_delete_attachment' ) ) : /** * Remove all retina versions if they exist. * * @since 1.0 * @since 1.8 Deprecated. * @deprecated * * @param int $attachment_id An attachment ID. */ function _imagify_wr2x_delete_attachment_on_restore( $attachment_id ) { _deprecated_function( __FUNCTION__ . '()', '1.8' ); wr2x_delete_attachment( $attachment_id ); } /** * Regenerate all retina versions. * * @since 1.0 * @since 1.8 Deprecated. * @deprecated * * @param int $attachment_id An attachment ID. */ function _imagify_wr2x_generate_images_on_restore( $attachment_id ) { _deprecated_function( __FUNCTION__ . '()', '1.8' ); wr2x_delete_attachment( $attachment_id ); wr2x_generate_images( wp_get_attachment_metadata( $attachment_id ) ); } /** * Filter the optimization data of each thumbnail. * * @since 1.0 * @since 1.8 Deprecated. * @deprecated * * @param array $data The statistics data. * @param object $response The API response. * @param int $id The attachment ID. * @param string $path The attachment path. * @param string $url The attachment URL. * @param string $size_key The attachment size key. * @param bool $optimization_level The optimization level. * @return array $data The new optimization data. */ function _imagify_optimize_wr2x( $data, $response, $id, $path, $url, $size_key, $optimization_level ) { _deprecated_function( __FUNCTION__ . '()', '1.8', 'Imagify_WP_Retina_2x::optimize_retina_version()' ); /** * Allow to optimize the retina version generated by WP Retina x2. * * @since 1.0 * * @param bool $do_retina True will force the optimization. */ $do_retina = apply_filters( 'do_imagify_optimize_retina', true ); $retina_path = wr2x_get_retina( $path ); if ( empty( $retina_path ) || ! $do_retina ) { return $data; } $response = do_imagify( $retina_path, array( 'backup' => false, 'optimization_level' => $optimization_level, 'context' => 'wp-retina', ) ); $attachment = get_imagify_attachment( 'wp', $id, 'imagify_fill_thumbnail_data' ); return $attachment->fill_data( $data, $response, $size_key . '@2x' ); } endif; if ( defined( 'WP_ROCKET_VERSION' ) ) : /** * Don't load Imagify CSS & JS files on WP Rocket options screen to avoid conflict with older version of SweetAlert. * Since 1.6.10 they should be enqueued only if one of our notices displays here. * * @since 1.6.9.1 * @since 1.6.10 Use the new class Imagify_Assets. * @since 1.9.3 Deprecated. * @author Jonathan Buttigieg * @author Grégory Viguier * @deprecated */ function imagify_dequeue_sweetalert_wprocket() { _deprecated_function( __FUNCTION__ . '()', '1.9.3', '\\Imagify\\ThirdParty\\WPRocket\\Main::dequeue_sweetalert()' ); if ( ! defined( 'WP_ROCKET_PLUGIN_SLUG' ) ) { return; } if ( ! imagify_is_screen( 'settings_page_' . WP_ROCKET_PLUGIN_SLUG ) && ! imagify_is_screen( 'settings_page_' . WP_ROCKET_PLUGIN_SLUG . '-network' ) ) { return; } Imagify_Assets::get_instance()->dequeue_script( array( 'sweetalert-core', 'sweetalert', 'notices' ) ); } endif; deprecated/Traits/Optimization/Process/AbstractProcessDeprecatedTrait.php 0000644 00000002004 15174671745 0023033 0 ustar 00 <?php namespace Imagify\Deprecated\Traits\Optimization\Process; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Trait containing deprecated methods of the class \Imagify\Optimization\Process\AbstractProcess. * * @since * @author Grégory Viguier */ trait AbstractProcessDeprecatedTrait { /** * Get the File instance. * * @since 1.9 * @since Deprecated * @access public * @author Grégory Viguier * @deprecated * * @return File|false */ public function get_file() { $full_class = get_class( $this ); $class_name = explode( '\\', trim( $full_class, '\\' ) ); $class_name = end( $class_name ); _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '', '( new \Imagify\Optimization\Process\\' . $class_name . '( $id ) )->get_fullsize_file()' ); if ( isset( $this->file ) ) { return $this->file; } $this->file = false; if ( $this->get_media() ) { $this->file = new File( $this->get_media()->get_raw_fullsize_path() ); } return $this->file; } } deprecated/Traits/Media/CustomFoldersDeprecatedTrait.php 0000644 00000001730 15174671745 0017442 0 ustar 00 <?php namespace Imagify\Deprecated\Traits\Media; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Trait containing deprecated methods of the class \Imagify\Media\CustomFolders. * * @since 1.9.8 * @author Grégory Viguier */ trait CustomFoldersDeprecatedTrait { /** * Get the original media's URL. * * @since 1.9 * @since 1.9.8 Deprecated * @access public * @author Grégory Viguier * @deprecated * * @return string|bool The file URL. False on failure. */ public function get_original_url() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '1.9.8', '( new \Imagify\Media\CustomFolders( $id ) )->get_fullsize_url()' ); if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_url(); } $row = $this->get_row(); if ( ! $row || empty( $row['path'] ) ) { return false; } return \Imagify_Files_Scan::remove_placeholder( $row['path'], 'url' ); } } deprecated/Traits/Media/NGGDeprecatedTrait.php 0000644 00000001520 15174671745 0015261 0 ustar 00 <?php namespace Imagify\Deprecated\Traits\Media; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Trait containing deprecated methods of the class \Imagify\Media\NGG. * * @since * @author Grégory Viguier */ trait NGGDeprecatedTrait { /** * Get the original media's URL. * * @since 1.9 * @since Deprecated * @access public * @author Grégory Viguier * @deprecated * * @return string|bool The file URL. False on failure. */ public function get_original_url() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '', '( new \Imagify\Media\NGG( $id ) )->get_fullsize_url()' ); if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_url(); } return ! empty( $this->image->imageURL ) ? $this->image->imageURL : false; } } deprecated/Traits/Media/NoopDeprecatedTrait.php 0000644 00000001224 15174671745 0015562 0 ustar 00 <?php namespace Imagify\Deprecated\Traits\Media; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Trait containing deprecated methods of the class \Imagify\Media\Noop. * * @since * @author Grégory Viguier */ trait NoopDeprecatedTrait { /** * Get the original media's URL. * * @since 1.9 * @since Deprecated * @access public * @author Grégory Viguier * @deprecated * * @return string|bool The file URL. False on failure. */ public function get_original_url() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '', '( new \Imagify\Media\Noop( $id ) )->get_fullsize_url()' ); return false; } } deprecated/Traits/Media/WPDeprecatedTrait.php 0000644 00000001514 15174671745 0015177 0 ustar 00 <?php namespace Imagify\Deprecated\Traits\Media; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Trait containing deprecated methods of the class \Imagify\Media\WP. * * @since * @author Grégory Viguier */ trait WPDeprecatedTrait { /** * Get the original media's URL. * * @since 1.9 * @since Deprecated * @access public * @author Grégory Viguier * @deprecated * * @return string|bool The file URL. False on failure. */ public function get_original_url() { _deprecated_function( get_class( $this ) . '::' . __FUNCTION__ . '()', '', '( new \Imagify\Media\WP( $id ) )->get_fullsize_url()' ); if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_url(); } $url = wp_get_attachment_url( $this->id ); return $url ? $url : false; } } main.php 0000644 00000007004 15174671745 0006223 0 ustar 00 <?php use WP_Rocket\Addon\Cloudflare\Cloudflare; use WP_Rocket\Dependencies\League\Container\Container; use WP_Rocket\Plugin; defined( 'ABSPATH' ) || exit; // Composer autoload. if ( file_exists( WP_ROCKET_PATH . 'vendor/autoload.php' ) ) { require WP_ROCKET_PATH . 'vendor/autoload.php'; } require_once WP_ROCKET_FUNCTIONS_PATH . 'files.php'; Cloudflare::fix_cf_flexible_ssl(); require_once WP_ROCKET_INC_PATH . 'Dependencies' . DIRECTORY_SEPARATOR . 'ActionScheduler' . DIRECTORY_SEPARATOR . 'action-scheduler.php'; /** * Tell WP what to do when plugin is loaded. * * @since 1.0 */ function rocket_init() { // Nothing to do if autosave. if ( defined( 'DOING_AUTOSAVE' ) ) { return; } /** * Fires when WP Rocket starts to load. */ do_action( 'wp_rocket_before_load' ); // Call defines and functions. require WP_ROCKET_FUNCTIONS_PATH . 'options.php'; // Last constants. define( 'WP_ROCKET_PLUGIN_NAME', 'WP Rocket' ); define( 'WP_ROCKET_PLUGIN_SLUG', sanitize_key( WP_ROCKET_PLUGIN_NAME ) ); require WP_ROCKET_INC_PATH . '/API/bypass.php'; $wp_rocket = new Plugin( WP_ROCKET_PATH . 'views', new Container() ); $wp_rocket->load(); // Call defines and functions. require_once WP_ROCKET_FUNCTIONS_PATH . 'api.php'; require WP_ROCKET_FUNCTIONS_PATH . 'posts.php'; require WP_ROCKET_FUNCTIONS_PATH . 'admin.php'; require WP_ROCKET_FUNCTIONS_PATH . 'formatting.php'; require WP_ROCKET_FUNCTIONS_PATH . 'i18n.php'; require WP_ROCKET_FUNCTIONS_PATH . 'htaccess.php'; require WP_ROCKET_DEPRECATED_PATH . 'deprecated.php'; require WP_ROCKET_DEPRECATED_PATH . '3.2.php'; require WP_ROCKET_DEPRECATED_PATH . '3.3.php'; require WP_ROCKET_DEPRECATED_PATH . '3.4.php'; require WP_ROCKET_DEPRECATED_PATH . '3.5.php'; require WP_ROCKET_DEPRECATED_PATH . '3.6.php'; require WP_ROCKET_DEPRECATED_PATH . '3.7.php'; require WP_ROCKET_DEPRECATED_PATH . '3.8.php'; require WP_ROCKET_DEPRECATED_PATH . '3.9.php'; require WP_ROCKET_DEPRECATED_PATH . '3.10.php'; require WP_ROCKET_DEPRECATED_PATH . '3.11.php'; require WP_ROCKET_DEPRECATED_PATH . '3.12.php'; require WP_ROCKET_DEPRECATED_PATH . '3.13.php'; require WP_ROCKET_DEPRECATED_PATH . '3.14.php'; require WP_ROCKET_DEPRECATED_PATH . '3.15.php'; require WP_ROCKET_3RD_PARTY_PATH . '3rd-party.php'; require WP_ROCKET_COMMON_PATH . 'admin-bar.php'; if ( rocket_valid_key() ) { require WP_ROCKET_COMMON_PATH . 'purge.php'; if ( is_multisite() && defined( 'SUNRISE' ) && SUNRISE === 'on' && function_exists( 'domain_mapping_siteurl' ) ) { require WP_ROCKET_INC_PATH . '/domain-mapping.php'; } } if ( is_admin() ) { require WP_ROCKET_ADMIN_PATH . 'upgrader.php'; require WP_ROCKET_ADMIN_PATH . 'options.php'; require WP_ROCKET_ADMIN_PATH . 'admin.php'; require WP_ROCKET_ADMIN_UI_PATH . 'enqueue.php'; require WP_ROCKET_ADMIN_UI_PATH . 'notices.php'; require WP_ROCKET_ADMIN_UI_PATH . 'meta-boxes.php'; } elseif ( rocket_valid_key() ) { require WP_ROCKET_FRONT_PATH . 'cookie.php'; require WP_ROCKET_FRONT_PATH . 'dns-prefetch.php'; } // You can hook this to trigger any action when WP Rocket is correctly loaded, so, not in AUTOSAVE mode. if ( rocket_valid_key() ) { /** * Fires when WP Rocket is correctly loaded * * @since 1.0 */ do_action( 'wp_rocket_loaded' ); } } add_action( 'plugins_loaded', 'rocket_init' ); register_deactivation_hook( WP_ROCKET_FILE, [ 'WP_Rocket\Engine\Deactivation\Deactivation', 'deactivate_plugin' ] ); register_activation_hook( WP_ROCKET_FILE, [ 'WP_Rocket\Engine\Activation\Activation', 'activate_plugin' ] ); Addon/WebP/Subscriber.php 0000644 00000037131 15174677547 0011276 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\WebP; use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Engine\CDN\Subscriber as CDNSubscriber; use WP_Rocket\Engine\Support\CommentTrait; /** * Subscriber for the WebP support. * * @since 3.4 */ class Subscriber extends AbstractWebp implements Subscriber_Interface { use CommentTrait; /** * Options_Data instance. * * @var Options_Data */ private $options_data; /** * Options instance. * * @var Options */ private $options_api; /** * Values of $_SERVER to use for some tests. * * @var array */ private $server; /** * \WP_Filesystem_Direct instance. * * @var null|\WP_Filesystem_Direct */ private $filesystem; /** * Constructor. * * @since 3.4 * * @param Options_Data $options_data Options_Data instance. * @param Options $options_api Options instance. * @param CDNSubscriber $cdn_subscriber CDNSubscriber instance. * @param array $server Values of $_SERVER to use for the tests. Default is $_SERVER. */ public function __construct( Options_Data $options_data, Options $options_api, CDNSubscriber $cdn_subscriber, $server = null ) { parent::__construct( $cdn_subscriber ); $this->options_data = $options_data; $this->options_api = $options_api; if ( ! isset( $server ) && ! empty( $_SERVER ) ) { $server = $_SERVER; } $this->server = $server ?: []; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'rocket_buffer' => [ 'convert_to_webp', 16 ], 'rocket_disable_webp_cache' => 'maybe_disable_webp_cache', 'rocket_third_party_webp_change' => 'sync_webp_cache_with_third_party_plugins', 'rocket_preload_before_preload_url' => 'add_accept_header', 'rocket_lazyload_youtube_thumbnail_extension' => 'change_youtube_thumbnail', ]; } /** * Converts images extension to WebP if the file exists. * * @since 3.4 * * @param string $html HTML content. * @return string */ public function convert_to_webp( $html ) { if ( empty( $html ) ) { return $html; } if ( ! $this->options_data->get( 'cache_webp', 0 ) ) { return $html; } /** This filter is documented in inc/classes/buffer/class-cache.php */ if ( apply_filters( 'rocket_disable_webp_cache', false ) ) { return $html; } if ( ! $this->is_browser_webp_compatible() ) { return $html; } $extensions = $this->get_extensions(); $attribute_names = $this->get_attribute_names(); if ( empty( $extensions ) || empty( $attribute_names ) ) { return $html . '<!-- Rocket no webp -->'; } $extensions = implode( '|', $extensions ); $attribute_names = implode( '|', $attribute_names ); if ( ! preg_match_all( '@["\'\s](?<name>(?:data-(?:[a-z0-9_-]+-)?)?(?:' . $attribute_names . '))\s*=\s*["\']\s*(?<value>(?:https?:/)?/[^"\']+\.(?:' . $extensions . ')[^"\']*?)\s*["\']@is', $html, $attributes, PREG_SET_ORDER ) ) { return $html . '<!-- Rocket no webp -->'; } if ( ! isset( $this->filesystem ) ) { $this->filesystem = \rocket_direct_filesystem(); } $has_webp = false; foreach ( $attributes as $attribute ) { if ( preg_match( '@srcset$@i', strtolower( $attribute['name'] ) ) ) { // This is a srcset attribute, with probably multiple URLs. $new_value = $this->srcset_to_webp( $attribute['value'], $extensions ); } else { // A single URL attribute. $new_value = $this->url_to_webp( $attribute['value'], $extensions ); } if ( ! $new_value ) { // No webp here. continue; } // Replace in content. $has_webp = true; $new_attr = preg_replace( '@' . $attribute['name'] . '\s*=\s*["\'][^"\']+["\']@s', $attribute['name'] . '="' . $new_value . '"', $attribute[0] ); $html = str_replace( $attribute[0], $new_attr, $html ); } $has_webp = apply_filters_deprecated( 'rocket_page_has_webp_files', [ $has_webp, $html ], '3.12.6', 'rocket_page_has_webp_files' ); /** * Tell if the page contains webp files. * * @since 3.4 * * @param bool $has_webp True if the page contains webp files. False otherwise. * @param string $html The page’s html contents. */ $has_webp = apply_filters( 'rocket_page_has_webp_files', $has_webp, $html ); if ( $has_webp ) { $html = $this->add_meta_comment( 'cache_webp', $html ); return $html . '<!-- Rocket has webp -->'; } return $html . '<!-- Rocket no webp -->'; } /** * Disable the WebP cache if a WebP plugin is in use. * * @since 3.4 * * @param bool $disable_webp_cache True to disable WebP cache. False otherwise (default). * * @return bool */ public function maybe_disable_webp_cache( $disable_webp_cache ): bool { return $disable_webp_cache || ! empty( $this->get_plugins_serving_webp() ); } /** * When a 3rd party plugin enables or disables its webp feature, disable or enable WPR feature accordingly. * * @since 3.4 */ public function sync_webp_cache_with_third_party_plugins() { if ( $this->options_data->get( 'cache_webp', 0 ) && $this->get_plugins_serving_webp() ) { // Disable the cache webp option. $this->options_data->set( 'cache_webp', 0 ); $this->options_api->set( 'settings', $this->options_data->get_options() ); } rocket_generate_config_file(); } /** * Add WebP to the HTTP_ACCEPT headers on preload request when the WebP option is active * * @param array $requests Requests to make. * @return array */ public function add_accept_header( array $requests ) { if ( ! $this->options_data->get( 'cache_webp', 0 ) ) { return $requests; } return array_map( function ( $request ) { $request['headers']['headers']['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'; $request['headers']['headers']['HTTP_ACCEPT'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'; return $request; }, $requests ); } /** * Checks if the browser is WebP compatible * * @since 3.12.6 * * @return bool */ private function is_browser_webp_compatible(): bool { // Only to supporting browsers. $http_accept = isset( $this->server['HTTP_ACCEPT'] ) ? $this->server['HTTP_ACCEPT'] : ''; if ( empty( $http_accept ) && function_exists( 'apache_request_headers' ) ) { $headers = apache_request_headers(); $http_accept = isset( $headers['Accept'] ) ? $headers['Accept'] : ''; } if ( ! empty( $http_accept ) && false !== strpos( $http_accept, 'webp' ) ) { return true; } return $this->is_user_agent_compatible(); } /** * Check the User Agent if the Accept headers is missing the WebP info * * @since 3.12.6 * * @return bool */ private function is_user_agent_compatible(): bool { $user_agent = isset( $this->server['HTTP_USER_AGENT'] ) ? $this->server['HTTP_USER_AGENT'] : ''; if ( empty( $user_agent ) ) { return false; } if ( preg_match( '#Firefox/(?<version>[0-9]{2,})#i', $user_agent, $matches ) ) { if ( 66 >= (int) $matches['version'] ) { return false; } } if ( preg_match( '#(?:iPad|iPhone)(.*)Version/(?<version>[0-9]{2,})#i', $user_agent, $matches ) ) { if ( 14 > (int) $matches['version'] ) { return false; } return true; } if ( preg_match( '#Version/(?<version>[0-9]{2,})(?:.*)Safari#i', $user_agent, $matches ) ) { if ( 16 > (int) $matches['version'] ) { return false; } } return true; } /** * Get the list of file extensions that may have a webp version. * * @since 3.4 * * @return array */ private function get_extensions() { $extensions = [ 'jpg', 'jpeg', 'jpe', 'png', 'gif' ]; /** * Filter the list of file extensions that may have a webp version. * * @since 3.4 * * @param array $extensions An array of file extensions. */ $extensions = apply_filters( 'rocket_file_extensions_for_webp', $extensions ); $extensions = array_filter( (array) $extensions, function ( $extension ) { return $extension && is_string( $extension ); } ); return array_unique( $extensions ); } /** * Get the names of the HTML attributes where WP Rocket must search for image files. * * @since 3.4 * * @return array */ private function get_attribute_names() { $attributes = [ 'href', 'src', 'srcset', 'data-large_image', 'data-thumb' ]; /** * Filter the names of the HTML attributes where WP Rocket must search for image files. * Don't prepend new names with `data-`, WPR will do it. For example if you want to add `data-foo-bar`, you only need to add `foo-bar` or `bar` to the list. * * @since 3.4 * * @param array $attributes An array of HTML attribute names. */ $attributes = apply_filters( 'rocket_attributes_for_webp', $attributes ); $attributes = array_filter( (array) $attributes, function ( $attributes ) { return $attributes && is_string( $attributes ); } ); return array_unique( $attributes ); } /** * Convert a URL to an absolute path. * * @since 3.4 * * @param string $url URL to convert. * @return string|bool */ private function url_to_path( $url ) { static $hosts, $site_host, $subdir_levels; $url_host = wp_parse_url( $url, PHP_URL_HOST ); // Relative path. if ( null === $url_host ) { if ( ! isset( $subdir_levels ) ) { $subdir_levels = substr_count( preg_replace( '@^https?://@', '', site_url() ), '/' ); } if ( $subdir_levels ) { $url = ltrim( $url, '/' ); $url = explode( '/', $url ); array_splice( $url, 0, $subdir_levels ); $url = implode( '/', $url ); } $url = site_url( $url ); } // CDN. if ( ! isset( $hosts ) ) { $hosts = $this->cdn_subscriber->get_cdn_hosts( [], [ 'all', 'images' ] ); $hosts = array_flip( $hosts ); } if ( isset( $hosts[ $url_host ] ) ) { if ( ! isset( $site_host ) ) { $site_host = wp_parse_url( site_url( '/' ), PHP_URL_HOST ); } if ( $site_host ) { $url = preg_replace( '@^(https?://)' . $url_host . '/@', '$1' . $site_host . '/', $url ); } } // URL to path. $url = preg_replace( '@^https?:@', '', $url ); $paths = $this->get_url_to_path_associations(); if ( ! $paths ) { // Uh? return false; } foreach ( $paths as $asso_url => $asso_path ) { if ( 0 === strpos( $url, $asso_url ) ) { $file = str_replace( $asso_url, $asso_path, $url ); break; } } if ( empty( $file ) ) { return false; } /** This filter is documented in inc/functions/formatting.php. */ return (string) apply_filters( 'rocket_url_to_path', $file, $url ); } /** * Add a webp extension to a URL. * * @since 3.4 * * @param string $url A URL (I see you're very surprised). * @param string $extensions Allowed image extensions. * @return string|bool The same URL with a webp extension if the file exists. False if the webp image doesn't exist. */ private function url_to_webp( $url, $extensions ) { if ( ! preg_match( '@^(?<src>.+\.(?<extension>' . $extensions . '))(?<query>(?:\?.*)?)$@i', $url, $src_url ) ) { // Probably something like "image.jpg.webp". return false; } $src_path = $this->url_to_path( $src_url['src'] ); if ( empty( $src_path ) ) { return false; } $src_path_webp = preg_replace( '@\.' . $src_url['extension'] . '$@', '.webp', $src_path ); if ( $this->filesystem->exists( $src_path_webp ) ) { // File name: image.jpg => image.webp. return preg_replace( '@\.' . $src_url['extension'] . '$@', '.webp', $src_url['src'] ) . $src_url['query']; } if ( $this->filesystem->exists( $src_path . '.webp' ) ) { // File name: image.jpg => image.jpg.webp. return $src_url['src'] . '.webp' . $src_url['query']; } return false; } /** * Add webp extension to URLs in a srcset attribute. * * @since 3.4 * * @param array|string $srcset_values Value of a srcset attribute. * @param string $extensions Allowed image extensions. * @return string|bool An array similar to $srcset_values, with webp extensions when the files exist. False if no images have webp versions. */ private function srcset_to_webp( $srcset_values, $extensions ) { if ( ! $srcset_values ) { return false; } if ( ! is_array( $srcset_values ) ) { $srcset_values = explode( ',', $srcset_values ); } $has_webp = false; foreach ( $srcset_values as $i => $srcset_value ) { $srcset_value = preg_split( '/\s+/', trim( $srcset_value ) ); if ( count( $srcset_value ) > 2 ) { // Not a good idea to have space characters in file name. $descriptor = array_pop( $srcset_value ); $srcset_value = [ 'url' => implode( ' ', $srcset_value ), 'descriptor' => $descriptor, ]; } else { $srcset_value = [ 'url' => $srcset_value[0], 'descriptor' => ! empty( $srcset_value[1] ) ? $srcset_value[1] : '1x', ]; } $url_webp = $this->url_to_webp( $srcset_value['url'], $extensions ); if ( ! $url_webp ) { $srcset_values[ $i ] = implode( ' ', $srcset_value ); continue; } $srcset_values[ $i ] = $url_webp . ' ' . $srcset_value['descriptor']; $has_webp = true; } if ( ! $has_webp ) { return false; } return implode( ',', $srcset_values ); } /** * Get a list of URL/path associations. * URLs are schema-less, starting by a double slash. * * @since 3.4 * * @return array A list of URLs as keys and paths as values. */ private function get_url_to_path_associations() { static $list; if ( isset( $list ) ) { return $list; } $content_url = preg_replace( '@^https?:@', '', content_url( '/' ) ); $content_dir = trailingslashit( rocket_get_constant( 'WP_CONTENT_DIR' ) ); $list = [ $content_url => $content_dir ]; $upload = wp_upload_dir(); $upload_dir = trailingslashit( $upload['basedir'] ); if ( strpos( $upload_dir, $content_dir ) === false ) { $upload_url = preg_replace( '@^https?:@', '', trailingslashit( $upload['baseurl'] ) ); $list[ $upload_url ] = $upload_dir; } /** * Filter the list of URL/path associations. * The URLs with the most levels must come first. * * @since 3.4 * * @param array $list The list of URL/path associations. URLs are schema-less, starting by a double slash. */ $list = apply_filters( 'rocket_url_to_path_associations', $list ); $list = array_filter( $list, function ( $path, $url ) { return $path && $url && is_string( $path ) && is_string( $url ); }, ARRAY_FILTER_USE_BOTH ); if ( $list ) { $list = array_unique( $list ); } return $list; } /** * Get a list of plugins that serve webp images on frontend. * If the CDN is used, this won't list plugins that use a technique not compatible with CDN. * * @since 3.4 * * @return array The WebP plugin names. */ private function get_plugins_serving_webp() { $webp_plugins = $this->get_webp_plugins(); if ( ! $webp_plugins ) { // Somebody probably messed up. return []; } $checks = []; $is_using_cdn = $this->is_using_cdn(); foreach ( $webp_plugins as $plugin ) { if ( $is_using_cdn && $plugin->is_serving_webp_compatible_with_cdn() ) { $checks[ $plugin->get_id() ] = $plugin->get_name(); } elseif ( ! $is_using_cdn && $plugin->is_serving_webp() ) { $checks[ $plugin->get_id() ] = $plugin->get_name(); } } return $checks; } /** * Change Youtube thumbnail extension. * * @param string $extension extension from the thumbnail. * * @return string */ public function change_youtube_thumbnail( $extension ) { if ( ! $this->options_data->get( 'cache_webp', 0 ) ) { return $extension; } return 'webp'; } } Addon/WebP/AbstractWebp.php 0000644 00000004535 15174677547 0011556 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\WebP; use WP_Rocket\Engine\CDN\Subscriber as CDNSubscriber; use WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp\Webp_Interface; abstract class AbstractWebp { /** * CDNSubscriber instance. * * @var CDNSubscriber */ protected $cdn_subscriber; /** * Constructor. * * @since 3.12.6 * * @param CDNSubscriber $cdn_subscriber CDNSubscriber instance. */ public function __construct( CDNSubscriber $cdn_subscriber ) { $this->cdn_subscriber = $cdn_subscriber; } /** * Get a list of active plugins that convert and/or serve webp images. * * @since 3.12.6 * * @return array An array of Webp_Interface objects. */ protected function get_webp_plugins() { /** * Add Webp plugins. * * @since 3.4 * * @param array $webp_plugins An array of Webp_Interface objects. */ $webp_plugins = (array) apply_filters( 'rocket_webp_plugins', [] ); if ( ! $webp_plugins ) { // Somebody probably messed up. return []; } foreach ( $webp_plugins as $plugin_key => $plugin ) { if ( ! $plugin instanceof Webp_Interface ) { unset( $webp_plugins[ $plugin_key ] ); continue; } if ( ! $this->is_plugin_active( $plugin->get_basename() ) ) { unset( $webp_plugins[ $plugin_key ] ); } } return $webp_plugins; } /** * Tell if a plugin is active. * * @since 3.12.6 * * @param string $plugin_basename A plugin basename. * @return bool */ protected function is_plugin_active( string $plugin_basename ): bool { if ( doing_action( 'deactivate_' . $plugin_basename ) ) { return false; } if ( doing_action( 'activate_' . $plugin_basename ) ) { return true; } return rocket_is_plugin_active( $plugin_basename ); } /** * Tell if WP Rocket uses a CDN for images. * * @since 3.12.6 * * @return bool */ protected function is_using_cdn(): bool { // Don't use `$this->options_data->get( 'cdn' )` here, we need an up-to-date value when the CDN option changes. $use = get_rocket_option( 'cdn', 0 ) && $this->cdn_subscriber->get_cdn_hosts( [], [ 'all', 'images' ] ); /** * Filter whether WP Rocket is using a CDN for webp images. * * @since 3.4 * * @param bool $use True if WP Rocket is using a CDN for webp images. False otherwise. */ return (bool) apply_filters( 'rocket_webp_is_using_cdn', $use ); } } Addon/WebP/AdminSubscriber.php 0000644 00000020570 15174677547 0012246 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\WebP; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Engine\CDN\Subscriber as CDNSubscriber; use WP_Rocket\Event_Management\Subscriber_Interface; class AdminSubscriber extends AbstractWebp implements Subscriber_Interface { /** * Options_Data instance. * * @var Options_Data */ private $options_data; /** * Beacon instance * * @var Beacon */ private $beacon; /** * Constructor. * * @since 3.4 * * @param Options_Data $options_data Options_Data instance. * @param CDNSubscriber $cdn_subscriber CDNSubscriber instance. * @param Beacon $beacon Beacon instance. */ public function __construct( Options_Data $options_data, CDNSubscriber $cdn_subscriber, Beacon $beacon ) { parent::__construct( $cdn_subscriber ); $this->options_data = $options_data; $this->beacon = $beacon; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'rocket_cache_webp_setting_field' => [ [ 'maybe_disable_setting_field' ], [ 'webp_section_description' ], ], ]; } /** * Modifies the WebP section description of WP Rocket settings. * * @since 3.4 * * @param array $cache_webp_field Section description. * @return array */ public function webp_section_description( $cache_webp_field ) { $webp_beacon = $this->beacon->get_suggest( 'webp' ); $webp_plugins = $this->get_webp_plugins(); $serving = []; $serving_not_compatible = []; $creating = []; if ( $webp_plugins ) { $is_using_cdn = $this->is_using_cdn(); foreach ( $webp_plugins as $plugin ) { if ( $plugin->is_serving_webp() ) { if ( $is_using_cdn && ! $plugin->is_serving_webp_compatible_with_cdn() ) { // Serving WebP using a method not compatible with CDN. $serving_not_compatible[ $plugin->get_id() ] = $plugin->get_name(); } else { // Serving WebP when no CDN or with a method compatible with CDN. $serving[ $plugin->get_id() ] = $plugin->get_name(); } } if ( $plugin->is_converting_to_webp() ) { // Generating WebP. $creating[ $plugin->get_id() ] = $plugin->get_name(); } } } if ( $serving ) { // 5, 8. $cache_webp_field['description'] = sprintf( // Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag. esc_html( _n( 'You are using %1$s to serve WebP images so you do not need to enable this option. %2$sMore info%3$s %4$s If you prefer to have WP Rocket serve WebP for you instead, please disable WebP display in %1$s.', 'You are using %1$s to serve WebP images so you do not need to enable this option. %2$sMore info%3$s %4$s If you prefer to have WP Rocket serve WebP for you instead, please disable WebP display in %1$s.', count( $serving ), 'rocket' ) ), esc_html( wp_sprintf_l( '%l', $serving ) ), '<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>', '<br>' ); return $cache_webp_field; } /** This filter is documented in inc/classes/buffer/class-cache.php */ if ( apply_filters( 'rocket_disable_webp_cache', false ) ) { $cache_webp_field['description'] = esc_html__( 'WebP cache is disabled by filter.', 'rocket' ); return $cache_webp_field; } if ( $serving_not_compatible ) { if ( ! $this->options_data->get( 'cache_webp', 0 ) ) { // 6. $cache_webp_field['description'] = sprintf( // Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag. esc_html( _n( 'You are using %1$s to convert images to WebP. If you want WP Rocket to serve them for you, activate this option. %2$sMore info%3$s', 'You are using %1$s to convert images to WebP. If you want WP Rocket to serve them for you, activate this option. %2$sMore info%3$s', count( $serving_not_compatible ), 'rocket' ) ), esc_html( wp_sprintf_l( '%l', $serving_not_compatible ) ), '<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ); return $cache_webp_field; } // 7. $cache_webp_field['description'] = sprintf( // Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag. esc_html( _n( 'You are using %1$s to convert images to WebP. WP Rocket will create separate cache files to serve your WebP images. %2$sMore info%3$s', 'You are using %1$s to convert images to WebP. WP Rocket will create separate cache files to serve your WebP images. %2$sMore info%3$s', count( $serving_not_compatible ), 'rocket' ) ), esc_html( wp_sprintf_l( '%l', $serving_not_compatible ) ), '<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ); return $cache_webp_field; } if ( $creating ) { if ( ! $this->options_data->get( 'cache_webp', 0 ) ) { // 3. $cache_webp_field['description'] = sprintf( // Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag. esc_html( _n( 'You are using %1$s to convert images to WebP. If you want WP Rocket to serve them for you, activate this option. %2$sMore info%3$s', 'You are using %1$s to convert images to WebP. If you want WP Rocket to serve them for you, activate this option. %2$sMore info%3$s', count( $creating ), 'rocket' ) ), esc_html( wp_sprintf_l( '%l', $creating ) ), '<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ); return $cache_webp_field; } // 4. $cache_webp_field['description'] = sprintf( // Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag. esc_html( _n( 'You are using %1$s to convert images to WebP. WP Rocket will create separate cache files to serve your WebP images. %2$sMore info%3$s', 'You are using %1$s to convert images to WebP. WP Rocket will create separate cache files to serve your WebP images. %2$sMore info%3$s', count( $creating ), 'rocket' ) ), esc_html( wp_sprintf_l( '%l', $creating ) ), '<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ); return $cache_webp_field; } if ( ! $this->options_data->get( 'cache_webp', 0 ) ) { // 1. if ( rocket_valid_key() && ! \Imagify_Partner::has_imagify_api_key() ) { $imagify_link = '<a href="#imagify">'; } else { // The Imagify page is not displayed. $imagify_link = '<a href="https://wordpress.org/plugins/imagify/" target="_blank" rel="noopener noreferrer">'; } $cache_webp_field['description'] = sprintf( // Translators: %1$s = opening <a> tag, %2$s = closing </a> tag. esc_html__( '%5$sWe have not detected any compatible WebP plugin!%6$s%4$s If you don’t already have WebP images on your site consider using %3$sImagify%2$s or another supported plugin. %1$sMore info%2$s %4$s If you are not using WebP do not enable this option.', 'rocket' ), '<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>', $imagify_link, '<br>', '<strong>', '</strong>' ); return $cache_webp_field; } // 2. $cache_webp_field['description'] = esc_html__( 'WP Rocket will create separate cache files to serve your WebP images.', 'rocket' ); return $cache_webp_field; } /** * Disable 'cache_webp' setting field if another plugin serves WebP. * * @since 3.4 * * @param array $cache_webp_field Data to be added to the setting field. * @return array */ public function maybe_disable_setting_field( $cache_webp_field ) { /** This filter is documented in inc/classes/buffer/class-cache.php */ if ( ! apply_filters( 'rocket_disable_webp_cache', false ) ) { return $cache_webp_field; } foreach ( [ 'input_attr', 'container_class' ] as $attr ) { if ( ! isset( $cache_webp_field[ $attr ] ) || ! is_array( $cache_webp_field[ $attr ] ) ) { $cache_webp_field[ $attr ] = []; } } $cache_webp_field['input_attr']['disabled'] = 1; return $cache_webp_field; } } Addon/ServiceProvider.php 0000644 00000002617 15174677547 0011452 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon; use WP_Rocket\Addon\Sucuri\Subscriber as SucuriSubscriber; use WP_Rocket\Addon\WebP\AdminSubscriber as WebPAdminSubscriber; use WP_Rocket\Addon\WebP\Subscriber as WebPSubscriber; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for WP Rocket addons. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'sucuri_subscriber', 'webp_subscriber', 'webp_admin_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container */ public function register(): void { $this->getContainer()->addShared( 'sucuri_subscriber', SucuriSubscriber::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'webp_admin_subscriber', WebPAdminSubscriber::class ) ->addArguments( [ 'options', 'cdn_subscriber', 'beacon', ] ); $this->getContainer()->addShared( 'webp_subscriber', WebPSubscriber::class ) ->addArguments( [ 'options', 'options_api', 'cdn_subscriber', ] ); } } Addon/Sucuri/Subscriber.php 0000644 00000021777 15174677547 0011724 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\Sucuri; use WP_Error; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Logger\Logger; class Subscriber implements Subscriber_Interface { /** * URL of the API. * * %s is here for the other query args. * * @var string */ const API_URL = 'https://waf.sucuri.net/api?v2&%s'; /** * Instance of the Option_Data class. * * @var Options_Data */ private $options; /** * Constructor. * * @param Options_Data $options Instance of the Option_Data class. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'rocket_after_clean_domain' => 'maybe_clean_firewall_cache', 'after_rocket_clean_post' => 'maybe_clean_firewall_cache', 'after_rocket_clean_term' => 'maybe_clean_firewall_cache', 'after_rocket_clean_user' => 'maybe_clean_firewall_cache', 'after_rocket_clean_home' => 'maybe_clean_firewall_cache', 'after_rocket_clean_files' => 'maybe_clean_firewall_cache', 'admin_post_rocket_purge_sucuri' => 'do_admin_post_rocket_purge_sucuri', 'admin_notices' => 'maybe_print_notice', 'rocket_cdn_helper_addons' => 'add_cdn_helper_message', ]; } /** * Clear Sucuri firewall cache. * * @since 3.2 */ public function maybe_clean_firewall_cache() { static $done = false; if ( $done ) { return; } $done = true; if ( ! $this->options->get( 'sucury_waf_cache_sync', 0 ) ) { return; } $this->clean_firewall_cache(); } /** * Ajax callback to empty Sucury cache. * * @since 3.2 */ public function do_admin_post_rocket_purge_sucuri() { if ( empty( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'rocket_purge_sucuri' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash wp_nonce_ays( '' ); } if ( ! current_user_can( 'rocket_purge_sucuri_cache' ) ) { wp_nonce_ays( '' ); } $purged = $this->clean_firewall_cache(); if ( is_wp_error( $purged ) ) { $purged_result = [ 'result' => 'error', /* translators: %s is the error message returned by the API. */ 'message' => sprintf( __( 'Sucuri cache purge error: %s', 'rocket' ), $purged->get_error_message() ), ]; } else { $purged_result = [ 'result' => 'success', 'message' => __( 'The Sucuri cache is being cleared. Note that it may take up to two minutes for it to be fully flushed.', 'rocket' ), ]; } set_transient( get_current_user_id() . '_sucuri_purge_result', $purged_result ); wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); die(); } /** * Print an admin notice if the cache failed to be cleared. * * @since 3.2 */ public function maybe_print_notice() { if ( ! current_user_can( 'rocket_purge_sucuri_cache' ) ) { return; } if ( ! is_admin() ) { return; } if ( ! $this->options->get( 'sucury_waf_cache_sync', 0 ) ) { return; } $user_id = get_current_user_id(); $notice = get_transient( $user_id . '_sucuri_purge_result' ); if ( ! $notice ) { return; } delete_transient( $user_id . '_sucuri_purge_result' ); rocket_notice_html( [ 'status' => $notice['result'], 'message' => $notice['message'], ] ); } /** * Tell if an API key is well formatted. * * @since 3.2.3 * * @param string $api_key An API key. * * @return array|false An array with the keys 'k' and 's' (required by the API) if valid. False otherwise. */ public static function is_api_key_valid( $api_key ) { if ( '' !== $api_key && preg_match( '@^(?<k>[a-z0-9]{32})/(?<s>[a-z0-9]{32})$@', $api_key, $matches ) ) { return $matches; } return false; } /** * Clear Sucuri firewall cache. * * @since 3.2 * * @return true|WP_Error True on success. A WP_Error object on failure. */ private function clean_firewall_cache() { $api_key = $this->get_api_key(); if ( is_wp_error( $api_key ) ) { return $api_key; } $response = $this->request_api( [ 'a' => 'clear_cache', 'k' => $api_key['k'], 's' => $api_key['s'], ] ); if ( is_wp_error( $response ) ) { return $response; } Logger::info( 'Sucuri firewall cache cleared.', [ 'sucuri firewall cache', ] ); return true; } /** * Get the API key. * * @since 3.2 * * @return array|WP_Error An array with the keys 'k' and 's', required by the API. A WP_Error object if no key or invalid key. */ private function get_api_key() { $api_key = trim( $this->options->get( 'sucury_waf_api_key', '' ) ); if ( ! $api_key ) { Logger::error( 'API key was not found.', [ 'sucuri firewall cache', ] ); return new WP_Error( 'no_sucuri_api_key', __( 'Sucuri firewall API key was not found.', 'rocket' ) ); } $matches = self::is_api_key_valid( $api_key ); if ( ! $matches ) { Logger::error( 'API key is invalid.', [ 'sucuri firewall cache', ] ); return new WP_Error( 'invalid_sucuri_api_key', __( 'Sucuri firewall API key is invalid.', 'rocket' ) ); } return [ 'k' => $matches['k'], 's' => $matches['s'], ]; } /** * Request against the API. * * @since 3.2 * * @param array $params Parameters to send. * * @return array|WP_Error The response data on success. A WP_Error object on failure. */ private function request_api( $params = [] ) { $params['time'] = time(); $params = $this->build_query( $params ); $url = sprintf( static::API_URL, $params ); /** * Filters the arguments for the Sucuri API request * * @since 3.3.4 * * @param array $args Arguments for the request. */ $args = apply_filters( 'rocket_sucuri_api_request_args', [ 'timeout' => 5, 'redirection' => 5, 'httpversion' => '1.1', 'blocking' => true, // This filter is documented in wp-includes/class-wp-http-streams.php. 'sslverify' => apply_filters( 'https_ssl_verify', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound ] ); $response = wp_remote_get( $url, $args ); if ( is_wp_error( $response ) ) { Logger::error( 'Error when contacting the API.', [ 'sucuri firewall cache', 'url' => $url, 'response' => $response->get_error_message(), ] ); // translators: %s is an error message. return new WP_Error( 'wp_error_sucuri_api', sprintf( __( 'Error when contacting Sucuri firewall API. Error message was: %s', 'rocket' ), $response->get_error_message() ) ); } $contents = wp_remote_retrieve_body( $response ); if ( empty( $contents ) ) { Logger::error( 'Could not get a response from the API.', [ 'sucuri firewall cache', 'url' => $url, 'response' => $response, ] ); return new WP_Error( 'sucuri_api_no_response', __( 'Could not get a response from the Sucuri firewall API.', 'rocket' ) ); } $data = json_decode( $contents, true ); if ( ! $data || ! is_array( $data ) ) { Logger::error( 'Invalid response from the API.', [ 'sucuri firewall cache', 'url' => $url, 'response_body' => $contents, ] ); return new WP_Error( 'sucuri_api_invalid_response', __( 'Got an invalid response from the Sucuri firewall API.', 'rocket' ) ); } if ( empty( $data['status'] ) ) { Logger::error( 'The action failed.', [ 'sucuri firewall cache', 'url' => $url, 'response_data' => $data, ] ); if ( empty( $data['messages'] ) || ! is_array( $data['messages'] ) ) { return new WP_Error( 'sucuri_api_error_status', __( 'The Sucuri firewall API returned an unknown error.', 'rocket' ) ); } // translators: %s is an error message. $message = _n( 'The Sucuri firewall API returned the following error: %s', 'The Sucuri firewall API returned the following errors: %s', count( $data['messages'] ), 'rocket' ); $message = sprintf( $message, '<br/>' . implode( '<br/>', $data['messages'] ) ); return new WP_Error( 'sucuri_api_error_status', $message ); } return $data; } /** * Add the helper message on the CDN settings. * * @param string[] $addons Name from the addon that requires the helper message. * @return string[] */ public function add_cdn_helper_message( array $addons ): array { if ( ! $this->options->get( 'sucury_waf_cache_sync', false ) ) { return $addons; } $addons[] = 'Sucuri'; return $addons; } /** * An i18n-friendly alternative to the built-in PHP method `http_build_query()`. * * @param array|object $params An array or object containing properties. * * @return string An URL-encoded string. */ private function build_query( $params ): string { if ( ! $params ) { return ''; } $params = (array) $params; foreach ( $params as $param => $value ) { $params[ $param ] = $param . '=' . rawurlencode( (string) $value ); } return implode( '&', $params ); } } Addon/Cloudflare/API/Client.php 0000644 00000013673 15174677547 0012252 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\Cloudflare\API; use WP_Error; use WP_Rocket\Addon\Cloudflare\Auth\AuthInterface; class Client { const CLOUDFLARE_API = 'https://api.cloudflare.com/client/v4/'; /** * Auth object * * @var AuthInterface */ private $auth; /** * An array of arguments for wp_remote_request() * * @var mixed[] */ protected $args = []; /** * Constructor. * * @param AuthInterface $auth Auth implementation. */ public function __construct( AuthInterface $auth ) { $this->auth = $auth; $this->args = [ 'sslverify' => true, 'body' => [], 'headers' => [], ]; } /** * Change client auth. * * @param AuthInterface $auth Client auth. * * @return void */ public function set_auth( AuthInterface $auth ) { $this->auth = $auth; } /** * API call method for sending requests using GET. * * @param string $path Path of the endpoint. * @param mixed[] $data Data to be sent along with the request. * * @return object */ public function get( $path, array $data = [] ) { return $this->request( $path, 'get', $data ); } /** * API call method for sending requests using POST. * * @param string $path Path of the endpoint. * @param array $data Data to be sent along with the request. * * @return object */ public function post( $path, array $data = [] ) { return $this->request( $path, 'post', $data ); } /** * API call method for sending requests using DELETE. * * @param string $path Path of the endpoint. * @param array $data Data to be sent along with the request. * * @return object */ public function delete( $path, array $data = [] ) { return $this->request( $path, 'delete', $data ); } /** * API call method for sending requests using PATCH. * * @param string $path Path of the endpoint. * @param array $data Data to be sent along with the request. * * @return object */ public function patch( $path, array $data = [] ) { return $this->request( $path, 'patch', $data ); } /** * API call method for sending requests * * @param string $path Path of the endpoint. * @param string $method Type of method that should be used. * @param array $data Data to be sent along with the request. * * @return object|WP_Error */ protected function request( $path, $method = 'get', array $data = [] ) { if ( '/ips' !== $path ) { $valid = $this->auth->is_valid_credentials(); if ( is_wp_error( $valid ) ) { return $valid; } if ( ! $valid ) { return new WP_Error( 'cloudflare_invalid_credentials', 'Cloudflare credentials are invalid.' ); } } $response = $this->do_remote_request( $path, $method, $data ); if ( is_wp_error( $response ) ) { return $response; } $content = wp_remote_retrieve_body( $response ); if ( empty( $content ) ) { return new WP_Error( 'cloudflare_no_reply', __( 'Cloudflare did not provide any reply. Please try again later.', 'rocket' ) ); } $content = json_decode( $content ); if ( ! is_object( $content ) ) { return new WP_Error( 'cloudflare_content_error', __( 'Cloudflare unexpected response', 'rocket' ) ); } if ( empty( $content->success ) ) { return $this->set_request_error( $content ); } if ( ! property_exists( $content, 'result' ) ) { return new WP_Error( 'cloudflare_no_reply', __( 'Missing Cloudflare result.', 'rocket' ) ); } return $content->result; } /** * Does the request remote request. * * @param string $path Path of the endpoint. * @param string $method Type of method that should be used. * @param array $data Data to be sent along with the request. * * @return array|WP_Error */ private function do_remote_request( string $path, string $method = 'GET', array $data = [] ) { $this->args['method'] = strtoupper( $method ); $headers = [ 'User-Agent' => 'wp-rocket/' . rocket_get_constant( 'WP_ROCKET_VERSION' ), 'Content-Type' => 'application/json', ]; if ( '/ips' !== $path ) { $this->args['headers'] = array_merge( $headers, $this->auth->get_headers() ); } $this->args['body'] = []; if ( ! empty( $data ) ) { $this->args['body'] = wp_json_encode( $data ); } $response = wp_remote_request( self::CLOUDFLARE_API . $path, $this->args ); return $response; } /** * Sets the WP_Error when request is not successful * * @param object $content Response object. * * @return WP_Error */ private function set_request_error( $content ) { $errors = []; foreach ( $content->errors as $error ) { if ( 6003 === $error->code || 9103 === $error->code ) { $msg = __( 'Incorrect Cloudflare email address or API key.', 'rocket' ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); return new WP_Error( 'cloudflare_incorrect_credentials', $msg ); } if ( 7003 === $error->code ) { $msg = __( 'Incorrect Cloudflare Zone ID.', 'rocket' ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); return new WP_Error( 'cloudflare_incorrect_zone_id', $msg ); } $errors[] = $error->message; } return new WP_Error( 'cloudflare_request_error', wp_sprintf_l( '%l ', $errors ) ); } } Addon/Cloudflare/API/Endpoints.php 0000644 00000007426 15174677547 0012776 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\Cloudflare\API; use WP_Rocket\Addon\Cloudflare\Auth\AuthInterface; class Endpoints { /** * Client instance * * @var Client */ private $client; /** * Constructor * * @param Client $client Client instance. */ public function __construct( Client $client ) { $this->client = $client; } /** * Get zone data. * * @param string $zone_id Zone ID. * * @return object */ public function get_zones( string $zone_id ) { return $this->client->get( "zones/{$zone_id}" ); } /** * Get the zone's page rules. * * @param string $zone_id Zone ID. * @param string $status Rule status. * * @return object */ public function list_pagerules( string $zone_id, string $status ) { return $this->client->get( "zones/{$zone_id}/pagerules?status={$status}" ); } /** * Purges the cache. * * @param string $zone_id Zone ID. * * @return object */ public function purge( string $zone_id ) { return $this->client->post( "zones/{$zone_id}/purge_cache", [ 'purge_everything' => true ] ); } /** * Purges the given URLs. * * @param string $zone_id Zone ID. * @param array $urls An array of URLs that should be removed from cache. * * @return object */ public function purge_files( string $zone_id, array $urls = [] ) { return $this->client->post( "zones/{$zone_id}/purge_cache", [ 'files' => $urls ] ); } /** * Updates the zone's browser cache TTL setting * * @param string $zone_id Zone ID. * @param string $value Cache TTL value. * * @return object */ public function update_browser_cache_ttl( string $zone_id, $value ) { return $this->update_setting( $zone_id, 'browser_cache_ttl', $value ); } /** * Updates the zone's rocket loader setting. * * @param string $zone_id Zone ID. * @param string $value Rocket Loader value. * * @return object */ public function update_rocket_loader( string $zone_id, $value ) { return $this->update_setting( $zone_id, 'rocket_loader', $value ); } /** * Updates the zone's minify setting. * * @param string $zone_id Zone ID. * @param string[] $value Minify value. * * @return object */ public function update_minify( string $zone_id, $value ) { return $this->update_setting( $zone_id, 'minify', $value ); } /** * Updates the zone's cache level. * * @param string $zone_id Zone ID. * @param string $value Cache level value. * * @return object */ public function change_cache_level( string $zone_id, $value ) { return $this->update_setting( $zone_id, 'cache_level', $value ); } /** * Changes the zone's development mode. * * @param string $zone_id Zone ID. * @param string $value Development mode value. * * @return object */ public function change_development_mode( string $zone_id, $value ) { return $this->update_setting( $zone_id, 'development_mode', $value ); } /** * Updates the given setting. * * @param string $zone_id Zone ID. * @param string $setting Name of the setting to change. * @param mixed $value Setting value. * * @return object */ protected function update_setting( string $zone_id, $setting, $value ) { return $this->client->patch( "zones/{$zone_id}/settings/{$setting}", [ 'value' => $value ] ); } /** * Gets all of the Cloudflare settings. * * @param string $zone_id Zone ID. * * @return object */ public function get_settings( string $zone_id ) { return $this->client->get( "zones/{$zone_id}/settings" ); } /** * Gets Cloudflare's IPs. * * @return object */ public function get_ips() { return $this->client->get( '/ips' ); } /** * Change client auth. * * @param AuthInterface $auth Client auth. * * @return void */ public function change_auth( AuthInterface $auth ) { $this->client->set_auth( $auth ); } } Addon/Cloudflare/Subscriber.php 0000644 00000046232 15174677547 0012523 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\Cloudflare; use WP_Post; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\{Options, Options_Data}; use WPMedia\Cloudflare\Auth\AuthFactoryInterface; class Subscriber implements Subscriber_Interface { /** * Cloudflare instance. * * @var Cloudflare */ private $cloudflare; /** * Options Data instance. * * @var Options_Data */ private $options; /** * Options instance. * * @var Options */ private $options_api; /** * Authentication factory. * * @var AuthFactoryInterface */ protected $auth_factory; /** * Creates an instance of the Cloudflare Subscriber. * * @param Cloudflare $cloudflare Cloudflare instance. * @param Options_Data $options WP Rocket options instance. * @param Options $options_api Options instance. * @param AuthFactoryInterface $auth_factory Authentication factory. */ public function __construct( Cloudflare $cloudflare, Options_Data $options, Options $options_api, AuthFactoryInterface $auth_factory ) { $this->options = $options; $this->options_api = $options_api; $this->cloudflare = $cloudflare; $this->auth_factory = $auth_factory; } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { $slug = rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ); return [ 'rocket_varnish_ip' => 'set_varnish_localhost', 'rocket_varnish_purge_request_host' => 'set_varnish_purge_request_host', 'rocket_cron_deactivate_cloudflare_devmode' => 'deactivate_devmode', 'rocket_after_clean_domain' => 'auto_purge', 'after_rocket_clean_post' => [ 'auto_purge_by_url', 10, 3 ], 'admin_post_rocket_purge_cloudflare' => 'purge_cache', 'init' => [ 'set_real_ip', 1 ], 'update_option_' . $slug => [ [ 'save_cloudflare_options', 10, 2 ], [ 'update_dev_mode', 11, 2 ], ], 'pre_update_option_' . $slug => [ [ 'change_auth', 8, 2 ], [ 'delete_connection_transient', 10, 2 ], [ 'save_cloudflare_old_settings', 10, 2 ], [ 'display_settings_notice', 11, 2 ], ], 'rocket_buffer' => [ 'protocol_rewrite', PHP_INT_MAX ], 'wp_calculate_image_srcset' => [ 'protocol_rewrite_srcset', PHP_INT_MAX ], 'rocket_cdn_helper_addons' => 'add_cdn_helper_message', ]; } /** * Sets the Varnish IP to localhost if Cloudflare is active. * * @param string|array $varnish_ip Varnish IP. * * @return array */ public function set_varnish_localhost( $varnish_ip ) { if ( ! $this->should_filter_varnish() ) { return $varnish_ip; } if ( is_string( $varnish_ip ) ) { $varnish_ip = (array) $varnish_ip; } $varnish_ip[] = 'localhost'; return $varnish_ip; } /** * Sets the Host header to the website domain if Cloudflare is active. * * @param string $host the host header value. * * @return string */ public function set_varnish_purge_request_host( $host ) { if ( ! $this->should_filter_varnish() ) { return $host; } return wp_parse_url( home_url(), PHP_URL_HOST ); } /** * Checks if we should filter the value for the Varnish purge. * * @return bool */ private function should_filter_varnish(): bool { // This filter is documented in inc/Addon/Varnish.php. return apply_filters( 'do_rocket_varnish_http_purge', false ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals || $this->options->get( 'varnish_auto_purge', 0 ); } /** * Automatically set Cloudflare development mode value to off after 3 hours to reflect Cloudflare behaviour. * * @return void */ public function deactivate_devmode() { $this->options->set( 'cloudflare_devmode', 0 ); $this->options_api->set( 'settings', $this->options->get_options() ); } /** * Purge Cloudflare cache automatically if Cache Everything is set as a Page Rule. * * @return void */ public function auto_purge() { if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) { return; } $settings = $this->options_api->get( 'settings', [] ); $this->options->set_values( $settings ); $auth = $this->auth_factory->create( $settings ); $this->cloudflare->change_auth( $auth ); if ( is_wp_error( $this->cloudflare->check_connection( $this->options->get( 'cloudflare_zone_id', '' ) ) ) ) { return; } $cf_cache_everything = $this->cloudflare->has_page_rule( 'cache_everything' ); if ( is_wp_error( $cf_cache_everything ) || ! $cf_cache_everything ) { return; } // Purge CloudFlare. $this->cloudflare->purge_cloudflare(); } /** * Purge Cloudflare cache URLs automatically if Cache Everything is set as a Page Rule. * * @param WP_Post $post The post object. * @param array $purge_urls URLs cache files to remove. * @param string $lang The post language. * * @return void */ public function auto_purge_by_url( $post, $purge_urls, $lang ) { if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) { return; } if ( is_wp_error( $this->cloudflare->check_connection() ) ) { return; } $cf_cache_everything = $this->cloudflare->has_page_rule( 'cache_everything' ); if ( is_wp_error( $cf_cache_everything ) || ! $cf_cache_everything ) { return; } // Add home URL and feeds URLs to Cloudflare clean cache URLs list. $purge_urls[] = get_rocket_i18n_home_url( $lang ); $feed_urls = []; $feed_urls[] = get_feed_link(); $feed_urls[] = get_feed_link( 'comments_' ); // this filter is documented in inc/functions/files.php. $feed_urls = apply_filters( 'rocket_clean_home_feeds', $feed_urls ); $purge_urls = array_unique( array_merge( $purge_urls, $feed_urls ) ); // Purge CloudFlare. $this->cloudflare->purge_by_url( $post, $purge_urls, $lang ); } /** * Purge CloudFlare cache. * * @return void */ public function purge_cache_no_die() { if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) { return; } $connection = $this->cloudflare->check_connection(); if ( is_wp_error( $connection ) ) { $cf_purge_result = [ 'result' => 'error', 'message' => sprintf( // translators: %1$s = <strong>, %2$s = </strong>, %3$s = CloudFare API return message. __( '%1$sWP Rocket:%2$s %3$s', 'rocket' ), '<strong>', '</strong>', $connection->get_error_message() ), ]; set_transient( get_current_user_id() . '_cloudflare_purge_result', $cf_purge_result ); return; } // Purge CloudFlare. $cf_purge = $this->cloudflare->purge_cloudflare(); $cf_purge_result = [ 'result' => 'success', 'message' => sprintf( // translators: %1$s = <strong>, %2$s = </strong>. __( '%1$sWP Rocket:%2$s Cloudflare cache successfully purged.', 'rocket' ), '<strong>', '</strong>' ), ]; if ( is_wp_error( $cf_purge ) ) { $cf_purge_result = [ 'result' => 'error', 'message' => sprintf( // translators: %1$s = <strong>, %2$s = </strong>, %3$s = CloudFare API return message. __( '%1$sWP Rocket:%2$s %3$s', 'rocket' ), '<strong>', '</strong>', $cf_purge->get_error_message() ), ]; } set_transient( get_current_user_id() . '_cloudflare_purge_result', $cf_purge_result ); } /** * Purge CloudFlare cache. * * @return void */ public function purge_cache() { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_purge_cloudflare' ) ) { wp_nonce_ays( '' ); } $this->purge_cache_no_die(); wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); defined( 'WPMEDIA_IS_TESTING' ) ? wp_die() : exit; } /** * Set Real IP from CloudFlare. * * @return void */ public function set_real_ip() { Cloudflare::set_ip_rewrite(); } /** * Save Cloudflare dev mode admin option. * * @param string $value New value for Cloudflare dev mode. * * @return string[] */ private function save_cloudflare_devmode( $value ) { $result = $this->cloudflare->set_devmode( $value ); if ( is_wp_error( $result ) ) { return [ 'result' => 'error', // translators: %s is the message returned by the CloudFlare API. 'message' => sprintf( __( 'Cloudflare development mode error: %s', 'rocket' ), $result->get_error_message() ), ]; } return [ 'result' => 'success', // translators: %s is the message returned by the CloudFlare API. 'message' => sprintf( __( 'Cloudflare development mode %s', 'rocket' ), $result ), ]; } /** * Save Cloudflare cache_level admin option. * * @param string $value New value for Cloudflare cache_level. * * @return string[] */ private function save_cache_level( $value ) { // Set Cache Level to Aggressive. $result = $this->cloudflare->set_cache_level( $value ); if ( is_wp_error( $result ) ) { return [ 'result' => 'error', // translators: %s is the message returned by the CloudFlare API. 'message' => sprintf( __( 'Cloudflare cache level error: %s', 'rocket' ), $result->get_error_message() ), ]; } $level = $value; if ( 'aggressive' === $result ) { $level = _x( 'standard', 'Cloudflare caching level', 'rocket' ); } return [ 'result' => 'success', // translators: %s is the caching level returned by the CloudFlare API. 'message' => sprintf( __( 'Cloudflare cache level set to %s', 'rocket' ), $level ), ]; } /** * Save Cloudflare rocket loader admin option. * * @param string $value New value for Cloudflare rocket loader. * * @return string[] */ private function save_rocket_loader( $value ) { $result = $this->cloudflare->set_rocket_loader( $value ); if ( is_wp_error( $result ) ) { return [ 'result' => 'error', // translators: %s is the message returned by the CloudFlare API. 'message' => sprintf( __( 'Cloudflare rocket loader error: %s', 'rocket' ), $result->get_error_message() ), ]; } return [ 'result' => 'success', // translators: %s is the message returned by the CloudFlare API. 'message' => sprintf( __( 'Cloudflare rocket loader %s', 'rocket' ), $result ), ]; } /** * Save Cloudflare browser cache ttl admin option. * * @param int $value New value for Cloudflare browser cache ttl. * * @return string[] */ private function save_browser_cache_ttl( $value ) { $result = $this->cloudflare->set_browser_cache_ttl( $value ); if ( is_wp_error( $result ) ) { return [ 'result' => 'error', // translators: %s is the message returned by the CloudFlare API. 'message' => sprintf( __( 'Cloudflare browser cache error: %s', 'rocket' ), $result->get_error_message() ), ]; } return [ 'result' => 'success', // translators: %s is the message returned by the CloudFlare API. 'message' => sprintf( __( 'Cloudflare browser cache set to %s', 'rocket' ), $result ), ]; } /** * Save Cloudflare auto settings admin option. * * @param int $auto_settings New value for Cloudflare auto_settings. * @param string $old_settings Cloudflare cloudflare_old_settings. * * @return array<int, array<string>> */ private function save_cloudflare_auto_settings( $auto_settings, $old_settings ) { $cf_old_settings = explode( ',', $old_settings ); $result = []; // Set Cache Level to Aggressive. $cf_cache_level = 0 === $auto_settings ? $cf_old_settings[0] : 'aggressive'; $result[] = $this->save_cache_level( $cf_cache_level ); // Deactivate Rocket Loader to prevent conflicts. $cf_rocket_loader = isset( $cf_old_settings[2] ) && 0 === $auto_settings ? $cf_old_settings[2] : 'off'; $result[] = $this->save_rocket_loader( $cf_rocket_loader ); // Set Browser cache to 1 year. $cf_browser_cache_ttl = isset( $cf_old_settings[3] ) && 0 === $auto_settings ? $cf_old_settings[3] : 31536000; $result[] = $this->save_browser_cache_ttl( $cf_browser_cache_ttl ); return $result; } /** * Update the development mode value on Cloudflare * * @param array $old_value An array of previous values for the settings. * @param array $value An array of submitted values for the settings. * * @return void */ public function update_dev_mode( $old_value, $value ) { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( ! isset( $old_value['cloudflare_devmode'], $value['cloudflare_devmode'] ) ) { return; } if ( (int) $old_value['cloudflare_devmode'] === (int) $value['cloudflare_devmode'] ) { return; } $connection = $this->cloudflare->check_connection( $value['cloudflare_zone_id'] ); if ( is_wp_error( $connection ) ) { return; } $result = [ 'pre' => sprintf( '%1$sWP Rocket:%2$s', '<strong>', '</strong> ' ), ]; $update = get_transient( get_current_user_id() . '_cloudflare_update_settings' ); if ( false !== $update ) { $result = $update; } $result[] = $this->save_cloudflare_devmode( $value['cloudflare_devmode'] ); set_transient( get_current_user_id() . '_cloudflare_update_settings', $result ); } /** * Save Cloudflare admin options. * * @param array $old_value An array of previous values for the settings. * @param array $value An array of submitted values for the settings. * * @return void */ public function save_cloudflare_options( $old_value, $value ) { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( ! isset( $old_value['cloudflare_auto_settings'], $value['cloudflare_auto_settings'] ) ) { return; } if ( (int) $old_value['cloudflare_auto_settings'] === (int) $value['cloudflare_auto_settings'] ) { return; } $connection = $this->cloudflare->check_connection( $value['cloudflare_zone_id'] ); if ( is_wp_error( $connection ) ) { return; } $result = [ 'pre' => sprintf( // translators: %1$s = strong opening tag, %2$s = strong closing tag. __( '%1$sWP Rocket:%2$s Optimal settings activated for Cloudflare:', 'rocket' ), '<strong>', '</strong>' ) . '<br>', ]; if ( 0 === (int) $value['cloudflare_auto_settings'] ) { $result['pre'] = sprintf( // translators: %1$s = strong opening tag, %2$s = strong closing tag. __( '%1$sWP Rocket:%2$s Optimal settings deactivated for Cloudflare, reverted to previous settings:', 'rocket' ), '<strong>', '</strong>' ) . '<br>'; } $result = array_merge( $result, $this->save_cloudflare_auto_settings( $value['cloudflare_auto_settings'], $value['cloudflare_old_settings'] ) ); set_transient( get_current_user_id() . '_cloudflare_update_settings', $result ); } /** * Save Cloudflare old settings when the auto settings option is enabled. * * @param array $value An array of previous values for the settings. * @param array $old_value An array of submitted values for the settings. * * @return array */ public function save_cloudflare_old_settings( $value, $old_value ) { if ( ! current_user_can( 'rocket_manage_options' ) ) { return $value; } if ( ! isset( $value['cloudflare_auto_settings'], $old_value ['cloudflare_auto_settings'] ) ) { return $value; } if ( $value['cloudflare_auto_settings'] === $old_value ['cloudflare_auto_settings'] ) { return $value; } if ( 0 === (int) $value['cloudflare_auto_settings'] ) { return $value; } $cloudflare_zone_id = key_exists( 'cloudflare_zone_id', $value ) ? $value['cloudflare_zone_id'] : ''; if ( is_wp_error( $this->cloudflare->check_connection( $cloudflare_zone_id ) ) ) { return $value; } $cf_settings = $this->cloudflare->get_settings(); $value['cloudflare_old_settings'] = ! is_wp_error( $cf_settings ) ? implode( ',', array_filter( $cf_settings ) ) : ''; return $value; } /** * Change the authentication. * * @param array $value An array of previous values for the settings. * @param array $old_value An array of submitted values for the settings. * * @return mixed */ public function change_auth( $value, $old_value ) { $auth = $this->auth_factory->create( $value ); $this->cloudflare->change_auth( $auth ); return $value; } /** * Delete the transient CF connection status when API Key, Email or Zone ID is changed * * @param array $value An array of previous values for the settings. * @param array $old_value An array of submitted values for the settings. * * @return array */ public function delete_connection_transient( $value, $old_value ) { $fields = [ 'cloudflare_api_key', 'cloudflare_email', 'cloudflare_zone_id', 'cloudflare_devmode', 'cloudflare_auto_settings', 'cloudflare_protocol_rewrite', ]; $change = false; foreach ( $fields as $field ) { $change |= ! isset( $old_value[ $field ], $value[ $field ] ) || $old_value[ $field ] !== $value[ $field ]; } if ( ! $change ) { return $value; } delete_transient( get_current_user_id() . '_cloudflare_update_settings' ); delete_transient( 'rocket_cloudflare_is_api_keys_valid' ); return $value; } /** * Display the error notice. * * @param array $value An array of previous values for the settings. * @param array $old_value An array of submitted values for the settings. * * @return mixed */ public function display_settings_notice( $value, $old_value ) { if ( ! key_exists( 'cloudflare_zone_id', $value ) ) { return $value; } $connection = $this->cloudflare->check_connection( $value['cloudflare_zone_id'] ); if ( is_wp_error( $connection ) ) { add_settings_error( 'general', 'cloudflare_api_key_invalid', __( 'WP Rocket: ', 'rocket' ) . '</strong>' . $connection->get_error_message() . '<strong>', 'error' ); } return $value; } /** * Remove HTTP protocol on script, link, img and form tags. * * @param string $buffer HTML content. * * @return string */ public function protocol_rewrite( $buffer ) { if ( ! $this->can_protocol_rewrite() ) { return $buffer; } $return = preg_replace( "/(<(script|link|img|form)(?!.*?[\"']\bcanonical\b[\"'])([^>]*)(href|src|action)=[\"'])https?:\\/\\//i", '$1//', $buffer ); if ( $return ) { $buffer = $return; } return $buffer; } /** * Remove HTTP protocol on srcset attribute generated by WordPress * * @param array $sources an Array of images sources for srcset. * * @return array */ public function protocol_rewrite_srcset( $sources ) { if ( ! $this->can_protocol_rewrite() ) { return $sources; } if ( empty( $sources ) ) { return $sources; } foreach ( $sources as $i => $source ) { $sources[ $i ]['url'] = str_replace( [ 'http:', 'https:' ], '', $source['url'] ); } return $sources; } /** * Can rewrite protocol * * @return bool */ private function can_protocol_rewrite(): bool { return $this->options->get( 'do_cloudflare', 0 ) && ( $this->options->get( 'cloudflare_protocol_rewrite', 0 ) || apply_filters( 'do_rocket_protocol_rewrite', false ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound ); } /** * Add the helper message on the CDN settings. * * @param string[] $addons Name from the addon that requires the helper message. * @return string[] */ public function add_cdn_helper_message( array $addons ): array { $addons[] = 'Cloudflare'; return $addons; } } Addon/Cloudflare/Auth/AuthFactoryInterface.php 0000644 00000000511 15174677547 0015361 0 ustar 00 <?php namespace WPMedia\Cloudflare\Auth; use WP_Rocket\Addon\Cloudflare\Auth\AuthInterface; interface AuthFactoryInterface { /** * Create a new authentication instance. * * @param array $data Data to inject into the client. * @return AuthInterface */ public function create( array $data = [] ): AuthInterface; } Addon/Cloudflare/Auth/AuthInterface.php 0000644 00000000620 15174677547 0014032 0 ustar 00 <?php namespace WP_Rocket\Addon\Cloudflare\Auth; use WP_Error; interface AuthInterface { /** * Gets headers for Cloudflare API request * * @return array */ public function get_headers(): array; /** * Checks if the credentials are set. * * @return bool|WP_Error true if authorized, false otherwise, WP_Error if missing credentials. */ public function is_valid_credentials(); } Addon/Cloudflare/Auth/APIKeyFactory.php 0000644 00000002154 15174677547 0013726 0 ustar 00 <?php namespace WPMedia\Cloudflare\Auth; use WP_Rocket\Addon\Cloudflare\Auth\APIKey; use WP_Rocket\Addon\Cloudflare\Auth\AuthInterface; use WP_Rocket\Admin\Options_Data; class APIKeyFactory implements AuthFactoryInterface { /** * Options Data instance. * * @var Options_Data */ protected $options; /** * Instantiate the class. * * @param Options_Data $options Options Data instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Create a new authentication instance. * * @param array $data Data to inject into the client. * @return AuthInterface */ public function create( array $data = [] ): AuthInterface { $cf_api_key = defined( 'WP_ROCKET_CF_API_KEY' ) ? rocket_get_constant( 'WP_ROCKET_CF_API_KEY', '' ) : $this->options->get( 'cloudflare_api_key', '' ); $email = key_exists( 'cloudflare_email', $data ) ? $data['cloudflare_email'] : $this->options->get( 'cloudflare_email', '' ); $api_key = key_exists( 'cloudflare_api_key', $data ) ? $data['cloudflare_api_key'] : $cf_api_key; return new APIKey( $email, $api_key ); } } Addon/Cloudflare/Auth/APIKey.php 0000644 00000003217 15174677547 0012377 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\Cloudflare\Auth; use WP_Error; class APIKey implements AuthInterface { /** * Cloudflare email * * @var string */ private $email = ''; /** * Cloudflare API Key * * @var string */ private $api_key = ''; /** * Constructor * * @param string $email Cloudflare email. * @param string $api_key Cloudflare API key. */ public function __construct( string $email = '', string $api_key = '' ) { $this->email = $email; $this->api_key = $api_key; } /** * Gets headers for Cloudflare API request * * @return array */ public function get_headers(): array { return [ 'X-Auth-Email' => $this->email, 'X-Auth-Key' => $this->api_key, ]; } /** * Checks if the credentials are set. * * @return bool|WP_Error true if authorized, false if not, WP_Error if either credential is empty. */ public function is_valid_credentials() { if ( empty( $this->email ) || empty( $this->api_key ) ) { return new WP_Error( 'cloudflare_credentials_empty', sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Cloudflare email and/or API key are not set. Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ) ); } return false !== filter_var( $this->email, FILTER_VALIDATE_EMAIL ); } } Addon/Cloudflare/ServiceProvider.php 0000644 00000003726 15174677547 0013534 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\Cloudflare; use WP_Rocket\Addon\Cloudflare\Admin\Subscriber as CloudflareAdminSubscriber; use WP_Rocket\Addon\Cloudflare\API\{Client, Endpoints}; use WP_Rocket\Addon\Cloudflare\Cloudflare; use WP_Rocket\Addon\Cloudflare\Subscriber as CloudflareSubscriber; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WPMedia\Cloudflare\Auth\APIKeyFactory; /** * Service provider for Cloudflare Addon. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'cloudflare_api_key_factory', 'cloudflare_client', 'cloudflare_endpoints', 'cloudflare', 'cloudflare_subscriber', 'cloudflare_admin_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container */ public function register(): void { $this->getContainer()->add( 'cloudflare_api_key_factory', APIKeyFactory::class )->addArgument( 'options' ); $this->getContainer()->add( 'cloudflare_client', Client::class ) ->addArgument( $this->getContainer()->get( 'cloudflare_api_key_factory' )->create() ); $this->getContainer()->add( 'cloudflare_endpoints', Endpoints::class ) ->addArgument( 'cloudflare_client' ); $this->getContainer()->add( 'cloudflare', Cloudflare::class ) ->addArguments( [ 'options', 'cloudflare_endpoints', ] ); $this->getContainer()->addShared( 'cloudflare_subscriber', CloudflareSubscriber::class ) ->addArguments( [ 'cloudflare', 'options', 'options_api', 'cloudflare_api_key_factory', ] ); $this->getContainer()->addShared( 'cloudflare_admin_subscriber', CloudflareAdminSubscriber::class ); } } Addon/Cloudflare/Cloudflare.php 0000644 00000027075 15174677547 0012504 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\Cloudflare; use CloudFlare\IpRewrite; use DateTimeImmutable; use WP_Error; use WP_Post; use WP_Rocket\Addon\Cloudflare\Auth\AuthInterface; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Addon\Cloudflare\API\Endpoints; class Cloudflare { /** * Options Data instance. * * @var Options_Data */ private $options; /** * Endpoints instance. * * @var Endpoints */ private $endpoints; /** * Constructor * * @param Options_Data $options WP Rocket options instance. * @param Endpoints $endpoints Endpoints instance. */ public function __construct( Options_Data $options, Endpoints $endpoints ) { $this->endpoints = $endpoints; $this->options = $options; } /** * Check valid connection with Cloudflare * * @param string $zone_id Cloudflare zone ID. * @return bool|mixed|WP_Error */ public function check_connection( string $zone_id = '' ) { $is_valid = get_transient( 'rocket_cloudflare_is_api_keys_valid' ); if ( false === $is_valid ) { if ( '' === $zone_id ) { $zone_id = $this->options->get( 'cloudflare_zone_id', '' ); } $is_valid = $this->is_auth_valid( $zone_id ); set_transient( 'rocket_cloudflare_is_api_keys_valid', $is_valid, 2 * WEEK_IN_SECONDS ); } return $is_valid; } /** * Validate Cloudflare input data. * * @param string $zone_id Cloudflare zone ID. * * @return bool|WP_Error true if credentials are ok, WP_Error otherwise. */ public function is_auth_valid( string $zone_id ) { if ( empty( $zone_id ) ) { $msg = __( 'Missing Cloudflare Zone ID.', 'rocket' ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); return new WP_Error( 'cloudflare_no_zone_id', $msg ); } $result = $this->endpoints->get_zones( $zone_id ); if ( is_wp_error( $result ) ) { return $result; } $zone_found = false; $site_url = get_site_url(); if ( function_exists( 'domain_mapping_siteurl' ) ) { $site_url = domain_mapping_siteurl( $site_url ); } $parsed_url = wp_parse_url( $site_url ); if ( property_exists( $result, 'name' ) && false !== strpos( strtolower( $parsed_url['host'] ), $result->name ) ) { $zone_found = true; } if ( ! $zone_found ) { $msg = __( 'It looks like your domain is not set up on Cloudflare.', 'rocket' ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); return new WP_Error( 'cloudflare_zone_not_found', $msg ); } return true; } /** * Checks if CF has the $action_value set as a Page Rule. * * @param string $action_value Action value. * * @return mixed true/false if $action_value was found or not, WP_Error otherwise. */ public function has_page_rule( $action_value ) { $result = $this->endpoints->list_pagerules( $this->options->get( 'cloudflare_zone_id', '' ), 'active' ); if ( is_wp_error( $result ) ) { return $result; } $page_rule = wp_json_encode( $result ); return (bool) preg_match( '/' . $action_value . '/', $page_rule ); } /** * Purge Cloudflare cache. * * @return mixed true if the purge is successful, WP_Error otherwise. */ public function purge_cloudflare() { $result = $this->endpoints->purge( $this->options->get( 'cloudflare_zone_id', '' ) ); if ( is_wp_error( $result ) ) { return $result; } return true; } /** * Purge Cloudflare Cache by URL * * @param WP_Post $post The post object. * @param array $purge_urls URLs cache files to remove. * @param string $lang The post language. * * @return mixed true if the purge is successful, WP_Error otherwise */ public function purge_by_url( $post, $purge_urls, $lang ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $result = $this->endpoints->purge_files( $this->options->get( 'cloudflare_zone_id', '' ), $purge_urls ); if ( is_wp_error( $result ) ) { return $result; } return true; } /** * Set the Browser Cache TTL in Cloudflare. * * @param string $value Value for Cloudflare browser cache TTL. * * @return mixed Value if the update is successful, WP_Error otherwise. */ public function set_browser_cache_ttl( $value ) { $result = $this->endpoints->update_browser_cache_ttl( $this->options->get( 'cloudflare_zone_id', '' ), (int) $value ); if ( is_wp_error( $result ) ) { return $result; } return $this->convert_time( (int) $value ); } /** * Convert value in seconds to seconds/minutes/hours/days * * @param int $value Value in seconds. * * @return string */ private function convert_time( int $value ): string { $base = new DateTimeImmutable( '@0' ); $time = new DateTimeImmutable( "@$value" ); $format = '%a ' . __( 'days', 'rocket' ); if ( 60 > $value ) { $format = '%s ' . __( 'seconds', 'rocket' ); } elseif ( 3600 > $value ) { $format = '%i ' . __( 'minutes', 'rocket' ); } elseif ( 86400 > $value ) { $format = '%h ' . __( 'hours', 'rocket' ); } return $base->diff( $time )->format( $format ); } /** * Set the Cloudflare Rocket Loader. * * @param string $value Value for Cloudflare Rocket Loader. * * @return mixed Value if the update is successful, WP_Error otherwise. */ public function set_rocket_loader( $value ) { $result = $this->endpoints->update_rocket_loader( $this->options->get( 'cloudflare_zone_id', '' ), $value ); if ( is_wp_error( $result ) ) { return $result; } return $value; } /** * Set the Cloudflare Minification. * * @param string $value Value for Cloudflare minification. * * @return mixed Value if the update is successful, WP_Error otherwise. */ public function set_minify( $value ) { $cf_minify_settings = [ 'css' => $value, 'html' => $value, 'js' => $value, ]; $result = $this->endpoints->update_minify( $this->options->get( 'cloudflare_zone_id', '' ), $cf_minify_settings ); if ( is_wp_error( $result ) ) { return $result; } return $value; } /** * Set the Cloudflare Caching level. * * @param string $value Value for Cloudflare caching level. * * @return mixed Value if the update is successful, WP_Error otherwise. */ public function set_cache_level( $value ) { $result = $this->endpoints->change_cache_level( $this->options->get( 'cloudflare_zone_id', '' ), $value ); if ( is_wp_error( $result ) ) { return $result; } return $value; } /** * Set the Cloudflare Development mode. * * @param string $value Value for Cloudflare development mode. * * @return mixed Value if the update is successful, WP_Error otherwise. */ public function set_devmode( $value ) { if ( 0 === (int) $value ) { $value = 'off'; } else { $value = 'on'; } $result = $this->endpoints->change_development_mode( $this->options->get( 'cloudflare_zone_id', '' ), $value ); if ( is_wp_error( $result ) ) { return $result; } switch ( $value ) { case 'on': wp_schedule_single_event( time() + 3 * HOUR_IN_SECONDS, 'rocket_cron_deactivate_cloudflare_devmode' ); break; case 'off': $next_event = wp_next_scheduled( 'rocket_cron_deactivate_cloudflare_devmode' ); if ( false !== $next_event ) { wp_unschedule_event( $next_event, 'rocket_cron_deactivate_cloudflare_devmode' ); } break; } return $value; } /** * Get all the current Cloudflare settings for a given domain. * * @return array|WP_Error Array of Cloudflare settings, WP_Error if any error connection to Cloudflare. */ public function get_settings() { $cf_settings = $this->endpoints->get_settings( $this->options->get( 'cloudflare_zone_id', '' ) ); if ( is_wp_error( $cf_settings ) ) { return $cf_settings; } $browser_cache_ttl = 0; $cache_level = ''; $rocket_loader = ''; $cf_minify = ''; foreach ( $cf_settings as $cloudflare_option ) { switch ( $cloudflare_option->id ) { case 'browser_cache_ttl': $browser_cache_ttl = $cloudflare_option->value; break; case 'cache_level': $cache_level = $cloudflare_option->value; break; case 'rocket_loader': $rocket_loader = $cloudflare_option->value; break; case 'minify': $cf_minify = $cloudflare_option->value; break; } } $cf_minify_value = 'on'; if ( 'off' === $cf_minify->js || 'off' === $cf_minify->css || 'off' === $cf_minify->html ) { $cf_minify_value = 'off'; } $cf_settings_array = [ 'cache_level' => $cache_level, 'minify' => $cf_minify_value, 'rocket_loader' => $rocket_loader, 'browser_cache_ttl' => $browser_cache_ttl, ]; return $cf_settings_array; } /** * Get Cloudflare IPs. No API validation needed, all exceptions returns the default CF IPs array. * * @return object Result of API request if successful, default CF IPs otherwise. */ public function get_cloudflare_ips() { $cf_ips = get_transient( 'rocket_cloudflare_ips' ); if ( false !== $cf_ips ) { return $cf_ips; } $cf_ips = $this->endpoints->get_ips(); if ( is_wp_error( $cf_ips ) ) { // Set default IPs from Cloudflare if call to Cloudflare /ips API does not contain a success. // Prevents from making API calls on each page load. $cf_ips = $this->get_default_ips(); } set_transient( 'rocket_cloudflare_ips', $cf_ips, 2 * WEEK_IN_SECONDS ); return $cf_ips; } /** * Get default Cloudflare IPs. * * @return object Default Cloudflare connecting IPs. */ private function get_default_ips() { $cf_ips = (object) [ 'ipv4_cidrs' => [], 'ipv6_cidrs' => [], ]; $cf_ips->ipv4_cidrs = [ '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/12', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22', ]; $cf_ips->ipv6_cidrs = [ '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32', '2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32', ]; return $cf_ips; } /** * Sets the Cloudflare IP Rewrite * * @return IpRewrite */ public static function set_ip_rewrite() { static $instance = null; if ( is_null( $instance ) ) { $instance = new IpRewrite(); return $instance; } return $instance; } /** * Fixes Cloudflare Flexible SSL redirect loop * * @return void */ public static function fix_cf_flexible_ssl() { $ip_rewrite = self::set_ip_rewrite(); if ( $ip_rewrite->isCloudFlare() ) { // Fixes Flexible SSL. if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) { $_SERVER['HTTPS'] = 'on'; } } } /** * Change client auth. * * @param AuthInterface $auth Client auth. * * @return void */ public function change_auth( AuthInterface $auth ) { $this->endpoints->change_auth( $auth ); } } Addon/Cloudflare/Admin/Subscriber.php 0000644 00000006637 15174677547 0013560 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\Cloudflare\Admin; use WP_Rocket\Engine\Admin\Settings\Settings; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Returns an array of events that this subscriber wants to listen to. */ public static function get_subscribed_events() { return [ 'admin_notices' => [ [ 'maybe_display_purge_notice' ], [ 'maybe_display_update_settings_notice' ], ], 'rocket_input_sanitize' => [ 'sanitize_options', 20, 2 ], ]; } /** * This notice is displayed after purging the CloudFlare cache. * * @return void */ public function maybe_display_purge_notice() { if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) { return; } $user_id = get_current_user_id(); $notice = get_transient( $user_id . '_cloudflare_purge_result' ); if ( ! $notice ) { return; } delete_transient( $user_id . '_cloudflare_purge_result' ); rocket_notice_html( [ 'status' => $notice['result'], 'message' => $notice['message'], ] ); } /** * This notice is displayed after modifying the CloudFlare settings. * * @return void */ public function maybe_display_update_settings_notice() { $screen = get_current_screen(); if ( ! current_user_can( 'rocket_manage_options' ) || 'settings_page_wprocket' !== $screen->id ) { return; } $user_id = get_current_user_id(); $notices = get_transient( $user_id . '_cloudflare_update_settings' ); if ( ! $notices ) { return; } $errors = ''; $success = ''; $pre = ''; delete_transient( $user_id . '_cloudflare_update_settings' ); if ( isset( $notices['pre'] ) ) { $pre = $notices['pre']; unset( $notices['pre'] ); } foreach ( $notices as $notice ) { if ( 'error' === $notice['result'] ) { $errors .= $notice['message'] . '<br>'; } elseif ( 'success' === $notice['result'] ) { $success .= $notice['message'] . '<br>'; } } if ( ! empty( $success ) ) { rocket_notice_html( [ 'message' => $pre . $success, ] ); } if ( ! empty( $errors ) ) { rocket_notice_html( [ 'status' => 'error', 'message' => $errors, ] ); } } /** * Sanitize Cloudflare options * * @param array $input gtArray of sanitized values after being submitted by the form. * @param Settings $settings Settings instance. * * @return array */ public function sanitize_options( $input, $settings ) { $input['do_cloudflare'] = $settings->sanitize_checkbox( $input, 'do_cloudflare' ); $input['cloudflare_devmode'] = $settings->sanitize_checkbox( $input, 'cloudflare_devmode' ); $input['cloudflare_auto_settings'] = $settings->sanitize_checkbox( $input, 'cloudflare_auto_settings' ); $input['cloudflare_protocol_rewrite'] = $settings->sanitize_checkbox( $input, 'cloudflare_protocol_rewrite' ); $input['cloudflare_email'] = isset( $input['cloudflare_email'] ) ? sanitize_email( $input['cloudflare_email'] ) : ''; $input['cloudflare_zone_id'] = isset( $input['cloudflare_zone_id'] ) ? sanitize_text_field( $input['cloudflare_zone_id'] ) : ''; $input['cloudflare_api_key'] = isset( $input['cloudflare_api_key'] ) ? sanitize_text_field( $input['cloudflare_api_key'] ) : ''; if ( defined( 'WP_ROCKET_CF_API_KEY' ) ) { $input['cloudflare_api_key'] = rocket_get_constant( 'WP_ROCKET_CF_API_KEY', '' ); } return $input; } } Addon/Varnish/Subscriber.php 0000644 00000005535 15174677547 0012056 0 ustar 00 <?php namespace WP_Rocket\Addon\Varnish; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the Varnish Purge. * * @since 3.5 */ class Subscriber implements Subscriber_Interface { /** * Varnish instance * * @var Varnish */ private $varnish; /** * WP Rocket options instance * * @var Options_Data */ private $options; /** * Constructor * * @param Varnish $varnish Varnish instance. * @param Options_Data $options WP Rocket options instance. */ public function __construct( Varnish $varnish, Options_Data $options ) { $this->varnish = $varnish; $this->options = $options; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'before_rocket_clean_domain' => [ 'clean_domain', 10, 3 ], 'before_rocket_clean_file' => [ 'clean_file' ], 'rocket_rucss_after_clearing_usedcss' => [ 'clean_file' ], 'rocket_performance_hints_data_after_clearing' => [ 'clean_file' ], 'before_rocket_clean_home' => [ 'clean_home', 10, 2 ], 'rocket_saas_complete_job_status' => [ 'clean_file' ], ]; } /** * Checks if Varnish cache should be purged * * @since 3.5 * * @return bool */ private function should_purge() { return ( /** * Filters the use of the Varnish compatibility add-on * * @param bool $varnish_purge True to use, false otherwise. */ apply_filters( 'do_rocket_varnish_http_purge', false ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals || (bool) $this->options->get( 'varnish_auto_purge', 0 ) ); } /** * Clears Varnish cache for the whole domain * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. * @param string $url The home url. * @return void */ public function clean_domain( $root, $lang, $url ) { if ( ! $this->should_purge() ) { return; } $this->varnish->purge( trailingslashit( $url ) . '?regex' ); } /** * Clears a specific page in Varnish cache * * @param string $url The url to purge. * @return void */ public function clean_file( $url ) { if ( ! $this->should_purge() ) { return; } $this->varnish->purge( trailingslashit( $url ) . '?regex' ); } /** * Clears the homepage in Varnish cache * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. * @return void */ public function clean_home( $root, $lang ) { if ( ! $this->should_purge() ) { return; } $home_url = trailingslashit( get_rocket_i18n_home_url( $lang ) ); $home_pagination_url = $home_url . trailingslashit( $GLOBALS['wp_rewrite']->pagination_base ) . '?regex'; $this->varnish->purge( $home_url ); $this->varnish->purge( $home_pagination_url ); } } Addon/Varnish/ServiceProvider.php 0000644 00000001712 15174677547 0013057 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Addon\Varnish; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for Varnish Addon. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'varnish', 'varnish_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'varnish', Varnish::class ); $this->getContainer()->addShared( 'varnish_subscriber', Subscriber::class ) ->addArguments( [ 'varnish', 'options', ] ); } } Addon/Varnish/Varnish.php 0000644 00000005441 15174677547 0011361 0 ustar 00 <?php namespace WP_Rocket\Addon\Varnish; /** * Varnish cache purge * * @since 3.5 */ class Varnish { /** * Send purge request to Varnish * * @since 3.5 * * @param string $url The URL to purge. * @return void */ public function purge( $url ) { $parse_url = get_rocket_parse_url( $url ); $x_purge_method = 'default'; $regex = ''; if ( 'regex' === $parse_url['query'] ) { $x_purge_method = 'regex'; $regex = '.*'; } /** * Filter the HTTP protocol (scheme) * * @since 2.7.3 * @param string $scheme The HTTP protocol */ $scheme = apply_filters( 'rocket_varnish_http_purge_scheme', 'http' ); /** * Filters the headers to send with the Varnish purge request * * @since 3.1 * * @param array $headers Headers to send. */ $headers = apply_filters( 'rocket_varnish_purge_headers', [ /** * Filters the host value passed in the request headers * * @since 2.8.15 * @param string $host The host value. */ 'host' => apply_filters( 'rocket_varnish_purge_request_host', $parse_url['host'] ), 'X-Purge-Method' => $x_purge_method, ] ); /** * Filters the arguments passed to the Varnish purge request * * @since 3.5 * * @param array $args Array of arguments for the request. */ $args = apply_filters( 'rocket_varnish_purge_request_args', [ 'method' => 'PURGE', 'blocking' => false, 'redirection' => 0, 'headers' => $headers, ] ); foreach ( $this->get_varnish_ips() as $ip ) { $host = ! empty( $ip ) ? $ip : str_replace( '*', '', $parse_url['host'] ); $purge_url_main = $scheme . '://' . $host . $parse_url['path']; /** * Filters the purge url. * * @since 3.6.3 * * @param string $purge_url_full Full url contains the main url plus regex pattern. * @param string $purge_url_main Main purge url without any additions params. * @param string $regex Regex string. */ $purge_url = apply_filters( 'rocket_varnish_purge_url', $purge_url_main . $regex, $purge_url_main, $regex ); wp_remote_request( $purge_url, $args ); } } /** * Gets an array of Varnish IPs to send the purge request to * * @return array */ private function get_varnish_ips() { /** * Filter the Varnish IP to call * * @since 2.6.8 * @param string|array $varnish_ip The Varnish IP */ $varnish_ip = apply_filters( 'rocket_varnish_ip', [] ); $constant = rocket_get_constant( 'WP_ROCKET_VARNISH_IP' ); if ( ! empty( $constant ) && empty( $varnish_ip ) ) { $varnish_ip = $constant; } if ( empty( $varnish_ip ) ) { $varnish_ip = [ '' ]; } elseif ( is_string( $varnish_ip ) ) { $varnish_ip = (array) $varnish_ip; } return $varnish_ip; } } Addon/Varnish/composer.json 0000644 00000004140 15174677547 0011753 0 ustar 00 { "name": "wp-media/module-varnish", "description": "Varnish Addon for WP Rocket", "homepage": "https://github.com/wp-media/module-varnish", "license": "GPL-2.0+", "authors": [ { "name": "WP Media", "email": "contact@wp-media.me", "homepage": "https://wp-media.me" } ], "type": "library", "config": { "sort-packages": true }, "support": { "issues": "https://github.com/wp-media/module-varnish/issues", "source": "https://github.com/wp-media/module-varnish" }, "require-dev": { "php": "^7 || ^8", "brain/monkey": "^2.0", "coenjacobs/mozart": "^0.7", "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", "league/container": "^3.3", "phpcompatibility/phpcompatibility-wp": "^2.0", "phpstan/phpstan": "^0.12.3", "phpunit/phpunit": "^7", "psr/container": "1.0.0", "roave/security-advisories": "dev-master", "szepeviktor/phpstan-wordpress": "^0.7", "wp-coding-standards/wpcs": "^2", "wp-media/event-manager": "^3.1", "wp-media/options": "^3.0", "wp-media/phpunit": "1.1.6" }, "autoload": { "psr-4": { "WP_Rocket\\Addon\\Varnish\\": "." } }, "autoload-dev": { "psr-4": { "WP_Rocket\\Tests\\": "Tests/", "WP_Rocket\\Dependencies\\": "Dependencies/" } }, "extra": { "mozart": { "dep_namespace": "WP_Rocket\\Dependencies\\", "dep_directory": "/Dependencies/", "classmap_directory": "/classes/dependencies/", "classmap_prefix": "WP_Rocket_", "packages": [ "league/container" ] } }, "scripts": { "test-unit": "\"vendor/bin/wpmedia-phpunit\" unit path=Tests/Unit", "test-integration": "\"vendor/bin/wpmedia-phpunit\" integration path=Tests/Integration/", "run-tests": [ "@test-unit", "@test-integration" ], "install-codestandards": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run", "phpcs": "\"vendor/bin/phpcs\" .", "phpcs:fix": "\"vendor/bin/phpcbf\" ", "phpstan": "\"vendor/bin/phpstan\" analyse", "post-install-cmd": [ "\"vendor/bin/mozart\" compose", "composer dump-autoload" ], "post-update-cmd": [ "\"vendor/bin/mozart\" compose", "composer dump-autoload" ] } } constants.php 0000644 00000001540 15174677547 0007320 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Checks if the constant is defined. * * NOTE: This function allows mocking constants when testing. * * @since 3.5 * * @param string $constant_name Name of the constant to check. * * @return bool true when constant is defined; else, false. */ function rocket_has_constant( $constant_name ) { return defined( $constant_name ); } /** * Gets the constant is defined. * * NOTE: This function allows mocking constants when testing. * * @since 3.5 * * @param string $constant_name Name of the constant to check. * @param mixed|null $default Optional. Default value to return if constant is not defined. * * @return mixed */ function rocket_get_constant( $constant_name, $default = null ) { if ( ! rocket_has_constant( $constant_name ) ) { return $default; } return constant( $constant_name ); } API/bypass.php 0000644 00000001116 15174677547 0007215 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Indicate to bypass rocket optimizations. * * Checks for "nowprocket" query string in the url to bypass rocket processes. * * @since 3.7 * * @return bool True to indicate should bypass; false otherwise. */ function rocket_bypass() { static $bypass = null; if ( rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ) { $bypass = null; } if ( ! is_null( $bypass ) ) { return $bypass; } $bypass = isset( $_GET['nowprocket'] ) && 0 !== $_GET['nowprocket']; // phpcs:ignore WordPress.Security.NonceVerification return $bypass; } Dependencies/Monolog/Utils.php 0000644 00000014652 15174677547 0012414 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog; class Utils { /** * @internal */ public static function getClass($object) { $class = \get_class($object); return 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class; } /** * Makes sure if a relative path is passed in it is turned into an absolute path * * @param string $streamUrl stream URL or path without protocol * * @return string */ public static function canonicalizePath($streamUrl) { $prefix = ''; if ('file://' === substr($streamUrl, 0, 7)) { $streamUrl = substr($streamUrl, 7); $prefix = 'file://'; } // other type of stream, not supported if (false !== strpos($streamUrl, '://')) { return $streamUrl; } // already absolute if (substr($streamUrl, 0, 1) === '/' || substr($streamUrl, 1, 1) === ':' || substr($streamUrl, 0, 2) === '\\\\') { return $prefix.$streamUrl; } $streamUrl = getcwd() . '/' . $streamUrl; return $prefix.$streamUrl; } /** * Return the JSON representation of a value * * @param mixed $data * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE * @param bool $ignoreErrors whether to ignore encoding errors or to throw on error, when ignored and the encoding fails, "null" is returned which is valid json for null * @throws \RuntimeException if encoding fails and errors are not ignored * @return string */ public static function jsonEncode($data, $encodeFlags = null, $ignoreErrors = false) { if (null === $encodeFlags && version_compare(PHP_VERSION, '5.4.0', '>=')) { $encodeFlags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; } if ($ignoreErrors) { $json = @json_encode($data, $encodeFlags); if (false === $json) { return 'null'; } return $json; } $json = json_encode($data, $encodeFlags); if (false === $json) { $json = self::handleJsonError(json_last_error(), $data); } return $json; } /** * Handle a json_encode failure. * * If the failure is due to invalid string encoding, try to clean the * input and encode again. If the second encoding attempt fails, the * initial error is not encoding related or the input can't be cleaned then * raise a descriptive exception. * * @param int $code return code of json_last_error function * @param mixed $data data that was meant to be encoded * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE * @throws \RuntimeException if failure can't be corrected * @return string JSON encoded data after error correction */ public static function handleJsonError($code, $data, $encodeFlags = null) { if ($code !== JSON_ERROR_UTF8) { self::throwEncodeError($code, $data); } if (is_string($data)) { self::detectAndCleanUtf8($data); } elseif (is_array($data)) { array_walk_recursive($data, array('WP_Rocket\Dependencies\Monolog\Utils', 'detectAndCleanUtf8')); } else { self::throwEncodeError($code, $data); } if (null === $encodeFlags && version_compare(PHP_VERSION, '5.4.0', '>=')) { $encodeFlags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; } $json = json_encode($data, $encodeFlags); if ($json === false) { self::throwEncodeError(json_last_error(), $data); } return $json; } /** * Throws an exception according to a given code with a customized message * * @param int $code return code of json_last_error function * @param mixed $data data that was meant to be encoded * @throws \RuntimeException */ private static function throwEncodeError($code, $data) { switch ($code) { case JSON_ERROR_DEPTH: $msg = 'Maximum stack depth exceeded'; break; case JSON_ERROR_STATE_MISMATCH: $msg = 'Underflow or the modes mismatch'; break; case JSON_ERROR_CTRL_CHAR: $msg = 'Unexpected control character found'; break; case JSON_ERROR_UTF8: $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; break; default: $msg = 'Unknown error'; } throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true)); } /** * Detect invalid UTF-8 string characters and convert to valid UTF-8. * * Valid UTF-8 input will be left unmodified, but strings containing * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed * original encoding of ISO-8859-15. This conversion may result in * incorrect output if the actual encoding was not ISO-8859-15, but it * will be clean UTF-8 output and will not rely on expensive and fragile * detection algorithms. * * Function converts the input in place in the passed variable so that it * can be used as a callback for array_walk_recursive. * * @param mixed $data Input to check and convert if needed, passed by ref * @private */ public static function detectAndCleanUtf8(&$data) { if (is_string($data) && !preg_match('//u', $data)) { $data = preg_replace_callback( '/[\x80-\xFF]+/', function ($m) { return utf8_encode($m[0]); }, $data ); $data = str_replace( array('¤', '¦', '¨', '´', '¸', '¼', '½', '¾'), array('€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'), $data ); } } } Dependencies/Monolog/Processor/ProcessorInterface.php 0000644 00000001116 15174677547 0017062 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Processor; /** * An optional interface to allow labelling WP_Rocket\Dependencies\Monolog processors. * * @author Nicolas Grekas <p@tchwork.com> */ interface ProcessorInterface { /** * @return array The processed records */ public function __invoke(array $records); } Dependencies/Monolog/Processor/IntrospectionProcessor.php 0000644 00000007111 15174677547 0020023 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Processor; use WP_Rocket\Dependencies\Monolog\Logger; /** * Injects line/file:class/function where the log message came from * * Warning: This only works if the handler processes the logs directly. * If you put the processor on a handler that is behind a FingersCrossedHandler * for example, the processor will only be called once the trigger level is reached, * and all the log records will have the same file/line/.. data from the call that * triggered the FingersCrossedHandler. * * @author Jordi Boggiano <j.boggiano@seld.be> */ class IntrospectionProcessor implements ProcessorInterface { private $level; private $skipClassesPartials; private $skipStackFramesCount; private $skipFunctions = array( 'call_user_func', 'call_user_func_array', ); public function __construct($level = Logger::DEBUG, array $skipClassesPartials = array(), $skipStackFramesCount = 0) { $this->level = Logger::toMonologLevel($level); $this->skipClassesPartials = array_merge(array('WP_Rocket\Dependencies\Monolog\\'), $skipClassesPartials); $this->skipStackFramesCount = $skipStackFramesCount; } /** * @param array $record * @return array */ public function __invoke(array $record) { // return if the level is not high enough if ($record['level'] < $this->level) { return $record; } /* * http://php.net/manual/en/function.debug-backtrace.php * As of 5.3.6, DEBUG_BACKTRACE_IGNORE_ARGS option was added. * Any version less than 5.3.6 must use the DEBUG_BACKTRACE_IGNORE_ARGS constant value '2'. */ $trace = debug_backtrace((PHP_VERSION_ID < 50306) ? 2 : DEBUG_BACKTRACE_IGNORE_ARGS); // skip first since it's always the current method array_shift($trace); // the call_user_func call is also skipped array_shift($trace); $i = 0; while ($this->isTraceClassOrSkippedFunction($trace, $i)) { if (isset($trace[$i]['class'])) { foreach ($this->skipClassesPartials as $part) { if (strpos($trace[$i]['class'], $part) !== false) { $i++; continue 2; } } } elseif (in_array($trace[$i]['function'], $this->skipFunctions)) { $i++; continue; } break; } $i += $this->skipStackFramesCount; // we should have the call source now $record['extra'] = array_merge( $record['extra'], array( 'file' => isset($trace[$i - 1]['file']) ? $trace[$i - 1]['file'] : null, 'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null, 'class' => isset($trace[$i]['class']) ? $trace[$i]['class'] : null, 'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null, ) ); return $record; } private function isTraceClassOrSkippedFunction(array $trace, $index) { if (!isset($trace[$index])) { return false; } return isset($trace[$index]['class']) || in_array($trace[$index]['function'], $this->skipFunctions); } } Dependencies/Monolog/SignalHandler.php 0000644 00000010340 15174677547 0014015 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog; use WP_Rocket\Dependencies\Psr\Log\LoggerInterface; use WP_Rocket\Dependencies\Psr\Log\LogLevel; use ReflectionExtension; /** * WP_Rocket\Dependencies\Monolog POSIX signal handler * * @author Robert Gust-Bardon <robert@gust-bardon.org> */ class SignalHandler { private $logger; private $previousSignalHandler = array(); private $signalLevelMap = array(); private $signalRestartSyscalls = array(); public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function registerSignalHandler($signo, $level = LogLevel::CRITICAL, $callPrevious = true, $restartSyscalls = true, $async = true) { if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) { return $this; } if ($callPrevious) { if (function_exists('pcntl_signal_get_handler')) { $handler = pcntl_signal_get_handler($signo); if ($handler === false) { return $this; } $this->previousSignalHandler[$signo] = $handler; } else { $this->previousSignalHandler[$signo] = true; } } else { unset($this->previousSignalHandler[$signo]); } $this->signalLevelMap[$signo] = $level; $this->signalRestartSyscalls[$signo] = $restartSyscalls; if (function_exists('pcntl_async_signals') && $async !== null) { pcntl_async_signals($async); } pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls); return $this; } public function handleSignal($signo, array $siginfo = null) { static $signals = array(); if (!$signals && extension_loaded('pcntl')) { $pcntl = new ReflectionExtension('pcntl'); $constants = $pcntl->getConstants(); if (!$constants) { // HHVM 3.24.2 returns an empty array. $constants = get_defined_constants(true); $constants = $constants['Core']; } foreach ($constants as $name => $value) { if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) { $signals[$value] = $name; } } unset($constants); } $level = isset($this->signalLevelMap[$signo]) ? $this->signalLevelMap[$signo] : LogLevel::CRITICAL; $signal = isset($signals[$signo]) ? $signals[$signo] : $signo; $context = isset($siginfo) ? $siginfo : array(); $this->logger->log($level, sprintf('Program received signal %s', $signal), $context); if (!isset($this->previousSignalHandler[$signo])) { return; } if ($this->previousSignalHandler[$signo] === true || $this->previousSignalHandler[$signo] === SIG_DFL) { if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch') && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill')) { $restartSyscalls = isset($this->signalRestartSyscalls[$signo]) ? $this->signalRestartSyscalls[$signo] : true; pcntl_signal($signo, SIG_DFL, $restartSyscalls); pcntl_sigprocmask(SIG_UNBLOCK, array($signo), $oldset); posix_kill(posix_getpid(), $signo); pcntl_signal_dispatch(); pcntl_sigprocmask(SIG_SETMASK, $oldset); pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls); } } elseif (is_callable($this->previousSignalHandler[$signo])) { if (PHP_VERSION_ID >= 70100) { $this->previousSignalHandler[$signo]($signo, $siginfo); } else { $this->previousSignalHandler[$signo]($signo); } } } } Dependencies/Monolog/Logger.php 0000644 00000054034 15174677547 0012531 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog; use WP_Rocket\Dependencies\Monolog\Handler\HandlerInterface; use WP_Rocket\Dependencies\Monolog\Handler\StreamHandler; use WP_Rocket\Dependencies\Psr\Log\LoggerInterface; use WP_Rocket\Dependencies\Psr\Log\InvalidArgumentException; use Exception; /** * WP_Rocket\Dependencies\Monolog log channel * * It contains a stack of Handlers and a stack of Processors, * and uses them to store records that are added to it. * * @author Jordi Boggiano <j.boggiano@seld.be> */ class Logger implements LoggerInterface, ResettableInterface { /** * Detailed debug information */ const DEBUG = 100; /** * Interesting events * * Examples: User logs in, SQL logs. */ const INFO = 200; /** * Uncommon events */ const NOTICE = 250; /** * Exceptional occurrences that are not errors * * Examples: Use of deprecated APIs, poor use of an API, * undesirable things that are not necessarily wrong. */ const WARNING = 300; /** * Runtime errors */ const ERROR = 400; /** * Critical conditions * * Example: Application component unavailable, unexpected exception. */ const CRITICAL = 500; /** * Action must be taken immediately * * Example: Entire website down, database unavailable, etc. * This should trigger the SMS alerts and wake you up. */ const ALERT = 550; /** * Urgent alert. */ const EMERGENCY = 600; /** * WP_Rocket\Dependencies\Monolog API version * * This is only bumped when API breaks are done and should * follow the major version of the library * * @var int */ const API = 1; /** * Logging levels from syslog protocol defined in RFC 5424 * * @var array $levels Logging levels */ protected static $levels = array( self::DEBUG => 'DEBUG', self::INFO => 'INFO', self::NOTICE => 'NOTICE', self::WARNING => 'WARNING', self::ERROR => 'ERROR', self::CRITICAL => 'CRITICAL', self::ALERT => 'ALERT', self::EMERGENCY => 'EMERGENCY', ); /** * @var \DateTimeZone */ protected static $timezone; /** * @var string */ protected $name; /** * The handler stack * * @var HandlerInterface[] */ protected $handlers; /** * Processors that will process all log records * * To process records of a single handler instead, add the processor on that specific handler * * @var callable[] */ protected $processors; /** * @var bool */ protected $microsecondTimestamps = true; /** * @var callable */ protected $exceptionHandler; /** * @param string $name The logging channel * @param HandlerInterface[] $handlers Optional stack of handlers, the first one in the array is called first, etc. * @param callable[] $processors Optional array of processors */ public function __construct($name, array $handlers = array(), array $processors = array()) { $this->name = $name; $this->setHandlers($handlers); $this->processors = $processors; } /** * @return string */ public function getName() { return $this->name; } /** * Return a new cloned instance with the name changed * * @return static */ public function withName($name) { $new = clone $this; $new->name = $name; return $new; } /** * Pushes a handler on to the stack. * * @param HandlerInterface $handler * @return $this */ public function pushHandler(HandlerInterface $handler) { array_unshift($this->handlers, $handler); return $this; } /** * Pops a handler from the stack * * @return HandlerInterface */ public function popHandler() { if (!$this->handlers) { throw new \LogicException('You tried to pop from an empty handler stack.'); } return array_shift($this->handlers); } /** * Set handlers, replacing all existing ones. * * If a map is passed, keys will be ignored. * * @param HandlerInterface[] $handlers * @return $this */ public function setHandlers(array $handlers) { $this->handlers = array(); foreach (array_reverse($handlers) as $handler) { $this->pushHandler($handler); } return $this; } /** * @return HandlerInterface[] */ public function getHandlers() { return $this->handlers; } /** * Adds a processor on to the stack. * * @param callable $callback * @return $this */ public function pushProcessor($callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given'); } array_unshift($this->processors, $callback); return $this; } /** * Removes the processor on top of the stack and returns it. * * @return callable */ public function popProcessor() { if (!$this->processors) { throw new \LogicException('You tried to pop from an empty processor stack.'); } return array_shift($this->processors); } /** * @return callable[] */ public function getProcessors() { return $this->processors; } /** * Control the use of microsecond resolution timestamps in the 'datetime' * member of new records. * * Generating microsecond resolution timestamps by calling * microtime(true), formatting the result via sprintf() and then parsing * the resulting string via \DateTime::createFromFormat() can incur * a measurable runtime overhead vs simple usage of DateTime to capture * a second resolution timestamp in systems which generate a large number * of log events. * * @param bool $micro True to use microtime() to create timestamps */ public function useMicrosecondTimestamps($micro) { $this->microsecondTimestamps = (bool) $micro; } /** * Adds a log record. * * @param int $level The logging level * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function addRecord($level, $message, array $context = array()) { if (!$this->handlers) { $this->pushHandler(new StreamHandler('php://stderr', static::DEBUG)); } $levelName = static::getLevelName($level); // check if any handler will handle this message so we can return early and save cycles $handlerKey = null; reset($this->handlers); while ($handler = current($this->handlers)) { if ($handler->isHandling(array('level' => $level))) { $handlerKey = key($this->handlers); break; } next($this->handlers); } if (null === $handlerKey) { return false; } if (!static::$timezone) { static::$timezone = new \DateTimeZone(date_default_timezone_get() ?: 'UTC'); } // php7.1+ always has microseconds enabled, so we do not need this hack if ($this->microsecondTimestamps && PHP_VERSION_ID < 70100) { $ts = \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true)), static::$timezone); } else { $ts = new \DateTime('now', static::$timezone); } $ts->setTimezone(static::$timezone); $record = array( 'message' => (string) $message, 'context' => $context, 'level' => $level, 'level_name' => $levelName, 'channel' => $this->name, 'datetime' => $ts, 'extra' => array(), ); try { foreach ($this->processors as $processor) { $record = call_user_func($processor, $record); } while ($handler = current($this->handlers)) { if (true === $handler->handle($record)) { break; } next($this->handlers); } } catch (Exception $e) { $this->handleException($e, $record); } return true; } /** * Ends a log cycle and frees all resources used by handlers. * * Closing a Handler means flushing all buffers and freeing any open resources/handles. * Handlers that have been closed should be able to accept log records again and re-open * themselves on demand, but this may not always be possible depending on implementation. * * This is useful at the end of a request and will be called automatically on every handler * when they get destructed. */ public function close() { foreach ($this->handlers as $handler) { if (method_exists($handler, 'close')) { $handler->close(); } } } /** * Ends a log cycle and resets all handlers and processors to their initial state. * * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal * state, and getting it back to a state in which it can receive log records again. * * This is useful in case you want to avoid logs leaking between two requests or jobs when you * have a long running process like a worker or an application server serving multiple requests * in one process. */ public function reset() { foreach ($this->handlers as $handler) { if ($handler instanceof ResettableInterface) { $handler->reset(); } } foreach ($this->processors as $processor) { if ($processor instanceof ResettableInterface) { $processor->reset(); } } } /** * Adds a log record at the DEBUG level. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function addDebug($message, array $context = array()) { return $this->addRecord(static::DEBUG, $message, $context); } /** * Adds a log record at the INFO level. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function addInfo($message, array $context = array()) { return $this->addRecord(static::INFO, $message, $context); } /** * Adds a log record at the NOTICE level. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function addNotice($message, array $context = array()) { return $this->addRecord(static::NOTICE, $message, $context); } /** * Adds a log record at the WARNING level. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function addWarning($message, array $context = array()) { return $this->addRecord(static::WARNING, $message, $context); } /** * Adds a log record at the ERROR level. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function addError($message, array $context = array()) { return $this->addRecord(static::ERROR, $message, $context); } /** * Adds a log record at the CRITICAL level. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function addCritical($message, array $context = array()) { return $this->addRecord(static::CRITICAL, $message, $context); } /** * Adds a log record at the ALERT level. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function addAlert($message, array $context = array()) { return $this->addRecord(static::ALERT, $message, $context); } /** * Adds a log record at the EMERGENCY level. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function addEmergency($message, array $context = array()) { return $this->addRecord(static::EMERGENCY, $message, $context); } /** * Gets all supported logging levels. * * @return array Assoc array with human-readable level names => level codes. */ public static function getLevels() { return array_flip(static::$levels); } /** * Gets the name of the logging level. * * @param int $level * @return string */ public static function getLevelName($level) { if (!isset(static::$levels[$level])) { throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels))); } return static::$levels[$level]; } /** * Converts PSR-3 levels to WP_Rocket\Dependencies\Monolog ones if necessary * * @param string|int $level Level number (monolog) or name (PSR-3) * @return int */ public static function toMonologLevel($level) { if (is_string($level)) { // Contains chars of all log levels and avoids using strtoupper() which may have // strange results depending on locale (for example, "i" will become "İ") $upper = strtr($level, 'abcdefgilmnortuwy', 'ABCDEFGILMNORTUWY'); if (defined(__CLASS__.'::'.$upper)) { return constant(__CLASS__ . '::' . $upper); } } return $level; } /** * Checks whether the Logger has a handler that listens on the given level * * @param int $level * @return bool */ public function isHandling($level) { $record = array( 'level' => $level, ); foreach ($this->handlers as $handler) { if ($handler->isHandling($record)) { return true; } } return false; } /** * Set a custom exception handler * * @param callable $callback * @return $this */ public function setExceptionHandler($callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException('Exception handler must be valid callable (callback or object with an __invoke method), '.var_export($callback, true).' given'); } $this->exceptionHandler = $callback; return $this; } /** * @return callable */ public function getExceptionHandler() { return $this->exceptionHandler; } /** * Delegates exception management to the custom exception handler, * or throws the exception if no custom handler is set. */ protected function handleException(Exception $e, array $record) { if (!$this->exceptionHandler) { throw $e; } call_user_func($this->exceptionHandler, $e, $record); } /** * Adds a log record at an arbitrary level. * * This method allows for compatibility with common interfaces. * * @param mixed $level The log level * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function log($level, $message, array $context = array()) { $level = static::toMonologLevel($level); return $this->addRecord($level, $message, $context); } /** * Adds a log record at the DEBUG level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function debug($message, array $context = array()) { return $this->addRecord(static::DEBUG, $message, $context); } /** * Adds a log record at the INFO level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function info($message, array $context = array()) { return $this->addRecord(static::INFO, $message, $context); } /** * Adds a log record at the NOTICE level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function notice($message, array $context = array()) { return $this->addRecord(static::NOTICE, $message, $context); } /** * Adds a log record at the WARNING level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function warn($message, array $context = array()) { return $this->addRecord(static::WARNING, $message, $context); } /** * Adds a log record at the WARNING level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function warning($message, array $context = array()) { return $this->addRecord(static::WARNING, $message, $context); } /** * Adds a log record at the ERROR level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function err($message, array $context = array()) { return $this->addRecord(static::ERROR, $message, $context); } /** * Adds a log record at the ERROR level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function error($message, array $context = array()) { return $this->addRecord(static::ERROR, $message, $context); } /** * Adds a log record at the CRITICAL level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function crit($message, array $context = array()) { return $this->addRecord(static::CRITICAL, $message, $context); } /** * Adds a log record at the CRITICAL level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function critical($message, array $context = array()) { return $this->addRecord(static::CRITICAL, $message, $context); } /** * Adds a log record at the ALERT level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function alert($message, array $context = array()) { return $this->addRecord(static::ALERT, $message, $context); } /** * Adds a log record at the EMERGENCY level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function emerg($message, array $context = array()) { return $this->addRecord(static::EMERGENCY, $message, $context); } /** * Adds a log record at the EMERGENCY level. * * This method allows for compatibility with common interfaces. * * @param string $message The log message * @param array $context The log context * @return bool Whether the record has been processed */ public function emergency($message, array $context = array()) { return $this->addRecord(static::EMERGENCY, $message, $context); } /** * Set the timezone to be used for the timestamp of log records. * * This is stored globally for all Logger instances * * @param \DateTimeZone $tz Timezone object */ public static function setTimezone(\DateTimeZone $tz) { self::$timezone = $tz; } } Dependencies/Monolog/Registry.php 0000644 00000010207 15174677547 0013114 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog; use InvalidArgumentException; /** * WP_Rocket\Dependencies\Monolog log registry * * Allows to get `Logger` instances in the global scope * via static method calls on this class. * * <code> * $application = new WP_Rocket\Dependencies\Monolog\Logger('application'); * $api = new WP_Rocket\Dependencies\Monolog\Logger('api'); * * WP_Rocket\Dependencies\Monolog\Registry::addLogger($application); * WP_Rocket\Dependencies\Monolog\Registry::addLogger($api); * * function testLogger() * { * WP_Rocket\Dependencies\Monolog\Registry::api()->addError('Sent to $api Logger instance'); * WP_Rocket\Dependencies\Monolog\Registry::application()->addError('Sent to $application Logger instance'); * } * </code> * * @author Tomas Tatarko <tomas@tatarko.sk> */ class Registry { /** * List of all loggers in the registry (by named indexes) * * @var Logger[] */ private static $loggers = array(); /** * Adds new logging channel to the registry * * @param Logger $logger Instance of the logging channel * @param string|null $name Name of the logging channel ($logger->getName() by default) * @param bool $overwrite Overwrite instance in the registry if the given name already exists? * @throws \InvalidArgumentException If $overwrite set to false and named Logger instance already exists */ public static function addLogger(Logger $logger, $name = null, $overwrite = false) { $name = $name ?: $logger->getName(); if (isset(self::$loggers[$name]) && !$overwrite) { throw new InvalidArgumentException('Logger with the given name already exists'); } self::$loggers[$name] = $logger; } /** * Checks if such logging channel exists by name or instance * * @param string|Logger $logger Name or logger instance */ public static function hasLogger($logger) { if ($logger instanceof Logger) { $index = array_search($logger, self::$loggers, true); return false !== $index; } else { return isset(self::$loggers[$logger]); } } /** * Removes instance from registry by name or instance * * @param string|Logger $logger Name or logger instance */ public static function removeLogger($logger) { if ($logger instanceof Logger) { if (false !== ($idx = array_search($logger, self::$loggers, true))) { unset(self::$loggers[$idx]); } } else { unset(self::$loggers[$logger]); } } /** * Clears the registry */ public static function clear() { self::$loggers = array(); } /** * Gets Logger instance from the registry * * @param string $name Name of the requested Logger instance * @throws \InvalidArgumentException If named Logger instance is not in the registry * @return Logger Requested instance of Logger */ public static function getInstance($name) { if (!isset(self::$loggers[$name])) { throw new InvalidArgumentException(sprintf('Requested "%s" logger instance is not in the registry', $name)); } return self::$loggers[$name]; } /** * Gets Logger instance from the registry via static method call * * @param string $name Name of the requested Logger instance * @param array $arguments Arguments passed to static method call * @throws \InvalidArgumentException If named Logger instance is not in the registry * @return Logger Requested instance of Logger */ public static function __callStatic($name, $arguments) { return self::getInstance($name); } } Dependencies/Monolog/ErrorHandler.php 0000644 00000021112 15174677547 0013670 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog; use WP_Rocket\Dependencies\Psr\Log\LoggerInterface; use WP_Rocket\Dependencies\Psr\Log\LogLevel; use WP_Rocket\Dependencies\Monolog\Handler\AbstractHandler; /** * WP_Rocket\Dependencies\Monolog error handler * * A facility to enable logging of runtime errors, exceptions and fatal errors. * * Quick setup: <code>ErrorHandler::register($logger);</code> * * @author Jordi Boggiano <j.boggiano@seld.be> */ class ErrorHandler { private $logger; private $previousExceptionHandler; private $uncaughtExceptionLevel; private $previousErrorHandler; private $errorLevelMap; private $handleOnlyReportedErrors; private $hasFatalErrorHandler; private $fatalLevel; private $reservedMemory; private $lastFatalTrace; private static $fatalErrors = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR); public function __construct(LoggerInterface $logger) { $this->logger = $logger; } /** * Registers a new ErrorHandler for a given Logger * * By default it will handle errors, exceptions and fatal errors * * @param LoggerInterface $logger * @param array|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling * @param int|false $exceptionLevel a LogLevel::* constant, or false to disable exception handling * @param int|false $fatalLevel a LogLevel::* constant, or false to disable fatal error handling * @return ErrorHandler */ public static function register(LoggerInterface $logger, $errorLevelMap = array(), $exceptionLevel = null, $fatalLevel = null) { //Forces the autoloader to run for LogLevel. Fixes an autoload issue at compile-time on PHP5.3. See https://github.com/Seldaek/monolog/pull/929 class_exists('\\Psr\\Log\\LogLevel', true); /** @phpstan-ignore-next-line */ $handler = new static($logger); if ($errorLevelMap !== false) { $handler->registerErrorHandler($errorLevelMap); } if ($exceptionLevel !== false) { $handler->registerExceptionHandler($exceptionLevel); } if ($fatalLevel !== false) { $handler->registerFatalHandler($fatalLevel); } return $handler; } public function registerExceptionHandler($level = null, $callPrevious = true) { $prev = set_exception_handler(array($this, 'handleException')); $this->uncaughtExceptionLevel = $level; if ($callPrevious && $prev) { $this->previousExceptionHandler = $prev; } } public function registerErrorHandler(array $levelMap = array(), $callPrevious = true, $errorTypes = -1, $handleOnlyReportedErrors = true) { $prev = set_error_handler(array($this, 'handleError'), $errorTypes); $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap); if ($callPrevious) { $this->previousErrorHandler = $prev ?: true; } $this->handleOnlyReportedErrors = $handleOnlyReportedErrors; } public function registerFatalHandler($level = null, $reservedMemorySize = 20) { register_shutdown_function(array($this, 'handleFatalError')); $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize); $this->fatalLevel = $level; $this->hasFatalErrorHandler = true; } protected function defaultErrorLevelMap() { return array( E_ERROR => LogLevel::CRITICAL, E_WARNING => LogLevel::WARNING, E_PARSE => LogLevel::ALERT, E_NOTICE => LogLevel::NOTICE, E_CORE_ERROR => LogLevel::CRITICAL, E_CORE_WARNING => LogLevel::WARNING, E_COMPILE_ERROR => LogLevel::ALERT, E_COMPILE_WARNING => LogLevel::WARNING, E_USER_ERROR => LogLevel::ERROR, E_USER_WARNING => LogLevel::WARNING, E_USER_NOTICE => LogLevel::NOTICE, E_STRICT => LogLevel::NOTICE, E_RECOVERABLE_ERROR => LogLevel::ERROR, E_DEPRECATED => LogLevel::NOTICE, E_USER_DEPRECATED => LogLevel::NOTICE, ); } /** * @private */ public function handleException($e) { $this->logger->log( $this->uncaughtExceptionLevel === null ? LogLevel::ERROR : $this->uncaughtExceptionLevel, sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), array('exception' => $e) ); if ($this->previousExceptionHandler) { call_user_func($this->previousExceptionHandler, $e); } exit(255); } /** * @private */ public function handleError($code, $message, $file = '', $line = 0, $context = array()) { if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) { return; } // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) { $level = isset($this->errorLevelMap[$code]) ? $this->errorLevelMap[$code] : LogLevel::CRITICAL; $this->logger->log($level, self::codeToString($code).': '.$message, array('code' => $code, 'message' => $message, 'file' => $file, 'line' => $line)); } else { // http://php.net/manual/en/function.debug-backtrace.php // As of 5.3.6, DEBUG_BACKTRACE_IGNORE_ARGS option was added. // Any version less than 5.3.6 must use the DEBUG_BACKTRACE_IGNORE_ARGS constant value '2'. $trace = debug_backtrace((PHP_VERSION_ID < 50306) ? 2 : DEBUG_BACKTRACE_IGNORE_ARGS); array_shift($trace); // Exclude handleError from trace $this->lastFatalTrace = $trace; } if ($this->previousErrorHandler === true) { return false; } elseif ($this->previousErrorHandler) { return call_user_func($this->previousErrorHandler, $code, $message, $file, $line, $context); } } /** * @private */ public function handleFatalError() { $this->reservedMemory = null; $lastError = error_get_last(); if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) { $this->logger->log( $this->fatalLevel === null ? LogLevel::ALERT : $this->fatalLevel, 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'], array('code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace) ); if ($this->logger instanceof Logger) { foreach ($this->logger->getHandlers() as $handler) { if ($handler instanceof AbstractHandler) { $handler->close(); } } } } } private static function codeToString($code) { switch ($code) { case E_ERROR: return 'E_ERROR'; case E_WARNING: return 'E_WARNING'; case E_PARSE: return 'E_PARSE'; case E_NOTICE: return 'E_NOTICE'; case E_CORE_ERROR: return 'E_CORE_ERROR'; case E_CORE_WARNING: return 'E_CORE_WARNING'; case E_COMPILE_ERROR: return 'E_COMPILE_ERROR'; case E_COMPILE_WARNING: return 'E_COMPILE_WARNING'; case E_USER_ERROR: return 'E_USER_ERROR'; case E_USER_WARNING: return 'E_USER_WARNING'; case E_USER_NOTICE: return 'E_USER_NOTICE'; case E_STRICT: return 'E_STRICT'; case E_RECOVERABLE_ERROR: return 'E_RECOVERABLE_ERROR'; case E_DEPRECATED: return 'E_DEPRECATED'; case E_USER_DEPRECATED: return 'E_USER_DEPRECATED'; } return 'Unknown PHP error'; } } Dependencies/Monolog/Formatter/FormatterInterface.php 0000644 00000001501 15174677547 0017030 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Formatter; /** * Interface for formatters * * @author Jordi Boggiano <j.boggiano@seld.be> */ interface FormatterInterface { /** * Formats a log record. * * @param array $record A record to format * @return mixed The formatted record */ public function format(array $record); /** * Formats a set of log records. * * @param array $records A set of records to format * @return mixed The formatted set of records */ public function formatBatch(array $records); } Dependencies/Monolog/Formatter/HtmlFormatter.php 0000644 00000011177 15174677547 0016046 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Formatter; use WP_Rocket\Dependencies\Monolog\Logger; use WP_Rocket\Dependencies\Monolog\Utils; /** * Formats incoming records into an HTML table * * This is especially useful for html email logging * * @author Tiago Brito <tlfbrito@gmail.com> */ class HtmlFormatter extends NormalizerFormatter { /** * Translates WP_Rocket\Dependencies\Monolog log levels to html color priorities. */ protected $logLevels = array( Logger::DEBUG => '#cccccc', Logger::INFO => '#468847', Logger::NOTICE => '#3a87ad', Logger::WARNING => '#c09853', Logger::ERROR => '#f0ad4e', Logger::CRITICAL => '#FF7708', Logger::ALERT => '#C12A19', Logger::EMERGENCY => '#000000', ); /** * @param string $dateFormat The format of the timestamp: one supported by DateTime::format */ public function __construct($dateFormat = null) { parent::__construct($dateFormat); } /** * Creates an HTML table row * * @param string $th Row header content * @param string $td Row standard cell content * @param bool $escapeTd false if td content must not be html escaped * @return string */ protected function addRow($th, $td = ' ', $escapeTd = true) { $th = htmlspecialchars($th, ENT_NOQUOTES, 'UTF-8'); if ($escapeTd) { $td = '<pre>'.htmlspecialchars($td, ENT_NOQUOTES, 'UTF-8').'</pre>'; } return "<tr style=\"padding: 4px;text-align: left;\">\n<th style=\"vertical-align: top;background: #ccc;color: #000\" width=\"100\">$th:</th>\n<td style=\"padding: 4px;text-align: left;vertical-align: top;background: #eee;color: #000\">".$td."</td>\n</tr>"; } /** * Create a HTML h1 tag * * @param string $title Text to be in the h1 * @param int $level Error level * @return string */ protected function addTitle($title, $level) { $title = htmlspecialchars($title, ENT_NOQUOTES, 'UTF-8'); return '<h1 style="background: '.$this->logLevels[$level].';color: #ffffff;padding: 5px;" class="monolog-output">'.$title.'</h1>'; } /** * Formats a log record. * * @param array $record A record to format * @return mixed The formatted record */ public function format(array $record) { $output = $this->addTitle($record['level_name'], $record['level']); $output .= '<table cellspacing="1" width="100%" class="monolog-output">'; $output .= $this->addRow('Message', (string) $record['message']); $output .= $this->addRow('Time', $record['datetime']->format($this->dateFormat)); $output .= $this->addRow('Channel', $record['channel']); if ($record['context']) { $embeddedTable = '<table cellspacing="1" width="100%">'; foreach ($record['context'] as $key => $value) { $embeddedTable .= $this->addRow($key, $this->convertToString($value)); } $embeddedTable .= '</table>'; $output .= $this->addRow('Context', $embeddedTable, false); } if ($record['extra']) { $embeddedTable = '<table cellspacing="1" width="100%">'; foreach ($record['extra'] as $key => $value) { $embeddedTable .= $this->addRow($key, $this->convertToString($value)); } $embeddedTable .= '</table>'; $output .= $this->addRow('Extra', $embeddedTable, false); } return $output.'</table>'; } /** * Formats a set of log records. * * @param array $records A set of records to format * @return mixed The formatted set of records */ public function formatBatch(array $records) { $message = ''; foreach ($records as $record) { $message .= $this->format($record); } return $message; } protected function convertToString($data) { if (null === $data || is_scalar($data)) { return (string) $data; } $data = $this->normalize($data); if (version_compare(PHP_VERSION, '5.4.0', '>=')) { return Utils::jsonEncode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, true); } return str_replace('\\/', '/', Utils::jsonEncode($data, null, true)); } } Dependencies/Monolog/Formatter/LineFormatter.php 0000644 00000013012 15174677547 0016017 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Formatter; use WP_Rocket\Dependencies\Monolog\Utils; /** * Formats incoming records into a one-line string * * This is especially useful for logging to files * * @author Jordi Boggiano <j.boggiano@seld.be> * @author Christophe Coevoet <stof@notk.org> */ class LineFormatter extends NormalizerFormatter { const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"; protected $format; protected $allowInlineLineBreaks; protected $ignoreEmptyContextAndExtra; protected $includeStacktraces; /** * @param string $format The format of the message * @param string $dateFormat The format of the timestamp: one supported by DateTime::format * @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries * @param bool $ignoreEmptyContextAndExtra */ public function __construct($format = null, $dateFormat = null, $allowInlineLineBreaks = false, $ignoreEmptyContextAndExtra = false) { $this->format = $format ?: static::SIMPLE_FORMAT; $this->allowInlineLineBreaks = $allowInlineLineBreaks; $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; parent::__construct($dateFormat); } public function includeStacktraces($include = true) { $this->includeStacktraces = $include; if ($this->includeStacktraces) { $this->allowInlineLineBreaks = true; } } public function allowInlineLineBreaks($allow = true) { $this->allowInlineLineBreaks = $allow; } public function ignoreEmptyContextAndExtra($ignore = true) { $this->ignoreEmptyContextAndExtra = $ignore; } /** * {@inheritdoc} */ public function format(array $record) { $vars = parent::format($record); $output = $this->format; foreach ($vars['extra'] as $var => $val) { if (false !== strpos($output, '%extra.'.$var.'%')) { $output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output); unset($vars['extra'][$var]); } } foreach ($vars['context'] as $var => $val) { if (false !== strpos($output, '%context.'.$var.'%')) { $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output); unset($vars['context'][$var]); } } if ($this->ignoreEmptyContextAndExtra) { if (empty($vars['context'])) { unset($vars['context']); $output = str_replace('%context%', '', $output); } if (empty($vars['extra'])) { unset($vars['extra']); $output = str_replace('%extra%', '', $output); } } foreach ($vars as $var => $val) { if (false !== strpos($output, '%'.$var.'%')) { $output = str_replace('%'.$var.'%', $this->stringify($val), $output); } } // remove leftover %extra.xxx% and %context.xxx% if any if (false !== strpos($output, '%')) { $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output); } return $output; } public function formatBatch(array $records) { $message = ''; foreach ($records as $record) { $message .= $this->format($record); } return $message; } public function stringify($value) { return $this->replaceNewlines($this->convertToString($value)); } protected function normalizeException($e) { // TODO 2.0 only check for Throwable if (!$e instanceof \Exception && !$e instanceof \Throwable) { throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e)); } $previousText = ''; if ($previous = $e->getPrevious()) { do { $previousText .= ', '.Utils::getClass($previous).'(code: '.$previous->getCode().'): '.$previous->getMessage().' at '.$previous->getFile().':'.$previous->getLine(); } while ($previous = $previous->getPrevious()); } $str = '[object] ('.Utils::getClass($e).'(code: '.$e->getCode().'): '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().$previousText.')'; if ($this->includeStacktraces) { $str .= "\n[stacktrace]\n".$e->getTraceAsString()."\n"; } return $str; } protected function convertToString($data) { if (null === $data || is_bool($data)) { return var_export($data, true); } if (is_scalar($data)) { return (string) $data; } if (version_compare(PHP_VERSION, '5.4.0', '>=')) { return $this->toJson($data, true); } return str_replace('\\/', '/', $this->toJson($data, true)); } protected function replaceNewlines($str) { if ($this->allowInlineLineBreaks) { if (0 === strpos($str, '{')) { return str_replace(array('\r', '\n'), array("\r", "\n"), $str); } return $str; } return str_replace(array("\r\n", "\r", "\n"), ' ', $str); } } Dependencies/Monolog/Formatter/NormalizerFormatter.php 0000644 00000013042 15174677547 0017255 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Formatter; use Exception; use WP_Rocket\Dependencies\Monolog\Utils; /** * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets * * @author Jordi Boggiano <j.boggiano@seld.be> */ class NormalizerFormatter implements FormatterInterface { const SIMPLE_DATE = "Y-m-d H:i:s"; protected $dateFormat; protected $maxDepth; /** * @param string $dateFormat The format of the timestamp: one supported by DateTime::format * @param int $maxDepth */ public function __construct($dateFormat = null, $maxDepth = 9) { $this->dateFormat = $dateFormat ?: static::SIMPLE_DATE; $this->maxDepth = $maxDepth; if (!function_exists('json_encode')) { throw new \RuntimeException('PHP\'s json extension is required to use WP_Rocket\Dependencies\Monolog\'s NormalizerFormatter'); } } /** * {@inheritdoc} */ public function format(array $record) { return $this->normalize($record); } /** * {@inheritdoc} */ public function formatBatch(array $records) { foreach ($records as $key => $record) { $records[$key] = $this->format($record); } return $records; } /** * @return int */ public function getMaxDepth() { return $this->maxDepth; } /** * @param int $maxDepth */ public function setMaxDepth($maxDepth) { $this->maxDepth = $maxDepth; } protected function normalize($data, $depth = 0) { if ($depth > $this->maxDepth) { return 'Over '.$this->maxDepth.' levels deep, aborting normalization'; } if (null === $data || is_scalar($data)) { if (is_float($data)) { if (is_infinite($data)) { return ($data > 0 ? '' : '-') . 'INF'; } if (is_nan($data)) { return 'NaN'; } } return $data; } if (is_array($data)) { $normalized = array(); $count = 1; foreach ($data as $key => $value) { if ($count++ > 1000) { $normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization'; break; } $normalized[$key] = $this->normalize($value, $depth+1); } return $normalized; } if ($data instanceof \DateTime) { return $data->format($this->dateFormat); } if (is_object($data)) { // TODO 2.0 only check for Throwable if ($data instanceof Exception || (PHP_VERSION_ID > 70000 && $data instanceof \Throwable)) { return $this->normalizeException($data); } // non-serializable objects that implement __toString stringified if (method_exists($data, '__toString') && !$data instanceof \JsonSerializable) { $value = $data->__toString(); } else { // the rest is json-serialized in some way $value = $this->toJson($data, true); } return sprintf("[object] (%s: %s)", Utils::getClass($data), $value); } if (is_resource($data)) { return sprintf('[resource] (%s)', get_resource_type($data)); } return '[unknown('.gettype($data).')]'; } protected function normalizeException($e) { // TODO 2.0 only check for Throwable if (!$e instanceof Exception && !$e instanceof \Throwable) { throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e)); } $data = array( 'class' => Utils::getClass($e), 'message' => $e->getMessage(), 'code' => (int) $e->getCode(), 'file' => $e->getFile().':'.$e->getLine(), ); if ($e instanceof \SoapFault) { if (isset($e->faultcode)) { $data['faultcode'] = $e->faultcode; } if (isset($e->faultactor)) { $data['faultactor'] = $e->faultactor; } if (isset($e->detail)) { if (is_string($e->detail)) { $data['detail'] = $e->detail; } elseif (is_object($e->detail) || is_array($e->detail)) { $data['detail'] = $this->toJson($e->detail, true); } } } $trace = $e->getTrace(); foreach ($trace as $frame) { if (isset($frame['file'])) { $data['trace'][] = $frame['file'].':'.$frame['line']; } } if ($previous = $e->getPrevious()) { $data['previous'] = $this->normalizeException($previous); } return $data; } /** * Return the JSON representation of a value * * @param mixed $data * @param bool $ignoreErrors * @throws \RuntimeException if encoding fails and errors are not ignored * @return string */ protected function toJson($data, $ignoreErrors = false) { return Utils::jsonEncode($data, null, $ignoreErrors); } } Dependencies/Monolog/Handler/HandlerInterface.php 0000644 00000005173 15174677547 0016065 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Handler; use WP_Rocket\Dependencies\Monolog\Formatter\FormatterInterface; /** * Interface that all WP_Rocket\Dependencies\Monolog Handlers must implement * * @author Jordi Boggiano <j.boggiano@seld.be> */ interface HandlerInterface { /** * Checks whether the given record will be handled by this handler. * * This is mostly done for performance reasons, to avoid calling processors for nothing. * * Handlers should still check the record levels within handle(), returning false in isHandling() * is no guarantee that handle() will not be called, and isHandling() might not be called * for a given record. * * @param array $record Partial log record containing only a level key * * @return bool */ public function isHandling(array $record); /** * Handles a record. * * All records may be passed to this method, and the handler should discard * those that it does not want to handle. * * The return value of this function controls the bubbling process of the handler stack. * Unless the bubbling is interrupted (by returning true), the Logger class will keep on * calling further handlers in the stack with a given log record. * * @param array $record The record to handle * @return bool true means that this handler handled the record, and that bubbling is not permitted. * false means the record was either not processed or that this handler allows bubbling. */ public function handle(array $record); /** * Handles a set of records at once. * * @param array $records The records to handle (an array of record arrays) */ public function handleBatch(array $records); /** * Adds a processor in the stack. * * @param callable $callback * @return self */ public function pushProcessor($callback); /** * Removes the processor on top of the stack and returns it. * * @return callable */ public function popProcessor(); /** * Sets the formatter. * * @param FormatterInterface $formatter * @return self */ public function setFormatter(FormatterInterface $formatter); /** * Gets the formatter. * * @return FormatterInterface */ public function getFormatter(); } Dependencies/Monolog/Handler/AbstractHandler.php 0000644 00000010660 15174677547 0015725 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Handler; use WP_Rocket\Dependencies\Monolog\Formatter\FormatterInterface; use WP_Rocket\Dependencies\Monolog\Formatter\LineFormatter; use WP_Rocket\Dependencies\Monolog\Logger; use WP_Rocket\Dependencies\Monolog\ResettableInterface; /** * Base Handler class providing the Handler structure * * @author Jordi Boggiano <j.boggiano@seld.be> */ abstract class AbstractHandler implements HandlerInterface, ResettableInterface { protected $level = Logger::DEBUG; protected $bubble = true; /** * @var FormatterInterface */ protected $formatter; protected $processors = array(); /** * @param int|string $level The minimum logging level at which this handler will be triggered * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct($level = Logger::DEBUG, $bubble = true) { $this->setLevel($level); $this->bubble = $bubble; } /** * {@inheritdoc} */ public function isHandling(array $record) { return $record['level'] >= $this->level; } /** * {@inheritdoc} */ public function handleBatch(array $records) { foreach ($records as $record) { $this->handle($record); } } /** * Closes the handler. * * This will be called automatically when the object is destroyed */ public function close() { } /** * {@inheritdoc} */ public function pushProcessor($callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given'); } array_unshift($this->processors, $callback); return $this; } /** * {@inheritdoc} */ public function popProcessor() { if (!$this->processors) { throw new \LogicException('You tried to pop from an empty processor stack.'); } return array_shift($this->processors); } /** * {@inheritdoc} */ public function setFormatter(FormatterInterface $formatter) { $this->formatter = $formatter; return $this; } /** * {@inheritdoc} */ public function getFormatter() { if (!$this->formatter) { $this->formatter = $this->getDefaultFormatter(); } return $this->formatter; } /** * Sets minimum logging level at which this handler will be triggered. * * @param int|string $level Level or level name * @return self */ public function setLevel($level) { $this->level = Logger::toMonologLevel($level); return $this; } /** * Gets minimum logging level at which this handler will be triggered. * * @return int */ public function getLevel() { return $this->level; } /** * Sets the bubbling behavior. * * @param bool $bubble true means that this handler allows bubbling. * false means that bubbling is not permitted. * @return self */ public function setBubble($bubble) { $this->bubble = $bubble; return $this; } /** * Gets the bubbling behavior. * * @return bool true means that this handler allows bubbling. * false means that bubbling is not permitted. */ public function getBubble() { return $this->bubble; } public function __destruct() { try { $this->close(); } catch (\Exception $e) { // do nothing } catch (\Throwable $e) { // do nothing } } public function reset() { foreach ($this->processors as $processor) { if ($processor instanceof ResettableInterface) { $processor->reset(); } } } /** * Gets the default formatter. * * @return FormatterInterface */ protected function getDefaultFormatter() { return new LineFormatter(); } } Dependencies/Monolog/Handler/ProcessableHandlerTrait.php 0000644 00000003204 15174677547 0017424 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Handler; use WP_Rocket\Dependencies\Monolog\ResettableInterface; /** * Helper trait for implementing ProcessableInterface * * This trait is present in monolog 1.x to ease forward compatibility. * * @author Jordi Boggiano <j.boggiano@seld.be> */ trait ProcessableHandlerTrait { /** * @var callable[] */ protected $processors = []; /** * {@inheritdoc} * @suppress PhanTypeMismatchReturn */ public function pushProcessor($callback): HandlerInterface { array_unshift($this->processors, $callback); return $this; } /** * {@inheritdoc} */ public function popProcessor(): callable { if (!$this->processors) { throw new \LogicException('You tried to pop from an empty processor stack.'); } return array_shift($this->processors); } /** * Processes a record. */ protected function processRecord(array $record): array { foreach ($this->processors as $processor) { $record = $processor($record); } return $record; } protected function resetProcessors(): void { foreach ($this->processors as $processor) { if ($processor instanceof ResettableInterface) { $processor->reset(); } } } } Dependencies/Monolog/Handler/FormattableHandlerTrait.php 0000644 00000002716 15174677547 0017431 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Handler; use WP_Rocket\Dependencies\Monolog\Formatter\FormatterInterface; use WP_Rocket\Dependencies\Monolog\Formatter\LineFormatter; /** * Helper trait for implementing FormattableInterface * * This trait is present in monolog 1.x to ease forward compatibility. * * @author Jordi Boggiano <j.boggiano@seld.be> */ trait FormattableHandlerTrait { /** * @var FormatterInterface */ protected $formatter; /** * {@inheritdoc} * @suppress PhanTypeMismatchReturn */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { $this->formatter = $formatter; return $this; } /** * {@inheritdoc} */ public function getFormatter(): FormatterInterface { if (!$this->formatter) { $this->formatter = $this->getDefaultFormatter(); } return $this->formatter; } /** * Gets the default formatter. * * Overwrite this if the LineFormatter is not a good default for your handler. */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter(); } } Dependencies/Monolog/Handler/FormattableHandlerInterface.php 0000644 00000001740 15174677547 0020242 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Handler; use WP_Rocket\Dependencies\Monolog\Formatter\FormatterInterface; /** * Interface to describe loggers that have a formatter * * This interface is present in monolog 1.x to ease forward compatibility. * * @author Jordi Boggiano <j.boggiano@seld.be> */ interface FormattableHandlerInterface { /** * Sets the formatter. * * @param FormatterInterface $formatter * @return HandlerInterface self */ public function setFormatter(FormatterInterface $formatter): HandlerInterface; /** * Gets the formatter. * * @return FormatterInterface */ public function getFormatter(): FormatterInterface; } Dependencies/Monolog/Handler/ProcessableHandlerInterface.php 0000644 00000002076 15174677547 0020247 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Handler; use WP_Rocket\Dependencies\Monolog\Processor\ProcessorInterface; /** * Interface to describe loggers that have processors * * This interface is present in monolog 1.x to ease forward compatibility. * * @author Jordi Boggiano <j.boggiano@seld.be> */ interface ProcessableHandlerInterface { /** * Adds a processor in the stack. * * @param ProcessorInterface|callable $callback * @return HandlerInterface self */ public function pushProcessor($callback): HandlerInterface; /** * Removes the processor on top of the stack and returns it. * * @throws \LogicException In case the processor stack is empty * @return callable */ public function popProcessor(): callable; } Dependencies/Monolog/Handler/StreamHandler.php 0000644 00000013255 15174677547 0015420 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Handler; use WP_Rocket\Dependencies\Monolog\Logger; use WP_Rocket\Dependencies\Monolog\Utils; /** * Stores to any stream resource * * Can be used to store into php://stderr, remote and local files, etc. * * @author Jordi Boggiano <j.boggiano@seld.be> */ class StreamHandler extends AbstractProcessingHandler { /** @private 512KB */ const CHUNK_SIZE = 524288; /** @var resource|null */ protected $stream; protected $url; private $errorMessage; protected $filePermission; protected $useLocking; private $dirCreated; /** * @param resource|string $stream * @param int $level The minimum logging level at which this handler will be triggered * @param bool $bubble Whether the messages that are handled can bubble up the stack or not * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) * @param bool $useLocking Try to lock log file before doing any writes * * @throws \Exception If a missing directory is not buildable * @throws \InvalidArgumentException If stream is not a resource or string */ public function __construct($stream, $level = Logger::DEBUG, $bubble = true, $filePermission = null, $useLocking = false) { parent::__construct($level, $bubble); if (is_resource($stream)) { $this->stream = $stream; $this->streamSetChunkSize(); } elseif (is_string($stream)) { $this->url = Utils::canonicalizePath($stream); } else { throw new \InvalidArgumentException('A stream must either be a resource or a string.'); } $this->filePermission = $filePermission; $this->useLocking = $useLocking; } /** * {@inheritdoc} */ public function close() { if ($this->url && is_resource($this->stream)) { fclose($this->stream); } $this->stream = null; $this->dirCreated = null; } /** * Return the currently active stream if it is open * * @return resource|null */ public function getStream() { return $this->stream; } /** * Return the stream URL if it was configured with a URL and not an active resource * * @return string|null */ public function getUrl() { return $this->url; } /** * {@inheritdoc} */ protected function write(array $record) { if (!is_resource($this->stream)) { if (null === $this->url || '' === $this->url) { throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().'); } $this->createDir(); $this->errorMessage = null; set_error_handler(array($this, 'customErrorHandler')); $this->stream = fopen($this->url, 'a'); if ($this->filePermission !== null) { @chmod($this->url, $this->filePermission); } restore_error_handler(); if (!is_resource($this->stream)) { $this->stream = null; throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $this->url)); } $this->streamSetChunkSize(); } if ($this->useLocking) { // ignoring errors here, there's not much we can do about them flock($this->stream, LOCK_EX); } $this->streamWrite($this->stream, $record); if ($this->useLocking) { flock($this->stream, LOCK_UN); } } /** * Write to stream * @param resource $stream * @param array $record */ protected function streamWrite($stream, array $record) { fwrite($stream, (string) $record['formatted']); } protected function streamSetChunkSize() { if (version_compare(PHP_VERSION, '5.4.0', '>=')) { return stream_set_chunk_size($this->stream, self::CHUNK_SIZE); } return false; } private function customErrorHandler($code, $msg) { $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg); } /** * @param string $stream * * @return null|string */ private function getDirFromStream($stream) { $pos = strpos($stream, '://'); if ($pos === false) { return dirname($stream); } if ('file://' === substr($stream, 0, 7)) { return dirname(substr($stream, 7)); } return null; } private function createDir() { // Do not try to create dir if it has already been tried. if ($this->dirCreated) { return; } $dir = $this->getDirFromStream($this->url); if (null !== $dir && !is_dir($dir)) { $this->errorMessage = null; set_error_handler(array($this, 'customErrorHandler')); $status = mkdir($dir, 0777, true); restore_error_handler(); if (false === $status && !is_dir($dir)) { throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and its not buildable: '.$this->errorMessage, $dir)); } } $this->dirCreated = true; } } Dependencies/Monolog/Handler/AbstractProcessingHandler.php 0000644 00000003101 15174677547 0017752 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog\Handler; use WP_Rocket\Dependencies\Monolog\ResettableInterface; /** * Base Handler class providing the Handler structure * * Classes extending it should (in most cases) only implement write($record) * * @author Jordi Boggiano <j.boggiano@seld.be> * @author Christophe Coevoet <stof@notk.org> */ abstract class AbstractProcessingHandler extends AbstractHandler { /** * {@inheritdoc} */ public function handle(array $record) { if (!$this->isHandling($record)) { return false; } $record = $this->processRecord($record); $record['formatted'] = $this->getFormatter()->format($record); $this->write($record); return false === $this->bubble; } /** * Writes the record down to the log of the implementing handler * * @param array $record * @return void */ abstract protected function write(array $record); /** * Processes a record. * * @param array $record * @return array */ protected function processRecord(array $record) { if ($this->processors) { foreach ($this->processors as $processor) { $record = call_user_func($processor, $record); } } return $record; } } Dependencies/Monolog/ResettableInterface.php 0000644 00000001735 15174677547 0015225 0 ustar 00 <?php /* * This file is part of the WP_Rocket\Dependencies\Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_Rocket\Dependencies\Monolog; /** * Handler or Processor implementing this interface will be reset when Logger::reset() is called. * * Resetting ends a log cycle gets them back to their initial state. * * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal * state, and getting it back to a state in which it can receive log records again. * * This is useful in case you want to avoid logs leaking between two requests or jobs when you * have a long running process like a worker or an application server serving multiple requests * in one process. * * @author Grégoire Pineau <lyrixx@lyrixx.info> */ interface ResettableInterface { public function reset(); } Dependencies/PathConverter/Converter.php 0000644 00000013230 15174677547 0014424 0 ustar 00 <?php namespace WP_Rocket\Dependencies\PathConverter; /** * Convert paths relative from 1 file to another. * * E.g. * ../../images/icon.jpg relative to /css/imports/icons.css * becomes * ../images/icon.jpg relative to /css/minified.css * * Please report bugs on https://github.com/matthiasmullie/path-converter/issues * * @author Matthias Mullie <pathconverter@mullie.eu> * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved * @license MIT License */ class Converter implements ConverterInterface { /** * @var string */ protected $from; /** * @var string */ protected $to; /** * @param string $from The original base path (directory, not file!) * @param string $to The new base path (directory, not file!) * @param string $root Root directory (defaults to `getcwd`) */ public function __construct($from, $to, $root = '') { $shared = $this->shared($from, $to); if ($shared === '') { // when both paths have nothing in common, one of them is probably // absolute while the other is relative $root = $root ?: getcwd(); $from = strpos($from, $root) === 0 ? $from : preg_replace('/\/+/', '/', $root.'/'.$from); $to = strpos($to, $root) === 0 ? $to : preg_replace('/\/+/', '/', $root.'/'.$to); // or traveling the tree via `..` // attempt to resolve path, or assume it's fine if it doesn't exist $from = @realpath($from) ?: $from; $to = @realpath($to) ?: $to; } $from = $this->dirname($from); $to = $this->dirname($to); $from = $this->normalize($from); $to = $this->normalize($to); $this->from = $from; $this->to = $to; } /** * Normalize path. * * @param string $path * * @return string */ protected function normalize($path) { // deal with different operating systems' directory structure $path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/'); // remove leading current directory. if (substr($path, 0, 2) === './') { $path = substr($path, 2); } // remove references to current directory in the path. $path = str_replace('/./', '/', $path); /* * Example: * /home/forkcms/frontend/cache/compiled_templates/../../core/layout/css/../images/img.gif * to * /home/forkcms/frontend/core/layout/images/img.gif */ do { $path = preg_replace('/[^\/]+(?<!\.\.)\/\.\.\//', '', $path, -1, $count); } while ($count); return $path; } /** * Figure out the shared path of 2 locations. * * Example: * /home/forkcms/frontend/core/layout/images/img.gif * and * /home/forkcms/frontend/cache/minified_css * share * /home/forkcms/frontend * * @param string $path1 * @param string $path2 * * @return string */ protected function shared($path1, $path2) { // $path could theoretically be empty (e.g. no path is given), in which // case it shouldn't expand to array(''), which would compare to one's // root / $path1 = $path1 ? explode('/', $path1) : array(); $path2 = $path2 ? explode('/', $path2) : array(); $shared = array(); // compare paths & strip identical ancestors foreach ($path1 as $i => $chunk) { if (isset($path2[$i]) && $path1[$i] == $path2[$i]) { $shared[] = $chunk; } else { break; } } return implode('/', $shared); } /** * Convert paths relative from 1 file to another. * * E.g. * ../images/img.gif relative to /home/forkcms/frontend/core/layout/css * should become: * ../../core/layout/images/img.gif relative to * /home/forkcms/frontend/cache/minified_css * * @param string $path The relative path that needs to be converted * * @return string The new relative path */ public function convert($path) { // quit early if conversion makes no sense if ($this->from === $this->to) { return $path; } $path = $this->normalize($path); // if we're not dealing with a relative path, just return absolute if (strpos($path, '/') === 0) { return $path; } // normalize paths $path = $this->normalize($this->from.'/'.$path); // strip shared ancestor paths $shared = $this->shared($path, $this->to); $path = mb_substr($path, mb_strlen($shared)); $to = mb_substr($this->to, mb_strlen($shared)); // add .. for every directory that needs to be traversed to new path $to = str_repeat('../', count(array_filter(explode('/', $to)))); return $to.ltrim($path, '/'); } /** * Attempt to get the directory name from a path. * * @param string $path * * @return string */ protected function dirname($path) { if (@is_file($path)) { return dirname($path); } if (@is_dir($path)) { return rtrim($path, '/'); } // no known file/dir, start making assumptions // ends in / = dir if (mb_substr($path, -1) === '/') { return rtrim($path, '/'); } // has a dot in the name, likely a file if (preg_match('/.*\..*$/', basename($path)) !== 0) { return dirname($path); } // you're on your own here! return $path; } } Dependencies/PathConverter/NoConverter.php 0000644 00000000741 15174677547 0014724 0 ustar 00 <?php namespace WP_Rocket\Dependencies\PathConverter; /** * Don't convert paths. * * Please report bugs on https://github.com/matthiasmullie/path-converter/issues * * @author Matthias Mullie <pathconverter@mullie.eu> * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved * @license MIT License */ class NoConverter implements ConverterInterface { /** * {@inheritdoc} */ public function convert($path) { return $path; } } Dependencies/PathConverter/ConverterInterface.php 0000644 00000001027 15174677547 0016246 0 ustar 00 <?php namespace WP_Rocket\Dependencies\PathConverter; /** * Convert file paths. * * Please report bugs on https://github.com/matthiasmullie/path-converter/issues * * @author Matthias Mullie <pathconverter@mullie.eu> * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved * @license MIT License */ interface ConverterInterface { /** * Convert file paths. * * @param string $path The path to be converted * * @return string The new path */ public function convert($path); } Dependencies/Psr/Container/ContainerInterface.php 0000644 00000002047 15174677547 0016126 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\Psr\Container; /** * Describes the interface of a container that exposes methods to read its entries. */ interface ContainerInterface { /** * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * * @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws ContainerExceptionInterface Error while retrieving the entry. * * @return mixed Entry. */ public function get(string $id); /** * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * * @param string $id Identifier of the entry to look for. * * @return bool */ public function has(string $id); } Dependencies/Psr/Container/NotFoundExceptionInterface.php 0000644 00000000265 15174677547 0017617 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Container; /** * No entry was found in the container. */ interface NotFoundExceptionInterface extends ContainerExceptionInterface { } Dependencies/Psr/Container/ContainerExceptionInterface.php 0000644 00000000255 15174677547 0020004 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Container; /** * Base interface representing a generic exception in a container. */ interface ContainerExceptionInterface { } Dependencies/Psr/Cache/CacheItemPoolInterface.php 0000644 00000011001 15174677547 0015727 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Cache; /** * CacheItemPoolInterface generates CacheItemInterface objects. * * The primary purpose of Cache\CacheItemPoolInterface is to accept a key from * the Calling Library and return the associated Cache\CacheItemInterface object. * It is also the primary point of interaction with the entire cache collection. * All configuration and initialization of the Pool is left up to an * Implementing Library. */ interface CacheItemPoolInterface { /** * Returns a Cache Item representing the specified key. * * This method must always return a CacheItemInterface object, even in case of * a cache miss. It MUST NOT return null. * * @param string $key * The key for which to return the corresponding Cache Item. * * @throws InvalidArgumentException * If the $key string is not a legal value a \WP_Rocket\Dependencies\Psr\Cache\InvalidArgumentException * MUST be thrown. * * @return CacheItemInterface * The corresponding Cache Item. */ public function getItem(string $key): CacheItemInterface; /** * Returns a traversable set of cache items. * * @param string[] $keys * An indexed array of keys of items to retrieve. * * @throws InvalidArgumentException * If any of the keys in $keys are not a legal value a \WP_Rocket\Dependencies\Psr\Cache\InvalidArgumentException * MUST be thrown. * * @return iterable * An iterable collection of Cache Items keyed by the cache keys of * each item. A Cache item will be returned for each key, even if that * key is not found. However, if no keys are specified then an empty * traversable MUST be returned instead. */ public function getItems(array $keys = []): iterable; /** * Confirms if the cache contains specified cache item. * * Note: This method MAY avoid retrieving the cached value for performance reasons. * This could result in a race condition with CacheItemInterface::get(). To avoid * such situation use CacheItemInterface::isHit() instead. * * @param string $key * The key for which to check existence. * * @throws InvalidArgumentException * If the $key string is not a legal value a \WP_Rocket\Dependencies\Psr\Cache\InvalidArgumentException * MUST be thrown. * * @return bool * True if item exists in the cache, false otherwise. */ public function hasItem(string $key): bool; /** * Deletes all items in the pool. * * @return bool * True if the pool was successfully cleared. False if there was an error. */ public function clear(): bool; /** * Removes the item from the pool. * * @param string $key * The key to delete. * * @throws InvalidArgumentException * If the $key string is not a legal value a \WP_Rocket\Dependencies\Psr\Cache\InvalidArgumentException * MUST be thrown. * * @return bool * True if the item was successfully removed. False if there was an error. */ public function deleteItem(string $key): bool; /** * Removes multiple items from the pool. * * @param string[] $keys * An array of keys that should be removed from the pool. * * @throws InvalidArgumentException * If any of the keys in $keys are not a legal value a \WP_Rocket\Dependencies\Psr\Cache\InvalidArgumentException * MUST be thrown. * * @return bool * True if the items were successfully removed. False if there was an error. */ public function deleteItems(array $keys): bool; /** * Persists a cache item immediately. * * @param CacheItemInterface $item * The cache item to save. * * @return bool * True if the item was successfully persisted. False if there was an error. */ public function save(CacheItemInterface $item): bool; /** * Sets a cache item to be persisted later. * * @param CacheItemInterface $item * The cache item to save. * * @return bool * False if the item could not be queued or if a commit was attempted and failed. True otherwise. */ public function saveDeferred(CacheItemInterface $item): bool; /** * Persists any deferred cache items. * * @return bool * True if all not-yet-saved items were successfully saved or there were none. False otherwise. */ public function commit(): bool; } Dependencies/Psr/Cache/CacheException.php 0000644 00000000271 15174677547 0014323 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Cache; /** * Exception interface for all exceptions thrown by an Implementing Library. */ interface CacheException extends \Throwable { } Dependencies/Psr/Cache/CacheItemInterface.php 0000644 00000007426 15174677547 0015115 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Cache; /** * CacheItemInterface defines an interface for interacting with objects inside a cache. * * Each Item object MUST be associated with a specific key, which can be set * according to the implementing system and is typically passed by the * Cache\CacheItemPoolInterface object. * * The Cache\CacheItemInterface object encapsulates the storage and retrieval of * cache items. Each Cache\CacheItemInterface is generated by a * Cache\CacheItemPoolInterface object, which is responsible for any required * setup as well as associating the object with a unique Key. * Cache\CacheItemInterface objects MUST be able to store and retrieve any type * of PHP value defined in the Data section of the specification. * * Calling Libraries MUST NOT instantiate Item objects themselves. They may only * be requested from a Pool object via the getItem() method. Calling Libraries * SHOULD NOT assume that an Item created by one Implementing Library is * compatible with a Pool from another Implementing Library. */ interface CacheItemInterface { /** * Returns the key for the current cache item. * * The key is loaded by the Implementing Library, but should be available to * the higher level callers when needed. * * @return string * The key string for this cache item. */ public function getKey(): string; /** * Retrieves the value of the item from the cache associated with this object's key. * * The value returned must be identical to the value originally stored by set(). * * If isHit() returns false, this method MUST return null. Note that null * is a legitimate cached value, so the isHit() method SHOULD be used to * differentiate between "null value was found" and "no value was found." * * @return mixed * The value corresponding to this cache item's key, or null if not found. */ public function get(): mixed; /** * Confirms if the cache item lookup resulted in a cache hit. * * Note: This method MUST NOT have a race condition between calling isHit() * and calling get(). * * @return bool * True if the request resulted in a cache hit. False otherwise. */ public function isHit(): bool; /** * Sets the value represented by this cache item. * * The $value argument may be any item that can be serialized by PHP, * although the method of serialization is left up to the Implementing * Library. * * @param mixed $value * The serializable value to be stored. * * @return static * The invoked object. */ public function set(mixed $value): static; /** * Sets the expiration time for this cache item. * * @param ?\DateTimeInterface $expiration * The point in time after which the item MUST be considered expired. * If null is passed explicitly, a default value MAY be used. If none is set, * the value should be stored permanently or for as long as the * implementation allows. * * @return static * The called object. */ public function expiresAt(?\DateTimeInterface $expiration): static; /** * Sets the expiration time for this cache item. * * @param int|\DateInterval|null $time * The period of time from the present after which the item MUST be considered * expired. An integer parameter is understood to be the time in seconds until * expiration. If null is passed explicitly, a default value MAY be used. * If none is set, the value should be stored permanently or for as long as the * implementation allows. * * @return static * The called object. */ public function expiresAfter($time): static; } Dependencies/Psr/Cache/InvalidArgumentException.php 0000644 00000000531 15174677547 0016410 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Cache; /** * Exception interface for invalid cache arguments. * * Any time an invalid argument is passed into a method it must throw an * exception class which implements WP_Rocket\Dependencies\Psr\Cache\InvalidArgumentException. */ interface InvalidArgumentException extends CacheException { } Dependencies/Psr/Log/LoggerTrait.php 0000644 00000006605 15174677547 0013411 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log; /** * This is a simple Logger trait that classes unable to extend AbstractLogger * (because they extend another class, etc) can include. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ trait LoggerTrait { /** * System is unusable. * * @param string $message * @param array $context * * @return void */ public function emergency($message, array $context = array()) { $this->log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \WP_Rocket\Dependencies\Psr\Log\InvalidArgumentException */ abstract public function log($level, $message, array $context = array()); } Dependencies/Psr/Log/Test/TestLogger.php 0000644 00000010735 15174677547 0014163 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log\Test; use WP_Rocket\Dependencies\Psr\Log\AbstractLogger; /** * Used for testing purposes. * * It records all records and gives you access to them for verification. * * @method bool hasEmergency($record) * @method bool hasAlert($record) * @method bool hasCritical($record) * @method bool hasError($record) * @method bool hasWarning($record) * @method bool hasNotice($record) * @method bool hasInfo($record) * @method bool hasDebug($record) * * @method bool hasEmergencyRecords() * @method bool hasAlertRecords() * @method bool hasCriticalRecords() * @method bool hasErrorRecords() * @method bool hasWarningRecords() * @method bool hasNoticeRecords() * @method bool hasInfoRecords() * @method bool hasDebugRecords() * * @method bool hasEmergencyThatContains($message) * @method bool hasAlertThatContains($message) * @method bool hasCriticalThatContains($message) * @method bool hasErrorThatContains($message) * @method bool hasWarningThatContains($message) * @method bool hasNoticeThatContains($message) * @method bool hasInfoThatContains($message) * @method bool hasDebugThatContains($message) * * @method bool hasEmergencyThatMatches($message) * @method bool hasAlertThatMatches($message) * @method bool hasCriticalThatMatches($message) * @method bool hasErrorThatMatches($message) * @method bool hasWarningThatMatches($message) * @method bool hasNoticeThatMatches($message) * @method bool hasInfoThatMatches($message) * @method bool hasDebugThatMatches($message) * * @method bool hasEmergencyThatPasses($message) * @method bool hasAlertThatPasses($message) * @method bool hasCriticalThatPasses($message) * @method bool hasErrorThatPasses($message) * @method bool hasWarningThatPasses($message) * @method bool hasNoticeThatPasses($message) * @method bool hasInfoThatPasses($message) * @method bool hasDebugThatPasses($message) */ class TestLogger extends AbstractLogger { /** * @var array */ public $records = []; public $recordsByLevel = []; /** * @inheritdoc */ public function log($level, $message, array $context = []) { $record = [ 'level' => $level, 'message' => $message, 'context' => $context, ]; $this->recordsByLevel[$record['level']][] = $record; $this->records[] = $record; } public function hasRecords($level) { return isset($this->recordsByLevel[$level]); } public function hasRecord($record, $level) { if (is_string($record)) { $record = ['message' => $record]; } return $this->hasRecordThatPasses(function ($rec) use ($record) { if ($rec['message'] !== $record['message']) { return false; } if (isset($record['context']) && $rec['context'] !== $record['context']) { return false; } return true; }, $level); } public function hasRecordThatContains($message, $level) { return $this->hasRecordThatPasses(function ($rec) use ($message) { return strpos($rec['message'], $message) !== false; }, $level); } public function hasRecordThatMatches($regex, $level) { return $this->hasRecordThatPasses(function ($rec) use ($regex) { return preg_match($regex, $rec['message']) > 0; }, $level); } public function hasRecordThatPasses(callable $predicate, $level) { if (!isset($this->recordsByLevel[$level])) { return false; } foreach ($this->recordsByLevel[$level] as $i => $rec) { if (call_user_func($predicate, $rec, $i)) { return true; } } return false; } public function __call($method, $args) { if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; $level = strtolower($matches[2]); if (method_exists($this, $genericMethod)) { $args[] = $level; return call_user_func_array([$this, $genericMethod], $args); } } throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); } public function reset() { $this->records = []; $this->recordsByLevel = []; } } Dependencies/Psr/Log/Test/DummyTest.php 0000644 00000000422 15174677547 0014027 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log\Test; /** * This class is internal and does not follow the BC promise. * * Do NOT use this class in any way. * * @internal */ class DummyTest { public function __toString() { return 'DummyTest'; } } Dependencies/Psr/Log/Test/LoggerInterfaceTest.php 0000644 00000011312 15174677547 0015774 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log\Test; use WP_Rocket\Dependencies\Psr\Log\LoggerInterface; use WP_Rocket\Dependencies\Psr\Log\LogLevel; use PHPUnit\Framework\TestCase; /** * Provides a base test class for ensuring compliance with the LoggerInterface. * * Implementors can extend the class and implement abstract methods to run this * as part of their test suite. */ abstract class LoggerInterfaceTest extends TestCase { /** * @return LoggerInterface */ abstract public function getLogger(); /** * This must return the log messages in order. * * The simple formatting of the messages is: "<LOG LEVEL> <MESSAGE>". * * Example ->error('Foo') would yield "error Foo". * * @return string[] */ abstract public function getLogs(); public function testImplements() { $this->assertInstanceOf('WP_Rocket\Dependencies\Psr\Log\LoggerInterface', $this->getLogger()); } /** * @dataProvider provideLevelsAndMessages */ public function testLogsAtAllLevels($level, $message) { $logger = $this->getLogger(); $logger->{$level}($message, array('user' => 'Bob')); $logger->log($level, $message, array('user' => 'Bob')); $expected = array( $level.' message of level '.$level.' with context: Bob', $level.' message of level '.$level.' with context: Bob', ); $this->assertEquals($expected, $this->getLogs()); } public function provideLevelsAndMessages() { return array( LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'), LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'), LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'), LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'), LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'), LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'), LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'), LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'), ); } /** * @expectedException \WP_Rocket\Dependencies\Psr\Log\InvalidArgumentException */ public function testThrowsOnInvalidLevel() { $logger = $this->getLogger(); $logger->log('invalid level', 'Foo'); } public function testContextReplacement() { $logger = $this->getLogger(); $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); $expected = array('info {Message {nothing} Bob Bar a}'); $this->assertEquals($expected, $this->getLogs()); } public function testObjectCastToString() { if (method_exists($this, 'createPartialMock')) { $dummy = $this->createPartialMock('WP_Rocket\Dependencies\Psr\Log\Test\DummyTest', array('__toString')); } else { $dummy = $this->getMock('WP_Rocket\Dependencies\Psr\Log\Test\DummyTest', array('__toString')); } $dummy->expects($this->once()) ->method('__toString') ->will($this->returnValue('DUMMY')); $this->getLogger()->warning($dummy); $expected = array('warning DUMMY'); $this->assertEquals($expected, $this->getLogs()); } public function testContextCanContainAnything() { $closed = fopen('php://memory', 'r'); fclose($closed); $context = array( 'bool' => true, 'null' => null, 'string' => 'Foo', 'int' => 0, 'float' => 0.5, 'nested' => array('with object' => new DummyTest), 'object' => new \DateTime, 'resource' => fopen('php://memory', 'r'), 'closed' => $closed, ); $this->getLogger()->warning('Crazy context data', $context); $expected = array('warning Crazy context data'); $this->assertEquals($expected, $this->getLogs()); } public function testContextExceptionKeyCanBeExceptionOrOtherValues() { $logger = $this->getLogger(); $logger->warning('Random message', array('exception' => 'oops')); $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail'))); $expected = array( 'warning Random message', 'critical Uncaught Exception!' ); $this->assertEquals($expected, $this->getLogs()); } } Dependencies/Psr/Log/LoggerInterface.php 0000644 00000006130 15174677547 0014217 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log; /** * Describes a logger instance. * * The message MUST be a string or object implementing __toString(). * * The message MAY contain placeholders in the form: {foo} where foo * will be replaced by the context data in key "foo". * * The context array can contain arbitrary data. The only assumption that * can be made by implementors is that if an Exception instance is given * to produce a stack trace, it MUST be in a key named "exception". * * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md * for the full interface specification. */ interface LoggerInterface { /** * System is unusable. * * @param string $message * @param mixed[] $context * * @return void */ public function emergency($message, array $context = array()); /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param mixed[] $context * * @return void */ public function alert($message, array $context = array()); /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param mixed[] $context * * @return void */ public function critical($message, array $context = array()); /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param mixed[] $context * * @return void */ public function error($message, array $context = array()); /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param mixed[] $context * * @return void */ public function warning($message, array $context = array()); /** * Normal but significant events. * * @param string $message * @param mixed[] $context * * @return void */ public function notice($message, array $context = array()); /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param mixed[] $context * * @return void */ public function info($message, array $context = array()); /** * Detailed debug information. * * @param string $message * @param mixed[] $context * * @return void */ public function debug($message, array $context = array()); /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param mixed[] $context * * @return void * * @throws \WP_Rocket\Dependencies\Psr\Log\InvalidArgumentException */ public function log($level, $message, array $context = array()); } Dependencies/Psr/Log/AbstractLogger.php 0000644 00000006067 15174677547 0014073 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log; /** * This is a simple Logger implementation that other Loggers can inherit from. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ abstract class AbstractLogger implements LoggerInterface { /** * System is unusable. * * @param string $message * @param mixed[] $context * * @return void */ public function emergency($message, array $context = array()) { $this->log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param mixed[] $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param mixed[] $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param mixed[] $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param mixed[] $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param mixed[] $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param mixed[] $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param mixed[] $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } } Dependencies/Psr/Log/NullLogger.php 0000644 00000001361 15174677547 0013232 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log; /** * This Logger can be used to avoid conditional log calls. * * Logging should always be optional, and if no logger is provided to your * library creating a NullLogger instance to have something to throw logs at * is a good way to avoid littering your code with `if ($this->logger) { }` * blocks. */ class NullLogger extends AbstractLogger { /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \WP_Rocket\Dependencies\Psr\Log\InvalidArgumentException */ public function log($level, $message, array $context = array()) { // noop } } Dependencies/Psr/Log/LoggerAwareTrait.php 0000644 00000000651 15174677547 0014364 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log; /** * Basic Implementation of LoggerAwareInterface. */ trait LoggerAwareTrait { /** * The logger instance. * * @var LoggerInterface|null */ protected $logger; /** * Sets a logger. * * @param LoggerInterface $logger */ public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } } Dependencies/Psr/Log/LogLevel.php 0000644 00000000547 15174677547 0012676 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log; /** * Describes log levels. */ class LogLevel { const EMERGENCY = 'emergency'; const ALERT = 'alert'; const CRITICAL = 'critical'; const ERROR = 'error'; const WARNING = 'warning'; const NOTICE = 'notice'; const INFO = 'info'; const DEBUG = 'debug'; } Dependencies/Psr/Log/InvalidArgumentException.php 0000644 00000000167 15174677547 0016133 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log; class InvalidArgumentException extends \InvalidArgumentException { } Dependencies/Psr/Log/LoggerAwareInterface.php 0000644 00000000500 15174677547 0015172 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\Log; /** * Describes a logger-aware instance. */ interface LoggerAwareInterface { /** * Sets a logger instance on the object. * * @param LoggerInterface $logger * * @return void */ public function setLogger(LoggerInterface $logger); } Dependencies/Psr/SimpleCache/CacheException.php 0000644 00000000304 15174677547 0015472 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\SimpleCache; /** * Interface used for all types of exceptions thrown by the implementing library. */ interface CacheException extends \Throwable { } Dependencies/Psr/SimpleCache/CacheInterface.php 0000644 00000011507 15174677547 0015443 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\SimpleCache; interface CacheInterface { /** * Fetches a value from the cache. * * @param string $key The unique key of this item in the cache. * @param mixed $default Default value to return if the key does not exist. * * @return mixed The value of the item from the cache, or $default in case of cache miss. * * @throws \WP_Rocket\Dependencies\Psr\SimpleCache\InvalidArgumentException * MUST be thrown if the $key string is not a legal value. */ public function get(string $key, $default = null); /** * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. * * @param string $key The key of the item to store. * @param mixed $value The value of the item to store, must be serializable. * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and * the driver supports TTL then the library may set a default value * for it or let the driver take care of that. * * @return bool True on success and false on failure. * * @throws \WP_Rocket\Dependencies\Psr\SimpleCache\InvalidArgumentException * MUST be thrown if the $key string is not a legal value. */ public function set(string $key, $value, $ttl = null): bool; /** * Delete an item from the cache by its unique key. * * @param string $key The unique cache key of the item to delete. * * @return bool True if the item was successfully removed. False if there was an error. * * @throws \WP_Rocket\Dependencies\Psr\SimpleCache\InvalidArgumentException * MUST be thrown if the $key string is not a legal value. */ public function delete(string $key): bool; /** * Wipes clean the entire cache's keys. * * @return bool True on success and false on failure. */ public function clear(): bool; /** * Obtains multiple cache items by their unique keys. * * @param iterable<string> $keys A list of keys that can be obtained in a single operation. * @param mixed $default Default value to return for keys that do not exist. * * @return iterable<string, mixed> A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value. * * @throws \WP_Rocket\Dependencies\Psr\SimpleCache\InvalidArgumentException * MUST be thrown if $keys is neither an array nor a Traversable, * or if any of the $keys are not a legal value. */ public function getMultiple(iterable $keys, $default = null): iterable; /** * Persists a set of key => value pairs in the cache, with an optional TTL. * * @param iterable $values A list of key => value pairs for a multiple-set operation. * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and * the driver supports TTL then the library may set a default value * for it or let the driver take care of that. * * @return bool True on success and false on failure. * * @throws \WP_Rocket\Dependencies\Psr\SimpleCache\InvalidArgumentException * MUST be thrown if $values is neither an array nor a Traversable, * or if any of the $values are not a legal value. */ public function setMultiple(iterable $values, $ttl = null): bool; /** * Deletes multiple cache items in a single operation. * * @param iterable<string> $keys A list of string-based keys to be deleted. * * @return bool True if the items were successfully removed. False if there was an error. * * @throws \WP_Rocket\Dependencies\Psr\SimpleCache\InvalidArgumentException * MUST be thrown if $keys is neither an array nor a Traversable, * or if any of the $keys are not a legal value. */ public function deleteMultiple(iterable $keys): bool; /** * Determines whether an item is present in the cache. * * NOTE: It is recommended that has() is only to be used for cache warming type purposes * and not to be used within your live applications operations for get/set, as this method * is subject to a race condition where your has() will return true and immediately after, * another script can remove it making the state of your app out of date. * * @param string $key The cache item key. * * @return bool * * @throws \WP_Rocket\Dependencies\Psr\SimpleCache\InvalidArgumentException * MUST be thrown if the $key string is not a legal value. */ public function has(string $key): bool; } Dependencies/Psr/SimpleCache/InvalidArgumentException.php 0000644 00000000433 15174677547 0017563 0 ustar 00 <?php namespace WP_Rocket\Dependencies\Psr\SimpleCache; /** * Exception interface for invalid cache arguments. * * When an invalid argument is passed it must throw an exception which implements * this interface */ interface InvalidArgumentException extends CacheException { } Dependencies/Minify/JS.php 0000644 00000107435 15174677547 0011453 0 ustar 00 <?php /** * JavaScript minifier * * Please report bugs on https://github.com/matthiasmullie/minify/issues * * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ namespace WP_Rocket\Dependencies\Minify; /** * JavaScript Minifier Class * * Please report bugs on https://github.com/matthiasmullie/minify/issues * * @package Minify * @author Matthias Mullie <minify@mullie.eu> * @author Tijs Verkoyen <minify@verkoyen.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ class JS extends Minify { /** * Var-matching regex based on http://stackoverflow.com/a/9337047/802993. * * Note that regular expressions using that bit must have the PCRE_UTF8 * pattern modifier (/u) set. * * @var string */ const REGEX_VARIABLE = '\b[$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}][$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}0-9\x{0300}-\x{036f}\x{0483}-\x{0487}\x{0591}-\x{05bd}\x{05bf}\x{05c1}\x{05c2}\x{05c4}\x{05c5}\x{05c7}\x{0610}-\x{061a}\x{064b}-\x{0669}\x{0670}\x{06d6}-\x{06dc}\x{06df}-\x{06e4}\x{06e7}\x{06e8}\x{06ea}-\x{06ed}\x{06f0}-\x{06f9}\x{0711}\x{0730}-\x{074a}\x{07a6}-\x{07b0}\x{07c0}-\x{07c9}\x{07eb}-\x{07f3}\x{0816}-\x{0819}\x{081b}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082d}\x{0859}-\x{085b}\x{08e4}-\x{08fe}\x{0900}-\x{0903}\x{093a}-\x{093c}\x{093e}-\x{094f}\x{0951}-\x{0957}\x{0962}\x{0963}\x{0966}-\x{096f}\x{0981}-\x{0983}\x{09bc}\x{09be}-\x{09c4}\x{09c7}\x{09c8}\x{09cb}-\x{09cd}\x{09d7}\x{09e2}\x{09e3}\x{09e6}-\x{09ef}\x{0a01}-\x{0a03}\x{0a3c}\x{0a3e}-\x{0a42}\x{0a47}\x{0a48}\x{0a4b}-\x{0a4d}\x{0a51}\x{0a66}-\x{0a71}\x{0a75}\x{0a81}-\x{0a83}\x{0abc}\x{0abe}-\x{0ac5}\x{0ac7}-\x{0ac9}\x{0acb}-\x{0acd}\x{0ae2}\x{0ae3}\x{0ae6}-\x{0aef}\x{0b01}-\x{0b03}\x{0b3c}\x{0b3e}-\x{0b44}\x{0b47}\x{0b48}\x{0b4b}-\x{0b4d}\x{0b56}\x{0b57}\x{0b62}\x{0b63}\x{0b66}-\x{0b6f}\x{0b82}\x{0bbe}-\x{0bc2}\x{0bc6}-\x{0bc8}\x{0bca}-\x{0bcd}\x{0bd7}\x{0be6}-\x{0bef}\x{0c01}-\x{0c03}\x{0c3e}-\x{0c44}\x{0c46}-\x{0c48}\x{0c4a}-\x{0c4d}\x{0c55}\x{0c56}\x{0c62}\x{0c63}\x{0c66}-\x{0c6f}\x{0c82}\x{0c83}\x{0cbc}\x{0cbe}-\x{0cc4}\x{0cc6}-\x{0cc8}\x{0cca}-\x{0ccd}\x{0cd5}\x{0cd6}\x{0ce2}\x{0ce3}\x{0ce6}-\x{0cef}\x{0d02}\x{0d03}\x{0d3e}-\x{0d44}\x{0d46}-\x{0d48}\x{0d4a}-\x{0d4d}\x{0d57}\x{0d62}\x{0d63}\x{0d66}-\x{0d6f}\x{0d82}\x{0d83}\x{0dca}\x{0dcf}-\x{0dd4}\x{0dd6}\x{0dd8}-\x{0ddf}\x{0df2}\x{0df3}\x{0e31}\x{0e34}-\x{0e3a}\x{0e47}-\x{0e4e}\x{0e50}-\x{0e59}\x{0eb1}\x{0eb4}-\x{0eb9}\x{0ebb}\x{0ebc}\x{0ec8}-\x{0ecd}\x{0ed0}-\x{0ed9}\x{0f18}\x{0f19}\x{0f20}-\x{0f29}\x{0f35}\x{0f37}\x{0f39}\x{0f3e}\x{0f3f}\x{0f71}-\x{0f84}\x{0f86}\x{0f87}\x{0f8d}-\x{0f97}\x{0f99}-\x{0fbc}\x{0fc6}\x{102b}-\x{103e}\x{1040}-\x{1049}\x{1056}-\x{1059}\x{105e}-\x{1060}\x{1062}-\x{1064}\x{1067}-\x{106d}\x{1071}-\x{1074}\x{1082}-\x{108d}\x{108f}-\x{109d}\x{135d}-\x{135f}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}\x{1753}\x{1772}\x{1773}\x{17b4}-\x{17d3}\x{17dd}\x{17e0}-\x{17e9}\x{180b}-\x{180d}\x{1810}-\x{1819}\x{18a9}\x{1920}-\x{192b}\x{1930}-\x{193b}\x{1946}-\x{194f}\x{19b0}-\x{19c0}\x{19c8}\x{19c9}\x{19d0}-\x{19d9}\x{1a17}-\x{1a1b}\x{1a55}-\x{1a5e}\x{1a60}-\x{1a7c}\x{1a7f}-\x{1a89}\x{1a90}-\x{1a99}\x{1b00}-\x{1b04}\x{1b34}-\x{1b44}\x{1b50}-\x{1b59}\x{1b6b}-\x{1b73}\x{1b80}-\x{1b82}\x{1ba1}-\x{1bad}\x{1bb0}-\x{1bb9}\x{1be6}-\x{1bf3}\x{1c24}-\x{1c37}\x{1c40}-\x{1c49}\x{1c50}-\x{1c59}\x{1cd0}-\x{1cd2}\x{1cd4}-\x{1ce8}\x{1ced}\x{1cf2}-\x{1cf4}\x{1dc0}-\x{1de6}\x{1dfc}-\x{1dff}\x{200c}\x{200d}\x{203f}\x{2040}\x{2054}\x{20d0}-\x{20dc}\x{20e1}\x{20e5}-\x{20f0}\x{2cef}-\x{2cf1}\x{2d7f}\x{2de0}-\x{2dff}\x{302a}-\x{302f}\x{3099}\x{309a}\x{a620}-\x{a629}\x{a66f}\x{a674}-\x{a67d}\x{a69f}\x{a6f0}\x{a6f1}\x{a802}\x{a806}\x{a80b}\x{a823}-\x{a827}\x{a880}\x{a881}\x{a8b4}-\x{a8c4}\x{a8d0}-\x{a8d9}\x{a8e0}-\x{a8f1}\x{a900}-\x{a909}\x{a926}-\x{a92d}\x{a947}-\x{a953}\x{a980}-\x{a983}\x{a9b3}-\x{a9c0}\x{a9d0}-\x{a9d9}\x{aa29}-\x{aa36}\x{aa43}\x{aa4c}\x{aa4d}\x{aa50}-\x{aa59}\x{aa7b}\x{aab0}\x{aab2}-\x{aab4}\x{aab7}\x{aab8}\x{aabe}\x{aabf}\x{aac1}\x{aaeb}-\x{aaef}\x{aaf5}\x{aaf6}\x{abe3}-\x{abea}\x{abec}\x{abed}\x{abf0}-\x{abf9}\x{fb1e}\x{fe00}-\x{fe0f}\x{fe20}-\x{fe26}\x{fe33}\x{fe34}\x{fe4d}-\x{fe4f}\x{ff10}-\x{ff19}\x{ff3f}]*\b'; /** * Full list of JavaScript reserved words. * Will be loaded from /data/js/keywords_reserved.txt. * * @see https://mathiasbynens.be/notes/reserved-keywords * * @var string[] */ protected $keywordsReserved = array(); /** * List of JavaScript reserved words that accept a <variable, value, ...> * after them. Some end of lines are not the end of a statement, like with * these keywords. * * E.g.: we shouldn't insert a ; after this else * else * console.log('this is quite fine') * * Will be loaded from /data/js/keywords_before.txt * * @var string[] */ protected $keywordsBefore = array(); /** * List of JavaScript reserved words that accept a <variable, value, ...> * before them. Some end of lines are not the end of a statement, like when * continued by one of these keywords on the newline. * * E.g.: we shouldn't insert a ; before this instanceof * variable * instanceof String * * Will be loaded from /data/js/keywords_after.txt * * @var string[] */ protected $keywordsAfter = array(); /** * List of all JavaScript operators. * * Will be loaded from /data/js/operators.txt * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators * * @var string[] */ protected $operators = array(); /** * List of JavaScript operators that accept a <variable, value, ...> after * them. Some end of lines are not the end of a statement, like with these * operators. * * Note: Most operators are fine, we've only removed ++ and --. * ++ & -- have to be joined with the value they're in-/decrementing. * * Will be loaded from /data/js/operators_before.txt * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators * * @var string[] */ protected $operatorsBefore = array(); /** * List of JavaScript operators that accept a <variable, value, ...> before * them. Some end of lines are not the end of a statement, like when * continued by one of these operators on the newline. * * Note: Most operators are fine, we've only removed ), ], ++, --, ! and ~. * There can't be a newline separating ! or ~ and whatever it is negating. * ++ & -- have to be joined with the value they're in-/decrementing. * ) & ] are "special" in that they have lots or usecases. () for example * is used for function calls, for grouping, in if () and for (), ... * * Will be loaded from /data/js/operators_after.txt * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators * * @var string[] */ protected $operatorsAfter = array(); /** * {@inheritdoc} */ public function __construct() { call_user_func_array(array('\\WP_Rocket\Dependencies\Minify\\Minify', '__construct'), func_get_args()); $dataDir = __DIR__.'/data/js/'; $options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES; $this->keywordsReserved = file($dataDir.'keywords_reserved.txt', $options); $this->keywordsBefore = file($dataDir.'keywords_before.txt', $options); $this->keywordsAfter = file($dataDir.'keywords_after.txt', $options); $this->operators = file($dataDir.'operators.txt', $options); $this->operatorsBefore = file($dataDir.'operators_before.txt', $options); $this->operatorsAfter = file($dataDir.'operators_after.txt', $options); } /** * Minify the data. * Perform JS optimizations. * * @param string[optional] $path Path to write the data to * * @return string The minified data */ public function execute($path = null) { $content = ''; /* * Let's first take out strings, comments and regular expressions. * All of these can contain JS code-like characters, and we should make * sure any further magic ignores anything inside of these. * * Consider this example, where we should not strip any whitespace: * var str = "a test"; * * Comments will be removed altogether, strings and regular expressions * will be replaced by placeholder text, which we'll restore later. */ $this->extractStrings('\'"`'); $this->stripComments(); $this->extractRegex(); // loop files foreach ($this->data as $source => $js) { // take out strings, comments & regex (for which we've registered // the regexes just a few lines earlier) $js = $this->replace($js); $js = $this->propertyNotation($js); $js = $this->shortenBools($js); $js = $this->stripWhitespace($js); // combine js: separating the scripts by a ; $content .= $js.";"; } // clean up leftover `;`s from the combination of multiple scripts $content = ltrim($content, ';'); $content = (string) substr($content, 0, -1); /* * Earlier, we extracted strings & regular expressions and replaced them * with placeholder text. This will restore them. */ $content = $this->restoreExtractedData($content); return $content; } /** * Strip comments from source code. */ protected function stripComments() { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier) { if ( substr($match[1], 0, 1) === '!' || strpos($match[1], '@license') !== false || strpos($match[1], '@preserve') !== false ) { // preserve multi-line comments that start with /*! // or contain @license or @preserve annotations $count = count($minifier->extracted); $placeholder = '/*'.$count.'*/'; $minifier->extracted[$placeholder] = $match[0]; return $placeholder; } return ''; }; // multi-line comments $this->registerPattern('/\n?\/\*(.*?)\*\/\n?/s', $callback); // single-line comments $this->registerPattern('/\/\/.*$/m', ''); } /** * JS can have /-delimited regular expressions, like: /ab+c/.match(string). * * The content inside the regex can contain characters that may be confused * for JS code: e.g. it could contain whitespace it needs to match & we * don't want to strip whitespace in there. * * The regex can be pretty simple: we don't have to care about comments, * (which also use slashes) because stripComments() will have stripped those * already. * * This method will replace all string content with simple REGEX# * placeholder text, so we've rid all regular expressions from characters * that may be misinterpreted. Original regex content will be saved in * $this->extracted and after doing all other minifying, we can restore the * original content via restoreRegex() */ protected function extractRegex() { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier) { $count = count($minifier->extracted); $placeholder = '"'.$count.'"'; $minifier->extracted[$placeholder] = $match[0]; return $placeholder; }; // match all chars except `/` and `\` // `\` is allowed though, along with whatever char follows (which is the // one being escaped) // this should allow all chars, except for an unescaped `/` (= the one // closing the regex) // then also ignore bare `/` inside `[]`, where they don't need to be // escaped: anything inside `[]` can be ignored safely $pattern = '\\/(?!\*)(?:[^\\[\\/\\\\\n\r]++|(?:\\\\.)++|(?:\\[(?:[^\\]\\\\\n\r]++|(?:\\\\.)++)++\\])++)++\\/[gimuy]*'; // a regular expression can only be followed by a few operators or some // of the RegExp methods (a `\` followed by a variable or value is // likely part of a division, not a regex) $keywords = array('do', 'in', 'new', 'else', 'throw', 'yield', 'delete', 'return', 'typeof'); $before = '(^|[=:,;\+\-\*\/\}\(\{\[&\|!]|'.implode('|', $keywords).')\s*'; $propertiesAndMethods = array( // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties_2 'constructor', 'flags', 'global', 'ignoreCase', 'multiline', 'source', 'sticky', 'unicode', // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Methods_2 'compile(', 'exec(', 'test(', 'toSource(', 'toString(', ); $delimiters = array_fill(0, count($propertiesAndMethods), '/'); $propertiesAndMethods = array_map('preg_quote', $propertiesAndMethods, $delimiters); $after = '(?=\s*([\.,;\)\}&\|+]|\/\/|$|\.('.implode('|', $propertiesAndMethods).')))'; $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback); // regular expressions following a `)` are rather annoying to detect... // quite often, `/` after `)` is a division operator & if it happens to // be followed by another one (or a comment), it is likely to be // confused for a regular expression // however, it's perfectly possible for a regex to follow a `)`: after // a single-line `if()`, `while()`, ... statement, for example // since, when they occur like that, they're always the start of a // statement, there's only a limited amount of ways they can be useful: // by calling the regex methods directly // if a regex following `)` is not followed by `.<property or method>`, // it's quite likely not a regex $before = '\)\s*'; $after = '(?=\s*\.('.implode('|', $propertiesAndMethods).'))'; $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback); // 1 more edge case: a regex can be followed by a lot more operators or // keywords if there's a newline (ASI) in between, where the operator // actually starts a new statement // (https://github.com/matthiasmullie/minify/issues/56) $operators = $this->getOperatorsForRegex($this->operatorsBefore, '/'); $operators += $this->getOperatorsForRegex($this->keywordsReserved, '/'); $after = '(?=\s*\n\s*('.implode('|', $operators).'))'; $this->registerPattern('/'.$pattern.$after.'/', $callback); } /** * Strip whitespace. * * We won't strip *all* whitespace, but as much as possible. The thing that * we'll preserve are newlines we're unsure about. * JavaScript doesn't require statements to be terminated with a semicolon. * It will automatically fix missing semicolons with ASI (automatic semi- * colon insertion) at the end of line causing errors (without semicolon.) * * Because it's sometimes hard to tell if a newline is part of a statement * that should be terminated or not, we'll just leave some of them alone. * * @param string $content The content to strip the whitespace for * * @return string */ protected function stripWhitespace($content) { // uniform line endings, make them all line feed $content = str_replace(array("\r\n", "\r"), "\n", $content); // collapse all non-line feed whitespace into a single space $content = preg_replace('/[^\S\n]+/', ' ', $content); // strip leading & trailing whitespace $content = str_replace(array(" \n", "\n "), "\n", $content); // collapse consecutive line feeds into just 1 $content = preg_replace('/\n+/', "\n", $content); $operatorsBefore = $this->getOperatorsForRegex($this->operatorsBefore, '/'); $operatorsAfter = $this->getOperatorsForRegex($this->operatorsAfter, '/'); $operators = $this->getOperatorsForRegex($this->operators, '/'); $keywordsBefore = $this->getKeywordsForRegex($this->keywordsBefore, '/'); $keywordsAfter = $this->getKeywordsForRegex($this->keywordsAfter, '/'); // strip whitespace that ends in (or next line begin with) an operator // that allows statements to be broken up over multiple lines unset($operatorsBefore['+'], $operatorsBefore['-'], $operatorsAfter['+'], $operatorsAfter['-']); $content = preg_replace( array( '/('.implode('|', $operatorsBefore).')\s+/', '/\s+('.implode('|', $operatorsAfter).')/', ), '\\1', $content ); // make sure + and - can't be mistaken for, or joined into ++ and -- $content = preg_replace( array( '/(?<![\+\-])\s*([\+\-])(?![\+\-])/', '/(?<![\+\-])([\+\-])\s*(?![\+\-])/', ), '\\1', $content ); // collapse whitespace around reserved words into single space $content = preg_replace('/(^|[;\}\s])\K('.implode('|', $keywordsBefore).')\s+/', '\\2 ', $content); $content = preg_replace('/\s+('.implode('|', $keywordsAfter).')(?=([;\{\s]|$))/', ' \\1', $content); /* * We didn't strip whitespace after a couple of operators because they * could be used in different contexts and we can't be sure it's ok to * strip the newlines. However, we can safely strip any non-line feed * whitespace that follows them. */ $operatorsDiffBefore = array_diff($operators, $operatorsBefore); $operatorsDiffAfter = array_diff($operators, $operatorsAfter); $content = preg_replace('/('.implode('|', $operatorsDiffBefore).')[^\S\n]+/', '\\1', $content); $content = preg_replace('/[^\S\n]+('.implode('|', $operatorsDiffAfter).')/', '\\1', $content); /* * Whitespace after `return` can be omitted in a few occasions * (such as when followed by a string or regex) * Same for whitespace in between `)` and `{`, or between `{` and some * keywords. */ $content = preg_replace('/\breturn\s+(["\'\/\+\-])/', 'return$1', $content); $content = preg_replace('/\)\s+\{/', '){', $content); $content = preg_replace('/}\n(else|catch|finally)\b/', '}$1', $content); /* * Get rid of double semicolons, except where they can be used like: * "for(v=1,_=b;;)", "for(v=1;;v++)" or "for(;;ja||(ja=true))". * I'll safeguard these double semicolons inside for-loops by * temporarily replacing them with an invalid condition: they won't have * a double semicolon and will be easy to spot to restore afterwards. */ $content = preg_replace('/\bfor\(([^;]*);;([^;]*)\)/', 'for(\\1;-;\\2)', $content); $content = preg_replace('/;+/', ';', $content); $content = preg_replace('/\bfor\(([^;]*);-;([^;]*)\)/', 'for(\\1;;\\2)', $content); /* * Next, we'll be removing all semicolons where ASI kicks in. * for-loops however, can have an empty body (ending in only a * semicolon), like: `for(i=1;i<3;i++);`, of `for(i in list);` * Here, nothing happens during the loop; it's just used to keep * increasing `i`. With that ; omitted, the next line would be expected * to be the for-loop's body... Same goes for while loops. * I'm going to double that semicolon (if any) so after the next line, * which strips semicolons here & there, we're still left with this one. */ $content = preg_replace('/(for\([^;\{]*;[^;\{]*;[^;\{]*\));(\}|$)/s', '\\1;;\\2', $content); $content = preg_replace('/(for\([^;\{]+\s+in\s+[^;\{]+\));(\}|$)/s', '\\1;;\\2', $content); /* * Below will also keep `;` after a `do{}while();` along with `while();` * While these could be stripped after do-while, detecting this * distinction is cumbersome, so I'll play it safe and make sure `;` * after any kind of `while` is kept. */ $content = preg_replace('/(while\([^;\{]+\));(\}|$)/s', '\\1;;\\2', $content); /* * We also can't strip empty else-statements. Even though they're * useless and probably shouldn't be in the code in the first place, we * shouldn't be stripping the `;` that follows it as it breaks the code. * We can just remove those useless else-statements completely. * * @see https://github.com/matthiasmullie/minify/issues/91 */ $content = preg_replace('/else;/s', '', $content); /* * We also don't really want to terminate statements followed by closing * curly braces (which we've ignored completely up until now) or end-of- * script: ASI will kick in here & we're all about minifying. * Semicolons at beginning of the file don't make any sense either. */ $content = preg_replace('/;(\}|$)/s', '\\1', $content); $content = ltrim($content, ';'); // get rid of remaining whitespace af beginning/end return trim($content); } /** * We'll strip whitespace around certain operators with regular expressions. * This will prepare the given array by escaping all characters. * * @param string[] $operators * @param string $delimiter * * @return string[] */ protected function getOperatorsForRegex(array $operators, $delimiter = '/') { // escape operators for use in regex $delimiters = array_fill(0, count($operators), $delimiter); $escaped = array_map('preg_quote', $operators, $delimiters); $operators = array_combine($operators, $escaped); // ignore + & - for now, they'll get special treatment unset($operators['+'], $operators['-']); // dot can not just immediately follow a number; it can be confused for // decimal point, or calling a method on it, e.g. 42 .toString() $operators['.'] = '(?<![0-9]\s)\.'; // don't confuse = with other assignment shortcuts (e.g. +=) $chars = preg_quote('+-*\=<>%&|', $delimiter); $operators['='] = '(?<!['.$chars.'])\='; return $operators; } /** * We'll strip whitespace around certain keywords with regular expressions. * This will prepare the given array by escaping all characters. * * @param string[] $keywords * @param string $delimiter * * @return string[] */ protected function getKeywordsForRegex(array $keywords, $delimiter = '/') { // escape keywords for use in regex $delimiter = array_fill(0, count($keywords), $delimiter); $escaped = array_map('preg_quote', $keywords, $delimiter); // add word boundaries array_walk($keywords, function ($value) { return '\b'.$value.'\b'; }); $keywords = array_combine($keywords, $escaped); return $keywords; } /** * Replaces all occurrences of array['key'] by array.key. * * @param string $content * * @return string */ protected function propertyNotation($content) { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $keywords = $this->keywordsReserved; $callback = function ($match) use ($minifier, $keywords) { $property = trim($minifier->extracted[$match[1]], '\'"'); /* * Check if the property is a reserved keyword. In this context (as * property of an object literal/array) it shouldn't matter, but IE8 * freaks out with "Expected identifier". */ if (in_array($property, $keywords)) { return $match[0]; } /* * See if the property is in a variable-like format (e.g. * array['key-here'] can't be replaced by array.key-here since '-' * is not a valid character there. */ if (!preg_match('/^'.$minifier::REGEX_VARIABLE.'$/u', $property)) { return $match[0]; } return '.'.$property; }; /* * Figure out if previous character is a variable name (of the array * we want to use property notation on) - this is to make sure * standalone ['value'] arrays aren't confused for keys-of-an-array. * We can (and only have to) check the last character, because PHP's * regex implementation doesn't allow unfixed-length look-behind * assertions. */ preg_match('/(\[[^\]]+\])[^\]]*$/', static::REGEX_VARIABLE, $previousChar); $previousChar = $previousChar[1]; /* * Make sure word preceding the ['value'] is not a keyword, e.g. * return['x']. Because -again- PHP's regex implementation doesn't allow * unfixed-length look-behind assertions, I'm just going to do a lot of * separate look-behind assertions, one for each keyword. */ $keywords = $this->getKeywordsForRegex($keywords); $keywords = '(?<!'.implode(')(?<!', $keywords).')'; return preg_replace_callback('/(?<='.$previousChar.'|\])'.$keywords.'\[\s*(([\'"])[0-9]+\\2)\s*\]/u', $callback, $content); } /** * Replaces true & false by !0 and !1. * * @param string $content * * @return string */ protected function shortenBools($content) { /* * 'true' or 'false' could be used as property names (which may be * followed by whitespace) - we must not replace those! * Since PHP doesn't allow variable-length (to account for the * whitespace) lookbehind assertions, I need to capture the leading * character and check if it's a `.` */ $callback = function ($match) { if (trim($match[1]) === '.') { return $match[0]; } return $match[1].($match[2] === 'true' ? '!0' : '!1'); }; $content = preg_replace_callback('/(^|.\s*)\b(true|false)\b(?!:)/', $callback, $content); // for(;;) is exactly the same as while(true), but shorter :) $content = preg_replace('/\bwhile\(!0\){/', 'for(;;){', $content); // now make sure we didn't turn any do ... while(true) into do ... for(;;) preg_match_all('/\bdo\b/', $content, $dos, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); // go backward to make sure positional offsets aren't altered when $content changes $dos = array_reverse($dos); foreach ($dos as $do) { $offsetDo = $do[0][1]; // find all `while` (now `for`) following `do`: one of those must be // associated with the `do` and be turned back into `while` preg_match_all('/\bfor\(;;\)/', $content, $whiles, PREG_OFFSET_CAPTURE | PREG_SET_ORDER, $offsetDo); foreach ($whiles as $while) { $offsetWhile = $while[0][1]; $open = substr_count($content, '{', $offsetDo, $offsetWhile - $offsetDo); $close = substr_count($content, '}', $offsetDo, $offsetWhile - $offsetDo); if ($open === $close) { // only restore `while` if amount of `{` and `}` are the same; // otherwise, that `for` isn't associated with this `do` $content = substr_replace($content, 'while(!0)', $offsetWhile, strlen('for(;;)')); break; } } } return $content; } } Dependencies/Minify/data/js/keywords_after.txt 0000644 00000000071 15174677547 0015530 0 ustar 00 in public extends private protected implements instanceof Dependencies/Minify/data/js/operators.txt 0000644 00000000170 15174677547 0014516 0 ustar 00 + - * / % = += -= *= /= %= <<= >>= >>>= &= ^= |= & | ^ ~ << >> >>> == === != !== > < >= <= && || ! . [ ] ? : , ; ( ) { } Dependencies/Minify/data/js/operators_before.txt 0000644 00000000163 15174677547 0016042 0 ustar 00 + - * / % = += -= *= /= %= <<= >>= >>>= &= ^= |= & | ^ ~ << >> >>> == === != !== > < >= <= && || ! . [ ? : , ; ( { Dependencies/Minify/data/js/operators_after.txt 0000644 00000000162 15174677547 0015700 0 ustar 00 + - * / % = += -= *= /= %= <<= >>= >>>= &= ^= |= & | ^ << >> >>> == === != !== > < >= <= && || . [ ] ? : , ; ( ) } Dependencies/Minify/data/js/keywords_reserved.txt 0000644 00000000636 15174677547 0016255 0 ustar 00 do if in for let new try var case else enum eval null this true void with break catch class const false super throw while yield delete export import public return static switch typeof default extends finally package private continue debugger function arguments interface protected implements instanceof abstract boolean byte char double final float goto int long native short synchronized throws transient volatile Dependencies/Minify/data/js/keywords_before.txt 0000644 00000000247 15174677547 0015676 0 ustar 00 do in let new var case else enum void with class const yield delete export import public static typeof extends package private function protected implements instanceof Dependencies/Minify/CSS.php 0000644 00000054561 15174677547 0011570 0 ustar 00 <?php /** * CSS Minifier * * Please report bugs on https://github.com/matthiasmullie/minify/issues * * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ namespace WP_Rocket\Dependencies\Minify; use WP_Rocket\Dependencies\Minify\Exceptions\FileImportException; use WP_Rocket\Dependencies\PathConverter\ConverterInterface; use WP_Rocket\Dependencies\PathConverter\Converter; /** * CSS minifier * * Please report bugs on https://github.com/matthiasmullie/minify/issues * * @package Minify * @author Matthias Mullie <minify@mullie.eu> * @author Tijs Verkoyen <minify@verkoyen.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ class CSS extends Minify { /** * @var int maximum import size in kB */ protected $maxImportSize = 5; /** * @var string[] valid import extensions */ protected $importExtensions = array( 'gif' => 'data:image/gif', 'png' => 'data:image/png', 'jpe' => 'data:image/jpeg', 'jpg' => 'data:image/jpeg', 'jpeg' => 'data:image/jpeg', 'svg' => 'data:image/svg+xml', 'woff' => 'data:application/x-font-woff', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'xbm' => 'image/x-xbitmap', ); /** * Set the maximum size if files to be imported. * * Files larger than this size (in kB) will not be imported into the CSS. * Importing files into the CSS as data-uri will save you some connections, * but we should only import relatively small decorative images so that our * CSS file doesn't get too bulky. * * @param int $size Size in kB */ public function setMaxImportSize($size) { $this->maxImportSize = $size; } /** * Set the type of extensions to be imported into the CSS (to save network * connections). * Keys of the array should be the file extensions & respective values * should be the data type. * * @param string[] $extensions Array of file extensions */ public function setImportExtensions(array $extensions) { $this->importExtensions = $extensions; } /** * Move any import statements to the top. * * @param string $content Nearly finished CSS content * * @return string */ protected function moveImportsToTop($content) { if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) { // remove from content foreach ($matches[0] as $import) { $content = str_replace($import, '', $content); } // add to top $content = implode(';', $matches[2]).';'.trim($content, ';'); } return $content; } /** * Combine CSS from import statements. * * @import's will be loaded and their content merged into the original file, * to save HTTP requests. * * @param string $source The file to combine imports for * @param string $content The CSS content to combine imports for * @param string[] $parents Parent paths, for circular reference checks * * @return string * * @throws FileImportException */ protected function combineImports($source, $content, $parents) { $importRegexes = array( // @import url(xxx) '/ # import statement @import # whitespace \s+ # open url() url\( # (optional) open path enclosure (?P<quotes>["\']?) # fetch path (?P<path>.+?) # (optional) close path enclosure (?P=quotes) # close url() \) # (optional) trailing whitespace \s* # (optional) media statement(s) (?P<media>[^;]*) # (optional) trailing whitespace \s* # (optional) closing semi-colon ;? /ix', // @import 'xxx' '/ # import statement @import # whitespace \s+ # open path enclosure (?P<quotes>["\']) # fetch path (?P<path>.+?) # close path enclosure (?P=quotes) # (optional) trailing whitespace \s* # (optional) media statement(s) (?P<media>[^;]*) # (optional) trailing whitespace \s* # (optional) closing semi-colon ;? /ix', ); // find all relative imports in css $matches = array(); foreach ($importRegexes as $importRegex) { if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) { $matches = array_merge($matches, $regexMatches); } } $search = array(); $replace = array(); // loop the matches foreach ($matches as $match) { // get the path for the file that will be imported $importPath = dirname($source).'/'.$match['path']; // only replace the import with the content if we can grab the // content of the file if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) { continue; } // check if current file was not imported previously in the same // import chain. if (in_array($importPath, $parents)) { throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.'); } // grab referenced file & minify it (which may include importing // yet other @import statements recursively) $minifier = new self($importPath); $minifier->setMaxImportSize($this->maxImportSize); $minifier->setImportExtensions($this->importExtensions); $importContent = $minifier->execute($source, $parents); // check if this is only valid for certain media if (!empty($match['media'])) { $importContent = '@media '.$match['media'].'{'.$importContent.'}'; } // add to replacement array $search[] = $match[0]; $replace[] = $importContent; } // replace the import statements return str_replace($search, $replace, $content); } /** * Import files into the CSS, base64-ized. * * @url(image.jpg) images will be loaded and their content merged into the * original file, to save HTTP requests. * * @param string $source The file to import files for * @param string $content The CSS content to import files for * * @return string */ protected function importFiles($source, $content) { $regex = '/url\((["\']?)(.+?)\\1\)/i'; if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { $search = array(); $replace = array(); // loop the matches foreach ($matches as $match) { $extension = substr(strrchr($match[2], '.'), 1); if ($extension && !array_key_exists($extension, $this->importExtensions)) { continue; } // get the path for the file that will be imported $path = $match[2]; $path = dirname($source).'/'.$path; // only replace the import with the content if we're able to get // the content of the file, and it's relatively small if ($this->canImportFile($path) && $this->canImportBySize($path)) { // grab content && base64-ize $importContent = $this->load($path); $importContent = base64_encode($importContent); // build replacement $search[] = $match[0]; $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')'; } } // replace the import statements $content = str_replace($search, $replace, $content); } return $content; } /** * Minify the data. * Perform CSS optimizations. * * @param string[optional] $path Path to write the data to * @param string[] $parents Parent paths, for circular reference checks * * @return string The minified data */ public function execute($path = null, $parents = array()) { $content = ''; // loop CSS data (raw data and files) foreach ($this->data as $source => $css) { /* * Let's first take out strings & comments, since we can't just * remove whitespace anywhere. If whitespace occurs inside a string, * we should leave it alone. E.g.: * p { content: "a test" } */ $this->extractStrings(); $this->stripComments(); $this->extractMath(); $this->extractCustomProperties(); $css = $this->replace($css); $css = $this->stripWhitespace($css); $css = $this->shortenColors($css); $css = $this->shortenZeroes($css); $css = $this->shortenFontWeights($css); $css = $this->stripEmptyTags($css); // restore the string we've extracted earlier $css = $this->restoreExtractedData($css); $source = is_int($source) ? '' : $source; $parents = $source ? array_merge($parents, array($source)) : $parents; $css = $this->combineImports($source, $css, $parents); $css = $this->importFiles($source, $css); /* * If we'll save to a new path, we'll have to fix the relative paths * to be relative no longer to the source file, but to the new path. * If we don't write to a file, fall back to same path so no * conversion happens (because we still want it to go through most * of the move code, which also addresses url() & @import syntax...) */ $converter = $this->getPathConverter($source, $path ?: $source); $css = $this->move($converter, $css); // combine css $content .= $css; } $content = $this->moveImportsToTop($content); return $content; } /** * Moving a css file should update all relative urls. * Relative references (e.g. ../images/image.gif) in a certain css file, * will have to be updated when a file is being saved at another location * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper). * * @param ConverterInterface $converter Relative path converter * @param string $content The CSS content to update relative urls for * * @return string */ protected function move(ConverterInterface $converter, $content) { /* * Relative path references will usually be enclosed by url(). @import * is an exception, where url() is not necessary around the path (but is * allowed). * This *could* be 1 regular expression, where both regular expressions * in this array are on different sides of a |. But we're using named * patterns in both regexes, the same name on both regexes. This is only * possible with a (?J) modifier, but that only works after a fairly * recent PCRE version. That's why I'm doing 2 separate regular * expressions & combining the matches after executing of both. */ $relativeRegexes = array( // url(xxx) '/ # open url() url\( \s* # open path enclosure (?P<quotes>["\'])? # fetch path (?P<path>.+?) # close path enclosure (?(quotes)(?P=quotes)) \s* # close url() \) /ix', // @import "xxx" '/ # import statement @import # whitespace \s+ # we don\'t have to check for @import url(), because the # condition above will already catch these # open path enclosure (?P<quotes>["\']) # fetch path (?P<path>.+?) # close path enclosure (?P=quotes) /ix', ); // find all relative urls in css $matches = array(); foreach ($relativeRegexes as $relativeRegex) { if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) { $matches = array_merge($matches, $regexMatches); } } $search = array(); $replace = array(); // loop all urls foreach ($matches as $match) { // determine if it's a url() or an @import match $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url'); $url = $match['path']; if ($this->canImportByPath($url)) { // attempting to interpret GET-params makes no sense, so let's discard them for awhile $params = strrchr($url, '?'); $url = $params ? substr($url, 0, -strlen($params)) : $url; // fix relative url $url = $converter->convert($url); // now that the path has been converted, re-apply GET-params $url .= $params; } /* * Urls with control characters above 0x7e should be quoted. * According to Mozilla's parser, whitespace is only allowed at the * end of unquoted urls. * Urls with `)` (as could happen with data: uris) should also be * quoted to avoid being confused for the url() closing parentheses. * And urls with a # have also been reported to cause issues. * Urls with quotes inside should also remain escaped. * * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378 * @see https://github.com/matthiasmullie/minify/issues/193 */ $url = trim($url); if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) { $url = $match['quotes'] . $url . $match['quotes']; } // build replacement $search[] = $match[0]; if ($type === 'url') { $replace[] = 'url('.$url.')'; } elseif ($type === 'import') { $replace[] = '@import "'.$url.'"'; } } // replace urls return str_replace($search, $replace, $content); } /** * Shorthand hex color codes. * #FF0000 -> #F00. * * @param string $content The CSS content to shorten the hex color codes for * * @return string */ protected function shortenColors($content) { $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content); // remove alpha channel if it's pointless... $content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content); $content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content); $colors = array( // we can shorten some even more by replacing them with their color name '#F0FFFF' => 'azure', '#F5F5DC' => 'beige', '#A52A2A' => 'brown', '#FF7F50' => 'coral', '#FFD700' => 'gold', '#808080' => 'gray', '#008000' => 'green', '#4B0082' => 'indigo', '#FFFFF0' => 'ivory', '#F0E68C' => 'khaki', '#FAF0E6' => 'linen', '#800000' => 'maroon', '#000080' => 'navy', '#808000' => 'olive', '#CD853F' => 'peru', '#FFC0CB' => 'pink', '#DDA0DD' => 'plum', '#800080' => 'purple', '#F00' => 'red', '#FA8072' => 'salmon', '#A0522D' => 'sienna', '#C0C0C0' => 'silver', '#FFFAFA' => 'snow', '#D2B48C' => 'tan', '#FF6347' => 'tomato', '#EE82EE' => 'violet', '#F5DEB3' => 'wheat', // or the other way around 'WHITE' => '#fff', 'BLACK' => '#000', ); return preg_replace_callback( '/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i', function ($match) use ($colors) { return $colors[strtoupper($match[0])]; }, $content ); } /** * Shorten CSS font weights. * * @param string $content The CSS content to shorten the font weights for * * @return string */ protected function shortenFontWeights($content) { $weights = array( 'normal' => 400, 'bold' => 700, ); $callback = function ($match) use ($weights) { return $match[1].$weights[$match[2]]; }; return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content); } /** * Shorthand 0 values to plain 0, instead of e.g. -0em. * * @param string $content The CSS content to shorten the zero values for * * @return string */ protected function shortenZeroes($content) { // we don't want to strip units in `calc()` expressions: // `5px - 0px` is valid, but `5px - 0` is not // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but // `10 * 0` is invalid // we've extracted calcs earlier, so we don't need to worry about this // reusable bits of code throughout these regexes: // before & after are used to make sure we don't match lose unintended // 0-like values (e.g. in #000, or in http://url/1.0) // units can be stripped from 0 values, or used to recognize non 0 // values (where wa may be able to strip a .0 suffix) $before = '(?<=[:(, ])'; $after = '(?=[ ,);}])'; $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)'; // strip units after zeroes (0px -> 0) // NOTE: it should be safe to remove all units for a 0 value, but in // practice, Webkit (especially Safari) seems to stumble over at least // 0%, potentially other units as well. Only stripping 'px' for now. // @see https://github.com/matthiasmullie/minify/issues/60 $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content); // strip 0-digits (.0 -> 0) $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content); // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content); // strip trailing 0: 50.00 -> 50, 50.00px -> 50px $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content); // strip leading 0: 0.1 -> .1, 01.1 -> 1.1 $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content); // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0) $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content); // IE doesn't seem to understand a unitless flex-basis value (correct - // it goes against the spec), so let's add it in again (make it `%`, // which is only 1 char: 0%, 0px, 0 anything, it's all just the same) // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content); $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content); return $content; } /** * Strip empty tags from source code. * * @param string $content * * @return string */ protected function stripEmptyTags($content) { $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content); $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content); return $content; } /** * Strip comments from source code. */ protected function stripComments() { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier) { $count = count($minifier->extracted); $placeholder = '/*'.$count.'*/'; $minifier->extracted[$placeholder] = $match[0]; return $placeholder; }; $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback); $this->registerPattern('/\/\*.*?\*\//s', ''); } /** * Strip whitespace. * * @param string $content The CSS content to strip the whitespace for * * @return string */ protected function stripWhitespace($content) { // remove leading & trailing whitespace $content = preg_replace('/^\s*/m', '', $content); $content = preg_replace('/\s*$/m', '', $content); // replace newlines with a single space $content = preg_replace('/\s+/', ' ', $content); // remove whitespace around meta characters // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content); $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content); $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content); $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content); // whitespace around + and - can only be stripped inside some pseudo- // classes, like `:nth-child(3+2n)` // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or // selectors like `div.weird- p` $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type'); $content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content); // remove semicolon/whitespace followed by closing bracket $content = str_replace(';}', '}', $content); return trim($content); } /** * Replace all occurrences of functions that may contain math, where * whitespace around operators needs to be preserved (e.g. calc, clamp) */ protected function extractMath() { $functions = array('calc', 'clamp', 'min', 'max'); $pattern = '/\b('. implode('|', $functions) .')(\(.+?)(?=$|;|})/m'; // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier, $pattern, &$callback) { $function = $match[1]; $length = strlen($match[2]); $expr = ''; $opened = 0; // the regular expression for extracting math has 1 significant problem: // it can't determine the correct closing parenthesis... // instead, it'll match a larger portion of code to where it's certain that // the calc() musts have ended, and we'll figure out which is the correct // closing parenthesis here, by counting how many have opened for ($i = 0; $i < $length; $i++) { $char = $match[2][$i]; $expr .= $char; if ($char === '(') { $opened++; } elseif ($char === ')' && --$opened === 0) { break; } } // now that we've figured out where the calc() starts and ends, extract it $count = count($minifier->extracted); $placeholder = 'math('.$count.')'; $minifier->extracted[$placeholder] = $function.'('.trim(substr($expr, 1, -1)).')'; // and since we've captured more code than required, we may have some leftover // calc() in here too - go recursive on the remaining but of code to go figure // that out and extract what is needed $rest = ""; $pos = strpos($match[0], $function.$expr); if ($pos !== false) { $rest = substr_replace($match[0], '', $pos, strlen($function.$expr)); } $rest = preg_replace_callback($pattern, $callback, $rest); return $placeholder.$rest; }; $this->registerPattern($pattern, $callback); } /** * Replace custom properties, whose values may be used in scenarios where * we wouldn't want them to be minified (e.g. inside calc) */ protected function extractCustomProperties() { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $this->registerPattern( '/(?<=^|[;}{])\s*(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m', function ($match) use ($minifier) { $placeholder = '--custom-'. count($minifier->extracted) . ':0'; $minifier->extracted[$placeholder] = $match[1] .':'. trim($match[2]); return $placeholder; } ); } /** * Check if file is small enough to be imported. * * @param string $path The path to the file * * @return bool */ protected function canImportBySize($path) { return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024; } /** * Check if file a file can be imported, going by the path. * * @param string $path * * @return bool */ protected function canImportByPath($path) { return preg_match('/^(data:|https?:|\\/)/', $path) === 0; } /** * Return a converter to update relative paths to be relative to the new * destination. * * @param string $source * @param string $target * * @return ConverterInterface */ protected function getPathConverter($source, $target) { return new Converter($source, $target); } } Dependencies/Minify/Minify.php 0000644 00000033076 15174677547 0012371 0 ustar 00 <?php /** * Abstract minifier class * * Please report bugs on https://github.com/matthiasmullie/minify/issues * * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ namespace WP_Rocket\Dependencies\Minify; use WP_Rocket\Dependencies\Minify\Exceptions\IOException; use Psr\Cache\CacheItemInterface; /** * Abstract minifier class. * * Please report bugs on https://github.com/matthiasmullie/minify/issues * * @package Minify * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ abstract class Minify { /** * The data to be minified. * * @var string[] */ protected $data = array(); /** * Array of patterns to match. * * @var string[] */ protected $patterns = array(); /** * This array will hold content of strings and regular expressions that have * been extracted from the JS source code, so we can reliably match "code", * without having to worry about potential "code-like" characters inside. * * @var string[] */ public $extracted = array(); /** * Init the minify class - optionally, code may be passed along already. */ public function __construct(/* $data = null, ... */) { // it's possible to add the source through the constructor as well ;) if (func_num_args()) { call_user_func_array(array($this, 'add'), func_get_args()); } } /** * Add a file or straight-up code to be minified. * * @param string|string[] $data * * @return static */ public function add($data /* $data = null, ... */) { // bogus "usage" of parameter $data: scrutinizer warns this variable is // not used (we're using func_get_args instead to support overloading), // but it still needs to be defined because it makes no sense to have // this function without argument :) $args = array($data) + func_get_args(); // this method can be overloaded foreach ($args as $data) { if (is_array($data)) { call_user_func_array(array($this, 'add'), $data); continue; } // redefine var $data = (string) $data; // load data $value = $this->load($data); $key = ($data != $value) ? $data : count($this->data); // replace CR linefeeds etc. // @see https://github.com/matthiasmullie/minify/pull/139 $value = str_replace(array("\r\n", "\r"), "\n", $value); // store data $this->data[$key] = $value; } return $this; } /** * Add a file to be minified. * * @param string|string[] $data * * @return static * * @throws IOException */ public function addFile($data /* $data = null, ... */) { // bogus "usage" of parameter $data: scrutinizer warns this variable is // not used (we're using func_get_args instead to support overloading), // but it still needs to be defined because it makes no sense to have // this function without argument :) $args = array($data) + func_get_args(); // this method can be overloaded foreach ($args as $path) { if (is_array($path)) { call_user_func_array(array($this, 'addFile'), $path); continue; } // redefine var $path = (string) $path; // check if we can read the file if (!$this->canImportFile($path)) { throw new IOException('The file "'.$path.'" could not be opened for reading. Check if PHP has enough permissions.'); } $this->add($path); } return $this; } /** * Minify the data & (optionally) saves it to a file. * * @param string[optional] $path Path to write the data to * * @return string The minified data */ public function minify($path = null) { $content = $this->execute($path); // save to path if ($path !== null) { $this->save($content, $path); } return $content; } /** * Minify & gzip the data & (optionally) saves it to a file. * * @param string[optional] $path Path to write the data to * @param int[optional] $level Compression level, from 0 to 9 * * @return string The minified & gzipped data */ public function gzip($path = null, $level = 9) { $content = $this->execute($path); $content = gzencode($content, $level, FORCE_GZIP); // save to path if ($path !== null) { $this->save($content, $path); } return $content; } /** * Minify the data & write it to a CacheItemInterface object. * * @param CacheItemInterface $item Cache item to write the data to * * @return CacheItemInterface Cache item with the minifier data */ public function cache(CacheItemInterface $item) { $content = $this->execute(); $item->set($content); return $item; } /** * Minify the data. * * @param string[optional] $path Path to write the data to * * @return string The minified data */ abstract public function execute($path = null); /** * Load data. * * @param string $data Either a path to a file or the content itself * * @return string */ protected function load($data) { // check if the data is a file if ($this->canImportFile($data)) { $data = file_get_contents($data); // strip BOM, if any if (substr($data, 0, 3) == "\xef\xbb\xbf") { $data = substr($data, 3); } } return $data; } /** * Save to file. * * @param string $content The minified data * @param string $path The path to save the minified data to * * @throws IOException */ protected function save($content, $path) { $handler = $this->openFileForWriting($path); $this->writeToFile($handler, $content); @fclose($handler); } /** * Register a pattern to execute against the source content. * * If $replacement is a string, it must be plain text. Placeholders like $1 or \2 don't work. * If you need that functionality, use a callback instead. * * @param string $pattern PCRE pattern * @param string|callable $replacement Replacement value for matched pattern */ protected function registerPattern($pattern, $replacement = '') { // study the pattern, we'll execute it more than once $pattern .= 'S'; $this->patterns[] = array($pattern, $replacement); } /** * We can't "just" run some regular expressions against JavaScript: it's a * complex language. E.g. having an occurrence of // xyz would be a comment, * unless it's used within a string. Of you could have something that looks * like a 'string', but inside a comment. * The only way to accurately replace these pieces is to traverse the JS one * character at a time and try to find whatever starts first. * * @param string $content The content to replace patterns in * * @return string The (manipulated) content */ protected function replace($content) { $contentLength = strlen($content); $output = ''; $processedOffset = 0; $positions = array_fill(0, count($this->patterns), -1); $matches = array(); while ($processedOffset < $contentLength) { // find first match for all patterns foreach ($this->patterns as $i => $pattern) { list($pattern, $replacement) = $pattern; // we can safely ignore patterns for positions we've unset earlier, // because we know these won't show up anymore if (array_key_exists($i, $positions) == false) { continue; } // no need to re-run matches that are still in the part of the // content that hasn't been processed if ($positions[$i] >= $processedOffset) { continue; } $match = null; if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) { $matches[$i] = $match; // we'll store the match position as well; that way, we // don't have to redo all preg_matches after changing only // the first (we'll still know where those others are) $positions[$i] = $match[0][1]; } else { // if the pattern couldn't be matched, there's no point in // executing it again in later runs on this same content; // ignore this one until we reach end of content unset($matches[$i], $positions[$i]); } } // no more matches to find: everything's been processed, break out if (!$matches) { // output the remaining content $output .= substr($content, $processedOffset); break; } // see which of the patterns actually found the first thing (we'll // only want to execute that one, since we're unsure if what the // other found was not inside what the first found) $matchOffset = min($positions); $firstPattern = array_search($matchOffset, $positions); $match = $matches[$firstPattern]; // execute the pattern that matches earliest in the content string list(, $replacement) = $this->patterns[$firstPattern]; // add the part of the input between $processedOffset and the first match; // that content wasn't matched by anything $output .= substr($content, $processedOffset, $matchOffset - $processedOffset); // add the replacement for the match $output .= $this->executeReplacement($replacement, $match); // advance $processedOffset past the match $processedOffset = $matchOffset + strlen($match[0][0]); } return $output; } /** * If $replacement is a callback, execute it, passing in the match data. * If it's a string, just pass it through. * * @param string|callable $replacement Replacement value * @param array $match Match data, in PREG_OFFSET_CAPTURE form * * @return string */ protected function executeReplacement($replacement, $match) { if (!is_callable($replacement)) { return $replacement; } // convert $match from the PREG_OFFSET_CAPTURE form to the form the callback expects foreach ($match as &$matchItem) { $matchItem = $matchItem[0]; } return $replacement($match); } /** * Strings are a pattern we need to match, in order to ignore potential * code-like content inside them, but we just want all of the string * content to remain untouched. * * This method will replace all string content with simple STRING# * placeholder text, so we've rid all strings from characters that may be * misinterpreted. Original string content will be saved in $this->extracted * and after doing all other minifying, we can restore the original content * via restoreStrings(). * * @param string[optional] $chars * @param string[optional] $placeholderPrefix */ protected function extractStrings($chars = '\'"', $placeholderPrefix = '') { // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier, $placeholderPrefix) { // check the second index here, because the first always contains a quote if ($match[2] === '') { /* * Empty strings need no placeholder; they can't be confused for * anything else anyway. * But we still needed to match them, for the extraction routine * to skip over this particular string. */ return $match[0]; } $count = count($minifier->extracted); $placeholder = $match[1].$placeholderPrefix.$count.$match[1]; $minifier->extracted[$placeholder] = $match[1].$match[2].$match[1]; return $placeholder; }; /* * The \\ messiness explained: * * Don't count ' or " as end-of-string if it's escaped (has backslash * in front of it) * * Unless... that backslash itself is escaped (another leading slash), * in which case it's no longer escaping the ' or " * * So there can be either no backslash, or an even number * * multiply all of that times 4, to account for the escaping that has * to be done to pass the backslash into the PHP string without it being * considered as escape-char (times 2) and to get it in the regex, * escaped (times 2) */ $this->registerPattern('/(['.$chars.'])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback); } /** * This method will restore all extracted data (strings, regexes) that were * replaced with placeholder text in extract*(). The original content was * saved in $this->extracted. * * @param string $content * * @return string */ protected function restoreExtractedData($content) { if (!$this->extracted) { // nothing was extracted, nothing to restore return $content; } $content = strtr($content, $this->extracted); $this->extracted = array(); return $content; } /** * Check if the path is a regular file and can be read. * * @param string $path * * @return bool */ protected function canImportFile($path) { $parsed = parse_url($path); if ( // file is elsewhere isset($parsed['host']) || // file responds to queries (may change, or need to bypass cache) isset($parsed['query']) ) { return false; } return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path); } /** * Attempts to open file specified by $path for writing. * * @param string $path The path to the file * * @return resource Specifier for the target file * * @throws IOException */ protected function openFileForWriting($path) { if ($path === '' || ($handler = @fopen($path, 'w')) === false) { throw new IOException('The file "'.$path.'" could not be opened for writing. Check if PHP has enough permissions.'); } return $handler; } /** * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions. * * @param resource $handler The resource to write to * @param string $content The content to write * @param string $path The path to the file (for exception printing only) * * @throws IOException */ protected function writeToFile($handler, $content, $path = '') { if ( !is_resource($handler) || ($result = @fwrite($handler, $content)) === false || ($result < strlen($content)) ) { throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.'); } } } Dependencies/Minify/Exception.php 0000644 00000000574 15174677547 0013071 0 ustar 00 <?php /** * Base Exception * * @deprecated Use Exceptions\BasicException instead * * @author Matthias Mullie <minify@mullie.eu> */ namespace WP_Rocket\Dependencies\Minify; /** * Base Exception Class * @deprecated Use Exceptions\BasicException instead * * @package Minify * @author Matthias Mullie <minify@mullie.eu> */ abstract class Exception extends \Exception { } Dependencies/Minify/Exceptions/IOException.php 0000644 00000000710 15174677547 0015432 0 ustar 00 <?php /** * IO Exception * * Please report bugs on https://github.com/matthiasmullie/minify/issues * * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ namespace WP_Rocket\Dependencies\Minify\Exceptions; /** * IO Exception Class * * @package Minify\Exception * @author Matthias Mullie <minify@mullie.eu> */ class IOException extends BasicException { } Dependencies/Minify/Exceptions/BasicException.php 0000644 00000001003 15174677547 0016140 0 ustar 00 <?php /** * Basic exception * * Please report bugs on https://github.com/matthiasmullie/minify/issues * * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ namespace WP_Rocket\Dependencies\Minify\Exceptions; use WP_Rocket\Dependencies\Minify\Exception; /** * Basic Exception Class * * @package Minify\Exception * @author Matthias Mullie <minify@mullie.eu> */ abstract class BasicException extends Exception { } Dependencies/Minify/Exceptions/FileImportException.php 0000644 00000000742 15174677547 0017202 0 ustar 00 <?php /** * File Import Exception * * Please report bugs on https://github.com/matthiasmullie/minify/issues * * @author Matthias Mullie <minify@mullie.eu> * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved * @license MIT License */ namespace WP_Rocket\Dependencies\Minify\Exceptions; /** * File Import Exception Class * * @package Minify\Exception * @author Matthias Mullie <minify@mullie.eu> */ class FileImportException extends BasicException { } Dependencies/WPMedia/PluginFamily/Model/PluginFamily.php 0000644 00000013273 15174677547 0017226 0 ustar 00 <?php namespace WP_Rocket\Dependencies\WPMedia\PluginFamily\Model; /** * Handles the data to be passed to the frontend. */ class PluginFamily { /** * An array of referrers for wp rocket. * * @var array */ protected $wp_rocket_referrer = [ 'imagify' => 'imagify', 'seo-by-rank-math' => '', 'backwpup' => '', 'uk-cookie-consent' => '', ]; /** * Get filtered plugins. * * @param string $main_plugin Main plugin installed. * * @return array */ public function get_filtered_plugins( string $main_plugin ): array { $plugins = require_once 'wp_media_plugins.php'; return $this->filter_plugins_by_activation( $plugins, $main_plugin ); } /** * Filter plugins family data by activation status and returns both categorized and uncategorized format. * * @param array $plugins Array of family plugins. * @param string $main_plugin Main plugin installed. * * @return array */ public function filter_plugins_by_activation( array $plugins, string $main_plugin ): array { if ( empty( $plugins ) ) { return []; } list( $active_plugins, $inactive_plugins ) = [ [], [] ]; foreach ( $plugins as $cat => $cat_data ) { foreach ( $cat_data['plugins'] as $plugin => $data ) { $plugin_path = $plugin . '.php'; $plugin_slug = dirname( $plugin ); $main_plugin_slug = dirname( $main_plugin ); $wpr_referrer = 'wp-rocket' !== $main_plugin_slug ? $this->wp_rocket_referrer[ $main_plugin_slug ] : ''; /** * Check for activated plugins and pop them out of the array * to re-add them back using array_merge to be displayed after * plugins that are not installed or not activated. */ if ( is_plugin_active( $plugin_path ) && $main_plugin . '.php' !== $plugin_path ) { // set cta data of active plugins. $plugins[ $cat ]['plugins'][ $plugin ]['cta'] = [ 'text' => __( 'Activated', 'rocket' ), 'url' => '#', ]; // Send active plugin to new array. $active_plugins[ $plugin ] = $plugins[ $cat ]['plugins'][ $plugin ]; // Remove active plugin from current category. $active_plugin = $plugins[ $cat ]['plugins'][ $plugin ]; unset( $plugins[ $cat ]['plugins'][ $plugin ] ); // Send active plugin to the end of array in current category. $plugins[ $cat ]['plugins'][ $plugin ] = $active_plugin; // Remove category with active plugin from current array. $active_cat = $plugins[ $cat ]; unset( $plugins[ $cat ] ); // Send category with active plugins to the end of array. $plugins[ $cat ] = $active_cat; continue; } $install_activate_url = admin_url( 'admin-post.php' ); $args = [ 'action' => 'plugin_family_install_' . $plugin_slug, '_wpnonce' => wp_create_nonce( 'plugin_family_install_' . $plugin_slug ), 'plugin_to_install' => rawurlencode( $plugin ), ]; if ( 'imagify' === $plugin_slug ) { $args = [ 'action' => 'install_imagify_from_partner_' . $main_plugin_slug, '_wpnonce' => wp_create_nonce( 'install_imagify_from_partner' ), '_wp_http_referer' => rawurlencode( $this->get_current_url() ), ]; } $install_activate_url = add_query_arg( $args, $install_activate_url ); // Set Installation link. $plugins[ $cat ]['plugins'][ $plugin ]['cta'] = [ 'text' => __( 'Install', 'rocket' ), 'url' => $install_activate_url, ]; // Create unique CTA data for WP Rocket. if ( 'wp-rocket/wp-rocket' === $plugin ) { $url = 'https://wp-rocket.me/?utm_source=' . $wpr_referrer . '-coupon&utm_medium=plugin&utm_campaign=' . $wpr_referrer; $plugins[ $cat ]['plugins'][ $plugin ]['cta'] = [ 'text' => __( 'Get it Now', 'rocket' ), 'url' => $url, ]; $plugins[ $cat ]['plugins'][ $plugin ]['link'] = $url; } // Set activation text. if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin_path ) ) { $plugins[ $cat ]['plugins'][ $plugin ]['cta']['text'] = __( 'Activate', 'rocket' ); if ( 'wp-rocket/wp-rocket' === $plugin ) { $plugins[ $cat ]['plugins'][ $plugin ]['cta']['url'] = $install_activate_url; } } // Send inactive plugins to new array. $inactive_plugins[ $plugin ] = $plugins[ $cat ]['plugins'][ $plugin ]; } // Remove main plugin from categorized array. if ( isset( $plugins[ $cat ]['plugins'][ $main_plugin ] ) ) { unset( $plugins[ $cat ]['plugins'][ $main_plugin ] ); } } $uncategorized = array_merge( $inactive_plugins, $active_plugins ); // Remove main plugin from uncategorized array. unset( $uncategorized[ $main_plugin ] ); return [ 'categorized' => $plugins, 'uncategorized' => $uncategorized, ]; } /** * Get the current URL. * Gotten from Imagify_Partner Package. * * @return string */ protected function get_current_url(): string { if ( ! isset( $_SERVER['SERVER_PORT'], $_SERVER['HTTP_HOST'] ) ) { return ''; } $port = (int) wp_unslash( $_SERVER['SERVER_PORT'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $port = 80 !== $port && 443 !== $port ? ( ':' . $port ) : ''; $url = ! empty( $GLOBALS['HTTP_SERVER_VARS']['REQUEST_URI'] ) ? $GLOBALS['HTTP_SERVER_VARS']['REQUEST_URI'] : ( ! empty( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash return 'http' . ( is_ssl() ? 's' : '' ) . '://' . $_SERVER['HTTP_HOST'] . $port . $url; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash } } Dependencies/WPMedia/PluginFamily/Model/wp_media_plugins.php 0000644 00000005130 15174677547 0020145 0 ustar 00 <?php /** * WP Media plugin family data. */ return [ 'optimize_performance' => [ 'title' => __( 'Optimize Performance', 'rocket' ), 'plugins' => [ 'wp-rocket/wp-rocket' => [ 'logo' => [ 'file' => 'logo-wp-rocket.svg', 'width' => '50%', ], 'title' => __( 'Speed Up Your Website, Instantly', 'rocket' ), 'desc' => __( 'WP Rocket is the easiest way to make your WordPress website faster and boost your Google PageSpeed score. Get more traffic, better engagement, and higher conversions effortlessly.', 'rocket' ), 'link' => '', ], 'imagify/imagify' => [ 'logo' => [ 'file' => 'logo-imagify.svg', 'width' => '50%', ], 'title' => __( 'Speed Up Your Website With Lighter Images', 'rocket' ), 'desc' => __( 'Imagify is the easiest WordPress image optimizer. It automatically compresses images, converts them to WebP and AVIF formats, and lets you resize and optimize with just one click!', 'rocket' ), 'link' => 'https://imagify.io/', ], ], ], 'boost_traffic' => [ 'title' => __( 'Boost Traffic', 'rocket' ), 'plugins' => [ 'seo-by-rank-math/rank-math' => [ 'logo' => [ 'file' => 'logo-rank-math.svg', 'width' => '60%', ], 'title' => __( 'The Swiss Army Knife of SEO Tools', 'rocket' ), 'desc' => __( 'Rank Math SEO is the Best WordPress SEO plugin with the features of many SEO and AI SEO tools in a single package to help multiply your SEO traffic.', 'rocket' ), 'link' => 'https://rankmath.com/wordpress/plugin/seo-suite/', ], ], ], 'protect_secure' => [ 'title' => __( 'Protect & Secure', 'rocket' ), 'plugins' => [ 'backwpup/backwpup' => [ 'logo' => [ 'file' => 'logo-backwpup.svg', 'width' => '60%', ], 'title' => __( 'The Easiest Way to Protect Your Website', 'rocket' ), 'desc' => __( 'BackWPup is the most comprehensive and user-friendly backup & restore plugin for WordPress. Easily schedule automatic backups, securely store and restore with just a few clicks!', 'rocket' ), 'link' => 'https://backwpup.com/', ], 'uk-cookie-consent/uk-cookie-consent' => [ 'logo' => [ 'file' => 'logo-termly.svg', 'width' => '50%', ], 'title' => __( 'GDPR/CCPA Cookie Consent Banner', 'rocket' ), 'desc' => __( 'One of the easiest, most comprehensive, and popular cookie consent plugins available. Google Gold Certified Partner to quickly comply with data privacy laws from around the world.', 'rocket' ), 'link' => 'https://termly.io/resources/articles/wordpress-cookies-guide/', ], ], ], ]; Dependencies/WPMedia/PluginFamily/Controller/PluginFamily.php 0000644 00000030350 15174677547 0020304 0 ustar 00 <?php namespace WP_Rocket\Dependencies\WPMedia\PluginFamily\Controller; /** * Handles installation and Activation of plugin family members. */ class PluginFamily implements PluginFamilyInterface { /** * Plugin family version. * * @var string */ private $version = '1.0.6'; /** * Error transient. * * @var string */ protected $error_transient = 'plugin_family_error'; /** * Returns an array of events this subscriber listens to * * @return array */ public static function get_subscribed_events(): array { $events = self::get_post_install_event(); $events['admin_notices'] = 'display_error_notice'; $events['enqueue_block_editor_assets'] = 'enqueue_assets'; $events['wp_ajax_install_imagify'] = 'install_imagify'; $events['wp_ajax_dismiss_promote_imagify'] = 'dismiss_promote_imagify'; $events['admin_enqueue_scripts'] = 'enqueue_admin_assets'; $events['admin_footer'] = 'insert_footer_templates'; return $events; } /** * Set post install event. * * @return array */ public static function get_post_install_event(): array { $allowed_plugin = [ 'uk-cookie-consent', 'backwpup', 'imagify', 'seo-by-rank-math', 'wp-rocket', ]; if ( ! isset( $_GET['action'], $_GET['_wpnonce'], $_GET['plugin_to_install'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return []; } $plugin = str_replace( 'plugin_family_install_', '', sanitize_text_field( wp_unslash( $_GET['action'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! in_array( $plugin, $allowed_plugin, true ) ) { return []; } return [ 'admin_post_plugin_family_install_' . $plugin => 'install_activate', ]; } /** * Process to install and activate plugin. * * @return void */ public function install_activate() { if ( ! $this->is_allowed() ) { wp_die( 'Plugin Installation is not allowed.', '', [ 'back_link' => true ] ); } // Install plugin. $this->install(); // Activate plugin. $result = activate_plugin( $this->get_plugin(), '', is_multisite() ); if ( is_wp_error( $result ) ) { $this->set_error( $result ); } wp_safe_redirect( wp_get_referer() ); exit; } /** * Install plugin. * * @param string $slug Plugin slug if found. * @return void */ private function install( $slug = '' ) { if ( $this->is_installed( $slug ) ) { return; } $upgrader_class = ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; if ( ! defined( 'ABSPATH' ) || ! file_exists( $upgrader_class ) ) { wp_die( 'Plugin Installation failed. class-wp-upgrader.php not found', '', [ 'back_link' => true ] ); } require_once $upgrader_class; // @phpstan-ignore-line $upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() ); $result = $upgrader->install( $this->get_download_url( $slug ) ); if ( is_wp_error( $result ) ) { $this->set_error( $result ); } clearstatcache(); } /** * Check if plugin is installed. * * @param string $slug Plugin slug if found. * @return boolean */ private function is_installed( $slug = '' ): bool { return file_exists( WP_PLUGIN_DIR . '/' . $this->get_plugin( $slug ) ); } /** * Check if installation is allowed. * * @return boolean */ private function is_allowed(): bool { if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'plugin_family_install_' . $this->get_slug() ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated return false; } if ( ! current_user_can( is_multisite() ? 'manage_network_plugins' : 'install_plugins' ) ) { return false; } return true; } /** * Get plugin slug. * * @return string */ private function get_slug(): string { return dirname( rawurldecode( sanitize_text_field( wp_unslash( $_GET['plugin_to_install'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated } /** * Get plugin identifier. * * @param string $slug Plugin slug if found. * @return string */ private function get_plugin( $slug = '' ): string { if ( 'imagify' === $slug ) { return 'imagify/imagify.php'; } if ( empty( $_GET['plugin_to_install'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return ''; } return rawurldecode( sanitize_text_field( wp_unslash( $_GET['plugin_to_install'] ) ) ) . '.php'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated } /** * Get plugin download url. * * @param string $slug Plugin slug if found. * @return string */ private function get_download_url( $slug = '' ): string { $custom_download_url = $this->maybe_get_custom_download_url( $slug ); if ( false !== $custom_download_url ) { return $custom_download_url; } $plugin_install = ABSPATH . 'wp-admin/includes/plugin-install.php'; if ( ! defined( 'ABSPATH' ) || ! file_exists( $plugin_install ) ) { wp_die( 'Plugin Installation failed. plugin-install.php not found', '', [ 'back_link' => true ] ); } require_once $plugin_install; // @phpstan-ignore-line if ( empty( $slug ) ) { $slug = $this->get_slug(); } $data = [ 'slug' => $slug, 'fields' => [ 'download_link' => true, 'short_description' => false, 'sections' => false, 'rating' => false, 'ratings' => false, 'downloaded' => false, 'last_updated' => false, 'added' => false, 'tags' => false, 'homepage' => false, 'donate_link' => false, ], ]; // Get Plugin Infos. $plugin_info = plugins_api( 'plugin_information', $data ); if ( is_wp_error( $plugin_info ) ) { $this->set_error( $plugin_info ); } // Ensure that $plugin_info is an object before accessing the property. if ( ! is_object( $plugin_info ) || ! isset( $plugin_info->download_link ) ) { return ''; } return $plugin_info->download_link; } /** * Maybe display error notice. * * @return void */ public function display_error_notice() { $errors = get_transient( $this->error_transient ); if ( ! $errors ) { return; } if ( ! is_wp_error( $errors ) ) { delete_transient( $this->error_transient ); return; } $errors = $errors->get_error_messages(); if ( ! $errors ) { $errors[] = 'Installation process failed'; } $notice = '<div class="error notice is-dismissible"><p>' . implode( '<br/>', $errors ) . '</p></div>'; echo wp_kses_post( $notice ); // Remove transient after displaying notice. delete_transient( $this->error_transient ); } /** * Store an error message in a transient then redirect. * * @param object $error A WP_Error object. * @return void */ private function set_error( $error ) { set_transient( $this->error_transient, $error, 30 ); wp_safe_redirect( wp_get_referer() ); exit; } /** * Returns a custom download url for plugin if exists. * * @param string $plugin_slug plugin slug. * @return string|bool */ private function maybe_get_custom_download_url( string $plugin_slug ) { $parent_plugin_slug = $this->get_parent_plugin_slug(); $urls = [ 'seo-by-rank-math' => 'https://rankmath.com/downloads/plugin-family/' . $parent_plugin_slug, ]; if ( ! isset( $urls[ $plugin_slug ] ) ) { return false; } return $urls[ $plugin_slug ]; } /** * Get parent plugin slug. * * @return string */ private function get_parent_plugin_slug(): string { $plugin_path = plugin_basename( __FILE__ ); $chunks = explode( '/', $plugin_path ); return $chunks[0]; } /** * Check if imagify is installed or not. * * @return bool */ private function is_imagify_installed(): bool { return file_exists( WP_PLUGIN_DIR . '/imagify/imagify.php' ); } /** * Check if imagify is activated or not. * * @return bool */ private function is_imagify_activated(): bool { return defined( 'IMAGIFY_VERSION' ); } /** * Enqueue block editor assets * * @return void */ public function enqueue_assets() { if ( $this->is_promote_imagify_dismissed() || $this->is_imagify_activated() || wp_script_is( 'plugin-family-script' ) ) { return; } $script_url = plugin_dir_url( __DIR__ ) . 'assets/js/index.js'; wp_enqueue_script( 'plugin-family-script', $script_url, [ 'react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives' ], $this->version, [ 'in_footer' => true, ] ); $this->add_install_imagify_localized_script( 'plugin-family-script' ); } /** * Install Imagify using the ajax request. * * @return void */ public function install_imagify() { check_ajax_referer( 'install-imagify-nonce' ); if ( ! current_user_can( is_multisite() ? 'manage_network_plugins' : 'install_plugins' ) ) { wp_send_json_error( __( 'Not Allowed', 'rocket' ) ); } if ( ! $this->is_imagify_installed() ) { $this->install( 'imagify' ); } $activated = activate_plugin( $this->get_plugin( 'imagify' ), '', is_multisite() ); if ( is_wp_error( $activated ) ) { wp_send_json_error( $activated->get_error_message() ); } $this->set_imagify_partner( 'wp-rocket' ); wp_send_json_success( __( 'Imagify installed! Click here to start using it.', 'rocket' ) ); } /** * Set the imagify plugin partner. * * @param string $plugin Current plugin. * @return void */ private function set_imagify_partner( $plugin ) { update_option( 'imagifyp_id', $plugin, false ); } /** * Check if we can enqueue admin assets or not. * * @param string $page Current page ID if found. * @return bool */ private function can_enqueue_admin_assets( $page = '' ): bool { if ( $this->is_promote_imagify_dismissed() ) { return false; } if ( empty( $page ) ) { return in_array( get_current_screen()->id, [ 'post', 'upload' ], true ); } return in_array( $page, [ 'post.php', 'post-new.php', 'upload.php' ], true ); } /** * Add localized script to be used by scripts. * * @param string $script_id Script ID. * @return void */ private function add_install_imagify_localized_script( $script_id ) { $data = [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'install-imagify-nonce' ), 'plugins_page_url' => admin_url( 'plugins.php' ), ]; wp_add_inline_script( $script_id, 'window.wpmedia_pluginfamily = ' . wp_json_encode( $data ) . ';', 'before' ); } /** * Enqueue Admin assets. * * @param string $page Page ID. * @return void */ public function enqueue_admin_assets( $page ) { if ( ! $this->can_enqueue_admin_assets( $page ) ) { return; } if ( $this->is_imagify_activated() || wp_script_is( 'plugin-family-admin-script' ) ) { return; } $script_url = plugin_dir_url( __DIR__ ) . 'assets/js/admin.js'; wp_enqueue_script( 'plugin-family-admin-script', $script_url, [ 'jquery' ], // jQuery as a dependency. $this->version, [ 'in_footer' => true, ] ); $this->add_install_imagify_localized_script( 'plugin-family-admin-script' ); $style_url = plugin_dir_url( __DIR__ ) . 'assets/css/style.css'; wp_enqueue_style( 'plugin-family-admin-style', $style_url, [], $this->version ); } /** * Insert admin footer JS templates. * * @return void */ public function insert_footer_templates() { if ( ! $this->can_enqueue_admin_assets() ) { return; } include_once __DIR__ . '/../View/promote-imagify-uploader.php'; } /** * Dismiss promote Imagify using the ajax request. * * @return void */ public function dismiss_promote_imagify() { check_ajax_referer( 'install-imagify-nonce' ); if ( ! current_user_can( is_multisite() ? 'manage_network_plugins' : 'install_plugins' ) ) { wp_send_json_error( __( 'Not Allowed', 'rocket' ) ); } update_option( 'plugin_family_dismiss_promote_imagify', true ); wp_send_json_success( __( 'Dismissed.', 'rocket' ) ); } /** * Check if promote imagify message is dismissed. * * @return bool */ private function is_promote_imagify_dismissed() { return ! empty( get_option( 'plugin_family_dismiss_promote_imagify' ) ); } } Dependencies/WPMedia/PluginFamily/Controller/PluginFamilyInterface.php 0000644 00000000507 15174677547 0022126 0 ustar 00 <?php namespace WP_Rocket\Dependencies\WPMedia\PluginFamily\Controller; interface PluginFamilyInterface { /** * Process to install and activate plugin. * * @return void */ public function install_activate(); /** * Maybe display error notice. * * @return void */ public function display_error_notice(); } Dependencies/WPMedia/PluginFamily/View/promote-imagify-uploader.php 0000644 00000001462 15174677547 0021416 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; ?> <script type="text/template" id="pluginfamily_promote_imagify_uploader_template"> <div class="pluginfamily-promote-imagify"> <button type="button" class="pluginfamily-promote-imagify-dismiss"> <svg class="pluginfamily-promote-imagify-dismiss-icon" viewBox="0 0 24 24" fill="currentColor"> <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> </svg> </button> <p> <?php printf( // translators: %1$is = Plugin Name. esc_html__( '%1$s recommends you to optimize your images for even better website performance.', 'rocket' ), 'WP Rocket' ); ?> </p> <button id="pluginfamily_install_imagify"><?php esc_html_e( 'Install Imagify Plugin', 'rocket' ); ?></button> </div> </script> Dependencies/WPMedia/PluginFamily/assets/css/style.css 0000644 00000001333 15174677547 0017013 0 ustar 00 .pluginfamily-promote-imagify{ padding: 20px 32px 20px 60px; border: 1px solid #007CBA; border-radius: 6px; margin: 20px 0; background: url("../imgs/plus.png") no-repeat 20px 20px; position: relative; } .pluginfamily-promote-imagify p{ margin: 0 0 10px; } .pluginfamily-promote-imagify button{ font-weight: bold; color: #000; border: none; background: transparent; cursor: pointer; padding: 0; text-align: left; } .pluginfamily-promote-imagify-dismiss{ width: 16px; height: 16px; position: absolute; right: 10px; top: 10px; } .pluginfamily-promote-imagify-dismiss-icon{ width: 100%; height: 100%; object-fit: contain; display: block; } Dependencies/WPMedia/PluginFamily/assets/imgs/plus.png 0000644 00000000724 15174677547 0017004 0 ustar 00 �PNG IHDR �w=� sRGB ��� �IDATH���=kTQ���.�`#�N�(�`,�X�A� I0�������X��~1Z�!~�Hj!؈�w��^ws�F�p8s���7�ƭ�o��.�9���'�$�y\��B��l�x��dw���&��&�2�o��� ��/�vy�Z����K� �J�~��.7X���>�,Fj{�%F_�XMd�:Z��Es�%�9�؊V�FĜ��N%��rZQ�F����-��s����pr@V�b����5̷c�aGƿ��WѸD<�$��e�]x�w�����5|(3-[�?6�Ǻ��b=�nt�j�l<��DfY��Ƞ��3}�,G��뿣j�`�-��8\�7�{v�% e&<<�J�9̆~�{�*��dZ�D�x�l�w%�� ��?�n� IEND�B`� Dependencies/WPMedia/PluginFamily/assets/js/admin.js 0000644 00000003125 15174677547 0016414 0 ustar 00 jQuery(document).ready(function(a){let e=!1;new MutationObserver(function(i){a(".attachment-info .details").each(function(){const i=a(this);if(0===i.find(".pluginfamily-promote-imagify").length&&!e){const e=a("#pluginfamily_promote_imagify_uploader_template").html();a(e).appendTo(i)}})}).observe(document.body,{childList:!0,subtree:!0}),a(document).on("click","#pluginfamily_install_imagify",async e=>{e.preventDefault();const i=a("#pluginfamily_install_imagify");i.fadeTo("slow",.5);try{const e=await fetch(wpmedia_pluginfamily.ajax_url,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:new URLSearchParams({action:"install_imagify",_ajax_nonce:wpmedia_pluginfamily.nonce}).toString()}),n=await e.json();n.success&&(i.text(n.data).attr("id",""),window.open(wpmedia_pluginfamily.plugins_page_url,"_blank")||0===a("#pluginfamily_open_plugins_fallback").length&&a("<a>").attr({href:wpmedia_pluginfamily.plugins_page_url,target:"_blank",id:"pluginfamily_open_plugins_fallback"}).text(n.data).insertAfter(i),i.remove())}catch(a){console.error("AJAX error: ",a)}i.fadeTo("slow",1)}),a(document).on("click",".pluginfamily-promote-imagify-dismiss-icon",async i=>{i.preventDefault();const n=a(".pluginfamily-promote-imagify");n.fadeTo("slow",.5);try{await fetch(wpmedia_pluginfamily.ajax_url,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:new URLSearchParams({action:"dismiss_promote_imagify",_ajax_nonce:wpmedia_pluginfamily.nonce}).toString()})}catch(a){console.error("AJAX error: ",a)}n.fadeTo("slow",1,()=>{n.remove(),e=!0})})}); Dependencies/WPMedia/PluginFamily/assets/js/index.js 0000644 00000005461 15174677547 0016440 0 ustar 00 (()=>{"use strict";const e=window.wp.hooks,i=window.wp.compose,n=window.wp.element,o=window.wp.blockEditor,t=window.wp.components,a=window.wp.i18n,s=window.wp.primitives,r=window.ReactJSXRuntime,l=(0,r.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 24 24",children:(0,r.jsx)(s.Path,{d:"M10 1c-5 0-9 4-9 9s4 9 9 9 9-4 9-9-4-9-9-9zm0 16c-3.9 0-7-3.1-7-7s3.1-7 7-7 7 3.1 7 7-3.1 7-7 7zm1-11H9v3H6v2h3v3h2v-3h3V9h-3V6zM10 1c-5 0-9 4-9 9s4 9 9 9 9-4 9-9-4-9-9-9zm0 16c-3.9 0-7-3.1-7-7s3.1-7 7-7 7 3.1 7 7-3.1 7-7 7zm1-11H9v3H6v2h3v3h2v-3h3V9h-3V6z"})}),c=(0,r.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(s.Path,{d:"M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"})}),m=(0,i.createHigherOrderComponent)(e=>i=>{const[s,m]=(0,n.useState)(!1),[d,w]=(0,n.useState)(!1),[p,g]=(0,n.useState)(!0);return"core/image"===i.name&&p?(0,r.jsxs)(n.Fragment,{children:[(0,r.jsx)(e,{...i}),(0,r.jsxs)(o.InspectorControls,{children:[(0,r.jsxs)("div",{style:{display:"flex",alignItems:"center",fontWeight:"bold",marginBottom:"8px"},children:[(0,r.jsx)(t.Icon,{icon:l,style:{marginRight:8,marginLeft:8}}),(0,r.jsx)("span",{style:{flex:1},children:(0,a.__)("Optimize Your Images","%domain%")}),(0,r.jsx)(t.Button,{onClick:async()=>{try{await fetch(wpmedia_pluginfamily.ajax_url,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:new URLSearchParams({action:"dismiss_promote_imagify",_ajax_nonce:wpmedia_pluginfamily.nonce}).toString()})}catch(e){console.error("AJAX error: ",e)}g(!1)},size:"compact",icon:c,iconPosition:"right"})]}),(0,r.jsxs)(t.PanelBody,{title:"",initialOpen:!0,children:[(0,r.jsx)("p",{style:{marginLeft:16},children:(0,a.__)("Boost your site’s performance by compressing images with Imagify, developed by WP Rocket.","%domain%")}),d?(0,r.jsx)("a",{href:wpmedia_pluginfamily.plugins_page_url,target:"_blank",rel:"noopener noreferrer",style:{marginLeft:16,color:"green",fontWeight:"bold",display:"block"},children:(0,a.__)("Imagify installed! Click here to start using it.","%domain%")}):(0,r.jsx)(t.Button,{style:{marginLeft:16},isSecondary:!0,isBusy:s,disabled:s,onClick:async()=>{m(!0);try{const e=await fetch(wpmedia_pluginfamily.ajax_url,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:new URLSearchParams({action:"install_imagify",_ajax_nonce:wpmedia_pluginfamily.nonce}).toString()});(await e.json()).success&&(w(!0),window.open(wpmedia_pluginfamily.plugins_page_url,"_blank"))}catch(e){console.error("AJAX error: ",e)}m(!1)},children:(0,a.__)("Install Imagify Now","%domain%")})]})]})]}):(0,r.jsx)(e,{...i})},"promoteImagifyButton");(0,e.addFilter)("editor.BlockEdit","wpmedia-plugin-family/promote-imagify",m)})(); Dependencies/RocketLazyload/Iframe.php 0000644 00000014564 15174677547 0014036 0 ustar 00 <?php /** * Handles lazyloading of iframes * * @package WP_Rocket\Dependencies\RocketLazyload */ namespace WP_Rocket\Dependencies\RocketLazyload; /** * A class to provide the methods needed to lazyload iframes in WP Rocket and Lazyload by WP Rocket */ class Iframe { /** * Finds iframes in the HTML provided and call the methods to lazyload them * * @param string $html Original HTML. * @param string $buffer Content to parse. * @param array<bool> $args Array of arguments to use. * * @return string */ public function lazyloadIframes( $html, $buffer, $args = [] ) { $defaults = [ 'youtube' => false, ]; $args = wp_parse_args( $args, $defaults ); if ( ! preg_match_all( '@<iframe(?<atts>\s.+)>.*</iframe>@iUs', $buffer, $iframes, PREG_SET_ORDER ) ) { return $html; } $iframes = array_unique( $iframes, SORT_REGULAR ); foreach ( $iframes as $iframe ) { if ( $this->isIframeExcluded( $iframe ) ) { continue; } // Given the previous regex pattern, $iframe['atts'] starts with a whitespace character. if ( ! preg_match( '@\ssrc\s*=\s*(\'|")(?<src>.*)\1@iUs', $iframe['atts'], $atts ) ) { continue; } $iframe['src'] = trim( $atts['src'] ); if ( '' === $iframe['src'] ) { continue; } $iframe_lazyload = ''; if ( $args['youtube'] ) { $iframe_lazyload = $this->replaceYoutubeThumbnail( $iframe ); } if ( empty( $iframe_lazyload ) ) { $iframe_lazyload = $this->replaceIframe( $iframe ); } $html = str_replace( $iframe[0], $iframe_lazyload, $html ); unset( $iframe_lazyload ); } return $html; } /** * Checks if the provided iframe is excluded from lazyload * * @param array<string> $iframe Array of matched patterns. * @return boolean */ public function isIframeExcluded( $iframe ) { foreach ( $this->getExcludedPatterns() as $excluded_pattern ) { if ( strpos( $iframe[0], $excluded_pattern ) !== false ) { return true; } } return false; } /** * Gets patterns excluded from lazyload for iframes * * @since 2.1.1 * * @return array<string> */ private function getExcludedPatterns() { /** * Filters the patterns excluded from lazyload for iframes * * @since 2.1.1 * * @param array $excluded_patterns Array of excluded patterns. */ return apply_filters( 'rocket_lazyload_iframe_excluded_patterns', [ 'gform_ajax_frame', 'data-no-lazy=', 'recaptcha/api/fallback', 'loading="eager"', 'data-skip-lazy', 'skip-lazy', 'google_ads_iframe_', ] ); } /** * Applies lazyload on the iframe provided * * @param array<string> $iframe Array of matched elements. * * @return string */ private function replaceIframe( $iframe ) { /** * Filter the LazyLoad placeholder on src attribute * * @since 1.0 * * @param string $placeholder placeholder that will be printed. */ $placeholder = apply_filters( 'rocket_lazyload_placeholder', 'about:blank' ); $placeholder_atts = str_replace( $iframe['src'], $placeholder, $iframe['atts'] ); $iframe_lazyload = str_replace( $iframe['atts'], $placeholder_atts . ' data-rocket-lazyload="fitvidscompatible" data-lazy-src="' . esc_url( $iframe['src'] ) . '"', $iframe[0] ); if ( ! preg_match( '@\sloading\s*=\s*(\'|")(?:lazy|auto)\1@i', $iframe_lazyload ) ) { $iframe_lazyload = str_replace( '<iframe', '<iframe loading="lazy"', $iframe_lazyload ); } /** * Filter the LazyLoad HTML output on iframes * * @since 1.0 * * @param string $html Output that will be printed. */ $iframe_lazyload = apply_filters( 'rocket_lazyload_iframe_html', $iframe_lazyload ); $iframe_lazyload .= '<noscript>' . $iframe[0] . '</noscript>'; return $iframe_lazyload; } /** * Replaces the iframe provided by the Youtube thumbnail * * @param array<string> $iframe Array of matched elements. * * @return string */ private function replaceYoutubeThumbnail( $iframe ) { $youtube_id = $this->getYoutubeIDFromURL( $iframe['src'] ); if ( '' === $youtube_id ) { return ''; } $query = wp_parse_url( htmlspecialchars_decode( $iframe['src'] ), PHP_URL_QUERY ); if ( ! is_string( $query ) ) { $query = ''; } $youtube_url = $this->changeYoutubeUrlForYoutuDotBe( $iframe['src'] ); $youtube_url = $this->cleanYoutubeUrl( $iframe['src'] ); preg_match( '@\s*title\s*=\s*(\'|")(?<title>.*)\1@iUs', $iframe['atts'], $atts ); $title = $atts['title'] ?? ''; /** * Filter the LazyLoad HTML output on Youtube iframes * * @since 2.11 * * @param string $html Output that will be printed. */ $youtube_lazyload = apply_filters( 'rocket_lazyload_youtube_html', '<div class="rll-youtube-player" data-src="' . esc_attr( $youtube_url ) . '" data-id="' . esc_attr( $youtube_id ) . '" data-query="' . esc_attr( $query ) . '" data-alt="' . esc_attr( $title ) . '"></div>' ); $youtube_lazyload .= '<noscript>' . $iframe[0] . '</noscript>'; return $youtube_lazyload; } /** * Gets the Youtube ID from the URL provided * * @param string $url URL to search. * * @return string */ public function getYoutubeIDFromURL( $url ) { $pattern = '#^(?:https?:)?(?://)?(?:www\.)?(?:youtu\.be|youtube\.com|youtube-nocookie\.com)/(?:embed/|v/|watch/?\?v=)?([\w-]{11})#iU'; $result = preg_match( $pattern, $url, $matches ); if ( ! $result ) { return ''; } // exclude playlist. if ( 'videoseries' === $matches[1] ) { return ''; } return $matches[1]; } /** * Changes URL youtu.be/ID to youtube.com/embed/ID * * @param string $url URL to replace. * @return string Unchanged URL or modified URL. */ public function changeYoutubeUrlForYoutuDotBe( $url ) { $pattern = '#^(?:https?:)?(?://)?(?:www\.)?(?:youtu\.be)/(?:embed/|v/|watch/?\?v=)?([\w-]{11})#iU'; $result = preg_match( $pattern, $url, $matches ); if ( ! $result ) { return $url; } return 'https://www.youtube.com/embed/' . $matches[1]; } /** * Cleans Youtube URL. Keeps only scheme, host and path. * * @param string $url URL to be cleaned. * @return string Cleaned URL */ public function cleanYoutubeUrl( $url ) { $parsed_url = wp_parse_url( $url, -1 ); $scheme = isset( $parsed_url['scheme'] ) ? $parsed_url['scheme'] . '://' : '//'; $host = isset( $parsed_url['host'] ) ? $parsed_url['host'] : ''; $path = isset( $parsed_url['path'] ) ? $parsed_url['path'] : ''; return $scheme . $host . $path; } } Dependencies/RocketLazyload/Assets.php 0000644 00000031616 15174677547 0014072 0 ustar 00 <?php declare(strict_types=1); /** * Handle the lazyload required assets: inline CSS and JS * * @package WP_Rocket\Dependencies\RocketLazyload */ namespace WP_Rocket\Dependencies\RocketLazyload; /** * Class containing the methods to return or print the assets needed for lazyloading */ class Assets { /** * Inserts the lazyload script in the HTML * * @param array<string> $args Array of arguments to populate the lazyload script tag. * @return void */ public function insertLazyloadScript( $args = [] ) { echo $this->getLazyloadScript( $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Gets the inline lazyload script configuration * * @param array<string, int> $args Array of arguments to populate the lazyload script options. * @return string */ public function getInlineLazyloadScript( $args = [] ) { $defaults = [ 'elements' => [ 'iframe', ], 'threshold' => 300, 'options' => [], ]; $allowed_options = [ 'container' => 1, 'thresholds' => 1, 'data_bg' => 1, 'data_bg_hidpi' => 1, 'data_bg_multi' => 1, 'data_bg_multi_hidpi' => 1, 'data_poster' => 1, 'class_applied' => 1, 'class_error' => 1, 'class_entered' => 1, 'class_exited' => 1, 'cancel_on_exit' => 1, 'unobserve_entered' => 1, 'unobserve_completed' => 1, 'callback_enter' => 1, 'callback_exit' => 1, 'callback_loading' => 1, 'callback_cancel' => 1, 'callback_loaded' => 1, 'callback_error' => 1, 'callback_applied' => 1, 'callback_finish' => 1, 'use_native' => 1, ]; $args = wp_parse_args( $args, $defaults ); $script = ''; $args['options'] = array_intersect_key( $args['options'], $allowed_options ); $script .= 'window.lazyLoadOptions = '; if ( isset( $args['elements']['background_image'] ) ) { $script .= '['; } $script .= '{ elements_selector: "' . esc_attr( implode( ',', $args['elements'] ) ) . '", data_src: "lazy-src", data_srcset: "lazy-srcset", data_sizes: "lazy-sizes", class_loading: "lazyloading", class_loaded: "lazyloaded", threshold: ' . esc_attr( $args['threshold'] ) . ', callback_loaded: function(element) { if ( element.tagName === "IFRAME" && element.dataset.rocketLazyload == "fitvidscompatible" ) { if (element.classList.contains("lazyloaded") ) { if (typeof window.jQuery != "undefined") { if (jQuery.fn.fitVids) { jQuery(element).parent().fitVids(); } } } } }'; if ( ! empty( $args['options'] ) ) { $script .= ',' . PHP_EOL; foreach ( $args['options'] as $option => $value ) { $script .= $option . ': ' . $value . ','; } $script = rtrim( $script, ',' ); } if ( isset( $args['elements']['background_image'] ) ) { $script .= '},{ elements_selector: "' . esc_attr( $args['elements']['background_image'] ) . '", data_src: "lazy-src", data_srcset: "lazy-srcset", data_sizes: "lazy-sizes", class_loading: "lazyloading", class_loaded: "lazyloaded", threshold: ' . esc_attr( $args['threshold'] ) . ', }];'; } else { $script .= '};'; } $script .= ' window.addEventListener(\'LazyLoad::Initialized\', function (e) { var lazyLoadInstance = e.detail.instance; if (window.MutationObserver) { var observer = new MutationObserver(function(mutations) { var image_count = 0; var iframe_count = 0; var rocketlazy_count = 0; mutations.forEach(function(mutation) { for (var i = 0; i < mutation.addedNodes.length; i++) { if (typeof mutation.addedNodes[i].getElementsByTagName !== \'function\') { continue; } if (typeof mutation.addedNodes[i].getElementsByClassName !== \'function\') { continue; } images = mutation.addedNodes[i].getElementsByTagName(\'img\'); is_image = mutation.addedNodes[i].tagName == "IMG"; iframes = mutation.addedNodes[i].getElementsByTagName(\'iframe\'); is_iframe = mutation.addedNodes[i].tagName == "IFRAME"; rocket_lazy = mutation.addedNodes[i].getElementsByClassName(\'rocket-lazyload\'); image_count += images.length; iframe_count += iframes.length; rocketlazy_count += rocket_lazy.length; if(is_image){ image_count += 1; } if(is_iframe){ iframe_count += 1; } } } ); if(image_count > 0 || iframe_count > 0 || rocketlazy_count > 0){ lazyLoadInstance.update(); } } ); var b = document.getElementsByTagName("body")[0]; var config = { childList: true, subtree: true }; observer.observe(b, config); } }, false);'; return $script; } /** * Returns the lazyload inline script * * @param array<string> $args Array of arguments to populate the lazyload script options. * @return string */ public function getLazyloadScript( $args = [] ) { $defaults = [ 'base_url' => '', 'version' => '', ]; $args = wp_parse_args( $args, $defaults ); $min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; /** * Filters the script tag for the lazyload script * * @since 2.2.6 * * @param string $script_tag HTML tag for the lazyload script. */ return apply_filters( 'rocket_lazyload_script_tag', '<script data-no-minify="1" async src="' . $args['base_url'] . $args['version'] . '/lazyload' . $min . '.js"></script>' ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript } /** * Inserts in the HTML the script to replace the Youtube thumbnail by the iframe. * * @param array<string, bool> $args Array of arguments to populate the script options. * @return void */ public function insertYoutubeThumbnailScript( $args = [] ) { echo $this->getYoutubeThumbnailScript( $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Returns the Youtube Thumbnail inline script * * @param array<string, bool> $args Array of arguments to populate the script options. * @return string */ public function getYoutubeThumbnailScript( $args = [] ) { $defaults = [ 'resolution' => 'hqdefault', 'lazy_image' => false, 'native' => true, 'extension' => 'jpg', 'button_aria_label' => 'play Youtube video', ]; $allowed_resolutions = [ 'default' => [ 'width' => 120, 'height' => 90, ], 'mqdefault' => [ 'width' => 320, 'height' => 180, ], 'hqdefault' => [ 'width' => 480, 'height' => 360, ], 'sddefault' => [ 'width' => 640, 'height' => 480, ], 'maxresdefault' => [ 'width' => 1280, 'height' => 720, ], ]; $args['resolution'] = ( isset( $args['resolution'] ) && isset( $allowed_resolutions[ $args['resolution'] ] ) ) ? $args['resolution'] : 'hqdefault'; $args = wp_parse_args( $args, $defaults ); $extension_uri = 'webp' === $args['extension'] ? 'vi_webp' : 'vi'; $image_url = 'https://i.ytimg.com/' . $extension_uri . '/ID/' . $args['resolution'] . '.' . $args['extension']; $image = '<img src="' . $image_url . '" alt="" width="' . $allowed_resolutions[ $args['resolution'] ]['width'] . '" height="' . $allowed_resolutions[ $args['resolution'] ]['height'] . '">'; if ( isset( $args['lazy_image'] ) && $args['lazy_image'] ) { $attributes = 'alt="" width="' . $allowed_resolutions[ $args['resolution'] ]['width'] . '" height="' . $allowed_resolutions[ $args['resolution'] ]['height'] . '"'; $image = '<img data-lazy-src="' . $image_url . '" ' . $attributes . '><noscript><img src="' . $image_url . '" ' . $attributes . '></noscript>'; if ( $args['native'] ) { $image = '<img loading="lazy" src="' . $image_url . '" ' . $attributes . '>'; } } $button_aria_label = $args['button_aria_label']; /** * Filters the patterns excluded from lazyload for youtube thumbnails. * * @param array $excluded_patterns Array of excluded patterns. */ $excluded_patterns = apply_filters( 'rocket_lazyload_exclude_youtube_thumbnail', [] ); if ( ! is_array( $excluded_patterns ) ) { $excluded_patterns = []; } $excluded_patterns = wp_json_encode( $excluded_patterns ); return "<script>function lazyLoadThumb(e,alt,l){var t='{$image}',a='<button class=\"play\" aria-label=\"{$button_aria_label}\"></button>';if(l){t=t.replace('data-lazy-','');t=t.replace('loading=\"lazy\"','');t=t.replace(/<noscript>.*?<\/noscript>/g,'');}t=t.replace('alt=\"\"','alt=\"'+alt+'\"');return t.replace(\"ID\",e)+a}function lazyLoadYoutubeIframe(){var e=document.createElement(\"iframe\"),t=\"ID?autoplay=1\";t+=0===this.parentNode.dataset.query.length?\"\":\"&\"+this.parentNode.dataset.query;e.setAttribute(\"src\",t.replace(\"ID\",this.parentNode.dataset.src)),e.setAttribute(\"frameborder\",\"0\"),e.setAttribute(\"allowfullscreen\",\"1\"),e.setAttribute(\"allow\",\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\"),this.parentNode.parentNode.replaceChild(e,this.parentNode)}document.addEventListener(\"DOMContentLoaded\",function(){var exclusions={$excluded_patterns};var e,t,p,u,l,a=document.getElementsByClassName(\"rll-youtube-player\");for(t=0;t<a.length;t++)(e=document.createElement(\"div\")),(u='{$image_url}'),(u=u.replace('ID',a[t].dataset.id)),(l=exclusions.some(exclusion=>u.includes(exclusion))),e.setAttribute(\"data-id\",a[t].dataset.id),e.setAttribute(\"data-query\",a[t].dataset.query),e.setAttribute(\"data-src\",a[t].dataset.src),(e.innerHTML=lazyLoadThumb(a[t].dataset.id,a[t].dataset.alt,l)),a[t].appendChild(e),(p=e.querySelector(\".play\")),(p.onclick=lazyLoadYoutubeIframe)});</script>"; } /** * Inserts the CSS to style the Youtube thumbnail container * * @param array<string, bool> $args Array of arguments to populate the CSS. * @return void */ public function insertYoutubeThumbnailCSS( $args = [] ) { wp_register_style( 'rocket-lazyload', false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_enqueue_style( 'rocket-lazyload' ); wp_add_inline_style( 'rocket-lazyload', $this->getYoutubeThumbnailCSS( $args ) ); } /** * Returns the CSS for the Youtube Thumbnail * * @param array<string, bool> $args Array of arguments to populate the CSS. * @return string */ public function getYoutubeThumbnailCSS( $args = [] ) { $defaults = [ 'base_url' => '', 'responsive_embeds' => true, ]; $args = wp_parse_args( $args, $defaults ); $css = '.rll-youtube-player{position:relative;padding-bottom:56.23%;height:0;overflow:hidden;max-width:100%;}.rll-youtube-player:focus-within{outline: 2px solid currentColor;outline-offset: 5px;}.rll-youtube-player iframe{position:absolute;top:0;left:0;width:100%;height:100%;z-index:100;background:0 0}.rll-youtube-player img{bottom:0;display:block;left:0;margin:auto;max-width:100%;width:100%;position:absolute;right:0;top:0;border:none;height:auto;-webkit-transition:.4s all;-moz-transition:.4s all;transition:.4s all}.rll-youtube-player img:hover{-webkit-filter:brightness(75%)}.rll-youtube-player .play{height:100%;width:100%;left:0;top:0;position:absolute;background:url(' . $args['base_url'] . 'img/youtube.png) no-repeat center;background-color: transparent !important;cursor:pointer;border:none;}'; if ( $args['responsive_embeds'] ) { $css .= '.wp-embed-responsive .wp-has-aspect-ratio .rll-youtube-player{position:absolute;padding-bottom:0;width:100%;height:100%;top:0;bottom:0;left:0;right:0}'; } return $css; } /** * Inserts the CSS needed when Javascript is not enabled to keep the display correct * * @return void */ public function insertNoJSCSS() { echo $this->getNoJSCSS(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Returns the CSS to correctly display images when JavaScript is disabled * * @return string */ public function getNoJSCSS() { return '<noscript><style id="rocket-lazyload-nojs-css">.rll-youtube-player, [data-lazy-src]{display:none !important;}</style></noscript>'; } } Dependencies/RocketLazyload/Image.php 0000644 00000045137 15174677547 0013655 0 ustar 00 <?php declare(strict_types=1); /** * Handles lazyloading of images * * @package WP_Rocket\Dependencies\RocketLazyload */ namespace WP_Rocket\Dependencies\RocketLazyload; /** * A class to provide the methods needed to lazyload images in WP Rocket and Lazyload by WP Rocket */ class Image { /** * Finds the images to be lazyloaded and call the callback method to replace them. * * @param string $html Original HTML. * @param string $buffer Content to parse. * @param bool $use_native Use native lazyload. * @return string */ public function lazyloadImages( $html, $buffer, $use_native = true ) { if ( ! preg_match_all( '#<img(?<atts>\s.+)\s?/?>#iUs', $buffer, $images, PREG_SET_ORDER ) ) { return $html; } $images = array_unique( $images, SORT_REGULAR ); foreach ( $images as $image ) { $original_image = $image; $image = $this->canLazyload( $image ); if ( ! $image ) { $image_no_lazy = preg_replace( '/loading=["\']lazy["\']/i', '', $original_image ); if ( null === $image_no_lazy ) { continue; } $html = str_replace( $original_image, $image_no_lazy, $html ); continue; } $image_lazyload = $this->replaceImage( $image, $use_native ); if ( ! $use_native && $this->noscriptEnabled() ) { $image_lazyload .= $this->noscript( $image[0] ); } $html = str_replace( $image[0], $image_lazyload, $html ); unset( $image_lazyload ); } return $html; } /** * Applies lazyload on background images defined in style attributes * * @param string $html Original HTML. * @param string $buffer Content to parse. * @return string */ public function lazyloadBackgroundImages( $html, $buffer ) { if ( ! preg_match_all( '#<(?<tag>div|figure|section|aside|span|li|a)\s+(?<before>[^>]+[\'"\s])?style\s*=\s*([\'"])(?<styles>.*?)\3(?<after>[^>]*)>#is', $buffer, $elements, PREG_SET_ORDER ) ) { return $html; } foreach ( $elements as $element ) { if ( $this->isExcluded( $element['before'] . $element['after'], $this->getExcludedAttributes() ) ) { continue; } /** * Regex to detect bg images inside CSS. * * @param string $regex regex to detect. * @return string */ $regex = apply_filters( 'rocket_lazyload_bg_images_regex', 'background-image\s*:\s*(?<attr>\s*url\s*\((?<url>[^)]+)\))\s*;?' ); if ( @preg_match( "#$regex#is", '' ) === false ) {// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $regex = 'background-image\s*:\s*(?<attr>\s*url\s*\((?<url>[^)]+)\))\s*;?'; } if ( ! preg_match( "#$regex#is", $element['styles'], $url ) ) { continue; } if ( preg_match( '#data:image#is', $url['url'], $img ) ) { continue; } $url['url'] = esc_url( trim( wp_strip_all_tags( html_entity_decode( $url['url'], ENT_QUOTES | ENT_HTML5 ) ), '\'" ' ) ); if ( $this->isExcluded( $url['url'], $this->getExcludedSrc() ) ) { continue; } $lazy_bg = $this->addLazyCLass( $element[0] ); $lazy_bg = str_replace( $url[0], '', $lazy_bg ); $lazy_bg = str_replace( '<' . $element['tag'], '<' . $element['tag'] . ' data-bg="' . esc_attr( $url['url'] ) . '"', $lazy_bg ); $html = str_replace( $element[0], $lazy_bg, $html ); unset( $lazy_bg ); } return $html; } /** * Add the identifier class to the element * * @param string $element Element to add the class to. * @return string */ private function addLazyClass( $element ) { $class = $this->getClasses( $element ); if ( ! $class ) { $result = preg_replace( '#<(img|div|figure|section|aside|li|span|a)([^>]*)>#is', '<\1 class="rocket-lazyload"\2>', $element ); if ( ! $result ) { return $element; } return $result; } if ( empty( $class['attribute'] ) || empty( $class['classes'] ) ) { return str_replace( $class['attribute'], 'class="rocket-lazyload"', $element ); } $quotes = $this->getAttributeQuotes( $class['classes'] ); $classes = $this->trimOuterQuotes( $class['classes'], $quotes ); if ( empty( $classes ) ) { return str_replace( $class['attribute'], 'class="rocket-lazyload"', $element ); } $classes .= ' rocket-lazyload'; return str_replace( $class['attribute'], 'class=' . $this->normalizeClasses( $classes, $quotes ), $element ); } /** * Gets the attribute value's outer quotation mark, if one exists, i.e. " or '. * * @param string $attribute_value The target attribute's value. * * @return false|string quotation character; else false when no quotation mark. */ private function getAttributeQuotes( $attribute_value ) { $attribute_value = trim( $attribute_value ); $first_char = $attribute_value[0]; if ( '"' === $first_char || "'" === $first_char ) { return $first_char; } return false; } /** * Gets the class attribute and values from the given element, if it exists. * * @param string $element Given HTML element to extract classes from. * * @return false|string[] { * @type string $attribute Class attribute and value, e.g. class="value" * @type string $classes String of class attribute's value(s) * }; else, false when no class attribute exists. */ private function getClasses( $element ) { if ( ! preg_match( '#class\s*=\s*(?<classes>["\'].*?["\']|[^\s]+)#is', $element, $class ) ) { return false; } if ( empty( $class['classes'] ) ) { return false; } return [ 'attribute' => $class[0], 'classes' => $class['classes'], ]; } /** * Removes outer single or double quotations. * * @param string $string String to strip quotes from. * @param false|string $quotes The outer quotes to remove. * * @return string string without quotes. */ private function trimOuterQuotes( $string, $quotes ) { $string = trim( $string ); if ( empty( $string ) ) { return ''; } if ( empty( $quotes ) ) { return $string; } $string = ltrim( $string, $quotes ); $string = rtrim( $string, $quotes ); return trim( $string ); } /** * Normalizes the class attribute values to ensure well-formed. * * @param string $classes String of class attribute value(s). * @param string|bool $quotes Optional. Quotation mark to wrap around the classes. * * @return string well-formed class attributes. */ private function normalizeClasses( $classes, $quotes = '"' ) { $array_of_classes = $this->stringToArray( $classes ); $classes = implode( ' ', $array_of_classes ); if ( false === $quotes ) { $quotes = '"'; } return $quotes . $classes . $quotes; } /** * Converts the given string into an array of strings. * * Note: * 1. Removes empties. * 2. Trims each string. * * @param string $string The target string to convert. * @param non-empty-string $delimiter Optional. Default: ' ' (one space). * * @return array<string> An array of trimmed strings. */ private function stringToArray( $string, $delimiter = ' ' ) { if ( empty( $string ) ) { return []; } $array = explode( $delimiter, $string ); if ( ! $array ) { return []; } $array = array_map( 'trim', $array ); // Remove empties. return array_filter( $array ); } /** * Applies lazyload on picture elements found in the HTML. * * @param string $html Original HTML. * @param string $buffer Content to parse. * @return string */ public function lazyloadPictures( $html, $buffer ) { if ( ! preg_match_all( '#<picture(?:.*)?>(?<sources>.*)</picture>#iUs', $buffer, $pictures, PREG_SET_ORDER ) ) { return $html; } $pictures = array_unique( $pictures, SORT_REGULAR ); $excluded = array_merge( $this->getExcludedAttributes(), $this->getExcludedSrc() ); foreach ( $pictures as $picture ) { if ( $this->isExcluded( $picture[0], $excluded ) ) { if ( ! preg_match( '#<img(?<atts>\s.+)\s?/?>#iUs', $picture[0], $img ) ) { continue; } $img = $this->canLazyload( $img ); if ( ! $img ) { continue; } $nolazy_picture = str_replace( '<img', '<img data-no-lazy=""', $picture[0] ); $html = str_replace( $picture[0], $nolazy_picture, $html ); continue; } if ( preg_match_all( '#<source(?<atts>\s.+)>#iUs', $picture['sources'], $sources, PREG_SET_ORDER ) ) { $lazy_sources = 0; $sources = array_unique( $sources, SORT_REGULAR ); $lazy_picture = $picture[0]; foreach ( $sources as $source ) { $lazyload_srcset = preg_replace( '/([\s"\'])srcset/i', '\1data-lazy-srcset', $source[0] ); if ( ! $lazyload_srcset ) { continue; } $lazy_picture = str_replace( $source[0], $lazyload_srcset, $lazy_picture ); unset( $lazyload_srcset ); $lazy_sources++; } if ( 0 === $lazy_sources ) { continue; } $html = str_replace( $picture[0], $lazy_picture, $html ); } if ( ! preg_match( '#<img(?<atts>\s.+)\s?/?>#iUs', $picture[0], $img ) ) { continue; } $img = $this->canLazyload( $img ); if ( ! $img ) { continue; } $img_lazy = $this->replaceImage( $img, false ); if ( $this->noscriptEnabled() ) { $img_lazy .= $this->noscript( $img[0] ); } $safe_img = str_replace( '/', '\/', preg_quote( $img[0], '#' ) ); $new_html = preg_replace( '#<noscript[^>]*>.*' . $safe_img . '.*<\/noscript>(*SKIP)(*FAIL)|' . $safe_img . '#i', $img_lazy, $html ); if ( ! $new_html ) { continue; } $html = $new_html; unset( $img_lazy ); } return $html; } /** * Checks if the image can be lazyloaded * * @param array<string> $image Array of image data coming from Regex. * * @return false|array<string> */ private function canLazyload( $image ) { if ( $this->isExcluded( $image['atts'], $this->getExcludedAttributes() ) ) { return false; } // Given the previous regex pattern, $image['atts'] starts with a whitespace character. if ( ! preg_match( '@\ssrc\s*=\s*(\'|")(?<src>.*)\1@iUs', $image['atts'], $atts ) ) { return false; } $image['src'] = trim( $atts['src'] ); if ( '' === $image['src'] ) { return false; } if ( $this->isExcluded( $image['src'], $this->getExcludedSrc() ) ) { return false; } return $image; } /** * Checks if the provided string matches with the provided excluded patterns * * @param string $string String to check. * @param array<string> $excluded_values Patterns to match against. * * @return bool */ public function isExcluded( $string, $excluded_values ) { if ( ! is_array( $excluded_values ) ) { $excluded_values = (array) $excluded_values; } $excluded_values = array_filter( $excluded_values ); if ( empty( $excluded_values ) ) { return false; } foreach ( $excluded_values as $excluded_value ) { if ( strpos( $string, $excluded_value ) !== false ) { return true; } } return false; } /** * Returns the list of excluded attributes * * @return array<string> */ public function getExcludedAttributes() { /** * Filters the attributes used to prevent lazylad from being applied * * @since 1.0 * * @param array $excluded_attributes An array of excluded attributes. */ return apply_filters( 'rocket_lazyload_excluded_attributes', [ 'data-src=', 'data-no-lazy=', 'data-lazy-original=', 'data-lazy-src=', 'data-lazysrc=', 'data-lazyload=', 'data-bgposition=', 'data-envira-src=', 'fullurl=', 'lazy-slider-img=', 'data-srcset=', 'class="ls-l', 'class="ls-bg', 'soliloquy-image', 'loading="eager"', 'swatch-img', 'data-height-percentage', 'data-large_image', 'avia-bg-style-fixed', 'data-skip-lazy', 'skip-lazy', 'image-compare__', ] ); } /** * Returns the list of excluded src * * @return array<string> */ public function getExcludedSrc() { /** * Filters the src used to prevent lazylad from being applied * * @since 1.0 * * @param array $excluded_src An array of excluded src. */ return apply_filters( 'rocket_lazyload_excluded_src', [ '/wpcf7_captcha/', 'timthumb.php?src', 'woocommerce/assets/images/placeholder.png', ] ); } /** * Replaces the original image by the lazyload one * * @param array<string> $image Array of matches elements. * @param bool $use_native Use native lazyload. * * @return string */ private function replaceImage( $image, $use_native = true ) { if ( empty( $image ) ) { return ''; } $native_pattern = '@\sloading\s*=\s*(\'|")(?:lazy|auto)\1@i'; $image_lazyload = $image[0]; if ( $use_native ) { if ( preg_match( $native_pattern, $image[0] ) ) { return $image[0]; } $image_lazyload = str_replace( '<img', '<img loading="lazy"', $image_lazyload ); } else { $width = 0; $height = 0; if ( preg_match( '@[\s"\']width\s*=\s*(\'|")(?<width>.*)\1@iUs', $image['atts'], $atts ) ) { $width = absint( $atts['width'] ); } if ( preg_match( '@[\s"\']height\s*=\s*(\'|")(?<height>.*)\1@iUs', $image['atts'], $atts ) ) { $height = absint( $atts['height'] ); } $placeholder_atts = preg_replace( '@\ssrc\s*=\s*(\'|")(?<src>.*)\1@iUs', ' src="' . $this->getPlaceholder( $width, $height ) . '"', $image['atts'] ); $image_lazyload = str_replace( $image['atts'], $placeholder_atts . ' data-lazy-src="' . $image['src'] . '"', $image_lazyload ); if ( preg_match( $native_pattern, $image_lazyload ) ) { $result = preg_replace( $native_pattern, '', $image_lazyload ); if ( is_string( $result ) ) { $image_lazyload = $result; } } } /** * Filter the LazyLoad HTML output * * @since 1.0 * * @param string $html Output that will be printed */ $image_lazyload = apply_filters( 'rocket_lazyload_html', $image_lazyload ); return $image_lazyload; } /** * Checks if the noscript tag is enabled * * @return mixed */ private function noscriptEnabled() { /** * Filter to enable or disable noscript tag * * @param bool $enable_noscript Enable or disable noscript tag. */ return wpm_apply_filters_typed( 'boolean', 'rocket_lazyload_noscript', true ); } /** * Returns the HTML tag wrapped inside noscript tags * * @param string $element Element to wrap. * @return string */ private function noscript( $element ) { return '<noscript>' . $element . '</noscript>'; } /** * Applies lazyload on srcset and sizes attributes * * @param string $html HTML image tag. * @return string */ public function lazyloadResponsiveAttributes( $html ) { $data_srcset = preg_replace( '/[\s|"|\'](srcset)\s*=\s*("|\')([^"|\']+)\2/i', ' data-lazy-$1=$2$3$2', $html ); if ( ! $data_srcset ) { return $html; } $html = $data_srcset; $lazy_responsive = preg_replace( '/[\s|"|\'](sizes)\s*=\s*("|\')([^"|\']+)\2/i', ' data-lazy-$1=$2$3$2', $html ); if ( ! $lazy_responsive ) { return $html; } return $lazy_responsive; } /** * Finds patterns matching smiley and call the callback method to replace them with the image * * @param string $text Content to search in. * @return string */ public function convertSmilies( $text ) { global $wp_smiliessearch; if ( empty( $text ) || ! is_string( $text ) ) { return $text; } if ( ! get_option( 'use_smilies' ) || empty( $wp_smiliessearch ) ) { return $text; } $output = ''; // HTML loop taken from texturize function, could possible be consolidated. $textarr = preg_split( '/(<.*>)/U', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); // capture the tags as well as in between. if ( ! $textarr ) { return $text; } $stop = count( $textarr );// loop stuff. // Ignore processing of specific tags. $tags_to_ignore = 'code|pre|style|script|textarea'; $ignore_block_element = ''; for ( $i = 0; $i < $stop; $i++ ) { $content = $textarr[ $i ]; // If we're in an ignore block, wait until we find its closing tag. if ( '' === $ignore_block_element && preg_match( '/^<(' . $tags_to_ignore . ')>/', $content, $matches ) ) { $ignore_block_element = $matches[1]; } // If it's not a tag and not in ignore block. if ( '' === $ignore_block_element && strlen( $content ) > 0 && '<' !== $content[0] ) { $content = preg_replace_callback( $wp_smiliessearch, [ $this, 'translateSmiley' ], $content ); } // did we exit ignore block. if ( '' !== $ignore_block_element && '</' . $ignore_block_element . '>' === $content ) { $ignore_block_element = ''; } $output .= $content; } return $output; } /** * Replace matches by smiley image, lazyloaded * * @param array<string> $matches Array of matches. * * @return string */ private function translateSmiley( $matches ) { global $wpsmiliestrans; if ( count( $matches ) === 0 ) { return ''; } $smiley = trim( reset( $matches ) ); $img = $wpsmiliestrans[ $smiley ]; $matches = []; $ext = preg_match( '/\.([^.]+)$/', $img, $matches ) ? strtolower( $matches[1] ) : false; $image_exts = [ 'jpg', 'jpeg', 'jpe', 'gif', 'png' ]; // Don't convert smilies that aren't images - they're probably emoji. if ( ! in_array( $ext, $image_exts, true ) ) { return $img; } /** * Filter the Smiley image URL before it's used in the image element. * * @since 2.9.0 * * @param string $smiley_url URL for the smiley image. * @param string $img Filename for the smiley image. * @param string $site_url Site URL, as returned by site_url(). */ $src_url = apply_filters( 'smilies_src', includes_url( "images/smilies/$img" ), $img, site_url() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound // Don't LazyLoad if process is stopped for these reasons. if ( is_feed() || is_preview() ) { return sprintf( ' <img src="%s" alt="%s" class="wp-smiley" /> ', esc_url( $src_url ), esc_attr( $smiley ) ); } return sprintf( ' <img src="%s" data-lazy-src="%s" alt="%s" class="wp-smiley" /> ', $this->getPlaceholder(), esc_url( $src_url ), esc_attr( $smiley ) ); } /** * Returns the placeholder for the src attribute * * @since 1.2 * * @param int $width Width of the placeholder image. Default 0. * @param int $height Height of the placeholder image. Default 0. * @return string */ public function getPlaceholder( $width = 0, $height = 0 ) { $width = 0 === $width ? 0 : absint( $width ); $height = 0 === $height ? 0 : absint( $height ); $placeholder = str_replace( ' ', '%20', "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 $width $height'%3E%3C/svg%3E" ); /** * Filter the image lazyLoad placeholder on src attribute * * @since 1.1 * * @param string $placeholder Placeholder that will be printed. * @param int $width Placeholder width. * @param int $height Placeholder height. */ return apply_filters( 'rocket_lazyload_placeholder', $placeholder, $width, $height ); } } Dependencies/BerlinDB/Database/Column.php 0000644 00000060067 15174677547 0014265 0 ustar 00 <?php /** * Base Custom Database Table Column Class. * * @package Database * @subpackage Column * @copyright Copyright (c) 2021 * @license https://opensource.org/licenses/MIT MIT * @since 1.0.0 */ namespace WP_Rocket\Dependencies\BerlinDB\Database; // Exit if accessed directly defined( 'ABSPATH' ) || exit; /** * Base class used for each column for a custom table. * * @since 1.0.0 * * @see Column::__construct() for accepted arguments. */ class Column extends Base { /** Table Attributes ******************************************************/ /** * Name for the database column. * * Required. Must contain lowercase alphabetical characters only. Use of any * other character (number, ascii, unicode, emoji, etc...) will result in * fatal application errors. * * @since 1.0.0 * @var string */ public $name = ''; /** * Type of database column. * * See: https://dev.mysql.com/doc/en/data-types.html * * @since 1.0.0 * @var string */ public $type = ''; /** * Length of database column. * * See: https://dev.mysql.com/doc/en/storage-requirements.html * * @since 1.0.0 * @var string */ public $length = false; /** * Is integer unsigned? * * See: https://dev.mysql.com/doc/en/numeric-type-overview.html * * @since 1.0.0 * @var bool */ public $unsigned = true; /** * Is integer filled with zeroes? * * See: https://dev.mysql.com/doc/en/numeric-type-overview.html * * @since 1.0.0 * @var bool */ public $zerofill = false; /** * Is data in a binary format? * * See: https://dev.mysql.com/doc/en/binary-varbinary.html * * @since 1.0.0 * @var bool */ public $binary = false; /** * Is null an allowed value? * * See: https://dev.mysql.com/doc/en/data-type-defaults.html * * @since 1.0.0 * @var bool */ public $allow_null = false; /** * Typically empty/null, or date value. * * See: https://dev.mysql.com/doc/en/data-type-defaults.html * * @since 1.0.0 * @var string */ public $default = ''; /** * auto_increment, etc... * * See: https://dev.mysql.com/doc/en/data-type-defaults.html * * @since 1.0.0 * @var string */ public $extra = ''; /** * Typically inherited from the database interface (wpdb). * * By default, this will use the globally available database encoding. You * most likely do not want to change this; if you do, you already know what * to do. * * See: https://dev.mysql.com/doc/mysql/en/charset-column.html * * @since 1.0.0 * @var string */ public $encoding = ''; /** * Typically inherited from the database interface (wpdb). * * By default, this will use the globally available database collation. You * most likely do not want to change this; if you do, you already know what * to do. * * See: https://dev.mysql.com/doc/mysql/en/charset-column.html * * @since 1.0.0 * @var string */ public $collation = ''; /** * Typically empty; probably ignore. * * By default, columns do not have comments. This is unused by any other * relative code, but you can include less than 1024 characters here. * * @since 1.0.0 * @var string */ public $comment = ''; /** Special Attributes ****************************************************/ /** * Is this the primary column? * * By default, columns are not the primary column. This is used by the Query * class for several critical functions, including (but not limited to) the * cache key, meta-key relationships, auto-incrementing, etc... * * @since 1.0.0 * @var bool */ public $primary = false; /** * Is this the column used as a created date? * * By default, columns do not represent the date a value was first entered. * This is used by the Query class to set its value automatically to the * current datetime value immediately before insert. * * @since 1.0.0 * @var bool */ public $created = false; /** * Is this the column used as a modified date? * * By default, columns do not represent the date a value was last changed. * This is used by the Query class to update its value automatically to the * current datetime value immediately before insert|update. * * @since 1.0.0 * @var bool */ public $modified = false; /** * Is this the column used as a unique universal identifier? * * By default, columns are not UUIDs. This is used by the Query class to * generate a unique string that can be used to identify a row in a database * table, typically in such a way that is unrelated to the row data itself. * * @since 1.0.0 * @var bool */ public $uuid = false; /** Query Attributes ******************************************************/ /** * What is the string-replace pattern? * * By default, column patterns will be guessed based on their type. Set this * manually to `%s|%d|%f` only if you are doing something weird, or are * explicitly storing numeric values in text-based column types. * * @since 1.0.0 * @var string */ public $pattern = ''; /** * Is this column searchable? * * By default, columns are not searchable. When `true`, the Query class will * add this column to the results of search queries. * * Avoid setting to `true` on large blobs of text, unless you've optimized * your database server to accommodate these kinds of queries. * * @since 1.0.0 * @var bool */ public $searchable = false; /** * Is this column a date? * * By default, columns do not support date queries. When `true`, the Query * class will accept complex statements to help narrow results down to * specific periods of time for values in this column. * * @since 1.0.0 * @var bool */ public $date_query = false; /** * Is this column used in orderby? * * By default, columns are not sortable. This ensures that the database * table does not perform costly operations on unindexed columns or columns * of an inefficient type. * * You can safely turn this on for most numeric columns, indexed columns, * and text columns with intentionally limited lengths. * * @since 1.0.0 * @var bool */ public $sortable = false; /** * Is __in supported? * * By default, columns support being queried using an `IN` statement. This * allows the Query class to retrieve rows that match your array of values. * * Consider setting this to `false` for longer text columns. * * @since 1.0.0 * @var bool */ public $in = true; /** * Is __not_in supported? * * By default, columns support being queried using a `NOT IN` statement. * This allows the Query class to retrieve rows that do not match your array * of values. * * Consider setting this to `false` for longer text columns. * * @since 1.0.0 * @var bool */ public $not_in = true; /** Cache Attributes ******************************************************/ /** * Does this column have its own cache key? * * By default, only primary columns are used as cache keys. If this column * is unique, or is frequently used to get database results, you may want to * consider setting this to true. * * Use in conjunction with a database index for speedy queries. * * @since 1.0.0 * @var string */ public $cache_key = false; /** Action Attributes *****************************************************/ /** * Does this column fire a transition action when it's value changes? * * By default, columns do not fire transition actions. In some cases, it may * be desirable to know when a database value changes, and what the old and * new values are when that happens. * * The Query class is responsible for triggering the event action. * * @since 1.0.0 * @var bool */ public $transition = false; /** Callback Attributes ***************************************************/ /** * Maybe validate this data before it is written to the database. * * By default, column data is validated based on the type of column that it * is. You can set this to a callback function of your choice to override * the default validation behavior. * * @since 1.0.0 * @var string */ public $validate = ''; /** * Array of capabilities used to interface with this column. * * These are used by the Query class to allow and disallow CRUD access to * column data, typically based on roles or capabilities. * * @since 1.0.0 * @var array */ public $caps = array(); /** * Array of possible aliases this column can be referred to as. * * These are used by the Query class to allow for columns to be renamed * without requiring complex architectural backwards compatibility support. * * @since 1.0.0 * @var array */ public $aliases = array(); /** * Array of possible relationships this column has with columns in other * database tables. * * These are typically unenforced foreign keys, and are used by the Query * class to help prime related items. * * @since 1.0.0 * @var array */ public $relationships = array(); /** Methods ***************************************************************/ /** * Sets up the order query, based on the query vars passed. * * @since 1.0.0 * * @param string|array $args { * Optional. Array or query string of order query parameters. Default empty. * * @type string $name Name of database column * @type string $type Type of database column * @type int $length Length of database column * @type bool $unsigned Is integer unsigned? * @type bool $zerofill Is integer filled with zeroes? * @type bool $binary Is data in a binary format? * @type bool $allow_null Is null an allowed value? * @type mixed $default Typically empty/null, or date value * @type string $extra auto_increment, etc... * @type string $encoding Typically inherited from wpdb * @type string $collation Typically inherited from wpdb * @type string $comment Typically empty * @type bool $pattern What is the string-replace pattern? * @type bool $primary Is this the primary column? * @type bool $created Is this the column used as a created date? * @type bool $modified Is this the column used as a modified date? * @type bool $uuid Is this the column used as a universally unique identifier? * @type bool $searchable Is this column searchable? * @type bool $sortable Is this column used in orderby? * @type bool $date_query Is this column a datetime? * @type bool $in Is __in supported? * @type bool $not_in Is __not_in supported? * @type bool $cache_key Is this column queried independently? * @type bool $transition Does this column transition between changes? * @type string $validate A callback function used to validate on save. * @type array $caps Array of capabilities to check. * @type array $aliases Array of possible column name aliases. * @type array $relationships Array of columns in other tables this column relates to. * } */ public function __construct( $args = array() ) { // Parse arguments $r = $this->parse_args( $args ); // Maybe set variables from arguments if ( ! empty( $r ) ) { $this->set_vars( $r ); } } /** Argument Handlers *****************************************************/ /** * Parse column arguments * * @since 1.0.0 * @param array $args Default empty array. * @return array */ private function parse_args( $args = array() ) { // Parse arguments $r = wp_parse_args( $args, array( // Table 'name' => '', 'type' => '', 'length' => '', 'unsigned' => false, 'zerofill' => false, 'binary' => false, 'allow_null' => false, 'default' => '', 'extra' => '', 'encoding' => $this->get_db()->charset, 'collation' => $this->get_db()->collate, 'comment' => '', // Query 'pattern' => false, 'searchable' => false, 'sortable' => false, 'date_query' => false, 'transition' => false, 'in' => true, 'not_in' => true, // Special 'primary' => false, 'created' => false, 'modified' => false, 'uuid' => false, // Cache 'cache_key' => false, // Validation 'validate' => '', // Capabilities 'caps' => array(), // Backwards Compatibility 'aliases' => array(), // Column Relationships 'relationships' => array() ) ); // Force some arguments for special column types $r = $this->special_args( $r ); // Set the args before they are sanitized $this->set_vars( $r ); // Return array return $this->validate_args( $r ); } /** * Validate arguments after they are parsed. * * @since 1.0.0 * @param array $args Default empty array. * @return array */ private function validate_args( $args = array() ) { // Sanitization callbacks $callbacks = array( 'name' => 'sanitize_key', 'type' => 'strtoupper', 'length' => 'intval', 'unsigned' => 'wp_validate_boolean', 'zerofill' => 'wp_validate_boolean', 'binary' => 'wp_validate_boolean', 'allow_null' => 'wp_validate_boolean', 'default' => array( $this, 'sanitize_default' ), 'extra' => 'wp_kses_data', 'encoding' => 'wp_kses_data', 'collation' => 'wp_kses_data', 'comment' => 'wp_kses_data', 'primary' => 'wp_validate_boolean', 'created' => 'wp_validate_boolean', 'modified' => 'wp_validate_boolean', 'uuid' => 'wp_validate_boolean', 'searchable' => 'wp_validate_boolean', 'sortable' => 'wp_validate_boolean', 'date_query' => 'wp_validate_boolean', 'transition' => 'wp_validate_boolean', 'in' => 'wp_validate_boolean', 'not_in' => 'wp_validate_boolean', 'cache_key' => 'wp_validate_boolean', 'pattern' => array( $this, 'sanitize_pattern' ), 'validate' => array( $this, 'sanitize_validation' ), 'caps' => array( $this, 'sanitize_capabilities' ), 'aliases' => array( $this, 'sanitize_aliases' ), 'relationships' => array( $this, 'sanitize_relationships' ) ); // Default args array $r = array(); // Loop through and try to execute callbacks foreach ( $args as $key => $value ) { // Callback is callable if ( isset( $callbacks[ $key ] ) && is_callable( $callbacks[ $key ] ) ) { $r[ $key ] = call_user_func( $callbacks[ $key ], $value ); // Callback is malformed so just let it through to avoid breakage } else { $r[ $key ] = $value; } } // Return sanitized arguments return $r; } /** * Force column arguments for special column types * * @since 1.0.0 * @param array $args Default empty array. * @return array */ private function special_args( $args = array() ) { // Primary key columns are always used as cache keys if ( ! empty( $args['primary'] ) ) { $args['cache_key'] = true; // All UUID columns need to follow a very specific pattern } elseif ( ! empty( $args['uuid'] ) ) { $args['name'] = 'uuid'; $args['type'] = 'varchar'; $args['length'] = '100'; $args['in'] = false; $args['not_in'] = false; $args['searchable'] = false; $args['sortable'] = false; } // Return args return (array) $args; } /** Public Helpers ********************************************************/ /** * Return if a column type is numeric or not. * * @since 1.0.0 * @return bool */ public function is_numeric() { return $this->is_type( array( 'tinyint', 'int', 'mediumint', 'bigint' ) ); } /** Private Helpers *******************************************************/ /** * Return if this column is of a certain type. * * @since 1.0.0 * @param mixed $type Default empty string. The type to check. Also accepts an array. * @return bool True if of type, False if not */ private function is_type( $type = '' ) { // If string, cast to array if ( is_string( $type ) ) { $type = (array) $type; } // Make them lowercase $types = array_map( 'strtolower', $type ); // Return if match or not return (bool) in_array( strtolower( $this->type ), $types, true ); } /** Private Sanitizers ****************************************************/ /** * Sanitize capabilities array * * @since 1.0.0 * @param array $caps Default empty array. * @return array */ private function sanitize_capabilities( $caps = array() ) { return wp_parse_args( $caps, array( 'select' => 'exist', 'insert' => 'exist', 'update' => 'exist', 'delete' => 'exist' ) ); } /** * Sanitize aliases array using `sanitize_key()` * * @since 1.0.0 * @param array $aliases Default empty array. * @return array */ private function sanitize_aliases( $aliases = array() ) { return array_map( 'sanitize_key', $aliases ); } /** * Sanitize relationships array * * @todo * @since 1.0.0 * @param array $relationships Default empty array. * @return array */ private function sanitize_relationships( $relationships = array() ) { return array_filter( $relationships ); } /** * Sanitize the default value * * @since 1.0.0 * @param string $default * @return string|null */ private function sanitize_default( $default = '' ) { // Null if ( ( true === $this->allow_null ) && is_null( $default ) ) { return null; // String } elseif ( is_string( $default ) ) { return wp_kses_data( $default ); // Integer } elseif ( $this->is_numeric() ) { return (int) $default; } // @todo datetime, decimal, and other column types // Unknown, so return the default's default return ''; } /** * Sanitize the pattern * * @since 1.0.0 * @param string $pattern * @return string */ private function sanitize_pattern( $pattern = '%s' ) { // Allowed patterns $allowed_patterns = array( '%s', '%d', '%f' ); // Return pattern if allowed if ( in_array( $pattern, $allowed_patterns, true ) ) { return $pattern; } // Fallback to digit or string return $this->is_numeric() ? '%d' : '%s'; } /** * Sanitize the validation callback * * @since 1.0.0 * @param string $callback Default empty string. A callable PHP function name or method * @return string The most appropriate callback function for the value */ private function sanitize_validation( $callback = '' ) { // Return callback if it's callable if ( is_callable( $callback ) ) { return $callback; } // UUID special column if ( true === $this->uuid ) { $callback = array( $this, 'validate_uuid' ); // Datetime fallback } elseif ( $this->is_type( 'datetime' ) ) { $callback = array( $this, 'validate_datetime' ); // Decimal fallback } elseif ( $this->is_type( 'decimal' ) ) { $callback = array( $this, 'validate_decimal' ); // Intval fallback } elseif ( $this->is_numeric() ) { $callback = 'intval'; } // Return the callback return $callback; } /** Public Validators *****************************************************/ /** * Fallback to validate a datetime value if no other is set. * * This assumes NO_ZERO_DATES is off or overridden. * * If MySQL drops support for zero dates, this method will need to be * updated to support different default values based on the environment. * * @since 1.0.0 * @param string $value Default ''. A datetime value that needs validating * @return string A valid datetime value */ public function validate_datetime( $value = '' ) { // Handle "empty" values if ( empty( $value ) || ( '0000-00-00 00:00:00' === $value ) ) { $value = ! empty( $this->default ) ? $this->default : ''; // Convert to MySQL datetime format via gmdate() && strtotime } elseif ( function_exists( 'gmdate' ) ) { $value = gmdate( 'Y-m-d H:i:s', strtotime( $value ) ); } // Return the validated value return $value; } /** * Validate a decimal * * (Recommended decimal column length is '18,9'.) * * This is used to validate a mixed value before it is saved into a decimal * column in a database table. * * Uses number_format() which does rounding to the last decimal if your * value is longer than specified. * * @since 1.0.0 * @param mixed $value Default empty string. The decimal value to validate * @param int $decimals Default 9. The number of decimal points to accept * @return float */ public function validate_decimal( $value = 0, $decimals = 9 ) { // Protect against non-numeric values if ( ! is_numeric( $value ) ) { $value = 0; } // Protect against non-numeric decimals if ( ! is_numeric( $decimals ) ) { $decimals = 9; } // Is the value negative? $negative_exponent = ( $value < 0 ) ? -1 : 1; // Only numbers and period $value = preg_replace( '/[^0-9\.]/', '', (string) $value ); // Format to number of decimals, and cast as float $formatted = number_format( $value, $decimals, '.', '' ); // Adjust for negative values $retval = $formatted * $negative_exponent; // Return return $retval; } /** * Validate a UUID. * * This uses the v4 algorithm to generate a UUID that is used to uniquely * and universally identify a given database row without any direct * connection or correlation to the data in that row. * * From http://php.net/manual/en/function.uniqid.php#94959 * * @since 1.0.0 * @param string $value The UUID value (empty on insert, string on update) * @return string Generated UUID. */ public function validate_uuid( $value = '' ) { // Default URN UUID prefix $prefix = 'urn:uuid:'; // Bail if not empty and correctly prefixed // (UUIDs should _never_ change once they are set) if ( ! empty( $value ) && ( 0 === strpos( $value, $prefix ) ) ) { return $value; } // Put the pieces together $value = sprintf( "{$prefix}%04x%04x-%04x-%04x-%04x-%04x%04x%04x", // 32 bits for "time_low" mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), // 16 bits for "time_mid" mt_rand( 0, 0xffff ), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 4 mt_rand( 0, 0x0fff ) | 0x4000, // 16 bits, 8 bits for "clk_seq_hi_res", // 8 bits for "clk_seq_low", // two most significant bits holds zero and one for variant DCE1.1 mt_rand( 0, 0x3fff ) | 0x8000, // 48 bits for "node" mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) ); // Return the new UUID return $value; } /** Table Helpers *********************************************************/ /** * Return a string representation of what this column's properties look like * in a MySQL. * * @todo * @since 1.0.0 * @return string */ public function get_create_string() { // Default return val $retval = ''; // Bail if no name if ( ! empty( $this->name ) ) { $retval .= $this->name; } // Type if ( ! empty( $this->type ) ) { $retval .= " {$this->type}"; } // Length if ( ! empty( $this->length ) ) { $retval .= '(' . $this->length . ')'; } // Unsigned if ( ! empty( $this->unsigned ) ) { $retval .= " unsigned"; } // Zerofill if ( ! empty( $this->zerofill ) ) { // TBD } // Binary if ( ! empty( $this->binary ) ) { // TBD } // Allow null if ( ! empty( $this->allow_null ) ) { $retval .= " NOT NULL "; } // Default if ( ! empty( $this->default ) ) { $retval .= " default '{$this->default}'"; // A literal false means no default value } elseif ( false !== $this->default ) { // Numeric if ( $this->is_numeric() ) { $retval .= " default '0'"; } elseif ( $this->is_type( 'datetime' ) ) { $retval .= " default '0000-00-00 00:00:00'"; } else { $retval .= " default ''"; } } // Extra if ( ! empty( $this->extra ) ) { $retval .= " {$this->extra}"; } // Encoding if ( ! empty( $this->encoding ) ) { } else { } // Collation if ( ! empty( $this->collation ) ) { } else { } // Return the create string return $retval; } } Dependencies/BerlinDB/Database/Queries/Meta.php 0000644 00000001276 15174677547 0015330 0 ustar 00 <?php /** * Base Custom Database Table Meta Query Class. * * @package Database * @subpackage Meta * @copyright Copyright (c) 2021 * @license https://opensource.org/licenses/MIT MIT * @since 1.1.0 */ namespace WP_Rocket\Dependencies\BerlinDB\Database\Queries; // Exit if accessed directly defined( 'ABSPATH' ) || exit; // @todo Remove the need for this dependency use \WP_Meta_Query; /** * Class for generating SQL clauses that filter a primary query according to meta. * * It currently extends the WP_Meta_Query class in WordPress, but in the future * will be derived completely from other registered tables. * * @since 1.1.0 */ class Meta extends WP_Meta_Query { } Dependencies/BerlinDB/Database/Queries/Date.php 0000644 00000115147 15174677547 0015322 0 ustar 00 <?php /** * Base Custom Database Table Date Query Class. * * @package Database * @subpackage Date * @copyright Copyright (c) 2021 * @license https://opensource.org/licenses/MIT MIT * @since 1.0.0 */ namespace WP_Rocket\Dependencies\BerlinDB\Database\Queries; // Exit if accessed directly defined( 'ABSPATH' ) || exit; use WP_Rocket\Dependencies\BerlinDB\Database\Base; /** * Class for generating SQL clauses that filter a primary query according to date. * * Is heavily inspired by the WP_Date_Query class in WordPress, with changes to make * it more flexible for custom tables and their columns. * * Date is a helper that allows primary query classes, such as WP_Query, to filter * their results by date columns, by generating `WHERE` subclauses to be attached to the * primary SQL query string. * * Attempting to filter by an invalid date value (eg month=13) will generate SQL that will * return no results. In these cases, a _doing_it_wrong() error notice is also thrown. * See Date::validate_date_values(). * * @link https://developer.wordpress.org/reference/classes/wp_query/ * * @since 1.0.0 */ class Date extends Base { /** * Array of date queries. * * See Date::__construct() for information on date query arguments. * * @since 1.0.0 * @var array */ public $queries = array(); /** * The default relation between top-level queries. Can be either 'AND' or 'OR'. * * @since 1.0.0 * @var string */ public $relation = 'AND'; /** * The column to query against. Can be changed via the query arguments. * * @since 1.0.0 * @var string */ public $column = 'date_created'; /** * The value comparison operator. Can be changed via the query arguments. * * @since 1.0.0 * @var array */ public $compare = '='; /** * The start of week operator. Can be changed via the query arguments. * * @since 1.1.0 * @var array */ public $start_of_week = 0; /** * The unix timestamp for this current time. * * @since 1.1.0 * @var int */ public $now = 0; /** * Supported time-related parameter keys. * * @since 1.0.0 * @var array */ public $time_keys = array( 'after', 'before', 'value', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second' ); /** * Supported comparison types * * @since 1.0.0 * @var array */ public $comparison_keys = array( '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ); /** * Supported multi-value comparison types * * @since 1.1.0 * @var array */ public $multi_value_keys = array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ); /** * Supported relation types * * @since 1.1.0 * @var array */ public $relation_keys = array( 'OR', 'AND' ); /** * Constructor. * * Time-related parameters that normally require integer values ('year', 'month', 'week', 'dayofyear', 'day', * 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second') accept arrays of integers for some values of * 'compare'. When 'compare' is 'IN' or 'NOT IN', arrays are accepted; when 'compare' is 'BETWEEN' or 'NOT * BETWEEN', arrays of two valid values are required. See individual argument descriptions for accepted values. * * @since 1.0.0 * * @param array $date_query { * Array of date query clauses. * * @type array ...$0 { * @type string $column Optional. The column to query against. If undefined, inherits the value of * 'date_created'. Accepts 'date_created', 'date_created_gmt', * 'post_modified','post_modified_gmt', 'comment_date', 'comment_date_gmt'. * Default 'date_created'. * @type string $compare Optional. The comparison operator. Accepts '=', '!=', '>', '>=', '<', '<=', * 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. Default '='. * @type string $relation Optional. The boolean relationship between the date queries. Accepts 'OR' or 'AND'. * Default 'OR'. * @type int|array $start_of_week Optional. Day that week starts on. Accepts numbers 0-6 * (0 = Sunday, 1 is Monday). Default 0. * @type array ...$0 { * Optional. An array of first-order clause parameters, or another fully-formed date query. * * @type string|array $before { * Optional. Date to retrieve posts before. Accepts `strtotime()`-compatible string, * or array of 'year', 'month', 'day' values. * * @type string $year The four-digit year. Default empty. Accepts any four-digit year. * @type string $month Optional when passing array.The month of the year. * Default (string:empty)|(array:1). Accepts numbers 1-12. * @type string $day Optional when passing array.The day of the month. * Default (string:empty)|(array:1). Accepts numbers 1-31. * } * @type string|array $after { * Optional. Date to retrieve posts after. Accepts `strtotime()`-compatible string, * or array of 'year', 'month', 'day' values. * * @type string $year The four-digit year. Accepts any four-digit year. Default empty. * @type string $month Optional when passing array. The month of the year. Accepts numbers 1-12. * Default (string:empty)|(array:12). * @type string $day Optional when passing array.The day of the month. Accepts numbers 1-31. * Default (string:empty)|(array:last day of month). * } * @type string $column Optional. Used to add a clause comparing a column other than the * column specified in the top-level `$column` parameter. Accepts * 'date_created', 'date_created_gmt', 'post_modified', 'post_modified_gmt', * 'comment_date', 'comment_date_gmt'. Default is the value of * top-level `$column`. * @type string $compare Optional. The comparison operator. Accepts '=', '!=', '>', '>=', * '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. 'IN', * 'NOT IN', 'BETWEEN', and 'NOT BETWEEN'. Comparisons support * arrays in some time-related parameters. Default '='. * @type int|array $start_of_week Optional. Day that week starts on. Accepts numbers 0-6 * (0 = Sunday, 1 is Monday). Default 0. * @type bool $inclusive Optional. Include results from dates specified in 'before' or * 'after'. Default false. * @type int|array $year Optional. The four-digit year number. Accepts any four-digit year * or an array of years if `$compare` supports it. Default empty. * @type int|array $month Optional. The two-digit month number. Accepts numbers 1-12 or an * array of valid numbers if `$compare` supports it. Default empty. * @type int|array $week Optional. The week number of the year. Accepts numbers 0-53 or an * array of valid numbers if `$compare` supports it. Default empty. * @type int|array $dayofyear Optional. The day number of the year. Accepts numbers 1-366 or an * array of valid numbers if `$compare` supports it. * @type int|array $day Optional. The day of the month. Accepts numbers 1-31 or an array * of valid numbers if `$compare` supports it. Default empty. * @type int|array $dayofweek Optional. The day number of the week. Accepts numbers 1-7 (1 is * Sunday) or an array of valid numbers if `$compare` supports it. * Default empty. * @type int|array $dayofweek_iso Optional. The day number of the week (ISO). Accepts numbers 1-7 * (1 is Monday) or an array of valid numbers if `$compare` supports it. * Default empty. * @type int|array $hour Optional. The hour of the day. Accepts numbers 0-23 or an array * of valid numbers if `$compare` supports it. Default empty. * @type int|array $minute Optional. The minute of the hour. Accepts numbers 0-60 or an array * of valid numbers if `$compare` supports it. Default empty. * @type int|array $second Optional. The second of the minute. Accepts numbers 0-60 or an * array of valid numbers if `$compare` supports it. Default empty. * } * } * } */ public function __construct( $date_query = array() ) { // Bail if empty or not an array. if ( empty( $date_query ) || ! is_array( $date_query ) ) { return; } // Set now, column, compare, relation, and start_of_week. $this->now = $this->get_now( $date_query ); $this->column = $this->get_column( $date_query ); $this->compare = $this->get_compare( $date_query ); $this->relation = $this->get_relation( $date_query ); $this->start_of_week = $this->get_start_of_week( $date_query ); // Support for passing time-based keys in the top level of the array. if ( ! isset( $date_query[0] ) ) { $date_query = array( $date_query ); } // Set the queries $this->queries = $this->sanitize_query( $date_query ); } /** * Recursive-friendly query sanitizer. * * Ensures that each query-level clause has a 'relation' key, and that * each first-order clause contains all the necessary keys from * `$defaults`. * * @since 1.0.0 * * @param array $queries * @param array $parent_query * * @return array Sanitized queries. */ public function sanitize_query( $queries = array(), $parent_query = array() ) { // Default return value. $retval = array(); // Setup defaults. $defaults = array( 'now' => $this->get_now(), 'column' => $this->get_column(), 'compare' => $this->get_compare(), 'relation' => $this->get_relation(), 'start_of_week' => $this->get_start_of_week() ); // Numeric keys should always have array values. foreach ( $queries as $qkey => $qvalue ) { if ( is_numeric( $qkey ) && ! is_array( $qvalue ) ) { unset( $queries[ $qkey ] ); } } // Each query should have a value for each default key. // Inherit from the parent when possible. foreach ( $defaults as $dkey => $dvalue ) { // Skip if already set. if ( isset( $queries[ $dkey ] ) ) { continue; } // Set the query. if ( isset( $parent_query[ $dkey ] ) ) { $queries[ $dkey ] = $parent_query[ $dkey ]; } else { $queries[ $dkey ] = $dvalue; } } // Validate the dates passed in the query. if ( $this->is_first_order_clause( $queries ) ) { $this->validate_date_values( $queries ); } // Add queries to return array. foreach ( $queries as $key => $q ) { // This is a first-order query. Trust the values and sanitize when building SQL. if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) { $retval[ $key ] = $q; // Any array without a time key is another query, so we recurse. } else { $retval[] = $this->sanitize_query( $q, $queries ); } } // Return sanitized queries. return $retval; } /** * Determine whether this is a first-order clause. * * Checks to see if the current clause has any time-related keys. * If so, it's first-order. * * @since 1.0.0 * * @param array $query Query clause. * * @return bool True if this is a first-order clause. */ protected function is_first_order_clause( $query = array() ) { $time_keys = array_intersect( $this->time_keys, array_keys( $query ) ); return ! empty( $time_keys ); } /** * Determines and validates what the current unix timestamp is. * * @since 1.1.0 * * @param array $query A date query or a date subquery. * * @return string The current unix timestamp. */ public function get_now( $query = array() ) { // Use now if passed $retval = ! empty( $query['now'] ) && is_numeric( $query['now'] ) ? absint( $query['now'] ) : time(); return $retval; } /** * Determines and validates what comparison operator to use. * * @since 1.0.0 * * @param array $query A date query or a date subquery. * * @return string The comparison operator. */ public function get_column( $query = array() ) { // Use column if passed $retval = ! empty( $query['column'] ) ? esc_sql( $this->validate_column( $query['column'] ) ) : $this->column; return $retval; } /** * Determines and validates what comparison operator to use. * * @since 1.0.0 * * @param array $query A date query or a date subquery. * * @return string The comparison operator. */ public function get_compare( $query = array() ) { // Compare must be in the allowed array $retval = ! empty( $query['compare'] ) && in_array( $query['compare'], $this->comparison_keys, true ) ? strtoupper( $query['compare'] ) : $this->compare; return $retval; } /** * Determines and validates what relation to use. * * @since 1.0.0 * * @param array $query A date query or a date subquery. * @return string The relation operator. */ public function get_relation( $query = array() ) { // Relation must be in the allowed array $retval = ! empty( $query['relation'] ) && in_array( $query['relation'], $this->relation_keys, true ) ? strtoupper( $query['relation'] ) : $this->relation; return $retval; } /** * Determines and validates what start_of_week to use. * * @since 1.1.0 * * @param array $query A date query or a date subquery. * * @return string The comparison operator. */ public function get_start_of_week( $query = array() ) { // Use start of week if passed and valid $retval = isset( $query['start_of_week'] ) && ( 6 >= (int) $query['start_of_week'] ) && ( 0 <= (int) $query['start_of_week'] ) ? $query['start_of_week'] : $this->start_of_week; return (int) $retval; } /** * Validates the given date_query values. * * Note that date queries with invalid date ranges are allowed to * continue (though of course no items will be found for impossible dates). * This method only generates debug notices for these cases. * * @since 1.0.0 * * @param array $date_query The date_query array. * * @return bool True if all values in the query are valid, false if one or more fail. */ public function validate_date_values( $date_query = array() ) { // Bail if empty. if ( empty( $date_query ) ) { return false; } $valid = true; /* * Validate 'before' and 'after' up front, then let the * validation routine continue to be sure that all invalid * values generate errors too. */ if ( array_key_exists( 'before', $date_query ) && is_array( $date_query['before'] ) ) { $valid = $this->validate_date_values( $date_query['before'] ); } if ( array_key_exists( 'after', $date_query ) && is_array( $date_query['after'] ) ) { $valid = $this->validate_date_values( $date_query['after'] ); } // Values are passthroughs. if ( array_key_exists( 'value', $date_query ) ) { $valid = true; } // Array containing all min-max checks. $min_max_checks = array(); // Days per year. if ( array_key_exists( 'year', $date_query ) ) { /* * If a year exists in the date query, we can use it to get the days. * If multiple years are provided (as in a BETWEEN), use the first one. */ if ( is_array( $date_query['year'] ) ) { $_year = reset( $date_query['year'] ); } else { $_year = $date_query['year']; } $max_days_of_year = gmdate( 'z', gmmktime( 0, 0, 0, 12, 31, $_year ) ) + 1; // Otherwise we use the max of 366 (leap-year) } else { $max_days_of_year = 366; } // Days of year. $min_max_checks['dayofyear'] = array( 'min' => 1, 'max' => $max_days_of_year, ); // Days per week. $min_max_checks['dayofweek'] = array( 'min' => 1, 'max' => 7, ); // Days per week. $min_max_checks['dayofweek_iso'] = array( 'min' => 1, 'max' => 7, ); // Months per year. $min_max_checks['month'] = array( 'min' => 1, 'max' => 12, ); // Weeks per year. if ( isset( $_year ) ) { /* * If we have a specific year, use it to calculate number of weeks. * Note: the number of weeks in a year is the date in which Dec 28 appears. */ $week_count = gmdate( 'W', gmmktime( 0, 0, 0, 12, 28, $_year ) ); // Otherwise set the week-count to a maximum of 53. } else { $week_count = 53; } // Weeks per year. $min_max_checks['week'] = array( 'min' => 1, 'max' => $week_count, ); // Days per month. $min_max_checks['day'] = array( 'min' => 1, 'max' => 31, ); // Hours per day. $min_max_checks['hour'] = array( 'min' => 0, 'max' => 23, ); // Minutes per hour. $min_max_checks['minute'] = array( 'min' => 0, 'max' => 59, ); // Seconds per minute. $min_max_checks['second'] = array( 'min' => 0, 'max' => 59, ); // Loop through min/max checks. foreach ( $min_max_checks as $key => $check ) { // Skip if not in query. if ( ! array_key_exists( $key, $date_query ) ) { continue; } // Check for invalid values. foreach ( (array) $date_query[ $key ] as $_value ) { $is_between = ( $_value >= $check['min'] ) && ( $_value <= $check['max'] ); if ( ! is_numeric( $_value ) || empty( $is_between ) ) { $valid = false; } } } // Bail if invalid query. if ( false === $valid ) { return $valid; } // Check what kinds of dates are being queried for. $day_exists = array_key_exists( 'day', $date_query ) && is_numeric( $date_query['day'] ); $month_exists = array_key_exists( 'month', $date_query ) && is_numeric( $date_query['month'] ); $year_exists = array_key_exists( 'year', $date_query ) && is_numeric( $date_query['year'] ); // Checking at least day & month. if ( ! empty( $day_exists ) && ! empty( $month_exists ) ) { // Check for year query, or fallback to 2012 (for flexibility). $year = ! empty( $year_exists ) ? $date_query['year'] : '2012'; // Parse the date to check. $to_check = sprintf( '%s-%s-%s', $year, $date_query['month'], $date_query['day'] ); // Check the date. if ( ! $this->checkdate( $date_query['month'], $date_query['day'], $year, $to_check ) ) { $valid = false; } } // Return if valid or not return $valid; } /** * Validates a column name parameter. * * @since 1.0.0 * * @param string $column The user-supplied column name. * * @return string A validated column name value. */ public function validate_column( $column = '' ) { return preg_replace( '/[^a-zA-Z0-9_$\.]/', '', $column ); } /** * Generate WHERE clause to be appended to a main query. * * @since 1.0.0 * * @return string MySQL WHERE clauses. */ public function get_sql() { $sql = $this->get_sql_clauses(); /** * Filters the date query clauses. * * @since 1.0.0 * * @param string $sql Clauses of the date query. * @param Date $this The Date query instance. */ return apply_filters( 'get_date_sql', $sql, $this ); } /** * Generate SQL clauses to be appended to a main query. * * Called by the public Date::get_sql(), this method is abstracted * out to maintain parity with the other Query classes. * * @since 1.0.0 * * @return array { * Array containing JOIN and WHERE SQL clauses to append to the main query. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ protected function get_sql_clauses() { $sql = $this->get_sql_for_query( $this->queries ); if ( ! empty( $sql['where'] ) ) { $sql['where'] = ' AND ' . $sql['where']; } return apply_filters( 'get_date_sql_clauses', $sql, $this ); } /** * Generate SQL clauses for a single query array. * * If nested subqueries are found, this method recurses the tree to * produce the properly nested SQL. * * @since 1.0.0 * * @param array $query Query to parse. * @param int $depth Optional. Number of tree levels deep we currently are. * Used to calculate indentation. Default 0. * @return array { * Array containing JOIN and WHERE SQL clauses to append to a single query array. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ protected function get_sql_for_query( $query = array(), $depth = 0 ) { $sql_chunks = array( 'join' => array(), 'where' => array(), ); $sql = array( 'join' => '', 'where' => '', ); $indent = ''; for ( $i = 0; $i < $depth; $i++ ) { $indent .= ' '; } foreach ( $query as $key => $clause ) { if ( 'relation' === $key ) { $relation = $query['relation']; } elseif ( is_array( $clause ) ) { // This is a first-order clause. if ( $this->is_first_order_clause( $clause ) ) { // Get clauses & where count $clause_sql = $this->get_sql_for_clause( $clause, $query ); $where_count = count( $clause_sql['where'] ); if ( 0 === $where_count ) { $sql_chunks['where'][] = ''; } elseif ( 1 === $where_count ) { $sql_chunks['where'][] = $clause_sql['where'][0]; } else { $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; } $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); // This is a subquery, so we recurse. } else { $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); $sql_chunks['where'][] = $clause_sql['where']; $sql_chunks['join'][] = $clause_sql['join']; } } } // Filter to remove empties. $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); if ( empty( $relation ) ) { $relation = 'AND'; } // Filter duplicate JOIN clauses and combine into a single string. if ( ! empty( $sql_chunks['join'] ) ) { $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); } // Generate a single WHERE clause with proper brackets and indentation. if ( ! empty( $sql_chunks['where'] ) ) { $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; } // Filter and return return apply_filters( 'get_date_sql_for_query', $sql, $query, $depth, $this ); } /** * Turns a first-order date query into SQL for a WHERE clause. * * @since 1.0.0 * * @param array $query Date query clause. * @param array $parent_query Parent query of the current date query. * * @return array { * Array containing JOIN and WHERE SQL clauses to append to the main query. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ protected function get_sql_for_clause( $query = array(), $parent_query = array() ) { // The sub-parts of a $where part. $where_parts = array(); // Get first-order clauses $now = $this->get_now( $query ); $column = $this->get_column( $query ); $compare = $this->get_compare( $query ); $start_of_week = $this->get_start_of_week( $query ); $inclusive = ! empty( $query['inclusive'] ); // Assign greater-than and less-than values. $lt = '<'; $gt = '>'; if ( true === $inclusive ) { $lt .= '='; $gt .= '='; } // Range queries. if ( ! empty( $query['after'] ) ) { $where_parts[] = $this->get_db()->prepare( "{$column} {$gt} %s", $this->build_mysql_datetime( $query['after'], ! $inclusive, $now ) ); } if ( ! empty( $query['before'] ) ) { $where_parts[] = $this->get_db()->prepare( "{$column} {$lt} %s", $this->build_mysql_datetime( $query['before'], $inclusive, $now ) ); } // Specific value queries. if ( isset( $query['year'] ) && $value = $this->build_numeric_value( $compare, $query['year'] ) ) { $where_parts[] = "YEAR( {$column} ) {$compare} {$value}"; } if ( isset( $query['month'] ) && $value = $this->build_numeric_value( $compare, $query['month'] ) ) { $where_parts[] = "MONTH( {$column} ) {$compare} {$value}"; } elseif ( isset( $query['monthnum'] ) && $value = $this->build_numeric_value( $compare, $query['monthnum'] ) ) { $where_parts[] = "MONTH( {$column} ) {$compare} {$value}"; } if ( isset( $query['week'] ) && false !== ( $value = $this->build_numeric_value( $compare, $query['week'] ) ) ) { $where_parts[] = $this->build_mysql_week( $column, $start_of_week ) . " {$compare} {$value}"; } elseif ( isset( $query['w'] ) && false !== ( $value = $this->build_numeric_value( $compare, $query['w'] ) ) ) { $where_parts[] = $this->build_mysql_week( $column, $start_of_week ) . " {$compare} {$value}"; } if ( isset( $query['dayofyear'] ) && $value = $this->build_numeric_value( $compare, $query['dayofyear'] ) ) { $where_parts[] = "DAYOFYEAR( {$column} ) {$compare} {$value}"; } if ( isset( $query['day'] ) && $value = $this->build_numeric_value( $compare, $query['day'] ) ) { $where_parts[] = "DAYOFMONTH( {$column} ) {$compare} {$value}"; } if ( isset( $query['dayofweek'] ) && $value = $this->build_numeric_value( $compare, $query['dayofweek'] ) ) { $where_parts[] = "DAYOFWEEK( {$column} ) {$compare} {$value}"; } if ( isset( $query['dayofweek_iso'] ) && $value = $this->build_numeric_value( $compare, $query['dayofweek_iso'] ) ) { $where_parts[] = "WEEKDAY( {$column} ) + 1 {$compare} {$value}"; } // Straight value compare if ( isset( $query['value'] ) ) { $value = $this->build_value( $compare, $query['value'] ); $where_parts[] = "{$column} {$compare} $value"; } // Hour/Minute/Second if ( isset( $query['hour'] ) || isset( $query['minute'] ) || isset( $query['second'] ) ) { // Avoid notices. foreach ( array( 'hour', 'minute', 'second' ) as $unit ) { if ( ! isset( $query[ $unit ] ) ) { $query[ $unit ] = null; } } $time_query = $this->build_time_query( $column, $compare, $query['hour'], $query['minute'], $query['second'] ); if ( ! empty( $time_query ) ) { $where_parts[] = $time_query; } } /* * Return an array of 'join' and 'where' for compatibility * with other query classes. */ return array( 'where' => $where_parts, 'join' => array(), ); } /** * Builds and validates a value string based on the comparison operator. * * @since 1.0.0 * * @param string $compare The compare operator to use * @param string|array $value The value * * @return string|false|int The value to be used in SQL or false on error. */ public function build_numeric_value( $compare = '=', $value = null ) { // Bail if null value if ( is_null( $value ) ) { return false; } switch ( $compare ) { case 'IN': case 'NOT IN': $value = (array) $value; // Remove non-numeric values. $value = array_filter( $value, 'is_numeric' ); if ( empty( $value ) ) { return false; } return '(' . implode( ',', array_map( 'intval', $value ) ) . ')'; case 'BETWEEN': case 'NOT BETWEEN': if ( ! is_array( $value ) || ( 2 !== count( $value ) ) ) { $value = array( $value, $value ); } else { $value = array_values( $value ); } // If either value is non-numeric, bail. foreach ( $value as $v ) { if ( ! is_numeric( $v ) ) { return false; } } $value = array_map( 'intval', $value ); return $value[0] . ' AND ' . $value[1]; default: if ( ! is_numeric( $value ) ) { return false; } return (int) $value; } } /** * Builds and validates a value string based on the comparison operator. * * @since 1.0.0 * * @param string $compare The compare operator to use * @param string|array $value The value * * @return string|false|int The value to be used in SQL or false on error. */ public function build_value( $compare = '=', $value = null ) { if ( in_array( $compare, $this->multi_value_keys, true ) ) { if ( ! is_array( $value ) ) { $value = preg_split( '/[,\s]+/', $value ); } } else { $value = trim( $value ); } switch ( $compare ) { case 'IN': case 'NOT IN': $compare_string = '(' . substr( str_repeat( ',%s', count( $value ) ), 1 ) . ')'; $where = $this->get_db()->prepare( $compare_string, $value ); break; case 'BETWEEN': case 'NOT BETWEEN': $value = array_slice( $value, 0, 2 ); $where = $this->get_db()->prepare( '%s AND %s', $value ); break; case 'LIKE': case 'NOT LIKE': $value = '%' . $this->get_db()->esc_like( $value ) . '%'; $where = $this->get_db()->prepare( '%s', $value ); break; // EXISTS with a value is interpreted as '='. case 'EXISTS': $compare = '='; $where = $this->get_db()->prepare( '%s', $value ); break; // 'value' is ignored for NOT EXISTS. case 'NOT EXISTS': $where = ''; break; default: $where = $this->get_db()->prepare( '%s', $value ); break; } return $where; } /** * Builds a MySQL format date/time based on some query parameters. * * You can pass an array of values (year, month, etc.) with missing parameter values being defaulted to * either the maximum or minimum values (controlled by the $default_to parameter). Alternatively you can * pass a string that will be run through strtotime(). * * @since 1.0.0 * * @param string|array $datetime An array of parameters or a strtotime() string * @param bool $default_to_max Whether to round up incomplete dates. Supported by values * of $datetime that are arrays, or string values that are a * subset of MySQL date format ('Y', 'Y-m', 'Y-m-d', 'Y-m-d H:i'). * Default: false. * @param string|int $now The current unix timestamp. * * @return string|false A MySQL format date/time or false on failure */ public function build_mysql_datetime( $datetime = '', $default_to_max = false, $now = 0 ) { // Datetime is string if ( is_string( $datetime ) ) { // Define matches so linters don't complain $matches = array(); /* * Try to parse some common date formats, so we can detect * the level of precision and support the 'inclusive' parameter. */ // Y if ( preg_match( '/^(\d{4})$/', $datetime, $matches ) ) { $datetime = array( 'year' => intval( $matches[1] ), ); // Y-m } elseif ( preg_match( '/^(\d{4})\-(\d{2})$/', $datetime, $matches ) ) { $datetime = array( 'year' => intval( $matches[1] ), 'month' => intval( $matches[2] ), ); // Y-m-d } elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2})$/', $datetime, $matches ) ) { $datetime = array( 'year' => intval( $matches[1] ), 'month' => intval( $matches[2] ), 'day' => intval( $matches[3] ), ); // Y-m-d H:i } elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2}) (\d{2}):(\d{2})$/', $datetime, $matches ) ) { $datetime = array( 'year' => intval( $matches[1] ), 'month' => intval( $matches[2] ), 'day' => intval( $matches[3] ), 'hour' => intval( $matches[4] ), 'minute' => intval( $matches[5] ), ); // Y-m-d H:i:s } elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', $datetime, $matches ) ) { $datetime = array( 'year' => intval( $matches[1] ), 'month' => intval( $matches[2] ), 'day' => intval( $matches[3] ), 'hour' => intval( $matches[4] ), 'minute' => intval( $matches[5] ), 'second' => intval( $matches[6] ), ); } } // No match; may be int or string if ( ! is_array( $datetime ) ) { // Maybe format or use as-is $datetime = ! is_int( $datetime ) ? strtotime( $datetime, $now ) : absint( $datetime ); // Return formatted return gmdate( 'Y-m-d H:i:s', $datetime ); } // Map to ints $datetime = array_map( 'absint', $datetime ); // Year if ( ! isset( $datetime['year'] ) ) { $datetime['year'] = gmdate( 'Y', $now ); } // Month if ( ! isset( $datetime['month'] ) ) { $datetime['month'] = ! empty( $default_to_max ) ? 12 : 1; } // Day if ( ! isset( $datetime['day'] ) ) { $datetime['day'] = ! empty( $default_to_max ) ? (int) gmdate( 't', gmmktime( 0, 0, 0, $datetime['month'], 1, $datetime['year'] ) ) : 1; } // Hour if ( ! isset( $datetime['hour'] ) ) { $datetime['hour'] = ! empty( $default_to_max ) ? 23 : 0; } // Minute if ( ! isset( $datetime['minute'] ) ) { $datetime['minute'] = ! empty( $default_to_max ) ? 59 : 0; } // Second if ( ! isset( $datetime['second'] ) ) { $datetime['second'] = ! empty( $default_to_max ) ? 59 : 0; } // Combine and return return sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $datetime['year'], $datetime['month'], $datetime['day'], $datetime['hour'], $datetime['minute'], $datetime['second'] ); } /** * Return a MySQL expression for selecting the week number based on the * day that the week starts. * * Uses the WordPress site option, if set. * * @since 1.0.0 * * @param string $column Database column. * @param int $start_of_week Day that week starts on. 0 = Sunday. * * @return string SQL clause. */ public function build_mysql_week( $column = '', $start_of_week = 0 ) { // When does the week start? switch ( $start_of_week ) { // Monday case 1: $retval = "WEEK( {$column}, 1 )"; break; // Tuesday - Saturday case 2: case 3: case 4: case 5: case 6: $retval = "WEEK( DATE_SUB( {$column}, INTERVAL {$start_of_week} DAY ), 0 )"; break; // Sunday case 0: default: $retval = "WEEK( {$column}, 0 )"; break; } // Return SQL return $retval; } /** * Builds a query string for comparing time values (hour, minute, second). * * If just hour, minute, or second is set than a normal comparison will be done. * However if multiple values are passed, a pseudo-decimal time will be created * in order to be able to accurately compare against. * * @since 1.0.0 * * @param string $column The column to query against. Needs to be pre-validated! * @param string $compare The comparison operator. Needs to be pre-validated! * @param int|null $hour Optional. An hour value (0-23). * @param int|null $minute Optional. A minute value (0-59). * @param int|null $second Optional. A second value (0-59). * * @return string|false A query part or false on failure. */ public function build_time_query( $column, $compare, $hour = null, $minute = null, $second = null ) { // Have to have at least one if ( ! isset( $hour ) && ! isset( $minute ) && ! isset( $second ) ) { return false; } // Complex combined queries aren't supported for multi-value queries if ( in_array( $compare, $this->multi_value_keys, true ) ) { $retval = array(); // Hour if ( isset( $hour ) && false !== ( $value = $this->build_numeric_value( $compare, $hour ) ) ) { $retval[] = "HOUR( {$column} ) {$compare} {$value}"; } // Minute if ( isset( $minute ) && false !== ( $value = $this->build_numeric_value( $compare, $minute ) ) ) { $retval[] = "MINUTE( {$column} ) {$compare} {$value}"; } // Second if ( isset( $second ) && false !== ( $value = $this->build_numeric_value( $compare, $second ) ) ) { $retval[] = "SECOND( {$column} ) {$compare} {$value}"; } return implode( ' AND ', $retval ); } // Cases where just one unit is set // Hour if ( isset( $hour ) && ! isset( $minute ) && ! isset( $second ) && false !== ( $value = $this->build_numeric_value( $compare, $hour ) ) ) { return "HOUR( {$column} ) {$compare} {$value}"; // Minute } elseif ( ! isset( $hour ) && isset( $minute ) && ! isset( $second ) && false !== ( $value = $this->build_numeric_value( $compare, $minute ) ) ) { return "MINUTE( {$column} ) {$compare} {$value}"; // Second } elseif ( ! isset( $hour ) && ! isset( $minute ) && isset( $second ) && false !== ( $value = $this->build_numeric_value( $compare, $second ) ) ) { return "SECOND( {$column} ) {$compare} {$value}"; } // Single units were already handled. Since hour & second isn't allowed, // minute must to be set. if ( ! isset( $minute ) ) { return false; } // Defaults $format = $time = ''; // Hour if ( null !== $hour ) { $format .= '%H.'; $time .= sprintf( '%02d', $hour ) . '.'; } else { $format .= '0.'; $time .= '0.'; } // Minute $format .= '%i'; $time .= sprintf( '%02d', $minute ); // Second if ( isset( $second ) ) { $format .= '%s'; $time .= sprintf( '%02d', $second ); } // Build the SQL $query = "DATE_FORMAT( {$column}, %s ) {$compare} %f"; // Return the prepared SQL return $this->get_db()->prepare( $query, $format, $time ); } /** * Test if the supplied date is valid for the Gregorian calendar. * * @since 1.0.0 * * @link https://www.php.net/manual/en/function.checkdate.php * * @param int $month Month number. * @param int $day Day number. * @param int $year Year number. * @param string $source_date The date to filter. * * @return bool True if valid date, false if not valid date. */ public function checkdate( $month = 0, $day = 0, $year = 0, $source_date = '' ) { // Check the date $retval = checkdate( $month, $day, $year ); /** * Filters whether the given date is valid for the Gregorian calendar. * * @since 1.0.0 * * @param bool $checkdate Whether the given date is valid. * @param string $source_date Date to check. */ return (bool) apply_filters( 'wp_checkdate', $retval, $source_date ); } } Dependencies/BerlinDB/Database/Queries/Compare.php 0000644 00000010601 15174677547 0016020 0 ustar 00 <?php /** * Base Custom Database Table Compare Query Class. * * @package Database * @subpackage Compare * @copyright Copyright (c) 2021 * @license https://opensource.org/licenses/MIT MIT * @since 1.0.0 */ namespace WP_Rocket\Dependencies\BerlinDB\Database\Queries; // Exit if accessed directly defined( 'ABSPATH' ) || exit; /** * Class used for generating SQL for compare clauses. * * This class is used to generate the SQL when a `compare` argument is passed to * the `Base` query class. It extends `Meta` so the `compare` key accepts * the same parameters as the ones passed to `Meta`. * * @since 1.0.0 */ class Compare extends Meta { // All supported SQL comparisons const ALL_COMPARES = array( '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'EXISTS', 'NOT EXISTS', 'REGEXP', 'NOT REGEXP', 'RLIKE', ); // IN and BETWEEN const IN_BETWEEN_COMPARES = array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ); /** * Generate SQL WHERE clauses for a first-order query clause. * * "First-order" means that it's an array with a 'key' or 'value'. * * @since 1.0.0 * * @param array $clause Query clause (passed by reference). * @param array $parent_query Parent query array. * @param string $clause_key Optional. The array key used to name the clause in the original `$meta_query` * parameters. If not provided, a key will be generated automatically. * @return array { * Array containing WHERE SQL clauses to append to a first-order query. * * @type string $where SQL fragment to append to the main WHERE clause. * } */ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) { global $wpdb; // Default chunks $sql_chunks = array( 'where' => array(), 'join' => array(), ); // Maybe format compare clause if ( isset( $clause['compare'] ) ) { $clause['compare'] = strtoupper( $clause['compare'] ); // Or set compare clause based on value } else { $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '='; } // Fallback to equals if ( ! in_array( $clause['compare'], self::ALL_COMPARES, true ) ) { $clause['compare'] = '='; } // Uppercase or equals if ( isset( $clause['compare_key'] ) && ( 'LIKE' === strtoupper( $clause['compare_key'] ) ) ) { $clause['compare_key'] = strtoupper( $clause['compare_key'] ); } else { $clause['compare_key'] = '='; } // Get comparison from clause $compare = $clause['compare']; /** Build the WHERE clause ********************************************/ // Column name and value. if ( array_key_exists( 'key', $clause ) && array_key_exists( 'value', $clause ) ) { $column = sanitize_key( $clause['key'] ); $value = $clause['value']; // IN or BETWEEN if ( in_array( $compare, self::IN_BETWEEN_COMPARES, true ) ) { if ( ! is_array( $value ) ) { $value = preg_split( '/[,\s]+/', $value ); } // Anything else } else { $value = trim( $value ); } // Format WHERE from compare value(s) switch ( $compare ) { case 'IN': case 'NOT IN': $compare_string = '(' . substr( str_repeat( ',%s', count( $value ) ), 1 ) . ')'; $where = $wpdb->prepare( $compare_string, $value ); break; case 'BETWEEN': case 'NOT BETWEEN': $value = array_slice( $value, 0, 2 ); $where = $wpdb->prepare( '%s AND %s', $value ); break; case 'LIKE': case 'NOT LIKE': $value = '%' . $wpdb->esc_like( $value ) . '%'; $where = $wpdb->prepare( '%s', $value ); break; // EXISTS with a value is interpreted as '='. case 'EXISTS': $compare = '='; $where = $wpdb->prepare( '%s', $value ); break; // 'value' is ignored for NOT EXISTS. case 'NOT EXISTS': $where = ''; break; default: $where = $wpdb->prepare( '%s', $value ); break; } // Maybe add column, compare, & where to chunks if ( ! empty( $where ) ) { $sql_chunks['where'][] = "{$column} {$compare} {$where}"; } } /* * Multiple WHERE clauses (for meta_key and meta_value) should * be joined in parentheses. */ if ( 1 < count( $sql_chunks['where'] ) ) { $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' ); } // Return return $sql_chunks; } } Dependencies/BerlinDB/Database/Schema.php 0000644 00000003605 15174677547 0014223 0 ustar 00 <?php /** * Base Custom Database Table Schema Class. * * @package Database * @subpackage Schema * @copyright Copyright (c) 2021 * @license https://opensource.org/licenses/MIT MIT * @since 1.0.0 */ namespace WP_Rocket\Dependencies\BerlinDB\Database; // Exit if accessed directly defined( 'ABSPATH' ) || exit; /** * A base database table schema class, which houses the collection of columns * that a table is made out of. * * This class is intended to be extended for each unique database table, * including global tables for multisite, and users tables. * * @since 1.0.0 */ class Schema extends Base { /** * Array of database column objects to turn into Column. * * @since 1.0.0 * @var array */ protected $columns = array(); /** * Invoke new column objects based on array of column data. * * @since 1.0.0 */ public function __construct() { // Bail if no columns if ( empty( $this->columns ) || ! is_array( $this->columns ) ) { return; } // Juggle original columns array $columns = $this->columns; $this->columns = array(); // Loop through columns and create objects from them foreach ( $columns as $column ) { if ( is_array( $column ) ) { $this->columns[] = new Column( $column ); } elseif ( $column instanceof Column ) { $this->columns[] = $column; } } } /** * Return the schema in string form. * * @since 1.0.0 * * @return string Calls get_create_string() on every column. */ protected function to_string() { // Default return value $retval = ''; // Bail if no columns to convert if ( empty( $this->columns ) ) { return $retval; } // Loop through columns... foreach ( $this->columns as $column_info ) { if ( method_exists( $column_info, 'get_create_string' ) ) { $retval .= '\n' . $column_info->get_create_string() . ', '; } } // Return the string return $retval; } } Dependencies/BerlinDB/Database/Row.php 0000644 00000002606 15174677547 0013572 0 ustar 00 <?php /** * Base Custom Database Table Row Class. * * @package Database * @subpackage Row * @copyright Copyright (c) 2021 * @license https://opensource.org/licenses/MIT MIT * @since 1.0.0 */ namespace WP_Rocket\Dependencies\BerlinDB\Database; // Exit if accessed directly defined( 'ABSPATH' ) || exit; /** * Base database row class. * * This class exists solely for other classes to extend (and to encapsulate * database schema changes for those objects) to help separate the needs of the * application layer from the requirements of the database layer. * * For example, if a database column is renamed or a return value needs to be * formatted differently, this class will make sure old values are still * supported and new values do not conflict. * * @since 1.0.0 */ class Row extends Base { /** * Construct a database object. * * @since 1.0.0 * * @param mixed Null by default, Array/Object if not */ public function __construct( $item = null ) { if ( ! empty( $item ) ) { $this->init( $item ); } } /** * Initialize class properties based on data array. * * @since 1.0.0 * * @param array $data */ private function init( $data = array() ) { $this->set_vars( $data ); } /** * Determines whether the current row exists. * * @since 1.0.0 * * @return bool */ public function exists() { return ! empty( $this->id ); } } Dependencies/BerlinDB/Database/Query.php 0000644 00000236715 15174677547 0014142 0 ustar 00 <?php /** * Base Custom Database Table Query Class. * * @package Database * @subpackage Query * @copyright Copyright (c) 2021 * @license https://opensource.org/licenses/MIT MIT * @since 1.0.0 */ namespace WP_Rocket\Dependencies\BerlinDB\Database; // Exit if accessed directly defined( 'ABSPATH' ) || exit; /** * Base class used for querying custom database tables. * * This class is intended to be extended for each unique database table, * including global tables for multisite, and users tables. * * @since 1.0.0 * * @see Query::__construct() for accepted arguments. * * @property string $prefix * @property string $table_name * @property string $table_alias * @property string $table_schema * @property string $item_name * @property string $item_name_plural * @property string $item_shape * @property string $cache_group * @property int $last_changed * @property array $columns * @property array $query_clauses * @property array $request_clauses * @property Queries\Meta $meta_query * @property Queries\Date $date_query * @property Queries\Compare $compare_query * @property array $query_vars * @property array $query_var_originals * @property array $query_var_defaults * @property string $query_var_default_value * @property array $items * @property int $found_items * @property int $max_num_pages * @property string $request */ class Query extends Base { /** Table Properties ******************************************************/ /** * Name of the database table to query. * * @since 1.0.0 * @var string */ protected $table_name = ''; /** * String used to alias the database table in MySQL statement. * * Keep this short, but descriptive. I.E. "tr" for term relationships. * * This is used to avoid collisions with JOINs. * * @since 1.0.0 * @var string */ protected $table_alias = ''; /** * Name of class used to setup the database schema. * * @since 1.0.0 * @var string */ protected $table_schema = '\\WP_Rocket\\Dependencies\\BerlinDB\\Database\\Schema'; /** Item ******************************************************************/ /** * Name for a single item. * * Use underscores between words. I.E. "term_relationship" * * This is used to automatically generate action hooks. * * @since 1.0.0 * @var string */ protected $item_name = ''; /** * Plural version for a group of items. * * Use underscores between words. I.E. "term_relationships" * * This is used to automatically generate action hooks. * * @since 1.0.0 * @var string */ protected $item_name_plural = ''; /** * Name of class used to turn IDs into first-class objects. * * This is used when looping through return values to guarantee their shape. * * @since 1.0.0 * @var mixed */ protected $item_shape = '\\WP_Rocket\\Dependencies\\BerlinDB\\Database\\Row'; /** Cache *****************************************************************/ /** * Group to cache queries and queried items in. * * Use underscores between words. I.E. "some_items" * * Do not use colons: ":". These are reserved for internal use only. * * @since 1.0.0 * @var string */ protected $cache_group = ''; /** * The last updated time. * * @since 1.0.0 * @var int */ protected $last_changed = 0; /** Columns ***************************************************************/ /** * Array of all database column objects. * * @since 1.0.0 * @var array */ protected $columns = array(); /** Clauses ***************************************************************/ /** * SQL query clauses. * * @since 1.0.0 * @var array */ protected $query_clauses = array( 'select' => '', 'from' => '', 'where' => array(), 'groupby' => '', 'orderby' => '', 'limits' => '' ); /** * Request clauses. * * @since 1.0.0 * @var array */ protected $request_clauses = array( 'select' => '', 'from' => '', 'where' => '', 'groupby' => '', 'orderby' => '', 'limits' => '' ); /** * Meta query container. * * @since 1.0.0 * @var object|Queries\Meta */ protected $meta_query = false; /** * Date query container. * * @since 1.0.0 * @var object|Queries\Date */ protected $date_query = false; /** * Compare query container. * * @since 1.0.0 * @var object|Queries\Compare */ protected $compare_query = false; /** Query Variables *******************************************************/ /** * Parsed query vars set by the application, possibly filtered and changed. * * This is specifically marked as public, to allow byref actions to change * them from outside the class methods proper and inside filter functions. * * @since 1.0.0 * @var array */ public $query_vars = array(); /** * Original query vars set by the application. * * These are the original query variables before any filters are applied, * and are the results of merging $query_var_defaults with $query_vars. * * @since 1.0.0 * @var array */ protected $query_var_originals = array(); /** * Default values for query vars. * * These are computed at runtime based on the registered columns for the * database table this query relates to. * * @since 1.0.0 * @var array */ protected $query_var_defaults = array(); /** * This private variable temporarily holds onto a random string used as the * default query var value. This is used internally when performing * comparisons, and allows for querying by falsy values. * * @since 1.1.0 * @var string */ protected $query_var_default_value = ''; /** Results ***************************************************************/ /** * List of items located by the query. * * @since 1.0.0 * @var array */ public $items = array(); /** * The amount of found items for the current query. * * @since 1.0.0 * @var int */ protected $found_items = 0; /** * The number of pages. * * @since 1.0.0 * @var int */ protected $max_num_pages = 0; /** * SQL for database query. * * @since 1.0.0 * @var string */ protected $request = ''; /** Methods ***************************************************************/ /** * Sets up the item query, based on the query vars passed. * * @since 1.0.0 * * @param string|array $query { * Optional. Array or query string of item query parameters. * Default empty. * * @type string $fields Site fields to return. Accepts 'ids' (returns an array of item IDs) * or empty (returns an array of complete item objects). Default empty. * To do a date query against a field, append the field name with _query * @type bool $count Whether to return a item count (true) or array of item objects. * Default false. * @type int $number Limit number of items to retrieve. Use 0 for no limit. * Default 100. * @type int $offset Number of items to offset the query. Used to build LIMIT clause. * Default 0. * @type bool $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query. * Default true. * @type string|array $orderby Accepts false, an empty array, or 'none' to disable `ORDER BY` clause. * Default '', to primary column ID. * @type string $order How to order retrieved items. Accepts 'ASC', 'DESC'. * Default 'DESC'. * @type string $search Search term(s) to retrieve matching items for. * Default empty. * @type array $search_columns Array of column names to be searched. * Default empty array. * @type bool $update_item_cache Whether to prime the cache for found items. * Default false. * @type bool $update_meta_cache Whether to prime the meta cache for found items. * Default false. * } */ public function __construct( $query = array() ) { // Setup $this->set_alias(); $this->set_prefix(); $this->set_columns(); $this->set_item_shape(); $this->set_query_var_defaults(); // Maybe execute a query if arguments were passed if ( ! empty( $query ) ) { $this->query( $query ); } } /** * Queries the database and retrieves items or counts. * * This method is public to allow subclasses to perform JIT manipulation * of the parameters passed into it. * * @since 1.0.0 * * @param string|array $query Array or URL query string of parameters. * @param bool $use_cache Use DB cache or not. (custom parameter added by us!) * @return array|int List of items, or number of items when 'count' is passed as a query var. */ public function query( $query = array(), bool $use_cache = true ) { $this->parse_query( $query ); return $this->get_items( $use_cache ); } /** Private Setters *******************************************************/ /** * Set the time when items were last changed. * * We set this locally to avoid inconsistencies between method calls. * * @since 1.0.0 */ private function set_last_changed() { $this->last_changed = microtime(); } /** * Set up the table alias if not already set in the class. * * This happens before prefixes are applied. * * @since 1.0.0 */ private function set_alias() { if ( empty( $this->table_alias ) ) { $this->table_alias = $this->first_letters( $this->table_name ); } } /** * Prefix table names, cache groups, and other things. * * This is to avoid conflicts with other plugins or themes that might be * doing their own things. * * @since 1.0.0 */ private function set_prefix() { $this->table_name = $this->apply_prefix( $this->table_name ); $this->table_alias = $this->apply_prefix( $this->table_alias ); $this->cache_group = $this->apply_prefix( $this->cache_group, '-' ); } /** * Set columns objects. * * @since 1.0.0 */ private function set_columns() { // Bail if no table schema if ( ! class_exists( $this->table_schema ) ) { return; } // Invoke a new table schema class $schema = new $this->table_schema; // Maybe get the column objects if ( ! empty( $schema->columns ) ) { $this->columns = $schema->columns; } } /** * Set the default item shape if none exists. * * @since 1.0.0 */ private function set_item_shape() { if ( empty( $this->item_shape ) || ! class_exists( $this->item_shape ) ) { $this->item_shape = __NAMESPACE__ . '\\Row'; } } /** * Set default query vars based on columns. * * @since 1.0.0 */ private function set_query_var_defaults() { // Default query variable value $this->query_var_default_value = function_exists( 'random_bytes' ) ? $this->apply_prefix( bin2hex( random_bytes( 18 ) ) ) : $this->apply_prefix( uniqid( '_', true ) ); // Get the primary column name $primary = $this->get_primary_column_name(); // Default query variables $this->query_var_defaults = array( 'fields' => '', 'number' => 100, 'offset' => '', 'orderby' => $primary, 'order' => 'DESC', 'groupby' => '', 'search' => '', 'search_columns' => array(), 'count' => false, // Disable SQL_CALC_FOUND_ROWS? 'no_found_rows' => true, // Queries 'meta_query' => null, // See Queries\Meta 'date_query' => null, // See Queries\Date 'compare_query' => null, // See Queries\Compare // Caching 'update_item_cache' => true, 'update_meta_cache' => true ); // Bail if no columns if ( empty( $this->columns ) ) { return; } // Direct column names $names = wp_list_pluck( $this->columns, 'name' ); foreach ( $names as $name ) { $this->query_var_defaults[ $name ] = $this->query_var_default_value; } // Possible ins $possible_ins = $this->get_columns( array( 'in' => true ), 'and', 'name' ); foreach ( $possible_ins as $in ) { $key = "{$in}__in"; $this->query_var_defaults[ $key ] = false; } // Possible not ins $possible_not_ins = $this->get_columns( array( 'not_in' => true ), 'and', 'name' ); foreach ( $possible_not_ins as $in ) { $key = "{$in}__not_in"; $this->query_var_defaults[ $key ] = false; } // Possible dates $possible_dates = $this->get_columns( array( 'date_query' => true ), 'and', 'name' ); foreach ( $possible_dates as $date ) { $key = "{$date}_query"; $this->query_var_defaults[ $key ] = false; } } /** * Set the request clauses. * * @since 1.0.0 * * @param array $clauses */ private function set_request_clauses( $clauses = array() ) { // Found rows $found_rows = empty( $this->query_vars['no_found_rows'] ) ? 'SQL_CALC_FOUND_ROWS' : ''; // Fields $fields = ! empty( $clauses['fields'] ) ? $clauses['fields'] : ''; // Join $join = ! empty( $clauses['join'] ) ? $clauses['join'] : ''; // Where $where = ! empty( $clauses['where'] ) ? "WHERE {$clauses['where']}" : ''; // Group by $groupby = ! empty( $clauses['groupby'] ) ? "GROUP BY {$clauses['groupby']}" : ''; // Order by $orderby = ! empty( $clauses['orderby'] ) ? "ORDER BY {$clauses['orderby']}" : ''; // Limits $limits = ! empty( $clauses['limits'] ) ? $clauses['limits'] : ''; // Select & From $table = $this->get_table_name(); $select = "SELECT {$found_rows} {$fields}"; $from = "FROM {$table} {$this->table_alias} {$join}"; // Put query into clauses array $this->request_clauses['select'] = $select; $this->request_clauses['from'] = $from; $this->request_clauses['where'] = $where; $this->request_clauses['groupby'] = $groupby; $this->request_clauses['orderby'] = $orderby; $this->request_clauses['limits'] = $limits; } /** * Set the request. * * @since 1.0.0 */ private function set_request() { $filtered = array_filter( $this->request_clauses ); $clauses = array_map( 'trim', $filtered ); $this->request = implode( ' ', $clauses ); } /** * Set items by mapping them through the single item callback. * * @since 1.0.0 * @param array $item_ids */ private function set_items( $item_ids = array() ) { // Bail if counting, to avoid shaping items if ( ! empty( $this->query_vars['count'] ) ) { $this->items = $item_ids; return; } // Cast to integers $item_ids = array_map( 'intval', $item_ids ); // Prime item caches $this->prime_item_caches( $item_ids ); // Shape the items $this->items = $this->shape_items( $item_ids ); } /** * Populates found_items and max_num_pages properties for the current query * if the limit clause was used. * * @since 1.0.0 * * @param array $item_ids Optional array of item IDs */ private function set_found_items( $item_ids = array() ) { // Items were not found if ( empty( $item_ids ) ) { return; } // Default to number of item IDs $this->found_items = count( (array) $item_ids ); // Count query if ( ! empty( $this->query_vars['count'] ) ) { // Not grouped if ( is_numeric( $item_ids ) && empty( $this->query_vars['groupby'] ) ) { $this->found_items = intval( $item_ids ); } // Not a count query } elseif ( is_array( $item_ids ) && ( ! empty( $this->query_vars['number'] ) && empty( $this->query_vars['no_found_rows'] ) ) ) { /** * Filters the query used to retrieve found item count. * * @since 1.0.0 * * @param string $found_items_query SQL query. Default 'SELECT FOUND_ROWS()'. * @param object $item_query The object instance. */ $found_items_query = (string) apply_filters_ref_array( $this->apply_prefix( "found_{$this->item_name_plural}_query" ), array( 'SELECT FOUND_ROWS()', &$this ) ); // Maybe query for found items if ( ! empty( $found_items_query ) ) { $this->found_items = (int) $this->get_db()->get_var( $found_items_query ); } } } /** Public Setters ********************************************************/ /** * Set a query var, to both defaults and request arrays. * * This method is used to expose the private query_vars array to hooks, * allowing them to manipulate query vars just-in-time. * * @since 1.0.0 * * @param string $key * @param string $value */ public function set_query_var( $key = '', $value = '' ) { $this->query_var_defaults[ $key ] = $value; $this->query_vars[ $key ] = $value; } /** * Check whether a query variable strictly equals the unique default * starting value. * * @since 1.1.0 * @param string $key * @return bool */ public function is_query_var_default( $key = '' ) { return (bool) ( $this->query_vars[ $key ] === $this->query_var_default_value ); } /** Private Getters *******************************************************/ /** * Pass-through method to return a new Meta object. * * @since 1.0.0 * * @param array $args See Queries\Meta * * @return Queries\Meta */ private function get_meta_query( $args = array() ) { return new Queries\Meta( $args ); } /** * Pass-through method to return a new Compare object. * * @since 1.0.0 * * @param array $args See Queries\Compare * * @return Queries\Compare */ private function get_compare_query( $args = array() ) { return new Queries\Compare( $args ); } /** * Pass-through method to return a new Queries\Date object. * * @since 1.0.0 * * @param array $args See Queries\Date * * @return Queries\Date */ private function get_date_query( $args = array() ) { return new Queries\Date( $args ); } /** * Return the current time as a UTC timestamp. * * This is used by add_item() and update_item() * * @since 1.0.0 * * @return string */ private function get_current_time() { return gmdate( "Y-m-d\TH:i:s\Z" ); } /** * Return the literal table name (with prefix) from the database interface. * * @since 1.0.0 * * @return string */ private function get_table_name() { return $this->get_db()->{$this->table_name}; } /** * Return array of column names. * * @since 1.0.0 * * @return array */ private function get_column_names() { return array_flip( $this->get_columns( array(), 'and', 'name' ) ); } /** * Return the primary database column name. * * @since 1.0.0 * * @return string Default "id", Primary column name if not empty */ private function get_primary_column_name() { return $this->get_column_field( array( 'primary' => true ), 'name', 'id' ); } /** * Get a column from an array of arguments. * * @since 1.0.0 * * @param array $args Arguments to get a column by. * @param string $field Field to get from a column. * @param mixed $default Default to use if no field is set. * @return mixed Column object, or false */ private function get_column_field( $args = array(), $field = '', $default = false ) { // Get the column $column = $this->get_column_by( $args ); // Return field, or default return isset( $column->{$field} ) ? $column->{$field} : $default; } /** * Get a column from an array of arguments. * * @since 1.0.0 * * @param array $args Arguments to get a column by. * @return mixed Column object, or false */ private function get_column_by( $args = array() ) { // Filter columns $filter = $this->get_columns( $args ); // Return column or false return ! empty( $filter ) ? reset( $filter ) : false; } /** * Get columns from an array of arguments. * * @since 1.0.0 * * @param array $args Arguments to filter columns by. * @param string $operator Optional. The logical operation to perform. * @param string $field Optional. A field from the object to place * instead of the entire object. Default false. * @return array Array of column. */ private function get_columns( $args = array(), $operator = 'and', $field = false ) { // Filter columns $filter = wp_filter_object_list( $this->columns, $args, $operator, $field ); // Return column or false return ! empty( $filter ) ? array_values( $filter ) : array(); } /** * Get a single database row by any column and value, skipping cache. * * @since 1.0.0 * * @param string $column_name Name of database column * @param string $column_value Value to query for * @return object|false False if empty/error, Object if successful */ private function get_item_raw( $column_name = '', $column_value = '' ) { // Bail if no name or value if ( empty( $column_name ) || empty( $column_value ) ) { return false; } // Bail if values aren't query'able if ( ! is_string( $column_name ) || ! is_scalar( $column_value ) ) { return false; } // Get query parts $table = $this->get_table_name(); $pattern = $this->get_column_field( array( 'name' => $column_name ), 'pattern', '%s' ); // Query database $query = "SELECT * FROM {$table} WHERE {$column_name} = {$pattern} LIMIT 1"; $select = $this->get_db()->prepare( $query, $column_value ); $result = $this->get_db()->get_row( $select ); // Bail on failure if ( ! $this->is_success( $result ) ) { return false; } // Return row return $result; } /** * Retrieves a list of items matching the query vars. * * @since 1.0.0 * * @param bool $use_cache Use DB cache or not. (custom parameter added by us!) * * @return array|int List of items, or number of items when 'count' is passed as a query var. */ private function get_items( bool $use_cache = true ) { /** * Fires before object items are retrieved. * * @since 1.0.0 * * @param Query &$this Current instance of Query, passed by reference. */ do_action_ref_array( $this->apply_prefix( "pre_get_{$this->item_name_plural}" ), array( &$this ) ); // Never limit, never update item/meta caches when counting if ( ! empty( $this->query_vars['count'] ) ) { $this->query_vars['number'] = false; $this->query_vars['no_found_rows'] = true; $this->query_vars['update_item_cache'] = false; $this->query_vars['update_meta_cache'] = false; } // Check the cache $cache_key = $this->get_cache_key(); $cache_value = $use_cache ? $this->cache_get( $cache_key, $this->cache_group ) : false; // No cache value if ( false === $cache_value ) { $item_ids = $this->get_item_ids(); // Set the number of found items $this->set_found_items( $item_ids ); // Format the cached value if ( $use_cache ) { // Format the cached value $cache_value = array( 'item_ids' => $item_ids, 'found_items' => intval( $this->found_items ), ); // Add value to the cache $this->cache_add( $cache_key, $cache_value, $this->cache_group ); } // Value exists in cache } else { $item_ids = $cache_value['item_ids']; $this->found_items = intval( $cache_value['found_items'] ); } // Pagination if ( ! empty( $this->found_items ) && ! empty( $this->query_vars['number'] ) ) { $this->max_num_pages = ceil( $this->found_items / $this->query_vars['number'] ); } // Cast to int if not grouping counts if ( ! empty( $this->query_vars['count'] ) && empty( $this->query_vars['groupby'] ) ) { $item_ids = intval( $item_ids ); } // Set items from IDs $this->set_items( $item_ids ); // Return array of items return $this->items; } /** * Used internally to get a list of item IDs matching the query vars. * * @since 1.0.0 * * @return int|array A single count of item IDs if a count query. An array * of item IDs if a full query. */ private function get_item_ids() { // Setup primary column, and parse the where clause $this->parse_where(); // Order & Order By $order = $this->parse_order( $this->query_vars['order'] ); $orderby = $this->get_order_by( $order ); // Limit & Offset $limit = absint( $this->query_vars['number'] ); $offset = absint( $this->query_vars['offset'] ); // Limits if ( ! empty( $limit ) ) { $limits = ! empty( $offset ) ? "LIMIT {$offset}, {$limit}" : "LIMIT {$limit}"; } else { $limits = ''; } // Where & Join $where = implode( ' AND ', $this->query_clauses['where'] ); $join = implode( ', ', $this->query_clauses['join'] ); // Group by $groupby = $this->parse_groupby( $this->query_vars['groupby'] ); // Fields $fields = $this->parse_fields( $this->query_vars['fields'] ); // Setup the query array (compact() is too opaque here) $query = array( 'fields' => $fields, 'join' => $join, 'where' => $where, 'orderby' => $orderby, 'limits' => $limits, 'groupby' => $groupby ); /** * Filters the item query clauses. * * @since 1.0.0 * * @param array $pieces A compacted array of item query clauses. * @param Query &$this Current instance passed by reference. */ $clauses = (array) apply_filters_ref_array( $this->apply_prefix( "{$this->item_name_plural}_query_clauses" ), array( $query, &$this ) ); // Setup request $this->set_request_clauses( $clauses ); $this->set_request(); // Return count if ( ! empty( $this->query_vars['count'] ) ) { // Get vars or results $retval = empty( $this->query_vars['groupby'] ) ? $this->get_db()->get_var( $this->request ) : $this->get_db()->get_results( $this->request, ARRAY_A ); // Return vars or results return $retval; } // Get IDs $item_ids = $this->get_db()->get_col( $this->request ); // Return parsed IDs return wp_parse_id_list( $item_ids ); } /** * Get the ORDERBY clause. * * @since 1.0.0 * * @param string $order * @return string */ private function get_order_by( $order = '' ) { // Default orderby primary column $parsed = $this->parse_orderby(); $orderby = "{$parsed} {$order}"; // Disable ORDER BY if counting, or: 'none', an empty array, or false. if ( ! empty( $this->query_vars['count'] ) || in_array( $this->query_vars['orderby'], array( 'none', array(), false ), true ) ) { $orderby = ''; // Ordering by something, so figure it out } elseif ( ! empty( $this->query_vars['orderby'] ) ) { // Array of keys, or comma separated $ordersby = is_array( $this->query_vars['orderby'] ) ? $this->query_vars['orderby'] : preg_split( '/[,\s]/', $this->query_vars['orderby'] ); $orderby_array = array(); $possible_ins = $this->get_columns( array( 'in' => true ), 'and', 'name' ); $sortables = $this->get_columns( array( 'sortable' => true ), 'and', 'name' ); // Loop through possible order by's foreach ( $ordersby as $_key => $_value ) { // Skip if empty if ( empty( $_value ) ) { continue; } // Key is numeric if ( is_int( $_key ) ) { $_orderby = $_value; $_item = $order; // Key is string } else { $_orderby = $_key; $_item = $_value; } // Skip if not sortable if ( ! in_array( $_value, $sortables, true ) ) { continue; } // Parse orderby $parsed = $this->parse_orderby( $_orderby ); // Skip if empty if ( empty( $parsed ) ) { continue; } // Set if __in if ( in_array( $_orderby, $possible_ins, true ) ) { $orderby_array[] = "{$parsed} {$order}"; continue; } // Append parsed orderby to array $orderby_array[] = $parsed . ' ' . $this->parse_order( $_item ); } // Only set if valid orderby if ( ! empty( $orderby_array ) ) { $orderby = implode( ', ', $orderby_array ); } } // Return parsed orderby return $orderby; } /** * Used internally to generate an SQL string for searching across multiple * columns. * * @since 1.0.0 * * @param string $string Search string. * @param array $columns Columns to search. * @return string Search SQL. */ private function get_search_sql( $string = '', $columns = array() ) { // Array or String $like = ( false !== strpos( $string, '*' ) ) ? '%' . implode( '%', array_map( array( $this->get_db(), 'esc_like' ), explode( '*', $string ) ) ) . '%' : '%' . $this->get_db()->esc_like( $string ) . '%'; // Default array $searches = array(); // Build search SQL foreach ( $columns as $column ) { $searches[] = $this->get_db()->prepare( "{$column} LIKE %s", $like ); } // Return the clause return '(' . implode( ' OR ', $searches ) . ')'; } /** Private Parsers *******************************************************/ /** * Parses arguments passed to the item query with default query parameters. * * @since 1.0.0 * * @see Query::__construct() * * @param string|array $query Array or string of Query arguments. */ private function parse_query( $query = array() ) { // Setup the query_vars_original var $this->query_var_originals = wp_parse_args( $query ); // Setup the query_vars parsed var $this->query_vars = wp_parse_args( $this->query_var_originals, $this->query_var_defaults ); /** * Fires after the item query vars have been parsed. * * @since 1.0.0 * * @param Query &$this The Query instance (passed by reference). */ do_action_ref_array( $this->apply_prefix( "parse_{$this->item_name_plural}_query" ), array( &$this ) ); } /** * Parse the where clauses for all known columns. * * @todo split this method into smaller parts * * @since 1.0.0 */ private function parse_where() { // Defaults $where = $join = $searchable = $date_query = array(); // Loop through columns foreach ( $this->columns as $column ) { // Maybe add name to searchable array if ( true === $column->searchable ) { $searchable[] = $column->name; } // Literal column comparison if ( ! $this->is_query_var_default( $column->name ) ) { // Array (unprepared) if ( is_array( $this->query_vars[ $column->name ] ) ) { $where_id = "'" . implode( "', '", $this->get_db()->_escape( $this->query_vars[ $column->name ] ) ) . "'"; $statement = "{$this->table_alias}.{$column->name} IN ({$where_id})"; // Add to where array $where[ $column->name ] = $statement; // Numeric/String/Float (prepared) } else { $pattern = $this->get_column_field( array( 'name' => $column->name ), 'pattern', '%s' ); $where_id = $this->query_vars[ $column->name ]; $statement = "{$this->table_alias}.{$column->name} = {$pattern}"; // Add to where array $where[ $column->name ] = $this->get_db()->prepare( $statement, $where_id ); } } // __in if ( true === $column->in ) { $where_id = "{$column->name}__in"; // Parse item for an IN clause. if ( isset( $this->query_vars[ $where_id ] ) && is_array( $this->query_vars[ $where_id ] ) ) { // Convert single item arrays to literal column comparisons if ( 1 === count( $this->query_vars[ $where_id ] ) ) { $column_value = reset( $this->query_vars[ $where_id ] ); $statement = "{$this->table_alias}.{$column->name} = %s"; $where[ $column->name ] = $this->get_db()->prepare( $statement, $column_value ); // Implode } else { $where[ $where_id ] = "{$this->table_alias}.{$column->name} IN ( '" . implode( "', '", $this->get_db()->_escape( $this->query_vars[ $where_id ] ) ) . "' )"; } } } // __not_in if ( true === $column->not_in ) { $where_id = "{$column->name}__not_in"; // Parse item for a NOT IN clause. if ( isset( $this->query_vars[ $where_id ] ) && is_array( $this->query_vars[ $where_id ] ) ) { // Convert single item arrays to literal column comparisons if ( 1 === count( $this->query_vars[ $where_id ] ) ) { $column_value = reset( $this->query_vars[ $where_id ] ); $statement = "{$this->table_alias}.{$column->name} != %s"; $where[ $column->name ] = $this->get_db()->prepare( $statement, $column_value ); // Implode } else { $where[ $where_id ] = "{$this->table_alias}.{$column->name} NOT IN ( '" . implode( "', '", $this->get_db()->_escape( $this->query_vars[ $where_id ] ) ) . "' )"; } } } // date_query if ( true === $column->date_query ) { $where_id = "{$column->name}_query"; $column_date = $this->query_vars[ $where_id ]; // Parse item if ( ! empty( $column_date ) ) { // Default arguments $defaults = array( 'column' => "{$this->table_alias}.{$column->name}", 'before' => $column_date, 'inclusive' => true ); // Default date query if ( is_string( $column_date ) ) { $date_query[] = $defaults; // Array query var } elseif ( is_array( $column_date ) ) { // Auto-fill column if empty if ( empty( $column_date['column'] ) ) { $column_date['column'] = $defaults['column']; } // Add clause to date query $date_query[] = $column_date; } } } } // Maybe search if columns are searchable. if ( ! empty( $searchable ) && strlen( $this->query_vars['search'] ) ) { $search_columns = array(); // Intersect against known searchable columns if ( ! empty( $this->query_vars['search_columns'] ) ) { $search_columns = array_intersect( $this->query_vars['search_columns'], $searchable ); } // Default to all searchable columns if ( empty( $search_columns ) ) { $search_columns = $searchable; } /** * Filters the columns to search in a Query search. * * @since 1.0.0 * * @param array $search_columns Array of column names to be searched. * @param string $search Text being searched. * @param object $this The current Query instance. */ $search_columns = (array) apply_filters( $this->apply_prefix( "{$this->item_name_plural}_search_columns" ), $search_columns, $this->query_vars['search'], $this ); // Add search query clause $where['search'] = $this->get_search_sql( $this->query_vars['search'], $search_columns ); } /** Query Classes *****************************************************/ // Get the primary column name $primary = $this->get_primary_column_name(); // Get the meta table $table = $this->get_meta_type(); // Set the " AND " regex pattern $and = '/^\s*AND\s*/'; // Maybe perform a meta query. $meta_query = $this->query_vars['meta_query']; if ( ! empty( $meta_query ) && is_array( $meta_query ) ) { $this->meta_query = $this->get_meta_query( $meta_query ); $clauses = $this->meta_query->get_sql( $table, $this->table_alias, $primary, $this ); // Not all objects have meta, so make sure this one exists if ( false !== $clauses ) { // Set join if ( ! empty( $clauses['join'] ) ) { $join['meta_query'] = $clauses['join']; } // Set where if ( ! empty( $clauses['where'] ) ) { // Remove " AND " from query query where clause $where['meta_query'] = preg_replace( $and, '', $clauses['where'] ); } } } // Maybe perform a compare query. $compare_query = $this->query_vars['compare_query']; if ( ! empty( $compare_query ) && is_array( $compare_query ) ) { $this->compare_query = $this->get_compare_query( $compare_query ); $clauses = $this->compare_query->get_sql( $table, $this->table_alias, $primary, $this ); // Not all objects can compare, so make sure this one exists if ( false !== $clauses ) { // Set join if ( ! empty( $clauses['join'] ) ) { $join['compare_query'] = $clauses['join']; } // Set where if ( ! empty( $clauses['where'] ) ) { // Remove " AND " from query where clause. $where['compare_query'] = preg_replace( $and, '', $clauses['where'] ); } } } // Only do a date query with an array $date_query = ! empty( $date_query ) ? $date_query : $this->query_vars['date_query']; // Maybe perform a date query if ( ! empty( $date_query ) && is_array( $date_query ) ) { $this->date_query = $this->get_date_query( $date_query ); $clauses = $this->date_query->get_sql( $this->table_name, $this->table_alias, $primary, $this ); // Not all objects are dates, so make sure this one exists if ( false !== $clauses ) { // Set join if ( ! empty( $clauses['join'] ) ) { $join['date_query'] = $clauses['join']; } // Set where if ( ! empty( $clauses['where'] ) ) { // Remove " AND " from query where clause. $where['date_query'] = preg_replace( $and, '', $clauses['where'] ); } } } // Set where and join clauses, removing possible empties $this->query_clauses['where'] = array_filter( $where ); $this->query_clauses['join'] = array_filter( $join ); } /** * Parse which fields to query for. * * @since 1.0.0 * * @param string $fields * @param bool $alias * @return string */ private function parse_fields( $fields = '', $alias = true ) { // Get the primary column name $primary = $this->get_primary_column_name(); // Default return value $retval = ( true === $alias ) ? "{$this->table_alias}.{$primary}" : $primary; // No fields if ( empty( $fields ) && ! empty( $this->query_vars['count'] ) ) { // Possible fields to group by $groupby_names = $this->parse_groupby( $this->query_vars['groupby'], $alias ); $groupby_names = ! empty( $groupby_names ) ? "{$groupby_names}" : ''; // Group by or total count $retval = ! empty( $groupby_names ) ? "{$groupby_names}, COUNT(*) as count" : 'COUNT(*)'; } // Return fields (or COUNT) return $retval; } /** * Parses and sanitizes the 'groupby' keys passed into the item query. * * @since 1.0.0 * * @param string $groupby * @param bool $alias * @return string */ private function parse_groupby( $groupby = '', $alias = true ) { // Bail if empty if ( empty( $groupby ) ) { return ''; } // Sanitize groupby columns $groupby = (array) array_map( 'sanitize_key', (array) $groupby ); // Re'flip column names back around $columns = array_flip( $this->get_column_names() ); // Get the intersection of allowed column names to groupby columns $intersect = array_intersect( $columns, $groupby ); // Bail if invalid column if ( empty( $intersect ) ) { return ''; } // Default return value $retval = array(); // Maybe prepend table alias to key foreach ( $intersect as $key ) { $retval[] = ( true === $alias ) ? "{$this->table_alias}.{$key}" : $key; } // Separate sanitized columns return implode( ',', array_values( $retval ) ); } /** * Parses and sanitizes 'orderby' keys passed to the item query. * * @since 1.0.0 * * @param string $orderby Field for the items to be ordered by. * @return string|false Value to used in the ORDER clause. False otherwise. */ private function parse_orderby( $orderby = '' ) { // Get the primary column name $primary = $this->get_primary_column_name(); // Default return value $parsed = "{$this->table_alias}.{$primary}"; // Default to primary column if ( empty( $orderby ) ) { $orderby = $primary; } // __in if ( false !== strstr( $orderby, '__in' ) ) { $column_name = str_replace( '__in', '', $orderby ); $column = $this->get_column_by( array( 'name' => $column_name ) ); $item_in = $column->is_numeric() ? implode( ',', array_map( 'absint', $this->query_vars[ $orderby ] ) ) : implode( ',', $this->query_vars[ $orderby ] ); $parsed = "FIELD( {$this->table_alias}.{$column->name}, {$item_in} )"; // Specific column } else { // Orderby is a literal, sortable column name $sortables = $this->get_columns( array( 'sortable' => true ), 'and', 'name' ); if ( in_array( $orderby, $sortables, true ) ) { $parsed = "{$this->table_alias}.{$orderby}"; } } // Return parsed value return $parsed; } /** * Parses an 'order' query variable and cast it to 'ASC' or 'DESC' as * necessary. * * @since 1.0.0 * * @param string $order The 'order' query variable. * @return string The sanitized 'order' query variable. */ private function parse_order( $order = '' ) { // Bail if malformed if ( empty( $order ) || ! is_string( $order ) ) { return 'DESC'; } // Ascending or Descending return ( 'ASC' === strtoupper( $order ) ) ? 'ASC' : 'DESC'; } /** Private Shapers *******************************************************/ /** * Shape items into their most relevant objects. * * This will try to use item_shape, but will fallback to a private * method for querying and caching items. * * If using the `fields` parameter, results will have unique shapes based on * exactly what was requested. * * @since 1.0.0 * * @param array $items * @return array */ private function shape_items( $items = array() ) { // Force to stdClass if querying for fields if ( ! empty( $this->query_vars['fields'] ) ) { $this->item_shape = 'stdClass'; } // Default return value $retval = array(); // Use foreach because it's faster than array_map() if ( ! empty( $items ) ) { foreach ( $items as $item ) { $retval[] = $this->get_item( $item ); } } /** * Filters the object query results. * * Looks like `edd_get_customers` * * @since 1.0.0 * * @param array $retval An array of items. * @param object &$this Current instance of Query, passed by reference. */ $retval = (array) apply_filters_ref_array( $this->apply_prefix( "the_{$this->item_name_plural}" ), array( $retval, &$this ) ); // Return filtered results return ! empty( $this->query_vars['fields'] ) ? $this->get_item_fields( $retval ) : $retval; } /** * Get specific item fields based on query_vars['fields']. * * @since 1.0.0 * * @param array $items * @return array */ private function get_item_fields( $items = array() ) { // Get the primary column name $primary = $this->get_primary_column_name(); // Get the query var fields $fields = $this->query_vars['fields']; // Strings need to be single columns if ( is_string( $fields ) ) { $field = sanitize_key( $fields ); $items = ( 'ids' === $fields ) ? wp_list_pluck( $items, $primary ) : wp_list_pluck( $items, $field, $primary ); // Arrays could be anything } elseif ( is_array( $fields ) ) { $new_items = array(); $fields = array_flip( $fields ); // Loop through items and pluck out the fields foreach ( $items as $item_id => $item ) { $new_items[ $item_id ] = (object) array_intersect_key( (array) $item, $fields ); } // Set the items and unset the new items $items = $new_items; unset( $new_items ); } // Return the item, possibly reduced return $items; } /** * Shape an item ID from an object, array, or numeric value. * * @since 1.0.0 * * @param mixed $item * @return int */ private function shape_item_id( $item = 0 ) { // Default return value $retval = 0; // Get the primary column name $primary = $this->get_primary_column_name(); // Numeric item ID if ( is_numeric( $item ) ) { $retval = $item; // Object item } elseif ( is_object( $item ) && isset( $item->{$primary} ) ) { $retval = $item->{$primary}; // Array item } elseif ( is_array( $item ) && isset( $item[ $primary ] ) ) { $retval = $item[ $primary ]; } // Return the item ID return absint( $retval ); } /** Queries ***************************************************************/ /** * Get a single database row by the primary column ID, possibly from cache. * * Accepts an integer, object, or array, and attempts to get the ID from it, * then attempts to retrieve that item fresh from the database or cache. * * @since 1.0.0 * * @param int|array|object $item_id The ID of the item * @return object|false False if empty/error, Object if successful */ public function get_item( $item_id = 0 ) { // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if no item to get by if ( empty( $item_id ) ) { return false; } // Get the primary column name $primary = $this->get_primary_column_name(); // Get item by ID return $this->get_item_by( $primary, $item_id ); } /** * Get a single database row by any column and value, possibly from cache. * * Take care to only use this method on columns with unique values, * preferably with a cache group for that column. See: get_item(). * * @since 1.0.0 * * @param string $column_name Name of database column * @param int|string $column_value Value to query for * @return object|false False if empty/error, Object if successful */ public function get_item_by( $column_name = '', $column_value = '' ) { // Default return value $retval = false; // Bail if no key or value if ( empty( $column_name ) || empty( $column_value ) ) { return $retval; } // Bail if name is not a string if ( ! is_string( $column_name ) ) { return $retval; } // Bail if value is not scalar (null values also not allowed) if ( ! is_scalar( $column_value ) ) { return $retval; } // Get all of the column names $columns = $this->get_column_names(); // Bail if column does not exist if ( ! isset( $columns[ $column_name ] ) ) { return $retval; } // Get all of the cache groups $groups = $this->get_cache_groups(); // Check cache if ( ! empty( $groups[ $column_name ] ) ) { $retval = $this->cache_get( $column_value, $groups[ $column_name ] ); } // Item not cached if ( false === $retval ) { // Get item by column name & value (from database, not cache) $retval = $this->get_item_raw( $column_name, $column_value ); // Bail on failure if ( ! $this->is_success( $retval ) ) { return false; } // Update item cache(s) $this->update_item_cache( $retval ); } // Reduce the item $retval = $this->reduce_item( 'select', $retval ); // Return result return $this->shape_item( $retval ); } /** * Add an item to the database. * * @since 1.0.0 * * @param array $data * @return bool */ public function add_item( $data = array() ) { // Get the primary column name $primary = $this->get_primary_column_name(); // If data includes primary column, check if item already exists if ( ! empty( $data[ $primary ] ) ) { // Shape the primary item ID $item_id = $this->shape_item_id( $data[ $primary ] ); // Get item by ID (from database, not cache) $item = $this->get_item_raw( $primary, $item_id ); // Bail if item already exists if ( ! empty( $item ) ) { return false; } // Set data primary ID to newly shaped ID $data[ $primary ] = $item_id; } // Get default values for item (from columns) $item = $this->default_item(); // Unset the primary key if not part of data array (auto-incremented) if ( empty( $data[ $primary ] ) ) { unset( $item[ $primary ] ); } // Cut out non-keys for meta $columns = $this->get_column_names(); $data = array_merge( $item, $data ); $meta = array_diff_key( $data, $columns ); $save = array_intersect_key( $data, $columns ); // Bail if nothing to save if ( empty( $save ) && empty( $meta ) ) { return false; } // Get the current time (maybe used by created/modified) $time = $this->get_current_time(); // If date-created exists, but is empty or default, use the current time $created = $this->get_column_by( array( 'created' => true ) ); if ( ! empty( $created ) && ( empty( $save[ $created->name ] ) || ( $save[ $created->name ] === $created->default ) ) ) { $save[ $created->name ] = $time; } // If date-modified exists, but is empty or default, use the current time $modified = $this->get_column_by( array( 'modified' => true ) ); if ( ! empty( $modified ) && ( empty( $save[ $modified->name ] ) || ( $save[ $modified->name ] === $modified->default ) ) ) { $save[ $modified->name ] = $time; } // Try to add $table = $this->get_table_name(); $reduce = $this->reduce_item( 'insert', $save ); $save = $this->validate_item( $reduce ); $result = ! empty( $save ) ? $this->get_db()->insert( $table, $save ) : false; // Bail on failure if ( ! $this->is_success( $result ) ) { return false; } // Get the new item ID $item_id = $this->get_db()->insert_id; // Maybe save meta keys if ( ! empty( $meta ) ) { $this->save_extra_item_meta( $item_id, $meta ); } // Update item cache(s) $this->update_item_cache( $item_id ); // Transition item data $this->transition_item( $save, array(), $item_id ); // Return result return $item_id; } /** * Copy an item in the database to a new item. * * @since 1.1.0 * * @param int $item_id * @param array $data * @return bool */ public function copy_item( $item_id = 0, $data = array() ) { // Get the primary column name $primary = $this->get_primary_column_name(); // Get item by ID (from database, not cache) $item = $this->get_item_raw( $primary, $item_id ); // Bail if item does not exist if ( empty( $item ) ) { return false; } // Cast object to array $save = (array) $item; // Maybe merge data with original item if ( ! empty( $data ) && is_array( $data ) ) { $save = array_merge( $save, $data ); } // Unset the primary key unset( $save[ $primary ] ); // Return result return $this->add_item( $save ); } /** * Update an item in the database. * * @since 1.0.0 * * @param int $item_id * @param array $data * @return bool */ public function update_item( $item_id = 0, $data = array() ) { // Bail early if no data to update if ( empty( $data ) ) { return false; } // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if no item ID if ( empty( $item_id ) ) { return false; } // Get the primary column name $primary = $this->get_primary_column_name(); // Get item to update (from database, not cache) $item = $this->get_item_raw( $primary, $item_id ); // Bail if item does not exist to update if ( empty( $item ) ) { return false; } // Cast as an array for easier manipulation $item = (array) $item; // Unset the primary key from item & data unset( $data[ $primary ], $item[ $primary ] ); // Slice data that has columns, and cut out non-keys for meta $columns = $this->get_column_names(); $data = array_diff_assoc( $data, $item ); $meta = array_diff_key( $data, $columns ); $save = array_intersect_key( $data, $columns ); // Maybe save meta keys if ( ! empty( $meta ) ) { $this->save_extra_item_meta( $item_id, $meta ); } // Bail if nothing to save if ( empty( $save ) ) { return false; } // If date-modified exists, use the current time $modified = $this->get_column_by( array( 'modified' => true ) ); if ( ! empty( $modified ) ) { $save[ $modified->name ] = $this->get_current_time(); } // Try to update $table = $this->get_table_name(); $reduce = $this->reduce_item( 'update', $save ); $save = $this->validate_item( $reduce ); $where = array( $primary => $item_id ); $result = ! empty( $save ) ? $this->get_db()->update( $table, $save, $where ) : false; // Bail on failure if ( ! $this->is_success( $result ) ) { return false; } // Update item cache(s) $this->update_item_cache( $item_id ); // Transition item data $this->transition_item( $save, $item, $item_id ); // Return result return $result; } /** * Delete an item from the database. * * @since 1.0.0 * * @param int $item_id * @return bool */ public function delete_item( $item_id = 0 ) { // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if no item ID if ( empty( $item_id ) ) { return false; } // Get the primary column name $primary = $this->get_primary_column_name(); // Get item by ID (from database, not cache) $item = $this->get_item_raw( $primary, $item_id ); // Bail if item does not exist to delete if ( empty( $item ) ) { return false; } // Attempt to reduce this item $item = $this->reduce_item( 'delete', $item ); // Bail if item was reduced to nothing if ( empty( $item ) ) { return false; } // Try to delete $table = $this->get_table_name(); $where = array( $primary => $item_id ); $result = $this->get_db()->delete( $table, $where ); // Bail on failure if ( ! $this->is_success( $result ) ) { return false; } // Clean caches on successful delete $this->delete_all_item_meta( $item_id ); $this->clean_item_cache( $item ); // Return result return $result; } /** * Filter an item before it is inserted of updated in the database. * * This method is public to allow subclasses to perform JIT manipulation * of the parameters passed into it. * * @since 1.0.0 * * @param array $item * @return array */ public function filter_item( $item = array() ) { return (array) apply_filters_ref_array( $this->apply_prefix( "filter_{$this->item_name}_item" ), array( $item, &$this ) ); } /** * Shape an item from the database into the type of object it always wanted * to be when it grew up. * * @since 1.0.0 * * @param mixed ID of item, or row from database * @return mixed False on error, Object of single-object class type on success */ private function shape_item( $item = 0 ) { // Get the item from an ID if ( is_numeric( $item ) ) { $item = $this->get_item( $item ); } // Return the item if it's already shaped if ( $item instanceof $this->item_shape ) { return $item; } // Shape the item as needed $item = ! empty( $this->item_shape ) ? new $this->item_shape( $item ) : (object) $item; // Return the item object return $item; } /** * Validate an item before it is updated in or added to the database. * * @since 1.0.0 * * @param array $item * @return array|false False on error, Array of validated values on success */ private function validate_item( $item = array() ) { // Bail if item is empty or not an array if ( empty( $item ) || ! is_array( $item ) ) { return $item; } // Loop through item attributes foreach ( $item as $key => $value ) { // Get the column $column = $this->get_column_by( array( 'name' => $key ) ); // Null value is special for all item keys if ( is_null( $value ) ) { // Bail if null is not allowed if ( false === $column->allow_null ) { return false; } // Attempt to validate } elseif ( ! empty( $column->validate ) && is_callable( $column->validate ) ) { $validated = call_user_func( $column->validate, $value ); // Bail if error if ( is_wp_error( $validated ) ) { return false; } // Update the value $item[ $key ] = $validated; /** * Fallback to using the raw value. * * Note: This may change at a later date, so do not rely on this. * Please always validate all data. */ } else { $item[ $key ] = $value; } } // Return the validated item return $this->filter_item( $item ); } /** * Reduce an item down to the keys and values the current user has the * appropriate capabilities to select|insert|update|delete. * * Note that internally, this method works with both arrays and objects of * any type, and also resets the key values. It looks weird, but is * currently by design to protect the integrity of the return value. * * @since 1.0.0 * * @param string $method select|insert|update|delete * @param mixed $item Object|Array of keys/values to reduce * * @return mixed Object|Array without keys the current user does not have caps for */ private function reduce_item( $method = 'update', $item = array() ) { // Bail if item is empty if ( empty( $item ) ) { return $item; } // Loop through item attributes foreach ( $item as $key => $value ) { // Get capabilities for this column $caps = $this->get_column_field( array( 'name' => $key ), 'caps' ); // Unset if not explicitly allowed if ( empty( $caps[ $method ] ) || ! current_user_can( $caps[ $method ] ) ) { if ( is_array( $item ) ) { unset( $item[ $key ] ); } elseif ( is_object( $item ) ) { $item->{$key} = null; } // Set if explicitly allowed } elseif ( is_array( $item ) ) { $item[ $key ] = $value; } elseif ( is_object( $item ) ) { $item->{$key} = $value; } } // Return the reduced item return $item; } /** * Return an item comprised of all default values. * * This is used by `add_item()` to populate known default values, to ensure * new item data is always what we expect it to be. * * @since 1.0.0 * * @return array */ private function default_item() { // Default return value $retval = array(); // Get the column names and their defaults $names = $this->get_columns( array(), 'and', 'name' ); $defaults = $this->get_columns( array(), 'and', 'default' ); // Put together an item using default values foreach ( $names as $key => $name ) { $retval[ $name ] = $defaults[ $key ]; } // Return return $retval; } /** * Transition an item when adding or updating. * * This method takes the data being saved, looks for any columns that are * known to transition between values, and fires actions on them. * * @since 1.0.0 * * @param array $new_data * @param array $old_data * @param int $item_id * @return array */ private function transition_item( $new_data = array(), $old_data = array(), $item_id = 0 ) { // Look for transition columns $columns = $this->get_columns( array( 'transition' => true ), 'and', 'name' ); // Bail if no columns to transition if ( empty( $columns ) ) { return; } // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if no item ID if ( empty( $item_id ) ) { return; } // If no old value(s), it's new if ( empty( $old_data ) || ! is_array( $old_data ) ) { $old_data = $new_data; // Set all old values to "new" foreach ( $old_data as $key => $value ) { $value = 'new'; $old_data[ $key ] = $value; } } // Compare $keys = array_flip( $columns ); $new = array_intersect_key( $new_data, $keys ); $old = array_intersect_key( $old_data, $keys ); // Get the difference $diff = array_diff( $new, $old ); // Bail if nothing is changing if ( empty( $diff ) ) { return; } // Do the actions foreach ( $diff as $key => $value ) { $old_value = $old_data[ $key ]; $new_value = $new_data[ $key ]; $key_action = $this->apply_prefix( "transition_{$this->item_name}_{$key}" ); /** * Fires after an object value has transitioned. * * @since 1.0.0 * * @param mixed $old_value The value being transitioned FROM. * @param mixed $new_value The value being transitioned TO. * @param int $item_id The ID of the item that is transitioning. */ do_action( $key_action, $old_value, $new_value, $item_id ); } } /** Meta ******************************************************************/ /** * Add meta data to an item. * * @since 1.0.0 * * @param int $item_id * @param string $meta_key * @param string $meta_value * @param string $unique * @return int|false The meta ID on success, false on failure. */ protected function add_item_meta( $item_id = 0, $meta_key = '', $meta_value = '', $unique = false ) { // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if no meta to add if ( empty( $item_id ) || empty( $meta_key ) ) { return false; } // Bail if no meta table exists if ( false === $this->get_meta_table_name() ) { return false; } // Get the meta type $meta_type = $this->get_meta_type(); // Return results of adding meta data return add_metadata( $meta_type, $item_id, $meta_key, $meta_value, $unique ); } /** * Get meta data for an item. * * @since 1.0.0 * * @param int $item_id * @param string $meta_key * @param bool $single * @return mixed Single metadata value, or array of values */ protected function get_item_meta( $item_id = 0, $meta_key = '', $single = false ) { // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if no meta was returned if ( empty( $item_id ) || empty( $meta_key ) ) { return false; } // Bail if no meta table exists if ( false === $this->get_meta_table_name() ) { return false; } // Get the meta type $meta_type = $this->get_meta_type(); // Return results of getting meta data return get_metadata( $meta_type, $item_id, $meta_key, $single ); } /** * Update meta data for an item. * * @since 1.0.0 * * @param int $item_id * @param string $meta_key * @param string $meta_value * @param string $prev_value * @return bool True on successful update, false on failure. */ protected function update_item_meta( $item_id = 0, $meta_key = '', $meta_value = '', $prev_value = '' ) { // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if no meta was returned if ( empty( $item_id ) || empty( $meta_key ) ) { return false; } // Bail if no meta table exists if ( false === $this->get_meta_table_name() ) { return false; } // Get the meta type $meta_type = $this->get_meta_type(); // Return results of updating meta data return update_metadata( $meta_type, $item_id, $meta_key, $meta_value, $prev_value ); } /** * Delete meta data for an item. * * @since 1.0.0 * * @param int $item_id * @param string $meta_key * @param string $meta_value * @param string $delete_all * @return bool True on successful delete, false on failure. */ protected function delete_item_meta( $item_id = 0, $meta_key = '', $meta_value = '', $delete_all = false ) { // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if no meta was returned if ( empty( $item_id ) || empty( $meta_key ) ) { return false; } // Bail if no meta table exists if ( false === $this->get_meta_table_name() ) { return false; } // Get the meta type $meta_type = $this->get_meta_type(); // Return results of deleting meta data return delete_metadata( $meta_type, $item_id, $meta_key, $meta_value, $delete_all ); } /** * Get registered meta data keys. * * @since 1.0.0 * * @param string $object_subtype The sub-type of meta keys * * @return array */ private function get_registered_meta_keys( $object_subtype = '' ) { // Get the object type $object_type = $this->get_meta_type(); // Return the keys return get_registered_meta_keys( $object_type, $object_subtype ); } /** * Maybe update meta values on item update/save. * * @since 1.0.0 * * @param array $meta */ private function save_extra_item_meta( $item_id = 0, $meta = array() ) { // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if there is no bulk meta to save if ( empty( $item_id ) || empty( $meta ) ) { return; } // Bail if no meta table exists if ( false === $this->get_meta_table_name() ) { return; } // Only save registered keys $keys = $this->get_registered_meta_keys(); $meta = array_intersect_key( $meta, $keys ); // Bail if no registered meta keys if ( empty( $meta ) ) { return; } // Save or delete meta data foreach ( $meta as $key => $value ) { ! empty( $value ) ? $this->update_item_meta( $item_id, $key, $value ) : $this->delete_item_meta( $item_id, $key ); } } /** * Delete all meta data for an item. * * @since 1.0.0 * * @param int $item_id */ private function delete_all_item_meta( $item_id = 0 ) { // Shape the item ID $item_id = $this->shape_item_id( $item_id ); // Bail if no item ID if ( empty( $item_id ) ) { return; } // Get the meta table name $table = $this->get_meta_table_name(); // Bail if no meta table exists if ( empty( $table ) ) { return; } // Get the primary column name $primary = $this->get_primary_column_name(); // Guess the item ID column for the meta table $item_id_column = $this->apply_prefix( "{$this->item_name}_{$primary}" ); // Get meta IDs $query = "SELECT meta_id FROM {$table} WHERE {$item_id_column} = %d"; $prepared = $this->get_db()->prepare( $query, $item_id ); $meta_ids = $this->get_db()->get_col( $prepared ); // Bail if no meta IDs to delete if ( empty( $meta_ids ) ) { return; } // Get the meta type $meta_type = $this->get_meta_type(); // Delete all meta data for this item ID foreach ( $meta_ids as $mid ) { delete_metadata_by_mid( $meta_type, $mid ); } } /** * Get the meta table for this query. * * Forked from WordPress\_get_meta_table() so it can be more accurately * predicted in a future iteration and default to returning false. * * @since 1.0.0 * * @return mixed Table name if exists, False if not */ private function get_meta_table_name() { // Get the meta-type $type = $this->get_meta_type(); // Append "meta" to end of meta-type $table_name = "{$type}meta"; // Variable'ize the database interface, to use inside empty() $db = $this->get_db(); // If not empty, return table name if ( ! empty( $db->{$table_name} ) ) { return $db->{$table_name}; } // Default return false return false; } /** * Get the meta type for this query. * * This method exists to reduce some duplication for now. Future iterations * will likely use Column::relationships to * * @since 1.1.0 * * @return string */ private function get_meta_type() { return $this->apply_prefix( $this->item_name ); } /** Cache *****************************************************************/ /** * Get cache key from query_vars and query_var_defaults. * * @since 1.0.0 * * @return string */ private function get_cache_key( $group = '' ) { // Slice query vars $slice = wp_array_slice_assoc( $this->query_vars, array_keys( $this->query_var_defaults ) ); // Unset `fields` so it does not effect the cache key unset( $slice['fields'] ); // Setup key & last_changed $key = md5( serialize( $slice ) ); $last_changed = $this->get_last_changed_cache( $group ); // Concatenate and return cache key return "get_{$this->item_name_plural}:{$key}:{$last_changed}"; } /** * Get the cache group, or fallback to the primary one. * * @since 1.0.0 * * @param string $group * @return string */ private function get_cache_group( $group = '' ) { // Get the primary column $primary = $this->get_primary_column_name(); // Default return value $retval = $this->cache_group; // Only allow non-primary groups if ( ! empty( $group ) && ( $group !== $primary ) ) { $retval = $group; } // Return the group return $retval; } /** * Get array of which database columns have uniquely cached groups. * * @since 1.0.0 * * @return array */ private function get_cache_groups() { // Return value $cache_groups = array(); // Get the cache groups $groups = $this->get_columns( array( 'cache_key' => true ), 'and', 'name' ); if ( ! empty( $groups ) ) { // Get the primary column name $primary = $this->get_primary_column_name(); // Setup return values foreach ( $groups as $name ) { if ( $primary !== $name ) { $cache_groups[ $name ] = "{$this->cache_group}-by-{$name}"; } else { $cache_groups[ $name ] = $this->cache_group; } } } // Return cache groups array return $cache_groups; } /** * Maybe prime item & item-meta caches by querying 1 time for all un-cached * items. * * Accepts a single ID, or an array of IDs. * * The reason this accepts only IDs is because it gets called immediately * after an item is inserted in the database, but before items have been * "shaped" into proper objects, so object properties may not be set yet. * * @since 1.0.0 * * @param array $item_ids * @param bool $force * * @return bool False if empty */ private function prime_item_caches( $item_ids = array(), $force = false ) { // Bail if no items to cache if ( empty( $item_ids ) ) { return false; } // Accepts single values, so cast to array $item_ids = (array) $item_ids; // Update item caches if ( ! empty( $force ) || ! empty( $this->query_vars['update_item_cache'] ) ) { // Look for non-cached IDs $ids = $this->get_non_cached_ids( $item_ids, $this->cache_group ); // Bail if IDs are cached if ( empty( $ids ) ) { return false; } // Get query parts $table = $this->get_table_name(); $primary = $this->get_primary_column_name(); // Query database $query = "SELECT * FROM {$table} WHERE {$primary} IN (%s)"; $ids = implode( ',', array_map( 'absint', $ids ) ); $prepare = sprintf( $query, $ids ); $results = $this->get_db()->get_results( $prepare ); // Update item cache(s) $this->update_item_cache( $results ); } // Update meta data caches if ( ! empty( $this->query_vars['update_meta_cache'] ) ) { $singular = rtrim( $this->table_name, 's' ); // sic update_meta_cache( $singular, $item_ids ); } } /** * Update the cache for an item. Does not update item-meta cache. * * Accepts a single object, or an array of objects. * * The reason this does not accept ID's is because this gets called * after an item is already updated in the database, so we want to avoid * querying for it again. It's just safer this way. * * @since 1.0.0 * * @param array $items */ private function update_item_cache( $items = array() ) { // Maybe query for single item if ( is_numeric( $items ) ) { // Get the primary column name $primary = $this->get_primary_column_name(); // Get item by ID (from database, not cache) $items = $this->get_item_raw( $primary, $items ); } // Bail if no items to cache if ( empty( $items ) ) { return false; } // Make sure items are an array (without casting objects to arrays) if ( ! is_array( $items ) ) { $items = array( $items ); } // Get the cache groups $groups = $this->get_cache_groups(); // Loop through all items and cache them foreach ( $items as $item ) { // Skip if item is not an object if ( ! is_object( $item ) ) { continue; } // Loop through groups and set cache if ( ! empty( $groups ) ) { foreach ( $groups as $key => $group ) { $this->cache_set( $item->{$key}, $item, $group ); } } } // Update last changed $this->update_last_changed_cache(); } /** * Clean the cache for an item. Does not clean item-meta. * * Accepts a single object, or an array of objects. * * The reason this does not accept ID's is because this gets called * after an item is already deleted from the database, so it cannot be * queried and may not exist in the cache. It's just safer this way. * * @since 1.0.0 * * @param mixed $items Single object item, or Array of object items * * @return bool */ private function clean_item_cache( $items = array() ) { // Bail if no items to clean if ( empty( $items ) ) { return false; } // Make sure items are an array if ( ! is_array( $items ) ) { $items = array( $items ); } // Get the cache groups $groups = $this->get_cache_groups(); // Loop through all items and clean them foreach ( $items as $item ) { // Skip if item is not an object if ( ! is_object( $item ) ) { continue; } // Loop through groups and delete cache if ( ! empty( $groups ) ) { foreach ( $groups as $key => $group ) { $this->cache_delete( $item->{$key}, $group ); } } } // Update last changed $this->update_last_changed_cache(); } /** * Update the last_changed key for the cache group. * * @since 1.0.0 * * @return string The last time a cache group was changed. */ private function update_last_changed_cache( $group = '' ) { // Fallback to microtime if ( empty( $this->last_changed ) ) { $this->set_last_changed(); } // Set the last changed time for this cache group $this->cache_set( 'last_changed', $this->last_changed, $group ); // Return the last changed time return $this->last_changed; } /** * Get the last_changed key for a cache group. * * @since 1.0.0 * * @param string $group Cache group. Defaults to $this->cache_group * * @return string The last time a cache group was changed. */ private function get_last_changed_cache( $group = '' ) { // Get the last changed cache value $last_changed = $this->cache_get( 'last_changed', $group ); // Maybe update the last changed value if ( false === $last_changed ) { $last_changed = $this->update_last_changed_cache( $group ); } // Return the last changed value for the cache group return $last_changed; } /** * Get array of non-cached item IDs. * * @since 1.0.0 * * @param array $item_ids Array of item IDs * @param string $group Cache group. Defaults to $this->cache_group * * @return array */ private function get_non_cached_ids( $item_ids = array(), $group = '' ) { // Default return value $retval = array(); // Bail if no item IDs if ( empty( $item_ids ) ) { return $retval; } // Loop through item IDs foreach ( $item_ids as $id ) { // Shape the item ID $id = $this->shape_item_id( $id ); // Add to return value if not cached if ( false === $this->cache_get( $id, $group ) ) { $retval[] = $id; } } // Return array of IDs return $retval; } /** * Add a cache value for a key and group. * * @since 1.0.0 * * @param string $key Cache key. * @param mixed $value Cache value. * @param string $group Cache group. Defaults to $this->cache_group * @param int $expire Expiration. */ private function cache_add( $key = '', $value = '', $group = '', $expire = 0 ) { // Bail if cache invalidation is suspended if ( wp_suspend_cache_addition() ) { return; } // Bail if no cache key if ( empty( $key ) ) { return; } // Get the cache group $group = $this->get_cache_group( $group ); // Add to the cache wp_cache_add( $key, $value, $group, $expire ); } /** * Get a cache value for a key and group. * * @since 1.0.0 * * @param string $key Cache key. * @param string $group Cache group. Defaults to $this->cache_group * @param bool $force */ private function cache_get( $key = '', $group = '', $force = false ) { // Bail if no cache key if ( empty( $key ) ) { return; } // Get the cache group $group = $this->get_cache_group( $group ); // Return from the cache return wp_cache_get( $key, $group, $force ); } /** * Set a cache value for a key and group. * * @since 1.0.0 * * @param string $key Cache key. * @param mixed $value Cache value. * @param string $group Cache group. Defaults to $this->cache_group * @param int $expire Expiration. */ private function cache_set( $key = '', $value = '', $group = '', $expire = 0 ) { // Bail if cache invalidation is suspended if ( wp_suspend_cache_addition() ) { return; } // Bail if no cache key if ( empty( $key ) ) { return; } // Get the cache group $group = $this->get_cache_group( $group ); // Update the cache wp_cache_set( $key, $value, $group, $expire ); } /** * Delete a cache key for a group. * * @since 1.0.0 * * @global bool $_wp_suspend_cache_invalidation * * @param string $key Cache key. * @param string $group Cache group. Defaults to $this->cache_group */ private function cache_delete( $key = '', $group = '' ) { global $_wp_suspend_cache_invalidation; // Bail if cache invalidation is suspended if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } // Bail if no cache key if ( empty( $key ) ) { return; } // Get the cache group $group = $this->get_cache_group( $group ); // Delete the cache wp_cache_delete( $key, $group ); } /** * Fetch raw results directly from the database. * * @since 1.0.0 * * @param array $cols Columns for `SELECT`. * @param array $where_cols Where clauses. Each key-value pair in the array * represents a column and a comparison. * @param int $limit Optional. LIMIT value. Default 25. * @param null $offset Optional. OFFSET value. Default null. * @param string $output Optional. Any of ARRAY_A | ARRAY_N | OBJECT | OBJECT_K constants. * Default OBJECT. * With one of the first three, return an array of * rows indexed from 0 by SQL result row number. * Each row is an associative array (column => value, ...), * a numerically indexed array (0 => value, ...), * or an object. ( ->column = value ), respectively. * With OBJECT_K, return an associative array of * row objects keyed by the value of each row's * first column's value. * * @return array|object|null Database query results. */ public function get_results( $cols = array(), $where_cols = array(), $limit = 25, $offset = null, $output = OBJECT ) { // Bail if no columns have been passed if ( empty( $cols ) ) { return null; } // Fetch all the columns for the table being queried $column_names = $this->get_column_names(); // Ensure valid column names have been passed for the `SELECT` clause foreach ( $cols as $index => $column ) { if ( ! array_key_exists( $column, $column_names ) ) { unset( $cols[ $index ] ); } } // Columns to retrieve $columns = implode( ',', $cols ); // Get the table name $table = $this->get_table_name(); // Setup base query $query = implode( ' ', array( "SELECT", $columns, "FROM {$table} {$this->table_alias}", "WHERE 1=1" ) ); // Ensure valid columns have been passed for the `WHERE` clause if ( ! empty( $where_cols ) ) { // Get keys from where columns $columns = array_keys( $where_cols ); // Loop through columns and unset any invalid names foreach ( $columns as $index => $column ) { if ( ! array_key_exists( $column, $column_names ) ) { unset( $where_cols[ $index ] ); } } // Parse WHERE clauses foreach ( $where_cols as $column => $compare ) { // Basic WHERE clause if ( ! is_array( $compare ) ) { $pattern = $this->get_column_field( array( 'name' => $column ), 'pattern', '%s' ); $statement = " AND {$this->table_alias}.{$column} = {$pattern} "; $query .= $this->get_db()->prepare( $statement, $compare ); // More complex WHERE clause } else { $value = isset( $compare['value'] ) ? $compare['value'] : false; // Skip if a value was not provided if ( false === $value ) { continue; } // Default compare clause to equals $compare_clause = isset( $compare['compare_query'] ) ? trim( strtoupper( $compare['compare_query'] ) ) : '='; // Array (unprepared) if ( is_array( $compare['value'] ) ) { // Default to IN if clause not specified if ( ! in_array( $compare_clause, array( 'IN', 'NOT IN', 'BETWEEN' ), true ) ) { $compare_clause = 'IN'; } // Parse & escape for IN and NOT IN if ( 'IN' === $compare_clause || 'NOT IN' === $compare_clause ) { $value = "('" . implode( "','", $this->get_db()->_escape( $compare['value'] ) ) . "')"; // Parse & escape for BETWEEN } elseif ( is_array( $value ) && 2 === count( $value ) && 'BETWEEN' === $compare_clause ) { $_this = $this->get_db()->_escape( $value[0] ); $_that = $this->get_db()->_escape( $value[1] ); $value = " {$_this} AND {$_that} "; } } // Add WHERE clause $query .= " AND {$this->table_alias}.{$column} {$compare_clause} {$value} "; } } } // Maybe set an offset if ( ! empty( $offset ) ) { $values = explode( ',', $offset ); $values = array_filter( $values, 'intval' ); $offset = implode( ',', $values ); $query .= " OFFSET {$offset} "; } // Maybe set a limit if ( ! empty( $limit ) && ( $limit > 0 ) ) { $limit = intval( $limit ); $query .= " LIMIT {$limit} "; } // Execute query $results = $this->get_db()->get_results( $query, $output ); // Return results return $results; } } Dependencies/BerlinDB/Database/Table.php 0000644 00000051033 15174677547 0014050 0 ustar 00 <?php /** * Base Custom Database Table Class. * * @package Database * @subpackage Table * @copyright Copyright (c) 2021 * @license https://opensource.org/licenses/MIT MIT * @since 1.0.0 */ namespace WP_Rocket\Dependencies\BerlinDB\Database; // Exit if accessed directly defined( 'ABSPATH' ) || exit; /** * A base database table class, which facilitates the creation of (and schema * changes to) individual database tables. * * This class is intended to be extended for each unique database table, * including global tables for multisite, and users tables. * * It exists to make managing database tables as easy as possible. * * Extending this class comes with several automatic benefits: * - Activation hook makes it great for plugins * - Tables store their versions in the database independently * - Tables upgrade via independent upgrade abstract methods * - Multisite friendly - site tables switch on "switch_blog" action * * @since 1.0.0 */ abstract class Table extends Base { /** * Table name, without the global table prefix. * * @since 1.0.0 * @var string */ protected $name = ''; /** * Optional description. * * @since 1.0.0 * @var string */ protected $description = ''; /** * Database version. * * @since 1.0.0 * @var mixed */ protected $version = ''; /** * Is this table for a site, or global. * * @since 1.0.0 * @var bool */ protected $global = false; /** * Database version key (saved in _options or _sitemeta) * * @since 1.0.0 * @var string */ protected $db_version_key = ''; /** * Current database version. * * @since 1.0.0 * @var mixed */ protected $db_version = 0; /** * Table prefix, including the site prefix. * * @since 1.0.0 * @var string */ protected $table_prefix = ''; /** * Table name. * * @since 1.0.0 * @var string */ protected $table_name = ''; /** * Table name, prefixed from the base. * * @since 1.0.0 * @var string */ protected $prefixed_name = ''; /** * Table schema. * * @since 1.0.0 * @var string */ protected $schema = ''; /** * Database character-set & collation for table. * * @since 1.0.0 * @var string */ protected $charset_collation = ''; /** * Key => value array of versions => methods. * * @since 1.0.0 * @var array */ protected $upgrades = array(); /** Methods ***************************************************************/ /** * Hook into queries, admin screens, and more! * * @since 1.0.0 */ public function __construct() { // Setup the database table $this->setup(); // Bail if setup failed if ( empty( $this->name ) || empty( $this->db_version_key ) ) { return; } // Add the table to the database interface $this->set_db_interface(); // Set the database schema $this->set_schema(); // Add hooks $this->add_hooks(); // Maybe force upgrade if testing if ( $this->is_testing() ) { $this->maybe_upgrade(); } } /** * Compatibility for clone() method for PHP versions less than 7.0. * * See: https://github.com/sugarcalendar/core/issues/105 * * This shim will be removed at a later date. * * @since 2.0.20 * * @param string $function * @param array $args */ public function __call( $function = '', $args = array() ) { if ( 'clone' === $function ) { call_user_func_array( array( $this, '_clone' ), $args ); } } /** Abstract **************************************************************/ /** * Setup this database table. * * @since 1.0.0 */ protected abstract function set_schema(); /** Multisite *************************************************************/ /** * Update table version & references. * * Hooked to the "switch_blog" action. * * @since 1.0.0 * * @param int $site_id The site being switched to */ public function switch_blog( $site_id = 0 ) { // Update DB version based on the current site if ( ! $this->is_global() ) { $this->db_version = get_blog_option( $site_id, $this->db_version_key, false ); } // Update interface for switched site $this->set_db_interface(); } /** Public Helpers ********************************************************/ /** * Maybe upgrade the database table. Handles creation & schema changes. * * Hooked to the `admin_init` action. * * @since 1.0.0 */ public function maybe_upgrade() { // Bail if not upgradeable if ( ! $this->is_upgradeable() ) { return; } // Bail if upgrade not needed if ( ! $this->needs_upgrade() ) { return; } // Upgrade if ( $this->exists() ) { $this->upgrade(); // Install } else { $this->install(); } } /** * Return whether this table needs an upgrade. * * @since 1.0.0 * * @param mixed $version Database version to check if upgrade is needed * * @return bool True if table needs upgrading. False if not. */ public function needs_upgrade( $version = false ) { // Use the current table version if none was passed if ( empty( $version ) ) { $version = $this->version; } // Get the current database version $this->get_db_version(); // Is the database table up to date? $is_current = version_compare( $this->db_version, $version, '>=' ); // Return false if current, true if out of date return ( true === $is_current ) ? false : true; } /** * Return whether this table can be upgraded. * * @since 1.0.0 * * @return bool True if table can be upgraded. False if not. */ public function is_upgradeable() { // Bail if global and upgrading global tables is not allowed if ( $this->is_global() && ! wp_should_upgrade_global_tables() ) { return false; } // Kinda weird, but assume it is return true; } /** * Return the current table version from the database. * * This is public method for accessing a private variable so that it cannot * be externally modified. * * @since 1.0.0 * * @return string */ public function get_version() { $this->get_db_version(); return $this->db_version; } /** * Install a database table * * Creates the table and sets the version information if successful. * * @since 1.0.0 */ public function install() { // Try to create the table $created = $this->create(); // Set the DB version if create was successful if ( true === $created ) { $this->set_db_version(); } } /** * Uninstall a database table * * Drops the table and deletes the version information if successful and/or * the table does not exist anymore. * * @since 1.0.0 */ public function uninstall() { // Try to drop the table $dropped = $this->drop(); // Delete the DB version if drop was successful or table does not exist if ( ( true === $dropped ) || ! $this->exists() ) { $this->delete_db_version(); } } /** Public Management *****************************************************/ /** * Check if table already exists. * * @since 1.0.0 * * @return bool */ public function exists() { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Query statement $query = "SHOW TABLES LIKE %s"; $like = $db->esc_like( $this->table_name ); $prepared = $db->prepare( $query, $like ); $result = $db->get_var( $prepared ); // Does the table exist? return $this->is_success( $result ); } /** * Get columns from table. * * @since 1.2.0 * * @return array */ public function columns() { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Query statement $query = "SHOW FULL COLUMNS FROM {$this->table_name}"; $result = $db->get_results( $query ); // Return the results return $this->is_success( $result ) ? $result : false; } /** * Create the table. * * @since 1.0.0 * * @return bool */ public function create() { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Query statement $query = "CREATE TABLE {$this->table_name} ( {$this->schema} ) {$this->charset_collation}"; $result = $db->query( $query ); // Was the table created? return $this->is_success( $result ); } /** * Drop the database table. * * @since 1.0.0 * * @return bool */ public function drop() { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Query statement $query = "DROP TABLE {$this->table_name}"; $result = $db->query( $query ); // Did the table get dropped? return $this->is_success( $result ); } /** * Truncate the database table. * * @since 1.0.0 * * @return bool */ public function truncate() { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Query statement $query = "TRUNCATE TABLE {$this->table_name}"; $result = $db->query( $query ); // Did the table get truncated? return $this->is_success( $result ); } /** * Delete all items from the database table. * * @since 1.0.0 * * @return bool */ public function delete_all() { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Query statement $query = "DELETE FROM {$this->table_name}"; $result = $db->query( $query ); // Return the results return $result; } /** * Clone this database table. * * Pair with copy(). * * @since 1.1.0 * * @param string $new_table_name The name of the new table, without prefix * * @return bool */ public function _clone( $new_table_name = '' ) { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Sanitize the new table name $table_name = $this->sanitize_table_name( $new_table_name ); // Bail if new table name is invalid if ( empty( $table_name ) ) { return false; } // Query statement $table = $this->apply_prefix( $table_name ); $query = "CREATE TABLE {$table} LIKE {$this->table_name}"; $result = $db->query( $query ); // Did the table get cloned? return $this->is_success( $result ); } /** * Copy the contents of this table to a new table. * * Pair with clone(). * * @since 1.1.0 * * @param string $new_table_name The name of the new table, without prefix * * @return bool */ public function copy( $new_table_name = '' ) { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Sanitize the new table name $table_name = $this->sanitize_table_name( $new_table_name ); // Bail if new table name is invalid if ( empty( $table_name ) ) { return false; } // Query statement $table = $this->apply_prefix( $table_name ); $query = "INSERT INTO {$table} SELECT * FROM {$this->table_name}"; $result = $db->query( $query ); // Did the table get copied? return $this->is_success( $result ); } /** * Count the number of items in the database table. * * @since 1.0.0 * * @return int */ public function count() { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return 0; } // Query statement $query = "SELECT COUNT(*) FROM {$this->table_name}"; $result = $db->get_var( $query ); // Query success/fail return intval( $result ); } /** * Check if column already exists. * * @since 1.0.0 * * @param string $name Value * * @return bool */ public function column_exists( $name = '' ) { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Query statement $query = "SHOW COLUMNS FROM {$this->table_name} LIKE %s"; $like = $db->esc_like( $name ); $prepared = $db->prepare( $query, $like ); $result = $db->query( $prepared ); // Does the column exist? return $this->is_success( $result ); } /** * Check if index already exists. * * @since 1.0.0 * * @param string $name Value * @param string $column Column name * * @return bool */ public function index_exists( $name = '', $column = 'Key_name' ) { // Get the database interface $db = $this->get_db(); // Bail if no database interface is available if ( empty( $db ) ) { return false; } // Limit $column to Key or Column name, until we can do better if ( ! in_array( $column, array( 'Key_name', 'Column_name' ), true ) ) { $column = 'Key_name'; } // Query statement $query = "SHOW INDEXES FROM {$this->table_name} WHERE {$column} LIKE %s"; $like = $db->esc_like( $name ); $prepared = $db->prepare( $query, $like ); $result = $db->query( $prepared ); // Does the index exist? return $this->is_success( $result ); } /** Upgrades **************************************************************/ /** * Upgrade this database table. * * @since 1.0.0 * * @return bool */ public function upgrade() { // Get pending upgrades $upgrades = $this->get_pending_upgrades(); // Bail if no upgrades if ( empty( $upgrades ) ) { $this->set_db_version(); // Return, without failure return true; } // Default result $result = false; // Try to do the upgrades foreach ( $upgrades as $version => $callback ) { // Do the upgrade $result = $this->upgrade_to( $version, $callback ); // Bail if an error occurs, to avoid skipping upgrades if ( ! $this->is_success( $result ) ) { return false; } } // Success/fail return $this->is_success( $result ); } /** * Return array of upgrades that still need to run. * * @since 1.1.0 * * @return array Array of upgrade callbacks, keyed by their db version. */ public function get_pending_upgrades() { // Default return value $upgrades = array(); // Bail if no upgrades, or no database version to compare to if ( empty( $this->upgrades ) || empty( $this->db_version ) ) { return $upgrades; } // Loop through all upgrades, and pick out the ones that need doing foreach ( $this->upgrades as $version => $callback ) { if ( true === version_compare( $version, $this->db_version, '>' ) ) { $upgrades[ $version ] = $callback; } } // Return return $upgrades; } /** * Upgrade to a specific database version. * * @since 1.0.0 * * @param mixed $version Database version to check if upgrade is needed * @param string $callback Callback function or class method to call * * @return bool */ public function upgrade_to( $version = '', $callback = '' ) { // Bail if no upgrade is needed if ( ! $this->needs_upgrade( $version ) ) { return false; } // Allow self-named upgrade callbacks if ( empty( $callback ) ) { $callback = $version; } // Is the callback... callable? $callable = $this->get_callable( $callback ); // Bail if no callable upgrade was found if ( empty( $callable ) ) { return false; } // Do the upgrade $result = call_user_func( $callable ); $success = $this->is_success( $result ); // Bail if upgrade failed if ( true !== $success ) { return false; } // Set the database version to this successful version $this->set_db_version( $version ); // Return success return true; } /** Private ***************************************************************/ /** * Setup the necessary table variables. * * @since 1.0.0 */ private function setup() { // Bail if no database interface is available if ( ! $this->get_db() ) { return; } // Sanitize the database table name $this->name = $this->sanitize_table_name( $this->name ); // Bail if database table name was garbage if ( false === $this->name ) { return; } // Separator $glue = '_'; // Setup the prefixed name $this->prefixed_name = $this->apply_prefix( $this->name, $glue ); // Maybe create database key if ( empty( $this->db_version_key ) ) { $this->db_version_key = implode( $glue, array( sanitize_key( $this->db_global ), $this->prefixed_name, 'version' ) ); } } /** * Set this table up in the database interface. * * This must be done directly because the database interface does not * have a common mechanism for manipulating them safely. * * @since 1.0.0 */ private function set_db_interface() { // Get the database once, to avoid duplicate function calls $db = $this->get_db(); // Bail if no database if ( empty( $db ) ) { return; } // Set variables for global tables if ( $this->is_global() ) { $site_id = 0; $tables = 'ms_global_tables'; // Set variables for per-site tables } else { $site_id = null; $tables = 'tables'; } // Set the table prefix and prefix the table name $this->table_prefix = $db->get_blog_prefix( $site_id ); // Get the prefixed table name $prefixed_table_name = "{$this->table_prefix}{$this->prefixed_name}"; // Set the database interface $db->{$this->prefixed_name} = $this->table_name = $prefixed_table_name; // Create the array if it does not exist if ( ! isset( $db->{$tables} ) ) { $db->{$tables} = array(); } // Add the table to the global table array $db->{$tables}[] = $this->prefixed_name; // Charset if ( ! empty( $db->charset ) ) { $this->charset_collation = "DEFAULT CHARACTER SET {$db->charset}"; } // Collation if ( ! empty( $db->collate ) ) { $this->charset_collation .= " COLLATE {$db->collate}"; } } /** * Set the database version for the table. * * @since 1.0.0 * * @param mixed $version Database version to set when upgrading/creating */ private function set_db_version( $version = '' ) { // If no version is passed during an upgrade, use the current version if ( empty( $version ) ) { $version = $this->version; } // Update the DB version $this->is_global() ? update_network_option( get_main_network_id(), $this->db_version_key, $version ) : update_option( $this->db_version_key, $version ); // Set the DB version $this->db_version = $version; } /** * Get the table version from the database. * * @since 1.0.0 */ private function get_db_version() { $this->db_version = $this->is_global() ? get_network_option( get_main_network_id(), $this->db_version_key, false ) : get_option( $this->db_version_key, false ); } /** * Delete the table version from the database. * * @since 1.0.0 */ private function delete_db_version() { $this->db_version = $this->is_global() ? delete_network_option( get_main_network_id(), $this->db_version_key ) : delete_option( $this->db_version_key ); } /** * Add class hooks to the parent application actions. * * @since 1.0.0 */ private function add_hooks() { // Add table to the global database object add_action( 'switch_blog', array( $this, 'switch_blog' ) ); add_action( 'admin_init', array( $this, 'maybe_upgrade' ) ); } /** * Check if the current request is from some kind of test. * * This is primarily used to skip 'admin_init' and force-install tables. * * @since 1.0.0 * * @return bool */ private function is_testing() { return (bool) // Tests constant is being used ( defined( 'WP_TESTS_DIR' ) && WP_TESTS_DIR ) || // Scaffolded (https://make.wordpress.org/cli/handbook/plugin-unit-tests/) function_exists( '_manually_load_plugin' ); } /** * Check if table is global. * * @since 1.0.0 * * @return bool */ private function is_global() { return ( true === $this->global ); } /** * Try to get a callable upgrade, with some magic to avoid needing to * do this dance repeatedly inside subclasses. * * @since 1.0.0 * * @param string $callback * * @return mixed Callable string, or false if not callable */ private function get_callable( $callback = '' ) { // Default return value $callable = $callback; // Look for global function if ( ! is_callable( $callable ) ) { // Fallback to local class method $callable = array( $this, $callback ); if ( ! is_callable( $callable ) ) { // Fallback to class method prefixed with "__" $callable = array( $this, "__{$callback}" ); if ( ! is_callable( $callable ) ) { $callable = false; } } } // Return callable string, or false if not callable return $callable; } } Dependencies/BerlinDB/Database/Base.php 0000644 00000016756 15174677547 0013710 0 ustar 00 <?php /** * Base Custom Database Class. * * @package Database * @subpackage Base * @copyright Copyright (c) 2021 * @license https://opensource.org/licenses/MIT MIT * @since 1.0.0 */ namespace WP_Rocket\Dependencies\BerlinDB\Database; // Exit if accessed directly defined( 'ABSPATH' ) || exit; /** * The base class that all other database base classes extend. * * This class attempts to provide some universal immutability to all other * classes that extend it, starting with a magic getter, but likely expanding * into a magic call handler and others. * * @since 1.0.0 */ #[\AllowDynamicProperties] class Base { /** * The name of the PHP global that contains the primary database interface. * * For example, WordPress traditionally uses 'wpdb', but other applications * may use something else, or you may be doing something really cool that * requires a custom interface. * * A future version of this utility may abstract this out entirely, so * custom calls to the get_db() should be avoided if at all possible. * * @since 1.0.0 * @var string */ protected $db_global = 'wpdb'; /** Global Properties *****************************************************/ /** * Global prefix used for tables/hooks/cache-groups/etc... * * @since 1.0.0 * @var string */ protected $prefix = ''; /** * The last database error, if any. * * @since 1.0.0 * @var mixed */ protected $last_error = false; /** Public ****************************************************************/ /** * Magic isset'ter for immutability. * * @since 1.0.0 * * @param string $key * @return mixed */ public function __isset( $key = '' ) { // No more uppercase ID properties ever if ( 'ID' === $key ) { $key = 'id'; } // Class method to try and call $method = "get_{$key}"; // Return property if exists if ( method_exists( $this, $method ) ) { return true; // Return get method results if exists } elseif ( property_exists( $this, $key ) ) { return true; } // Return false if not exists return false; } /** * Magic getter for immutability. * * @since 1.0.0 * * @param string $key * @return mixed */ public function __get( $key = '' ) { // No more uppercase ID properties ever if ( 'ID' === $key ) { $key = 'id'; } // Class method to try and call $method = "get_{$key}"; // Return property if exists if ( method_exists( $this, $method ) ) { return call_user_func( array( $this, $method ) ); // Return get method results if exists } elseif ( property_exists( $this, $key ) ) { return $this->{$key}; } // Return null if not exists return null; } /** * Converts the given object to an array. * * @since 1.0.0 * * @return array Array version of the given object. */ public function to_array() { return get_object_vars( $this ); } /** Protected *************************************************************/ /** * Maybe append the prefix to string. * * @since 1.0.0 * * @param string $string * @param string $sep * @return string */ protected function apply_prefix( $string = '', $sep = '_' ) { return ! empty( $this->prefix ) ? "{$this->prefix}{$sep}{$string}" : $string; } /** * Return the first letters of a string of words with a separator. * * Used primarily to guess at table aliases when none is manually set. * * Applies the following formatting to a string: * - Trim whitespace * - No accents * - No trailing underscores * * @since 1.0.0 * * @param string $string * @param string $sep * @return string */ protected function first_letters( $string = '', $sep = '_' ) { // Set empty default return value $retval = ''; // Bail if empty or not a string if ( empty( $string ) || ! is_string( $string ) ) { return $retval; } // Trim spaces off the ends $unspace = trim( $string ); // Only non-accented table names (avoid truncation) $accents = remove_accents( $unspace ); // Only lowercase letters are allowed $lower = strtolower( $accents ); // Explode into parts $parts = explode( $sep, $lower ); // Loop through parts and concatenate the first letters together foreach ( $parts as $part ) { $retval .= substr( $part, 0, 1 ); } // Return the result return $retval; } /** * Sanitize a table name string. * * Used to make sure that a table name value meets MySQL expectations. * * Applies the following formatting to a string: * - Trim whitespace * - No accents * - No special characters * - No hyphens * - No double underscores * - No trailing underscores * * @since 1.0.0 * * @param string $name The name of the database table * * @return string Sanitized database table name */ protected function sanitize_table_name( $name = '' ) { // Bail if empty or not a string if ( empty( $name ) || ! is_string( $name ) ) { return false; } // Trim spaces off the ends $unspace = trim( $name ); // Only non-accented table names (avoid truncation) $accents = remove_accents( $unspace ); // Only lowercase characters, hyphens, and dashes (avoid index corruption) $lower = sanitize_key( $accents ); // Replace hyphens with single underscores $under = str_replace( '-', '_', $lower ); // Single underscores only $single = str_replace( '__', '_', $under ); // Remove trailing underscores $clean = trim( $single, '_' ); // Bail if table name was garbaged if ( empty( $clean ) ) { return false; } // Return the cleaned table name return $clean; } /** * Set class variables from arguments. * * @since 1.0.0 * @param array $args */ protected function set_vars( $args = array() ) { // Bail if empty or not an array if ( empty( $args ) ) { return; } // Cast to an array if ( ! is_array( $args ) ) { $args = (array) $args; } // Set all properties foreach ( $args as $key => $value ) { $this->{$key} = $value; } } /** * Return the global database interface. * * See: https://core.trac.wordpress.org/ticket/31556 * * @since 1.0.0 * * @return \wpdb|false Database interface, or False if not set */ protected function get_db() { // Default database return value (might change) $retval = false; // Look for a commonly used global database interface if ( isset( $GLOBALS[ $this->db_global ] ) ) { $retval = $GLOBALS[ $this->db_global ]; } /* * Developer note: * * It should be impossible for a database table to be interacted with * before the primary database interface is setup. * * However, because applications are complicated, it is unsafe to assume * anything, so this silently returns false instead of halting everything. * * If you are here because this method is returning false for you, that * means the database table is being invoked too early in the lifecycle * of the application. * * In WordPress, that means before the $wpdb global is created; in other * environments, you will need to adjust accordingly. */ // Return the database interface return $retval; } /** * Check if an operation succeeded. * * @since 1.0.0 * * @param mixed $result * @return bool */ protected function is_success( $result = false ) { // Bail if no row exists if ( empty( $result ) ) { $retval = false; // Bail if an error occurred } elseif ( is_wp_error( $result ) ) { $this->last_error = $result; $retval = false; // No errors } else { $retval = true; } // Return the result return (bool) $retval; } } Dependencies/League/Container/Container.php 0000644 00000013522 15174677547 0014743 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container; use WP_Rocket\Dependencies\League\Container\Definition\{DefinitionAggregate, DefinitionInterface, DefinitionAggregateInterface}; use WP_Rocket\Dependencies\League\Container\Exception\{NotFoundException, ContainerException}; use WP_Rocket\Dependencies\League\Container\Inflector\{InflectorAggregate, InflectorInterface, InflectorAggregateInterface}; use WP_Rocket\Dependencies\League\Container\ServiceProvider\{ServiceProviderAggregate, ServiceProviderAggregateInterface, ServiceProviderInterface}; use WP_Rocket\Dependencies\Psr\Container\ContainerInterface; class Container implements DefinitionContainerInterface { /** * @var boolean */ protected $defaultToShared = false; /** * @var DefinitionAggregateInterface */ protected $definitions; /** * @var ServiceProviderAggregateInterface */ protected $providers; /** * @var InflectorAggregateInterface */ protected $inflectors; /** * @var ContainerInterface[] */ protected $delegates = []; public function __construct( ?DefinitionAggregateInterface $definitions = null, ?ServiceProviderAggregateInterface $providers = null, ?InflectorAggregateInterface $inflectors = null ) { $this->definitions = $definitions ?? new DefinitionAggregate(); $this->providers = $providers ?? new ServiceProviderAggregate(); $this->inflectors = $inflectors ?? new InflectorAggregate(); if ($this->definitions instanceof ContainerAwareInterface) { $this->definitions->setContainer($this); } if ($this->providers instanceof ContainerAwareInterface) { $this->providers->setContainer($this); } if ($this->inflectors instanceof ContainerAwareInterface) { $this->inflectors->setContainer($this); } } public function add(string $id, $concrete = null): DefinitionInterface { $concrete = $concrete ?? $id; if (true === $this->defaultToShared) { return $this->addShared($id, $concrete); } return $this->definitions->add($id, $concrete); } public function addShared(string $id, $concrete = null): DefinitionInterface { $concrete = $concrete ?? $id; return $this->definitions->addShared($id, $concrete); } public function defaultToShared(bool $shared = true): ContainerInterface { $this->defaultToShared = $shared; return $this; } public function extend(string $id): DefinitionInterface { if ($this->providers->provides($id)) { $this->providers->register($id); } if ($this->definitions->has($id)) { return $this->definitions->getDefinition($id); } throw new NotFoundException(sprintf( 'Unable to extend alias (%s) as it is not being managed as a definition', $id )); } public function addServiceProvider(ServiceProviderInterface $provider): DefinitionContainerInterface { $this->providers->add($provider); return $this; } /** * @template RequestedType * * @param class-string<RequestedType>|string $id * * @return RequestedType|mixed */ public function get($id) { return $this->resolve($id); } /** * @template RequestedType * * @param class-string<RequestedType>|string $id * * @return RequestedType|mixed */ public function getNew($id) { return $this->resolve($id, true); } public function has($id): bool { if ($this->definitions->has($id)) { return true; } if ($this->definitions->hasTag($id)) { return true; } if ($this->providers->provides($id)) { return true; } foreach ($this->delegates as $delegate) { if ($delegate->has($id)) { return true; } } return false; } public function inflector(string $type, ?callable $callback = null): InflectorInterface { return $this->inflectors->add($type, $callback); } public function delegate(ContainerInterface $container): self { $this->delegates[] = $container; if ($container instanceof ContainerAwareInterface) { $container->setContainer($this); } return $this; } protected function resolve($id, bool $new = false) { if ($this->definitions->has($id)) { $resolved = (true === $new) ? $this->definitions->resolveNew($id) : $this->definitions->resolve($id); return $this->inflectors->inflect($resolved); } if ($this->definitions->hasTag($id)) { $arrayOf = (true === $new) ? $this->definitions->resolveTaggedNew($id) : $this->definitions->resolveTagged($id); array_walk($arrayOf, function (&$resolved) { $resolved = $this->inflectors->inflect($resolved); }); return $arrayOf; } if ($this->providers->provides($id)) { $this->providers->register($id); if (!$this->definitions->has($id) && !$this->definitions->hasTag($id)) { throw new ContainerException(sprintf('Service provider lied about providing (%s) service', $id)); } return $this->resolve($id, $new); } foreach ($this->delegates as $delegate) { if ($delegate->has($id)) { $resolved = $delegate->get($id); return $this->inflectors->inflect($resolved); } } throw new NotFoundException(sprintf('Alias (%s) is not being managed by the container or delegates', $id)); } } Dependencies/League/Container/Exception/NotFoundException.php 0000644 00000000446 15174677547 0020373 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Exception; use WP_Rocket\Dependencies\Psr\Container\NotFoundExceptionInterface; use InvalidArgumentException; class NotFoundException extends InvalidArgumentException implements NotFoundExceptionInterface { } Dependencies/League/Container/Exception/ContainerException.php 0000644 00000000431 15174677547 0020553 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Exception; use WP_Rocket\Dependencies\Psr\Container\ContainerExceptionInterface; use RuntimeException; class ContainerException extends RuntimeException implements ContainerExceptionInterface { } Dependencies/League/Container/Argument/ArgumentResolverInterface.php 0000644 00000000656 15174677547 0021734 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument; use WP_Rocket\Dependencies\League\Container\ContainerAwareInterface; use ReflectionFunctionAbstract; interface ArgumentResolverInterface extends ContainerAwareInterface { public function resolveArguments(array $arguments): array; public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []): array; } Dependencies/League/Container/Argument/ArgumentResolverTrait.php 0000644 00000007125 15174677547 0021115 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument; use WP_Rocket\Dependencies\League\Container\DefinitionContainerInterface; use WP_Rocket\Dependencies\League\Container\Exception\{ContainerException, NotFoundException}; use WP_Rocket\Dependencies\League\Container\ReflectionContainer; use WP_Rocket\Dependencies\Psr\Container\ContainerInterface; use ReflectionFunctionAbstract; use ReflectionNamedType; trait ArgumentResolverTrait { public function resolveArguments(array $arguments): array { try { $container = $this->getContainer(); } catch (ContainerException $e) { $container = ($this instanceof ReflectionContainer) ? $this : null; } foreach ($arguments as &$arg) { // if we have a literal, we don't want to do anything more with it if ($arg instanceof LiteralArgumentInterface) { $arg = $arg->getValue(); continue; } if ($arg instanceof ArgumentInterface) { $argValue = $arg->getValue(); } else { $argValue = $arg; } if (!is_string($argValue)) { continue; } // resolve the argument from the container, if it happens to be another // argument wrapper, use that value if ($container instanceof ContainerInterface && $container->has($argValue)) { try { $arg = $container->get($argValue); if ($arg instanceof ArgumentInterface) { $arg = $arg->getValue(); } continue; } catch (NotFoundException $e) { } } // if we have a default value, we use that, no more resolution as // we expect a default/optional argument value to be literal if ($arg instanceof DefaultValueInterface) { $arg = $arg->getDefaultValue(); } } return $arguments; } public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []): array { $params = $method->getParameters(); $arguments = []; foreach ($params as $param) { $name = $param->getName(); // if we've been given a value for the argument, treat as literal if (array_key_exists($name, $args)) { $arguments[] = new LiteralArgument($args[$name]); continue; } $type = $param->getType(); if ($type instanceof ReflectionNamedType) { // in PHP 8, nullable arguments have "?" prefix $typeHint = ltrim($type->getName(), '?'); if ($param->isDefaultValueAvailable()) { $arguments[] = new DefaultValueArgument($typeHint, $param->getDefaultValue()); continue; } $arguments[] = new ResolvableArgument($typeHint); continue; } if ($param->isDefaultValueAvailable()) { $arguments[] = new LiteralArgument($param->getDefaultValue()); continue; } throw new NotFoundException(sprintf( 'Unable to resolve a value for parameter (%s) in the function/method (%s)', $name, $method->getName() )); } return $this->resolveArguments($arguments); } abstract public function getContainer(): DefinitionContainerInterface; } Dependencies/League/Container/Argument/LiteralArgumentInterface.php 0000644 00000000237 15174677547 0021522 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument; interface LiteralArgumentInterface extends ArgumentInterface { } Dependencies/League/Container/Argument/LiteralArgument.php 0000644 00000002320 15174677547 0017674 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument; use InvalidArgumentException; class LiteralArgument implements LiteralArgumentInterface { public const TYPE_ARRAY = 'array'; public const TYPE_BOOL = 'boolean'; public const TYPE_BOOLEAN = 'boolean'; public const TYPE_CALLABLE = 'callable'; public const TYPE_DOUBLE = 'double'; public const TYPE_FLOAT = 'double'; public const TYPE_INT = 'integer'; public const TYPE_INTEGER = 'integer'; public const TYPE_OBJECT = 'object'; public const TYPE_STRING = 'string'; /** * @var mixed */ protected $value; public function __construct($value, ?string $type = null) { if ( null === $type || ($type === self::TYPE_CALLABLE && is_callable($value)) || ($type === self::TYPE_OBJECT && is_object($value)) || gettype($value) === $type ) { $this->value = $value; } else { throw new InvalidArgumentException('Incorrect type for value.'); } } /** * {@inheritdoc} */ public function getValue() { return $this->value; } } Dependencies/League/Container/Argument/DefaultValueInterface.php 0000644 00000000350 15174677547 0021000 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument; interface DefaultValueInterface extends ArgumentInterface { /** * @return mixed */ public function getDefaultValue(); } Dependencies/League/Container/Argument/Literal/BooleanArgument.php 0000644 00000000531 15174677547 0021255 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument\Literal; use WP_Rocket\Dependencies\League\Container\Argument\LiteralArgument; class BooleanArgument extends LiteralArgument { public function __construct(bool $value) { parent::__construct($value, LiteralArgument::TYPE_BOOL); } } Dependencies/League/Container/Argument/Literal/CallableArgument.php 0000644 00000000542 15174677547 0021377 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument\Literal; use WP_Rocket\Dependencies\League\Container\Argument\LiteralArgument; class CallableArgument extends LiteralArgument { public function __construct(callable $value) { parent::__construct($value, LiteralArgument::TYPE_CALLABLE); } } Dependencies/League/Container/Argument/Literal/IntegerArgument.php 0000644 00000000527 15174677547 0021300 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument\Literal; use WP_Rocket\Dependencies\League\Container\Argument\LiteralArgument; class IntegerArgument extends LiteralArgument { public function __construct(int $value) { parent::__construct($value, LiteralArgument::TYPE_INT); } } Dependencies/League/Container/Argument/Literal/StringArgument.php 0000644 00000000534 15174677547 0021147 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument\Literal; use WP_Rocket\Dependencies\League\Container\Argument\LiteralArgument; class StringArgument extends LiteralArgument { public function __construct(string $value) { parent::__construct($value, LiteralArgument::TYPE_STRING); } } Dependencies/League/Container/Argument/Literal/FloatArgument.php 0000644 00000000531 15174677547 0020743 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument\Literal; use WP_Rocket\Dependencies\League\Container\Argument\LiteralArgument; class FloatArgument extends LiteralArgument { public function __construct(float $value) { parent::__construct($value, LiteralArgument::TYPE_FLOAT); } } Dependencies/League/Container/Argument/Literal/ArrayArgument.php 0000644 00000000531 15174677547 0020754 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument\Literal; use WP_Rocket\Dependencies\League\Container\Argument\LiteralArgument; class ArrayArgument extends LiteralArgument { public function __construct(array $value) { parent::__construct($value, LiteralArgument::TYPE_ARRAY); } } Dependencies/League/Container/Argument/Literal/ObjectArgument.php 0000644 00000000534 15174677547 0021107 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument\Literal; use WP_Rocket\Dependencies\League\Container\Argument\LiteralArgument; class ObjectArgument extends LiteralArgument { public function __construct(object $value) { parent::__construct($value, LiteralArgument::TYPE_OBJECT); } } Dependencies/League/Container/Argument/ResolvableArgument.php 0000644 00000000544 15174677547 0020404 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument; class ResolvableArgument implements ResolvableArgumentInterface { protected $value; public function __construct(string $value) { $this->value = $value; } public function getValue(): string { return $this->value; } } Dependencies/League/Container/Argument/ResolvableArgumentInterface.php 0000644 00000000312 15174677547 0022216 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument; interface ResolvableArgumentInterface extends ArgumentInterface { public function getValue(): string; } Dependencies/League/Container/Argument/DefaultValueArgument.php 0000644 00000000773 15174677547 0020673 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument; class DefaultValueArgument extends ResolvableArgument implements DefaultValueInterface { protected $defaultValue; public function __construct(string $value, $defaultValue = null) { $this->defaultValue = $defaultValue; parent::__construct($value); } /** * @return mixed|null */ public function getDefaultValue() { return $this->defaultValue; } } Dependencies/League/Container/Argument/ArgumentInterface.php 0000644 00000000303 15174677547 0020177 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Argument; interface ArgumentInterface { /** * @return mixed */ public function getValue(); } Dependencies/League/Container/Inflector/InflectorAggregate.php 0000644 00000001760 15174677547 0020503 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Inflector; use Generator; use WP_Rocket\Dependencies\League\Container\ContainerAwareTrait; class InflectorAggregate implements InflectorAggregateInterface { use ContainerAwareTrait; /** * @var Inflector[] */ protected $inflectors = []; public function add(string $type, ?callable $callback = null): Inflector { $inflector = new Inflector($type, $callback); $this->inflectors[] = $inflector; return $inflector; } public function inflect($object) { foreach ($this->getIterator() as $inflector) { $type = $inflector->getType(); if ($object instanceof $type) { $inflector->setContainer($this->getContainer()); $inflector->inflect($object); } } return $object; } public function getIterator(): Generator { yield from $this->inflectors; } } Dependencies/League/Container/Inflector/InflectorAggregateInterface.php 0000644 00000000624 15174677547 0022322 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Inflector; use IteratorAggregate; use WP_Rocket\Dependencies\League\Container\ContainerAwareInterface; interface InflectorAggregateInterface extends ContainerAwareInterface, IteratorAggregate { public function add(string $type, ?callable $callback = null): Inflector; public function inflect(object $object); } Dependencies/League/Container/Inflector/Inflector.php 0000644 00000004603 15174677547 0016673 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Inflector; use WP_Rocket\Dependencies\League\Container\Argument\ArgumentResolverInterface; use WP_Rocket\Dependencies\League\Container\Argument\ArgumentResolverTrait; use WP_Rocket\Dependencies\League\Container\ContainerAwareTrait; class Inflector implements ArgumentResolverInterface, InflectorInterface { use ArgumentResolverTrait; use ContainerAwareTrait; /** * @var string */ protected $type; /** * @var callable|null */ protected $callback; /** * @var array */ protected $methods = []; /** * @var array */ protected $properties = []; public function __construct(string $type, ?callable $callback = null) { $this->type = $type; $this->callback = $callback; } public function getType(): string { return $this->type; } public function invokeMethod(string $name, array $args): InflectorInterface { $this->methods[$name] = $args; return $this; } public function invokeMethods(array $methods): InflectorInterface { foreach ($methods as $name => $args) { $this->invokeMethod($name, $args); } return $this; } public function setProperty(string $property, $value): InflectorInterface { $this->properties[$property] = $this->resolveArguments([$value])[0]; return $this; } public function setProperties(array $properties): InflectorInterface { foreach ($properties as $property => $value) { $this->setProperty($property, $value); } return $this; } public function inflect(object $object): void { $properties = $this->resolveArguments(array_values($this->properties)); $properties = array_combine(array_keys($this->properties), $properties); // array_combine() can technically return false foreach ($properties ?: [] as $property => $value) { $object->{$property} = $value; } foreach ($this->methods as $method => $args) { $args = $this->resolveArguments($args); $callable = [$object, $method]; call_user_func_array($callable, $args); } if ($this->callback !== null) { call_user_func($this->callback, $object); } } } Dependencies/League/Container/Inflector/InflectorInterface.php 0000644 00000001013 15174677547 0020504 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Inflector; interface InflectorInterface { public function getType(): string; public function inflect(object $object): void; public function invokeMethod(string $name, array $args): InflectorInterface; public function invokeMethods(array $methods): InflectorInterface; public function setProperties(array $properties): InflectorInterface; public function setProperty(string $property, $value): InflectorInterface; } Dependencies/League/Container/Definition/DefinitionAggregate.php 0000644 00000006045 15174677547 0021012 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Definition; use Generator; use WP_Rocket\Dependencies\League\Container\ContainerAwareTrait; use WP_Rocket\Dependencies\League\Container\Exception\NotFoundException; class DefinitionAggregate implements DefinitionAggregateInterface { use ContainerAwareTrait; /** * @var DefinitionInterface[] */ protected $definitions = []; public function __construct(array $definitions = []) { $this->definitions = array_filter($definitions, static function ($definition) { return ($definition instanceof DefinitionInterface); }); } public function add(string $id, $definition): DefinitionInterface { if (false === ($definition instanceof DefinitionInterface)) { $definition = new Definition($id, $definition); } $this->definitions[] = $definition->setAlias($id); return $definition; } public function addShared(string $id, $definition): DefinitionInterface { $definition = $this->add($id, $definition); return $definition->setShared(true); } public function has(string $id): bool { $id = Definition::normaliseAlias($id); foreach ($this->getIterator() as $definition) { if ($id === $definition->getAlias()) { return true; } } return false; } public function hasTag(string $tag): bool { foreach ($this->getIterator() as $definition) { if ($definition->hasTag($tag)) { return true; } } return false; } public function getDefinition(string $id): DefinitionInterface { $id = Definition::normaliseAlias($id); foreach ($this->getIterator() as $definition) { if ($id === $definition->getAlias()) { return $definition->setContainer($this->getContainer()); } } throw new NotFoundException(sprintf('Alias (%s) is not being handled as a definition.', $id)); } public function resolve(string $id) { return $this->getDefinition($id)->resolve(); } public function resolveNew(string $id) { return $this->getDefinition($id)->resolveNew(); } public function resolveTagged(string $tag): array { $arrayOf = []; foreach ($this->getIterator() as $definition) { if ($definition->hasTag($tag)) { $arrayOf[] = $definition->setContainer($this->getContainer())->resolve(); } } return $arrayOf; } public function resolveTaggedNew(string $tag): array { $arrayOf = []; foreach ($this->getIterator() as $definition) { if ($definition->hasTag($tag)) { $arrayOf[] = $definition->setContainer($this->getContainer())->resolveNew(); } } return $arrayOf; } public function getIterator(): Generator { yield from $this->definitions; } } Dependencies/League/Container/Definition/DefinitionAggregateInterface.php 0000644 00000001423 15174677547 0022626 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Definition; use IteratorAggregate; use WP_Rocket\Dependencies\League\Container\ContainerAwareInterface; interface DefinitionAggregateInterface extends ContainerAwareInterface, IteratorAggregate { public function add(string $id, $definition): DefinitionInterface; public function addShared(string $id, $definition): DefinitionInterface; public function getDefinition(string $id): DefinitionInterface; public function has(string $id): bool; public function hasTag(string $tag): bool; public function resolve(string $id); public function resolveNew(string $id); public function resolveTagged(string $tag): array; public function resolveTaggedNew(string $tag): array; } Dependencies/League/Container/Definition/DefinitionInterface.php 0000644 00000001761 15174677547 0021024 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Definition; use WP_Rocket\Dependencies\League\Container\ContainerAwareInterface; interface DefinitionInterface extends ContainerAwareInterface { public function addArgument($arg): DefinitionInterface; public function addArguments(array $args): DefinitionInterface; public function addMethodCall(string $method, array $args = []): DefinitionInterface; public function addMethodCalls(array $methods = []): DefinitionInterface; public function addTag(string $tag): DefinitionInterface; public function getAlias(): string; public function getConcrete(); public function hasTag(string $tag): bool; public function isShared(): bool; public function resolve(); public function resolveNew(); public function setAlias(string $id): DefinitionInterface; public function setConcrete($concrete): DefinitionInterface; public function setShared(bool $shared): DefinitionInterface; } Dependencies/League/Container/Definition/Definition.php 0000644 00000013031 15174677547 0017174 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\Definition; use WP_Rocket\Dependencies\League\Container\Argument\{ ArgumentResolverInterface, ArgumentResolverTrait, ArgumentInterface, LiteralArgumentInterface }; use WP_Rocket\Dependencies\League\Container\ContainerAwareTrait; use WP_Rocket\Dependencies\League\Container\Exception\ContainerException; use WP_Rocket\Dependencies\Psr\Container\ContainerInterface; use ReflectionClass; class Definition implements ArgumentResolverInterface, DefinitionInterface { use ArgumentResolverTrait; use ContainerAwareTrait; /** * @var string */ protected $alias; /** * @var mixed */ protected $concrete; /** * @var boolean */ protected $shared = false; /** * @var array */ protected $tags = []; /** * @var array */ protected $arguments = []; /** * @var array */ protected $methods = []; /** * @var mixed */ protected $resolved; /** * @param string $id * @param mixed|null $concrete */ public function __construct(string $id, $concrete = null) { $id = static::normaliseAlias($id); $concrete = $concrete ?? $id; $this->alias = $id; $this->concrete = $concrete; } public function addTag(string $tag): DefinitionInterface { $this->tags[$tag] = true; return $this; } public function hasTag(string $tag): bool { return isset($this->tags[$tag]); } public function setAlias(string $id): DefinitionInterface { $id = static::normaliseAlias($id); $this->alias = $id; return $this; } public function getAlias(): string { return $this->alias; } public function setShared(bool $shared = true): DefinitionInterface { $this->shared = $shared; return $this; } public function isShared(): bool { return $this->shared; } public function getConcrete() { return $this->concrete; } public function setConcrete($concrete): DefinitionInterface { $this->concrete = $concrete; $this->resolved = null; return $this; } public function addArgument($arg): DefinitionInterface { $this->arguments[] = $arg; return $this; } public function addArguments(array $args): DefinitionInterface { foreach ($args as $arg) { $this->addArgument($arg); } return $this; } public function addMethodCall(string $method, array $args = []): DefinitionInterface { $this->methods[] = [ 'method' => $method, 'arguments' => $args ]; return $this; } public function addMethodCalls(array $methods = []): DefinitionInterface { foreach ($methods as $method => $args) { $this->addMethodCall($method, $args); } return $this; } public function resolve() { if (null !== $this->resolved && $this->isShared()) { return $this->resolved; } return $this->resolveNew(); } public function resolveNew() { $concrete = $this->concrete; if (is_callable($concrete)) { $concrete = $this->resolveCallable($concrete); } if ($concrete instanceof LiteralArgumentInterface) { $this->resolved = $concrete->getValue(); return $concrete->getValue(); } if ($concrete instanceof ArgumentInterface) { $concrete = $concrete->getValue(); } if (is_string($concrete)) { if (class_exists($concrete)) { $concrete = $this->resolveClass($concrete); } elseif ($this->getAlias() === $concrete) { return $concrete; } } if (is_object($concrete)) { $concrete = $this->invokeMethods($concrete); } try { $container = $this->getContainer(); } catch (ContainerException $e) { $container = null; } // if we still have a string, try to pull it from the container // this allows for `alias -> alias -> ... -> concrete if (is_string($concrete) && $container instanceof ContainerInterface && $container->has($concrete)) { $concrete = $container->get($concrete); } $this->resolved = $concrete; return $concrete; } /** * @param callable $concrete * @return mixed */ protected function resolveCallable(callable $concrete) { $resolved = $this->resolveArguments($this->arguments); return call_user_func_array($concrete, $resolved); } protected function resolveClass(string $concrete): object { $resolved = $this->resolveArguments($this->arguments); $reflection = new ReflectionClass($concrete); return $reflection->newInstanceArgs($resolved); } protected function invokeMethods(object $instance): object { foreach ($this->methods as $method) { $args = $this->resolveArguments($method['arguments']); $callable = [$instance, $method['method']]; call_user_func_array($callable, $args); } return $instance; } public static function normaliseAlias(string $alias): string { if (strpos($alias, '\\') === 0) { return substr($alias, 1); } return $alias; } } Dependencies/League/Container/ServiceProvider/ServiceProviderInterface.php 0000644 00000000676 15174677547 0023076 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\ServiceProvider; use WP_Rocket\Dependencies\League\Container\ContainerAwareInterface; interface ServiceProviderInterface extends ContainerAwareInterface { public function getIdentifier(): string; public function provides(string $id): bool; public function register(): void; public function setIdentifier(string $id): ServiceProviderInterface; } Dependencies/League/Container/ServiceProvider/BootableServiceProviderInterface.php 0000644 00000000643 15174677547 0024540 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\ServiceProvider; interface BootableServiceProviderInterface extends ServiceProviderInterface { /** * Method will be invoked on registration of a service provider implementing * this interface. Provides ability for eager loading of Service Providers. * * @return void */ public function boot(): void; } Dependencies/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php 0000644 00000000752 15174677547 0024700 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\ServiceProvider; use IteratorAggregate; use WP_Rocket\Dependencies\League\Container\ContainerAwareInterface; interface ServiceProviderAggregateInterface extends ContainerAwareInterface, IteratorAggregate { public function add(ServiceProviderInterface $provider): ServiceProviderAggregateInterface; public function provides(string $id): bool; public function register(string $service): void; } Dependencies/League/Container/ServiceProvider/AbstractServiceProvider.php 0000644 00000001115 15174677547 0022726 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\ServiceProvider; use WP_Rocket\Dependencies\League\Container\ContainerAwareTrait; abstract class AbstractServiceProvider implements ServiceProviderInterface { use ContainerAwareTrait; /** * @var string */ protected $identifier; public function getIdentifier(): string { return $this->identifier ?? get_class($this); } public function setIdentifier(string $id): ServiceProviderInterface { $this->identifier = $id; return $this; } } Dependencies/League/Container/ServiceProvider/ServiceProviderAggregate.php 0000644 00000003621 15174677547 0023055 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container\ServiceProvider; use Generator; use WP_Rocket\Dependencies\League\Container\Exception\ContainerException; use WP_Rocket\Dependencies\League\Container\{ContainerAwareInterface, ContainerAwareTrait}; class ServiceProviderAggregate implements ServiceProviderAggregateInterface { use ContainerAwareTrait; /** * @var ServiceProviderInterface[] */ protected $providers = []; /** * @var array */ protected $registered = []; public function add(ServiceProviderInterface $provider): ServiceProviderAggregateInterface { if (in_array($provider, $this->providers, true)) { return $this; } $provider->setContainer($this->getContainer()); if ($provider instanceof BootableServiceProviderInterface) { $provider->boot(); } $this->providers[] = $provider; return $this; } public function provides(string $service): bool { foreach ($this->getIterator() as $provider) { if ($provider->provides($service)) { return true; } } return false; } public function getIterator(): Generator { yield from $this->providers; } public function register(string $service): void { if (false === $this->provides($service)) { throw new ContainerException( sprintf('(%s) is not provided by a service provider', $service) ); } foreach ($this->getIterator() as $provider) { if (in_array($provider->getIdentifier(), $this->registered, true)) { continue; } if ($provider->provides($service)) { $provider->register(); $this->registered[] = $provider->getIdentifier(); } } } } Dependencies/League/Container/DefinitionContainerInterface.php 0000644 00000001560 15174677547 0020574 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container; use WP_Rocket\Dependencies\League\Container\Definition\DefinitionInterface; use WP_Rocket\Dependencies\League\Container\Inflector\InflectorInterface; use WP_Rocket\Dependencies\League\Container\ServiceProvider\ServiceProviderInterface; use WP_Rocket\Dependencies\Psr\Container\ContainerInterface; interface DefinitionContainerInterface extends ContainerInterface { public function add(string $id, $concrete = null): DefinitionInterface; public function addServiceProvider(ServiceProviderInterface $provider): self; public function addShared(string $id, $concrete = null): DefinitionInterface; public function extend(string $id): DefinitionInterface; public function getNew($id); public function inflector(string $type, ?callable $callback = null): InflectorInterface; } Dependencies/League/Container/ContainerAwareInterface.php 0000644 00000000441 15174677547 0017540 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container; interface ContainerAwareInterface { public function getContainer(): DefinitionContainerInterface; public function setContainer(DefinitionContainerInterface $container): ContainerAwareInterface; } Dependencies/League/Container/ReflectionContainer.php 0000644 00000006303 15174677547 0016755 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container; use WP_Rocket\Dependencies\League\Container\Argument\{ArgumentResolverInterface, ArgumentResolverTrait}; use WP_Rocket\Dependencies\League\Container\Exception\ContainerException; use WP_Rocket\Dependencies\League\Container\Exception\NotFoundException; use WP_Rocket\Dependencies\Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionFunction; use ReflectionMethod; class ReflectionContainer implements ArgumentResolverInterface, ContainerInterface { use ArgumentResolverTrait; use ContainerAwareTrait; /** * @var boolean */ protected $cacheResolutions; /** * @var array */ protected $cache = []; public function __construct(bool $cacheResolutions = false) { $this->cacheResolutions = $cacheResolutions; } public function get($id, array $args = []) { if ($this->cacheResolutions === true && array_key_exists($id, $this->cache)) { return $this->cache[$id]; } if (!$this->has($id)) { throw new NotFoundException( sprintf('Alias (%s) is not an existing class and therefore cannot be resolved', $id) ); } $reflector = new ReflectionClass($id); $construct = $reflector->getConstructor(); if ($construct && !$construct->isPublic()) { throw new NotFoundException( sprintf('Alias (%s) has a non-public constructor and therefore cannot be instantiated', $id) ); } $resolution = $construct === null ? new $id() : $reflector->newInstanceArgs($this->reflectArguments($construct, $args)) ; if ($this->cacheResolutions === true) { $this->cache[$id] = $resolution; } return $resolution; } public function has($id): bool { return class_exists($id); } public function call(callable $callable, array $args = []) { if (is_string($callable) && strpos($callable, '::') !== false) { $callable = explode('::', $callable); } if (is_array($callable)) { if (is_string($callable[0])) { // if we have a definition container, try that first, otherwise, reflect try { $callable[0] = $this->getContainer()->get($callable[0]); } catch (ContainerException $e) { $callable[0] = $this->get($callable[0]); } } $reflection = new ReflectionMethod($callable[0], $callable[1]); if ($reflection->isStatic()) { $callable[0] = null; } return $reflection->invokeArgs($callable[0], $this->reflectArguments($reflection, $args)); } if (is_object($callable)) { $reflection = new ReflectionMethod($callable, '__invoke'); return $reflection->invokeArgs($callable, $this->reflectArguments($reflection, $args)); } $reflection = new ReflectionFunction(\Closure::fromCallable($callable)); return $reflection->invokeArgs($this->reflectArguments($reflection, $args)); } } Dependencies/League/Container/ContainerAwareTrait.php 0000644 00000002014 15174677547 0016721 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Dependencies\League\Container; use BadMethodCallException; use WP_Rocket\Dependencies\League\Container\Exception\ContainerException; trait ContainerAwareTrait { /** * @var ?DefinitionContainerInterface */ protected $container; public function setContainer(DefinitionContainerInterface $container): ContainerAwareInterface { $this->container = $container; if ($this instanceof ContainerAwareInterface) { return $this; } throw new BadMethodCallException(sprintf( 'Attempt to use (%s) while not implementing (%s)', ContainerAwareTrait::class, ContainerAwareInterface::class )); } public function getContainer(): DefinitionContainerInterface { if ($this->container instanceof DefinitionContainerInterface) { return $this->container; } throw new ContainerException('No container implementation has been set.'); } } common/admin-bar.php 0000644 00000017256 15174677547 0010441 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Add menu in admin bar. * From this menu, you can preload the cache files, clear entire domain cache or post cache (front & back-end). * * @since 1.3.5 Compatibility with qTranslate * @since 1.3.0 Compatibility with WPML * @since 1.0 * * @param Object $wp_admin_bar Admin bar object. */ function rocket_admin_bar( $wp_admin_bar ) { global $pagenow, $post; if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { $uri = filter_var( wp_unslash( $_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL ) ); /** * Filters to act on the referer url for the admin bar. * * @param string $uri Current uri */ $referer = (string) apply_filters( 'rocket_admin_bar_referer', $uri ); $referer = esc_url_raw( $referer ); $referer = '&_wp_http_referer=' . rawurlencode( remove_query_arg( 'fl_builder', $referer ) ); } else { $referer = ''; } $has_cap = false; $capabilities = [ 'rocket_manage_options', 'rocket_purge_cache', 'rocket_preload_cache', 'rocket_regenerate_critical_css', 'rocket_remove_unused_css', ]; foreach ( $capabilities as $cap ) { if ( current_user_can( $cap ) ) { $has_cap = true; break; } } if ( $has_cap ) { /** * Parent. */ $wp_admin_bar->add_menu( [ 'id' => 'wp-rocket', 'title' => WP_ROCKET_PLUGIN_NAME, 'href' => current_user_can( 'rocket_manage_options' ) ? admin_url( 'options-general.php?page=' . WP_ROCKET_PLUGIN_SLUG ) : false, ] ); } if ( current_user_can( 'rocket_manage_options' ) ) { /** * Settings. */ $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'rocket-settings', 'title' => __( 'Settings', 'rocket' ), 'href' => admin_url( 'options-general.php?page=' . WP_ROCKET_PLUGIN_SLUG ), ] ); } if ( current_user_can( 'rocket_purge_cache' ) ) { /** * Purge Cache. */ $action = 'purge_cache'; if ( rocket_valid_key() ) { $i18n_plugin = rocket_has_i18n(); if ( $i18n_plugin ) { // Parent. $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'purge-all', 'title' => (bool) get_rocket_option( 'manual_preload', false ) ? __( 'Clear and Preload Cache', 'rocket' ) : __( 'Clear Cache', 'rocket' ), ] ); $langlinks_default = []; // Add submenu for each active langs. switch ( $i18n_plugin ) { case 'wpml': $langlinks = get_rocket_wpml_langs_for_admin_bar(); break; case 'qtranslate': $langlinks = get_rocket_qtranslate_langs_for_admin_bar(); break; case 'qtranslate-x': $langlinks = get_rocket_qtranslate_langs_for_admin_bar( 'x' ); break; case 'polylang': $langlinks = get_rocket_polylang_langs_for_admin_bar(); break; default: /** * Filters the value of the lang links menu * * @param array $langlinks Array of languages. */ $langlinks = apply_filters( 'rocket_i18n_admin_bar_menu', [] ); if ( ! is_array( $langlinks ) ) { $langlinks = $langlinks_default; } } if ( $langlinks ) { if ( 'wpml' !== $i18n_plugin ) { // Add subemnu "All langs" (the one for WPML is already printed). $wp_admin_bar->add_menu( [ 'parent' => 'purge-all', 'id' => 'purge-all-all', 'title' => '<div class="dashicons-before dashicons-admin-site" style="line-height:1.5"> ' . __( 'All languages', 'rocket' ) . '</div>', 'href' => wp_nonce_url( admin_url( 'admin-post.php?action=' . $action . '&type=all&lang=all' . $referer ), $action . '_all' ), ] ); } foreach ( $langlinks as $lang ) { $wp_admin_bar->add_menu( [ 'parent' => 'purge-all', 'id' => 'purge-all-' . $lang['code'], 'title' => $lang['flag'] . ' ' . $lang['anchor'], 'href' => wp_nonce_url( admin_url( 'admin-post.php?action=' . $action . '&type=all&lang=' . $lang['code'] . $referer ), $action . '_all' ), ] ); } } } else { // Purge All. $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'purge-all', 'title' => (bool) get_rocket_option( 'manual_preload', false ) ? __( 'Clear and Preload Cache', 'rocket' ) : __( 'Clear Cache', 'rocket' ), 'href' => wp_nonce_url( admin_url( 'admin-post.php?action=' . $action . '&type=all' . $referer ), $action . '_all' ), ] ); } /** * Filters the rocket clear post admin bar menu. * * @since 3.11.4 * * @param bool $should_skip Should skip adding clear post to rocket option in admin bar. * @param type $post Post object. */ if ( ! apply_filters( 'rocket_skip_admin_bar_cache_purge_option', false, $post ) && rocket_can_display_options() ) { if ( is_admin() ) { /** * Purge a post. */ if ( $post && 'post.php' === $pagenow && isset( $_GET['action'], $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'purge-post', 'title' => __( 'Clear this post', 'rocket' ), 'href' => wp_nonce_url( admin_url( 'admin-post.php?action=' . $action . '&type=post-' . $post->ID . $referer ), $action . '_post-' . $post->ID ), ] ); } } else { /** * Purge this URL (frontend). */ $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'purge-url', 'title' => __( 'Purge this URL', 'rocket' ), 'href' => wp_nonce_url( admin_url( 'admin-post.php?action=' . $action . '&type=url' . $referer ), $action . '_url' ), ] ); } } } } if ( current_user_can( 'rocket_purge_sucuri_cache' ) ) { /** * Purge Sucuri cache if Sucuri is active. */ if ( get_rocket_option( 'sucury_waf_cache_sync', 0 ) ) { $action = 'rocket_purge_sucuri'; $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'purge-sucuri', 'title' => __( 'Purge Sucuri cache', 'rocket' ), 'href' => wp_nonce_url( admin_url( 'admin-post.php?action=' . $action . $referer ), $action ), ] ); } } /** * Fires when adding WP Rocket admin bar items * * @since 3.6 * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. */ do_action( 'rocket_admin_bar_items', $wp_admin_bar ); if ( current_user_can( 'rocket_manage_options' ) ) { $rocketcdn_status = get_transient( 'rocketcdn_status' ); if ( isset( $rocketcdn_status['subscription_active'] ) && 'running' === $rocketcdn_status['subscription_active'] ) { $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'purge-cdn-cache', 'title' => __( 'Purge RocketCDN cache', 'rocket' ), 'href' => wp_nonce_url( admin_url( 'admin-post.php?action=rocket_purge_rocketcdn' . $referer ), 'rocket_purge_rocketcdn' ), ] ); } /** * Go to WP Rocket Documentation. */ $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'docs', 'title' => __( 'Documentation', 'rocket' ), 'href' => get_rocket_documentation_url(), ] ); /** * Go to WP Rocket FAQ. */ $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'faq', 'title' => __( 'FAQ', 'rocket' ), 'href' => get_rocket_faq_url(), ] ); /** * Go to WP Rocket Support. */ $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'support', 'title' => __( 'Support', 'rocket' ), 'href' => rocket_get_external_url( 'support', [ 'utm_source' => 'wp_plugin', 'utm_medium' => 'wp_rocket', ] ), ] ); } } add_action( 'admin_bar_menu', 'rocket_admin_bar', PHP_INT_MAX - 10 ); common/purge.php 0000644 00000051053 15174677547 0007722 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; // Launch hooks that deletes all the cache domain. add_action( 'switch_theme', 'rocket_clean_domain' ); // When user change theme. add_action( 'wp_update_nav_menu', 'rocket_clean_domain' ); // When a custom menu is update. add_action( 'update_option_sidebars_widgets', 'rocket_clean_domain' ); // When you change the order of widgets. add_action( 'update_option_category_base', 'rocket_clean_domain' ); // When category permalink is updated. add_action( 'update_option_tag_base', 'rocket_clean_domain' ); // When tag permalink is updated. add_action( 'permalink_structure_changed', 'rocket_clean_domain' ); // When permalink structure is update. add_action( 'add_link', 'rocket_clean_domain' ); // When a link is added. add_action( 'edit_link', 'rocket_clean_domain' ); // When a link is updated. add_action( 'delete_link', 'rocket_clean_domain' ); // When a link is deleted. add_action( 'customize_save', 'rocket_clean_domain' ); // When customizer is saved. add_action( 'update_option_theme_mods_' . get_option( 'stylesheet' ), 'rocket_clean_domain' ); // When location of a menu is updated. /** * Purge cache When a widget is updated. * * @since 1.1.1 * * @param object $instance Widget instance. * @return object Widget instance. */ function rocket_widget_update_callback( $instance ) { rocket_clean_domain(); return $instance; } add_filter( 'widget_update_callback', 'rocket_widget_update_callback' ); if ( ! function_exists( 'rocket_get_purge_urls' ) ) { /** * Get post purge urls. * * @since 3.4.3 * * @param int $post_id The post ID. * @param WP_Post $post WP_Post object. * @return array Array with all URLs which need to be purged. */ function rocket_get_purge_urls( $post_id, $post ) { $purge_urls = []; // Get the permalink structure. $permalink_structure = get_rocket_sample_permalink( $post_id ); // Get permalink. $permalink = str_replace( [ '%postname%', '%pagename%' ], $permalink_structure[1], $permalink_structure[0] ); // Add permalink. if ( rocket_extract_url_component( $permalink, PHP_URL_PATH ) !== '/' ) { $purge_urls[] = $permalink; } // Add Posts page. if ( 'post' === $post->post_type && (int) get_option( 'page_for_posts' ) > 0 ) { $purge_urls[] = get_permalink( get_option( 'page_for_posts' ) ); } // Add Post Type archive. $post_type = $post->post_type; if ( 'post' !== $post_type ) { $post_type_archive = get_post_type_archive_link( $post_type ); if ( $post_type_archive ) { $post_type_archive = trailingslashit( $post_type_archive ); $purge_urls[] = $post_type_archive . 'index(.*).html'; $purge_urls[] = $post_type_archive . $GLOBALS['wp_rewrite']->pagination_base; } } // Add next post. $next_post = get_adjacent_post( false, '', false ); if ( $next_post ) { $purge_urls[] = get_permalink( $next_post ); } // Add next post in same category. $next_in_same_cat_post = get_adjacent_post( true, '', false ); if ( $next_in_same_cat_post && $next_in_same_cat_post !== $next_post ) { $purge_urls[] = get_permalink( $next_in_same_cat_post ); } // Add previous post. $previous_post = get_adjacent_post( false, '', true ); if ( $previous_post ) { $purge_urls[] = get_permalink( $previous_post ); } // Add previous post in same category. $previous_in_same_cat_post = get_adjacent_post( true, '', true ); if ( $previous_in_same_cat_post && $previous_in_same_cat_post !== $previous_post ) { $purge_urls[] = get_permalink( $previous_in_same_cat_post ); } // Add urls page to purge every time a post is save. $cache_purge_pages = get_rocket_option( 'cache_purge_pages' ); if ( $cache_purge_pages ) { global $blog_id; $home_url = get_option( 'home' ); if ( ! empty( $blog_id ) && is_multisite() ) { switch_to_blog( $blog_id ); $home_url = get_option( 'home' ); restore_current_blog(); } $home_parts = get_rocket_parse_url( $home_url ); $home_url = "{$home_parts['scheme']}://{$home_parts['host']}"; $cache_path = rocket_get_constant( 'WP_ROCKET_CACHE_PATH' ) . $home_parts['host']; foreach ( $cache_purge_pages as $page ) { // Check if it contains regex pattern. if ( strstr( $page, '*' ) ) { $matches_files = _rocket_get_recursive_dir_files_by_regex( '#' . $page . '#i' ); foreach ( $matches_files as $file ) { // Convert path to URL. $purge_urls[] = str_replace( $cache_path, untrailingslashit( $home_url ), $file->getPath() ); } continue; } $purge_urls[] = trailingslashit( $home_url ) . ltrim( $page, '/' ); } } // Add the author page. $author_url = trailingslashit( get_author_posts_url( $post->post_author ) ); if ( trailingslashit( site_url() ) !== $author_url && trailingslashit( home_url() ) !== $author_url ) { $purge_urls[] = $author_url; } // Add all parents. $parents = get_post_ancestors( $post_id ); if ( (bool) $parents ) { foreach ( $parents as $parent_id ) { $purge_urls[] = get_permalink( $parent_id ); } } // Remove entries with empty values in array. $purge_urls = array_filter( $purge_urls, 'is_string' ); return array_flip( array_flip( $purge_urls ) ); } } /** * Update cache when a post is updated or commented * * @since 3.0.5 Don't purge for attachment post type * @since 2.8 Only add post type archive if post type is not post * @since 2.6 Purge the page defined in "Posts page" option * @since 2.5.5 Don't cache for auto-draft post status * @since 1.3.2 Add wp_update_comment_count to purge cache when a comment is added/updated/deleted * @since 1.3.0 Compatibility with WPML * @since 1.3.0 Add 2 hooks : before_rocket_clean_post, after_rocket_clean_post * @since 1.3.0 Purge all parents of the post and the author page * @since 1.2.2 Add wp_trash_post and delete_post to purge cache when a post is trashed or deleted * @since 1.1.3 Use clean_post_cache instead of transition_post_status, transition_comment_status and preprocess_comment * @since 1.0 * * @param int $post_id The post ID. * @param WP_Post $post WP_Post object. */ function rocket_clean_post( $post_id, $post = null ) { if ( rocket_is_importing() ) { return; } static $done = []; if ( isset( $done[ $post_id ] ) ) { return false; } $done[ $post_id ] = 1; if ( defined( 'DOING_AUTOSAVE' ) ) { return false; } $purge_urls = []; // Get all post infos if the $post object was not supplied. if ( is_null( $post ) ) { $post = get_post( $post_id ); } // Return if $post is not an object. if ( ! is_object( $post ) ) { return false; } // No purge for specific conditions. if ( 'auto-draft' === $post->post_status || 'draft' === $post->post_status || empty( $post->post_type ) || 'nav_menu_item' === $post->post_type || 'attachment' === $post->post_type ) { return false; } // Don't purge if post's post type is not public or not publicly queryable. $post_type = get_post_type_object( $post->post_type ); if ( ! is_object( $post_type ) || true !== $post_type->public ) { return false; } // Get the post language. $i18n_plugin = rocket_has_i18n(); $lang = ''; if ( 'wpml' === $i18n_plugin && ! rocket_is_plugin_active( 'woocommerce-multilingual/wpml-woocommerce.php' ) ) { // WPML. $lang = $GLOBALS['sitepress']->get_language_for_element( $post_id, 'post_' . get_post_type( $post_id ) ); } elseif ( 'polylang' === $i18n_plugin && function_exists( 'pll_get_post_language' ) ) { // Polylang. $lang = pll_get_post_language( $post_id ); } $purge_urls = rocket_get_purge_urls( $post_id, $post ); /** * Fires before cache files related with the post are deleted * * @since 1.3.0 * * @param WP_Post $post The post object * @param array $purge_urls URLs cache files to remove * @param string $lang The post language */ do_action( 'before_rocket_clean_post', $post, $purge_urls, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals /** * Filter URLs cache files to remove * * @since 1.0 * * @param array $purge_urls List of URLs cache files to remove */ $purge_urls = apply_filters( 'rocket_post_purge_urls', $purge_urls, $post ); // Purge all files. rocket_clean_files( $purge_urls ); if ( wpm_apply_filters_typed( 'boolean', 'rocket_clean_home_after_clean_post', true, $post_id ) ) { // Never forget to purge homepage and their pagination. rocket_clean_home( $lang ); } // Purge home feeds (blog & comments). if ( has_filter( 'rocket_cache_reject_uri', 'wp_rocket_cache_feed' ) !== false ) { rocket_clean_home_feeds(); } /** * URLs cache files to remove after cache files related with the post are deleted * * @param array $purge_urls URLs cache files to remove * @param WP_Post $post The post object * @returns array $purge_urls URLs cache files to remove */ $purge_urls = (array) apply_filters( 'after_rocket_clean_post_urls', $purge_urls, $post ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals /** * Fires after cache files related with the post are deleted * * @since 1.3.0 * * @param WP_Post $post The post object * @param array $purge_urls URLs cache files to remove * @param string $lang The post language */ do_action( 'after_rocket_clean_post', $post, $purge_urls, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals return true; } add_action( 'wp_trash_post', 'rocket_clean_post' ); add_action( 'delete_post', 'rocket_clean_post' ); add_action( 'clean_post_cache', 'rocket_clean_post' ); add_action( 'wp_update_comment_count', 'rocket_clean_post' ); /** * Purge WP Rocket cache when post status is changed from publish to draft. * * @since 3.4.3 * * @param int $post_id The post ID. * @param array $post_data Array of unslashed post data. */ function rocket_clean_post_cache_on_status_change( $post_id, $post_data ) { if ( rocket_is_importing() ) { return; } if ( 'publish' !== get_post_field( 'post_status', $post_id ) || 'draft' !== $post_data['post_status'] ) { return; } $purge_urls = []; $post = get_post( $post_id ); $post_type = get_post_type_object( $post->post_type ); // Return if $post is not an object or $post_type is not public. if ( ! is_object( $post ) || true !== $post_type->public ) { return; } // Get the post language. $i18n_plugin = rocket_has_i18n(); $lang = false; if ( 'wpml' === $i18n_plugin && ! rocket_is_plugin_active( 'woocommerce-multilingual/wpml-woocommerce.php' ) ) { // WPML. $lang = $GLOBALS['sitepress']->get_language_for_element( $post_id, 'post_' . get_post_type( $post_id ) ); } elseif ( 'polylang' === $i18n_plugin && function_exists( 'pll_get_post_language' ) ) { // Polylang. $lang = pll_get_post_language( $post_id ); } $purge_urls = rocket_get_purge_urls( $post_id, $post ); /** * Filter URLs cache files to remove * * @since 1.0 * * @param array $purge_urls List of URLs cache files to remove */ $purge_urls = apply_filters( 'rocket_post_purge_urls', $purge_urls, $post ); /** * Fires before cache files related with the post are deleted * * @since 1.3.0 * * @param WP_Post $post The post object * @param array $purge_urls URLs cache files to remove * @param string $lang The post language */ do_action( 'before_rocket_clean_post', $post, $purge_urls, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound // Purge all files. rocket_clean_files( $purge_urls ); // Never forget to purge homepage and their pagination. rocket_clean_home( $lang ); // Purge home feeds (blog & comments). rocket_clean_home_feeds(); /** * URLs cache files to remove after cache files related with the post are deleted * * @param array $purge_urls URLs cache files to remove * @param WP_Post $post The post object * @returns array $purge_urls URLs cache files to remove */ $purge_urls = (array) apply_filters( 'after_rocket_clean_post_urls', $purge_urls, $post ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals /** * Fires after cache files related with the post are deleted * * @since 1.3.0 * * @param WP_Post $post The post object * @param array $purge_urls URLs cache files to remove * @param string $lang The post language */ do_action( 'after_rocket_clean_post', $post, $purge_urls, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } add_action( 'pre_post_update', 'rocket_clean_post_cache_on_status_change', 10, 2 ); /** * Add pattern to clean files of connected users * * @since 2.0 * * @param array $urls An array of URLs to clean. * @return array An array of pattern to use for clearing the cache */ function rocket_clean_files_users( $urls ) { $pattern_urls = []; foreach ( $urls as $url ) { $parse_url = get_rocket_parse_url( $url ); $pattern_urls[] = $parse_url['scheme'] . '://' . $parse_url['host'] . '*' . $parse_url['path']; } return $pattern_urls; } add_filter( 'rocket_clean_files', 'rocket_clean_files_users' ); /** * Return all translated version of a post when qTranslate is used. * Use the "rocket_post_purge_urls" filter to insert URLs of traduction post. * * @since 1.3.5 * * @param array $urls An array of URLs to clean. * @return array Updated array of URLs to clean */ function rocket_post_purge_urls_for_qtranslate( $urls ) { global $q_config; if ( ! $urls ) { return []; } $i18n_plugin = rocket_has_i18n(); if ( 'qtranslate' !== $i18n_plugin && 'qtranslate-x' !== $i18n_plugin ) { return $urls; } // Get all languages. $enabled_languages = $q_config['enabled_languages']; // Remove default language. $enabled_languages = array_diff( $enabled_languages, [ $q_config['default_language'] ] ); // Add translate URLs. foreach ( $urls as $url ) { foreach ( $enabled_languages as $lang ) { if ( 'qtranslate' === $i18n_plugin ) { $urls[] = qtrans_convertURL( $url, $lang, true ); } elseif ( 'qtranslate-x' === $i18n_plugin ) { $urls[] = qtranxf_convertURL( $url, $lang, true ); } } } return $urls; } add_filter( 'rocket_post_purge_urls', 'rocket_post_purge_urls_for_qtranslate' ); /** * Purge Cache file System in Admin Bar * * @since 1.3.0 Compatibility with WPML * @since 1.0 */ function do_admin_post_rocket_purge_cache() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals if ( isset( $_GET['type'], $_GET['_wpnonce'] ) ) { $type_raw = sanitize_key( $_GET['type'] ); $type_array = explode( '-', $type_raw ); $type = $type_array[0]; $id = isset( $type_array[1] ) && is_numeric( $type_array[1] ) ? absint( $type_array[1] ) : 0; $taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_title( wp_unslash( $_GET['taxonomy'] ) ) : ''; $url = ''; if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'purge_cache_' . $type_raw ) ) { wp_nonce_ays( '' ); return; } if ( ! current_user_can( 'rocket_purge_cache' ) ) { return; } switch ( $type ) { // Clear all cache domain. case 'all': set_transient( 'rocket_clear_cache', 'all', HOUR_IN_SECONDS ); // Remove all cache files. $lang = isset( $_GET['lang'] ) && 'all' !== $_GET['lang'] ? sanitize_key( $_GET['lang'] ) : ''; // Remove all cache files. rocket_clean_domain( $lang ); if ( '' === $lang ) { // Remove all minify cache files. rocket_clean_minify(); rocket_clean_cache_busting(); // Generate a new random key for minify cache file. $options = get_option( WP_ROCKET_SLUG ); $options['minify_css_key'] = create_rocket_uniqid(); $options['minify_js_key'] = create_rocket_uniqid(); remove_all_filters( 'update_option_' . WP_ROCKET_SLUG ); update_option( WP_ROCKET_SLUG, $options ); } if ( get_rocket_option( 'manual_preload' ) && ( ! defined( 'WP_ROCKET_DEBUG' ) || ! WP_ROCKET_DEBUG ) ) { $home_url = get_rocket_i18n_home_url( $lang ); /** * Filters the arguments for the preload request being triggered after clearing the cache. * * @since 3.4 * * @param array $args Request arguments. */ $args = (array) apply_filters( 'rocket_preload_after_purge_cache_request_args', [ 'blocking' => false, 'timeout' => 0.01, 'user-agent' => 'WP Rocket/Homepage_Preload_After_Purge_Cache', 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals ] ); wp_safe_remote_get( $home_url, $args ); /** * Fires after automatically preloading the homepage, which occurs after purging the cache. * * @since 3.5 * * @param string $home_url URL to the homepage being preloaded. * @param string $lang The lang of the homepage. * @param array $args Arguments used for the preload request. */ do_action( 'rocket_after_preload_after_purge_cache', $home_url, $lang, $args ); } rocket_dismiss_box( 'rocket_warning_plugin_modification' ); rocket_renew_box( 'preload_notice' ); break; // Clear terms, homepage and other files associated at current post in back-end. case 'post': rocket_clean_post( $id ); set_transient( 'rocket_clear_cache', 'post', HOUR_IN_SECONDS ); break; // Clear a specific term. case 'term': rocket_clean_term( $id, $taxonomy ); set_transient( 'rocket_clear_cache', 'term', HOUR_IN_SECONDS ); break; // Clear a specific user. case 'user': rocket_clean_user( $id ); set_transient( 'rocket_clear_cache', 'user', HOUR_IN_SECONDS ); break; // Clear cache file of the current page in front-end. case 'url': $url = wp_get_referer(); if ( 0 !== strpos( $url, 'http' ) ) { $parse_url = get_rocket_parse_url( untrailingslashit( home_url() ) ); $url = $parse_url['scheme'] . '://' . $parse_url['host'] . $url; } if ( home_url( '/' ) === $url ) { rocket_clean_home(); } else { rocket_clean_files( $url ); } break; default: wp_nonce_ays( '' ); return; } /** * Fires after the cache is cleared. * * @since 3.6 * * @param string $type Type of cache clearance: 'all', 'post', 'term', 'user', 'url'. * @param int $id The post ID, term ID, or user ID being cleared. 0 when $type is not 'post', 'term', or 'user'. * @param string $taxonomy The taxonomy the term being cleared belong to. '' when $type is not 'term'. * @param string $url The URL being cleared. '' when $type is not 'url'. */ do_action( 'rocket_purge_cache', $type, $id, $taxonomy, $url ); wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } } add_action( 'admin_post_purge_cache', 'do_admin_post_rocket_purge_cache' ); /** * Clean the cache when the current theme is updated. * * @param WP_Upgrader $wp_upgrader WP_Upgrader instance. * @param array $hook_extra Array of bulk item update data. */ function rocket_clean_cache_theme_update( $wp_upgrader, $hook_extra ) { if ( rocket_is_importing() ) { return; } if ( ! isset( $hook_extra['action'] ) || 'update' !== $hook_extra['action'] ) { return; } if ( ! isset( $hook_extra['type'] ) || 'theme' !== $hook_extra['type'] ) { return; } if ( ! isset( $hook_extra['themes'] ) || ! is_array( $hook_extra['themes'] ) ) { return; } $current_theme = wp_get_theme(); $themes = [ $current_theme->get_template(), // Parent theme. $current_theme->get_stylesheet(), // Child theme. ]; // Bail out if the current theme or its parent is not updating. if ( empty( array_intersect( $hook_extra['themes'], $themes ) ) ) { return; } rocket_clean_domain(); } add_action( 'upgrader_process_complete', 'rocket_clean_cache_theme_update', 10, 2 ); // When a theme is updated. /** * Purge WP Rocket cache on Slug / Permalink change. * * @since 3.4.2 * * @param int $post_id The post ID. * @param array $post_data Array of unslashed post data. */ function rocket_clean_post_cache_on_slug_change( $post_id, $post_data ) { if ( rocket_is_importing() ) { return; } // Bail out if the post status is draft, pending or auto-draft. if ( in_array( get_post_field( 'post_status', $post_id ), [ 'draft', 'pending', 'auto-draft', 'trash' ], true ) ) { return; } $post_name = get_post_field( 'post_name', $post_id ); // Bail out if the slug hasn't changed. if ( $post_name === $post_data['post_name'] ) { return; } // Bail out if the old slug has changed, but is empty. if ( empty( $post_name ) ) { return; } rocket_clean_files( get_the_permalink( $post_id ) ); } add_action( 'pre_post_update', 'rocket_clean_post_cache_on_slug_change', PHP_INT_MAX, 2 ); classes/dependencies/wp-media/background-processing/wp-background-process.php 0000644 00000026117 15174677547 0023611 0 ustar 00 <?php /** * WP Background Process * * @package WP-Background-Processing */ /** * Abstract WP_Rocket_WP_Background_Process class. * * @abstract * @extends WP_Rocket_WP_Async_Request */ abstract class WP_Rocket_WP_Background_Process extends WP_Rocket_WP_Async_Request { /** * Action * * (default value: 'background_process') * * @var string * @access protected */ protected $action = 'background_process'; /** * Start time of current process. * * (default value: 0) * * @var int * @access protected */ protected $start_time = 0; /** * Cron_hook_identifier * * @var mixed * @access protected */ protected $cron_hook_identifier; /** * Cron_interval_identifier * * @var mixed * @access protected */ protected $cron_interval_identifier; /** * Initiate new background process */ public function __construct() { parent::__construct(); $this->cron_hook_identifier = $this->identifier . '_cron'; $this->cron_interval_identifier = $this->identifier . '_cron_interval'; add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); } /** * Dispatch * * @access public * @return void */ public function dispatch() { // Schedule the cron healthcheck. $this->schedule_event(); // Perform remote post. return parent::dispatch(); } /** * Push to queue * * @param mixed $data Data. * * @return $this */ public function push_to_queue( $data ) { $this->data[] = $data; return $this; } /** * Save queue * * @return $this */ public function save() { $key = $this->generate_key(); if ( ! empty( $this->data ) ) { update_site_option( $key, $this->data ); } return $this; } /** * Update queue * * @param string $key Key. * @param array $data Data. * * @return $this */ public function update( $key, $data ) { if ( ! empty( $data ) ) { update_site_option( $key, $data ); } return $this; } /** * Delete queue * * @param string $key Key. * * @return $this */ public function delete( $key ) { delete_site_option( $key ); return $this; } /** * Generate key * * Generates a unique key based on microtime. Queue items are * given a unique key so that they can be merged upon save. * * @param int $length Length. * * @return string */ protected function generate_key( $length = 64 ) { $unique = md5( microtime() . rand() ); $prepend = $this->identifier . '_batch_'; return substr( $prepend . $unique, 0, $length ); } /** * Maybe process queue * * Checks whether data exists within the queue and that * the process is not already running. */ public function maybe_handle() { // Don't lock up other requests while processing session_write_close(); if ( $this->is_process_running() ) { // Background process already running. wp_die(); } if ( $this->is_queue_empty() ) { // No data to process. wp_die(); } check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); wp_die(); } /** * Is queue empty * * @return bool */ protected function is_queue_empty() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; } $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; $count = $wpdb->get_var( $wpdb->prepare( " SELECT COUNT(*) FROM {$table} WHERE {$column} LIKE %s ", $key ) ); return ( $count > 0 ) ? false : true; } /** * Is process running * * Check whether the current process is already running * in a background process. */ protected function is_process_running() { if ( get_site_transient( $this->identifier . '_process_lock' ) ) { // Process already running. return true; } return false; } /** * Is process cancelled * * Check whether the current process is cancelled * in a background process. */ protected function is_process_cancelled() { if ( ! \rocket_direct_filesystem()->exists( WP_ROCKET_CACHE_ROOT_PATH . '.' . $this->identifier . '_process_cancelled' ) ) { return false; } return true; } /** * Lock process * * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that * defined in the time_exceeded() method. */ protected function lock_process() { $this->start_time = time(); // Set start time of current process. $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); } /** * Unlock process * * Unlock the process so that other instances can spawn. * * @return $this */ protected function unlock_process() { delete_site_transient( $this->identifier . '_process_lock' ); return $this; } /** * Get batch * * @return stdClass Return the first batch from the queue */ protected function get_batch() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; $key_column = 'option_id'; $value_column = 'option_value'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; $key_column = 'meta_id'; $value_column = 'meta_value'; } $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; $query = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$table} WHERE {$column} LIKE %s ORDER BY {$key_column} ASC LIMIT 1 ", $key ) ); $batch = new stdClass(); $batch->key = $query->$column; $batch->data = maybe_unserialize( $query->$value_column ); return $batch; } /** * Handle * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. */ protected function handle() { $this->lock_process(); do { $batch = $this->get_batch(); foreach ( $batch->data as $key => $value ) { $task = $this->task( $value ); if ( false !== $task ) { $batch->data[ $key ] = $task; } else { unset( $batch->data[ $key ] ); } if ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_process_cancelled() ) { // Batch limits reached. break; } } // Update or delete current batch. if ( ! empty( $batch->data ) && ! $this->is_process_cancelled() ) { $this->update( $batch->key, $batch->data ); } else { $this->complete_batch(); $this->delete( $batch->key ); } } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() && ! $this->is_process_cancelled() ); $this->unlock_process(); // Start next batch or complete process. if ( ! $this->is_queue_empty() ) { $this->dispatch(); } else { $this->complete(); } wp_die(); } /** * Memory exceeded * * Ensures the batch process never exceeds 90% * of the maximum WordPress memory. * * @return bool */ protected function memory_exceeded() { $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory $current_memory = memory_get_usage( true ); $return = false; if ( $current_memory >= $memory_limit ) { $return = true; } return apply_filters( $this->identifier . '_memory_exceeded', $return ); } /** * Get memory limit * * @return int */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { // Sensible default. $memory_limit = '128M'; } if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } return wp_convert_hr_to_bytes( $memory_limit ); } /** * Time exceeded. * * Ensures the batch never exceeds a sensible time limit. * A timeout limit of 30s is common on shared hosting. * * @return bool */ protected function time_exceeded() { $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds $return = false; if ( time() >= $finish ) { $return = true; } return apply_filters( $this->identifier . '_time_exceeded', $return ); } /** * Current batch is completed. */ protected function complete_batch(){ // Override this code on the instantiated process class WP_Rocket_just if needed. } /** * Complete. * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). */ protected function complete() { // Unschedule the cron healthcheck. $this->clear_scheduled_event(); \rocket_direct_filesystem()->delete( WP_ROCKET_CACHE_ROOT_PATH . '.' . $this->identifier . '_process_cancelled' ); } /** * Schedule cron healthcheck * * @param mixed $schedules Schedules. * * @return mixed */ public function schedule_cron_healthcheck( $schedules ) { $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); if ( property_exists( $this, 'cron_interval' ) ) { $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); } // Adds every 5 minutes to the existing schedules. $schedules[ $this->identifier . '_cron_interval' ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), ); return $schedules; } /** * Handle cron healthcheck * * Restart the background process if not already running * and data exists in the queue. */ public function handle_cron_healthcheck() { if ( $this->is_process_running() ) { // Background process already running. exit; } if ( $this->is_queue_empty() ) { // No data to process. $this->clear_scheduled_event(); exit; } $this->handle(); exit; } /** * Schedule event */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); } } /** * Clear scheduled event */ protected function clear_scheduled_event() { $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); if ( $timestamp ) { wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); } } /** * Cancel Process * * Stop processing queue items, clear cronjob and delete batch. * */ public function cancel_process() { if ( ! $this->is_queue_empty() ) { $batch = $this->get_batch(); $this->delete( $batch->key ); $this->unlock_process(); wp_clear_scheduled_hook( $this->cron_hook_identifier ); \rocket_direct_filesystem()->touch( WP_ROCKET_CACHE_ROOT_PATH . '.' . $this->identifier . '_process_cancelled' ); } } /** * Task * * Override this method to perform any actions required on each * queue item. Return the modified item WP_Rocket_for further processing * in the next pass through. Or, return false to remove the * item from the queue. * * @param mixed $item Queue item to iterate over. * * @return mixed */ abstract protected function task( $item ); } classes/dependencies/wp-media/background-processing/wp-async-request.php 0000644 00000006265 15174677547 0022623 0 ustar 00 <?php /** * WP Async Request * * @package WP-Background-Processing */ /** * Abstract WP_Rocket_WP_Async_Request class. * * @abstract */ abstract class WP_Rocket_WP_Async_Request { /** * Prefix * * (default value: 'wp') * * @var string * @access protected */ protected $prefix = 'wp'; /** * Action * * (default value: 'async_request') * * @var string * @access protected */ protected $action = 'async_request'; /** * Identifier * * @var mixed * @access protected */ protected $identifier; /** * Data * * (default value: array()) * * @var array * @access protected */ protected $data = array(); /** * Initiate new async request */ public function __construct() { $this->identifier = $this->prefix . '_' . $this->action; add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); } /** * Set data used during the request * * @param array $data Data. * * @return $this */ public function data( $data ) { $this->data = $data; return $this; } /** * Dispatch the async request * * @return array|WP_Error */ public function dispatch() { $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); $args = $this->get_post_args(); return wp_remote_post( esc_url_raw( $url ), $args ); } /** * Get query args * * @return array */ protected function get_query_args() { if ( property_exists( $this, 'query_args' ) ) { return $this->query_args; } $args = array( 'action' => $this->identifier, 'nonce' => wp_create_nonce( $this->identifier ), ); /** * Filters the post arguments used during an async request. * * @param array $url */ return apply_filters( $this->identifier . '_query_args', $args ); } /** * Get query URL * * @return string */ protected function get_query_url() { if ( property_exists( $this, 'query_url' ) ) { return $this->query_url; } $url = admin_url( 'admin-ajax.php' ); /** * Filters the post arguments used during an async request. * * @param string $url */ return apply_filters( $this->identifier . '_query_url', $url ); } /** * Get post args * * @return array */ protected function get_post_args() { if ( property_exists( $this, 'post_args' ) ) { return $this->post_args; } $args = array( 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, 'cookies' => $_COOKIE, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), ); /** * Filters the post arguments used during an async request. * * @param array $args */ return apply_filters( $this->identifier . '_post_args', $args ); } /** * Maybe handle * * Check WP_Rocket_for correct nonce and pass to handler. */ public function maybe_handle() { // Don't lock up other requests while processing session_write_close(); check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); wp_die(); } /** * Handle * * Override this method to perform any actions required * during the async request. */ abstract protected function handle(); } classes/dependencies/wp-media/background-processing/composer.json 0000644 00000002445 15174677547 0021401 0 ustar 00 { "name": "wp-media/background-processing", "description": "Async & Background Tasks Processing", "homepage": "https://github.com/wp-media/background-processing", "license": "GPL-2.0+", "authors": [ { "name": "WP Media", "email": "contact@wp-media.me", "homepage": "https://wp-media.me" } ], "type": "library", "config": { "sort-packages": true }, "support": { "issues": "https://github.com/wp-media/background-processing/issues", "source": "https://github.com/wp-media/background-processing" }, "require-dev": { "php": "^5.6 || ^7", "brain/monkey": "^2.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", "phpcompatibility/phpcompatibility-wp": "^2.0", "phpunit/phpunit": "^5.7 || ^7", "wp-coding-standards/wpcs": "^2", "wp-media/phpunit": "^1.0" }, "autoload": { "classmap": [ "" ] }, "autoload-dev": {}, "scripts": { "test-unit": "\"vendor/bin/wpmedia-phpunit\" unit path=Tests/Unit", "test-integration": "\"vendor/bin/wpmedia-phpunit\" integration path=Tests/Integration/", "run-tests": [ "@test-unit", "@test-integration" ], "install-codestandards": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run", "phpcs": "phpcs --basepath=.", "phpcs-changed": "./bin/phpcs-changed.sh", "phpcs:fix": "phpcbf" } } classes/dependencies/mobiledetect/mobiledetectlib/Mobile_Detect.php 0000644 00000262523 15174677547 0021667 0 ustar 00 <?php /** * Mobile Detect Library * Motto: "Every business should have a mobile detection script to detect mobile readers" * * WP_Rocket_Mobile_Detect is a lightweight PHP class WP_Rocket_for detecting mobile devices (including tablets). * It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment. * * Homepage: http://mobiledetect.net * GitHub: https://github.com/serbanghita/Mobile-Detect * README: https://github.com/serbanghita/Mobile-Detect/blob/master/README.md * CONTRIBUTING: https://github.com/serbanghita/Mobile-Detect/blob/master/docs/CONTRIBUTING.md * KNOWN LIMITATIONS: https://github.com/serbanghita/Mobile-Detect/blob/master/docs/KNOWN_LIMITATIONS.md * EXAMPLES: https://github.com/serbanghita/Mobile-Detect/wiki/Code-examples * * @license https://github.com/serbanghita/Mobile-Detect/blob/master/LICENSE * @author Serban Ghita <serbanghita@gmail.com> (since 2012) * @author Nick Ilyin <nick.ilyin@gmail.com> * @author: Victor Stanciu <vic.stanciu@gmail.com> (original author) * * @version 2.8.45 * * Auto-generated isXXXX() magic methods. * php -a examples/dump_magic_methods.php * * @method bool isiPhone() * @method bool isBlackBerry() * @method bool isPixel() * @method bool isHTC() * @method bool isNexus() * @method bool isDell() * @method bool isMotorola() * @method bool isSamsung() * @method bool isLG() * @method bool isSony() * @method bool isAsus() * @method bool isXiaomi() * @method bool isNokiaLumia() * @method bool isMicromax() * @method bool isPalm() * @method bool isVertu() * @method bool isPantech() * @method bool isFly() * @method bool isWiko() * @method bool isiMobile() * @method bool isSimValley() * @method bool isWolfgang() * @method bool isAlcatel() * @method bool isNintendo() * @method bool isAmoi() * @method bool isINQ() * @method bool isOnePlus() * @method bool isGenericPhone() * @method bool isiPad() * @method bool isNexusTablet() * @method bool isGoogleTablet() * @method bool isSamsungTablet() * @method bool isKindle() * @method bool isSurfaceTablet() * @method bool isHPTablet() * @method bool isAsusTablet() * @method bool isBlackBerryTablet() * @method bool isHTCtablet() * @method bool isMotorolaTablet() * @method bool isNookTablet() * @method bool isAcerTablet() * @method bool isToshibaTablet() * @method bool isLGTablet() * @method bool isFujitsuTablet() * @method bool isPrestigioTablet() * @method bool isLenovoTablet() * @method bool isDellTablet() * @method bool isYarvikTablet() * @method bool isMedionTablet() * @method bool isArnovaTablet() * @method bool isIntensoTablet() * @method bool isIRUTablet() * @method bool isMegafonTablet() * @method bool isEbodaTablet() * @method bool isAllViewTablet() * @method bool isArchosTablet() * @method bool isAinolTablet() * @method bool isNokiaLumiaTablet() * @method bool isSonyTablet() * @method bool isPhilipsTablet() * @method bool isCubeTablet() * @method bool isCobyTablet() * @method bool isMIDTablet() * @method bool isMSITablet() * @method bool isSMiTTablet() * @method bool isRockChipTablet() * @method bool isFlyTablet() * @method bool isbqTablet() * @method bool isHuaweiTablet() * @method bool isNecTablet() * @method bool isPantechTablet() * @method bool isBronchoTablet() * @method bool isVersusTablet() * @method bool isZyncTablet() * @method bool isPositivoTablet() * @method bool isNabiTablet() * @method bool isKoboTablet() * @method bool isDanewTablet() * @method bool isTexetTablet() * @method bool isPlaystationTablet() * @method bool isTrekstorTablet() * @method bool isPyleAudioTablet() * @method bool isAdvanTablet() * @method bool isDanyTechTablet() * @method bool isGalapadTablet() * @method bool isMicromaxTablet() * @method bool isKarbonnTablet() * @method bool isAllFineTablet() * @method bool isPROSCANTablet() * @method bool isYONESTablet() * @method bool isChangJiaTablet() * @method bool isGUTablet() * @method bool isPointOfViewTablet() * @method bool isOvermaxTablet() * @method bool isHCLTablet() * @method bool isDPSTablet() * @method bool isVistureTablet() * @method bool isCrestaTablet() * @method bool isMediatekTablet() * @method bool isConcordeTablet() * @method bool isGoCleverTablet() * @method bool isModecomTablet() * @method bool isVoninoTablet() * @method bool isECSTablet() * @method bool isStorexTablet() * @method bool isVodafoneTablet() * @method bool isEssentielBTablet() * @method bool isRossMoorTablet() * @method bool isiMobileTablet() * @method bool isTolinoTablet() * @method bool isAudioSonicTablet() * @method bool isAMPETablet() * @method bool isSkkTablet() * @method bool isTecnoTablet() * @method bool isJXDTablet() * @method bool isiJoyTablet() * @method bool isFX2Tablet() * @method bool isXoroTablet() * @method bool isViewsonicTablet() * @method bool isVerizonTablet() * @method bool isOdysTablet() * @method bool isCaptivaTablet() * @method bool isIconbitTablet() * @method bool isTeclastTablet() * @method bool isOndaTablet() * @method bool isJaytechTablet() * @method bool isBlaupunktTablet() * @method bool isDigmaTablet() * @method bool isEvolioTablet() * @method bool isLavaTablet() * @method bool isAocTablet() * @method bool isMpmanTablet() * @method bool isCelkonTablet() * @method bool isWolderTablet() * @method bool isMediacomTablet() * @method bool isMiTablet() * @method bool isNibiruTablet() * @method bool isNexoTablet() * @method bool isLeaderTablet() * @method bool isUbislateTablet() * @method bool isPocketBookTablet() * @method bool isKocasoTablet() * @method bool isHisenseTablet() * @method bool isHudl() * @method bool isTelstraTablet() * @method bool isGenericTablet() * @method bool isAndroidOS() * @method bool isBlackBerryOS() * @method bool isPalmOS() * @method bool isSymbianOS() * @method bool isWindowsMobileOS() * @method bool isWindowsPhoneOS() * @method bool isiOS() * @method bool isiPadOS() * @method bool isSailfishOS() * @method bool isMeeGoOS() * @method bool isMaemoOS() * @method bool isJavaOS() * @method bool iswebOS() * @method bool isbadaOS() * @method bool isBREWOS() * @method bool isChrome() * @method bool isDolfin() * @method bool isOpera() * @method bool isSkyfire() * @method bool isEdge() * @method bool isIE() * @method bool isFirefox() * @method bool isBolt() * @method bool isTeaShark() * @method bool isBlazer() * @method bool isSafari() * @method bool isWeChat() * @method bool isUCBrowser() * @method bool isbaiduboxapp() * @method bool isbaidubrowser() * @method bool isDiigoBrowser() * @method bool isMercury() * @method bool isObigoBrowser() * @method bool isNetFront() * @method bool isGenericBrowser() * @method bool isPaleMoon() * @method bool isBot() * @method bool isMobileBot() * @method bool isDesktopMode() * @method bool isTV() * @method bool isWebKit() * @method bool isConsole() * @method bool isWatch() */ class WP_Rocket_Mobile_Detect { /** * Mobile detection type. * * @deprecated since version 2.6.9 */ const DETECTION_TYPE_MOBILE = 'mobile'; /** * Extended detection type. * * @deprecated since version 2.6.9 */ const DETECTION_TYPE_EXTENDED = 'extended'; /** * A frequently used regular expression to extract version #s. * * @deprecated since version 2.6.9 */ const VER = '([\w._\+]+)'; /** * Top-level device. */ const MOBILE_GRADE_A = 'A'; /** * Mid-level device. */ const MOBILE_GRADE_B = 'B'; /** * Low-level device. */ const MOBILE_GRADE_C = 'C'; /** * Stores the version number of the current release. */ const VERSION = '2.8.45'; /** * A type WP_Rocket_for the version() method indicating a string return value. */ const VERSION_TYPE_STRING = 'text'; /** * A type WP_Rocket_for the version() method indicating a float return value. */ const VERSION_TYPE_FLOAT = 'float'; /** * A cache WP_Rocket_for resolved matches * @var array */ protected $cache = array(); /** * The User-Agent HTTP header is stored in here. * @var string */ protected $userAgent = null; /** * HTTP headers in the PHP-flavor. So HTTP_USER_AGENT and SERVER_SOFTWARE. * @var array */ protected $httpHeaders = array(); /** * CloudFront headers. E.g. CloudFront-Is-Desktop-Viewer, CloudFront-Is-Mobile-Viewer & CloudFront-Is-Tablet-Viewer. * @var array */ protected $cloudfrontHeaders = array(); /** * The matching Regex. * This is good WP_Rocket_for debug. * @var string */ protected $matchingRegex = null; /** * The matches extracted from the regex expression. * This is good WP_Rocket_for debug. * * @var string */ protected $matchesArray = null; /** * The detection type, using self::DETECTION_TYPE_MOBILE or self::DETECTION_TYPE_EXTENDED. * * @deprecated since version 2.6.9 * * @var string */ protected $detectionType = self::DETECTION_TYPE_MOBILE; /** * HTTP headers that trigger the 'isMobile' detection * to be true. * * @var array */ protected static $mobileHeaders = array( 'HTTP_ACCEPT' => array('matches' => array( // Opera Mini; @reference: http://dev.opera.com/articles/view/opera-binary-markup-language/ 'application/x-obml2d', // BlackBerry devices. 'application/vnd.rim.html', 'text/vnd.wap.wml', 'application/vnd.wap.xhtml+xml' )), 'HTTP_X_WAP_PROFILE' => null, 'HTTP_X_WAP_CLIENTID' => null, 'HTTP_WAP_CONNECTION' => null, 'HTTP_PROFILE' => null, // Reported by Opera on Nokia devices (eg. C3). 'HTTP_X_OPERAMINI_PHONE_UA' => null, 'HTTP_X_NOKIA_GATEWAY_ID' => null, 'HTTP_X_ORANGE_ID' => null, 'HTTP_X_VODAFONE_3GPDPCONTEXT' => null, 'HTTP_X_HUAWEI_USERID' => null, // Reported by Windows Smartphones. 'HTTP_UA_OS' => null, // Reported by Verizon, Vodafone proxy system. 'HTTP_X_MOBILE_GATEWAY' => null, // Seen this on HTC Sensation. SensationXE_Beats_Z715e. 'HTTP_X_ATT_DEVICEID' => null, // Seen this on a HTC. 'HTTP_UA_CPU' => array('matches' => array('ARM')), ); /** * List of mobile devices (phones). * * @var array */ protected static $phoneDevices = array( 'iPhone' => '\biPhone\b|\biPod\b', // |\biTunes 'BlackBerry' => 'BlackBerry|\bBB10\b|rim[0-9]+|\b(BBA100|BBB100|BBD100|BBE100|BBF100|STH100)\b-[0-9]+', 'Pixel' => '; \bPixel\b', 'HTC' => 'HTC|HTC.*(Sensation|Evo|Vision|Explorer|6800|8100|8900|A7272|S510e|C110e|Legend|Desire|T8282)|APX515CKT|Qtek9090|APA9292KT|HD_mini|Sensation.*Z710e|PG86100|Z715e|Desire.*(A8181|HD)|ADR6200|ADR6400L|ADR6425|001HT|Inspire 4G|Android.*\bEVO\b|T-Mobile G1|Z520m|Android [0-9.]+; Pixel', 'Nexus' => 'Nexus One|Nexus S|Galaxy.*Nexus|Android.*Nexus.*Mobile|Nexus 4|Nexus 5|Nexus 5X|Nexus 6', // @todo: Is 'Dell Streak' a tablet or a phone? ;) 'Dell' => 'Dell[;]? (Streak|Aero|Venue|Venue Pro|Flash|Smoke|Mini 3iX)|XCD28|XCD35|\b001DL\b|\b101DL\b|\bGS01\b', 'Motorola' => 'Motorola|DROIDX|DROID BIONIC|\bDroid\b.*Build|Android.*Xoom|HRI39|MOT-|A1260|A1680|A555|A853|A855|A953|A955|A956|Motorola.*ELECTRIFY|Motorola.*i1|i867|i940|MB200|MB300|MB501|MB502|MB508|MB511|MB520|MB525|MB526|MB611|MB612|MB632|MB810|MB855|MB860|MB861|MB865|MB870|ME501|ME502|ME511|ME525|ME600|ME632|ME722|ME811|ME860|ME863|ME865|MT620|MT710|MT716|MT720|MT810|MT870|MT917|Motorola.*TITANIUM|WX435|WX445|XT300|XT301|XT311|XT316|XT317|XT319|XT320|XT390|XT502|XT530|XT531|XT532|XT535|XT603|XT610|XT611|XT615|XT681|XT701|XT702|XT711|XT720|XT800|XT806|XT860|XT862|XT875|XT882|XT883|XT894|XT901|XT907|XT909|XT910|XT912|XT928|XT926|XT915|XT919|XT925|XT1021|\bMoto E\b|XT1068|XT1092|XT1052', 'Samsung' => '\bSamsung\b|SM-G950F|SM-G955F|SM-G9250|GT-19300|SGH-I337|BGT-S5230|GT-B2100|GT-B2700|GT-B2710|GT-B3210|GT-B3310|GT-B3410|GT-B3730|GT-B3740|GT-B5510|GT-B5512|GT-B5722|GT-B6520|GT-B7300|GT-B7320|GT-B7330|GT-B7350|GT-B7510|GT-B7722|GT-B7800|GT-C3010|GT-C3011|GT-C3060|GT-C3200|GT-C3212|GT-C3212I|GT-C3262|GT-C3222|GT-C3300|GT-C3300K|GT-C3303|GT-C3303K|GT-C3310|GT-C3322|GT-C3330|GT-C3350|GT-C3500|GT-C3510|GT-C3530|GT-C3630|GT-C3780|GT-C5010|GT-C5212|GT-C6620|GT-C6625|GT-C6712|GT-E1050|GT-E1070|GT-E1075|GT-E1080|GT-E1081|GT-E1085|GT-E1087|GT-E1100|GT-E1107|GT-E1110|GT-E1120|GT-E1125|GT-E1130|GT-E1160|GT-E1170|GT-E1175|GT-E1180|GT-E1182|GT-E1200|GT-E1210|GT-E1225|GT-E1230|GT-E1390|GT-E2100|GT-E2120|GT-E2121|GT-E2152|GT-E2220|GT-E2222|GT-E2230|GT-E2232|GT-E2250|GT-E2370|GT-E2550|GT-E2652|GT-E3210|GT-E3213|GT-I5500|GT-I5503|GT-I5700|GT-I5800|GT-I5801|GT-I6410|GT-I6420|GT-I7110|GT-I7410|GT-I7500|GT-I8000|GT-I8150|GT-I8160|GT-I8190|GT-I8320|GT-I8330|GT-I8350|GT-I8530|GT-I8700|GT-I8703|GT-I8910|GT-I9000|GT-I9001|GT-I9003|GT-I9010|GT-I9020|GT-I9023|GT-I9070|GT-I9082|GT-I9100|GT-I9103|GT-I9220|GT-I9250|GT-I9300|GT-I9305|GT-I9500|GT-I9505|GT-M3510|GT-M5650|GT-M7500|GT-M7600|GT-M7603|GT-M8800|GT-M8910|GT-N7000|GT-S3110|GT-S3310|GT-S3350|GT-S3353|GT-S3370|GT-S3650|GT-S3653|GT-S3770|GT-S3850|GT-S5210|GT-S5220|GT-S5229|GT-S5230|GT-S5233|GT-S5250|GT-S5253|GT-S5260|GT-S5263|GT-S5270|GT-S5300|GT-S5330|GT-S5350|GT-S5360|GT-S5363|GT-S5369|GT-S5380|GT-S5380D|GT-S5560|GT-S5570|GT-S5600|GT-S5603|GT-S5610|GT-S5620|GT-S5660|GT-S5670|GT-S5690|GT-S5750|GT-S5780|GT-S5830|GT-S5839|GT-S6102|GT-S6500|GT-S7070|GT-S7200|GT-S7220|GT-S7230|GT-S7233|GT-S7250|GT-S7500|GT-S7530|GT-S7550|GT-S7562|GT-S7710|GT-S8000|GT-S8003|GT-S8500|GT-S8530|GT-S8600|SCH-A310|SCH-A530|SCH-A570|SCH-A610|SCH-A630|SCH-A650|SCH-A790|SCH-A795|SCH-A850|SCH-A870|SCH-A890|SCH-A930|SCH-A950|SCH-A970|SCH-A990|SCH-I100|SCH-I110|SCH-I400|SCH-I405|SCH-I500|SCH-I510|SCH-I515|SCH-I600|SCH-I730|SCH-I760|SCH-I770|SCH-I830|SCH-I910|SCH-I920|SCH-I959|SCH-LC11|SCH-N150|SCH-N300|SCH-R100|SCH-R300|SCH-R351|SCH-R400|SCH-R410|SCH-T300|SCH-U310|SCH-U320|SCH-U350|SCH-U360|SCH-U365|SCH-U370|SCH-U380|SCH-U410|SCH-U430|SCH-U450|SCH-U460|SCH-U470|SCH-U490|SCH-U540|SCH-U550|SCH-U620|SCH-U640|SCH-U650|SCH-U660|SCH-U700|SCH-U740|SCH-U750|SCH-U810|SCH-U820|SCH-U900|SCH-U940|SCH-U960|SCS-26UC|SGH-A107|SGH-A117|SGH-A127|SGH-A137|SGH-A157|SGH-A167|SGH-A177|SGH-A187|SGH-A197|SGH-A227|SGH-A237|SGH-A257|SGH-A437|SGH-A517|SGH-A597|SGH-A637|SGH-A657|SGH-A667|SGH-A687|SGH-A697|SGH-A707|SGH-A717|SGH-A727|SGH-A737|SGH-A747|SGH-A767|SGH-A777|SGH-A797|SGH-A817|SGH-A827|SGH-A837|SGH-A847|SGH-A867|SGH-A877|SGH-A887|SGH-A897|SGH-A927|SGH-B100|SGH-B130|SGH-B200|SGH-B220|SGH-C100|SGH-C110|SGH-C120|SGH-C130|SGH-C140|SGH-C160|SGH-C170|SGH-C180|SGH-C200|SGH-C207|SGH-C210|SGH-C225|SGH-C230|SGH-C417|SGH-C450|SGH-D307|SGH-D347|SGH-D357|SGH-D407|SGH-D415|SGH-D780|SGH-D807|SGH-D980|SGH-E105|SGH-E200|SGH-E315|SGH-E316|SGH-E317|SGH-E335|SGH-E590|SGH-E635|SGH-E715|SGH-E890|SGH-F300|SGH-F480|SGH-I200|SGH-I300|SGH-I320|SGH-I550|SGH-I577|SGH-I600|SGH-I607|SGH-I617|SGH-I627|SGH-I637|SGH-I677|SGH-I700|SGH-I717|SGH-I727|SGH-i747M|SGH-I777|SGH-I780|SGH-I827|SGH-I847|SGH-I857|SGH-I896|SGH-I897|SGH-I900|SGH-I907|SGH-I917|SGH-I927|SGH-I937|SGH-I997|SGH-J150|SGH-J200|SGH-L170|SGH-L700|SGH-M110|SGH-M150|SGH-M200|SGH-N105|SGH-N500|SGH-N600|SGH-N620|SGH-N625|SGH-N700|SGH-N710|SGH-P107|SGH-P207|SGH-P300|SGH-P310|SGH-P520|SGH-P735|SGH-P777|SGH-Q105|SGH-R210|SGH-R220|SGH-R225|SGH-S105|SGH-S307|SGH-T109|SGH-T119|SGH-T139|SGH-T209|SGH-T219|SGH-T229|SGH-T239|SGH-T249|SGH-T259|SGH-T309|SGH-T319|SGH-T329|SGH-T339|SGH-T349|SGH-T359|SGH-T369|SGH-T379|SGH-T409|SGH-T429|SGH-T439|SGH-T459|SGH-T469|SGH-T479|SGH-T499|SGH-T509|SGH-T519|SGH-T539|SGH-T559|SGH-T589|SGH-T609|SGH-T619|SGH-T629|SGH-T639|SGH-T659|SGH-T669|SGH-T679|SGH-T709|SGH-T719|SGH-T729|SGH-T739|SGH-T746|SGH-T749|SGH-T759|SGH-T769|SGH-T809|SGH-T819|SGH-T839|SGH-T919|SGH-T929|SGH-T939|SGH-T959|SGH-T989|SGH-U100|SGH-U200|SGH-U800|SGH-V205|SGH-V206|SGH-X100|SGH-X105|SGH-X120|SGH-X140|SGH-X426|SGH-X427|SGH-X475|SGH-X495|SGH-X497|SGH-X507|SGH-X600|SGH-X610|SGH-X620|SGH-X630|SGH-X700|SGH-X820|SGH-X890|SGH-Z130|SGH-Z150|SGH-Z170|SGH-ZX10|SGH-ZX20|SHW-M110|SPH-A120|SPH-A400|SPH-A420|SPH-A460|SPH-A500|SPH-A560|SPH-A600|SPH-A620|SPH-A660|SPH-A700|SPH-A740|SPH-A760|SPH-A790|SPH-A800|SPH-A820|SPH-A840|SPH-A880|SPH-A900|SPH-A940|SPH-A960|SPH-D600|SPH-D700|SPH-D710|SPH-D720|SPH-I300|SPH-I325|SPH-I330|SPH-I350|SPH-I500|SPH-I600|SPH-I700|SPH-L700|SPH-M100|SPH-M220|SPH-M240|SPH-M300|SPH-M305|SPH-M320|SPH-M330|SPH-M350|SPH-M360|SPH-M370|SPH-M380|SPH-M510|SPH-M540|SPH-M550|SPH-M560|SPH-M570|SPH-M580|SPH-M610|SPH-M620|SPH-M630|SPH-M800|SPH-M810|SPH-M850|SPH-M900|SPH-M910|SPH-M920|SPH-M930|SPH-N100|SPH-N200|SPH-N240|SPH-N300|SPH-N400|SPH-Z400|SWC-E100|SCH-i909|GT-N7100|GT-N7105|SCH-I535|SM-N900A|SGH-I317|SGH-T999L|GT-S5360B|GT-I8262|GT-S6802|GT-S6312|GT-S6310|GT-S5312|GT-S5310|GT-I9105|GT-I8510|GT-S6790N|SM-G7105|SM-N9005|GT-S5301|GT-I9295|GT-I9195|SM-C101|GT-S7392|GT-S7560|GT-B7610|GT-I5510|GT-S7582|GT-S7530E|GT-I8750|SM-G9006V|SM-G9008V|SM-G9009D|SM-G900A|SM-G900D|SM-G900F|SM-G900H|SM-G900I|SM-G900J|SM-G900K|SM-G900L|SM-G900M|SM-G900P|SM-G900R4|SM-G900S|SM-G900T|SM-G900V|SM-G900W8|SHV-E160K|SCH-P709|SCH-P729|SM-T2558|GT-I9205|SM-G9350|SM-J120F|SM-G920F|SM-G920V|SM-G930F|SM-N910C|SM-A310F|GT-I9190|SM-J500FN|SM-G903F|SM-J330F|SM-G610F|SM-G981B|SM-G892A|SM-A530F|SM-G988N|SM-G781B|SM-A805N|SM-G965F', 'LG' => '\bLG\b;|LG[- ]?(C800|C900|E400|E610|E900|E-900|F160|F180K|F180L|F180S|730|855|L160|LS740|LS840|LS970|LU6200|MS690|MS695|MS770|MS840|MS870|MS910|P500|P700|P705|VM696|AS680|AS695|AX840|C729|E970|GS505|272|C395|E739BK|E960|L55C|L75C|LS696|LS860|P769BK|P350|P500|P509|P870|UN272|US730|VS840|VS950|LN272|LN510|LS670|LS855|LW690|MN270|MN510|P509|P769|P930|UN200|UN270|UN510|UN610|US670|US740|US760|UX265|UX840|VN271|VN530|VS660|VS700|VS740|VS750|VS910|VS920|VS930|VX9200|VX11000|AX840A|LW770|P506|P925|P999|E612|D955|D802|MS323|M257)|LM-G710', 'Sony' => 'SonyST|SonyLT|SonyEricsson|SonyEricssonLT15iv|LT18i|E10i|LT28h|LT26w|SonyEricssonMT27i|C5303|C6902|C6903|C6906|C6943|D2533|SOV34|601SO|F8332', 'Asus' => 'Asus.*Galaxy|PadFone.*Mobile|ASUS_Z01QD|ASUS_X00TD', 'Xiaomi' => '^(?!.*\bx11\b).*xiaomi.*$|POCOPHONE F1|\bMI\b 8|\bMi\b 10|Redmi Note 9S|Redmi 5A|Redmi Note 5A Prime|Redmi Note 7 Pro|N2G47H|M2001J2G|M2001J2I|M1805E10A|M2004J11G|M1902F1G|M2002J9G|M2004J19G|M2003J6A1G|M2012K11C|M2007J1SC', 'NokiaLumia' => 'Lumia [0-9]{3,4}', // http://www.micromaxinfo.com/mobiles/smartphones // Added because the codes might conflict with Acer Tablets. 'Micromax' => 'Micromax.*\b(A210|A92|A88|A72|A111|A110Q|A115|A116|A110|A90S|A26|A51|A35|A54|A25|A27|A89|A68|A65|A57|A90)\b', // @todo Complete the regex. 'Palm' => 'PalmSource|Palm', // avantgo|blazer|elaine|hiptop|plucker|xiino ; 'Vertu' => 'Vertu|Vertu.*Ltd|Vertu.*Ascent|Vertu.*Ayxta|Vertu.*Constellation(F|Quest)?|Vertu.*Monika|Vertu.*Signature', // Just WP_Rocket_for fun ;) // http://www.pantech.co.kr/en/prod/prodList.do?gbrand=VEGA (PANTECH) // Most of the VEGA devices are legacy. PANTECH seem to be newer devices based on Android. 'Pantech' => 'PANTECH|IM-A850S|IM-A840S|IM-A830L|IM-A830K|IM-A830S|IM-A820L|IM-A810K|IM-A810S|IM-A800S|IM-T100K|IM-A725L|IM-A780L|IM-A775C|IM-A770K|IM-A760S|IM-A750K|IM-A740S|IM-A730S|IM-A720L|IM-A710K|IM-A690L|IM-A690S|IM-A650S|IM-A630K|IM-A600S|VEGA PTL21|PT003|P8010|ADR910L|P6030|P6020|P9070|P4100|P9060|P5000|CDM8992|TXT8045|ADR8995|IS11PT|P2030|P6010|P8000|PT002|IS06|CDM8999|P9050|PT001|TXT8040|P2020|P9020|P2000|P7040|P7000|C790', // http://www.fly-phone.com/devices/smartphones/ ; Included only smartphones. 'Fly' => 'IQ230|IQ444|IQ450|IQ440|IQ442|IQ441|IQ245|IQ256|IQ236|IQ255|IQ235|IQ245|IQ275|IQ240|IQ285|IQ280|IQ270|IQ260|IQ250', // http://fr.wikomobile.com 'Wiko' => 'KITE 4G|HIGHWAY|GETAWAY|STAIRWAY|DARKSIDE|DARKFULL|DARKNIGHT|DARKMOON|SLIDE|WAX 4G|RAINBOW|BLOOM|SUNSET|GOA(?!nna)|LENNY|BARRY|IGGY|OZZY|CINK FIVE|CINK PEAX|CINK PEAX 2|CINK SLIM|CINK SLIM 2|CINK +|CINK KING|CINK PEAX|CINK SLIM|SUBLIM', 'iMobile' => 'i-mobile (IQ|i-STYLE|idea|ZAA|Hitz)', // Added simvalley mobile WP_Rocket_just WP_Rocket_for fun. They have some interesting devices. // http://www.simvalley.fr/telephonie---gps-_22_telephonie-mobile_telephones_.html 'SimValley' => '\b(SP-80|XT-930|SX-340|XT-930|SX-310|SP-360|SP60|SPT-800|SP-120|SPT-800|SP-140|SPX-5|SPX-8|SP-100|SPX-8|SPX-12)\b', // Wolfgang - a brand that is sold by Aldi supermarkets. // http://www.wolfgangmobile.com/ 'Wolfgang' => 'AT-B24D|AT-AS50HD|AT-AS40W|AT-AS55HD|AT-AS45q2|AT-B26D|AT-AS50Q', 'Alcatel' => 'Alcatel', 'Nintendo' => 'Nintendo (3DS|Switch)', // http://en.wikipedia.org/wiki/Amoi 'Amoi' => 'Amoi', // http://en.wikipedia.org/wiki/INQ 'INQ' => 'INQ', 'OnePlus' => 'ONEPLUS', // @Tapatalk is a mobile app; http://support.tapatalk.com/threads/smf-2-0-2-os-and-browser-detection-plugin-and-tapatalk.15565/#post-79039 'GenericPhone' => 'Tapatalk|PDA;|SAGEM|\bmmp\b|pocket|\bpsp\b|symbian|Smartphone|smartfon|treo|up.browser|up.link|vodafone|\bwap\b|nokia|Series40|Series60|S60|SonyEricsson|N900|MAUI.*WAP.*Browser', ); /** * List of tablet devices. * * @var array */ protected static $tabletDevices = array( // @todo: check WP_Rocket_for mobile friendly emails topic. 'iPad' => 'iPad|iPad.*Mobile', // Removed |^.*Android.*Nexus(?!(?:Mobile).)*$ // @see #442 // @todo Merge NexusTablet into GoogleTablet. 'NexusTablet' => 'Android.*Nexus[\s]+(7|9|10)', // https://en.wikipedia.org/wiki/Pixel_C 'GoogleTablet' => 'Android.*Pixel C', 'SamsungTablet' => 'SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5105|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925|GT-I9200|GT-P5200|GT-P5210|GT-P5210X|SM-T311|SM-T310|SM-T310X|SM-T210|SM-T210R|SM-T211|SM-P600|SM-P601|SM-P605|SM-P900|SM-P901|SM-T217|SM-T217A|SM-T217S|SM-P6000|SM-T3100|SGH-I467|XE500|SM-T110|GT-P5220|GT-I9200X|GT-N5110X|GT-N5120|SM-P905|SM-T111|SM-T2105|SM-T315|SM-T320|SM-T320X|SM-T321|SM-T520|SM-T525|SM-T530NU|SM-T230NU|SM-T330NU|SM-T900|XE500T1C|SM-P605V|SM-P905V|SM-T337V|SM-T537V|SM-T707V|SM-T807V|SM-P600X|SM-P900X|SM-T210X|SM-T230|SM-T230X|SM-T325|GT-P7503|SM-T531|SM-T330|SM-T530|SM-T705|SM-T705C|SM-T535|SM-T331|SM-T800|SM-T700|SM-T537|SM-T807|SM-P907A|SM-T337A|SM-T537A|SM-T707A|SM-T807A|SM-T237|SM-T807P|SM-P607T|SM-T217T|SM-T337T|SM-T807T|SM-T116NQ|SM-T116BU|SM-P550|SM-T350|SM-T550|SM-T9000|SM-P9000|SM-T705Y|SM-T805|GT-P3113|SM-T710|SM-T810|SM-T815|SM-T360|SM-T533|SM-T113|SM-T335|SM-T715|SM-T560|SM-T670|SM-T677|SM-T377|SM-T567|SM-T357T|SM-T555|SM-T561|SM-T713|SM-T719|SM-T813|SM-T819|SM-T580|SM-T355Y?|SM-T280|SM-T817A|SM-T820|SM-W700|SM-P580|SM-T587|SM-P350|SM-P555M|SM-P355M|SM-T113NU|SM-T815Y|SM-T585|SM-T285|SM-T825|SM-W708|SM-T835|SM-T830|SM-T837V|SM-T720|SM-T510|SM-T387V|SM-P610|SM-T290|SM-T515|SM-T590|SM-T595|SM-T725|SM-T817P|SM-P585N0|SM-T395|SM-T295|SM-T865|SM-P610N|SM-P615|SM-T970|SM-T380|SM-T5950|SM-T905|SM-T231|SM-T500|SM-T860|SM-T536|SM-T837A|SM-X200|SM-T220|SM-T870|SM-X906C|SM-X700|SM-X706|SM-X706B|SM-X706U|SM-X706N|SM-X800|SM-X806|SM-X806B|SM-X806U|SM-X806N|SM-X900|SM-X906|SM-X906B|SM-X906U|SM-X906N|SM-P613', // SCH-P709|SCH-P729|SM-T2558|GT-I9205 - Samsung Mega - treat them like a regular phone. // http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html 'Kindle' => 'Kindle|Silk.*Accelerated|Android.*\b(KFOT|KFTT|KFJWI|KFJWA|KFOTE|KFSOWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|WFJWAE|KFSAWA|KFSAWI|KFASWI|KFARWI|KFFOWI|KFGIWI|KFMEWI)\b|Android.*Silk/[0-9.]+ like Chrome/[0-9.]+ (?!Mobile)', // Only the Surface tablets with Windows RT are considered mobile. // http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx 'SurfaceTablet' => 'Windows NT [0-9.]+; ARM;.*(Tablet|ARMBJS)', // http://shopping1.hp.com/is-bin/INTERSHOP.enfinity/WFS/WW-USSMBPublicStore-Site/en_US/-/USD/ViewStandardCatalog-Browse?CatalogCategoryID=JfIQ7EN5lqMAAAEyDcJUDwMT 'HPTablet' => 'HP Slate (7|8|10)|HP ElitePad 900|hp-tablet|EliteBook.*Touch|HP 8|Slate 21|HP SlateBook 10', // Watch out WP_Rocket_for PadFone, see #132. // http://www.asus.com/de/Tablets_Mobile/Memo_Pad_Products/ 'AsusTablet' => '^.*PadFone((?!Mobile).)*$|Transformer|TF101|TF101G|TF300T|TF300TG|TF300TL|TF700T|TF700KL|TF701T|TF810C|ME171|ME301T|ME302C|ME371MG|ME370T|ME372MG|ME172V|ME173X|ME400C|Slider SL101|\bK00F\b|\bK00C\b|\bK00E\b|\bK00L\b|TX201LA|ME176C|ME102A|\bM80TA\b|ME372CL|ME560CG|ME372CG|ME302KL| K01A | K010 | K011 | K017 | K01E |ME572C|ME103K|ME170C|ME171C|\bME70C\b|ME581C|ME581CL|ME8510C|ME181C|P01Y|PO1MA|P01Z|\bP027\b|\bP024\b|\bP00C\b', 'BlackBerryTablet' => 'PlayBook|RIM Tablet', 'HTCtablet' => 'HTC_Flyer_P512|HTC Flyer|HTC Jetstream|HTC-P715a|HTC EVO View 4G|PG41200|PG09410', 'MotorolaTablet' => 'xoom|sholest|MZ615|MZ605|MZ505|MZ601|MZ602|MZ603|MZ604|MZ606|MZ607|MZ608|MZ609|MZ615|MZ616|MZ617', 'NookTablet' => 'Android.*Nook|NookColor|nook browser|BNRV200|BNRV200A|BNTV250|BNTV250A|BNTV400|BNTV600|LogicPD Zoom2', // http://www.acer.ro/ac/ro/RO/content/drivers // http://www.packardbell.co.uk/pb/en/GB/content/download (Packard Bell is part of Acer) // http://us.acer.com/ac/en/US/content/group/tablets // http://www.acer.de/ac/de/DE/content/models/tablets/ // Can conflict with Micromax and Motorola phones codes. 'AcerTablet' => 'Android.*; \b(A100|A101|A110|A200|A210|A211|A500|A501|A510|A511|A700|A701|W500|W500P|W501|W501P|W510|W511|W700|G100|G100W|B1-A71|B1-710|B1-711|A1-810|A1-811|A1-830)\b|W3-810|\bA3-A10\b|\bA3-A11\b|\bA3-A20\b|\bA3-A30|A3-A40', // http://eu.computers.toshiba-europe.com/innovation/family/Tablets/1098744/banner_id/tablet_footerlink/ // http://us.toshiba.com/tablets/tablet-finder // http://www.toshiba.co.jp/regza/tablet/ 'ToshibaTablet' => 'Android.*(AT100|AT105|AT200|AT205|AT270|AT275|AT300|AT305|AT1S5|AT500|AT570|AT700|AT830)|TOSHIBA.*FOLIO', // http://www.nttdocomo.co.jp/english/service/developer/smart_phone/technical_info/spec/index.html // http://www.lg.com/us/tablets 'LGTablet' => '\bL-06C|LG-V909|LG-V900|LG-V700|LG-V510|LG-V500|LG-V410|LG-V400|LG-VK810\b', 'FujitsuTablet' => 'Android.*\b(F-01D|F-02F|F-05E|F-10D|M532|Q572)\b', // Prestigio Tablets http://www.prestigio.com/support 'PrestigioTablet' => 'PMP3170B|PMP3270B|PMP3470B|PMP7170B|PMP3370B|PMP3570C|PMP5870C|PMP3670B|PMP5570C|PMP5770D|PMP3970B|PMP3870C|PMP5580C|PMP5880D|PMP5780D|PMP5588C|PMP7280C|PMP7280C3G|PMP7280|PMP7880D|PMP5597D|PMP5597|PMP7100D|PER3464|PER3274|PER3574|PER3884|PER5274|PER5474|PMP5097CPRO|PMP5097|PMP7380D|PMP5297C|PMP5297C_QUAD|PMP812E|PMP812E3G|PMP812F|PMP810E|PMP880TD|PMT3017|PMT3037|PMT3047|PMT3057|PMT7008|PMT5887|PMT5001|PMT5002', // http://support.lenovo.com/en_GB/downloads/default.page?# 'LenovoTablet' => 'Lenovo TAB|Idea(Tab|Pad)( A1|A10| K1|)|ThinkPad([ ]+)?Tablet|YT3-850M|YT3-X90L|YT3-X90F|YT3-X90X|Lenovo.*(S2109|S2110|S5000|S6000|K3011|A3000|A3500|A1000|A2107|A2109|A1107|A5500|A7600|B6000|B8000|B8080)(-|)(FL|F|HV|H|)|TB-X103F|TB-X304X|TB-X304F|TB-X304L|TB-X505F|TB-X505L|TB-X505X|TB-X605F|TB-X605L|TB-8703F|TB-8703X|TB-8703N|TB-8704N|TB-8704F|TB-8704X|TB-8704V|TB-7304F|TB-7304I|TB-7304X|Tab2A7-10F|Tab2A7-20F|TB2-X30L|YT3-X50L|YT3-X50F|YT3-X50M|YT-X705F|YT-X703F|YT-X703L|YT-X705L|YT-X705X|TB2-X30F|TB2-X30L|TB2-X30M|A2107A-F|A2107A-H|TB3-730F|TB3-730M|TB3-730X|TB-7504F|TB-7504X|TB-X704F|TB-X104F|TB3-X70F|TB-X705F|TB-8504F|TB3-X70L|TB3-710F|TB-X704L|TB-J606F|TB-X606F|TB-X306X|YT-J706X', // http://www.dell.com/support/home/us/en/04/Products/tab_mob/tablets 'DellTablet' => 'Venue 11|Venue 8|Venue 7|Dell Streak 10|Dell Streak 7', 'XiaomiTablet' => '21051182G', // http://www.yarvik.com/en/matrix/tablets/ 'YarvikTablet' => 'Android.*\b(TAB210|TAB211|TAB224|TAB250|TAB260|TAB264|TAB310|TAB360|TAB364|TAB410|TAB411|TAB420|TAB424|TAB450|TAB460|TAB461|TAB464|TAB465|TAB467|TAB468|TAB07-100|TAB07-101|TAB07-150|TAB07-151|TAB07-152|TAB07-200|TAB07-201-3G|TAB07-210|TAB07-211|TAB07-212|TAB07-214|TAB07-220|TAB07-400|TAB07-485|TAB08-150|TAB08-200|TAB08-201-3G|TAB08-201-30|TAB09-100|TAB09-211|TAB09-410|TAB10-150|TAB10-201|TAB10-211|TAB10-400|TAB10-410|TAB13-201|TAB274EUK|TAB275EUK|TAB374EUK|TAB462EUK|TAB474EUK|TAB9-200)\b', 'MedionTablet' => 'Android.*\bOYO\b|LIFE.*(P9212|P9514|P9516|S9512)|LIFETAB', 'ArnovaTablet' => '97G4|AN10G2|AN7bG3|AN7fG3|AN8G3|AN8cG3|AN7G3|AN9G3|AN7dG3|AN7dG3ST|AN7dG3ChildPad|AN10bG3|AN10bG3DT|AN9G2', // http://www.intenso.de/kategorie_en.php?kategorie=33 // @todo: http://www.nbhkdz.com/read/b8e64202f92a2df129126bff.html - investigate 'IntensoTablet' => 'INM8002KP|INM1010FP|INM805ND|Intenso Tab|TAB1004', // IRU.ru Tablets http://www.iru.ru/catalog/soho/planetable/ 'IRUTablet' => 'M702pro', 'MegafonTablet' => 'MegaFon V9|\bZTE V9\b|Android.*\bMT7A\b', // http://www.e-boda.ro/tablete-pc.html 'EbodaTablet' => 'E-Boda (Supreme|Impresspeed|Izzycomm|Essential)', // http://www.allview.ro/produse/droseries/lista-tablete-pc/ 'AllViewTablet' => 'Allview.*(Viva|Alldro|City|Speed|All TV|Frenzy|Quasar|Shine|TX1|AX1|AX2)', // http://wiki.archosfans.com/index.php?title=Main_Page // @note Rewrite the regex format after we add more UAs. 'ArchosTablet' => '\b(101G9|80G9|A101IT)\b|Qilive 97R|Archos5|\bARCHOS (70|79|80|90|97|101|FAMILYPAD|)(b|c|)(G10| Cobalt| TITANIUM(HD|)| Xenon| Neon|XSK| 2| XS 2| PLATINUM| CARBON|GAMEPAD)\b', // http://www.ainol.com/plugin.php?identifier=ainol&module=product 'AinolTablet' => 'NOVO7|NOVO8|NOVO10|Novo7Aurora|Novo7Basic|NOVO7PALADIN|novo9-Spark', 'NokiaLumiaTablet' => 'Lumia 2520', // @todo: inspect http://esupport.sony.com/US/p/select-system.pl?DIRECTOR=DRIVER // Readers http://www.atsuhiro-me.net/ebook/sony-reader/sony-reader-web-browser // http://www.sony.jp/support/tablet/ 'SonyTablet' => 'Sony.*Tablet|Xperia Tablet|Sony Tablet S|SO-03E|SGPT12|SGPT13|SGPT114|SGPT121|SGPT122|SGPT123|SGPT111|SGPT112|SGPT113|SGPT131|SGPT132|SGPT133|SGPT211|SGPT212|SGPT213|SGP311|SGP312|SGP321|EBRD1101|EBRD1102|EBRD1201|SGP351|SGP341|SGP511|SGP512|SGP521|SGP541|SGP551|SGP621|SGP641|SGP612|SOT31|SGP771|SGP611|SGP612|SGP712', // http://www.support.philips.com/support/catalog/worldproducts.jsp?userLanguage=en&userCountry=cn&categoryid=3G_LTE_TABLET_SU_CN_CARE&title=3G%20tablets%20/%20LTE%20range&_dyncharset=UTF-8 'PhilipsTablet' => '\b(PI2010|PI3000|PI3100|PI3105|PI3110|PI3205|PI3210|PI3900|PI4010|PI7000|PI7100)\b', // db + http://www.cube-tablet.com/buy-products.html 'CubeTablet' => 'Android.*(K8GT|U9GT|U10GT|U16GT|U17GT|U18GT|U19GT|U20GT|U23GT|U30GT)|CUBE U8GT', // http://www.cobyusa.com/?p=pcat&pcat_id=3001 'CobyTablet' => 'MID1042|MID1045|MID1125|MID1126|MID7012|MID7014|MID7015|MID7034|MID7035|MID7036|MID7042|MID7048|MID7127|MID8042|MID8048|MID8127|MID9042|MID9740|MID9742|MID7022|MID7010', // http://www.match.net.cn/products.asp 'MIDTablet' => 'M9701|M9000|M9100|M806|M1052|M806|T703|MID701|MID713|MID710|MID727|MID760|MID830|MID728|MID933|MID125|MID810|MID732|MID120|MID930|MID800|MID731|MID900|MID100|MID820|MID735|MID980|MID130|MID833|MID737|MID960|MID135|MID860|MID736|MID140|MID930|MID835|MID733|MID4X10', // http://www.msi.com/support // @todo Research the Windows Tablets. 'MSITablet' => 'MSI \b(Primo 73K|Primo 73L|Primo 81L|Primo 77|Primo 93|Primo 75|Primo 76|Primo 73|Primo 81|Primo 91|Primo 90|Enjoy 71|Enjoy 7|Enjoy 10)\b', // @todo http://www.kyoceramobile.com/support/drivers/ // 'KyoceraTablet' => null, // @todo http://intexuae.com/index.php/category/mobile-devices/tablets-products/ // 'IntextTablet' => null, // http://pdadb.net/index.php?m=pdalist&list=SMiT (NoName Chinese Tablets) // http://www.imp3.net/14/show.php?itemid=20454 'SMiTTablet' => 'Android.*(\bMID\b|MID-560|MTV-T1200|MTV-PND531|MTV-P1101|MTV-PND530)', // http://www.rock-chips.com/index.php?do=prod&pid=2 'RockChipTablet' => 'Android.*(RK2818|RK2808A|RK2918|RK3066)|RK2738|RK2808A', // http://www.fly-phone.com/devices/tablets/ ; http://www.fly-phone.com/service/ 'FlyTablet' => 'IQ310|Fly Vision', // http://www.bqreaders.com/gb/tablets-prices-sale.html 'bqTablet' => 'Android.*(bq)?.*\b(Elcano|Curie|Edison|Maxwell|Kepler|Pascal|Tesla|Hypatia|Platon|Newton|Livingstone|Cervantes|Avant|Aquaris ([E|M]10|M8))\b|Maxwell.*Lite|Maxwell.*Plus', // http://www.huaweidevice.com/worldwide/productFamily.do?method=index&directoryId=5011&treeId=3290 // http://www.huaweidevice.com/worldwide/downloadCenter.do?method=index&directoryId=3372&treeId=0&tb=1&type=software (including legacy tablets) 'HuaweiTablet' => 'MediaPad|MediaPad 7 Youth|IDEOS S7|S7-201c|S7-202u|S7-101|S7-103|S7-104|S7-105|S7-106|S7-201|S7-Slim|M2-A01L|BAH-L09|BAH-W09|AGS-L09|CMR-AL19|KOB2-L09|BG2-U01|BG2-W09|BG2-U03', // Nec or Medias Tab 'NecTablet' => '\bN-06D|\bN-08D', // Pantech Tablets: http://www.pantechusa.com/phones/ 'PantechTablet' => 'Pantech.*P4100', // Broncho Tablets: http://www.broncho.cn/ (hard to find) 'BronchoTablet' => 'Broncho.*(N701|N708|N802|a710)', // http://versusuk.com/support.html 'VersusTablet' => 'TOUCHPAD.*[78910]|\bTOUCHTAB\b', // http://www.zync.in/index.php/our-products/tablet-phablets 'ZyncTablet' => 'z1000|Z99 2G|z930|z990|z909|Z919|z900', // Removed "z999" because of https://github.com/serbanghita/Mobile-Detect/issues/717 // http://www.positivoinformatica.com.br/www/pessoal/tablet-ypy/ 'PositivoTablet' => 'TB07STA|TB10STA|TB07FTA|TB10FTA', // https://www.nabitablet.com/ 'NabiTablet' => 'Android.*\bNabi', 'KoboTablet' => 'Kobo Touch|\bK080\b|\bVox\b Build|\bArc\b Build', // French Danew Tablets http://www.danew.com/produits-tablette.php 'DanewTablet' => 'DSlide.*\b(700|701R|702|703R|704|802|970|971|972|973|974|1010|1012)\b', // Texet Tablets and Readers http://www.texet.ru/tablet/ 'TexetTablet' => 'NaviPad|TB-772A|TM-7045|TM-7055|TM-9750|TM-7016|TM-7024|TM-7026|TM-7041|TM-7043|TM-7047|TM-8041|TM-9741|TM-9747|TM-9748|TM-9751|TM-7022|TM-7021|TM-7020|TM-7011|TM-7010|TM-7023|TM-7025|TM-7037W|TM-7038W|TM-7027W|TM-9720|TM-9725|TM-9737W|TM-1020|TM-9738W|TM-9740|TM-9743W|TB-807A|TB-771A|TB-727A|TB-725A|TB-719A|TB-823A|TB-805A|TB-723A|TB-715A|TB-707A|TB-705A|TB-709A|TB-711A|TB-890HD|TB-880HD|TB-790HD|TB-780HD|TB-770HD|TB-721HD|TB-710HD|TB-434HD|TB-860HD|TB-840HD|TB-760HD|TB-750HD|TB-740HD|TB-730HD|TB-722HD|TB-720HD|TB-700HD|TB-500HD|TB-470HD|TB-431HD|TB-430HD|TB-506|TB-504|TB-446|TB-436|TB-416|TB-146SE|TB-126SE', // Avoid detecting 'PLAYSTATION 3' as mobile. 'PlaystationTablet' => 'Playstation.*(Portable|Vita)', // http://www.trekstor.de/surftabs.html 'TrekstorTablet' => 'ST10416-1|VT10416-1|ST70408-1|ST702xx-1|ST702xx-2|ST80208|ST97216|ST70104-2|VT10416-2|ST10216-2A|SurfTab', // http://www.pyleaudio.com/Products.aspx?%2fproducts%2fPersonal-Electronics%2fTablets 'PyleAudioTablet' => '\b(PTBL10CEU|PTBL10C|PTBL72BC|PTBL72BCEU|PTBL7CEU|PTBL7C|PTBL92BC|PTBL92BCEU|PTBL9CEU|PTBL9CUK|PTBL9C)\b', // http://www.advandigital.com/index.php?link=content-product&jns=JP001 // because of the short codenames we have to include whitespaces to reduce the possible conflicts. 'AdvanTablet' => 'Android.* \b(E3A|T3X|T5C|T5B|T3E|T3C|T3B|T1J|T1F|T2A|T1H|T1i|E1C|T1-E|T5-A|T4|E1-B|T2Ci|T1-B|T1-D|O1-A|E1-A|T1-A|T3A|T4i)\b ', // http://www.danytech.com/category/tablet-pc 'DanyTechTablet' => 'Genius Tab G3|Genius Tab S2|Genius Tab Q3|Genius Tab G4|Genius Tab Q4|Genius Tab G-II|Genius TAB GII|Genius TAB GIII|Genius Tab S1', // http://www.galapad.net/product.html ; https://github.com/serbanghita/Mobile-Detect/issues/761 'GalapadTablet' => 'Android [0-9.]+; [a-z-]+; \bG1\b', // http://www.micromaxinfo.com/tablet/funbook 'MicromaxTablet' => 'Funbook|Micromax.*\b(P250|P560|P360|P362|P600|P300|P350|P500|P275)\b', // http://www.karbonnmobiles.com/products_tablet.php 'KarbonnTablet' => 'Android.*\b(A39|A37|A34|ST8|ST10|ST7|Smart Tab3|Smart Tab2)\b', // http://www.myallfine.com/Products.asp 'AllFineTablet' => 'Fine7 Genius|Fine7 Shine|Fine7 Air|Fine8 Style|Fine9 More|Fine10 Joy|Fine11 Wide', // http://www.proscanvideo.com/products-search.asp?itemClass=TABLET&itemnmbr= 'PROSCANTablet' => '\b(PEM63|PLT1023G|PLT1041|PLT1044|PLT1044G|PLT1091|PLT4311|PLT4311PL|PLT4315|PLT7030|PLT7033|PLT7033D|PLT7035|PLT7035D|PLT7044K|PLT7045K|PLT7045KB|PLT7071KG|PLT7072|PLT7223G|PLT7225G|PLT7777G|PLT7810K|PLT7849G|PLT7851G|PLT7852G|PLT8015|PLT8031|PLT8034|PLT8036|PLT8080K|PLT8082|PLT8088|PLT8223G|PLT8234G|PLT8235G|PLT8816K|PLT9011|PLT9045K|PLT9233G|PLT9735|PLT9760G|PLT9770G)\b', // http://www.yonesnav.com/products/products.php 'YONESTablet' => 'BQ1078|BC1003|BC1077|RK9702|BC9730|BC9001|IT9001|BC7008|BC7010|BC708|BC728|BC7012|BC7030|BC7027|BC7026', // http://www.cjshowroom.com/eproducts.aspx?classcode=004001001 // China manufacturer makes tablets WP_Rocket_for different small brands (eg. http://www.zeepad.net/index.html) 'ChangJiaTablet' => 'TPC7102|TPC7103|TPC7105|TPC7106|TPC7107|TPC7201|TPC7203|TPC7205|TPC7210|TPC7708|TPC7709|TPC7712|TPC7110|TPC8101|TPC8103|TPC8105|TPC8106|TPC8203|TPC8205|TPC8503|TPC9106|TPC9701|TPC97101|TPC97103|TPC97105|TPC97106|TPC97111|TPC97113|TPC97203|TPC97603|TPC97809|TPC97205|TPC10101|TPC10103|TPC10106|TPC10111|TPC10203|TPC10205|TPC10503', // http://www.gloryunion.cn/products.asp // http://www.allwinnertech.com/en/apply/mobile.html // http://www.ptcl.com.pk/pd_content.php?pd_id=284 (EVOTAB) // @todo: Softwiner tablets? // aka. Cute or Cool tablets. Not sure yet, must research to avoid collisions. 'GUTablet' => 'TX-A1301|TX-M9002|Q702|kf026', // A12R|D75A|D77|D79|R83|A95|A106C|R15|A75|A76|D71|D72|R71|R73|R77|D82|R85|D92|A97|D92|R91|A10F|A77F|W71F|A78F|W78F|W81F|A97F|W91F|W97F|R16G|C72|C73E|K72|K73|R96G // http://www.pointofview-online.com/showroom.php?shop_mode=product_listing&category_id=118 'PointOfViewTablet' => 'TAB-P506|TAB-navi-7-3G-M|TAB-P517|TAB-P-527|TAB-P701|TAB-P703|TAB-P721|TAB-P731N|TAB-P741|TAB-P825|TAB-P905|TAB-P925|TAB-PR945|TAB-PL1015|TAB-P1025|TAB-PI1045|TAB-P1325|TAB-PROTAB[0-9]+|TAB-PROTAB25|TAB-PROTAB26|TAB-PROTAB27|TAB-PROTAB26XL|TAB-PROTAB2-IPS9|TAB-PROTAB30-IPS9|TAB-PROTAB25XXL|TAB-PROTAB26-IPS10|TAB-PROTAB30-IPS10', // http://www.overmax.pl/pl/katalog-produktow,p8/tablety,c14/ // @todo: add more tests. 'OvermaxTablet' => 'OV-(SteelCore|NewBase|Basecore|Baseone|Exellen|Quattor|EduTab|Solution|ACTION|BasicTab|TeddyTab|MagicTab|Stream|TB-08|TB-09)|Qualcore 1027', // http://hclmetablet.com/India/index.php 'HCLTablet' => 'HCL.*Tablet|Connect-3G-2.0|Connect-2G-2.0|ME Tablet U1|ME Tablet U2|ME Tablet G1|ME Tablet X1|ME Tablet Y2|ME Tablet Sync', // http://www.edigital.hu/Tablet_es_e-book_olvaso/Tablet-c18385.html 'DPSTablet' => 'DPS Dream 9|DPS Dual 7', // http://www.visture.com/index.asp 'VistureTablet' => 'V97 HD|i75 3G|Visture V4( HD)?|Visture V5( HD)?|Visture V10', // http://www.mijncresta.nl/tablet 'CrestaTablet' => 'CTP(-)?810|CTP(-)?818|CTP(-)?828|CTP(-)?838|CTP(-)?888|CTP(-)?978|CTP(-)?980|CTP(-)?987|CTP(-)?988|CTP(-)?989', // MediaTek - http://www.mediatek.com/_en/01_products/02_proSys.php?cata_sn=1&cata1_sn=1&cata2_sn=309 'MediatekTablet' => '\bMT8125|MT8389|MT8135|MT8377\b', // Concorde tab 'ConcordeTablet' => 'Concorde([ ]+)?Tab|ConCorde ReadMan', // GoClever Tablets - http://www.goclever.com/uk/products,c1/tablet,c5/ 'GoCleverTablet' => 'GOCLEVER TAB|A7GOCLEVER|M1042|M7841|M742|R1042BK|R1041|TAB A975|TAB A7842|TAB A741|TAB A741L|TAB M723G|TAB M721|TAB A1021|TAB I921|TAB R721|TAB I720|TAB T76|TAB R70|TAB R76.2|TAB R106|TAB R83.2|TAB M813G|TAB I721|GCTA722|TAB I70|TAB I71|TAB S73|TAB R73|TAB R74|TAB R93|TAB R75|TAB R76.1|TAB A73|TAB A93|TAB A93.2|TAB T72|TAB R83|TAB R974|TAB R973|TAB A101|TAB A103|TAB A104|TAB A104.2|R105BK|M713G|A972BK|TAB A971|TAB R974.2|TAB R104|TAB R83.3|TAB A1042', // Modecom Tablets - http://www.modecom.eu/tablets/portal/ 'ModecomTablet' => 'FreeTAB 9000|FreeTAB 7.4|FreeTAB 7004|FreeTAB 7800|FreeTAB 2096|FreeTAB 7.5|FreeTAB 1014|FreeTAB 1001 |FreeTAB 8001|FreeTAB 9706|FreeTAB 9702|FreeTAB 7003|FreeTAB 7002|FreeTAB 1002|FreeTAB 7801|FreeTAB 1331|FreeTAB 1004|FreeTAB 8002|FreeTAB 8014|FreeTAB 9704|FreeTAB 1003', // Vonino Tablets 'VoninoTablet' => '\b(Argus[ _]?S|Diamond[ _]?79HD|Emerald[ _]?78E|Luna[ _]?70C|Onyx[ _]?S|Onyx[ _]?Z|Orin[ _]?HD|Orin[ _]?S|Otis[ _]?S|SpeedStar[ _]?S|Magnet[ _]?M9|Primus[ _]?94[ _]?3G|Primus[ _]?94HD|Primus[ _]?QS|Android.*\bQ8\b|Sirius[ _]?EVO[ _]?QS|Sirius[ _]?QS|Spirit[ _]?S)\b', // ECS Tablets - http://www.ecs.com.tw/ECSWebSite/Product/Product_Tablet_List.aspx?CategoryID=14&MenuID=107&childid=M_107&LanID=0 'ECSTablet' => 'V07OT2|TM105A|S10OT1|TR10CS1', // Storex Tablets - http://storex.fr/espace_client/support.html // @note: no need to add all the tablet codes since they are guided by the first regex. 'StorexTablet' => 'eZee[_\']?(Tab|Go)[0-9]+|TabLC7|Looney Tunes Tab', // Generic Vodafone tablets. 'VodafoneTablet' => 'SmartTab([ ]+)?[0-9]+|SmartTabII10|SmartTabII7|VF-1497|VFD 1400', // French tablets - Essentiel B http://www.boulanger.fr/tablette_tactile_e-book/tablette_tactile_essentiel_b/cl_68908.htm?multiChoiceToDelete=brand&mc_brand=essentielb // Aka: http://www.essentielb.fr/ 'EssentielBTablet' => 'Smart[ \']?TAB[ ]+?[0-9]+|Family[ \']?TAB2', // Ross & Moor - http://ross-moor.ru/ 'RossMoorTablet' => 'RM-790|RM-997|RMD-878G|RMD-974R|RMT-705A|RMT-701|RME-601|RMT-501|RMT-711', // i-mobile http://product.i-mobilephone.com/Mobile_Device 'iMobileTablet' => 'i-mobile i-note', // http://www.tolino.de/de/vergleichen/ 'TolinoTablet' => 'tolino tab [0-9.]+|tolino shine', // AudioSonic - a Kmart brand // http://www.kmart.com.au/webapp/wcs/stores/servlet/Search?langId=-1&storeId=10701&catalogId=10001&categoryId=193001&pageSize=72¤tPage=1&searchCategory=193001%2b4294965664&sortBy=p_MaxPrice%7c1 'AudioSonicTablet' => '\bC-22Q|T7-QC|T-17B|T-17P\b', // AMPE Tablets - http://www.ampe.com.my/product-category/tablets/ // @todo: add them gradually to avoid conflicts. 'AMPETablet' => 'Android.* A78 ', // Skk Mobile - http://skkmobile.com.ph/product_tablets.php 'SkkTablet' => 'Android.* (SKYPAD|PHOENIX|CYCLOPS)', // Tecno Mobile (only tablet) - http://www.tecno-mobile.com/index.php/product?filterby=smart&list_order=all&page=1 'TecnoTablet' => 'TECNO P9|TECNO DP8D', // JXD (consoles & tablets) - http://jxd.hk/products.asp?selectclassid=009008&clsid=3 'JXDTablet' => 'Android.* \b(F3000|A3300|JXD5000|JXD3000|JXD2000|JXD300B|JXD300|S5800|S7800|S602b|S5110b|S7300|S5300|S602|S603|S5100|S5110|S601|S7100a|P3000F|P3000s|P101|P200s|P1000m|P200m|P9100|P1000s|S6600b|S908|P1000|P300|S18|S6600|S9100)\b', // i-Joy tablets - http://www.i-joy.es/en/cat/products/tablets/ 'iJoyTablet' => 'Tablet (Spirit 7|Essentia|Galatea|Fusion|Onix 7|Landa|Titan|Scooby|Deox|Stella|Themis|Argon|Unique 7|Sygnus|Hexen|Finity 7|Cream|Cream X2|Jade|Neon 7|Neron 7|Kandy|Scape|Saphyr 7|Rebel|Biox|Rebel|Rebel 8GB|Myst|Draco 7|Myst|Tab7-004|Myst|Tadeo Jones|Tablet Boing|Arrow|Draco Dual Cam|Aurix|Mint|Amity|Revolution|Finity 9|Neon 9|T9w|Amity 4GB Dual Cam|Stone 4GB|Stone 8GB|Andromeda|Silken|X2|Andromeda II|Halley|Flame|Saphyr 9,7|Touch 8|Planet|Triton|Unique 10|Hexen 10|Memphis 4GB|Memphis 8GB|Onix 10)', // http://www.intracon.eu/tablet 'FX2Tablet' => 'FX2 PAD7|FX2 PAD10', // http://www.xoro.de/produkte/ // @note: Might be the same brand with 'Simply tablets' 'XoroTablet' => 'KidsPAD 701|PAD[ ]?712|PAD[ ]?714|PAD[ ]?716|PAD[ ]?717|PAD[ ]?718|PAD[ ]?720|PAD[ ]?721|PAD[ ]?722|PAD[ ]?790|PAD[ ]?792|PAD[ ]?900|PAD[ ]?9715D|PAD[ ]?9716DR|PAD[ ]?9718DR|PAD[ ]?9719QR|PAD[ ]?9720QR|TelePAD1030|Telepad1032|TelePAD730|TelePAD731|TelePAD732|TelePAD735Q|TelePAD830|TelePAD9730|TelePAD795|MegaPAD 1331|MegaPAD 1851|MegaPAD 2151', // http://www1.viewsonic.com/products/computing/tablets/ 'ViewsonicTablet' => 'ViewPad 10pi|ViewPad 10e|ViewPad 10s|ViewPad E72|ViewPad7|ViewPad E100|ViewPad 7e|ViewSonic VB733|VB100a', // https://www.verizonwireless.com/tablets/verizon/ 'VerizonTablet' => 'QTAQZ3|QTAIR7|QTAQTZ3|QTASUN1|QTASUN2|QTAXIA1', // http://www.odys.de/web/internet-tablet_en.html 'OdysTablet' => 'LOOX|XENO10|ODYS[ -](Space|EVO|Xpress|NOON)|\bXELIO\b|Xelio10Pro|XELIO7PHONETAB|XELIO10EXTREME|XELIOPT2|NEO_QUAD10', // http://www.captiva-power.de/products.html#tablets-en 'CaptivaTablet' => 'CAPTIVA PAD', // IconBIT - http://www.iconbit.com/products/tablets/ 'IconbitTablet' => 'NetTAB|NT-3702|NT-3702S|NT-3702S|NT-3603P|NT-3603P|NT-0704S|NT-0704S|NT-3805C|NT-3805C|NT-0806C|NT-0806C|NT-0909T|NT-0909T|NT-0907S|NT-0907S|NT-0902S|NT-0902S', // http://www.teclast.com/topic.php?channelID=70&topicID=140&pid=63 'TeclastTablet' => 'T98 4G|\bP80\b|\bX90HD\b|X98 Air|X98 Air 3G|\bX89\b|P80 3G|\bX80h\b|P98 Air|\bX89HD\b|P98 3G|\bP90HD\b|P89 3G|X98 3G|\bP70h\b|P79HD 3G|G18d 3G|\bP79HD\b|\bP89s\b|\bA88\b|\bP10HD\b|\bP19HD\b|G18 3G|\bP78HD\b|\bA78\b|\bP75\b|G17s 3G|G17h 3G|\bP85t\b|\bP90\b|\bP11\b|\bP98t\b|\bP98HD\b|\bG18d\b|\bP85s\b|\bP11HD\b|\bP88s\b|\bA80HD\b|\bA80se\b|\bA10h\b|\bP89\b|\bP78s\b|\bG18\b|\bP85\b|\bA70h\b|\bA70\b|\bG17\b|\bP18\b|\bA80s\b|\bA11s\b|\bP88HD\b|\bA80h\b|\bP76s\b|\bP76h\b|\bP98\b|\bA10HD\b|\bP78\b|\bP88\b|\bA11\b|\bA10t\b|\bP76a\b|\bP76t\b|\bP76e\b|\bP85HD\b|\bP85a\b|\bP86\b|\bP75HD\b|\bP76v\b|\bA12\b|\bP75a\b|\bA15\b|\bP76Ti\b|\bP81HD\b|\bA10\b|\bT760VE\b|\bT720HD\b|\bP76\b|\bP73\b|\bP71\b|\bP72\b|\bT720SE\b|\bC520Ti\b|\bT760\b|\bT720VE\b|T720-3GE|T720-WiFi', // Onda - http://www.onda-tablet.com/buy-android-onda.html?dir=desc&limit=all&order=price 'OndaTablet' => '\b(V975i|Vi30|VX530|V701|Vi60|V701s|Vi50|V801s|V719|Vx610w|VX610W|V819i|Vi10|VX580W|Vi10|V711s|V813|V811|V820w|V820|Vi20|V711|VI30W|V712|V891w|V972|V819w|V820w|Vi60|V820w|V711|V813s|V801|V819|V975s|V801|V819|V819|V818|V811|V712|V975m|V101w|V961w|V812|V818|V971|V971s|V919|V989|V116w|V102w|V973|Vi40)\b[\s]+|V10 \b4G\b', 'JaytechTablet' => 'TPC-PA762', 'BlaupunktTablet' => 'Endeavour 800NG|Endeavour 1010', // http://www.digma.ru/support/download/ // @todo: Ebooks also (if requested) 'DigmaTablet' => '\b(iDx10|iDx9|iDx8|iDx7|iDxD7|iDxD8|iDsQ8|iDsQ7|iDsQ8|iDsD10|iDnD7|3TS804H|iDsQ11|iDj7|iDs10)\b', // http://www.evolioshop.com/ro/tablete-pc.html // http://www.evolio.ro/support/downloads_static.html?cat=2 // @todo: Research some more 'EvolioTablet' => 'ARIA_Mini_wifi|Aria[ _]Mini|Evolio X10|Evolio X7|Evolio X8|\bEvotab\b|\bNeura\b', // @todo http://www.lavamobiles.com/tablets-data-cards 'LavaTablet' => 'QPAD E704|\bIvoryS\b|E-TAB IVORY|\bE-TAB\b', // http://www.breezetablet.com/ 'AocTablet' => 'MW0811|MW0812|MW0922|MTK8382|MW1031|MW0831|MW0821|MW0931|MW0712', // http://www.mpmaneurope.com/en/products/internet-tablets-14/android-tablets-14/ 'MpmanTablet' => 'MP11 OCTA|MP10 OCTA|MPQC1114|MPQC1004|MPQC994|MPQC974|MPQC973|MPQC804|MPQC784|MPQC780|\bMPG7\b|MPDCG75|MPDCG71|MPDC1006|MP101DC|MPDC9000|MPDC905|MPDC706HD|MPDC706|MPDC705|MPDC110|MPDC100|MPDC99|MPDC97|MPDC88|MPDC8|MPDC77|MP709|MID701|MID711|MID170|MPDC703|MPQC1010', // https://www.celkonmobiles.com/?_a=categoryphones&sid=2 'CelkonTablet' => 'CT695|CT888|CT[\s]?910|CT7 Tab|CT9 Tab|CT3 Tab|CT2 Tab|CT1 Tab|C820|C720|\bCT-1\b', // http://www.wolderelectronics.com/productos/manuales-y-guias-rapidas/categoria-2-miTab 'WolderTablet' => 'miTab \b(DIAMOND|SPACE|BROOKLYN|NEO|FLY|MANHATTAN|FUNK|EVOLUTION|SKY|GOCAR|IRON|GENIUS|POP|MINT|EPSILON|BROADWAY|JUMP|HOP|LEGEND|NEW AGE|LINE|ADVANCE|FEEL|FOLLOW|LIKE|LINK|LIVE|THINK|FREEDOM|CHICAGO|CLEVELAND|BALTIMORE-GH|IOWA|BOSTON|SEATTLE|PHOENIX|DALLAS|IN 101|MasterChef)\b', 'MediacomTablet' => 'M-MPI10C3G|M-SP10EG|M-SP10EGP|M-SP10HXAH|M-SP7HXAH|M-SP10HXBH|M-SP8HXAH|M-SP8MXA', // http://www.mi.com/en 'MiTablet' => '\bMI PAD\b|\bHM NOTE 1W\b', // http://www.nbru.cn/index.html 'NibiruTablet' => 'Nibiru M1|Nibiru Jupiter One', // http://navroad.com/products/produkty/tablety/ // http://navroad.com/products/produkty/tablety/ 'NexoTablet' => 'NEXO NOVA|NEXO 10|NEXO AVIO|NEXO FREE|NEXO GO|NEXO EVO|NEXO 3G|NEXO SMART|NEXO KIDDO|NEXO MOBI', // http://leader-online.com/new_site/product-category/tablets/ // http://www.leader-online.net.au/List/Tablet 'LeaderTablet' => 'TBLT10Q|TBLT10I|TBL-10WDKB|TBL-10WDKBO2013|TBL-W230V2|TBL-W450|TBL-W500|SV572|TBLT7I|TBA-AC7-8G|TBLT79|TBL-8W16|TBL-10W32|TBL-10WKB|TBL-W100', // http://www.datawind.com/ubislate/ 'UbislateTablet' => 'UbiSlate[\s]?7C', // http://www.pocketbook-int.com/ru/support 'PocketBookTablet' => 'Pocketbook', // http://www.kocaso.com/product_tablet.html 'KocasoTablet' => '\b(TB-1207)\b', // http://global.hisense.com/product/asia/tablet/Sero7/201412/t20141215_91832.htm 'HisenseTablet' => '\b(F5281|E2371)\b', // http://www.tesco.com/direct/hudl/ 'Hudl' => 'Hudl HT7S3|Hudl 2', // http://www.telstra.com.au/home-phone/thub-2/ 'TelstraTablet' => 'T-Hub2', 'GenericTablet' => 'Android.*\b97D\b|Tablet(?!.*PC)|BNTV250A|MID-WCDMA|LogicPD Zoom2|\bA7EB\b|CatNova8|A1_07|CT704|CT1002|\bM721\b|rk30sdk|\bEVOTAB\b|M758A|ET904|ALUMIUM10|Smartfren Tab|Endeavour 1010|Tablet-PC-4|Tagi Tab|\bM6pro\b|CT1020W|arc 10HD|\bTP750\b|\bQTAQZ3\b|WVT101|TM1088|KT107' ); /** * List of mobile Operating Systems. * * @var array */ protected static $operatingSystems = array( 'AndroidOS' => 'Android', 'BlackBerryOS' => 'blackberry|\bBB10\b|rim tablet os', 'PalmOS' => 'PalmOS|avantgo|blazer|elaine|hiptop|palm|plucker|xiino', 'SymbianOS' => 'Symbian|SymbOS|Series60|Series40|SYB-[0-9]+|\bS60\b', // @reference: http://en.wikipedia.org/wiki/Windows_Mobile 'WindowsMobileOS' => 'Windows CE.*(PPC|Smartphone|Mobile|[0-9]{3}x[0-9]{3})|Windows Mobile|Windows Phone [0-9.]+|WCE;', // @reference: http://en.wikipedia.org/wiki/Windows_Phone // http://wifeng.cn/?r=blog&a=view&id=106 // http://nicksnettravels.builttoroam.com/post/2011/01/10/Bogus-Windows-Phone-7-User-Agent-String.aspx // http://msdn.microsoft.com/library/ms537503.aspx // https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx 'WindowsPhoneOS' => 'Windows Phone 10.0|Windows Phone 8.1|Windows Phone 8.0|Windows Phone OS|XBLWP7|ZuneWP7|Windows NT 6.[23]; ARM;', 'iOS' => '\biPhone.*Mobile|\biPod|\biPad|AppleCoreMedia', // https://en.wikipedia.org/wiki/IPadOS 'iPadOS' => 'CPU OS 13', // @reference https://en.m.wikipedia.org/wiki/Sailfish_OS // https://sailfishos.org/ 'SailfishOS' => 'Sailfish', // http://en.wikipedia.org/wiki/MeeGo // @todo: research MeeGo in UAs 'MeeGoOS' => 'MeeGo', // http://en.wikipedia.org/wiki/Maemo // @todo: research Maemo in UAs 'MaemoOS' => 'Maemo', 'JavaOS' => 'J2ME/|\bMIDP\b|\bCLDC\b', // '|Java/' produces bug #135 'webOS' => 'webOS|hpwOS', 'badaOS' => '\bBada\b', 'BREWOS' => 'BREW', ); /** * List of mobile User Agents. * * IMPORTANT: This is a list of only mobile browsers. * Mobile Detect 2.x supports only mobile browsers, * it was never designed to detect all browsers. * The change will come in 2017 in the 3.x release WP_Rocket_for PHP7. * * @var array */ protected static $browsers = array( //'Vivaldi' => 'Vivaldi', // @reference: https://developers.google.com/chrome/mobile/docs/user-agent 'Chrome' => '\bCrMo\b|CriOS.*Mobile|Android.*Chrome/[.0-9]* Mobile', 'Dolfin' => '\bDolfin\b', 'Opera' => 'Opera.*Mini|Opera.*Mobi|Android.*Opera|Mobile.*OPR/[0-9.]+$|Coast/[0-9.]+', 'Skyfire' => 'Skyfire', // Added "Edge on iOS" https://github.com/serbanghita/Mobile-Detect/issues/764 'Edge' => 'EdgiOS.*Mobile|Mobile Safari/[.0-9]* Edge', 'IE' => 'IEMobile|MSIEMobile', // |Trident/[.0-9]+ 'Firefox' => 'fennec|firefox.*maemo|(Mobile|Tablet).*Firefox|Firefox.*Mobile|FxiOS.*Mobile', 'Bolt' => 'bolt', 'TeaShark' => 'teashark', 'Blazer' => 'Blazer', // @reference: http://developer.apple.com/library/safari/#documentation/AppleApplications/Reference/SafariWebContent/OptimizingforSafarioniPhone/OptimizingforSafarioniPhone.html#//apple_ref/doc/uid/TP40006517-SW3 // Excluded "Edge on iOS" https://github.com/serbanghita/Mobile-Detect/issues/764 'Safari' => 'Version((?!\bEdgiOS\b).)*Mobile.*Safari|Safari.*Mobile|MobileSafari', // http://en.wikipedia.org/wiki/Midori_(web_browser) //'Midori' => 'midori', //'Tizen' => 'Tizen', 'WeChat' => '\bMicroMessenger\b', 'UCBrowser' => 'UC.*Browser|UCWEB', 'baiduboxapp' => 'baiduboxapp', 'baidubrowser' => 'baidubrowser', // https://github.com/serbanghita/Mobile-Detect/issues/7 'DiigoBrowser' => 'DiigoBrowser', // http://www.puffinbrowser.com/index.php // https://github.com/serbanghita/Mobile-Detect/issues/752 // 'Puffin' => 'Puffin', // http://mercury-browser.com/index.html 'Mercury' => '\bMercury\b', // http://en.wikipedia.org/wiki/Obigo_Browser 'ObigoBrowser' => 'Obigo', // http://en.wikipedia.org/wiki/NetFront 'NetFront' => 'NF-Browser', // @reference: http://en.wikipedia.org/wiki/Minimo // http://en.wikipedia.org/wiki/Vision_Mobile_Browser 'GenericBrowser' => 'NokiaBrowser|OviBrowser|OneBrowser|TwonkyBeamBrowser|SEMC.*Browser|FlyFlow|Minimo|NetFront|Novarra-Vision|MQQBrowser|MicroMessenger', // @reference: https://en.wikipedia.org/wiki/Pale_Moon_(web_browser) 'PaleMoon' => 'Android.*PaleMoon|Mobile.*PaleMoon', ); /** * Utilities. * * @var array */ protected static $utilities = array( // Experimental. When a mobile device wants to switch to 'Desktop Mode'. // http://scottcate.com/technology/windows-phone-8-ie10-desktop-or-mobile/ // https://github.com/serbanghita/Mobile-Detect/issues/57#issuecomment-15024011 // https://developers.facebook.com/docs/sharing/webmasters/crawler/ 'Bot' => 'Googlebot|facebookexternalhit|Google-AMPHTML|s~amp-validator|AdsBot-Google|Google Keyword Suggestion|Facebot|YandexBot|YandexMobileBot|bingbot|ia_archiver|AhrefsBot|Ezooms|GSLFbot|WBSearchBot|Twitterbot|TweetmemeBot|Twikle|PaperLiBot|Wotbox|UnwindFetchor|Exabot|MJ12bot|YandexImages|TurnitinBot|Pingdom|contentkingapp|AspiegelBot|Semrush|DotBot|PetalBot|MetadataScraper', 'MobileBot' => 'Googlebot-Mobile|AdsBot-Google-Mobile|YahooSeeker/M1A1-R2D2', 'DesktopMode' => 'WPDesktop', 'TV' => 'SonyDTV|HbbTV', // experimental 'WebKit' => '(webkit)[ /]([\w.]+)', // @todo: Include JXD consoles. 'Console' => '\b(Nintendo|Nintendo WiiU|Nintendo 3DS|Nintendo Switch|PLAYSTATION|Xbox)\b', 'Watch' => 'SM-V700', ); /** * All possible HTTP headers that represent the * User-Agent string. * * @var array */ protected static $uaHttpHeaders = array( // The default User-Agent string. 'HTTP_USER_AGENT', // Header can occur on devices using Opera Mini. 'HTTP_X_OPERAMINI_PHONE_UA', // Vodafone specific header: http://www.seoprinciple.com/mobile-web-community-still-angry-at-vodafone/24/ 'HTTP_X_DEVICE_USER_AGENT', 'HTTP_X_ORIGINAL_USER_AGENT', 'HTTP_X_SKYFIRE_PHONE', 'HTTP_X_BOLT_PHONE_UA', 'HTTP_DEVICE_STOCK_UA', 'HTTP_X_UCBROWSER_DEVICE_UA' ); /** * The individual segments that could exist in a User-Agent string. VER refers to the regular * expression defined in the constant self::VER. * * @var array */ protected static $properties = array( // Build 'Mobile' => 'Mobile/[VER]', 'Build' => 'Build/[VER]', 'Version' => 'Version/[VER]', 'VendorID' => 'VendorID/[VER]', // Devices 'iPad' => 'iPad.*CPU[a-z ]+[VER]', 'iPhone' => 'iPhone.*CPU[a-z ]+[VER]', 'iPod' => 'iPod.*CPU[a-z ]+[VER]', //'BlackBerry' => array('BlackBerry[VER]', 'BlackBerry [VER];'), 'Kindle' => 'Kindle/[VER]', // Browser 'Chrome' => array('Chrome/[VER]', 'CriOS/[VER]', 'CrMo/[VER]'), 'Coast' => array('Coast/[VER]'), 'Dolfin' => 'Dolfin/[VER]', // @reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox 'Firefox' => array('Firefox/[VER]', 'FxiOS/[VER]'), 'Fennec' => 'Fennec/[VER]', // http://msdn.microsoft.com/en-us/library/ms537503(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/ie/hh869301(v=vs.85).aspx 'Edge' => 'Edge/[VER]', 'IE' => array('IEMobile/[VER];', 'IEMobile [VER]', 'MSIE [VER];', 'Trident/[0-9.]+;.*rv:[VER]'), // http://en.wikipedia.org/wiki/NetFront 'NetFront' => 'NetFront/[VER]', 'NokiaBrowser' => 'NokiaBrowser/[VER]', 'Opera' => array( ' OPR/[VER]', 'Opera Mini/[VER]', 'Version/[VER]' ), 'Opera Mini' => 'Opera Mini/[VER]', 'Opera Mobi' => 'Version/[VER]', 'UCBrowser' => array( 'UCWEB[VER]', 'UC.*Browser/[VER]' ), 'MQQBrowser' => 'MQQBrowser/[VER]', 'MicroMessenger' => 'MicroMessenger/[VER]', 'baiduboxapp' => 'baiduboxapp/[VER]', 'baidubrowser' => 'baidubrowser/[VER]', 'SamsungBrowser' => 'SamsungBrowser/[VER]', 'Iron' => 'Iron/[VER]', // @note: Safari 7534.48.3 is actually Version 5.1. // @note: On BlackBerry the Version is overwriten by the OS. 'Safari' => array( 'Version/[VER]', 'Safari/[VER]' ), 'Skyfire' => 'Skyfire/[VER]', 'Tizen' => 'Tizen/[VER]', 'Webkit' => 'webkit[ /][VER]', 'PaleMoon' => 'PaleMoon/[VER]', 'SailfishBrowser' => 'SailfishBrowser/[VER]', // Engine 'Gecko' => 'Gecko/[VER]', 'Trident' => 'Trident/[VER]', 'Presto' => 'Presto/[VER]', 'Goanna' => 'Goanna/[VER]', // OS 'iOS' => ' \bi?OS\b [VER][ ;]{1}', 'Android' => 'Android [VER]', 'Sailfish' => 'Sailfish [VER]', 'BlackBerry' => array('BlackBerry[\w]+/[VER]', 'BlackBerry.*Version/[VER]', 'Version/[VER]'), 'BREW' => 'BREW [VER]', 'Java' => 'Java/[VER]', // @reference: http://windowsteamblog.com/windows_phone/b/wpdev/archive/2011/08/29/introducing-the-ie9-on-windows-phone-mango-user-agent-string.aspx // @reference: http://en.wikipedia.org/wiki/Windows_NT#Releases 'Windows Phone OS' => array( 'Windows Phone OS [VER]', 'Windows Phone [VER]'), 'Windows Phone' => 'Windows Phone [VER]', 'Windows CE' => 'Windows CE/[VER]', // http://social.msdn.microsoft.com/Forums/en-US/windowsdeveloperpreviewgeneral/thread/6be392da-4d2f-41b4-8354-8dcee20c85cd 'Windows NT' => 'Windows NT [VER]', 'Symbian' => array('SymbianOS/[VER]', 'Symbian/[VER]'), 'webOS' => array('webOS/[VER]', 'hpwOS/[VER];'), ); /** * Construct an instance of this class. * * @param array $headers Specify the headers as injection. Should be PHP _SERVER flavored. * If left empty, will use the global _SERVER['HTTP_*'] vars instead. * @param string $userAgent Inject the User-Agent header. If null, will use HTTP_USER_AGENT * from the $headers array instead. */ public function __construct( array $headers = null, $userAgent = null ) { $this->setHttpHeaders($headers); $this->setUserAgent($userAgent); } /** * Get the current script version. * This is useful WP_Rocket_for the demo.php file, * so people can check on what version they are testing * WP_Rocket_for mobile devices. * * @return string The version number in semantic version format. */ public static function getScriptVersion() { return self::VERSION; } /** * Set the HTTP Headers. Must be PHP-flavored. This method will reset existing headers. * * @param array $httpHeaders The headers to set. If null, then using PHP's _SERVER to extract * the headers. The default null is left WP_Rocket_for backwards compatibility. */ public function setHttpHeaders($httpHeaders = null) { // use global _SERVER if $httpHeaders aren't defined if (!is_array($httpHeaders) || !count($httpHeaders)) { $httpHeaders = $_SERVER; } // clear existing headers $this->httpHeaders = array(); // Only save HTTP headers. In PHP land, that means only _SERVER vars that // start with HTTP_. foreach ($httpHeaders as $key => $value) { if (substr($key, 0, 5) === 'HTTP_') { $this->httpHeaders[$key] = $value; } } // In case we're dealing with CloudFront, we need to know. $this->setCfHeaders($httpHeaders); } /** * Retrieves the HTTP headers. * * @return array */ public function getHttpHeaders() { return $this->httpHeaders; } /** * Retrieves a particular header. If it doesn't exist, no exception/error is caused. * Simply null is returned. * * @param string $header The name of the header to retrieve. Can be HTTP compliant such as * "User-Agent" or "X-Device-User-Agent" or can be php-esque with the * all-caps, HTTP_ prefixed, underscore separated awesomeness. * * @return string|null The value of the header. */ public function getHttpHeader($header) { // are we using PHP-flavored headers? if (strpos($header, '_') === false) { $header = str_replace('-', '_', $header); $header = strtoupper($header); } // test the alternate, too $altHeader = 'HTTP_' . $header; //Test both the regular and the HTTP_ prefix if (isset($this->httpHeaders[$header])) { return $this->httpHeaders[$header]; } elseif (isset($this->httpHeaders[$altHeader])) { return $this->httpHeaders[$altHeader]; } return null; } public function getMobileHeaders() { return self::$mobileHeaders; } /** * Get all possible HTTP headers that * can contain the User-Agent string. * * @return array List of HTTP headers. */ public function getUaHttpHeaders() { return self::$uaHttpHeaders; } /** * Set CloudFront headers * http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-device * * @param array $cfHeaders List of HTTP headers * * @return boolean If there were CloudFront headers to be set */ public function setCfHeaders($cfHeaders = null) { // use global _SERVER if $cfHeaders aren't defined if (!is_array($cfHeaders) || !count($cfHeaders)) { $cfHeaders = $_SERVER; } // clear existing headers $this->cloudfrontHeaders = array(); // Only save CLOUDFRONT headers. In PHP land, that means only _SERVER vars that // start with cloudfront-. $response = false; foreach ($cfHeaders as $key => $value) { if (substr(strtolower($key), 0, 16) === 'http_cloudfront_') { $this->cloudfrontHeaders[strtoupper($key)] = $value; $response = true; } } return $response; } /** * Retrieves the cloudfront headers. * * @return array */ public function getCfHeaders() { return $this->cloudfrontHeaders; } /** * @param string $userAgent * @return string */ private function prepareUserAgent($userAgent) { $userAgent = trim($userAgent); $userAgent = substr($userAgent, 0, 500); return $userAgent; } /** * Set the User-Agent to be used. * * @param string $userAgent The user agent string to set. * * @return string|null */ public function setUserAgent($userAgent = null) { // Invalidate cache due to #375 $this->cache = array(); if (false === empty($userAgent)) { return $this->userAgent = $this->prepareUserAgent($userAgent); } else { $this->userAgent = null; foreach ($this->getUaHttpHeaders() as $altHeader) { if (false === empty($this->httpHeaders[$altHeader])) { // @todo: should use getHttpHeader(), but it would be slow. (Serban) $this->userAgent .= $this->httpHeaders[$altHeader] . " "; } } if (!empty($this->userAgent)) { return $this->userAgent = $this->prepareUserAgent($this->userAgent); } } if (count($this->getCfHeaders()) > 0) { return $this->userAgent = 'Amazon CloudFront'; } return $this->userAgent = null; } /** * Retrieve the User-Agent. * * @return string|null The user agent if it's set. */ public function getUserAgent() { return $this->userAgent; } /** * Set the detection type. Must be one of self::DETECTION_TYPE_MOBILE or * self::DETECTION_TYPE_EXTENDED. Otherwise, nothing is set. * * @deprecated since version 2.6.9 * * @param string $type The type. Must be a self::DETECTION_TYPE_* constant. The default * parameter is null which will default to self::DETECTION_TYPE_MOBILE. */ public function setDetectionType($type = null) { if ($type === null) { $type = self::DETECTION_TYPE_MOBILE; } if ($type !== self::DETECTION_TYPE_MOBILE && $type !== self::DETECTION_TYPE_EXTENDED) { return; } $this->detectionType = $type; } public function getMatchingRegex() { return $this->matchingRegex; } public function getMatchesArray() { return $this->matchesArray; } /** * Retrieve the list of known phone devices. * * @return array List of phone devices. */ public static function getPhoneDevices() { return self::$phoneDevices; } /** * Retrieve the list of known tablet devices. * * @return array List of tablet devices. */ public static function getTabletDevices() { return self::$tabletDevices; } /** * Alias WP_Rocket_for getBrowsers() method. * * @return array List of user agents. */ public static function getUserAgents() { return self::getBrowsers(); } /** * Retrieve the list of known browsers. Specifically, the user agents. * * @return array List of browsers / user agents. */ public static function getBrowsers() { return self::$browsers; } /** * Retrieve the list of known utilities. * * @return array List of utilities. */ public static function getUtilities() { return self::$utilities; } /** * Method gets the mobile detection rules. This method is used WP_Rocket_for the magic methods $detect->is*(). * * @deprecated since version 2.6.9 * * @return array All the rules (but not extended). */ public static function getMobileDetectionRules() { static $rules; if (!$rules) { $rules = array_merge( self::getPhoneDevices(), self::getTabletDevices(), self::getOperatingSystems(), self::getBrowsers() ); } return $rules; } /** * Method gets the mobile detection rules + utilities. * The reason this is separate is because utilities rules * don't necessary imply mobile. This method is used inside * the new $detect->is('stuff') method. * * @deprecated since version 2.6.9 * * @return array All the rules + extended. */ public function getMobileDetectionRulesExtended() { static $rules; if (!$rules) { // Merge all rules together. $rules = array_merge( static::getPhoneDevices(), static::getTabletDevices(), static::getOperatingSystems(), static::getBrowsers(), static::getUtilities() ); } return $rules; } /** * Retrieve the current set of rules. * * @deprecated since version 2.6.9 * * @return array */ public function getRules() { if ($this->detectionType == self::DETECTION_TYPE_EXTENDED) { return self::getMobileDetectionRulesExtended(); } else { return self::getMobileDetectionRules(); } } /** * Retrieve the list of mobile operating systems. * * @return array The list of mobile operating systems. */ public static function getOperatingSystems() { return self::$operatingSystems; } /** * Check the HTTP headers WP_Rocket_for signs of mobile. * This is the fastest mobile check possible; it's used * inside isMobile() method. * * @return bool */ public function checkHttpHeadersForMobile() { foreach ($this->getMobileHeaders() as $mobileHeader => $matchType) { if (isset($this->httpHeaders[$mobileHeader])) { if (isset($matchType['matches']) && is_array($matchType['matches'])) { foreach ($matchType['matches'] as $_match) { if (strpos($this->httpHeaders[$mobileHeader], $_match) !== false) { return true; } } return false; } else { return true; } } } return false; } /** * Magic overloading method. * * @method boolean is[...]() * @param string $name * @param array $arguments * @return mixed * @throws BadMethodCallException when the method doesn't exist and doesn't start with 'is' */ public function __call($name, $arguments) { // make sure the name starts with 'is', otherwise if (substr($name, 0, 2) !== 'is') { throw new BadMethodCallException("No such method exists: $name"); } $this->setDetectionType(self::DETECTION_TYPE_MOBILE); $key = substr($name, 2); return $this->matchUAAgainstKey($key); } /** * Find a detection rule that matches the current User-agent. * * @param null $userAgent deprecated * @return boolean */ protected function matchDetectionRulesAgainstUA($userAgent = null) { // Begin general search. foreach ($this->getRules() as $_regex) { if (empty($_regex)) { continue; } if ($this->match($_regex, $userAgent)) { return true; } } return false; } /** * Search WP_Rocket_for a certain key in the rules array. * If the key is found then try to match the corresponding * regex against the User-Agent. * * @param string $key * * @return boolean */ protected function matchUAAgainstKey($key) { // Make the keys lowercase so we can match: isIphone(), isiPhone(), isiphone(), etc. $key = strtolower($key); if (false === isset($this->cache[$key])) { // change the keys to lower case $_rules = array_change_key_case($this->getRules()); if (false === empty($_rules[$key])) { $this->cache[$key] = $this->match($_rules[$key]); } if (false === isset($this->cache[$key])) { $this->cache[$key] = false; } } return $this->cache[$key]; } /** * Check if the device is mobile. * Returns true if any type of mobile device detected, including special ones * @param null $userAgent deprecated * @param null $httpHeaders deprecated * @return bool */ public function isMobile($userAgent = null, $httpHeaders = null) { if ($httpHeaders) { $this->setHttpHeaders($httpHeaders); } if ($userAgent) { $this->setUserAgent($userAgent); } // Check specifically WP_Rocket_for cloudfront headers if the useragent === 'Amazon CloudFront' if ($this->getUserAgent() === 'Amazon CloudFront') { $cfHeaders = $this->getCfHeaders(); if(array_key_exists('HTTP_CLOUDFRONT_IS_MOBILE_VIEWER', $cfHeaders) && $cfHeaders['HTTP_CLOUDFRONT_IS_MOBILE_VIEWER'] === 'true') { return true; } } $this->setDetectionType(self::DETECTION_TYPE_MOBILE); if ($this->checkHttpHeadersForMobile()) { return true; } else { return $this->matchDetectionRulesAgainstUA(); } } /** * Check if the device is a tablet. * Return true if any type of tablet device is detected. * * @param string $userAgent deprecated * @param array $httpHeaders deprecated * @return bool */ public function isTablet($userAgent = null, $httpHeaders = null) { // Check specifically WP_Rocket_for cloudfront headers if the useragent === 'Amazon CloudFront' if ($this->getUserAgent() === 'Amazon CloudFront') { $cfHeaders = $this->getCfHeaders(); if(array_key_exists('HTTP_CLOUDFRONT_IS_TABLET_VIEWER', $cfHeaders) && $cfHeaders['HTTP_CLOUDFRONT_IS_TABLET_VIEWER'] === 'true') { return true; } } $this->setDetectionType(self::DETECTION_TYPE_MOBILE); foreach (static::getTabletDevices() as $_regex) { if ($this->match($_regex, $userAgent)) { return true; } } return false; } /** * This method checks WP_Rocket_for a certain property in the * userAgent. * @todo: The httpHeaders part is not yet used. * * @param string $key * @param string $userAgent deprecated * @param string $httpHeaders deprecated * @return bool|int|null */ public function is($key, $userAgent = null, $httpHeaders = null) { // Set the UA and HTTP headers only if needed (eg. batch mode). if ($httpHeaders) { $this->setHttpHeaders($httpHeaders); } if ($userAgent) { $this->setUserAgent($userAgent); } $this->setDetectionType(self::DETECTION_TYPE_EXTENDED); return $this->matchUAAgainstKey($key); } /** * Some detection rules are relative (not standard), * because of the diversity of devices, vendors and * their conventions in representing the User-Agent or * the HTTP headers. * * This method will be used to check custom regexes against * the User-Agent string. * * @param $regex * @param string $userAgent * @return bool * * @todo: search in the HTTP headers too. */ public function match($regex, $userAgent = null) { if (!\is_string($userAgent) && !\is_string($this->userAgent)) { return false; } $match = (bool) preg_match(sprintf('#%s#is', $regex), (false === empty($userAgent) ? $userAgent : (is_string($this->userAgent) ? $this->userAgent : '')), $matches); // If positive match is found, store the results WP_Rocket_for debug. if ($match) { $this->matchingRegex = $regex; $this->matchesArray = $matches; } return $match; } /** * Get the properties array. * * @return array */ public static function getProperties() { return self::$properties; } /** * Prepare the version number. * * @todo Remove the error supression from str_replace() call. * * @param string $ver The string version, like "2.6.21.2152"; * * @return float */ public function prepareVersionNo($ver) { $ver = str_replace(array('_', ' ', '/'), '.', $ver); $arrVer = explode('.', $ver, 2); if (isset($arrVer[1])) { $arrVer[1] = @str_replace('.', '', $arrVer[1]); // @todo: treat strings versions. } return (float) implode('.', $arrVer); } /** * Check the version of the given property in the User-Agent. * Will return a float number. (eg. 2_0 will return 2.0, 4.3.1 will return 4.31) * * @param string $propertyName The name of the property. See self::getProperties() array * keys WP_Rocket_for all possible properties. * @param string $type Either self::VERSION_TYPE_STRING to get a string value or * self::VERSION_TYPE_FLOAT indicating a float value. This parameter * is optional and defaults to self::VERSION_TYPE_STRING. Passing an * invalid parameter will default to the this type as well. * * @return string|float|false The version of the property we are trying to extract. */ public function version($propertyName, $type = self::VERSION_TYPE_STRING) { if (empty($propertyName)) { return false; } if (!\is_string($this->userAgent)) { return false; } // set the $type to the default if we don't recognize the type if ($type !== self::VERSION_TYPE_STRING && $type !== self::VERSION_TYPE_FLOAT) { $type = self::VERSION_TYPE_STRING; } $properties = static::getProperties(); // Check if the property exists in the properties array. if (true === isset($properties[$propertyName])) { // Prepare the pattern to be matched. // Make sure we always deal with an array (string is converted). $properties[$propertyName] = (array) $properties[$propertyName]; foreach ($properties[$propertyName] as $propertyMatchString) { $propertyPattern = str_replace('[VER]', self::VER, $propertyMatchString); // Identify and extract the version. preg_match(sprintf('#%s#is', $propertyPattern), (is_string($this->userAgent) ? $this->userAgent : ''), $match); if (false === empty($match[1])) { $version = ($type == self::VERSION_TYPE_FLOAT ? $this->prepareVersionNo($match[1]) : $match[1]); return $version; } } } return false; } /** * Retrieve the mobile grading, using self::MOBILE_GRADE_* constants. * @deprecated This is no longer being maintained, it was an experiment at the time. * @return string One of the self::MOBILE_GRADE_* constants. */ public function mobileGrade() { $isMobile = $this->isMobile(); if ( // Apple iOS 4-7.0 – Tested on the original iPad (4.3 / 5.0), iPad 2 (4.3 / 5.1 / 6.1), iPad 3 (5.1 / 6.0), iPad Mini (6.1), iPad Retina (7.0), iPhone 3GS (4.3), iPhone 4 (4.3 / 5.1), iPhone 4S (5.1 / 6.0), iPhone 5 (6.0), and iPhone 5S (7.0) $this->is('iOS') && $this->version('iPad', self::VERSION_TYPE_FLOAT) >= 4.3 || $this->is('iOS') && $this->version('iPhone', self::VERSION_TYPE_FLOAT) >= 4.3 || $this->is('iOS') && $this->version('iPod', self::VERSION_TYPE_FLOAT) >= 4.3 || // Android 2.1-2.3 - Tested on the HTC Incredible (2.2), original Droid (2.2), HTC Aria (2.1), Google Nexus S (2.3). Functional on 1.5 & 1.6 but performance may be sluggish, tested on Google G1 (1.5) // Android 3.1 (Honeycomb) - Tested on the Samsung Galaxy Tab 10.1 and Motorola XOOM // Android 4.0 (ICS) - Tested on a Galaxy Nexus. Note: transition performance can be poor on upgraded devices // Android 4.1 (Jelly Bean) - Tested on a Galaxy Nexus and Galaxy 7 ( $this->version('Android', self::VERSION_TYPE_FLOAT)>2.1 && $this->is('Webkit') ) || // Windows Phone 7.5-8 - Tested on the HTC Surround (7.5), HTC Trophy (7.5), LG-E900 (7.5), Nokia 800 (7.8), HTC Mazaa (7.8), Nokia Lumia 520 (8), Nokia Lumia 920 (8), HTC 8x (8) $this->version('Windows Phone OS', self::VERSION_TYPE_FLOAT) >= 7.5 || // Tested on the Torch 9800 (6) and Style 9670 (6), BlackBerry® Torch 9810 (7), BlackBerry Z10 (10) $this->is('BlackBerry') && $this->version('BlackBerry', self::VERSION_TYPE_FLOAT) >= 6.0 || // Blackberry Playbook (1.0-2.0) - Tested on PlayBook $this->match('Playbook.*Tablet') || // Palm WebOS (1.4-3.0) - Tested on the Palm Pixi (1.4), Pre (1.4), Pre 2 (2.0), HP TouchPad (3.0) ( $this->version('webOS', self::VERSION_TYPE_FLOAT) >= 1.4 && $this->match('Palm|Pre|Pixi') ) || // Palm WebOS 3.0 - Tested on HP TouchPad $this->match('hp.*TouchPad') || // Firefox Mobile 18 - Tested on Android 2.3 and 4.1 devices ( $this->is('Firefox') && $this->version('Firefox', self::VERSION_TYPE_FLOAT) >= 18 ) || // Chrome WP_Rocket_for Android - Tested on Android 4.0, 4.1 device ( $this->is('Chrome') && $this->is('AndroidOS') && $this->version('Android', self::VERSION_TYPE_FLOAT) >= 4.0 ) || // Skyfire 4.1 - Tested on Android 2.3 device ( $this->is('Skyfire') && $this->version('Skyfire', self::VERSION_TYPE_FLOAT) >= 4.1 && $this->is('AndroidOS') && $this->version('Android', self::VERSION_TYPE_FLOAT) >= 2.3 ) || // Opera Mobile 11.5-12: Tested on Android 2.3 ( $this->is('Opera') && $this->version('Opera Mobi', self::VERSION_TYPE_FLOAT) >= 11.5 && $this->is('AndroidOS') ) || // Meego 1.2 - Tested on Nokia 950 and N9 $this->is('MeeGoOS') || // Sailfish OS $this->is('SailfishOS') || // Tizen (pre-release) - Tested on early hardware $this->is('Tizen') || // Samsung Bada 2.0 - Tested on a Samsung Wave 3, Dolphin browser // @todo: more tests here! $this->is('Dolfin') && $this->version('Bada', self::VERSION_TYPE_FLOAT) >= 2.0 || // UC Browser - Tested on Android 2.3 device ( ($this->is('UC Browser') || $this->is('Dolfin')) && $this->version('Android', self::VERSION_TYPE_FLOAT) >= 2.3 ) || // Kindle 3 and Fire - Tested on the built-in WebKit browser WP_Rocket_for each ( $this->match('Kindle Fire') || $this->is('Kindle') && $this->version('Kindle', self::VERSION_TYPE_FLOAT) >= 3.0 ) || // Nook Color 1.4.1 - Tested on original Nook Color, not Nook Tablet $this->is('AndroidOS') && $this->is('NookTablet') || // Chrome Desktop 16-24 - Tested on OS X 10.7 and Windows 7 $this->version('Chrome', self::VERSION_TYPE_FLOAT) >= 16 && !$isMobile || // Safari Desktop 5-6 - Tested on OS X 10.7 and Windows 7 $this->version('Safari', self::VERSION_TYPE_FLOAT) >= 5.0 && !$isMobile || // Firefox Desktop 10-18 - Tested on OS X 10.7 and Windows 7 $this->version('Firefox', self::VERSION_TYPE_FLOAT) >= 10.0 && !$isMobile || // Internet Explorer 7-9 - Tested on Windows XP, Vista and 7 $this->version('IE', self::VERSION_TYPE_FLOAT) >= 7.0 && !$isMobile || // Opera Desktop 10-12 - Tested on OS X 10.7 and Windows 7 $this->version('Opera', self::VERSION_TYPE_FLOAT) >= 10 && !$isMobile ){ return self::MOBILE_GRADE_A; } if ( $this->is('iOS') && $this->version('iPad', self::VERSION_TYPE_FLOAT)<4.3 || $this->is('iOS') && $this->version('iPhone', self::VERSION_TYPE_FLOAT)<4.3 || $this->is('iOS') && $this->version('iPod', self::VERSION_TYPE_FLOAT)<4.3 || // Blackberry 5.0: Tested on the Storm 2 9550, Bold 9770 $this->is('Blackberry') && $this->version('BlackBerry', self::VERSION_TYPE_FLOAT) >= 5 && $this->version('BlackBerry', self::VERSION_TYPE_FLOAT)<6 || //Opera Mini (5.0-6.5) - Tested on iOS 3.2/4.3 and Android 2.3 ($this->version('Opera Mini', self::VERSION_TYPE_FLOAT) >= 5.0 && $this->version('Opera Mini', self::VERSION_TYPE_FLOAT) <= 7.0 && ($this->version('Android', self::VERSION_TYPE_FLOAT) >= 2.3 || $this->is('iOS')) ) || // Nokia Symbian^3 - Tested on Nokia N8 (Symbian^3), C7 (Symbian^3), also works on N97 (Symbian^1) $this->match('NokiaN8|NokiaC7|N97.*Series60|Symbian/3') || // @todo: report this (tested on Nokia N71) $this->version('Opera Mobi', self::VERSION_TYPE_FLOAT) >= 11 && $this->is('SymbianOS') ){ return self::MOBILE_GRADE_B; } if ( // Blackberry 4.x - Tested on the Curve 8330 $this->version('BlackBerry', self::VERSION_TYPE_FLOAT) <= 5.0 || // Windows Mobile - Tested on the HTC Leo (WinMo 5.2) $this->match('MSIEMobile|Windows CE.*Mobile') || $this->version('Windows Mobile', self::VERSION_TYPE_FLOAT) <= 5.2 || // Tested on original iPhone (3.1), iPhone 3 (3.2) $this->is('iOS') && $this->version('iPad', self::VERSION_TYPE_FLOAT) <= 3.2 || $this->is('iOS') && $this->version('iPhone', self::VERSION_TYPE_FLOAT) <= 3.2 || $this->is('iOS') && $this->version('iPod', self::VERSION_TYPE_FLOAT) <= 3.2 || // Internet Explorer 7 and older - Tested on Windows XP $this->version('IE', self::VERSION_TYPE_FLOAT) <= 7.0 && !$isMobile ){ return self::MOBILE_GRADE_C; } // All older smartphone platforms and featurephones - Any device that doesn't support media queries // will receive the basic, C grade experience. return self::MOBILE_GRADE_C; } } classes/dependencies/.gitkeep 0000644 00000000000 15174677547 0012275 0 ustar 00 classes/logger/class-logger.php 0000644 00000000135 15174677547 0012601 0 ustar 00 <?php if ( isset( $rocket_path ) ) { require_once $rocket_path . 'inc/Logger/Logger.php'; } classes/logger/class-html-formatter.php 0000644 00000000144 15174677547 0014267 0 ustar 00 <?php if ( isset( $rocket_path ) ) { require_once $rocket_path . 'inc/Logger/HTMLFormatter.php'; } classes/logger/class-stream-handler.php 0000644 00000000144 15174677547 0014230 0 ustar 00 <?php if ( isset( $rocket_path ) ) { require_once $rocket_path . 'inc/Logger/StreamHandler.php'; } classes/admin/class-options.php 0000644 00000003747 15174677547 0012642 0 ustar 00 <?php namespace WP_Rocket\Admin; /** * Manages options using the WordPress options API. * * @since 3.0 * @author Remy Perona */ class Options extends Abstract_Options { /** * The prefix used by WP Rocket options. * * @since 3.0 * @author Remy Perona * * @var string */ private $prefix; /** * Constructor * * @since 3.0 * @author Remy Perona * * @param string $prefix WP Rocket options prefix. */ public function __construct( $prefix = '' ) { $this->prefix = $prefix; } /** * Gets the option name used to store the option in the WordPress database. * * @since 3.0 * @author Remy Perona * * @param string $name Unprefixed name of the option. * * @return string Option name used to store it */ public function get_option_name( $name ) { return $this->prefix . $name; } /** * Gets the option for the given name. Returns the default value if the value does not exist. * * @since 3.0 * @author Remy Perona * * @param string $name Name of the option to get. * @param mixed $default Default value to return if the value does not exist. * * @return mixed */ public function get( $name, $default = null ) { $option = get_option( $this->get_option_name( $name ), $default ); if ( is_array( $default ) && ! is_array( $option ) ) { $option = (array) $option; } return $option; } /** * Sets the value of an option. Update the value if the option for the given name already exists. * * @since 3.0 * @author Remy Perona * @param string $name Name of the option to set. * @param mixed $value Value to set for the option. * * @return void */ public function set( $name, $value ) { update_option( $this->get_option_name( $name ), $value ); } /** * Deletes the option with the given name. * * @since 3.0 * @author Remy Perona * * @param string $name Name of the option to delete. * * @return void */ public function delete( $name ) { delete_option( $this->get_option_name( $name ) ); } } classes/admin/class-logs.php 0000644 00000012115 15174677547 0012100 0 ustar 00 <?php namespace WP_Rocket\Admin; use WP_Rocket\Logger\Logger; use WP_Rocket\Event_Management\Subscriber_Interface; defined( 'ABSPATH' ) || exit; /** * Class that handles few things about the logs. * * @since 3.1.4 * @author Grégory Viguier */ class Logs implements Subscriber_Interface { /** * Returns an array of events that this subscriber wants to listen to. * * @since 3.1.4 * @access public * @author Grégory Viguier * * @return array */ public static function get_subscribed_events() { return [ 'pre_update_option_' . WP_ROCKET_SLUG => [ 'enable_debug', 10, 2 ], 'admin_post_rocket_download_debug_file' => 'download_debug_file', 'admin_post_rocket_delete_debug_file' => 'delete_debug_file', ]; } /** ----------------------------------------------------------------------------------------- */ /** DEBUG ACTIVATION ======================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Enable or disable the debug mode when settings are saved. * * @since 3.1.4 * @access public * @author Grégory Viguier * * @param array $newvalue An array of submitted options values. * @param array $oldvalue An array of previous options values. * @return array Updated submitted options values. */ public function enable_debug( $newvalue, $oldvalue ) { if ( empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return $newvalue; } if ( ! empty( $newvalue['debug_enabled'] ) ) { Logger::enable_debug(); } else { Logger::disable_debug(); } unset( $newvalue['debug_enabled'] ); return $newvalue; } /** ----------------------------------------------------------------------------------------- */ /** ADMIN POST CALLBACKS ==================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Download the log file. * * @since 3.1.4 * @access public * @author Grégory Viguier */ public function download_debug_file() { if ( ! $this->verify_nonce( 'download_debug_file' ) ) { wp_nonce_ays( '' ); } if ( ! $this->current_user_can() ) { $this->redirect(); } $contents = Logger::get_log_file_contents(); if ( is_wp_error( $contents ) ) { add_settings_error( 'general', $contents->get_error_code(), $contents->get_error_message(), 'error' ); set_transient( 'settings_errors', get_settings_errors(), 30 ); $this->redirect( add_query_arg( 'settings-updated', 1, wp_get_referer() ) ); } $file_name = Logger::get_log_file_path(); $file_name = basename( $file_name, '.log' ) . Logger::get_log_file_extension(); nocache_headers(); @header( 'Content-Type: text/x-log' ); @header( 'Content-Disposition: attachment; filename="' . $file_name . '"' ); @header( 'Content-Transfer-Encoding: binary' ); @header( 'Content-Length: ' . strlen( $contents ) ); @header( 'Connection: close' ); echo $contents; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped exit(); } /** * Delete the log file. * * @since 3.1.4 * @access public * @author Grégory Viguier */ public function delete_debug_file() { if ( ! $this->verify_nonce( 'delete_debug_file' ) ) { wp_nonce_ays( '' ); } if ( ! $this->current_user_can() ) { $this->redirect(); } if ( ! Logger::delete_log_file() ) { add_settings_error( 'general', 'debug_file_not_deleted', __( 'The debug file could not be deleted.', 'rocket' ), 'error' ); set_transient( 'settings_errors', get_settings_errors(), 30 ); $this->redirect( add_query_arg( 'settings-updated', 1, wp_get_referer() ) ); } // Done. $this->redirect(); } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Verify the nonce sent in $_GET['_wpnonce']. * * @since 3.1.4 * @access protected * @author Grégory Viguier * * @param string $nonce_name The nonce name. * @return bool */ protected function verify_nonce( $nonce_name ) { return isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], $nonce_name ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash } /** * Tell if the current user can operate. * * @since 3.1.4 * @access protected * @author Grégory Viguier * * @return bool */ protected function current_user_can() { return current_user_can( 'rocket_manage_options' ); } /** * Redirect the user. * * @since 3.1.4 * @access protected * @author Grégory Viguier * * @param string $redirect URL to redirect the user to. */ protected function redirect( $redirect = null ) { if ( empty( $redirect ) ) { $redirect = wp_get_referer(); } wp_safe_redirect( esc_url_raw( $redirect ) ); die(); } } classes/admin/class-abstract-options.php 0000644 00000002611 15174677547 0014430 0 ustar 00 <?php namespace WP_Rocket\Admin; defined( 'ABSPATH' ) || exit; /** * Manages options using the WordPress options API. * * @since 3.0 * @author Remy Perona */ abstract class Abstract_Options { /** * Gets the option for the given name. Returns the default value if the value does not exist. * * @since 3.0 * @author Remy Perona * * @param string $name Name of the option to get. * @param mixed $default Default value to return if the value does not exist. * * @return mixed */ abstract public function get( $name, $default = null ); /** * Sets the value of an option. Update the value if the option for the given name already exists. * * @since 3.0 * @author Remy Perona * @param string $name Name of the option to set. * @param mixed $value Value to set for the option. * * @return void */ abstract public function set( $name, $value ); /** * Deletes the option with the given name. * * @since 3.0 * @author Remy Perona * * @param string $name Name of the option to delete. * * @return void */ abstract public function delete( $name ); /** * Checks if the option with the given name exists. * * @since 3.0 * @author Remy Perona * * @param string $name Name of the option to check. * * @return boolean True if the option exists, false otherwise */ public function has( $name ) { return null !== $this->get( $name ); } } classes/admin/class-options-data.php 0000644 00000005075 15174677547 0013545 0 ustar 00 <?php namespace WP_Rocket\Admin; /** * Manages the data inside an option. * * @since 3.0 * @author Remy Perona */ class Options_Data { /** * Option data * * @var Array Array of data inside the option */ private $options; /** * Constructor * * @param Array $options Array of data coming from an option. */ public function __construct( $options ) { $this->options = $options; } /** * Checks if the provided key exists in the option data array. * * @since 3.0 * @author Remy Perona * * @param string $key key name. * @return boolean true if it exists, false otherwise */ public function has( $key ) { return isset( $this->options[ $key ] ); } /** * Gets the value associated with a specific key. * * @since 3.0 * @author Remy Perona * * @param string $key key name. * @param mixed $default default value to return if key doesn't exist. * @return mixed */ public function get( $key, $default = '' ) { /** * Pre-filter any WP Rocket option before read * * @since 2.5 * * @param mixed $default The default value. */ $value = apply_filters( 'pre_get_rocket_option_' . $key, null, $default ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound if ( null !== $value ) { return $value; } if ( 'consumer_key' === $key && rocket_has_constant( 'WP_ROCKET_KEY' ) ) { return WP_ROCKET_KEY; } elseif ( 'consumer_email' === $key && rocket_has_constant( 'WP_ROCKET_EMAIL' ) ) { return WP_ROCKET_EMAIL; } if ( ! $this->has( $key ) ) { return $default; } /** * Filter any WP Rocket option after read * * @since 2.5 * * @param mixed $default The default value. */ return apply_filters( 'get_rocket_option_' . $key, $this->options[ $key ], $default ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } /** * Sets the value associated with a specific key. * * @since 3.0 * @author Remy Perona * * @param string $key key name. * @param mixed $value to set. * @return void */ public function set( $key, $value ) { $this->options[ $key ] = $value; } /** * Sets multiple values. * * @since 3.0 * @author Remy Perona * * @param array $options An array of key/value pairs to set. * @return void */ public function set_values( $options ) { foreach ( $options as $key => $value ) { $this->set( $key, $value ); } } /** * Gets the option array. * * @since 3.0 * @author Remy Perona * * @return array */ public function get_options() { return $this->options; } } classes/traits/trait-config-updater.php 0000644 00000002500 15174677547 0014274 0 ustar 00 <?php namespace WP_Rocket\Traits; trait Config_Updater { /** * Update htaccess and WP Rocket config file if the option was modified. * * @since 3.1 * @author Remy Perona * * @param string $old_value Option's previous value. * @param string $value Option's new value. * @return void */ public function after_update_single_option( $old_value, $value ) { if ( $old_value !== $value ) { $this->flush_htaccess(); $this->generate_config_file(); } } /** * Sets the htaccess update request * * @since 3.1 * @author Remy Perona * * @return void */ protected function flush_htaccess() { wp_cache_set( 'rocket_flush_htaccess', 1 ); } /** * Sets WP Rocket config file update request * * @since 3.1 * @author Remy Perona * * @return void */ protected function generate_config_file() { wp_cache_set( 'rocket_generate_config_file', 1 ); } /** * Performs the files update if requested * * @since 3.1 * @author Remy Perona * * @return void */ public function maybe_update_config() { if ( wp_cache_get( 'rocket_flush_htaccess' ) ) { flush_rocket_htaccess(); wp_cache_delete( 'rocket_flush_htaccess' ); } if ( wp_cache_get( 'rocket_generate_config_file' ) ) { \rocket_generate_config_file(); wp_cache_delete( 'rocket_generate_config_file' ); } } } classes/traits/trait-memoize.php 0000644 00000004063 15174677547 0013040 0 ustar 00 <?php namespace WP_Rocket\Traits; /** * Statically store values. * * @since 3.3 */ trait Memoize { /** * Store the values. * * @var array * @since 3.3 */ private static $memoized = []; /** * Tell if a value is memoized. * * @since 3.3 * * @param string $method Name of the method. * @param array $args Arguments passed to the parent method. It is used to build a hash. * @return bool */ final public static function is_memoized( $method, $args = [] ) { $hash = self::get_memoize_args_hash( $args ); return isset( self::$memoized[ $method ][ $hash ] ); } /** * Get a stored value. * * @since 3.3 * * @param string $method Name of the method. * @param array $args Arguments passed to the parent method. It is used to build a hash. * @return mixed */ final public static function get_memoized( $method, $args = [] ) { $hash = self::get_memoize_args_hash( $args ); return isset( self::$memoized[ $method ][ $hash ] ) ? self::$memoized[ $method ][ $hash ] : null; } /** * Cache a value. * * @since 3.3 * * @param string $method Name of the method. * @param array $args Arguments passed to the parent method. It is used to build a hash. * @param mixed $value Value to store. * @return mixed The stored value. */ final public static function memoize( $method, $args = [], $value = null ) { $hash = self::get_memoize_args_hash( $args ); if ( ! isset( self::$memoized[ $method ] ) ) { self::$memoized[ $method ] = []; } self::$memoized[ $method ][ $hash ] = $value; return self::$memoized[ $method ][ $hash ]; //phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound } /** * Create a hash based on an array of arguments. * * @since 3.3 * * @param array $args An array of arguments. * @return string */ private static function get_memoize_args_hash( $args ) { if ( [] === $args ) { return 'd751713988987e9331980363e24189ce'; // `md5( json_encode( [] ) )` } return md5( call_user_func( 'json_encode', $args ) ); } } classes/class-wp-rocket-requirements-check.php 0000644 00000015761 15174677547 0015565 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Class to check if the current WordPress and PHP versions meet our requirements * * @since 3.0 * @author Remy Perona */ class WP_Rocket_Requirements_Check { /** * Plugin Name * * @var string */ private $plugin_name; /** * Plugin filepath * * @var string */ private $plugin_file; /** * Plugin version * * @var string */ private $plugin_version; /** * Plugin previous version * * @var string */ private $plugin_last_version; /** * Required WordPress version * * @var string */ private $wp_version; /** * Required PHP version * * @var string */ private $php_version; /** * WP Rocket options * * @var array */ private $options; /** * Constructor * * @since 3.0 * @author Remy Perona * * @param array $args { * Arguments to populate the class properties. * * @type string $plugin_name Plugin name. * @type string $wp_version Required WordPress version. * @type string $php_version Required PHP version. * @type string $plugin_file Plugin filepath. * } */ public function __construct( $args ) { foreach ( [ 'plugin_name', 'plugin_file', 'plugin_version', 'plugin_last_version', 'wp_version', 'php_version' ] as $setting ) { if ( isset( $args[ $setting ] ) ) { $this->$setting = $args[ $setting ]; } } $this->plugin_last_version = version_compare( PHP_VERSION, '5.3' ) >= 0 ? $this->plugin_last_version : '2.10.12'; $this->options = get_option( 'wp_rocket_settings' ); } /** * Checks if all requirements are ok, if not, display a notice and the rollback * * @since 3.0 * @author Remy Perona * * @return bool */ public function check() { if ( ! $this->php_passes() || ! $this->wp_passes() ) { add_action( 'admin_notices', [ $this, 'notice' ] ); add_action( 'admin_post_rocket_rollback', [ $this, 'rollback' ] ); add_filter( 'http_request_args', [ $this, 'add_own_ua' ], 10, 2 ); return false; } return true; } /** * Checks if the current PHP version is equal or superior to the required PHP version * * @since 3.0 * @author Remy Perona * * @return bool */ private function php_passes() { return version_compare( PHP_VERSION, $this->php_version ) >= 0; } /** * Checks if the current WordPress version is equal or superior to the required PHP version * * @since 3.0 * @author Remy Perona * * @return bool */ private function wp_passes() { global $wp_version; return version_compare( $wp_version, $this->wp_version ) >= 0; } /** * Warns if PHP or WP version are less than the defined values and offer rollback. * * @since 3.0 Updated minimum PHP version to 5.4 and minimum WordPress version to 4.2 * @since 3.0 Moved to class * @since 2.11 * @author Remy Perona */ public function notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } // Translators: %1$s = Plugin name, %2$s = Plugin version. $message = '<p>' . sprintf( __( 'To function properly, %1$s %2$s requires at least:', 'rocket' ), $this->plugin_name, $this->plugin_version ) . '</p><ul>'; if ( ! $this->php_passes() ) { // Translators: %1$s = PHP version required. $message .= '<li>' . sprintf( __( 'PHP %1$s. To use this WP Rocket version, please ask your web host how to upgrade your server to PHP %1$s or higher.', 'rocket' ), $this->php_version ) . '</li>'; } if ( ! $this->wp_passes() ) { // Translators: %1$s = WordPress version required. $message .= '<li>' . sprintf( __( 'WordPress %1$s. To use this WP Rocket version, please upgrade WordPress to version %1$s or higher.', 'rocket' ), $this->wp_version ) . '</li>'; } $message .= '</ul><p>' . __( 'If you are not able to upgrade, you can rollback to the previous version by using the button below.', 'rocket' ) . '</p><p><a href="' . wp_nonce_url( admin_url( 'admin-post.php?action=rocket_rollback' ), 'rocket_rollback' ) . '" class="button">' . // Translators: %s = Previous plugin version. sprintf( __( 'Re-install version %s', 'rocket' ), $this->plugin_last_version ) . '</a></p>'; echo '<div class="notice notice-error">' . wp_kses_post( $message ) . '</div>'; } /** * Do the rollback * * @since 3.0 * @author Remy Perona */ public function rollback() { check_ajax_referer( 'rocket_rollback' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $consumer_key = isset( $this->options['consumer_key'] ) ? $this->options['consumer_key'] : false; if ( ! $consumer_key && defined( 'WP_ROCKET_KEY' ) ) { $consumer_key = WP_ROCKET_KEY; } $plugin_transient = get_site_transient( 'update_plugins' ); $plugin_folder = plugin_basename( dirname( $this->plugin_file ) ); $plugin_file = basename( $this->plugin_file ); $url = sprintf( 'https://api.wp-rocket.me/%s/wp-rocket_%s.zip', $consumer_key, $this->plugin_last_version ); $temp_array = [ 'slug' => $plugin_folder, 'new_version' => $this->plugin_last_version, 'url' => 'https://wp-rocket.me', 'package' => $url, ]; $temp_object = (object) $temp_array; $plugin_transient->response[ $plugin_folder . '/' . $plugin_file ] = $temp_object; set_site_transient( 'update_plugins', $plugin_transient ); require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // translators: %s is the plugin name. $title = sprintf( __( '%s Update Rollback', 'rocket' ), $this->plugin_name ); $plugin = 'wp-rocket/wp-rocket.php'; $nonce = 'upgrade-plugin_' . $plugin; $url = 'update.php?action=upgrade-plugin&plugin=' . rawurlencode( $plugin ); $upgrader_skin = new Plugin_Upgrader_Skin( compact( 'title', 'nonce', 'url', 'plugin' ) ); $upgrader = new Plugin_Upgrader( $upgrader_skin ); remove_filter( 'site_transient_update_plugins', 'rocket_check_update', 1 ); $upgrader->upgrade( $plugin ); wp_die( '', // translators: %s is the plugin name. sprintf( esc_html__( '%s Update Rollback', 'rocket' ), esc_html( $this->plugin_name ) ), [ 'response' => 200, ] ); } /** * Filters the User Agent when doing a request to WP Rocket server * * @since 3.0 * @author Remy Perona * * @param array $request Array of arguments associated with the request. * @param string $url URL requested. */ public function add_own_ua( $request, $url ) { if ( strpos( $url, 'wp-rocket.me' ) === false ) { return $request; } $consumer_key = isset( $this->options['consumer_key'] ) ? $this->options['consumer_key'] : false; if ( ! $consumer_key && defined( 'WP_ROCKET_KEY' ) ) { $consumer_key = WP_ROCKET_KEY; } $consumer_email = isset( $this->options['consumer_email'] ) ? $this->options['consumer_email'] : false; if ( ! $consumer_email && defined( 'WP_ROCKET_EMAIL' ) ) { $consumer_email = WP_ROCKET_EMAIL; } $request['user-agent'] = sprintf( '%s;WP-Rocket|%s%s|%s|%s|%s|;', $request['user-agent'], $this->plugin_version, '', $consumer_key, $consumer_email, esc_url( home_url() ) ); return $request; } } classes/ServiceProvider/class-options.php 0000644 00000001647 15174677547 0014662 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ServiceProvider; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Admin\Options_Data; /** * Service provider for the WP Rocket options */ class Options extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'options', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the option array in the container * * @return void */ public function register(): void { $this->getContainer()->add( 'options', Options_Data::class ) ->addArgument( $this->getContainer()->get( 'options_api' )->get( 'settings', [] ) ); } } classes/ServiceProvider/class-common-subscribers.php 0000644 00000001715 15174677547 0016777 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ServiceProvider; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Subscriber\Tools\Detect_Missing_Tags_Subscriber; /** * Service provider for WP Rocket features common for admin and front */ class Common_Subscribers extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'detect_missing_tags_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'detect_missing_tags_subscriber', Detect_Missing_Tags_Subscriber::class ); } } classes/subscriber/Tools/class-detect-missing-tags-subscriber.php 0000644 00000013346 15174677547 0021332 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Tools; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Logger\Logger; /** * Detect and report when <html>, wp_footer() and <body> tags are missing. * * @since 3.4.2 * @author Soponar Cristina */ class Detect_Missing_Tags_Subscriber implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @since 3.4.2 * @author Soponar Cristina * * @return array */ public static function get_subscribed_events() { return [ 'admin_notices' => 'rocket_notice_missing_tags', 'rocket_before_maybe_process_buffer' => 'maybe_missing_tags', 'wp_rocket_upgrade' => 'delete_transient_after_upgrade', ]; } /** * Check if there is a missing </html> or </body> tag * * @since 3.4.2 * @author Soponar Cristina * * @param string $html HTML content. */ public function maybe_missing_tags( $html ) { // If there is a redirect the content is empty and can display a false positive notice. if ( strlen( $html ) <= 255 ) { return; } // If the http response is not 200 do not report missing tags. if ( http_response_code() !== 200 ) { return; } // If content type is not HTML do not report missing tags. if ( empty( $_SERVER['content_type'] ) || false === strpos( wp_unslash( $_SERVER['content_type'] ), 'text/html' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return; } // If the content does not contain HTML Doctype, do not report missing tags. if ( false === stripos( $html, '<!DOCTYPE html' ) ) { return; } Logger::info( 'START Detect Missing closing tags ( <html>, </body> or wp_footer() )', [ 'maybe_missing_tags', 'URI' => $this->get_raw_request_uri(), ] ); // Remove all comments before testing tags. If </html> or </body> tags are commented this will identify it as a missing tag. $html = preg_replace( '/<!--([\\s\\S]*?)-->/', '', $html ); $missing_tags = []; if ( false === strpos( $html, '</html>' ) ) { $missing_tags[] = '</html>'; Logger::debug( 'Not found closing </html> tag.', [ 'maybe_missing_tags', 'URI' => $this->get_raw_request_uri(), ] ); } if ( false === strpos( $html, '</body>' ) ) { $missing_tags[] = '</body>'; Logger::debug( 'Not found closing </body> tag.', [ 'maybe_missing_tags', 'URI' => $this->get_raw_request_uri(), ] ); } if ( did_action( 'wp_footer' ) === 0 ) { $missing_tags[] = 'wp_footer()'; Logger::debug( 'wp_footer() function did not run.', [ 'maybe_missing_tags', 'URI' => $this->get_raw_request_uri(), ] ); } if ( ! $missing_tags ) { return; } $transient = get_transient( 'rocket_notice_missing_tags' ); $transient = is_array( $transient ) ? $transient : []; $missing_tags = array_unique( array_merge( $transient, $missing_tags ) ); if ( count( $transient ) === count( $missing_tags ) ) { return; } // Prevent saving the transient if the notice is dismissed. $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( 'rocket_notice_missing_tags', (array) $boxes, true ) ) { return; } set_transient( 'rocket_notice_missing_tags', $missing_tags ); } /** * This notice is displayed if there is a missing required tag or function: </html>, </body> or wp_footer() * * @since 3.4.2 * @author Soponar Cristina */ public function rocket_notice_missing_tags() { $screen = get_current_screen(); if ( ! current_user_can( 'rocket_manage_options' ) || 'settings_page_wprocket' !== $screen->id ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $notice = get_transient( 'rocket_notice_missing_tags' ); if ( empty( $notice ) || ! is_array( $notice ) ) { return; } foreach ( $notice as $i => $tag ) { $notice[ $i ] = '<code>' . esc_html( $tag ) . '</code>'; } $msg = '<b>' . __( 'WP Rocket: ', 'rocket' ) . '</b>'; $msg .= sprintf( /* translators: %1$s = missing tags; */ esc_html( _n( 'Failed to detect the following requirement in your theme: closing %1$s.', 'Failed to detect the following requirements in your theme: closing %1$s.', count( $notice ), 'rocket' ) ), // translators: Documentation exists in EN, FR. wp_sprintf_l( '%l', $notice ) ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/99-pages-not-cached-or-minify-cssjs-not-working/?utm_source=wp_plugin&utm_medium=wp_rocket#theme', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); \rocket_notice_html( [ 'status' => 'info', 'dismissible' => '', 'message' => $msg, 'dismiss_button' => __FUNCTION__, ] ); } /** * Get the request URI. * * @since 3.4.2 * @author Soponar Cristina * * @return string */ public function get_raw_request_uri() { if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { return ''; } if ( '' === $_SERVER['REQUEST_URI'] ) { return ''; } return '/' . esc_html( ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } /** * Deletes the transient storing the missing tags when updating the plugin * * @since 3.4.2.2 * @author Soponar Cristina */ public function delete_transient_after_upgrade() { delete_transient( 'rocket_notice_missing_tags' ); } } classes/subscriber/third-party/plugins/class-ngg-subscriber.php 0000644 00000002054 15174677547 0021054 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Third_Party\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; defined( 'ABSPATH' ) || exit; /** * Class that handles events related to Next Gen Gallery. * * @since 3.3.1 * @author Remy Perona */ class NGG_Subscriber implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3.1 * @author Remy Perona * * @return array */ public static function get_subscribed_events() { if ( ! class_exists( 'C_NextGEN_Bootstrap' ) ) { return; } return [ 'run_ngg_resource_manager' => 'deactivate_resource_manager', ]; } /** * Deactivate NGG Resource Manager to prevent conflict with WP Rocket output buffering * * @since 3.3.1 * @author Remy Perona * * @param bool $valid_request Indicates if the current request is valid for the NGG resource manager. * * @return bool */ public function deactivate_resource_manager( $valid_request ) { if ( is_admin() ) { return $valid_request; } return false; } } classes/subscriber/third-party/plugins/class-mobile-subscriber.php 0000644 00000027300 15174677547 0021551 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Third_Party\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; defined( 'ABSPATH' ) || exit; /** * Class that handles events related to plugins that add mobile themes. * * @since 3.2 * @author Grégory Viguier */ class Mobile_Subscriber implements Subscriber_Interface { /** * Options to activate when a mobile plugin is active. * * @since 3.2 * @access protected * @author Grégory Viguier * * @var array */ protected static $options = [ 'cache_mobile' => 1, 'do_caching_mobile_files' => 1, ]; /** * Cache the value of self::is_mobile_plugin_active(). * * @since 3.2 * @access protected * @author Grégory Viguier * * @var array An array of arrays of booleans. * First level of keys corresponds to the network ID. Second level of keys corresponds to the blog ID. */ protected static $is_mobile_active = []; /** * Returns an array of events that this subscriber wants to listen to. * * @since 3.2 * @access public * @author Grégory Viguier * * @return array */ public static function get_subscribed_events() { // In case a mobile plugin has already been activated. $do = []; $undo = []; $plugin_events = []; if ( ! function_exists( '\is_plugin_active' ) ) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; } foreach ( static::get_mobile_plugins() as $plugin => $plugin_data ) { if ( \did_action( 'activate_' . $plugin ) && ! isset( $plugin_data['is_active_callback'] ) ) { $do[] = $plugin; } if ( \did_action( 'activate_' . $plugin ) && isset( $plugin_data['is_active_callback'] ) && call_user_func( $plugin_data['is_active_callback'] ) ) { $do[] = $plugin; } if ( \did_action( 'deactivate_' . $plugin ) ) { $undo[] = $plugin; } if ( \is_plugin_active( $plugin ) ) { if ( isset( $plugin_data['activation_hook'] ) ) { $plugin_events[ $plugin_data['activation_hook'] ] = 'maybe_update_mobile_cache_activation_plugin_hook'; } if ( isset( $plugin_data['deactivation_hook'] ) ) { $plugin_events[ $plugin_data['deactivation_hook'] ] = 'maybe_update_mobile_cache_activation_plugin_hook'; } } } if ( array_diff( $do, $undo ) || array_diff( $undo, $do ) ) { static::update_mobile_cache_activation(); } // Register events. $events = [ // Plugin activation/deactivation. 'add_option_active_plugins' => [ 'add_option_callback', 10, 2 ], 'update_option_active_plugins' => [ 'update_option_callback', 10, 2 ], 'delete_option_active_plugins' => 'delete_option_callback', 'add_site_option_active_sitewide_plugins' => [ 'add_site_option_callback', 10, 3 ], 'update_site_option_active_sitewide_plugins' => [ 'update_site_option_callback', 10, 4 ], 'delete_site_option_active_sitewide_plugins' => [ 'delete_site_option_callback', 10, 2 ], // WPR settings (`get_option()`). 'option_' . WP_ROCKET_SLUG => 'mobile_options_filter', ]; foreach ( static::$options as $option => $value ) { // WPR settings (`get_rocket_option()`). $events[ 'pre_get_rocket_option_' . $option ] = 'is_mobile_plugin_active_callback'; } $events = array_merge( $events, $plugin_events ); return $events; } /** ----------------------------------------------------------------------------------------- */ /** HOOK CALLBACKS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Callback triggered after the option `active_plugins` is created. * This should normally never be triggered. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $option Name of the option to add. * @param mixed $value Value of the option. */ public function add_option_callback( $option, $value ) { $this->maybe_update_mobile_cache_activation( $value, [] ); } /** * Callback triggered after the option `active_plugins` is updated. * * @since 3.2 * @access public * @author Grégory Viguier * * @param mixed $old_value The old option value. * @param mixed $value Value of the option. */ public function update_option_callback( $old_value, $value ) { $this->maybe_update_mobile_cache_activation( $value, $old_value ); } /** * Callback triggered after the option `active_plugins` is deleted. * Very low probability to be triggered. * * @since 3.2 * @access public * @author Grégory Viguier */ public function delete_option_callback() { static::update_mobile_cache_activation(); } /** * Callback triggered after the option `active_sitewide_plugins` is created. * This should normally never be triggered. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $option Name of the option to add. * @param mixed $value Value of the option. * @param int $network_id ID of the network. */ public function add_site_option_callback( $option, $value, $network_id ) { if ( get_current_network_id() === $network_id ) { $this->maybe_update_mobile_cache_activation( $value, [] ); } } /** * Callback triggered after the option `active_sitewide_plugins` is updated. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $option Name of the option to add. * @param mixed $value Value of the option. * @param mixed $old_value The old option value. * @param int $network_id ID of the network. */ public function update_site_option_callback( $option, $value, $old_value, $network_id ) { if ( get_current_network_id() === $network_id ) { $this->maybe_update_mobile_cache_activation( $value, $old_value ); } } /** * Callback triggered after the option `active_sitewide_plugins` is deleted. * Very low probability to be triggered. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $option Name of the option to add. * @param int $network_id ID of the network. */ public function delete_site_option_callback( $option, $network_id ) { if ( get_current_network_id() === $network_id ) { static::update_mobile_cache_activation(); } } /** * Enable mobile caching when a mobile plugin is activated, or revert it back to its previous state when a mobile plugin is deactivated. * * @since 3.2 * @access public * @author Grégory Viguier * * @param mixed $value The new option value. * @param mixed $old_value The old option value. */ public function maybe_update_mobile_cache_activation( $value, $old_value ) { $plugins = static::get_mobile_plugins(); $plugins = array_keys( $plugins ); $value = array_intersect( $plugins, (array) $value ); $old_value = array_intersect( $plugins, (array) $old_value ); if ( $value !== $old_value ) { static::update_mobile_cache_activation(); } } /** * Enables mobile caching when a mobile plugin option is activated, or reverts it back to its previous state when a mobile plugin option is deactivated. * * @since 3.4.2 * @access public * @author Soponar Cristina * * @return void */ public function maybe_update_mobile_cache_activation_plugin_hook() { $is_mobile_plugin_active = static::is_mobile_plugin_active(); static::reset_class_cache(); $is_new_mobile_plugin_active = static::is_mobile_plugin_active(); if ( $is_mobile_plugin_active !== $is_new_mobile_plugin_active ) { static::update_mobile_cache_activation(); } } /** * Forces the values for the mobile options if a mobile plugin is active. * * @since 3.2 * @access public * @author Grégory Viguier * * @param array $values Option values. * @return array */ public function mobile_options_filter( $values ) { if ( static::is_mobile_plugin_active() ) { return array_merge( (array) $values, static::$options ); } return $values; } /** * Forces the value for a mobile option if a mobile plugin is active. * * @since 3.2 * @access public * @author Grégory Viguier * * @param int|null $value Option value. * @return int|null */ public function is_mobile_plugin_active_callback( $value ) { if ( static::is_mobile_plugin_active() ) { return 1; } return $value; } /** ----------------------------------------------------------------------------------------- */ /** MAIN HELPERS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Update the config file and the advanced cache file. * * @since 3.2 * @access public * @author Grégory Viguier */ public static function update_mobile_cache_activation() { // Reset class cache. static::reset_class_cache(); // Update the config file. rocket_generate_config_file(); // Update the advanced cache file. rocket_generate_advanced_cache_file(); // Flush htaccess file. flush_rocket_htaccess(); } /** * Reset `is_mobile_plugin_active()` cache. * * @since 3.2 * @access public * @author Grégory Viguier */ public static function reset_class_cache() { // Reset class cache. unset( static::$is_mobile_active[ get_current_network_id() ][ get_current_blog_id() ] ); } /** * Get the concerned plugins. * * @since 3.2 * @access public * @author Grégory Viguier * * @return array */ public static function get_mobile_plugins() { return [ 'jetpack/jetpack.php' => [ 'is_active_callback' => function () { if ( ! class_exists( 'Jetpack' ) ) { return false; } return \Jetpack::is_active() && \Jetpack::is_module_active( 'minileven' ); }, 'activation_hook' => 'jetpack_activate_module_minileven', 'deactivation_hook' => 'jetpack_deactivate_module_minileven', ], 'wptouch/wptouch.php' => [], 'wiziapp-create-your-own-native-iphone-app/wiziapp.php' => [], 'wordpress-mobile-pack/wordpress-mobile-pack.php' => [], 'wp-mobilizer/wp-mobilizer.php' => [], 'wp-mobile-edition/wp-mobile-edition.php' => [], 'device-theme-switcher/dts_controller.php' => [], 'wp-mobile-detect/wp-mobile-detect.php' => [], 'easy-social-share-buttons3/easy-social-share-buttons3.php' => [], 'jet-menu/jet-menu.php' => [], 'wpdiscuz/class.WpdiscuzCore.php' => [], ]; } /** * Tell if a mobile plugin is active. * * @since 3.2 * @access public * @author Grégory Viguier * * @return bool True if a mobile plugin in the list is active, false otherwise. */ public static function is_mobile_plugin_active() { $network_id = get_current_network_id(); $blog_id = get_current_blog_id(); if ( isset( static::$is_mobile_active[ $network_id ][ $blog_id ] ) ) { return static::$is_mobile_active[ $network_id ][ $blog_id ]; } if ( ! function_exists( '\is_plugin_active' ) ) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( ! isset( static::$is_mobile_active[ $network_id ] ) ) { static::$is_mobile_active[ $network_id ] = []; } foreach ( static::get_mobile_plugins() as $mobile_plugin => $mobile_plugin_data ) { if ( \is_plugin_active( $mobile_plugin ) && isset( $mobile_plugin_data['is_active_callback'] ) && call_user_func( $mobile_plugin_data['is_active_callback'] ) ) { static::$is_mobile_active[ $network_id ][ $blog_id ] = true; return true; } if ( \is_plugin_active( $mobile_plugin ) && ! isset( $mobile_plugin_data['is_active_callback'] ) ) { static::$is_mobile_active[ $network_id ][ $blog_id ] = true; return true; } } static::$is_mobile_active[ $network_id ][ $blog_id ] = false; return false; } } classes/subscriber/third-party/plugins/class-syntaxhighlighter-subscriber.php 0000644 00000003333 15174677547 0024047 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Third_Party\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Compatibility class for SyntaxHighlighter plugin * * @since 3.3.1 * @author Remy Perona */ class SyntaxHighlighter_Subscriber implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3.1 * @author Remy Perona * * @return array */ public static function get_subscribed_events() { if ( ! class_exists( 'SyntaxHighlighter' ) ) { return []; } return [ 'rocket_exclude_defer_js' => 'exclude_defer_js_syntaxhighlighter_scripts', 'rocket_exclude_js' => 'exclude_minify_js_syntaxhighlighter_scripts', ]; } /** * Adds SyntaxHighlighter scripts to defer JS exclusion * * @since 3.3.1 * @author Remy Perona * * @param array $excluded_scripts Array of scripts to exclude. * @return array */ public function exclude_defer_js_syntaxhighlighter_scripts( $excluded_scripts ) { return array_merge( $excluded_scripts, [ 'syntaxhighlighter/syntaxhighlighter3/scripts/(.*).js', 'syntaxhighlighter/syntaxhighlighter2/scripts/(.*).js', ] ); } /** * Adds SyntaxHighlighter scripts to minify/combine JS exclusion * * @since 3.3.1 * @author Remy Perona * * @param array $excluded_scripts Array of scripts to exclude. * @return array */ public function exclude_minify_js_syntaxhighlighter_scripts( $excluded_scripts ) { return array_merge( $excluded_scripts, [ rocket_clean_exclude_file( plugins_url( 'syntaxhighlighter/syntaxhighlighter3/scripts/(.*).js' ) ), rocket_clean_exclude_file( plugins_url( 'syntaxhighlighter/syntaxhighlighter2/scripts/(.*).js' ) ), ] ); } } classes/subscriber/third-party/plugins/Images/Webp/class-ewww-subscriber.php 0000644 00000023767 15174677547 0023412 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the WebP support with EWWW. * * @since 3.4 * @author Grégory Viguier */ class EWWW_Subscriber implements Webp_Interface, Subscriber_Interface { use Webp_Common; /** * Options_Data instance. * * @var Options_Data * @access private * @author Remy Perona */ private $options; /** * EWWW basename. * * @var string * @access private * @author Grégory Viguier */ private $plugin_basename; /** * Constructor. * * @since 3.4 * @access public * @author Grégory Viguier * * @param Options_Data $options Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'rocket_webp_plugins' => 'register', 'wp_rocket_loaded' => 'load_hooks', ]; } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Launch filters. * * @since 3.4 * @access public * @author Grégory Viguier */ public function load_hooks() { if ( ! $this->options->get( 'cache_webp' ) ) { return; } /** * Every time EWWW is (de)activated, we must "sync" our webp cache option. */ if ( did_action( 'activate_' . $this->get_basename() ) ) { $this->plugin_activation(); } if ( did_action( 'deactivate_' . $this->get_basename() ) ) { $this->plugin_deactivation(); } add_action( 'activate_' . $this->get_basename(), [ $this, 'plugin_activation' ], 20 ); add_action( 'deactivate_' . $this->get_basename(), [ $this, 'plugin_deactivation' ], 20 ); if ( ! function_exists( 'ewww_image_optimizer_get_option' ) ) { return; } /** * Since Rocket already updates the config file after updating its options, there is no need to do it again if the CDN or zone options change. * Sadly, we can’t monitor EWWW options accurately to update our config file. */ add_filter( 'rocket_cdn_cnames', [ $this, 'maybe_remove_images_cnames' ], 1000, 2 ); add_filter( 'rocket_allow_cdn_images', [ $this, 'maybe_remove_images_from_cdn_dropdown' ] ); $option_names = [ 'ewww_image_optimizer_exactdn', 'ewww_image_optimizer_webp_for_cdn', ]; foreach ( $option_names as $option_name ) { if ( $this->is_active_for_network() ) { add_filter( 'add_site_option_' . $option_name, [ $this, 'trigger_webp_change' ] ); add_filter( 'update_site_option_' . $option_name, [ $this, 'trigger_webp_change' ] ); add_filter( 'delete_site_option_' . $option_name, [ $this, 'trigger_webp_change' ] ); } else { add_filter( 'add_option_' . $option_name, [ $this, 'trigger_webp_change' ] ); add_filter( 'update_option_' . $option_name, [ $this, 'trigger_webp_change' ] ); add_filter( 'delete_option_' . $option_name, [ $this, 'trigger_webp_change' ] ); } } } /** * Remove CDN hosts for images if EWWW uses ExactDN. * * @since 3.4 * @access public * @author Grégory Viguier * * @param array $hosts List of CDN URLs. * @param array $zones List of zones. Default is [ 'all' ]. * @return array */ public function maybe_remove_images_cnames( $hosts, $zones ) { if ( ! $hosts ) { return $hosts; } if ( ! ewww_image_optimizer_get_option( 'ewww_image_optimizer_exactdn' ) ) { return $hosts; } // EWWW uses ExactDN: WPR CDN should be disabled for images. if ( ! in_array( 'images', $zones, true ) ) { // Not asking for images. return $hosts; } if ( ! array_diff( $zones, [ 'all', 'images' ] ) ) { // This is clearly for images: return an empty list of hosts. return []; } // We also want other things, like js and css: let's only remove the hosts for 'images'. $cdn_urls = $this->options->get( 'cdn_cnames', [] ); if ( ! $cdn_urls ) { return $hosts; } // Separate image hosts from the other ones. $image_hosts = []; $other_hosts = []; $cdn_zones = $this->options->get( 'cdn_zone', [] ); foreach ( $cdn_urls as $k => $urls ) { if ( ! in_array( $cdn_zones[ $k ], $zones, true ) ) { continue; } $urls = explode( ',', $urls ); $urls = array_map( 'trim', $urls ); if ( 'images' === $cdn_zones[ $k ] ) { foreach ( $urls as $url ) { $image_hosts[] = $url; } } else { foreach ( $urls as $url ) { $other_hosts[] = $url; } } } // Make sure the image hosts are not also used for other things (duplicate). $image_hosts = array_diff( $image_hosts, $other_hosts ); // Then remove the remaining from the final list. return array_diff( $hosts, $image_hosts ); } /** * Maybe remove the images option from the CDN dropdown. * * @since 3.4 * @access public * @author Grégory Viguier * * @param bool $allow true to add the option, false otherwise. * @return bool */ public function maybe_remove_images_from_cdn_dropdown( $allow ) { if ( ! $allow ) { return $allow; } if ( ! ewww_image_optimizer_get_option( 'ewww_image_optimizer_exactdn' ) ) { return $allow; } // EWWW uses ExactDN: WPR CDN should be disabled for images. return false; } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC TOOLS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the plugin name. * * @since 3.4 * @access public * @author Grégory Viguier * * @return string */ public function get_name() { return 'EWWW'; } /** * Get the plugin identifier. * * @since 3.4 * @access public * @author Grégory Viguier * * @return string */ public function get_id() { return 'ewww'; } /** * Tell if the plugin converts images to webp. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_converting_to_webp() { if ( ! function_exists( 'ewww_image_optimizer_get_option' ) ) { // No EWWW, no webp. return false; } return (bool) ewww_image_optimizer_get_option( 'ewww_image_optimizer_webp' ); } /** * Tell if the plugin serves webp images on frontend. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_serving_webp() { if ( ! function_exists( 'ewww_image_optimizer_get_option' ) ) { // No EWWW, no webp. return false; } if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_exactdn' ) ) { // EWWW uses ExactDN (WPR CDN should be disabled for images). return true; } if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_webp_for_cdn' ) ) { // EWWW uses JS to rewrite file extensions. return true; } // Decide if rewrite rules are used. if ( ! function_exists( 'ewww_image_optimizer_webp_rewrite_verify' ) ) { // Uh? return false; } if ( ! function_exists( 'get_home_path' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! function_exists( 'extract_from_markers' ) ) { require_once ABSPATH . 'wp-admin/includes/misc.php'; } /** * This function returns null if rules are present and valid. Otherwise it returns rules to be inserted. * Note: this also returns null if WP Fastest Cache rules for webp are found in the file. * * @see ewww_image_optimizer_wpfc_webp_enabled() */ $use_rewrite_rules = ! ewww_image_optimizer_webp_rewrite_verify(); /** * Filter whether EWW is using rewrite rules for webp. * * @since 3.4 * @author Grégory Viguier * * @param bool $use_rewrite_rules True when EWWW uses rewrite rules. False otherwise. */ return (bool) apply_filters( 'rocket_webp_ewww_use_rewrite_rules', $use_rewrite_rules ); } /** * Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_serving_webp_compatible_with_cdn() { if ( ! function_exists( 'ewww_image_optimizer_get_option' ) ) { // No EWWW, no webp. return false; } if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_exactdn' ) ) { // EWWW uses ExactDN. return true; } if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_webp_for_cdn' ) ) { // EWWW uses JS to rewrite file extensions. return true; } // At this point, the plugin is using rewrite rules or nothing. return false; } /** * Get the plugin basename. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function get_basename() { if ( empty( $this->plugin_basename ) ) { $this->plugin_basename = rocket_has_constant( 'EWWW_IMAGE_OPTIMIZER_PLUGIN_FILE' ) ? plugin_basename( rocket_get_constant( 'EWWW_IMAGE_OPTIMIZER_PLUGIN_FILE' ) ) : 'ewww-image-optimizer/ewww-image-optimizer.php'; } return $this->plugin_basename; } /** ----------------------------------------------------------------------------------------- */ /** PRIVATE TOOLS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if EWWW is active for network. * * @since 3.4 * @access private * @author Grégory Viguier * * @return bool */ private function is_active_for_network() { static $is; if ( isset( $is ) ) { return $is; } if ( ! is_multisite() ) { $is = false; return $is; } if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $is = is_plugin_active_for_network( $this->get_basename() ) && ! get_site_option( 'ewww_image_optimizer_allow_multisite_override' ); return $is; } } classes/subscriber/third-party/plugins/Images/Webp/class-imagify-subscriber.php 0000644 00000031646 15174677547 0024041 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the WebP support with Imagify. * * @since 3.4 * @author Grégory Viguier */ class Imagify_Subscriber implements Webp_Interface, Subscriber_Interface { use Webp_Common; /** * Options_Data instance. * * @var Options_Data * @access private * @author Remy Perona */ private $options; /** * Imagify basename. * * @var string * @access private * @author Grégory Viguier */ private $plugin_basename; /** * Imagify’s "serve webp" option name. * * @var string * @access private * @author Grégory Viguier */ private $plugin_option_name_to_serve_webp; /** * Temporarily store the result of $this->is_serving_webp(). * * @var bool * @access private * @author Grégory Viguier */ private $tmp_is_serving_webp; /** * Constructor. * * @since 3.4 * @access public * @author Grégory Viguier * * @param Options_Data $options Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Returns an array of events that this subscriber wants to listen to. * * @since 3.4 * @access public * @author Grégory Viguier * * @return array */ public static function get_subscribed_events() { return [ 'rocket_webp_plugins' => 'register', 'wp_rocket_loaded' => 'load_hooks', ]; } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Launch filters. * * @since 3.4 * @access public * @author Grégory Viguier */ public function load_hooks() { if ( ! $this->options->get( 'cache_webp' ) ) { return; } /** * Every time Imagify is (de)activated, we must "sync" our webp cache option. */ add_action( 'imagify_activation', [ $this, 'plugin_activation' ], 20 ); add_action( 'imagify_deactivation', [ $this, 'plugin_deactivation' ], 20 ); if ( ! rocket_has_constant( 'IMAGIFY_VERSION' ) ) { return; } /** * Since Rocket already updates the config file after updating its options, there is no need to do it again if the CDN or zone options change. */ /** * Every time Imagify’s option changes, we must "sync" our webp cache option. */ $option_name = $this->get_option_name_to_serve_webp(); if ( $this->is_active_for_network() ) { add_filter( 'add_site_option_' . $option_name, [ $this, 'sync_on_network_option_add' ], 10, 3 ); add_filter( 'update_site_option_' . $option_name, [ $this, 'sync_on_network_option_update' ], 10, 4 ); add_filter( 'pre_delete_site_option_' . $option_name, [ $this, 'store_option_value_before_network_delete' ], 10, 2 ); add_filter( 'delete_site_option_' . $option_name, [ $this, 'sync_on_network_option_delete' ], 10, 2 ); return; } add_filter( 'add_option_' . $option_name, [ $this, 'sync_on_option_add' ], 10, 2 ); add_filter( 'update_option_' . $option_name, [ $this, 'sync_on_option_update' ], 10, 2 ); add_filter( 'delete_option', [ $this, 'store_option_value_before_delete' ] ); add_filter( 'delete_option_' . $option_name, [ $this, 'sync_on_option_delete' ] ); } /** * Maybe deactivate webp cache after Imagify network option has been successfully added. * * @since 3.4 * @access public * @author Grégory Viguier * * @param string $option Name of the network option. * @param mixed $value Value of the network option. * @param int $network_id ID of the network. */ public function sync_on_network_option_add( $option, $value, $network_id ) { if ( get_current_network_id() === $network_id && ! empty( $value['display_webp'] ) ) { $this->trigger_webp_change(); } } /** * Maybe activate or deactivate webp cache after Imagify network option has been modified. * * @since 3.4 * @access public * @author Grégory Viguier * * @param string $option Name of the network option. * @param mixed $value Current value of the network option. * @param mixed $old_value Old value of the network option. * @param int $network_id ID of the network. */ public function sync_on_network_option_update( $option, $value, $old_value, $network_id ) { if ( get_current_network_id() === $network_id ) { $this->sync_on_option_update( $old_value, $value ); } } /** * Store the Imagify network option value before it is deleted. * * @since 3.4 * @access public * @author Grégory Viguier * * @param string $option Option name. * @param int $network_id ID of the network. */ public function store_option_value_before_network_delete( $option, $network_id ) { if ( get_current_network_id() === $network_id ) { $this->tmp_is_serving_webp = $this->is_serving_webp(); } } /** * Maybe activate webp cache after Imagify network option has been deleted. * * @since 3.4 * @access public * @author Grégory Viguier * * @param string $option Name of the network option. * @param int $network_id ID of the network. */ public function sync_on_network_option_delete( $option, $network_id ) { if ( get_current_network_id() === $network_id && false !== $this->tmp_is_serving_webp ) { $this->trigger_webp_change(); } } /** * Maybe deactivate webp cache after Imagify option has been successfully added. * * @since 3.4 * @access public * @author Grégory Viguier * * @param string $option Name of the option to add. * @param mixed $value Value of the option. */ public function sync_on_option_add( $option, $value ) { if ( ! empty( $value['display_webp'] ) ) { $this->trigger_webp_change(); } } /** * This function is used to synchronize the option update for Imagify plugin. * It checks for the existence of certain keys in the old and new values of the options. * Depending on the version of Imagify, the keys may differ. * For Imagify version 2.2 and above, the keys are 'display_nextgen' and 'display_nextgen_method'. * For Imagify version 2.1 and below, the keys are 'display_webp' and 'display_webp_method'. * The function then checks if the old and new values of the display and method options have changed. * If they have, it triggers a webp change. * * @param mixed $old_value The old option value. * @param mixed $value The new option value. * @since 3.4 * @access public * @author Grégory Viguier */ public function sync_on_option_update( $old_value, $value ) { // Determine the key for the display option in the old and new values. $old_display_key = array_key_exists( 'display_nextgen', $old_value ) ? 'display_nextgen' : 'display_webp'; $display_key = array_key_exists( 'display_nextgen', $value ) ? 'display_nextgen' : 'display_webp'; // Get the old and new values of the display option. $old_display = ! empty( $old_value[ $old_display_key ] ); $display = ! empty( $value[ $display_key ] ); // Determine the key for the method option in the old and new values. $old_method_key = array_key_exists( 'display_nextgen_method', $old_value ) ? 'display_nextgen_method' : 'display_webp_method'; $method_key = array_key_exists( 'display_nextgen_method', $value ) ? 'display_nextgen_method' : 'display_webp_method'; // If the old value of the method option is not set, set it to the corresponding display option. if ( ! isset( $old_value[ $old_method_key ] ) ) { $old_value[ $old_method_key ] = $old_value[ $old_display_key . '_method' ] ?? ''; } // If the new value of the method option is not set, set it to the corresponding display option. if ( ! isset( $value[ $method_key ] ) ) { $value[ $method_key ] = $value[ $display_key . '_method' ] ?? ''; } // If the old and new values of the display or method options have changed, trigger a webp change. if ( $old_display !== $display || $old_value[ $old_method_key ] !== $value[ $method_key ] ) { $this->trigger_webp_change(); } } /** * Store the Imagify option value before it is deleted. * * @since 3.4 * @access public * @author Grégory Viguier * * @param string $option Name of the option to delete. */ public function store_option_value_before_delete( $option ) { if ( $this->get_option_name_to_serve_webp() === $option ) { $this->tmp_is_serving_webp = $this->is_serving_webp(); } } /** * Maybe activate webp cache after Imagify option has been deleted. * * @since 3.4 * @access public * @author Grégory Viguier * * @param string $option Name of the deleted option. */ public function sync_on_option_delete( $option ) { if ( false !== $this->tmp_is_serving_webp ) { $this->trigger_webp_change(); } } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC TOOLS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the plugin name. * * @since 3.4 * @access public * @author Grégory Viguier * * @return string */ public function get_name() { return 'Imagify'; } /** * Get the plugin identifier. * * @since 3.4 * @access public * @author Grégory Viguier * * @return string */ public function get_id() { return 'imagify'; } /** * Tell if the plugin converts images to webp. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_converting_to_webp() { if ( ! function_exists( 'get_imagify_option' ) ) { // No Imagify, no webp. return false; } return ( defined( 'IMAGIFY_VERSION' ) && version_compare( IMAGIFY_VERSION, '2.2', '>=' ) ) || get_imagify_option( 'convert_to_webp' ); } /** * Tell if the plugin serves webp images on frontend. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_serving_webp() { if ( ! function_exists( 'get_imagify_option' ) ) { // No Imagify, no webp. return false; } return get_imagify_option( 'display_webp' ) || get_imagify_option( 'display_nextgen' ); } /** * Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_serving_webp_compatible_with_cdn() { if ( ! $this->is_serving_webp() ) { return false; } return 'rewrite' !== get_imagify_option( 'display_webp_method' ) || 'rewrite' !== get_imagify_option( 'display_nextgen_method' ); } /** * Get the plugin basename. * * @since 3.4 * @access public * @author Grégory Viguier * * @return string */ public function get_basename(): string { if ( empty( $this->plugin_basename ) ) { $this->plugin_basename = rocket_has_constant( 'IMAGIFY_FILE' ) ? plugin_basename( rocket_get_constant( 'IMAGIFY_FILE' ) ) : 'imagify/imagify.php'; } return $this->plugin_basename; } /** ----------------------------------------------------------------------------------------- */ /** PRIVATE TOOLS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the name of the Imagify’s "serve webp" option. * * @since 3.4 * @access private * @author Grégory Viguier * * @return string */ private function get_option_name_to_serve_webp() { if ( ! empty( $this->plugin_option_name_to_serve_webp ) ) { return $this->plugin_option_name_to_serve_webp; } $default = 'imagify_settings'; if ( ! class_exists( '\Imagify_Options' ) || ! method_exists( '\Imagify_Options', 'get_instance' ) ) { $this->plugin_option_name_to_serve_webp = $default; return $this->plugin_option_name_to_serve_webp; } $instance = \Imagify_Options::get_instance(); if ( ! method_exists( $instance, 'get_option_name' ) ) { $this->plugin_option_name_to_serve_webp = $default; return $this->plugin_option_name_to_serve_webp; } $this->plugin_option_name_to_serve_webp = $instance->get_option_name(); return $this->plugin_option_name_to_serve_webp; } /** * Tell if Imagify is active for network. * * @since 3.4 * @access private * @author Grégory Viguier * * @return bool */ private function is_active_for_network() { static $is; if ( isset( $is ) ) { return $is; } if ( function_exists( 'imagify_is_active_for_network' ) ) { $is = imagify_is_active_for_network(); return $is; } if ( ! is_multisite() ) { $is = false; return $is; } if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $is = is_plugin_active_for_network( $this->get_basename() ); return $is; } } classes/subscriber/third-party/plugins/Images/Webp/class-optimus-subscriber.php 0000644 00000006316 15174677547 0024110 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the WebP support with Optimus. * * @since 3.4 * @author Grégory Viguier */ class Optimus_Subscriber implements Webp_Interface, Subscriber_Interface { /** * Optimus basename. * * @var string * @access private * @author Grégory Viguier */ private $plugin_basename; /** * Returns an array of events that this subscriber wants to listen to. * * @since 3.4 * @access public * @author Grégory Viguier * * @return array */ public static function get_subscribed_events() { if ( ! defined( 'OPTIMUS_FILE' ) ) { return []; } return [ 'rocket_webp_plugins' => 'register', ]; } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Register the plugin. * * @since 3.4 * @access public * @author Grégory Viguier * * @param array $webp_plugins An array of Webp_Interface objects. * @return array */ public function register( $webp_plugins ) { $webp_plugins[] = $this; return $webp_plugins; } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC TOOLS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the plugin name. * * @since 3.4 * @access public * @author Grégory Viguier * * @return string */ public function get_name() { return 'Optimus'; } /** * Get the plugin identifier. * * @since 3.4 * @access public * @author Grégory Viguier * * @return string */ public function get_id() { return 'optimus'; } /** * Tell if the plugin converts images to webp. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_converting_to_webp() { if ( class_exists( '\Optimus' ) && method_exists( '\Optimus', 'get_options' ) ) { $options = \Optimus::get_options(); } else { $options = get_option( 'optimus' ); } return ! empty( $options['webp_convert'] ); } /** * Tell if the plugin serves webp images on frontend. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_serving_webp() { return false; } /** * Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_serving_webp_compatible_with_cdn() { return false; } /** * Get the plugin basename. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function get_basename() { if ( empty( $this->plugin_basename ) ) { $this->plugin_basename = defined( 'OPTIMUS_FILE' ) ? plugin_basename( OPTIMUS_FILE ) : 'optimus/optimus.php'; } return $this->plugin_basename; } } classes/subscriber/third-party/plugins/Images/Webp/trait-webp-common.php 0000644 00000002740 15174677547 0022505 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp; /** * Trait for webp subscribers, focussed on plugins that serve webp images on frontend. * * @since 3.4 * @author Grégory Viguier */ trait Webp_Common { /** * Register the plugin. * * @since 3.4 * @access public * @author Grégory Viguier * * @param array $webp_plugins An array of Webp_Interface objects. * @return array */ public function register( $webp_plugins ) { $webp_plugins[] = $this; return $webp_plugins; } /** * On plugin activation, deactivate Rocket webp cache if the plugin is serving webp. * * @since 3.4 * @access public * @author Grégory Viguier */ public function plugin_activation() { if ( $this->is_serving_webp() ) { $this->trigger_webp_change(); } } /** * On plugin deactivation, activate Rocket webp cache if the plugin is serving webp. * * @since 3.4 * @access public * @author Grégory Viguier */ public function plugin_deactivation() { if ( $this->is_serving_webp() ) { $this->trigger_webp_change(); } } /** * Trigger an action when the webp feature is enabled/disabled in a third party plugin. * * @since 3.4 * @access public * @author Grégory Viguier */ public function trigger_webp_change() { /** * Trigger an action when the webp feature is enabled/disabled in a third party plugin. * * @since 3.4 * @author Grégory Viguier */ do_action( 'rocket_third_party_webp_change' ); } } classes/subscriber/third-party/plugins/Images/Webp/class-shortpixel-subscriber.php 0000644 00000015572 15174677547 0024615 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the WebP support with ShortPixel. * * @since 3.4 * @author Grégory Viguier */ class ShortPixel_Subscriber implements Webp_Interface, Subscriber_Interface { use Webp_Common; /** * Options_Data instance. * * @var Options_Data * @access private * @author Remy Perona */ private $options; /** * ShortPixel basename. * * @var string * @access private * @author Grégory Viguier */ private $plugin_basename; /** * ShortPixel’s "serve webp" option name. * * @var string * @access private * @author Grégory Viguier */ private $plugin_option_name_to_serve_webp = 'wp-short-pixel-create-webp-markup'; /** * Temporarily store the result of $this->is_serving_webp(). * * @var bool * @access private * @author Grégory Viguier */ private $tmp_is_serving_webp; /** * Constructor. * * @since 3.4 * @access public * @author Grégory Viguier * * @param Options_Data $options Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Returns an array of events that this subscriber wants to listen to. * * @since 3.4 * @access public * @author Grégory Viguier * * @return array */ public static function get_subscribed_events() { return [ 'rocket_webp_plugins' => 'register', 'wp_rocket_loaded' => 'load_hooks', ]; } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Launch filters. * * @since 3.4 * @access public * @author Grégory Viguier */ public function load_hooks() { if ( ! $this->options->get( 'cache_webp' ) ) { return; } /** * Every time ShortPixel is (de)activated, we must "sync" our webp cache option. */ if ( did_action( 'activate_' . $this->get_basename() ) ) { $this->plugin_activation(); } if ( did_action( 'deactivate_' . $this->get_basename() ) ) { $this->plugin_deactivation(); } add_action( 'activate_' . $this->get_basename(), [ $this, 'plugin_activation' ], 20 ); add_action( 'deactivate_' . $this->get_basename(), [ $this, 'plugin_deactivation' ], 20 ); if ( ! defined( 'SHORTPIXEL_IMAGE_OPTIMISER_VERSION' ) ) { return; } /** * Since Rocket already updates the config file after updating its options, there is no need to do it again if the CDN or zone options change. */ /** * Every time ShortPixel’s option changes, we must "sync" our webp cache option. */ $option_name = $this->plugin_option_name_to_serve_webp; add_filter( 'add_option_' . $option_name, [ $this, 'sync_on_option_add' ], 10, 2 ); add_filter( 'update_option_' . $option_name, [ $this, 'sync_on_option_update' ], 10, 2 ); add_filter( 'delete_option', [ $this, 'store_option_value_before_delete' ] ); add_filter( 'delete_option_' . $option_name, [ $this, 'sync_on_option_delete' ] ); } /** * Maybe deactivate webp cache after ShortPixel option has been successfully added. * * @since 3.4 * @access public * @author Grégory Viguier * * @param string $option Name of the option to add. * @param mixed $value Value of the option. */ public function sync_on_option_add( $option, $value ) { if ( $value ) { $this->trigger_webp_change(); } } /** * Maybe activate or deactivate webp cache after ShortPixel option has been modified. * * @since 3.4 * @access public * @author Grégory Viguier * * @param mixed $old_value The old option value. * @param mixed $value The new option value. */ public function sync_on_option_update( $old_value, $value ) { /** * 0 = Don’t serve webp. * 1 = <picture> + buffer * 2 = <picture> + hooks * 3 = .htaccess */ $old_value = $old_value > 0; $value = $value > 0; if ( $old_value !== $value ) { $this->trigger_webp_change(); } } /** * Store the ShortPixel option value before it is deleted. * * @since 3.4 * @access public * @author Grégory Viguier * * @param string $option Name of the option to delete. */ public function store_option_value_before_delete( $option ) { if ( $this->plugin_option_name_to_serve_webp === $option ) { $this->tmp_is_serving_webp = $this->is_serving_webp(); } } /** * Maybe activate webp cache after ShortPixel option has been deleted. * * @since 3.4 * @access public * @author Grégory Viguier */ public function sync_on_option_delete() { if ( false !== $this->tmp_is_serving_webp ) { $this->trigger_webp_change(); } } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC TOOLS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the plugin name. * * @since 3.4 * @access public * @author Grégory Viguier * * @return string */ public function get_name() { return 'ShortPixel'; } /** * Get the plugin identifier. * * @since 3.4 * @access public * @author Grégory Viguier * * @return string */ public function get_id() { return 'shortpixel'; } /** * Tell if the plugin converts images to webp. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_converting_to_webp() { return (bool) get_option( 'wp-short-create-webp' ); } /** * Tell if the plugin serves webp images on frontend. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_serving_webp() { return (bool) get_option( $this->plugin_option_name_to_serve_webp ); } /** * Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function is_serving_webp_compatible_with_cdn() { $display = (int) get_option( $this->plugin_option_name_to_serve_webp ); if ( ! $display ) { // The option is not enabled, no webp. return false; } if ( 3 === $display ) { // The option is set to "rewrite rules". return false; } return true; } /** * Get the plugin basename. * * @since 3.4 * @access public * @author Grégory Viguier * * @return bool */ public function get_basename() { if ( empty( $this->plugin_basename ) ) { $this->plugin_basename = defined( 'SHORTPIXEL_PLUGIN_FILE' ) ? plugin_basename( SHORTPIXEL_PLUGIN_FILE ) : 'shortpixel-image-optimiser/wp-shortpixel.php'; } return $this->plugin_basename; } } classes/subscriber/third-party/plugins/Images/Webp/webp-interface.php 0000644 00000001723 15174677547 0022034 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp; /** * Interface to use for webp subscribers. * * @since 3.4 */ interface Webp_Interface { /** * Get the plugin name. * * @since 3.4 * * @return string */ public function get_name(); /** * Get the plugin identifier. * * @since 3.4 * * @return string */ public function get_id(); /** * Tell if the plugin converts images to webp. * * @since 3.4 * * @return bool */ public function is_converting_to_webp(); /** * Tell if the plugin serves webp images on frontend. * * @since 3.4 * * @return bool */ public function is_serving_webp(); /** * Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend. * * @since 3.4 * * @return bool */ public function is_serving_webp_compatible_with_cdn(); /** * Get the plugin basename. * * @since 3.4 * * @return string */ public function get_basename(); } classes/class-abstract-render.php 0000644 00000007367 15174677547 0013141 0 ustar 00 <?php namespace WP_Rocket; use WP_Rocket\Interfaces\Render_Interface; /** * Handle rendering of HTML content created by WP Rocket. * * @since 3.0 * @author Remy Perona */ abstract class Abstract_Render implements Render_Interface { /** * Path to the templates * * @since 3.0 * @author Remy Perona * * @var string */ private $template_path; /** * Constructor * * @since 3.0 * @author Remy Perona * * @param string $template_path Path to the templates. */ public function __construct( $template_path ) { $this->template_path = $template_path; } /** * Renders the given template if it's readable. * * @since 3.0 * @author Remy Perona * * @param string $template Template slug. * @param array $data Data to pass to the template. */ public function generate( $template, $data = [] ) { $template_path = $this->get_template_path( $template ); if ( ! rocket_direct_filesystem()->is_readable( $template_path ) ) { return; } ob_start(); include $template_path; return trim( ob_get_clean() ); } /** * Returns the path a specific template. * * @since 3.0 * @author Remy Perona * * @param string $path Relative path to the template. * @return string */ private function get_template_path( $path ) { return $this->template_path . '/' . $path . '.php'; } /** * Displays the button template. * * @since 3.0 * @author Remy Perona * * @param string $type Type of button (can be button or link). * @param string $action Action to be performed. * @param array $args Optional array of arguments to populate the button attributes. * @return void */ public function render_action_button( $type, $action, $args = [] ) { $default = [ 'label' => '', 'action' => '', 'url' => '', 'parameter' => '', 'attributes' => '', ]; $args = wp_parse_args( $args, $default ); if ( ! empty( $args['attributes'] ) ) { $attributes = ''; foreach ( $args['attributes'] as $key => $value ) { $attributes .= ' ' . sanitize_key( $key ) . '="' . esc_attr( $value ) . '"'; } $args['attributes'] = $attributes; } if ( 'link' !== $type ) { $args['action'] = $action; echo $this->generate( 'buttons/button', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. return; } switch ( $action ) { case 'ask_support': case 'view_account': $args['url'] = rocket_get_external_url( 'ask_support' === $action ? 'support' : 'account', [ 'utm_source' => 'wp_plugin', 'utm_medium' => 'wp_rocket', ] ); break; case 'purge_cache': case 'preload': case 'rocket_purge_cloudflare': case 'rocket_purge_sucuri': case 'rocket_rollback': case 'rocket_export': case 'rocket_generate_critical_css': case 'rocket_purge_rocketcdn': case 'rocket_clean_saas': case 'rocket_clean_performance_hints': $referer = ''; if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { $referer = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ); $referer = '&_wp_http_referer=' . rawurlencode( remove_query_arg( 'fl_builder', $referer ) ); } $url = admin_url( 'admin-post.php?action=' . $action ); $url .= $referer; if ( ! empty( $args['parameters'] ) ) { $url = add_query_arg( $args['parameters'], $url ); } if ( 'purge_cache' === $action ) { $action .= '_all'; } $args['url'] = wp_nonce_url( $url, $action ); break; case 'documentation': $args['url'] = get_rocket_documentation_url(); break; } echo $this->generate( 'buttons/link', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } } classes/interfaces/class-render-interface.php 0000644 00000000610 15174677547 0015401 0 ustar 00 <?php namespace WP_Rocket\Interfaces; /** * Render interface * * @since 3.0 * @author Remy Perona */ interface Render_Interface { /** * Renders the given template if it's readable. * * @since 3.0 * @author Remy Perona * * @param string $template Template slug. * @param array $data Data to pass to the template. */ public function generate( $template, $data ); } classes/Buffer/class-cache.php 0000644 00000052447 15174677547 0012334 0 ustar 00 <?php namespace WP_Rocket\Buffer; /** * Handle page cache. * * @since 3.3 */ class Cache extends Abstract_Buffer { /** * Process identifier used by the logger. * * @var string * @since 3.3 */ protected $process_id = 'caching process'; /** * Tests instance * * @var Tests */ protected $tests; /** * Config instance * * @var Config */ private $config; /** * Path to the directory containing the cache files. * * @var string * @since 3.3 */ private $cache_dir_path; /** * Exclude urls from wp canonical redirect. * * @var array Array of url patterns to exclude from wp canonical redirect. */ private $wp_redirect_exclusions = [ '(.*)wp\-json(/.*|$)', ]; /** * Constructor. * * @since 3.3 * * @param Tests $tests Tests instance. * @param Config $config Config instance. * @param array $args { * An array of arguments. * * @type string $cache_dir_path Path to the directory containing the cache files. * } */ public function __construct( Tests $tests, Config $config, array $args ) { $this->config = $config; $this->cache_dir_path = rtrim( $args['cache_dir_path'], '/' ) . '/'; parent::__construct( $tests ); $this->log( 'CACHING PROCESS STARTED.', [], 'info' ); } /** ----------------------------------------------------------------------------------------- */ /** CACHE =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Serve the cache file if it exists. If not, init the buffer. * * @since 3.3 */ public function maybe_init_process() { if ( ! $this->tests->can_init_process() ) { $this->define_donotoptimize_true(); $this->log_last_test_error(); return; } if ( $this->maybe_allow_wp_redirect() ) { return; } /** * Serve the cache file if it exists. */ $cache_filepath = $this->get_cache_path(); $this->log( 'Looking for cache file.', [ 'path' => $cache_filepath, ] ); $cache_filepath_gzip = $cache_filepath . '_gzip'; $accept_encoding = $this->config->get_server_input( 'HTTP_ACCEPT_ENCODING' ); $accept_gzip = $accept_encoding && false !== strpos( $accept_encoding, 'gzip' ); // Check if cache file exist. if ( $accept_gzip && is_readable( $cache_filepath_gzip ) ) { $this->serve_gzip_cache_file( $cache_filepath_gzip ); } if ( is_readable( $cache_filepath ) ) { $this->serve_cache_file( $cache_filepath ); } // Maybe we're looking for a webp file. $cache_filename = basename( $cache_filepath ); if ( strpos( $cache_filename, '-webp' ) !== false ) { // We're looking for a webp file that doesn't exist: try to locate any `.no-webp` file. $cache_dir_path = rtrim( dirname( $cache_filepath ), '/\\' ) . DIRECTORY_SEPARATOR; if ( file_exists( $cache_dir_path . '.no-webp' ) ) { // We have a `.no-webp` file: try to deliver a non-webp cache file. $cache_filepath = $cache_dir_path . str_replace( '-webp', '', $cache_filename ); $cache_filepath_gzip = $cache_filepath . '_gzip'; $this->log( 'Looking for non-webp cache file.', [ 'path' => $cache_filepath, ] ); // Try to deliver the non-webp version instead. if ( $accept_gzip && is_readable( $cache_filepath_gzip ) ) { $this->serve_gzip_cache_file( $cache_filepath_gzip ); } if ( is_readable( $cache_filepath ) ) { $this->serve_cache_file( $cache_filepath ); } } } /** * No cache file yet: launch caching process. */ $this->log( 'Start buffer.', [ 'path' => $cache_filepath, ] ); ob_start( [ $this, 'maybe_process_buffer' ] ); } /** * Serve a cache file. * * @since 3.3 * * @param string $cache_filepath Path to the cache file. */ private function serve_cache_file( $cache_filepath ) { header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_filepath ) ) . ' GMT' ); $if_modified_since = $this->get_if_modified_since(); // Checking if the client is validating his cache and if it is current. if ( $if_modified_since && ( strtotime( $if_modified_since ) === @filemtime( $cache_filepath ) ) ) { // Client's cache is current, so we just respond '304 Not Modified'. header( $this->config->get_server_input( 'SERVER_PROTOCOL', '' ) . ' 304 Not Modified', true, 304 ); header( 'Expires: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' ); header( 'Cache-Control: no-cache, must-revalidate' ); $this->log( 'Serving `304` cache file.', [ 'path' => $cache_filepath, 'modified' => $if_modified_since, ], 'info' ); exit; } // Serve the cache if file isn't store in the client browser cache. readfile( $cache_filepath ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile $this->log( 'Serving cache file.', [ 'path' => $cache_filepath, 'modified' => $if_modified_since, ], 'info' ); exit; } /** * Serve a gzipped cache file. * * @since 3.3 * * @param string $cache_filepath Path to the gzip cache file. */ private function serve_gzip_cache_file( $cache_filepath ) { header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_filepath ) ) . ' GMT' ); $if_modified_since = $this->get_if_modified_since(); // Checking if the client is validating his cache and if it is current. if ( $if_modified_since && ( strtotime( $if_modified_since ) === @filemtime( $cache_filepath ) ) ) { // Client's cache is current, so we just respond '304 Not Modified'. header( $this->config->get_server_input( 'SERVER_PROTOCOL', '' ) . ' 304 Not Modified', true, 304 ); header( 'Expires: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' ); header( 'Cache-Control: no-cache, must-revalidate' ); $this->log( 'Serving `304` gzip cache file.', [ 'path' => $cache_filepath, 'modified' => $if_modified_since, ], 'info' ); exit; } // Serve the cache if file isn't store in the client browser cache. readgzfile( $cache_filepath ); $this->log( 'Serving gzip cache file.', [ 'path' => $cache_filepath, 'modified' => $if_modified_since, ], 'info' ); exit; } /** * Maybe cache the page content. * * @since 3.3 * * @param string $buffer The buffer content. * @return string The buffered content. */ public function maybe_process_buffer( $buffer ) { if ( ! $this->tests->can_process_buffer( $buffer ) ) { $this->log_last_test_error(); return $buffer; } $footprint = ''; $is_html = $this->is_html( $buffer ); if ( ! static::can_generate_caching_files() ) { // Not allowed to generate cache files. if ( $is_html ) { $footprint = $this->get_rocket_footprint(); } $this->log( 'Page not cached by filter.', [ 'filter' => 'do_rocket_generate_caching_files', ] ); return $buffer . $footprint; } $webp_enabled = preg_match( '@<!-- Rocket (has|no) webp -->@', $buffer, $webp_tag ); $has_webp = ! empty( $webp_tag ) ? 'has' === $webp_tag[1] : false; $cache_filepath = $this->get_cache_path( [ 'webp' => $has_webp ] ); $cache_dir_path = dirname( $cache_filepath ); // Create cache folders. rocket_mkdir_p( $cache_dir_path ); if ( $is_html ) { $footprint = $this->get_rocket_footprint( time() ); } // Webp request. if ( $webp_enabled ) { $buffer = str_replace( $webp_tag[0], '', $buffer ); if ( ! $has_webp ) { // The buffer doesn’t contain webp files. $cache_dir_path = rtrim( dirname( $cache_filepath ), '/\\' ); $this->maybe_create_nowebp_file( $cache_dir_path ); } } $this->write_cache_file( $cache_filepath, $buffer . $footprint ); $this->maybe_create_nginx_mobile_file( $cache_dir_path ); // Send headers with the last modified time of the cache file. if ( file_exists( $cache_filepath ) ) { header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_filepath ) ) . ' GMT' ); } if ( $is_html ) { $footprint = $this->get_rocket_footprint(); } $this->log( 'Page cached.', [ 'path' => $cache_filepath, ], 'info' ); return $buffer . $footprint; } /** * Writes the cache file(s) * * @since 3.5 * * @param string $cache_filepath Absolute path to the cache file. * @param string $content Content to write in the cache file. * @return void */ private function write_cache_file( $cache_filepath, $content ) { $gzip_filepath = $cache_filepath . '_gzip'; $temp_filepath = $cache_filepath . '_temp'; $temp_gzip_filepath = $gzip_filepath . '_temp'; if ( rocket_direct_filesystem()->exists( $temp_filepath ) ) { return; } // Save the cache file. if ( ! rocket_put_content( $temp_filepath, $content ) ) { return; } rocket_direct_filesystem()->move( $temp_filepath, $cache_filepath, true ); if ( function_exists( 'gzencode' ) ) { /** * Filters the Gzip compression level to use for the cache file * * @param int $compression_level Compression level between 0 and 9. */ $compression_level = apply_filters( 'rocket_gzencode_level_compression', 6 ); if ( ! rocket_put_content( $temp_gzip_filepath, gzencode( $content, $compression_level ) ) ) { return; } rocket_direct_filesystem()->move( $temp_gzip_filepath, $gzip_filepath, true ); } } /** * Get the path to the cache file. * * @since 3.3 * * @param array $args { * A list of arguments. * * @type bool $webp Set to false to prevent adding the part related to webp. * } * @return string */ public function get_cache_path( $args = [] ) { $args = array_merge( [ 'webp' => true, ], $args ); $cookies = $this->tests->get_cookies(); $request_uri_path = $this->get_request_cache_path( $cookies ); $filename = 'index'; $filename = $this->maybe_mobile_filename( $filename ); // Rename the caching filename for SSL URLs. if ( is_ssl() && $this->config->get_config( 'cache_ssl' ) ) { $filename .= '-https'; } if ( $args['webp'] ) { $filename = $this->maybe_webp_filename( $filename ); } $filename = $this->maybe_dynamic_cookies_filename( $filename, $cookies ); // Ensure proper formatting of the path. $request_uri_path = preg_replace_callback( '/%[0-9A-F]{2}/', [ $this, 'reset_lowercase' ], $request_uri_path ); // Directories in Windows can't contain question marks. $request_uri_path = str_replace( '?', '#', $request_uri_path ); // Limit filename max length to 255 characters. $request_uri_path .= '/' . substr( $filename, 0, 250 ) . '.html'; return $request_uri_path; } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS TOOLS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Declares and sets value of constant preventing Optimizations. * * @since 3.3 */ private function define_donotoptimize_true() { if ( ! defined( 'DONOTROCKETOPTIMIZE' ) ) { define( 'DONOTROCKETOPTIMIZE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } } /** * Gets If-modified-since header value * * @since 3.3 * @return string */ private function get_if_modified_since() { if ( function_exists( 'apache_request_headers' ) ) { $headers = apache_request_headers(); return isset( $headers['If-Modified-Since'] ) ? $headers['If-Modified-Since'] : ''; } return $this->config->get_server_input( 'HTTP_IF_MODIFIED_SINCE', '' ); } /** * Get WP Rocket footprint * * @since 3.0.5 White label footprint if WP_ROCKET_WHITE_LABEL_FOOTPRINT is defined. * @since 2.0 * * @param int $time UNIX timestamp when the cache file was saved. * @return string The footprint that will be printed */ private function get_rocket_footprint( $time = '' ) { $footprint = defined( 'WP_ROCKET_WHITE_LABEL_FOOTPRINT' ) ? "\n" . '<!-- Cached for great performance' : "\n" . '<!-- This website is like a Rocket, isn\'t it? Performance optimized by ' . WP_ROCKET_PLUGIN_NAME . '. Learn more: https://wp-rocket.me'; if ( ! empty( $time ) ) { $footprint .= ' - Debug: cached@' . $time; } $footprint .= ' -->'; return $footprint; } /** * Create a hidden empty file for mobile detection on NGINX with the Rocket NGINX configuration. * * @param string $cache_dir_path Path to the current cache directory. * @return void */ private function maybe_create_nginx_mobile_file( $cache_dir_path ) { global $is_nginx; if ( ! $this->config->get_config( 'do_caching_mobile_files' ) ) { return; } if ( ! $is_nginx ) { return; } $nginx_mobile_detect = $cache_dir_path . '/.mobile-active'; if ( rocket_direct_filesystem()->exists( $nginx_mobile_detect ) ) { return; } rocket_direct_filesystem()->touch( $nginx_mobile_detect ); } /** * Create a hidden empty file when webp is enabled but the buffer doesn’t contain webp files. * * @since 3.4 * * @param string $cache_dir_path Path to the current cache directory (without trailing slah). */ private function maybe_create_nowebp_file( $cache_dir_path ) { $nowebp_filepath = $cache_dir_path . DIRECTORY_SEPARATOR . '.no-webp'; if ( rocket_direct_filesystem()->exists( $nowebp_filepath ) ) { return; } rocket_direct_filesystem()->touch( $nowebp_filepath ); } /** * Tell if generating cache files is allowed. * * @since 3.3 * * @return bool */ public static function can_generate_caching_files() { /** * Allow to the generate the caching file. * * @since 2.5 * * @param bool True will force the cache file generation. */ return (bool) apply_filters( 'do_rocket_generate_caching_files', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Gets the base cache path for the current request * * @since 3.3 * * @param array $cookies Cookies for the current request. * @return string */ private function get_request_cache_path( $cookies ) { $host = $this->config->get_host(); if ( $this->config->get_config( 'url_no_dots' ) ) { $host = str_replace( '.', '_', $host ); } $request_uri = $this->tests->get_clean_request_uri(); $cookie_hash = $this->config->get_config( 'cookie_hash' ); $logged_in_cookie = $this->config->get_config( 'logged_in_cookie' ); $logged_in_cookie_no_hash = str_replace( $cookie_hash, '', $logged_in_cookie ); // Get cache folder of host name. if ( $logged_in_cookie && isset( $cookies[ $logged_in_cookie ] ) && ! $this->tests->has_rejected_cookie( $logged_in_cookie_no_hash ) ) { if ( $this->config->get_config( 'common_cache_logged_users' ) ) { return $this->cache_dir_path . $host . '-loggedin-' . $this->config->get_config( 'secret_cache_key' ) . rtrim( $request_uri, '/' ); } $user_key = explode( '|', $cookies[ $logged_in_cookie ] ); $user_key = reset( $user_key ); $user_key = $this->sanitize_user( $user_key ) . '-' . $this->config->get_config( 'secret_cache_key' ); // Get cache folder of host name. return $this->cache_dir_path . $host . '-' . $user_key . rtrim( $request_uri, '/' ); } return $this->cache_dir_path . $host . rtrim( $request_uri, '/' ); } /** * Modifies the filename if the request is from a mobile device. * * @since 3.3 * * @param string $filename Cache filename. * @return string */ private function maybe_mobile_filename( $filename ) { $cache_mobile_files_tablet = $this->config->get_config( 'cache_mobile_files_tablet' ); if ( ! ( $this->config->get_config( 'cache_mobile' ) && $this->config->get_config( 'do_caching_mobile_files' ) ) ) { return $filename; } if ( ! $cache_mobile_files_tablet ) { return $filename; } if ( ! class_exists( 'WP_Rocket_Mobile_Detect' ) ) { return $filename; } $detect = new \WP_Rocket_Mobile_Detect(); if ( ( $detect->isMobile() && ! $detect->isTablet() && 'desktop' === $cache_mobile_files_tablet ) || ( ( $detect->isMobile() || $detect->isTablet() ) && 'mobile' === $cache_mobile_files_tablet ) ) { return $filename .= '-mobile'; } return $filename; } /** * Modifies the filename if the request is WebP compatible * * @since 3.4 * * @param string $filename Cache filename. * @return string */ private function maybe_webp_filename( $filename ) { if ( ! $this->config->get_config( 'cache_webp' ) ) { return $filename; } /** * Force WP Rocket to disable its webp cache. * * @since 3.4 * * @param bool $disable_webp_cache Set to true to disable the webp cache. */ $disable_webp_cache = apply_filters( 'rocket_disable_webp_cache', false ); if ( $disable_webp_cache ) { return $filename; } if ( ! $this->is_browser_webp_compatible() ) { return $filename; } return $filename . '-webp'; } /** * Checks if the browser is WebP compatible * * @since 3.12.6 * * @return bool */ private function is_browser_webp_compatible(): bool { // Only to supporting browsers. $http_accept = $this->config->get_server_input( 'HTTP_ACCEPT', '' ); if ( empty( $http_accept ) && function_exists( 'apache_request_headers' ) ) { $headers = apache_request_headers(); $http_accept = isset( $headers['Accept'] ) ? $headers['Accept'] : ''; } if ( ! empty( $http_accept ) && false !== strpos( $http_accept, 'webp' ) ) { return true; } return $this->is_user_agent_compatible(); } /** * Check the User Agent if the Accept headers is missing the WebP info * * @since 3.12.6 * * @return bool */ private function is_user_agent_compatible(): bool { $user_agent = $this->config->get_server_input( 'HTTP_USER_AGENT' ); if ( empty( $user_agent ) ) { return false; } if ( preg_match( '#Firefox/(?<version>[0-9]{2,})#i', $user_agent, $matches ) ) { if ( 66 >= (int) $matches['version'] ) { return false; } } if ( preg_match( '#(?:iPad|iPhone)(.*)Version/(?<version>[0-9]{2,})#i', $user_agent, $matches ) ) { if ( 14 > (int) $matches['version'] ) { return false; } return true; } if ( preg_match( '#Version/(?<version>[0-9]{2,})(?:.*)Safari#i', $user_agent, $matches ) ) { if ( 16 > (int) $matches['version'] ) { return false; } } return true; } /** * Modifies the filename if dynamic cookies are set * * @param string $filename Cache filename. * @param array $cookies Cookies for the request. * @return string */ private function maybe_dynamic_cookies_filename( $filename, $cookies ) { $cache_dynamic_cookies = $this->config->get_config( 'cache_dynamic_cookies' ); if ( ! $cache_dynamic_cookies ) { return $filename; } foreach ( $cache_dynamic_cookies as $key => $cookie_name ) { if ( is_array( $cookie_name ) ) { if ( isset( $_COOKIE[ $key ] ) ) { foreach ( $cookie_name as $cookie_key ) { if ( '' !== $cookies[ $key ][ $cookie_key ] ) { $cache_key = $cookies[ $key ][ $cookie_key ]; $cache_key = preg_replace( '/[^a-z0-9_\-]/i', '-', $cache_key ); $filename .= '-' . $cache_key; } } } continue; } if ( isset( $cookies[ $cookie_name ] ) && '' !== $cookies[ $cookie_name ] ) { $cache_key = $cookies[ $cookie_name ]; $cache_key = preg_replace( '/[^a-z0-9_\-]/i', '-', $cache_key ); $filename .= '-' . $cache_key; } } return $filename; } /** * Force lowercase on encoded url strings from different alphabets to prevent issues on some hostings. * * @since 3.3 * * @param array $matches Cache path. * @return string Cache path in lowercase. */ protected function reset_lowercase( $matches ) { return strtolower( $matches[0] ); } /** * Sanitizes a string username. * * @param string $user String username. * * @return string */ private function sanitize_user( string $user = '' ): string { return strtolower( rawurlencode( $user ) ); } /** * Check if permalink structure and url match. * * @return bool */ private function maybe_allow_wp_redirect(): bool { $exclusions = implode( '|', $this->wp_redirect_exclusions ); // Return early for excluded urls. if ( preg_match( '#' . $exclusions . '#', $this->tests->get_request_uri_base() ) ) { return false; } $permalink_structure = $this->config->get_config( 'permalink_structure' ); // Last character of permalink. $permalink_last_char = '/' !== substr( $permalink_structure, -1 ) ? '' : '/'; // Request uri without protocol & domain name. $request_uri = $this->tests->get_request_uri_base(); // Last character of request uri. $request_uri_last_char = '/' !== substr( $request_uri, -1 ) ? '' : '/'; // In cases where we have the home with a trailng slash (visible or invisible) // and permalink is without trailing slash. if ( '' === $permalink_last_char ) { // Check for root installation. $request_uri_last_char = '/' === $request_uri ? '' : $request_uri_last_char; /** * Check for subdir installation. * Use config file name to get home request_uri. */ $home = str_replace( $this->config->get_host(), '', basename( $this->config->get_config_file_path()['path'] ) ); $home = str_replace( '.', '/', str_replace( '.php', '', $home ) ); if ( '/' !== $request_uri && rtrim( $request_uri, '/' ) === $home ) { $request_uri_last_char = ''; } } return $permalink_last_char !== $request_uri_last_char; } } classes/Buffer/class-config.php 0000644 00000017522 15174677547 0012531 0 ustar 00 <?php namespace WP_Rocket\Buffer; /** * Configuration class for WP Rocket cache * * @since 3.3 * @author Remy Perona */ class Config { use \WP_Rocket\Traits\Memoize; /** * Path to the directory containing the config files. * * @var string * @since 3.3 * @access private * @author Grégory Viguier */ private static $config_dir_path; /** * Values of $_SERVER to use for some tests. * * @var array * @since 3.3 * @access private * @author Grégory Viguier */ private static $server; /** * Constructor * * @param array $args { * An array of arguments. * * @type string $config_dir_path WP Rocket config directory path. * @type array $server Values of $_SERVER to use for the tests. Default is $_SERVER. * } */ public function __construct( $args ) { if ( isset( self::$config_dir_path ) ) { // Make sure to keep the same values all along. return; } if ( ! isset( $args['server'] ) && ! empty( $_SERVER ) && is_array( $_SERVER ) ) { $args['server'] = $_SERVER; } self::$config_dir_path = rtrim( $args['config_dir_path'], '/' ) . '/'; self::$server = ! empty( $args['server'] ) && is_array( $args['server'] ) ? $args['server'] : []; } /** * Get a $_SERVER entry. * * @since 3.3 * @access public * @author Grégory Viguier * * @param string $entry_name Name of the entry. * @param mixed $default Value to return if the entry is not set. * @return mixed */ public function get_server_input( $entry_name, $default = null ) { if ( ! isset( self::$server[ $entry_name ] ) ) { return $default; } return self::$server[ $entry_name ]; } /** * Get the `server` property. * * @since 3.3 * @access public * @author Grégory Viguier * * @return array */ public function get_server() { return self::$server; } /** * Get a specific config/option value. * * @since 3.3 * @access public * @author Grégory Viguier * * @param string $config_name Name of a specific config/option. * @return mixed */ public function get_config( $config_name ) { $config = $this->get_configs(); return isset( $config[ $config_name ] ) ? $config[ $config_name ] : null; } /** * Get the whole current configuration. * * @since 3.3 * @access public * @author Grégory Viguier * * @return array|bool An array containing the configuration. False on failure. */ public function get_configs() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } $config_file_path = $this->get_config_file_path(); if ( ! $config_file_path['success'] ) { return self::memoize( __FUNCTION__, [], false ); } include $config_file_path['path']; $config = [ 'cookie_hash' => '', 'logged_in_cookie' => '', 'common_cache_logged_users' => 0, 'cache_mobile_files_tablet' => 'desktop', 'cache_ssl' => 0, 'cache_webp' => 0, 'cache_mobile' => 0, 'do_caching_mobile_files' => 0, 'secret_cache_key' => '', 'cache_reject_uri' => '', 'cache_query_strings' => [], 'cache_ignored_parameters' => [], 'cache_reject_cookies' => '', 'cache_reject_ua' => '', 'cache_mandatory_cookies' => '', 'cache_dynamic_cookies' => [], 'url_no_dots' => 0, 'permalink_structure' => '', ]; foreach ( $config as $entry_name => $entry_value ) { $var_name = 'rocket_' . $entry_name; if ( isset( $$var_name ) ) { $config[ $entry_name ] = $$var_name; } } return self::memoize( __FUNCTION__, [], $config ); } /** * Get the host, to use for config and cache file path. * * @since 3.3 * @access public * @author Grégory Viguier * * @return string */ public function get_host() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } $host = $this->get_server_input( 'HTTP_HOST', (string) time() ); $host = preg_replace( '/:\d+$/', '', $host ); $host = trim( strtolower( $host ), '.' ); return self::memoize( __FUNCTION__, [], rawurlencode( $host ) ); } /** * Get the path to an existing config file. * * @since 3.3 * @access protected * @author Grégory Viguier * * @return string|bool The path to the file. False if no file is found. */ public function get_config_file_path() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } $config_dir_real_path = realpath( self::$config_dir_path ) . DIRECTORY_SEPARATOR; $host = $this->get_host(); $path = str_replace( '\\', '/', strtok( $this->get_server_input( 'REQUEST_URI', '' ), '?' ) ); $path = preg_replace( '|(?<=.)/+|', '/', $path ); $path = explode( '%2F', preg_replace( '/^(?:%2F)*(.*?)(?:%2F)*$/', '$1', rawurlencode( $path ) ) ); // Remove empty array values. $path = array_filter( $path ); /** * If path is not empty. * i.e url with something like this `multisite/green/sample-page` after the host. */ if ( ! empty( $path ) ) { $config_file_paths = []; // Loop through paths and store valid config file paths matching the url current path in an array. foreach ( $path as $p ) { static $dir; if ( realpath( self::$config_dir_path . $host . '.' . $p . '.php' ) && 0 === stripos( realpath( self::$config_dir_path . $host . '.' . $p . '.php' ), $config_dir_real_path ) ) { $config_file_paths[] = self::$config_dir_path . $host . '.' . $p . '.php'; } if ( realpath( self::$config_dir_path . $host . '.' . $dir . $p . '.php' ) && 0 === stripos( realpath( self::$config_dir_path . $host . '.' . $dir . $p . '.php' ), $config_dir_real_path ) ) { $config_file_paths[] = self::$config_dir_path . $host . '.' . $dir . $p . '.php'; } $dir .= $p . '.'; } // Reverse array order so that subsite config file paths can come first. $config_file_paths = array_reverse( $config_file_paths ); /** * Check if there was a matching config file for the url current path * and return the first */ if ( ! empty( $config_file_paths ) ) { return self::memoize( __FUNCTION__, [], [ 'success' => true, 'path' => $config_file_paths[0], ] ); } } if ( realpath( self::$config_dir_path . $host . '.php' ) && 0 === stripos( realpath( self::$config_dir_path . $host . '.php' ), $config_dir_real_path ) ) { $config_file_path = self::$config_dir_path . $host . '.php'; return self::memoize( __FUNCTION__, [], [ 'success' => true, 'path' => $config_file_path, ] ); } return self::memoize( __FUNCTION__, [], [ 'success' => false, 'path' => self::$config_dir_path . $host . implode( '/', $path ) . '.php', ] ); } /** ----------------------------------------------------------------------------------------- */ /** SPECIFIC CONFIG GETTERS ================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get rejected cookies as a regex pattern. * `#` is used as pattern delimiter. * * @since 3.3 * @access protected * @author Grégory Viguier * * @return string */ public function get_rejected_cookies() { $rejected_cookies = $this->get_config( 'cache_reject_cookies' ); if ( '' === $rejected_cookies ) { return $rejected_cookies; } return '#' . $rejected_cookies . '#'; } /** * Get mandatory cookies as a regex pattern. * `#` is used as pattern delimiter. * * @since 3.3 * @access protected * @author Grégory Viguier * * @return string */ public function get_mandatory_cookies() { $mandatory_cookies = $this->get_config( 'cache_mandatory_cookies' ); if ( '' === $mandatory_cookies ) { return $mandatory_cookies; } return '#' . $mandatory_cookies . '#'; } } classes/Buffer/class-abstract-buffer.php 0000644 00000007533 15174677547 0014337 0 ustar 00 <?php namespace WP_Rocket\Buffer; use WP_Rocket\Logger\Logger; /** * Handle page cache and optimizations. * * @since 3.3 * @author Grégory Viguier */ abstract class Abstract_Buffer { /** * Process identifier used by the logger. * * @var string * @since 3.3 * @access protected * @author Grégory Viguier */ protected $process_id; /** * Instance of the Tests class. * * @var Tests * @since 3.3 * @access protected * @author Grégory Viguier */ protected $tests; /** * Constructor. * * @since 3.3 * @access public * @author Grégory Viguier * * @param Tests $tests Tests instance. */ public function __construct( Tests $tests ) { $this->tests = $tests; } /** ----------------------------------------------------------------------------------------- */ /** PROCESS ================================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Launch the process if the tests succeed. * This should be the first thing to use after initializing the class. * * @since 3.3 * @access public * @see $this->tests->can_init_process() * @author Grégory Viguier */ abstract public function maybe_init_process(); /** * Process the page buffer if the 2nd set of tests succeed. * It should be used like this: * ob_start( [ $this, 'maybe_process_buffer' ] ); * * @since 3.3 * @access public * @see $this->tests->can_process_buffer() * @author Grégory Viguier * * @param string $buffer The buffer content. * @return string The buffered content */ abstract public function maybe_process_buffer( $buffer ); /** ----------------------------------------------------------------------------------------- */ /** LOG ===================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Log the last test "error". * * @since 3.3 * @access protected * @author Grégory Viguier */ protected function log_last_test_error() { $error = $this->tests->get_last_error(); $this->log( $error['message'], $error['data'] ); } /** * Log events. * * @since 3.3 * @access protected * @author Grégory Viguier * * @param string $message A message to log. * @param array $data Related data. * @param string $type Event type to log. Possible values are 'info', 'error', and 'debug' (default). */ protected function log( $message, $data = [], $type = 'debug' ) { $data = array_merge( [ $this->get_process_id(), 'request_uri' => $this->tests->get_raw_request_uri(), ], $data ); if ( isset( $data['cookies'] ) ) { $data['cookies'] = Logger::remove_auth_cookies( $data['cookies'] ); } switch ( $type ) { case 'info': Logger::info( $message, $data ); break; case 'error': Logger::error( $message, $data ); break; default: Logger::debug( $message, $data ); } } /** * Get the process identifier. * * @since 3.3 * @access public * @author Grégory Viguier * * @return string */ public function get_process_id() { return $this->process_id . ' - Thread #' . Logger::get_thread_id(); } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS TOOLS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the page content is HTML. * * @since 3.3 * @access protected * @author Grégory Viguier * * @param string $buffer The buffer content. * @return bool */ protected function is_html( $buffer ) { return preg_match( '/<\/html>/i', $buffer ); } } classes/Buffer/class-tests.php 0000644 00000104701 15174677547 0012422 0 ustar 00 <?php namespace WP_Rocket\Buffer; /** * Handle the tests for page cache and optimizations. * * @since 3.3 * @author Grégory Viguier */ class Tests { use \WP_Rocket\Traits\Memoize; /** * Config instance * * @var Config */ private $config; /** * Values of $_COOKIE to use for some tests. * * @var array * @since 3.3 * @author Grégory Viguier */ private static $cookies; /** * Values of $_POST to use for some tests. * * @var array * @since 3.3 * @author Grégory Viguier */ private static $post; /** * Values of $_GET to use for some tests. * * @var array * @since 3.3 * @author Grégory Viguier */ private static $get; /** * Array of complementary tests to perform. * * @var array Tests are listed as array keys. * @since 3.3 * @author Grégory Viguier */ private $tests = [ 'query_string' => 1, 'ssl' => 1, 'uri' => 1, 'rejected_cookie' => 1, 'mandatory_cookie' => 1, 'user_agent' => 1, 'mobile' => 1, 'donotcachepage' => 1, 'wp_404' => 1, 'search' => 1, 'is_html' => 1, ]; /** * Information about the last "error". * Here an "error" is a test failure. * * @var array { * @type string $message A message. * @type array $data Related data. * } * @since 3.3 * @author Grégory Viguier */ private $last_error = []; /** * Constructor. * * @since 3.3 * @author Grégory Viguier * * @param Config $config Config instance. * @param array $args { * An array of arguments. * * @type array $cookies Values of $_COOKIE to use for the tests. Default is $_COOKIE. * @type array $post Values of $_POST to use for the tests. Default is $_POST * @type array $get Values of $_GET to use for the tests. Default is $_GET. * @type array $tests List of complementary tests to perform. Optional. * } */ public function __construct( Config $config, array $args = [] ) { $this->config = $config; if ( ! empty( $args['tests'] ) ) { $this->set_tests( (array) $args['tests'] ); } // Provide fallback values. if ( ! isset( $args['cookies'] ) && ! empty( $_COOKIE ) && is_array( $_COOKIE ) ) { $args['cookies'] = $_COOKIE; } if ( ! isset( $args['post'] ) && ! empty( $_POST ) && is_array( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $args['post'] = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing } if ( ! isset( $args['get'] ) && ! empty( $_GET ) && is_array( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended $args['get'] = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended } self::$cookies = ! empty( $args['cookies'] ) && is_array( $args['cookies'] ) ? $args['cookies'] : []; self::$post = ! empty( $args['post'] ) && is_array( $args['post'] ) ? $args['post'] : []; self::$get = ! empty( $args['get'] ) && is_array( $args['get'] ) ? $args['get'] : []; if ( self::$post ) { self::$post = array_intersect_key( // Limit self::$post to the values we need, to save a bit of memory. self::$post, [ 'wp_customize' => '', ] ); } } /** ----------------------------------------------------------------------------------------- */ /** TESTS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if any buffer process should be initiated. * * @return bool */ public function can_process_any_buffer() { $this->last_error = []; // Don't process robots.txt && .htaccess files (it has happened sometimes with weird server configuration). if ( $this->is_rejected_file() ) { $this->set_error( 'Robots.txt or .htaccess file is excluded.' ); return false; } // Don't process disallowed file extensions (like php, xml, xsl). if ( $this->is_rejected_extension() ) { $this->set_error( 'PHP, XML, or XSL file is excluded.' ); return false; } // Don't cache if in admin or ajax. if ( $this->is_admin() ) { $this->set_error( 'Admin or AJAX URL is excluded.' ); return false; } // Don't process the customizer preview. if ( $this->is_customizer_preview() ) { $this->set_error( 'Customizer preview is excluded.' ); return false; } // Don't process without GET method. if ( ! $this->is_allowed_request_method() ) { $this->set_error( 'Request method is not allowed. Page cannot be cached.', [ 'request_method' => $this->get_request_method(), ] ); return false; } return true; } /** * Tell if the process should be initiated. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function can_init_process() { if ( ! $this->can_process_any_buffer() ) { return false; } if ( ! $this->has_test() ) { $this->last_error = []; return true; } $config = $this->config->get_config_file_path(); // Exit if no config file exists. if ( ! $config['success'] ) { $this->set_error( 'No config file found.', [ 'config_path' => $config['path'], ] ); return false; } // Don’t process with query strings parameters, but the processed content is served if the visitor comes from an RSS feed, a Facebook action or Google Adsense tracking. if ( $this->has_test( 'query_string' ) && ! $this->can_process_query_string() ) { $this->set_error( 'Query string URL is excluded.' . PHP_EOL . print_r( $_GET, true ) );// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r, WordPress.Security.NonceVerification.Recommended return false; } // Don't process SSL. if ( $this->has_test( 'ssl' ) && ! $this->can_process_ssl() ) { $this->set_error( 'SSL cache not applied to page.' ); return false; } // Don't process these pages. if ( $this->has_test( 'uri' ) && ! $this->can_process_uri() ) { $this->set_error( 'Page is excluded.' ); return false; } // Don't process page with these cookies. if ( $this->has_test( 'rejected_cookie' ) && $this->has_rejected_cookie() ) { $this->set_error( 'Excluded cookie found.', [ 'excluded_cookies' => $this->has_rejected_cookie(), ] ); return false; } // Don't process page when these cookies don't exist. if ( $this->has_test( 'mandatory_cookie' ) && ! $this->is_speed_tool() && is_array( $this->has_mandatory_cookie() ) ) { $this->set_error( 'Missing mandatory cookie: page not processed.', [ 'missing_cookies' => $this->has_mandatory_cookie(), ] ); return false; } // Don't process page with these user agents. if ( $this->has_test( 'user_agent' ) && ! $this->can_process_user_agent() ) { $this->set_error( 'User Agent is excluded.', [ 'user_agent' => $this->config->get_server_input( 'HTTP_USER_AGENT' ), ] ); return false; } // Don't process if mobile detection is activated. if ( $this->has_test( 'mobile' ) && ! $this->can_process_mobile() ) { $this->set_error( 'Mobile User Agent is excluded.', [ 'user_agent' => $this->config->get_server_input( 'HTTP_USER_AGENT' ), ] ); return false; } $this->last_error = []; return true; } /** * Tell if a test should be performed. * * @since 3.3 * @author Grégory Viguier * * @param string $test_name Identifier of the test. * Possible values are: 'query_string', 'ssl', 'uri', 'rejected_cookie', 'mandatory_cookie', 'user_agent', 'mobile'. * @return bool */ public function has_test( $test_name = '' ) { if ( empty( $test_name ) ) { return ! empty( $this->tests ); } return isset( $this->tests[ $test_name ] ); } /** * Set the list of tests to perform. * * @since 3.3 * @author Grégory Viguier * * @param array $tests An array of test names. */ public function set_tests( array $tests ) { $tests = array_flip( $tests ); array_merge( $this->tests, $tests ); } /** * Tell if the buffer should be processed. * * @since 3.3 * @author Grégory Viguier * * @param string $buffer The buffer content. * @return bool */ public function can_process_buffer( $buffer ) { $this->last_error = []; if ( ! function_exists( 'rocket_mkdir_p' ) ) { // Uh? $this->set_error( 'WP Rocket not found - page cannot be cached.' ); return false; } if ( strlen( $buffer ) <= 255 ) { // Buffer length must be > 255 (IE does not read pages under 255 c). $this->set_error( 'Buffer content under 255 characters.' ); return false; } if ( $this->get_http_response_code() !== 200 ) { // Only cache 200. $this->set_error( 'Page is not a 200 HTTP response and cannot be cached.' ); return false; } if ( $this->has_test( 'donotcachepage' ) && $this->has_donotcachepage() ) { // Don't process templates that use the DONOTCACHEPAGE constant. $this->set_error( 'DONOTCACHEPAGE is defined. Page cannot be cached.' ); return false; } if ( $this->has_test( 'wp_404' ) && $this->is_404() ) { // Don't process WP 404 page. $this->set_error( 'WP 404 page is excluded.' ); return false; } if ( $this->has_test( 'search' ) && $this->is_search() ) { // Don't process search results. $this->set_error( 'Search page is excluded.' ); return false; } if ( $this->has_test( 'is_html' ) ) { if ( $this->is_feed_uri() || defined( 'REST_REQUEST' ) ) { unset( $this->tests['is_html'] ); } } if ( $this->has_test( 'is_html' ) && ! $this->is_html( $buffer ) ) { // Don't process if there isn't a closing </html>. $this->set_error( 'No closing </html> was found.' ); return false; } $this->last_error = []; return true; } /** * Return http response to prevent a bug while testing. * * @return bool|int */ public function get_http_response_code() { return http_response_code(); } /** ----------------------------------------------------------------------------------------- */ /** SEPARATED TESTS ========================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the current URI corresponds to a file that must not be processed. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function is_rejected_file() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } $request_uri = $this->get_request_uri_base(); if ( ! $request_uri ) { return self::memoize( __FUNCTION__, [], false ); } $files = [ 'robots.txt', '.htaccess', ]; foreach ( $files as $file ) { if ( false !== strpos( $request_uri, '/' . $file ) ) { return self::memoize( __FUNCTION__, [], true ); } } return self::memoize( __FUNCTION__, [], false ); } /** * Tell if the current URI corresponds to a file extension that must not be processed. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function is_rejected_extension() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } $request_uri = $this->get_request_uri_base(); if ( ! $request_uri ) { return self::memoize( __FUNCTION__, [], false ); } if ( strtolower( $request_uri ) === '/index.php' ) { // `index.php` is allowed. return self::memoize( __FUNCTION__, [], false ); } $extension = pathinfo( $request_uri, PATHINFO_EXTENSION ); $extensions = [ 'php' => 1, 'xml' => 1, 'xsl' => 1, ]; $is_rejected = $extension && isset( $extensions[ $extension ] ); return self::memoize( __FUNCTION__, [], $is_rejected ); } /** * Tell if the current url is a feed. * * @return bool */ public function is_feed_uri() { global $wp_rewrite; $feed_uri = '/(?:.+/)?' . $wp_rewrite->feed_base . '(?:/(?:.+/?)?)?$'; return (bool) preg_match( '#^(' . $feed_uri . ')$#i', $this->get_clean_request_uri() ); } /** * Tell if we're in the admin area (or ajax) or not. * Test against ajax added in 2e3c0fa74246aa13b36835f132dfd55b90d4bf9e for whatever reason. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function is_admin() { return is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ); } /** * Tell if we're displaying a customizer preview. * Test added in 769c7377e764a6a8decb4015a167b34043b4b462 for whatever reason. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function is_customizer_preview() { return isset( self::$post['wp_customize'] ); } /** * Tell if the request method is allowed to be cached. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function is_allowed_request_method() { $allowed = [ 'GET' => 1, 'HEAD' => 1, ]; if ( isset( $allowed[ $this->get_request_method() ] ) ) { return true; } return false; } /** ----------------------------------------------------------------------------------------- */ /** SEPARATED TESTS THAT USE THE CONFIG FILE ================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Don't process with query string parameters, some parameters are allowed though. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function can_process_query_string() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } $params = $this->get_query_params(); if ( ! $params ) { return self::memoize( __FUNCTION__, [], true ); } // The page can be processed if at least one of these parameters is present. $allowed_params = [ 'lang' => 1, 's' => 1, 'permalink_name' => 1, 'lp-variation-id' => 1, ]; if ( array_intersect_key( $params, $allowed_params ) ) { return self::memoize( __FUNCTION__, [], true ); } // The page can be processed if at least one of these parameters is present. $allowed_params = $this->config->get_config( 'cache_query_strings' ); if ( ! $allowed_params ) { // We have query strings but none is in the list set by the user. return self::memoize( __FUNCTION__, [], false ); } $can = (bool) array_intersect_key( $params, array_flip( $allowed_params ) ); return self::memoize( __FUNCTION__, [], $can ); } /** * Process SSL only if set in the plugin settings. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function can_process_ssl() { return ! $this->is_ssl() || $this->config->get_config( 'cache_ssl' ); } /** * Some URIs set in the plugin settings must not be processed. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function can_process_uri() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } // URIs not to cache. $uri_pattern = $this->config->get_config( 'cache_reject_uri' ); if ( ! $uri_pattern ) { return self::memoize( __FUNCTION__, [], true ); } $can = ! preg_match( '#^(' . $uri_pattern . ')$#i', $this->get_request_uri_base() ); return self::memoize( __FUNCTION__, [], $can ); } /** * Don't process if some cookies are present. * * @since 3.3 * @author Grégory Viguier * * @return bool|array */ public function has_rejected_cookie() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } if ( ! self::$cookies ) { return self::memoize( __FUNCTION__, [], false ); } $rejected_cookies = $this->config->get_rejected_cookies(); if ( ! $rejected_cookies ) { return self::memoize( __FUNCTION__, [], false ); } $excluded_cookies = []; foreach ( array_keys( self::$cookies ) as $cookie_name ) { if ( preg_match( $rejected_cookies, $cookie_name ) ) { $excluded_cookies[] = $cookie_name; } } if ( ! empty( $excluded_cookies ) ) { return self::memoize( __FUNCTION__, [], $excluded_cookies ); } return self::memoize( __FUNCTION__, [], false ); } /** * Don't process if some cookies are NOT present. * * @since 3.3 * @author Grégory Viguier * * @return bool|array */ public function has_mandatory_cookie() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } $mandatory_cookies = $this->config->get_mandatory_cookies(); if ( ! $mandatory_cookies ) { return self::memoize( __FUNCTION__, [], true ); } $missing_cookies = array_flip( explode( '|', $this->config->get_config( 'cache_mandatory_cookies' ) ) ); if ( ! self::$cookies ) { return self::memoize( __FUNCTION__, [], $missing_cookies ); } foreach ( array_keys( self::$cookies ) as $cookie_name ) { if ( preg_match( $mandatory_cookies, $cookie_name ) ) { unset( $missing_cookies[ $cookie_name ] ); } } if ( empty( $missing_cookies ) ) { return self::memoize( __FUNCTION__, [], true ); } return self::memoize( __FUNCTION__, [], array_flip( $missing_cookies ) ); } /** * Don't process if the user agent is in the forbidden list. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function can_process_user_agent() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } if ( ! $this->config->get_server_input( 'HTTP_USER_AGENT' ) ) { return self::memoize( __FUNCTION__, [], true ); } $rejected_uas = $this->config->get_config( 'cache_reject_ua' ); if ( ! $rejected_uas ) { return self::memoize( __FUNCTION__, [], true ); } $can = ! preg_match( '#' . $rejected_uas . '#', $this->config->get_server_input( 'HTTP_USER_AGENT' ) ); return self::memoize( __FUNCTION__, [], $can ); } /** * Don't process if the user agent is in the forbidden list. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function can_process_mobile() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } if ( ! $this->config->get_server_input( 'HTTP_USER_AGENT' ) ) { return self::memoize( __FUNCTION__, [], true ); } if ( $this->config->get_config( 'cache_mobile' ) ) { return self::memoize( __FUNCTION__, [], true ); } $uas = '2.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800'; if ( preg_match( '#^.*(' . $uas . ').*#i', $this->config->get_server_input( 'HTTP_USER_AGENT' ) ) ) { return self::memoize( __FUNCTION__, [], false ); } $uas = 'w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-'; if ( preg_match( '#^(' . $uas . ').*#i', $this->config->get_server_input( 'HTTP_USER_AGENT' ) ) ) { return self::memoize( __FUNCTION__, [], false ); } return self::memoize( __FUNCTION__, [], true ); } /** ----------------------------------------------------------------------------------------- */ /** SEPARATED TESTS AFTER PAGE RESPONSE ===================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the constant DONOTCACHEPAGE is set and not overridden. * When defined, the page must not be cached. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function has_donotcachepage() { if ( ! defined( 'DONOTCACHEPAGE' ) || ! DONOTCACHEPAGE ) { return false; } /** * At this point the constant DONOTCACHEPAGE is set to true. * This filter allows to force the page caching. * It prevents conflict with some plugins like Thrive Leads. * * @since 2.5 * * @param bool $override_donotcachepage True will force the page to be cached. */ return ! apply_filters( 'rocket_override_donotcachepage', false ); } /** * Tell if we're in the WP’s 404 page. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function is_404() { return ! function_exists( 'is_404' ) || is_404(); } /** * Tell if we're in the WP’s search page. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function is_search() { if ( function_exists( 'is_search' ) && ! is_search() ) { return false; } /** * At this point we’re in the WP’s search page. * This filter allows to cache search results. * * @since 2.3.8 * * @param bool $cache_search True will force caching search results. */ return ! apply_filters( 'rocket_cache_search', false ); } /** * Tell if the page content has a closing </html>. * * @since 3.9 * * @param string $buffer The buffer content. * @return bool */ public function is_html( $buffer ) { return (bool) preg_match( '/<\s*\/\s*html\s*>/i', $buffer ); } /** ----------------------------------------------------------------------------------------- */ /** $_SERVER ================================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the IP address from which the user is viewing the current page. * * @since 3.3 * @author Grégory Viguier */ public function get_ip() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } $keys = [ 'HTTP_CF_CONNECTING_IP', // CF = CloudFlare. 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR', ]; foreach ( $keys as $key ) { if ( ! $this->config->get_server_input( $key ) ) { continue; } $ip = explode( ',', $this->config->get_server_input( $key ) ); $ip = end( $ip ); if ( false !== filter_var( $ip, FILTER_VALIDATE_IP ) ) { return self::memoize( __FUNCTION__, [], $ip ); } } return self::memoize( __FUNCTION__, [], '0.0.0.0' ); } /** * Tell if the request comes from a speed test tool. * * @since 3.3 * @author Grégory Viguier * * @return bool */ public function is_speed_tool() { if ( self::is_memoized( __FUNCTION__ ) ) { return self::get_memoized( __FUNCTION__ ); } $ips = [ '208.70.247.157' => '', // GT Metrix - Vancouver 1. '172.255.48.130' => '', // GT Metrix - Vancouver 2. '172.255.48.131' => '', // GT Metrix - Vancouver 3. '172.255.48.132' => '', // GT Metrix - Vancouver 4. '172.255.48.133' => '', // GT Metrix - Vancouver 5. '172.255.48.134' => '', // GT Metrix - Vancouver 6. '172.255.48.135' => '', // GT Metrix - Vancouver 7. '172.255.48.136' => '', // GT Metrix - Vancouver 8. '172.255.48.137' => '', // GT Metrix - Vancouver 9. '172.255.48.138' => '', // GT Metrix - Vancouver 10. '172.255.48.139' => '', // GT Metrix - Vancouver 11. '172.255.48.140' => '', // GT Metrix - Vancouver 12. '172.255.48.141' => '', // GT Metrix - Vancouver 13. '172.255.48.142' => '', // GT Metrix - Vancouver 14. '172.255.48.143' => '', // GT Metrix - Vancouver 15. '172.255.48.144' => '', // GT Metrix - Vancouver 16. '172.255.48.145' => '', // GT Metrix - Vancouver 17. '172.255.48.146' => '', // GT Metrix - Vancouver 18. '172.255.48.147' => '', // GT Metrix - Vancouver 19. '52.229.122.240' => '', // GT Metrix - Quebec City. '104.214.72.101' => '', // GT Metrix - San Antonio, TX 1. '13.66.7.11' => '', // GT Metrix - San Antonio, TX 2. '13.85.24.83' => '', // GT Metrix - San Antonio, TX 3. '13.85.24.90' => '', // GT Metrix - San Antonio, TX 4. '13.85.82.26' => '', // GT Metrix - San Antonio, TX 5. '40.74.242.253' => '', // GT Metrix - San Antonio, TX 6. '40.74.243.13' => '', // GT Metrix - San Antonio, TX 7. '40.74.243.176' => '', // GT Metrix - San Antonio, TX 8. '104.214.48.247' => '', // GT Metrix - San Antonio, TX 9. '157.55.189.189' => '', // GT Metrix - San Antonio, TX 10. '104.214.110.135' => '', // GT Metrix - San Antonio, TX 11. '70.37.83.240' => '', // GT Metrix - San Antonio, TX 12. '65.52.36.250' => '', // GT Metrix - San Antonio, TX 13. '13.78.216.56' => '', // GT Metrix - Cheyenne, WY. '52.162.212.163' => '', // GT Metrix - Chicago, IL. '23.96.34.105' => '', // GT Metrix - Danville, VA. '65.52.113.236' => '', // GT Metrix - San Francisco, CA. '172.255.61.34' => '', // GT Metrix - London 1. '172.255.61.35' => '', // GT Metrix - London 2. '172.255.61.36' => '', // GT Metrix - London 3. '172.255.61.37' => '', // GT Metrix - London 4. '172.255.61.38' => '', // GT Metrix - London 5. '172.255.61.39' => '', // GT Metrix - London 6. '172.255.61.40' => '', // GT Metrix - London 7. '52.237.235.185' => '', // GT Metrix - Sydney 1. '52.237.250.73' => '', // GT Metrix - Sydney 2. '52.237.236.145' => '', // GT Metrix - Sydney 3. '104.41.2.19' => '', // GT Metrix - São Paulo 1. '191.235.98.164' => '', // GT Metrix - São Paulo 2. '191.235.99.221' => '', // GT Metrix - São Paulo 3. '191.232.194.51' => '', // GT Metrix - São Paulo 4. '104.211.143.8' => '', // GT Metrix - Mumbai 1. '104.211.165.53' => '', // GT Metrix - Mumbai 2. '52.172.14.87' => '', // GT Metrix - Chennai. '40.83.89.214' => '', // GT Metrix - Hong Kong 1. '52.175.57.81' => '', // GT Metrix - Hong Kong 2. '20.188.63.151' => '', // GT Metrix - Paris. '20.52.36.49' => '', // GT Metrix - Frankfurt. '52.246.165.153' => '', // GT Metrix - Tokyo. '51.144.102.233' => '', // GT Metrix - Amsterdam. '13.76.97.224' => '', // GT Metrix - Singapore. '102.133.169.66' => '', // GT Metrix - Johannesburg. '52.231.199.170' => '', // GT Metrix - Busan. '13.53.162.7' => '', // GT Metrix - Stockholm. '40.123.218.94' => '', // GT Metrix - Dubai. ]; if ( isset( $ips[ $this->get_ip() ] ) ) { return self::memoize( __FUNCTION__, [], true ); } if ( ! $this->config->get_server_input( 'HTTP_USER_AGENT' ) ) { return self::memoize( __FUNCTION__, [], false ); } $user_agent = preg_match( '#PingdomPageSpeed|DareBoost|Google|PTST|Chrome-Lighthouse|WP Rocket#i', $this->config->get_server_input( 'HTTP_USER_AGENT' ) ); return self::memoize( __FUNCTION__, [], (bool) $user_agent ); } /** * Determines if SSL is used. * This is basically a copy of the WP function, where $_SERVER is not used directly. * * @since 3.3 * @author Grégory Viguier * * @return bool True if SSL, otherwise false. */ public function is_ssl() { if ( null !== $this->config->get_server_input( 'HTTPS' ) ) { if ( 'on' === strtolower( $this->config->get_server_input( 'HTTPS' ) ) ) { return true; } if ( '1' === (string) $this->config->get_server_input( 'HTTPS' ) ) { return true; } } elseif ( '443' === (string) $this->config->get_server_input( 'SERVER_PORT' ) ) { return true; } return false; } /** ----------------------------------------------------------------------------------------- */ /** REQUEST URI AND METHOD ================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the request URI. * * @since 3.3 * @author Grégory Viguier * * @return string */ public function get_raw_request_uri() { if ( '' === $this->config->get_server_input( 'REQUEST_URI', '' ) ) { return ''; } return '/' . ltrim( $this->config->get_server_input( 'REQUEST_URI' ), '/' ); } /** * Get the request URI without the query strings. * * @since 3.3 * @author Grégory Viguier * * @return string */ public function get_request_uri_base() { $request_uri = $this->get_raw_request_uri(); if ( ! $request_uri ) { return ''; } $request_uri = explode( '?', $request_uri ); return reset( $request_uri ); } /** * Get the request URI. The query string is sorted and some parameters are removed. * * @since 3.3 * @author Grégory Viguier * * @return string */ public function get_clean_request_uri() { $request_uri = $this->get_request_uri_base(); $request_uri = $this->remove_dot_segments( $request_uri ); if ( ! $request_uri ) { return ''; } $query_string = $this->get_query_string(); if ( ! $query_string ) { return $request_uri; } return $request_uri . '?' . $query_string; } /** * Get the request method. * * @since 3.3 * @author Grégory Viguier * * @return string */ public function get_request_method() { if ( '' === $this->config->get_server_input( 'REQUEST_METHOD', '' ) ) { return ''; } return strtoupper( $this->config->get_server_input( 'REQUEST_METHOD' ) ); } /** * Remove dot segments from a path * * @param string $input Path to process. * * @return string */ private function remove_dot_segments( string $input ) { $output = ''; while ( strpos( $input, './' ) !== false || strpos( $input, '/.' ) !== false || '.' === $input || '..' === $input ) { /** * A: If the input buffer begins with a prefix of "../" or "./", * then remove that prefix from the input buffer; otherwise, */ if ( strpos( $input, '../' ) === 0 ) { $input = substr( $input, 3 ); } elseif ( strpos( $input, './' ) === 0 ) { $input = substr( $input, 2 ); } /** * B: if the input buffer begins with a prefix of "/./" or "/.", * where "." is a complete path segment, then replace that prefix * with "/" in the input buffer; otherwise, */ elseif ( strpos( $input, '/./' ) === 0 ) { $input = substr( $input, 2 ); } elseif ( '/.' === $input ) { $input = '/'; } /** * C: if the input buffer begins with a prefix of "/../" or "/..", * where ".." is a complete path segment, then replace that prefix * with "/" in the input buffer and remove the last segment and its * preceding "/" (if any) from the output buffer; otherwise, */ elseif ( strpos( $input, '/../' ) === 0 ) { $input = substr( $input, 3 ); $output = substr_replace( $output, '', strrpos( $output, '/' ) ); } elseif ( '/..' === $input ) { $input = '/'; $output = substr_replace( $output, '', strrpos( $output, '/' ) ); } /** * D: if the input buffer consists only of "." or "..", then remove * that from the input buffer; otherwise, */ elseif ( '.' === $input || '..' === $input ) { $input = ''; } /** * E: move the first path segment in the input buffer to the end of * the output buffer, including the initial "/" character (if any) * and any subsequent characters up to, but not including, the next * "/" character or the end of the input buffer */ elseif ( strpos( $input, '/', 1 ) !== false ) { $pos = strpos( $input, '/', 1 ); $output .= substr( $input, 0, $pos ); $input = substr_replace( $input, '', 0, $pos ); } else { $output .= $input; $input = ''; } } return $output . $input; } /** ----------------------------------------------------------------------------------------- */ /** QUERY STRING ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the query string as an array. Parameters are sorted and some are removed. * * @since 3.3 * @author Grégory Viguier * * @return array */ public function get_query_params() { if ( ! self::$get ) { return []; } // Remove some parameters. $params = array_diff_key( self::$get, $this->config->get_config( 'cache_ignored_parameters' ) ); if ( $params ) { ksort( $params ); } return $params; } /** * Get the query string with sorted parameters, and some other removed. * * @since 3.3 * @author Grégory Viguier * * @return string */ public function get_query_string() { return http_build_query( $this->get_query_params() ); } /** * Get the original query string * * @since 3.11.4 * * @return string */ public function get_original_query_string() { return http_build_query( $this->get_get() ); } /** ----------------------------------------------------------------------------------------- */ /** PROPERTY GETTERS ======================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the `cookies` property. * * @since 3.3 * @author Grégory Viguier * * @return array */ public function get_cookies() { return self::$cookies; } /** * Get the `post` property. * * @since 3.3 * @author Grégory Viguier * * @return array */ public function get_post() { return self::$post; } /** * Get the `get` property. * * @since 3.3 * @author Grégory Viguier * * @return array */ public function get_get() { return self::$get; } /** ----------------------------------------------------------------------------------------- */ /** ERRORS ================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Set an "error". * * @since 3.3 * @author Grégory Viguier * * @param string $message A message. * @param array $data Related data. */ protected function set_error( $message, $data = [] ) { $this->last_error = [ 'message' => $message, 'data' => (array) $data, ]; } /** * Get the last "error". * * @since 3.3 * @author Grégory Viguier * * @return array */ public function get_last_error() { return array_merge( [ 'message' => '', 'data' => [], ], (array) $this->last_error ); } } classes/event-management/event-manager-aware-subscriber-interface.php 0000644 00000000566 15174677547 0022130 0 ustar 00 <?php namespace WP_Rocket\Event_Management; interface Event_Manager_Aware_Subscriber_Interface extends Subscriber_Interface { /** * Set the WordPress event manager for the subscriber. * * @since 3.1 * @author Remy Perona * * @param Event_Manager $event_manager Event_Manager instance. */ public function set_event_manager( Event_Manager $event_manager ); } classes/event-management/subscriber-interface.php 0000644 00000002130 15174677547 0016271 0 ustar 00 <?php namespace WP_Rocket\Event_Management; /** * A Subscriber knows what specific WordPress events it wants to listen to. * * When an EventManager adds a Subscriber, it gets all the WordPress events that * it wants to listen to. It then adds the subscriber as a listener for each of them. * * @author Carl Alexander <contact@carlalexander.ca> */ interface Subscriber_Interface { /** * Returns an array of events that this subscriber wants to listen to. * * The array key is the event name. The value can be: * * * The method name * * An array with the method name and priority * * An array with the method name, priority and number of accepted arguments * * For instance: * * * array('hook_name' => 'method_name') * * array('hook_name' => array('method_name', $priority)) * * array('hook_name' => array('method_name', $priority, $accepted_args)) * * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2))) * * @return array */ public static function get_subscribed_events(); } classes/event-management/class-event-manager.php 0000644 00000011501 15174677547 0016026 0 ustar 00 <?php namespace WP_Rocket\Event_Management; /** * The event manager manages events using the WordPress plugin API. * * @since 3.1 * @author Carl Alexander <contact@carlalexander.ca> */ class Event_Manager { /** * Adds a callback to a specific hook of the WordPress plugin API. * * @uses add_filter() * * @param string $hook_name Name of the hook. * @param callable $callback Callback function. * @param int $priority Priority. * @param int $accepted_args Number of arguments. */ public function add_callback( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { add_filter( $hook_name, $callback, $priority, $accepted_args ); } /** * Add an event subscriber. * * The event manager registers all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param Subscriber_Interface $subscriber Subscriber_Interface implementation. */ public function add_subscriber( Subscriber_Interface $subscriber ) { if ( $subscriber instanceof Event_Manager_Aware_Subscriber_Interface ) { $subscriber->set_event_manager( $this ); } $events = $subscriber->get_subscribed_events(); if ( empty( $events ) ) { return; } foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) { $this->add_subscriber_callback( $subscriber, $hook_name, $parameters ); } } /** * Checks the WordPress plugin API to see if the given hook has * the given callback. The priority of the callback will be returned * or false. If no callback is given will return true or false if * there's any callbacks registered to the hook. * * @uses has_filter() * * @param string $hook_name Hook name. * @param mixed $callback Callback. * * @return bool|int */ public function has_callback( $hook_name, $callback = false ) { return has_filter( $hook_name, $callback ); } /** * Removes the given callback from the given hook. The WordPress plugin API only * removes the hook if the callback and priority match a registered hook. * * @uses remove_filter() * * @param string $hook_name Hook name. * @param callable $callback Callback. * @param int $priority Priority. * * @return bool */ public function remove_callback( $hook_name, $callback, $priority = 10 ) { return remove_filter( $hook_name, $callback, $priority ); } /** * Remove an event subscriber. * * The event manager removes all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param Subscriber_Interface $subscriber Subscriber_Interface implementation. */ public function remove_subscriber( Subscriber_Interface $subscriber ) { foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) { $this->remove_subscriber_callback( $subscriber, $hook_name, $parameters ); } } /** * Adds the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param Subscriber_Interface $subscriber Subscriber_Interface implementation. * @param string $hook_name Hook name. * @param mixed $parameters Parameters, can be a string, an array or a multidimensional array. */ private function add_subscriber_callback( Subscriber_Interface $subscriber, $hook_name, $parameters ) { if ( is_string( $parameters ) ) { $this->add_callback( $hook_name, [ $subscriber, $parameters ] ); } elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) { foreach ( $parameters as $parameter ) { $this->add_subscriber_callback( $subscriber, $hook_name, $parameter ); } } elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) { $this->add_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10, isset( $parameters[2] ) ? $parameters[2] : 1 ); } } /** * Removes the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param Subscriber_Interface $subscriber Subscriber_Interface implementation. * @param string $hook_name Hook name. * @param mixed $parameters Parameters, can be a string, an array or a multidimensional array. */ private function remove_subscriber_callback( Subscriber_Interface $subscriber, $hook_name, $parameters ) { if ( is_string( $parameters ) ) { $this->remove_callback( $hook_name, [ $subscriber, $parameters ] ); } elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) { foreach ( $parameters as $parameter ) { $this->remove_subscriber_callback( $subscriber, $hook_name, $parameter ); } } elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) { $this->remove_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10 ); } } } admin/ui/enqueue.php 0000644 00000006026 15174677547 0010464 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Add the CSS and JS files for WP Rocket options page * * @since 1.0.0 */ function rocket_add_admin_css_js() { $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; wp_enqueue_style( 'wpr-admin', WP_ROCKET_ASSETS_CSS_URL . 'wpr-admin' . $suffix . '.css', null, WP_ROCKET_VERSION ); wp_enqueue_script( 'micromodal', WP_ROCKET_ASSETS_JS_URL . 'micromodal.min.js', null, '0.4.10', true ); wp_enqueue_script( 'wpr-admin', WP_ROCKET_ASSETS_JS_URL . 'wpr-admin' . $suffix . '.js', [ 'micromodal' ], WP_ROCKET_VERSION, true ); wp_localize_script( 'wpr-admin', 'rocket_ajax_data', /** * Filters the data passed to the localize script function for WP Rocket admin JS * * @since 3.7.4 * * @param array $data Localize script data. */ apply_filters( 'rocket_localize_admin_script', [ 'nonce' => wp_create_nonce( 'rocket-ajax' ), 'origin_url' => 'https://api.wp-rocket.me', ] ) ); if ( is_rtl() ) { wp_enqueue_style( 'wpr-admin-rtl', WP_ROCKET_ASSETS_CSS_URL . 'wpr-admin-rtl' . $suffix . '.css', null, WP_ROCKET_VERSION ); } } add_action( 'admin_print_styles-settings_page_' . WP_ROCKET_PLUGIN_SLUG, 'rocket_add_admin_css_js' ); /** * Add the CSS and JS files needed by WP Rocket everywhere on admin pages * * @since 2.1 */ function rocket_add_admin_css_js_everywhere() { wp_enqueue_script( 'wpr-admin-common', WP_ROCKET_ASSETS_JS_URL . 'wpr-admin-common.js', [ 'jquery' ], WP_ROCKET_VERSION, true ); wp_enqueue_style( 'wpr-admin-common', WP_ROCKET_ASSETS_CSS_URL . 'wpr-admin-common.css', [], WP_ROCKET_VERSION ); } add_action( 'admin_enqueue_scripts', 'rocket_add_admin_css_js_everywhere', 11 ); /** * Adds mixpanel JS code in header when analytics data should be sent * * @since 2.11 * @deprecated 3.19.2 Use WP_Rocket\Engine\Tracking\Tracking class instead * @author Remy Perona */ function rocket_add_mixpanel_code() { _deprecated_function( __FUNCTION__, '3.19.2', 'WP_Rocket\Engine\Tracking\Tracking' ); // This functionality has been moved to the new Tracking system. // The Tracking class now handles Mixpanel script injection via the // inject_mixpanel_script() method. } // Remove the action hook - the new Tracking system handles this. /** * Add CSS & JS files for the Imagify installation call to action * * @since 2.7 */ function rocket_enqueue_modal_plugin() { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( defined( 'IMAGIFY_VERSION' ) || in_array( 'rocket_imagify_notice', (array) $boxes, true ) || 1 === get_option( 'wp_rocket_dismiss_imagify_notice' ) || ! current_user_can( 'manage_options' ) ) { return; } wp_enqueue_style( 'plugin-install' ); wp_enqueue_script( 'plugin-install' ); wp_enqueue_script( 'updates' ); add_thickbox(); } add_action( 'admin_print_styles-media-new.php', 'rocket_enqueue_modal_plugin' ); add_action( 'admin_print_styles-upload.php', 'rocket_enqueue_modal_plugin' ); add_action( 'admin_print_styles-settings_page_' . WP_ROCKET_PLUGIN_SLUG, 'rocket_enqueue_modal_plugin' ); admin/ui/meta-boxes.php 0000644 00000001670 15174677547 0011061 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Add a link "Purge cache" in the post submit area * * @since 1.0 */ function rocket_post_submitbox_start() { if ( ! rocket_can_display_options() ) { return; } if ( current_user_can( 'rocket_purge_posts' ) ) { global $post; $cpts = get_post_types( [ 'public' => true, ], 'objects' ); /** * Filters the post type on submitbox. * * @since 3.12.1 * * @param array $cpts Post Types. */ $cpts = apply_filters( 'rocket_submitbox_options_post_types', $cpts ); if ( isset( $cpts[ $post->post_type ] ) ) { $url = wp_nonce_url( admin_url( 'admin-post.php?action=purge_cache&type=post-' . $post->ID ), 'purge_cache_post-' . $post->ID ); printf( '<div id="purge-action"><a class="button-secondary" href="%s">%s</a></div>', esc_url( $url ), esc_html__( 'Clear cache', 'rocket' ) ); } } } add_action( 'post_submitbox_start', 'rocket_post_submitbox_start' ); admin/ui/notices.php 0000644 00000062012 15174677547 0010456 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * This warnings are displayed when the plugin can not be deactivated correctly * * @since 2.0.0 */ function rocket_bad_deactivations() { global $current_user; $screen = get_current_screen(); if ( 'plugins' !== $screen->id ) { return; } $msgs = get_transient( $current_user->ID . '_donotdeactivaterocket' ); if ( current_user_can( 'rocket_manage_options' ) && $msgs ) { delete_transient( $current_user->ID . '_donotdeactivaterocket' ); $errors = []; foreach ( $msgs as $msg ) { switch ( $msg ) { case 'wpconfig': $errors['wpconfig'] = '<p>' . sprintf( // translators: %1$s WP Rocket plugin name; %2$s = file name. __( '<strong>%1$s</strong> has not been deactivated due to missing writing permissions.<br> Make <strong>%2$s</strong> writeable and retry deactivation, or force deactivation now:', 'rocket' ), WP_ROCKET_PLUGIN_NAME, 'wp-config.php' ) . '</p>'; break; case 'htaccess': $errors['htaccess'] = '<p>' . sprintf( // translators: %1$s WP Rocket plugin name; %2$s = file name. __( '<strong>%1$s</strong> has not been deactivated due to missing writing permissions.<br> Make <strong>%2$s</strong> writeable and retry deactivation, or force deactivation now:', 'rocket' ), WP_ROCKET_PLUGIN_NAME, '.htaccess' ) . '</p>'; break; } /** * Filter the output messages for each bad deactivation attempt. * * @since 2.0.0 * * @param array $errors Contains the error messages to be filtered * @param string $msg Contains the error type (wpconfig or htaccess) */ $errors = apply_filters( 'rocket_bad_deactivations', $errors, $msg ); } rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => implode( '', $errors ), 'action' => 'force_deactivation', ] ); } } add_action( 'admin_notices', 'rocket_bad_deactivations' ); /** * This warning is displayed to inform the user that a plugin de/activation can be followed by a cache clear * * @since 1.3.0 */ function rocket_warning_plugin_modification() { if ( current_user_can( 'rocket_manage_options' ) && rocket_valid_key() ) { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } rocket_notice_html( [ 'status' => 'warning', 'dismissible' => '', // translators: %s is WP Rocket plugin name. 'message' => sprintf( __( '<strong>%s</strong>: One or more plugins have been enabled or disabled, clear the cache if they affect the front end of your site.', 'rocket' ), WP_ROCKET_PLUGIN_NAME ), 'action' => 'clear_cache', 'dismiss_button' => __FUNCTION__, ] ); } } add_action( 'admin_notices', 'rocket_warning_plugin_modification' ); /** * This warning is displayed when some plugins may conflict with WP Rocket * * @since 1.3.0 */ function rocket_plugins_to_deactivate() { $plugins = []; $plugins_explanations = []; /** * Filter the recommended plugins to deactivate to prevent conflicts * * @since 2.6.4 * * @param array $plugins List of recommended plugins to deactivate. */ $plugins = apply_filters( 'rocket_plugins_to_deactivate', $plugins ); /** * Filter the recommended plugins to deactivate explanations * * @since 3.10.5 * * @param array $plugins List of recommended plugins to deactivate explanations. */ $plugins_explanations = apply_filters( 'rocket_plugins_to_deactivate_explanations', $plugins_explanations ); $plugins = array_filter( $plugins, 'is_plugin_active' ); if ( current_user_can( 'rocket_manage_options' ) && count( $plugins ) && rocket_valid_key() ) { // translators: %s is WP Rocket plugin name. $warning = '<p>' . sprintf( __( '<strong>%s</strong>: The following plugins are not compatible with this plugin and may cause unexpected results:', 'rocket' ), WP_ROCKET_PLUGIN_NAME ) . '</p>'; $warning .= '<ul class="rocket-plugins-error">'; foreach ( $plugins as $k => $plugin ) { $plugin_data = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin ); $warning .= '<li><b>' . $plugin_data['Name'] . '</b>' . ( isset( $plugins_explanations[ $k ] ) ? ' - ' . $plugins_explanations[ $k ] : '' ) . '</span> <a href="' . wp_nonce_url( admin_url( 'admin-post.php?action=deactivate_plugin&plugin=' . rawurlencode( $plugin ) ), 'deactivate_plugin' ) . '" class="button-secondary alignright">' . __( 'Deactivate', 'rocket' ) . '</a></li>'; } $warning .= '</ul>'; rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $warning, ] ); } } add_action( 'admin_notices', 'rocket_plugins_to_deactivate' ); /** * Displays a warning if Rocket Footer JS plugin is active * * @since 3.2.3 * @author Remy Perona * * @return void */ function rocket_warning_footer_js_plugin() { $screen = get_current_screen(); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } if ( ! is_plugin_active( 'rocket-footer-js/rocket-footer-js.php' ) ) { return; } rocket_notice_html( [ 'status' => 'warning', 'message' => __( 'WP Rocket Footer JS is not an official add-on. It prevents some options in WP Rocket from working correctly. Please deactivate it if you have problems.', 'rocket' ), 'dismiss_button' => true, ] ); } add_action( 'admin_notices', 'rocket_warning_footer_js_plugin' ); /** * Display a warning if Endurance Cache is not disabled * * @since 3.3.7 * @author Remy Perona * * @return void */ function rocket_warning_endurance_cache() { $screen = get_current_screen(); // This filter is documented in inc/admin-bar.php. if ( ! current_user_can( apply_filters( 'rocket_capacity', 'manage_options' ) ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } if ( ! class_exists( 'Endurance_Page_Cache' ) ) { return; } if ( 0 === (int) get_option( 'endurance_cache_level' ) ) { return; } rocket_notice_html( [ 'status' => 'error', 'message' => sprintf( // translators: %1$s = opening link tag, %2$s = closing link tag. __( 'Endurance Cache is currently enabled, which will conflict with WP Rocket Cache. Please set the Endurance Cache cache level to Off (Level 0) on the %1$sSettings > General%2$s page to prevent any issues.', 'rocket' ), '<a href="' . admin_url( 'options-general.php#epc_settings' ) . '">', '</a>' ), ] ); } add_action( 'admin_notices', 'rocket_warning_endurance_cache' ); /** * This warning is displayed when there is no permalink structure in the configuration. * * @since 1.0 */ function rocket_warning_using_permalinks() { if ( current_user_can( 'rocket_manage_options' ) && ! $GLOBALS['wp_rewrite']->using_permalinks() && rocket_valid_key() ) { $message = sprintf( /* translators: %1$s WP Rocket plugin name; %2$s = opening link; %3$s = closing link */ __( '%1$s: A custom permalink structure is required for the plugin to work properly. %2$sGo to permalinks settings%3$s', 'rocket' ), '<strong>' . WP_ROCKET_PLUGIN_NAME . '</strong>', '<a href="' . admin_url( 'options-permalink.php' ) . '">', '</a>' ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, ] ); } } add_action( 'admin_notices', 'rocket_warning_using_permalinks' ); /** * This warning is displayed when the .htaccess file doesn't exist or isn't writeable * * @since 1.0 */ function rocket_warning_htaccess_permissions() { global $is_apache; $htaccess_file = get_home_path() . '.htaccess'; if ( ! current_user_can( 'rocket_manage_options' ) || ( rocket_direct_filesystem()->is_writable( $htaccess_file ) ) || ! $is_apache // This filter is documented in inc/functions/htaccess.php. || apply_filters( 'rocket_disable_htaccess', false ) || ! rocket_valid_key() ) { return; } if ( rocket_check_htaccess_rules() ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = sprintf( // translators: %s = plugin name. __( '%s could not modify the .htaccess file due to missing writing permissions.', 'rocket' ), '<strong>' . WP_ROCKET_PLUGIN_NAME . '</strong>' ); $message .= '<br>' . sprintf( /* translators: This is a doc title! %1$s = opening link; %2$s = closing link */ __( 'Troubleshoot: %1$sHow to make system files writeable%2$s', 'rocket' ), /* translators: Documentation exists in EN, DE, FR, ES, IT; use loaclised URL if applicable */ '<a href="' . __( 'https://docs.wp-rocket.me/article/626-how-to-make-system-files-htaccess-wp-config-writeable/?utm_source=wp_plugin&utm_medium=wp_rocket', 'rocket' ) . '" target="_blank">', '</a>' ); add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 42 ); $message .= '<p>' . __( 'Don’t worry, WP Rocket’s page caching and settings will still function correctly.', 'rocket' ) . '<br>' . __( 'For optimal performance, adding the following lines into your .htaccess is recommended (not required):', 'rocket' ) . '<br><textarea readonly="readonly" id="rocket_htaccess_rules" name="rocket_htaccess_rules" class="large-text readonly" rows="6">' . esc_textarea( get_rocket_htaccess_marker() ) . '</textarea></p>'; remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 42 ); rocket_notice_html( [ 'status' => 'warning', 'dismissible' => '', 'message' => $message, 'dismiss_button' => __FUNCTION__, ] ); } add_action( 'admin_notices', 'rocket_warning_htaccess_permissions' ); /** * This warning is displayed when the config dir isn't writeable * * @since 2.0.2 */ function rocket_warning_config_dir_permissions() { if ( current_user_can( 'rocket_manage_options' ) && ( ! rocket_direct_filesystem()->is_writable( WP_ROCKET_CONFIG_PATH ) ) && rocket_valid_key() ) { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = rocket_notice_writing_permissions( trim( str_replace( ABSPATH, '', WP_ROCKET_CONFIG_PATH ), '/' ) ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, ] ); } } add_action( 'admin_notices', 'rocket_warning_config_dir_permissions' ); /** * This warning is displayed when the cache dir isn't writeable * * @since 1.0 */ function rocket_warning_cache_dir_permissions() { if ( current_user_can( 'rocket_manage_options' ) && ( ! rocket_direct_filesystem()->is_writable( WP_ROCKET_CACHE_PATH ) ) && rocket_valid_key() ) { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = rocket_notice_writing_permissions( trim( str_replace( ABSPATH, '', WP_ROCKET_CACHE_PATH ), '/' ) ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, ] ); } } add_action( 'admin_notices', 'rocket_warning_cache_dir_permissions' ); /** * This warning is displayed when the minify cache dir isn't writeable * * @since 2.1 */ function rocket_warning_minify_cache_dir_permissions() { if ( current_user_can( 'rocket_manage_options' ) && ( ! rocket_direct_filesystem()->is_writable( WP_ROCKET_MINIFY_CACHE_PATH ) ) && ( get_rocket_option( 'minify_css', false ) || get_rocket_option( 'minify_js', false ) ) && rocket_valid_key() ) { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = rocket_notice_writing_permissions( trim( str_replace( ABSPATH, '', WP_ROCKET_MINIFY_CACHE_PATH ), '/' ) ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, ] ); } } add_action( 'admin_notices', 'rocket_warning_minify_cache_dir_permissions' ); /** * This warning is displayed when the busting cache dir isn't writeable * * @since 2.9 */ function rocket_warning_busting_cache_dir_permissions() { if ( current_user_can( 'rocket_manage_options' ) && ( ! rocket_direct_filesystem()->is_writable( WP_ROCKET_CACHE_BUSTING_PATH ) ) && rocket_valid_key() ) { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = rocket_notice_writing_permissions( trim( str_replace( ABSPATH, '', WP_ROCKET_CACHE_BUSTING_PATH ), '/' ) ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, ] ); } } add_action( 'admin_notices', 'rocket_warning_busting_cache_dir_permissions' ); /** * Confirming notice when the site has been added * * @since 2.2 */ function rocket_thank_you_license() { if ( '1' === get_rocket_option( 'license' ) ) { $options = get_option( WP_ROCKET_SLUG ); $options['license'] = time(); $options['ignore'] = true; update_option( WP_ROCKET_SLUG, $options ); $message = sprintf( /* translators: %1$s = plugin name, %2$s + %3$s = opening links, %4$s = closing link */ __( '%1$s is good to go! %2$sTest your load time%4$s, or visit your %3$ssettings%4$s.', 'rocket' ), '<strong>' . WP_ROCKET_PLUGIN_NAME . '</strong>', '<a href="https://wp-rocket.me/blog/how-to-test-wordpress-site-performance-measure-speed-results/?utm_source=wp_plugin&utm_medium=wp_rocket" target="_blank">', '<a href="' . admin_url( 'options-general.php?page=' . WP_ROCKET_PLUGIN_SLUG ) . '">', '</a>' ); rocket_notice_html( [ 'message' => $message ] ); } } add_action( 'admin_notices', 'rocket_thank_you_license' ); /** * Displays a notice for analytics opt-in * * @since 2.11 * @author Remy Perona */ function rocket_analytics_optin_notice() { $screen = get_current_screen(); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } if ( 1 === (int) get_option( 'rocket_analytics_notice_displayed' ) ) { return; } if ( get_rocket_option( 'analytics_enabled' ) ) { return; } $analytics_notice = sprintf( // Opening <p> provided by rocket_notice_html(). '<strong>%1$s</strong><br>%2$s</p>', __( 'Would you allow WP Rocket to collect non-sensitive diagnostic data from this website?', 'rocket' ), __( 'This would help us to improve WP Rocket for you in the future.', 'rocket' ) ); $analytics_notice .= sprintf( '<p><button class="hide-if-no-js button-rocket-reveal rocket-preview-analytics-data">%s</button></p>', /* translators: button text, click will expand data collection preview */ __( 'What info will we collect?', 'rocket' ) ); $analytics_notice .= sprintf( '<div class="rocket-analytics-data-container"><p class="description">%1$s</p>%2$s</div>', __( 'Below is a detailed view of all data WP Rocket will collect if granted permission. WP Rocket will never transmit any domain names or email addresses (except for license validation), IP addresses, or third-party API keys.', 'rocket' ), rocket_data_collection_preview_table() ); $analytics_notice .= sprintf( '<p><a href="%1$s" class="button button-primary">%2$s</a> <a href="%3$s" class="button button-secondary">%4$s</a>', // Closing </p> provided by rocket_notice_html(). wp_nonce_url( admin_url( 'admin-post.php?action=rocket_analytics_optin&value=yes' ), 'analytics_optin' ), /* translators: button text for data collection opt-in */ __( 'Yes, allow', 'rocket' ), wp_nonce_url( admin_url( 'admin-post.php?action=rocket_analytics_optin&value=no' ), 'analytics_optin' ), /* translators: button text for data collection opt-in */ __( 'No, thanks', 'rocket' ) ); // Status should be as neutral as possible; nothing has happened yet. rocket_notice_html( [ 'status' => 'info', 'message' => $analytics_notice, ] ); } add_action( 'admin_notices', 'rocket_analytics_optin_notice' ); /** * Displays a notice after analytics opt-in * * @since 2.11 * @author Remy Perona */ function rocket_analytics_optin_thankyou_notice() { $screen = get_current_screen(); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } $analytics_optin = get_transient( 'rocket_analytics_optin' ); if ( ! $analytics_optin ) { return; } $thankyou_message = sprintf( // Opening <p> provided by rocket_notice_html(). '<strong>%s</strong></p>', __( 'Thank you!', 'rocket' ) ); $thankyou_message .= sprintf( '<p>%1$s</p><div>%2$s</div>', __( 'WP Rocket now collects these metrics from your website:', 'rocket' ), rocket_data_collection_preview_table() ); // Closing </p> provided by rocket_notice_html(). $thankyou_message .= '<p>'; rocket_notice_html( [ 'message' => $thankyou_message, ] ); delete_transient( 'rocket_analytics_optin' ); } add_action( 'admin_notices', 'rocket_analytics_optin_thankyou_notice' ); /** * Displays a notice after clearing the cache * * @since 2.11 * @author Remy Perona */ function rocket_clear_cache_notice() { $cleared_cache = get_transient( 'rocket_clear_cache' ); if ( ! $cleared_cache ) { return; } delete_transient( 'rocket_clear_cache' ); $notice = ''; switch ( $cleared_cache ) { case 'all': if ( current_user_can( 'rocket_purge_cache' ) ) { // translators: %s = plugin name. $notice = sprintf( __( '%s: Cache cleared.', 'rocket' ), '<strong>' . WP_ROCKET_PLUGIN_NAME . '</strong>' ); $notice .= '<em> (' . date_i18n( get_option( 'date_format' ) ) . ' @ ' . date_i18n( get_option( 'time_format' ) ) . ') </em>'; } break; case 'post': if ( current_user_can( 'rocket_purge_posts' ) ) { // translators: %s = plugin name. $notice = sprintf( __( '%s: Post cache cleared.', 'rocket' ), '<strong>' . WP_ROCKET_PLUGIN_NAME . '</strong>' ); $notice .= '<em> (' . date_i18n( get_option( 'date_format' ) ) . ' @ ' . date_i18n( get_option( 'time_format' ) ) . ') </em>'; } break; case 'term': if ( current_user_can( 'rocket_purge_terms' ) ) { // translators: %s = plugin name. $notice = sprintf( __( '%s: Term cache cleared.', 'rocket' ), '<strong>' . WP_ROCKET_PLUGIN_NAME . '</strong>' ); $notice .= '<em> (' . date_i18n( get_option( 'date_format' ) ) . ' @ ' . date_i18n( get_option( 'time_format' ) ) . ') </em>'; } break; case 'user': if ( current_user_can( 'rocket_purge_users' ) ) { // translators: %s = plugin name). $notice = sprintf( __( '%s: User cache cleared.', 'rocket' ), '<strong>' . WP_ROCKET_PLUGIN_NAME . '</strong>' ); $notice .= '<em> (' . date_i18n( get_option( 'date_format' ) ) . ' @ ' . date_i18n( get_option( 'time_format' ) ) . ') </em>'; } break; default: break; } if ( empty( $notice ) ) { return; } rocket_notice_html( [ 'message' => $notice, ] ); } add_action( 'admin_notices', 'rocket_clear_cache_notice' ); /** * Outputs notice HTML * * @since 2.11 * @author Remy Perona * * @param array $args An array of arguments used to determine the notice output. * @return void */ function rocket_notice_html( $args ) { $defaults = [ 'status' => 'success', 'dismissible' => 'is-dismissible', 'message' => '', 'action' => '', 'dismiss_button' => false, 'dismiss_button_message' => __( 'Dismiss this notice', 'rocket' ), 'readonly_content' => '', 'id' => '', ]; $args = wp_parse_args( $args, $defaults ); switch ( $args['action'] ) { case 'clear_cache': $args['action'] = '<a class="wp-core-ui button" href="' . wp_nonce_url( admin_url( 'admin-post.php?action=purge_cache&type=all' ), 'purge_cache_all' ) . '">' . __( 'Clear cache', 'rocket' ) . '</a>'; break; case 'clear_used_css': $params = [ 'action' => 'rocket_clean_saas', ]; if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { $referer_url = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ); $params['_wp_http_referer'] = rawurlencode( $referer_url ); } $args['action'] = '<a class="wp-core-ui button" href="' . add_query_arg( $params, wp_nonce_url( admin_url( 'admin-post.php' ), $params['action'] ) ) . '">' . __( 'Clear Used CSS', 'rocket' ) . '</a>'; break; case 'stop_preload': $args['action'] = '<a class="wp-core-ui button" href="' . wp_nonce_url( admin_url( 'admin-post.php?action=rocket_stop_preload&type=all' ), 'rocket_stop_preload' ) . '">' . __( 'Stop Preload', 'rocket' ) . '</a>'; break; case 'switch_to_rucss': $params = [ 'action' => 'switch_to_rucss', ]; $args['action'] = '<a class="wp-core-ui button" href="' . add_query_arg( $params, wp_nonce_url( admin_url( 'admin-post.php' ), 'rucss_switch' ) ) . '">' . __( 'Turn on Remove Unused CSS', 'rocket' ) . '</a>'; break; case 'enable_separate_mobile_cache': $params = [ 'action' => 'rocket_enable_separate_mobile_cache', ]; $args['action'] = '<a class="wp-core-ui button" href="' . add_query_arg( $params, wp_nonce_url( admin_url( 'admin-post.php' ), 'rocket_enable_separate_mobile_cache' ) ) . '">' . __( 'Enable “Separate Cache Files for Mobile Devices” now', 'rocket' ) . '</a>'; break; case 'force_deactivation': /** * Allow a "force deactivation" link to be printed, use at your own risks * * @since 2.0.0 * * @param bool $permit_force_deactivation true will print the link. */ $permit_force_deactivation = apply_filters( 'rocket_permit_force_deactivation', true ); // We add a link to permit "force deactivation", use at your own risks. if ( $permit_force_deactivation ) { global $status, $page, $s; $plugin_file = 'wp-rocket/wp-rocket.php'; $rocket_nonce = wp_create_nonce( 'force_deactivation' ); $args['action'] = '<a href="' . wp_nonce_url( 'plugins.php?action=deactivate&rocket_nonce=' . $rocket_nonce . '&plugin=' . $plugin_file . '&plugin_status=' . $status . '&paged=' . $page . '&s=' . $s, 'deactivate-plugin_' . $plugin_file ) . '">' . __( 'Force deactivation ', 'rocket' ) . '</a>'; } break; } /** * Notice arguments. * * @param array $args arguments from the notice. * @return array */ $filtered_args = apply_filters( 'rocket_notice_args', $args ); if ( is_array( $filtered_args ) ) { $args = wp_parse_args( $filtered_args, $defaults ); } $notice_id = ''; if ( ! empty( $args['id'] ) ) { $notice_id = ' id="' . esc_attr( $args['id'] ) . '"'; } ?> <div class="notice notice-<?php echo esc_attr( $args['status'] ); ?> <?php echo esc_attr( $args['dismissible'] ); ?>"<?php echo $notice_id; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>> <?php $tag = 0 !== strpos( $args['message'], '<p' ) && 0 !== strpos( $args['message'], '<ul' ); echo ( $tag ? '<p>' : '' ) . $args['message'] . ( $tag ? '</p>' : '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. ?> <?php if ( ! empty( $args['readonly_content'] ) ) : ?> <p><?php esc_html_e( 'The following code should have been written to this file:', 'rocket' ); ?> <br><textarea readonly="readonly" id="rules" name="rules" class="large-text readonly" rows="6"><?php echo esc_textarea( $args['readonly_content'] ); ?></textarea> </p> <?php endif; if ( $args['action'] || $args['dismiss_button'] ) : ?> <p> <?php echo $args['action']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <?php if ( $args['dismiss_button'] ) : ?> <a class="rocket-dismiss <?php echo $args['dismiss_button_class'] ?? ''; ?>" href="<?php echo wp_nonce_url( admin_url( 'admin-post.php?action=rocket_ignore&box=' . $args['dismiss_button'] ), 'rocket_ignore_' . $args['dismiss_button'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><?php echo esc_html( $args['dismiss_button_message'] ); ?></a> <?php endif; ?> </p> <?php endif; ?> </div> <?php } /** * Outputs formatted notice about issues with writing permissions * * @since 2.11 * @author Caspar Hübinger * * @param string $file File or folder name. * @return string Message HTML */ function rocket_notice_writing_permissions( $file ) { $message = sprintf( // translators: %s = plugin name. __( '%s cannot configure itself due to missing writing permissions.', 'rocket' ), '<strong>' . WP_ROCKET_PLUGIN_NAME . '</strong>' ); $message .= '<br>' . sprintf( /* translators: %s = file/folder name */ __( 'Affected file/folder: %s', 'rocket' ), '<code>' . $file . '</code>' ); $message .= '<br>' . sprintf( /* translators: This is a doc title! %1$s = opening link; %2$s = closing link */ __( 'Troubleshoot: %1$sHow to make system files writeable%2$s', 'rocket' ), /* translators: Documentation exists in EN, DE, FR, ES, IT; use loaclised URL if applicable */ '<a href="' . __( 'https://docs.wp-rocket.me/article/626-how-to-make-system-files-htaccess-wp-config-writeable/?utm_source=wp_plugin&utm_medium=wp_rocket', 'rocket' ) . '" target="_blank">', '</a>' ); return $message; } admin/admin.php 0000644 00000041001 15174677547 0007460 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Link to the configuration page of the plugin, support & documentation * * @since 1.0 * * @param array $actions Array of links to display. * @return array Updated array of links */ function rocket_settings_action_links( $actions ) { if ( ! current_user_can( 'rocket_manage_options' ) ) { return $actions; } array_unshift( $actions, sprintf( '<a href="%s">%s</a>', 'https://wp-rocket.me/support/?utm_source=wp_plugin&utm_medium=wp_rocket', __( 'Support', 'rocket' ) ) ); array_unshift( $actions, sprintf( '<a href="%s">%s</a>', get_rocket_documentation_url(), __( 'Docs', 'rocket' ) ) ); array_unshift( $actions, sprintf( '<a href="%s">%s</a>', get_rocket_faq_url(), __( 'FAQ', 'rocket' ) ) ); array_unshift( $actions, sprintf( '<a href="%s">%s</a>', admin_url( 'options-general.php?page=' . WP_ROCKET_PLUGIN_SLUG ), __( 'Settings', 'rocket' ) ) ); return $actions; } add_filter( 'plugin_action_links_' . plugin_basename( WP_ROCKET_FILE ), 'rocket_settings_action_links' ); /** * Add a link "Renew your licence" when you can't do it automatically (expired licence but new version available) * * @since 2.2 * * @param array $plugin_meta An array of the plugin's metadata, including the version, author, author URI, and plugin URI. * @param string $plugin_file Path to the plugin file, relative to the plugins directory. * @return array Updated meta content if license is expired */ function rocket_plugin_row_meta( $plugin_meta, $plugin_file ) { if ( 'wp-rocket/wp-rocket.php' === $plugin_file ) { $update_plugins = get_site_transient( 'update_plugins' ); if ( false !== $update_plugins && isset( $update_plugins->response[ $plugin_file ] ) && empty( $update_plugins->response[ $plugin_file ]->package ) ) { $link = '<span class="dashicons dashicons-update rocket-dashicons"></span> <span class="rocket-renew">Renew your licence of WP Rocket to receive access to automatic upgrades and support.</span> <a href="http://wp-rocket.me" target="_blank" class="rocket-purchase">Purchase now</a>.'; $plugin_meta = array_merge( (array) $link, $plugin_meta ); } } return $plugin_meta; } add_action( 'plugin_row_meta', 'rocket_plugin_row_meta', 10, 2 ); /** * Add a link "Purge this cache" in the post edit area * * @since 1.0 * * @param array $actions An array of row action links. * @param object $post The post object. * @return array Updated array of row action links */ function rocket_post_row_actions( $actions, $post ) { if ( ! rocket_can_display_options() ) { return $actions; } if ( ! current_user_can( 'rocket_purge_posts' ) ) { return $actions; } $cpts = get_post_types( [ 'public' => true, ], 'objects' ); /** * Filters the post type on row actions. * * @since 3.11.4 * * @param array $cpts Post Types. */ $cpts = apply_filters( 'rocket_skip_post_row_actions', $cpts ); if ( ! isset( $cpts[ $post->post_type ] ) ) { return $actions; } $url = wp_nonce_url( admin_url( 'admin-post.php?action=purge_cache&type=post-' . $post->ID ), 'purge_cache_post-' . $post->ID ); $actions['rocket_purge'] = sprintf( '<a href="%s">%s</a>', $url, __( 'Clear this cache', 'rocket' ) ); return $actions; } add_filter( 'page_row_actions', 'rocket_post_row_actions', 10, 2 ); add_filter( 'post_row_actions', 'rocket_post_row_actions', 10, 2 ); /** * Add a link "Purge this cache" in the user edit area * * @since 2.6.12 * @param array $actions An array of row action links. * @param object $user The user object. * @return array Updated array of row action links */ function rocket_user_row_actions( $actions, $user ) { if ( ! current_user_can( 'rocket_purge_users' ) || ! get_rocket_option( 'cache_logged_user', false ) ) { return $actions; } $url = wp_nonce_url( admin_url( 'admin-post.php?action=purge_cache&type=user-' . $user->ID ), 'purge_cache_user-' . $user->ID ); $actions['rocket_purge'] = sprintf( '<a href="%s">%s</a>', $url, __( 'Clear this cache', 'rocket' ) ); return $actions; } add_filter( 'user_row_actions', 'rocket_user_row_actions', 10, 2 ); /** * Manage the dismissed boxes. * * @since 3.6 Reverse dependency with rocket_dismiss_box(). * @since 2.4 Add a delete_transient on function name (box name). * @since 1.3.0 $args can replace $_GET when called internally. * @since 1.1.10 * * @param array $args An array of query args. Should not be used: see rocket_dismiss_box(). */ function rocket_dismiss_boxes( $args = [] ) { global $pagenow; $args = empty( $args ) ? $_GET : $args; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $args['box'], $args['action'], $args['_wpnonce'] ) ) { return; } if ( ! wp_verify_nonce( $args['_wpnonce'], "{$args['action']}_{$args['box']}" ) ) { if ( rocket_get_constant( 'DOING_AJAX' ) ) { wp_send_json( [ 'error' => 1 ] ); } else { wp_nonce_ays( '' ); } return; } if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_nonce_ays( '' ); } rocket_dismiss_box( $args['box'] ); if ( 'admin-post.php' === $pagenow ) { if ( rocket_get_constant( 'DOING_AJAX' ) ) { wp_send_json( [ 'error' => 0 ] ); } else { wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } } } add_action( 'wp_ajax_rocket_ignore', 'rocket_dismiss_boxes' ); add_action( 'admin_post_rocket_ignore', 'rocket_dismiss_boxes' ); /** * Renew the plugin modification warning on plugin de/activation * * @since 1.3.0 * * @param string $plugin plugin name. */ function rocket_dismiss_plugin_box( $plugin ) { if ( plugin_basename( WP_ROCKET_FILE ) !== $plugin ) { rocket_renew_box( 'rocket_warning_plugin_modification' ); } } add_action( 'activated_plugin', 'rocket_dismiss_plugin_box' ); add_action( 'deactivated_plugin', 'rocket_dismiss_plugin_box' ); /** * Display a prevention message when enabling or disabling a plugin can be in conflict with WP Rocket * * @since 1.3.0 */ function rocket_deactivate_plugin() { if ( ! isset( $_GET['plugin'], $_GET['_wpnonce'] ) ) { return; } if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'deactivate_plugin' ) ) { wp_nonce_ays( '' ); } if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_nonce_ays( '' ); } deactivate_plugins( sanitize_text_field( wp_unslash( $_GET['plugin'] ) ) ); wp_safe_redirect( wp_get_referer() ); die(); } add_action( 'admin_post_deactivate_plugin', 'rocket_deactivate_plugin' ); /** * This function will force the direct download of the plugin's options, compressed. * * @since 2.2 */ function rocket_do_options_export() { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_export' ) ) { wp_nonce_ays( '' ); } if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_nonce_ays( '' ); } list( $filename, $options ) = rocket_export_options(); nocache_headers(); @header( 'Content-Type: application/json' ); @header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); @header( 'Content-Transfer-Encoding: binary' ); @header( 'Content-Length: ' . strlen( $options ) ); @header( 'Connection: close' ); echo $options; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped exit(); } add_action( 'admin_post_rocket_export', 'rocket_do_options_export' ); if ( ! defined( 'DOING_AJAX' ) && ! defined( 'DOING_AUTOSAVE' ) ) { add_action( 'admin_init', 'rocket_init_cache_dir' ); add_action( 'admin_init', 'rocket_maybe_generate_advanced_cache_file' ); add_action( 'admin_init', 'rocket_maybe_generate_config_files' ); } /** * Regenerate the advanced-cache.php file if an issue is detected. * * @since 2.6 */ function rocket_maybe_generate_advanced_cache_file() { if ( ! defined( 'WP_ROCKET_ADVANCED_CACHE' ) || ( defined( 'WP_ROCKET_ADVANCED_CACHE_PROBLEM' ) && WP_ROCKET_ADVANCED_CACHE_PROBLEM ) ) { rocket_generate_advanced_cache_file(); } } /** * Regenerate config file if an issue is detected. * * @since 2.6.5 */ function rocket_maybe_generate_config_files() { $home = get_rocket_parse_url( rocket_get_home_url() ); $path = ( ! empty( $home['path'] ) ) ? str_replace( '/', '.', untrailingslashit( $home['path'] ) ) : ''; if ( ! file_exists( WP_ROCKET_CONFIG_PATH . strtolower( $home['host'] ) . $path . '.php' ) ) { rocket_generate_config_file(); } } /** * Gets all data to send to the analytics system * * @since 3.0 Send CDN zones, sitemaps paths, and count the number of CDN URLs used * @since 2.11 * @author Remy Perona * * @return mixed An array of data, or false if WP Rocket options is not an array */ function rocket_analytics_data() { global $wp_version, $is_nginx, $is_apache, $is_iis7, $is_IIS; if ( ! is_array( get_option( WP_ROCKET_SLUG ) ) ) { return false; } $untracked_wp_rocket_options = [ 'license' => 1, 'consumer_email' => 1, 'consumer_key' => 1, 'secret_key' => 1, 'secret_cache_key' => 1, 'minify_css_key' => 1, 'minify_js_key' => 1, 'cloudflare_email' => 1, 'cloudflare_api_key' => 1, 'cloudflare_zone_id' => 1, 'cloudflare_old_settings' => 1, 'submit_optimize' => 1, 'analytics_enabled' => 1, ]; $theme = wp_get_theme(); $data = array_diff_key( get_option( WP_ROCKET_SLUG ), $untracked_wp_rocket_options ); $locale = explode( '_', get_locale() ); $data['web_server'] = 'Unknown'; if ( $is_nginx ) { $data['web_server'] = 'NGINX'; } elseif ( $is_apache ) { $data['web_server'] = 'Apache'; } elseif ( $is_iis7 ) { $data['web_server'] = 'IIS 7'; } elseif ( $is_IIS ) { $data['web_server'] = 'IIS'; } $data['php_version'] = preg_replace( '@^(\d\.\d+).*@', '\1', phpversion() ); $data['wordpress_version'] = preg_replace( '@^(\d\.\d+).*@', '\1', $wp_version ); $data['current_theme'] = $theme->get( 'Name' ); $data['active_plugins'] = rocket_get_active_plugins(); $data['locale'] = $locale[0]; $data['multisite'] = is_multisite(); if ( ! empty( $data['cdn_cnames'] ) && is_array( $data['cdn_cnames'] ) ) { $data['cdn_cnames'] = count( $data['cdn_cnames'] ); } else { $data['cdn_cnames'] = 0; } $customer_data = get_transient( 'wp_rocket_customer_data' ); $data['license_type'] = ''; if ( false !== $customer_data ) { $data['license_type'] = rocket_get_license_type( $customer_data ); } $media_font_data = get_transient( 'rocket_fonts_data_collection' ); if ( false !== $media_font_data ) { $data = array_merge( $data, $media_font_data ); } return $data; } /** * Determines if we should send the analytics data * * @since 2.11 * @author Remy Perona * * @return bool True if we should send them, false otherwise */ function rocket_send_analytics_data() { if ( ! get_rocket_option( 'analytics_enabled' ) ) { return false; } if ( ! current_user_can( 'rocket_manage_options' ) ) { return false; } if ( false === get_transient( 'rocket_send_analytics_data' ) ) { set_transient( 'rocket_send_analytics_data', 1, 7 * DAY_IN_SECONDS ); return true; } return false; } /** * Handles the analytics opt-in notice selection and prevent further display * * @since 2.11 * @author Remy Perona */ function rocket_analytics_optin() { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'analytics_optin' ) ) { wp_nonce_ays( '' ); } if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_safe_redirect( wp_get_referer() ); die(); } if ( isset( $_GET['value'] ) && 'yes' === $_GET['value'] ) { update_rocket_option( 'analytics_enabled', 1 ); set_transient( 'rocket_analytics_optin', 1 ); } update_option( 'rocket_analytics_notice_displayed', 1 ); wp_safe_redirect( wp_get_referer() ); die(); } add_action( 'admin_post_rocket_analytics_optin', 'rocket_analytics_optin' ); /** * Handle WP Rocket settings import. * * @since 3.10 disable async_css if both async_css and remove_unused_css are enabled * @since 3.0 Hooked on admin_post now * @since 2.10.7 * @author Remy Perona * * @return void */ function rocket_handle_settings_import() { check_ajax_referer( 'rocket_import_settings', 'rocket_import_settings_nonce' ); if ( ! current_user_can( 'rocket_manage_options' ) ) { rocket_settings_import_redirect( __( 'Settings import failed: you do not have the permissions to do this.', 'rocket' ), 'error' ); } if ( ! isset( $_FILES['import'] ) || ( isset( $_FILES['import']['size'] ) && 0 === $_FILES['import']['size'] ) ) { rocket_settings_import_redirect( __( 'Settings import failed: no file uploaded.', 'rocket' ), 'error' ); } if ( isset( $_FILES['import']['name'] ) && ! preg_match( '/wp-rocket-settings(?:-.*)?-20\d{2}-\d{2}-\d{2}-[a-f0-9]{13}\.(?:txt|json)/', sanitize_file_name( $_FILES['import']['name'] ) ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash rocket_settings_import_redirect( __( 'Settings import failed: incorrect filename.', 'rocket' ), 'error' ); } add_filter( 'mime_types', 'rocket_allow_json_mime_type' ); add_filter( 'wp_check_filetype_and_ext', 'rocket_check_json_filetype', 10, 4 ); $mimes = get_allowed_mime_types(); $mimes = rocket_allow_json_mime_type( $mimes ); $file_data = wp_check_filetype_and_ext( $_FILES['import']['tmp_name'], sanitize_file_name( $_FILES['import']['name'] ), $mimes ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotValidated if ( 'text/plain' !== $file_data['type'] && 'application/json' !== $file_data['type'] ) { rocket_settings_import_redirect( __( 'Settings import failed: incorrect filetype.', 'rocket' ), 'error' ); } $_post_action = isset( $_POST['action'] ) ? wp_unslash( sanitize_key( $_POST['action'] ) ) : ''; $_POST['action'] = 'wp_handle_sideload'; $overrides = []; $overrides['mimes'] = $mimes; $file = wp_handle_sideload( $_FILES['import'], $overrides ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash if ( isset( $file['error'] ) ) { rocket_settings_import_redirect( __( 'Settings import failed: ', 'rocket' ) . $file['error'], 'error' ); } $_POST['action'] = $_post_action; $settings = rocket_direct_filesystem()->get_contents( $file['file'] ); remove_filter( 'mime_types', 'rocket_allow_json_mime_type' ); remove_filter( 'wp_check_filetype_and_ext', 'rocket_check_json_filetype', 10 ); if ( 'text/plain' === $file_data['type'] ) { $gz = 'gz' . strrev( 'etalfni' ); $settings = $gz( $settings ); $settings = maybe_unserialize( $settings ); } elseif ( 'application/json' === $file_data['type'] ) { $settings = json_decode( $settings, true ); if ( null === $settings ) { rocket_settings_import_redirect( __( 'Settings import failed: unexpected file content.', 'rocket' ), 'error' ); } } rocket_put_content( $file['file'], '' ); rocket_direct_filesystem()->delete( $file['file'] ); if ( is_array( $settings ) ) { $options_api = new WP_Rocket\Admin\Options( 'wp_rocket_' ); $current_options = $options_api->get( 'settings', [] ); $regenerate_configs = false; $settings['consumer_key'] = $current_options['consumer_key']; $settings['consumer_email'] = $current_options['consumer_email']; $settings['secret_key'] = $current_options['secret_key']; $settings['secret_cache_key'] = $current_options['secret_cache_key']; $settings['minify_css_key'] = $current_options['minify_css_key']; $settings['minify_js_key'] = $current_options['minify_js_key']; $settings['version'] = $current_options['version']; if ( isset( $settings['async_css'] ) && $settings['async_css'] && isset( $settings['remove_unused_css'] ) && $settings['remove_unused_css'] ) { $settings['async_css'] = 0; } if ( ! empty( $settings['cache_webp'] ) && apply_filters( 'rocket_disable_webp_cache', false ) ) { $settings['cache_webp'] = 0; } if ( $settings['cache_mobile'] && ! $settings['do_caching_mobile_files'] ) { $settings['do_caching_mobile_files'] = 1; $regenerate_configs = true; } $options_api->set( 'settings', $settings ); /** * Fires after imported settings have been saved. * * @since 3.16 * * @param boolean $regenerate_configs Returns whether to regenerate config. */ do_action( 'rocket_after_save_import', $regenerate_configs ); rocket_settings_import_redirect( __( 'Settings imported and saved.', 'rocket' ), 'updated' ); } } add_action( 'admin_post_rocket_import_settings', 'rocket_handle_settings_import' ); admin/options.php 0000644 00000022177 15174677547 0010100 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * When our settings are saved: purge, flush, preload! * * @since 1.0 * * When the settings menu is hidden, redirect on the main settings page to avoid the same thing * (Only when a form is sent from our options page ) * * @since 2.1 * * @param array $oldvalue An array of previous values for the settings. * @param array $value An array of submitted values for the settings. */ function rocket_after_save_options( $oldvalue, $value ) { if ( ! is_array( $oldvalue ) || ! is_array( $value ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ( array_key_exists( 'minify_js', $oldvalue ) && array_key_exists( 'minify_js', $value ) && $oldvalue['minify_js'] !== $value['minify_js'] ) || ( array_key_exists( 'exclude_js', $oldvalue ) && array_key_exists( 'exclude_js', $value ) && $oldvalue['exclude_js'] !== $value['exclude_js'] ) || ( array_key_exists( 'cdn', $oldvalue ) && array_key_exists( 'cdn', $value ) && $oldvalue['cdn'] !== $value['cdn'] ) || ( array_key_exists( 'cdn_cnames', $oldvalue ) && array_key_exists( 'cdn_cnames', $value ) && $oldvalue['cdn_cnames'] !== $value['cdn_cnames'] ) ) { rocket_clean_minify( 'js' ); } // Regenerate advanced-cache.php file. if ( ! empty( $_POST ) // phpcs:ignore WordPress.Security.NonceVerification.Missing && ( ( isset( $oldvalue['do_caching_mobile_files'] ) && ! isset( $value['do_caching_mobile_files'] ) ) || ( ! isset( $oldvalue['do_caching_mobile_files'] ) && isset( $value['do_caching_mobile_files'] ) ) || ( isset( $oldvalue['do_caching_mobile_files'], $value['do_caching_mobile_files'] ) && $oldvalue['do_caching_mobile_files'] !== $value['do_caching_mobile_files'] ) || $oldvalue['cache_mobile'] !== $value['cache_mobile'] ) ) { rocket_generate_advanced_cache_file(); } // Update .htaccess file rules. flush_rocket_htaccess( ! rocket_valid_key() ); // Update config file. rocket_generate_config_file(); if ( isset( $oldvalue['analytics_enabled'], $value['analytics_enabled'] ) && $oldvalue['analytics_enabled'] !== $value['analytics_enabled'] && 1 === (int) $value['analytics_enabled'] ) { set_transient( 'rocket_analytics_optin', 1 ); } // If it's different, clean the domain. if ( rocket_create_options_hash( $value ) !== rocket_create_options_hash( $oldvalue ) ) { // Purge all cache files. rocket_clean_domain(); /** * Fires after WP Rocket options that require a cache purge have changed * * @since 3.11 * * @param array $value An array of submitted values for the settings. */ do_action( 'rocket_options_changed', $value ); } } add_action( 'update_option_' . rocket_get_constant( 'WP_ROCKET_SLUG' ), 'rocket_after_save_options', 10, 2 ); /** * Perform actions when settings are saved. * * @since 1.0 * * @param array $newvalue An array of submitted options values. * @param array $oldvalue An array of previous options values. * @return array Updated submitted options values. */ function rocket_pre_main_option( $newvalue, $oldvalue ) { $rocket_settings_errors = []; // Make sure that fields that allow users to enter patterns are well formatted. $is_form_submit = isset( $_POST['option_page'] ) ? sanitize_key( $_POST['option_page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing $is_form_submit = WP_ROCKET_PLUGIN_SLUG === $is_form_submit; $errors = []; $pattern_labels = [ 'exclude_css' => __( 'Excluded CSS Files', 'rocket' ), 'exclude_inline_js' => __( 'Excluded Inline JavaScript', 'rocket' ), 'exclude_js' => __( 'Excluded JavaScript Files', 'rocket' ), 'exclude_defer_js' => __( 'Defer JavaScript Files', 'rocket' ), 'delay_js_exclusions' => __( 'Excluded Delay JavaScript Files', 'rocket' ), 'cache_reject_uri' => __( 'Never Cache URL(s)', 'rocket' ), 'cache_reject_ua' => __( 'Never Cache User Agent(s)', 'rocket' ), 'cache_purge_pages' => __( 'Always Purge URL(s)', 'rocket' ), 'cdn_reject_files' => __( 'Exclude files from CDN', 'rocket' ), ]; // unset the *_mask value as we don't need to save them. unset( $newvalue['cloudflare_api_key_mask'], $newvalue['cloudflare_zone_id_mask'] ); foreach ( $pattern_labels as $pattern_field => $label ) { if ( empty( $newvalue[ $pattern_field ] ) ) { // The field is empty. continue; } // Sanitize. $newvalue[ $pattern_field ] = rocket_sanitize_textarea_field( $pattern_field, $newvalue[ $pattern_field ] ); // Validate. $newvalue[ $pattern_field ] = array_filter( $newvalue[ $pattern_field ], function ( $excluded ) use ( $pattern_field, $label, $is_form_submit, &$errors ) { if ( false === @preg_match( '#' . str_replace( '#', '\#', $excluded ) . '#', 'dummy-sample' ) && $is_form_submit ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged /* translators: 1 and 2 can be anything. */ $errors[ $pattern_field ][] = sprintf( __( '%1$s: <em>%2$s</em>', 'rocket' ), $label, esc_html( $excluded ) ); return false; } return true; } ); } if ( $errors ) { $error_message = _n( 'The following pattern is invalid and has been removed:', 'The following patterns are invalid and have been removed:', array_sum( array_map( 'count', $errors ) ), 'rocket' ); $error_message .= '</strong></p>'; // close out default opening tags from WP's settings_errors(). foreach ( $errors as $error ) { $error_message .= '<ul><li>' . implode( '</li><li>', $error ) . '</li></ul>'; } $error_message .= '<p><strong>'; // Re-open tags that WP's settings_errors() will close at end of notice box. $container = apply_filters( 'rocket_container', [] ); $invalid_exclusions_beacon = $container->get( 'beacon' )->get_suggest( 'invalid_exclusions' ); $error_message .= sprintf( '<a href="%1$s" data-beacon-article="%2$s" rel="noopener noreferrer" target="_blank">%3$s</a>', $invalid_exclusions_beacon['url'], $invalid_exclusions_beacon['id'], __( 'More info', 'rocket' ) ); $errors = []; $rocket_settings_errors[] = [ 'setting' => 'general', 'code' => 'invalid_patterns', 'message' => __( 'WP Rocket: ', 'rocket' ) . $error_message, 'type' => 'error', ]; } // Clear WP Rocket database optimize cron if the setting has been modified. if ( ( ( isset( $newvalue['schedule_automatic_cleanup'], $oldvalue['schedule_automatic_cleanup'] ) && $newvalue['schedule_automatic_cleanup'] !== $oldvalue['schedule_automatic_cleanup'] ) ) || ( ( isset( $newvalue['automatic_cleanup_frequency'], $oldvalue['automatic_cleanup_frequency'] ) && $newvalue['automatic_cleanup_frequency'] !== $oldvalue['automatic_cleanup_frequency'] ) ) ) { if ( wp_next_scheduled( 'rocket_database_optimization_time_event' ) ) { wp_clear_scheduled_hook( 'rocket_database_optimization_time_event' ); } } // Regenerate the minify key if JS files have been modified. // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual if ( ( isset( $newvalue['minify_js'], $oldvalue['minify_js'] ) && $newvalue['minify_js'] != $oldvalue['minify_js'] ) // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual || ( isset( $newvalue['exclude_js'], $oldvalue['exclude_js'] ) && $newvalue['exclude_js'] !== $oldvalue['exclude_js'] ) || ( ( isset( $oldvalue['cdn'] ) && ! isset( $newvalue['cdn'] ) ) || ( ! isset( $oldvalue['cdn'] ) && isset( $newvalue['cdn'] ) ) ) ) { $newvalue['minify_js_key'] = create_rocket_uniqid(); } // Checked the SSL option if the whole website is on SSL. if ( rocket_is_ssl_website() ) { $newvalue['cache_ssl'] = 1; } if ( 'local' === wp_get_environment_type() ) { $newvalue['optimize_css_delivery'] = 0; $newvalue['remove_unused_css'] = 0; $newvalue['async_css'] = 0; } if ( ! rocket_get_constant( 'WP_ROCKET_ADVANCED_CACHE' ) ) { rocket_generate_advanced_cache_file(); } $keys = get_transient( WP_ROCKET_SLUG ); if ( $keys ) { delete_transient( WP_ROCKET_SLUG ); $newvalue = array_merge( $newvalue, $keys ); } if ( ! $rocket_settings_errors ) { return $newvalue; } $transient_errors = get_transient( 'settings_errors' ); if ( ! $transient_errors || ! is_array( $transient_errors ) ) { $transient_errors = []; } $settings_errors = array_merge( $transient_errors, $rocket_settings_errors ); foreach ( $settings_errors as $error ) { add_settings_error( $error['setting'], $error['code'], $error['message'], $error['type'] ); } return $newvalue; } add_filter( 'pre_update_option_' . rocket_get_constant( 'WP_ROCKET_SLUG' ), 'rocket_pre_main_option', 10, 2 ); /** * Auto-activate the SSL cache if the website URL is updated with https protocol * * @since 2.7 * * @param array $old_value An array of previous options values. * @param array $value An array of submitted options values. */ function rocket_update_ssl_option_after_save_home_url( $old_value, $value ) { if ( $old_value === $value || get_rocket_option( 'cache_ssl' ) ) { return; } $scheme = rocket_extract_url_component( $value, PHP_URL_SCHEME ); update_rocket_option( 'cache_ssl', 'https' === $scheme ? 1 : 0 ); } add_action( 'update_option_home', 'rocket_update_ssl_option_after_save_home_url', 10, 2 ); 3rd-party/themes/studiopress.php 0000644 00000003277 15174677547 0013013 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; add_action( 'admin_init', 'rocket_clear_cache_after_studiopress_accelerator' ); /** * Clear WP Rocket cache after purged the StudioPress Accelerator cache * * @since 2.5.5 * * @return void */ function rocket_clear_cache_after_studiopress_accelerator() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( isset( $GLOBALS['sp_accel_nginx_proxy_cache_purge'] ) && is_a( $GLOBALS['sp_accel_nginx_proxy_cache_purge'], 'SP_Accel_Nginx_Proxy_Cache_Purge' ) && isset( $_REQUEST['_wpnonce'] ) ) { $nonce = $_REQUEST['_wpnonce']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Recommended if ( wp_verify_nonce( $nonce, 'sp-accel-purge-url' ) && ! empty( $_REQUEST['cache-purge-url'] ) ) { $submitted_url = sanitize_text_field( wp_unslash( $_REQUEST['cache-purge-url'] ) ); // Clear the URL. rocket_clean_files( [ $submitted_url ] ); } elseif ( wp_verify_nonce( $nonce, 'sp-accel-purge-theme' ) ) { // Clear all caching files. rocket_clean_domain(); // Preload cache. run_rocket_bot(); run_rocket_sitemap_preload(); } } } add_action( 'rocket_after_clean_domain', 'rocket_clean_studiopress_accelerator' ); /** * Call the cache server to purge the cache with StudioPress Accelerator. * * @since 2.5.5 * * @return void */ function rocket_clean_studiopress_accelerator() { if ( isset( $GLOBALS['sp_accel_nginx_proxy_cache_purge'] ) && is_a( $GLOBALS['sp_accel_nginx_proxy_cache_purge'], 'SP_Accel_Nginx_Proxy_Cache_Purge' ) ) { $GLOBALS['sp_accel_nginx_proxy_cache_purge']->cache_flush_theme(); } } 3rd-party/plugins/advanced-custom-fields.php 0000644 00000000617 15174677547 0015137 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Clear cache when ACF options page is updated. * * @since 2.10.7 * @author Remy Perona * * @param string $post_id ACF options page ID is 'options'. */ function rocket_clear_cache_on_acf_options_save( $post_id ) { if ( 'options' === $post_id ) { rocket_clean_domain(); } } add_action( 'acf/save_post', 'rocket_clear_cache_on_acf_options_save' ); 3rd-party/plugins/wp-rest-api.php 0000644 00000002511 15174677547 0012761 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Add WP REST API path to cache exclusion * * @since 2.5.12 * * @param array $uri URLs to exclude from cache. * @return array Updated URLs */ function rocket_exclude_wp_rest_api( $uri ) { global $wp_rewrite; /** * By default, don't cache the WP REST API. * * @since 2.5.12 * * @param bool false will force to cache the WP REST API */ $reject_wp_rest_api = apply_filters( 'rocket_cache_reject_wp_rest_api', true ); if ( ! $reject_wp_rest_api ) { return $uri; } /** * `(/[^/]+)?` is used instead of `(/.+)?` to match only one level. * This prevents to match a taxonomy term named `wp-json` (on multisite, the main site's posts and taxonomy archives are prefixed with `blog` => example.com/blog/category/wp-json/). */ $prefix = rocket_is_subfolder_install() ? '(/[^/]+)?' : ''; $index = ! empty( $wp_rewrite->index ) ? $wp_rewrite->index : 'index.php'; $index = preg_quote( $index, '/' ); $suffix = rest_get_url_prefix(); $suffix = preg_quote( trim( $suffix, '/' ), '/' ); /** * Results in: * - Single site: (/index\.php)?/wp\-json(/.*|$) * - Multisite: (/[^/]+)?(/index\.php)?/wp\-json(/.*|$) */ $uri[] = $prefix . '/(' . $index . '/)?(.*)' . $suffix . '(/.*|$)'; return $uri; } add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_wp_rest_api' ); 3rd-party/plugins/rating/kk-star-ratings.php 0000644 00000000617 15174677547 0015123 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( class_exists( 'BhittaniPlugin_kkStarRatings' ) ) : /** * Conflict with kk Star Ratings: Clear the cache when a post gets rated. * * @since 2.5.3 * * @param int $post_id Post ID. */ function rocket_clear_cache_on_kksr_rate( $post_id ) { rocket_clean_post( $post_id ); } add_action( 'kksr_rate', 'rocket_clear_cache_on_kksr_rate' ); endif; 3rd-party/plugins/rating/wp-postratings.php 0000644 00000000711 15174677547 0015076 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( defined( 'WP_POSTRATINGS_VERSION' ) ) : /** * Conflict with WP-PostRatings: Clear the cache when a post gets rated. * * @since 2.6.6 * * @param int $user_id user ID. * @param int $post_id post ID. */ function rocket_clear_cache_on_wp_postratings_rate( $user_id, $post_id ) { rocket_clean_post( $post_id ); } add_action( 'rate_post', 'rocket_clear_cache_on_wp_postratings_rate', 10, 2 ); endif; 3rd-party/plugins/envira-gallery.php 0000644 00000001654 15174677547 0013541 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Conflict with Envira Gallery: don't apply LazyLoad on all images * * @since 2.3.10 * * @param string $attr Envira gallery image attributes. * @return string Updated attributes */ function rocket_deactivate_lazyload_on_envira_gallery( $attr ) { return $attr . ' data-no-lazy="1" '; } add_filter( 'envira_gallery_output_image_attr', 'rocket_deactivate_lazyload_on_envira_gallery', PHP_INT_MAX ); /** * Conflict with Envira Gallery: don't apply LazyLoad on all images * * @since 2.3.10 * * @param string $images Envira gallery images HTML code. * @return string Updated HTML code */ function rocket_deactivate_lazyload_on_envira_gallery_indexable_images( $images ) { $images = str_replace( '<img', '<img data-no-lazy="1" ', $images ); return $images; } add_filter( 'envira_gallery_indexable_images', 'rocket_deactivate_lazyload_on_envira_gallery_indexable_images', PHP_INT_MAX ); 3rd-party/plugins/seo/premium-seo-pack.php 0000644 00000001260 15174677547 0014555 0 ustar 00 <?php /** * Compatibility with Premium SEO Pack * * @link http://premiumseopack.com */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'psp' ) ) { /** * Dequeue the stylesheet of Premium SEO Pack on WP Rocket settings page. * * @since 2.11.6 * @author Arun Basil Lal */ function rocket_dequeue_premium_seo_pack_stylesheet() { // Return on all pages but WP Rocket settings page. $screen = get_current_screen(); if ( 'settings_page_wprocket' !== $screen->id ) { return; } // Dequeueing this stylesheet unfreezes WP Rocket. wp_dequeue_style( 'psp-main-style' ); } add_action( 'admin_print_styles', 'rocket_dequeue_premium_seo_pack_stylesheet', 11 ); } 3rd-party/plugins/wp-offload-s3-assets.php 0000644 00000002151 15174677547 0014472 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( is_admin() && function_exists( 'as3cf_assets_init' ) ) : add_action( 'aws_init', 'rocket_as3cf_assets_compatibility', 13 ); add_action( 'update_option_as3cf_assets', 'rocket_maybe_deactivate_cdn', 10, 2 ); endif; /** * Compatibility with WP Offload S3 assets addon. * * @since 2.10.7 * @author Remy Perona */ function rocket_as3cf_assets_compatibility() { global $as3cf_assets; if ( isset( $as3cf_assets ) && $as3cf_assets->is_plugin_setup() && 1 === (int) $as3cf_assets->get_setting( 'enable-addon' ) ) { // Disable WP Rocket CDN option. add_filter( 'rocket_readonly_cdn_option', '__return_true' ); } } /** * Deactivate WP Rocket CDN if WP Offload S3 assets addon copy & serve is active. * * @since 2.10.7 * @author Remy Perona * * @param string $old_value Previous assets option value. * @param string $new_value New assets option value. */ function rocket_maybe_deactivate_cdn( $old_value, $new_value ) { if ( $old_value['enable-addon'] !== $new_value['enable-addon'] && 1 === (int) $new_value['enable-addon'] ) { update_rocket_option( 'cdn', 0 ); } } 3rd-party/plugins/s2member.php 0000644 00000000766 15174677547 0012337 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Excludes s2 Member dynamic file from saving as static resource * * @since 2.11.4 * @author Remy Perona * * @param array $excluded_files Array of excluded files. */ function rocket_exclude_s2member_dynamic_files( $excluded_files ) { $excluded_files[] = rocket_clean_exclude_file( plugins_url( '/s2member/s2member-o.php' ) ); return $excluded_files; } add_action( 'rocket_exclude_static_dynamic_resources', 'rocket_exclude_s2member_dynamic_files' ); 3rd-party/plugins/cookies/weepie-cookie-allow.php 0000644 00000003475 15174677547 0016120 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Compatibility with WeePie Cookie Allow * * @since 2.9 */ if ( class_exists( 'WpieCookieAllow' ) ) : add_action( 'update_option_wpca_settings_general', 'rocket_after_update_wp_cookie_allow_options', 10, 2 ); add_action( 'update_option_wpca_settings_style', 'rocket_after_update_wp_cookie_allow_options', 10, 2 ); add_action( 'update_option_wpca_settings_content', 'rocket_after_update_wp_cookie_allow_options', 10, 2 ); add_action( 'update_option_wpca_settings_consent_log', 'rocket_after_update_wp_cookie_allow_options', 10, 2 ); endif; /** * Update .htaccess & config files when the "Enabled" and "Autoblock" options are turned on. * * @since 2.9 * @author Remy Perona * * @param array $old_value Previous values for the plugin options. * @param array $value New values for the plugin options. */ function rocket_after_update_wp_cookie_allow_options( $old_value, $value ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed // clear the cache because WeePie Cookie Allow plugin settings might have been changed. rocket_clean_domain(); } /** * Add cookies when we activate the plugin. * * @since 2.9 * @author Remy Perona */ function rocket_activate_wp_cookie_allow() { // clear the cache because plugin might be enabled already before. rocket_clean_domain(); } add_action( 'activate_wp-cookie-allow/wp-cookie-allow.php', 'rocket_activate_wp_cookie_allow', 11 ); /** * Remove cookies when we deactivate the plugin * * @since 2.9 * @author Remy Perona */ function rocket_deactivate_wp_cookie_allow() { // clear the cache because the bar/box and other WeePie Cookie Allow plugin frontend HTML is not needed anymore. rocket_clean_domain(); } add_action( 'deactivate_wp-cookie-allow/wp-cookie-allow.php', 'rocket_deactivate_wp_cookie_allow', 11 ); 3rd-party/plugins/cookies/eu-cookie-law.php 0000644 00000005316 15174677547 0014714 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Compatibility with EU Cookie Law * https://wordpress.org/plugins/eu-cookie-law/ * * @since 2.7 */ if ( function_exists( 'eucookie_start' ) ) : add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_eu_cookie_law_mandatory_cookie' ); /** * Update .htaccess & config files when the "Activate" and "Autoblock" options are turned on * * @since 2.7 * * @param Array $old_value Array of previous values. * @param Array $value Array of submitted values. */ function rocket_after_update_eu_cookie_law_options( $old_value, $value ) { if ( ( isset( $old_value['enabled'], $value['enabled'] ) && ( $old_value['enabled'] === $value['enabled'] ) ) && isset( $old_value['autoblock'], $value['autoblock'] ) && $old_value['autoblock'] === $value['autoblock'] ) { return; } // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Update the config file. rocket_generate_config_file(); } add_action( 'update_option_peadig_eucookie', 'rocket_after_update_eu_cookie_law_options', 10, 2 ); // Don't add the WP Rocket rewrite rules to avoid issues. add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 58 ); endif; /** * Add mandatory cookie when we activate the plugin * * @since 2.7 */ function rocket_activate_eu_cookie_law() { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 58 ); add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_eu_cookie_law_mandatory_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_eu-cookie-law/eu-cookie-law.php', 'rocket_activate_eu_cookie_law', 11 ); /** * Remove mandatory cookie when we deactivate the plugin * * @since 2.7 */ function rocket_deactivate_eu_cookie_law() { remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 58 ); remove_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_eu_cookie_law_mandatory_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_eu-cookie-law/eu-cookie-law.php', 'rocket_deactivate_eu_cookie_law', 11 ); /** * Add the EU Cookie Law to the list of mandatory cookies before generating caching files. * * @since 2.7 * * @param Array $cookies Array of mandatory cookies. * @return Array Updated array of mandatory cookies */ function rocket_add_eu_cookie_law_mandatory_cookie( $cookies ) { $options = get_option( 'peadig_eucookie' ); if ( ! empty( $options['enabled'] ) && ! empty( $options['autoblock'] ) ) { $cookies['eu-cookie-law'] = 'euCookie'; } return $cookies; } 3rd-party/plugins/cookies/gdpr.php 0000644 00000004623 15174677547 0013207 0 ustar 00 <?php /** * Compatibility with GDPR by Trew Knowledge * * @link https://github.com/trewknowledge/GDPR/ * @link https://wordpress.org/plugins/gdpr/ */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'GDPR' ) ) { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 60 ); // Create cache version based on value set in gdpr[] cookies. add_filter( 'rocket_cache_dynamic_cookies', 'rocket_get_gdpr_dynamic_cookies' ); } /** * Return the cookie names set by GDPR plugin * * @since 3.1.4 * @author jorditarrida * * @param array $cookies array List of dynamic cookies. * @return array List of dynamic cookies with the GDPR cookie appended */ function rocket_get_gdpr_mandatory_cookies( $cookies ) { $cookies[] = 'allowed_cookies'; $cookies[] = 'consent_types'; return $cookies; } /** * Return the cookie names set by GDPR plugin * * @since 3.1.4 * @author jorditarrida * * @param array $cookies array List of dynamic cookies. * @return array List of dynamic cookies with the GDPR cookie appended */ function rocket_get_gdpr_dynamic_cookies( $cookies ) { $cookies['gdpr'] = [ 'allowed_cookies', 'consent_types', ]; return $cookies; } /** * Add dynamic cookie when GDPR plugin is activated * * @since 3.1.4 * @author jorditarrida */ function rocket_add_gdpr_mandatory_cookies() { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 60 ); // Create cache version based on value set in GDPR cookies. add_filter( 'rocket_cache_dynamic_cookies', 'rocket_get_gdpr_dynamic_cookies' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); // Clear WP Rocket cache. rocket_clean_domain(); } add_action( 'activate_gdpr/gdpr.php', 'rocket_add_gdpr_mandatory_cookies', 11 ); /** * Remove dynamic cookie when GDPR plugin is deactivated. * * @since 3.1.4 * @author jorditarrida */ function rocket_remove_gdpr_mandatory_cookies() { remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 60 ); // Delete the dynamic cookie filter. remove_filter( 'rocket_cache_dynamic_cookies', 'rocket_get_gdpr_dynamic_cookies' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); // Clear WP Rocket cache. rocket_clean_domain(); } add_action( 'deactivate_gdpr/gdpr.php', 'rocket_remove_gdpr_mandatory_cookies', 11 ); 3rd-party/plugins/cookies/cookie-notice.php 0000644 00000005023 15174677547 0014776 0 ustar 00 <?php /** * Compatibility with Cookie Notice by dFactory. * * @since 2.11.6 * @author Arun Basil Lal * @link https://wordpress.org/plugins/cookie-notice/ */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'Cookie_Notice' ) ) { // Don't add the WP Rocket rewrite rules to avoid issues. if ( ! class_exists( 'CC4R_options' ) || ! CC4R_options::rewrite_enabled() ) { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 56 ); } // Create cache version based on value set in cookie_notice_accepted cookie. add_filter( 'rocket_cache_dynamic_cookies', 'rocket_get_cookie_notice_cookie' ); } /** * Return the cookie name set by Cookie Notice plugin. * * @since 2.11.6 * @author Arun Basil Lal * * @param array $cookies List of dynamic cookies. * @return array List of dynamic cookies with the Cookie Notice cookie appended. */ function rocket_get_cookie_notice_cookie( $cookies ) { $cookies[] = 'cookie_notice_accepted'; return $cookies; } /** * Add dynamic cookie and mandatory cookie when Cookie Notice plugin is activated. * * @since 2.11.6 * @author Arun Basil Lal */ function rocket_add_cookie_notice_dynamic_cookie() { // Don't add the WP Rocket rewrite rules to avoid issues. if ( ! class_exists( 'CC4R_options' ) || ! CC4R_options::rewrite_enabled() ) { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 56 ); } // Create cache version based on value set in cookie_notice_accepted cookie. add_filter( 'rocket_cache_dynamic_cookies', 'rocket_get_cookie_notice_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); // Clear WP Rocket cache. rocket_clean_domain(); } add_action( 'activate_cookie-notice/cookie-notice.php', 'rocket_add_cookie_notice_dynamic_cookie', 11 ); /** * Remove dynamic cookie when Cookie Notice plugin is deactivated. * * @since 2.11.6 * @author Arun Basil Lal */ function rocket_remove_cookie_notice_dynamic_cookie() { if ( ! class_exists( 'CC4R_options' ) || ! CC4R_options::rewrite_enabled() ) { remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 56 ); } remove_filter( 'rocket_cache_dynamic_cookies', 'rocket_get_cookie_notice_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); // Clear WP Rocket cache. rocket_clean_domain(); } add_action( 'deactivate_cookie-notice/cookie-notice.php', 'rocket_remove_cookie_notice_dynamic_cookie', 11 ); 3rd-party/plugins/cookies/uk-cookie-consent.php 0000644 00000004255 15174677547 0015611 0 ustar 00 <?php /** * Compatibility with UK Cookie Consent. * * @since 3.2 * @author TheZoker * @link https://wordpress.org/plugins/uk-cookie-consent/ */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'CTCC_Public' ) ) { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 62 ); // Create cache version based on value set in cookie_consent_accepted cookie. add_filter( 'rocket_cache_dynamic_cookies', 'rocket_get_cookie_uk_consent_cookie' ); } /** * Return the cookie name set by UK Cookie Consent plugin. * * @since 3.2 * @author TheZoker * * @param array $cookies List of dynamic cookies. * @return array List of dynamic cookies with the UK Cookie Consent cookie appended. */ function rocket_get_cookie_uk_consent_cookie( $cookies ) { $cookies[] = 'catAccCookies'; return $cookies; } /** * Add dynamic cookie and mandatory cookie when UK Cookie Consent plugin is activated. * * @since 3.2 * @author TheZoker */ function rocket_add_uk_cookie_consent_dynamic_cookie() { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 62 ); // Create cache version based on value set in cookie_consent_accepted cookie. add_filter( 'rocket_cache_dynamic_cookies', 'rocket_get_cookie_uk_consent_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); // Clear WP Rocket cache. rocket_clean_domain(); } add_action( 'activate_uk-cookie-consent/uk-cookie-consent.php', 'rocket_add_uk_cookie_consent_dynamic_cookie', 11 ); /** * Remove dynamic cookie when Cookie Consent plugin is deactivated. * * @since 3.2 * @author TheZoker */ function rocket_remove_uk_cookie_consent_dynamic_cookie() { remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 62 ); remove_filter( 'rocket_cache_dynamic_cookies', 'rocket_get_cookie_uk_consent_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); // Clear WP Rocket cache. rocket_clean_domain(); } add_action( 'deactivate_uk-cookie-consent/uk-cookie-consent.php', 'rocket_remove_uk_cookie_consent_dynamic_cookie', 11 ); 3rd-party/plugins/wp-offload-s3.php 0000644 00000001120 15174677547 0013165 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( is_admin() && ( function_exists( 'as3cf_init' ) || function_exists( 'as3cf_pro_init' ) ) ) : add_action( 'aws_init', 'rocket_as3cf_compatibility', 12 ); endif; /** * Compatibility with WP Offload S3. * * @since 2.10.7 * @author Remy Perona */ function rocket_as3cf_compatibility() { global $as3cf; if ( isset( $as3cf ) && $as3cf->is_plugin_setup() && 1 === (int) $as3cf->get_setting( 'serve-from-s3' ) ) { // Remove images option from WP Rocket CDN dropdown settings. add_filter( 'rocket_allow_cdn_images', '__return_false' ); } } 3rd-party/plugins/i18n/polylang.php 0000644 00000011314 15174677547 0013216 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( defined( 'POLYLANG_VERSION' ) && POLYLANG_VERSION ) : /** * Conflict with Polylang: Clear the whole cache when the "The language is set from content" option is activated. * * @since 2.6.8 */ function rocket_force_clean_domain_on_polylang() { $pll = function_exists( 'PLL' ) ? PLL() : $GLOBALS['polylang']; if ( isset( $pll ) && 0 === $pll->options['force_lang'] ) { rocket_clean_cache_dir(); } } add_action( 'rocket_after_clean_domain', 'rocket_force_clean_domain_on_polylang' ); // Filter mandatory cookies and WP Rocket rewrite rules if Polylang module 'Detect browser language' is enabled. if ( function_exists( 'PLL' ) && PLL()->options['browser'] ) { // Add Polylang's language cookie as a mandatory cookie. add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_polylang_mandatory_cookie' ); // Remove WP Rocket rewrite rules from .htaccess file. add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 74 ); } endif; /** * Add Polylang's language cookie to the mandatory cookies of WP Rocket. * * Polylang saves the users preferred language in this cookie by detecting browser language or by user choice * Adding this as a mandatory cookie prevents WP Rocket from serving the cache when the cookie is not set. * * @param array $cookies Array with mandatory cookies. * @return (array) Array of mandatory cookies with the Polylang cookie appended * * @author Arun Basil Lal * @since 3.0.5 */ function rocket_add_polylang_mandatory_cookie( $cookies ) { $cookies[] = defined( 'PLL_COOKIE' ) ? PLL_COOKIE : 'pll_language'; return $cookies; } /** * Add mandatory cookie to WP Rocket config and remove rewrite rules from .htaccess on Polylang activation. * * Add mandatory cookie only if the Polylang module 'Detect browser language' is active. * Also purge the homepage cache. * * @author Arun Basil Lal * @since 3.0.5 */ function rocket_activate_polylang() { // Read Polylang settings from db. $polylang_settings = get_option( 'polylang' ); if ( isset( $polylang_settings['browser'] ) && ( 1 === (int) $polylang_settings['browser'] ) ) { // Add Polylang's language cookie as a mandatory cookie. add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_polylang_mandatory_cookie' ); // Remove WP Rocket rewrite rules from .htaccess file. add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 74 ); // Regenerate the config file. rocket_generate_config_file(); // Regenerate .htaccess file. flush_rocket_htaccess(); // Purge homepage cache. rocket_clean_home(); } } add_action( 'activate_polylang/polylang.php', 'rocket_activate_polylang', 11 ); /** * Remove mandatory cookie and add rewrite rules back to .htaccess when Polylang is deactivated. * * @author Arun Basil Lal * @since 3.0.5 */ function rocket_deactivate_polylang() { // Remove Polylang's language cookie as a mandatory cookie. remove_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_polylang_mandatory_cookie' ); // Add back WP Rocket rewrite rules from .htaccess file. remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 74 ); // Regenerate the config file. rocket_generate_config_file(); // Regenerate .htaccess file. flush_rocket_htaccess(); } add_action( 'deactivate_polylang/polylang.php', 'rocket_deactivate_polylang', 11 ); /** * Update mandatory cookie in WP Rocket config file and remove rewrite rules from .htaccess * when Detect browser language module is enabled / disabled. * * @param array $value Array containing Polylang settings before its written to db. * @return array * * @author Arun Basil Lal * @since 3.0.5 */ function rocket_detect_browser_language_status_change( $value ) { if ( function_exists( 'PLL' ) && PLL()->options['browser'] ) { // Add Polylang's language cookie as a mandatory cookie. add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_polylang_mandatory_cookie' ); // Remove WP Rocket rewrite rules from .htaccess file. add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 74 ); // Regenerate the config file. rocket_generate_config_file(); // Regenerate .htaccess file. flush_rocket_htaccess(); // Purge homepage cache. rocket_clean_home(); } else { // Remove Polylang's language cookie as a mandatory cookie. remove_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_polylang_mandatory_cookie' ); // Add back WP Rocket rewrite rules from .htaccess file. remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 74 ); // Regenerate the config file. rocket_generate_config_file(); // Regenerate .htaccess file. flush_rocket_htaccess(); } return $value; } add_filter( 'pre_update_option_polylang', 'rocket_detect_browser_language_status_change' ); 3rd-party/plugins/page-builder/visual-composer.php 0000644 00000000360 15174677547 0016301 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( defined( 'WPB_VC_VERSION' ) && class_exists( 'Vc_Manager' ) ) : /** * Disable nonce checking for Visual Composer grid */ add_filter( 'vc_grid_get_grid_data_access', '__return_true' ); endif; 3rd-party/plugins/page-builder/thrive-visual-editor.php 0000644 00000000665 15174677547 0017247 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( function_exists( 'tve_editor_url' ) ) { /** * Forces Thrive Visual Editor’s bot detection to assume a human visitor. * * @since 2.8.11 * @author Remy Perona * * @param boolean|integer $bot_detection 1|0 when crawler has|not been detected, FALSE when user agent string is unavailable * @return integer */ add_filter( 'tve_dash_is_crawler', '__return_zero', PHP_INT_MAX ); } 3rd-party/plugins/thrive-leads.php 0000644 00000000613 15174677547 0013201 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Conflict with Thrive Leads: override the DONOTCACHEPAGE behavior because this plugin add this constant! * * @since 2.5 */ function rocket_override_donotcachepage_on_thrive_leads() { return defined( 'TVE_LEADS_VERSION' ) && TVE_LEADS_VERSION > 0; } add_filter( 'rocket_override_donotcachepage', 'rocket_override_donotcachepage_on_thrive_leads' ); 3rd-party/plugins/sumome.php 0000644 00000002321 15174677547 0012115 0 ustar 00 <?php /** * Compatibility with SumoMe * * Prevents conflict with SumoMe and the WP Rocket UI by removing SumoMe * styles and scripts on WP Rocket admin pages. * * @link https://wordpress.org/plugins/sumome/ * @since 3.0.4 */ defined( 'ABSPATH' ) || exit; if ( class_exists( 'WP_Plugin_SumoMe' ) ) { /** * Dequeue SumoMe styles * * @since 3.0.4 * @author Arun Basil Lal */ function rocket_dequeue_sumo_me_css() { // Return on all pages but WP Rocket settings page. $screen = get_current_screen(); if ( 'settings_page_wprocket' !== $screen->id ) { return; } wp_dequeue_style( 'sumome-admin-styles' ); wp_dequeue_style( 'sumome-admin-media' ); } add_action( 'admin_enqueue_scripts', 'rocket_dequeue_sumo_me_css', PHP_INT_MAX ); /** * Dequeue SumoMe inline script * * @since 3.0.4 * @author Arun Basil Lal */ function rocket_dequeue_sumo_me_js() { // Return on all pages but WP Rocket settings page. $screen = get_current_screen(); if ( 'settings_page_wprocket' !== $screen->id ) { return; } global $wp_plugin_sumome; remove_action( 'admin_footer', [ $wp_plugin_sumome, 'append_admin_script_code' ] ); } add_action( 'admin_head', 'rocket_dequeue_sumo_me_js' ); } 3rd-party/plugins/autoptimize.php 0000644 00000012466 15174677547 0013175 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( class_exists( 'autoptimizeCache' ) ) : /** * Deactivate WP Rocket lazyload if Autoptimize lazyload is enabled * * @since 3.3.4 * * @param string $old_value Previous autoptimize option value. * @param string $value New autoptimize option value. * @return void */ function rocket_maybe_deactivate_lazyload( $old_value, $value ) { if ( empty( $old_value['autoptimize_imgopt_checkbox_field_3'] ) && ! empty( $value['autoptimize_imgopt_checkbox_field_3'] ) ) { update_rocket_option( 'lazyload', 0 ); } } add_action( 'update_option_autoptimize_imgopt_settings', 'rocket_maybe_deactivate_lazyload', 10, 2 ); /** * Improvement with Autoptimize: clear the cache when Autoptimize's cache is cleared * * @since 2.7 */ add_action( 'autoptimize_action_cachepurged', 'rocket_clean_domain' ); endif; if ( class_exists( 'autoptimizeConfig' ) ) : /** * Deactivate WP Rocket CSS Minification if Autoptimize CSS minification is enabled * * @since 2.9.5 * @author Remy Perona * * @param string $old_value Previous autoptimize option value. * @param string $value New autoptimize option value. */ function rocket_maybe_deactivate_minify_css( $old_value, $value ) { if ( $value !== $old_value && 'on' === $value ) { update_rocket_option( 'minify_css', 0 ); } } add_action( 'update_option_autoptimize_css', 'rocket_maybe_deactivate_minify_css', 10, 2 ); /** * Deactivate WP Rocket JS Minification if Autoptimize JS minification is enabled * * @since 2.9.5 * @author Remy Perona * * @param string $old_value Previous autoptimize option value. * @param string $value New autoptimize option value. */ function rocket_maybe_deactivate_minify_js( $old_value, $value ) { if ( $value !== $old_value && 'on' === $value ) { update_rocket_option( 'minify_js', 0 ); update_rocket_option( 'minify_concatenate_js', 0 ); } } add_action( 'update_option_autoptimize_js', 'rocket_maybe_deactivate_minify_js', 10, 2 ); /** * Deactivate WP Rocket async CSS if Autoptimize async CSS is enabled * * @since 2.10 * @author Remy Perona * * @param string $old_value Previous autoptimize option value. * @param string $value New autoptimize option value. */ function rocket_maybe_deactivate_css_defer( $old_value, $value ) { if ( $value !== $old_value && 'on' === $value ) { update_rocket_option( 'autoptimize_css_defer', 0 ); } } add_action( 'update_option_autoptimize_css_defer', 'rocket_maybe_deactivate_css_defer', 10, 2 ); endif; /** * Disable WP Rocket minification options when activating Autoptimize and values are already in the database. * * @since 2.9.5 * @author Remy Perona */ function rocket_activate_autoptimize() { if ( 'on' === get_option( 'autoptimize_css' ) ) { update_rocket_option( 'minify_css', 0 ); } if ( 'on' === get_option( 'autoptimize_js' ) ) { update_rocket_option( 'minify_js', 0 ); update_rocket_option( 'minify_concatenate_js', 0 ); } if ( 'on' === get_option( 'autoptimize_css_defer' ) ) { update_rocket_option( 'async_css', 0 ); } $lazyload = get_option( 'autoptimize_imgopt_settings' ); if ( ! empty( $lazyload['autoptimize_imgopt_checkbox_field_3'] ) ) { update_rocket_option( 'lazyload', 0 ); } } add_action( 'activate_autoptimize/autoptimize.php', 'rocket_activate_autoptimize', 11 ); /** * Disable WP Rocket minification if Autoptimize css /js minification is enabled. * * @param array $options WP Rocket options array. * * @since 3.18 * * @return array */ function rocket_maybe_disable_minification( $options ) { // Bail early if plugin is not active. if ( ! is_plugin_active( 'autoptimize/autoptimize.php' ) ) { return $options; } if ( 'on' === get_option( 'autoptimize_css' ) ) { $options['minify_css'] = 0; } if ( 'on' === get_option( 'autoptimize_js' ) ) { $options['minify_js'] = 0; } return $options; } add_filter( 'rocket_first_install_options', 'rocket_maybe_disable_minification' ); /** * Disable WP Rocket lazyload fields if Autoptimize lazyload is enabled * * @since 3.3.4 * * @return bool */ function rocket_maybe_disable_lazyload() { $lazyload = get_option( 'autoptimize_imgopt_settings' ); if ( is_plugin_active( 'autoptimize/autoptimize.php' ) && ! empty( $lazyload['autoptimize_imgopt_checkbox_field_3'] ) ) { return true; } return false; } /** * Disable WP Rocket CSS minification field if Autoptimize CSS minification is enabled * * @since 2.9.5 * @author Remy Perona * * @return bool|null True if it is active */ function rocket_maybe_disable_minify_css() { if ( is_plugin_active( 'autoptimize/autoptimize.php' ) && 'on' === get_option( 'autoptimize_css' ) ) { return true; } } /** * Disable WP Rocket JS minification field if Autoptimize JS minification is enabled * * @since 2.9.5 * @author Remy Perona * * @return bool|null True if it is active */ function rocket_maybe_disable_minify_js() { if ( is_plugin_active( 'autoptimize/autoptimize.php' ) && 'on' === get_option( 'autoptimize_js' ) ) { return true; } } /** * Disable WP Rocket async CSS field if Autoptimize async CSS is enabled * * @since 2.10 * @author Remy Perona * * @return bool|null True if it is active */ function rocket_maybe_disable_async_css() { if ( is_plugin_active( 'autoptimize/autoptimize.php' ) && 'on' === get_option( 'autoptimize_css_defer' ) ) { return true; } } 3rd-party/plugins/security/sf-move-login.php 0000644 00000004605 15174677547 0015150 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( defined( 'SFML_VERSION' ) ) : add_filter( 'rocket_cache_reject_uri', 'rocket_add_sfml_exclude_pages', 2, 2 ); add_action( 'update_option_sfml', 'rocket_after_update_single_options', 10, 2 ); endif; /** * Exclude SF Move Login custom urls from caching * * @since 2.9.3 Moved to 3rd party file and improved * @since 2.6 * * @param array $urls An array of URLs to exclude from cache. * @param bool $show_safe_content show sensitive uris. * @return array Updated array of URLs */ function rocket_add_sfml_exclude_pages( $urls, $show_safe_content = true ) { if ( ! $show_safe_content ) { return $urls; } if ( ! function_exists( 'sfml_get_slugs' ) ) { if ( file_exists( SFML_PLUGIN_DIR . 'inc/utilities.php' ) ) { include SFML_PLUGIN_DIR . 'inc/utilities.php'; } else { return $urls; } } if ( ! class_exists( 'SFML_Options' ) && ! defined( 'SFML_NOOP_VERSION' ) ) { if ( file_exists( SFML_PLUGIN_DIR . 'inc/class-sfml-options.php' ) ) { include SFML_PLUGIN_DIR . 'inc/class-sfml-options.php'; } else { return $urls; } } $sfml_slugs = sfml_get_slugs(); $sfml_slugs = array_map( 'home_url', $sfml_slugs ); $sfml_slugs = array_map( 'trailingslashit', $sfml_slugs ); $sfml_slugs = array_map( 'rocket_clean_exclude_file', $sfml_slugs ); foreach ( $sfml_slugs as $key => $slug ) { $sfml_slugs[ $key ] = $slug . '?'; } return array_merge( $urls, $sfml_slugs ); } /** * Add SFML custom urls to caching exclusion when activating the plugin * * @since 2.9.3 */ function rocket_activate_sfml() { if ( defined( 'SFML_VERSION' ) ) { add_filter( 'rocket_cache_reject_uri', 'rocket_add_sfml_exclude_pages', 2, 2 ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } } add_action( 'activate_sf-move-login/sf-move-login.php', 'rocket_activate_sfml', 11 ); /** * Remove SFML custom urls from caching exclusion when deactivating the plugin * * @since 2.9.3 */ function rocket_deactivate_sfml() { if ( defined( 'SFML_VERSION' ) ) { remove_filter( 'rocket_cache_reject_uri', 'rocket_add_sfml_exclude_pages' ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } } add_action( 'deactivate_sf-move-login/sf-move-login.php', 'rocket_deactivate_sfml', 11 ); 3rd-party/plugins/security/secupress.php 0000644 00000005120 15174677547 0014473 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; add_action( 'update_option_secupress_users-login_settings', 'rocket_after_update_single_options', 10, 2 ); /** * Add SecuPress move login pages to cache exclusion * * @since 2.9 * @author Remy Perona * * @param array $urls URLs to exclude from cache. * @param bool $show_safe_content show sensitive uris. * @return array Updated URLs to exclude */ function rocket_exclude_secupress_move_login( $urls, $show_safe_content = true ) { if ( ! $show_safe_content ) { return $urls; } if ( ! function_exists( 'secupress_move_login_get_slugs' ) ) { return $urls; } $bases = secupress_get_rewrite_bases(); $slugs = secupress_move_login_get_slugs(); foreach ( $slugs as $slug ) { $urls[] = $bases['base'] . ltrim( $bases['site_from'], '/' ) . $slug . '/?'; } return $urls; } add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_secupress_move_login', 2, 2 ); /** * Add SecuPress move login pages to cache exclusion when activating the plugin * * @since 2.9 * @author Remy Perona */ function rocket_maybe_activate_secupress() { if ( function_exists( 'secupress_move_login_get_slugs' ) ) { rocket_activate_secupress(); } } add_action( 'secupress.plugins.activation', 'rocket_maybe_activate_secupress', 10001 ); /** * Add SecuPress move login pages to cache exclusion when activating the move login module * * @since 2.9 * @author Remy Perona */ function rocket_activate_secupress() { add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_secupress_move_login', 2, 2 ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'secupress.plugin.move_login.activate', 'rocket_activate_secupress' ); /** * Remove SecuPress move login pages from cache exclusion when deactivating the plugin * * @since 2.9 * @author Remy Perona */ function rocket_maybe_deactivate_secupress() { if ( function_exists( 'secupress_move_login_get_slugs' ) ) { rocket_deactivate_secupress(); } } add_action( 'secupress.deactivation', 'rocket_maybe_deactivate_secupress', 10001 ); /** * Remove SecuPress move login pages from cache exclusion when deactivating the move login module * * @since 2.9 * @author Remy Perona */ function rocket_deactivate_secupress() { remove_filter( 'rocket_cache_reject_uri', 'rocket_exclude_secupress_move_login' ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'secupress.plugin.move_login.deactivate', 'rocket_deactivate_secupress' ); 3rd-party/plugins/security/wps-hide-login.php 0000644 00000004142 15174677547 0015310 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( class_exists( 'WPS_Hide_Login' ) || defined( 'WPS_HIDE_LOGIN_VERSION' ) ) : add_action( 'update_option_whl_page', 'rocket_after_update_single_options', 10, 2 ); add_filter( 'rocket_cache_reject_uri', 'rocket_exlude_wps_hide_login_page', 2, 2 ); endif; /** * Exclude WPS Hide Login custom url from caching * * @since 3.1 Switch implementation depending on the WPS Hide Login version * @since 2.11.7 Login url is retrieved using new_login_url() method of WPS_Hide_Login() class. * @since 2.11 Moved to 3rd party file * @since 2.6 * * @param array $urls An array of URLs to exclude from cache. * @param bool $show_safe_content show sensitive uris. * @return array Updated array of URLs */ function rocket_exlude_wps_hide_login_page( $urls, $show_safe_content = true ) { if ( ! $show_safe_content ) { return $urls; } if ( class_exists( 'WPS_Hide_Login' ) ) { $wps_hide_login = new WPS_Hide_Login(); $urls[] = rocket_clean_exclude_file( $wps_hide_login->new_login_url() ); } elseif ( class_exists( '\WPS\WPS_Hide_Login\Plugin' ) ) { $urls[] = rocket_clean_exclude_file( \WPS\WPS_Hide_Login\Plugin::get_instance()->new_login_url() ); } return $urls; } /** * Add WPS Hide Login custom url to caching exclusion when activating the plugin * * @since 2.11 */ function rocket_activate_wps_hide_login() { add_filter( 'rocket_cache_reject_uri', 'rocket_exlude_wps_hide_login_page', 2, 2 ); // Update .htaccess file rules. flush_rocket_htaccess(); // Update config file. rocket_generate_config_file(); } add_action( 'activate_wps-hide-login/wps-hide-login.php', 'rocket_activate_wps_hide_login', 11 ); /** * Remove WPS Hide Login custom url from caching exclusion when deactivating the plugin * * @since 2.11 */ function rocket_deactivate_wps_hide_login() { remove_filter( 'rocket_cache_reject_uri', 'rocket_exlude_wps_hide_login_page' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Update config file. rocket_generate_config_file(); } add_action( 'deactivate_wps-hide-login/wps-hide-login.php', 'rocket_deactivate_wps_hide_login', 11 ); 3rd-party/plugins/custom-login.php 0000644 00000003767 15174677547 0013247 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Exclude plugin custom login page template from cache. * * @since 2.7 */ if ( class_exists( 'Custom_Login_Page_Template' ) ) { add_filter( 'rocket_cache_reject_uri', 'rocket_add_custom_login_exclude_pages', 2, 2 ); add_action( 'update_option_custom_login_page_template', 'rocket_after_update_single_options', 10, 2 ); } /** * Add custom login page to excluded urls array * * @since 2.7 * @author Remy Perona * * @param Array $urls Array of pages to exclude. * @param bool $show_safe_content show sensitive uris. * @return Array Updated array of pages to exclude */ function rocket_add_custom_login_exclude_pages( $urls, $show_safe_content = true ) { if ( ! $show_safe_content ) { return $urls; } $clpt_options = get_option( 'custom_login_page_template' ); $urls = array_merge( $urls, get_rocket_i18n_translated_post_urls( $clpt_options['login_page_id'], 'page' ) ); return $urls; } /** * Add custom login page to excluded urls array on plugin activation * * @since 2.7 * @author Remy Perona */ function rocket_activate_custom_login_page_template() { add_filter( 'rocket_cache_reject_uri', 'rocket_add_custom_login_exclude_pages', 2, 2 ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_custom-login-page-template/custom-login-page-template.php', 'rocket_activate_custom_login_page_template', 11 ); /** * Remove custom login page from excluded urls array on plugin activation * * @since 2.7 * @author Remy Perona */ function rocket_remove_custom_login_exclude_pages() { remove_filter( 'rocket_cache_reject_uri', 'rocket_add_custom_login_exclude_pages' ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_custom-login-page-template/custom-login-page-template.php', 'rocket_remove_custom_login_exclude_pages', 11 ); 3rd-party/plugins/mobile/wp-appkit.php 0000644 00000002534 15174677547 0014001 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( class_exists( 'WpAppKit' ) ) : add_filter( 'rocket_cache_reject_uri', 'rocket_add_appkit_exclude_pages' ); endif; /** * Add WP Appkit path to cache exclusion * * @since 2.9 * @author Remy Perona * * @param array $urls URLs to exclude from cache. * @return array Updated URLs array */ function rocket_add_appkit_exclude_pages( $urls ) { $urls[] = '/wp-appkit-api/(.*)'; return $urls; } /** * Add WP Appkit path to cache exclusion when activating the plugin * * @since 2.9 * @author Remy Perona */ function rocket_activate_wp_appkit() { add_filter( 'rocket_cache_reject_uri', 'rocket_add_appkit_exclude_pages' ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_wp-appkit/wp-appkit.php', 'rocket_activate_wp_appkit', 11 ); /** * Remove Appkit path from cache exclusion when deactivating the plugin * * @since 2.9 * @author Remy Perona */ function rocket_deactivate_wp_appkit() { remove_filter( 'rocket_cache_reject_uri', 'rocket_add_appkit_exclude_pages' ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_wp-appkit/wp-appkit.php', 'rocket_deactivate_wp_appkit', 11 ); 3rd-party/plugins/slider/meta-slider.php 0000644 00000000666 15174677547 0014312 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Conflict with Meta Slider (Nivo Slider): don't apply LazyLoad on all images * * @since 2.4 * * @param array $slide Slide attributes. * @return array Updated slide attributes */ function rocket_deactivate_lazyload_on_metaslider( $slide ) { $slide['data-no-lazy'] = 1; return $slide; } add_filter( 'metaslider_nivo_slider_image_attributes', 'rocket_deactivate_lazyload_on_metaslider' ); 3rd-party/plugins/slider/soliloquy.php 0000644 00000001551 15174677547 0014136 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Conflict with Soliloquy: don't apply LazyLoad on all images * * @since 2.4.2 * * @param string $attr Image attributes. * @return string Updated attributes */ function rocket_deactivate_lazyload_on_soliloquy( $attr ) { return $attr . ' data-no-lazy="1" '; } add_filter( 'soliloquy_output_image_attr', 'rocket_deactivate_lazyload_on_soliloquy', PHP_INT_MAX ); /** * Conflict with Soliloquy: don't apply LazyLoad on all images * * @since 2.4.2 * * @param string $images Image HTML code. * @return string Updated image HTML code */ function rocket_deactivate_lazyload_on_soliloquy_indexable_images( $images ) { $images = str_replace( '<img', '<img data-no-lazy="1" ', $images ); return $images; } add_filter( 'soliloquy_indexable_images', 'rocket_deactivate_lazyload_on_soliloquy_indexable_images', PHP_INT_MAX ); 3rd-party/plugins/ecommerce/aelia-tax-display-by-country.php 0000644 00000004242 15174677547 0020174 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Compatibility with Aelia Tax Display by Country. * * @since 2.8.15 */ if ( class_exists( 'Aelia\WC\TaxDisplayByCountry\WC_Aelia_Tax_Display_By_Country' ) ) : /** * Generate a caching file depending to the tax display cookie values */ add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 68 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_aelia_tax_display_by_country_dynamic_cookies' ); endif; /** * Add cookies when we activate the plugin * * @since 2.8.15 * @author Remy Perona */ function rocket_activate_aelia_tax_display_by_country() { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 68 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_aelia_tax_display_by_country_dynamic_cookies' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_woocommerce-tax-display-by-country/woocommerce-tax-display-by-country.php', 'rocket_activate_aelia_tax_display_by_country', 11 ); /** * Remove cookies when we deactivate the plugin * * @since 2.8.15 * @author Remy Perona */ function rocket_deactivate_aelia_tax_display_by_country() { remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 68 ); remove_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_aelia_tax_display_by_country_dynamic_cookies' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_woocommerce-tax-display-by-country/woocommerce-tax-display-by-country.php', 'rocket_deactivate_aelia_tax_display_by_country', 11 ); /** * Add the Aelia Tax Display by Country cookies to generate caching files depending on their values * * @since 2.8.15 * @author Remy Perona * * @param array $cookies Cookies list to use for dynamic caching. * @return array Updated cookies list */ function rocket_add_aelia_tax_display_by_country_dynamic_cookies( $cookies ) { $cookies[] = 'aelia_customer_country'; $cookies[] = 'aelia_customer_state'; $cookies[] = 'aelia_tax_exempt'; return $cookies; } 3rd-party/plugins/ecommerce/easy-digital-downloads.php 0000644 00000002751 15174677547 0017122 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( function_exists( 'EDD' ) ) : add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_edd_pages' ); add_action( 'update_option_edd_settings', 'rocket_after_update_array_options', 10, 2 ); endif; /** * Exclude EDD pages from cache * * @param array $urls Array of URLs to exclude from cache. * @return array Updated array of URLs */ function rocket_exclude_edd_pages( $urls ) { $edd_settings = get_option( 'edd_settings' ); if ( isset( $edd_settings['purchase_page'] ) ) { $checkout_urls = get_rocket_i18n_translated_post_urls( $edd_settings['purchase_page'], 'page', '(.*)' ); $urls = array_merge( $urls, $checkout_urls ); } return $urls; } /** * Exclude EDD pages from cache on EDD activation. */ function rocket_activate_edd() { add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_edd_pages' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_easy-digital-downloads/easy-digital-downloads.php', 'rocket_activate_edd', 11 ); /** * Remove EDD pages from cache exclusion on EDD deactivation. */ function rocket_deactivate_edd() { remove_filter( 'rocket_cache_reject_uri', 'rocket_exclude_edd_pages' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_easy-digital-downloads/easy-digital-downloads.php', 'rocket_deactivate_edd', 11 ); 3rd-party/plugins/ecommerce/edd-software-licencing.php 0000644 00000003126 15174677547 0017070 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Compatibility with Easy Digital Downloads Software Licensing addon. * * @since 2.7 */ if ( class_exists( 'EDD_Software_Licensing' ) && defined( 'EDD_SL_VERSION' ) ) : // Exclude EDD SL endpoint from cache on WP Rocket activation. add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_edd_sl_endpoint' ); endif; /** * Exclude EDD SL endpoint from cache when activating the plugin * * @since 2.7 */ function rocket_activate_edd_software_licensing() { add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_edd_sl_endpoint' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_edd-software-licensing/edd-software-licenses.php', 'rocket_activate_edd_software_licensing', 11 ); /** * Remove exclusion of EDD SL endpoint from cache when deactivating the plugin * * @since 2.7 */ function rocket_deactivate_edd_software_licensing() { remove_filter( 'rocket_cache_reject_uri', 'rocket_exclude_edd_sl_endpoint' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_edd-software-licensing/edd-software-licenses.php', 'rocket_deactivate_edd_software_licensing', 11 ); /** * Exclude EDD SL endpoint from caching * * @since 2.7 * * @param array $uri URLs to exclude from caching. * @return array Updated list of URLs to exclude */ function rocket_exclude_edd_sl_endpoint( $uri ) { $uri[] = '/edd-sl/(.*)'; return $uri; } 3rd-party/plugins/ecommerce/jigoshop.php 0000644 00000004031 15174677547 0014371 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( defined( 'JIGOSHOP_VERSION' ) && function_exists( 'jigoshop_get_page_id' ) ) : add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_jigoshop_pages' ); add_action( 'update_option_jigoshop_options', 'rocket_after_update_array_options', 10, 2 ); endif; /** * Exclude Jigoshop pages from cache. * * @param array $urls Array of URLs to exclude from cache. * @return array Updated array of URLs to exclude from cache */ function rocket_exclude_jigoshop_pages( $urls ) { if ( jigoshop_get_page_id( 'checkout' ) && jigoshop_get_page_id( 'checkout' ) !== '-1' ) { $checkout_urls = get_rocket_i18n_translated_post_urls( jigoshop_get_page_id( 'checkout' ), 'page', '(.*)' ); $urls = array_merge( $urls, $checkout_urls ); } if ( jigoshop_get_page_id( 'cart' ) && jigoshop_get_page_id( 'cart' ) !== '-1' ) { $cart_urls = get_rocket_i18n_translated_post_urls( jigoshop_get_page_id( 'cart' ) ); $urls = array_merge( $urls, $cart_urls ); } if ( jigoshop_get_page_id( 'myaccount' ) && jigoshop_get_page_id( 'myaccount' ) !== '-1' ) { $cart_urls = get_rocket_i18n_translated_post_urls( jigoshop_get_page_id( 'myaccount' ), 'page', '(.*)' ); $urls = array_merge( $urls, $cart_urls ); } return $urls; } /** * Exclude Jigoshop pages from cache on plugin activation. */ function rocket_activate_jigoshop() { add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_jigoshop_pages' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_jigoshop/jigoshop.php', 'rocket_activate_jigoshop', 11 ); /** * Remove Jigoshop pages from cache exclusion on plugin deactivation. */ function rocket_deactivate_jigoshop() { remove_filter( 'rocket_cache_reject_uri', 'rocket_exclude_jigoshop_pages' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_jigoshop/jigoshop.php', 'rocket_deactivate_jigoshop', 11 ); 3rd-party/plugins/ecommerce/aelia-currencyswitcher.php 0000644 00000007215 15174677547 0017232 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Compatibility with Aelia Currency Switcher. * * @since 2.7 */ if ( class_exists( 'WC_Aelia_CurrencySwitcher' ) ) : /** * Update .htaccess & config files when the Geolocation option is updated * * @since 2.7 * * @param array $old_value Previous setting values. * @param array $value Submitted settings values. */ function rocket_after_update_aelia_currencyswitcher_options( $old_value, $value ) { if ( ( ! isset( $old_value['ipgeolocation_enabled'] ) && isset( $value['ipgeolocation_enabled'] ) ) || ( isset( $old_value['ipgeolocation_enabled'], $value['ipgeolocation_enabled'] ) && $old_value['ipgeolocation_enabled'] !== $value['ipgeolocation_enabled'] ) ) { // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Update the config file. rocket_generate_config_file(); } } add_action( 'update_option_wc_aelia_currency_switcher', 'rocket_after_update_aelia_currencyswitcher_options', 10, 2 ); /** * Generate a caching file depending on the currency cookie value */ add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 64 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_aelia_currencyswitcher_dynamic_cookies' ); add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_aelia_currencyswitcher_mandatory_cookie' ); endif; /** * Add the Aelia Currency Switcher cookies when activating the plugin * * @since 2.7 */ function rocket_activate_aelia_currencyswitcher() { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 64 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_aelia_currencyswitcher_dynamic_cookies' ); add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_aelia_currencyswitcher_mandatory_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_woocommerce-aelia-currencyswitcher/woocommerce-aelia-currencyswitcher.php', 'rocket_activate_aelia_currencyswitcher', 11 ); /** * Remove the Aelia Currency Switcher cookies when deactivating the plugin * * @since 2.7 */ function rocket_deactivate_aelia_currencyswitcher() { remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 64 ); remove_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_aelia_currencyswitcher_dynamic_cookies' ); remove_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_aelia_currencyswitcher_mandatory_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_woocommerce-aelia-currencyswitcher/woocommerce-aelia-currencyswitcher.php', 'rocket_deactivate_aelia_currencyswitcher', 11 ); /** * Add the Aelia Currency Switcher cookies to the dynamic cookies list * * @since 2.7 * * @param array $cookies Cookies to use for dynamic caching. * @return array Updated cookies list */ function rocket_add_aelia_currencyswitcher_dynamic_cookies( $cookies ) { $cookies[] = 'aelia_cs_recalculate_cart_totals'; $cookies[] = 'aelia_cs_selected_currency'; $cookies[] = 'aelia_customer_country'; return $cookies; } /** * Add the Aelia Currency Switcher to the list of mandatory cookies before generating caching files * * @since 2.7 * * @param array $cookies Mandatory cookies to serve the cache. * @return array Updated cookies list */ function rocket_add_aelia_currencyswitcher_mandatory_cookie( $cookies ) { $acs_options = get_option( 'wc_aelia_currency_switcher' ); if ( ! empty( $acs_options['ipgeolocation_enabled'] ) ) { $cookies[] = 'aelia_cs_selected_currency'; } return $cookies; } 3rd-party/plugins/ecommerce/ithemes-exchange.php 0000644 00000003703 15174677547 0015772 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( function_exists( 'it_exchange_get_page_type' ) && function_exists( 'it_exchange_get_page_url' ) ) : add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_ithemes_exchange_pages' ); add_action( 'update_option_it-storage-exchange_settings_pages', 'rocket_after_update_single_options', 10, 2 ); endif; /** * Exclude iThemes Exchange pages from cache. * * @param array $urls Array of URLs to exclude from cache. * @return array Updated array of URLs to exclude from cache */ function rocket_exclude_ithemes_exchange_pages( $urls ) { $pages = [ 'purchases', 'confirmation', 'account', 'profile', 'downloads', 'purchases', 'log-in', 'log-out', ]; foreach ( $pages as $page ) { if ( it_exchange_get_page_type( $page ) === 'WordPress' ) { $exchange_urls = get_rocket_i18n_translated_post_urls( it_exchange_get_page_wpid( $page ) ); } else { $exchange_urls = [ rocket_extract_url_component( it_exchange_get_page_url( $page ), PHP_URL_PATH ) ]; } $urls = array_merge( $urls, $exchange_urls ); } return $urls; } /** * Exclude iThemes Exchanges pages from cache on plugin activation. */ function rocket_activate_ithemes_exchange() { add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_ithemes_exchange_pages' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_ithemes-exchange/ithemes-exchange.php', 'rocket_activate_ithemes_exchange', 11 ); /** * Remove iThemes Exchanges pages from cache exclusion on plugin deactivation. */ function rocket_deactivate_ithemes_exchange() { remove_filter( 'rocket_cache_reject_uri', 'rocket_exclude_ithemes_exchange_pages' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_ithemes-exchange/ithemes-exchange.php', 'rocket_deactivate_ithemes_exchange', 11 ); 3rd-party/plugins/ecommerce/aelia-prices-by-country.php 0000644 00000004000 15174677547 0017212 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Compatibility with Aelia Prices by Country. * * @since 2.8.15 */ if ( class_exists( 'Aelia\WC\PricesByCountry\WC_Aelia_Prices_By_Country' ) ) : /** * Generate a caching file depending on the country cookie value */ add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 66 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_aelia_prices_by_country_dynamic_cookies' ); endif; /** * Add cookie when we activate the plugin * * @since 2.8.15 * @author Remy Perona */ function rocket_activate_aelia_prices_by_country() { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 66 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_aelia_prices_by_country_dynamic_cookies' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_woocommerce-prices-by-country/woocommerce-prices-by-country.php', 'rocket_activate_aelia_prices_by_country', 11 ); /** * Remove cookies when we deactivate the plugin * * @since 2.8.15 * @author Remy Perona */ function rocket_deactivate_aelia_prices_by_country() { remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 66 ); remove_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_aelia_prices_by_country_dynamic_cookies' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_woocommerce-prices-by-country/woocommerce-prices-by-country.php', 'rocket_deactivate_aelia_prices_by_country', 11 ); /** * Add the Aelia Prices by Country cookie to generate caching files depending on its value * * @since 2.8.15 * @author Remy Perona * * @param array $cookies Cookies list to use for dynamic caching. * @return array Updated cookies list */ function rocket_add_aelia_prices_by_country_dynamic_cookies( $cookies ) { $cookies[] = 'aelia_customer_country'; return $cookies; } 3rd-party/plugins/ecommerce/woocommerce-multilingual.php 0000644 00000006711 15174677547 0017601 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Check if WCML is active and has minimum requirements. * * @return bool */ function rocket_wcml_has_requirements() { return defined( 'WCML_VERSION' ) && version_compare( WCML_VERSION, '4.12.6', '>=' ); } if ( rocket_wcml_has_requirements() ) : /** * Use Cookie instead of WCSession * * @return string */ function rocket_wcml_use_cookie_storage() { return 'cookie'; } add_filter( 'wcml_user_store_strategy', 'rocket_wcml_use_cookie_storage', 10, 2 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_wcml_add_dynamic_cookies' ); add_filter( 'rocket_cache_mandatory_cookies', 'rocket_wcml_add_mandatory_cookies' ); add_action( 'updated_option', 'rocket_wcml_reset_settings', 10, 3 ); /** * Reset WP Rocket settings on WCML deactivation. */ function rocket_wcml_deactivate() { remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 64 ); remove_filter( 'rocket_cache_dynamic_cookies', 'rocket_wcml_add_dynamic_cookies' ); remove_filter( 'rocket_cache_mandatory_cookies', 'rocket_wcml_add_mandatory_cookies' ); flush_rocket_htaccess(); rocket_generate_config_file(); } add_action( 'deactivate_woocommerce-multilingual/wpml-woocommerce.php', 'rocket_wcml_deactivate', 11 ); add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 64 ); endif; /** * Add dynamic cookies for WCML. * * @param array $cookies Cookies. * * @return array */ function rocket_wcml_add_dynamic_cookies( $cookies ) { $cookies[] = 'wcml_client_currency'; $cookies[] = 'wcml_client_currency_language'; $cookies[] = 'wcml_client_country'; return $cookies; } /** * Add mandatory cookies for WCML. * * @param array $cookies Cookies. * * @return array */ function rocket_wcml_add_mandatory_cookies( $cookies ) { // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound if ( apply_filters( 'wcml_geolocation_is_used', false ) ) { // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $cookies[] = 'wcml_client_country'; } return $cookies; } /** * Reset WP Rocket settings when a relevant WCML setting is changed. * * @param string $option Option name. * @param mixed $old_data Old data. * @param mixed $data New data. */ function rocket_wcml_reset_settings( $option, $old_data, $data ) { $keys_to_check = [ 'enable_multi_currency', 'currency_mode', 'default_currencies', ]; $check_key = function ( $result, $key ) use ( $old_data, $data ) { $has_value_changed = function ( $key ) use ( $old_data, $data ) { $get_value = function ( $key, $data ) { return isset( $data[ $key ] ) ? $data[ $key ] : null; }; return $get_value( $key, $old_data ) !== $get_value( $key, $data ); }; return $result || $has_value_changed( $key ); }; if ( '_wcml_settings' === $option && array_reduce( $keys_to_check, $check_key, false ) ) { flush_rocket_htaccess(); rocket_generate_config_file(); } } /** * Reset WP Rocket settings on WCML activation. */ function rocket_wcml_activate() { if ( rocket_wcml_has_requirements() ) { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 64 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_wcml_add_dynamic_cookies' ); add_filter( 'rocket_cache_mandatory_cookies', 'rocket_wcml_add_mandatory_cookies' ); flush_rocket_htaccess(); rocket_generate_config_file(); } } add_action( 'activate_woocommerce-multilingual/wpml-woocommerce.php', 'rocket_wcml_activate', 11 ); 3rd-party/plugins/ecommerce/wpshop.php 0000644 00000004142 15174677547 0014072 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( defined( 'WPSHOP_VERSION' ) && class_exists( 'wpshop_tools' ) && method_exists( 'wpshop_tools', 'get_page_id' ) ) : add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_wpshop_pages' ); add_action( 'update_option_wpshop_cart_page_id', 'rocket_after_update_single_options', 10, 2 ); add_action( 'update_option_wpshop_checkout_page_id', 'rocket_after_update_single_options', 10, 2 ); add_action( 'update_option_wpshop_payment_return_page_id', 'rocket_after_update_single_options', 10, 2 ); add_action( 'update_option_wpshop_payment_return_nok_page_id', 'rocket_after_update_single_options', 10, 2 ); add_action( 'update_option_wpshop_myaccount_page_id', 'rocket_after_update_single_options', 10, 2 ); endif; /** * Exclude WP Shop pages from cache. * * @param array $urls Array of URLs to exclude from cache. * @return array Updated array of URLs to exclude from cache */ function rocket_exclude_wpshop_pages( $urls ) { $pages = [ 'wpshop_cart_page_id', 'wpshop_checkout_page_id', 'wpshop_payment_return_page_id', 'wpshop_payment_return_nok_page_id', 'wpshop_myaccount_page_id', ]; foreach ( $pages as $page ) { $page_id = wpshop_tools::get_page_id( get_option( $page ) ); if ( $page_id ) { $urls = array_merge( $urls, get_rocket_i18n_translated_post_urls( $page_id ) ); } } return $urls; } /** * Exclude WP Shop pages from cache on plugin activation. */ function rocket_activate_wpshop() { add_filter( 'rocket_cache_reject_uri', 'rocket_exclude_wpshop_pages' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_wpshop/wpshop.php', 'rocket_activate_wpshop', 11 ); /** * Remove WP Shop pages from cache exclusion on plugin deactivation. */ function rocket_deactivate_wpshop() { remove_filter( 'rocket_cache_reject_uri', 'rocket_exclude_wpshop_pages' ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_wpshop/wpshop.php', 'rocket_deactivate_wpshop', 11 ); 3rd-party/plugins/ecommerce/woocommerce-currency-converter-widget.php 0000644 00000006101 15174677547 0022204 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Compatibility with WooCommerce Currency Converter Widget. * * @since 2.7 */ if ( class_exists( 'WC_Currency_Converter' ) ) : // Add cookie to config file when WP Rocket is activated and WooCommerce Currency Converter Widget is already active. add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 70 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_woocommerce_currency_converter_dynamic_cookies', 11 ); add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_woocommerce_currency_converter_mandatory_cookie', 11 ); add_action( 'update_option_woocommerce_default_customer_address', 'rocket_after_update_single_options', 10, 2 ); endif; /** * Add cookies when we activating the plugin * * @since 2.7 */ function rocket_activate_woocommerce_currency_converter() { add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 70 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_woocommerce_currency_converter_dynamic_cookies' ); add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_woocommerce_currency_converter_mandatory_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_woocommerce-currency-converter-widget/currency-converter.php', 'rocket_activate_woocommerce_currency_converter', 11 ); /** * Remove cookies when deactivating the plugin * * @since 2.7 */ function rocket_deactivate_woocommerce_currency_converter() { remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 70 ); remove_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_woocommerce_currency_converter_dynamic_cookies' ); remove_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_woocommerce_currency_converter_mandatory_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_woocommerce-currency-converter-widget/currency-converter.php', 'rocket_deactivate_woocommerce_currency_converter', 11 ); /** * Add the WC Currency Converter Widget cookie to generate caching files depending on its value * * @since 2.7 * * @param array $cookies Cookies list to use for dynamic caching. * @return array Updated cookies list */ function rocket_add_woocommerce_currency_converter_dynamic_cookies( $cookies ) { $cookies[] = 'woocommerce_current_currency'; return $cookies; } /** * Add the WC Currency Converter Widget cookie to the list of mandatory cookies before generating the caching files * * @since 2.7 * * @param array $cookies Mandatory cookies list. * @return array Updated cookies list */ function rocket_add_woocommerce_currency_converter_mandatory_cookie( $cookies ) { $widget_woocommerce_currency_converter = get_option( 'widget_woocommerce_currency_converter' ); if ( ! empty( $widget_woocommerce_currency_converter ) && 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) { $cookies[] = 'woocommerce_current_currency'; } return $cookies; } 3rd-party/plugins/ecommerce/give.php 0000644 00000003507 15174677547 0013510 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Exclude pages of Give plugin from cache. * * @since 2.7 */ if ( defined( 'GIVE_VERSION' ) && function_exists( 'give_get_settings' ) ) { add_filter( 'rocket_cache_reject_uri', 'rocket_add_give_exclude_pages' ); if ( is_admin() ) { add_action( 'update_option_give_settings', 'rocket_after_update_single_options', 10, 2 ); } } /** * Add give pages to the excluded pages * * @since 2.7 * * @param Array $urls Array of excluded pages. * @return Array Updated array of excluded pages */ function rocket_add_give_exclude_pages( $urls ) { $give_options = give_get_settings(); $urls = array_merge( $urls, get_rocket_i18n_translated_post_urls( $give_options['success_page'], 'page' ) ); $urls = array_merge( $urls, get_rocket_i18n_translated_post_urls( $give_options['history_page'], 'page' ) ); $urls = array_merge( $urls, get_rocket_i18n_translated_post_urls( $give_options['failure_page'], 'page' ) ); return $urls; } /** * Add give pages to the excluded pages when activating the plugin * * @since 2.7 */ function rocket_activate_give() { add_filter( 'rocket_cache_reject_uri', 'rocket_add_give_exclude_pages' ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'activate_give/give.php', 'rocket_activate_give', 11 ); /** * Remove give pages from the excluded pages when activating the plugin * * @since 2.7 */ function rocket_remove_give_exclude_pages() { remove_filter( 'rocket_cache_reject_uri', 'rocket_add_give_exclude_pages' ); // Update the WP Rocket rules on the .htaccess. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } add_action( 'deactivate_give/give.php', 'rocket_remove_give_exclude_pages', 11 ); 3rd-party/plugins/buddypress.php 0000644 00000003340 15174677547 0012776 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( class_exists( 'BuddyPress' ) ) : /** * Conflict with BuddyPress: don't apply LazyLoad on BuddyPress profil pages & group creation * * @since 2.8.15 Improve checks for user profile & disable for cover images * @since 2.6.9 */ if ( function_exists( 'bp_is_user_profile' ) ) : /** * Prevent lazyload if on a BuddyPress profil page * * @param Bool $run_filter Do lazyload value. * @return Bool False if on a BuddyPress profil page, initial value otherwise */ function rocket_deactivate_lazyload_on_buddypress_profil_pages( $run_filter ) { if ( bp_is_user_profile() ) { return false; } return $run_filter; } add_filter( 'do_rocket_lazyload', 'rocket_deactivate_lazyload_on_buddypress_profil_pages' ); add_filter( 'do_rocket_lazyload_iframes', 'rocket_deactivate_lazyload_on_buddypress_profil_pages' ); endif; if ( function_exists( 'bp_is_group_creation_step' ) && function_exists( 'bp_is_group_admin_screen' ) ) : /** * Prevent lazyload if on a BuddyPress group creation page * * @param Bool $run_filter Do lazyload value. * @return Bool False if on a BuddyPress profil page, initial value otherwise */ function rocket_deactivate_lazyload_on_buddypress_group_pages( $run_filter ) { if ( bp_is_group_creation_step( 'group-avatar' ) || bp_is_group_creation_step( 'group-cover-image' ) || bp_is_group_admin_screen( 'group-avatar' ) || bp_is_group_admin_screen( 'group-cover-image' ) ) { return false; } return $run_filter; } add_filter( 'do_rocket_lazyload', 'rocket_deactivate_lazyload_on_buddypress_group_pages' ); add_filter( 'do_rocket_lazyload_iframes', 'rocket_deactivate_lazyload_on_buddypress_group_pages' ); endif; endif; 3rd-party/plugins/mailchimp.php 0000644 00000002016 15174677547 0012554 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Conflict with MailChimp List Subscribe Form: Enqueue style without lack of performance, grrrr!!! * * @since 2.6 */ function rocket_fix_mailchimp_main_css() { if ( ! defined( 'MCSF_VER' ) || ! function_exists( 'mailchimpSF_main_css' ) ) { return; } $blog_id = get_current_blog_id(); $cache_path = WP_ROCKET_MINIFY_CACHE_PATH . $blog_id . '/'; $cache_url = WP_ROCKET_MINIFY_CACHE_URL . $blog_id . '/'; $css_path = $cache_path . 'mailchimpSF_main_css.css'; if ( ! rocket_direct_filesystem()->is_dir( $cache_path ) ) { rocket_mkdir_p( $cache_path ); } if ( ! rocket_direct_filesystem()->exists( $css_path ) ) { ob_start(); mailchimpSF_main_css(); $content = ob_get_contents(); ob_end_clean(); rocket_put_content( $css_path, $content ); } wp_deregister_style( 'mailchimpSF_main_css' ); wp_register_style( 'mailchimpSF_main_css', $cache_url . 'mailchimpSF_main_css.css', null, MCSF_VER ); } add_action( 'init', 'rocket_fix_mailchimp_main_css', PHP_INT_MAX ); 3rd-party/plugins/wp-print.php 0000644 00000000667 15174677547 0012403 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( function_exists( 'print_link' ) ) : /** * Conflict with WP-Print: don't apply LazyLoad on print pages * * @since 2.6.8 */ function rocket_deactivate_lazyload_on_print_pages() { global $wp_query; if ( isset( $wp_query->query_vars['print'] ) ) { add_filter( 'do_rocket_lazyload', '__return_false' ); } } add_action( 'wp', 'rocket_deactivate_lazyload_on_print_pages' ); endif; 3rd-party/plugins/disqus.php 0000644 00000001242 15174677547 0012121 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( defined( 'DISQUS_VERSION' ) ) : /** * Excludes Disqus scripts from JS minification * * @since 2.9 * @author Remy Perona * * @param Array $excluded_js An array of JS handles enqueued in WordPress. * @return Array the updated array of handles */ function rocket_exclude_js_disqus( $excluded_js ) { $excluded_js[] = str_replace( home_url(), '', plugins_url( '/disqus-comment-system/media/js/disqus.js' ) ); $excluded_js[] = str_replace( home_url(), '', plugins_url( '/disqus-comment-system/media/js/count.js' ) ); return $excluded_js; } add_filter( 'rocket_exclude_js', 'rocket_exclude_js_disqus' ); endif; 3rd-party/plugins/nginx-helper.php 0000644 00000007341 15174677547 0013217 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; global $nginx_helper; if ( isset( $nginx_helper ) ) : global $nginx_purger; /** * Clear WP Rocket cache after the NGINX cache is purged from Nginx Helper. * * @since 3.3.0.1 * * @return void */ function rocket_clear_cache_after_nginx_helper_purge() { if ( ! isset( $_GET['nginx_helper_action'] ) ) { return; } if ( ! check_admin_referer( 'nginx_helper-purge_all' ) ) { return; } if ( 'done' !== sanitize_text_field( wp_unslash( $_GET['nginx_helper_action'] ) ) ) { return; } if ( ! current_user_can( 'rocket_purge_cache' ) ) { return; } // Clear all caching files. rocket_clean_domain(); } add_action( 'admin_init', 'rocket_clear_cache_after_nginx_helper_purge' ); /** * Clear WP Rocket cache for the current page after the NGINX cache is purged from Nginx Helper. * * @since 3.3.0.1 * * @return void */ function rocket_clear_current_page_after_nginx_helper_purge() { if ( ! isset( $_GET['nginx_helper_action'], $_GET['_wpnonce'] ) ) { return; } if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'nginx_helper-purge_all' ) ) { return; } if ( is_admin() ) { return; } if ( ! current_user_can( 'rocket_purge_posts' ) ) { return; } $referer = wp_get_referer(); if ( 0 !== strpos( $referer, 'http' ) ) { $parse_url = get_rocket_parse_url( untrailingslashit( home_url() ) ); $referer = $parse_url['scheme'] . '://' . $parse_url['host'] . $referer; } if ( home_url( '/' ) === $referer ) { rocket_clean_home(); return; } rocket_clean_files( $referer ); } add_action( 'init', 'rocket_clear_current_page_after_nginx_helper_purge' ); /** * Clears NGINX cache for the homepage URL when using "Purge this URL" from the admin bar on the front end * * @since 3.3.0.1 * @author Remy Perona * * @param string $root WP Rocket root cache path. * @param string $lang Current language. * @return void */ function rocket_clean_nginx_cache_home( $root = '', $lang = '' ) { global $nginx_purger; if ( ! isset( $nginx_purger ) ) { return; } $url = get_rocket_i18n_home_url( $lang ); $nginx_purger->purge_url( $url ); } add_action( 'after_rocket_clean_home', 'rocket_clean_nginx_cache_home', 10, 2 ); /** * Clears NGINX cache for a specific URL when using "Purge this URL" from the admin bar on the front end * * @since 3.3.0.1 * @author Remy Perona * * @param string $url URL to purge. * @return void */ function rocket_clean_nginx_cache_url( $url ) { global $nginx_purger; if ( ! isset( $nginx_purger ) ) { return; } if ( ! isset( $_GET['type'], $_GET['_wpnonce'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( false !== strpos( $url, 'index.html' ) ) { return; } if ( 'page' === substr( $url, -4 ) ) { return; } $url = str_replace( '*', '', $url ); $nginx_purger->purge_url( $url ); } add_action( 'after_rocket_clean_file', 'rocket_clean_nginx_cache_url' ); /** * Clean the NGINX cache using Nginx Helper after WP Rocket's cache is purged. * * @since 3.3.0.1 */ function rocket_clean_nginx_helper_cache() { if ( isset( $_GET['nginx_helper_action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } do_action( 'rt_nginx_helper_purge_all' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } add_action( 'rocket_after_clean_domain', 'rocket_clean_nginx_helper_cache' ); /** * Clean the NGINX cache after the Used CSS has been generated. * * @since 3.12.3 */ add_action( 'rocket_rucss_after_clearing_usedcss', 'rocket_clean_nginx_cache_url' ); add_action( 'rocket_saas_complete_job_status', 'rocket_clean_nginx_helper_cache' ); endif; 3rd-party/plugins/varnish-http-purge.php 0000644 00000003552 15174677547 0014366 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( class_exists( 'VarnishPurger' ) ) : add_action( 'admin_init', 'rocket_clear_cache_after_varnish_http_purge' ); /** * Clear WP Rocket cache after purged the Varnish cache via Varnish HTTP Purge plugin * * @since 2.5.5 * * @return void */ function rocket_clear_cache_after_varnish_http_purge() { if ( isset( $_GET['vhp_flush_all'] ) && current_user_can( 'manage_options' ) && check_admin_referer( 'varnish-http-purge' ) ) { // Clear all caching files. rocket_clean_domain(); // Preload cache. run_rocket_bot(); run_rocket_sitemap_preload(); } } endif; add_action( 'rocket_after_clean_domain', 'rocket_clean_varnish_http_purge' ); /** * Call the cache server to purge the cache with Varnish HTTP Purge. * * @since 2.5.5 * * @return void */ function rocket_clean_varnish_http_purge() { if ( class_exists( 'VarnishPurger' ) ) { $url = home_url( '/?vhp-regex' ); $p = wp_parse_url( $url ); $path = ''; $pregex = '.*'; // Build a varniship. if ( defined( 'VHP_VARNISH_IP' ) && VHP_VARNISH_IP ) { $varniship = VHP_VARNISH_IP; } else { $varniship = get_option( 'vhp_varnish_ip' ); } if ( isset( $p['path'] ) ) { $path = $p['path']; } $schema = apply_filters( 'varnish_http_purge_schema', 'http://' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // If we made varniship, let it sail. if ( ! empty( $varniship ) ) { $purgeme = $schema . $varniship . $path . $pregex; } else { $purgeme = $schema . $p['host'] . $path . $pregex; } wp_remote_request( $purgeme, [ 'method' => 'PURGE', 'blocking' => false, 'headers' => [ 'host' => $p['host'], 'X-Purge-Method' => 'regex', ], ] ); do_action( 'after_purge_url', $url, $purgeme ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } } 3rd-party/hosting/presslabs.php 0000644 00000005620 15174677547 0012605 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; require_once WP_CONTENT_DIR . '/advanced-cache.php'; add_action( 'pl_pre_cache_refresh', 'rocket_clean_files', 0 ); add_filter( 'rocket_display_varnish_options_tab', '__return_false' ); add_filter( 'do_rocket_generate_caching_files', '__return_false', PHP_INT_MAX ); add_filter( 'rocket_cache_mandatory_cookies', '__return_empty_array', PHP_INT_MAX ); add_action( 'after_rocket_clean_home', 'rocket_pl_clean_home', 10, 2 ); add_action( 'after_rocket_clean_file', 'rocket_pl_clean_post', 2 ); add_action( 'pl_pre_url_button_cache_refresh', 'rocket_clean_files' ); add_action( 'wp_rocket_loaded', 'rocket_pl_remove_partial_purge_hooks' ); /** * We clear the cache only on the post, homepage and listings when creating/updating/deleting posts. * * @since 3.3 * * @param object $post The Post object itself for which the action occurred. * @param array $permalink A list of permalinks to be flushed from cache. * * @return void */ function rocket_pl_clean_post( $post = false, $permalink = false ) { if ( ! $post || ! $permalink ) { return; } $cache_handler = new \Presslabs\Cache\CacheHandler(); $cache_handler->invalidate_url( $permalink[0], true ); $cache_handler->invalidate_url( home_url( '/' ), true ); $cache_handler->purge_cache( 'listing' ); } /** * We clear the cache for the homepage URL when using "Purge this URL" from the admin bar on the front end. * * @since 3.3 * * @param string $root WP Rocket root cache path. * @param string $lang Current language. * * @return void */ function rocket_pl_clean_home( $root = false, $lang = false ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( ! $post || ! $permalink ) { return; } $cache_handler = new \Presslabs\Cache\CacheHandler(); $cache_handler->invalidate_url( home_url( '/' ), true ); } /** * Remove WP Rocket functions on WP core action hooks to prevent triggering a double cache clear. * * @since 3.3 * * @return void */ function rocket_pl_remove_partial_purge_hooks() { // WP core action hooks rocket_clean_post() gets hooked into. $clean_post_hooks = [ // Disables the refreshing of partial cache when content is edited. 'wp_trash_post', 'delete_post', 'clean_post_cache', 'wp_update_comment_count', ]; // Remove rocket_clean_post() from core action hooks. array_map( function ( $hook ) { remove_action( $hook, 'rocket_clean_post' ); }, $clean_post_hooks ); remove_filter( 'rocket_clean_files', 'rocket_clean_files_users' ); } if ( ! defined( 'DISABLE_CDN_OFFLOAD' ) && defined( 'PL_CDN_HOST' ) ) { /** * If we have CDN enabled we'll add our HOST to the list. * * @since 3.3 * * @param array $hosts Array of CDN hosts. * * @return array Updated array of CDN hosts */ function rocket_add_pl_cdn( $hosts ) { $hosts[] = constant( 'PL_CDN_HOST' ); return $hosts; } add_filter( 'rocket_cdn_cnames', 'rocket_add_pl_cdn', 1 ); } 3rd-party/hosting/pagely.php 0000644 00000000547 15174677547 0012073 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Call the cache server to purge the cache with Pagely hosting. * * @since 2.5.7 * * @return void */ function rocket_clean_pagely() { if ( class_exists( 'PagelyCachePurge' ) ) { $purger = new PagelyCachePurge(); $purger->purgeAll(); } } add_action( 'rocket_after_clean_domain', 'rocket_clean_pagely' ); 3rd-party/hosting/wp-serveur.php 0000644 00000001712 15174677547 0012724 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Allow to purge Varnish on WP Serveur websites. * * @since 2.6.11 */ add_filter( 'do_rocket_varnish_http_purge', '__return_true' ); // Prevent mandatory cookies on hosting with server cache. add_filter( 'rocket_cache_mandatory_cookies', '__return_empty_array', PHP_INT_MAX ); /** * Changes the text on the Varnish one-click block. * * @since 3.0 * @author Remy Perona * * @param array $settings Field settings data. * * @return array modified field settings data. */ function rocket_wpserveur_varnish_field( $settings ) { $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'WP Serveur' ); return $settings; } add_filter( 'rocket_varnish_field_settings', 'rocket_wpserveur_varnish_field' ); add_filter( 'rocket_display_input_varnish_auto_purge', '__return_false' ); 3rd-party/hosting/nginx.php 0000644 00000001037 15174677547 0011730 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Compatibility with an usual NGINX configuration which include: * try_files $uri $uri/ /index.php?q=$uri&$args * * @since 2.3.9 * * @param array $query_strings Array of query strings to cache. * * @return array Updated array of query strings. */ function rocket_better_nginx_compatibility( $query_strings ) { global $is_nginx; if ( $is_nginx ) { $query_strings[] = 'q'; } return $query_strings; } add_filter( 'rocket_cache_query_strings', 'rocket_better_nginx_compatibility' ); front/dns-prefetch.php 0000644 00000001435 15174677547 0011021 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Adds domain names to the list of DNS Prefetch printed by wp_resource_hints * * @since 2.8.9 * @author Remy Perona * * @param Array $hints URLs to print for resource hints. * @param String $relation_type The relation type the URL are printed for. * @return Array URLs to print */ function rocket_dns_prefetch( $hints, $relation_type ) { // Don't add prefetch for uncached pages. if ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $hints; } $domains = rocket_get_dns_prefetch_domains(); if ( (bool) $domains ) { foreach ( $domains as $domain ) { if ( 'dns-prefetch' === $relation_type ) { $hints[] = $domain; } } } return $hints; } add_filter( 'wp_resource_hints', 'rocket_dns_prefetch', 10, 2 ); front/process.php 0000644 00000000006 15174677547 0010106 0 ustar 00 <?php front/cookie.php 0000644 00000001103 15174677547 0007700 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * By default, the lifetime of the cookie comment is one year. * Life is reduced to 3 minutes so that the visitor can enjoy the cached site. * * @since 1.0 */ function rocket_comment_cookie_lifetime() { /** * Filter the lifetime of the cookie comment * * @since 2.2 * * @param int The lifetime of the cookie in seconds */ $cookie_lifetime = apply_filters( 'rocket_comment_cookie_lifetime', 3 * MINUTE_IN_SECONDS ); return $cookie_lifetime; } add_filter( 'comment_cookie_lifetime', 'rocket_comment_cookie_lifetime' ); functions/files.php 0000644 00000126652 15174677547 0010432 0 ustar 00 <?php use WP_Rocket\Logger\Logger; use WP_Rocket\Engine\Cache\AdvancedCache; defined( 'ABSPATH' ) || exit; /** * Creates the advanced-cache.php file. * * @since 3.6 Uses AdvancedCache::get_advanced_cache_content(). * @since 2.0 * * @param AdvancedCache $advanced_cache Optional. Instance of the advanced cache handler. */ function rocket_generate_advanced_cache_file( $advanced_cache = null ) { /** * Filters whether to generate the advanced-cache.php file. * * @since 3.6.3 * * @param bool True (default) to go ahead with advanced cache file generation; false to stop generation. */ if ( ! (bool) apply_filters( 'rocket_generate_advanced_cache_file', true ) ) { return false; } static $done = false; if ( rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ) { $done = false; } if ( $done ) { return false; } $done = true; if ( is_null( $advanced_cache ) ) { $container = apply_filters( 'rocket_container', null ); $advanced_cache = $container->get( 'advanced_cache' ); } return rocket_put_content( rocket_get_constant( 'WP_CONTENT_DIR' ) . '/advanced-cache.php', $advanced_cache->get_advanced_cache_content() ); } /** * Generates the configuration file for the current domain based on the values of options * * @since 2.0 * * @return array Names of all config files & The content that will be printed */ function get_rocket_config_file() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $options = get_option( WP_ROCKET_SLUG ); if ( ! $options ) { return [ [], '', ]; } $buffer = "<?php\n"; $buffer .= "defined( 'ABSPATH' ) || exit;\n\n"; $buffer .= '$rocket_cookie_hash = \'' . COOKIEHASH . "';\n"; $buffer .= '$rocket_logged_in_cookie = \'' . LOGGED_IN_COOKIE . "';\n"; /** * Filters the activation of the common cache for logged-in users. * * @since 2.10 * @author Remy Perona * * @param bool True to activate the common cache, false to ignore. */ if ( apply_filters( 'rocket_common_cache_logged_users', false ) ) { $buffer .= '$rocket_common_cache_logged_users = 1;' . "\n"; } if ( ! empty( $options['cache_webp'] ) ) { /** This filter is documented in inc/classes/buffer/class-cache.php */ $disable_webp_cache = apply_filters( 'rocket_disable_webp_cache', false ); if ( $disable_webp_cache ) { $options['cache_webp'] = 0; } } /** * Filters the use of the mobile cache version for tablets * 'desktop' will serve desktop to tablets, 'mobile' will serve mobile to tablets * * @since 3.2 * @author Remy Perona * * @param string $tablet_version valid values are 'mobile' or 'desktop' */ $buffer .= '$rocket_cache_mobile_files_tablet = \'' . apply_filters( 'rocket_cache_mobile_files_tablet', 'desktop' ) . "';\n"; foreach ( $options as $option => $value ) { if ( 'cache_ssl' === $option ) { if ( 1 !== (int) $value ) { if ( rocket_is_ssl_website() ) { update_rocket_option( 'cache_ssl', 1 ); $value = 1; } } $buffer .= '$rocket_' . $option . ' = ' . (int) $value . ";\n"; } if ( 'cache_mobile' === $option || 'do_caching_mobile_files' === $option || 'cache_webp' === $option ) { $buffer .= '$rocket_' . $option . ' = ' . (int) $value . ";\n"; } if ( 'secret_cache_key' === $option ) { $buffer .= '$rocket_' . $option . ' = \'' . sanitize_key( $value ) . "';\n"; } if ( 'cache_reject_uri' === $option ) { $buffer .= '$rocket_' . $option . ' = \'' . get_rocket_cache_reject_uri( true ) . "';\n"; } if ( 'cache_query_strings' === $option ) { $buffer .= '$rocket_' . $option . ' = ' . call_user_func( 'var_export', get_rocket_cache_query_string(), true ) . ";\n"; } if ( 'cache_reject_cookies' === $option ) { $cookies = get_rocket_cache_reject_cookies(); if ( $cookies && get_rocket_option( 'cache_logged_user' ) ) { // Make sure the "logged-in cookies" are not rejected. $logged_in_cookie = explode( COOKIEHASH, LOGGED_IN_COOKIE ); $logged_in_cookie = array_map( 'preg_quote', $logged_in_cookie ); $logged_in_cookie = implode( '[^|]*', $logged_in_cookie ); $cookies = preg_replace( '/\|' . $logged_in_cookie . '\|/', '|', '|' . $cookies . '|' ); $cookies = trim( $cookies, '|' ); } $buffer .= '$rocket_' . $option . ' = \'' . $cookies . "';\n"; } if ( 'cache_reject_ua' === $option ) { $buffer .= '$rocket_' . $option . ' = \'' . get_rocket_cache_reject_ua() . "';\n"; } } $buffer .= '$rocket_cache_ignored_parameters = ' . call_user_func( 'var_export', rocket_get_ignored_parameters(), true ) . ";\n"; $buffer .= '$rocket_cache_mandatory_cookies = ' . call_user_func( 'var_export', get_rocket_cache_mandatory_cookies(), true ) . ";\n"; $buffer .= '$rocket_cache_dynamic_cookies = ' . call_user_func( 'var_export', get_rocket_cache_dynamic_cookies(), true ) . ";\n"; $buffer .= '$rocket_permalink_structure = \'' . wp_slash( get_option( 'permalink_structure' ) ) . "';\n"; /** This filter is documented in inc/front/htaccess.php */ if ( apply_filters( 'rocket_url_no_dots', false ) ) { $buffer .= '$rocket_url_no_dots = 1;'; } $config_files_path = []; $urls = [ rocket_get_home_url() ]; // Check if a translation plugin is activated and this configuration is in subdomain. $subdomains = get_rocket_i18n_subdomains(); if ( $subdomains ) { $urls = $subdomains; } foreach ( $urls as $url ) { $file = get_rocket_parse_url( untrailingslashit( $url ) ); $file['path'] = ( ! empty( $file['path'] ) ) ? str_replace( '/', '.', untrailingslashit( $file['path'] ) ) : ''; $config_files_path[] = WP_ROCKET_CONFIG_PATH . strtolower( $file['host'] ) . $file['path'] . '.php'; } /** * Filter all config files path * * @since 2.6.5 * * @param array $config_files_path Path of all config files. */ $config_files_path = apply_filters( 'rocket_config_files_path', $config_files_path ); /** * Filter the content of all config files * * @since 2.1 * * @param string $buffer The content that will be printed. * @param array $config_files_path Names of all config files. */ $buffer = apply_filters( 'rocket_config_file', $buffer, $config_files_path ); $buffer = preg_replace( '@array\s+\(@i', 'array(', $buffer ); $buffer = preg_replace( '@array\(\s+\)@i', 'array()', $buffer ); return [ $config_files_path, $buffer ]; } /** * Create the current config domain file * For example, if home_url() return example.com, the config domain file will be in /config/example.com * * @since 2.0 * * @return void */ function rocket_generate_config_file() { list( $config_files_path, $buffer ) = get_rocket_config_file(); if ( count( $config_files_path ) ) { rocket_init_config_dir(); foreach ( $config_files_path as $file ) { rocket_put_content( $file, $buffer ); } } } /** * Remove the current config domain file * * @since 2.6 * * @return void */ function rocket_delete_config_file() { list( $config_files_path ) = get_rocket_config_file(); foreach ( $config_files_path as $config_file ) { rocket_direct_filesystem()->delete( $config_file ); } // Bail out if WP Rocket is multisite. if ( is_multisite() ) { return; } try { $config_dir = new FilesystemIterator( (string) rocket_get_constant( 'WP_ROCKET_CONFIG_PATH' ) ); } catch ( Exception $e ) { return; } // Remove all files with php extension in the config folder. foreach ( $config_dir as $file ) { if ( ! $file->isFile() || 'php' !== strtolower( $file->getExtension() ) ) { continue; } if ( 1 === substr_count( $file->getFilename(), '.' ) ) { continue; } if ( false === strpos( rocket_direct_filesystem()->get_contents( $file->getPathname() ), '$rocket_cookie_hash' ) ) { continue; } rocket_direct_filesystem()->delete( $file->getPathname() ); } } /** * Create all cache folders (wp-rocket & min) * * @since 2.6 * * @return void */ function rocket_init_cache_dir() { global $is_apache; $filesystem = rocket_direct_filesystem(); // Create cache folder if not exist. if ( ! $filesystem->is_dir( WP_ROCKET_CACHE_PATH ) ) { rocket_mkdir_p( WP_ROCKET_CACHE_PATH ); } if ( ! $filesystem->is_file( WP_ROCKET_CACHE_PATH . 'index.html' ) ) { $filesystem->touch( WP_ROCKET_CACHE_PATH . 'index.html' ); } if ( $is_apache ) { $htaccess_path = WP_ROCKET_CACHE_PATH . '.htaccess'; if ( ! $filesystem->is_file( $htaccess_path ) ) { $filesystem->touch( $htaccess_path ); rocket_put_content( $htaccess_path, "<IfModule mod_autoindex.c>\nOptions -Indexes\n</IfModule>" ); } } // Create minify cache folder if not exist. if ( ! $filesystem->is_dir( WP_ROCKET_MINIFY_CACHE_PATH ) ) { rocket_mkdir_p( WP_ROCKET_MINIFY_CACHE_PATH ); } if ( ! $filesystem->is_file( WP_ROCKET_MINIFY_CACHE_PATH . 'index.html' ) ) { $filesystem->touch( WP_ROCKET_MINIFY_CACHE_PATH . 'index.html' ); } // Create busting cache folder if not exist. if ( ! $filesystem->is_dir( WP_ROCKET_CACHE_BUSTING_PATH ) ) { rocket_mkdir_p( WP_ROCKET_CACHE_BUSTING_PATH ); } if ( ! $filesystem->is_file( WP_ROCKET_CACHE_BUSTING_PATH . 'index.html' ) ) { $filesystem->touch( WP_ROCKET_CACHE_BUSTING_PATH . 'index.html' ); } // Create critical CSS folder if not exist. if ( ! $filesystem->is_dir( WP_ROCKET_CRITICAL_CSS_PATH ) ) { rocket_mkdir_p( WP_ROCKET_CRITICAL_CSS_PATH ); } if ( ! $filesystem->is_file( WP_ROCKET_CRITICAL_CSS_PATH . 'index.html' ) ) { $filesystem->touch( WP_ROCKET_CRITICAL_CSS_PATH . 'index.html' ); } } /** * Create the config folder (wp-rocket-config) * * @since 2.6 * * @return void */ function rocket_init_config_dir() { $filesystem = rocket_direct_filesystem(); // Create config domain folder if not exist. if ( ! $filesystem->is_dir( WP_ROCKET_CONFIG_PATH ) ) { rocket_mkdir_p( WP_ROCKET_CONFIG_PATH ); } // Initialize the config directory with index.html to prevent indexing. if ( ! $filesystem->is_file( WP_ROCKET_CONFIG_PATH . 'index.html' ) ) { $filesystem->touch( WP_ROCKET_CONFIG_PATH . 'index.html' ); } } /** * Delete all minify cache files. * * @since 3.5.3 Replaces glob. * @since 2.1 * * @param string|array $extensions Optional. File extensions to minify. Default: js and css. */ function rocket_clean_minify( $extensions = [ 'js', 'css' ] ) { // Bails out if there are no extensions to target. if ( empty( $extensions ) ) { return; } if ( is_string( $extensions ) ) { $extensions = (array) $extensions; } $min_cache_path = rocket_get_constant( 'WP_ROCKET_MINIFY_CACHE_PATH' ); $min_path = $min_cache_path . get_current_blog_id() . '/'; $iterator = _rocket_get_cache_path_iterator( $min_path ); if ( false === $iterator ) { return; } $filesystem = rocket_direct_filesystem(); $min_path_regex = str_replace( '/', '\/', $min_path ); foreach ( $extensions as $ext ) { /** * Fires before the minify cache files are deleted. * * @since 2.1 * * @param string $ext File extensions to minify. */ do_action( 'before_rocket_clean_minify', $ext ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals try { $entries = new RegexIterator( $iterator, "/{$min_path_regex}.*\.{$ext}/" ); } catch ( Exception $e ) { return; } foreach ( $entries as $entry ) { $filesystem->delete( $entry->getPathname() ); } /** * Fires after the minify cache files was deleted. * * @since 2.1 * * @param string $ext File extensions to minify. */ do_action( 'after_rocket_clean_minify', $ext ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } // Delete any directories. foreach ( $iterator as $item ) { if ( $filesystem->is_dir( $item ) ) { $filesystem->delete( $item ); } } // Clean the cache/min/3rd-party items. try { $files = new FilesystemIterator( "{$min_cache_path}3rd-party" ); foreach ( $files as $file ) { if ( $filesystem->is_file( $file ) ) { $filesystem->delete( $file ); } } } catch ( UnexpectedValueException $e ) { // No logging yet. return; } } /** * Delete all cache busting files. * * @since 2.9 * * @param string|array $extensions (default: array('js','css') File extensions to clean. * @return void */ function rocket_clean_cache_busting( $extensions = [ 'js', 'css' ] ) { if ( empty( $extensions ) ) { return; } if ( is_string( $extensions ) ) { $extensions = (array) $extensions; } $cache_busting_path = rocket_get_constant( 'WP_ROCKET_CACHE_BUSTING_PATH' ) . get_current_blog_id() . '/'; $iterator = _rocket_get_cache_path_iterator( $cache_busting_path ); if ( false === $iterator ) { return; } $filesystem = rocket_direct_filesystem(); $busting_path_regex = str_replace( '/', '\/', $cache_busting_path ); if ( ! rocket_direct_filesystem()->is_dir( $cache_busting_path ) ) { rocket_mkdir_p( $cache_busting_path ); Logger::debug( 'No Cache Busting folder found.', [ 'mkdir cache busting folder', 'cache_busting_path' => $cache_busting_path, ] ); return; } foreach ( $extensions as $ext ) { /** * Fires before the cache busting files are deleted * * @since 2.9 * * @param string $ext File extensions to clean. */ do_action( 'before_rocket_clean_busting', $ext ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals try { $entries = new RegexIterator( $iterator, "/{$busting_path_regex}.*\.{$ext}/" ); } catch ( Exception $e ) { return; } foreach ( $entries as $entry ) { $filesystem->delete( $entry->getPathname() ); } /** * Fires after the cache busting files was deleted * * @since 2.9 * * @param string $ext File extensions to clean. */ do_action( 'after_rocket_clean_cache_busting', $ext ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } foreach ( $iterator as $item ) { if ( $filesystem->is_dir( $item ) ) { $filesystem->delete( $item ); } } } /** * Returns the right path when the post is trashed. * * @param array $parsed_url current parsed url. * @param int $post_id ID from the post. * * @return array */ function rocket_maybe_find_right_trash_url( array $parsed_url, int $post_id ) { $post = get_post( $post_id ); if ( ! $post || 'trash' !== $post->post_status ) { return $parsed_url; } $post->post_status = 'publish'; $permalink = get_permalink( $post ); if ( ! $permalink ) { return $parsed_url; } $new_permalink = str_replace( '__trashed', '', $permalink ); return get_rocket_parse_url( $new_permalink ); } /** * Delete one or several cache files. * * @since 3.5.5 Optimizes by grabbing root cache dirs once, bailing out when file/dir doesn't exist, & directly * deleting files. * @since 3.5.4 Replaces glob and optimizes. * @since 2.0 Delete cache files for all users. * @since 1.1.0 Add filter rocket_clean_files. * @since 1.0 * * @param string|array $urls URLs of cache files to be deleted. * @param WP_Filesystem_Direct|null $filesystem Optional. Instance of filesystem handler. * @param bool $run_actions Run actions. */ function rocket_clean_files( $urls, $filesystem = null, $run_actions = true ) { $urls = (array) $urls; if ( empty( $urls ) ) { return; } $urls = array_filter( $urls ); if ( empty( $urls ) ) { return; } /** This filter is documented in inc/front/htaccess.php */ $url_no_dots = (bool) apply_filters( 'rocket_url_no_dots', false ); $cache_path = _rocket_get_wp_rocket_cache_path(); if ( empty( $filesystem ) ) { $filesystem = rocket_direct_filesystem(); } if ( $run_actions ) { /** * Fires before all cache files are deleted. * * @since 3.2.2 * * @param array $urls The URLs corresponding to the deleted cache files. */ do_action( 'before_rocket_clean_files', $urls ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } foreach ( $urls as $url_key => $url ) { if ( $run_actions ) { /** * Fires before the cache file is deleted. * * @param string $url The URL that the cache file to be deleted. * @since 1.0 */ do_action( 'before_rocket_clean_file', $url ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } if ( $url_no_dots ) { $url = str_replace( '.', '_', $url ); } $parsed_url = get_rocket_parse_url( $url ); if ( ! empty( $parsed_url['host'] ) ) { foreach ( _rocket_get_cache_dirs( $parsed_url['host'], $cache_path ) as $dir ) { // Decode url path. $url_chunks = explode( '/', $parsed_url['path'] ); $matches = preg_grep( '/%/', $url_chunks ); if ( ! empty( $matches ) ) { $parsed_url['path'] = rawurldecode( $parsed_url['path'] ); } // Encode Non-latin characters if found in url path. if ( false !== preg_match_all( '/(?<non_latin>[^\x00-\x7F]+)/', $parsed_url['path'], $matches ) ) { $cb_encode_non_latin = function ( $non_latin ) { return strtolower( rawurlencode( $non_latin ) ); }; $parsed_url['path'] = str_replace( $matches['non_latin'], array_map( $cb_encode_non_latin, $matches['non_latin'] ), $parsed_url['path'] ); } $entry = $dir . $parsed_url['path']; // For regex we use it for file names only, and it should include the * character. if ( str_contains( $entry, '*' ) ) { $regex_part = basename( $entry ); $search_dir = str_replace( $regex_part, '', $entry ); $matched_files = _rocket_get_dir_files_by_regex( $search_dir, '#' . $regex_part . '#i' ); foreach ( $matched_files as $item ) { $current_file = $item->getPath() . DIRECTORY_SEPARATOR . $item->getFilename(); if ( $filesystem->exists( $current_file ) ) { $filesystem->delete( $current_file ); } } // Remove the regex part from the url. $url = str_replace( $regex_part, '', $url ); $urls[ $url_key ] = $url; } // Skip if the dir/file does not exist. if ( ! $filesystem->exists( $entry ) ) { continue; } if ( $filesystem->is_dir( $entry ) ) { rocket_rrmdir( $entry, [], $filesystem ); } else { $filesystem->delete( $entry ); } } } if ( $run_actions ) { /** * Fires after the cache file is deleted. * * @param string $url The URL that the cache file was deleted. * @since 1.0 */ do_action( 'after_rocket_clean_file', $url ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } } if ( ! $run_actions ) { return; } /** * Fires after all cache files are deleted. * * @since 3.2.2 * * @param array $urls The URLs corresponding to the deleted cache files. */ do_action( 'after_rocket_clean_files', $urls ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Remove the home cache file and pagination * * $since 2.2 Add $lang argument * * @since 2.0 Delete cache files for all users * @since 1.0 * * @param string $lang (default: '') The language code. * @return void */ function rocket_clean_home( $lang = '' ) { $parse_url = get_rocket_parse_url( get_rocket_i18n_home_url( $lang ) ); /** This filter is documented in inc/front/htaccess.php */ if ( apply_filters( 'rocket_url_no_dots', false ) ) { $parse_url['host'] = str_replace( '.', '_', $parse_url['host'] ); } $root = WP_ROCKET_CACHE_PATH . $parse_url['host'] . '*' . untrailingslashit( $parse_url['path'] ); /** * Filter the homepage caching folder root * * @since 2.6.5 * @param array $root The root that will be returned. * @param string $host The website host. * @param string $path The website path. */ $root = apply_filters( 'rocket_clean_home_root', $root, $parse_url['host'], $parse_url['path'] ); /** * Fires before the home cache file is deleted * * @since 1.0 * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. */ do_action( 'before_rocket_clean_home', $root, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // Delete homepage. $files = glob( $root . '/*', GLOB_NOSORT ); if ( $files ) { foreach ( $files as $file ) { if ( preg_match( '#/index(?:-.+\.|\.)html(?:_gzip)?$#', $file ) ) { rocket_direct_filesystem()->delete( $file ); } } } // Delete homepage pagination. $dirs = glob( $root . '*/' . $GLOBALS['wp_rewrite']->pagination_base, GLOB_NOSORT ); if ( $dirs ) { foreach ( $dirs as $dir ) { rocket_rrmdir( $dir ); } } $param_dirs = glob( $root . '/#*', GLOB_NOSORT ); if ( $param_dirs ) { foreach ( $param_dirs as $dir ) { rocket_rrmdir( $dir ); } } // Remove the hidden empty file for mobile detection on NGINX with the Rocket NGINX configuration. $nginx_mobile_detect_files = glob( $root . '/.mobile-active', GLOB_NOSORT ); if ( $nginx_mobile_detect_files ) { foreach ( $nginx_mobile_detect_files as $nginx_mobile_detect_file ) { // no array map to use @. rocket_direct_filesystem()->delete( $nginx_mobile_detect_file ); } } // Remove the hidden empty file for webp. $nowebp_detect_files = glob( $root . '/.no-webp', GLOB_NOSORT ); if ( $nowebp_detect_files ) { foreach ( $nowebp_detect_files as $nowebp_detect_file ) { // no array map to use @. rocket_direct_filesystem()->delete( $nowebp_detect_file ); } } /** * Fires after the home cache file was deleted * * @since 1.0 * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. */ do_action( 'after_rocket_clean_home', $root, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Remove the home cache feed * * @since 2.7 * * @return void */ function rocket_clean_home_feeds() { if ( ! has_filter( 'rocket_cache_reject_uri', 'wp_rocket_cache_feed' ) ) { return; } $urls = []; $urls[] = get_feed_link(); $urls[] = get_feed_link( 'comments_' ); /** * Filter the home feeds urls * * @since 2.7 * @param array $urls The urls of the home feeds. */ $urls = apply_filters( 'rocket_clean_home_feeds', $urls ); /** * Fires before the home feeds cache is deleted * * @since 2.7 * * @param array $urls The urls of the home feeds. */ do_action( 'before_rocket_clean_home_feeds', $urls ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals rocket_clean_files( $urls ); /** * Fires after the home feeds cache was deleted * * @since 2.7 * * @param array $urls The urls of the home feeds. */ do_action( 'after_rocket_clean_home_feeds', $urls ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Remove all cache files for the domain. * * @since 3.5.5 Optimizes by grabbing root cache dirs once, bailing out when file/dir doesn't exist, & directly * deleting files. * @since 3.5.3 Replaces glob with SPL. * @since 2.0 Delete domain cache files for all users * @since 1.0 * * @param string $lang Optional. The language code. Default: empty string. * @param WP_Filesystem_Direct|null $filesystem Optional. Instance of filesystem handler. */ function rocket_clean_domain( $lang = '', $filesystem = null ) { if ( did_action( 'rocket_after_clean_domain' ) ) { return; } if ( rocket_is_importing() ) { return; } $urls = ( ! $lang || is_object( $lang ) || is_array( $lang ) || is_int( $lang ) ) ? (array) get_rocket_i18n_uri() : (array) get_rocket_i18n_home_url( $lang ); /** * Filter URLs to delete all caching files from a domain. * * @since 2.6.4 * * @param array URLs that will be returned. * @param string The language code. */ $urls = (array) apply_filters( 'rocket_clean_domain_urls', $urls, $lang ); $urls = array_filter( $urls ); if ( empty( $urls ) ) { return false; } /** This filter is documented in inc/front/htaccess.php */ $url_no_dots = (bool) apply_filters( 'rocket_url_no_dots', false ); $cache_path = _rocket_get_wp_rocket_cache_path(); $dirs_to_preserve = get_rocket_i18n_to_preserve( $lang, $cache_path ); if ( empty( $filesystem ) ) { $filesystem = rocket_direct_filesystem(); } foreach ( $urls as $url ) { $parsed_url = get_rocket_parse_url( $url ); if ( $url_no_dots ) { $parsed_url['host'] = str_replace( '.', '_', $parsed_url['host'] ); } $root = $cache_path . $parsed_url['host'] . $parsed_url['path']; /** * Fires before all cache files are deleted. * * @since 1.0 * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. * @param string $url The home url. */ do_action( 'before_rocket_clean_domain', $root, $lang, $url ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals foreach ( _rocket_get_cache_dirs( $parsed_url['host'], $cache_path ) as $dir ) { $entry = $dir . $parsed_url['path']; // Skip if the dir/file does not exist. if ( ! $filesystem->exists( $entry ) ) { continue; } if ( $filesystem->is_dir( $entry ) ) { rocket_rrmdir( $entry, $dirs_to_preserve, $filesystem ); } else { $filesystem->delete( $entry ); } } /** * Fires after all cache files was deleted. * * @since 1.0 * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. * @param string $url The home url. */ do_action( 'after_rocket_clean_domain', $root, $lang, $url ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Fires after all cache files was deleted. * * @since 3.15.5 * * @param string $lang The current lang to purge. * @param array|string[] $urls All urls to clean. */ do_action( 'rocket_after_clean_domain', $lang, $urls ); return true; } /** * Delete the caching files of a specific term. * * $since 2.6.8 * * @param int $term_id The term ID. * @param string $taxonomy_slug The taxonomy slug. * @return void */ function rocket_clean_term( $term_id, $taxonomy_slug ) { $purge_urls = []; // Get all term infos. $term = get_term_by( 'id', $term_id, $taxonomy_slug ); // Get the term language. $i18n_plugin = rocket_has_i18n(); if ( 'wpml' === $i18n_plugin && ! rocket_is_plugin_active( 'woocommerce-multilingual/wpml-woocommerce.php' ) ) { // WPML. $lang = $GLOBALS['sitepress']->get_language_for_element( $term_id, 'tax_' . $taxonomy_slug ); } elseif ( 'polylang' === $i18n_plugin ) { // Polylang. $lang = pll_get_term_language( $term_id ); } else { $lang = false; } // Get permalink. $permalink = get_term_link( $term, $taxonomy_slug ); // Add permalink. if ( '/' !== rocket_extract_url_component( $permalink, PHP_URL_PATH ) ) { array_push( $purge_urls, $permalink ); } /** * Fires before deleted caching files related with the term * * @since 2.6.8 * @param obj $term The term object. * @param array $purge_urls URLs cache files to remove. * @param string $lang The term language. */ do_action( 'before_rocket_clean_term', $term, $purge_urls, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals /** * Filter URLs cache files to remove * * @since 2.6.8 * @param array $purge_urls List of URLs cache files to remove. * @param obj $term The term object. */ $purge_urls = apply_filters( 'rocket_term_purge_urls', $purge_urls, $term ); // Purge all files. rocket_clean_files( $purge_urls ); // Never forget to purge homepage and their pagination. rocket_clean_home( $lang ); /** * Fires before deleted caching files related with the term * * @since 2.6.8 * @param obj $term The term object. * @param array $purge_urls URLs cache files to remove. * @param string $lang The term language. */ do_action( 'after_rocket_clean_term', $term, $purge_urls, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Delete the caching files of a specific user * * $since 2.6.12 * * @param int $user_id The user ID. * @param string $lang The language code. * @return void */ function rocket_clean_user( $user_id, $lang = '' ) { $urls = ( ! $lang || is_object( $lang ) ) ? get_rocket_i18n_uri() : get_rocket_i18n_home_url( $lang ); $urls = (array) $urls; /** This filter is documented in inc/functions/files.php */ $urls = apply_filters( 'rocket_clean_domain_urls', $urls, $lang ); $urls = array_filter( $urls ); $user = get_user_by( 'id', $user_id ); if ( ! $user ) { return; } $user_key = rawurlencode( $user->user_login ) . '-' . get_rocket_option( 'secret_cache_key' ); foreach ( $urls as $url ) { $parse_url = get_rocket_parse_url( $url ); /** This filter is documented in inc/front/htaccess.php */ if ( apply_filters( 'rocket_url_no_dots', false ) ) { $parse_url['host'] = str_replace( '.', '_', $parse_url['host'] ); } $cache_dir = $parse_url['host'] . '-' . strtolower( $user_key ); $cache_dir = $cache_dir . $parse_url['path']; $root = rocket_get_constant( 'WP_ROCKET_CACHE_PATH' ) . $cache_dir; /** * Fires before all caching files are deleted for a specific user * * @since 2.6.12 * * @param int $user_id The path of home cache file. * @param string $lang The language code. */ do_action( 'before_rocket_clean_user', $user_id, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // Delete cache domain files. $dirs = glob( $root . '*', GLOB_NOSORT ); if ( $dirs ) { foreach ( $dirs as $dir ) { rocket_rrmdir( $dir, get_rocket_i18n_to_preserve( $lang ) ); } } /** * Fires after all caching files are deleted for a specific user * * @since 2.6.12 * * @param int $user_id The path of home cache file. * @param string $lang The language code. */ do_action( 'after_rocket_clean_user', $user_id, $lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } } /** * Remove all caching files in the cache folder * * @since 2.6.8 * * @return void */ function rocket_clean_cache_dir() { /** * Fires before deleting all caching files in the cache folder * * @since 2.6.8 */ do_action( 'before_rocket_clean_cache_dir' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // Delete all caching files. $dirs = glob( WP_ROCKET_CACHE_PATH . '*', GLOB_NOSORT ); if ( $dirs ) { foreach ( $dirs as $dir ) { rocket_rrmdir( $dir ); } } /** * Fires after deleting all caching files in the cache folder * * @since 2.6.8 */ do_action( 'after_rocket_clean_cache_dir' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Remove a single file or a folder recursively. * * @since 3.5.3 Replaces glob and optimizes. * @since 1.0 * @since 3.5.3 Bails if given dir should be preserved; replaces glob; optimizes. * * @param string $dir File/Directory to delete. * @param array $dirs_to_preserve Optional. Dirs that should not be deleted. * @param WP_Filesystem_Direct|null $filesystem Optional. Instance of filesystem handler. */ function rocket_rrmdir( $dir, array $dirs_to_preserve = [], $filesystem = null ) { $dir = untrailingslashit( $dir ); if ( empty( $filesystem ) ) { $filesystem = rocket_direct_filesystem(); } /** * Fires before a file/directory cache is deleted * * @since 1.1.0 * * @param string $dir File/Directory to delete. * @param array $dirs_to_preserve Directories that should not be deleted. */ do_action( 'before_rocket_rrmdir', $dir, $dirs_to_preserve ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // Remove the hidden empty file for mobile detection on NGINX with the Rocket NGINX configuration. $nginx_mobile_detect_file = $dir . '/.mobile-active'; if ( $filesystem->is_dir( $dir ) && $filesystem->exists( $nginx_mobile_detect_file ) ) { $filesystem->delete( $nginx_mobile_detect_file ); } // Remove the hidden empty file for webp. $nowebp_detect_file = $dir . '/.no-webp'; if ( $filesystem->is_dir( $dir ) && $filesystem->exists( $nowebp_detect_file ) ) { $filesystem->delete( $nowebp_detect_file ); } if ( ! $filesystem->is_dir( $dir ) ) { $filesystem->delete( $dir ); return; } // Get the directory entries. $entries = []; try { foreach ( new FilesystemIterator( $dir ) as $entry ) { $entries[] = $entry->getPathname(); } } catch ( Exception $e ) { // phpcs:disable Generic.CodeAnalysis.EmptyStatement.DetectedCatch // No action required, as logging not enabled. } // Exclude directories to preserve from the entries. if ( ! empty( $dirs_to_preserve ) && ! empty( $entries ) ) { $keys = []; foreach ( $dirs_to_preserve as $dir_to_preserve ) { $matches = preg_grep( "#^$dir_to_preserve$#", $entries ); $keys[] = reset( $matches ); } if ( ! empty( $keys ) ) { $keys = array_filter( $keys ); if ( ! empty( $keys ) ) { $entries = array_diff( $entries, $keys ); } } } foreach ( $entries as $entry ) { // If not a directory, delete it. if ( ! $filesystem->is_dir( $entry ) ) { $filesystem->delete( $entry ); } else { rocket_rrmdir( $entry, $dirs_to_preserve, $filesystem ); } } $filesystem->delete( $dir ); /** * Fires after a file/directory cache was deleted * * @since 1.1.0 * * @param string $dir File/Directory to delete. * @param array $dirs_to_preserve Dirs that should not be deleted. */ do_action( 'after_rocket_rrmdir', $dir, $dirs_to_preserve ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Instantiate the filesystem class * * @since 2.10 * * @return WP_Filesystem_Direct WP_Filesystem_Direct instance */ function rocket_direct_filesystem() { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; return new WP_Filesystem_Direct( new StdClass() ); } /** * Directory creation based on WordPress Filesystem * * @since 1.3.4 * * @param string $dir The path of directory will be created. * @return bool */ function rocket_mkdir( $dir ) { $chmod = rocket_get_filesystem_perms( 'dir' ); return rocket_direct_filesystem()->mkdir( $dir, $chmod ); } /** * Recursive directory creation based on full path. * * @param string $target path to the directory we want to create. * @param WP_Filesystem_Direct|null $filesystem WordPress filesystem. * @return bool True if directory is created/exists, false otherwise * @since 1.3.4 * * @source wp_mkdir_p() in /wp-includes/functions.php */ function rocket_mkdir_p( $target, $filesystem = null ) { $wrapper = null; $filesystem = $filesystem ?: rocket_direct_filesystem(); if ( rocket_is_stream( $target ) ) { list( $wrapper, $target ) = explode( '://', $target, 2 ); } // from php.net/mkdir user contributed notes. $target = str_replace( '//', '/', $target ); // Put the wrapper back on the target. if ( null !== $wrapper ) { $target = $wrapper . '://' . $target; } // safe mode fails with a trailing slash under certain PHP versions. $target = rtrim( $target, '/\\' ); if ( empty( $target ) ) { $target = '/'; } if ( $filesystem->exists( $target ) ) { return $filesystem->is_dir( $target ); } // Attempting to create the directory may clutter up our display. if ( rocket_mkdir( $target ) ) { return true; } elseif ( $filesystem->is_dir( dirname( $target ) ) ) { return false; } // If the above failed, attempt to create the parent node, then try again. if ( ( '/' !== $target ) && ( rocket_mkdir_p( dirname( $target ), $filesystem ) ) ) { return rocket_mkdir_p( $target, $filesystem ); } return false; } /** * Test if a given path is a stream URL. * * @since 3.5.3 * * @source wp_is_stream() in /wp-includes/functions.php * * @param string $path The resource path or URL. * * @return bool true if the path is a stream URL; else false. */ function rocket_is_stream( $path ) { $scheme_separator = strpos( $path, '://' ); if ( false === $scheme_separator ) { // $path isn't a stream. return false; } $stream = substr( $path, 0, $scheme_separator ); return in_array( $stream, stream_get_wrappers(), true ); } /** * File creation based on WordPress Filesystem. * * @since 1.3.5 * * @param string $file The path of file will be created. * @param string $content The content that will be printed in advanced-cache.php. * * @return bool true on success; else, false on failure. */ function rocket_put_content( $file, $content ) { $chmod = rocket_get_filesystem_perms( 'file' ); return rocket_direct_filesystem()->put_contents( $file, $content, $chmod ); } /** * Get the permissions to apply to files and folders. * * Reminder: * `$perm = fileperms( $file );` * * WHAT | TYPE | FILE | FOLDER | * ----------------------------------------------+--------+--------+--------| * `$perm` | int | 33188 | 16877 | * `substr( decoct( $perm ), -4 )` | string | '0644' | '0755' | * `substr( sprintf( '%o', $perm ), -4 )` | string | '0644' | '0755' | * `$perm & 0777` | int | 420 | 493 | * `decoct( $perm & 0777 )` | string | '644' | '755' | * `substr( sprintf( '%o', $perm & 0777 ), -4 )` | string | '644' | '755' | * * @since 3.2.4 * * @param string $type The type: 'dir' or 'file'. * * @return int Octal integer. */ function rocket_get_filesystem_perms( $type ) { static $perms = []; if ( rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ) { $perms = []; } // Allow variants. switch ( $type ) { case 'dir': case 'dirs': case 'folder': case 'folders': $type = 'dir'; break; case 'file': case 'files': $type = 'file'; break; default: return 0755; } if ( isset( $perms[ $type ] ) ) { return $perms[ $type ]; } // If the constants are not defined, use fileperms() like WordPress does. if ( 'dir' === $type ) { $fs_chmod_dir = (int) rocket_get_constant( 'FS_CHMOD_DIR', 0 ); $perms[ $type ] = $fs_chmod_dir > 0 ? $fs_chmod_dir : fileperms( rocket_get_constant( 'ABSPATH' ) ) & 0777 | 0755; } else { $fs_chmod_file = (int) rocket_get_constant( 'FS_CHMOD_FILE', 0 ); $perms[ $type ] = $fs_chmod_file > 0 ? $fs_chmod_file : fileperms( rocket_get_constant( 'ABSPATH' ) . 'index.php' ) & 0777 | 0644; } return $perms[ $type ]; } /** * Gets Directory files matches regex. * * @since 3.6.3 * @access private * * @param string $dir Directory to search for files inside it. * @param string $regex Regular expression for files need to be searched for. * * @return array|RegexIterator List of files matches this regular expression. */ function _rocket_get_dir_files_by_regex( $dir, $regex ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound try { $iterator = new IteratorIterator( new FilesystemIterator( $dir ) ); return new RegexIterator( $iterator, $regex ); } catch ( Exception $e ) { return []; } } /** * Get the recursive iterator for the cache path. * * @since 3.5.4 * @access private * * @param string $cache_path Path to the cache directory. * * @return bool|RecursiveIteratorIterator Iterator on success; else false; */ function _rocket_get_cache_path_iterator( $cache_path ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound try { return new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $cache_path ), RecursiveIteratorIterator::SELF_FIRST, RecursiveIteratorIterator::CATCH_GET_CHILD ); } catch ( Exception $e ) { // No logging yet. return false; } } /** * Gets the directories for the given URL host from the cache/wp-rocket/ directory or stored memory. * * @since 3.5.5 * @access private * * @param string $url_host The URL's host. * @param string $cache_path Cache's path, e.g. cache/wp-rocket/. * @param bool $hard_reset Optional. When true, resets the static domain directories array and bails out. * * @return array */ function _rocket_get_cache_dirs( $url_host, $cache_path = '', $hard_reset = false ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound static $domain_dirs = []; if ( true === $hard_reset ) { $domain_dirs = []; return; } if ( isset( $domain_dirs[ $url_host ] ) ) { return $domain_dirs[ $url_host ]; } if ( empty( $cache_path ) ) { $cache_path = _rocket_get_wp_rocket_cache_path(); } try { $iterator = new IteratorIterator( new FilesystemIterator( $cache_path ) ); } catch ( Exception $e ) { return []; } $regex = sprintf( '/%1$s%2$s(.*)/i', _rocket_normalize_path( $cache_path, true ), $url_host ); try { $entries = new RegexIterator( $iterator, $regex ); } catch ( Exception $e ) { return []; } $domain_dirs[ $url_host ] = []; foreach ( $entries as $entry ) { $domain_dirs[ $url_host ][] = $entry->getPathname(); } return $domain_dirs[ $url_host ]; } /** * Normalizes the given filesystem path: * - Windows/IIS-based servers: converts all directory separators to "\\" or, when escaping, to "\\\\". * - Linux-based servers: if $forced is true, uses wp_normalize_path(); else, returns the original path. * * @since 3.5.5 * @access private * * @param string $path Filesystem path (file or directory) to normalize. * @param bool $escape Optional. When true, escapes the directory separator(s). * @param bool $force Optional. When true, forces normalizing off non-Windows' paths. * * @return string */ function _rocket_normalize_path( $path, $escape = false, $force = false ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound if ( _rocket_is_windows_fs( $path ) ) { $path = str_replace( '/', '\\', $path ); return $escape ? str_replace( '\\', '\\\\', $path ) : $path; } if ( $escape ) { return str_replace( '/', '\/', $path ); } if ( ! $force ) { return $path; } return wp_normalize_path( $path ); } /** * Checks if the filesystem (fs) is for Windows/IIS server. * * @since 3.5.5 * @access private * * @param bool $hard_reset Optional. When true, resets the memoization. * * @return bool */ function _rocket_is_windows_fs( $hard_reset = false ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound static $is_windows = null; if ( $hard_reset ) { $is_windows = null; } if ( is_null( $is_windows ) ) { $is_windows = ( DIRECTORY_SEPARATOR === '\\' && ! rocket_get_constant( 'WP_ROCKET_RUNNING_VFS', false ) ); } return $is_windows; } /** * Gets the normalized cache path, i.e. normalizes constant "WP_ROCKET_CACHE_PATH". * * @since 3.5.5 * @access private * * @return string */ function _rocket_get_wp_rocket_cache_path() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound return _rocket_normalize_path( rocket_get_constant( 'WP_ROCKET_CACHE_PATH' ) ); } /** * Gets .php files in a directory as an array of SplFileInfo objects. * * @since 3.6.3 * * @param string $dir_path Directory to check. * * @return array .php files in the directory. [...SplFileInfo] */ function _rocket_get_php_files_in_dir( $dir_path ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound try { $config_dir = new FilesystemIterator( (string) $dir_path ); } catch ( Exception $e ) { return []; } $files = []; foreach ( $config_dir as $file ) { if ( $file->isFile() && 'php' === $file->getExtension() ) { $files[] = $file; } } return $files; } /** * Get recursive files matched by regex. * * @since 3.6.3 * * @param string $regex Regular Expression to be applied. * * @return array|RegexIterator List of files which match the regular expression (SplFileInfo). */ function _rocket_get_recursive_dir_files_by_regex( $regex ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound try { $cache_path = _rocket_get_wp_rocket_cache_path(); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $cache_path, FilesystemIterator::SKIP_DOTS ) ); return new RegexIterator( $iterator, $regex, RecursiveRegexIterator::MATCH ); } catch ( Exception $e ) { return []; } } functions/posts.php 0000644 00000021251 15174677547 0010465 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Get the permalink post * * @since 1.3.1 * * @source : get_sample_permalink() in wp-admin/includes/post.php * * @param int $id The post ID. * @param string $title The post title. * @param string $name The post name. * * @return array */ function get_rocket_sample_permalink( $id, $title = null, $name = null ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $post = get_post( $id ); if ( ! $post ) { return [ '', '' ]; } $ptype = get_post_type_object( $post->post_type ); $original_status = $post->post_status; $original_date = $post->post_date; $original_name = $post->post_name; // Hack: get_permalink() would return ugly permalink for drafts, so we will fake that our post is published. if ( in_array( $post->post_status, [ 'draft', 'pending' ], true ) ) { $post->post_status = 'publish'; $post->post_name = sanitize_title( $post->post_name ? $post->post_name : $post->post_title, $post->ID ); } // If the user wants to set a new name -- override the current one. // Note: if empty name is supplied -- use the title instead, see #6072. if ( ! is_null( $name ) ) { $post->post_name = sanitize_title( $name ? $name : $title, $post->ID ); } $post->post_name = wp_unique_post_slug( $post->post_name, $post->ID, $post->post_status, $post->post_type, $post->post_parent ); $post->filter = 'sample'; $permalink = get_permalink( $post, false ); // Replace custom post_type Token with generic pagename token for ease of use. $permalink = str_replace( "%$post->post_type%", '%pagename%', $permalink ); // Handle page hierarchy. if ( $ptype->hierarchical ) { $uri = get_page_uri( $post ); $uri = untrailingslashit( $uri ); $uri = strrev( stristr( strrev( $uri ), '/' ) ); $uri = untrailingslashit( $uri ); /** This filter is documented in wp-admin/edit-tag-form.php */ $uri = apply_filters( 'editable_slug', $uri, $post ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals if ( ! empty( $uri ) ) { $uri .= '/'; } $permalink = str_replace( '%pagename%', "{$uri}%pagename%", $permalink ); } /** This filter is documented in wp-admin/edit-tag-form.php */ $permalink = [ $permalink, apply_filters( 'editable_slug', $post->post_name, $post ) ]; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $post->post_status = $original_status; $post->post_date = $original_date; $post->post_name = $original_name; unset( $post->filter ); return $permalink; } if ( ! function_exists( 'rocket_url_to_postid' ) ) { /** * Get the post ID from the URL. * * @param string $url URL of the page. * @param array|string[] $search_in_post_statuses Post statuses to search in. * @return float|int Post ID. */ function rocket_url_to_postid( string $url, array $search_in_post_statuses = [ 'publish', 'private' ] ) { global $wp_rewrite; /** * Filters the URL to derive the post ID from. * * @since 2.2.0 * * @param string $url The URL to derive the post ID from. */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $url = apply_filters( 'url_to_postid', $url ); $url_host = wp_parse_url( $url, PHP_URL_HOST ); if ( is_string( $url_host ) ) { $url_host = str_replace( 'www.', '', $url_host ); } else { $url_host = ''; } $home_url_host = wp_parse_url( home_url(), PHP_URL_HOST ); if ( is_string( $home_url_host ) ) { $home_url_host = str_replace( 'www.', '', $home_url_host ); } else { $home_url_host = ''; } // Bail early if the URL does not belong to this site. if ( $url_host && $url_host !== $home_url_host ) { return 0; } // First, check to see if there is a 'p=N' or 'page_id=N' to match against. if ( preg_match( '#[?&](p|page_id|attachment_id)=(\d+)#', $url, $values ) ) { $id = absint( $values[2] ); if ( $id ) { if ( empty( $search_in_post_statuses ) || ! in_array( get_post_status( $id ), $search_in_post_statuses, true ) ) { return 0; } return $id; } } // Get rid of the #anchor. $url_split = explode( '#', $url ); $url = $url_split[0]; // Get rid of URL ?query=string. $url_split = explode( '?', $url ); $url = $url_split[0]; // Set the correct URL scheme. $scheme = wp_parse_url( home_url(), PHP_URL_SCHEME ); $url = set_url_scheme( $url, $scheme ); // Add 'www.' if it is absent and should be there. if ( str_contains( home_url(), '://www.' ) && ! str_contains( $url, '://www.' ) ) { $url = str_replace( '://', '://www.', $url ); } // Strip 'www.' if it is present and shouldn't be. if ( ! str_contains( home_url(), '://www.' ) ) { $url = str_replace( '://www.', '://', $url ); } if ( trim( $url, '/' ) === home_url() && 'page' === get_option( 'show_on_front' ) ) { $page_on_front = get_option( 'page_on_front' ); if ( $page_on_front && get_post( $page_on_front ) instanceof WP_Post ) { if ( empty( $search_in_post_statuses ) || ! in_array( get_post_status( (int) $page_on_front ), $search_in_post_statuses, true ) ) { return 0; } return (int) $page_on_front; } } // Check to see if we are using rewrite rules. $rewrite = $wp_rewrite->wp_rewrite_rules(); // Not using rewrite rules, and 'p=N' and 'page_id=N' methods failed, so we're out of options. if ( empty( $rewrite ) ) { return 0; } // Strip 'index.php/' if we're not using path info permalinks. if ( ! $wp_rewrite->using_index_permalinks() ) { $url = str_replace( $wp_rewrite->index . '/', '', $url ); } if ( str_contains( trailingslashit( $url ), home_url( '/' ) ) ) { // Chop off http://domain.com/[path]. $url = str_replace( home_url(), '', $url ); } else { // Chop off /path/to/blog. $home_path = wp_parse_url( home_url( '/' ) ); $home_path = isset( $home_path['path'] ) ? $home_path['path'] : ''; $url = preg_replace( sprintf( '#^%s#', preg_quote( $home_path, '#' ) ), '', trailingslashit( $url ) ); } // Trim leading and lagging slashes. $url = trim( $url, '/' ); $request = $url; $post_type_query_vars = []; foreach ( get_post_types( [], 'objects' ) as $post_type => $t ) { if ( ! empty( $t->query_var ) ) { $post_type_query_vars[ $t->query_var ] = $post_type; } } // Look for matches. $request_match = $request; foreach ( (array) $rewrite as $match => $query ) { /* * If the requesting file is the anchor of the match, * prepend it to the path info. */ if ( ! empty( $url ) && ( $url !== $request ) && str_starts_with( $match, $url ) ) { $request_match = $url . '/' . $request; } if ( preg_match( "#^$match#", $request_match, $matches ) ) { if ( $wp_rewrite->use_verbose_page_rules && preg_match( '/pagename=\$matches\[([0-9]+)\]/', $query, $varmatch ) ) { // This is a verbose page match, let's check to be sure about it. $page = get_page_by_path( $matches[ $varmatch[1] ] ); if ( ! $page ) { continue; } $post_status_obj = get_post_status_object( $page->post_status ); if ( ! $post_status_obj->public && ! $post_status_obj->protected && ! $post_status_obj->private && $post_status_obj->exclude_from_search ) { continue; } } /* * Got a match. * Trim the query of everything up to the '?'. */ $query = preg_replace( '!^.+\?!', '', $query ); // Substitute the substring matches into the query. $query = addslashes( WP_MatchesMapRegex::apply( $query, $matches ) ); // Filter out non-public query vars. global $wp; parse_str( $query, $query_vars ); $query = []; foreach ( (array) $query_vars as $key => $value ) { if ( in_array( (string) $key, $wp->public_query_vars, true ) ) { $query[ $key ] = $value; if ( isset( $post_type_query_vars[ $key ] ) ) { $query['post_type'] = $post_type_query_vars[ $key ]; $query['name'] = $value; } } } // Resolve conflicts between posts with numeric slugs and date archive queries. $query = wp_resolve_numeric_slug_conflicts( $query ); if ( ! empty( $search_in_post_statuses ) ) { $query['post_status'] = $search_in_post_statuses; } /** * Filters WP_Query class passed args. * * @param array $query WP_Query passed args. * @param string $url The URL to derive the post ID from. */ $query = (array) apply_filters( 'rocket_url_to_postid_query_args', $query, $url ); $query['no_found_rows'] = true; $query['update_post_term_cache'] = false; $query['update_post_meta_cache'] = false; // Do the query. $query = new WP_Query( $query ); if ( ! empty( $query->posts ) && $query->is_singular ) { return $query->post->ID; } else { return 0; } } } return 0; } } functions/htaccess.php 0000644 00000062723 15174677547 0011123 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Used to flush the .htaccess file. * * @since 1.0 * @since 1.1.0 Remove empty spacings when .htaccess is generated. * * @param bool $remove_rules True to remove WPR rules, false to renew them. Default is false. * * @return bool */ function flush_rocket_htaccess( $remove_rules = false ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals global $is_apache; /** * Filters disabling of WP Rocket htaccess rules * * @since 3.2.5 * * @param bool $disable True to disable, false otherwise. */ if ( ! $is_apache || ( apply_filters( 'rocket_disable_htaccess', false ) && ! $remove_rules ) ) { return false; } if ( ! function_exists( 'get_home_path' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $filename = get_home_path() . '.htaccess'; $insertion = ''; if ( ! $remove_rules ) { $insertion = get_rocket_htaccess_marker(); } if ( ! file_exists( $filename ) ) { if ( ! is_writable( dirname( $filename ) ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable return false; } if ( ! touch( $filename ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_touch return false; } // Make sure the file is created with a minimum set of permissions. $perms = fileperms( $filename ); if ( $perms ) { chmod( $filename, $perms | 0644 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod } } elseif ( ! is_writable( $filename ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable return false; } if ( ! is_array( $insertion ) ) { $insertion = explode( "\n", $insertion ); } $start_marker = '# BEGIN WP Rocket'; $end_marker = '# END WP Rocket'; $pointer = fopen( $filename, 'r+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen if ( ! $pointer ) { return false; } // Attempt to get a lock. If the filesystem supports locking, this will block until the lock is acquired. flock( $pointer, LOCK_EX ); $lines = []; while ( ! feof( $pointer ) ) { $lines[] = rtrim( fgets( $pointer ), "\r\n" ); } // Split out the existing file into the preceding lines, and those that appear after the marker. $pre_lines = []; $post_lines = []; $existing_lines = []; $found_marker = false; $found_end_marker = false; foreach ( $lines as $line ) { if ( ! $found_marker && str_contains( $line, $start_marker ) ) { $found_marker = true; continue; } elseif ( ! $found_end_marker && str_contains( $line, $end_marker ) ) { $found_end_marker = true; continue; } if ( ! $found_marker ) { $pre_lines[] = $line; } elseif ( $found_marker && $found_end_marker ) { $post_lines[] = $line; } else { $existing_lines[] = $line; } } // Check to see if there was a change. if ( $existing_lines === $insertion ) { flock( $pointer, LOCK_UN ); fclose( $pointer ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose return true; } if ( ! $found_marker ) { // Generate the new file data. $new_file_data = implode( "\n", array_merge( [ $start_marker ], $insertion, [ $end_marker ], $pre_lines, $post_lines ) ); } elseif ( $remove_rules ) { // Generate the new file data. $new_file_data = implode( "\n", array_merge( $pre_lines, $post_lines ) ); } else { // Generate the new file data. $new_file_data = implode( "\n", array_merge( $pre_lines, [ $start_marker ], $insertion, [ $end_marker ], $post_lines ) ); } // Write to the start of the file, and truncate it to that length. fseek( $pointer, 0 ); $bytes = fwrite( $pointer, $new_file_data ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite if ( false !== $bytes ) { ftruncate( $pointer, ftell( $pointer ) ); } fflush( $pointer ); flock( $pointer, LOCK_UN ); fclose( $pointer ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose return (bool) $bytes; } /** * Test if a server error is triggered by our rules * * @since 2.10 * @author Remy Perona * * @param (string) $rules_name The rules block to test. * * @return (object|bool) Return true if the server does not trigger an error 500, false otherwise. * Return a WP_Error object if the sandbox creation fails or if the HTTP request fails. */ function rocket_htaccess_rules_test( $rules_name ) { /** * Filters the request arguments * * @author Remy Perona * @since 2.10 * * @param array $args Array of argument for the request. */ $request_args = apply_filters( 'rocket_htaccess_rules_test_args', [ 'redirection' => 0, 'timeout' => 5, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals 'user-agent' => 'wprocketbot', 'cookies' => $_COOKIE, ] ); $response = wp_remote_get( site_url( WP_ROCKET_URL . 'tests/' . $rules_name . '/index.html' ), $request_args ); if ( is_wp_error( $response ) ) { return $response; } return 500 !== wp_remote_retrieve_response_code( $response ); } /** * Return the markers for htaccess rules * * @since 1.0 * * @return string $marker Rules that will be printed */ function get_rocket_htaccess_marker() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals /** * Add custom rules before rules added by WP Rocket * * @since 2.6 * * @param string $before_marker The content of all rules. */ $marker = apply_filters( 'before_rocket_htaccess_rules', '' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $marker .= get_rocket_htaccess_charset(); $marker .= get_rocket_htaccess_etag(); $marker .= get_rocket_htaccess_web_fonts_access(); $marker .= get_rocket_htaccess_files_match(); $marker .= get_rocket_htaccess_mod_expires(); $marker .= get_rocket_htaccess_mod_deflate(); if ( \WP_Rocket\Buffer\Cache::can_generate_caching_files() && ! is_rocket_generate_caching_mobile_files() ) { $marker .= get_rocket_htaccess_mod_rewrite(); } /** * Add custom rules after rules added by WP Rocket * * @since 2.6 * * @param string $after_marker The content of all rules. */ $marker .= apply_filters( 'after_rocket_htaccess_rules', '' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals /** * Filter rules added by WP Rocket in .htaccess * * @since 2.1 * * @param string $marker The content of all rules. */ $marker = apply_filters( 'rocket_htaccess_marker', $marker ); return $marker; } /** * Rewrite rules to serve the cache file * * @since 1.0 * * @return string $rules Rules that will be printed */ function get_rocket_htaccess_mod_rewrite() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // No rewrite rules for multisite. if ( is_multisite() ) { return; } // No rewrite rules for Korean. if ( ( defined( 'WPLANG' ) && 'ko_KR' === WPLANG ) || 'ko_KR' === get_locale() ) { return; } // Get root base. $home_root = rocket_extract_url_component( home_url(), PHP_URL_PATH ); $home_root = isset( $home_root ) ? trailingslashit( $home_root ) : '/'; $site_root = rocket_extract_url_component( site_url(), PHP_URL_PATH ); $site_root = isset( $site_root ) ? trailingslashit( $site_root ) : ''; // Get cache root. if ( strpos( WP_ROCKET_CACHE_PATH, ABSPATH ) === false && isset( $_SERVER['DOCUMENT_ROOT'] ) ) { $cache_root = '/' . ltrim( str_replace( sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ), '', WP_ROCKET_CACHE_PATH ), '/' ); } else { $cache_root = '/' . ltrim( $site_root . str_replace( ABSPATH, '', WP_ROCKET_CACHE_PATH ), '/' ); } /** * Replace the dots by underscores to avoid some bugs on some shared hosting services on filenames (not multisite compatible!) * * @since 1.3.0 * * @param bool true will replace the . by _. */ $http_host = apply_filters( 'rocket_url_no_dots', false ) ? rocket_remove_url_protocol( home_url() ) : '%{HTTP_HOST}'; /** * Allow the path to be fully printed or dependant od %DOCUMENT_ROOT (forced for 1&1 by default) * * @since 1.3.0 * * @param bool true will force the path to be full. */ $is_1and1_or_force = apply_filters( 'rocket_force_full_path', strpos( sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ), '/kunden/' ) === 0 ); $rules = ''; $gzip_rules = ''; $enc = ''; if ( $is_1and1_or_force ) { $cache_dir_path = str_replace( '/kunden/', '/', WP_ROCKET_CACHE_PATH ) . $http_host . '%{REQUEST_URI}'; } else { $cache_dir_path = '%{DOCUMENT_ROOT}/' . ltrim( $cache_root, '/' ) . $http_host . '%{REQUEST_URI}'; } // @codingStandardsIgnoreStart /** * Allow to serve gzip cache file * * @since 2.4 * * @param bool true will force to serve gzip cache file. */ if ( function_exists( 'gzencode' ) && apply_filters( 'rocket_force_gzip_htaccess_rules', true ) ) { $rules = '<IfModule mod_mime.c>' . PHP_EOL; $rules .= 'AddType text/html .html_gzip' . PHP_EOL; $rules .= 'AddEncoding gzip .html_gzip' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL; $rules .= '<IfModule mod_setenvif.c>' . PHP_EOL; $rules .= 'SetEnvIfNoCase Request_URI \.html_gzip$ no-gzip' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL . PHP_EOL; $gzip_rules .= 'RewriteCond %{HTTP:Accept-Encoding} gzip' . PHP_EOL; $gzip_rules .= 'RewriteRule .* - [E=WPR_ENC:_gzip]' . PHP_EOL; $enc = '%{ENV:WPR_ENC}'; } $rules .= '<IfModule mod_rewrite.c>' . PHP_EOL; $rules .= 'RewriteEngine On' . PHP_EOL; $rules .= 'RewriteBase ' . $home_root . PHP_EOL; $rules .= get_rocket_htaccess_ssl_rewritecond(); $rules .= rocket_get_webp_rewritecond( $cache_dir_path ); $rules .= $gzip_rules; $rules .= 'RewriteCond %{REQUEST_METHOD} GET' . PHP_EOL; $rules .= 'RewriteCond %{QUERY_STRING} =""' . PHP_EOL; $cookies = get_rocket_cache_reject_cookies(); if ( $cookies ) { $rules .= 'RewriteCond %{HTTP:Cookie} !(' . $cookies . ') [NC]' . PHP_EOL; } $uri = get_rocket_cache_reject_uri(); if ( $uri ) { $rules .= 'RewriteCond %{REQUEST_URI} !^(' . $uri . ')$ [NC]' . PHP_EOL; } $rules .= ! is_rocket_cache_mobile() ? get_rocket_htaccess_mobile_rewritecond() : ''; $ua = get_rocket_cache_reject_ua(); if ( $ua ) { $rules .= 'RewriteCond %{HTTP_USER_AGENT} !^(' . $ua . ').* [NC]' . PHP_EOL; } $rules .= 'RewriteCond "' . $cache_dir_path . '/index%{ENV:WPR_SSL}%{ENV:WPR_WEBP}.html' . $enc . '" -f' . PHP_EOL; $rules .= 'RewriteRule .* "' . $cache_root . $http_host . '%{REQUEST_URI}/index%{ENV:WPR_SSL}%{ENV:WPR_WEBP}.html' . $enc . '" [L]' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL; /** * Filter rewrite rules to serve the cache file * * @since 1.0 * * @param string $rules Rules that will be printed. */ $rules = apply_filters( 'rocket_htaccess_mod_rewrite', $rules ); return $rules; } /** * Rules for detect mobile version * * @since 1.0 * * @return string $rules Rules that will be printed */ function get_rocket_htaccess_mobile_rewritecond() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // No rewrite rules for multisite. if ( is_multisite() ) { return; } $rules = 'RewriteCond %{HTTP:X-Wap-Profile} !^[a-z0-9\"]+ [NC]' . PHP_EOL; $rules .= 'RewriteCond %{HTTP:Profile} !^[a-z0-9\"]+ [NC]' . PHP_EOL; $rules .= 'RewriteCond %{HTTP_USER_AGENT} !^.*(2.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800).* [NC]' . PHP_EOL; $rules .= 'RewriteCond %{HTTP_USER_AGENT} !^(w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-).* [NC]' . PHP_EOL; /** * Filter rules for detect mobile version * * @since 2.0 * * @param string $rules Rules that will be printed. */ $rules = apply_filters( 'rocket_htaccess_mobile_rewritecond', $rules ); return $rules; } /** * Rules for SSL requests * * @since 2.7 Added rewrite condition for `%{HTTP:X-Forwarded-Proto}`. * @since 2.0 * * @return string $rules Rules that will be printed */ function get_rocket_htaccess_ssl_rewritecond() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $rules = 'RewriteCond %{HTTPS} on [OR]' . PHP_EOL; $rules .= 'RewriteCond %{SERVER_PORT} ^443$ [OR]' . PHP_EOL; $rules .= 'RewriteCond %{HTTP:X-Forwarded-Proto} https' . PHP_EOL; $rules .= 'RewriteRule .* - [E=WPR_SSL:-https]' . PHP_EOL; /** * Filter rules for SSL requests * * @since 2.0 * * @param string $rules Rules that will be printed. */ $rules = apply_filters( 'rocket_htaccess_ssl_rewritecond', $rules ); return $rules; } /** * Rules for webp compatible browsers. * * @since 3.4 * @author Grégory Viguier * * @param string $cache_dir_path Path to the cache directory, without trailing slash. * @return string Rules that will be printed. */ function rocket_get_webp_rewritecond( $cache_dir_path ) { if ( ! get_rocket_option( 'cache_webp' ) ) { return ''; } $rules = 'RewriteCond %{HTTP_ACCEPT} image/webp' . PHP_EOL; $rules .= 'RewriteCond "' . $cache_dir_path . '/.no-webp" !-f' . PHP_EOL; $rules .= 'RewriteRule .* - [E=WPR_WEBP:-webp]' . PHP_EOL; /** * Filter rules for webp. * * @since 3.4 * @author Grégory Viguier * * @param string $rules Rules that will be printed. */ return apply_filters( 'rocket_webp_rewritecond', $rules ); } /** * Rules to improve performances with GZIP Compression * * @since 1.0 * * @return string $rules Rules that will be printed */ function get_rocket_htaccess_mod_deflate() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $rules = '# Gzip compression' . PHP_EOL; $rules .= '<IfModule mod_deflate.c>' . PHP_EOL; $rules .= '# Active compression' . PHP_EOL; $rules .= 'SetOutputFilter DEFLATE' . PHP_EOL; $rules .= '# Force deflate for mangled headers' . PHP_EOL; $rules .= '<IfModule mod_setenvif.c>' . PHP_EOL; $rules .= '<IfModule mod_headers.c>' . PHP_EOL; $rules .= 'SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding' . PHP_EOL; $rules .= 'RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding' . PHP_EOL; $rules .= '# Don’t compress images and other uncompressible content' . PHP_EOL; $rules .= 'SetEnvIfNoCase Request_URI \\' . PHP_EOL; $rules .= '\\.(?:gif|jpe?g|png|rar|zip|exe|flv|mov|wma|mp3|avi|swf|mp?g|mp4|webm|webp|pdf)$ no-gzip dont-vary' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL . PHP_EOL; $rules .= '# Compress all output labeled with one of the following MIME-types' . PHP_EOL; $rules .= '<IfModule mod_filter.c>' . PHP_EOL; $rules .= 'AddOutputFilterByType DEFLATE application/atom+xml \ application/javascript \ application/json \ application/rss+xml \ application/vnd.ms-fontobject \ application/x-font-ttf \ application/xhtml+xml \ application/xml \ font/opentype \ image/svg+xml \ image/x-icon \ text/css \ text/html \ text/plain \ text/x-component \ text/xml' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL; $rules .= '<IfModule mod_headers.c>' . PHP_EOL; $rules .= 'Header append Vary: Accept-Encoding' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL . PHP_EOL; /** * Filter rules to improve performances with GZIP Compression * * @since 1.0 * * @param string $rules Rules that will be printed. */ $rules = apply_filters( 'rocket_htaccess_mod_deflate', $rules ); return $rules; } /** * Rules to improve performances with Expires Headers * * @since 1.0 * * @return string $rules Rules that will be printed */ function get_rocket_htaccess_mod_expires() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $rules = <<<HTACCESS <IfModule mod_mime.c> AddType image/avif avif AddType image/avif-sequence avifs </IfModule> # Expires headers (for better cache control) <IfModule mod_expires.c> ExpiresActive on ExpiresDefault "access plus 1 month" # cache.appcache needs re-requests in FF 3.6 (thanks Remy ~Introducing HTML5) ExpiresByType text/cache-manifest "access plus 0 seconds" # Your document html ExpiresByType text/html "access plus 0 seconds" # Data ExpiresByType text/xml "access plus 0 seconds" ExpiresByType application/xml "access plus 0 seconds" ExpiresByType application/json "access plus 0 seconds" # Feed ExpiresByType application/rss+xml "access plus 1 hour" ExpiresByType application/atom+xml "access plus 1 hour" # Favicon (cannot be renamed) ExpiresByType image/x-icon "access plus 1 week" # Media: images, video, audio ExpiresByType image/gif "access plus 4 months" ExpiresByType image/png "access plus 4 months" ExpiresByType image/jpeg "access plus 4 months" ExpiresByType image/webp "access plus 4 months" ExpiresByType video/ogg "access plus 4 months" ExpiresByType audio/ogg "access plus 4 months" ExpiresByType video/mp4 "access plus 4 months" ExpiresByType video/webm "access plus 4 months" ExpiresByType image/avif "access plus 4 months" ExpiresByType image/avif-sequence "access plus 4 months" # HTC files (css3pie) ExpiresByType text/x-component "access plus 1 month" # Webfonts ExpiresByType font/ttf "access plus 4 months" ExpiresByType font/otf "access plus 4 months" ExpiresByType font/woff "access plus 4 months" ExpiresByType font/woff2 "access plus 4 months" ExpiresByType image/svg+xml "access plus 4 months" ExpiresByType application/vnd.ms-fontobject "access plus 1 month" # CSS and JavaScript ExpiresByType text/css "access plus 1 year" ExpiresByType application/javascript "access plus 1 year" </IfModule> HTACCESS; /** * Filter rules to improve performances with Expires Headers * * @since 1.0 * * @param string $rules Rules that will be printed. */ $rules = apply_filters( 'rocket_htaccess_mod_expires', $rules ); return $rules; } /** * Rules for default charset on static files * * @since 1.0 * * @return string $rules Rules that will be printed */ function get_rocket_htaccess_charset() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // Get charset of the blog. $charset = preg_replace( '/[^a-zA-Z0-9_\-\.:]+/', '', get_bloginfo( 'charset', 'display' ) ); if ( empty( $charset ) ) { return ''; } $rules = "# Use $charset encoding for anything served text/plain or text/html" . PHP_EOL; $rules .= "AddDefaultCharset $charset" . PHP_EOL; $rules .= "# Force $charset for a number of file formats" . PHP_EOL; $rules .= '<IfModule mod_mime.c>' . PHP_EOL; $rules .= "AddCharset $charset .atom .css .js .json .rss .vtt .xml" . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL . PHP_EOL; /** * Filter rules for default charset on static files * * @since 1.0 * * @param string $rules Rules that will be printed. */ $rules = apply_filters( 'rocket_htaccess_charset', $rules ); return $rules; } /** * Rules for cache control * * @since 1.1.6 * * @return string $rules Rules that will be printed */ function get_rocket_htaccess_files_match() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $rules = '<IfModule mod_alias.c>' . PHP_EOL; $rules .= '<FilesMatch "\.(html|htm|rtf|rtx|txt|xsd|xsl|xml)$">' . PHP_EOL; $rules .= '<IfModule mod_headers.c>' . PHP_EOL; $rules .= 'Header set X-Powered-By "WP Rocket/' . WP_ROCKET_VERSION . '"' . PHP_EOL; $rules .= 'Header unset Pragma' . PHP_EOL; $rules .= 'Header append Cache-Control "public"' . PHP_EOL; $rules .= 'Header unset Last-Modified' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL; $rules .= '</FilesMatch>' . PHP_EOL . PHP_EOL; $rules .= '<FilesMatch "\.(css|htc|js|asf|asx|wax|wmv|wmx|avi|bmp|class|divx|doc|docx|eot|exe|gif|gz|gzip|ico|jpg|jpeg|jpe|json|mdb|mid|midi|mov|qt|mp3|m4a|mp4|m4v|mpeg|mpg|mpe|mpp|otf|odb|odc|odf|odg|odp|ods|odt|ogg|pdf|png|pot|pps|ppt|pptx|ra|ram|svg|svgz|swf|tar|tif|tiff|ttf|ttc|wav|wma|wri|xla|xls|xlsx|xlt|xlw|zip)$">' . PHP_EOL; $rules .= '<IfModule mod_headers.c>' . PHP_EOL; $rules .= 'Header unset Pragma' . PHP_EOL; $rules .= 'Header append Cache-Control "public"' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL; $rules .= '</FilesMatch>' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL . PHP_EOL; /** * Filter rules for cache control * * @since 1.1.6 * * @param string $rules Rules that will be printed. */ $rules = apply_filters( 'rocket_htaccess_files_match', $rules ); return $rules; } /** * Rules to remove the etag * * @since 1.0 * * @return string $rules Rules that will be printed */ function get_rocket_htaccess_etag() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $rules = '# FileETag None is not enough for every server.' . PHP_EOL; $rules .= '<IfModule mod_headers.c>' . PHP_EOL; $rules .= 'Header unset ETag' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL . PHP_EOL; $rules .= '# Since we’re sending far-future expires, we don’t need ETags for static content.' . PHP_EOL; $rules .= '# developer.yahoo.com/performance/rules.html#etags' . PHP_EOL; $rules .= 'FileETag None' . PHP_EOL . PHP_EOL; /** * Filter rules to remove the etag * * @since 1.0 * * @param string $rules Rules that will be printed. */ $rules = apply_filters( 'rocket_htaccess_etag', $rules ); return $rules; } /** * Rules to Cross-origin fonts sharing when CDN is used * * @since 2.4 * * @return string $rules Rules that will be printed */ function get_rocket_htaccess_web_fonts_access() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals if ( ! get_rocket_option( 'cdn', false ) ) { return; } $rules = '# Send CORS headers if browsers request them; enabled by default for images.' . PHP_EOL; $rules .= '<IfModule mod_setenvif.c>' . PHP_EOL; $rules .= '<IfModule mod_headers.c>' . PHP_EOL; $rules .= '# mod_headers, y u no match by Content-Type?!' . PHP_EOL; $rules .= '<FilesMatch "\.(avifs?|cur|gif|png|jpe?g|svgz?|ico|webp)$">' . PHP_EOL; $rules .= 'SetEnvIf Origin ":" IS_CORS' . PHP_EOL; $rules .= 'Header set Access-Control-Allow-Origin "*" env=IS_CORS' . PHP_EOL; $rules .= '</FilesMatch>' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL . PHP_EOL; $rules .= '# Allow access to web fonts from all domains.' . PHP_EOL; $rules .= '<FilesMatch "\.(eot|otf|tt[cf]|woff2?)$">' . PHP_EOL; $rules .= '<IfModule mod_headers.c>' . PHP_EOL; $rules .= 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL; $rules .= '</IfModule>' . PHP_EOL; $rules .= '</FilesMatch>' . PHP_EOL . PHP_EOL; // @codingStandardsIgnoreEnd /** * Filter rules to Cross-origin fonts sharing * * @since 1.0 * * @param string $rules Rules that will be printed. */ $rules = apply_filters( 'rocket_htaccess_web_fonts_access', $rules ); return $rules; } /** * Tell if WP rewrite rules are present in a given string. * * @since 3.2.4 * @author Grégory Viguier * * @param string $content Htaccess content. * @return bool */ function rocket_has_wp_htaccess_rules( $content ) { if ( is_multisite() ) { $has_wp_rules = strpos( $content, '# add a trailing slash to /wp-admin' ) !== false; } else { $has_wp_rules = strpos( $content, '# BEGIN WordPress' ) !== false; } /** * Tell if WP rewrite rules are present in a given string. * * @since 3.2.4 * @author Grégory Viguier * * @param bool $has_wp_rules True when present. False otherwise. * @param string $content .htaccess content. */ return apply_filters( 'rocket_has_wp_htaccess_rules', $has_wp_rules, $content ); } /** * Check if WP Rocket htaccess rules are already present in the file * * @since 3.3.5 * @author Remy Perona * * @return bool */ function rocket_check_htaccess_rules() { if ( ! function_exists( 'get_home_path' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $htaccess_file = get_home_path() . '.htaccess'; if ( ! rocket_direct_filesystem()->is_readable( $htaccess_file ) ) { return false; } $htaccess = rocket_direct_filesystem()->get_contents( $htaccess_file ); if ( preg_match( '/\s*# BEGIN WP Rocket.*# END WP Rocket\s*?/isU', $htaccess ) ) { return true; } return false; } compat.php 0000644 00000001654 15174677547 0006575 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( ! function_exists( 'str_starts_with' ) ) { /** * Polyfill for str_starts_with() function added in PHP 8.0. * * @param string $haystack The string to search in. * @param string $needle The substring to search for in the haystack. * * @return bool True if $needle is in $haystack, otherwise false. */ function str_starts_with( $haystack, $needle ) { if ( '' === $needle ) { return true; } return 0 === strpos( $haystack, $needle ); } } if ( ! function_exists( 'str_contains' ) ) { /** * Polyfill for str_contains() function added in PHP 8.0. * * @param string $haystack The string to search in. * @param string $needle The substring to search for in the haystack. * * @return bool True if $needle is in $haystack, otherwise false. */ function str_contains( $haystack, $needle ) { return ( '' === $needle || false !== strpos( $haystack, $needle ) ); } } Plugin.php 0000644 00000037034 15174677547 0006551 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket; use Imagify_Partner; use WP_Rocket\Dependencies\League\Container\Container; use WP_Rocket\Admin\{Options, Options_Data}; use WP_Rocket\Engine\Admin\API\ServiceProvider as APIServiceProvider; use WP_Rocket\Engine\Common\ExtractCSS\ServiceProvider as CommonExtractCSSServiceProvider; use WP_Rocket\Engine\Common\Head\ServiceProvider as CommonHeadServiceProvider; use WP_Rocket\Engine\Common\JobManager\ServiceProvider as JobManagerServiceProvider; use WP_Rocket\Engine\Media\Lazyload\CSS\ServiceProvider as LazyloadCSSServiceProvider; use WP_Rocket\Engine\Media\Lazyload\CSS\Admin\ServiceProvider as AdminLazyloadCSSServiceProvider; use WP_Rocket\Event_Management\Event_Manager; use WP_Rocket\Logger\ServiceProvider as LoggerServiceProvider; use WP_Rocket\ThirdParty\Hostings\HostResolver; use WP_Rocket\Addon\ServiceProvider as AddonServiceProvider; use WP_Rocket\Addon\Cloudflare\ServiceProvider as CloudflareServiceProvider; use WP_Rocket\Addon\Varnish\ServiceProvider as VarnishServiceProvider; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Engine\Admin\Beacon\ServiceProvider as BeaconServiceProvider; use WP_Rocket\Engine\Admin\Database\ServiceProvider as AdminDatabaseServiceProvider; use WP_Rocket\Engine\Admin\ServiceProvider as EngineAdminServiceProvider; use WP_Rocket\Engine\Admin\Settings\ServiceProvider as SettingsServiceProvider; use WP_Rocket\Engine\Cache\ServiceProvider as CacheServiceProvider; use WP_Rocket\Engine\Capabilities\ServiceProvider as CapabilitiesServiceProvider; use WP_Rocket\Engine\CDN\RocketCDN\ServiceProvider as RocketCDNServiceProvider; use WP_Rocket\Engine\CDN\ServiceProvider as CDNServiceProvider; use WP_Rocket\Engine\CriticalPath\ServiceProvider as CriticalPathServiceProvider; use WP_Rocket\Engine\HealthCheck\ServiceProvider as HealthCheckServiceProvider; use WP_Rocket\Engine\Heartbeat\ServiceProvider as HeartbeatServiceProvider; use WP_Rocket\Engine\License\ServiceProvider as LicenseServiceProvider; use WP_Rocket\Engine\Media\ServiceProvider as MediaServiceProvider; use WP_Rocket\Engine\Media\AboveTheFold\ServiceProvider as ATFServiceProvider; use WP_Rocket\Engine\Optimization\AdminServiceProvider as OptimizationAdminServiceProvider; use WP_Rocket\Engine\Optimization\DeferJS\ServiceProvider as DeferJSServiceProvider; use WP_Rocket\Engine\Optimization\DelayJS\ServiceProvider as DelayJSServiceProvider; use WP_Rocket\Engine\Optimization\DynamicLists\ServiceProvider as DynamicListsServiceProvider; use WP_Rocket\Engine\Optimization\RUCSS\ServiceProvider as RUCSSServiceProvider; use WP_Rocket\Engine\Optimization\ServiceProvider as OptimizationServiceProvider; use WP_Rocket\Engine\Plugin\ServiceProvider as PluginServiceProvider; use WP_Rocket\Engine\Preload\Links\ServiceProvider as PreloadLinksServiceProvider; use WP_Rocket\Engine\Preload\ServiceProvider as PreloadServiceProvider; use WP_Rocket\Engine\Saas\ServiceProvider as SaasAdminServiceProvider; use WP_Rocket\Engine\Support\ServiceProvider as SupportServiceProvider; use WP_Rocket\ServiceProvider\Common_Subscribers; use WP_Rocket\ServiceProvider\Options as OptionsServiceProvider; use WP_Rocket\ThirdParty\Hostings\ServiceProvider as HostingsServiceProvider; use WP_Rocket\ThirdParty\ServiceProvider as ThirdPartyServiceProvider; use WP_Rocket\ThirdParty\Themes\ServiceProvider as ThemesServiceProvider; use WP_Rocket\Engine\Admin\DomainChange\ServiceProvider as DomainChangeServiceProvider; use WP_Rocket\ThirdParty\Themes\ThemeResolver; use WP_Rocket\Engine\Debug\Resolver as DebugResolver; use WP_Rocket\Engine\Debug\ServiceProvider as DebugServiceProvider; use WP_Rocket\Engine\Common\PerformanceHints\ServiceProvider as PerformanceHintsServiceProvider; use WP_Rocket\Engine\Optimization\LazyRenderContent\ServiceProvider as LRCServiceProvider; use WP_Rocket\Engine\Media\Fonts\ServiceProvider as MediaFontsServiceProvider; use WP_Rocket\Engine\Media\PreloadFonts\ServiceProvider as PreloadFontsServiceProvider; use WP_Rocket\Engine\Media\PreconnectExternalDomains\ServiceProvider as PreconnectExternalDomainsServiceProvider; use WP_Rocket\Engine\Tracking\ServiceProvider as TrackingServiceProvider; /** * Plugin Manager. */ class Plugin { /** * Instance of Container class. * * @since 3.3 * * @var Container instance */ private $container; /** * Instance of the event manager. * * @since 3.6 * * @var Event_Manager */ private $event_manager; /** * Flag for if the license key is valid. * * @since 3.6 * * @var bool */ private $is_valid_key; /** * Instance of the Options_Data. * * @since 3.6 * * @var Options_Data */ private $options; /** * Creates an instance of the Plugin. * * @since 3.0 * * @param string $template_path Path to the views. * @param Container $container Instance of the container. */ public function __construct( $template_path, Container $container ) { $this->container = $container; add_filter( 'rocket_container', [ $this, 'get_container' ] ); $this->container->add( 'template_path', new StringArgument( $template_path ) ); } /** * Returns the Rocket container instance. * * @return Container */ public function get_container() { return $this->container; } /** * Loads the plugin into WordPress. * * @since 3.0 * * @return void */ public function load() { $this->event_manager = new Event_Manager(); $this->container->addShared( 'event_manager', $this->event_manager ); $this->container->add( 'options_api', Options::class ) ->addArgument( new StringArgument( 'wp_rocket_' ) ); $this->container->addServiceProvider( new OptionsServiceProvider() ); $this->options = $this->container->get( 'options' ); $this->container->add( 'debug_resolver', DebugResolver::class ) ->addArgument( $this->options ); $this->container->addServiceProvider( new LoggerServiceProvider() ); $this->container->get( 'logger' ); $this->container->addServiceProvider( new AdminDatabaseServiceProvider() ); $this->container->addServiceProvider( new SupportServiceProvider() ); $this->container->addServiceProvider( new BeaconServiceProvider() ); $this->container->addServiceProvider( new RocketCDNServiceProvider() ); $this->container->addServiceProvider( new CacheServiceProvider() ); $this->container->addServiceProvider( new CriticalPathServiceProvider() ); $this->container->addServiceProvider( new HealthCheckServiceProvider() ); $this->container->addServiceProvider( new MediaServiceProvider() ); $this->container->addServiceProvider( new DeferJSServiceProvider() ); $this->is_valid_key = rocket_valid_key(); foreach ( $this->get_subscribers() as $subscriber ) { $this->event_manager->add_subscriber( $this->container->get( $subscriber ) ); } } /** * Get the subscribers to add to the event manager. * * @since 3.6 * * @return array array of subscribers. */ private function get_subscribers() { $subscribers = []; if ( is_admin() ) { $subscribers = $this->init_admin_subscribers(); } elseif ( $this->is_valid_key ) { $subscribers = $this->init_valid_key_subscribers(); } return array_merge( $subscribers, $this->init_common_subscribers() ); } /** * Initializes the admin subscribers. * * @since 3.6 * * @return array array of subscribers. */ private function init_admin_subscribers() { if ( ! Imagify_Partner::has_imagify_api_key() ) { $imagify = new Imagify_Partner( 'wp-rocket' ); $imagify->init(); remove_action( 'imagify_assets_enqueued', 'imagify_dequeue_sweetalert_wprocket' ); } $this->container->addServiceProvider( new SettingsServiceProvider() ); $this->container->addServiceProvider( new EngineAdminServiceProvider() ); $this->container->addServiceProvider( new OptimizationAdminServiceProvider() ); $this->container->addServiceProvider( new DomainChangeServiceProvider() ); $this->container->addServiceProvider( new AdminLazyloadCSSServiceProvider() ); $subscribers = [ 'beacon', 'settings_page_subscriber', 'deactivation_intent_subscriber', 'hummingbird_subscriber', 'rocketcdn_admin_subscriber', 'rocketcdn_notices_subscriber', 'rocketcdn_data_manager_subscriber', 'critical_css_admin_subscriber', 'health_check', 'minify_css_admin_subscriber', 'admin_cache_subscriber', 'google_fonts_admin_subscriber', 'image_dimensions_admin_subscriber', 'defer_js_admin_subscriber', 'lazyload_admin_subscriber', 'preload_admin_subscriber', 'minify_admin_subscriber', 'action_scheduler_check', 'actionscheduler_admin_subscriber', 'domain_change_subscriber', 'lazyload_css_admin_subscriber', 'post_edit_options_subscriber', 'preconnect_external_domains_admin_subscriber', 'media_fonts_admin_subscriber', 'preload_fonts_admin_subscriber', ]; // Only add tracking service provider if cURL extension is loaded. // MixPanel (used by TrackingServiceProvider) requires cURL and will throw a fatal error if not available. // This prevents crashes when WP Rocket is installed on servers without cURL support. if ( function_exists( 'curl_init' ) ) { $this->container->addServiceProvider( new TrackingServiceProvider() ); $subscribers[] = 'tracking_subscriber'; } return $subscribers; } /** * For plugins with a valid key, initialize the subscribers. * * @since 3.6 * * @return array array of subscribers. */ private function init_valid_key_subscribers() { $this->container->addServiceProvider( new OptimizationServiceProvider() ); $subscribers = [ 'buffer_subscriber', 'ie_conditionals_subscriber', 'combine_google_fonts_subscriber', 'minify_css_subscriber', 'minify_js_subscriber', 'cache_dynamic_resource', 'emojis_subscriber', 'delay_js_subscriber', 'image_dimensions_subscriber', 'defer_js_subscriber', ]; // Don't insert the LazyLoad file if Rocket LazyLoad is activated. if ( ! rocket_is_plugin_active( 'rocket-lazy-load/rocket-lazy-load.php' ) ) { $subscribers[] = 'lazyload_subscriber'; } return $subscribers; } /** * Initializes the common subscribers. * * @since 3.6 * * @return array array of common subscribers. */ private function init_common_subscribers() { $this->container->addServiceProvider( new CapabilitiesServiceProvider() ); $this->container->addServiceProvider( new AddonServiceProvider() ); $this->container->addServiceProvider( new VarnishServiceProvider() ); $this->container->addServiceProvider( new PreloadServiceProvider() ); $this->container->addServiceProvider( new PreloadLinksServiceProvider() ); $this->container->addServiceProvider( new CDNServiceProvider() ); $this->container->addServiceProvider( new Common_Subscribers() ); $this->container->addServiceProvider( new HostingsServiceProvider() ); $this->container->addServiceProvider( new PluginServiceProvider() ); $this->container->addServiceProvider( new DynamicListsServiceProvider() ); $this->container->addServiceProvider( new DelayJSServiceProvider() ); $this->container->addServiceProvider( new RUCSSServiceProvider() ); $this->container->addServiceProvider( new HeartbeatServiceProvider() ); $this->container->addServiceProvider( new LicenseServiceProvider() ); $this->container->addServiceProvider( new ThemesServiceProvider() ); $this->container->addServiceProvider( new APIServiceProvider() ); $this->container->addServiceProvider( new CommonExtractCSSServiceProvider() ); $this->container->addServiceProvider( new CommonHeadServiceProvider() ); $this->container->addServiceProvider( new LazyloadCSSServiceProvider() ); $this->container->addServiceProvider( new DebugServiceProvider() ); $this->container->addServiceProvider( new ATFServiceProvider() ); $this->container->addServiceProvider( new JobManagerServiceProvider() ); $this->container->addServiceProvider( new SaasAdminServiceProvider() ); $this->container->addServiceProvider( new PerformanceHintsServiceProvider() ); $this->container->addServiceProvider( new LRCServiceProvider() ); $this->container->addServiceProvider( new MediaFontsServiceProvider() ); $this->container->addServiceProvider( new PreloadFontsServiceProvider() ); $this->container->addServiceProvider( new ThirdPartyServiceProvider() ); $this->container->addServiceProvider( new PreconnectExternalDomainsServiceProvider() ); $common_subscribers = [ 'license_subscriber', 'cdn_subscriber', 'cdn_admin_subscriber', 'critical_css_subscriber', 'sucuri_subscriber', 'common_extractcss_subscriber', 'common_head_subscriber', 'expired_cache_purge_subscriber', 'heartbeat_subscriber', 'db_optimization_subscriber', 'mobile_subscriber', 'woocommerce_subscriber', 'bigcommerce_subscriber', 'syntaxhighlighter_subscriber', 'elementor_subscriber', 'ngg_subscriber', 'smush_subscriber', 'plugin_updater_common_subscriber', 'plugin_information_subscriber', 'plugin_updater_subscriber', 'capabilities_subscriber', 'varnish_subscriber', 'rocketcdn_rest_subscriber', 'detect_missing_tags_subscriber', 'purge_actions_subscriber', 'beaverbuilder_subscriber', 'amp_subscriber', 'rest_cpcss_subscriber', 'simple_custom_css', 'pdfembedder', 'delay_js_admin_subscriber', 'rucss_admin_subscriber', 'rucss_option_subscriber', 'rucss_frontend_subscriber', 'preload_subscriber', 'preload_front_subscriber', 'preload_links_admin_subscriber', 'preload_links_subscriber', 'preload_cron_subscriber', 'support_subscriber', 'mod_pagespeed', 'webp_subscriber', 'webp_admin_subscriber', 'imagify_webp_subscriber', 'shortpixel_webp_subscriber', 'ewww_webp_subscriber', 'optimus_webp_subscriber', 'adthrive', 'autoptimize', 'wp-meteor', 'revolution_slider_subscriber', 'wordfence_subscriber', 'ezoic', 'thirstyaffiliates', 'pwa', 'yoast_seo', 'convertplug', 'dynamic_lists_subscriber', 'unlimited_elements', 'inline_related_posts', 'jetpack', 'rank_math_seo', 'all_in_one_seo_pack', 'seopress', 'the_seo_framework', 'wpml', 'cloudflare_plugin_subscriber', 'cache_config', 'rocket_lazy_load', 'cache_config', 'the_events_calendar', 'admin_api_subscriber', 'perfmatters', 'rapidload', 'translatepress', 'wpgeotargeting', 'lazyload_css_subscriber', 'weglot', 'cron_subscriber', 'contactform7', 'debug_subscriber', 'rucss_cron_subscriber', 'saas_admin_subscriber', 'atf_subscriber', 'performance_hints_ajax_subscriber', 'performance_hints_frontend_subscriber', 'performance_hints_cron_subscriber', 'performance_hints_warmup_subscriber', 'performance_hints_admin_subscriber', 'lrc_frontend_subscriber', 'taxonomy_subscriber', 'termly_subscriber', 'media_fonts_frontend_subscriber', 'media_fonts_admin_subscriber', 'media_fonts_clean_subscriber', 'preload_fonts_frontend_subscriber', 'preload_fonts_admin_subscriber', 'preconnect_frontend_subscriber', 'post_subscriber', ]; $host_type = HostResolver::get_host_service(); $theme = ThemeResolver::get_current_theme(); if ( ! empty( $host_type ) ) { $common_subscribers[] = $host_type; } if ( ! empty( $theme ) ) { $common_subscribers[] = $theme; } if ( $this->options->get( 'do_cloudflare', false ) ) { $this->container->addServiceProvider( new CloudflareServiceProvider() ); $common_subscribers[] = 'cloudflare_admin_subscriber'; $common_subscribers[] = 'cloudflare_subscriber'; } $services = $this->container->get( 'debug_resolver' )->get_services(); if ( ! empty( $services ) ) { foreach ( $services as $service ) { $common_subscribers[] = $service['service']; } } return $common_subscribers; } } deprecated/3.2.php 0000644 00000030061 15174677547 0007706 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Add a message about Imagify on the "Upload New Media" screen and WP Rocket options page. * * @since 2.7 * @deprecated 3.2 */ function rocket_imagify_notice() { _deprecated_function( __FUNCTION__, '3.2' ); $current_screen = get_current_screen(); // Add the notice only on the "WP Rocket" settings, "Media Library" & "Upload New Media" screens. if ( 'admin_notices' === current_filter() && ( isset( $current_screen ) && 'settings_page_wprocket' !== $current_screen->base ) ) { return; } $boxes = get_user_meta( $GLOBALS['current_user']->ID, 'rocket_boxes', true ); if ( defined( 'IMAGIFY_VERSION' ) || in_array( __FUNCTION__, (array) $boxes, true ) || 1 === get_option( 'wp_rocket_dismiss_imagify_notice' ) || ! current_user_can( 'manage_options' ) ) { return; } $imagify_plugin = 'imagify/imagify.php'; $is_imagify_installed = rocket_is_plugin_installed( $imagify_plugin ); $action_url = $is_imagify_installed ? rocket_get_plugin_activation_link( $imagify_plugin ) : wp_nonce_url( add_query_arg( array( 'action' => 'install-plugin', 'plugin' => 'imagify', ), admin_url( 'update.php' ) ), 'install-plugin_imagify' ); $details_url = add_query_arg( array( 'tab' => 'plugin-information', 'plugin' => 'imagify', 'TB_iframe' => true, 'width' => 722, 'height' => 949, ), admin_url( 'plugin-install.php' ) ); $classes = $is_imagify_installed ? '' : ' install-now'; $cta_txt = $is_imagify_installed ? esc_html__( 'Activate Imagify', 'rocket' ) : esc_html__( 'Install Imagify for Free', 'rocket' ); $dismiss_url = wp_nonce_url( admin_url( 'admin-post.php?action=rocket_ignore&box=' . __FUNCTION__ ), 'rocket_ignore_' . __FUNCTION__ ); ?> <div id="plugin-filter" class="updated plugin-card plugin-card-imagify rkt-imagify-notice"> <a href="<?php echo $dismiss_url; ?>" class="rkt-cross"><span class="dashicons dashicons-no"></span></a> <p class="rkt-imagify-logo"> <img src="<?php echo WP_ROCKET_ASSETS_IMG_URL; ?>logo-imagify.png" srcset="<?php echo WP_ROCKET_ASSETS_IMG_URL; ?>logo-imagify.svg 2x" alt="Imagify" width="150" height="18"> </p> <p class="rkt-imagify-msg"> <?php _e( 'Speed up your website and boost your SEO by reducing image file sizes without losing quality with Imagify.', 'rocket' ); ?> </p> <p class="rkt-imagify-cta"> <a data-slug="imagify" href="<?php echo $action_url; ?>" class="button button-primary<?php echo $classes; ?>"><?php echo $cta_txt; ?></a> <?php if ( ! $is_imagify_installed ) : ?> <br><a data-slug="imagify" data-name="Imagify Image Optimizer" class="thickbox open-plugin-details-modal" href="<?php echo $details_url; ?>"><?php _e( 'More details', 'rocket' ); ?></a> <?php endif; ?> </p> </div> <?php } if ( ! function_exists( 'run_rocket_preload_cache' ) ) : /** * Launches the preload * * @since 2.8 * @author Remy Perona * @deprecated 3.2 * * @param string $spider The spider name. * @param bool $do_sitemap_preload Do the sitemap preload. * * @return void */ function run_rocket_preload_cache( $spider, $do_sitemap_preload = true ) { _deprecated_function( __FUNCTION__, '3.2' ); // Preload cache. run_rocket_bot( $spider ); if ( $do_sitemap_preload & get_rocket_option( 'sitemap_preload', false ) ) { $rocket_background_process = $GLOBALS['rocket_sitemap_background_process']; if ( method_exists( $rocket_background_process, 'cancel_process' ) ) { $rocket_background_process->cancel_process(); } delete_transient( 'rocket_sitemap_preload_running' ); delete_transient( 'rocket_sitemap_preload_complete' ); run_rocket_sitemap_preload(); } } endif; if ( ! function_exists( 'do_rocket_bot_cache_json' ) ) : /** * Run WP Rocket Bot when a post is added, updated or deleted * * @since 1.3.2 * @deprecated 3.2 */ function do_rocket_bot_cache_json() { _deprecated_function( __FUNCTION__, '3.2' ); return false; } endif; if ( ! function_exists( 'rocket_process_sitemap' ) ) { /** * Processes the sitemaps recursively * * @since 2.8 * @author Remy Perona * * @param string $sitemap_url URL of the sitemap. * @param array $urls An array of URLs. * @return array Empty array or array containing URLs */ function rocket_process_sitemap( $sitemap_url, $urls = array() ) { _deprecated_function( __FUNCTION__, '3.2' ); $tmp_urls = array(); /** * Filters the arguments for the sitemap preload request * * @since 2.10.8 * @author Remy Perona * * @param array $args Arguments for the request. */ $args = apply_filters( 'rocket_preload_sitemap_request_args', array( 'user-agent' => 'WP Rocket/Sitemaps', 'sslverify' => apply_filters( 'https_local_ssl_verify', true ), ) ); $sitemap = wp_remote_get( esc_url( $sitemap_url ), $args ); if ( is_wp_error( $sitemap ) ) { return array(); } $xml_data = wp_remote_retrieve_body( $sitemap ); if ( empty( $xml_data ) ) { return array(); } libxml_use_internal_errors( true ); $xml = simplexml_load_string( $xml_data ); if ( false === $xml ) { libxml_clear_errors(); return array(); } $url_count = count( $xml->url ); if ( $url_count > 0 ) { for ( $i = 0; $i < $url_count; $i++ ) { $page_url = (string) $xml->url[ $i ]->loc; $tmp_urls[] = $page_url; } } else { // Sub sitemap? $sitemap_children = count( $xml->sitemap ); if ( $sitemap_children > 0 ) { for ( $i = 0; $i < $sitemap_children; $i++ ) { $sub_sitemap_url = (string) $xml->sitemap[ $i ]->loc; $urls = rocket_process_sitemap( $sub_sitemap_url, $urls ); } } } $urls = array_merge( $urls, $tmp_urls ); return $urls; } } if ( ! function_exists( 'rocket_sitemap_preload_complete' ) ) { /** * This notice is displayed after the sitemap preload is complete * * @since 2.11 * @deprecated 3.2 * @author Remy Perona */ function rocket_sitemap_preload_complete() { _deprecated_function( __FUNCTION__, '3.2' ); $screen = get_current_screen(); /** This filter is documented in inc/admin-bar.php */ if ( ! current_user_can( apply_filters( 'rocket_capacity', 'manage_options' ) ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } $result = get_transient( 'rocket_sitemap_preload_complete' ); if ( false === $result ) { return; } delete_transient( 'rocket_sitemap_preload_complete' ); rocket_notice_html( array( // translators: %d is the number of pages preloaded. 'message' => sprintf( __( 'Sitemap preload: %d pages have been cached.', 'rocket' ), $result ), ) ); } } if ( ! function_exists( 'rocket_sitemap_preload_running' ) ) { /** * This notice is displayed when the sitemap preload is running * * @since 2.11 * @deprecated 3.2 * @author Remy Perona */ function rocket_sitemap_preload_running() { _deprecated_function( __FUNCTION__, '3.2' ); $screen = get_current_screen(); // This filter is documented in inc/admin-bar.php. if ( ! current_user_can( apply_filters( 'rocket_capacity', 'manage_options' ) ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } $running = get_transient( 'rocket_sitemap_preload_running' ); if ( false === $running ) { return; } rocket_notice_html( array( // translators: %d = Number of pages preloaded. 'message' => sprintf( __( 'Sitemap preload: %d uncached pages have now been preloaded. (refresh to see progress)', 'rocket' ), $running ), ) ); } } if ( ! function_exists( 'run_rocket_bot_after_clean_post' ) ) { /** * Actions to be done after the purge cache files of a post * By Default, this hook call the WP Rocket Bot (cache json) * * @deprecated 3.2 * @since 1.3.0 * * @param object $post The post object. * @param array $purge_urls An array of URLs to clean. * @param string $lang The language to clean. */ function run_rocket_bot_after_clean_post( $post, $purge_urls, $lang ) { _deprecated_function( __FUNCTION__, '3.2' ); // Run robot only if post is published. if ( 'publish' !== $post->post_status ) { return false; } // Add Homepage URL to $purge_urls for bot crawl. array_push( $purge_urls, get_rocket_i18n_home_url( $lang ) ); // Add default WordPress feeds (posts & comments). array_push( $purge_urls, get_feed_link() ); array_push( $purge_urls, get_feed_link( 'comments_' ) ); // Get the author page. $purge_author = array( get_author_posts_url( $post->post_author ) ); // Get all dates archive page. $purge_dates = get_rocket_post_dates_urls( $post->ID ); // Remove dates archives page and author page to preload cache. $purge_urls = array_diff( $purge_urls, $purge_dates, $purge_author ); // Create json file and run WP Rocket Bot. $json_encode_urls = '["' . implode( '","', array_filter( $purge_urls ) ) . '"]'; if ( rocket_put_content( WP_ROCKET_PATH . 'cache.json', $json_encode_urls ) ) { global $do_rocket_bot_cache_json; $do_rocket_bot_cache_json = true; } } } if ( ! function_exists( 'run_rocket_bot_after_clean_term' ) ) { /** * Actions to be done after the purge cache files of a term * By Default, this hook call the WP Rocket Bot (cache json) * * @deprecated 3.2 * @since 2.6.8 * * @param object $post The post object. * @param array $purge_urls An array of URLs to clean. * @param string $lang The language to clean. */ function run_rocket_bot_after_clean_term( $post, $purge_urls, $lang ) { _deprecated_function( __FUNCTION__, '3.2' ); // Add Homepage URL to $purge_urls for bot crawl. array_push( $purge_urls, get_rocket_i18n_home_url( $lang ) ); // Create json file and run WP Rocket Bot. $json_encode_urls = '["' . implode( '","', array_filter( $purge_urls ) ) . '"]'; if ( rocket_put_content( WP_ROCKET_PATH . 'cache.json', $json_encode_urls ) ) { global $do_rocket_bot_cache_json; $do_rocket_bot_cache_json = true; } } } if ( ! function_exists( 'rocket_clean_directory_for_default_language_on_wpml' ) ) { /** * Conflict with WPML: Clear the homepage when the "Use directory for default language" option is activated. * * @since 2.6.8 * @deprecated 3.2.4 */ function rocket_clean_directory_for_default_language_on_wpml() { _deprecated_function( __FUNCTION__, '3.2.4' ); $option = get_option( 'icl_sitepress_settings' ); if ( 1 === $option['language_negotiation_type'] && $option['urls']['directory_for_default_language'] ) { rocket_clean_files( home_url() ); } } } if ( ! function_exists( 'rocket_fetch_and_cache_busting' ) ) { /** * Fetch and save the cache busting file content * * @since 2.10 * @deprecated 3.2.5 * @author Remy Perona * * @param string $src Original URL of the asset. * @param array $cache_busting_paths Paths used to generated the cache busting file. * @param string $abspath_src Absolute path to the asset. * @param string $current_filter Current filter value. * @return bool true if successful, false otherwise */ function rocket_fetch_and_cache_busting( $src, $cache_busting_paths, $abspath_src, $current_filter ) { _deprecated_function( __FUNCTION__, '3.2.5' ); if ( wp_is_stream( $src ) ) { $response = wp_remote_get( $src ); $content = wp_remote_retrieve_body( $response ); } else { $content = rocket_direct_filesystem()->get_contents( $src ); } if ( ! $content ) { return false; } if ( 'style_loader_src' === $current_filter ) { /** * Filters the Document Root path to use during CSS minification to rewrite paths * * @since 2.7 * * @param string The Document Root path. */ $document_root = apply_filters( 'rocket_min_documentRoot', wp_normalize_path( dirname( $_SERVER['SCRIPT_FILENAME'] ) ) ); // Rewrite import/url in CSS content to add the absolute path to the file. $content = Minify_CSS_UriRewriter::rewrite( $content, dirname( $abspath_src ), $document_root ); } if ( ! rocket_direct_filesystem()->is_dir( $cache_busting_paths['bustingpath'] ) ) { rocket_mkdir_p( $cache_busting_paths['bustingpath'] ); } rocket_mkdir_p( dirname( $cache_busting_paths['filepath'] ) ); return rocket_put_content( $cache_busting_paths['filepath'], $content ); } } deprecated/3.3.php 0000644 00000075342 15174677547 0007722 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; if ( ! function_exists( 'get_rocket_footprint' ) ) : /** * Get WP Rocket footprint * * @deprecated 3.3 * @since 3.0.5 White label footprint if WP_ROCKET_WHITE_LABEL_FOOTPRINT is defined. * @since 2.0 * * @param bool $debug (default: true) If true, adds the date of generation cache file. * @return string The footprint that will be printed */ function get_rocket_footprint( $debug = true ) { _deprecated_function( __FUNCTION__ . '()', '3.3', '\WP_Rocket\Buffer\Cache->get_rocket_footprint()' ); $footprint = defined( 'WP_ROCKET_WHITE_LABEL_FOOTPRINT' ) ? "\n" . '<!-- Cached for great performance' : "\n" . '<!-- This website is like a Rocket, isn\'t it? Performance optimized by ' . WP_ROCKET_PLUGIN_NAME . '. Learn more: https://wp-rocket.me'; if ( $debug ) { $footprint .= ' - Debug: cached@' . time(); } $footprint .= ' -->'; return $footprint; } endif; if ( ! function_exists( 'rocket_lazyload_script' ) ) : /** * Add lazyload options to the footer * * @deprecated 3.3 * @since 2.11 load options in the footer and add filter for the threshold * @since 1.3.5 It's possible to exclude LazyLoad process by used do_rocket_lazyload filter * @since 1.1.0 This code is insert in head with inline script for more performance * @since 1.0 */ function rocket_lazyload_script() { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Assets::insertLazyloadScript()' ); if ( ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) ) { return; } if ( ( ! get_rocket_option( 'lazyload' ) && ! get_rocket_option( 'lazyload_iframes' ) ) || ( ! apply_filters( 'do_rocket_lazyload', true ) && ! apply_filters( 'do_rocket_lazyload_iframes', true ) ) ) { return; } $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; $elements = []; if ( get_rocket_option( 'lazyload' ) ) { $elements[] = 'img'; } if ( get_rocket_option( 'lazyload_iframes' ) ) { $elements[] = 'iframe'; } /** * Filters the threshold at which lazyload is triggered * * @since 2.11 * @author Remy Perona * * @param int $threshold Threshold value. */ $threshold = apply_filters( 'rocket_lazyload_threshold', 300 ); echo '<script>(function(w, d){ var b = d.getElementsByTagName("body")[0]; var s = d.createElement("script"); s.async = true; s.src = !("IntersectionObserver" in w) ? "' . get_rocket_cdn_url( WP_ROCKET_FRONT_JS_URL, array( 'all', 'css_and_js', 'js' ) ) . 'lazyload-8.15.2' . $suffix . '.js" : "' . get_rocket_cdn_url( WP_ROCKET_FRONT_JS_URL, array( 'all', 'css_and_js', 'js' ) ) . 'lazyload-10.17' . $suffix . '.js"; w.lazyLoadOptions = { elements_selector: "' . esc_attr( implode( ',', $elements ) ) . '", data_src: "lazy-src", data_srcset: "lazy-srcset", data_sizes: "lazy-sizes", skip_invisible: false, class_loading: "lazyloading", class_loaded: "lazyloaded", threshold: ' . esc_attr( $threshold ) . ', callback_load: function(element) { if ( element.tagName === "IFRAME" && element.dataset.rocketLazyload == "fitvidscompatible" ) { if (element.classList.contains("lazyloaded") ) { if (typeof window.jQuery != "undefined") { if (jQuery.fn.fitVids) { jQuery(element).parent().fitVids(); } } } } } }; // Your options here. See "recipes" for more information about async. b.appendChild(s); }(window, document)); // Listen to the Initialized event window.addEventListener(\'LazyLoad::Initialized\', function (e) { // Get the instance and puts it in the lazyLoadInstance variable var lazyLoadInstance = e.detail.instance; var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { lazyLoadInstance.update(); } ); } ); var b = document.getElementsByTagName("body")[0]; var config = { childList: true, subtree: true }; observer.observe(b, config); }, false); </script>'; if ( get_rocket_option( 'lazyload_youtube' ) ) { /** * Filters the resolution of the YouTube thumbnail * * @since 2.11.5 * @author Arun Basil Lal * * @param string $thumbnail_resolution The resolution of the thumbnail. Accepted values: default, mqdefault, sddefault, hqdefault, maxresdefault */ $thumbnail_resolution = apply_filters( 'rocket_youtube_thumbnail_resolution', 'hqdefault' ); echo <<<HTML <script>function lazyLoadThumb(e){var t='<img src="https://i.ytimg.com/vi/ID/$thumbnail_resolution.jpg">',a='<div class="play"></div>';return t.replace("ID",e)+a}function lazyLoadYoutubeIframe(){var e=document.createElement("iframe"),t="https://www.youtube.com/embed/ID?autoplay=1";t+=0===this.dataset.query.length?'':'&'+this.dataset.query;e.setAttribute("src",t.replace("ID",this.dataset.id)),e.setAttribute("frameborder","0"),e.setAttribute("allowfullscreen","1"),this.parentNode.replaceChild(e,this)}document.addEventListener("DOMContentLoaded",function(){var e,t,a=document.getElementsByClassName("rll-youtube-player");for(t=0;t<a.length;t++)e=document.createElement("div"),e.setAttribute("data-id",a[t].dataset.id),e.setAttribute("data-query", a[t].dataset.query),e.innerHTML=lazyLoadThumb(a[t].dataset.id),e.onclick=lazyLoadYoutubeIframe,a[t].appendChild(e)});</script> HTML; } } endif; if ( ! function_exists( 'rocket_lazyload_enqueue' ) ) : /** * Enqueue the CSS code for Youtube lazyload styling * * @deprecated 3.3 * @since 2.11 * @author Remy Perona */ function rocket_lazyload_enqueue() { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Assets::insertYoutubeThumbnailCSS()' ); if ( ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) ) { return; } if ( ( ! get_rocket_option( 'lazyload_iframes' ) ) || ( ! apply_filters( 'do_rocket_lazyload', true ) && ! apply_filters( 'do_rocket_lazyload_iframes', true ) ) ) { return; } if ( get_rocket_option( 'lazyload_youtube' ) ) { $css = '.rll-youtube-player{position:relative;padding-bottom:56.23%;height:0;overflow:hidden;max-width:100%;background:#000;margin:5px}.rll-youtube-player iframe{position:absolute;top:0;left:0;width:100%;height:100%;z-index:100;background:0 0}.rll-youtube-player img{bottom:0;display:block;left:0;margin:auto;max-width:100%;width:100%;position:absolute;right:0;top:0;border:none;height:auto;cursor:pointer;-webkit-transition:.4s all;-moz-transition:.4s all;transition:.4s all}.rll-youtube-player img:hover{-webkit-filter:brightness(75%)}.rll-youtube-player .play{height:72px;width:72px;left:50%;top:50%;margin-left:-36px;margin-top:-36px;position:absolute;background:url(' . WP_ROCKET_FRONT_URL . 'img/youtube.png) no-repeat;cursor:pointer}'; wp_register_style( 'rocket-lazyload', false ); wp_enqueue_style( 'rocket-lazyload' ); wp_add_inline_style( 'rocket-lazyload', $css ); } } endif; if ( ! function_exists( 'rocket_lazyload_images' ) ) : /** * Replace Gravatar, thumbnails, images in post content and in widget text by LazyLoad * * @deprecated 3.3 * @since 2.6 Add the get_image_tag filter * @since 2.2 Better regex pattern in a replace_callback * @since 1.3.5 It's possible to exclude LazyLoad process by used do_rocket_lazyload filter * @since 1.2.0 It's possible to not lazyload an image with data-no-lazy attribute * @since 1.1.0 Don't lazyload if the thumbnail has already been run through previously * @since 1.0.1 Add priority of hooks at maximum later with PHP_INT_MAX * @since 1.0 * * @param string $html HTML content. * @return string Modified HTML content */ function rocket_lazyload_images( $html ) { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Images::lazyloadImages()' ); // Don't LazyLoad if process is stopped for these reasons. if ( ! get_rocket_option( 'lazyload' ) || ! apply_filters( 'do_rocket_lazyload', true ) || is_feed() || is_preview() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) || empty( $html ) || ( defined( 'DONOTLAZYLOAD' ) && DONOTLAZYLOAD ) || wp_script_is( 'twentytwenty-twentytwenty', 'enqueued' ) ) { return $html; } $html = preg_replace_callback( '#<img([^>]*) src=("(?:[^"]+)"|\'(?:[^\']+)\'|(?:[^ >]+))([^>]*)>#', 'rocket_lazyload_replace_callback', $html ); return $html; } endif; if ( ! function_exists( 'rocket_lazyload_replace_callback' ) ) : /** * Used to check if we have to LazyLoad this or not * * @deprecated 3.3 * @since 2.5.5 Don't apply LazyLoad on images from WP Retina x2 * @since 2.5 Don't apply LazyLoad on all images from LayerSlider * @since 2.4.2 Don't apply LazyLoad on all images from Media Grid * @since 2.3.11 Don't apply LazyLoad on all images from Timthumb * @since 2.3.10 Don't apply LazyLoad on all images from Revolution Slider & Justified Image Grid * @since 2.3.8 Don't apply LazyLoad on captcha from Really Simple CAPTCHA * @since 2.2 * * @param array $matches Images matching the regex. * @return string Modified HTML content */ function rocket_lazyload_replace_callback( $matches ) { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Images::lazyloadImages()' ); // Don't apply LazyLoad on images from WP Retina x2. if ( function_exists( 'wr2x_picture_rewrite' ) ) { if ( wr2x_get_retina( trailingslashit( ABSPATH ) . wr2x_get_pathinfo_from_image_src( trim( $matches[2], '"' ) ) ) ) { return $matches[0]; } } /** * Filters the attributes used to prevent lazylad from being applied * * @since 2.11 * @author Remy Perona * * @param array $excluded_attributes An array of excluded attributes. */ $excluded_attributes = apply_filters( 'rocket_lazyload_excluded_attributes', array( 'data-src=', 'data-no-lazy=', 'data-lazy-original=', 'data-lazy-src=', 'data-lazysrc=', 'data-lazyload=', 'data-bgposition=', 'data-envira-src=', 'fullurl=', 'lazy-slider-img=', 'data-srcset=', 'class="ls-l', 'class="ls-bg', ) ); /** * Filters the src used to prevent lazylad from being applied * * @since 2.11 * @author Remy Perona * * @param array $excluded_src An array of excluded src. */ $excluded_src = apply_filters( 'rocket_lazyload_excluded_src', array( '/wpcf7_captcha/', 'timthumb.php?src', ) ); if ( rocket_is_excluded_lazyload( $matches[1] . $matches[3], $excluded_attributes ) || rocket_is_excluded_lazyload( $matches[2], $excluded_src ) ) { return $matches[0]; } /** * Filter the LazyLoad placeholder on src attribute * * @since 1.1 * * @param string $placeholder Placeholder that will be printed. */ $placeholder = apply_filters( 'rocket_lazyload_placeholder', 'data:image/gif;base64,R0lGODdhAQABAPAAAP///wAAACwAAAAAAQABAEACAkQBADs=' ); $html = sprintf( '<img%1$s src="%4$s" data-lazy-src=%2$s%3$s>', $matches[1], $matches[2], $matches[3], $placeholder ); $html_noscript = sprintf( '<noscript><img%1$s src=%2$s%3$s></noscript>', $matches[1], $matches[2], $matches[3] ); /** * Filter the LazyLoad HTML output * * @since 1.0.2 * * @param array $html Output that will be printed */ $html = apply_filters( 'rocket_lazyload_html', $html, true ); return $html . $html_noscript; } endif; if ( ! function_exists( 'rocket_is_excluded_lazyload' ) ) : /** * Determine if the current image should be excluded from lazyload * * @deprecated 3.3 * @since 1.1 * @author Remy Perona * * @param string $string String to search. * @param array $excluded_values Array of excluded values to search in the string. * @return bool True if one of the excluded values was found, false otherwise */ function rocket_is_excluded_lazyload( $string, $excluded_values ) { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Images::isExcluded()' ); foreach ( $excluded_values as $excluded_value ) { if ( strpos( $string, $excluded_value ) !== false ) { return true; } } return false; } endif; if ( ! function_exists( 'rocket_lazyload_smilies' ) ) : /** * Replace WordPress smilies by Lazy Load * * @since 3.3 * @since 2.0 New system for replace smilies by Lazy Load * @since 1.3.5 It's possible to exclude LazyLoad process by used do_rocket_lazyload filter * @since 1.1.0 Don't lazy-load if the thumbnail has already been run through previously * @since 1.0.1 Add priority of hooks at maximum later with PHP_INT_MAX * @since 1.0 */ function rocket_lazyload_smilies() { _deprecated_function( __FUNCTION__ . '()', '3.3', '\WP_Rocket\Subscriber\Optimization\Lazyload_Subscriber::lazyload_smilies()' ); if ( ! get_rocket_option( 'lazyload' ) || ! apply_filters( 'do_rocket_lazyload', true, 'smilies' ) || ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) || ( defined( 'DONOTLAZYLOAD' ) && DONOTLAZYLOAD ) ) { return; } remove_filter( 'the_content', 'convert_smilies' ); remove_filter( 'the_excerpt', 'convert_smilies' ); remove_filter( 'comment_text', 'convert_smilies', 20 ); add_filter( 'the_content', 'rocket_convert_smilies' ); add_filter( 'the_excerpt', 'rocket_convert_smilies' ); add_filter( 'comment_text', 'rocket_convert_smilies', 20 ); } endif; if ( ! function_exists( 'rocket_convert_smilies' ) ) : /** * Convert text equivalent of smilies to images. * * @source convert_smilies() in /wp-includes/formattings.php * @since 2.0 * @deprecated 3.3 * @param string $text Text to process. * @return string Modified text */ function rocket_convert_smilies( $text ) { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Image::convertSmilies()' ); global $wp_smiliessearch; if ( ! get_option( 'use_smilies' ) || empty( $wp_smiliessearch ) ) { return $text; } $output = ''; // HTML loop taken from texturize function, could possible be consolidated. $textarr = preg_split( '/(<.*>)/U', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); // capture the tags as well as in between. $stop = count( $textarr );// loop stuff. // Ignore processing of specific tags. $tags_to_ignore = 'code|pre|style|script|textarea'; $ignore_block_element = ''; for ( $i = 0; $i < $stop; $i++ ) { $content = $textarr[ $i ]; // If we're in an ignore block, wait until we find its closing tag. if ( '' === $ignore_block_element && preg_match( '/^<(' . $tags_to_ignore . ')>/', $content, $matches ) ) { $ignore_block_element = $matches[1]; } // If it's not a tag and not in ignore block. if ( '' === $ignore_block_element && strlen( $content ) > 0 && '<' !== $content[0] ) { $content = preg_replace_callback( $wp_smiliessearch, 'rocket_translate_smiley', $content ); } // did we exit ignore block. if ( '' !== $ignore_block_element && '</' . $ignore_block_element . '>' === $content ) { $ignore_block_element = ''; } $output .= $content; } return $output; } endif; if ( ! function_exists( 'rocket_translate_smiley' ) ) : /** * Convert one smiley code to the icon graphic file equivalent. * * @source translate_smiley() in /wp-includes/formattings.php * @since 2.0 * @deprecated 3.3 * * @param array $matches An array of matching content. * @return string HTML code for smiley */ function rocket_translate_smiley( $matches ) { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Image::translateSmiley()' ); global $wpsmiliestrans; if ( count( $matches ) === 0 ) { return ''; } $smiley = trim( reset( $matches ) ); $img = $wpsmiliestrans[ $smiley ]; $matches = array(); $ext = preg_match( '/\.([^.]+)$/', $img, $matches ) ? strtolower( $matches[1] ) : false; $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png' ); // Don't convert smilies that aren't images - they're probably emoji. if ( ! in_array( $ext, $image_exts, true ) ) { return $img; } /** * Filter the Smiley image URL before it's used in the image element. * * @since 2.9.0 * * @param string $smiley_url URL for the smiley image. * @param string $img Filename for the smiley image. * @param string $site_url Site URL, as returned by site_url(). */ $src_url = apply_filters( 'smilies_src', includes_url( "images/smilies/$img" ), $img, site_url() ); // Don't LazyLoad if process is stopped for these reasons. if ( is_feed() || is_preview() ) { return sprintf( ' <img src="%s" alt="%s" class="wp-smiley" /> ', esc_url( $src_url ), esc_attr( $smiley ) ); } /** This filter is documented in inc/front/lazyload.php */ $placeholder = apply_filters( 'rocket_lazyload_placeholder', 'data:image/gif;base64,R0lGODdhAQABAPAAAP///wAAACwAAAAAAQABAEACAkQBADs=' ); return sprintf( ' <img src="%s" data-lazy-src="%s" alt="%s" class="wp-smiley" /> ', $placeholder, esc_url( $src_url ), esc_attr( $smiley ) ); } endif; if ( ! function_exists( 'rocket_lazyload_iframes' ) ) : /** * Replace iframes by LazyLoad * * @deprecated 3.3 * @since 2.6 * * @param string $html HTML content. * @return string Modified HTML content */ function rocket_lazyload_iframes( $html ) { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Iframe::lazyloadIframes()' ); // Don't LazyLoad if process is stopped for these reasons. if ( ! get_rocket_option( 'lazyload_iframes' ) || ! apply_filters( 'do_rocket_lazyload_iframes', true ) || is_feed() || is_preview() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || empty( $html ) || ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) || ( defined( 'DONOTLAZYLOAD' ) && DONOTLAZYLOAD ) ) { return $html; } preg_match_all( '@<iframe(?<atts>\s.+)>.*</iframe>@iUs', $html, $matches, PREG_SET_ORDER ); if ( empty( $matches ) ) { return $html; } foreach ( $matches as $iframe ) { // Don't mess with the Gravity Forms ajax iframe. if ( strpos( $iframe[0], 'gform_ajax_frame' ) ) { continue; } // Don't lazyload if iframe has data-no-lazy attribute. if ( strpos( $iframe[0], 'data-no-lazy=' ) ) { continue; } // Don't lazyload if iframe is google recaptcha fallback. if ( strpos( $iframe[0], 'recaptcha/api/fallback' ) ) { continue; } // Given the previous regex pattern, $iframe['atts'] starts with a whitespace character. if ( ! preg_match( '@\ssrc\s*=\s*(\'|")(?<src>.*)\1@iUs', $iframe['atts'], $atts ) ) { continue; } $iframe['src'] = trim( $atts['src'] ); if ( '' === $iframe['src'] ) { continue; } if ( get_rocket_option( 'lazyload_youtube' ) ) { $youtube_id = rocket_lazyload_get_youtube_id_from_url( $iframe['src'] ); if ( $youtube_id ) { $query = wp_parse_url( htmlspecialchars_decode( $iframe['src'] ), PHP_URL_QUERY ); /** * Filter the LazyLoad HTML output on Youtube iframes * * @since 2.11 * * @param array $html Output that will be printed. */ $youtube_lazyload = apply_filters( 'rocket_lazyload_youtube_html', '<div class="rll-youtube-player" data-id="' . esc_attr( $youtube_id ) . '" data-query="' . esc_attr( $query ) . '"></div>' ); $youtube_lazyload .= '<noscript>' . $iframe[0] . '</noscript>'; $html = str_replace( $iframe[0], $youtube_lazyload, $html ); continue; } } /** * Filter the LazyLoad placeholder on src attribute * * @since 2.11 * * @param string $placeholder placeholder that will be printed. */ $placeholder = apply_filters( 'rocket_lazyload_placeholder', 'about:blank' ); $placeholder_atts = str_replace( $iframe['src'], $placeholder, $iframe['atts'] ); $iframe_lazyload = str_replace( $iframe['atts'], $placeholder_atts . ' data-rocket-lazyload="fitvidscompatible" data-lazy-src="' . esc_url( $iframe['src'] ) . '"', $iframe[0] ); /** * Filter the LazyLoad HTML output on iframes * * @since 2.11 * * @param array $html Output that will be printed. */ $iframe_lazyload = apply_filters( 'rocket_lazyload_iframe_html', $iframe_lazyload ); $iframe_lazyload .= '<noscript>' . $iframe[0] . '</noscript>'; $html = str_replace( $iframe[0], $iframe_lazyload, $html ); } return $html; } endif; if ( ! function_exists( 'rocket_deactivate_lazyload_on_specific_posts' ) ) : /** * Check if we need to exclude LazyLoad on specific posts * * @since 3.3 * @since 2.5 */ function rocket_deactivate_lazyload_on_specific_posts() { _deprecated_function( __FUNCTION__ . '()', '3.3', '\WP_Rocket\Subscriber\Optimization\Lazyload_Subscriber::deactivate_lazyload_on_specific_posts()' ); if ( is_rocket_post_excluded_option( 'lazyload' ) ) { add_filter( 'do_rocket_lazyload', '__return_false' ); } if ( is_rocket_post_excluded_option( 'lazyload_iframes' ) ) { add_filter( 'do_rocket_lazyload_iframes', '__return_false' ); } } endif; if ( ! function_exists( 'rocket_lazyload_on_srcset' ) ) : /** * Compatibility with images with srcset attribute * * @author Remy Perona * * @since 3.3 * @since 2.8 Also add sizes to the data-lazy-* attributes to prevent error in W3C validator * @since 2.7 * * @param string $html HTML content. * @return string Modified HTML content */ function rocket_lazyload_on_srcset( $html ) { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Image:lazyloadResponsiveAttributes()' ); if ( preg_match( '/srcset=("(?:[^"]+)"|\'(?:[^\']+)\'|(?:[^ >]+))/i', $html ) ) { $html = str_replace( 'srcset=', 'data-lazy-srcset=', $html ); } if ( preg_match( '/sizes=("(?:[^"]+)"|\'(?:[^\']+)\'|(?:[^ >]+))/i', $html ) ) { $html = str_replace( 'sizes=', 'data-lazy-sizes=', $html ); } return $html; } endif; if ( ! function_exists( 'rocket_lazyload_get_youtube_id_from_url' ) ) : /** * Gets youtube video ID from URL * * @author Remy Perona * @deprecated 3.3 * @since 2.11 * * @param string $url URL to parse. * @return string Youtube video id or false if none found. */ function rocket_lazyload_get_youtube_id_from_url( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.3', '\RocketLazyload\Iframe:getYoutubeIDFromURL()' ); $pattern = '#^(?:https?:)?(?://)?(?:www\.)?(?:youtu\.be|youtube\.com|youtube-nocookie\.com)/(?:embed/|v/|watch/?\?v=)([\w-]{11})#iU'; $result = preg_match( $pattern, $url, $matches ); if ( ! $result ) { return false; } if ( 'videoseries' === $matches[1] ) { return false; } return $matches[1]; } endif; if ( ! function_exists( 'rocket_user_agent' ) ) : /** * Add Rocket information into USER_AGENT * * @since 1.1.0 * @deprecated 3.3.6 * * @param string $user_agent User Agent value. * @return string WP Rocket user agent */ function rocket_user_agent( $user_agent ) { _deprecated_function( __FUNCTION__ . '()', '3.3.6', '\WP_Rocket\Subscriber\Plugin\Updater_Api_Common_Subscriber->get_rocket_user_agent()' ); $consumer_key = ''; if ( isset( $_POST[ WP_ROCKET_SLUG ]['consumer_key'] ) ) { $consumer_key = $_POST[ WP_ROCKET_SLUG ]['consumer_key']; } elseif ( '' !== (string) get_rocket_option( 'consumer_key' ) ) { $consumer_key = (string) get_rocket_option( 'consumer_key' ); } $consumer_email = ''; if ( isset( $_POST[ WP_ROCKET_SLUG ]['consumer_email'] ) ) { $consumer_email = $_POST[ WP_ROCKET_SLUG ]['consumer_email']; } elseif ( '' !== (string) get_rocket_option( 'consumer_email' ) ) { $consumer_email = (string) get_rocket_option( 'consumer_email' ); } $bonus = ! get_rocket_option( 'do_beta' ) ? '' : '+'; $php_version = preg_replace( '@^(\d\.\d+).*@', '\1', phpversion() ); $new_ua = sprintf( '%s;WP-Rocket|%s%s|%s|%s|%s|%s;', $user_agent, WP_ROCKET_VERSION, $bonus, $consumer_key, $consumer_email, esc_url( home_url() ), $php_version ); return $new_ua; } endif; if ( ! function_exists( 'rocket_add_own_ua' ) ) : /** * Force our user agent header when we hit our urls * * @since 2.4 * @deprecated 3.3.6 * * @param array $request An array of request arguments. * @param string $url Requested URL. * @return array An array of requested arguments */ function rocket_add_own_ua( $request, $url ) { _deprecated_function( __FUNCTION__ . '()', '3.3.6', '\WP_Rocket\Subscriber\Plugin\Updater_Api_Common_Subscriber->maybe_set_rocket_user_agent()' ); if ( ! is_string( $url ) ) { return $request; } if ( strpos( $url, 'wp-rocket.me' ) !== false ) { $request['user-agent'] = rocket_user_agent( $request['user-agent'] ); } return $request; } endif; if ( ! function_exists( 'rocket_updates_exclude' ) ) : /** * Excludes WP Rocket from WP updates * * @since 1.0 * @deprecated 3.3.6 * * @param array $request An array of HTTP request arguments. * @param string $url The request URL. * @return array Updated array of HTTP request arguments. */ function rocket_updates_exclude( $request, $url ) { _deprecated_function( __FUNCTION__ . '()', '3.3.6', '\WP_Rocket\Subscriber\Plugin\Updater_Subscriber->exclude_rocket_from_wp_updates()' ); if ( ! is_string( $url ) ) { return $request; } if ( 0 !== strpos( $url, 'http://api.wordpress.org/plugins/update-check' ) || ! isset( $request['body']['plugins'] ) ) { return $request; // Not a plugin update request. Stop immediately. } $plugins = maybe_unserialize( $request['body']['plugins'] ); if ( isset( $plugins->plugins[ plugin_basename( WP_ROCKET_FILE ) ], $plugins->active[ array_search( plugin_basename( WP_ROCKET_FILE ), $plugins->active, true ) ] ) ) { unset( $plugins->plugins[ plugin_basename( WP_ROCKET_FILE ) ] ); unset( $plugins->active[ array_search( plugin_basename( WP_ROCKET_FILE ), $plugins->active, true ) ] ); } $request['body']['plugins'] = maybe_serialize( $plugins ); return $request; } endif; if ( ! function_exists( 'rocket_force_info' ) ) : /** * Hack the returned object * * @since 1.0 * @deprecated 3.3.6 * * @param false|object|array $bool The result object or array. Default false. * @param string $action The type of information being requested from the Plugin Install API. * @param object $args Plugin API arguments. * @return false|object|array Empty object if slug is WP Rocket, default value otherwise */ function rocket_force_info( $bool, $action, $args ) { _deprecated_function( __FUNCTION__ . '()', '3.3.6', '\WP_Rocket\Subscriber\Plugin\Information_Subscriber->exclude_rocket_from_wp_info()' ); if ( 'plugin_information' === $action && 'wp-rocket' === $args->slug ) { return new stdClass(); } return $bool; } endif; if ( ! function_exists( 'rocket_force_info_result' ) ) : /** * Hack the returned result with our content * * @since 1.0 * @deprecated 3.3.6 * * @param object|WP_Error $res Response object or WP_Error. * @param string $action The type of information being requested from the Plugin Install API. * @param object $args Plugin API arguments. * @return object|WP_Error Updated response object or WP_Error */ function rocket_force_info_result( $res, $action, $args ) { _deprecated_function( __FUNCTION__ . '()', '3.3.6', '\WP_Rocket\Subscriber\Plugin\Information_Subscriber->add_rocket_info()' ); if ( 'plugin_information' === $action && isset( $args->slug ) && 'wp-rocket' === $args->slug && isset( $res->external ) && $res->external ) { $request = wp_remote_post( WP_ROCKET_WEB_INFO, array( 'timeout' => 30, 'action' => 'plugin_information', 'request' => serialize( $args ), ) ); if ( is_wp_error( $request ) ) { // translators: %s is an URL. $res = new WP_Error( 'plugins_api_failed', sprintf( __( 'An unexpected error occurred. Something may be wrong with WP-Rocket.me or this server’s configuration. If you continue to have problems, <a href="%s">contact support</a>.', 'rocket' ), rocket_get_external_url( 'support', array( 'utm_source' => 'wp_plugin', 'utm_medium' => 'wp_rocket', ) ) ), $request->get_error_message() ); } else { $res = maybe_unserialize( wp_remote_retrieve_body( $request ) ); if ( ! is_object( $res ) && ! is_array( $res ) ) { // translators: %s is an URL. $res = new WP_Error( 'plugins_api_failed', sprintf( __( 'An unexpected error occurred. Something may be wrong with WP-Rocket.me or this server’s configuration. If you continue to have problems, <a href="%s">contact support</a>.', 'rocket' ), rocket_get_external_url( 'support', array( 'utm_source' => 'wp_plugin', 'utm_medium' => 'wp_rocket', ) ) ), wp_remote_retrieve_body( $request ) ); } } } return $res; } endif; if ( ! function_exists( 'rocket_check_update' ) ) : /** * When WP sets the update_plugins site transient, we set our own transient, then see rocket_add_response_to_updates * * @since 2.6.5 * @deprecated 3.3.6 * * @param Object $value Site transient object. */ function rocket_check_update( $value ) { _deprecated_function( __FUNCTION__ . '()', '3.3.6', '\WP_Rocket\Subscriber\Plugin\Updater_Subscriber->maybe_add_rocket_update_data()' ); $timer_update_wprocket = (int) get_site_transient( 'update_wprocket' ); $temp_object = get_site_transient( 'update_wprocket_response' ); if ( ( ! isset( $_GET['rocket_force_update'] ) || defined( 'WP_INSTALLING' ) ) && ( 12 * HOUR_IN_SECONDS ) > ( time() - $timer_update_wprocket ) // retry in 12 hours. ) { if ( is_object( $value ) && false !== $temp_object ) { if ( version_compare( $temp_object->new_version, WP_ROCKET_VERSION ) > 0 ) { $value->response[ $temp_object->plugin ] = $temp_object; } else { delete_site_transient( 'update_wprocket_response' ); } } return $value; } if ( isset( $_GET['rocket_force_update'] ) ) { $_SERVER['REQUEST_URI'] = remove_query_arg( 'rocket_force_update' ); } $plugin_folder = plugin_basename( dirname( WP_ROCKET_FILE ) ); $plugin_file = basename( WP_ROCKET_FILE ); $version = true; if ( ! $value ) { $value = new stdClass(); $value->last_checked = time(); } $response = wp_remote_get( WP_ROCKET_WEB_CHECK, array( 'timeout' => 30, ) ); if ( ! is_a( $response, 'WP_Error' ) && 200 === $response['response']['code'] && strlen( $response['body'] ) > 32 ) { set_site_transient( 'update_wprocket', time() ); list( $version, $url ) = explode( '|', $response['body'] ); if ( version_compare( $version, WP_ROCKET_VERSION ) <= 0 ) { return $value; } $temp_array = array( 'slug' => $plugin_folder, 'plugin' => $plugin_folder . '/' . $plugin_file, 'new_version' => $version, 'url' => 'https://wp-rocket.me', 'package' => $url, ); $temp_object = (object) $temp_array; $value->response[ $plugin_folder . '/' . $plugin_file ] = $temp_object; set_site_transient( 'update_wprocket_response', $temp_object ); } else { set_site_transient( 'update_wprocket', ( time() + ( 11 * HOUR_IN_SECONDS ) ) ); // retry in 1 hour in case of error.. } return $value; } endif; if ( ! function_exists( 'rocket_reset_check_update_timer' ) ) : /** * When WP deletes the update_plugins site transient or updates the plugins, we delete our own transients to avoid another 12 hours waiting * * @since 2.6.8 * @deprecated 3.3.6 * * @param string $transient Transient name. * @param object $value Transient object. */ function rocket_reset_check_update_timer( $transient = 'update_plugins', $value = null ) { _deprecated_function( __FUNCTION__ . '()', '3.3.6' ); // $value used by set. if ( 'update_plugins' === $transient ) { if ( is_null( $value ) || is_object( $value ) && ! isset( $value->response ) ) { delete_site_transient( 'update_wprocket' ); } } } endif; deprecated/classes/busting/class-facebook-pickles.php 0000644 00000100742 15174677547 0017024 0 ustar 00 <?php namespace WP_Rocket\Busting; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Logger\Logger; /** * Manages the cache busting of the Facebook Pixel files. * * @since 3.9 deprecated * @since 3.2 * @author Grégory Viguier */ class Facebook_Pickles { use DeprecatedClassTrait; /** * Regex pattern to capture a locale. * * @var string * @since 3.2 * @author Grégory Viguier */ const LOCALE_CAPTURE = '(?<locale>[a-zA-Z_-]+)'; /** * Regex pattern to capture a version. * * @var string * @since 3.2 * @author Grégory Viguier */ const VERSION_CAPTURE = '(?<version>[\d\.]+)'; /** * Regex pattern to capture an app ID. * * @var string * @since 3.2 * @author Grégory Viguier */ const APP_ID_CAPTURE = '(?<app_id>\d+)'; /** * Cache busting files base path. * * @var string * @since 3.2 * @access private * @author Grégory Viguier */ private $busting_path; /** * Cache busting base URL. * * @var string * @since 3.2 * @access private * @author Grégory Viguier */ private $busting_url; /** * Main file URL (remote). * %s is a locale like "en_US". * * @var string * @since 3.2 * @access private * @author Grégory Viguier */ private $main_file_url = 'https://connect.facebook.net/%s/fbevents.js'; /** * Main file name (local). * %s is like "{{locale}}-{{version}}". * * @var string * @since 3.2 * @access private * @author Grégory Viguier */ private $main_file_name = 'fbpix-events-%s.js'; /** * Config file URL (remote). * %d is an app ID (a number), %s is a version like "2.8.30". * The "r" argument is the release segment: it is considered "stable". * * @var string * @since 3.2 * @access private * @author Grégory Viguier */ private $config_file_url = 'https://connect.facebook.net/signals/config/%s?v=%s&r=stable'; /** * Config file name (local). * %s is like "{{app_id}}-{{version}}". * * @var string * @since 3.2 * @access private * @author Grégory Viguier */ private $config_file_name = 'fbpix-config-%s.js'; /** * Plugins file URL (remote). * 1st %s is a plugin name like "identity", 2nd %s is a version like "2.8.30". * * @var string * @since 3.2 * @access private * @author Grégory Viguier */ private $plugins_file_url = 'https://connect.facebook.net/signals/plugins/%s?v=%s'; /** * Plugins file name (local). * %s is like "{{plugin_name}}-{{version}}". * * @var string * @since 3.2 * @access private * @author Grégory Viguier */ private $plugins_file_name = 'fbpix-plugin-%s.js'; /** * Flag to track the replacement. * * @var bool * @since 3.2 * @access private * @author Grégory Viguier */ private $is_replaced = false; /** * Constructor. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $busting_path Path to the busting directory. * @param string $busting_url URL of the busting directory. */ public function __construct( $busting_path, $busting_url ) { self::deprecated_class( '3.9' ); /** Warning: all file names and script URLs are dynamic, and must be run through sprintf(). */ $this->busting_path = $busting_path . 'facebook-tracking/'; $this->busting_url = $busting_url . 'facebook-tracking/'; } /** ----------------------------------------------------------------------------------------- */ /** MAIN PROCESS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Perform the URL replacement process. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $html HTML contents. * @return string HTML contents. */ public function replace_url( $html ) { $this->is_replaced = false; $tags = $this->find_tags( $html ); if ( ! $tags ) { return $html; } Logger::info( 'FACEBOOK PIXEL CACHING PROCESS STARTED.', [ 'fb pixel', 'tag' => $tags['tag_to_search'], ] ); $all_files = []; /** * Fetch the main file: https://connect.facebook.net/{{locale}}/fbevents.js. */ $version = $this->get_most_recent_local_version(); $locale = $this->get_locale_from_url( $tags['tag_to_search'] ); $main_file_url = $this->get_main_file_url( $locale ); if ( $version ) { // At least 1 main file exists locally (but maybe not in the right locale). $main_file_path = $this->get_busting_file_path( $locale, $version ); $main_file_contents = $this->get_file_contents( $main_file_path, $main_file_url ); } else { // No cached files yet. $main_file_contents = $this->get_remote_contents( $main_file_url ); } if ( ! $main_file_contents ) { return $html; } /** * Grab some data from the main file and the inline tag: app_id and version. */ $variables = $this->get_variables( $main_file_contents, $tags['tag_to_search'] ); if ( ! $variables ) { return $html; } if ( ! $version ) { // The local file doesn't exist yet, so we couldn't get its version (and so, can't know its path yet) until we fetch a fresh copy. $main_file_path = $this->get_busting_file_path( $locale, $variables['version'] ); } $all_files[] = $main_file_path; unset( $version ); /** * Fetch the config file: https://connect.facebook.net/signals/config/{{app_id}}?v={{version}}&r={{release_segment}}. */ $config_file_path = $this->get_config_file_path( $variables ); if ( ! $config_file_path ) { return $html; } $all_files[] = $config_file_path; /** * Fetch all plugin files: https://connect.facebook.net/signals/plugins/{{pluginName}}.js?v={{version}}. */ $plugin_file_paths = $this->get_plugin_file_paths( $variables ); if ( ! $plugin_file_paths ) { return $html; } $all_files = array_merge( $all_files, $plugin_file_paths ); /** * Modify the main file contents. */ $busting_file_url = $this->get_busting_file_url( $locale, $variables['version'] ); $busting_dir_url = dirname( $busting_file_url ) . '/'; $main_file_contents = $this->replace_main_file_contents( $main_file_contents, $busting_dir_url ); if ( ! $main_file_contents ) { return $html; } /** * Save all the changes to the main file. */ $updated = $this->update_file_contents( $main_file_path, $main_file_contents ); if ( ! $updated ) { return $html; } /** * Finally, replace the main file URL by the local one in the inline script tag. */ $replace_tag = preg_replace( '@(?:https?:)?//connect\.facebook\.net/[a-zA-Z_-]+/fbevents\.js@i', $busting_file_url, $tags['tag_to_replace'], -1, $count ); if ( ! $count || false === strpos( $html, $tags['tag_to_replace'] ) ) { Logger::error( 'The local file URL could not be replaced in the page contents.', [ 'fb pixel' ] ); return $html; } $html = str_replace( $tags['tag_to_replace'], $replace_tag, $html ); $this->is_replaced = true; /** * Triggered once the Facebook pixel URL has been replaced in the page contents. * * @since 3.2 * @author Grégory Viguier * * @param string $busting_file_url URL of the local main file. * @param array $all_files An array of all file paths. */ do_action( 'rocket_after_facebook_pixel_url_replaced', $busting_file_url, $all_files ); Logger::info( 'Facebook pixel caching process succeeded.', [ 'fb pixel', 'files' => $all_files, ] ); return $html; } /** * Tell if the replacement was successful or not. * * @since 3.2 * @access public * @author Grégory Viguier * * @return bool */ public function is_replaced() { return $this->is_replaced; } /** ----------------------------------------------------------------------------------------- */ /** GRAB/MANIPULATE DATA IN CONTENTS ======================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Search for elements in the DOM. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $html HTML contents. * @return array|bool { * An array on success, described as below. False if nothing is found. * * @type string $tag_to_replace The script tag that contains the facebook.net URL: this is the tag that will be replaced in the page HTML. * @type string $tag_to_search It contains both app ID and facebook.net URL: this is what will be searched in for this data. * * When the app ID and the URL are in the same tag, $tag_to_replace and $tag_to_search are the same. * } */ private function find_tags( $html ) { preg_match_all( '@<script[^>]*?>(.*)</script>@Umsi', $html, $matches, PREG_SET_ORDER ); if ( empty( $matches ) ) { return false; } $tags = [ 'app_id' => [], 'url' => [], 'both' => [], ]; foreach ( $matches as $match ) { list( $tag, $script ) = $match; if ( ! trim( $script ) ) { continue; } $has_app_id = false; $has_url = false; if ( preg_match( '@fbq\s*\(\s*["\']init["\']\s*,\s*["\']' . self::APP_ID_CAPTURE . '["\']@', $script, $matches_init ) ) { if ( (int) $matches_init['app_id'] > 0 ) { $has_app_id = true; } } $has_url = (bool) $this->get_locale_from_url( $script ); if ( $has_app_id && $has_url ) { // OK we have both. $tags['both'] = $tag; break; } if ( $has_app_id ) { $tags['app_id'] = $tag; if ( $tags['url'] ) { // OK we have both. break; } } elseif ( $has_url ) { $tags['url'] = $tag; if ( $tags['app_id'] ) { // OK we have both. break; } } } if ( ! empty( $tags['both'] ) ) { return [ 'tag_to_replace' => $tags['both'], 'tag_to_search' => $tags['both'], ]; } if ( ! empty( $tags['app_id'] ) && ! empty( $tags['url'] ) ) { return [ 'tag_to_replace' => $tags['url'], 'tag_to_search' => $tags['url'] . $tags['app_id'], ]; } return false; } /** * Get some values from the main file and the inline script contents. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $main_file_contents Main file contents. * @param string $tag_contents Inline script contents. * @return array|bool { * An array of values. False on failure. * * @type string $app_id The app ID. * @type string $version The file version. * } */ private function get_variables( $main_file_contents = null, $tag_contents = null ) { $variables = []; if ( isset( $tag_contents ) ) { // Retrieve the app ID from the tag contents. preg_match( '@fbq\s*\(\s*["\']init["\']\s*,\s*["\']' . self::APP_ID_CAPTURE . '["\']@', $tag_contents, $matches ); if ( empty( $matches['app_id'] ) ) { Logger::error( 'The app ID could not be retrieved from the inline script contents.', [ 'fb pixel' ] ); return false; } $variables['app_id'] = $matches['app_id']; } if ( isset( $main_file_contents ) ) { // Retrieve the version from the main file contents. preg_match( '@fbq\.version\s*=\s*["\']' . self::VERSION_CAPTURE . '["\']\s*;@', $main_file_contents, $matches ); if ( empty( $matches['version'] ) ) { Logger::error( 'The version could not be retrieved from the main file contents.', [ 'fb pixel' ] ); return false; } $variables['version'] = $matches['version']; } return $variables; } /** * Perform some replacements in the main file contents. Will be replaced: * - the CDN_BASE_URL value, * - the config file URL, * - the plugins file URL. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $main_file_contents The file contents. * @param string $busting_dir_url URL of the folder containing the files. * @return string|bool The new contents on success. False on failure. */ private function replace_main_file_contents( $main_file_contents, $busting_dir_url ) { /** * Replace the CDN_BASE_URL value. * From: CDN_BASE_URL:"https://connect.facebook.net/" * To: CDN_BASE_URL:"https://example.com/wp-content/cache/busting/facebook-tracking/" */ $replacement = 'CDN_BASE_URL:"' . $busting_dir_url . '"'; if ( ! strpos( $main_file_contents, $replacement ) ) { $main_file_contents = preg_replace( '@CDN_BASE_URL\s*:\s*["\'][^"\']+["\']@', $replacement, $main_file_contents, -1, $count ); if ( ! $count ) { Logger::error( 'The CDN_BASE_URL could not be replaced in the main file contents.', [ 'fb pixel' ] ); return false; } } /** * Replace the config file URL (https://connect.facebook.net/signals/config/{{app_id}}?v={{version}}&r={{release_segment}}). * From: CDN_BASE_URL+"signals/config/"+a+"?v="+b+"&r="+c * To: CDN_BASE_URL+"fbpix-config-"+a+"-"+b+".js" (the release segment is not taken into account, we consider it "stable") */ $replacement_pattern = $this->escape_file_name( $this->config_file_name ); $replacement_pattern = sprintf( $replacement_pattern, '"\+[a-zA-Z._]+\+"\-"\+[a-zA-Z._]+\+"' ); $replacement_pattern = 'CDN_BASE_URL\+"' . $replacement_pattern . '"'; if ( ! preg_match( '/' . $replacement_pattern . '/', $main_file_contents ) ) { $pattern = '@CDN_BASE_URL\s*\+\s*["\']signals/config/["\']\s*\+\s*([a-zA-Z._]+)\s*\+\s*["\']\?v=["\']\s*\+\s*([a-zA-Z._]+)\s*\+\s*["\']&r=["\']\s*\+\s*[a-zA-Z._]+@'; $replacement = 'CDN_BASE_URL+"' . sprintf( $this->config_file_name, '"+$1+"-"+$2+"' ) . '"'; $main_file_contents = preg_replace( $pattern, $replacement, $main_file_contents, -1, $count ); if ( ! $count ) { Logger::error( 'The config file URL could not be replaced in the main file contents.', [ 'fb pixel' ] ); return false; } } /** * Replace the plugins file URL (https://connect.facebook.net/signals/plugins/{{plugin_name}}.js?v={{version}}). * From: CDN_BASE_URL+"signals/plugins/"+b+".js?v="+a.version * To : CDN_BASE_URL+"fbpix-plugin-"+b+"-"+a.version+".js" */ $replacement_pattern = $this->escape_file_name( $this->plugins_file_name ); $replacement_pattern = sprintf( $replacement_pattern, '"\+[a-zA-Z._]+\+"-"\+[a-zA-Z._]+\+"' ); $replacement_pattern = 'CDN_BASE_URL\+"' . $replacement_pattern . '"'; if ( ! preg_match( '/' . $replacement_pattern . '/', $main_file_contents ) ) { $pattern = '@CDN_BASE_URL\s*\+\s*["\']signals/plugins/["\']\s*\+\s*([a-zA-Z._]+)\s*\+\s*["\']\.js\?v=["\']\s*\+\s*([a-zA-Z._]+)@'; $replacement = 'CDN_BASE_URL+"' . sprintf( $this->plugins_file_name, '"+$1+"-"+$2+"' ) . '"'; $main_file_contents = preg_replace( $pattern, $replacement, $main_file_contents, -1, $count ); if ( ! $count ) { Logger::error( 'The plugins file URL could not be replaced in the main file contents.', [ 'fb pixel' ] ); return false; } } return $main_file_contents; } /** ----------------------------------------------------------------------------------------- */ /** UPDATE/SAVE A LOCAL FILE ================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Save the contents of a URL into a local file if it doesn't exist yet. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $url URL to get the contents from. * @param string $path Path to the file that will store the URL contents. * @return bool True on success. False on failure. */ private function maybe_save( $url, $path ) { $filesystem = \rocket_direct_filesystem(); if ( $filesystem->exists( $path ) ) { // If a previous version is present, keep it. return true; } return (bool) $this->save( $url, $path ); } /** * Save the contents of a URL into a local file. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $url URL to get the contents from. * @param string $path Path to the file that will store the URL contents. * @return string|bool The file contents on success. False on failure. */ private function save( $url, $path ) { $contents = $this->get_remote_contents( $url ); if ( ! $contents ) { // Error, we couldn't fetch the file contents. return false; } return $this->update_file_contents( $path, $contents ); } /** * Add new contents to a file. If the file doesn't exist, it is created. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $file_path Path to the file to update. * @param string $file_contents New contents. * @return string|bool The file contents on success. False on failure. */ private function update_file_contents( $file_path, $file_contents ) { if ( ! \rocket_direct_filesystem()->exists( $this->busting_path ) ) { \rocket_mkdir_p( $this->busting_path ); } if ( ! \rocket_put_content( $file_path, $file_contents ) ) { Logger::error( 'Contents could not be written into file.', [ 'fb pixel', 'path' => $file_path, ] ); return false; } /** * Triggered once a file contents have been updated. * * @since 3.2 * @author Grégory Viguier * * @param string $file_path Path to the file to update. * @param string $file_contents The file contents. */ do_action( 'rocket_after_facebook_pixel_file_updated', $file_path, $file_contents ); return $file_contents; } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC BULK ACTIONS ON LOCAL FILES ====================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Look for existing local files and update their contents if there's a new version available. * Actually, if a more recent version exists on the FB side, it will delete all local files and hit the home page to recreate them. * * @since 3.2 * @access public * @author Grégory Viguier * * @return bool True on success. False on failure. */ public function refresh_all() { // Get all local main files. $main_files = $this->get_all_main_files(); if ( ! $main_files ) { // No files (or there's an error). return false !== $main_files; } $updated = false; foreach ( $main_files as $local_main_file ) { $remote_file_contents = $this->get_remote_contents( $this->get_main_file_url( $local_main_file['locale'] ) ); if ( ! $remote_file_contents ) { continue; } $variables = $this->get_variables( $remote_file_contents ); if ( ! $variables ) { unset( $remote_file_contents, $variables ); continue; } if ( version_compare( $local_main_file['version'], $variables['version'] ) >= 0 ) { unset( $remote_file_contents, $variables ); continue; } unset( $remote_file_contents ); $updated = true; break; } if ( ! $updated ) { return true; } // Delete all local files. $deleted = $this->delete_all(); // Purge all cache files (the URL of the new files changed). \rocket_clean_domain(); // Preload the home page to recreate the files. $home_url = user_trailingslashit( home_url(), 'home' ); wp_remote_get( $home_url, [ 'user-agent' => 'WP Rocket/Homepage Preload', 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound ] ); /** * Triggered once the local files have been refreshed. * * @since 3.2 * @author Grégory Viguier * * @param string $version The new version. */ do_action( 'rocket_after_facebook_pixel_files_refreshed', $variables['version'] ); return true; } /** * Delete all Facebook Pixel busting files. * * @since 3.2 * @access public * @author Grégory Viguier * * @return bool True on success. False on failure. */ public function delete_all() { $filesystem = \rocket_direct_filesystem(); $files = $this->get_all_files(); if ( ! $files ) { // No files (or there's an error). return false !== $files; } $error_paths = []; foreach ( $files as $file_name ) { if ( ! $filesystem->delete( $this->busting_path . $file_name, false, 'f' ) ) { $error_paths[] = $this->busting_path . $file_name; } } if ( $error_paths ) { Logger::error( 'Local file(s) could not be deleted.', [ 'fb pixel', 'paths' => $error_paths, ] ); } /** * Triggered once all local files have been deleted (or not). * * @since 3.2 * @author Grégory Viguier * * @param array $files An array of file names. * @param array $error_paths Paths to the files that couldn't be deleted. An empty array if everything is fine. */ do_action( 'rocket_after_facebook_pixel_files_deleted', $files, $error_paths ); return ! $error_paths; } /** ----------------------------------------------------------------------------------------- */ /** SCAN FOR LOCAL FILES ==================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get all cached files in the directory. * * @since 3.2 * @access private * @author Grégory Viguier * * @return array|bool A list of file names. False on failure. */ private function get_all_files() { $filesystem = \rocket_direct_filesystem(); $dir_path = rtrim( $this->busting_path, '\\/' ); if ( ! $filesystem->exists( $dir_path ) ) { return []; } if ( ! $filesystem->is_writable( $dir_path ) ) { Logger::error( 'Directory is not writable.', [ 'fb pixel', 'path' => $dir_path, ] ); return false; } $dir = $filesystem->dirlist( $dir_path ); if ( false === $dir ) { Logger::error( 'Could not get the directory contents.', [ 'fb pixel', 'path' => $dir_path, ] ); return false; } if ( ! $dir ) { return []; } $list = []; foreach ( $dir as $entry ) { if ( 'f' !== $entry['type'] ) { continue; } if ( preg_match( '@^fbpix-(?:config|events|plugin)-.+\.js$@', $entry['name'], $matches ) ) { $list[ $entry['name'] ] = $entry['name']; } } return $list; } /** * Get all main files in the directory. * * @since 3.2 * @access private * @author Grégory Viguier * * @return array|bool { * An array of file names (array keys) with following data as values. False on failure. * * @type string $locale The locale, like "en_US". * @type string $version The file version. * } */ private function get_all_main_files() { $filesystem = \rocket_direct_filesystem(); $dir_path = rtrim( $this->busting_path, '\\/' ); if ( ! $filesystem->exists( $dir_path ) ) { return []; } if ( ! $filesystem->is_writable( $dir_path ) ) { Logger::error( 'Directory is not writable.', [ 'fb pixel', 'path' => $dir_path, ] ); return false; } $dir = $filesystem->dirlist( $dir_path ); if ( false === $dir ) { Logger::error( 'could not get the directory contents.', [ 'fb pixel', 'path' => $dir_path, ] ); return false; } if ( ! $dir ) { return []; } $list = []; $pattern = $this->escape_file_name( $this->main_file_name ); $pattern = sprintf( $pattern, self::LOCALE_CAPTURE . '-' . self::VERSION_CAPTURE ); foreach ( $dir as $entry ) { if ( 'f' !== $entry['type'] ) { continue; } if ( preg_match( '@^' . $pattern . '$@', $entry['name'], $matches ) ) { unset( $matches[0] ); $list[ $entry['name'] ] = $matches; } } return $list; } /** * Get the most recent "version" of the main file cached locally. * * @since 3.2 * @access private * @author Grégory Viguier * * @return string|bool The version on success. False on failure. */ private function get_most_recent_local_version() { $main_files = $this->get_all_main_files(); if ( ! $main_files ) { return false; } $version = false; foreach ( $main_files as $file_name => $data ) { if ( ! $version || version_compare( $data['version'], $version ) > 0 ) { $version = $data['version']; } } return $version; } /** * Get the oldest "version" of the main file cached locally. * * @since 3.2 * @access private * @author Grégory Viguier * * @return string|bool The version on success. False on failure. */ private function get_oldest_local_version() { $main_files = $this->get_all_main_files(); if ( ! $main_files ) { return false; } $version = false; foreach ( $main_files as $file_name => $data ) { if ( ! $version || version_compare( $data['version'], $version ) < 0 ) { $version = $data['version']; } } return $version; } /** ----------------------------------------------------------------------------------------- */ /** REMOTE MAIN FILE ======================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the remote Facebook Pixel URL. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $locale A locale string, like 'en_US'. * @return string */ public function get_main_file_url( $locale ) { return sprintf( $this->main_file_url, $locale ); } /** * Extract the locale from a URL to bust. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $url Any string containing the URL to bust. * @return string|bool The locale on success. False on failure. */ private function get_locale_from_url( $url ) { $pattern = '@//connect\.facebook\.net/' . self::LOCALE_CAPTURE . '/fbevents\.js@i'; if ( ! preg_match( $pattern, $url, $matches ) ) { return false; } return $matches['locale']; } /** ----------------------------------------------------------------------------------------- */ /** BUSTING FILE (aka: cached copy of the main file) ======================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the local Facebook Pixel URL (the "main" file). * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $locale A locale string, like 'en_US'. * @param string $version The script version. * @return string */ private function get_busting_file_url( $locale, $version ) { $filename = $this->get_busting_file_name( $locale, $version ); // This filter is documented in inc/functions/minify.php. return apply_filters( 'rocket_js_url', $this->busting_url . $filename ); } /** * Get the local Facebook Pixel file name. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $locale A locale string, like 'en_US'. * @param string $version The script version. * @return string */ private function get_busting_file_name( $locale, $version ) { return sprintf( $this->main_file_name, $locale . '-' . $version ); } /** * Get the local Facebook Pixel file path. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $locale A locale string, like 'en_US'. * @param string $version The script version. * @return string */ private function get_busting_file_path( $locale, $version ) { return $this->busting_path . $this->get_busting_file_name( $locale, $version ); } /** ----------------------------------------------------------------------------------------- */ /** CONFIG FILE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the path to the local "config" file. If the file doesn't exist, it is created by fetching its contents remotely, then saved locally. * * @since 3.2 * @access private * @see $this->get_variables() * @author Grégory Viguier * * @param array $variables { * An array of variable values. * * @type int $app_id The app ID. * @type string $version The file version. * } * @return string|bool The file path on success. False on failure. */ private function get_config_file_path( $variables ) { $config_file_url = sprintf( $this->config_file_url, $variables['app_id'], $variables['version'] ); $config_file_name = sprintf( $this->config_file_name, $variables['app_id'] . '-' . $variables['version'] ); $config_file_path = $this->busting_path . $config_file_name; if ( ! $this->maybe_save( $config_file_url, $config_file_path ) ) { return false; } return $config_file_path; } /** ----------------------------------------------------------------------------------------- */ /** PLUGIN FILES ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the paths to all local "plugin" files. If the files don't exist, they are created by fetching their contents remotely, then saved locally. * * @since 3.2 * @access private * @see $this->get_variables() * @author Grégory Viguier * * @param array $variables { * An array of variable values. * * @type string $app_id The app ID. * @type string $version The file version. * } * @return array|bool An array of file paths on success. False on failure. */ private function get_plugin_file_paths( $variables ) { $paths = []; $plugin_names = [ 'identity', 'microdata', 'inferredEvents', 'dwell', 'sessions', 'timespent', 'ga2fbq', ]; foreach ( $plugin_names as $plugin_name ) { $plugin_file_url = sprintf( $this->plugins_file_url, $plugin_name, $variables['version'] ); $plugin_file_name = sprintf( $this->plugins_file_name, $plugin_name . '-' . $variables['version'] ); $plugin_file_path = $this->busting_path . $plugin_file_name; if ( ! $this->maybe_save( $plugin_file_url, $plugin_file_path ) ) { return false; } $paths[] = $plugin_file_path; } return $paths; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get a file contents. If the file doesn't exist or is not writtable, new contents are fetched remotely. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $file_path Path to the file. * @param string $file_url URL to the remote file. * @return string|bool The contents on success, false on failure. */ private function get_file_contents( $file_path, $file_url = false ) { $filesystem = \rocket_direct_filesystem(); if ( $filesystem->is_writable( $file_path ) ) { // If a previous version is present, return its contents. $contents = $filesystem->get_contents( $file_path ); if ( $contents ) { return $contents; } // In case the file is empty or we could not get its contents, try to get a fresh copy from remote location. } if ( ! $file_url ) { return false; } return $this->get_remote_contents( $file_url ); } /** * Get the contents of a URL. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $url The URL to request. * @return string|bool The contents on success. False on failure. */ private function get_remote_contents( $url ) { try { $response = wp_remote_get( $url ); } catch ( \Exception $e ) { Logger::error( 'Remote file could not be fetched.', [ 'fb pixel', 'url' => $url, 'response' => $e->getMessage(), ] ); return false; } if ( is_wp_error( $response ) ) { Logger::error( 'Remote file could not be fetched.', [ 'fb pixel', 'url' => $url, 'response' => $response->get_error_message(), ] ); return false; } $contents = wp_remote_retrieve_body( $response ); if ( ! $contents ) { Logger::error( 'Remote file could not be fetched.', [ 'fb pixel', 'url' => $url, 'response' => $response, ] ); return false; } return $contents; } /** * Escape a file name, to be used in a regex pattern (delimiter is `/`). * `%s` conversion specifications are protected. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $file_name The file name. * @return string */ private function escape_file_name( $file_name ) { $file_name = explode( '%s', $file_name ); $file_name = array_map( 'preg_quote', $file_name ); return implode( '%s', $file_name ); } } deprecated/classes/busting/class-facebook-sdk.php 0000644 00000042536 15174677547 0016161 0 ustar 00 <?php namespace WP_Rocket\Busting; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Logger\Logger; /** * Manages the cache busting of the Facebook SDK file. * * @since 3.9 deprecated. * @since 3.2 * @author Grégory Viguier */ class Facebook_SDK extends Abstract_Busting { use DeprecatedClassTrait; /** * Facebook SDK URL. * %s is a locale like "en_US". * * @var string * @since 3.2 * @access protected * @author Grégory Viguier */ protected $url = 'https://connect.facebook.net/%s/sdk.js'; /** * Filename for the cache busting file. * %s is a locale like "en_US". * * @var string * @since 3.2 * @access protected * @author Grégory Viguier */ protected $filename = 'fbsdk-%s.js'; /** * Flag to track the replacement. * * @var bool * @since 3.2 * @access private * @author Grégory Viguier */ protected $is_replaced = false; /** * Constructor. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $busting_path Path to the busting directory. * @param string $busting_url URL of the busting directory. */ public function __construct( $busting_path, $busting_url ) { self::deprecated_class( '3.9' ); /** Warning: the file name and script URL are dynamic, and must be run through sprintf(). */ $this->busting_path = $busting_path . 'facebook-tracking/'; $this->busting_url = $busting_url . 'facebook-tracking/'; } /** * Perform the URL replacement process. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $html HTML contents. * @return string HTML contents. */ public function replace_url( $html ) { $this->is_replaced = false; $tag = $this->find( '<script[^>]*?>(.*)<\/script>', $html ); if ( ! $tag ) { return $html; } Logger::info( 'FACEBOOK SDK CACHING PROCESS STARTED.', [ 'fb sdk', 'tag' => $tag, ] ); $locale = $this->get_locale_from_url( $tag ); $remote_url = $this->get_url( $locale ); if ( ! $this->save( $remote_url ) ) { return $html; } $file_url = $this->get_busting_file_url( $locale ); $replace_tag = preg_replace( '@(?:https?:)?//connect\.facebook\.net/[a-zA-Z_-]+/sdk\.js@i', $file_url, $tag, -1, $count ); if ( ! $count || false === strpos( $html, $tag ) ) { Logger::error( 'The local file URL could not be replaced in the page contents.', [ 'fb sdk' ] ); return $html; } $html = str_replace( $tag, $replace_tag, $html ); $file_path = $this->get_busting_file_path( $locale ); $xfbml = $this->get_xfbml_from_url( $tag ); // Default value should be set to false. $app_id = $this->get_appId_from_url( $tag ); // APP_ID is the only required value. $url_version = $this->get_version_from_url( $tag ); $version = false === $url_version ? 'v5.0' : $url_version; // If version is not available set it to the latest: v.5.0. if ( false !== $app_id ) { // Add FB async init. $fb_async_script = '<script>window.fbAsyncInit = function fbAsyncInit () {FB.init({appId: \'' . $app_id . '\',xfbml: ' . $xfbml . ',version: \'' . $version . '\'})}</script>'; $html = str_replace( '</body>', $fb_async_script . '</body>', $html ); } $this->is_replaced = true; /** * Triggered once the Facebook SDK URL has been replaced in the page contents. * * @since 3.2 * @author Grégory Viguier * * @param string $file_url URL of the local main file. * @param string $file_path Path to the local file. */ do_action( 'rocket_after_facebook_sdk_url_replaced', $file_url, $file_path ); Logger::info( 'Facebook SDK caching process succeeded.', [ 'fb sdk', 'file' => $file_path, ] ); return $html; } /** * Tell if the replacement was successful or not. * * @since 3.2 * @access public * @author Grégory Viguier * * @return bool */ public function is_replaced() { return $this->is_replaced; } /** ----------------------------------------------------------------------------------------- */ /** GRAB/MANIPULATE DATA IN CONTENTS ======================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Search for an element in the DOM. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $pattern Pattern to match. * @param string $html HTML contents. * @return string|bool The matched HTML on success. False if nothing is found. */ protected function find( $pattern, $html ) { preg_match_all( '/' . $pattern . '/Umsi', $html, $matches, PREG_SET_ORDER ); if ( empty( $matches ) ) { return false; } foreach ( $matches as $match ) { if ( trim( $match[1] ) && preg_match( '@//connect\.facebook\.net/[a-zA-Z_-]+/sdk\.js@i', $match[1] ) ) { return $match[0]; } } return false; } /** ----------------------------------------------------------------------------------------- */ /** UPDATE/SAVE A LOCAL FILE ================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Save the contents of a URL into a local file if it doesn't exist yet. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $url URL to get the contents from. * @return bool True on success. False on failure. */ public function save( $url ) { $locale = $this->get_locale_from_url( $url ); $path = $this->get_busting_file_path( $locale ); if ( \rocket_direct_filesystem()->exists( $path ) ) { // If a previous version is present, keep it. return true; } return $this->refresh_save( $url ); } /** * Save the contents of a URL into a local file. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $url URL to get the contents from. * @return bool True on success. False on failure. */ public function refresh_save( $url ) { $content = $this->get_file_content( $url ); if ( ! $content ) { // Error, we couldn't fetch the file contents. return false; } $locale = $this->get_locale_from_url( $url ); $path = $this->get_busting_file_path( $locale ); return (bool) $this->update_file_contents( $path, $content ); } /** * Add new contents to a file. If the file doesn't exist, it is created. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $file_path Path to the file to update. * @param string $file_contents New contents. * @return string|bool The file contents on success. False on failure. */ private function update_file_contents( $file_path, $file_contents ) { if ( ! \rocket_direct_filesystem()->exists( $this->busting_path ) ) { \rocket_mkdir_p( $this->busting_path ); } if ( ! \rocket_put_content( $file_path, $file_contents ) ) { Logger::error( 'Contents could not be written into file.', [ 'fb sdk', 'path' => $file_path, ] ); return false; } /** * Triggered once a file contents have been updated. * * @since 3.2 * @author Grégory Viguier * * @param string $file_path Path to the file to update. * @param string $file_contents The file contents. */ do_action( 'rocket_after_facebook_sdk_file_updated', $file_path, $file_contents ); return $file_contents; } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC BULK ACTIONS ON LOCAL FILES ====================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Look for existing local files and update their contents if there's a new version available. * Actually, if a more recent version exists on the FB side, it will delete all local files and hit the home page to recreate them. * * @since 3.2 * @access public * @author Grégory Viguier * * @return bool True on success. False on failure. */ public function refresh() { $files = $this->get_files(); if ( ! $files ) { // No files (or there's an error). return false !== $files; } $error_paths = []; $pattern = $this->escape_file_name( $this->filename ); $pattern = sprintf( $pattern, '(?<locale>[a-zA-Z_-]+)' ); foreach ( $files as $file ) { preg_match( '/^' . $pattern . '$/', $file, $matches ); $remote_url = $this->get_url( $matches['locale'] ); if ( ! $this->refresh_save( $remote_url ) ) { $error_paths[] = $this->get_busting_file_path( $matches['locale'] ); } } if ( $error_paths ) { Logger::error( 'Local file(s) could not be updated.', [ 'fb sdk', 'paths' => $error_paths, ] ); } /** * Triggered once all local files have been updated (or not). * * @since 3.2 * @author Grégory Viguier * * @param array $files An array of file names. * @param array $error_paths Paths to the files that couldn't be updated. An empty array if everything is fine. */ do_action( 'rocket_after_facebook_sdk_files_refresh', $files, $error_paths ); return ! $error_paths; } /** * Delete all Facebook SDK busting files. * * @since 3.2 * @access public * @author Grégory Viguier * * @return bool True on success. False on failure. */ public function delete() { $filesystem = \rocket_direct_filesystem(); $files = $this->get_files(); if ( ! $files ) { // No files (or there's an error). return false !== $files; } $error_paths = []; foreach ( $files as $file_name ) { if ( ! $filesystem->delete( $this->busting_path . $file_name, false, 'f' ) ) { $error_paths[] = $this->busting_path . $file_name; } } if ( $error_paths ) { Logger::error( 'Local file(s) could not be deleted.', [ 'fb sdk', 'paths' => $error_paths, ] ); } /** * Triggered once all local files have been deleted (or not). * * @since 3.2 * @author Grégory Viguier * * @param array $files An array of file names. * @param array $error_paths Paths to the files that couldn't be deleted. An empty array if everything is fine. */ do_action( 'rocket_after_facebook_sdk_files_deleted', $files, $error_paths ); return ! $error_paths; } /** ----------------------------------------------------------------------------------------- */ /** SCAN FOR LOCAL FILES ==================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get all cached files in the directory. * * @since 3.2 * @access private * @author Grégory Viguier * * @return array|bool A list of file names. False on failure. */ private function get_files() { $filesystem = \rocket_direct_filesystem(); $dir_path = rtrim( $this->busting_path, '\\/' ); if ( ! $filesystem->exists( $dir_path ) ) { return []; } if ( ! $filesystem->is_writable( $dir_path ) ) { Logger::error( 'Directory is not writable.', [ 'fb sdk', 'path' => $dir_path, ] ); return false; } $dir = $filesystem->dirlist( $dir_path ); if ( false === $dir ) { Logger::error( 'Could not get the directory contents.', [ 'fb sdk', 'path' => $dir_path, ] ); return false; } if ( ! $dir ) { return []; } $list = []; $pattern = $this->escape_file_name( $this->filename ); $pattern = sprintf( $pattern, '[a-zA-Z_-]+' ); foreach ( $dir as $entry ) { if ( 'f' !== $entry['type'] ) { continue; } if ( preg_match( '/^' . $pattern . '$/', $entry['name'], $matches ) ) { $list[ $entry['name'] ] = $entry['name']; } } return $list; } /** ----------------------------------------------------------------------------------------- */ /** REMOTE SDK FILE ========================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the remote Facebook SDK URL. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $locale A locale string, like 'en_US'. * @return string */ public function get_url( $locale ) { return sprintf( $this->url, $locale ); } /** * Extract the locale from a URL to bust. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $url Any string containing the URL to bust. * @return string|bool The locale on success. False on failure. */ private function get_locale_from_url( $url ) { $pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js@i'; if ( ! preg_match( $pattern, $url, $matches ) ) { return false; } return $matches['locale']; } /** * Extract XFBML from a URL to bust. * * @since 3.4.3 * @access private * @author Soponar Cristina * * @param string $url Any string containing the URL to bust. * @return string|bool The XFBML on success. False on failure. */ private function get_xfbml_from_url( $url ) { $pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js#(?:.+&)?xfbml=(?<xfbml>[0-9]+)@i'; if ( ! preg_match( $pattern, $url, $matches ) ) { return false; } return $matches['xfbml']; } /** * Extract appId from a URL to bust. * * @since 3.4.3 * @access private * @author Soponar Cristina * * @param string $url Any string containing the URL to bust. * @return string|bool The appId on success. False on failure. */ private function get_appId_from_url( $url ) { $pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js#(?:.+&)?appId=(?<appId>[0-9]+)@i'; if ( ! preg_match( $pattern, $url, $matches ) ) { return false; } return $matches['appId']; } /** * Extract version from a URL to bust. * * @since 3.4.3 * @access private * @author Soponar Cristina * * @param string $url Any string containing the URL to bust. * @return string|bool The version on success. False on failure. */ private function get_version_from_url( $url ) { $pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js#(?:.+&)?version=(?<version>[a-zA-Z0-9.]+)@i'; if ( ! preg_match( $pattern, $url, $matches ) ) { return false; } return $matches['version']; } /** ----------------------------------------------------------------------------------------- */ /** BUSTING FILE ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the local Facebook SDK URL. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $locale A locale string, like 'en_US'. * @return string */ private function get_busting_file_url( $locale ) { $filename = $this->get_busting_file_name( $locale ); // This filter is documented in inc/functions/minify.php. return apply_filters( 'rocket_js_url', apply_filters( 'rocket_facebook_sdk_url', $this->busting_url . $filename ) ); } /** * Get the local Facebook SDK file name. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $locale A locale string, like 'en_US'. * @return string */ private function get_busting_file_name( $locale ) { return sprintf( $this->filename, $locale ); } /** * Get the local Facebook SDK file path. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $locale A locale string, like 'en_US'. * @return string */ private function get_busting_file_path( $locale ) { return $this->busting_path . $this->get_busting_file_name( $locale ); } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the contents of a URL. * * @since 3.2 * @access protected * @author Grégory Viguier * * @param string $url The URL to request. * @return string|bool The contents on success. False on failure. */ protected function get_file_content( $url ) { try { $response = wp_remote_get( $url ); } catch ( \Exception $e ) { Logger::error( 'Remote file could not be fetched.', [ 'fb sdk', 'url' => $url, 'response' => $e->getMessage(), ] ); return false; } if ( is_wp_error( $response ) ) { Logger::error( 'Remote file could not be fetched.', [ 'fb sdk', 'url' => $url, 'response' => $response->get_error_message(), ] ); return false; } $contents = wp_remote_retrieve_body( $response ); if ( ! $contents ) { Logger::error( 'Remote file could not be fetched.', [ 'fb sdk', 'url' => $url, 'response' => $response, ] ); return false; } return $contents; } /** * Escape a file name, to be used in a regex pattern (delimiter is `/`). * `%s` conversion specifications are protected. * * @since 3.2 * @access private * @author Grégory Viguier * * @param string $file_name The file name. * @return string */ private function escape_file_name( $file_name ) { $file_name = explode( '%s', $file_name ); $file_name = array_map( 'preg_quote', $file_name ); return implode( '%s', $file_name ); } } deprecated/classes/busting/class-abstract-busting.php 0000644 00000005062 15174677547 0017076 0 ustar 00 <?php namespace WP_Rocket\Busting; /** * Abstract class for assets busting * * @since 3.1 * @author Remy Perona */ abstract class Abstract_Busting { /** * Cache busting files base path * * @var string */ protected $busting_path; /** * Cache busting base URL * * @var string */ protected $busting_url; /** * Filename for the cache busting file. * * @var string */ protected $filename; /** * Gets the content of an URL * * @since 3.1 * @author Remy Perona * * @param string $url The URL to request. * @return string|bool */ protected function get_file_content( $url ) { $content = wp_remote_retrieve_body( wp_remote_get( $url ) ); if ( ! $content ) { return false; } return $content; } /** * Saves the content of the URL to bust to the busting file if it doesn't exist yet. * * @since 3.1 * @author Remy Perona * * @param string $url URL to get the content from. * @return bool */ public function save( $url ) { $path = $this->busting_path . $this->filename; if ( \rocket_direct_filesystem()->exists( $path ) ) { return true; } return $this->refresh_save( $url ); } /** * Saves the content of the URL to bust to the busting file. * * @since 3.2 * @access public * @author Grégory Viguier * * @param string $url URL to get the content from. * @return bool */ public function refresh_save( $url ) { $path = $this->busting_path . $this->filename; $content = $this->get_file_content( $url ); if ( ! $content ) { // If a previous version is present, it is kept in place. return false; } if ( ! \rocket_direct_filesystem()->exists( $this->busting_path ) ) { \rocket_mkdir_p( $this->busting_path ); } if ( ! \rocket_put_content( $path, $content ) ) { return false; } return true; } /** * Gets the final URL for the cache busting file. * * @since 3.1 * @author Remy Perona * * @return string */ protected function get_busting_url() { // This filter is documented in inc/functions/minify.php. return apply_filters( 'rocket_js_url', $this->busting_url . $this->filename ); } /** * Performs the replacement process. * * @since 3.1 * @author Remy Perona * * @param string $html HTML content. * @return string */ abstract public function replace_url( $html ); /** * Searches for element(s) in the DOM * * @since 3.1 * @author Remy Perona * * @param string $pattern Pattern to match. * @param string $html HTML content. * @return string */ abstract protected function find( $pattern, $html ); } deprecated/3.5.php 0000644 00000072161 15174677547 0007720 0 ustar 00 <?php // phpcs:ignoreFile defined( 'ABSPATH' ) || exit; /** * Class aliases. */ class_alias( '\WP_Rocket\Engine\Admin\Settings\Page', '\WP_Rocket\Admin\Settings\Page' ); class_alias( '\WP_Rocket\Engine\Admin\Settings\Render', '\WP_Rocket\Admin\Settings\Render' ); class_alias( '\WP_Rocket\Engine\Admin\Settings\Settings', '\WP_Rocket\Admin\Settings\Settings' ); class_alias( '\WP_Rocket\Engine\Admin\Settings\ServiceProvider', '\WP_Rocket\ServiceProvider\Settings' ); class_alias( '\WP_Rocket\Engine\Admin\Settings\Subscriber', '\WP_Rocket\Subscriber\Admin\Settings\Page_Subscriber' ); class_alias( '\WP_Rocket\Engine\Optimization\GoogleFonts\Combine', '\WP_Rocket\Optimization\CSS\Combine_Google_Fonts' ); class_alias( '\WP_Rocket\Engine\Optimization\GoogleFonts\Subscriber', '\WP_Rocket\Subscriber\Optimization\Combine_Google_Fonts_Subscriber' ); //RocketCDN Start class_alias('\WP_Rocket\Engine\CDN\RocketCDN\AdminPageSubscriber', '\WP_Rocket\Subscriber\CDN\RocketCDN\AdminPageSubscriber'); class_alias('\WP_Rocket\Engine\CDN\RocketCDN\APIClient', '\WP_Rocket\CDN\RocketCDN\APIClient'); class_alias('\WP_Rocket\Engine\CDN\RocketCDN\CDNOptionsManager', '\WP_Rocket\CDN\RocketCDN\CDNOptionsManager'); class_alias('\WP_Rocket\Engine\CDN\RocketCDN\DataManagerSubscriber', '\WP_Rocket\Subscriber\CDN\RocketCDN\DataManagerSubscriber'); class_alias('\WP_Rocket\Engine\CDN\RocketCDN\NoticesSubscriber', '\WP_Rocket\Subscriber\CDN\RocketCDN\NoticesSubscriber'); class_alias('\WP_Rocket\Engine\CDN\RocketCDN\RESTSubscriber', '\WP_Rocket\Subscriber\CDN\RocketCDN\RESTSubscriber'); class_alias('\WP_Rocket\Engine\CDN\RocketCDN\ServiceProvider', '\WP_Rocket\ServiceProvider\RocketCDN'); //RocketCDN End /** * Removes Minification, DNS Prefetch, LazyLoad, Defer JS when on an AMP version of a post with the AMP for WordPress plugin from Auttomatic * * @since 2.8.10 Compatibility with wp_resource_hints in WP 4.6 * @since 2.7 * @deprecated 3.5.2 * * @author Remy Perona */ function rocket_disable_options_on_amp() { _deprecated_function( __FUNCTION__ . '()', '3.5.2', '\WP_Rocket\ThirdParty\Plugins\Optimization\AMP::disable_options_on_amp()' ); global $wp_filter; if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) { remove_filter( 'wp_resource_hints', 'rocket_dns_prefetch', 10, 2 ); add_filter( 'do_rocket_lazyload', '__return_false' ); unset( $wp_filter['rocket_buffer'] ); // this filter is documented in inc/front/protocol.php. $do_rocket_protocol_rewrite = apply_filters( 'do_rocket_protocol_rewrite', false ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals if ( ( get_rocket_option( 'do_cloudflare', 0 ) && get_rocket_option( 'cloudflare_protocol_rewrite', 0 ) || $do_rocket_protocol_rewrite ) ) { remove_filter( 'rocket_buffer', 'rocket_protocol_rewrite', PHP_INT_MAX ); remove_filter( 'wp_calculate_image_srcset', 'rocket_protocol_rewrite_srcset', PHP_INT_MAX ); } } } /** * Validate Cloudflare input data * * @since 3.4.1 * @deprecated 3.5 * @author Soponar Cristina * * @param string $cf_email - Cloudflare email. * @param string $cf_api_key - Cloudflare API key. * @param string $cf_zone_id - Cloudflare zone ID. * @param bool $basic_validation - Bypass Cloudflare API user and zone validation. * @return Object - true if credentials are ok, WP_Error otherwise. */ function rocket_is_api_keys_valid_cloudflare( $cf_email, $cf_api_key, $cf_zone_id, $basic_validation = true ) { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::is_api_keys_valid()' ); if ( ! function_exists( 'curl_init' ) || ! function_exists( 'curl_exec' ) ) { return new WP_Error( 'curl_disabled', __( 'Curl is disabled on your server. Please ask your host to enable it. This is required for the Cloudflare Add-on to work correctly.', 'rocket' ) ); } if ( ! isset( $cf_email, $cf_api_key ) || empty( $cf_email ) || empty( $cf_api_key ) ) { return new WP_Error( 'cloudflare_credentials_empty', sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Cloudflare email, API key and Zone ID are not set. Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ) ); } if ( ! isset( $cf_zone_id ) || empty( $cf_zone_id ) ) { $msg = __( 'Missing Cloudflare Zone ID.', 'rocket' ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); return new WP_Error( 'cloudflare_no_zone_id', $msg ); } if ( $basic_validation ) { return true; } try { $cf_api_instance = new Cloudflare\Api( $cf_email, $cf_api_key ); $cf_zone = $cf_api_instance->get( 'zones/' . $cf_zone_id ); if ( ! isset( $cf_zone->success ) || empty( $cf_zone->success ) ) { foreach ( $cf_zone->errors as $error ) { if ( 6003 === $error->code ) { $msg = __( 'Incorrect Cloudflare email address or API key.', 'rocket' ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); return new WP_Error( 'cloudflare_invalid_auth', $msg ); } } $msg = __( 'Incorrect Cloudflare Zone ID.', 'rocket' ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); return new WP_Error( 'cloudflare_invalid_auth', $msg ); } if ( true === $cf_zone->success ) { $zone_found = false; $site_url = get_site_url(); if ( function_exists( 'domain_mapping_siteurl' ) ) { $site_url = domain_mapping_siteurl( $site_url ); } if ( ! empty( $cf_zone->result ) ) { $parsed_url = wp_parse_url( $site_url ); if ( false !== strpos( strtolower( $parsed_url['host'] ), $cf_zone->result->name ) ) { $zone_found = true; } } if ( ! $zone_found ) { $msg = __( 'It looks like your domain is not set up on Cloudflare.', 'rocket' ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); return new WP_Error( 'cloudflare_wrong_zone_id', $msg ); } return true; } } catch ( Exception $e ) { $msg = __( 'Incorrect Cloudflare email address or API key.', 'rocket' ); $msg .= ' ' . sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); return new WP_Error( 'cloudflare_invalid_auth', $msg ); } } /** * Get a Cloudflare\Api instance * * @since 2.8.21 * @deprecated 3.5 * @author Soponar Cristina * * @return Object Cloudflare\Api instance if crendentials are set, WP_Error otherwise */ function get_rocket_cloudflare_api_instance() { _deprecated_function( __FUNCTION__ . '()', '3.5' ); if ( ! function_exists( 'curl_init' ) || ! function_exists( 'curl_exec' ) ) { return new WP_Error( 'curl_disabled', __( 'Curl is disabled on your server. Please ask your host to enable it. This is required for the Cloudflare Add-on to work correctly.', 'rocket' ) ); } $cf_email = get_rocket_option( 'cloudflare_email', null ); $cf_api_key = ( defined( 'WP_ROCKET_CF_API_KEY' ) ) ? WP_ROCKET_CF_API_KEY : get_rocket_option( 'cloudflare_api_key', null ); if ( ! isset( $cf_email, $cf_api_key ) ) { return new WP_Error( 'cloudflare_credentials_empty', sprintf( /* translators: %1$s = opening link; %2$s = closing link */ __( 'Cloudflare email and API key are not set. Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ), // translators: Documentation exists in EN, FR; use localized URL if applicable. '<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ) ); } return new Cloudflare\Api( $cf_email, $cf_api_key ); } /** * Get a Cloudflare\Api instance & the zone_id corresponding to the domain * * @since 2.8.21 Get the zone ID from the options * @since 2.8.18 Add try/catch to prevent fatal error Uncaugh Exception * @since 2.8.16 Update to Cloudflare API v4 * @since 2.5 * @deprecated 3.5 * * @return Object Cloudflare instance & zone_id if credentials are correct, WP_Error otherwise */ function get_rocket_cloudflare_instance() { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::get_instance()' ); $cf_email = get_rocket_option( 'cloudflare_email', null ); $cf_api_key = ( defined( 'WP_ROCKET_CF_API_KEY' ) ) ? WP_ROCKET_CF_API_KEY : get_rocket_option( 'cloudflare_api_key', null ); $cf_zone_id = get_rocket_option( 'cloudflare_zone_id', null ); $is_api_keys_valid_cf = rocket_is_api_keys_valid_cloudflare( $cf_email, $cf_api_key, $cf_zone_id, true ); if ( is_wp_error( $is_api_keys_valid_cf ) ) { return $is_api_keys_valid_cf; } $cf_api_instance = get_rocket_cloudflare_api_instance(); $cf_instance = (object) [ 'auth' => $cf_api_instance, 'zone_id' => $cf_zone_id, ]; return $cf_instance; } /** * Test the connection with Cloudflare * * @since 2.9 * @deprecated 3.5 * @author Remy Perona * * @throws Exception If the connection to Cloudflare failed. * @return Object True if connection is successful, WP_Error otherwise */ function rocket_cloudflare_valid_auth() { _deprecated_function( __FUNCTION__ . '()', '3.5' ); $cf_api_instance = get_rocket_cloudflare_api_instance(); if ( is_wp_error( $cf_api_instance ) ) { return $cf_api_instance; } try { $cf_zone_instance = new Cloudflare\Zone( $cf_api_instance ); $cf_zones = $cf_zone_instance->zones(); if ( ! isset( $cf_zones->success ) || empty( $cf_zones->success ) ) { throw new Exception( __( 'Connection to Cloudflare failed', 'rocket' ) ); } if ( true === $cf_zones->success ) { return true; } } catch ( Exception $e ) { return new WP_Error( 'cloudflare_invalid_auth', $e->getMessage() ); } } /** * Get all the current Cloudflare settings for a given domain. * * @since 2.8.16 Update to Cloudflare API v4 * @since 2.5 * @deprecated 3.5 * * @return mixed bool|Array Array of Cloudflare settings, false if any error connection to Cloudflare */ function get_rocket_cloudflare_settings() { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::get_settings()' ); } /** * Set the Cloudflare Development mode. * * @since 2.9 Now returns a value * @since 2.8.16 Update to Cloudflare API v4 * @since 2.5 * @deprecated 3.5 * * @param string $mode Value for Cloudflare development mode. * @throws Exception If any error occurs when doing the API request. * @return mixed Object|String Mode value if the update is successful, WP_Error otherwise */ function set_rocket_cloudflare_devmode( $mode ) { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::set_devmode()' ); } /** * Set the Cloudflare Caching level. * * @since 2.9 Now returns a value * @since 2.8.16 Update to Cloudflare API v4 * @since 2.5 * @deprecated 3.5 * * @param string $mode Value for Cloudflare caching level. * @throws Exception If any error occurs when doing the API request. * @return mixed Object|String Mode value if the update is successful, WP_Error otherwise */ function set_rocket_cloudflare_cache_level( $mode ) { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::set_cache_level()' ); } /** * Set the Cloudflare Minification. * * @since 2.9 Now returns a value * @since 2.8.16 Update to Cloudflare API v4 * @since 2.5 * @deprecated 3.5 * * @param string $mode Value for Cloudflare minification. * @throws Exception If any error occurs when doing the API request. * @return mixed Object|String Mode value if the update is successful, WP_Error otherwise */ function set_rocket_cloudflare_minify( $mode ) { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::set_minify()' ); } /** * Set the Cloudflare Rocket Loader. * * @since 2.9 Now returns value * @since 2.8.16 Update to Cloudflare API v4 * @since 2.5 * @deprecated 3.5 * * @param string $mode Value for Cloudflare Rocket Loader. * @throws Exception If any error occurs when doing the API request. * @return mixed Object|String Mode value if the update is successful, WP_Error otherwise */ function set_rocket_cloudflare_rocket_loader( $mode ) { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::set_rocket_loader()' ); } /** * Set the Browser Cache TTL in Cloudflare. * * @since 2.9 Now returns value * @since 2.8.16 * @deprecated 3.5 * * @param string $mode Value for Cloudflare browser cache TTL. * @throws Exception If any error occurs when doing the API request. * @return mixed Object|String Mode value if the update is successful, WP_Error otherwise */ function set_rocket_cloudflare_browser_cache_ttl( $mode ) { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::set_browser_cache_ttl()' ); } /** * Purge Cloudflare cache. * * @since 2.9 Now returns value * @since 2.8.16 Update to Cloudflare API v4 * @since 2.5 * @deprecated 3.5 * * @throws Exception If any error occurs when doing the API request. * @return mixed Object|bool true if the purge is successful, WP_Error otherwise */ function rocket_purge_cloudflare() { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::purge_cloudflare()' ); } /** * Get Cloudflare IPs. * * @since 2.8.21 Save IPs in a transient to prevent calling the API everytime * @since 2.8.16 * @deprecated 3.5 * * @author Remy Perona * * @throws Exception If any error occurs when doing the API request. * @return Object Result of API request if successful, WP_Error otherwise */ function rocket_get_cloudflare_ips() { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::get_cloudflare_ips()' ); $cf_instance = get_rocket_cloudflare_api_instance(); if ( is_wp_error( $cf_instance ) ) { return $cf_instance; } $cf_ips = get_transient( 'rocket_cloudflare_ips' ); if ( false === $cf_ips ) { try { $cf_ips_instance = new Cloudflare\IPs( $cf_instance ); $cf_ips = $cf_ips_instance->ips(); if ( ! isset( $cf_ips->success ) || ! $cf_ips->success ) { throw new Exception( 'Error connecting to Cloudflare' ); } set_transient( 'rocket_cloudflare_ips', $cf_ips, 2 * WEEK_IN_SECONDS ); } catch ( Exception $e ) { $cf_ips = (object) [ 'success' => true, 'result' => (object) [], ]; $cf_ips->result->ipv4_cidrs = [ '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '104.16.0.0/12', '108.162.192.0/18', '131.0.72.0/22', '141.101.64.0/18', '162.158.0.0/15', '172.64.0.0/13', '173.245.48.0/20', '188.114.96.0/20', '190.93.240.0/20', '197.234.240.0/22', '198.41.128.0/17', ]; $cf_ips->result->ipv6_cidrs = [ '2400:cb00::/32', '2405:8100::/32', '2405:b500::/32', '2606:4700::/32', '2803:f800::/32', '2c0f:f248::/32', '2a06:98c0::/29', ]; set_transient( 'rocket_cloudflare_ips', $cf_ips, 2 * WEEK_IN_SECONDS ); return $cf_ips; } } return $cf_ips; } /** * Set Real IP from CloudFlare * * @since 2.8.16 Uses CloudFlare API v4 to get CloudFlare IPs * @since 2.5.4 * * @deprecated 3.5 * * @source cloudflare.php - https://wordpress.org/plugins/cloudflare/ */ function rocket_set_real_ip_cloudflare() { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::set_real_ip()' ); global $is_cf; $is_cf = ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) ? true : false; if ( ! $is_cf ) { return; } // only run this logic if the REMOTE_ADDR is populated, to avoid causing notices in CLI mode. if ( isset( $_SERVER['REMOTE_ADDR'] ) ) { $cf_ips_values = rocket_get_cloudflare_ips(); if ( is_wp_error( $cf_ips_values ) || ! isset( $cf_ips_values->success ) || ! $cf_ips_values->success ) { return; } if ( strpos( $_SERVER['REMOTE_ADDR'], ':' ) === false ) { $cf_ip_ranges = $cf_ips_values->result->ipv4_cidrs; // IPV4: Update the REMOTE_ADDR value if the current REMOTE_ADDR value is in the specified range. foreach ( $cf_ip_ranges as $range ) { if ( rocket_ipv4_in_range( $_SERVER['REMOTE_ADDR'], $range ) ) { if ( $_SERVER['HTTP_CF_CONNECTING_IP'] ) { $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP']; } break; } } } else { $cf_ip_ranges = $cf_ips_values->result->ipv6_cidrs; $ipv6 = get_rocket_ipv6_full( $_SERVER['REMOTE_ADDR'] ); foreach ( $cf_ip_ranges as $range ) { if ( rocket_ipv6_in_range( $ipv6, $range ) ) { if ( $_SERVER['HTTP_CF_CONNECTING_IP'] ) { $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP']; } break; } } } } // Let people know that the CF WP plugin is turned on. if ( ! headers_sent() ) { header( 'X-CF-Powered-By: WP Rocket ' . WP_ROCKET_VERSION ); } } /** * This notice is displayed after purging the CloudFlare cache * * @since 2.9 * @author Remy Perona * * @deprecated 3.5 * */ function rocket_cloudflare_purge_result() { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::maybe_print_notice()' ); global $current_user; if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) { return; } if ( ! is_admin() ) { return; } $notice = get_transient( $current_user->ID . '_cloudflare_purge_result' ); if ( ! $notice ) { return; } delete_transient( $current_user->ID . '_cloudflare_purge_result' ); rocket_notice_html( [ 'status' => $notice['result'], 'message' => $notice['message'], ] ); } /** * Purge CloudFlare cache * * @since 2.5 * * @deprecated 3.5 * */ function do_admin_post_rocket_purge_cloudflare() { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::do_purge_cloudflare()' ); if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'rocket_purge_cloudflare' ) ) { wp_nonce_ays( '' ); } if ( ! current_user_can( 'rocket_purge_cloudflare_cache' ) ) { return; } // Purge CloudFlare. $cf_purge = rocket_purge_cloudflare(); if ( is_wp_error( $cf_purge ) ) { $cf_purge_result = [ 'result' => 'error', // translators: %s = CloudFare API return message. 'message' => sprintf( __( '<strong>WP Rocket:</strong> %s', 'rocket' ), $cf_purge->get_error_message() ), ]; } else { $cf_purge_result = [ 'result' => 'success', 'message' => __( '<strong>WP Rocket:</strong> Cloudflare cache successfully purged.', 'rocket' ), ]; } set_transient( get_current_user_id() . '_cloudflare_purge_result', $cf_purge_result ); wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); die(); } /** * This notice is displayed after modifying the CloudFlare settings * * @since 2.9 * @author Remy Perona * * @deprecated 3.5 */ function rocket_cloudflare_update_settings() { _deprecated_function( __FUNCTION__ . '()', '3.5', '\WP_Rocket\Subscriber\Tools\Cloudflare_Subscriber::maybe_print_update_settings_notice()' ); global $current_user; $screen = get_current_screen(); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } $notices = get_transient( $current_user->ID . '_cloudflare_update_settings' ); if ( $notices ) { $errors = ''; $success = ''; delete_transient( $current_user->ID . '_cloudflare_update_settings' ); foreach ( $notices as $notice ) { if ( 'error' === $notice['result'] ) { $errors .= $notice['message'] . '<br>'; } elseif ( 'success' === $notice['result'] ) { $success .= $notice['message'] . '<br>'; } } if ( ! empty( $success ) ) { rocket_notice_html( [ 'message' => $success, ] ); } if ( ! empty( $errors ) ) { rocket_notice_html( [ 'status' => 'error', 'message' => $success, ] ); } } } /** * Purge all the domain * * @since 2.6.8 * @deprecated 3.5 * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. * @param string $url The home url. */ function rocket_varnish_clean_domain( $root, $lang, $url ) { _deprecated_function( __FUNCTION__ . '()', '3.5', 'WP_Rocket\Subscriber\Addons\Varnish\VarnishSubscriber::clean_domain()' ); rocket_varnish_http_purge( trailingslashit( $url ) . '?vregex' ); } /** * Purge a specific page * * @since 2.6.8 * @deprecated 3.5 * * @param string $url The url to purge. */ function rocket_varnish_clean_file( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.5', 'WP_Rocket\Subscriber\Addons\Varnish\VarnishSubscriber::clean_file()' ); rocket_varnish_http_purge( trailingslashit( $url ) . '?vregex' ); } /** * Purge the homepage and its pagination * * @since 2.6.8 * @deprecated 3.5 * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. */ function rocket_varnish_clean_home( $root, $lang ) { _deprecated_function( __FUNCTION__ . '()', '3.5', 'WP_Rocket\Subscriber\Addons\Varnish\VarnishSubscriber::clean_home()' ); $home_url = trailingslashit( get_rocket_i18n_home_url( $lang ) ); $home_pagination_url = $home_url . trailingslashit( $GLOBALS['wp_rewrite']->pagination_base ) . '?vregex'; rocket_varnish_http_purge( $home_url ); rocket_varnish_http_purge( $home_pagination_url ); } /** * Sets the Varnish IP to localhost if Cloudflare is active * * @since 3.3.5 * @deprecated 3.5 * @author Remy Perona * * @return string */ function rocket_varnish_proxy_host() { _deprecated_function( __FUNCTION__ . '()', '3.5', 'WP_Rocket\Subscriber\Addons\Cloudflare\CloudflareSubscriber::set_varnish_localhost()' ); return 'localhost'; } /** * Sets the Host header to the website domain if Cloudflare is active * * @since 3.3.5 * @deprecated 3.5 * @author Remy Perona * * @return string */ function rocket_varnish_proxy_request_host() { _deprecated_function( __FUNCTION__ . '()', '3.5', 'WP_Rocket\Subscriber\Addons\Cloudflare\CloudflareSubscriber::set_varnish_purge_request_host()' ); return wp_parse_url( home_url(), PHP_URL_HOST ); } /** * Send data to Varnish * * @since 2.6.8 * @deprecated 3.5 * * @param string $url The URL to purge. * @return void */ function rocket_varnish_http_purge( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.5', 'WP_Rocket\Addons\Varnish\Varnish::purge()' ); $parse_url = get_rocket_parse_url( $url ); $varnish_x_purgemethod = 'default'; $regex = ''; if ( 'vregex' === $parse_url['query'] ) { $varnish_x_purgemethod = 'regex'; $regex = '.*'; } /** * Filter the Varnish IP to call * * @since 2.6.8 * @param string The Varnish IP */ $varnish_ip = apply_filters( 'rocket_varnish_ip', '' ); if ( defined( 'WP_ROCKET_VARNISH_IP' ) && ! $varnish_ip ) { $varnish_ip = WP_ROCKET_VARNISH_IP; } /** * Filter the HTTP protocol (scheme) * * @since 2.7.3 * @param string The HTTP protocol */ $scheme = apply_filters( 'rocket_varnish_http_purge_scheme', 'http' ); $parse_url['host'] = ( $varnish_ip ) ? $varnish_ip : $parse_url['host']; $purgeme = $scheme . '://' . $parse_url['host'] . $parse_url['path'] . $regex; wp_remote_request( $purgeme, array( 'method' => 'PURGE', 'blocking' => false, 'redirection' => 0, /** * Filters the headers to send with the Varnish purge request * * @since 3.1 * @author Remy Perona * * @param array $headers Headers to send. */ 'headers' => apply_filters( 'rocket_varnish_purge_headers', [ /** * Filters the host value passed in the request headers * * @since 2.8.15 * @param string The host */ 'host' => apply_filters( 'rocket_varnish_purge_request_host', $parse_url['host'] ), 'X-Purge-Method' => $varnish_x_purgemethod, ] ), ) ); } /** * Display a warning notice if WP Rocket scheduled events are not running properly * * @since 3.5.4 deprecated * @since 3.3.7 * @author Remy Perona * * @return void */ function rocket_warning_cron() { _deprecated_function( __FUNCTION__ . '()', '3.5.4', 'WP_Rocket\Engine\Admin\HealthCheck::missed_cron()' ); $screen = get_current_screen(); // This filter is documented in inc/admin-bar.php. if ( ! current_user_can( apply_filters( 'rocket_capacity', 'manage_options' ) ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } if ( 0 === (int) get_rocket_option( 'purge_cron_interval' ) && 0 === get_rocket_option( 'async_css' ) && 0 === get_rocket_option( 'manual_preload' ) && 0 === get_rocket_option( 'schedule_automatic_cleanup' ) ) { return; } $events = [ 'rocket_purge_time_event' => 'Scheduled Cache Purge', 'rocket_database_optimization_time_event' => 'Scheduled Database Optimization', 'rocket_database_optimization_cron_interval' => 'Database Optimization Process', 'rocket_preload_cron_interval' => 'Preload', 'rocket_critical_css_generation_cron_interval' => 'Critical Path CSS Generation Process', ]; foreach ( $events as $event => $description ) { $timestamp = wp_next_scheduled( $event ); if ( false === $timestamp ) { unset( $events[ $event ] ); continue; } if ( $timestamp - time() > 0 ) { unset( $events[ $event ] ); continue; } } if ( empty( $events ) ) { return; } $message = '<p>' . _n( 'The following scheduled event failed to run. This may indicate the CRON system is not running properly, which can prevent some WP Rocket features from working as intended:', 'The following scheduled events failed to run. This may indicate the CRON system is not running properly, which can prevent some WP Rocket features from working as intended:', count( $events ), 'rocket' ) . '</p>'; $message .= '<ul>'; foreach ( $events as $description ) { $message .= '<li>' . $description . '</li>'; } $message .= '</ul>'; $message .= '<p>' . __( 'Please contact your host to check if CRON is working.', 'rocket' ) . '</p>'; rocket_notice_html( [ 'status' => 'warning', 'dismissible' => '', 'message' => $message, 'dismiss_button' => __FUNCTION__, ] ); } /** * Add a link "Purge this cache" in the taxonomy edit area * * @since 3.5.5 deprecated * @since 1.0 * * @param array $actions An array of row action links. * @param object $term The term object. * @return array Updated array of row action links */ function rocket_tag_row_actions( $actions, $term ) { _deprecated_function( __FUNCTION__ . '()', '3.5.5', 'WP_Rocket\Engine\Cache\AdminSubscriber::add_purge_term_link()' ); global $taxnow; if ( ! current_user_can( 'rocket_purge_terms' ) ) { return $actions; } $url = wp_nonce_url( admin_url( 'admin-post.php?action=purge_cache&type=term-' . $term->term_id . '&taxonomy=' . $taxnow ), 'purge_cache_term-' . $term->term_id ); $actions['rocket_purge'] = sprintf( '<a href="%s">%s</a>', $url, __( 'Clear this cache', 'rocket' ) ); return $actions; } deprecated/3.8.php 0000644 00000023700 15174677547 0007716 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; class_alias( '\WP_Rocket\Engine\Cache\PurgeExpired\PurgeExpiredCache', '\WP_Rocket\Cache\Expired_Cache_Purge'); class_alias( '\WP_Rocket\Engine\Cache\PurgeExpired\Subscriber', '\WP_Rocket\Subscriber\Cache\Expired_Cache_Purge_Subscriber'); class_alias( '\WP_Rocket\Engine\Media\Lazyload\Subscriber', '\WP_Rocket\Engine\Media\LazyloadSubscriber'); if ( ! class_exists( 'WP_Rocket\Subscriber\Optimization\Dequeue_JQuery_Migrate_Subscriber' ) ) { require_once __DIR__ . '/subscriber/Optimization/class-dequeue-jquery-migrate-subscriber.php'; } /** * Deactivate WP Rocket lazyload if Avada lazyload is enabled * * @since 3.8.1 deprecated * @since 3.3.4 * * @param string $old_value Previous Avada option value. * @param string $value New Avada option value. * @return void */ function rocket_avada_maybe_deactivate_lazyload( $old_value, $value ) { _deprecated_function( __FUNCTION__ . '()', '3.8.1', 'WP_Rocket\ThirdParty\Themes\Avada::maybe_deactivate_lazyload()' ); if ( empty( $old_value['lazy_load'] ) || ( ! empty( $value['lazy_load'] ) && 'avada' === $value['lazy_load'] ) ) { update_rocket_option( 'lazyload', 0 ); } } /** * Disable WP Rocket lazyload field if Avada lazyload is enabled * * @since 3.8.1 deprecated * @since 3.3.4 * * @return bool */ function rocket_avada_maybe_disable_lazyload() { _deprecated_function( __FUNCTION__ . '()', '3.8.1', 'WP_Rocket\ThirdParty\Themes\Avada::maybe_disable_lazyload()' ); $avada_options = get_option( 'fusion_options' ); $current_theme = wp_get_theme(); if ( 'Avada' !== $current_theme->get( 'Name' ) ) { return false; } if ( empty( $avada_options['lazy_load'] ) ) { return false; } if ( ! empty( $avada_options['lazy_load'] && 'avada' !== $avada_options['lazy_load'] ) ) { return false; } return true; } /** * Clears WP Rocket's cache after Avada's Fusion Patcher flushes their caches * * @since 3.8.1 deprecated * @since 3.3.5 * * @return void */ function rocket_avada_clear_cache_fusion_patcher() { _deprecated_function( __FUNCTION__ . '()', '3.8.1', 'WP_Rocket\ThirdParty\Themes\Avada::clear_cache_fusion_patcher()' ); rocket_clean_domain(); } /** * Defer all JS files. * * @since 3.8 deprecated * @since 2.10 * @author Remy Perona * * @param string $buffer HTML content. * @return string Updated HTML content */ function rocket_defer_js( $buffer ) { _deprecated_function( __FUNCTION__ . '()', '3.8', 'WP_Rocket\Engine\Optimization\DeferJS\DeferJS::defer_js()' ); if ( ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) || ( defined( 'DONOTASYNCCSS' ) && DONOTASYNCCSS ) ) { return; } if ( ! get_rocket_option( 'defer_all_js' ) ) { return $buffer; } if ( is_rocket_post_excluded_option( 'defer_all_js' ) ) { return $buffer; } $buffer_nocomments = preg_replace( '/<!--(.*)-->/Uis', '', $buffer ); // Get all JS files with this regex. preg_match_all( '#<script\s+([^>]+[\s\'"])?src\s*=\s*[\'"]\s*?([^\'"]+\.js(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>#iU', $buffer_nocomments, $tags_match ); if ( ! isset( $tags_match[0] ) ) { return $buffer; } $exclude_defer_js = implode( '|', get_rocket_exclude_defer_js() ); foreach ( $tags_match[0] as $i => $tag ) { // Check if this file should be deferred. if ( preg_match( '#(' . $exclude_defer_js . ')#i', $tags_match[2][ $i ] ) ) { continue; } // Don't add defer if already async. if ( false !== strpos( $tags_match[1][ $i ], 'async' ) || false !== strpos( $tags_match[3][ $i ], 'async' ) ) { continue; } // Don't add defer if already defer. if ( false !== strpos( $tags_match[1][ $i ], 'defer' ) || false !== strpos( $tags_match[3][ $i ], 'defer' ) ) { continue; } $deferred_tag = str_replace( '>', ' defer>', $tag ); $buffer = str_replace( $tag, $deferred_tag, $buffer ); } return $buffer; } /** * Get list of JS files to be excluded from defer JS. * * @since 3.8 deprecated * @since 2.10 * @author Remy Perona * * @return array An array of URLs for the JS files to be excluded. */ function get_rocket_exclude_defer_js() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__ . '()', '3.8', 'WP_Rocket\Engine\Optimization\DeferJS\DeferJS::get_excluded()' ); $exclude_defer_js = [ 'gist.github.com', 'content.jwplatform.com', 'js.hsforms.net', 'www.uplaunch.com', 'google.com/recaptcha', 'widget.reviews.co.uk', 'verify.authorize.net/anetseal', 'lib/admin/assets/lib/webfont/webfont.min.js', 'app.mailerlite.com', 'widget.reviews.io', 'simplybook.(.*)/v2/widget/widget.js', '/wp-includes/js/dist/i18n.min.js', '/wp-content/plugins/wpfront-notification-bar/js/wpfront-notification-bar(.*).js', '/wp-content/plugins/oxygen/component-framework/vendor/aos/aos.js', 'static.mailerlite.com/data/(.*).js', 'cdn.voxpow.com/static/libs/v1/(.*).js', 'cdn.voxpow.com/media/trackers/js/(.*).js', ]; if ( get_rocket_option( 'defer_all_js', 0 ) && get_rocket_option( 'defer_all_js_safe', 0 ) ) { $jquery = site_url( wp_scripts()->registered['jquery-core']->src ); $jetpack_jquery = 'c0.wp.com/c/(?:.+)/wp-includes/js/jquery/jquery.js'; $googleapis_jquery = 'ajax.googleapis.com/ajax/libs/jquery/(?:.+)/jquery(?:\.min)?.js'; $cdnjs_jquery = 'cdnjs.cloudflare.com/ajax/libs/jquery/(?:.+)/jquery(?:\.min)?.js'; $code_jquery = 'code.jquery.com/jquery-.*(?:\.min|slim)?.js'; $exclude_defer_js[] = rocket_clean_exclude_file( $jquery ); $exclude_defer_js[] = $jetpack_jquery; $exclude_defer_js[] = $googleapis_jquery; $exclude_defer_js[] = $cdnjs_jquery; $exclude_defer_js[] = $code_jquery; } /** * Filter list of Deferred JavaScript files * * @since 2.10 * @author Remy Perona * * @param array $exclude_defer_js An array of URLs for the JS files to be excluded. */ $exclude_defer_js = apply_filters( 'rocket_exclude_defer_js', $exclude_defer_js ); foreach ( $exclude_defer_js as $i => $exclude ) { $exclude_defer_js[ $i ] = str_replace( '#', '\#', $exclude ); } return $exclude_defer_js; } /** * Add width and height attributes on all images * * @since 3.8 deprecated * @since 2.2.2 This feature is enabled by a hook * @since 1.3.0 This process is called via the new filter rocket_buffer * @since 1.3.0 It's possible to not specify dimensions of an image with data-no-image-dimensions attribute * @since 1.1.2 Fix Bug : No conflict with Photon Plugin (Jetpack) * @since 1.1.0 * * @param string $buffer HTML content. * @return string Modified HTML content */ function rocket_specify_image_dimensions( $buffer ) { _deprecated_function( __FUNCTION__ . '()', '3.8', 'WP_Rocket\Engine\Media\ImagesSubscriber::specify_image_dimensions()' ); /** * Filter images dimensions attributes * * @since 2.2 * * @param bool Do the job or not. */ if ( ! apply_filters( 'rocket_specify_image_dimensions', false ) ) { return $buffer; } // Get all images without width or height attribute. preg_match_all( '/<img(?:[^>](?!(height|width)=))*+>/i', $buffer, $images_match ); foreach ( $images_match[0] as $image ) { // Don't touch lazy-load file (no conflict with Photon (Jetpack)). if ( strpos( $image, 'data-lazy-original' ) || strpos( $image, 'data-no-image-dimensions' ) ) { continue; } $tmp = $image; // Get link of the file. preg_match( '/src=[\'"]([^\'"]+)/', $image, $src_match ); // Get infos of the URL. $image_url = wp_parse_url( $src_match[1] ); // Check if the link isn't external. if ( empty( $image_url['host'] ) || rocket_remove_url_protocol( home_url() ) === $image_url['host'] ) { // Get image attributes. $sizes = getimagesize( ABSPATH . $image_url['path'] ); } else { /** * Filter distant images dimensions attributes * * @since 2.2 * * @param bool Do the job or not */ if ( ini_get( 'allow_url_fopen' ) && apply_filters( 'rocket_specify_image_dimensions_for_distant', false ) ) { // Get image attributes. $sizes = getimagesize( $image_url['scheme'] . '://' . $image_url['host'] . $image_url['path'] ); } } if ( ! empty( $sizes ) ) { // Add width and width attribute. $image = str_replace( '<img', '<img ' . $sizes[3], $image ); // Replace image with new attributes. $buffer = str_replace( $tmp, $image, $buffer ); } } return $buffer; } /** * Conflict with LayerSlider: don't add width and height attributes on all images * * @since 3.8 deprecated * @since 2.1 */ function rocket_deactivate_specify_image_dimensions_with_layerslider() { _deprecated_function( __FUNCTION__ . '()', '3.8', 'WP_Rocket\ThirdParty\Plugins\Slider\LayerSlider::get_subscribed_events()' ); remove_filter( 'rocket_buffer', 'rocket_specify_image_dimensions' ); } /** * Add age-verified to the list of mandatory cookies * * @since 3.8.6 deprecated * @since 2.7 * * @param Array $cookies Array of mandatory cookies. * @return Array Updated array of mandatory cookies */ function rocket_add_cache_mandatory_cookie_for_age_verify( $cookies ) { _deprecated_function( __FUNCTION__ . '()', '3.8.6' ); $cookies[] = 'age-verified'; return $cookies; } /** * Add age-verified cookie when we activate the plugin * * @since 3.8.6 deprecated * @since 2.7 */ function rocket_activate_age_verify() { _deprecated_function( __FUNCTION__ . '()', '3.8.6' ); add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 18 ); add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_cache_mandatory_cookie_for_age_verify' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } /** * Remove age-verified cookie when we deactivate the plugin * * @since 3.8.6 deprecated * @since 2.7 */ function rocket_deactivate_age_verify() { _deprecated_function( __FUNCTION__ . '()', '3.8.6' ); remove_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_cache_mandatory_cookie_for_age_verify' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } deprecated/subscriber/Optimization/class-dequeue-jquery-migrate-subscriber.php 0000644 00000003364 15174677547 0024107 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Optimization; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\Options_Data as Options; use WP_Scripts; /** * Dequeue jQuery Migrate * * @since 3.5 * @author Soponar Cristina */ class Dequeue_JQuery_Migrate_Subscriber implements Subscriber_Interface { use DeprecatedClassTrait; /** * Plugin options * * @since 3.5 * @author Soponar Cristina * * @var Options */ private $options; /** * Constructor * * @since 3.5 * @author Soponar Cristina * * @param Options $options Plugin options. */ public function __construct( Options $options ) { self::deprecated_class( '3.8' ); $this->options = $options; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'wp_default_scripts' => [ 'dequeue_jquery_migrate' ], ]; } /** * Dequeue jquery migrate * * @since 3.5 * @author Soponar Cristina * * @param WP_Scripts $scripts WP_Scripts instance. * @return bool|void */ public function dequeue_jquery_migrate( $scripts ) { if ( ! $this->is_allowed() ) { return false; } if ( ! empty( $scripts->registered['jquery'] ) ) { $jquery_dependencies = $scripts->registered['jquery']->deps; $scripts->registered['jquery']->deps = array_diff( $jquery_dependencies, [ 'jquery-migrate' ] ); } } /** * Check if dequeue jquery migrate option is enabled * * @since 3.5 * @author Soponar Cristina * * @return boolean */ protected function is_allowed() { if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE', false ) ) { return false; } if ( ! $this->options->get( 'dequeue_jquery_migrate' ) ) { return false; } return true; } } deprecated/subscriber/admin/Optimization/class-minify-html-subscriber.php 0000644 00000004276 15174677547 0023037 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Optimization; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\Options_Data as Options; use WP_Rocket\Dependencies\Minify; /** * HTML minification subscriber * * @since 3.1 * @author Remy Perona */ class Minify_HTML_Subscriber implements Subscriber_Interface { use DeprecatedClassTrait; /** * Plugin options * * @since 3.1 * @author Remy Perona * * @var Options */ private $options; /** * Constructor * * @since 3.1 * @author Remy Perona * * @param Options $options Plugin options. */ public function __construct( Options $options ) { self::deprecated_class( '3.7' ); $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.1 * @author Remy Perona * * @return array */ public static function get_subscribed_events() { return [ 'rocket_buffer' => [ 'process', 14 ], ]; } /** * Minifies HTML * * @since 3.1 * @author Remy Perona * * @param string $html HTML content. * @return string */ public function process( $html ) { if ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $html; } if ( ! $this->options->get( 'minify_html' ) || \is_rocket_post_excluded_option( 'minify_html' ) ) { return $html; } $html_options = [ 'cssMinifier' => [ $this, 'minify_inline_css' ], ]; /** * Filter options of minify inline HTML * * @since 1.1.12 * * @param array $html_options Options of minify inline HTML. */ $html_options = apply_filters( 'rocket_minify_html_options', $html_options ); return \Minify_HTML::minify( $html, $html_options ); } /** * Minifies inline CSS * * @since 1.1.6 * * @param string $css HTML content. * @return string */ public function minify_inline_css( $css ) { $minify = new Minify\CSS( $css ); return $minify->minify(); } /** * Minifies inline JavaScript * * @since 1.1.6 * * @param string $javascript HTML content. * @return string */ public function minify_inline_js( $javascript ) { $minify = new Minify\JS( $javascript ); return $minify->minify(); } } deprecated/subscriber/admin/Settings/class-beacon-subscriber.php 0000644 00000002275 15174677547 0021140 0 ustar 00 <?php namespace WP_Rocket\Subscriber\Admin\Settings; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\Settings\Beacon; /** * Beacon Subscriber to WordPress * * @since 3.2 * @author Remy Perona */ class Beacon_Subscriber implements Subscriber_Interface { use DeprecatedClassTrait; /** * Beacon instance * * @var Beacon */ private $beacon; /** * Constructor * * @param Beacon $beacon Beacon instance. */ public function __construct( Beacon $beacon ) { self::deprecated_class( '3.6' ); $this->beacon = $beacon; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.2 * @author Remy Perona * * @return array */ public static function get_subscribed_events() { return [ 'admin_print_footer_scripts-settings_page_wprocket' => 'insert_script', ]; } /** * Insert HelpScout Beacon script * * @since 3.0 * @author Remy Perona * * @return void */ public function insert_script() { echo $this->beacon->insert_script(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } } deprecated/3.11.php 0000644 00000017317 15174677547 0007777 0 ustar 00 <?php /** * Add Yoast SEO sitemap option to WP Rocket default options * * @since 2.8 * @since 3.11.1 deprecated * * @author Remy Perona * * @param array $options WP Rocket options array. * @return array Updated WP Rocket options array */ function rocket_add_yoast_seo_sitemap_option( $options ) { _deprecated_function( __FUNCTION__ . '()', '3.11.1' ); $options['yoast_xml_sitemap'] = 0; return $options; } /** * Sanitize Yoast SEO sitemap option value * * @since 2.8 * @since 3.11.1 deprecated * * @author Remy Perona * * @param array $inputs WP Rocket inputs array. * @return array Sanitized WP Rocket inputs array */ function rocket_yoast_seo_sitemap_option_sanitize( $inputs ) { _deprecated_function( __FUNCTION__ . '()', '3.11.1' ); $inputs['yoast_xml_sitemap'] = ! empty( $inputs['yoast_xml_sitemap'] ) ? 1 : 0; return $inputs; } /** * Add Yoast SEO sitemap URL to the sitemaps to preload * * @since 2.8 * @since 3.11.1 deprecated * * @author Remy Perona * * @param array $sitemaps Sitemaps to preload. * @return array Updated Sitemaps to preload */ function rocket_add_yoast_seo_sitemap( $sitemaps ) { _deprecated_function( __FUNCTION__ . '()', '3.11.1' ); if ( get_rocket_option( 'yoast_xml_sitemap', false ) ) { $sitemaps[] = WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ); } return $sitemaps; } /** * Add Yoast SEO option to WP Rocket settings * * @since 2.8 * @since 3.11.1 deprecated * * @author Remy Perona * * @param array $options WP Rocket settings array. * @return array Updated WP Rocket settings array */ function rocket_sitemap_preload_yoast_seo_option( $options ) { _deprecated_function( __FUNCTION__ . '()', '3.11.1' ); $options['yoast_xml_sitemap'] = [ 'type' => 'checkbox', 'container_class' => [ 'wpr-field--children', ], 'label' => __( 'Yoast SEO XML sitemap', 'rocket' ), // translators: %s = Name of the plugin. 'description' => sprintf( __( 'We automatically detected the sitemap generated by the %s plugin. You can check the option to preload it.', 'rocket' ), 'Yoast SEO' ), 'parent' => 'sitemap_preload', 'section' => 'preload_section', 'page' => 'preload', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ]; return $options; } /** * Clear Kinsta cache when clearing WP Rocket cache * * @since 3.0 * @author Remy Perona * * @return void */ function rocket_clean_kinsta_cache() { global $kinsta_cache; _deprecated_function( __FUNCTION__ . '()', '3.11.1' ); if ( ! empty( $kinsta_cache->kinsta_cache_purge ) ) { $kinsta_cache->kinsta_cache_purge->purge_complete_caches(); } } /** * Partially clear Kinsta cache when partially clearing WP Rocket cache * * @since 3.0 * @author Remy Perona * * @param object $post Post object. * @return void */ function rocket_clean_kinsta_post_cache( $post ) { _deprecated_function( __FUNCTION__ . '()', '3.11.1' ); global $kinsta_cache; $kinsta_cache->kinsta_cache_purge->initiate_purge( $post->ID, 'post' ); } /** * Clears Kinsta cache for the homepage URL when using "Purge this URL" from the admin bar on the front end * * @since 3.0.4 * @author Remy Perona * * @param string $root WP Rocket root cache path. * @param string $lang Current language. * @return void */ function rocket_clean_kinsta_cache_home( $root = '', $lang = '' ) { _deprecated_function( __FUNCTION__ . '()', '3.11.1' ); $url = get_rocket_i18n_home_url( $lang ); $url = trailingslashit( $url ) . 'kinsta-clear-cache/'; wp_remote_get( $url, [ 'blocking' => false, 'timeout' => 0.01, ] ); } /** * Clears Kinsta cache for a specific URL when using "Purge this URL" from the admin bar on the front end * * @since 3.0.4 * @author Remy Perona * * @param string $url URL to purge. * @return void */ function rocket_clean_kinsta_cache_url( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.11.1' ); $url = trailingslashit( $url ) . 'kinsta-clear-cache/'; wp_remote_get( $url, [ 'blocking' => false, 'timeout' => 0.01, ] ); } /** * Remove WP Rocket functions on WP core action hooks to prevent triggering a double cache clear. * * @since 3.0 * @author Remy Perona * * @return void */ function rocket_remove_partial_purge_hooks() { _deprecated_function( __FUNCTION__ . '()', '3.11.1' ); // WP core action hooks rocket_clean_post() gets hooked into. $clean_post_hooks = [ // Disables the refreshing of partial cache when content is edited. 'wp_trash_post', 'delete_post', 'clean_post_cache', 'wp_update_comment_count', ]; // Remove rocket_clean_post() from core action hooks. array_map( function( $hook ) { remove_action( $hook, 'rocket_clean_post' ); }, $clean_post_hooks ); remove_filter( 'rocket_clean_files', 'rocket_clean_files_users' ); } /** * Do the rollback * * @since 3.11.5 deprecated * @since 2.4 */ function rocket_rollback() { _deprecated_function( __FUNCTION__ . '()', '3.11.5' ); if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_rollback' ) ) { wp_nonce_ays( '' ); } /** * Fires before doing the rollback */ do_action( 'rocket_before_rollback' ); $plugin_transient = get_site_transient( 'update_plugins' ); $plugin_folder = plugin_basename( dirname( WP_ROCKET_FILE ) ); $plugin = $plugin_folder . '/' . basename( WP_ROCKET_FILE ); $plugin_transient->response[ $plugin ] = (object) [ 'slug' => $plugin_folder, 'new_version' => WP_ROCKET_LASTVERSION, 'url' => 'https://wp-rocket.me', 'package' => sprintf( 'https://wp-rocket.me/%s/wp-rocket_%s.zip', get_rocket_option( 'consumer_key' ), WP_ROCKET_LASTVERSION ), ]; set_site_transient( 'update_plugins', $plugin_transient ); require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // translators: %s is the plugin name. $title = sprintf( __( '%s Update Rollback', 'rocket' ), WP_ROCKET_PLUGIN_NAME ); $nonce = 'upgrade-plugin_' . $plugin; $url = 'update.php?action=upgrade-plugin&plugin=' . rawurlencode( $plugin ); $upgrader_skin = new Plugin_Upgrader_Skin( compact( 'title', 'nonce', 'url', 'plugin' ) ); $upgrader = new Plugin_Upgrader( $upgrader_skin ); remove_filter( 'site_transient_update_plugins', 'rocket_check_update', 1 ); add_filter( 'update_plugin_complete_actions', 'rocket_rollback_add_return_link' ); rocket_put_content( WP_CONTENT_DIR . '/advanced-cache.php', '' ); $upgrader->upgrade( $plugin ); wp_die( '', // translators: %s is the plugin name. esc_html( sprintf( __( '%s Update Rollback', 'rocket' ), WP_ROCKET_PLUGIN_NAME ) ), [ 'response' => 200, ] ); } /** * After a rollback has been done, replace the "return to" link by a link pointing to WP Rocket's tools page. * A link to the plugins page is kept in case the plugin is not reactivated correctly. * * @since 3.11.5 deprecated * @since 3.2.4 * @author Grégory Viguier * @author Arun Basil Lal * * @param array $update_actions Array of plugin action links. * @return array The array of links where the "return to" link has been replaced. */ function rocket_rollback_add_return_link( $update_actions ) { _deprecated_function( __FUNCTION__ . '()', '3.11.5' ); if ( ! isset( $update_actions['plugins_page'] ) ) { return $update_actions; } $update_actions['plugins_page'] = sprintf( /* translators: 1 and 3 are link openings, 2 is a link closing. */ __( '%1$sReturn to WP Rocket%2$s or %3$sgo to Plugins page%2$s', 'rocket' ), '<a href="' . esc_url( admin_url( 'options-general.php?page=' . WP_ROCKET_PLUGIN_SLUG ) . '#tools' ) . '" target="_parent">', '</a>', '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '" target="_parent">' ); return $update_actions; } deprecated/3.7.php 0000644 00000020665 15174677547 0007724 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Require deprecated classes. */ if ( ! class_exists( 'Minify_HTML' ) ) { require_once __DIR__ . '/vendors/classes/class-minify-html.php'; } if ( ! class_exists( 'WP_Rocket\Subscriber\Optimization\Minify_HTML_Subscriber' ) ) { require_once __DIR__ . '/subscriber/admin/Optimization/class-minify-html-subscriber.php'; } class_alias( '\WP_Rocket\Engine\Heartbeat\HeartbeatSubscriber', '\WP_Rocket\Subscriber\Heartbeat_Subscriber' ); /** * Conflict with WP Serveur hosting: don't apply inline JS on all pages. * * @since 3.7 deprecated * @since 2.6.11 * * @param array $html_options WP Rocket options array. * * @return array Updated WP Rocket options array. */ function rocket_deactivate_inline_js_on_wp_serveur( $html_options ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( isset( $html_options['jsMinifier'] ) ) { unset( $html_options['jsMinifier'] ); } return $html_options; } /** * Conflict with AppBanners: don't minify inline script when HTML minification is activated * * @since 3.7 deprecated * @since 2.2.4 * * @param array $html_options An array of WP Rocket options. * * @return array Array without the inline js minify option */ function rocket_deactivate_js_minifier_with_appbanner( $html_options ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( isset( $html_options['jsMinifier'] ) && class_exists( 'AppBanners' ) ) { unset( $html_options['jsMinifier'] ); } return $html_options; } /** * Deactivate WP Rocket HTML Minification if Autoptimize HTML minification is enabled * * @since 3.7 deprecated * @since 2.9.5 * * @param string $old_value Previous autoptimize option value. * @param string $value New autoptimize option value. * * @author Remy Perona * */ function rocket_maybe_deactivate_minify_html( $old_value, $value ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( $value !== $old_value && 'on' === $value ) { update_rocket_option( 'minify_html', 0 ); } } /** * Disable WP Rocket HTML minification field if Autoptimize HTML minification is enabled * * @since 3.7 deprecated * @since 2.9.5 * @return bool|null True if it is active * @author Remy Perona * */ function rocket_maybe_disable_minify_html() { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( is_plugin_active( 'autoptimize/autoptimize.php' ) && 'on' === get_option( 'autoptimize_html' ) ) { return true; } } /** * Conflict with Revolution Slider: don't minify inline script when HTML minification is activated * * @since 3.7 deprecated * @since 2.6.8 * * @param array $html_options WP Rocket options array. * * @return array Updated WP Rocket options */ function rocket_deactivate_js_minifier_with_revslider( $html_options ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( isset( $html_options['jsMinifier'] ) && class_exists( 'RevSliderFront' ) ) { unset( $html_options['jsMinifier'] ); } return $html_options; } /** * Disable the emoji functionality to reduce then number of external HTTP requests. * * @since 3.7 Deprecated. * @since 2.7 * * @deprecated */ function rocket_disable_emoji() { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( rocket_bypass() ) { return; } remove_action( 'wp_head', 'print_emoji_detection_script', 7 ); remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); remove_filter( 'the_content_feed', 'wp_staticize_emoji' ); remove_filter( 'comment_text_rss', 'wp_staticize_emoji' ); remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' ); add_filter( 'emoji_svg_url', '__return_false' ); } /** * Remove the tinymce emoji plugin. * * @since 3.7 Deprecated. * @since 2.7 * * @param array $plugins Plugins loaded for TinyMCE. * * @return array * * @deprecated */ function rocket_disable_emoji_tinymce( $plugins ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( is_array( $plugins ) ) { return array_diff( $plugins, [ 'wpemoji' ] ); } return []; } /** * Disable embeds on init. * * - Removes the needed query vars. * - Disables oEmbed discovery. * - Completely removes the related JavaScript. * * @since 3.7 Deprecated. * @since 2.10 * * @deprecated */ function rocket_disable_embeds_init() { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( rocket_bypass() ) { return; } global $wp; // Remove the embed query var. $wp->public_query_vars = array_diff( $wp->public_query_vars, [ 'embed', ] ); // Remove the oembed/1.0/embed REST route. add_filter( 'rest_endpoints', 'rocket_disable_embeds_remove_embed_endpoint' ); // Disable handling of internal embeds in oembed/1.0/proxy REST route. add_filter( 'oembed_response_data', 'rocket_disable_embeds_filter_oembed_response_data' ); // Turn off oEmbed auto discovery. add_filter( 'embed_oembed_discover', '__return_false' ); // Don't filter oEmbed results. remove_filter( 'oembed_dataparse', 'wp_filter_oembed_result', 10 ); // Remove oEmbed discovery links. remove_action( 'wp_head', 'wp_oembed_add_discovery_links' ); // Remove oEmbed-specific JavaScript from the front-end and back-end. remove_action( 'wp_head', 'wp_oembed_add_host_js' ); add_filter( 'tiny_mce_plugins', 'rocket_disable_embeds_tiny_mce_plugin' ); // Remove all embeds rewrite rules. add_filter( 'rewrite_rules_array', 'rocket_disable_embeds_rewrites' ); // Remove filter of the oEmbed result before any HTTP requests are made. remove_filter( 'pre_oembed_result', 'wp_filter_pre_oembed_result', 10 ); // Load block editor JavaScript. add_action( 'enqueue_block_editor_assets', 'rocket_disable_embeds_enqueue_block_editor_assets' ); // Remove wp-embed dependency of wp-edit-post script handle. add_action( 'wp_default_scripts', 'rocket_disable_embeds_remove_script_dependencies' ); } /** * Removes the 'wpembed' TinyMCE plugin. * * @since 3.7 Deprecated. * @since 2.10 * * @param array $plugins List of TinyMCE plugins. * * @return array The modified list. * * @deprecated */ function rocket_disable_embeds_tiny_mce_plugin( $plugins ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); return array_diff( $plugins, [ 'wpembed' ] ); } /** * Remove all rewrite rules related to embeds. * * @since 3.7 Deprecated. * @since 2.10 * * @param array $rules WordPress rewrite rules. * * @return array Rewrite rules without embeds rules. * * @deprecated */ function rocket_disable_embeds_rewrites( $rules ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( empty( $rules ) ) { return $rules; } foreach ( $rules as $rule => $rewrite ) { if ( false !== strpos( $rewrite, 'embed=true' ) ) { unset( $rules[ $rule ] ); } } return $rules; } /** * Removes the oembed/1.0/embed REST route. * * @since 3.6 Deprecated. * @since 3.3.3 * * @param array $endpoints Registered REST API endpoints. * * @return array Filtered REST API endpoints. * * @deprecated */ function rocket_disable_embeds_remove_embed_endpoint( $endpoints ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); unset( $endpoints['/oembed/1.0/embed'] ); return $endpoints; } /** * Disables sending internal oEmbed response data in proxy endpoint. * * @since 3.7 Deprecated. * @since 3.3.3 * * @param array $data The response data. * * @return array|false Response data or false if in a REST API context. * * @deprecated */ function rocket_disable_embeds_filter_oembed_response_data( $data ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return false; } return $data; } /** * Enqueues JavaScript for the block editor. * * This is used to unregister the `core-embed/wordpress` block type. * * @since 3.7 Deprecated. * @since 3.3.3 * * @deprecated */ function rocket_disable_embeds_enqueue_block_editor_assets() { _deprecated_function( __FUNCTION__ . '()', '3.7' ); wp_enqueue_script( 'rocket-disable-embeds', WP_ROCKET_ASSETS_JS_URL . 'editor/editor.js', [ 'wp-edit-post', 'wp-editor', 'wp-dom', ], WP_ROCKET_VERSION, true ); } /** * Removes wp-embed dependency of core packages. * * @since 3.7 deprecated * @since 3.3.3 * * @param \WP_Scripts $scripts WP_Scripts instance, passed by reference. * * @deprecated */ function rocket_disable_embeds_remove_script_dependencies( $scripts ) { _deprecated_function( __FUNCTION__ . '()', '3.7' ); if ( ! empty( $scripts->registered['wp-edit-post'] ) ) { $scripts->registered['wp-edit-post']->deps = array_diff( $scripts->registered['wp-edit-post']->deps, [ 'wp-embed' ] ); } } deprecated/3.14.php 0000644 00000031503 15174677547 0007773 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Remove HTTP protocol on script, link, img and form tags. * * @since 2.7 * @deprecated 3.14 * * @param string $buffer HTML content. * @return string Updated HTML content */ function rocket_protocol_rewrite( $buffer ) { _deprecated_function( __FUNCTION__, '3.14' ); $re = "/(<(script|link|img|form)([^>]*)(href|src|action)=[\"'])https?:\\/\\//i"; $subst = '$1//'; $return = preg_replace( $re, $subst, $buffer ); if ( $return ) { $buffer = $return; } return $buffer; } /** * Remove HTTP protocol on srcset attribute generated by WordPress * * @since 2.7 * @deprecated 3.14 * * @param array $sources an Array of images sources for srcset. * @return array Updated array of images sources */ function rocket_protocol_rewrite_srcset( $sources ) { _deprecated_function( __FUNCTION__, '3.14' ); if ( (bool) $sources ) { foreach ( $sources as $i => $source ) { $sources[ $i ]['url'] = str_replace( [ 'http:', 'https:' ], '', $source['url'] ); } } return $sources; } /** * Check if request is from Cloudflare * * @since 3.4.1 * @author Soponar Cristina * * @return bool */ function rocket_is_cloudflare() { _deprecated_function( __FUNCTION__, '3.14' ); if ( ! isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) { return false; } // Check if original ip has already been restored, e.g. by nginx - assume it was from cloudflare then. if ( isset( $_SERVER['REMOTE_ADDR'] ) && $_SERVER['REMOTE_ADDR'] === $_SERVER['HTTP_CF_CONNECTING_IP'] ) { return true; } return rocket_is_cf_ip(); } /** * Check if a request comes from a CloudFlare IP. * * @since 3.4.1 * @author Soponar Cristina * * @return bool */ function rocket_is_cf_ip() { _deprecated_function( __FUNCTION__, '3.14' ); // Store original remote address in $original_ip. $original_ip = filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP ); if ( ! isset( $original_ip ) ) { return false; } $cf_ips_values = get_transient( 'rocket_cloudflare_ips' ); // Cloudflare IPS should always be populated because the code runs before loading Cloudflare addon. if ( false === $cf_ips_values ) { $cf_ips_values = (object) [ 'success' => true, 'result' => (object) [], ]; $cf_ips_values->result->ipv4_cidrs = [ '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '104.16.0.0/12', '108.162.192.0/18', '131.0.72.0/22', '141.101.64.0/18', '162.158.0.0/15', '172.64.0.0/13', '173.245.48.0/20', '188.114.96.0/20', '190.93.240.0/20', '197.234.240.0/22', '198.41.128.0/17', ]; $cf_ips_values->result->ipv6_cidrs = [ '2400:cb00::/32', '2405:8100::/32', '2405:b500::/32', '2606:4700::/32', '2803:f800::/32', '2c0f:f248::/32', '2a06:98c0::/29', ]; } if ( strpos( $original_ip, ':' ) === false ) { $cf_ip_ranges = $cf_ips_values->result->ipv4_cidrs; foreach ( $cf_ip_ranges as $range ) { if ( rocket_ipv4_in_range( $original_ip, $range ) ) { return true; } } } else { $cf_ip_ranges = $cf_ips_values->result->ipv6_cidrs; $ipv6 = get_rocket_ipv6_full( $original_ip ); foreach ( $cf_ip_ranges as $range ) { if ( rocket_ipv6_in_range( $ipv6, $range ) ) { return true; } } } return false; } /** * Fixes Cloudflare Flexible SSL redirect loop * * @since 3.4.1 * @author Soponar Cristina */ function rocket_fix_cf_flexible_ssl() { _deprecated_function( __FUNCTION__, '3.14' ); $is_cf = rocket_is_cloudflare(); if ( $is_cf ) { // Fixes Flexible SSL. if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) { $_SERVER['HTTPS'] = 'on'; } } } /* * ip_in_range.php - Function to determine if an IP is located in a * specific range as specified via several alternative * formats. * * Network ranges can be specified as: * 1. Wildcard format: 1.2.3.* * 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0 * 3. Start-End IP format: 1.2.3.0-1.2.3.255 * * Return value BOOLEAN : ip_in_range($ip, $range); * * Copyright 2008: Paul Gregg <pgregg@pgregg.com> * 10 January 2008 * Version: 1.2 * * Source website: http://www.pgregg.com/projects/php/ip_in_range/ * Version 1.2 * * This software is Donationware - if you feel you have benefited from * the use of this tool then please consider a donation. The value of * which is entirely left up to your discretion. * http://www.pgregg.com/donate/ * * Please do not remove this header, or source attibution from this file. */ /* * Modified by James Greene <james@cloudflare.com> to include IPV6 support * (original version only supported IPV4). * 21 May 2012 */ // In order to simplify working with IP addresses (in binary) and their // netmasks, it is easier to ensure that the binary strings are padded // with zeros out to 32 characters - IP addresses are 32 bit numbers function rocket_decbin32($dec) { _deprecated_function( __FUNCTION__, '3.14' ); return str_pad(decbin($dec), 32, '0', STR_PAD_LEFT); } // This function takes 2 arguments, an IP address and a "range" in several // different formats. // Network ranges can be specified as: // 1. Wildcard format: 1.2.3.* // 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0 // 3. Start-End IP format: 1.2.3.0-1.2.3.255 // The function will return true if the supplied IP is within the range. // Note little validation is done on the range inputs - it expects you to // use one of the above 3 formats. function rocket_ipv4_in_range($ip, $range) { _deprecated_function( __FUNCTION__, '3.14' ); if (strpos($range, '/') !== false) { // $range is in IP/NETMASK format list($range, $netmask) = explode('/', $range, 2); if (strpos($netmask, '.') !== false) { // $netmask is a 255.255.0.0 format $netmask = str_replace('*', '0', $netmask); $netmask_dec = ip2long($netmask); return ( (ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec) ); } else { // $netmask is a CIDR size block // fix the range argument $x = explode('.', $range); while(count($x)<4) $x[] = '0'; list($a,$b,$c,$d) = $x; $range = sprintf("%u.%u.%u.%u", empty($a)?'0':$a, empty($b)?'0':$b,empty($c)?'0':$c,empty($d)?'0':$d); $range_dec = ip2long($range); $ip_dec = ip2long($ip); # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0')); # Strategy 2 - Use math to create it $wildcard_dec = pow(2, (32-$netmask)) - 1; $netmask_dec = ~ $wildcard_dec; return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec)); } } else { // range might be 255.255.*.* or 1.2.3.0-1.2.3.255 if (strpos($range, '*') !==false) { // a.b.*.* format // Just convert to A-B format by setting * to 0 for A and 255 for B $lower = str_replace('*', '0', $range); $upper = str_replace('*', '255', $range); $range = "$lower-$upper"; } if (strpos($range, '-')!==false) { // A-B format list($lower, $upper) = explode('-', $range, 2); $lower_dec = (float)sprintf("%u",ip2long($lower)); $upper_dec = (float)sprintf("%u",ip2long($upper)); $ip_dec = (float)sprintf("%u",ip2long($ip)); return ( ($ip_dec>=$lower_dec) && ($ip_dec<=$upper_dec) ); } return false; } } function rocket_ip2long6($ip) { _deprecated_function( __FUNCTION__, '3.14' ); if (substr_count($ip, '::')) { $ip = str_replace('::', str_repeat(':0000', 8 - substr_count($ip, ':')) . ':', $ip); } $ip = explode(':', $ip); $r_ip = ''; foreach ($ip as $v) { $r_ip .= str_pad( base_convert( preg_replace("/[^0-9a-fA-F]/", "", $v ), 16, 2 ), 16, 0, STR_PAD_LEFT ); } return base_convert($r_ip, 2, 10); } // Get the ipv6 full format and return it as a decimal value. function get_rocket_ipv6_full($ip) { _deprecated_function( __FUNCTION__, '3.14' ); $pieces = explode ("/", $ip, 2); $left_piece = $pieces[0]; $right_piece = null; if (count($pieces) > 1) $right_piece = $pieces[1]; // Extract out the main IP pieces $ip_pieces = explode("::", $left_piece, 2); $main_ip_piece = $ip_pieces[0]; $last_ip_piece = ""; if (count($ip_pieces) > 1) $last_ip_piece = $ip_pieces[1]; // Pad out the shorthand entries. $main_ip_pieces = explode(":", $main_ip_piece); foreach($main_ip_pieces as $key=>$val) { $main_ip_pieces[$key] = str_pad($main_ip_pieces[$key], 4, "0", STR_PAD_LEFT); } // Check to see if the last IP block (part after ::) is set $last_piece = ""; $size = count($main_ip_pieces); if (trim($last_ip_piece) != "") { $last_piece = str_pad($last_ip_piece, 4, "0", STR_PAD_LEFT); // Build the full form of the IPV6 address considering the last IP block set for ($i = $size; $i < 7; $i++) { $main_ip_pieces[$i] = "0000"; } $main_ip_pieces[7] = $last_piece; } else { // Build the full form of the IPV6 address for ($i = $size; $i < 8; $i++) { $main_ip_pieces[$i] = "0000"; } } // Rebuild the final long form IPV6 address $final_ip = implode(":", $main_ip_pieces); return rocket_ip2long6($final_ip); } // Determine whether the IPV6 address is within range. // $ip is the IPV6 address in decimal format to check if its within the IP range created by the cloudflare IPV6 address, $range_ip. // $ip and $range_ip are converted to full IPV6 format. // Returns true if the IPV6 address, $ip, is within the range from $range_ip. False otherwise. function rocket_ipv6_in_range($ip, $range_ip) { _deprecated_function( __FUNCTION__, '3.14' ); $pieces = explode ("/", $range_ip, 2); $left_piece = $pieces[0]; $right_piece = $pieces[1]; // Extract out the main IP pieces $ip_pieces = explode("::", $left_piece, 2); $main_ip_piece = $ip_pieces[0]; $last_ip_piece = $ip_pieces[1]; // Pad out the shorthand entries. $main_ip_pieces = explode(":", $main_ip_piece); foreach($main_ip_pieces as $key=>$val) { $main_ip_pieces[$key] = str_pad($main_ip_pieces[$key], 4, "0", STR_PAD_LEFT); } // Create the first and last pieces that will denote the IPV6 range. $first = $main_ip_pieces; $last = $main_ip_pieces; // Check to see if the last IP block (part after ::) is set $last_piece = ""; $size = count($main_ip_pieces); if (trim($last_ip_piece) != "") { $last_piece = str_pad($last_ip_piece, 4, "0", STR_PAD_LEFT); // Build the full form of the IPV6 address considering the last IP block set for ($i = $size; $i < 7; $i++) { $first[$i] = "0000"; $last[$i] = "ffff"; } $main_ip_pieces[7] = $last_piece; } else { // Build the full form of the IPV6 address for ($i = $size; $i < 8; $i++) { $first[$i] = "0000"; $last[$i] = "ffff"; } } // Rebuild the final long form IPV6 address $first = rocket_ip2long6(implode(":", $first)); $last = rocket_ip2long6(implode(":", $last)); $in_range = ($ip >= $first && $ip <= $last); return $in_range; } /** * Filter plugin fetching API results to inject Imagify * * @since 2.10.7 * @since 3.14.2 deprecated * @author Remy Perona * * @param object|WP_Error $result Response object or WP_Error. * @param string $action The type of information being requested from the Plugin Install API. * @param object $args Plugin API arguments. * * @return array Updated array of results */ function rocket_add_imagify_api_result( $result, $action, $args ) { if ( empty( $args->browse ) ) { return $result; } if ( 'featured' !== $args->browse && 'recommended' !== $args->browse && 'popular' !== $args->browse ) { return $result; } if ( ! isset( $result->info['page'] ) || 1 < $result->info['page'] ) { return $result; } if ( is_plugin_active( 'imagify/imagify.php' ) || is_plugin_active_for_network( 'imagify/imagify.php' ) ) { return $result; } // grab all slugs from the api results. $result_slugs = wp_list_pluck( $result->plugins, 'slug' ); if ( in_array( 'imagify', $result_slugs, true ) ) { return $result; } $query_args = [ 'slug' => 'imagify', 'fields' => [ 'icons' => true, 'active_installs' => true, 'short_description' => true, 'group' => true, ], ]; $imagify_data = plugins_api( 'plugin_information', $query_args ); if ( is_wp_error( $imagify_data ) ) { return $result; } if ( 'featured' === $args->browse ) { array_push( $result->plugins, $imagify_data ); } else { array_unshift( $result->plugins, $imagify_data ); } return $result; } deprecated/DeprecatedClassTrait.php 0000644 00000005546 15174677547 0013450 0 ustar 00 <?php namespace WP_Rocket\deprecated; /** * Trait to use in a deprecated class, or a class containing deprecated methods. */ trait DeprecatedClassTrait { /** * Marks a class as deprecated and informs when it has been used. * Similar to _deprecated_constructor(), but with different strings. * The current behavior is to trigger a user error if `WP_DEBUG` is true. * * @since 3.6 * * @param string $version The version of WordPress that deprecated the class. * @param string $replacement Optional. The method that should have been called. Default null. */ private static function deprecated_class( $version, $replacement = null ) { /** * Fires when a deprecated class is called. * * @since 3.6 * * @param string $class The class containing the deprecated constructor. * @param string $version The version of WordPress that deprecated the class. * @param string $replacement Optional. The method that should have been called. */ do_action( 'rocket_deprecated_class_run', static::class, $version, $replacement ); if ( ! WP_DEBUG ) { return; } /** * Filters whether to trigger an error for deprecated classes. * `WP_DEBUG` must be true in addition to the filter evaluating to true. * * @since 3.6 * * @param bool $trigger Whether to trigger the error for deprecated classes. Default true. */ if ( ! apply_filters( 'rocket_deprecated_class_trigger_error', true ) ) { return; } if ( function_exists( '__' ) ) { if ( ! empty( $replacement ) ) { /** * With replacement. */ $message = sprintf( /* translators: 1: PHP class name, 2: version number, 3: replacement class name. */ __( 'The called class %1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.', 'rocket' ), '<code>' . static::class . '</code>', '<strong>' . $version . '</strong>', '<code>' . $replacement . '</code>' ); } else { /** * Without replacement. */ $message = sprintf( /* translators: 1: PHP class name, 2: version number. */ __( 'The called class %1$s is <strong>deprecated</strong> since version %2$s!', 'rocket' ), '<code>' . static::class . '</code>', '<strong>' . $version . '</strong>' ); } } elseif ( ! empty( $replacement ) ) { /** * With replacement. */ $message = sprintf( 'The called class %1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.', '<code>' . static::class . '</code>', '<strong>' . $version . '</strong>', '<code>' . $replacement . '</code>' ); } else { /** * Without replacement. */ $message = sprintf( 'The called class %1$s is <strong>deprecated</strong> since version %2$s!', '<code>' . static::class . '</code>', '<strong>' . $version . '</strong>' ); } call_user_func( 'trigger_error', $message ); } } deprecated/3.4.php 0000644 00000043216 15174677547 0007716 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Get Zones linked to a Cloudflare account * * @since 2.9 * @deprecated 3.4.1.2 * @author Remy Perona * * @return Array List of zones or default no domain */ function get_rocket_cloudflare_zones() { _deprecated_function( __FUNCTION__ . '()', '3.4.1.2' ); $cf_api_instance = get_rocket_cloudflare_api_instance(); $domains = array( '' => __( 'Choose a domain from the list', 'rocket' ), ); if ( is_wp_error( $cf_api_instance ) ) { return $domains; } try { $cf_zone_instance = new Cloudflare\Zone( $cf_api_instance ); $cf_zones = $cf_zone_instance->zones( null, 'active', null, 50 ); $cf_zones_list = $cf_zones->result; if ( ! (bool) $cf_zones_list ) { $domains[] = __( 'No domain available in your Cloudflare account', 'rocket' ); return $domains; } foreach ( $cf_zones_list as $cf_zone ) { $domains[ $cf_zone->name ] = $cf_zone->name; } return $domains; } catch ( Exception $e ) { return $domains; } } /** * Get CNAMES hosts * * @since 2.3 * @deprecated 3.4 * * @param string $zones CNAMES zones. * @return array $hosts CNAMES hosts */ function get_rocket_cnames_host( $zones = array( 'all' ) ) { _deprecated_function( __FUNCTION__ . '()', '3.4', '\WP_Rocket\Subscriber\CDN\CDNSubscriber::get_cdn_hosts()' ); $hosts = array(); $cnames = get_rocket_cdn_cnames( $zones ); if ( $cnames ) { foreach ( $cnames as $cname ) { $cname = rocket_add_url_protocol( $cname ); $hosts[] = rocket_extract_url_component( $cname, PHP_URL_HOST ); } } return $hosts; } /** * Apply CDN on CSS properties (background, background-image, @import, src:url (fonts)) * * @since 2.6 * @since 3.4 * * @param string $buffer file content. * @return string modified file content */ function rocket_cdn_css_properties( $buffer ) { _deprecated_function( __FUNCTION__ . '()', '3.4', '\WP_Rocket\Subscriber\CDN\CDN::rewrite_css_properties()' ); $zone = array( 'all', 'images', 'css_and_js', 'css', ); $cnames = get_rocket_cdn_cnames( $zone ); /** * Filters the application of the CDN on CSS properties * * @since 2.6 * * @param bool true to apply CDN to properties, false otherwise */ $do_rocket_cdn_css_properties = apply_filters( 'do_rocket_cdn_css_properties', true ); if ( ! get_rocket_option( 'cdn' ) || ! $cnames || ! $do_rocket_cdn_css_properties ) { return $buffer; } preg_match_all( '/url\((?![\'"]?data)([^\)]+)\)/i', $buffer, $matches ); if ( is_array( $matches ) ) { $i = 0; foreach ( $matches[1] as $url ) { $url = trim( $url, " \t\n\r\0\x0B\"'" ); /** * Filters the URL of the CSS property * * @since 2.8 * * @param string $url URL of the CSS property */ $url = get_rocket_cdn_url( apply_filters( 'rocket_cdn_css_properties_url', $url ), $zone ); $property = str_replace( $matches[1][ $i ], $url, $matches[0][ $i ] ); $buffer = str_replace( $matches[0][ $i ], $property, $buffer ); $i++; } } return $buffer; } /** * Apply CDN on custom data attributes. * * @since 2.5.5 * @deprecated 3.4 * * @param string $html Original Output. * @return string $html Output that will be printed */ function rocket_add_cdn_on_custom_attr( $html ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( preg_match( '/(data-lazy-src|data-lazyload|data-src|data-retina)=[\'"]?([^\'"\s>]+)[\'"]/i', $html, $matches ) ) { $html = str_replace( $matches[2], get_rocket_cdn_url( $matches[2], array( 'all', 'images' ) ), $html ); } return $html; } /** * Replace URL by CDN of all thumbnails and smilies. * * @since 2.1 * @deprecated 3.4 * * @param string $url URL of the file to replace the domain with the CDN. * @return string modified URL */ function rocket_cdn_file( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $url; } if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { return $url; } $ext = pathinfo( $url, PATHINFO_EXTENSION ); if ( is_admin() || 'php' === $ext ) { return $url; } $filter = current_filter(); $rejected_files = get_rocket_cdn_reject_files(); if ( 'template_directory_uri' === $filter && ! empty( $rejected_files ) ) { return $url; } switch ( $filter ) { case 'wp_get_attachment_url': case 'wp_calculate_image_srcset': $zone = array( 'all', 'images' ); break; case 'smilies_src': $zone = array( 'all', 'images' ); break; case 'stylesheet_uri': case 'wp_minify_css_url': case 'wp_minify_js_url': case 'bwp_get_minify_src': $zone = array( 'all', 'css_and_js', $ext ); break; default: $zone = array( 'all', $ext ); break; } $cnames = get_rocket_cdn_cnames( $zone ); if ( $cnames ) { $url = get_rocket_cdn_url( $url, $zone ); } return $url; } /** * Replace URL by CDN of images displayed using wp_get_attachment_image_src * * @since 2.9.2 * @deprecated 3.4 * @author Remy Perona * @source https://github.com/wp-media/wp-rocket/issues/271#issuecomment-269849927 * * @param array $image An array containing the src, width and height of the image. * @return array Array with updated src URL */ function rocket_cdn_attachment_image_src( $image ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $image; } if ( ! (bool) $image ) { return $image; } if ( is_admin() || is_preview() || is_feed() ) { return $image; } $zones = array( 'all', 'images' ); if ( ! (bool) get_rocket_cdn_cnames( $zones ) ) { return $image; } $image[0] = get_rocket_cdn_url( $image[0], $zones ); return $image; } /** * Replace srcset URLs by CDN URLs for WP responsive images * * @since WP 4.4 * @since 2.6.14 * @deprecated 3.4 * @author Remy Perona * * @param array $sources multidimensional array containing srcset images urls. * @return array $sources */ function rocket_add_cdn_on_srcset( $sources ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $sources; } if ( (bool) $sources ) { foreach ( $sources as $width => $data ) { $sources[ $width ]['url'] = rocket_cdn_file( $data['url'] ); } } return $sources; } /** * Replace URL by CDN of all images display in a post content or a widget text. * * @since 2.1 * @deprecated 3.4 * * @param string $html HTML content to parse. * @return string modified HTML content */ function rocket_cdn_images( $html ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); // Don't use CDN if the image is in admin, a feed or in a post preview. if ( is_admin() || is_feed() || is_preview() || empty( $html ) || defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $html; } $zone = array( 'all', 'images' ); $cnames = get_rocket_cdn_cnames( $zone ); if ( $cnames ) { $cnames = array_flip( $cnames ); $wp_content_dirname = wp_parse_url( content_url(), PHP_URL_PATH ); $custom_media_uploads_dirname = ''; $uploads_info = wp_upload_dir(); if ( ! empty( $uploads_info['baseurl'] ) ) { $custom_media_uploads_dirname = '|' . trailingslashit( wp_parse_url( $uploads_info['baseurl'], PHP_URL_PATH ) ); } // Get all images of the content. preg_match_all( '#<img([^>]+?)src=([\'"\\\]*)([^\'"\s\\\>]+)([\'"\\\]*)([^>]*)>#i', $html, $images_match ); foreach ( $images_match[3] as $k => $image_url ) { $parse_url = get_rocket_parse_url( $image_url ); $path = trim( $parse_url['path'] ); $host = $parse_url['host']; if ( empty( $path ) || ! preg_match( '#(' . $wp_content_dirname . $custom_media_uploads_dirname . '|wp-includes)#', $path ) ) { continue; } if ( isset( $cnames[ $host ] ) ) { continue; } // Image path is relative, apply the host to it. if ( empty( $host ) ) { $image_url = home_url( '/' ) . ltrim( $image_url, '/' ); $host = rocket_extract_url_component( $image_url, PHP_URL_HOST ); } // Check if the link isn't external. if ( rocket_extract_url_component( home_url(), PHP_URL_HOST ) !== $host ) { continue; } // Check if the URL isn't a DATA-URI. if ( false !== strpos( $image_url, 'data:image' ) ) { continue; } $html = str_replace( $images_match[0][ $k ], /** * Filter the image HTML output with the CDN link * * @since 2.5.5 * * @param array $html Output that will be printed. */ apply_filters( 'rocket_cdn_images_html', sprintf( '<img %1$s %2$s %3$s>', trim( $images_match[1][ $k ] ), 'src=' . $images_match[2][ $k ] . get_rocket_cdn_url( $image_url, $zone ) . $images_match[4][ $k ], trim( $images_match[5][ $k ] ) ) ), $html ); } } return $html; } /** * Replace URL by CDN of all inline styles containing url() * * @since 2.9 * @deprecated 3.4 * @author Remy Perona * * @param string $html HTML content of the page. * @return string modified HTML content */ function rocket_cdn_inline_styles( $html ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( is_preview() || empty( $html ) || defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $html; } $zone = array( 'all', 'images', ); $cnames = get_rocket_cdn_cnames( $zone ); if ( $cnames ) { preg_match_all( '/url\((?![\'\"]?data)[\"\']?([^\)\"\']+)[\"\']?\)/i', $html, $matches ); if ( (bool) $matches ) { foreach ( $matches[1] as $k => $url ) { $url = str_replace( array( ' ', '\t', '\n', '\r', '\0', '\x0B', '"', "'", '"', ''' ), '', $url ); if ( '#' === substr( $url, 0, 1 ) ) { continue; } $url = get_rocket_cdn_url( $url, $zone ); $property = str_replace( $matches[1][ $k ], $url, $matches[0][ $k ] ); $html = str_replace( $matches[0][ $k ], $property, $html ); } } } return $html; } /** * Replace URL by CDN for custom files * * @since 2.9 * @deprecated 3.4 * @author Remy Perona * * @param string $html HTML content of the page. * @return string modified HTML content */ function rocket_cdn_custom_files( $html ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( is_preview() || empty( $html ) || defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $html; } $image_types = [ 'jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp', 'bmp', 'tiff', ]; $other_types = [ 'mp3', 'ogg', 'mp4', 'm4v', 'avi', 'mov', 'flv', 'swf', 'webm', 'pdf', 'doc', 'docx', 'txt', 'zip', 'tar', 'bz2', 'tgz', 'rar', ]; $zones = array_filter( array_unique( get_rocket_option( 'cdn_zone', [] ) ) ); if ( empty( $zones ) ) { return $html; } if ( ! in_array( 'all', $zones, true ) && ! in_array( 'images', $zones, true ) ) { return $html; } $cdn_zones = []; $file_types = []; if ( in_array( 'images', $zones, true ) ) { $cdn_zones[] = 'images'; $file_types = array_merge( $file_types, $image_types ); } if ( in_array( 'all', $zones, true ) ) { $cdn_zones[] = 'all'; $file_types = array_merge( $file_types, $image_types, $other_types ); } $cnames = get_rocket_cdn_cnames( $cdn_zones ); if ( empty( $cnames ) ) { return $html; } /** * Filters the filetypes allowed for the CDN * * @since 2.9 * @author Remy Perona * * @param array $filetypes Array of file types. */ $file_types = apply_filters( 'rocket_cdn_custom_filetypes', $file_types ); $file_types = implode( '|', $file_types ); preg_match_all( '#<a[^>]+?href=[\'"]?([^"\'>]+\.(?:' . $file_types . '))[\'"]?[^>]*>#i', $html, $matches ); if ( ! (bool) $matches ) { return $html; } foreach ( $matches[1] as $key => $url ) { $url = trim( $url, " \t\n\r\0\x0B\"'" ); $url = get_rocket_cdn_url( $url, $cdn_zones ); $src = str_replace( $matches[1][ $key ], $url, $matches[0][ $key ] ); $html = str_replace( $matches[0][ $key ], $src, $html ); } return $html; } /** * Replace URL by CDN of all scripts and styles enqueues with WordPress functions * * @since 2.9 Only add protocol if $src is an absolute url * @since 2.1 * @deprecated 3.4 * * @param string $src URL of the file. * @return string modified URL */ function rocket_cdn_enqueue( $src ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); // Don't use CDN if in admin, in login page, in register page or in a post preview. if ( is_admin() || is_preview() || in_array( $GLOBALS['pagenow'], array( 'wp-login.php', 'wp-register.php' ), true ) || defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return $src; } if ( rocket_extract_url_component( $src, PHP_URL_HOST ) !== '' ) { $src = rocket_add_url_protocol( $src ); } $zone = array( 'all', 'css_and_js' ); // Add only CSS zone. if ( 'style_loader_src' === current_filter() ) { $zone[] = 'css'; } // Add only JS zone. if ( 'script_loader_src' === current_filter() ) { $zone[] = 'js'; } $cnames = get_rocket_cdn_cnames( $zone ); if ( $cnames ) { // Check if the path isn't empty. if ( trim( rocket_extract_url_component( $src, PHP_URL_PATH ), '/' ) !== '' ) { $src = get_rocket_cdn_url( $src, $zone ); } } return $src; } /** * Get all files we don't allow to get in CDN. * * @since 2.5 * @deprecated 3.4 * * @return string A pipe-separated list of rejected files. */ function get_rocket_cdn_reject_files() { _deprecated_function( __FUNCTION__ . '()', '3.4', '\WP_Rocket\Subscriber\CDN\CDN::get_excluded_files()' ); $files = get_rocket_option( 'cdn_reject_files', [] ); /** * Filter the rejected files. * * @since 2.5 * * @param array $files List of rejected files. */ $files = (array) apply_filters( 'rocket_cdn_reject_files', $files ); $files = array_filter( $files ); $files = array_flip( array_flip( $files ) ); return implode( '|', $files ); } /** * Conflict with Envira Gallery: changes the URL argument if using WP Rocket CDN and Envira * * @since 2.6.5 * @since 3.4 * * @param array $args An array of arguments. * @return array Updated array of arguments */ function rocket_cdn_resize_image_args_on_envira_gallery( $args ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( ! isset( $args['url'] ) || (int) get_rocket_option( 'cdn' ) === 0 ) { return $args; } $cnames_host = array_flip( get_rocket_cnames_host() ); $url_host = rocket_extract_url_component( $args['url'], PHP_URL_HOST ); $home_host = rocket_extract_url_component( home_url(), PHP_URL_HOST ); if ( isset( $cnames_host[ $url_host ] ) ) { $args['url'] = str_replace( $url_host, $home_host , $args['url'] ); } return $args; } /** * Conflict with Envira Gallery: changes the resized URL if using WP Rocket CDN and Envira * * @since 2.6.5 * @since 3.4 * * @param string $url Resized image URL. * @return string Resized image URL using the CDN URL */ function rocket_cdn_resized_url_on_envira_gallery( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( (int) get_rocket_option( 'cdn' ) === 0 ) { return $url; } $url = get_rocket_cdn_url( $url, array( 'all', 'images' ) ); return $url; } /** * Apply CDN settings to Beaver Builder parallax. * * @since 3.2.1 * @deprecated 3.4 * @author Grégory Viguier * * @param array $attrs HTML attributes. * @return array */ function rocket_beaver_builder_add_cdn_to_parallax( $attrs ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( ! empty( $attrs['data-parallax-image'] ) ) { $attrs['data-parallax-image'] = get_rocket_cdn_url( $attrs['data-parallax-image'], [ 'all', 'images' ] ); } return $attrs; } if ( class_exists( 'WR2X_Admin' ) ) : /** * Conflict with WP Retina x2: Apply CDN on srcset attribute. * * @since 2.9.1 Use global $wr2x_admin * @since 2.5.5 * @deprecated 3.4 * * @param string $url URL of the image. * @return string Updated URL with CDN */ function rocket_cdn_on_images_from_wp_retina_x2( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); global $wr2x_admin; if ( ! method_exists( $wr2x_admin, 'is_pro' ) || ! $wr2x_admin->is_pro() ) { return $url; } $cdn_domain = get_option( 'wr2x_cdn_domain' ); if ( ! empty( $cdn_domain ) ) { return $url; } return get_rocket_cdn_url( $url, array( 'all', 'images' ) ); } endif; /** * Conflict with Avada theme and WP Rocket CDN * * @since 2.6.1 * @deprecated 3.4 * * @param array $vars An array of variables. * @param string $handle Name of the avada resource. * @return array updated array of variables */ function rocket_fix_cdn_for_avada_theme( $vars, $handle ) { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( 'avada-dynamic' === $handle && get_rocket_option( 'cdn' ) ) { $src = get_rocket_cdn_url( get_template_directory_uri() . '/assets/less/theme/dynamic.less' ); $vars['template-directory'] = sprintf( '~"%s"', dirname( dirname( dirname( dirname( $src ) ) ) ) ); $vars['lessurl'] = sprintf( '~"%s"', dirname( $src ) ); } return $vars; } /** * Conflict with Aqua Resizer & IrishMiss Framework: Apply CDN without blank src!! * * @since 2.5.8 Add compatibility with IrishMiss Framework * @since 2.5.5 * @deprecated 3.4 */ function rocket_cdn_on_aqua_resizer() { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( function_exists( 'aq_resize' ) || function_exists( 'miss_display_image' ) ) { remove_filter( 'wp_get_attachment_url' , 'rocket_cdn_file', PHP_INT_MAX ); add_filter( 'rocket_lazyload_html', 'rocket_add_cdn_on_custom_attr' ); } } /** * Conflict with Revolution Slider & Master Slider: Apply CDN on data-lazyload|data-src attribute. * * @since 2.5.5 * @deprecated 3.4 */ function rocket_cdn_on_sliders_with_lazyload() { _deprecated_function( __FUNCTION__ . '()', '3.4' ); if ( class_exists( 'RevSliderFront' ) || class_exists( 'Master_Slider' ) ) { add_filter( 'rocket_cdn_images_html', 'rocket_add_cdn_on_custom_attr' ); } } deprecated/vendors/classes/class-minify-html.php 0000644 00000022545 15174677547 0016071 0 ustar 00 <?php use WP_Rocket\deprecated\DeprecatedClassTrait; /** * Class Minify_HTML * @package Minify */ /** * Compress HTML * * This is a heavy regex-based removal of whitespace, unnecessary comments and * tokens. IE conditional comments are preserved. There are also options to have * STYLE and SCRIPT blocks compressed by callback functions. * * A test suite is available. * * @package Minify * @author Stephen Clay <steve@mrclay.org> */ class Minify_HTML { use DeprecatedClassTrait; /** * @var boolean */ protected $_jsCleanComments = true; /** * "Minify" an HTML page * * @param string $html * * @param array $options * * 'cssMinifier' : (optional) callback function to process content of STYLE * elements. * * 'jsMinifier' : (optional) callback function to process content of SCRIPT * elements. Note: the type attribute is ignored. * * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If * unset, minify will sniff for an XHTML doctype. * * @return string */ public static function minify($html, $options = array()) { $min = new self($html, $options); return $min->process(); } /** * Create a minifier object * * @param string $html * * @param array $options * * 'cssMinifier' : (optional) callback function to process content of STYLE * elements. * * 'jsMinifier' : (optional) callback function to process content of SCRIPT * elements. Note: the type attribute is ignored. * * 'jsCleanComments' : (optional) whether to remove HTML comments beginning and end of script block * * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If * unset, minify will sniff for an XHTML doctype. */ public function __construct($html, $options = array()) { self::deprecated_class( '3.7' ); $this->_html = str_replace("\r\n", "\n", trim($html)); if (isset($options['xhtml'])) { $this->_isXhtml = (bool)$options['xhtml']; } if (isset($options['cssMinifier'])) { $this->_cssMinifier = $options['cssMinifier']; } if (isset($options['jsMinifier'])) { $this->_jsMinifier = $options['jsMinifier']; } if (isset($options['jsCleanComments'])) { $this->_jsCleanComments = (bool)$options['jsCleanComments']; } } /** * Minify the markeup given in the constructor * * @return string */ public function process() { if ($this->_isXhtml === null) { $this->_isXhtml = (false !== strpos($this->_html, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML')); } $this->_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']); $this->_placeholders = array(); // replace SCRIPTs (and minify) with placeholders // preg_replace_callback - on errors the return is NULL // On big scripts PREG_BACKTRACK_LIMIT_ERROR is reached and causes the empty page $pregJs = preg_replace_callback( '/(\\s*)<script(\\b[^>]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i' ,array($this, '_removeScriptCB') ,$this->_html); if ( isset($pregJs) && ! empty( $pregJs ) ) { $this->_html = $pregJs; } // replace STYLEs (and minify) with placeholders // preg_replace_callback - on errors the return is NULL // On big scripts PREG_BACKTRACK_LIMIT_ERROR is reached and causes the empty page $pregCSS = preg_replace_callback( '/\\s*<style(\\b[^>]*>)([\\s\\S]*?)<\\/style>\\s*/i' ,array($this, '_removeStyleCB') ,$this->_html); if ( isset( $pregCSS ) && ! empty( $pregCSS ) ) { $this->_html = $pregCSS; } // remove HTML comments (not containing IE conditional comments). $this->_html = preg_replace_callback( '/<!--([\\s\\S]*?)-->/' ,array($this, '_commentCB') ,$this->_html); // replace PREs with placeholders $this->_html = preg_replace_callback('/\\s*<pre(\\b[^>]*?>[\\s\\S]*?<\\/pre>)\\s*/i' ,array($this, '_removePreCB') ,$this->_html); // replace TEXTAREAs with placeholders $this->_html = preg_replace_callback( '/\\s*<textarea(\\b[^>]*?>[\\s\\S]*?<\\/textarea>)\\s*/i' ,array($this, '_removeTextareaCB') ,$this->_html); // trim each line. // @todo take into account attribute values that span multiple lines. // Fixed attribute values which span on multiple lines. Causes double spaces " " $this->_html = preg_replace('/^\s+|\s+$/m', ' ', $this->_html); // Fixed double spaces. Replaced with a single space $this->_html = preg_replace('/\s+/', ' ', $this->_html); // remove ws around block/undisplayed elements $this->_html = preg_replace('/\\s+(<\\/?(?:area|article|aside|base(?:font)?|blockquote|body' .'|canvas|caption|center|col(?:group)?|dd|dir|div|dl|dt|fieldset|figcaption|figure|footer|form' .'|frame(?:set)?|h[1-6]|head|header|hgroup|hr|html|legend|li|link|main|map|menu|meta|nav' .'|ol|opt(?:group|ion)|output|p|param|section|t(?:able|body|head|d|h||r|foot|itle)' .'|ul|video)\\b[^>]*>)/i', '$1', $this->_html); // remove ws outside of all elements $this->_html = preg_replace_callback( '/>([^<]+)</' ,array($this, '_outsideTagCB') ,$this->_html); // fill placeholders $this->_html = str_replace( array_keys($this->_placeholders) ,array_values($this->_placeholders) ,$this->_html ); // issue 229: multi-pass to catch scripts that didn't get replaced in textareas $this->_html = str_replace( array_keys($this->_placeholders) ,array_values($this->_placeholders) ,$this->_html ); return $this->_html; } protected function _commentCB($m) { return ( false !== strpos($m[1], 'fwp-loop') || false !== strpos($m[1], 'ngg_resource_manager_marker') || 0 === strpos($m[1], '[') || false !== strpos($m[1], '<![') || 0 === strpos($m[1], 'esi') || 0 === strpos($m[1], 'noindex') || 0 === strpos($m[1], '/noindex') || 0 === strpos($m[1], 'start_content') || 0 === strpos($m[1], 'end_content') || 0 === strpos($m[1], '{{WP_ROCKET_CONDITIONAL}}') ) ? $m[0] : ''; } protected function _reservePlace($content) { $placeholder = '%' . $this->_replacementHash . count($this->_placeholders) . '%'; $this->_placeholders[$placeholder] = $content; return $placeholder; } protected $_isXhtml = null; protected $_replacementHash = null; protected $_placeholders = array(); protected $_cssMinifier = null; protected $_jsMinifier = null; protected function _outsideTagCB($m) { return '>' . preg_replace('/^\\s+|\\s+$/', ' ', $m[1]) . '<'; } protected function _removePreCB($m) { return $this->_reservePlace("<pre{$m[1]}"); } protected function _removeTextareaCB($m) { return $this->_reservePlace("<textarea{$m[1]}"); } protected function _removeStyleCB($m) { $openStyle = "<style{$m[1]}"; $css = $m[2]; // remove HTML comments $css = preg_replace('/(?:^\\s*<!--|-->\\s*$)/u', '', $css); // remove CDATA section markers $data = $this->_removeCdata($css); // minify $minifier = $this->_cssMinifier ? $this->_cssMinifier : 'trim'; $css = call_user_func($minifier, $data['content']); return $this->_reservePlace( $data['cdata'] ? "{$openStyle}/* <![CDATA[ */ {$css} /* ]]> */</style>" : "{$openStyle}{$css}</style>" ); } protected function _removeScriptCB($m) { $openScript = "<script{$m[2]}"; $js = $m[3]; // whitespace surrounding? preserve at least one space $ws1 = ($m[1] === '') ? '' : ' '; $ws2 = ($m[4] === '') ? '' : ' '; // remove HTML comments (and ending "//" if present) if ($this->_jsCleanComments) { $js = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/u', '', $js); } // remove CDATA section markers $data = $this->_removeCdata($js); // minify $minifier = $this->_jsMinifier ? $this->_jsMinifier : 'trim'; $js = call_user_func($minifier, $data['content']); return $this->_reservePlace($data['cdata'] ? "{$ws1}{$openScript}/* <![CDATA[ */ {$js} /* ]]> */</script>{$ws2}" : "{$ws1}{$openScript}{$js}</script>{$ws2}" ); } protected function _removeCdata($str) { $data = array(); if ( false !== strpos( $str, '<![CDATA[' ) ) { $data['content'] = str_replace( array( '/* <![CDATA[ */', '/* ]]> */' ), '', $str ); $data['cdata'] = true; } else { $data['content'] = $str; $data['cdata'] = false; } return $data; } } deprecated/Engine/Addon/Busting/BustingFactory.php 0000644 00000002741 15174677547 0016220 0 ustar 00 <?php namespace WP_Rocket\Addon\Busting; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Addon\GoogleTracking\GoogleAnalytics; use WP_Rocket\Addon\GoogleTracking\GoogleTagManager; use WP_Rocket\Busting\Facebook_Pickles; use WP_Rocket\Busting\Facebook_SDK; /** * Busting classes Factory * * @since 3.9 deprecated. * @since 3.6.2 */ class BustingFactory { use DeprecatedClassTrait; /** * Base cache busting filepath. * * @var string */ private $busting_path; /** * Base cache busting URL. * * @var string */ private $busting_url; /** * Constructor * * @param string $busting_path Base cache busting filepath. * @param string $busting_url Base cache busting URL. */ public function __construct( $busting_path, $busting_url ) { self::deprecated_class( '3.9' ); $this->busting_path = $busting_path; $this->busting_url = $busting_url; } /** * Creator method * * @param string $type Type of busting class to create. * @return Busting_Interface */ public function type( $type ) { switch ( $type ) { case 'fbpix': return new Facebook_Pickles( $this->busting_path, $this->busting_url ); case 'fbsdk': return new Facebook_SDK( $this->busting_path, $this->busting_url ); case 'ga': return new GoogleAnalytics( $this->busting_path, $this->busting_url ); case 'gtm': return new GoogleTagManager( $this->busting_path, $this->busting_url, new GoogleAnalytics( $this->busting_path, $this->busting_url ) ); } } } deprecated/Engine/Addon/Busting/FileBustingTrait.php 0000644 00000025505 15174677547 0016477 0 ustar 00 <?php namespace WP_Rocket\Addon\Busting; use WP_Rocket\Logger\Logger; trait FileBustingTrait { /** * Saves the content of the URL to bust to the busting file if it doesn't exist yet. * * @since 3.2.4 * @access public * * @param string $url URL to get the content from. * @return bool */ public function save( $url ) { if ( $this->get_busting_version() ) { // We have a local copy. Logger::debug( 'Found local file.', [ self::LOGGER_CONTEXT, 'path' => $this->get_busting_path(), ] ); return true; } if ( $this->refresh_save( $url ) ) { // We downloaded a fresh copy. Logger::debug( 'New copy downloaded.', [ self::LOGGER_CONTEXT, 'path' => $this->get_busting_path(), ] ); return true; } return false; } /** * Deletes the busting file. * * @since 3.1 * @since 3.2.4 Handle versioning. * @access public * @author Remy Perona * @author Grégory Viguier * * @return bool True on success. False on failure. */ public function delete() { $files = $this->get_all_files(); if ( false === $files ) { // Error. return false; } $this->file_version = null; if ( ! $files ) { // No local files yet. return true; } return $this->delete_files( array_keys( $files ) ); } /** ----------------------------------------------------------------------------------------- */ /** LOCAL FILE ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the version of the current busting file. * * @since 3.2.4 * @access protected * @author Grégory Viguier * * @return string|bool Version of the file. False if the file does not exist. */ protected function get_busting_version() { if ( ! empty( $this->file_version ) ) { return $this->file_version; } $files = $this->get_all_files(); if ( ! $files ) { // Error or no local files yet. return false; } // Since we're not supposed to have several files, return the first one. $this->file_version = reset( $files ); return $this->file_version; } /** * Get all cached files in the directory. * In a perfect world, there should be only one. * * @since 3.2.4 * @access private * * @return bool|array A list of file names (as array keys) and versions (as array values). False on failure. */ private function get_all_files() { $dir_path = rtrim( $this->busting_path, '\\/' ); if ( ! $this->filesystem->exists( $dir_path ) ) { return []; } if ( ! $this->filesystem->is_readable( $dir_path ) ) { Logger::error( 'Directory is not readable.', [ self::LOGGER_CONTEXT, 'path' => $dir_path, ] ); return false; } $pattern = '/' . sprintf( $this->escape_file_name( $this->filename_pattern ), '([a-f0-9]{32}|local)' ) . '/'; $entries = _rocket_get_dir_files_by_regex( $dir_path, $pattern ); $list = []; foreach ( $entries as $entry ) { $filename = $entry->getFilename(); preg_match( $pattern, $filename, $file_details_match ); if ( ! empty( $file_details_match[1] ) ) { $list[ $filename ] = $file_details_match[1]; } } return $list; } /** * Get the final URL for the current cache busting file. * * @since 3.2.4 * @access protected * * @return string|bool URL of the file. False if the file does not exist. */ public function get_busting_url() { return $this->get_busting_file_url( $this->get_busting_version() ); } /** * Get the path to the current cache busting file. * * @since 3.2.4 * @access protected * @author Grégory Viguier * * @return string|bool URL of the file. False if the file does not exist. */ protected function get_busting_path() { return $this->get_busting_file_path( $this->get_busting_version() ); } /** * Get the final URL for a cache busting file. * * @since 3.2.4 * @access private * @author Grégory Viguier * * @param string $version The file version. * @return string|bool URL of the file with this version. False if no versions are provided. */ private function get_busting_file_url( $version ) { if ( ! $version ) { return false; } $filename = $this->get_busting_file_name( $version ); // This filter is documented in inc/functions/minify.php. return apply_filters( 'rocket_js_url', $this->busting_url . $filename ); } /** * Get the local file name. * * @since 3.2.4 * @access private * @author Grégory Viguier * * @param string $version The file version. * @return string|bool The name of the file with this version. False if no versions are provided. */ private function get_busting_file_name( $version ) { if ( ! $version ) { return false; } return sprintf( $this->filename_pattern, $version ); } /** * Get the local file path. * * @since 3.2.4 * @access private * @author Grégory Viguier * * @param string $version The file version. * @return string|bool Path to the file with this version. False if no versions are provided. */ private function get_busting_file_path( $version ) { if ( ! $version ) { return false; } return $this->busting_path . $this->get_busting_file_name( $version ); } /** * Escape a file name, to be used in a regex pattern (delimiter is `/`). * `%s` conversion specifications are protected. * * @since 3.2.4 * @access private * * @param string $filename_pattern The file name. * @return string */ private function escape_file_name( $filename_pattern ) { return preg_quote( $filename_pattern, '/' ); } /** * Delete busting files. * * @since 3.2.4 * @access private * @author Grégory Viguier * * @param array $files A list of file names. * @return bool True if files have been deleted (or no files have been provided). False on failure. */ private function delete_files( $files ) { if ( ! $files ) { // ¯\_(ツ)_/¯ return true; } $has_deleted = false; $error_paths = []; foreach ( $files as $file_name ) { if ( ! $this->filesystem->delete( $this->busting_path . $file_name, false, 'f' ) ) { $error_paths[] = $this->busting_path . $file_name; } else { $has_deleted = true; } } if ( $error_paths ) { // Group all deletion errors into one log. Logger::error( 'Local file(s) could not be deleted.', [ self::LOGGER_CONTEXT, 'paths' => $error_paths, ] ); } return $has_deleted; } /** ----------------------------------------------------------------------------------------- */ /** UPDATE THE LOCAL FILE =================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Add new contents to a file. If the file doesn't exist, it is created. * * @since 3.2.4 * @access private * @author Grégory Viguier * * @param string $file_path Path to the file to update. * @param string $file_contents New contents. * @return string|bool The file contents on success. False on failure. */ private function update_file_contents( $file_path, $file_contents ) { if ( ! $this->is_busting_dir_writable() ) { return false; } if ( ! rocket_put_content( $file_path, $file_contents ) ) { Logger::error( 'Contents could not be written into file.', [ self::LOGGER_CONTEXT, 'path' => $file_path, ] ); return false; } return $file_contents; } /** * Tell if the directory containing the busting file is writable. * * @since 3.2 * @access private * @author Grégory Viguier * * @return bool */ private function is_busting_dir_writable() { if ( ! $this->filesystem->exists( $this->busting_path ) ) { rocket_mkdir_p( $this->busting_path ); } if ( ! $this->filesystem->is_writable( $this->busting_path ) ) { Logger::error( 'Directory is not writable.', [ self::LOGGER_CONTEXT, 'paths' => $this->busting_path, ] ); return false; } return true; } /** ----------------------------------------------------------------------------------------- */ /** GET LOCAL/REMOTE CONTENTS =============================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get a file contents. If the file doesn't exist, new contents are fetched remotely. * * @since 3.2.4 * @access private * @author Grégory Viguier * * @param string $file_path Path to the file. * @param string $file_url URL to the remote file. * @return string|bool The contents on success, false on failure. */ private function get_file_or_remote_contents( $file_path, $file_url ) { $content = $this->get_file_contents( $file_path ); if ( $content ) { // We have a local file. return $content; } return $this->get_remote_contents( $file_url ); } /** * Get a file contents. * * @since 3.2.4 * @access private * @author Grégory Viguier * * @param string $file_path Path to the file. * @return string|bool The contents on success, false on failure. */ private function get_file_contents( $file_path ) { if ( ! $this->filesystem->exists( $file_path ) ) { Logger::error( 'Local file does not exist.', [ self::LOGGER_CONTEXT, 'path' => $file_path, ] ); return false; } if ( ! $this->filesystem->is_readable( $file_path ) ) { Logger::error( 'Local file is not readable.', [ self::LOGGER_CONTEXT, 'path' => $file_path, ] ); return false; } $content = $this->filesystem->get_contents( $file_path ); if ( ! $content ) { Logger::error( 'Local file is empty.', [ self::LOGGER_CONTEXT, 'path' => $file_path, ] ); return false; } return $content; } /** * Get the contents of a URL. * * @since 3.2.4 * @access private * @author Grégory Viguier * * @param string $url The URL to request. * @return string|bool The contents on success. False on failure. */ private function get_remote_contents( $url ) { try { $response = wp_remote_get( $url ); } catch ( Exception $e ) { Logger::error( 'Remote file could not be fetched.', [ self::LOGGER_CONTEXT, 'url' => $url, 'response' => $e->getMessage(), ] ); return false; } if ( is_wp_error( $response ) ) { Logger::error( 'Remote file could not be fetched.', [ self::LOGGER_CONTEXT, 'url' => $url, 'response' => $response->get_error_message(), ] ); return false; } $contents = wp_remote_retrieve_body( $response ); if ( ! $contents ) { Logger::error( 'Remote file could not be fetched.', [ self::LOGGER_CONTEXT, 'url' => $url, 'response' => $response, ] ); return false; } return $contents; } } deprecated/Engine/Addon/GoogleTracking/Subscriber.php 0000644 00000010170 15174677547 0016637 0 ustar 00 <?php namespace WP_Rocket\Addon\GoogleTracking; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Addon\Busting\BustingFactory; use WP_Rocket\Admin\Options_Data as Options; /** * Event subscriber for Google tracking cache busting * * @since 3.9 deprecated. * @since 3.1 */ class Subscriber implements Subscriber_Interface { use DeprecatedClassTrait; /** * Instance of the Busting Factory class * * @var BustingFactory */ private $busting_factory; /** * Instance of the Option_Data class * * @var Options */ private $options; /** * Constructor * * @param BustingFactory $busting_factory Instance of the Busting Factory class. * @param Options $options Instance of the Option_Data class. */ public function __construct( BustingFactory $busting_factory, Options $options ) { self::deprecated_class( '3.9' ); $this->busting_factory = $busting_factory; $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.1 * * @return array */ public static function get_subscribed_events() { $events = [ 'cron_schedules' => 'add_schedule', 'init' => 'schedule_tracking_cache_update', 'rocket_google_tracking_cache_update' => 'update_tracking_cache', 'rocket_purge_cache' => 'delete_tracking_cache', 'rocket_buffer' => 'cache_busting_google_tracking', ]; return $events; } /** * Processes the cache busting on the HTML content * * Google Analytics replacement is performed first, and if no replacement occurred, Google Tag Manager replacement is performed. * * @since 3.1 * * @param string $html HTML content. * @return string */ public function cache_busting_google_tracking( $html ) { if ( ! $this->is_allowed() ) { return $html; } $processor = $this->busting_factory->type( 'ga' ); $html = $processor->replace_url( $html ); $processor = $this->busting_factory->type( 'gtm' ); $html = $processor->replace_url( $html ); return $html; } /** * Schedules the auto-update of Google Analytics cache busting file * * @since 3.1 * * @return void */ public function schedule_tracking_cache_update() { if ( ! $this->is_busting_active() ) { return; } if ( ! wp_next_scheduled( 'rocket_google_tracking_cache_update' ) ) { wp_schedule_event( time(), 'weekly', 'rocket_google_tracking_cache_update' ); } } /** * Updates Google Analytics cache busting file * * @since 3.1 * * @return bool */ public function update_tracking_cache() { if ( ! $this->is_busting_active() ) { return false; } $processor = $this->busting_factory->type( 'ga' ); return $processor->refresh_save( $processor->get_url() ); } /** * Adds weekly interval to cron schedules * * @since 3.1 * * @param Array $schedules An array of intervals used by cron jobs. * @return Array */ public function add_schedule( $schedules ) { if ( ! $this->is_busting_active() ) { return $schedules; } $schedules['weekly'] = [ 'interval' => 604800, 'display' => __( 'weekly', 'rocket' ), ]; return $schedules; } /** * Deletes the GA busting file. * * @since 3.1 * @since 3.6 Argument replacement. * * @param string $type Type of cache clearance: 'all', 'post', 'term', 'user', 'url'. * @return bool */ public function delete_tracking_cache( $type ) { if ( 'all' !== $type || ! $this->is_busting_active() ) { return false; } $this->busting_factory->type( 'gtm' )->delete(); return $this->busting_factory->type( 'ga' )->delete(); } /** * Checks if the cache busting should happen * * @since 3.1 * * @return boolean */ private function is_allowed() { if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return false; } return $this->is_busting_active(); } /** * Tell if the cache busting option is active. * * @since 3.6 * * @return bool */ private function is_busting_active() { return (bool) $this->options->get( 'google_analytics_cache', 0 ); } } deprecated/Engine/Addon/GoogleTracking/GoogleAnalytics.php 0000644 00000013201 15174677547 0017616 0 ustar 00 <?php namespace WP_Rocket\Addon\GoogleTracking; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Addon\Busting\FileBustingTrait; use WP_Rocket\Busting\Abstract_Busting; use WP_Rocket\Logger\Logger; /** * Manages the cache busting of the Google Analytics file. * * @since 3.9 deprecated * @since 3.1 */ class GoogleAnalytics extends Abstract_Busting { use FileBustingTrait; use DeprecatedClassTrait; /** * Context used for the logger. * * @var string * @since 3.2.4 * @author Grégory Viguier */ const LOGGER_CONTEXT = 'gg analytics'; /** * Google Analytics URL. * * @var string * @since 3.1 * @access protected * @author Remy Perona */ protected $url = 'https://www.google-analytics.com/analytics.js'; /** * File name (local). * %s is a "version": a md5 hash of the file contents. * * @var string * @since 3.2.4 * @access protected * @author Grégory Viguier */ protected $filename_pattern = 'ga-%s.js'; /** * Current file version (local): a md5 hash of the file contents. * * @var string * @since 3.2.4 * @access protected * @author Grégory Viguier */ protected $file_version; /** * Flag to track the replacement. * * @var bool * @since 3.1 * @access protected * @author Remy Perona */ protected $is_replaced = false; /** * Filesystem object. * * @var object * @since 3.2.4 * @access protected * @author Grégory Viguier */ protected $filesystem = false; /** * Constructor. * * @since 3.1 * @access public * @author Remy Perona * * @param string $busting_path Path to the busting directory. * @param string $busting_url URL of the busting directory. */ public function __construct( $busting_path, $busting_url ) { self::deprecated_class( '3.9' ); $this->busting_path = $busting_path . 'google-tracking/'; $this->busting_url = $busting_url . 'google-tracking/'; $this->filesystem = rocket_direct_filesystem(); } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC METHODS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Performs the replacement process. * * @since 3.1 * @access public * @author Remy Perona * * @param string $html HTML content. * @return string */ public function replace_url( $html ) { $this->is_replaced = false; $tag = $this->find( '<script\s*(?<attr>[^>]*)?>(?<content>.*)?<\/script>', $html ); if ( ! $tag ) { return $html; } Logger::info( 'GOOGLE ANALYTICS CACHING PROCESS STARTED.', [ self::LOGGER_CONTEXT, 'tag' => $tag, ] ); if ( ! $this->save( $this->url ) ) { return $html; } $replace_tag = preg_replace( '/(?:https?:)?\/\/www\.google-analytics\.com\/analytics\.js/i', $this->get_busting_url(), $tag ); $html = str_replace( $tag, $replace_tag, $html ); $this->is_replaced = true; Logger::info( 'Google Analytics caching process succeeded.', [ self::LOGGER_CONTEXT, 'file' => $this->get_busting_path(), ] ); return $html; } /** * Tell if the replacement was successful or not. * * @since 3.1 * @access public * @author Remy Perona * * @return bool */ public function is_replaced() { return $this->is_replaced; } /** * Saves the content of the URL to cache to the busting file. * * @since 3.2.4 * @access public * @author Grégory Viguier * * @param string $url URL to get the content from. * @return bool */ public function refresh_save( $url ) { // Before doing anything, make sure the busting file can be created. if ( ! $this->is_busting_dir_writable() ) { return false; } // Get remote content. $content = $this->get_remote_contents( $url ); if ( ! $content ) { // Could not get the remote contents. return false; } $version = md5( $content ); $path = $this->get_busting_file_path( $version ); return $this->update_file_contents( $path, $content ); } /** ----------------------------------------------------------------------------------------- */ /** REMOTE FILE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the Google Analytics URL. * * @since 3.1 * @access public * @author Remy Perona * * @return string */ public function get_url() { return $this->url; } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS INTERNAL TOOLS ================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Searches for element(s) in the DOM. * * @since 3.1 * @access public * @author Remy Perona * * @param string $pattern Pattern to match. * @param string $html HTML content. * @return string */ protected function find( $pattern, $html ) { preg_match_all( '/' . $pattern . '/si', $html, $all_matches, PREG_SET_ORDER ); $matches = array_map( function( $match ) { if ( ! preg_match( '/src\s*=\s*[\'"]\s*(?:https?:)?\/\/www\.google-analytics\.com\/analytics\.js\s*[\'"]/i', $match['attr'] . $match['content'] ) && false === strpos( $match['content'], 'GoogleAnalyticsObject' ) ) { return; } return $match[0]; }, $all_matches ); $matches = array_values( array_filter( $matches ) ); if ( ! $matches ) { return false; } return $matches[0]; } } deprecated/Engine/Addon/GoogleTracking/GoogleTagManager.php 0000644 00000012245 15174677547 0017704 0 ustar 00 <?php namespace WP_Rocket\Addon\GoogleTracking; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Addon\Busting\FileBustingTrait; use WP_Rocket\Logger\Logger; use WP_Rocket\Busting\Abstract_Busting; use WP_Filesystem_Direct; /** * Manages the cache busting of the Google Tag Manager file * * @since 3.9 deprecated. * @since 3.1 */ class GoogleTagManager extends Abstract_Busting { use FileBustingTrait; use DeprecatedClassTrait; /** * Context used for the logger. * * @var string * @since 3.2.4 */ const LOGGER_CONTEXT = 'gg tag manager'; /** * File name (local). * %s is a "version": a md5 hash of the file contents. * * @var string * @since 3.2.4 * @access protected */ protected $filename_pattern = 'gtm-%s.js'; /** * Current file version (local): a md5 hash of the file contents. * * @var string * @since 3.2.4 * @access protected */ protected $file_version; /** * Filesystem object. * * @var object * @since 3.2.4 * @access protected */ protected $filesystem = false; /** * Google Analytics object. * * @var object * @since 3.2.4 * @access protected */ protected $ga_busting = false; /** * Constructor. * * @since 3.1 * @access public * * @param string $busting_path Path to the busting directory. * @param string $busting_url URL of the busting directory. * @param GoogleAnalytics $ga_busting A GoogleAnalytics instance. * @param WP_Filesystem_Direct $filesystem Instance of the filesystem handler. */ public function __construct( $busting_path, $busting_url, GoogleAnalytics $ga_busting, $filesystem = null ) { self::deprecated_class( '3.9' ); $blog_id = get_current_blog_id(); $this->busting_path = $busting_path . $blog_id . '/'; $this->busting_url = $busting_url . $blog_id . '/'; $this->ga_busting = $ga_busting; $this->filesystem = is_null( $filesystem ) ? rocket_direct_filesystem() : $filesystem; } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC METHODS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Performs the replacement process. * * @since 3.1 * @access public * * @param string $html HTML content. * @return string */ public function replace_url( $html ) { $script = $this->find( '<script(\s+[^>]+)?\s+src\s*=\s*[\'"]\s*?((?:https?:)?\/\/www\.googletagmanager\.com(?:.+)?)\s*?[\'"]([^>]+)?\/?>', $html ); if ( ! $script ) { return $html; } // replace relative protocol // with full https://. $gtm_url = preg_replace( '/^\/\//', 'https://', $script[2] ); Logger::info( 'GOOGLE TAG MANAGER CACHING PROCESS STARTED.', [ self::LOGGER_CONTEXT, 'tag' => $script, ] ); if ( ! $this->save( $gtm_url ) ) { return $html; } $replace_script = str_replace( $script[2], $this->get_busting_url(), $script[0] ); $replace_script = str_replace( '<script', '<script data-no-minify="1"', $replace_script ); $html = str_replace( $script[0], $replace_script, $html ); Logger::info( 'Google Tag Manager caching process succeeded.', [ self::LOGGER_CONTEXT, 'file' => $this->get_busting_path(), ] ); return $html; } /** * Saves the content of the URL to cache to the busting file. * * @since 3.2 * @access public * * @param string $url URL to get the content from. * @return bool */ public function refresh_save( $url ) { // Before doing anything, make sure the busting file can be created. if ( ! $this->is_busting_dir_writable() ) { return false; } // Get remote content. $content = $this->get_remote_contents( $url ); if ( ! $content ) { // Could not get the remote contents. return false; } $version = md5( $content ); $path = $this->get_busting_file_path( $version ); $content = $this->replace_ga_url( $content ); return $this->update_file_contents( $path, $content ); } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS INTERNAL TOOLS ================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Searches for element(s) in the DOM. * * @since 3.1 * @access public * * @param string $pattern Pattern to match. * @param string $html HTML content. * @return string */ protected function find( $pattern, $html ) { preg_match_all( '/' . $pattern . '/Umsi', $html, $matches, PREG_SET_ORDER ); if ( empty( $matches ) ) { return false; } return $matches[0]; } /** * Replaces the Google Analytics URL by the local copy inside the gtm-local.js file content * * @since 3.1 * * @param string $content JavaScript content. * @return string */ protected function replace_ga_url( $content ) { if ( ! $this->ga_busting->save( $this->ga_busting->get_url() ) ) { return $content; } return str_replace( $this->ga_busting->get_url(), $this->ga_busting->get_busting_url(), $content ); } } deprecated/Engine/Addon/FacebookTracking/Subscriber.php 0000644 00000010076 15174677547 0017141 0 ustar 00 <?php namespace WP_Rocket\Addon\FacebookTracking; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Addon\Busting\BustingFactory; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\Options_Data as Options; /** * Event subscriber for Facebook tracking cache busting. * * @since 3.9 deprecated. * @since 3.2 */ class Subscriber implements Subscriber_Interface { use DeprecatedClassTrait; /** * Name of the cron. * * @var string * @since 3.2 */ const CRON_NAME = 'rocket_facebook_tracking_cache_update'; /** * Instance of the Busting Factory class. * * @var BustingFactory * @since 3.2 */ private $busting_factory; /** * Instance of the Option_Data class. * * @var Options * @since 3.2 */ private $options; /** * Constructor. * * @since 3.2 * * @param BustingFactory $busting_factory Instance of the Busting Factory class. * @param Options $options Instance of the Options_Data class. */ public function __construct( BustingFactory $busting_factory, Options $options ) { self::deprecated_class( '3.9' ); $this->busting_factory = $busting_factory; $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.2 * * @return array */ public static function get_subscribed_events() { $events = [ 'cron_schedules' => 'add_schedule', 'init' => 'schedule_cache_update', self::CRON_NAME => 'update_cache', 'rocket_purge_cache' => 'delete_cache', 'rocket_buffer' => 'cache_busting_facebook_tracking', ]; return $events; } /** * Add weekly interval to cron schedules. * * @since 3.2 * * @param array $schedules An array of intervals used by cron jobs. * @return array */ public function add_schedule( $schedules ) { if ( ! $this->is_busting_active() ) { return $schedules; } $schedules['weekly'] = [ 'interval' => 604800, 'display' => __( 'weekly', 'rocket' ), ]; return $schedules; } /** * (Un)Schedule the auto-update of the cache busting files. * * @since 3.2 */ public function schedule_cache_update() { $scheduled = wp_next_scheduled( self::CRON_NAME ); if ( ! $this->is_busting_active() ) { if ( $scheduled ) { wp_clear_scheduled_hook( self::CRON_NAME ); } return; } if ( ! $scheduled ) { wp_schedule_event( time(), 'weekly', self::CRON_NAME ); } } /** * Update the Facebook Pixel cache busting files. * * @since 3.2 * * @return bool */ public function update_cache() { if ( ! $this->is_busting_active() ) { return false; } $html = $this->busting_factory->type( 'fbsdk' )->refresh(); return $this->busting_factory->type( 'fbpix' )->refresh_all(); } /** * Delete Facebook Pixel cache busting files. * * @since 3.2 * @since 3.6 Argument replacement. * * @param string $type Type of cache clearance: 'all', 'post', 'term', 'user', 'url'. * @return bool */ public function delete_cache( $type ) { if ( 'all' !== $type || ! $this->is_busting_active() ) { return false; } $html = $this->busting_factory->type( 'fbsdk' )->delete(); return $this->busting_factory->type( 'fbpix' )->delete_all(); } /** * Process the cache busting on the HTML contents. * * @since 3.2 * * @param string $html HTML contents. * @return string */ public function cache_busting_facebook_tracking( $html ) { if ( ! $this->is_allowed() ) { return $html; } $html = $this->busting_factory->type( 'fbsdk' )->replace_url( $html ); return $this->busting_factory->type( 'fbpix' )->replace_url( $html ); } /** * Tell if the cache busting should happen. * * @since 3.2 * * @return bool */ private function is_allowed() { if ( defined( 'DONOTROCKETOPTIMIZE' ) && DONOTROCKETOPTIMIZE ) { return false; } return $this->is_busting_active(); } /** * Tell if the cache busting option is active. * * @since 3.2 * * @return bool */ private function is_busting_active() { return (bool) $this->options->get( 'facebook_pixel_cache', 0 ); } } deprecated/Engine/Optimization/QueryString/RemoveSubscriber.php 0000644 00000003362 15174677547 0021060 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\QueryString; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Hooks into WordPress to remove query strings for static files. * * @since 3.1 * @since 3.6 Deprecated. * @author Remy Perona * @deprecated */ class RemoveSubscriber implements Subscriber_Interface { use DeprecatedClassTrait; /** * Remove Query String instance. * * @since 3.1 * @author Remy Perona * * @var Remove */ protected $remove_query_string; /** * Constructor * * @since 3.1 * @author Remy Perona * * @param Remove $remove_query_string Remove Query String instance. */ public function __construct( Remove $remove_query_string ) { self::deprecated_class( '3.6' ); $this->remove_query_string = $remove_query_string; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.1 * @author Remy Perona * * @return array */ public static function get_subscribed_events() { return [ 'rocket_buffer' => [ 'process', 30 ], ]; } /** * Filters the HTML to fetch static files with a query string and remove it * * @since 3.1 * @author Remy Perona * * @param string $html HTML content. * @return string */ public function process( $html ) { if ( ! $this->is_allowed() ) { return $html; } $html = $this->remove_query_string->remove_query_strings_css( $html ); $html = $this->remove_query_string->remove_query_strings_js( $html ); return $html; } /** * Checks if is allowed to remove query strings for static files. * * @since 3.1 * @author Remy Perona * * @return bool */ protected function is_allowed() { return $this->remove_query_string->is_allowed(); } } deprecated/Engine/Optimization/QueryString/Remove.php 0000644 00000017764 15174677547 0017047 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\QueryString; use WP_Rocket\Admin\Options_Data; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Engine\Optimization\AbstractOptimization; use WP_Rocket\Engine\Optimization\CSSTrait; /** * Remove query string from static resources. * * @since 3.1 * @since 3.6 Deprecated. * @author Remy Perona * @deprecated */ class Remove extends AbstractOptimization { use DeprecatedClassTrait; use CSSTrait; /** * Plugin options instance. * * @since 3.1 * @author Remy Perona * * @var Options_Data */ protected $options; /** * Cache busting base path * * @since 3.1 * @author Remy Perona * * @var string */ protected $busting_path; /** * Cache busting base URL * * @since 3.1 * @author Remy Perona * * @var string */ protected $busting_url; /** * Excluded files from optimization * * @since 3.1 * @author Remy Perona * * @var string */ protected $excluded_files; /** * Constructor * * @since 3.1 * @author Remy Perona * * @param Options_Data $options Plugin options instance. * @param string $busting_path Base cache busting files path. * @param string $busting_url Base cache busting files URL. */ public function __construct( Options_Data $options, $busting_path, $busting_url ) { self::deprecated_class( '3.6' ); $this->options = $options; $this->busting_path = $busting_path . get_current_blog_id() . '/'; $this->busting_url = $busting_url . get_current_blog_id() . '/'; } /** * Returns a regex-ready string with the excluded filepaths for the Remove Query Strings option * * @since 3.3.3 * @author Remy Perona * * @return string */ protected function get_excluded_files() { static $excluded_files; if ( isset( $excluded_files ) ) { return $excluded_files; } /** * Filters files to exclude from cache busting * * @since 2.9.3 * @author Remy Perona * * @param array $excluded_files An array of filepath to exclude. */ $excluded_files = apply_filters( 'rocket_exclude_cache_busting', [] ); if ( empty( $excluded_files ) ) { $excluded_files = ''; return $excluded_files; } foreach ( $excluded_files as $i => $excluded_file ) { // Escape character for future use in regex pattern. $excluded_files[ $i ] = str_replace( '#', '\#', $excluded_file ); } $excluded_files = implode( '|', $excluded_files ); return $excluded_files; } /** * Remove query strings for CSS files that have one * * @since 3.1 * @author Remy Perona * * @param string $html HTML content. * @return string */ public function remove_query_strings_css( $html ) { $html_nocomments = preg_replace( '/<!--(.*)-->/Uis', '', $html ); $styles = $this->find( '<link\s+([^>]+[\s\'"])?href\s*=\s*[\'"]\s*?([^\'"]+\.css(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>', $html_nocomments ); if ( ! $styles ) { return $html; } foreach ( $styles as $style ) { $url = $style[2]; $url = $this->can_replace( $url ); if ( ! $url ) { continue; } $optimized_url = $this->replace_url( $url, 'css' ); if ( ! $optimized_url ) { continue; } $replace_style = str_replace( $style[2], $optimized_url, $style[0] ); $html = str_replace( $style[0], $replace_style, $html ); } return $html; } /** * Remove query strings for JS files that have one * * @since 3.1 * @author Remy Perona * * @param string $html HTML content. * @return string */ public function remove_query_strings_js( $html ) { $html_nocomments = $this->hide_comments( $html ); $scripts = $this->find( '<script\s+([^>]+[\s\'"])?src\s*=\s*[\'"]\s*?([^\'"]+\.js(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>', $html_nocomments ); if ( ! $scripts ) { return $html; } foreach ( $scripts as $script ) { $url = $script[2]; $url = $this->can_replace( $url ); if ( ! $url ) { continue; } $optimized_url = $this->replace_url( $url, 'js' ); if ( ! $optimized_url ) { continue; } $replace_script = str_replace( $script[2], $optimized_url, $script[0] ); $html = str_replace( $script[0], $replace_script, $html ); } return $html; } /** * Gets the CDN zones. * * @since 3.1 * @author Remy Perona * * @return array */ public function get_zones() { return [ 'all', 'css_and_js', 'css', 'js' ]; } /** * Determines if we can optimize * * @since 3.1 * @author Remy Perona * * @return boolean */ public function is_allowed() { if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return false; } if ( ! $this->options->get( 'remove_query_strings' ) ) { return false; } return true; } /** * Determines if we can perform the remove query string on that URL * * @since 3.1 * @author Remy Perona * * @param string $url source URL. * @return bool\string */ protected function can_replace( $url ) { $parsed_url = get_rocket_parse_url( $url ); if ( empty( $parsed_url['query'] ) ) { return false; } $version = get_bloginfo( 'version' ); if ( false !== strpos( $url, 'ver=' . $version ) ) { $url = rtrim( str_replace( [ 'ver=' . $version, '?&', '&&' ], [ '', '?', '&' ], $url ), '?&' ); } if ( $this->is_external_file( $url ) ) { return false; } if ( $this->is_excluded( $url ) ) { return false; } return $url; } /** * Determines if the URL is excluded * * @since 3.1 * @author Remy Perona * * @param string $url source URL. * @return bool */ protected function is_excluded( $url ) { $excluded_files = $this->get_excluded_files(); if ( ! empty( $excluded_files ) && preg_match( '#^' . $excluded_files . '$#', rocket_clean_exclude_file( $url ) ) ) { return true; } return false; } /** * Replaces the original URL with the cache busting URL * * @since 3.1 * @author Remy Perona * * @param string $url source URL. * @param string $extension file extension. * @return bool|string */ protected function replace_url( $url, $extension ) { $query = wp_parse_url( $url, PHP_URL_QUERY ); if ( empty( $query ) ) { return $url; } // This filter is documented in /inc/classes/optimization/class-abstract-optimization.php. $internal_url = apply_filters( 'rocket_asset_url', $url, $this->get_zones() ); $parsed_url = get_rocket_parse_url( $internal_url ); $relative_src = ltrim( $parsed_url['path'] . '?' . $parsed_url['query'], '/' ); $filename = preg_replace( '/\.(' . $extension . ')\?(?:timestamp|ver)=([^&]+)(?:.*)/', '-$2.$1', $relative_src ); if ( $relative_src === $filename ) { return $url; } $busting_file = $this->busting_path . $filename; $busting_url = $this->get_busting_url( $filename, $extension, $url ); if ( rocket_direct_filesystem()->is_readable( $busting_file ) ) { return $busting_url; } $file = $this->get_file_path( $url ); if ( ! $file ) { return false; } $busting_content = $this->get_file_content( $file ); if ( ! $busting_content ) { return false; } if ( 'css' === $extension ) { $busting_content = $this->rewrite_paths( $file, $busting_file, $busting_content ); } if ( ! $this->write_file( $busting_content, $busting_file ) ) { return false; } return $busting_url; } /** * Gets the cache busting URL * * @since 3.1 * @author Remy Perona * * @param string $filename Cache busting filename. * @param string $extension File extension. * @param string $original_url Original URL for the file. * @return string */ protected function get_busting_url( $filename, $extension, $original_url ) { $url = $this->busting_url . $filename; switch ( $extension ) { case 'css': // This filter is documented in inc/classes/optimization/css/class-abstract-css-optimization.php. $url = apply_filters( 'rocket_css_url', $url, $original_url ); break; case 'js': // This filter is documented in inc/classes/optimization/css/class-abstract-js-optimization.php. $url = apply_filters( 'rocket_js_url', $url, $original_url ); break; } return $url; } } deprecated/Engine/Media/Embeds/EmbedsSubscriber.php 0000644 00000011240 15174677547 0016250 0 ustar 00 <?php namespace WP_Rocket\deprecated\Engine\Media\Embeds; use WP_Rocket\Admin\Options_Data; use WP_Rocket\deprecated\DeprecatedClassTrait; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; /** * Event subscriber to control Embeds behavior. * * @since 3.10 deprecated * @since 3.7 Moved to new architecture. * @since 3.2 */ class EmbedsSubscriber implements Subscriber_Interface { use DeprecatedClassTrait; use ReturnTypesTrait; /** * The Options Data instance. * * @var Options_Data */ private $options; /** * EmbedsSubscriber constructor. * * @param Options_Data $options An Options Data instance. */ public function __construct( Options_Data $options ) { self::deprecated_class( '3.10' ); $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.7 * * @return array */ public static function get_subscribed_events() { return [ 'init' => [ 'remove_wp_vars_and_hooks', 9999 ], 'rest_endpoints' => 'remove_embed_endpoint', 'oembed_response_data' => 'empty_oembed_response_data', 'embed_oembed_discover' => 'return_false', 'rewrite_rules_array' => 'remove_embeds_rewrite_rules', 'enqueue_block_editor_assets' => 'enqueue_disable_embeds_script', 'wp_default_scripts' => 'remove_wp_embed_dependency', ]; } /** * Remove WP Query Vars and hooks relating to embeds. * * Replaces old architecture's rocket_disable_embeds_init(). * * @since 3.7 * * @return void */ public function remove_wp_vars_and_hooks() { if ( ! $this->can_disable_embeds() ) { return; } global $wp; // Remove the embed query var. $wp->public_query_vars = array_diff( $wp->public_query_vars, [ 'embed', ] ); // Don't filter oEmbed results. remove_filter( 'oembed_dataparse', 'wp_filter_oembed_result', 10 ); // Remove oEmbed discovery links. remove_action( 'wp_head', 'wp_oembed_add_discovery_links' ); // Remove oEmbed-specific JavaScript from the front-end and back-end. remove_action( 'wp_head', 'wp_oembed_add_host_js' ); // Remove filter of the oEmbed result before any HTTP requests are made. remove_filter( 'pre_oembed_result', 'wp_filter_pre_oembed_result', 10 ); } /** * Remove all rewrite rules related to embeds. * * @since 3.7 Moved to new architecture. * @since 2.10 * * @param array $rules WordPress rewrite rules. * * @return array Rewrite rules without embeds rules. */ public function remove_embeds_rewrite_rules( $rules ) { if ( empty( $rules ) || ! $this->can_disable_embeds() ) { return $rules; } foreach ( $rules as $rule => $rewrite ) { if ( false !== strpos( $rewrite, 'embed=true' ) ) { unset( $rules[ $rule ] ); } } return $rules; } /** * Remove the oembed/1.0/embed REST route. * * @since 3.7 Moved to new architecture. * @since 3.3.3 * * @param array $endpoints Registered REST API endpoints. * * @return array Filtered REST API endpoints. */ public function remove_embed_endpoint( $endpoints ) { if ( ! $this->can_disable_embeds() ) { return $endpoints; } unset( $endpoints['/oembed/1.0/embed'] ); return $endpoints; } /** * Disables sending internal oEmbed response data in proxy endpoint. * * @since 3.7 Moved to new architecture. * @since 3.3.3 * * @param array $data The response data. * * @return array Response data */ public function empty_oembed_response_data( $data ) { if ( ! rocket_get_constant( 'REST_REQUEST' ) || ! $this->can_disable_embeds() ) { return $data; } return []; } /** * Enqueue JavaScript for the block editor. * * This is used to unregister the `core-embed/wordpress` block type. * * @since 3.7 Moved to new architecture. * @since 3.3.3 * * @return void */ public function enqueue_disable_embeds_script() { if ( ! $this->can_disable_embeds() ) { return; } } /** * Remove wp-embed dependency of core packages. * * @since 3.7 Moved to new architecture. * @since 3.3.3 * * @param \WP_Scripts $scripts WP_Scripts instance, passed by reference. */ public function remove_wp_embed_dependency( $scripts ) { if ( ! $this->can_disable_embeds() ) { return; } if ( ! empty( $scripts->registered['wp-edit-post'] ) ) { $scripts->registered['wp-edit-post']->deps = array_diff( $scripts->registered['wp-edit-post']->deps, [ 'wp-embed' ] ); } } /** * Check for embeds enabled. * * @since 3.7 * * @return bool */ private function can_disable_embeds() { return ! rocket_bypass() && (bool) $this->options->get( 'embeds', 0 ); } } deprecated/3.6.php 0000644 00000103305 15174677547 0007714 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Require deprecated classes. */ require_once __DIR__ . '/DeprecatedClassTrait.php'; require_once __DIR__ . '/Engine/Optimization/QueryString/Remove.php'; require_once __DIR__ . '/Engine/Optimization/QueryString/RemoveSubscriber.php'; /** * Class aliases. */ class_alias( '\WP_Rocket\Engine\Admin\Beacon\ServiceProvider', '\WP_Rocket\ServiceProvider\Beacon' ); class_alias( '\WP_Rocket\Engine\HealthCheck\HealthCheck', '\WP_Rocket\Engine\Admin\HealthCheck' ); class_alias( '\WP_Rocket\Engine\Optimization\ServiceProvider', '\WP_Rocket\ServiceProvider\Optimization_Subscribers' ); class_alias( '\WP_Rocket\Engine\Optimization\IEConditionalSubscriber', '\WP_Rocket\Subscriber\Optimization\IE_Conditionals_Subscriber' ); class_alias( '\WP_Rocket\ThirdParty\Plugins\Smush', '\WP_Rocket\Subscriber\Third_Party\Plugins\Smush_Subscriber' ); class_alias( '\WP_Rocket\Engine\Capabilities\Subscriber', '\WP_Rocket\Subscriber\Plugin\Capabilities_Subscriber' ); /** * Generate the content of advanced-cache.php file. * * @since 3.6 deprecated * @since 3.5.5 Uses rocket_get_constant() for constants. * @since 2.1 Add filter rocket_advanced_cache_file. * @since 2.0.3 * * @return string $buffer The content of advanced-cache.php file */ function get_rocket_advanced_cache_file() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__ . '()', '3.6', '\WP_Rocket\Engine\Cache\AdvancedCache::get_advanced_cache_content()' ); $buffer = "<?php\n"; $buffer .= "defined( 'ABSPATH' ) || exit;\n\n"; // Add a constant to be sure this is our file. $buffer .= "define( 'WP_ROCKET_ADVANCED_CACHE', true );\n\n"; $buffer .= "if ( ! defined( 'WP_ROCKET_CONFIG_PATH' ) ) {\n"; $buffer .= "\tdefine( 'WP_ROCKET_CONFIG_PATH', WP_CONTENT_DIR . '/wp-rocket-config/' );\n"; $buffer .= "}\n\n"; // Include the Mobile Detect class if we have to create a different caching file for mobile. if ( is_rocket_generate_caching_mobile_files() ) { $vendor_path = rocket_get_constant( 'WP_ROCKET_VENDORS_PATH' ); $buffer .= "if ( file_exists( '" . $vendor_path . "classes/class-rocket-mobile-detect.php' ) && ! class_exists( 'Rocket_Mobile_Detect' ) ) {\n"; $buffer .= "\tinclude_once '" . $vendor_path . "classes/class-rocket-mobile-detect.php';\n"; $buffer .= "}\n\n"; } // Register a class autoloader and include the process file. $buffer .= "if ( version_compare( phpversion(), '" . rocket_get_constant( 'WP_ROCKET_PHP_VERSION' ) . "' ) >= 0 ) {\n\n"; // Class autoloader. $autoloader = rocket_direct_filesystem()->get_contents( rocket_get_constant( 'WP_ROCKET_INC_PATH' ) . 'process-autoloader.php' ); if ( $autoloader ) { $autoloader = preg_replace( '@^<\?php\s*@', '', $autoloader ); $autoloader = str_replace( [ "\n", "\n\t\n" ], [ "\n\t", "\n\n" ], trim( $autoloader ) ); $autoloader = str_replace( 'WP_ROCKET_PATH', "'" . rocket_get_constant( 'WP_ROCKET_PATH' ) . "'", $autoloader ); $buffer .= "\t$autoloader\n\n"; } // Initialize the Cache class and process. $buffer .= "\t" . 'if ( ! class_exists( \'\WP_Rocket\Buffer\Cache\' ) ) { if ( ! defined( \'DONOTROCKETOPTIMIZE\' ) ) { define( \'DONOTROCKETOPTIMIZE\', true ); // WPCS: prefix ok. } return; } $rocket_config_class = new \WP_Rocket\Buffer\Config( [ \'config_dir_path\' => \'' . rocket_get_constant( 'WP_ROCKET_CONFIG_PATH' ) . '\', ] ); ( new \WP_Rocket\Buffer\Cache( new \WP_Rocket\Buffer\Tests( $rocket_config_class ), $rocket_config_class, [ \'cache_dir_path\' => \'' . rocket_get_constant( 'WP_ROCKET_CACHE_PATH' ) . '\', ] ) )->maybe_init_process();' . "\n"; $buffer .= "} else {\n"; // Add a constant to provent include issue. $buffer .= "\tdefine( 'WP_ROCKET_ADVANCED_CACHE_PROBLEM', true );\n"; $buffer .= "}\n"; /** * Filter the content of advanced-cache.php file. * * @since 2.1 * * @param string $buffer The content that will be printed in advanced-cache.php. */ return (string) apply_filters( 'rocket_advanced_cache_file', $buffer ); } /** * This warning is displayed when the advanced-cache.php file isn't writeable * * @since 3.6 deprecated * @since 2.0 */ function rocket_warning_advanced_cache_permissions() { _deprecated_function( __FUNCTION__ . '()', '3.6', '\WP_Rocket\Engine\Cache\AdvancedCache::notice_permissions()' ); $advanced_cache_file = WP_CONTENT_DIR . '/advanced-cache.php'; if ( current_user_can( 'rocket_manage_options' ) && ! rocket_direct_filesystem()->is_writable( $advanced_cache_file ) && ( ! defined( 'WP_ROCKET_ADVANCED_CACHE' ) || ! WP_ROCKET_ADVANCED_CACHE ) && rocket_valid_key() ) { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = rocket_notice_writing_permissions( basename( WP_CONTENT_DIR ) . '/advanced-cache.php' ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, 'dismiss_button' => __FUNCTION__, 'readonly_content' => get_rocket_advanced_cache_file(), ] ); } } /** * This warning is displayed when the advanced-cache.php file isn't ours * * @since 3.6 Deprecated * @since 2.2 */ function rocket_warning_advanced_cache_not_ours() { _deprecated_function( __FUNCTION__ . '()', '3.6', '\WP_Rocket\Engine\Cache\AdvancedCache::notice_content_not_ours()' ); if ( ! ( 'plugins.php' === $GLOBALS['pagenow'] && isset( $_GET['activate'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended && current_user_can( 'rocket_manage_options' ) && ! defined( 'WP_ROCKET_ADVANCED_CACHE' ) && ( defined( 'WP_CACHE' ) && WP_CACHE ) && get_rocket_option( 'version' ) === WP_ROCKET_VERSION && rocket_valid_key() ) { $message = rocket_notice_writing_permissions( basename( WP_CONTENT_DIR ) . '/advanced-cache.php' ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, ] ); } } /** * Exclude fusion styles from cache busting to prevent cache dir issues * * @deprecated 3.6 * @author Remy Perona * * @param array $excluded_files An array of excluded files. * @return array */ function rocket_exclude_avada_dynamic_css( $excluded_files ) { _deprecated_function( __FUNCTION__ . '()', '3.6' ); $upload_dir = wp_upload_dir(); $excluded_files[] = rocket_clean_exclude_file( $upload_dir['baseurl'] . '/fusion-styles/(.*)' ); return $excluded_files; } /** * Excludes Uncode JS files from remove query strings * * @deprecated 3.6 * @since 3.3.3 * @author Remy Perona * * @param array $exclude_busting Array of CSS and JS filepaths to be excluded. * @return array */ function rocket_exclude_busting_uncode( $exclude_busting ) { _deprecated_function( __FUNCTION__ . '()', '3.6' ); // CSS files. $exclude_busting[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/css/style.css' ); $exclude_busting[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/css/uncode-icons.css' ); $exclude_busting[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/css/style-custom.css' ); // JS files. $exclude_busting[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.js' ); $exclude_busting[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/init.min.js' ); $exclude_busting[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/app.js' ); $exclude_busting[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/app.min.js' ); $exclude_busting[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/plugins.js' ); $exclude_busting[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/plugins.min.js' ); return $exclude_busting; } /** * Purge the cache when the beaver builder layout is updated to update the minified files content & URL * * @deprecated 3.6 * @since 2.9 Also clear the cache busting folder * @since 2.8.6 */ function rocket_beaver_builder_clean_domain() { _deprecated_function( __FUNCTION__ . '()', '3.6', 'WP_Rocket\ThirdParty\Plugins\PageBuilder\BeaverBuilder::purge_cache' ); rocket_clean_minify(); rocket_clean_domain(); } /** * Returns paths used for cache busting * * @since 2.9 * @deprecated 3.6 * @author Remy Perona * * @param string $filename name of the cache busting file. * @param string $extension file extension. * @return array Array of paths used for cache busting */ function rocket_get_cache_busting_paths( $filename, $extension ) { _deprecated_function( __FUNCTION__ . '()', '3.6' ); $blog_id = get_current_blog_id(); $cache_busting_path = WP_ROCKET_CACHE_BUSTING_PATH . $blog_id; $filename = rocket_realpath( rtrim( str_replace( [ ' ', '%20' ], '-', $filename ) ) ); $cache_busting_filepath = $cache_busting_path . $filename; $cache_busting_url = WP_ROCKET_CACHE_BUSTING_URL . $blog_id . $filename; switch ( $extension ) { case 'css': /** This filter is documented in inc/functions/minify.php */ $cache_busting_url = apply_filters( 'rocket_css_url', $cache_busting_url ); break; case 'js': /** This filter is documented in inc/functions/minify.php */ $cache_busting_url = apply_filters( 'rocket_js_url', $cache_busting_url ); break; } return [ 'bustingpath' => $cache_busting_path, 'filepath' => $cache_busting_filepath, 'url' => $cache_busting_url, ]; } /** * Caches SCCSS code & remove the default enqueued URL * * @since 2.9 * @deprecated 3.6 * * @author Remy Perona */ function rocket_cache_sccss() { _deprecated_function( __FUNCTION__ . '()', '3.6', '\WP_Rocket\ThirdParty\Plugins\SimpleCustomCss::cache_sccss()' ); $sccss = rocket_get_cache_busting_paths( 'sccss.css', 'css' ); if ( ! file_exists( $sccss['filepath'] ) ) { rocket_sccss_create_cache_file( $sccss['bustingpath'], $sccss['filepath'] ); } if ( file_exists( $sccss['filepath'] ) ) { wp_enqueue_style( 'scss', $sccss['url'], '', filemtime( $sccss['filepath'] ) ); remove_action( 'wp_enqueue_scripts', 'sccss_register_style', 99 ); } } /** * Deletes & recreates cache for SCCSS code * * @since 2.9 * @deprecated 3.6 * * @author Remy Perona */ function rocket_delete_sccss_cache_file() { _deprecated_function( __FUNCTION__ . '()', '3.6', '\WP_Rocket\ThirdParty\Plugins\SimpleCustomCss::update_cache_file()' ); $sccss = rocket_get_cache_busting_paths( 'sccss.css', 'css' ); array_map( 'unlink', glob( $sccss['bustingpath'] . 'sccss*.css' ) ); rocket_clean_domain(); rocket_sccss_create_cache_file( $sccss['bustingpath'], $sccss['filepath'] ); } /** * Returns the filename for SCSSS cache file * * @since 2.9 * @deprecated 3.6 * * @author Remy Perona * * @param string $filename filename. * @return string filename */ function rocket_sccss_cache_busting_filename( $filename ) { _deprecated_function( __FUNCTION__ . '()', '3.6' ); if ( false !== strpos( $filename, 'sccss' ) ) { return preg_replace( '/(?:.*)(sccss(?:.*))/i', '$1', $filename ); } return $filename; } /** * Creates the cache file for SCCSS code * * @since 2.9 * @deprecated 3.6 * * @author Remy Perona * * @param string $cache_busting_path Path to the cache busting directory. * @param string $cache_sccss_filepath Path to the sccss cache file. */ function rocket_sccss_create_cache_file( $cache_busting_path, $cache_sccss_filepath ) { _deprecated_function( __FUNCTION__ . '()', '3.6', '\WP_Rocket\ThirdParty\Plugins\SimpleCustomCss::create_cache_file()' ); $options = get_option( 'sccss_settings' ); $raw_content = isset( $options['sccss-content'] ) ? $options['sccss-content'] : ''; $content = wp_kses( $raw_content, [ '\'', '\"' ] ); $content = str_replace( '>', '>', $content ); if ( ! rocket_direct_filesystem()->is_dir( $cache_busting_path ) ) { rocket_mkdir_p( $cache_busting_path ); } rocket_put_content( $cache_sccss_filepath, $content ); } /** * This warning is displayed when the wp-config.php file isn't writable * * @since 3.6.1 deprecated * @since 2.0 */ function rocket_warning_wp_config_permissions() { _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\Engine\Cache\WPCache::notice_wp_config_permissions()' ); $config_file = rocket_find_wpconfig_path(); if ( ! ( 'plugins.php' === $GLOBALS['pagenow'] && isset( $_GET['activate'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended && current_user_can( 'rocket_manage_options' ) && ( ! rocket_direct_filesystem()->is_writable( $config_file ) && ( ! defined( 'WP_CACHE' ) || ! WP_CACHE ) ) && rocket_valid_key() ) { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = rocket_notice_writing_permissions( 'wp-config.php' ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, 'dismiss_button' => __FUNCTION__, 'readonly_content' => '/** Enable Cache by ' . WP_ROCKET_PLUGIN_NAME . " */\r\ndefine( 'WP_CACHE', true );\r\n", ] ); } } /** * Try to find the correct wp-config.php file, support one level up in file tree. * * @since 3.6 deprecated * @since 2.1 * * @return string|bool The path of wp-config.php file or false if not found. */ function rocket_find_wpconfig_path() { _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\Engine\Cache\WPCache::find_wpconfig_path()' ); /** * Filter the wp-config's filename. * * @since 2.11 * * @param string $filename The WP Config filename, without the extension. */ $config_file_name = apply_filters( 'rocket_wp_config_name', 'wp-config' ); $abspath = rocket_get_constant( 'ABSPATH' ); $config_file = "{$abspath}{$config_file_name}.php"; $filesystem = rocket_direct_filesystem(); if ( $filesystem->exists( $config_file ) && $filesystem->is_writable( $config_file ) ) { return $config_file; } $abspath_parent = dirname( $abspath ) . DIRECTORY_SEPARATOR; $config_file_alt = "{$abspath_parent}{$config_file_name}.php"; if ( $filesystem->exists( $config_file_alt ) && $filesystem->is_writable( $config_file_alt ) && ! $filesystem->exists( "{$abspath_parent}wp-settings.php" ) ) { return $config_file_alt; } // No writable file found. return false; } /** * Define WP_CACHE to true if it's not defined yet. * * @since 3.6.1 deprecated * @since 2.6 */ function rocket_maybe_set_wp_cache_define() { _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\Engine\Cache\WPCache::maybe_set_wp_cache()' ); if ( defined( 'WP_CACHE' ) && ! WP_CACHE ) { set_rocket_wp_cache_define( true ); } } /** * Get all dates archives urls associated to a specific post. * * @since 3.6.1 deprecated * @since 1.0 * * @param int $post_id The post ID. * * @return array $urls List of dates URLs on success; else, an empty []. */ function get_rocket_post_dates_urls( $post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\Engine\Cache\Purge::purge_dates_archives()' ); $time = get_the_time( 'Y-m-d', $post_id ); if ( empty( $time ) ) { return []; } // Extract and prep the year, month, and day. $date = explode( '-', $time ); $year = trailingslashit( get_year_link( $date[0] ) ); $month = trailingslashit( get_month_link( $date[0], $date[1] ) ); $urls = [ "{$year}index.html", "{$year}index.html_gzip", $year . $GLOBALS['wp_rewrite']->pagination_base, "{$month}index.html", "{$month}index.html_gzip", $month . $GLOBALS['wp_rewrite']->pagination_base, get_day_link( $date[0], $date[1], $date[2] ), ]; /** * Filter the list of dates URLs. * * @since 1.1.0 * * @param array $urls List of dates URLs. */ return (array) apply_filters( 'rocket_post_dates_urls', $urls ); } /** * Added or set the value of the WP_CACHE constant * * @since 3.6.1 deprecated * @since 2.0 * * @param bool $turn_it_on The value of WP_CACHE constant. * @return void */ function set_rocket_wp_cache_define( $turn_it_on ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\Engine\Cache\WPCache::set_wp_cache_constant()' ); // If WP_CACHE is already define, return to get a coffee. if ( ! rocket_valid_key() || ( $turn_it_on && defined( 'WP_CACHE' ) && WP_CACHE ) ) { return; } if ( defined( 'IS_PRESSABLE' ) && IS_PRESSABLE ) { return; } // Get path of the config file. $config_file_path = rocket_find_wpconfig_path(); if ( ! $config_file_path ) { return; } $filesystem = rocket_direct_filesystem(); // Get content of the config file. $config_file_contents = $filesystem->get_contents( $config_file_path ); // Get the value of WP_CACHE constant. $turn_it_on = $turn_it_on ? 'true' : 'false'; /** * Filter allow to change the value of WP_CACHE constant * * @since 2.1 * * @param string $turn_it_on The value of WP_CACHE constant. */ $turn_it_on = apply_filters( 'set_rocket_wp_cache_define', $turn_it_on ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals // Get WP_CACHE constant define. $constant = "define('WP_CACHE', $turn_it_on); // Added by WP Rocket"; // Lets find out if the constant WP_CACHE is defined or not. $wp_cache_found = preg_match( '/^define\(\s*\'WP_CACHE\',(.*)\)/m', $config_file_contents, $matches ); if ( ! $wp_cache_found ) { $config_file_contents = preg_replace( '/(<\?php)/i', "<?php\r\n{$constant}\r\n", $config_file_contents ); } elseif ( ! empty( $matches[1] ) && $matches[1] !== $turn_it_on ) { $config_file_contents = preg_replace( '/^define\(\s*\'WP_CACHE\',(.*)\).+/m', $constant, $config_file_contents ); } // Insert the constant in wp-config.php file. rocket_put_content( $config_file_path, $config_file_contents ); } /** * Get all terms archives urls associated to a specific post * * @since 3.6.1 deprecated * @since 1.0 * * @param int $post_id The post ID. * @return array $urls List of taxonomies URLs */ function get_rocket_post_terms_urls( $post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\Engine\Cache\Purge::get_post_terms_urls()' ); $urls = []; $taxonomies = get_object_taxonomies( get_post_type( $post_id ), 'objects' ); foreach ( $taxonomies as $taxonomy ) { if ( ! $taxonomy->public || 'product_shipping_class' === $taxonomy->name ) { continue; } // Get the terms related to post. $terms = get_the_terms( $post_id, $taxonomy->name ); if ( empty( $terms ) ) { continue; } foreach ( $terms as $term ) { $term_url = get_term_link( $term->slug, $taxonomy->name ); if ( ! is_wp_error( $term_url ) ) { $urls[] = $term_url; } if ( ! is_taxonomy_hierarchical( $taxonomy->name ) ) { continue; } $ancestors = (array) get_ancestors( $term->term_id, $taxonomy->name ); foreach ( $ancestors as $ancestor ) { $ancestor_object = get_term( $ancestor, $taxonomy->name ); if ( ! $ancestor_object instanceof WP_Term ) { continue; } $ancestor_term_url = get_term_link( $ancestor_object->slug, $taxonomy->name ); if ( ! is_wp_error( $ancestor_term_url ) ) { $urls[] = $ancestor_term_url; } } } } /** * Filter the list of taxonomies URLs * * @since 1.1.0 * * @param array $urls List of taxonomies URLs */ return apply_filters( 'rocket_post_terms_urls', $urls ); } /** * Rules to serve gzip compressed CSS & JS files if they exists and client accepts gzip * * @since 3.6.0.3 deprecated * @since 3.6.0.2 Update rules used to prevent content encoding issue * @since 3.6 * @author Remy Perona * * @return string */ function rocket_get_compressed_assets_rules() { _deprecated_function( __FUNCTION__ . '()', '3.6.0.3' ); $rules = <<<HTACCESS <IfModule mod_headers.c> RewriteCond %{HTTP:Accept-Encoding} gzip RewriteCond %{REQUEST_FILENAME}\.gz -f RewriteRule \.(css|js)$ %{REQUEST_URI}.gz [L] # Prevent mod_deflate double gzip RewriteRule \.gz$ - [E=no-gzip:1] <FilesMatch "\.gz$"> # Serve correct content types <IfModule mod_mime.c> # (1) RemoveType gz # Serve correct content types AddType text/css css.gz AddType text/javascript js.gz # Serve correct content charset AddCharset utf-8 .css.gz \ .js.gz </IfModule> # Force proxies to cache gzipped and non-gzipped files separately Header append Vary Accept-Encoding </FilesMatch> # Serve correct encoding type AddEncoding gzip .gz </IfModule> HTACCESS; return apply_filters( 'rocket_htaccess_compressed_assets', $rules ); } /** * Get list of CSS files to be excluded from async CSS. * * @since 3.6.2 deprecated * @since 2.10 * @author Remy Perona * * @return array An array of URLs for the CSS files to be excluded. */ function get_rocket_exclude_async_css() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__ . '()', '3.6.2', '\WP_Rocket\Engine\CriticalPath\CriticalCSS::get_exclude_async_css()' ); /** * Filter list of async CSS files * * @since 2.10 * @author Remy Perona * * @param array $exclude_async_css An array of URLs for the CSS files to be excluded. */ $exclude_async_css = (array) apply_filters( 'rocket_exclude_async_css', [] ); $exclude_async_css = array_filter( $exclude_async_css ); $exclude_async_css = array_flip( array_flip( $exclude_async_css ) ); return $exclude_async_css; } /** * Changes the text on the Varnish one-click block. * * @deprecated 3.6.1 * @since 3.0 * * @param array $settings Field settings data. * * @return array modified field settings data. */ function rocket_wpengine_varnish_field( $settings ) { _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\ThirdParty\Hostings\WPEngine::varnish_addon_title' ); $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'WP Engine' ); return $settings; } /** * Conflict with WP Engine caching system. * * @deprecated 3.6.1 * @since 2.6.4 * */ function rocket_stop_generate_caching_files_on_wpengine() { _deprecated_function( __FUNCTION__ . '()', '3.6.1' ); add_filter( 'do_rocket_generate_caching_files', '__return_false' ); } /** * Run WP Rocket preload bot after purged the Varnish cache via WP Engine Hosting. * * @deprecated 3.6.1 * @since 2.6.4 */ function rocket_run_rocket_bot_after_wpengine() { _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\ThirdParty\Hostings\WPEngine::run_rocket_bot_after_wpengine' ); if ( wpe_param( 'purge-all' ) && defined( 'PWP_NAME' ) && check_admin_referer( PWP_NAME . '-config' ) ) { // Preload cache. run_rocket_bot(); run_rocket_sitemap_preload(); } } /** * Call the cache server to purge the cache with WP Engine hosting. * * @deprecated 3.6.1 * @since 2.6.4 */ function rocket_clean_wpengine() { _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\ThirdParty\Hostings\WPEngine::clean_wpengine' ); if ( method_exists( 'WpeCommon', 'purge_memcached' ) ) { WpeCommon::purge_memcached(); } if ( method_exists( 'WpeCommon', 'purge_varnish_cache' ) ) { WpeCommon::purge_varnish_cache(); } } /** * Gets WP Engine CDN Domain. * * @deprecated 3.6.1 * @since 2.8.6 * * return string $cdn_domain the WP Engine CDN Domain. */ function rocket_get_wp_engine_cdn_domain() { _deprecated_function( __FUNCTION__ . '()', '3.6.1' ); global $wpe_netdna_domains, $wpe_netdna_domains_secure; $cdn_domain = ''; $is_ssl = ''; if ( isset( $_SERVER['HTTPS'] ) ) { $is_ssl = sanitize_text_field( wp_unslash( $_SERVER['HTTPS'] ) ); } if ( preg_match( '/^[oO][fF]{2}$/', $is_ssl ) ) { $is_ssl = false; // have seen this! } $native_schema = $is_ssl ? 'https' : 'http'; $domains = $wpe_netdna_domains; // Determine the CDN, if any. if ( $is_ssl ) { $domains = $wpe_netdna_domains_secure; } $wpengine = WpeCommon::instance(); $cdn_domain = $wpengine->get_cdn_domain( $domains, home_url(), $is_ssl ); if ( ! empty( $cdn_domain ) ) { $cdn_domain = $native_schema . '://' . $cdn_domain; } return $cdn_domain; } /** * Add WP Rocket footprint on Buffer. * * @deprecated 3.6.1 * @since 3.3.2 * * @param string $buffer HTML content. * * @return string HTML with WP Rocket footprint. */ function rocket_wpengine_add_footprint( $buffer ) { _deprecated_function( __FUNCTION__ . '()', '3.6.1', '\WP_Rocket\ThirdParty\Hostings\WPEngine::add_footprint' ); if ( ! preg_match( '/<\/html>/i', $buffer ) ) { return $buffer; } $footprint = defined( 'WP_ROCKET_WHITE_LABEL_FOOTPRINT' ) ? "\n" . '<!-- Optimized for great performance' : "\n" . '<!-- This website is like a Rocket, isn\'t it? Performance optimized by ' . WP_ROCKET_PLUGIN_NAME . '. Learn more: https://wp-rocket.me'; $footprint .= ' -->'; return $buffer . $footprint; } /** * Tell WP what to do when plugin is deactivated. * * @since 3.6.3 deprecated * @since 1.0 */ function rocket_deactivation() { _deprecated_function( __FUNCTION__ . '()', '3.6.3', '\WP_Rocket\Engine\Deactivation\Deactivation::deactivate_plugin' ); global $is_apache; $filesystem = rocket_direct_filesystem(); $wp_cache = new WPCache( $filesystem ); if ( ! isset( $_GET['rocket_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['rocket_nonce'] ), 'force_deactivation' ) ) { $causes = []; // .htaccess problem. if ( $is_apache && ! $filesystem->is_writable( get_home_path() . '.htaccess' ) ) { $causes[] = 'htaccess'; } // wp-config problem. if ( ! $wp_cache->find_wpconfig_path() && // This filter is documented in inc/Engine/Cache/WPCache.php. (bool) apply_filters( 'rocket_set_wp_cache_constant', true ) ) { $causes[] = 'wpconfig'; } if ( count( $causes ) ) { set_transient( get_current_user_id() . '_donotdeactivaterocket', $causes ); wp_safe_redirect( wp_get_referer() ); die(); } } // Delete config files. rocket_delete_config_file(); if ( ! count( glob( WP_ROCKET_CONFIG_PATH . '*.php' ) ) ) { // Delete All WP Rocket rules of the .htaccess file. flush_rocket_htaccess( true ); // Remove WP_CACHE constant in wp-config.php. $wp_cache->set_wp_cache_constant( false ); // Delete content of advanced-cache.php. rocket_put_content( WP_CONTENT_DIR . '/advanced-cache.php', '' ); } // Update customer key & licence. wp_remote_get( WP_ROCKET_WEB_API . 'pause-licence.php', [ 'blocking' => false, ] ); // Delete transients. delete_transient( 'rocket_check_licence_30' ); delete_transient( 'rocket_check_licence_1' ); delete_site_transient( 'update_wprocket_response' ); // Unschedule WP Cron events. wp_clear_scheduled_hook( 'rocket_facebook_tracking_cache_update' ); wp_clear_scheduled_hook( 'rocket_google_tracking_cache_update' ); wp_clear_scheduled_hook( 'rocket_cache_dir_size_check' ); /** * WP Rocket deactivation. * * @since 3.1.5 * @author Grégory Viguier */ do_action( 'rocket_deactivation' ); ( new Capabilities_Subscriber() )->remove_rocket_capabilities(); } /** * Tell WP what to do when plugin is activated. * * @since 3.6.3 * @since 1.1.0 */ function rocket_activation() { _deprecated_function( __FUNCTION__ . '()', '3.6.3', '\WP_Rocket\Engine\Activation\Activation::deactivate_plugin' ); ( new Capabilities_Subscriber() )->add_rocket_capabilities(); $filesystem = rocket_direct_filesystem(); $wp_cache = new WPCache( $filesystem ); // Last constants. define( 'WP_ROCKET_PLUGIN_NAME', 'WP Rocket' ); define( 'WP_ROCKET_PLUGIN_SLUG', sanitize_key( WP_ROCKET_PLUGIN_NAME ) ); if ( defined( 'SUNRISE' ) && SUNRISE === 'on' && function_exists( 'domain_mapping_siteurl' ) ) { require WP_ROCKET_INC_PATH . 'domain-mapping.php'; } require WP_ROCKET_FUNCTIONS_PATH . 'options.php'; require WP_ROCKET_FUNCTIONS_PATH . 'formatting.php'; require WP_ROCKET_FUNCTIONS_PATH . 'i18n.php'; require WP_ROCKET_FUNCTIONS_PATH . 'htaccess.php'; if ( rocket_valid_key() ) { // Add All WP Rocket rules of the .htaccess file. flush_rocket_htaccess(); // Add WP_CACHE constant in wp-config.php. $wp_cache->set_wp_cache_constant( true ); } // Create the cache folders (wp-rocket & min). rocket_init_cache_dir(); // Create the config folder (wp-rocket-config). rocket_init_config_dir(); // Create advanced-cache.php file. rocket_generate_advanced_cache_file( new AdvancedCache( WP_ROCKET_PATH . 'views/cache/', $filesystem ) ); /** * WP Rocket activation. * * @since 3.1.5 * @author Grégory Viguier */ do_action( 'rocket_activation' ); // Update customer key & licence. wp_remote_get( WP_ROCKET_WEB_API . 'activate-licence.php', [ 'blocking' => false, ] ); wp_remote_get( home_url(), [ 'timeout' => 0.01, 'blocking' => false, 'user-agent' => 'WP Rocket/Homepage Preload', 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound ] ); } /** * Excludes Divi's Salvatorre script from JS minification * * Exclude it to prevent an error after minification/concatenation * * @since 3.6.3 deprecated * @since 2.9 * @author Remy Perona * * @param Array $excluded_js An array of JS paths to be excluded. * @return Array the updated array of paths */ function rocket_exclude_js_divi( $excluded_js ) { _deprecated_function( __FUNCTION__ . '()', '3.6.3', '\WP_Rocket\ThirdParty\Themes\Divi::exclude_js' ); if ( defined( 'ET_BUILDER_URI' ) ) { $excluded_js[] = str_replace( home_url(), '', ET_BUILDER_URI ) . '/scripts/salvattore.min.js'; } return $excluded_js; } /** * Changes the text on the Varnish one-click block. * * @since 3.1 * * @param array $settings Field settings data. * * @return array modified field settings data. */ function rocket_o2switch_varnish_field( $settings ) { _deprecated_function( __FUNCTION__ . '()', '3.6.3', '\WP_Rocket\ThirdParty\Hostings\O2Switch::varnish_addon_title' ); // Translators: %s = Hosting name. $settings['varnish_auto_purge']['title'] = sprintf( __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'o2switch' ); return $settings; } /** * Purge all the domain. * * @since 3.1 * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. * @param string $url The home url. */ function rocket_o2switch_varnish_clean_domain( $root, $lang, $url ) { _deprecated_function( __FUNCTION__ . '()', '3.6.3', '\WP_Rocket\ThirdParty\Hostings\O2Switch::varnish_clean_domain' ); rocket_o2switch_varnish_http_purge( trailingslashit( $url ) . '?vregex' ); } /** * Purge a specific page. * * @since 3.1 * * @param string $url The url to purge. */ function rocket_o2switch_varnish_clean_file( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.6.3', '\WP_Rocket\ThirdParty\Hostings\O2Switch::varnish_clean_file' ); rocket_o2switch_varnish_http_purge( trailingslashit( $url ) . '?vregex' ); } /** * Purge the homepage and its pagination. * * @since 3.1 * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. */ function rocket_o2switch_varnish_clean_home( $root, $lang ) { _deprecated_function( __FUNCTION__ . '()', '3.6.3', '\WP_Rocket\ThirdParty\Hostings\O2Switch::varnish_clean_home' ); $home_url = trailingslashit( get_rocket_i18n_home_url( $lang ) ); $home_pagination_url = $home_url . trailingslashit( $GLOBALS['wp_rewrite']->pagination_base ) . '?vregex'; rocket_o2switch_varnish_http_purge( $home_url ); rocket_o2switch_varnish_http_purge( $home_pagination_url ); } /** * Send data to Varnish. * * @since 3.1 * * @param string $url The URL to purge. */ function rocket_o2switch_varnish_http_purge( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.6.3', '\WP_Rocket\ThirdParty\Hostings\O2Switch::varnish_http_purge' ); $parse_url = get_rocket_parse_url( $url ); // This filter is documented in inc/functions/varnish.php. $headers = apply_filters( 'rocket_varnish_purge_headers', [ /** * Filters the host value passed in the request headers * * @since 2.8.15 * @param string The host */ 'host' => apply_filters( 'rocket_varnish_purge_request_host', $parse_url['host'] ), 'X-VC-Purge-Key' => O2SWITCH_VARNISH_PURGE_KEY, ] ); if ( 'vregex' === $parse_url['query'] ) { $headers['X-Purge-Regex'] = '.*'; } /** * Filter the Varnish IP to call * * @since 2.6.8 * * @param string The Varnish IP */ $varnish_ip = apply_filters( 'rocket_varnish_ip', [] ); if ( defined( 'WP_ROCKET_VARNISH_IP' ) && ! $varnish_ip ) { $varnish_ip = WP_ROCKET_VARNISH_IP; } /** * Filter the HTTP protocol (scheme) * * @since 2.7.3 * * @param string The HTTP protocol */ $scheme = apply_filters( 'rocket_varnish_http_purge_scheme', $parse_url['scheme'] ); $parse_url['host'] = ( $varnish_ip ) ? $varnish_ip : $parse_url['host']; $purgeme = $scheme . '://' . $parse_url['host'] . $parse_url['path']; wp_remote_request( $purgeme, [ 'method' => 'PURGE', 'blocking' => false, 'redirection' => 0, 'headers' => $headers, ] ); } /** * Remove expiration on HTML to prevent issue with Varnish cache. * * @since 3.1 * * @param string $rules htaccess rules. * * @return string Updated htaccess rules. */ function rocket_o2switch_remove_html_expire( $rules ) { _deprecated_function( __FUNCTION__ . '()', '3.6.3', '\WP_Rocket\ThirdParty\Hostings\O2Switch::remove_html_expire' ); $rules = preg_replace( '@\s*#\s*Your document html@', '', $rules ); $rules = preg_replace( '@\s*ExpiresByType text/html\s*"access plus \d+ (seconds|minutes|hour|week|month|year)"@', '', $rules ); return $rules; } deprecated/3.13.php 0000644 00000005442 15174677547 0007775 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Add cookies when we activate any goetargetingWP plugin. * * @since 2.10.3 * @author Damian Logghe */ function rocket_activate_geotargetingwp() { _deprecated_function( __FUNCTION__ . '()', '3.13.3' ); add_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 72 ); add_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_geotargetingwp_dynamic_cookies' ); add_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_geotargetingwp_mandatory_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } /** * Remove cookies when we deactivate the plugin. * * @since 2.10.3 * @author Damian Logghe */ function rocket_deactivate_geotargetingwp() { _deprecated_function( __FUNCTION__ . '()', '3.13.3' ); // add into db a record saying we deactivated one of the family plugins. update_option( 'geotWP-deactivated', true ); remove_filter( 'rocket_htaccess_mod_rewrite', '__return_false', 72 ); remove_filter( 'rocket_cache_dynamic_cookies', 'rocket_add_geotargetingwp_dynamic_cookies' ); remove_filter( 'rocket_cache_mandatory_cookies', 'rocket_add_geotargetingwp_mandatory_cookie' ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } /** * Add the GeotargetingWP cookies to generate caching files depending on their values. * * @since 2.10.3 * @author Damian Logghe * * @param Array $cookies An array of cookies. * @return Array Updated array of cookies */ function rocket_add_geotargetingwp_dynamic_cookies( $cookies ) { _deprecated_function( __FUNCTION__ . '()', '3.13.3' ); return rocket_add_geot_cookies( $cookies ); } /** * Add the GeotargetingWP cookies to the list of mandatory cookies before to generate caching files. * * @since 2.10.3 * @author Damian Logghe * * @param Array $cookies An array of cookies. * @return Array Updated array of cookies */ function rocket_add_geotargetingwp_mandatory_cookie( $cookies ) { _deprecated_function( __FUNCTION__ . '()', '3.13.3' ); return rocket_add_geot_cookies( $cookies ); } /** * Let users modify cache level by default set to country. * * @since 2.10.3 * @author Damian Logghe * * @param Array $cookies An array of cookies. * @return Array Updated array of cookies */ function rocket_add_geot_cookies( $cookies ) { _deprecated_function( __FUNCTION__ . '()', '3.13.3' ); // valid options are country, state, city. $enabled_cookies = apply_filters( 'rocket_geotargetingwp_enabled_cookies', [ 'country' ] ); foreach ( $enabled_cookies as $enabled_cookie ) { if ( ! in_array( 'geot_rocket_' . $enabled_cookie, $cookies, true ) ) { $cookies[] = 'geot_rocket_' . $enabled_cookie; } } return $cookies; } deprecated/3.9.php 0000644 00000007256 15174677547 0007727 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; class_alias( '\WP_Rocket\ThirdParty\Hostings\LiteSpeed', '\WP_Rocket\Subscriber\Third_Party\Hostings\Litespeed_Subscriber'); /** * Changes the text on the Varnish one-click block. * * @since 3.9.1 deprecated * @since 3.0 * @author Remy Perona * * @param array $settings Field settings data. * * @return array modified field settings data. */ function rocket_godaddy_varnish_field( $settings ) { _deprecated_function( __FUNCTION__ . '()', '3.9.1', '\WP_Rocket\ThirdParty\Hostings\Godaddy::godaddy_varnish_field' ); $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'GoDaddy' ); return $settings; } /** * Remove expiration on HTML to prevent issue with Varnish cache. * * @since 3.9.1 deprecated * @since 2.9.5 * @author Remy Perona * * @param string $rules htaccess rules. * @return string Updated htaccess rules. */ function rocket_remove_html_expire_goddady( $rules ) { _deprecated_function( __FUNCTION__ . '()', '3.9.1', '\WP_Rocket\ThirdParty\Hostings\Godaddy::remove_html_expire_goddady' ); $rules = preg_replace( '@\s*#\s*Your document html@', '', $rules ); $rules = preg_replace( '@\s*ExpiresByType text/html\s*"access plus \d+ (seconds|minutes|hour|week|month|year)"@', '', $rules ); return $rules; } /** * Call the Varnish server to purge the cache with GoDaddy. * * @since 3.9.1 deprecated * @since 2.9.5 * * @return void */ function rocket_clean_domain_godaddy() { _deprecated_function( __FUNCTION__ . '()', '3.9.1', '\WP_Rocket\ThirdParty\Hostings\Godaddy::clean_domain_godaddy' ); rocket_godaddy_request( 'BAN' ); } /** * Call the Varnish server to purge a specific URL with GoDaddy. * * @since 3.9.1 deprecated * @since 2.9.5 * * @param string $url URL to purge. * @return void */ function rocket_clean_file_godaddy( $url ) { _deprecated_function( __FUNCTION__ . '()', '3.9.1', '\WP_Rocket\ThirdParty\Hostings\Godaddy::clean_file_godaddy' ); rocket_godaddy_request( 'PURGE', home_url( $url ) ); } /** * Call the Varnish server to purge the home with GoDaddy. * * @since 3.9.1 deprecated * @since 2.9.5 * * @param string $root root URL. * @param string $lang language code. * @return void */ function rocket_clean_home_godaddy( $root, $lang ) { _deprecated_function( __FUNCTION__ . '()', '3.9.1', '\WP_Rocket\ThirdParty\Hostings\Godaddy::clean_home_godaddy' ); $home_url = trailingslashit( get_rocket_i18n_home_url( $lang ) ); $home_pagination_url = $home_url . trailingslashit( $GLOBALS['wp_rewrite']->pagination_base ); rocket_godaddy_request( 'PURGE', $home_url ); rocket_godaddy_request( 'PURGE', $home_pagination_url ); } /** * Perform the call to the Varnish server to purge * * @since 3.9.1 deprecated * @since 2.9.5 * @source WPaaS\Cache * * @param string $method can be BAN or PURGE. * @param string $url URL to purge. * @return void */ function rocket_godaddy_request( $method, $url = null ) { _deprecated_function( __FUNCTION__ . '()', '3.9.0.4', '\WP_Rocket\ThirdParty\Hostings\Godaddy::godaddy_request' ); if ( ! method_exists( 'WPaas\Plugin', 'vip' ) ) { return; } if ( empty( $url ) ) { $url = home_url(); } $host = rocket_extract_url_component( $url, PHP_URL_HOST ); $url = set_url_scheme( str_replace( $host, WPaas\Plugin::vip(), $url ), 'http' ); wp_cache_flush(); // This forces the APC cache to flush across the server. update_option( 'gd_system_last_cache_flush', time() ); wp_remote_request( esc_url_raw( $url ), [ 'method' => $method, 'blocking' => false, 'headers' => [ 'Host' => $host, ], ] ); } deprecated/3.12.php 0000644 00000053222 15174677547 0007773 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Add All in One SEO Sitemap option to WP Rocket options * * @since 3.12 deprecated * @since 2.8 * @author Remy Perona * * @param Array $options Array of WP Rocket options. * @return Array Updated array of WP Rocket options */ function rocket_add_all_in_one_seo_sitemap_option( $options ) { _deprecated_function( __FUNCTION__, '3.12' ); $options['all_in_one_seo_xml_sitemap'] = 0; return $options; } /** * Sanitize the AIO SEO option value * * @since 3.12 deprecated * @since 2.8 * @author Remy Perona * * @param Array $inputs Array of inputs values. * @return Array Updated array of inputs $values */ function rocket_all_in_one_seo_sitemap_option_sanitize( $inputs ) { _deprecated_function( __FUNCTION__, '3.12' ); $inputs['all_in_one_seo_xml_sitemap'] = ! empty( $inputs['all_in_one_seo_xml_sitemap'] ) ? 1 : 0; return $inputs; } /** * Add All in One SEO Sitemap sub-option on WP Rocket settings page * * @since 3.12 deprecated * @since 2.8 * @author Remy Perona * * @param Array $options Array of WP Rocket options. * @return Array Updated array of WP Rocket options */ function rocket_sitemap_preload_all_in_one_seo_option( $options ) { _deprecated_function( __FUNCTION__, '3.12' ); $options['all_in_one_seo_xml_sitemap'] = [ 'type' => 'checkbox', 'container_class' => [ 'wpr-field--children', ], 'label' => __( 'All in One SEO XML sitemap', 'rocket' ), // translators: %s = Name of the plugin. 'description' => sprintf( __( 'We automatically detected the sitemap generated by the %s plugin. You can check the option to preload it.', 'rocket' ), 'All in One SEO' ), 'parent' => 'sitemap_preload', 'section' => 'preload_section', 'page' => 'preload', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ]; return $options; } /** * Add sitemap option to WP Rocket settings * * @since 3.12 deprecated * @since 3.2.3 * * @param array $options WP Rocket settings array. * @return array Updated WP Rocket settings array */ function rank_math_rocket_sitemap_preload_option( $options ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__, '3.12' ); $options['rank_math_xml_sitemap'] = [ 'type' => 'checkbox', 'container_class' => [ 'wpr-field--children', ], 'label' => __( 'Rank Math XML sitemap', 'rocket' ), // translators: %s = Name of the plugin. 'description' => sprintf( __( 'We automatically detected the sitemap generated by the %s plugin. You can check the option to preload it.', 'rocket' ), 'Rank Math SEO' ), 'parent' => 'sitemap_preload', 'section' => 'preload_section', 'page' => 'preload', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ]; return $options; } /** * Add sitemap option to WP Rocket default options * * @since 3.12 deprecated * @since 3.2.3 * * @param array $options WP Rocket options array. * @return array Updated WP Rocket options array */ function rank_math_rocket_add_sitemap_option( $options ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__, '3.12' ); $options['rank_math_xml_sitemap'] = 0; return $options; } /** * Sanitize SEO sitemap option value * * @since 3.12 deprecated * @since 3.2.3 * * @param array $inputs WP Rocket inputs array. * @return array Sanitized WP Rocket inputs array */ function rank_math_rocket_sitemap_option_sanitize( $inputs ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__, '3.12' ); $inputs['rank_math_xml_sitemap'] = ! empty( $inputs['rank_math_xml_sitemap'] ) ? 1 : 0; return $inputs; } /** * Add SEOPress sitemap option to WP Rocket default options * * @since 3.12 deprecated * @since 3.3.6 * @author Benjamin Denis * @source ./yoast-seo.php (Remy Perona) * * @param array $options WP Rocket options array. * @return array Updated WP Rocket options array */ function rocket_add_seopress_sitemap_option( $options ) { _deprecated_function( __FUNCTION__, '3.12' ); $options['seopress_xml_sitemap'] = 0; return $options; } /** * Sanitize SEOPress sitemap option value * * @since 3.12 deprecated * @since 3.3.6 * @author Benjamin Denis * @source ./yoast-seo.php (Remy Perona) * * @param array $inputs WP Rocket inputs array. * @return array Sanitized WP Rocket inputs array */ function rocket_seopress_sitemap_option_sanitize( $inputs ) { _deprecated_function( __FUNCTION__, '3.12' ); $inputs['seopress_xml_sitemap'] = ! empty( $inputs['seopress_xml_sitemap'] ) ? 1 : 0; return $inputs; } /** * Add SEOPress option to WP Rocket settings * * @since 3.12 deprecated * @since 3.3.6 * @author Benjamin Denis * @source ./yoast-seo.php (Remy Perona) * * @param array $options WP Rocket settings array. * @return array Updated WP Rocket settings array */ function rocket_sitemap_preload_seopress_option( $options ) { _deprecated_function( __FUNCTION__, '3.12' ); $options['seopress_xml_sitemap'] = [ 'type' => 'checkbox', 'container_class' => [ 'wpr-field--children', ], 'label' => __( 'SEOPress XML sitemap', 'rocket' ), // translators: %s = Name of the plugin. 'description' => sprintf( __( 'We automatically detected the sitemap generated by the %s plugin. You can check the option to preload it.', 'rocket' ), 'SEOPress' ), 'parent' => 'sitemap_preload', 'section' => 'preload_section', 'page' => 'preload', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ]; return $options; } /** * Adds a sitemap option in WP Rocket for The SEO Framework. * * @since 3.12 deprecated * @since 3.2.1 * @author Sybre Waaijer * @source ./yoast-seo.php (Remy Perona) * * @param array $options WP Rocket options array. * @return array Updated WP Rocket options array */ function rocket_add_tsf_seo_sitemap_option( $options ) { _deprecated_function( __FUNCTION__, '3.12' ); $options['tsf_xml_sitemap'] = 0; return $options; } /** * Sanitizes the added sitemap option for The SEO Framework. * * @since 3.12 deprecated * @since 3.2.1 * @author Sybre Waaijer * @source ./yoast-seo.php (Remy Perona) * * @param array $inputs WP Rocket inputs array. * @return array Sanitized WP Rocket inputs array */ function rocket_tsf_seo_sitemap_option_sanitize( $inputs ) { _deprecated_function( __FUNCTION__, '3.12' ); $inputs['tsf_xml_sitemap'] = ! empty( $inputs['tsf_xml_sitemap'] ) ? 1 : 0; return $inputs; } /** * Add The SEO Framework SEO option to WP Rocket settings * * @since 3.12 deprecated * @since 3.2.1 * @author Sybre Waaijer * @source ./yoast-seo.php (Remy Perona) * * @param array $options WP Rocket settings array. * @return array Updated WP Rocket settings array */ function rocket_sitemap_add_tsf_sitemap_to_preload_option( $options ) { _deprecated_function( __FUNCTION__, '3.12' ); $options['tsf_xml_sitemap'] = [ 'type' => 'checkbox', 'container_class' => [ 'wpr-field--children', ], 'label' => __( 'The SEO Framework XML sitemap', 'rocket' ), // translators: %s = Name of the plugin. 'description' => sprintf( __( 'We automatically detected the sitemap generated by the %s plugin. You can check the option to preload it.', 'rocket' ), 'The SEO Framework' ), 'parent' => 'sitemap_preload', 'section' => 'preload_section', 'page' => 'preload', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ]; return $options; } /** * Add Jetpack option to WP Rocket options * * @param Array $options WP Rocket options array. * @return Array Updated WP Rocket options array * @since 2.8 * @author Remy Perona * */ function rocket_add_jetpack_sitemap_option($options) { _deprecated_function( __FUNCTION__, '3.12' ); $options['jetpack_xml_sitemap'] = 0; return $options; } /** * Sanitize jetpack option value * * @param Array $inputs Array of inputs values. * @return Array Array of inputs values * @since 2.8 * @author Remy Perona * */ function rocket_jetpack_sitemap_option_sanitize($inputs) { _deprecated_function( __FUNCTION__, '3.12' ); $inputs['jetpack_xml_sitemap'] = !empty($inputs['jetpack_xml_sitemap']) ? 1 : 0; return $inputs; } /** * Add Jetpack sitemap to preload list * * @param Array $sitemaps Array of sitemaps to preload. * @return Array Updated Array of sitemaps to preload * @since 2.8 * @author Remy Perona * */ function rocket_add_jetpack_sitemap($sitemaps) { _deprecated_function( __FUNCTION__, '3.12' ); if (get_rocket_option('jetpack_xml_sitemap', false)) { $sitemaps['jetpack'] = jetpack_sitemap_uri(); } return $sitemaps; } /** * Add Jetpack sub-option to WP Rocket settings page * * @param Array $options WP Rocket options array. * @return Array Updated WP Rocket options array * @since 2.8 * @author Remy Perona * */ function rocket_sitemap_preload_jetpack_option($options) { _deprecated_function( __FUNCTION__, '3.12' ); $options[] = [ 'parent' => 'sitemap_preload', 'type' => 'checkbox', 'label' => __('Jetpack XML Sitemaps', 'rocket'), 'label_for' => 'jetpack_xml_sitemap', 'label_screen' => sprintf(__('Preload the sitemap from the Jetpack plugin', 'rocket'), 'Jetpack'), 'default' => 0, ]; $options[] = [ 'parent' => 'sitemap_preload', 'type' => 'helper_description', 'name' => 'jetpack_xml_sitemap_desc', // translators: %s = plugin name, e.g. Yoast SEO. 'description' => sprintf(__('We automatically detected the sitemap generated by the %s plugin. You can check the option to preload it.', 'rocket'), 'Jetpack'), ]; return $options; } /** * Add the EU Cookie Law to the list of mandatory cookies before generating caching files. * * @param array $cookies List of mandatory cookies. * @author Jeremy Herve * * @since 2.10.1 */ function rocket_add_jetpack_cookie_law_mandatory_cookie($cookies) { _deprecated_function( __FUNCTION__, '3.12' ); $cookies['jetpack-eu-cookie-law'] = 'eucookielaw'; return $cookies; } /** * Add Jetpack cookie when: * - Jetpack is active. * - Jetpack's Extra Sidebar Widgets module is active. * - The widget is active. * - the rocket_jetpack_eu_cookie_widget option is empty or not set. * * @since 2.10.1 * @author Jeremy Herve */ function rocket_activate_jetpack_cookie_law() { _deprecated_function( __FUNCTION__, '3.12' ); $rocket_jp_eu_cookie_widget = get_option('rocket_jetpack_eu_cookie_widget'); if ( is_active_widget(false, false, 'eu_cookie_law_widget') && empty($rocket_jp_eu_cookie_widget) ) { add_filter('rocket_htaccess_mod_rewrite', '__return_false', 76); add_filter('rocket_cache_mandatory_cookies', 'rocket_add_jetpack_cookie_law_mandatory_cookie'); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); // Set the option, so this is not triggered again. update_option('rocket_jetpack_eu_cookie_widget', 1, true); } } /** * Remove cookies if Jetpack gets deactivated. * * @since 2.10.1 * @author Jeremy Herve */ function rocket_remove_jetpack_cookie_law_mandatory_cookie() { _deprecated_function( __FUNCTION__, '3.12' ); remove_filter('rocket_htaccess_mod_rewrite', '__return_false', 76); remove_filter('rocket_cache_mandatory_cookies', '_rocket_add_eu_cookie_law_mandatory_cookie'); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); // Delete our option. delete_option('rocket_jetpack_eu_cookie_widget'); } /** * Add SEO sitemap URL to the sitemaps to preload * * @since 3.2.3 * * @param array $sitemaps Sitemaps to preload. * @return array Updated Sitemaps to preload */ function rank_math_rocket_sitemap( $sitemaps ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__, '3.12' ); if ( get_rocket_option( 'rank_math_xml_sitemap', false ) ) { $sitemaps[] = \RankMath\Sitemap\Router::get_base_url( 'sitemap_index.xml' ); } return $sitemaps; } /** * Add All in One SEO Sitemap to the preload list * * @since 2.8 * @author Remy Perona * * @param Array $sitemaps Array of sitemaps to preload. * @return Array Updated array of sitemaps to preload */ function rocket_add_all_in_one_seo_sitemap( $sitemaps ) { _deprecated_function( __FUNCTION__, '3.12' ); if ( ! get_rocket_option( 'all_in_one_seo_xml_sitemap', false ) ) { return $sitemaps; } $aioseo_v3 = defined( 'AIOSEOP_VERSION' ); $aioseo_v4 = defined( 'AIOSEO_VERSION' ) && function_exists( 'aioseo' ); if ( ! $aioseo_v3 && ! $aioseo_v4 ) { return $sitemaps; } $sitemap_enabled = false; if ( $aioseo_v3 ) { $aioseop_options = get_option( 'aioseop_options' ); $sitemap_enabled = ( isset( $aioseop_options['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] ) && 'on' === $aioseop_options['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] ) || ( ! isset( $aioseop_options['modules']['aiosp_feature_manager_options'] ) && isset( $aioseop_options['modules']['aiosp_sitemap_options'] ) ); } if ( ( $aioseo_v3 && ! $sitemap_enabled ) || ( $aioseo_v4 && ! aioseo()->options->sitemap->general->enable ) ) { return $sitemaps; } if ( $aioseo_v3 ) { $sitemaps[] = trailingslashit( home_url() ) . apply_filters( 'aiosp_sitemap_filename', 'sitemap' ) . '.xml'; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } elseif ( $aioseo_v4 ) { $sitemaps[] = trailingslashit( home_url() ) . apply_filters( 'aioseo_sitemap_filename', 'sitemap' ) . '.xml'; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } return $sitemaps; } /** * Add SEOPress sitemap URL to the sitemaps to preload * * @since 3.3.6 * @author Benjamin Denis * @source ./yoast-seo.php (Remy Perona) * * @param array $sitemaps Sitemaps to preload. * @return array Updated Sitemaps to preload */ function rocket_add_seopress_sitemap( $sitemaps ) { _deprecated_function( __FUNCTION__, '3.12' ); if ( get_rocket_option( 'seopress_xml_sitemap', false ) ) { $sitemaps[] = get_home_url() . '/sitemaps.xml'; } return $sitemaps; } /** * Runs detection and adds extra compatibility for The SEO Framework plugin. * * @since 3.2.1 * @since TODO Removed "conflicting sitemap detection" (detect_sitemap_plugin) call. * TSF always tries to output it now while trying to give WP Rewrite priority for display. * @author Sybre Waaijer */ function rocket_add_tsf_compat() { _deprecated_function( __FUNCTION__, '3.12' ); $tsf = the_seo_framework(); // Either TSF < 3.1, or the plugin's silenced (soft-disabled) via a drop-in. if ( empty( $tsf->loaded ) ) { return; } /** * 1. Performs option & other checks. * 2. Checks for conflicting sitemap plugins that might prevent loading. * * These methods cache their output at runtime. * * @link https://github.com/wp-media/wp-rocket/issues/899 */ if ( $tsf->can_run_sitemap() ) { rocket_add_tsf_sitemap_compat(); } } /** * Adds compatibility for the sitemap functionality in The SEO Framework plugin. * * @since 3.2.1 * @author Sybre Waaijer */ function rocket_add_tsf_sitemap_compat() { _deprecated_function( __FUNCTION__, '3.12' ); add_filter( 'rocket_sitemap_preload_list', 'rocket_add_tsf_sitemap_to_preload' ); } /** * Adds TSF sitemap URLs to preload. * * @since 3.2.1 * @since TODO Added compatibility support for The SEO Framework v4.0+ * @author Sybre Waaijer * @source ./yoast-seo.php (Remy Perona) * * @param array $sitemaps Sitemaps to preload. * @return array Updated Sitemaps to preload */ function rocket_add_tsf_sitemap_to_preload( $sitemaps ) { _deprecated_function( __FUNCTION__, '3.12' ); if ( get_rocket_option( 'tsf_xml_sitemap', false ) ) { // The autoloader in TSF doesn't check for file_exists(). So, use version compare instead to prevent fatal errors. if ( version_compare( THE_SEO_FRAMEWORK_VERSION, '4.0', '>=' ) ) { // TSF 4.0+. Expect the class to exist indefinitely. $sitemap_bridge = The_SEO_Framework\Bridges\Sitemap::get_instance(); foreach ( $sitemap_bridge->get_sitemap_endpoint_list() as $id => $data ) { // When the sitemap is good enough for a robots display, we determine it as valid for precaching. // Non-robots display types are among the stylesheet endpoint, or the Yoast SEO-compatible endpoint. // In other words, this enables support for ALL current and future public sitemap endpoints. if ( ! empty( $data['robots'] ) ) { $sitemaps[] = $sitemap_bridge->get_expected_sitemap_endpoint_url( $id ); } } } else { // Deprecated. TSF <4.0. $sitemaps[] = the_seo_framework()->get_sitemap_xml_url(); } } return $sitemaps; } /** * Launches the Homepage preload (helper function for backward compatibility) * * @since 2.6.4 Don't preload localhost & .dev domains * @since 1.0 * * @param string $spider (default: 'cache-preload') The spider name: cache-preload or cache-json. * @param string $lang (default: '') The language code to preload. * * @return bool Status of preload. */ function run_rocket_bot( $spider = 'cache-preload', $lang = '' ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__, '3.12' ); if ( ! get_rocket_option( 'manual_preload' ) ) { return false; } $urls = []; if ( ! $lang ) { $urls = get_rocket_i18n_uri(); } else { $urls[] = get_rocket_i18n_home_url( $lang ); } $container = apply_filters( 'rocket_container', null ); if ( ! $container ) { return false; } $controller = $container->get( 'preload_clean_controller' ); $controller->partial_clean( $urls ); return true; } /** * Launches the sitemap preload (helper function for backward compatibility) * * @since 2.8 * @author Remy Perona * * @return void */ function run_rocket_sitemap_preload() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__, '3.12' ); if ( ! get_rocket_option( 'manual_preload' ) ) { return; } $container = apply_filters( 'rocket_container', null ); if ( ! $container ) { return; } $controller = $container->get( 'load_initial_sitemap_controller' ); $controller->load_initial_sitemap(); } /** * Excludes Uncode init and ai-uncode JS files from minification/combine * * @since 3.12.3 deprecated * @since 3.1 * @author Remy Perona * * @param array $excluded_js Array of JS filepaths to be excluded. * @return array */ function rocket_exclude_js_uncode( $excluded_js ) { _deprecated_function( __FUNCTION__, '3.12.3' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/init.min.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.min.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/ai-uncode.min.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.min.js' ); return $excluded_js; } /** * Excludes some Uncode inline scripts from combine JS * * @since 3.12.3 deprecated * @since 3.1 * @author Remy Perona * * @param array $inline_js Array of patterns to match for exclusion. * @return array */ function rocket_exclude_inline_js_uncode( $inline_js ) { _deprecated_function( __FUNCTION__, '3.12.3' ); $inline_js[] = 'SiteParameters'; $inline_js[] = 'script-'; $inline_js[] = 'initBox'; $inline_js[] = 'initHeader'; $inline_js[] = 'fixMenuHeight'; return $inline_js; } /** * Excludes Uncode JS files from defer JS * * @since 3.12.3 deprecated * @since 3.2.5 * @author Remy Perona * * @param array $exclude_defer_js Array of JS filepaths to be excluded. * @return array */ function rocket_exclude_defer_js_uncode( $exclude_defer_js ) { _deprecated_function( __FUNCTION__, '3.12.3' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/init.min.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.min.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/ai-uncode.min.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.min.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.js' ); return $exclude_defer_js; } /** * Excludes Uncode JS files from delay JS * * @since 3.12.3 deprecated * @since 3.10.5 * * @param array $exclude_delay_js Array of JS to be excluded. * @return array */ function rocket_exclude_delay_js_uncode( $exclude_delay_js ) { _deprecated_function( __FUNCTION__, '3.12.3' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/init.min.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.min.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/ai-uncode.min.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.min.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.js' ); $exclude_delay_js[] = 'UNCODE\.'; return $exclude_delay_js; } deprecated/3.15.php 0000644 00000013303 15174677547 0007772 0 ustar 00 <?php /** * Add "Cache options" metabox * * @since 3.15 deprecated * @since 2.5 */ function rocket_cache_options_meta_boxes() { if ( ! rocket_can_display_options() ) { return; } if ( current_user_can( 'rocket_manage_options' ) ) { $cpts = get_post_types( [ 'public' => true, ], 'objects' ); unset( $cpts['attachment'] ); $cpts = apply_filters( 'rocket_metabox_options_post_types', $cpts ); foreach ( $cpts as $cpt => $cpt_object ) { $label = $cpt_object->labels->singular_name; add_meta_box( 'rocket_post_exclude', sprintf( __( 'WP Rocket Options', 'rocket' ), $label ), 'rocket_display_cache_options_meta_boxes', $cpt, 'side', 'core' ); } } } /** * Displays some checkbox to de/activate some cache options * * @since 3.15 deprecated * @since 2.5 */ function rocket_display_cache_options_meta_boxes() { if ( current_user_can( 'rocket_manage_options' ) ) { global $post, $pagenow; wp_nonce_field( 'rocket_box_option', '_rocketnonce', false, true ); ?> <div class="misc-pub-section"> <?php $reject_current_uri = false; if ( 'post-new.php' !== $pagenow ) { $rejected_uris = array_flip( get_rocket_option( 'cache_reject_uri', [] ) ); $path = rocket_clean_exclude_file( get_permalink( $post->ID ) ); if ( isset( $rejected_uris[ $path ] ) ) { $reject_current_uri = true; } } ?> <input name="rocket_post_nocache" id="rocket_post_nocache" type="checkbox" title="<?php esc_html_e( 'Never cache this page', 'rocket' ); ?>" <?php checked( $reject_current_uri, true ); ?>><label for="rocket_post_nocache"><?php esc_html_e( 'Never cache this page', 'rocket' ); ?></label> </div> <div class="misc-pub-section"> <p><?php esc_html_e( 'Activate these options on this post:', 'rocket' ); ?></p> <?php $fields = []; $old_fields = $fields; /** * WP Rocket Metabox fields on post edit page. * * @param string[] $fields Metaboxes fields. */ $fields = apply_filters( 'rocket_meta_boxes_fields', $fields ); if ( ! is_array( $fields ) ) { $fields = $old_fields; } foreach ( $fields as $field => $label ) { $disabled = disabled( ! get_rocket_option( $field ), true, false ); // translators: %s is the name of the option. $title = $disabled ? ' title="' . esc_attr( sprintf( __( 'Activate first the %s option.', 'rocket' ), $label ) ) . '"' : ''; $class = $disabled ? ' class="rkt-disabled"' : ''; $checked = ! $disabled ? checked( ! get_post_meta( $post->ID, '_rocket_exclude_' . $field, true ), true, false ) : ''; ?> <input name="rocket_post_exclude_hidden[<?php echo esc_attr( $field ); ?>]" type="hidden" value="on"> <input name="rocket_post_exclude[<?php echo esc_attr( $field ); ?>]" id="rocket_post_exclude_<?php echo esc_attr( $field ); ?>" type="checkbox"<?php echo $title; ?><?php echo $checked; ?><?php echo $disabled; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. ?>> <label for="rocket_post_exclude_<?php echo esc_attr( $field ); ?>"<?php echo $title; ?><?php echo $class; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. ?>><?php echo esc_html( $label ); ?></label><br> <?php } ?> <p class="rkt-note"> <?php // translators: %1$s = opening strong tag, %2$s = closing strong tag. printf( esc_html__( '%1$sNote:%2$s None of these options will be applied if this post has been excluded from cache in the global cache settings.', 'rocket' ), '<strong>', '</strong>' ); ?> </p> </div> <?php /** * Fires after WP Rocket’s metabox. * * @since 3.6 */ do_action( 'rocket_after_options_metabox' ); } } /** * Manage the cache options from the metabox. * * @since 3.15 deprecated * @since 2.5 */ function rocket_save_metabox_options() { if ( current_user_can( 'rocket_manage_options' ) && isset( $_POST['post_ID'], $_POST['rocket_post_exclude_hidden'], $_POST['_rocketnonce'] ) ) { check_admin_referer( 'rocket_box_option', '_rocketnonce' ); // No cache field. if ( isset( $_POST['post_status'] ) && 'publish' === $_POST['post_status'] ) { $new_cache_reject_uri = $cache_reject_uri = get_rocket_option( 'cache_reject_uri', [] ); // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found $rejected_uris = array_flip( $cache_reject_uri ); $path = rocket_clean_exclude_file( get_permalink( (int) $_POST['post_ID'] ) ); if ( isset( $_POST['rocket_post_nocache'] ) ) { if ( ! isset( $rejected_uris[ $path ] ) ) { array_push( $new_cache_reject_uri, $path ); } } else { if ( isset( $rejected_uris[ $path ] ) ) { unset( $new_cache_reject_uri[ $rejected_uris[ $path ] ] ); } } if ( $new_cache_reject_uri !== $cache_reject_uri ) { // Update the "Never cache the following pages" option. update_rocket_option( 'cache_reject_uri', $new_cache_reject_uri ); // Update config file. rocket_generate_config_file(); } } // Options fields. $fields = []; $old_fields = $fields; /** * Metaboxes fields. * * @param string[] $fields Metaboxes fields. */ $fields = apply_filters( 'rocket_meta_boxes_fields', $fields ); if ( ! is_array( $old_fields ) ) { $fields = $old_fields; } $fields = array_keys( $fields ); foreach ( $fields as $field ) { if ( isset( $_POST['rocket_post_exclude_hidden'][ $field ] ) ) { if ( isset( $_POST['rocket_post_exclude'][ $field ] ) ) { delete_post_meta( (int) $_POST['post_ID'], '_rocket_exclude_' . $field ); } else { if ( get_rocket_option( $field ) ) { update_post_meta( (int) $_POST['post_ID'], '_rocket_exclude_' . $field, true ); } } } } } } deprecated/3.10.php 0000644 00000010411 15174677547 0007762 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; class_alias( '\WP_Rocket\deprecated\Engine\Media\Embeds\EmbedsSubscriber', '\WP_Rocket\Engine\Media\Embeds\EmbedsSubscriber' ); class_alias( '\WP_Rocket\Engine\Admin\Database\Optimization','\WP_Rocket\Admin\Database\Optimization' ); class_alias( '\WP_Rocket\Engine\Admin\Database\OptimizationProcess','\WP_Rocket\Admin\Database\Optimization_Process' ); class_alias( '\WP_Rocket\Engine\Admin\Database\ServiceProvider','\WP_Rocket\ServiceProvider\Database' ); class_alias( '\WP_Rocket\Engine\Admin\Database\Subscriber','\WP_Rocket\Subscriber\Admin\Database\Optimization_Subscriber' ); /** * Maybe reset opcache after WP Rocket update. * * @since 3.10.8 deprecated * @since 3.1 * @author Grégory Viguier * * @param object $wp_upgrader Plugin_Upgrader instance. * @param array $hook_extra { * Array of bulk item update data. * * @type string $action Type of action. Default 'update'. * @type string $type Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'. * @type bool $bulk Whether the update process is a bulk update. Default true. * @type array $plugins Array of the basename paths of the plugins' main files. * } */ function rocket_maybe_reset_opcache( $wp_upgrader, $hook_extra ) { _deprecated_function( __FUNCTION__ . '()', '3.10.8' ); static $rocket_path; if ( ! isset( $hook_extra['action'], $hook_extra['type'], $hook_extra['plugins'] ) ) { return; } if ( 'update' !== $hook_extra['action'] || 'plugin' !== $hook_extra['type'] || ! is_array( $hook_extra['plugins'] ) ) { return; } $plugins = array_flip( $hook_extra['plugins'] ); if ( ! isset( $rocket_path ) ) { $rocket_path = plugin_basename( WP_ROCKET_FILE ); } if ( ! isset( $plugins[ $rocket_path ] ) ) { return; } rocket_reset_opcache(); } /** * Reset PHP opcache. * * @since 3.10.8 deprecated * @since 3.1 * @author Grégory Viguier */ function rocket_reset_opcache() { _deprecated_function( __FUNCTION__ . '()', '3.10.8' ); static $can_reset; /** * Triggers before WP Rocket tries to reset OPCache * * @since 3.2.5 * @author Remy Perona */ do_action( 'rocket_before_reset_opcache' ); if ( ! isset( $can_reset ) ) { if ( ! function_exists( 'opcache_reset' ) ) { $can_reset = false; return false; } $restrict_api = ini_get( 'opcache.restrict_api' ); if ( $restrict_api && strpos( __FILE__, $restrict_api ) !== 0 ) { $can_reset = false; return false; } $can_reset = true; } if ( ! $can_reset ) { return false; } $opcache_reset = opcache_reset(); /** * Triggers after WP Rocket tries to reset OPCache * * @since 3.2.5 * @author Remy Perona */ do_action( 'rocket_after_reset_opcache' ); return $opcache_reset; } /** * This notice is displayed after purging OPcache * * @since 3.10.8 deprecated * @since 3.4.1 * @author Soponar Cristina */ function rocket_opcache_purge_result() { _deprecated_function( __FUNCTION__ . '()', '3.10.8' ); if ( ! current_user_can( 'rocket_purge_opcache' ) ) { return; } if ( ! is_admin() ) { return; } $user_id = get_current_user_id(); $notice = get_transient( $user_id . '_opcache_purge_result' ); if ( ! $notice ) { return; } delete_transient( $user_id . '_opcache_purge_result' ); rocket_notice_html( [ 'status' => $notice['result'], 'message' => $notice['message'], ] ); } /** * Purge OPCache content in Admin Bar * * @since 3.10.8 deprecated * @since 2.7 */ function do_admin_post_rocket_purge_opcache() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals _deprecated_function( __FUNCTION__ . '()', '3.10.8' ); if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_purge_opcache' ) ) { wp_nonce_ays( '' ); } if ( ! current_user_can( 'rocket_purge_opcache' ) ) { return; } $reset_opcache = rocket_reset_opcache(); if ( ! $reset_opcache ) { $op_purge_result = [ 'result' => 'error', 'message' => __( 'OPcache purge failed.', 'rocket' ), ]; } else { $op_purge_result = [ 'result' => 'success', 'message' => __( 'OPcache successfully purged', 'rocket' ), ]; } set_transient( get_current_user_id() . '_opcache_purge_result', $op_purge_result ); wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); die(); } Logger/ServiceProvider.php 0000644 00000001544 15174677547 0011642 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Logger; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'logger', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Register classes provided. */ public function register(): void { $this->getContainer()->addShared( 'logger', Logger::class ); $this->getContainer() ->inflector( LoggerAwareInterface::class ) ->invokeMethod( 'set_logger', [ $this->getContainer()->get( 'logger' ) ] ); } } Logger/Logger.php 0000644 00000033131 15174677547 0007743 0 ustar 00 <?php namespace WP_Rocket\Logger; use WP_Rocket\Dependencies\Monolog\Logger as Monologger; use WP_Rocket\Dependencies\Monolog\Registry; use WP_Rocket\Dependencies\Monolog\Processor\IntrospectionProcessor; use WP_Rocket\Dependencies\Monolog\Handler\StreamHandler as MonoStreamHandler; use WP_Rocket\Dependencies\Monolog\Formatter\LineFormatter; use WP_Rocket\Logger\{HTMLFormatter, StreamHandler}; /** * Class used to log events. * * @since 3.1.4 * @since 3.2 Changed namespace from \WP_Rocket to \WP_Rocket\Logger. * @author Grégory Viguier */ class Logger { /** * Logger name. * * @var string */ const LOGGER_NAME = 'wp_rocket'; /** * Name of the logs file. * * @var string */ const LOG_FILE_NAME = 'wp-rocket-debug.log.html'; /** * A unique ID given to the current thread. * * @var string */ private static $thread_id; /** ----------------------------------------------------------------------------------------- */ /** LOG ===================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Adds a log record at the DEBUG level. * * @since 3.1.4 * * @param string $message The log message. * @param array $context The log context. * @return bool|null Whether the record has been processed. */ public static function debug( $message, array $context = [] ) { return static::debug_enabled() ? static::get_logger()->debug( $message, $context ) : null; } /** * Adds a log record at the INFO level. * * @since 3.1.4 * * @param string $message The log message. * @param array $context The log context. * @return bool|null Whether the record has been processed. */ public static function info( $message, array $context = [] ) { return static::debug_enabled() ? static::get_logger()->info( $message, $context ) : null; } /** * Adds a log record at the NOTICE level. * * @since 3.1.4 * * @param string $message The log message. * @param array $context The log context. * @return bool|null Whether the record has been processed. */ public static function notice( $message, array $context = [] ) { return static::debug_enabled() ? static::get_logger()->notice( $message, $context ) : null; } /** * Adds a log record at the WARNING level. * * @since 3.1.4 * * @param string $message The log message. * @param array $context The log context. * @return bool|null Whether the record has been processed. */ public static function warning( $message, array $context = [] ) { return static::debug_enabled() ? static::get_logger()->warning( $message, $context ) : null; } /** * Adds a log record at the ERROR level. * * @since 3.1.4 * * @param string $message The log message. * @param array $context The log context. * @return bool|null Whether the record has been processed. */ public static function error( $message, array $context = [] ) { return static::debug_enabled() ? static::get_logger()->error( $message, $context ) : null; } /** * Adds a log record at the CRITICAL level. * * @since 3.1.4 * * @param string $message The log message. * @param array $context The log context. * @return bool|null Whether the record has been processed. */ public static function critical( $message, array $context = [] ) { return static::debug_enabled() ? static::get_logger()->critical( $message, $context ) : null; } /** * Adds a log record at the ALERT level. * * @since 3.1.4 * * @param string $message The log message. * @param array $context The log context. * @return bool|null Whether the record has been processed. */ public static function alert( $message, array $context = [] ) { return static::debug_enabled() ? static::get_logger()->alert( $message, $context ) : null; } /** * Adds a log record at the EMERGENCY level. * * @since 3.1.4 * * @param string $message The log message. * @param array $context The log context. * @return bool|null Whether the record has been processed. */ public static function emergency( $message, array $context = [] ) { return static::debug_enabled() ? static::get_logger()->emergency( $message, $context ) : null; } /** * Get the logger instance. * * @since 3.1.4 * * @return Logger A Logger instance. */ public static function get_logger() { $logger_name = static::LOGGER_NAME; $log_level = Monologger::DEBUG; if ( Registry::hasLogger( $logger_name ) ) { return Registry::$logger_name(); } /** * File handler. * HTML formatter is used. */ $handler = new StreamHandler( static::get_log_file_path(), $log_level ); $formatter = new HtmlFormatter(); $handler->setFormatter( $formatter ); /** * Thanks to the processors, add data to each log: * - `debug_backtrace()` (exclude this class and Abstract_Buffer). */ $trace_processor = new IntrospectionProcessor( $log_level, [ get_called_class(), 'Abstract_Buffer' ] ); // Create the logger. $logger = new Monologger( $logger_name, [ $handler ], [ $trace_processor ] ); // Store the logger. Registry::addLogger( $logger ); return $logger; } /** ----------------------------------------------------------------------------------------- */ /** LOG FILE ================================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get the path to the log file. * * @since 3.1.4 * * @return string */ public static function get_log_file_path() { if ( defined( 'WP_ROCKET_DEBUG_LOG_FILE' ) && WP_ROCKET_DEBUG_LOG_FILE && is_string( WP_ROCKET_DEBUG_LOG_FILE ) ) { // Make sure the file uses a ".log" extension. return preg_replace( '/\.[^.]*$/', '', WP_ROCKET_DEBUG_LOG_FILE ) . '.log'; } if ( defined( 'WP_ROCKET_DEBUG_INTERVAL' ) ) { // Adds an optional logs rotator depending on a constant value - WP_ROCKET_DEBUG_INTERVAL (interval by minutes). $rotator = str_pad( round( ( strtotime( 'now' ) - strtotime( 'today midnight' ) ) / 60 / WP_ROCKET_DEBUG_INTERVAL ), 4, '0', STR_PAD_LEFT ); return WP_CONTENT_DIR . '/wp-rocket-config/' . $rotator . '-' . static::LOG_FILE_NAME; } else { return WP_CONTENT_DIR . '/wp-rocket-config/' . static::LOG_FILE_NAME; } } /** * Get the log file contents. * * @since 3.1.4 * * @return string|object The file contents on success. A WP_Error object on failure. */ public static function get_log_file_contents() { $filesystem = \rocket_direct_filesystem(); $file_path = static::get_log_file_path(); if ( ! $filesystem->exists( $file_path ) ) { return new \WP_Error( 'no_file', __( 'The log file does not exist.', 'rocket' ) ); } $contents = $filesystem->get_contents( $file_path ); if ( false === $contents ) { return new \WP_Error( 'file_not_read', __( 'The log file could not be read.', 'rocket' ) ); } return $contents; } /** * Get the log file size and number of entries. * * @since 3.1.4 * * @return array|object An array of statistics on success. A WP_Error object on failure. */ public static function get_log_file_stats() { $formatter = static::get_stream_formatter(); if ( ! $formatter ) { return new \WP_Error( 'no_stream_formatter', __( 'The logs are not saved into a file.', 'rocket' ) ); } $filesystem = \rocket_direct_filesystem(); $file_path = static::get_log_file_path(); if ( ! $filesystem->exists( $file_path ) ) { return new \WP_Error( 'no_file', __( 'The log file does not exist.', 'rocket' ) ); } $contents = $filesystem->get_contents( $file_path ); if ( false === $contents ) { return new \WP_Error( 'file_not_read', __( 'The log file could not be read.', 'rocket' ) ); } if ( $formatter instanceof HtmlFormatter ) { $entries = preg_split( '@<h1 @', $contents ); } elseif ( $formatter instanceof LineFormatter ) { $entries = preg_split( '@^\[\d{4,}-\d{2,}-\d{2,} \d{2,}:\d{2,}:\d{2,}] @m', $contents ); } else { $entries = 0; } $entries = $entries ? number_format_i18n( count( $entries ) ) : '0'; $bytes = $filesystem->size( $file_path ); $decimals = $bytes > pow( 1024, 3 ) ? 1 : 0; $bytes = @size_format( $bytes, $decimals ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $bytes = str_replace( ' ', ' ', $bytes ); // Non-breaking space character. return compact( 'entries', 'bytes' ); } /** * Get the log file extension related to the formatter in use. This can be used when the file is downloaded. * * @since 3.1.4 * * @return string The corresponding file extension with the heading dot. */ public static function get_log_file_extension() { $formatter = static::get_stream_formatter(); if ( ! $formatter ) { return '.log'; } if ( $formatter instanceof HtmlFormatter ) { return '.html'; } if ( $formatter instanceof LineFormatter ) { return '.txt'; } return '.log'; } /** * Delete the log file. * * @since 3.1.4 * * @return bool True on success. False on failure. */ public static function delete_log_file() { $filesystem = \rocket_direct_filesystem(); $file_path = static::get_log_file_path(); if ( ! $filesystem->exists( $file_path ) ) { return true; } $filesystem->put_contents( $file_path, '' ); $filesystem->delete( $file_path, false, 'f' ); return ! $filesystem->exists( $file_path ); } /** * Get the handler used for the log file. * * @since 3.2 * * @return object|bool The formatter object on success. False on failure. */ public static function get_stream_handler() { $handlers = static::get_logger()->getHandlers(); if ( ! $handlers ) { return false; } foreach ( $handlers as $_handler ) { if ( $_handler instanceof MonoStreamHandler ) { $handler = $_handler; break; } } if ( empty( $handler ) ) { return false; } return $handler; } /** * Get the formatter used for the log file. * * @since 3.1.4 * * @return object|bool The formatter object on success. False on failure. */ public static function get_stream_formatter() { $handler = static::get_stream_handler(); if ( empty( $handler ) ) { return false; } return $handler->getFormatter(); } /** ----------------------------------------------------------------------------------------- */ /** CONSTANT ================================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if debug is enabled. * * @since 3.1.4 * * @return bool */ public static function debug_enabled() { return defined( 'WP_ROCKET_DEBUG' ) && WP_ROCKET_DEBUG; } /** * Enable debug mode by adding a constant in the `wp-config.php` file. * * @since 3.1.4 */ public static function enable_debug() { static::define_debug( true ); } /** * Disable debug mode by removing the constant in the `wp-config.php` file. * * @since 3.1.4 */ public static function disable_debug() { static::define_debug( false ); } /** * Enable or disable debug mode by adding or removing a constant in the `wp-config.php` file. * * @since 3.1.4 * * @param bool $enable True to enable debug, false to disable. */ public static function define_debug( $enable ) { if ( $enable && static::debug_enabled() ) { // Debug is already enabled. return; } if ( ! $enable && ! static::debug_enabled() ) { // Debug is already disabled. return; } // Get the path to the file. $file_path = \rocket_find_wpconfig_path(); if ( ! $file_path ) { // Couldn't get the path to the file. return; } // Get the content of the file. $filesystem = \rocket_direct_filesystem(); $content = $filesystem->get_contents( $file_path ); if ( false === $content ) { // Couldn't get the content of the file. return; } // Remove previous value. $placeholder = '## WP_ROCKET_DEBUG placeholder ##'; $content = preg_replace( '@^[\t ]*define\s*\(\s*["\']WP_ROCKET_DEBUG["\'].*$@miU', $placeholder, $content ); $content = preg_replace( "@\n$placeholder@", '', $content ); if ( $enable ) { // Add the constant. $define = "define( 'WP_ROCKET_DEBUG', true ); // Added by WP Rocket.\r\n"; $content = preg_replace( '@<\?php\s*@i', "<?php\n$define", $content, 1 ); } // Save the file. $chmod = rocket_get_filesystem_perms( 'file' ); $filesystem->put_contents( $file_path, $content, $chmod ); } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the thread identifier. * * @since 3.3 * * @return string */ public static function get_thread_id() { if ( ! isset( self::$thread_id ) ) { self::$thread_id = uniqid( '', true ); } return self::$thread_id; } /** * Remove cookies related to WP auth. * * @since 3.1.4 * * @param array $cookies An array of cookies. * @return array */ public static function remove_auth_cookies( $cookies = [] ) { if ( ! $cookies || ! is_array( $cookies ) ) { $cookies = $_COOKIE; } unset( $cookies['wordpress_test_cookie'] ); if ( ! $cookies ) { return []; } $pattern = strtolower( '@^WordPress(?:user|pass|_sec|_logged_in)?_@' ); // Trolling PHPCS. foreach ( $cookies as $cookie_name => $value ) { if ( preg_match( $pattern, $cookie_name ) ) { $cookies[ $cookie_name ] = 'Value removed by WP Rocket.'; } } return $cookies; } } Logger/LoggerAware.php 0000644 00000000457 15174677547 0010730 0 ustar 00 <?php namespace WP_Rocket\Logger; trait LoggerAware { /** * Logger instance. * * @var Logger */ protected $logger; /** * Set the logger. * * @param Logger $logger Logger instance. * @return void */ public function set_logger( Logger $logger ) { $this->logger = $logger; } } Logger/StreamHandler.php 0000644 00000006731 15174677547 0011263 0 ustar 00 <?php namespace WP_Rocket\Logger; use UnexpectedValueException; use WP_Rocket\Dependencies\Monolog\Handler\StreamHandler as MonoStreamHandler; /** * Class used to log records into a local file. * * @since 3.2 */ class StreamHandler extends MonoStreamHandler { /** * Tell if the .htaccess file exists. * * @var bool * * @since 3.2 */ private $htaccess_exists; /** * Tell if there is an error. * * @var bool * * @since 3.2 */ private $has_error; /** * Contains an error message. * * @var string * * @since 3.2 */ private $error_message; /** * Writes the record down to the log of the implementing handler. * * @since 3.2 * * @param array $record Log contents. * * @return void */ protected function write( array $record ): void { parent::write( $record ); $this->create_htaccess_file(); } /** * Create a .htaccess file in the log folder, to prevent direct access and directory listing. * * @since 3.2 * * @throws \UnexpectedValueException When the .htaccess file could not be created. * * @return bool True if the file exists or has been created. False on failure. */ public function create_htaccess_file() { if ( $this->htaccess_exists ) { return true; } if ( $this->has_error ) { return false; } $dir = $this->get_dir_from_stream( $this->url ); if ( ! $dir || ! is_dir( $dir ) ) { $this->has_error = true; return false; } $file_path = $dir . '/.htaccess'; if ( file_exists( $file_path ) ) { $this->htaccess_exists = true; return true; } $this->error_message = null; set_error_handler( [ $this, 'custom_error_handler' ] ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler $file_resource = fopen( $file_path, 'a' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen restore_error_handler(); if ( ! is_resource( $file_resource ) ) { $this->has_error = true; throw new UnexpectedValueException( sprintf( 'The file "%s" could not be opened: ' . $this->error_message, $file_path ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } $new_content = "<Files ~ \"\.log$\">\nOrder allow,deny\nDeny from all\n</Files>\nOptions -Indexes"; fwrite( $file_resource, $new_content ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite fclose( $file_resource ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose @chmod( $file_path, 0644 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_chmod $this->htaccess_exists = true; return true; } /** * Temporary error handler that "cleans" the error messages. * * @since 3.2 * * @see parent::customErrorHandler() * * @param int $code Error code. * @param string $msg Error message. */ private function custom_error_handler( int $code, string $msg ) { $this->error_message = preg_replace( '{^(fopen|mkdir)\(.*?\): }', '', $msg ); } /** * A dirname() that also works for streams, by removing the protocol. * * @since 3.2 * * @see parent::getDirFromStream() * * @param string $stream Path to a file. * * @return null|string */ private function get_dir_from_stream( string $stream ) { $pos = strpos( $stream, '://' ); if ( false === $pos ) { return dirname( $stream ); } if ( 'file://' === substr( $stream, 0, 7 ) ) { return dirname( substr( $stream, 7 ) ); } } } Logger/HTMLFormatter.php 0000644 00000003302 15174677547 0011151 0 ustar 00 <?php namespace WP_Rocket\Logger; use WP_Rocket\Dependencies\Monolog\Formatter\HtmlFormatter as MonoHtmlFormatter; defined( 'ABSPATH' ) || exit; /** * Class used to format log records as HTML. * * @since 3.2 * @author Grégory Viguier */ class HTMLFormatter extends MonoHtmlFormatter { /** * Formats a log record. * Compared to the parent method, it removes the "channel" row. * * @since 3.2 * @access public * @author Grégory Viguier * * @param array $record A record to format. * @return mixed The formatted record. */ public function format( array $record ): string { $output = $this->addTitle( $record['level_name'], $record['level'] ); $output .= '<table cellspacing="1" width="100%" class="monolog-output">'; $output .= $this->addRow( 'Message', (string) $record['message'] ); $output .= $this->addRow( 'Time', $record['datetime']->format( $this->dateFormat ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( $record['context'] ) { $embedded_table = '<table cellspacing="1" width="100%">'; foreach ( $record['context'] as $key => $value ) { $embedded_table .= $this->addRow( $key, $this->convertToString( $value ) ); } $embedded_table .= '</table>'; $output .= $this->addRow( 'Context', $embedded_table, false ); } if ( $record['extra'] ) { $embedded_table = '<table cellspacing="1" width="100%">'; foreach ( $record['extra'] as $key => $value ) { $embedded_table .= $this->addRow( $key, $this->convertToString( $value ) ); } $embedded_table .= '</table>'; $output .= $this->addRow( 'Extra', $embedded_table, false ); } return $output . '</table>'; } } Logger/LoggerAwareInterface.php 0000644 00000000325 15174677547 0012543 0 ustar 00 <?php namespace WP_Rocket\Logger; interface LoggerAwareInterface { /** * Set the logger. * * @param Logger $logger Logger instance. * @return void */ public function set_logger( Logger $logger ); } ThirdParty/ReturnTypesTrait.php 0000644 00000001153 15174677547 0012706 0 ustar 00 <?php namespace WP_Rocket\ThirdParty; trait ReturnTypesTrait { /** * Returns false. * * @since 3.6.1 * * @return bool */ public function return_false() { return false; } /** * Returns true. * * @since 3.6.1 * * @return true */ public function return_true() { return true; } /** * Returns an empty string. * * @since 3.6.1 * * @return string Empty string */ public function return_empty_string() { return ''; } /** * Returns an empty array. * * @since 3.6.1 * * @return array Empty array. */ public function return_empty_array() { return []; } } ThirdParty/Themes/Divi.php 0000644 00000020725 15174677547 0011524 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Admin\{Options, Options_Data}; use WP_Rocket\Engine\Optimization\DelayJS\HTML; use WP_Rocket\Engine\Optimization\RUCSS\Controller\UsedCSS; use WP_Rocket\Event_Management\Subscriber_Interface; class Divi implements Subscriber_Interface { /** * Options API instance. * * @var Options */ private $options_api; /** * WP Rocket options instance. * * @var Options_Data */ private $options; /** * Delay JS HTML class. * * @var HTML */ private $delayjs_html; /** * Used CSS controller instance. * * @var UsedCSS */ private $used_css; /** * Instantiate the class * * @param Options $options_api Options API instance. * @param Options_Data $options WP Rocket options instance. * @param HTML $delayjs_html DelayJS HTML class. * @param UsedCSS $used_css Used CSS controller instance. */ public function __construct( Options $options_api, Options_Data $options, HTML $delayjs_html, UsedCSS $used_css ) { $this->options_api = $options_api; $this->options = $options; $this->delayjs_html = $delayjs_html; $this->used_css = $used_css; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { $events = [ 'after_switch_theme' => [ 'maybe_disable_youtube_preview', PHP_INT_MAX ], 'rocket_specify_dimension_images' => 'disable_image_dimensions_height_percentage', ]; $events['rocket_exclude_js'] = 'exclude_js'; $events['rocket_maybe_disable_youtube_lazyload_helper'] = 'add_divi_to_description'; $events['wp_enqueue_scripts'] = 'disable_divi_jquery_body'; $events['wp'] = 'disable_dynamic_css_on_rucss'; $events['after_setup_theme'] = 'remove_assets_generated'; $events['et_save_post'] = 'handle_save_template'; $events['admin_notices'] = 'handle_divi_admin_notice'; $slug = rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ); $events[ 'update_option_' . $slug ] = [ 'remove_transient_when_enabling_rucss_option', 10, 2 ]; $events['rocket_after_clean_used_css'] = 'clear_divi_notice'; $events['wp_ajax_et_theme_builder_api_save'] = [ 'show_rucss_notice_with_save_changes', 9 ]; return $events; } /** * Excludes Divi's Salvatorre script from JS minification * * Prevent an error after minification/concatenation * * @since 3.6.3 * * @param array $excluded_js An array of JS paths to be excluded. * * @return array the updated array of paths */ public function exclude_js( $excluded_js ) { if ( ! rocket_get_constant( 'ET_BUILDER_URI' ) ) { return $excluded_js; } $excluded_js[] = str_replace( home_url(), '', rocket_get_constant( 'ET_BUILDER_URI' ) . '/scripts/salvattore.min.js' ); return $excluded_js; } /** * Disables the Replace Youtube iframe by preview thumbnail option if new theme (or parent) is Divi * * @since 3.6.3 * * @return void */ public function maybe_disable_youtube_preview() { $this->options->set( 'lazyload_youtube', 0 ); $this->options_api->set( 'settings', $this->options->get_options() ); } /** * Adds Divi to the array of items disabling Youtube lazyload * * @since 3.6.3 * * @param array $disable_youtube_lazyload Array of items names. * * @return array */ public function add_divi_to_description( $disable_youtube_lazyload ) { $disable_youtube_lazyload[] = 'Divi'; return $disable_youtube_lazyload; } /** * Disables setting explicit dimensions on images where Divi calculates height as percentage. * * @since 3.8.2 * * @param array $images The array of images selected for adding image dimensions. * * @return array The array without images using data-height-percentage. */ public function disable_image_dimensions_height_percentage( array $images ) { foreach ( $images as $key => $image ) { if ( false !== strpos( strtolower( $image ), 'data-height-percentage' ) ) { unset( $images[ $key ] ); } } return $images; } /** * Disable divi jquery body. * * @since 3.9.3 */ public function disable_divi_jquery_body() { if ( $this->delayjs_html->is_allowed() && defined( 'ET_CORE_VERSION' ) && version_compare( ET_CORE_VERSION, '4.10', '>=' ) ) { add_filter( 'et_builder_enable_jquery_body', '__return_false' ); } } /** * Disable Divi dynamic CSS when RUCSS is activated * * @return void */ public function disable_dynamic_css_on_rucss() { if ( ! $this->options->get( 'remove_unused_css', false ) ) { return; } add_filter( 'et_use_dynamic_css', '__return_false' ); } /** * Remove dynamic late assets action. * * @return void */ public function remove_assets_generated() { remove_all_actions( 'et_dynamic_late_assets_generated' ); } /** * Get layout IDs for a template. * * @param int $template_post_id Template post ID. * * @return array */ private function get_layout_ids( $template_post_id ) { $allowed_post_types = [ 'et_header_layout', 'et_footer_layout', 'et_body_layout', ]; $current_post_type = get_post_type( $template_post_id ); if ( ! in_array( $current_post_type, $allowed_post_types, true ) ) { return []; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery return (array) $wpdb->get_col( $wpdb->prepare( 'SELECT post_id from ' . $wpdb->postmeta . ' WHERE meta_key = %s AND meta_value = %d', '_' . $current_post_type . '_id', $template_post_id ) ); } /** * Is allowed for RUCSS notice functionality when saving templates. * * @return bool */ private function is_allowed_for_rucss() { return $this->options->get( 'remove_unused_css', 0 ) && current_user_can( 'rocket_manage_options' ); } /** * Save template handler. * * @param int $template_post_id Template post ID. * * @return void */ public function handle_save_template( $template_post_id ) { /** * Filters Bypassing saving template functionality. * * @param bool $bypass Bypass save template functionality. * @param int $template_post_id Currently saved template post id. */ if ( apply_filters( 'rocket_divi_bypass_save_template', false, $template_post_id ) ) { return; } // If the transient is there, we don't need to do the check again as the admin notice will be there already. if ( false !== get_transient( 'rocket_divi_notice' ) ) { return; } $layout_post_ids = $this->get_layout_ids( $template_post_id ); if ( empty( $layout_post_ids ) ) { return; } foreach ( $layout_post_ids as $layout_post_id ) { if ( 'publish' !== get_post_status( $layout_post_id ) ) { continue; } set_transient( 'rocket_divi_notice', true ); return; } } /** * Admin notices handler. * * @return void */ public function handle_divi_admin_notice() { if ( ! $this->is_allowed_for_rucss() ) { return; } $notice = get_transient( 'rocket_divi_notice' ); if ( ! $notice ) { return; } rocket_notice_html( [ 'status' => 'warning', 'dismissible' => '', 'dismiss_button' => 'rocket_divi_notice', 'action' => 'clear_used_css', 'message' => sprintf( '%1$sWP Rocket:%2$s ', '<strong>', '</strong>' ) . // Splitting it because I think the plugin name is not a translatable string. esc_html__( 'Your Divi template was updated. Clear the Used CSS if the layout, design or CSS styles were changed.', 'rocket' ), ] ); } /** * Clear divi notice. * * @return void */ public function clear_divi_notice() { delete_transient( 'rocket_divi_notice' ); } /** * Remove notice transient when enabling RUCSS. * * @param array $old_value An array of submitted values for the settings. * @param array $new_value An array of previous values for the settings. * * @return void */ public function remove_transient_when_enabling_rucss_option( $old_value, $new_value ) { if ( ! isset( $new_value['remove_unused_css'], $old_value['remove_unused_css'] ) ) { return; } if ( $new_value['remove_unused_css'] === $old_value['remove_unused_css'] ) { return; } if ( ! $new_value['remove_unused_css'] ) { return; } if ( $this->used_css->has_one_completed_row_at_least() ) { return; } $this->clear_divi_notice(); } /** * Set the transient when save changes button is clicked. * * @return void */ public function show_rucss_notice_with_save_changes() { set_transient( 'rocket_divi_notice', true ); } } ThirdParty/Themes/MinimalistBlogger.php 0000644 00000001411 15174677547 0014230 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Event_Management\Subscriber_Interface; class MinimalistBlogger implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_delay_js_exclusions' => 'exclude_jquery_from_delay_js', ]; } /** * Excludes some MinimalistBlogger JS from delay JS execution * * @since 3.11.3 * * @param array $exclusions Array of exclusion patterns. * * @return array */ public function exclude_jquery_from_delay_js( array $exclusions = [] ) { $exclusions[] = '\/jquery(-migrate)?-?([0-9.]+)?(.min|.slim|.slim.min)?.js(\?(.*))?( |\'|"|>)'; return $exclusions; } } ThirdParty/Themes/Polygon.php 0000644 00000001241 15174677547 0012250 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Event_Management\Subscriber_Interface; class Polygon implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_rucss_inline_content_exclusions' => 'add_rucss_content_excluded', ]; } /** * Add excluded elements to rocket_rucss_inline_content_exclusions filter. * * @param array $excluded excluded elements. * @return array */ public function add_rucss_content_excluded( $excluded ) { $excluded [] = '.expanding_bar_'; return $excluded; } } ThirdParty/Themes/SubscriberFactory.php 0000644 00000003053 15174677547 0014257 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Themes; class SubscriberFactory { /** * Get a theme subscriber data * * @return array */ public function get_subscriber() { $theme = ThemeResolver::get_current_theme(); switch ( $theme ) { case 'avada': return [ 'class' => Avada::class, 'arguments' => [ 'options', ], ]; case 'bridge': return [ 'class' => Bridge::class, 'arguments' => [ 'options', ], ]; case 'divi': return [ 'class' => Divi::class, 'arguments' => [ 'options_api', 'options', 'delay_js_html', 'rucss_used_css_controller', ], ]; case 'flatsome': return [ 'class' => Flatsome::class, 'arguments' => [], ]; case 'jevelin': return [ 'class' => Jevelin::class, 'arguments' => [], ]; case 'minimalist_blogger': return [ 'class' => MinimalistBlogger::class, 'arguments' => [], ]; case 'polygon': return [ 'class' => Polygon::class, 'arguments' => [], ]; case 'uncode': return [ 'class' => Uncode::class, 'arguments' => [], ]; case 'xstore': return [ 'class' => Xstore::class, 'arguments' => [], ]; case 'themify': return [ 'class' => Themify::class, 'arguments' => [ 'options', ], ]; case 'shoptimizer': return [ 'class' => Shoptimizer::class, 'arguments' => [], ]; default: return []; } } } ThirdParty/Themes/Xstore.php 0000644 00000001336 15174677547 0012112 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Event_Management\Subscriber_Interface; class Xstore implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_rucss_inline_content_exclusions' => 'exclude_inline_content', ]; } /** * Excludes the contents with patterns from being processed by RUCSS * * @param array $patterns Array of patterns to preserve. * * @return array */ public function exclude_inline_content( $patterns ): array { $patterns[] = '.slider-'; $patterns[] = '.slider-item-'; return $patterns; } } ThirdParty/Themes/Themify.php 0000644 00000007171 15174677547 0012236 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class Themify implements Subscriber_Interface { /** * WP Rocket options instance. * * @var Options_Data */ private $options; /** * Instantiate the class. * * @param Options_Data $options WP Rocket options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'after_switch_theme' => 'disabling_concat_on_theme', 'update_option_' . rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ) => [ 'disabling_concat_on_rucss', 10, 2 ], 'themify_save_data' => 'disable_concat_on_saving_data', 'themify_dev_mode' => 'maybe_enable_dev_mode', ]; } /** * Change the value on change theme. * * @return void */ public function disabling_concat_on_theme() { // @phpstan-ignore-next-line $data = themify_get_data(); $remove_unused_css = $this->options->get( 'remove_unused_css', false ); if ( ! $remove_unused_css ) { $data = $this->maybe_disable( $data ); } if ( $remove_unused_css ) { $data = $this->maybe_enable( $data ); } // @phpstan-ignore-next-line themify_set_data( $data ); } /** * Disable concat on saving theme options. * * @param array $value theme options. * @return array */ public function disable_concat_on_saving_data( $value ) { if ( ! $this->options->get( 'remove_unused_css', false ) ) { return $value; } return $this->maybe_enable( $value ); } /** * Disable concat on RUCSS enabled. * * @param array $old Old configurations. * @param array $new New configurations. * * @return void */ public function disabling_concat_on_rucss( $old, $new ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.newFound if ( ! key_exists( 'remove_unused_css', $old ) || ! key_exists( 'remove_unused_css', $new ) || $old['remove_unused_css'] === $new['remove_unused_css'] ) { return; } // @phpstan-ignore-next-line $data = themify_get_data(); if ( ! $new['remove_unused_css'] ) { $data = $this->maybe_disable( $data ); } if ( $new['remove_unused_css'] ) { $data = $this->maybe_enable( $data ); } // @phpstan-ignore-next-line themify_set_data( $data ); } /** * Maybe disable concate CSS. * * @param array $data Themify data. * * @return array */ protected function maybe_disable( array $data ): array { if ( key_exists( 'setting-dev-mode-concate', $data ) && ! $data['setting-dev-mode-concate'] && key_exists( 'setting-dev-mode', $data ) && ! $data['setting-dev-mode'] ) { return $data; } $data['setting-dev-mode-concate'] = false; $data['setting-dev-mode'] = false; return $data; } /** * Maybe enable dev mode and concat. * * @param array $data Themify data. * * @return array */ protected function maybe_enable( array $data ): array { if ( key_exists( 'setting-dev-mode-concate', $data ) && $data['setting-dev-mode-concate'] && key_exists( 'setting-dev-mode', $data ) && $data['setting-dev-mode'] ) { return $data; } $data['setting-dev-mode'] = true; $data['setting-dev-mode-concate'] = true; return $data; } /** * Enable the dev mode when RUCSS is activated. * * @param bool $is_enabled Is dev mode enabled. * @return bool */ public function maybe_enable_dev_mode( $is_enabled ) { if ( $this->options->get( 'remove_unused_css', false ) ) { return true; } return $is_enabled; } } ThirdParty/Themes/Uncode.php 0000644 00000007424 15174677547 0012047 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Event_Management\Subscriber_Interface; class Uncode implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_exclude_js' => 'exclude_js', 'rocket_excluded_inline_js_content' => 'exclude_inline_js', 'rocket_exclude_defer_js' => 'exclude_defer_js', 'rocket_delay_js_exclusions' => 'exclude_delay_js', ]; } /** * Excludes Uncode init and ai-uncode JS files from minification/combine * * @since 3.1 * * @param array $excluded_js Array of JS filepaths to be excluded. * @return array */ public function exclude_js( $excluded_js ): array { $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/init.min.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.min.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/ai-uncode.min.js' ); $excluded_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.min.js' ); return $excluded_js; } /** * Excludes some Uncode inline scripts from combine JS * * @since 3.1 * * @param array $inline_js Array of patterns to match for exclusion. * @return array */ public function exclude_inline_js( $inline_js ): array { $inline_js[] = 'SiteParameters'; $inline_js[] = 'script-'; $inline_js[] = 'initBox'; $inline_js[] = 'initHeader'; $inline_js[] = 'fixMenuHeight'; return $inline_js; } /** * Excludes Uncode JS files from defer JS * * @since 3.2.5 * * @param array $exclude_defer_js Array of JS filepaths to be excluded. * @return array */ public function exclude_defer_js( $exclude_defer_js ): array { $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/init.min.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.min.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/ai-uncode.min.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.min.js' ); $exclude_defer_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.js' ); return $exclude_defer_js; } /** * Excludes Uncode JS files from delay JS * * @since 3.10.5 * * @param array $exclude_delay_js Array of JS to be excluded. * @return array */ public function exclude_delay_js( $exclude_delay_js ): array { $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/init.min.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/init.min.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/min/ai-uncode.min.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.min.js' ); $exclude_delay_js[] = rocket_clean_exclude_file( get_template_directory_uri() . '/library/js/ai-uncode.js' ); $exclude_delay_js[] = 'UNCODE\.(.*)\)\;'; return $exclude_delay_js; } } ThirdParty/Themes/ServiceProvider.php 0000644 00000002655 15174677547 0013746 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Dependencies\League\Container\ServiceProvider\{AbstractServiceProvider, BootableServiceProviderInterface}; class ServiceProvider extends AbstractServiceProvider implements BootableServiceProviderInterface { /** * Array of services provided by this service provider * * @var array */ protected $provides = []; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Register the service in the provider array * * @return void */ public function boot(): void { $theme = ThemeResolver::get_current_theme(); if ( ! empty( $theme ) ) { $this->provides[] = $theme; } } /** * Registers the subscribers in the container * * @return void */ public function register(): void { $theme = ThemeResolver::get_current_theme(); if ( ! empty( $theme ) ) { $factory = new SubscriberFactory(); $theme_data = $factory->get_subscriber(); $arguments = []; if ( empty( $theme_data ) ) { return; } foreach ( $theme_data['arguments'] as $arg ) { $arguments[] = $this->getContainer()->get( $arg ); } $this->getContainer() ->addShared( $theme, $theme_data['class'] ) ->addArguments( $arguments ); } } } ThirdParty/Themes/Avada.php 0000644 00000007763 15174677547 0011654 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Compatibility class for Avada theme */ class Avada implements Subscriber_Interface { /** * Options instance * * @var Options_Data */ private $options; /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3.1 * * @return array */ public static function get_subscribed_events() { return [ 'avada_clear_dynamic_css_cache' => 'clean_domain', 'rocket_exclude_defer_js' => 'exclude_defer_js', 'rocket_maybe_disable_lazyload_helper' => 'maybe_disable_lazyload', 'fusion_cache_reset_after' => 'clean_domain', 'update_option_fusion_options' => [ 'maybe_deactivate_lazyload', 10, 2 ], 'rocket_wc_product_gallery_delay_js_exclusions' => 'exclude_delay_js', 'init' => 'disable_compilers', 'rocket_lazyload_bg_images_regex' => 'fix_regex_lazyload_bg_images', ]; } /** * Constructor * * @param Options_Data $options WP Rocket options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * When Avada theme purge its own cache. * Clear WP Rocket cache when Avada dynamic CSS is updated. * * @return void */ public function clean_domain() { rocket_clean_domain(); } /** * Deactivate WP Rocket lazyload if Avada lazyload is enabled. * * @since 3.3.4 * * @param array $old_value Previous Avada option value. * @param array $value New Avada option value. * @return void */ public function maybe_deactivate_lazyload( $old_value, $value ) { if ( empty( $old_value['lazy_load'] ) || ( ! empty( $value['lazy_load'] ) && 'avada' === $value['lazy_load'] ) ) { update_rocket_option( 'lazyload', 0 ); } } /** * Excludes Avada Google Maps JS files from defer JS * * @param array $exclude_defer_js Array of JS filepaths to be excluded. * @return array */ public function exclude_defer_js( $exclude_defer_js ) { $exclude_defer_js[] = '/jquery-?[0-9.]*(.min|.slim|.slim.min)?.js'; $exclude_defer_js[] = 'maps.googleapis.com'; return $exclude_defer_js; } /** * Disable WP Rocket lazyload field if Avada lazyload is enabled * * @since 3.3.4 * @param array $disable_images_lazyload Array with plugins which disable lazyload functionality. * @return array */ public function maybe_disable_lazyload( $disable_images_lazyload ) { $avada_options = get_option( 'fusion_options' ); if ( empty( $avada_options['lazy_load'] ) ) { return $disable_images_lazyload; } if ( 'avada' !== $avada_options['lazy_load'] ) { return $disable_images_lazyload; } $disable_images_lazyload[] = __( 'Avada', 'rocket' ); return $disable_images_lazyload; } /** * Excludes some Avada JS from delay JS execution when WC product gallery has images * * @since 3.10.2 * * @param array $exclusions Array of exclusion patterns. * * @return array */ public function exclude_delay_js( $exclusions ): array { $base_path = wp_parse_url( get_stylesheet_directory_uri(), PHP_URL_PATH ); if ( empty( $base_path ) ) { return $exclusions; } $exclusions[] = $base_path . '/includes/lib/assets/min/js/library/jquery.flexslider.js'; $exclusions[] = $base_path . '/assets/min/js/general/avada-woo-product-images.js'; return $exclusions; } /** * Disable CSS and JS combine file from Avada. */ public function disable_compilers() { if ( $this->options->get( 'remove_unused_css', false ) && ! defined( 'FUSION_DISABLE_COMPILERS' ) ) { define( 'FUSION_DISABLE_COMPILERS', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound } } /** * Add a fix to the lazyload regex on background images. * * @param string $regex regex used to deleted background images. * * @return string */ public function fix_regex_lazyload_bg_images( $regex ) { return '(--awb-)?' . $regex; } } ThirdParty/Themes/Flatsome.php 0000644 00000002074 15174677547 0012400 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Event_Management\Subscriber_Interface; class Flatsome implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_rucss_inline_content_exclusions' => 'preserve_patterns', ]; } /** * Preserves the CSS patterns when adding the used CSS to the page * * @since 3.11 * * @param array $patterns Array of patterns to preserve. * * @return array */ public function preserve_patterns( $patterns ): array { $preserve = [ '#section_', '#text-box-', '#banner-', '#slider-', '#gap-', '#image_', '#row-', '#text-', '#banner-grid-', '#cats-', '#col-', '#gallery-', '#instagram-', '#map-', '#page-header-', '#pages-', '#panel-', '#portfolio-', '#product-flip-', '#product-grid-', '#stack-', '#timer-', '#title-', ]; return array_merge( $patterns, $preserve ); } } ThirdParty/Themes/Jevelin.php 0000644 00000002727 15174677547 0012227 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Event_Management\Subscriber_Interface; class Jevelin implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_rucss_inline_content_exclusions' => 'preserve_patterns', ]; } /** * Preserves the CSS patterns when adding the used CSS to the page * * @since 3.11.3 * * @param array $patterns Array of patterns to preserve. * * @return array */ public function preserve_patterns( $patterns ): array { return array_merge( $patterns, [ '#heading-', '#button-', '#image-points-', '#image-comparison-', '#partners-', '#icon-', '#text-block-', '#instagram-feed-', '#woocommerce-products-', '#tabs-', '#icon-group-', '#alert-', '#divider-', '#image-gallery-', '#portfolio-fancy-', '#counter-', '#text-group-', '#piechart-', '#single-image-', '#progress-', '#countdown-', '#testimonials-', '#portfolio-', '#event-', '.vc_column_', '.vc_row_', '.sh-empty-space-', '.sh-element-titlebar-title-', '.sh-footer-builder-widgets-', '.sh-image-gallery-simple-', '.header-navigation-', '.sh-google-maps-', '.sh-element-titlebar-breadcrumbs-', '.sh-header-builder-', '.sh-text-group-', '.sh-footer-builder-title-', ] ); } } ThirdParty/Themes/Bridge.php 0000644 00000003401 15174677547 0012015 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; class Bridge implements Subscriber_Interface { use ReturnTypesTrait; /** * Options instance * * @var Options_Data */ private $options; /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3.1 * * @return array */ public static function get_subscribed_events() { return [ 'rocket_lazyload_background_images' => 'return_false', 'update_option_qode_options_proya' => [ 'maybe_clear_cache', 10, 2 ], ]; } /** * Constructor * * @param Options_Data $options WP Rocket options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Maybe clear WP Rocket cache when Bridge custom CSS/JS is updated * * @since 3.3.7 * * @param array $old_value Previous option values. * @param array $new_value New option values. * @return void */ public function maybe_clear_cache( $old_value, $new_value ) { $clear = false; if ( $this->options->get( 'minify_css', 0 ) ) { if ( isset( $old_value['custom_css'], $new_value['custom_css'] ) && $old_value['custom_css'] !== $new_value['custom_css'] ) { $clear = true; } if ( isset( $old_value['custom_svg_css'], $new_value['custom_svg_css'] ) && $old_value['custom_svg_css'] !== $new_value['custom_svg_css'] ) { $clear = true; } } if ( $this->options->get( 'minify_js', 0 ) ) { if ( isset( $old_value['custom_js'], $new_value['custom_js'] ) && $old_value['custom_js'] !== $new_value['custom_js'] ) { $clear = true; } } if ( $clear ) { rocket_clean_domain(); rocket_clean_minify(); } } } ThirdParty/Themes/Shoptimizer.php 0000644 00000001571 15174677547 0013144 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Themes; use WP_Rocket\Event_Management\Subscriber_Interface; class Shoptimizer implements Subscriber_Interface { /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_exclude_defer_js' => 'exclude_jquery_deferjs_with_cart_drawer', ]; } /** * Exclude Jquery from defer JS. * * @param array $exclusions Excluded values from defer JS. * * @return array */ public function exclude_jquery_deferjs_with_cart_drawer( $exclusions ) { if ( ! function_exists( 'shoptimizer_get_option' ) || ! shoptimizer_get_option( 'shoptimizer_layout_woocommerce_enable_sidebar_cart' ) ) { return $exclusions; } $exclusions[] = '\/jquery(-migrate)?-?([0-9.]+)?(.min|.slim|.slim.min)?.js(\?(.*))?'; return $exclusions; } } ThirdParty/Themes/ThemeResolver.php 0000644 00000001364 15174677547 0013413 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Themes; class ThemeResolver { /** * Array of themes names with compatibility classes * * @var array */ private static $compatibilities = [ 'avada', 'bridge', 'divi', 'flatsome', 'jevelin', 'minimalist_blogger', 'polygon', 'uncode', 'xstore', 'themify', 'shoptimizer', ]; /** * Return name of current theme * * @return string */ public static function get_current_theme(): string { $theme = wp_get_theme(); $template = $theme->get_template(); if ( empty( $template ) ) { return ''; } $template = strtolower( $template ); if ( ! in_array( $template, self::$compatibilities, true ) ) { return ''; } return $template; } } ThirdParty/NullSubscriber.php 0000644 00000000764 15174677547 0012343 0 ustar 00 <?php namespace WP_Rocket\ThirdParty; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Null Subscriber. * * Provides a base class to extend for common Subscribers. * * @since 3.6.3 */ class NullSubscriber implements Subscriber_Interface { /** * Get an array of subscribed events. * * To be overloaded with actual subscribed events in extending Subscribers. * * @since 3.6.3 * * @return array */ public static function get_subscribed_events() { return []; } } ThirdParty/ServiceProvider.php 0000644 00000020735 15174677547 0012520 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp\EWWW_Subscriber; use WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp\Imagify_Subscriber; use WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp\Optimus_Subscriber; use WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp\ShortPixel_Subscriber; use WP_Rocket\Subscriber\Third_Party\Plugins\Mobile_Subscriber; use WP_Rocket\Subscriber\Third_Party\Plugins\NGG_Subscriber; use WP_Rocket\Subscriber\Third_Party\Plugins\SyntaxHighlighter_Subscriber; use WP_Rocket\ThirdParty\Plugins\Ads\Adthrive; use WP_Rocket\ThirdParty\Plugins\ConvertPlug; use WP_Rocket\ThirdParty\Plugins\Cookie\Termly; use WP_Rocket\ThirdParty\Plugins\Ecommerce\BigCommerce; use WP_Rocket\ThirdParty\Plugins\Ecommerce\WooCommerceSubscriber; use WP_Rocket\ThirdParty\Plugins\I18n\TranslatePress; use WP_Rocket\ThirdParty\Plugins\I18n\WPML; use WP_Rocket\ThirdParty\Plugins\InlineRelatedPosts; use WP_Rocket\ThirdParty\Plugins\ModPagespeed; use WP_Rocket\ThirdParty\Plugins\Optimization\AMP; use WP_Rocket\ThirdParty\Plugins\Optimization\Autoptimize; use WP_Rocket\ThirdParty\Plugins\Optimization\Ezoic; use WP_Rocket\ThirdParty\Plugins\Optimization\WPMeteor; use WP_Rocket\ThirdParty\Plugins\PageBuilder\BeaverBuilder; use WP_Rocket\ThirdParty\Plugins\PageBuilder\Elementor; use WP_Rocket\ThirdParty\Plugins\PDFEmbedder; use WP_Rocket\ThirdParty\Plugins\PWA; use WP_Rocket\ThirdParty\Plugins\RevolutionSlider; use WP_Rocket\ThirdParty\Plugins\Security\WordFenceCompatibility; use WP_Rocket\ThirdParty\Plugins\SEO\Yoast; use WP_Rocket\ThirdParty\Plugins\SimpleCustomCss; use WP_Rocket\ThirdParty\Plugins\Smush; use WP_Rocket\ThirdParty\Plugins\TheEventsCalendar; use WP_Rocket\ThirdParty\Plugins\ThirstyAffiliates; use WP_Rocket\ThirdParty\Plugins\UnlimitedElements; use WP_Rocket\ThirdParty\Plugins\CDN\{Cloudflare, CloudflareFacade}; use WP_Rocket\ThirdParty\Plugins\Jetpack; use WP_Rocket\ThirdParty\Plugins\WPGeotargeting; use WP_Rocket\ThirdParty\Plugins\ContactForm7; use WP_Rocket\ThirdParty\Plugins\SEO\RankMathSEO; use WP_Rocket\ThirdParty\Plugins\SEO\AllInOneSEOPack; use WP_Rocket\ThirdParty\Plugins\SEO\SEOPress; use WP_Rocket\ThirdParty\Plugins\SEO\TheSEOFramework; use WP_Rocket\ThirdParty\Plugins\Optimization\RocketLazyLoad; use WP_Rocket\ThirdParty\Plugins\Optimization\Perfmatters; use WP_Rocket\ThirdParty\Plugins\Optimization\RapidLoad; use WP_Rocket\ThirdParty\Plugins\I18n\Weglot; /** * Service provider for WP Rocket third party compatibility */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'mobile_subscriber', 'woocommerce_subscriber', 'syntaxhighlighter_subscriber', 'elementor_subscriber', 'ngg_subscriber', 'smush_subscriber', 'imagify_webp_subscriber', 'shortpixel_webp_subscriber', 'ewww_webp_subscriber', 'optimus_webp_subscriber', 'bigcommerce_subscriber', 'beaverbuilder_subscriber', 'amp_subscriber', 'simple_custom_css', 'pdfembedder', 'mod_pagespeed', 'adthrive', 'autoptimize', 'wp-meteor', 'revolution_slider_subscriber', 'wordfence_subscriber', 'ezoic', 'pwa', 'convertplug', 'unlimited_elements', 'inline_related_posts', 'jetpack', 'rank_math_seo', 'all_in_one_seo_pack', 'seopress', 'the_seo_framework', 'wpml', 'cloudflare_plugin_facade', 'cloudflare_plugin_subscriber', 'rocket_lazy_load', 'the_events_calendar', 'perfmatters', 'rapidload', 'translatepress', 'wpgeotargeting', 'weglot', 'contactform7', 'termly_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the subscribers in the container * * @since 3.3 * * @return void */ public function register(): void { $this->getContainer()->addShared( 'mobile_subscriber', Mobile_Subscriber::class ); $this->getContainer()->addShared( 'elementor_subscriber', Elementor::class ) ->addArguments( [ 'options', rocket_direct_filesystem(), 'delay_js_html', ] ); $this->getContainer()->addShared( 'woocommerce_subscriber', WooCommerceSubscriber::class ) ->addArgument( 'delay_js_html' ); $this->getContainer()->addShared( 'syntaxhighlighter_subscriber', SyntaxHighlighter_Subscriber::class ); $this->getContainer()->addShared( 'ngg_subscriber', NGG_Subscriber::class ); $this->getContainer()->addShared( 'smush_subscriber', Smush::class ) ->addArguments( [ 'options_api', 'options', ] ); $this->getContainer()->addShared( 'imagify_webp_subscriber', Imagify_Subscriber::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'shortpixel_webp_subscriber', ShortPixel_Subscriber::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'ewww_webp_subscriber', EWWW_Subscriber::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'optimus_webp_subscriber', Optimus_Subscriber::class ); $this->getContainer()->addShared( 'bigcommerce_subscriber', BigCommerce::class ); $this->getContainer()->addShared( 'beaverbuilder_subscriber', BeaverBuilder::class ); $this->getContainer()->addShared( 'amp_subscriber', AMP::class ) ->addArgument( 'cdn_subscriber' ); $this->getContainer()->addShared( 'simple_custom_css', SimpleCustomCss::class ) ->addArguments( [ new StringArgument( rocket_get_constant( 'WP_ROCKET_CACHE_BUSTING_PATH', '' ) ), new StringArgument( rocket_get_constant( 'WP_ROCKET_CACHE_BUSTING_URL', '' ) ), ] ); $this->getContainer()->addShared( 'pdfembedder', PDFEmbedder::class ); $this->getContainer()->addShared( 'mod_pagespeed', ModPagespeed::class ); $this->getContainer()->addShared( 'adthrive', Adthrive::class ); $this->getContainer()->addShared( 'autoptimize', Autoptimize::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'wp-meteor', WPMeteor::class ); $this->getContainer()->addShared( 'revolution_slider_subscriber', RevolutionSlider::class ); $this->getContainer()->addShared( 'wordfence_subscriber', WordFenceCompatibility::class ); $this->getContainer()->addShared( 'ezoic', Ezoic::class ); $this->getContainer()->addShared( 'thirstyaffiliates', ThirstyAffiliates::class ); $this->getContainer()->addShared( 'pwa', PWA::class ); $this->getContainer()->addShared( 'yoast_seo', Yoast::class ); $this->getContainer()->addShared( 'convertplug', ConvertPlug::class ); $this->getContainer()->addShared( 'unlimited_elements', UnlimitedElements::class ); $this->getContainer()->addShared( 'inline_related_posts', InlineRelatedPosts::class ); $this->getContainer()->addShared( 'wpml', WPML::class ); $this->getContainer()->add( 'cloudflare_plugin_facade', CloudflareFacade::class ); $this->getContainer()->addShared( 'cloudflare_plugin_subscriber', Cloudflare::class ) ->addArguments( [ 'options', 'options_api', 'beacon', 'cloudflare_plugin_facade', ] ); $this->getContainer()->addShared( 'jetpack', Jetpack::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'convertplug', ConvertPlug::class ); $this->getContainer()->addShared( 'rank_math_seo', RankMathSEO::class ); $this->getContainer()->addShared( 'all_in_one_seo_pack', AllInOneSEOPack::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'seopress', SEOPress::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'the_seo_framework', TheSEOFramework::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'rocket_lazy_load', RocketLazyLoad::class ); $this->getContainer()->addShared( 'the_events_calendar', TheEventsCalendar::class ); $this->getContainer()->addShared( 'perfmatters', Perfmatters::class ); $this->getContainer()->addShared( 'rapidload', RapidLoad::class ); $this->getContainer()->addShared( 'weglot', Weglot::class ); $this->getContainer()->addShared( 'translatepress', TranslatePress::class ); $this->getContainer()->addShared( 'wpgeotargeting', WPGeotargeting::class ); $this->getContainer()->addShared( 'contactform7', ContactForm7::class ); $this->getContainer()->addShared( 'termly_subscriber', Termly::class ); } } ThirdParty/Plugins/Optimization/Perfmatters.php 0000644 00000002656 15174677547 0016012 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\Optimization; use WP_Rocket\Event_Management\Subscriber_Interface; class Perfmatters implements Subscriber_Interface { /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events() { if ( ! defined( 'PERFMATTERS_VERSION' ) ) { return []; } return [ 'rocket_disable_rucss_setting' => 'disable_rucss_setting', 'pre_get_rocket_option_remove_unused_css' => 'maybe_disable_rucss', ]; } /** * Disable RUCSS setting. * * @param array $status RUCSS option status. * @return array */ public function disable_rucss_setting( array $status ): array { if ( ! $this->is_perfmatters_rucss_active() ) { return $status; } return [ 'disable' => true, 'text' => __( 'Remove Unused CSS is currently activated in Perfmatters. If you want to use WP Rocket\'s Remove Unused CSS feature, disable this option in Perfmatters.', 'rocket' ), ]; } /** * Disable RUCSS option. * * @return bool|null */ public function maybe_disable_rucss() { return $this->is_perfmatters_rucss_active() ? false : null; } /** * Check if perfmatters rucss is active. * * @return bool */ private function is_perfmatters_rucss_active(): bool { $perfmatters_options = get_option( 'perfmatters_options' ); return ! empty( $perfmatters_options['assets']['remove_unused_css'] ); } } ThirdParty/Plugins/Optimization/Hummingbird.php 0000644 00000016054 15174677547 0015760 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\Optimization; use WP_Hummingbird_Settings; use WP_Hummingbird_Utils; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Hummingbird compatibility class */ class Hummingbird implements Subscriber_Interface { /** * WP Rocket Options instance * * @var Options_Data */ private $options; /** * Array containing the errors * * @var array */ private $errors = []; /** * Constructor * * @param Options_Data $options WP Rocket Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3.3 * @author Remy Perona * * @return array */ public static function get_subscribed_events() { return [ 'admin_notices' => 'warning_notice', ]; } /** * Display a notice if conflicting options are active * * @since 3.3.3 * @author Remy Perona * * @return void */ public function warning_notice() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( ! is_plugin_active( 'hummingbird-performance/wp-hummingbird.php' ) && ! is_plugin_active( 'wp-hummingbird/wp-hummingbird.php' ) ) { return; } $this->check_cache(); $this->check_assets(); $this->check_browser_caching(); $this->check_gzip(); $this->check_emoji(); if ( 0 === count( $this->errors ) ) { return; } // Translators: %s = Plugin name. $message = '<p>' . sprintf( _nx( 'Please deactivate the following %s option which conflicts with WP Rocket features:', 'Please deactivate the following %s options which conflict with WP Rocket features:', count( $this->errors ), 'Hummingbird notice', 'rocket' ), 'Hummingbird' ) . '</p>'; $message .= '<ul>'; foreach ( $this->errors as $error ) { $message .= '<li>' . $error . '</li>'; } $message .= '</ul>'; rocket_notice_html( [ 'status' => 'error', 'message' => $message, ] ); } /** * Checks if the Hummingbird Utils class exists * * @since 3.3.3 * @author Remy Perona * * @return boolean */ private function is_utils_available() { if ( ! class_exists( 'WP_Hummingbird_Utils' ) ) { return false; } if ( ! method_exists( 'WP_Hummingbird_Utils', 'get_module' ) ) { // @phpstan-ignore-line return false; } return true; } /** * Checks if the Hummingbird settings class exists * * @since 3.3.3 * @author Remy Perona * * @return boolean */ private function is_settings_available() { if ( ! class_exists( 'WP_Hummingbird_Settings' ) ) { return false; } if ( ! method_exists( 'WP_Hummingbird_Settings', 'get_setting' ) ) { // @phpstan-ignore-line return false; } return true; } /** * Checks if Hummingbird and WP Rocket disable emoji options are active at the same time * * @since 3.3.3 * @author Remy Perona * * @return bool */ private function check_emoji() { if ( ! $this->is_settings_available() ) { return false; } if ( $this->options->get( 'emoji' ) && WP_Hummingbird_Settings::get_setting( 'emoji', 'advanced' ) ) { // Translators: %1$s = Plugin name, %2$s = <em>, %3$s = </em>. $this->errors[] = sprintf( _x( '%1$s %2$sdisable emoji%3$s conflicts with WP Rockets %2$sdisable emoji%3$s', 'Hummingbird notice', 'rocket' ), 'Hummingbird', '<em>', '</em>' ); return true; } return false; } /** * Checks if Hummingbird Gzip rules are in the htaccess file. * * @since 3.3.3 * @author Remy Perona * * @return bool */ private function check_gzip() { if ( ! $this->is_utils_available() ) { return false; } $gzip = WP_Hummingbird_Utils::get_module( 'gzip' ); if ( ! $gzip instanceof \WP_Hummingbird_Module_GZip ) { return false; } if ( ! method_exists( $gzip, 'is_htaccess_written' ) ) { // @phpstan-ignore-line return false; } if ( ! method_exists( $gzip, 'get_server_type' ) ) { // @phpstan-ignore-line return false; } if ( $gzip::is_htaccess_written( 'gzip' ) && 'apache' === $gzip::get_server_type() ) { // Translators: %1$s = Plugin name, %2$s = <em>, %3$s = </em>. $this->errors[] = sprintf( _x( '%1$s %2$sGZIP compression%3$s conflicts with WP Rocket %2$sGZIP compression%3$s', 'Hummingbird notice', 'rocket' ), 'Hummingbird', '<em>', '</em>' ); return true; } return false; } /** * Checks if Hummingbird browser caching rules are in the htaccess file * * @since 3.3.3 * @author Remy Perona * * @return bool */ private function check_browser_caching() { if ( ! $this->is_utils_available() ) { return false; } $caching = WP_Hummingbird_Utils::get_module( 'caching' ); if ( ! $caching instanceof \WP_Hummingbird_Module_Caching ) { return false; } if ( ! method_exists( $caching, 'is_htaccess_written' ) ) { // @phpstan-ignore-line return false; } if ( ! method_exists( $caching, 'get_server_type' ) ) { // @phpstan-ignore-line return false; } if ( $caching::is_htaccess_written( 'caching' ) && 'apache' === $caching::get_server_type() ) { // Translators: %1$s = Plugin name, %2$s = <em>, %3$s = </em>. $this->errors[] = sprintf( _x( '%1$s %2$sbrowser caching%3$s conflicts with WP Rocket %2$sbrowser caching%3$s', 'Hummingbird notice', 'rocket' ), 'Hummingbird', '<em>', '</em>' ); return true; } return false; } /** * Checks if Hummingbird Cache is active * * @since 3.3.3 * @author Remy Perona * * @return bool */ private function check_cache() { if ( ! $this->is_utils_available() ) { return false; } $cache = WP_Hummingbird_Utils::get_module( 'page_cache' ); if ( ! $cache instanceof \WP_Hummingbird_Module_Page_Cache ) { return false; } if ( ! method_exists( $cache, 'is_active' ) ) { // @phpstan-ignore-line return false; } if ( $cache->is_active() ) { // Translators: %1$s = Plugin name, %2$s = <em>, %3$s = </em>. $this->errors[] = sprintf( _x( '%1$s %2$spage caching%3$s conflicts with WP Rocket %2$spage caching%3$s', 'Hummingbird notice', 'rocket' ), 'Hummingbird', '<em>', '</em>' ); return true; } return false; } /** * Checks if Hummingbird Assets optimization is active * * Checks against WP Rocket Minify CSS, Minify JS and Defer JS options. * * @since 3.3.3 * @author Remy Perona * * @return bool */ private function check_assets() { if ( ! $this->is_utils_available() ) { return false; } $minify = WP_Hummingbird_Utils::get_module( 'minify' ); if ( ! $minify instanceof \WP_Hummingbird_Module_Minify ) { return false; } if ( ! method_exists( $minify, 'is_active' ) ) { // @phpstan-ignore-line return false; } if ( $minify->is_active() && ( $this->options->get( 'minify_css' ) || $this->options->get( 'minify_js' ) || $this->options->get( 'defer_all_js' ) ) ) { // Translators: %1$s = Plugin name, %2$s = <em>, %3$s = </em>. $this->errors[] = sprintf( _x( '%1$s %2$sasset optimization%3$s conflicts with WP Rocket %2$sfile optimization%3$s', 'Hummingbird notice', 'rocket' ), 'Hummingbird', '<em>', '</em>' ); return true; } return false; } } ThirdParty/Plugins/Optimization/WPMeteor.php 0000644 00000005136 15174677547 0015214 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\Optimization; use WP_Rocket\Event_Management\Subscriber_Interface; class WPMeteor implements Subscriber_Interface { /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_delay_js_settings_field' => 'maybe_disable_delay_js_field', 'activate_wp-meteor/wp-meteor.php' => 'disable_delay_js', 'wp_rocket_upgrade' => [ 'maybe_disable_delay_js', 10, 2 ], 'pre_update_option_wp_rocket_settings' => 'disable_delay_js_on_option_update', ]; } /** * Disable the delay JS field when WP Meteor is active * * @since 3.9.2 * * @param array $field Delay JS field data array. * * @return array */ public function maybe_disable_delay_js_field( $field ): array { if ( ! is_plugin_active( 'wp-meteor/wp-meteor.php' ) ) { return $field; } $field['container_class'][] = 'wpr-isDisabled'; $field['value'] = 0; $field['input_attr']['disabled'] = 1; $field['helper'] = sprintf( // translators: %1$s = plugin name. __( 'Delay JS is currently activated in %1$s. If you want to use WP Rocket’s delay JS, disable %1$s', 'rocket' ), 'WP Meteor' ); return $field; } /** * Disable delay JS option when WP Meteor is activated * * @since 3.9.2 * * @return void */ public function disable_delay_js() { $options = get_option( 'wp_rocket_settings', [] ); $options['delay_js'] = 0; update_option( 'wp_rocket_settings', $options ); } /** * Disable delay JS when updating to 3.9.2 and above and WP Meteor is active * * @since 3.9.2 * * @param string $new_version Plugin new version. * @param string $old_version Plugin old version. * * @return void */ public function maybe_disable_delay_js( $new_version, $old_version ) { if ( version_compare( $old_version, '3.9.2', '>' ) ) { return; } if ( ! is_plugin_active( 'wp-meteor/wp-meteor.php' ) ) { return; } $this->disable_delay_js(); } /** * Disable delay JS on WP Rocket settings update if WP Meteor is active * * @since 3.9.2 * * @param mixed $value The new, unserialized option value. * * @return mixed */ public function disable_delay_js_on_option_update( $value ) { if ( ! is_plugin_active( 'wp-meteor/wp-meteor.php' ) ) { return $value; } if ( ! isset( $value['delay_js'] ) ) { return $value; } if ( 0 === (int) $value['delay_js'] ) { return $value; } $value['delay_js'] = 0; return $value; } } ThirdParty/Plugins/Optimization/Autoptimize.php 0000644 00000010275 15174677547 0016024 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\ThirdParty\Plugins\Optimization; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class Autoptimize implements Subscriber_Interface { /** * WP Rocket Options instance * * @var Options_Data */ private $options; /** * Constructor * * @param Options_Data $options WP Rocket Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Return an array of events that this subscriber listens to. * * @since 3.10.4 * @return array */ public static function get_subscribed_events() { return [ 'admin_notices' => [ [ 'warn_when_js_aggregation_and_delay_js_active' ], [ 'warn_when_aggregate_inline_css_and_cpcss_active' ], ], ]; } /** * Add an admin warning notice when Delay JS and JS Aggregation are both activated. * * @since 3.10.4 */ public function warn_when_js_aggregation_and_delay_js_active() { if ( ! $this->can_notify() ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( ! ( 'on' === get_option( 'autoptimize_js' ) && 'on' === get_option( 'autoptimize_js_aggregate' ) ) || false === (bool) $this->options->get( 'delay_js' ) ) { if ( ! is_array( $boxes ) ) { return; } $this->remove_warning_dismissal( $boxes, __FUNCTION__ ); return; } if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = sprintf( /* Translators: %1$s is an opening <strong> tag; %2$s is a closing </strong> tag */ __( '%1$sWP Rocket: %2$sWe have detected that Autoptimize\'s JavaScript Aggregation feature is enabled. WP Rocket\'s Delay JavaScript Execution will not be applied to the file it creates. We suggest disabling %1$sJavaScript Aggregation%2$s to take full advantage of Delay JavaScript Execution.', 'rocket' ), '<strong>', '</strong>' ); rocket_notice_html( [ 'status' => 'info', 'message' => $message, 'dismissible' => '', 'dismiss_button' => __FUNCTION__, ] ); } /** * Add an admin warning notice when CPCSS and Aggregate Inline CSS are both activated. * * @since 3.10.4 */ public function warn_when_aggregate_inline_css_and_cpcss_active() { if ( ! $this->can_notify() ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( ! ( 'on' === get_option( 'autoptimize_css' ) && 'on' === get_option( 'autoptimize_css_aggregate' ) && 'on' === get_option( 'autoptimize_css_include_inline' ) ) || false === (bool) $this->options->get( 'async_css' ) ) { if ( ! is_array( $boxes ) ) { return; } $this->remove_warning_dismissal( $boxes, __FUNCTION__ ); return; } if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = sprintf( /* Translators: %1$s is an opening <strong> tag; %2$s is a closing </strong> tag */ __( '%1$sWP Rocket: %2$sWe have detected that Autoptimize\'s Aggregate Inline CSS feature is enabled. WP Rocket\'s Load CSS Asynchronously will not work correctly. We suggest disabling %1$sAggregate Inline CSS%2$s to take full advantage of Load CSS Asynchronously Execution.', 'rocket' ), '<strong>', '</strong>' ); rocket_notice_html( [ 'status' => 'info', 'message' => $message, 'dismissible' => '', 'dismiss_button' => __FUNCTION__, ] ); } /** * Whether this compatibility can use notifications. * * @return bool */ private function can_notify() { if ( 'settings_page_wprocket' !== get_current_screen()->id ) { return false; } return rocket_get_constant( 'AUTOPTIMIZE_PLUGIN_VERSION', false ) && current_user_can( 'rocket_manage_options' ); } /** * Remove a warning box dismissal. * * @param array $boxes The rocket_boxes user meta. * @param string $name Slug for the box to be removed. */ private function remove_warning_dismissal( $boxes, $name ) { if ( ! in_array( $name, $boxes, true ) ) { return; } unset( $boxes[ array_search( $name, $boxes, true ) ] ); update_user_meta( get_current_user_id(), 'rocket_boxes', $boxes ); } } ThirdParty/Plugins/Optimization/RapidLoad.php 0000644 00000004131 15174677547 0015343 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\Optimization; use WP_Rocket\Event_Management\Subscriber_Interface; class RapidLoad implements Subscriber_Interface { /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events() { if ( ! defined( 'UUCSS_VERSION' ) ) { return []; } return [ 'rocket_disable_rucss_setting' => 'disable_rucss_setting', 'pre_get_rocket_option_remove_unused_css' => 'maybe_disable_rucss', 'deactivated_plugin' => [ 'rocket_clean_cache_on_deactivation', 12 ], ]; } /** * Disable RUCSS setting. * * @param array $status RUCSS option status. * @return array */ public function disable_rucss_setting( array $status ): array { if ( ! $this->is_rapidload_active() ) { return $status; } return [ 'disable' => true, 'text' => __( 'Automated unused CSS removal is currently activated in RapidLoad Power-Up for Autoptimize. If you want to use WP Rocket\'s Remove Unused CSS feature, disable the RapidLoad Power-Up for Autoptimize plugin.', 'rocket' ), ]; } /** * Disable RUCSS option. * * @return bool|null */ public function maybe_disable_rucss() { return $this->is_rapidload_active() ? false : null; } /** * Clean WP Rocket Cache when Rapidload is deactivated. * * @param string $plugin Plugin file. * @return void */ public function rocket_clean_cache_on_deactivation( string $plugin ): void { if ( ! $this->is_rapidload_active() ) { return; } if ( 'unusedcss/unusedcss.php' !== $plugin ) { return; } rocket_dismiss_box( 'rocket_warning_plugin_modification' ); rocket_clean_domain(); } /** * Check if RapidLoad Power-Up for Autoptimize is active. * * @return boolean */ private function is_rapidload_active(): bool { $autoptimize_uucss_settings = get_option( 'autoptimize_uucss_settings' ); return ( isset( $autoptimize_uucss_settings['uucss_api_key_verified'] ) && 1 === $autoptimize_uucss_settings['uucss_api_key_verified'] && $autoptimize_uucss_settings['valid_domain'] ); } } ThirdParty/Plugins/Optimization/RocketLazyLoad.php 0000644 00000001422 15174677547 0016373 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\Optimization; use WP_Rocket\Event_Management\Subscriber_Interface; class RocketLazyLoad implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { if ( ! defined( 'ROCKET_LL_VERSION' ) ) { return []; } return [ 'rocket_delay_js_exclusions' => 'exclude_rocket_lazyload_script', ]; } /** * Excludes rocket lazyload script from delay js. * * @param array $excluded List of excluded files. * * @return array */ public function exclude_rocket_lazyload_script( $excluded ): array { $excluded[] = 'rocket-lazy-load/assets/js/\d\d\.\d/lazyload.min.js'; return $excluded; } } ThirdParty/Plugins/Optimization/Ezoic.php 0000644 00000002736 15174677547 0014566 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\Optimization; use WP_Rocket\Event_Management\Subscriber_Interface; class Ezoic implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_plugins_to_deactivate' => 'add_conflict', 'rocket_plugins_to_deactivate_explanations' => 'add_conflict_explanations', ]; } /** * Adds Ezoic plugin to plugins to deactivate array * * @param array $plugins List of recommended plugins to deactivate. * * @return array */ public function add_conflict( $plugins ) { $plugins['ezoic'] = 'ezoic-integration/ezoic-integration.php'; return $plugins; } /** * Adds explanation for deactivation recommendation * * @param array $plugins_explanations List of recommended plugins to deactivate explanations. * * @return array */ public function add_conflict_explanations( $plugins_explanations ) { $plugins_explanations['ezoic'] = sprintf( // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. __( 'This plugin blocks WP Rocket\'s caching and optimizations. Deactivate it and use %1$sEzoic\'s nameserver integration%2$s instead.', 'rocket' ), '<a href="https://support.ezoic.com/kb/article/how-can-i-integrate-my-site-with-ezoic" target="_blank" rel="noopener noreferrer">', '</a>' ); return $plugins_explanations; } } ThirdParty/Plugins/Optimization/AMP.php 0000644 00000011714 15174677547 0014126 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\Optimization; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\CDN\Subscriber; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with AMP * * @since 3.5.2 */ class AMP implements Subscriber_Interface { const QUERY = 'amp'; const AMP_OPTIONS = 'amp-options'; /** * WP Rocket CDN Subscriber. * * @var Subscriber_Interface */ private $cdn_subscriber; /** * Constructor * * @param Subscriber_Interface $cdn_subscriber WP Rocket CDN Subscriber. */ public function __construct( Subscriber_Interface $cdn_subscriber ) { $this->cdn_subscriber = $cdn_subscriber; } /** * Subscribed events for AMP. * * @since 3.5.2 */ public static function get_subscribed_events() { $events = [ 'activate_amp/amp.php' => 'generate_config_file', 'deactivate_amp/amp.php' => 'generate_config_file', 'wp' => [ 'disable_options_on_amp', 20 ], 'rocket_cache_query_strings' => 'is_amp_compatible_callback', ]; if ( function_exists( 'is_amp_endpoint' ) ) { $events['update_option_amp-options'] = 'generate_config_file'; $events['rocket_delay_js_exclusions'] = 'exclude_script_from_delay_js'; } return $events; } /** * Regenerate config file on plugin activation / deactivation. * * @since 3.5.2 */ public function generate_config_file() { rocket_generate_config_file(); } /** * Add compatibility with AMP query string by adding it as a cached query string. * * @since 3.5.2 * * @param array $value WP Rocket cache_query_strings value. * @return array */ public function is_amp_compatible_callback( $value ) { if ( ! function_exists( 'is_amp_endpoint' ) ) { return $value; } $options = get_option( self::AMP_OPTIONS, [] ); $query_strings = array_diff( $value, [ static::QUERY ] ); if ( empty( $options['theme_support'] ) ) { return $query_strings; } if ( in_array( $options['theme_support'], [ 'transitional', 'reader' ], true ) ) { $query_strings[] = static::QUERY; } return $query_strings; } /** * Removes Minification, DNS Prefetch, LazyLoad, Defer JS when on an AMP document. * * This covers AMP documents as output by the official AMP plugin for WordPress * (https://amp-wp.org/) as well as Web Stories for WordPress (https://wp.stories.google/), * which both support the `is_amp_endpoint` function checks. * * However, in the case of Web Stories, `is_amp_endpoint` is only defined on * the `wp` action, not earlier. Hence doing the `function_exists` check at this stage * instead of in the `get_subscribed_events()` method. * * @since 3.5.2 */ public function disable_options_on_amp() { // No endpoint function means we're not running amp here. if ( ! function_exists( 'is_amp_endpoint' ) ) { return; } // We can get a false negative from is_amp_endpoint when web stories is active, so we have to make sure neither is in play. if ( ! is_amp_endpoint() && ! ( is_singular( 'web-story' ) && ! is_embed() && ! post_password_required() ) ) { return; } global $wp_filter; remove_filter( 'wp_resource_hints', 'rocket_dns_prefetch', 10 ); add_filter( 'do_rocket_lazyload', '__return_false' ); add_filter( 'do_rocket_lazyload_iframes', '__return_false' ); add_filter( 'pre_get_rocket_option_async_css', '__return_false' ); add_filter( 'pre_get_rocket_option_delay_js', '__return_false' ); add_filter( 'pre_get_rocket_option_preload_links', '__return_false' ); add_filter( 'pre_get_rocket_option_minify_js', '__return_false' ); add_filter( 'pre_get_rocket_option_minify_google_fonts', '__return_false' ); add_filter( 'pre_get_rocket_option_lazyload_css_bg_img', '__return_false' ); add_filter( 'pre_get_cloudflare_protocol_rewrite', '__return_false' ); add_filter( 'do_rocket_protocol_rewrite', '__return_false' ); unset( $wp_filter['rocket_buffer'] ); $options = get_option( self::AMP_OPTIONS, [] ); if ( ! empty( $options['theme_support'] ) && in_array( $options['theme_support'], [ 'transitional', 'reader' ], true ) ) { add_filter( 'rocket_cdn_reject_files', [ $this, 'reject_files' ], PHP_INT_MAX ); add_filter( 'rocket_buffer', [ $this->cdn_subscriber, 'rewrite' ] ); add_filter( 'rocket_buffer', [ $this->cdn_subscriber, 'rewrite_srcset' ] ); } } /** * Adds all CSS and JS files to the list of excluded CDN files. * * @since 3.5.5 * * @param array $files List of excluded files. * @return array List of excluded files. */ public function reject_files( $files ) { return array_merge( $files, [ '(.*).css', '(.*).js', ] ); } /** * Adds the switching script from AMP to delay JS excluded files * * @since 3.11.1 * * @param array $excluded List of excluded files. * @return array List of excluded files. */ public function exclude_script_from_delay_js( $excluded ) { $excluded[] = 'amp-mobile-version-switcher'; return $excluded; } } ThirdParty/Plugins/ContactForm7.php 0000644 00000006313 15174677547 0013330 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; class ContactForm7 implements Subscriber_Interface { /** * Required CF6 version. * * Version in which the wpcf7_shortcode_callback action was introduced. * * @var string */ const REQUIRED_CF7_VERSION = '5.8.1'; /** * CF7 scripts load status. * * @var bool */ private $load_js; /** * Subscribed events. */ public static function get_subscribed_events() { return [ 'template_redirect' => 'maybe_optimize_contact_form_7', ]; } /** * Optimize ContactForm7 scripts. */ public function maybe_optimize_contact_form_7() { /** * Filters register this compatibility events or not. * * @param bool $status Load the compatibility file or not, default is True. * @param string $thirdparty Thirdparty id. */ if ( ! apply_filters( 'rocket_thirdparty_load', true, 'contact-form-7' ) ) { return; } // The wpcf7_shortcode_callback action was added in CF7 version 5.8.1. if ( ! defined( 'WPCF7_VERSION' ) || version_compare( WPCF7_VERSION, self::REQUIRED_CF7_VERSION, '<' ) ) { return; } // Force scripts and styles to not load by default. add_filter( 'wpcf7_load_js', '__return_false' ); add_filter( 'wpcf7_load_css', '__return_false' ); $this->load_js = false; add_action( 'wp_enqueue_scripts', [ $this, 'load_scripts_fallback' ], PHP_INT_MAX ); add_action( 'wpcf7_enqueue_scripts', [ $this, 'scripts_loaded' ] ); // Conditionally enqueue scripts. add_action( 'wpcf7_shortcode_callback', [ $this, 'conditionally_enqueue_scripts' ] ); add_action( 'wpcf7_shortcode_callback', [ $this, 'conditionally_enqueue_styles' ] ); } /** * Enqueue scripts if not already enqueued. */ public function conditionally_enqueue_scripts() { if ( $this->load_js ) { // Prevent double-enqueueing when multiple forms present. return; } if ( did_action( 'wp_enqueue_scripts' ) ) { // @phpstan-ignore-next-line wpcf7_enqueue_scripts(); return; } add_filter( 'wpcf7_load_js', '__return_true', 11 ); } /** * Enqueue styles if not already enqueued. */ public function conditionally_enqueue_styles() { if ( did_action( 'wpcf7_enqueue_styles' ) ) { // Prevent double-enqueueing when multiple forms present. return; } if ( did_action( 'wp_enqueue_scripts' ) ) { // @phpstan-ignore-next-line wpcf7_enqueue_styles(); return; } add_filter( 'wpcf7_load_css', '__return_true', 11 ); } /** * Load CF7 scripts only when CF7 main script is added as a dependency. * * @return void */ public function load_scripts_fallback() { if ( $this->load_js || ! $this->cf7_script_enqueued_as_dependency() ) { return; } // @phpstan-ignore-next-line wpcf7_enqueue_scripts(); } /** * Check if CF7 main script is added as a dependency for any script. * * @return bool */ private function cf7_script_enqueued_as_dependency() { foreach ( wp_scripts()->registered as $script ) { foreach ( $script->deps as $dep ) { if ( 'contact-form-7' === $dep ) { return true; } } } return false; } /** * Set a flag that scripts are loaded. * * @return void */ public function scripts_loaded() { $this->load_js = true; } } ThirdParty/Plugins/PWA.php 0000644 00000001240 15174677547 0011443 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; class PWA implements Subscriber_Interface { /** * Subscribed events. */ public static function get_subscribed_events() { return [ 'rocket_cache_reject_uri' => 'exclude_service_worker', ]; } /** * Excludes the PWA service worker URL * * @param array $excluded Array of excluded URLs. * * @return array */ public function exclude_service_worker( $excluded ): array { if ( ! function_exists( 'wp_get_service_worker_url' ) ) { return $excluded; } $excluded[] = '/wp.serviceworker/?'; return $excluded; } } ThirdParty/Plugins/SimpleCustomCss.php 0000644 00000005647 15174677547 0014130 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Engine\Optimization\CSSTrait; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with Simple Custom CSS plugin. * * @since 3.6 * @author Soponar Cristina */ class SimpleCustomCss implements Subscriber_Interface { use CSSTrait; const FILENAME = 'sccss.css'; /** * Base cache busting folder path * * @var string */ private $cache_busting_path; /** * SCCSS cache busting file path * * @var string */ private $filepath; /** * SCCSS cache busting file URL * * @var string */ private $file_url; /** * Constructor * * @param string $cache_busting_path Base cache busting folder path. * @param string $cache_busting_url Base cache busting URL. */ public function __construct( $cache_busting_path, $cache_busting_url ) { $blog_id = get_current_blog_id(); $this->cache_busting_path = $cache_busting_path . $blog_id . '/'; $this->filepath = $this->cache_busting_path . self::FILENAME; $this->file_url = $cache_busting_url . $blog_id . '/' . self::FILENAME; } /** * Subscribed events for AMP. * * @since 3.5.3 * @author Soponar Cristina * @inheritDoc */ public static function get_subscribed_events() { if ( ! defined( 'SCCSS_FILE' ) ) { return []; } return [ 'wp_enqueue_scripts' => [ 'cache_sccss', 98 ], 'update_option_sccss_settings' => 'update_cache_file', ]; } /** * Caches SCCSS code & remove the default enqueued URL * * @since 2.9 * @author Remy Perona */ public function cache_sccss() { if ( ! rocket_direct_filesystem()->exists( $this->filepath ) ) { $this->create_cache_file(); } // This filter is documented in inc/Engine/Optimization/CSS/AbstractCSSOptimization.php. wp_enqueue_style( 'scss', apply_filters( 'rocket_css_url', $this->file_url ), '', rocket_direct_filesystem()->mtime( $this->filepath ) ); remove_action( 'wp_enqueue_scripts', 'sccss_register_style', 99 ); } /** * Deletes & recreates cache for SCCSS code * * @since 3.6 * @author Remy Perona */ public function update_cache_file() { rocket_clean_domain(); $this->create_cache_file(); } /** * Creates cache file for SCCSS code if it does not exist. * * @since 2.9 * @author Remy Perona * * @return bool Returns bool if the files exists or could not be created. */ private function create_cache_file() { $options = get_option( 'sccss_settings' ); $raw_content = isset( $options['sccss-content'] ) ? $options['sccss-content'] : ''; $content = wp_kses( $raw_content, [ '\'', '\"' ] ); $content = str_replace( '>', '>', $content ); $content = $this->apply_font_display_swap( $content ); if ( ! rocket_direct_filesystem()->is_dir( $this->cache_busting_path ) ) { rocket_mkdir_p( $this->cache_busting_path ); } return rocket_put_content( $this->filepath, $content ); } } ThirdParty/Plugins/SEO/Yoast.php 0000644 00000003432 15174677547 0012546 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\SEO; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class Yoast implements Subscriber_Interface { /** * Array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { return [ 'rocket_sitemap_preload_list' => [ 'add_sitemap', 15 ], ]; } /** * Add Yoast SEO sitemap URL to the sitemaps to preload * * @since 2.8 * * @param array $sitemaps An array of sitemaps to preload. * * @return array */ public function add_sitemap( array $sitemaps ): array { if ( ! $this->is_sitemap_enabled() ) { return $sitemaps; } if ( ! class_exists( 'WPSEO_Sitemaps_Router' ) ) { return $sitemaps; } $sitemaps[] = \WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ); return $sitemaps; } /** * Checks if sitemap is enabled in Yoast SEO * * @since 3.11.1 * * @return bool */ private function is_sitemap_enabled(): bool { static $enabled = null; if ( ! is_null( $enabled ) ) { return $enabled; } if ( ! rocket_has_constant( 'WPSEO_VERSION' ) ) { $enabled = false; return $enabled; } $yoast_seo_xml = get_option( 'wpseo_xml', [] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals if ( version_compare( rocket_get_constant( 'WPSEO_VERSION', '' ), '7.0' ) >= 0 ) { $yoast_seo = get_option( 'wpseo', [] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $yoast_seo_xml['enablexmlsitemap'] = isset( $yoast_seo['enable_xml_sitemap'] ) && $yoast_seo['enable_xml_sitemap']; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } $enabled = (bool) $yoast_seo_xml['enablexmlsitemap']; return $enabled; } } ThirdParty/Plugins/SEO/RankMathSEO.php 0000644 00000001730 15174677547 0013522 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\SEO; use RankMath\Helper; use RankMath\Sitemap\Router; use WP_Rocket\Event_Management\Subscriber_Interface; class RankMathSEO implements Subscriber_Interface { /** * Returns an array of events this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { if ( ! class_exists( 'RankMath\Helper' ) ) { return []; } if ( ! defined( 'RANK_MATH_FILE' ) || ! Helper::is_module_active( 'sitemap' ) ) { return []; } return [ 'rocket_sitemap_preload_list' => [ 'add_sitemap', 15 ], ]; } /** * Add SEO sitemap URL to the sitemaps to preload * * @param array $sitemaps Sitemaps to preload. * * @return array */ public function add_sitemap( $sitemaps ) { if ( ! class_exists( 'RankMath\Sitemap\Router' ) ) { return $sitemaps; } $sitemaps[] = Router::get_base_url( 'sitemap_index.xml' ); return $sitemaps; } } ThirdParty/Plugins/SEO/SEOPress.php 0000644 00000002275 15174677547 0013116 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\SEO; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class SEOPress implements Subscriber_Interface { /** * Option instance. * * @var Options_Data */ protected $option; /** * Instantiate class. * * @param Options_Data $option Option instance. */ public function __construct( Options_Data $option ) { $this->option = $option; } /** * Subscribed events. */ public static function get_subscribed_events() { if ( ! function_exists( 'seopress_get_toggle_option' ) || 1 !== (int) seopress_get_toggle_option( 'xml-sitemap' ) ) { return []; } if ( ! method_exists( seopress_get_service( 'SitemapOption' ), 'isEnabled' ) || 1 !== (int) seopress_get_service( 'SitemapOption' )->isEnabled() ) { return []; } return [ 'rocket_sitemap_preload_list' => [ 'add_seopress_sitemap', 15 ], ]; } /** * Add SEOPress sitemap URL to the sitemaps to preload * * @param array $sitemaps Sitemaps to preload. * @return array Updated Sitemaps to preload */ public function add_seopress_sitemap( $sitemaps ) { $sitemaps[] = get_home_url() . '/sitemaps.xml'; return $sitemaps; } } ThirdParty/Plugins/SEO/AllInOneSEOPack.php 0000644 00000004367 15174677547 0014266 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\SEO; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class AllInOneSEOPack implements Subscriber_Interface { /** * Option instance. * * @var Options_Data */ protected $option; /** * Instantiate class. * * @param Options_Data $option Option instance. */ public function __construct( Options_Data $option ) { $this->option = $option; } /** * Subscribed events. */ public static function get_subscribed_events() { $aioseo_v3 = defined( 'AIOSEOP_VERSION' ); $aioseo_v4 = defined( 'AIOSEO_VERSION' ) && function_exists( 'aioseo' ); if ( ! $aioseo_v3 && ! $aioseo_v4 ) { return []; } return [ 'rocket_sitemap_preload_list' => [ 'add_all_in_one_seo_sitemap', 15 ], ]; } /** * Add All in One SEO Sitemap to the preload list * * @param Array $sitemaps Array of sitemaps to preload. * @return Array Updated array of sitemaps to preload */ public function add_all_in_one_seo_sitemap( $sitemaps ) { $aioseo_v3 = defined( 'AIOSEOP_VERSION' ); $aioseo_v4 = defined( 'AIOSEO_VERSION' ) && function_exists( 'aioseo' ); $sitemap_enabled = false; if ( $aioseo_v3 && ! $aioseo_v4 ) { $aioseop_options = get_option( 'aioseop_options' ); $sitemap_enabled = ( isset( $aioseop_options['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] ) && 'on' === $aioseop_options['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] ) || ( ! isset( $aioseop_options['modules']['aiosp_feature_manager_options'] ) && isset( $aioseop_options['modules']['aiosp_sitemap_options'] ) ); } if ( ( ! $aioseo_v4 && ! $sitemap_enabled ) || // @phpstan-ignore-next-line ( $aioseo_v4 && ! aioseo()->options->sitemap->general->enable ) ) { return $sitemaps; } if ( $aioseo_v3 ) { $sitemaps[] = trailingslashit( home_url() ) . apply_filters( 'aiosp_sitemap_filename', 'sitemap' ) . '.xml'; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } elseif ( $aioseo_v4 ) { $sitemaps[] = trailingslashit( home_url() ) . apply_filters( 'aioseo_sitemap_filename', 'sitemap' ) . '.xml'; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } return $sitemaps; } } ThirdParty/Plugins/SEO/TheSEOFramework.php 0000644 00000004541 15174677547 0014416 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\SEO; use The_SEO_Framework\Bridges\Sitemap; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class TheSEOFramework implements Subscriber_Interface { /** * Option instance. * * @var Options_Data */ protected $option; /** * Instantiate the class. * * @param Options_Data $option Option instance. */ public function __construct( Options_Data $option ) { $this->option = $option; } /** * Subscribed events. */ public static function get_subscribed_events() { if ( ! function_exists( 'the_seo_framework' ) ) { return []; } $tsf = the_seo_framework(); // Either TSF < 3.1, or the plugin's silenced (soft-disabled) via a drop-in. if ( empty( $tsf->loaded ) ) { return []; } /** * 1. Performs option & other checks. * 2. Checks for conflicting sitemap plugins that might prevent loading. * * These methods cache their output at runtime. * * @link https://github.com/wp-media/wp-rocket/issues/899 */ // @phpstan-ignore-next-line if ( ! $tsf->can_run_sitemap() ) { return []; } return [ 'rocket_sitemap_preload_list' => [ 'add_tsf_sitemap_to_preload', 15 ], ]; } /** * Adds TSF sitemap URLs to preload. * * @param array $sitemaps Sitemaps to preload. * @return array Updated Sitemaps to preload */ public function add_tsf_sitemap_to_preload( $sitemaps ) { // The autoloader in TSF doesn't check for file_exists(). So, use version compare instead to prevent fatal errors. if ( version_compare( rocket_get_constant( 'THE_SEO_FRAMEWORK_VERSION', false ), '4.0', '>=' ) ) { // TSF 4.0+. Expect the class to exist indefinitely. $sitemap_bridge = Sitemap::get_instance(); foreach ( $sitemap_bridge->get_sitemap_endpoint_list() as $id => $data ) { // When the sitemap is good enough for a robots display, we determine it as valid for precaching. // Non-robots display types are among the stylesheet endpoint, or the Yoast SEO-compatible endpoint. // In other words, this enables support for ALL current and future public sitemap endpoints. if ( ! empty( $data['robots'] ) ) { $sitemaps[] = $sitemap_bridge->get_expected_sitemap_endpoint_url( $id ); } } } else { // Deprecated. TSF <4.0. $sitemaps[] = the_seo_framework()->get_sitemap_xml_url(); } return $sitemaps; } } ThirdParty/Plugins/Ecommerce/WooCommerceSubscriber.php 0000644 00000040275 15174677547 0017171 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\Ecommerce; use WP_Post; use WP_Rocket\Engine\Optimization\DelayJS\HTML; use WP_Rocket\Event_Management\Event_Manager; use WP_Rocket\Event_Management\Event_Manager_Aware_Subscriber_Interface; use WP_Rocket\Traits\Config_Updater; /** * WooCommerce compatibility * * @since 3.1 */ class WooCommerceSubscriber implements Event_Manager_Aware_Subscriber_Interface { use Config_Updater; /** * The WordPress Event Manager * * @var Event_Manager */ protected $event_manager; /** * Delay JS HTML class. * * @var HTML */ private $delayjs_html; /** * WooCommerceSubscriber constructor. * * @param HTML $delayjs_html DelayJS HTML class. */ public function __construct( HTML $delayjs_html ) { $this->delayjs_html = $delayjs_html; } /** * {@inheritdoc} * * @param Event_Manager $event_manager The WordPress Event Manager. */ public function set_event_manager( Event_Manager $event_manager ) { $this->event_manager = $event_manager; } /** * {@inheritdoc} */ public static function get_subscribed_events() { $events = [ 'activate_woocommerce/woocommerce.php' => [ 'activate_woocommerce', 11 ], 'deactivate_woocommerce/woocommerce.php' => [ 'deactivate_woocommerce', 11 ], ]; if ( class_exists( 'WooCommerce' ) ) { $events['update_option_woocommerce_cart_page_id'] = [ 'after_update_single_option', 10, 2 ]; $events['update_option_woocommerce_checkout_page_id'] = [ 'after_update_single_option', 10, 2 ]; $events['update_option_woocommerce_myaccount_page_id'] = [ 'after_update_single_option', 10, 2 ]; $events['update_option_woocommerce_default_customer_address'] = [ 'after_update_single_option', 10, 2 ]; $events['shutdown'] = 'maybe_update_config'; $events['woocommerce_save_product_variation'] = 'clean_cache_after_woocommerce_save_product_variation'; $events['transition_post_status'] = [ 'maybe_exclude_page', 10, 3 ]; $events['rocket_cache_reject_uri'] = [ [ 'exclude_pages' ], ]; $events['rocket_preload_exclude_urls'] = [ [ 'exclude_pages' ], ]; $events['rocket_cache_query_strings'] = 'cache_geolocation_query_string'; $events['rocket_cpcss_excluded_taxonomies'] = 'exclude_product_attributes_cpcss'; $events['after_rocket_clean_post_urls'] = [ 'reformat_shop_url_for_preload', 10, 2 ]; $events['rocket_exclude_post_taxonomy'] = 'exclude_product_shipping_taxonomy'; /** * Filters activation of WooCommerce empty cart caching * * @since 3.1 * * @param bool $cache_cart true to activate, false to deactivate. */ if ( apply_filters( 'rocket_cache_wc_empty_cart', true ) ) { $events['init'] = [ 'serve_cache_empty_cart', 11 ]; $events['template_redirect'] = [ 'cache_empty_cart', -1 ]; $events['switch_theme'] = 'delete_cache_empty_cart'; } $events['wp_head'] = 'show_empty_product_gallery_with_delayJS'; $events['rocket_delay_js_exclusions'] = 'show_notempty_product_gallery_with_delayJS'; $events['wp_ajax_woocommerce_product_ordering'] = [ 'disallow_rocket_clean_post', 9 ]; $events['woocommerce_after_product_ordering'] = [ 'allow_rocket_clean_post' ]; } if ( class_exists( 'WC_API' ) ) { $events['rocket_cache_reject_uri'][] = [ 'exclude_wc_rest_api' ]; } return $events; } /** * Reformat Shop URL to prevent error on preload. * * @param array $urls urls cleared. * @param WP_Post $object post object. * @return array */ public function reformat_shop_url_for_preload( array $urls, $object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound $post_type = $object->post_type; if ( 'product' !== $post_type ) { return $urls; } $post_type_archive = get_post_type_archive_link( $post_type ); if ( ! $post_type_archive ) { return $urls; } // Rename the caching filename for SSL URLs. $filename = 'index'; if ( is_ssl() ) { $filename .= '-https'; } $post_type_archive = trailingslashit( $post_type_archive ); $index_url = $post_type_archive . $filename . '.html'; $index_url_gzip = $post_type_archive . $filename . '.html_gzip'; $pagination_url = $post_type_archive . $GLOBALS['wp_rewrite']->pagination_base; foreach ( $urls as $index => $url ) { if ( in_array( $url, [ $index_url, $index_url_gzip, $pagination_url ], true ) ) { $urls[ $index ] = $post_type_archive; } } return array_unique( $urls ); } /** * Add query string to exclusion when activating the plugin * * @since 2.8.6 * @author Rémy Perona */ public function activate_woocommerce() { $this->event_manager->add_callback( 'rocket_cache_reject_uri', [ $this, 'exclude_pages' ] ); $this->event_manager->add_callback( 'rocket_cache_reject_uri', [ $this, 'exclude_wc_rest_api' ] ); $this->event_manager->add_callback( 'rocket_cache_query_strings', [ $this, 'cache_geolocation_query_string' ] ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } /** * Remove query string from exclusion when deactivating the plugin * * @since 2.8.6 * @author Rémy Perona */ public function deactivate_woocommerce() { $this->event_manager->remove_callback( 'rocket_cache_reject_uri', [ $this, 'exclude_pages' ] ); $this->event_manager->remove_callback( 'rocket_cache_reject_uri', [ $this, 'exclude_wc_rest_api' ] ); $this->event_manager->remove_callback( 'rocket_cache_query_strings', [ $this, 'cache_geolocation_query_string' ] ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } /** * Clean product cache on variation update * * @since 2.9 * @author Remy Perona * * @param int $variation_id ID of the variation. * @return bool */ public function clean_cache_after_woocommerce_save_product_variation( $variation_id ) { $product_id = wp_get_post_parent_id( $variation_id ); if ( ! $product_id ) { return false; } rocket_clean_post( $product_id ); return true; } /** * Maybe regenerate the htaccess & config file if a WooCommerce page is published * * @since 3.1 * @author Remy Perona * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. * @return bool */ public function maybe_exclude_page( $new_status, $old_status, $post ) { if ( 'publish' === $old_status || 'publish' !== $new_status ) { return false; } if ( ! function_exists( 'wc_get_page_id' ) ) { return false; } if ( wc_get_page_id( 'checkout' ) !== $post->ID && wc_get_page_id( 'cart' ) !== $post->ID && wc_get_page_id( 'myaccount' ) !== $post->ID ) { return false; } // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); return true; } /** * Exclude WooCommerce cart, checkout and account pages from caching * * @since 2.11 Moved to 3rd party * @since 2.4 * * @param array $urls An array of excluded pages. * @return array Updated array of excluded pages */ public function exclude_pages( $urls ) { if ( ! function_exists( 'wc_get_page_id' ) ) { return $urls; } $checkout_urls = $this->exclude_page( wc_get_page_id( 'checkout' ), 'page', '?(.*)' ); $cart_urls = $this->exclude_page( wc_get_page_id( 'cart' ) ); $account_urls = $this->exclude_page( wc_get_page_id( 'myaccount' ), 'page', '?(.*)' ); return array_merge( $urls, $checkout_urls, $cart_urls, $account_urls ); } /** * Excludes WooCommerce checkout page from cache * * @since 3.1 * @author Remy Perona * * @param int $page_id ID of page to exclude. * @param string $post_type Post type of the page. * @param string $pattern Pattern to use for the exclusion. * @return array */ private function exclude_page( $page_id, $post_type = 'page', $pattern = '' ) { global $wp_rewrite; $urls = []; if ( $page_id <= 0 || (int) get_option( 'page_on_front' ) === $page_id ) { return $urls; } if ( 'publish' !== get_post_status( $page_id ) ) { return $urls; } if ( $wp_rewrite->use_trailing_slashes ) { $pattern = "?$pattern"; } $urls = get_rocket_i18n_translated_post_urls( $page_id, $post_type, $pattern ); return $urls; } /** * Automatically cache v query string when WC geolocation with cache compatibility option is active * * @since 2.8.6 * @author Rémy Perona * * @param array $query_strings list of query strings to cache. * @return array Updated list of query strings to cache */ public function cache_geolocation_query_string( $query_strings ) { if ( 'geolocation_ajax' !== get_option( 'woocommerce_default_customer_address' ) ) { return $query_strings; } $query_strings[] = 'v'; return $query_strings; } /** * Returns WooCommerce API endpoint * * @since 3.1 * @author Remy Perona * * @return string */ private function get_wc_api_endpoint() { return home_url( '/wc-api/v(.*)' ); } /** * Exclude WooCommerce REST API URL from cache * * @since 2.6.5 * * @param array $urls URLs to exclude from cache. * @return array Updated list of URLs to exclude from cache */ public function exclude_wc_rest_api( $urls ) { /** * By default, don't cache the WooCommerce REST API. * * @since 2.6.5 * * @param bool $cache_wc_rest_api false will force to cache the WooCommerce REST API */ if ( apply_filters( 'rocket_cache_reject_wc_rest_api', true ) ) { $urls[] = rocket_clean_exclude_file( $this->get_wc_api_endpoint() ); } return $urls; } /** * Serves the empty cart cache * * @since 3.1 * @author Remy Perona * * @return void */ public function serve_cache_empty_cart() { if ( ! $this->is_get_refreshed_fragments() || rocket_bypass() ) { return; } $cart = $this->get_cache_empty_cart(); if ( false !== $cart ) { @header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged echo $cart; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. die(); } } /** * Creates the empty cart cache * * @since 3.1 * @author Remy Perona * * @return void */ public function cache_empty_cart() { if ( ! $this->is_get_refreshed_fragments() || rocket_bypass() ) { return; } $cart = $this->get_cache_empty_cart(); if ( false !== $cart ) { return; } ob_start( [ $this, 'save_cache_empty_cart' ] ); } /** * Gets the empty cart cache * * @since 3.1 * @author Remy Perona * * @return mixed */ private function get_cache_empty_cart() { $lang = rocket_get_current_language(); if ( $lang ) { return get_transient( 'rocket_get_refreshed_fragments_cache_' . $lang ); } return get_transient( 'rocket_get_refreshed_fragments_cache' ); } /** * Saves the empty cart JSON in a transient * * @since 3.1 * @author Remy Perona * * @param string $content Current buffer content. * @return string */ private function save_cache_empty_cart( $content ) { $lang = rocket_get_current_language(); if ( $lang ) { set_transient( 'rocket_get_refreshed_fragments_cache_' . $lang, $content, 7 * DAY_IN_SECONDS ); return $content; } set_transient( 'rocket_get_refreshed_fragments_cache', $content, 7 * DAY_IN_SECONDS ); return $content; } /** * Checks if the request is for get_refreshed_fragments and the cart is empty * * @since 3.1 * @author Remy Perona * * @return boolean */ private function is_get_refreshed_fragments() { if ( ! isset( $_GET['wc-ajax'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return false; } if ( 'get_refreshed_fragments' !== $_GET['wc-ajax'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return false; } if ( ! empty( $_COOKIE['woocommerce_cart_hash'] ) ) { return false; } if ( ! empty( $_COOKIE['woocommerce_items_in_cart'] ) ) { return false; } return true; } /** * Deletes the empty cart cache * * @since 3.1 * @author Remy Perona * * @return void */ public function delete_cache_empty_cart() { $langs = get_rocket_i18n_code(); if ( $langs ) { foreach ( $langs as $lang ) { delete_transient( 'rocket_get_refreshed_fragments_cache_' . $lang ); } } delete_transient( 'rocket_get_refreshed_fragments_cache' ); } /** * Excludes WC product attributes taxonomies from CPCSS generation * * @since 3.3.5 * @author Remy Perona * * @param array $excluded_taxonomies Taxonomies excluded from CPCSS generation. * @return array */ public function exclude_product_attributes_cpcss( $excluded_taxonomies ) { if ( ! function_exists( 'wc_get_attribute_taxonomy_names' ) ) { return $excluded_taxonomies; } return array_merge( $excluded_taxonomies, wc_get_attribute_taxonomy_names() ); } /** * Exclude product_shipping_class taxonomy from post purge * * @since 3.9.1 * * @param array $excluded_taxonomies Array of excluded taxonomies names. * * @return array */ public function exclude_product_shipping_taxonomy( $excluded_taxonomies ) { $excluded_taxonomies[] = 'product_shipping_class'; return $excluded_taxonomies; } /** * Check if current product page has images in gallery. * * @since 3.9.1 * * @return bool */ private function product_has_gallery_images() { $product = wc_get_product( get_the_ID() ); if ( empty( $product ) ) { return false; } return ! empty( $product->get_gallery_image_ids() ); } /** * Show product gallery main image directly when delay JS is enabled. * * @since 3.9.1 */ public function show_empty_product_gallery_with_delayJS() { if ( ! $this->delayjs_html->is_allowed() ) { return; } if ( ! is_product() ) { return; } if ( $this->product_has_gallery_images() ) { return; } echo '<style>.woocommerce-product-gallery{ opacity: 1 !important; }</style>'; } /** * Exclude some JS files from delay JS when product gallery has images. * * @since 3.9.1 * * @param array $exclusions Exclusions array. * * @return array */ public function show_notempty_product_gallery_with_delayJS( array $exclusions = [] ): array { global $wp_version; if ( ! $this->delayjs_html->is_allowed() ) { return $exclusions; } if ( ! is_product() ) { return $exclusions; } if ( ! $this->product_has_gallery_images() ) { return $exclusions; } $exclusions_gallery = [ '/jquery-?[0-9.]*(.min|.slim|.slim.min)?.js', '/woocommerce/assets/js/zoom/jquery.zoom(.min)?.js', '/woocommerce/assets/js/photoswipe/', '/woocommerce/assets/js/flexslider/jquery.flexslider(.min)?.js', '/woocommerce/assets/js/frontend/single-product(.min)?.js', ]; if ( isset( $wp_version ) && version_compare( $wp_version, '5.7', '<' ) ) { $exclusions_gallery[] = '/jquery-migrate(.min)?.js'; } /** * Filters the JS files excluded from delay JS when WC product gallery has images. * * @since 3.10.2 * * @param array $exclusions_gallery Array of excluded filepaths. */ $exclusions_gallery = apply_filters( 'rocket_wc_product_gallery_delay_js_exclusions', $exclusions_gallery ); return array_merge( $exclusions, $exclusions_gallery ); } /** * Disable post cache clearing during product sorting. * * @return void */ public function disallow_rocket_clean_post(): void { $this->event_manager->remove_callback( 'clean_post_cache', 'rocket_clean_post' ); } /** * Re-enable post cache clearing after product sorting. * * @param integer $product_id ID of sorted product. * @return void */ public function allow_rocket_clean_post( int $product_id ): void { $urls = []; $category_list = wc_get_product_category_list( $product_id ); if ( preg_match_all( '/<a\s+(?:[^>]*?\s+)?href=(["\'])(?<urls>.*?)\1/i', $category_list, $matches ) ) { $urls = $matches['urls']; } $shop_page = get_permalink( wc_get_page_id( 'shop' ) ); if ( empty( $shop_page ) ) { $shop_page = home_url( 'shop' ); } $urls[] = $shop_page; rocket_clean_files( $urls ); $this->event_manager->add_callback( 'clean_post_cache', 'rocket_clean_post' ); } } ThirdParty/Plugins/Ecommerce/BigCommerce.php 0000644 00000011771 15174677547 0015101 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\Ecommerce; use WP_Post; use WP_Rocket\Event_Management\Event_Manager; use WP_Rocket\Event_Management\Event_Manager_Aware_Subscriber_Interface; use WP_Rocket\Traits\Config_Updater; /** * BigCommerce compatibility subscriber * * @since 3.3.7 */ class BigCommerce implements Event_Manager_Aware_Subscriber_Interface { use Config_Updater; /** * The WordPress Event Manager * * @var Event_Manager */ protected $event_manager; /** * {@inheritdoc} * * @param Event_Manager $event_manager The WordPress Event Manager. */ public function set_event_manager( Event_Manager $event_manager ) { $this->event_manager = $event_manager; } /** * {@inheritdoc} */ public static function get_subscribed_events() { $events = [ 'activate_bigcommerce/bigcommerce.php' => [ 'activate_bigcommerce', 11 ], 'deactivate_bigcommerce/bigcommerce.php' => [ 'deactivate_bigcommerce', 11 ], ]; if ( function_exists( 'bigcommerce_init' ) ) { $events['update_option_bigcommerce_login_page_id'] = [ 'after_update_single_option', 10, 2 ]; $events['update_option_bigcommerce_account_page_id'] = [ 'after_update_single_option', 10, 2 ]; $events['update_option_bigcommerce_address_page_id'] = [ 'after_update_single_option', 10, 2 ]; $events['update_option_bigcommerce_orders_page_id'] = [ 'after_update_single_option', 10, 2 ]; $events['update_option_bigcommerce_cart_page_id'] = [ 'after_update_single_option', 10, 2 ]; $events['update_option_bigcommerce_checkout_page_id'] = [ 'after_update_single_option', 10, 2 ]; $events['shutdown'] = 'maybe_update_config'; $events['transition_post_status'] = [ 'maybe_exclude_page', 10, 3 ]; $events['rocket_cache_reject_uri'] = [ [ 'exclude_pages' ], ]; } return $events; } /** * Add exclusions when activating the BigCommerce plugin * * @since 3.3.7 */ public function activate_bigcommerce() { $this->event_manager->add_callback( 'rocket_cache_reject_uri', [ $this, 'exclude_pages' ] ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } /** * Remove exclusions when deactivating the BigCommerce plugin * * @since 3.3.7 */ public function deactivate_bigcommerce() { $this->event_manager->remove_callback( 'rocket_cache_reject_uri', [ $this, 'exclude_pages' ] ); // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } /** * Maybe regenerate the htaccess & config file if a BigCommerce page is published * * @since 3.3.7 * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. * @return bool */ public function maybe_exclude_page( $new_status, $old_status, $post ) { if ( 'publish' === $old_status || 'publish' !== $new_status ) { return false; } if ( get_option( 'bigcommerce_login_page_id' ) !== $post->ID && get_option( 'bigcommerce_account_page_id' ) !== $post->ID && get_option( 'bigcommerce_address_page_id' ) !== $post->ID && get_option( 'bigcommerce_orders_page_id' ) !== $post->ID && get_option( 'bigcommerce_cart_page_id' ) !== $post->ID && get_option( 'bigcommerce_checkout_page_id' ) !== $post->ID ) { return false; } // Update .htaccess file rules. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); return true; } /** * Exclude BigCommerce login, cart, checkout, account, address and orders pages from caching * * @since 3.3.7 * * @param array $urls An array of excluded pages. * @return array */ public function exclude_pages( $urls ) { $checkout_urls = $this->exclude_page( get_option( 'bigcommerce_checkout_page_id' ) ); $cart_urls = $this->exclude_page( get_option( 'bigcommerce_cart_page_id' ) ); $account_urls = $this->exclude_page( get_option( 'bigcommerce_account_page_id' ) ); $login_urls = $this->exclude_page( get_option( 'bigcommerce_login_page_id' ) ); $address_urls = $this->exclude_page( get_option( 'bigcommerce_address_page_id' ) ); $orders_urls = $this->exclude_page( get_option( 'bigcommerce_orders_page_id' ) ); return array_merge( $urls, $checkout_urls, $cart_urls, $account_urls, $login_urls, $address_urls, $orders_urls ); } /** * Excludes BigCommerce checkout page from cache * * @since 3.3.7 * * @param int $page_id ID of page to exclude. * @param string $post_type Post type of the page. * @param string $pattern Pattern to use for the exclusion. * @return array */ private function exclude_page( $page_id, $post_type = 'page', $pattern = '' ) { $urls = []; if ( ! $page_id ) { return $urls; } if ( $page_id <= 0 || (int) get_option( 'page_on_front' ) === $page_id ) { return $urls; } if ( 'publish' !== get_post_status( $page_id ) ) { return $urls; } $urls = get_rocket_i18n_translated_post_urls( $page_id, $post_type, $pattern ); return $urls; } } ThirdParty/Plugins/InlineRelatedPosts.php 0000644 00000001375 15174677547 0014575 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with Inline Related Posts. */ class InlineRelatedPosts implements Subscriber_Interface { /** * Subscriber for Inline Related Posts. * * @return array */ public static function get_subscribed_events() { if ( ! defined( 'IRP_PLUGIN_SLUG' ) ) { return []; } return [ 'rocket_rucss_inline_content_exclusions' => 'exclude_inline_from_rucss' ]; } /** * Exclude inline style from RUCSS. * * @param array $excluded excluded css. * @return array */ public function exclude_inline_from_rucss( $excluded ) { $excluded[] = '.centered-text-area'; $excluded[] = '.ctaText'; return $excluded; } } ThirdParty/Plugins/PDFEmbedder.php 0000644 00000004442 15174677547 0013064 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with PDF Embedder Free / Premium / Secure plugin. * * @since 3.6.2 */ class PDFEmbedder implements Subscriber_Interface { /** * Subscribed events. * * @since 3.6.2 */ public static function get_subscribed_events() { // All 3 plugins use the same core class. if ( ! class_exists( 'core_pdf_embedder' ) ) { return []; } return [ 'rocket_exclude_js' => 'exclude_pdfembedder_scripts', ]; } /** * Adds PDFEmbedder scripts to defer JS exclusion * * @since 3.6.2 * * @param array $excluded_js Array of scripts to exclude. * @return array */ public function exclude_pdfembedder_scripts( $excluded_js ) { if ( class_exists( 'PDF_Embedder_Basic' ) || class_exists( 'pdfemb_basic_pdf_embedder' ) ) { // Exclude Free version. return array_merge( $excluded_js, $this->pdfembedder_free_scripts() ); } if ( class_exists( 'pdfemb_premium_mobile_pdf_embedder' ) ) { // Excludes PDFEmbedder-premium. return array_merge( $excluded_js, $this->pdfembedder_premium_scripts() ); } if ( class_exists( 'pdfemb_premium_secure_pdf_embedder' ) ) { // Excludes PDFEmbedder-premium-secure. return array_merge( $excluded_js, $this->pdfembedder_secure_scripts() ); } return $excluded_js; } /** * PDFEmbedder Free JS scripts. * * @return array JS files to be excluded. */ private function pdfembedder_free_scripts() { return [ rocket_clean_exclude_file( plugins_url( '/pdf-embedder/js/(.*).js' ) ), ]; } /** * PDFEmbedder Premium JS scripts. * * @return array JS files to be excluded. */ private function pdfembedder_premium_scripts() { return [ rocket_clean_exclude_file( plugins_url( '/PDFEmbedder-premium/js/pdfjs/(.*).js' ) ), rocket_clean_exclude_file( plugins_url( '/PDFEmbedder-premium/js/(.*).js' ) ), ]; } /** * PDFEmbedder Secure JS scripts. * * @return array JS files to be excluded. */ private function pdfembedder_secure_scripts() { return [ rocket_clean_exclude_file( plugins_url( '/PDFEmbedder-premium-secure/js/pdfjs/(.*).js' ) ), rocket_clean_exclude_file( plugins_url( '/PDFEmbedder-premium-secure/js/(.*).js' ) ), ]; } } ThirdParty/Plugins/ModPagespeed.php 0000644 00000005445 15174677547 0013364 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with Mod_pagespeed * * @since 3.8.2 */ class ModPagespeed implements Subscriber_Interface { /** * Subscribed events for Pagespeed module. * * @since 3.8.2 * @inheritDoc */ public static function get_subscribed_events(): array { return [ 'admin_notices' => 'show_admin_notice', ]; } /** * Check if mod_pagespeed is enabled on this server with and cache the result on transient. * * @since 3.8.2 * * @return bool Status of mod_pagespeed. */ private function has_pagespeed_with_cache() { $has_pagespeed = get_transient( 'rocket_mod_pagespeed_enabled' ); if ( false === $has_pagespeed ) { $has_pagespeed = $this->has_pagespeed(); set_transient( 'rocket_mod_pagespeed_enabled', (int) $has_pagespeed, DAY_IN_SECONDS ); } return 1 === (int) $has_pagespeed; } /** * Check if mod_pagespeed is enabled on this server. * * @since 3.8.2 * * @return bool */ private function has_pagespeed(): bool { if ( false === strpos( ini_get( 'disable_functions' ), 'apache_get_modules' ) ) { $apache_module_loaded = apache_mod_loaded( 'mod_pagespeed', false ); if ( $apache_module_loaded ) { return true; } } $home_request = wp_remote_get( home_url(), [ 'timeout' => 3, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound ] ); $headers = wp_remote_retrieve_headers( $home_request ); if ( empty( $headers ) ) { return false; } return ( ! empty( $headers['X-Mod-Pagespeed'] ) || ! empty( $headers['X-Page-Speed'] ) ); } /** * Show admin notice message. * * @since 3.8.2 */ public function show_admin_notice() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== get_current_screen()->id ) { return; } if ( ! $this->has_pagespeed_with_cache() ) { return; } $notice_name = 'rocket_error_mod_pagespeed'; if ( in_array( $notice_name, (array) get_user_meta( get_current_user_id(), 'rocket_boxes', true ), true ) ) { return; } // translators: %1$s is WP Rocket plugin name, %2$s is opening <a> tag, %3$s is closing </a> tag. $error_message = sprintf( __( '<strong>%1$s</strong>: Mod PageSpeed is not compatible with this plugin and may cause unexpected results. %2$sMore Info%3$s', 'rocket' ), rocket_get_constant( 'WP_ROCKET_PLUGIN_NAME' ), '<a target="_blank" href="https://docs.wp-rocket.me/article/1376-mod-pagespeed">', '</a>' ); rocket_notice_html( [ 'status' => 'error', 'message' => $error_message, 'dismissible' => '', 'dismiss_button' => $notice_name, ] ); } } ThirdParty/Plugins/Cookie/Termly.php 0000644 00000002037 15174677547 0013506 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\Cookie; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Compatibility class for Termly. */ class Termly implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { if ( ! defined( 'TERMLY_VERSION' ) ) { return []; } return [ 'rocket_exclude_defer_js' => 'exclude_termly_defer_and_delay_js', 'rocket_delay_js_exclusions' => 'exclude_termly_defer_and_delay_js', ]; } /** * Defer and delay Termly Resources * * @param array $exclude_delay_js Array of JS to be excluded. * * @return array */ public function exclude_termly_defer_and_delay_js( array $exclude_delay_js ): array { $auto_block = get_option( 'termly_display_auto_blocker', 'off' ); if ( 'on' !== $auto_block ) { return $exclude_delay_js; } $exclude_delay_js[] = 'app.termly.io/resource-blocker/(.*)'; return $exclude_delay_js; } } ThirdParty/Plugins/ThirstyAffiliates.php 0000644 00000002311 15174677547 0014452 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; class ThirstyAffiliates implements Subscriber_Interface { /** * Returns an array of events this subscriber listens to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_preload_links_exclusions' => [ 'exclude_link_prefix', PHP_INT_MAX, 2 ], ]; } /** * Excludes the link prefix from preload links * * @since 3.10.8 * * @param string[] $excluded Array of excluded patterns. * @param string[] $default Array of default excluded patterns. * * @return array */ public function exclude_link_prefix( $excluded, $default ): array { if ( ! is_array( $excluded ) ) { // @phpstan-ignore-line $excluded = (array) $excluded; } if ( ! is_plugin_active( 'thirstyaffiliates/thirstyaffiliates.php' ) ) { return $excluded; } $excluded = array_diff( $excluded, $default ); $link_prefix = get_option( 'ta_link_prefix', 'recommends' ); if ( 'custom' === $link_prefix ) { $link_prefix = get_option( 'ta_link_prefix_custom', 'recommends' ); } $excluded[] = '/' . $link_prefix . '/'; return $excluded; } } ThirdParty/Plugins/Ads/Adthrive.php 0000644 00000004712 15174677547 0013300 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\Ads; use WP_Rocket\Event_Management\Subscriber_Interface; class Adthrive implements Subscriber_Interface { /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'wp_rocket_upgrade' => [ 'add_delay_js_exclusion_on_plugin_update', 20, 2 ], 'activate_adthrive-ads/adthrive-ads.php' => 'add_delay_js_exclusion', 'pre_update_option_wp_rocket_settings' => [ 'maybe_add_delay_js_exclusion', 10, 2 ], ]; } /** * Adds adthrive to delay JS exclusion field * * @since 3.9.3 * * @return void */ public function add_delay_js_exclusion() { $options = get_option( 'wp_rocket_settings', [] ); if ( ! isset( $options['delay_js'] ) || empty( $options['delay_js'] ) ) { return; } $exclusions = $options['delay_js_exclusions'] ?? []; if ( in_array( 'adthrive', $exclusions, true ) ) { return; } $exclusions[] = 'adthrive'; $options['delay_js_exclusions'] = $exclusions; update_option( 'wp_rocket_settings', $options ); } /** * Adds adthrive to delay JS exclusion field on update to 3.9.3 * * @since 3.9.3 * * @param string $new_version Plugin new version. * @param string $old_version Plugin old version. * * @return void */ public function add_delay_js_exclusion_on_plugin_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.9.3', '>' ) ) { return; } if ( ! is_plugin_active( 'adthrive-ads/adthrive-ads.php' ) ) { return; } $this->add_delay_js_exclusion(); } /** * Adds Adthrive pattern when saving WPR options and Adthrive is enabled * * @since 3.9.3 * * @param array $value The new, unserialized option value. * @param array $old_value The old option value. * * @return array */ public function maybe_add_delay_js_exclusion( $value, $old_value ): array { if ( ! is_plugin_active( 'adthrive-ads/adthrive-ads.php' ) ) { return $value; } if ( empty( $value['delay_js'] ) ) { return $value; } if ( isset( $old_value['delay_js'] ) && $old_value['delay_js'] === $value['delay_js'] ) { return $value; } if ( isset( $value['delay_js_exclusions'] ) && in_array( 'adthrive', $value['delay_js_exclusions'], true ) ) { return $value; } $value['delay_js_exclusions'][] = 'adthrive'; return $value; } } ThirdParty/Plugins/CDN/CloudflareFacade.php 0000644 00000001553 15174677547 0014573 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\CDN; use CF\WordPress\Hooks; class CloudflareFacade { /** * Hooks class instance * * @var null|Hooks */ private $hooks = null; /** * Instantiate the hooks class * * @return void */ private function set_hooks() { $this->hooks = new Hooks(); } /** * Calls purge everything from CF hooks class * * @return void */ public function purge_everything() { if ( is_null( $this->hooks ) ) { $this->set_hooks(); } $this->hooks->purgeCacheEverything(); } /** * Calls purge relevant URLs from CF hooks class * * @param int|array $post_ids Post ID or array of post IDs. * * @return void */ public function purge_urls( $post_ids ) { if ( is_null( $this->hooks ) ) { $this->set_hooks(); } $this->hooks->purgeCacheByRelevantURLs( $post_ids ); } } ThirdParty/Plugins/CDN/Cloudflare.php 0000644 00000031503 15174677547 0013505 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\CDN; use WP_Rocket\Admin\{Options, Options_Data}; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Engine\Deactivation\DeactivationInterface; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Compatibility class for cloudflare. */ class Cloudflare implements Subscriber_Interface, DeactivationInterface { /** * Options instance. * * @var Options_Data */ private $options; /** * Options API instance. * * @var Options */ private $options_api; /** * Beacon * * @var Beacon Beacon instance. */ private $beacon; /** * CloudflareFacade instance. * * @var CloudflareFacade */ private $facade; /** * Constructor. * * @param Options_Data $options Options instance. * @param Options $options_api Options API instance. * @param Beacon $beacon Beacon instance. * @param CloudflareFacade $facade CloudflareFacade instance. */ public function __construct( Options_Data $options, Options $options_api, Beacon $beacon, CloudflareFacade $facade ) { $this->options = $options; $this->options_api = $options_api; $this->beacon = $beacon; $this->facade = $facade; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'admin_notices' => [ [ 'display_server_pushing_mode_notice' ], [ 'display_apo_cookies_notice' ], [ 'display_apo_cache_notice' ], ], 'rocket_display_input_do_cloudflare' => 'hide_addon_radio', 'rocket_cloudflare_field_settings' => 'update_addon_field', 'pre_get_rocket_option_do_cloudflare' => 'disable_cloudflare_option', 'rocket_after_clean_domain' => 'purge_cloudflare', 'after_rocket_clean_files' => 'purge_cloudflare_partial', 'rocket_saas_complete_job_status' => 'purge_cloudflare_after_usedcss', 'rocket_rucss_after_clearing_usedcss' => 'purge_cloudflare_after_usedcss', 'admin_post_rocket_enable_separate_mobile_cache' => 'enable_separate_mobile_cache', 'rocket_cdn_helper_addons' => 'add_cdn_helper_message', 'init' => 'unregister_cloudflare_clean_on_post', ]; } /** * Display notice for server pushing mode. * * @return void */ public function display_server_pushing_mode_notice() { if ( ! rocket_get_constant( 'CLOUDFLARE_PLUGIN_DIR' ) ) { return; } if ( ! rocket_get_constant( 'CLOUDFLARE_HTTP2_SERVER_PUSH_ACTIVE' ) ) { return; } $screen = get_current_screen(); // If current screen is wprocket settings. if ( isset( $screen->id ) && 'settings_page_wprocket' !== $screen->id ) { return; } // if current user has required capapabilities. if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } // If RUCSS is enabled. if ( ! (bool) $this->options->get( 'remove_unused_css', 0 ) ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); $notice_name = 'cloudflare_server_push'; if ( in_array( $notice_name, (array) $boxes, true ) ) { return; } $message = sprintf( // translators: %1$s = plugin name. __( '%1$s: Cloudflare\'s HTTP/2 Server Push is incompatible with the features of Remove Unused CSS and Combine CSS files. We strongly recommend disabling it.', 'rocket' ), '<strong>WP Rocket</strong>' ); rocket_notice_html( [ 'status' => 'warning', 'dismissible' => '', 'message' => $message, 'id' => 'cloudflare_server_push_notice', 'dismiss_button' => $notice_name, 'dismiss_button_class' => 'button-primary', ] ); } /** * Hide WP Rocket CF Addon activation button if the official CF plugin is enabled * * @param bool $enable True to display, False otherwise. * * @return bool */ public function hide_addon_radio( $enable ) { if ( ! $this->is_plugin_active() ) { return $enable; } return false; } /** * Updates WP Rocket CF Addon field when the official CF plugin is enabled * * @param array $settings Array of values to populate the field. * * @return array */ public function update_addon_field( $settings ) { if ( ! $this->is_plugin_active() ) { return $settings; } $settings['do_cloudflare']['title'] = __( 'Your site is using the official Cloudflare plugin. We have enabled Cloudflare auto-purge for compatibility. If you have APO activated, it is also compatible.', 'rocket' ); $settings['do_cloudflare']['description'] = __( 'Cloudflare cache will be purged each time WP Rocket clears its cache to ensure content is always up-to-date.', 'rocket' ); $settings['do_cloudflare']['helper'] = ''; $settings['do_cloudflare']['settings_page'] = ''; return $settings; } /** * Disable WP Rocket CF option when Cloudflare plugin is enabled * * @param mixed $value Pre option value. * * @return bool */ public function disable_cloudflare_option( $value ) { if ( ! $this->is_plugin_active() ) { return $value; } return false; } /** * Display a notice when APO is enabled and mandatory/dynamic cookies exists * * @return void */ public function display_apo_cookies_notice() { if ( ! $this->can_display_notice() ) { return; } if ( empty( get_rocket_cache_mandatory_cookies() ) && empty( get_rocket_cache_dynamic_cookies() ) ) { return; } $doc = $this->beacon->get_suggest( 'cloudflare_apo' ); $message = sprintf( // Translators: %1$s = strong opening tag, %2$s = strong closing tag. __( '%1$sWP Rocket:%2$s You are using "Dynamic Cookies Cache". Cloudflare APO is not yet compatible with that feature.', 'rocket' ) . '<br>', '<strong>', '</strong>' ); $message .= sprintf( // Translators:%1$s = opening <a> tag, %2$s = closing </a> tag. __( 'You should either disable Cloudflare APO or check with the theme/plugin requiring the use of “Dynamic Cookies Cache” developers for an alternative way to be page-cache friendly. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $doc['url'] ) . '" data-beacon-article="' . esc_attr( $doc['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ); rocket_notice_html( [ 'status' => 'warning', 'dismissible' => '', 'message' => $message, ] ); } /** * Display a notice when there is a mismatch between WP Rocket separate cache by mobile value and APO cache by device type * * @return void */ public function display_apo_cache_notice() { if ( ! $this->can_display_notice() ) { return; } $cf_device_type = get_option( 'automatic_platform_optimization_cache_by_device_type', [] ); if ( ! key_exists( 'value', $cf_device_type ) ) { return; } $mobile_cache = $this->options->get( 'do_caching_mobile_files', 0 ); if ( (int) $mobile_cache === (int) $cf_device_type['value'] ) { return; } $doc = $this->beacon->get_suggest( 'cloudflare_apo' ); $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( 1 === (int) $mobile_cache && 0 === (int) $cf_device_type['value'] ) { rocket_notice_html( [ 'status' => 'warning', 'dismissible' => '', 'message' => sprintf( // Translators: %1$s = strong opening tag, %2$s = strong closing tag, %3$s = opening <a> tag, %4$s = closing </a> tag, %5$s = opening <a> tag. __( '%1$sWP Rocket:%2$s You are using "Separate cache files for mobile devices". You need to activate "Cache by Device Type" %3$ssetting%5$s on Cloudflare APO to serve the right version of the cache. %4$sMore info%5$s', 'rocket' ), '<strong>', '</strong>', '<a href="' . esc_url( admin_url( 'options-general.php?page=cloudflare' ) ) . '">', '<a href="' . esc_url( $doc['url'] ) . '" data-beacon-article="' . esc_attr( $doc['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ), ] ); } elseif ( 0 === (int) $mobile_cache && 1 === (int) $cf_device_type['value'] && ! in_array( __FUNCTION__, (array) $boxes, true ) ) { rocket_notice_html( [ 'status' => 'warning', 'message' => sprintf( // Translators: %1$s = strong opening tag, %2$s = strong closing tag. __( '%1$sWP Rocket:%2$s You have "Cache by Device Type" enabled on Cloudflare APO. If you judge it necessary for the website to have a different cache on mobile and desktop, we suggest you enable our “Separate Cache Files for Mobiles Devices” to ensure the generated cache is accurate.', 'rocket' ), '<strong>', '</strong>' ), 'dismiss_button' => __FUNCTION__, 'dismissible' => '', 'action' => 'enable_separate_mobile_cache', ] ); } } /** * Checks if APO notices should be displayed * * @return bool */ private function can_display_notice(): bool { if ( ! current_user_can( 'rocket_manage_options' ) ) { return false; } $screen = get_current_screen(); if ( isset( $screen->id ) && 'settings_page_wprocket' !== $screen->id ) { return false; } if ( ! $this->is_plugin_active() ) { return false; } return $this->is_apo_enabled(); } /** * Purge everything on Cloudflare * * @return void */ public function purge_cloudflare() { if ( ! $this->is_plugin_active() ) { return; } $this->facade->purge_everything(); } /** * Purges posts when using purge this URL button * * @param array $urls Array of URLs. * * @return void */ public function purge_cloudflare_partial( $urls ) { if ( ! $this->is_plugin_active() ) { return; } $post_ids = array_filter( array_map( 'url_to_postid', $urls ) ); $this->facade->purge_urls( $post_ids ); } /** * Purges CF after Used CSS generation or clean * * @param string $url URL to purge. * * @return void */ public function purge_cloudflare_after_usedcss( $url ) { if ( ! $this->is_plugin_active() ) { return; } $post_id = url_to_postid( $url ); if ( empty( $post_id ) ) { return; } $this->facade->purge_urls( $post_id ); } /** * Enable separate cache for mobile option * * @return void */ public function enable_separate_mobile_cache() { check_admin_referer( 'rocket_enable_separate_mobile_cache' ); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } $this->options->set( 'cache_mobile', 1 ); $this->options->set( 'do_caching_mobile_files', 1 ); $this->options_api->set( 'settings', $this->options->get_options() ); wp_safe_redirect( wp_get_referer() ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } /** * Checks if CF plugin is enabled & credentials saved * * @return bool */ private function is_plugin_active(): bool { if ( ! is_plugin_active( 'cloudflare/cloudflare.php' ) ) { return false; } if ( empty( get_option( 'cloudflare_api_email', '' ) ) || empty( get_option( 'cloudflare_api_key', '' ) ) || empty( get_option( 'cloudflare_cached_domain_name', '' ) ) ) { return false; } return true; } /** * Checks if CF APO is enabled * * @return bool */ private function is_apo_enabled(): bool { $is_apo_enabled = get_option( 'automatic_platform_optimization', [] ); if ( ! key_exists( 'value', $is_apo_enabled ) ) { return false; } return (bool) $is_apo_enabled['value']; } /** * Add the helper message on the CDN settings. * * @param string[] $addons Name from the addon that requires the helper message. * @return string[] */ public function add_cdn_helper_message( array $addons ): array { if ( ! $this->is_plugin_active() ) { return $addons; } $addons[] = 'Cloudflare'; return $addons; } /** * Purge Cloudflare on deactivate. * * @return void */ public function deactivate() { $this->purge_cloudflare(); } /** * Unregister Call on clean posts. * * @return void */ public function unregister_cloudflare_clean_on_post() { $this->unregister_callback( 'deleted_post', 'purgeCacheByRelevantURLs' ); $this->unregister_callback( 'transition_post_status', 'purgeCacheOnPostStatusChange', PHP_INT_MAX ); } /** * Unregister a callback. * * @param string $hook Hook on which to unregister. * @param string $method The callback to unregister. * @param int $priority the priority from the callback. * @return void */ protected function unregister_callback( string $hook, string $method, int $priority = 10 ) { global $wp_filter; if ( ! key_exists( $hook, $wp_filter ) ) { return; } $original_wp_filter = $wp_filter[ $hook ]->callbacks; if ( ! key_exists( $priority, $original_wp_filter ) ) { return; } foreach ( $original_wp_filter[ $priority ] as $key => $config ) { if ( substr( $key, - strlen( $method ) ) !== $method ) { continue; } unset( $wp_filter[ $hook ]->callbacks[ $priority ][ $key ] ); } } } ThirdParty/Plugins/WPGeotargeting.php 0000644 00000006701 15174677547 0013711 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; class WPGeotargeting implements Subscriber_Interface { use ReturnTypesTrait; /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { $events = [ 'geotWP/activated' => [ 'activate_geotargetingwp', 11 ], 'geotWP/deactivated' => [ 'deactivate_geotargetingwp', 11 ], ]; if ( ! class_exists( '\GeotWP\GeotargetingWP' ) ) { return $events; } $events['rocket_htaccess_mod_rewrite'] = [ 'return_false', 72 ]; $events['rocket_cache_dynamic_cookies'] = 'add_geot_cookies'; $events['rocket_cache_mandatory_cookies'] = 'add_geot_cookies'; $events['geot/pass_basic_rules'] = [ 'maybe_disable_rules', 10, 3 ]; if ( ! get_option( 'geotWP-deactivated' ) ) { return $events; } // Update the WP Rocket rules on the .htaccess file. $events['admin_init'] = [ [ 'flush_rocket_htaccess' ], [ 'rocket_generate_config_file' ], ]; delete_option( 'geotWP-deactivated' ); return $events; } /** * Disable rules on nowprocket parameter. * * @param bool $bool Is Disabled. * @param array $opts Options. * @param string $current_url Current URL. * @return bool */ public function maybe_disable_rules( $bool, $opts, $current_url ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.boolFound $query = wp_parse_url( $current_url, PHP_URL_QUERY ); return ! str_contains( $query ? $query : '', 'nowprocket' ); } /** * Add cookies when we activate any goetargetingWP plugin. * * @author Damian Logghe */ public function activate_geotargetingwp() { add_filter( 'rocket_htaccess_mod_rewrite', [ $this, 'return_false' ], 72 ); add_filter( 'rocket_cache_dynamic_cookies', [ $this, 'add_geot_cookies' ] ); add_filter( 'rocket_cache_mandatory_cookies', [ $this, 'add_geot_cookies' ] ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } /** * Remove cookies when we deactivate the plugin. * * @author Damian Logghe */ public function deactivate_geotargetingwp() { // add into db a record saying we deactivated one of the family plugins. update_option( 'geotWP-deactivated', true ); remove_filter( 'rocket_htaccess_mod_rewrite', [ $this, 'return_false' ], 72 ); remove_filter( 'rocket_cache_dynamic_cookies', [ $this, 'add_geot_cookies' ] ); remove_filter( 'rocket_cache_mandatory_cookies', [ $this, 'add_geot_cookies' ] ); // Update the WP Rocket rules on the .htaccess file. flush_rocket_htaccess(); // Regenerate the config file. rocket_generate_config_file(); } /** * Let users modify cache level by default set to country. * * @author Damian Logghe * * @param array $cookies An array of cookies. * @return array Updated array of cookies */ public function add_geot_cookies( $cookies ) { /** * Geotargeting cookies. * * @param array $types Types from cookies (country, state, city). * @return array */ $enabled_cookies = apply_filters( 'rocket_geotargetingwp_enabled_cookies', [ 'country' ] ); foreach ( $enabled_cookies as $enabled_cookie ) { if ( ! in_array( 'geot_rocket_' . $enabled_cookie, $cookies, true ) ) { $cookies[] = 'geot_rocket_' . $enabled_cookie; } } return $cookies; } } ThirdParty/Plugins/Security/WordFenceCompatibility.php 0000644 00000005030 15174677547 0017232 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\Security; use WP_Rocket\Event_Management\Subscriber_Interface; use wordfence; use wfConfig; /** * Compatibility file for WordFence plugin * * @since 3.10 */ class WordFenceCompatibility implements Subscriber_Interface { /** * Whitelisted_IPS. */ const WHITELISTED_IPS = [ '141.94.254.72' ]; /** * Old Whitelisted IP. * * @var string */ private $old_rucss_ip = '135.125.83.227'; /** * Return an array of events that this subscriber wants to listen to. * * @since 3.10 * * @return array */ public static function get_subscribed_events() { if ( ! defined( 'WORDFENCE_VERSION' ) ) { return []; } return [ 'init' => [ 'whitelist_wordfence_firewall_ips', 11 ], 'rocket_deactivation' => 'pop_ip_from_whitelist', ]; } /** * Removes old ip from whitelist. * * @param string $old_rucss_ip Old RUCSS IP. * @return void */ public function pop_old_ip( string $old_rucss_ip ) { $whitelist = $this->can_pop_ip( $old_rucss_ip ); if ( ! $whitelist ) { return; } // Update whitelist. wfConfig::set( 'whitelisted', implode( ',', $whitelist ) ); } /** * Remove ip from whitelist. * * @return void */ public function pop_ip_from_whitelist() { $whitelist = $this->can_pop_ip( self::WHITELISTED_IPS[0] ); if ( ! $whitelist ) { return; } // Update whitelist. wfConfig::set( 'whitelisted', implode( ',', $whitelist ) ); } /** * Check if ip can be removed. * * @param string $ip IP. * * @return array */ private function can_pop_ip( string $ip ) { // Get all whitelists. $whitelists = wfConfig::get( 'whitelisted', '' ); // Convert to array. $whitelist_array = explode( ',', $whitelists ); // Get ip index. $ip = array_search( $ip, $whitelist_array, true ); if ( false === $ip ) { return []; } // Remove ip from whitelist. unset( $whitelist_array[ $ip ] ); return $whitelist_array; } /** * Whitelist wp-rocket ips in wordfence firewall * * @since 3.10 * * @return void */ public function whitelist_wordfence_firewall_ips() { // Pop old rucss ip. $this->pop_old_ip( $this->old_rucss_ip ); /** * Rocket wordfence whitelisted ips filter which adds IPs to wordfence whitelist. * * @since 3.10 * * @param array $ips list of IPs should be whitelisted */ $ips = apply_filters( 'rocket_wordfence_whitelisted_ips', self::WHITELISTED_IPS ); if ( empty( $ips ) ) { return; } foreach ( $ips as $ip ) { wordfence::whitelistIP( $ip ); } } } ThirdParty/Plugins/RevolutionSlider.php 0000644 00000002071 15174677547 0014330 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use Smush\Core\Settings; use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with RevolutionSlider. * * @since 3.9.2 */ class RevolutionSlider implements Subscriber_Interface { /** * Subscribed events for RevolutionSlider. * * @since 3.9.2 * * @inheritDoc */ public static function get_subscribed_events() { if ( ! defined( 'RS_REVISION' ) || version_compare( RS_REVISION, '6.5.5', '<' ) ) { // @phpstan-ignore-line return []; } return [ 'rocket_exclude_defer_js' => 'exclude_defer_js', ]; } /** * Excludes jquery and jquery migrate JS files from defer JS * * @since 3.9.2 * * @param array $exclude_defer_js Array of JS file paths to be excluded. * @return array */ public function exclude_defer_js( $exclude_defer_js ) { $exclude_defer_js[] = '/jquery-?[0-9.]*(.min|.slim|.slim.min)?.js'; $exclude_defer_js[] = '/jquery-migrate(.min)?.js'; return $exclude_defer_js; } } ThirdParty/Plugins/ConvertPlug.php 0000644 00000001554 15174677547 0013274 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with ConvertPlug. */ class ConvertPlug implements Subscriber_Interface { /** * Subscriber for compatibility with ConvertPlug. * * @return array */ public static function get_subscribed_events() { $events = []; if ( ! self::isActivated() ) { return $events; } $events['rocket_rucss_inline_atts_exclusions'] = 'excluded_from_rucss'; return $events; } /** * Check if the plugin is activated. * * @return bool */ protected static function isActivated() { return defined( 'CP_VERSION' ); } /** * Exclude css from RUCSS. * * @param array $excluded excluded css. * @return array */ public function excluded_from_rucss( $excluded ) { $excluded[] = 'cp-form-css'; return $excluded; } } ThirdParty/Plugins/Smush.php 0000644 00000011635 15174677547 0012124 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use Smush\Core\Settings; use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with Smush * * @since 3.4.2 * @author Soponar Cristina */ class Smush implements Subscriber_Interface { /** * WP Options API instance * * @var Options */ private $options_api; /** * Options instance * * @var Options_Data */ private $options; /** * Subscribed events for Smush. * * @since 3.4.2 * @author Soponar Cristina * @inheritDoc */ public static function get_subscribed_events() { if ( ! rocket_has_constant( 'WP_SMUSH_VERSION' ) ) { return [ 'activate_wp-smushit/wp-smush.php' => [ 'maybe_deactivate_rocket_lazyload', 10 ], ]; } return [ 'update_option_wp-smush-settings' => [ 'maybe_deactivate_rocket_lazyload', 11 ], 'update_site_option_wp-smush-settings' => [ 'maybe_deactivate_rocket_lazyload', 11 ], 'update_option_wp-smush-lazy_load' => [ 'maybe_deactivate_rocket_lazyload', 11 ], 'update_site_option_wp-smush-lazy_load' => [ 'maybe_deactivate_rocket_lazyload', 11 ], 'rocket_maybe_disable_lazyload_helper' => 'is_smush_lazyload_active', 'rocket_maybe_disable_iframes_lazyload_helper' => 'is_smush_iframes_lazyload_active', ]; } /** * Constructor. * * @since 3.5.5 * * @param Options $options_api WP Options API instance. * @param Options_Data $options WP Rocket Options instance. */ public function __construct( Options $options_api, Options_Data $options ) { $this->options_api = $options_api; $this->options = $options; } /** * Disable WP Rocket lazyload when activating WP Smush and values are already in the database. * * @since 3.4.2 * @author Soponar Cristina */ public function maybe_deactivate_rocket_lazyload() { $enabled = $this->is_smush_lazyload_enabled(); $updated = false; if ( $enabled['images'] && $this->options->get( 'lazyload' ) ) { $this->options->set( 'lazyload', 0 ); $updated = true; } if ( $enabled['iframes'] && $this->options->get( 'lazyload_iframes' ) ) { $this->options->set( 'lazyload_iframes', 0 ); $updated = true; } if ( ! $updated ) { return; } $this->options_api->set( 'settings', $this->options->get_options() ); } /** * Add "Smush" to the provided array if WP Smush lazyload is enabled for images. * * @since 3.4.2 * @author Soponar Cristina * * @param array $disable_images_lazyload Array with plugins which disable lazyload functionality. * @return array A list of plugin names. */ public function is_smush_lazyload_active( array $disable_images_lazyload ) { $enabled = $this->is_smush_lazyload_enabled(); if ( $enabled['images'] ) { $disable_images_lazyload[] = __( 'Smush', 'rocket' ); } return $disable_images_lazyload; } /** * Add "Smush" to the provided array if WP Smush lazyload is enabled for iframes. * * @since 3.5.5 * * @param array $disable_iframes_lazyload Array with plugins which disable lazyload functionality. * @return array A list of plugin names. */ public function is_smush_iframes_lazyload_active( $disable_iframes_lazyload ) { $enabled = $this->is_smush_lazyload_enabled(); if ( $enabled['iframes'] ) { $disable_iframes_lazyload[] = __( 'Smush', 'rocket' ); } return $disable_iframes_lazyload; } /** * Tell if Smush’s lazyload is enabled for each type of content. * * @since 3.5.5 * * @return array { * bool $images True when lazyload is enabled for images. False otherwise. * bool $iframes True when lazyload is enabled for iframes. False otherwise. * } */ private function is_smush_lazyload_enabled() { $enabled = [ 'images' => false, 'iframes' => false, ]; if ( ! class_exists( '\Smush\Core\Settings' ) ) { return $enabled; } if ( ! method_exists( '\Smush\Core\Settings', 'get_instance' ) ) { // @phpstan-ignore-line return $enabled; } $smush_settings = Settings::get_instance(); if ( ! method_exists( $smush_settings, 'get' ) || ! method_exists( $smush_settings, 'get_setting' ) ) { return $enabled; } if ( ! $smush_settings->get( 'lazy_load' ) ) { return $enabled; } $formats = $smush_settings->get_setting( 'wp-smush-lazy_load' ); $formats = ! empty( $formats['format'] ) && is_array( $formats['format'] ) ? array_filter( $formats['format'] ) : []; $image_formats = array_intersect_key( $formats, // Allowlist image formats. [ 'jpeg' => false, 'png' => false, 'gif' => false, 'svg' => false, ] ); if ( ! empty( $image_formats ) ) { // One or several image formats are enabled in Smush. $enabled['images'] = true; } if ( ! empty( $formats['iframe'] ) ) { // Iframe is enabled in Smush. $enabled['iframes'] = true; } return $enabled; } } ThirdParty/Plugins/Jetpack.php 0000644 00000002237 15174677547 0012404 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; class Jetpack implements Subscriber_Interface { use ReturnTypesTrait; /** * Option instance. * * @var Options_Data */ protected $option; /** * Instantiate class. * * @param Options_Data $option Option instance. */ public function __construct( Options_Data $option ) { $this->option = $option; } /** * Subscribed events. */ public static function get_subscribed_events() { $events = []; if ( ! class_exists( 'Jetpack' ) ) { return $events; } if ( \Jetpack::is_module_active( 'sitemaps' ) ) { $events['rocket_sitemap_preload_list'] = 'add_jetpack_sitemap'; } return $events; } /** * Add Jetpack sitemap to preload list * * @param Array $sitemaps Array of sitemaps to preload. * @return Array Updated Array of sitemaps to preload */ public function add_jetpack_sitemap( $sitemaps ) { if ( ! function_exists( 'jetpack_sitemap_uri' ) ) { return $sitemaps; } $sitemaps['jetpack'] = jetpack_sitemap_uri(); return $sitemaps; } } ThirdParty/Plugins/TheEventsCalendar.php 0000644 00000001551 15174677547 0014360 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with the Events Calendar. */ class TheEventsCalendar implements Subscriber_Interface { /** * Subscribed events for The Events Calendar. */ public static function get_subscribed_events() { if ( ! rocket_get_constant( 'TRIBE_EVENTS_FILE', false ) ) { return []; } return [ 'rocket_preload_exclude_urls' => 'exclude_from_preload_calendars', ]; } /** * Exclude calendars from the preload. * * @param array $excluded excluded urls. * @return array */ public function exclude_from_preload_calendars( $excluded ) { if ( ! function_exists( 'tribe_get_option' ) ) { return $excluded; } $uri = tribe_get_option( 'eventsSlug', 'event' ); $excluded[] = "/$uri/20(.*)"; return $excluded; } } ThirdParty/Plugins/I18n/TranslatePress.php 0000644 00000023210 15174677547 0014506 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\I18n; use TRP_Translate_Press; use WP_Rocket\Event_Management\Subscriber_Interface; class TranslatePress implements Subscriber_Interface { /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { if ( ! class_exists( 'TRP_Translate_Press' ) ) { return []; } return [ 'rocket_saas_is_home_url' => [ 'detect_homepage', 10, 2 ], 'rocket_has_i18n' => 'is_translatepress', 'rocket_i18n_admin_bar_menu' => 'add_langs_to_admin_bar', 'rocket_i18n_current_language' => 'set_current_language', 'rocket_get_i18n_uri' => 'get_active_languages_uri', 'rocket_get_i18n_code' => 'get_active_languages_codes', 'rocket_i18n_subdomains' => 'get_active_languages_uri', 'rocket_i18n_home_url' => [ 'get_home_url_for_lang', 10, 2 ], 'rocket_i18n_translated_post_urls' => [ 'get_translated_post_urls', 10, 4 ], 'post_updated' => 'clear_post_languages', 'trp_save_editor_translations_regular_strings' => [ 'clear_post_after_updating_translation', 10, 2 ], 'rocket_current_url' => 'adjust_current_url', ]; } /** * Detect homepage. * * @param string $home_url home url. * @param string $url url of current page. * @return string */ public function detect_homepage( $home_url, $url ) { $translatepress = TRP_Translate_Press::get_trp_instance(); $converter = $translatepress->get_component( 'url_converter' ); $language = $converter->get_lang_from_url_string( $url ); $url_language = $converter->get_url_for_language( $language, home_url() ); return untrailingslashit( $url ) === untrailingslashit( $url_language ) ? $url : $home_url; } /** * Adds TranslatePress as identifier for i18n detection * * @param string|bool $identifier An identifier value, false otherwise. * * @return string|bool */ public function is_translatepress( $identifier ) { if ( function_exists( 'trp_get_languages' ) && ! empty( trp_get_languages( 'nodefault' ) ) ) { return 'translatepress'; } return $identifier; } /** * Adds languages to the admin bar menu * * @param array $langlinks Array of languages. * * @return array */ public function add_langs_to_admin_bar( $langlinks ) { $translatepress = TRP_Translate_Press::get_trp_instance(); $language_switcher = $translatepress->get_component( 'language_switcher' ); $settings = $translatepress->get_component( 'settings' ); $languages = $translatepress->get_component( 'languages' ); $trp_settings = $settings->get_settings(); $languages_to_display = $trp_settings['publish-languages']; $published_languages = $languages->get_language_names( $languages_to_display ); foreach ( $published_languages as $code => $name ) { $langlinks[ $code ] = [ 'code' => $trp_settings['url-slugs'][ $code ], 'flag' => $language_switcher->add_flag( $code, $name ), 'anchor' => $name, ]; } return $langlinks; } /** * Sets the current language value * * @param string|bool $current_language Current language. * * @return string|bool */ public function set_current_language( $current_language ) { if ( empty( $GLOBALS['TRP_LANGUAGE'] ) ) { return $current_language; } return $GLOBALS['TRP_LANGUAGE']; } /** * Gets URLs for active languages * * @param array $urls Array of active languages URI. * * @return array */ public function get_active_languages_uri( array $urls ): array { $home_url = home_url(); $translatepress = TRP_Translate_Press::get_trp_instance(); $settings = $translatepress->get_component( 'settings' ); $languages = $translatepress->get_component( 'languages' ); $converter = $translatepress->get_component( 'url_converter' ); $trp_settings = $settings->get_settings(); $languages_to_display = $trp_settings['publish-languages']; $published_languages = $languages->get_language_names( $languages_to_display ); foreach ( $published_languages as $code => $name ) { $urls[] = $converter->get_url_for_language( $code, $home_url ); } return $urls; } /** * Gets the active languages slugs * * @param array $codes Array of languages codes. * * @return array */ public function get_active_languages_codes( $codes ) { if ( ! is_array( $codes ) ) { // @phpstan-ignore-line $codes = (array) $codes; } $translatepress = TRP_Translate_Press::get_trp_instance(); $settings = $translatepress->get_component( 'settings' ); $languages = $translatepress->get_component( 'languages' ); $trp_settings = $settings->get_settings(); $languages_to_display = $trp_settings['publish-languages']; $published_languages = $languages->get_language_names( $languages_to_display ); foreach ( $published_languages as $code => $name ) { $codes[] = $trp_settings['url-slugs'][ $code ]; } return $codes; } /** * Gets home URL in given language * * @param string $home_url Home URL. * @param string $lang Language code. * * @return string */ public function get_home_url_for_lang( $home_url, $lang ) { if ( empty( $lang ) ) { return $home_url; } $translatepress = TRP_Translate_Press::get_trp_instance(); $converter = $translatepress->get_component( 'url_converter' ); $settings = $translatepress->get_component( 'settings' ); $trp_settings = $settings->get_settings(); $code = ''; add_filter( 'trp_add_language_to_home_url_check_for_admin', '__return_false' ); foreach ( $trp_settings['url-slugs'] as $index => $slug ) { if ( $lang === $slug ) { $code = $index; break; } } $url = $converter->get_url_for_language( $code, $home_url ); remove_filter( 'trp_add_language_to_home_url_check_for_admin', '__return_false' ); return $url; } /** * Gets all translations URLs for a post * * @param array $urls Array of translated URLs. * @param string $url URL to use. * @param string $post_type Post type. * @param string $regex Pattern to include at the end. * * @return array */ public function get_translated_post_urls( $urls, $url, $post_type, $regex ) { if ( ! is_array( $urls ) ) { // @phpstan-ignore-line $urls = (array) $urls; } $translatepress = TRP_Translate_Press::get_trp_instance(); $settings = $translatepress->get_component( 'settings' ); $languages = $translatepress->get_component( 'languages' ); $converter = $translatepress->get_component( 'url_converter' ); $trp_settings = $settings->get_settings(); $languages_to_display = $trp_settings['publish-languages']; $published_languages = $languages->get_language_names( $languages_to_display ); foreach ( $published_languages as $code => $name ) { $urls[] = wp_parse_url( $converter->get_url_for_language( $code, $url ), PHP_URL_PATH ) . $regex; } return $urls; } /** * Clear all languages of a specific post * * @param int $post_id Post ID. * * @return void */ public function clear_post_languages( $post_id ) { $translatepress = TRP_Translate_Press::get_trp_instance(); $converter = $translatepress->get_component( 'url_converter' ); $settings = $translatepress->get_component( 'settings' ); $trp_settings = $settings->get_settings(); add_filter( 'trp_add_language_to_home_url_check_for_admin', '__return_false' ); $clear_urls = []; $default_permalink = get_permalink( $post_id ); foreach ( $trp_settings['translation-languages'] as $language ) { if ( $language === $trp_settings['default-language'] ) { continue; } $clear_urls[] = $converter->get_url_for_language( $language, $default_permalink, '' ); } remove_filter( 'trp_add_language_to_home_url_check_for_admin', '__return_false' ); if ( empty( $clear_urls ) ) { return; } rocket_clean_files( $clear_urls ); } /** * Clear the post cache when the translation is updated * * @param array $update_strings Array of updated strings. * @param array $settings Array of settings. * * @return void */ public function clear_post_after_updating_translation( $update_strings, $settings ) { $translatepress = TRP_Translate_Press::get_trp_instance(); $converter = $translatepress->get_component( 'url_converter' ); if ( empty( $_POST['url'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return; } $url = esc_url_raw( wp_unslash( $_POST['url'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $clear_urls = []; $current_url = remove_query_arg( 'trp-edit-translation', $url ); foreach ( $settings['translation-languages'] as $language ) { if ( ! empty( $update_strings[ $language ] ) ) { $clear_urls[] = $converter->get_url_for_language( $language, $current_url, '' ); } } rocket_clean_files( $clear_urls ); } /** * Adjusts the current URL to match the language-specific URL format used by TranslatePress. * * Removes the '#TRPLINKPROCESSED' marker and returns the correct URL for the detected language. * * @param string $current_url The current URL to adjust. * @return string The adjusted URL for the current language. */ public function adjust_current_url( $current_url ) { $translatepress = \TRP_Translate_Press::get_trp_instance(); $converter = $translatepress->get_component( 'url_converter' ); $language = $converter->get_lang_from_url_string( $current_url ); return str_replace( '#TRPLINKPROCESSED', '', $converter->get_url_for_language( $language, $current_url ) ); } } ThirdParty/Plugins/I18n/Weglot.php 0000644 00000002034 15174677547 0012776 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\I18n; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with Weglot. */ class Weglot implements Subscriber_Interface { /** * Returns an array of events that this subscriber wants to listen to. * * @return array|string[] */ public static function get_subscribed_events() { if ( ! class_exists( 'Context_Weglot' ) ) { return []; } return [ 'rocket_admin_bar_referer' => 'add_langs_to_referer', ]; } /** * Modify the referer URL by appending the current language from Weglot as a prefix to the URL path. * * @param string $referer The original referer URL. * @return string The modified referer URL with the language as a prefix. */ public function add_langs_to_referer( $referer ) { if ( ! function_exists( 'weglot_get_request_url_service' ) || ! function_exists( 'weglot_get_current_full_url' ) ) { return $referer; } return weglot_get_request_url_service()->url_to_relative( weglot_get_current_full_url() ); } } ThirdParty/Plugins/I18n/WPML.php 0000644 00000012623 15174677547 0012321 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\I18n; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Filesystem_Direct; use WP_Rocket\ThirdParty\ReturnTypesTrait; /** * Subscriber for compatibility with WPML. */ class WPML implements Subscriber_Interface { use ReturnTypesTrait; /** * Filesystem instance. * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Instantiate class. * * @param WP_Filesystem_Direct|null $filesystem Filesystem instance. */ public function __construct( ?WP_Filesystem_Direct $filesystem = null ) { $this->filesystem = ! empty( $filesystem ) ? $filesystem : rocket_direct_filesystem(); } /** * Events for subscriber to listen to. * * @return array */ public static function get_subscribed_events() { $events = [ 'activate_sitepress-multilingual-cms/sitepress.php' => 'maybe_clear_on_disable', 'deactivate_sitepress-multilingual-cms/sitepress.php' => 'maybe_clear_on_disable', ]; if ( ! defined( 'ICL_SITEPRESS_VERSION' ) ) { return $events; } $events['rocket_saas_is_home_url'] = [ 'is_secondary_home', 10, 2 ]; $events['rocket_preload_all_to_pending_condition'] = 'clean_only_right_domain'; $events['rocket_preload_sitemap_before_queue'] = 'add_languages_sitemaps'; $events['after_rocket_clean_home'] = 'remove_root_cached_files'; $events['rocket_after_clean_domain'] = 'remove_root_cached_files'; $events['pre_update_option_icl_sitepress_settings'] = [ 'on_change_directory_for_default_language_clean_cache', 10, 2 ]; return $events; } /** * Checks if page to be processed is secondary homepage. * * @param string $home_url home url. * @param string $url url of current page. * @return string */ public function is_secondary_home( string $home_url, string $url ): string { global $sitepress; // Get active languages on site. $languages = $sitepress->get_active_languages(); foreach ( $languages as $lang ) { $lang_url = $sitepress->language_url( $lang['code'] ); // Check if current url is a secondary homepage. if ( untrailingslashit( $lang_url ) !== $url ) { continue; } return $url; } return $home_url; } /** * Add a condition to clean only urls from the domain when it is the case. * * @param string $condition condition used to clean URLS in the database. * @return string */ public function clean_only_right_domain( $condition ): string { global $sitepress; $lang = isset( $_GET['lang'] ) && 'all' !== $_GET['lang'] ? sanitize_key( $_GET['lang'] ) : '';// phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! $lang ) { return $condition; } $lang_url = $sitepress->language_url( $lang ); return ' WHERE url LIKE "' . $lang_url . '%"'; } /** * Add sitemaps from translations. * * @param array $sitemaps list of sitemaps to be fetched. * @return array */ public function add_languages_sitemaps( $sitemaps ): array { global $sitepress; $new_sitemaps = []; // Get active languages on site. $languages = $sitepress->get_active_languages(); $base_url = home_url(); foreach ( $sitemaps as $sitemap ) { $new_sitemaps[] = $sitemap; foreach ( $languages as $lang ) { $lang_url = $sitepress->language_url( $lang['code'] ); $new_sitemaps[] = str_replace( $base_url, $lang_url, $sitemap ); } } return array_unique( $new_sitemaps ); } /** * Remove root files when WPML is active. * * @return void */ public function remove_root_cached_files(): void { $site_url = home_url(); $host_name = wp_parse_url( $site_url, PHP_URL_HOST ); $cache_folder_path = _rocket_get_wp_rocket_cache_path() . $host_name . '/'; $cache_folder_directory = $this->filesystem->dirlist( $cache_folder_path ); if ( ! is_array( $cache_folder_directory ) ) { return; } foreach ( array_keys( $cache_folder_directory ) as $entry ) { if ( $this->filesystem->is_dir( $cache_folder_path . $entry ) ) { continue; } $this->filesystem->delete( $cache_folder_path . $entry ); } } /** * Reset cache when changing the option. * * @param array $new new configurations. * @param array $old old configurations. * * @return array */ public function on_change_directory_for_default_language_clean_cache( array $new, array $old ): array { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.newFound if ( ! key_exists( 'urls', $old ) || ! key_exists( 'directory_for_default_language', $old['urls'] ) || ! key_exists( 'urls', $new ) || ! key_exists( 'directory_for_default_language', $new['urls'] ) || $new['urls']['directory_for_default_language'] === $old['urls']['directory_for_default_language'] ) { return $new; } /** * Reset WP Rocket Preload. */ do_action( 'rocket_reset_preload' ); rocket_clean_domain(); return $new; } /** * Clear the cache when the option language directory is enabled. * * @return void */ public function maybe_clear_on_disable(): void { $option = get_option( 'icl_sitepress_settings' ); if ( ! $option || ! is_array( $option ) || ! key_exists( 'urls', $option ) || ! key_exists( 'directory_for_default_language', $option['urls'] ) || false === $option['urls']['directory_for_default_language'] ) { return; } /** * Reset WP Rocket Preload. */ do_action( 'rocket_reset_preload' ); rocket_clean_cache_dir(); rocket_clean_domain(); } } ThirdParty/Plugins/UnlimitedElements.php 0000644 00000001324 15174677547 0014446 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with Unlimited Elements. */ class UnlimitedElements implements Subscriber_Interface { /** * Subscriber for Unlimited Elements. * * @return array */ public static function get_subscribed_events() { if ( ! defined( 'UNLIMITED_ELEMENTS_INC' ) ) { return []; } return [ 'rocket_rucss_inline_content_exclusions' => 'exclude_inline_from_rucss' ]; } /** * Exclude inline style from RUCSS. * * @param array $excluded excluded css. * @return array */ public function exclude_inline_from_rucss( $excluded ) { $excluded[] = '#uc_'; return $excluded; } } ThirdParty/Plugins/PageBuilder/Elementor.php 0000644 00000013026 15174677547 0015136 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Plugins\PageBuilder; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Engine\Optimization\DelayJS\HTML; /** * Compatibility file for Elementor plugin */ class Elementor implements Subscriber_Interface { /** * WP Rocket options. * * @var Options_Data */ private $options; /** * WP_Filesystem_Direct instance. * * @var \WP_Filesystem_Direct */ private $filesystem; /** * Delay JS HTML class. * * @var HTML */ private $delayjs_html; /** * Constructor * * @param Options_Data $options WP Rocket options. * @param \WP_Filesystem_Direct $filesystem The Filesystem object. * @param HTML $delayjs_html DelayJS HTML class. */ public function __construct( Options_Data $options, $filesystem, HTML $delayjs_html ) { $this->options = $options; $this->filesystem = $filesystem; $this->delayjs_html = $delayjs_html; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { if ( ! defined( 'ELEMENTOR_VERSION' ) ) { return []; } return [ 'wp_rocket_loaded' => 'remove_widget_callback', 'rocket_exclude_css' => 'exclude_post_css', 'elementor/core/files/clear_cache' => 'clear_cache', 'elementor/maintenance_mode/mode_changed' => 'clear_cache', 'update_option__elementor_global_css' => 'clear_cache', 'delete_option__elementor_global_css' => 'clear_cache', 'rocket_buffer' => [ 'add_fix_animation_script', 28 ], 'rocket_exclude_js' => 'exclude_js', 'rocket_skip_post_row_actions' => 'remove_rocket_option', 'rocket_metabox_options_post_types' => 'remove_rocket_option', 'rocket_skip_admin_bar_cache_purge_option' => [ 'skip_admin_bar_option', 1, 2 ], 'rocket_submitbox_options_post_types' => 'remove_rocket_option', 'rocket_skip_admin_bar_clear_used_css_option' => [ 'skip_admin_bar_option', 1, 2 ], ]; } /** * Remove the callback to clear the cache on widget update * * @return void */ public function remove_widget_callback() { remove_filter( 'widget_update_callback', 'rocket_widget_update_callback' ); } /** * Clear WP Rocket caches when Elementor changes the CSS * * @return void */ public function clear_cache() { if ( ! $this->elementor_use_external_file() ) { return; } rocket_clean_domain(); rocket_clean_minify( 'css' ); } /** * Checks whether elementor is set use external CSS file or not. * * @return bool */ private function elementor_use_external_file() { return 'internal' !== get_option( 'elementor_css_print_method' ); } /** * Add Fix Elementor Pro animations script. * * @since 3.9.2 * * @param string $html HTML content. * * @return string HTML with Fix Elementor Pro animations script. */ public function add_fix_animation_script( $html ) { if ( ! $this->delayjs_html->is_allowed() ) { return $html; } $pattern = '/<\/body*>/i'; /** * Select the version of the JS script used for delay js. * * @param string $version Version of the script. */ $version = wpm_apply_filters_typesafe( 'rocket_delay_js_version_js_script', '' ); $path_script = rocket_get_constant( 'WP_ROCKET_PATH' ) . "assets/js/elementor-animation$version.js"; if ( ! $this->filesystem->exists( $path_script ) ) { $path_script = rocket_get_constant( 'WP_ROCKET_PATH' ) . 'assets/js/elementor-animation.js'; } $fix_elementor_animation_script = $this->filesystem->get_contents( $path_script ); if ( false !== $fix_elementor_animation_script ) { $html = preg_replace( $pattern, "<script>{$fix_elementor_animation_script}</script>$0", $html, 1 ); } return $html; } /** * Excludes Elementor CSS from minify/combine * * @since 3.10.9 * * @param array $excluded Array of excluded patterns. * * @return array */ public function exclude_post_css( $excluded ): array { if ( ! $this->elementor_use_external_file() ) { return $excluded; } $upload = wp_get_upload_dir(); $basepath = wp_parse_url( $upload['baseurl'], PHP_URL_PATH ); if ( empty( $basepath ) ) { return $excluded; } $excluded[] = $basepath . '/elementor/css/(.*).css'; return $excluded; } /** * Excludes JS files from minify/combine JS * * @since 3.10.9 * * @param array $excluded_files Array of excluded patterns. * * @return array */ public function exclude_js( $excluded_files ): array { if ( ! $this->options->get( 'minify_concatenate_js', false ) ) { return $excluded_files; } $excluded_files[] = '/wp-includes/js/dist/hooks(.min)?.js'; return $excluded_files; } /** * Remove rocket metabox option from post. * * @param array $cpts Custom post type. * @return array */ public function remove_rocket_option( array $cpts ): array { if ( isset( $cpts['elementor_library'] ) ) { unset( $cpts['elementor_library'] ); } return $cpts; } /** * Remove cache or purge option from elementor template post. * * @param boolean $should_skip Should skip rocket option to admin bar. * @param mixed $post Post object. * @return boolean */ public function skip_admin_bar_option( bool $should_skip, $post ): bool { if ( null === $post ) { return $should_skip; } if ( 'elementor_library' === $post->post_type ) { return true; } return $should_skip; } } ThirdParty/Plugins/PageBuilder/BeaverBuilder.php 0000644 00000001425 15174677547 0015717 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Plugins\PageBuilder; use WP_Rocket\Event_Management\Subscriber_Interface; class BeaverBuilder implements Subscriber_Interface { /** * Events this subscriber listens to * * @inheritDoc */ public static function get_subscribed_events() { if ( ! rocket_get_constant( 'FL_BUILDER_VERSION' ) ) { return []; } return [ 'fl_builder_before_save_layout' => 'purge_cache', 'fl_builder_cache_cleared' => 'purge_cache', ]; } /** * Purge the cache when the beaver builder layout is updated to update the minified files content & URL * * Previously rocket_beaver_builder_clean_domain() * * @since 3.6 * @author Remy Perona */ public function purge_cache() { rocket_clean_minify(); rocket_clean_domain(); } } ThirdParty/SubscriberFactoryInterface.php 0000644 00000000527 15174677547 0014656 0 ustar 00 <?php namespace WP_Rocket\ThirdParty; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Interface for Subscriber Factories * * @since 3.6.3 */ interface SubscriberFactoryInterface { /** * Get a Subscriber Interface object. * * @since 3.6.3 * * @return Subscriber_Interface */ public function get_subscriber(); } ThirdParty/Hostings/HostSubscriberFactory.php 0000644 00000002575 15174677547 0015476 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\NullSubscriber; use WP_Rocket\ThirdParty\SubscriberFactoryInterface; /** * Host Subscriber Factory * * @since 3.6.3 */ class HostSubscriberFactory implements SubscriberFactoryInterface { /** * Get a Subscriber Interface object. * * @since 3.6.3 * * @return Subscriber_Interface A Subscribe Interface for the current host. */ public function get_subscriber() { $host_service = HostResolver::get_host_service( rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ); switch ( $host_service ) { case 'pressable': return new Pressable(); case 'cloudways': return new Cloudways(); case 'spinupwp': return new SpinUpWP(); case 'wpengine': return new WPEngine(); case 'o2switch': return new O2Switch(); case 'wordpresscom': return new WordPressCom(); case 'savvii': return new Savvii(); case 'dreampress': return new Dreampress(); case 'wpxcloud': return new WPXCloud(); case 'litespeed': return new LiteSpeed(); case 'godaddy': return new Godaddy(); case 'kinsta': return new Kinsta(); case 'onecom': return new OneCom(); case 'proisp': return new ProIsp(); case 'pressidium': return new Pressidium(); default: return new NullSubscriber(); } } } ThirdParty/Hostings/HostResolver.php 0000644 00000005712 15174677547 0013640 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; /** * Host Resolver. * * @since 3.6.3 */ class HostResolver { /** * Name of the current host service. * * @var string */ private static $hostname = ''; /** * Get the name of an identifiable hosting service. * * @since 3.6.3 * * @param bool $ignore_cached_hostname (optional) Don't use cached hostname when true. * * @return string Name of the hosting service or '' if no service is recognized. */ public static function get_host_service( $ignore_cached_hostname = false ) { if ( ! $ignore_cached_hostname && ! empty( self::$hostname ) ) { return self::$hostname; } if ( isset( $_SERVER['GROUPONE_BRAND_NAME'] ) ) { $group_one_brand_name = strtolower( sanitize_text_field( wp_unslash( $_SERVER['GROUPONE_BRAND_NAME'] ) ) ); switch ( $group_one_brand_name ) { case 'one.com': self::$hostname = 'onecom'; return 'onecom'; case 'proisp.no': self::$hostname = 'proisp'; return 'proisp'; } } if ( isset( $_SERVER['cw_allowed_ip'] ) ) { self::$hostname = 'cloudways'; return 'cloudways'; } if ( rocket_get_constant( 'IS_PRESSABLE' ) ) { self::$hostname = 'pressable'; return 'pressable'; } if ( getenv( 'SPINUPWP_CACHE_PATH' ) ) { self::$hostname = 'spinupwp'; return 'spinupwp'; } if ( ( class_exists( 'WpeCommon' ) && function_exists( 'wpe_param' ) ) ) { self::$hostname = 'wpengine'; return 'wpengine'; } if ( rocket_has_constant( 'O2SWITCH_VARNISH_PURGE_KEY' ) ) { self::$hostname = 'o2switch'; return 'o2switch'; } if ( rocket_get_constant( 'WPCOMSH_VERSION' ) ) { self::$hostname = 'wordpresscom'; return 'wordpresscom'; } if ( rocket_get_constant( '\Savvii\CacheFlusherPlugin::NAME_FLUSH_NOW' ) && rocket_get_constant( '\Savvii\CacheFlusherPlugin::NAME_DOMAINFLUSH_NOW' ) ) { return 'savvii'; } if ( self::is_dreampress() ) { return 'dreampress'; } if ( isset( $_SERVER['HTTP_WPXCLOUD'] ) ) { self::$hostname = 'wpxcloud'; return 'wpxcloud'; } if ( isset( $_SERVER['X-LSCACHE'] ) ) { self::$hostname = 'litespeed'; return 'litespeed'; } if ( class_exists( '\WPaas\Plugin' ) ) { self::$hostname = 'godaddy'; return 'godaddy'; } if ( isset( $_SERVER['KINSTA_CACHE_ZONE'] ) ) { self::$hostname = 'kinsta'; return 'kinsta'; } if ( defined( 'WP_NINUKIS_WP_NAME' ) || class_exists( 'NinukisCaching' ) ) { self::$hostname = 'pressidium'; return self::$hostname; } return ''; } /** * Checks if the current host is DreamPress * * @since 3.7.2 * * @return boolean */ private static function is_dreampress() { if ( ! isset( $_SERVER['DH_USER'] ) ) { return false; } if ( ! rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) && 'dp-' !== substr( gethostname(), 0, 3 ) ) { return false; } return 'wp_' === substr( sanitize_key( wp_unslash( $_SERVER['DH_USER'] ) ), 0, 3 ); } } ThirdParty/Hostings/OneCom.php 0000644 00000010640 15174677547 0012355 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; /** * Subscriber for compatibility with One.com hosting. * * @since 3.12.1 */ class OneCom implements Subscriber_Interface { use ReturnTypesTrait; /** * Array of events this subscriber wants to listen to. * * @since 3.6.3 * * @return array */ public static function get_subscribed_events() { return [ 'pre_get_rocket_option_cdn' => 'maybe_enable_cdn_option', 'pre_get_rocket_option_cdn_cnames' => 'maybe_update_cdn_cname', 'pre_get_rocket_option_cdn_zone' => 'maybe_update_cdn_zone', 'rocket_cdn_reject_files' => 'exclude_from_cdn', 'rocket_disable_cdn_option_change' => 'is_oc_cdn_enabled', 'rocket_cdn_settings_fields' => 'disable_cdn_change', 'do_rocket_varnish_http_purge' => 'is_varnish_active', 'rocket_varnish_field_settings' => 'maybe_set_varnish_addon_title', 'rocket_display_input_varnish_auto_purge' => 'should_display_varnish_auto_purge_input', 'rocket_display_rocketcdn_cta' => 'return_false', 'rocket_display_rocketcdn_status' => 'return_false', 'rocket_promote_rocketcdn_notice' => 'return_false', ]; } /** * Check if one.com cdn is enabled. * * @return boolean */ public function is_oc_cdn_enabled(): bool { return rocket_get_constant( 'vcaching', false ) && rest_sanitize_boolean( get_option( 'oc_cdn_enabled' ) ); } /** * Enable CDN option. * * @param string|null $cdn CDN Option. * @return bool|null */ public function maybe_enable_cdn_option( ?string $cdn ) { return $this->is_oc_cdn_enabled() ? true : $cdn; } /** * Update CNAME * * @param string|null $cname CDN CNAME. * @return array|null */ public function maybe_update_cdn_cname( ?string $cname ) { return $this->is_oc_cdn_enabled() ? [ $this->build_cname() ] : $cname; } /** * Update CDN Zones. * * @param string|null $zone CDN ZONES. * @return array|null */ public function maybe_update_cdn_zone( ?string $zone ) { return $this->is_oc_cdn_enabled() ? [ 'all' ] : $zone; } /** * Exclude files from being rewritten. * From 3.12.5.2 we are excluding new wp-content directory paths if it's not the normal one. * * @param array $files Array of files to be excluded. * @return array */ public function exclude_from_cdn( array $files ): array { if ( ! $this->is_oc_cdn_enabled() ) { return $files; } $files[] = '/wp-includes/(.*)'; return $files; } /** * Disable CDN option change. * * @param array $settings CDN field settings data. * @return array */ public function disable_cdn_change( array $settings ): array { if ( ! $this->is_oc_cdn_enabled() ) { return $settings; } $settings['cdn']['container_class'][] = 'wpr-isDisabled'; $settings['cdn']['input_attr']['disabled'] = 1; return $settings; } /** * Purge varnish cache. * * @return boolean */ public function should_display_varnish_auto_purge_input(): bool { return ! $this->is_varnish_active(); } /** * Set varnish addon title * * @param array $settings Varnish settings field data. * @return array */ public function maybe_set_varnish_addon_title( array $settings ): array { // Bail out if varnish is disabled. if ( ! $this->is_varnish_active() ) { return $settings; } $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'One.com' ); return $settings; } /** * Check if varnish option is enabled. * * @return boolean */ public function is_varnish_active() { return rocket_get_constant( 'vcaching', false ) && rest_sanitize_boolean( get_option( 'varnish_caching_enable' ) ); } /** * Build CDN CNAME. * * @return string */ public function build_cname(): string { if ( ! isset( $_SERVER['ONECOM_DOMAIN_NAME'] ) && ! isset( $_SERVER['HTTP_HOST'] ) ) { return ''; } $domain_name = sanitize_text_field( wp_unslash( $_SERVER['ONECOM_DOMAIN_NAME'] ) ); $http_host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ); $is_subdomain = '' === str_replace( $domain_name, '', $http_host ) ? false : true; return $is_subdomain ? "usercontent.one/wp/$http_host" : "usercontent.one/wp/www.$http_host"; } } ThirdParty/Hostings/Savvii.php 0000644 00000005033 15174677547 0012436 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use Savvii\CacheFlusherPlugin; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; /** * Compatibility for Savvii. * * @since 3.6.3 */ class Savvii implements Subscriber_Interface { use ReturnTypesTrait; /** * Returns an array of events that this subscriber wants to listen to. * * @see Subscriber_Interface. * * @since 3.6.3 * * @return array */ public static function get_subscribed_events() { return [ 'do_rocket_generate_caching_files' => [ 'return_false', PHP_INT_MAX ], 'rocket_varnish_field_settings' => 'varnish_addon_title', 'rocket_display_input_varnish_auto_purge' => 'return_false', 'rocket_cache_mandatory_cookies' => 'return_empty_array', 'init' => 'clear_cache_after_savvii', 'rocket_after_clean_domain' => 'clean_savvii', ]; } /** * Changes the text on the Varnish one-click block. * * @since 3.6.3 Rename and move to new architecture. * @since 3.0 * * @param array $settings Field settings data. * * @return array modified field settings data. */ public function varnish_addon_title( $settings ) { $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'Savvii' ); return $settings; } /** * Clear WP Rocket cache after purged the Varnish cache via Savvii Hosting. * * @since 3.6.3 Rename and move to new architecture. Refactor. Fix wrong nonce names/actions. * @since 2.6.5 */ public function clear_cache_after_savvii() { if ( ! ( isset( $_REQUEST[ CacheFlusherPlugin::NAME_FLUSH_NOW ] ) && check_admin_referer( CacheFlusherPlugin::NAME_FLUSH_NOW ) ) && ! ( isset( $_REQUEST[ CacheFlusherPlugin::NAME_DOMAINFLUSH_NOW ] ) && check_admin_referer( CacheFlusherPlugin::NAME_DOMAINFLUSH_NOW ) ) ) { return; } if ( ! current_user_can( 'rocket_purge_cache' ) ) { return; } // Clear all caching files. rocket_clean_domain(); // Preload cache. run_rocket_bot(); run_rocket_sitemap_preload(); } /** * Call the cache server to purge the cache with Savvii hosting. * * @since 3.6.3 Rename and move to new architecture. * @since 2.6.5 */ public function clean_savvii() { do_action( 'warpdrive_domain_flush' ); //phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } } ThirdParty/Hostings/Cloudways.php 0000644 00000005165 15174677547 0013155 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\NullSubscriber; /** * Compatibility class for Cloudways Varnish * * @since 3.5.5 */ class Cloudways extends NullSubscriber implements Subscriber_Interface { /** * Array of events this subscriber wants to listen to. * * @since 3.5.5 * * @return array */ public static function get_subscribed_events() { return [ 'rocket_display_input_varnish_auto_purge' => 'return_false', 'do_rocket_varnish_http_purge' => 'should_purge', 'rocket_varnish_field_settings' => 'varnish_addon_title', 'rocket_varnish_ip' => 'varnish_ip', ]; } /** * Determine if the Varnish server is up and running. * * @since 3.6.1 */ private static function is_varnish_running() { if ( ! isset( $_SERVER['HTTP_X_VARNISH'] ) ) { return false; } if ( ! isset( $_SERVER['HTTP_X_APPLICATION'] ) ) { return false; } return ( 'varnishpass' !== trim( strtolower( $_SERVER['HTTP_X_APPLICATION'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash } /** * Returns false * * @since 3.5.5 * * @return bool */ public function return_false() { return false; } /** * Returns should purge Varnish. * * @since 3.5.5 * * @return true */ public function should_purge() { return self::is_varnish_running(); } /** * Displays custom title for the Varnish add-on * * @since 3.5.5 * * @param array $settings Array of settings for Varnish. * @return array */ public function varnish_addon_title( array $settings ) { if ( ! self::is_varnish_running() ) { $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Varnish auto-purge will be automatically enabled once Varnish is enabled on your %s server.', 'rocket' ), 'Cloudways' ); return $settings; } $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'Cloudways' ); return $settings; } /** * Adds Cloudways Varnish IP to varnish IPs array * * @since 3.5.5 * * @param mixed $varnish_ip Varnish IP. * @return array */ public function varnish_ip( $varnish_ip ) { if ( ! self::is_varnish_running() ) { return $varnish_ip; } if ( ! is_array( $varnish_ip ) ) { $varnish_ip = (array) $varnish_ip; } $varnish_ip[] = '127.0.0.1:8080'; return $varnish_ip; } } ThirdParty/Hostings/AbstractNoCacheHost.php 0000644 00000003012 15174677547 0015012 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Engine\Activation\ActivationInterface; use WP_Rocket\Engine\Deactivation\DeactivationInterface; use WP_Rocket\Event_Management\Event_Manager; use WP_Rocket\Event_Management\Event_Manager_Aware_Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; abstract class AbstractNoCacheHost implements ActivationInterface, DeactivationInterface, Event_Manager_Aware_Subscriber_Interface { use ReturnTypesTrait; /** * Event Manager instance * * @var Event_Manager */ protected $event_manager; /** * Actions to perform on plugin activation * * @since 3.6.3 * * @return void */ public function activate() { add_action( 'rocket_activation', [ $this, 'no_cache_config' ] ); } /** * Actions to perform on plugin deactivation * * @since 3.6.3 * * @return void */ public function deactivate() { add_action( 'rocket_deactivation', [ $this, 'no_cache_config' ] ); } /** * Prevent writing in advanced-cache.php & wp-config.php when on self-caching host. * * @since 3.6.3 * * @return void */ public function no_cache_config() { add_filter( 'rocket_set_wp_cache_constant', [ $this, 'return_false' ] ); add_filter( 'rocket_generate_advanced_cache_file', [ $this, 'return_false' ] ); } /** * Sets the event manager for the subscriber. * * @param Event_Manager $event_manager Event Manager instance. */ public function set_event_manager( Event_Manager $event_manager ) { $this->event_manager = $event_manager; } } ThirdParty/Hostings/ServiceProvider.php 0000644 00000002720 15174677547 0014310 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Dependencies\League\Container\ServiceProvider\BootableServiceProviderInterface; use WP_Rocket\ThirdParty\Hostings\HostResolver; use WP_Rocket\ThirdParty\Hostings\HostSubscriberFactory; /** * Hostings compatibility service provider */ class ServiceProvider extends AbstractServiceProvider implements BootableServiceProviderInterface { /** * Array of services provided by this service provider * * @var array */ protected $provides = []; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Register the service in the provider array * * @return void */ public function boot(): void { $hosting_service = HostResolver::get_host_service(); if ( ! empty( $hosting_service ) ) { $this->provides[] = $hosting_service; } } /** * Registers the current hosting subscriber in the container * * @since 3.6.3 * * @return void */ public function register(): void { $hosting_service = HostResolver::get_host_service(); if ( ! empty( $hosting_service ) ) { $this->getContainer() ->addShared( $hosting_service, ( new HostSubscriberFactory() )->get_subscriber() ); } } } ThirdParty/Hostings/WordPressCom.php 0000644 00000001611 15174677547 0013562 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; /** * Subscriber for compatibility with WordPress.com hosting. * * @since 3.6.3 */ class WordPressCom extends AbstractNoCacheHost { /** * Array of events this subscriber listens to. * * @since 3.6.3 * * @return array The array of subscribed events. */ public static function get_subscribed_events() { return [ 'do_rocket_generate_caching_files' => 'return_false', 'rocket_cache_mandatory_cookies' => 'return_empty_array', 'rocket_display_varnish_options_tab' => 'return_false', 'rocket_set_wp_cache_constant' => 'return_false', 'rocket_generate_advanced_cache_file' => 'return_false', 'rocket_after_clean_domain' => 'purge_wpcom_cache', ]; } /** * Purge WordPress.com cache * * @since 3.6.3 * * @return void */ public function purge_wpcom_cache() { wp_cache_flush(); } } ThirdParty/Hostings/Pressable.php 0000644 00000003351 15174677547 0013116 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; /** * Subscriber for compatibility with Pressable hosting * * @since 3.3 */ class Pressable extends AbstractNoCacheHost { /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3 * * @return array */ public static function get_subscribed_events() { return [ 'do_rocket_generate_caching_files' => [ 'return_false', PHP_INT_MAX ], 'rocket_display_varnish_options_tab' => 'return_false', 'rocket_cache_mandatory_cookies' => [ 'return_empty_array', PHP_INT_MAX ], 'rocket_after_clean_domain' => 'purge_pressable_cache', 'rocket_url_to_path' => 'fix_wp_includes_path', 'rocket_set_wp_cache_constant' => 'return_false', 'rocket_generate_advanced_cache_file' => 'return_false', 'rocket_cdn_cnames' => [ 'add_pressable_cdn_cname', 1 ], ]; } /** * Purge Pressable cache * * @since 3.3 * * @return void */ public function purge_pressable_cache() { wp_cache_flush(); } /** * Modify wp-includes absolute path to be able to optimize assets in this directory on Pressable * * @since 3.3 * * @param string $file Absolute path to the file. * @return string */ public function fix_wp_includes_path( $file ) { return preg_replace( '#^(.+)(wp-includes(?:.+))$#is', ABSPATH . '$2', $file ); } /** * Add Pressable CDN cname to WP Rocket list to recognize assets as internal ones. * * @since 3.3 * * @param array $hosts Array of CDN URLs. * @return array */ public function add_pressable_cdn_cname( $hosts ) { if ( ! rocket_get_constant( 'WP_STACK_CDN_DOMAIN' ) ) { return $hosts; } $hosts[] = WP_STACK_CDN_DOMAIN; return $hosts; } } ThirdParty/Hostings/Godaddy.php 0000644 00000011044 15174677547 0012547 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; class Godaddy implements Subscriber_Interface { use ReturnTypesTrait; /** * Godaddy vip url * * @var string */ private $vip_url = ''; /** * Godaddy constructor. * * @param string $vip_url Godaddy vip url. */ public function __construct( $vip_url = '' ) { $this->vip_url = method_exists( '\WPaas\Plugin', 'vip' ) ? \WPaas\Plugin::vip() : $vip_url; // @phpstan-ignore-line } /** * Returns an array of events that this subscriber wants to listen to. * * @see Subscriber_Interface. * * @since 3.9.1 * * @return array */ public static function get_subscribed_events() { return [ 'rocket_varnish_field_settings' => 'varnish_field', 'rocket_display_input_varnish_auto_purge' => [ 'return_false' ], 'set_rocket_wp_cache_define' => [ 'return_true' ], 'rocket_cache_mandatory_cookies' => [ 'return_empty_array', PHP_INT_MAX ], 'rocket_htaccess_mod_rewrite' => [ 'return_false' ], 'rocket_htaccess_mod_expires' => [ 'remove_html_expire', 5 ], 'before_rocket_clean_domain' => 'clean_domain', 'before_rocket_clean_file' => 'clean_file', 'before_rocket_clean_home' => [ 'clean_home', 10, 2 ], ]; } /** * Changes the text on the Varnish one-click block. * * @since 3.9.1 * * @param array $settings Field settings data. * * @return array modified field settings data. */ public function varnish_field( $settings ): array { $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'GoDaddy' ); return $settings; } /** * Remove expiration on HTML to prevent issue with Varnish cache. * * @since 3.9.1 * * @param string $rules htaccess rules. * * @return string */ public function remove_html_expire( $rules ): string { $rules = preg_replace( '@\s*#\s*Your document html@', '', $rules ); $rules = preg_replace( '@\s*ExpiresByType text/html\s*"access plus \d+ (seconds|minutes|hour|week|month|year)"@', '', $rules ); return $rules; } /** * Call the Varnish server to purge the cache with GoDaddy. * * @since 3.9.1 * * @return void */ public function clean_domain() { $this->purge_request( 'BAN' ); } /** * Call the Varnish server to purge a specific URL with GoDaddy. * * @since 3.9.1 * * @param string $url URL to purge. * * @return void */ public function clean_file( $url ) { $this->purge_request( 'BAN', $url ); } /** * Call the Varnish server to purge the home with GoDaddy. * * @since 3.9.1 * * @param string $root root URL. * @param string $lang language code. * * @return void */ public function clean_home( $root, $lang ) { $home_url = trailingslashit( get_rocket_i18n_home_url( $lang ) ); $home_pagination_url = $home_url . trailingslashit( $GLOBALS['wp_rewrite']->pagination_base ); $this->purge_request( 'BAN', $home_url ); $this->purge_request( 'BAN', $home_pagination_url ); } /** * Perform the call to the Varnish server to purge * * @since 3.9.1 * @source WPaaS\Cache * * @param string $method can be BAN or PURGE. * @param string $url URL to purge. * * @return void */ private function purge_request( string $method, string $url = '' ) { if ( empty( $this->vip_url ) ) { return; } if ( empty( $url ) ) { $url = home_url(); } $host = wp_parse_url( $url, PHP_URL_HOST ); $url = untrailingslashit( set_url_scheme( str_replace( $host, $this->vip_url, $url ), 'http' ) ); wp_cache_flush(); // This forces the APC cache to flush across the server. update_option( 'gd_system_last_cache_flush', time() ); wp_remote_request( esc_url_raw( $url ), [ 'method' => $method, 'blocking' => false, 'headers' => [ 'Host' => $host, ], ] ); } /** * Performs these actions during the plugin activation * * @since 3.9.1 * * @return void */ public function activate() { add_action( 'rocket_activation', [ $this, 'activate_no_htaccess_html_expire' ] ); } /** * Remove expiration on HTML on activation to prevent issue with Varnish cache. * * @since 3.9.1 * * @return void */ public function activate_no_htaccess_html_expire() { add_filter( 'rocket_htaccess_mod_expires', [ $this, 'remove_htaccess_html_expire' ] ); } } ThirdParty/Hostings/LiteSpeed.php 0000644 00000006172 15174677547 0013060 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Event_Management\Subscriber_Interface; class LiteSpeed implements Subscriber_Interface { /** * Litespeed headers * * @var array */ private $headers = []; /** * Subscribed events for Litespeed. * * @since 3.9.2 * @inheritDoc */ public static function get_subscribed_events() { return [ 'before_rocket_clean_domain' => 'litespeed_clean_domain', 'before_rocket_clean_file' => 'litespeed_clean_file', 'before_rocket_clean_home' => [ 'litespeed_clean_home', 10, 2 ], ]; } /** * Add purge headers to the request * * @since 3.9.2 * * @return void */ public function litespeed_send_headers() { if ( empty( $this->headers ) ) { return; } foreach ( $this->headers as $header ) { @header( "X-LiteSpeed-Purge: {$header}", false ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } } /** * Purge all pages in LiteSpeed * * @since 3.9.2 * * @return void */ public function litespeed_clean_domain() { $this->litespeed_header_purge_all(); } /** * Purge a specific page * * @since 3.9.2 * * @param string $url The url to purge. * * @return void */ public function litespeed_clean_file( $url ) { $this->litespeed_header_purge_url( trailingslashit( $url ) ); } /** * Purge the homepage and its pagination * * @since 3.9.2 * * @param string $root The path of home cache file. * @param string $lang The current lang to purge. * * @return void */ public function litespeed_clean_home( $root, $lang ) { $home_url = trailingslashit( get_rocket_i18n_home_url( $lang ) ); $urls = [ 'home' => trailingslashit( get_rocket_i18n_home_url( $lang ) ), 'pagination' => $home_url . trailingslashit( $GLOBALS['wp_rewrite']->pagination_base ), ]; array_walk( $urls, function ( &$url ) { $url = wp_parse_url( $url, PHP_URL_PATH ); } ); $urls = array_filter( $urls ); if ( empty( $urls ) ) { return; } $this->send_header( 'X-LiteSpeed-Purge: ' . $urls['home'] ); $this->send_header( 'X-LiteSpeed-Purge: ' . $urls['pagination'] ); } /** * Set LiteSpeed header for the URL to purge * * @since 3.9.2 * * @param string $url The URL to purge. * * @return void */ private function litespeed_header_purge_url( $url ) { $path = wp_parse_url( $url, PHP_URL_PATH ); if ( ! $path ) { return; } $header = 'X-LiteSpeed-Purge: ' . $path; $this->send_header( $header ); } /** * Set LiteSpeed header to purge all * * @since 3.9.2 * * @return void */ private function litespeed_header_purge_all() { $this->send_header( 'X-LiteSpeed-Purge: *', true ); } /** * If header is not in header_list() send it * * @since 3.9.2 * * @param string $header To be sent. * @param boolean $replace header. * * @return void */ private function send_header( $header, $replace = false ) { if ( headers_sent() || in_array( $header, headers_list(), true ) ) { return; } $this->headers[] = $header; @header( $header, $replace ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } } ThirdParty/Hostings/Kinsta.php 0000644 00000010434 15174677547 0012427 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; class Kinsta implements Subscriber_Interface { use ReturnTypesTrait; /** * Subscribed events for Kinsta. * * @inheritDoc */ public static function get_subscribed_events() { global $kinsta_cache; $events = [ 'do_rocket_generate_caching_files' => [ 'return_false', PHP_INT_MAX ], 'rocket_display_varnish_options_tab' => 'return_false', 'rocket_cache_mandatory_cookies' => [ 'return_empty_array', PHP_INT_MAX ], ]; if ( isset( $kinsta_cache ) ) { $events['rocket_after_clean_domain'] = 'clean_kinsta_cache'; $events['after_rocket_clean_post'] = 'clean_kinsta_post_cache'; $events['rocket_rucss_after_clearing_usedcss'] = 'clean_kinsta_cache_url'; $events['rocket_saas_complete_job_status'] = 'clean_kinsta_cache_url'; $events['after_rocket_clean_home'] = [ 'clean_kinsta_cache_home', 10, 2 ]; $events['after_rocket_clean_file'] = 'clean_kinsta_cache_url'; $events['wp_rocket_loaded'] = 'remove_partial_purge_hooks'; return $events; } $events['admin_notices'] = 'display_error_notice'; return $events; } /** * Clear Kinsta cache when clearing WP Rocket cache * * @since 3.0 * * @return void */ public function clean_kinsta_cache() { global $kinsta_cache; if ( ! empty( $kinsta_cache->kinsta_cache_purge ) ) { $kinsta_cache->kinsta_cache_purge->purge_complete_caches(); } } /** * Partially clear Kinsta cache when partially clearing WP Rocket cache * * @since 3.0 * * @param object $post Post object. * @return void */ public function clean_kinsta_post_cache( $post ) { global $kinsta_cache; $kinsta_cache->kinsta_cache_purge->initiate_purge( $post->ID, 'post' ); } /** * Clears Kinsta cache for the homepage URL when using "Purge this URL" from the admin bar on the front end * * @since 3.0.4 * * @param string $root WP Rocket root cache path. * @param string $lang Current language. * @return void */ public function clean_kinsta_cache_home( $root = '', $lang = '' ) { $url = get_rocket_i18n_home_url( $lang ); $url = trailingslashit( $url ) . 'kinsta-clear-cache/'; wp_safe_remote_get( $url, [ 'blocking' => false, 'timeout' => 0.01, ] ); } /** * Clears Kinsta cache for a specific URL when using "Purge this URL" from the admin bar on the front end * * @since 3.0.4 * * @param string $url URL to purge. * @return void */ public function clean_kinsta_cache_url( $url ) { $url = trailingslashit( $url ) . 'kinsta-clear-cache/'; wp_safe_remote_get( $url, [ 'blocking' => false, 'timeout' => 0.01, ] ); } /** * Remove WP Rocket functions on WP core action hooks to prevent triggering a double cache clear. * * @since 3.0 * * @return void */ public function remove_partial_purge_hooks() { // WP core action hooks clean_post() gets hooked into. $clean_post_hooks = [ // Disables the refreshing of partial cache when content is edited. 'wp_trash_post', 'delete_post', 'clean_post_cache', 'wp_update_comment_count', ]; // Remove rocket_clean_post() from core action hooks. array_map( function ( $hook ) { remove_action( $hook, 'rocket_clean_post' ); }, $clean_post_hooks ); remove_filter( 'rocket_clean_files', 'rocket_clean_files_users' ); } /** * Display notice when we are on Kinsta but the plugin is not present. * * @return void */ public function display_error_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } $screen = get_current_screen(); if ( 'settings_page_wprocket' !== $screen->id ) { return; } rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', // translators: %1$s = opening link tag, %2$s = closing link tag. 'message' => sprintf( __( 'Your installation seems to be missing core Kinsta files managing Cache clearing, which will prevent your Kinsta installation and WP Rocket from working correctly. Please get in touch with Kinsta support through your %1$sMyKinsta%2$s account to resolve this issue.', 'rocket' ), '<a href="https://my.kinsta.com/login/" target="_blank">', '</a>' ), ] ); } } ThirdParty/Hostings/Dreampress.php 0000644 00000005414 15174677547 0013305 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; /** * Compatibility class for DreamPress * * @since 3.7.2 */ class Dreampress extends AbstractNoCacheHost { /** * Array of events this subscriber wants to listen to. * * @since 3.7.2 * * @return array */ public static function get_subscribed_events() { return [ 'do_rocket_varnish_http_purge' => 'return_true', 'rocket_varnish_field_settings' => 'set_varnish_addon_title', 'rocket_display_input_varnish_auto_purge' => 'return_false', 'rocket_varnish_ip' => 'set_varnish_host', 'rocket_set_wp_cache_constant' => 'return_false', 'do_rocket_generate_caching_files' => 'return_false', 'rocket_generate_advanced_cache_file' => 'return_false', 'rocket_cache_mandatory_cookies' => [ 'return_empty_array', PHP_INT_MAX ], 'rocket_htaccess_mod_expires' => [ 'remove_htaccess_html_expire', 5 ], ]; } /** * Changes the text on the Varnish one-click block. * * @since 3.7.2 * * @param array $settings Field settings data. * * @return array modified field settings data. */ public function set_varnish_addon_title( $settings ) { $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'DreamPress' ); return $settings; } /** * Sets the Varnish host to localhost * * @since 3.7.2 * * @param mixed $hosts Varnish hosts. * @return array */ public function set_varnish_host( $hosts ) { if ( ! is_array( $hosts ) ) { $hosts = (array) $hosts; } if ( in_array( 'localhost', $hosts, true ) ) { return $hosts; } $hosts[] = 'localhost'; return $hosts; } /** * Remove expiration on HTML to prevent issue with Varnish cache. * * @since 3.7.2 * * @param string $rules htaccess rules. * * @return string Updated htaccess rules. */ public function remove_htaccess_html_expire( $rules ) { $rules = preg_replace( '@\s*#\s*Your document html@', '', $rules ); $rules = preg_replace( '@\s*ExpiresByType text/html\s*"access plus \d+ (seconds|minutes|hour|week|month|year)"@', '', $rules ); return $rules; } /** * Performs these actions during the plugin activation * * @since 3.7.2 * * @return void */ public function activate() { parent::activate(); add_action( 'rocket_activation', [ $this, 'activate_no_htaccess_html_expire' ] ); } /** * Remove expiration on HTML on activation to prevent issue with Varnish cache. * * @since 3.7.2 * * @return void */ public function activate_no_htaccess_html_expire() { add_filter( 'rocket_htaccess_mod_expires', [ $this, 'remove_htaccess_html_expire' ] ); } } ThirdParty/Hostings/WPXCloud.php 0000644 00000004363 15174677547 0012647 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\ThirdParty\ReturnTypesTrait; /** * Compatibility class for WPX Cloud. */ class WPXCloud extends AbstractNoCacheHost { /** * Array of events this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_varnish_ip' => 'varnish_ip', 'rocket_display_input_varnish_auto_purge' => 'return_false', 'do_rocket_varnish_http_purge' => 'return_true', 'rocket_varnish_field_settings' => 'varnish_addon_title', 'after_rocket_htaccess_rules' => 'append_cache_control_header', ]; } /** * Adds WPX Cloud Varnish IP to varnish IPs array * * @param mixed $varnish_ip Varnish IP. * @return array */ public function varnish_ip( $varnish_ip ): array { if ( ! is_array( $varnish_ip ) ) { $varnish_ip = (array) $varnish_ip; } $varnish_ip[] = '127.0.0.1:6081'; return $varnish_ip; } /** * Displays custom title for the Varnish add-on * * @param array $settings Array of settings for Varnish. * @return array */ public function varnish_addon_title( array $settings ): array { $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'WPX' ); return $settings; } /** * Append cache control header. * * @return string */ public function append_cache_control_header(): string { $header = '<IfModule mod_headers.c>' . PHP_EOL; $header .= 'Header append Cache-Control " s-maxage=3600, stale-while-revalidate=21600" "expr=%{CONTENT_TYPE} =~ m#text/html#"' . PHP_EOL; $header .= '</IfModule>' . PHP_EOL; return $header; } /** * Performs these actions during the plugin activation. * * @return void */ public function activate(): void { parent::activate(); add_action( 'rocket_activation', [ $this, 'append_cache_control_header_on_activation' ] ); } /** * Append cache control header. * * @return void */ public function append_cache_control_header_on_activation(): void { add_filter( 'after_rocket_htaccess_rules', [ $this, 'append_cache_control_header' ] ); } } ThirdParty/Hostings/SpinUpWP.php 0000644 00000004361 15174677547 0012665 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\{NullSubscriber, ReturnTypesTrait}; use WP_Term; /** * Compatibility class for SpinUpWP * * @since 3.6.2 */ class SpinUpWP extends NullSubscriber implements Subscriber_Interface { use ReturnTypesTrait; /** * Array of events this subscriber wants to listen to. * * @since 3.6.2 * * @return array */ public static function get_subscribed_events() { return [ 'do_rocket_generate_caching_files' => 'return_false', 'rocket_display_varnish_options_tab' => 'return_false', 'rocket_cache_mandatory_cookies' => 'return_empty_array', 'rocket_after_clean_domain' => 'purge_site', 'wp_rocket_loaded' => 'remove_actions', 'after_rocket_clean_file' => 'purge_url', 'rocket_rucss_after_clearing_usedcss' => 'purge_url', 'rocket_saas_complete_job_status' => 'purge_url', 'after_rocket_clean_term' => [ 'purge_term_urls', 10, 2 ], 'rocket_after_clean_terms' => 'purge_urls', ]; } /** * Purge SpinUpWP cache after clean domain. * * @since 3.6.2 * * @return void */ public function purge_site() { if ( ! function_exists( 'spinupwp_purge_site' ) ) { return; } spinupwp_purge_site(); } /** * Remove rocket_clean_domain which prevents a double clear of the cache. * * @since 3.6.2 * * @return void */ public function remove_actions() { remove_action( 'switch_theme', 'rocket_clean_domain' ); } /** * Purge URL in SpinUpWP * * @param string $url URL. * * @return void */ public function purge_url( $url ) { if ( ! function_exists( 'spinupwp_purge_url' ) ) { return; } spinupwp_purge_url( trailingslashit( $url ) ); } /** * Purge multiple URLs * * @param array $urls Array of URLs. * * @return void */ public function purge_urls( $urls ) { foreach ( $urls as $url ) { $this->purge_url( $url ); } } /** * Purge URLs related to a term * * @param WP_Term $term The term object. * @param array $urls Array of URLs. * * @return void */ public function purge_term_urls( $term, $urls ) { $this->purge_urls( $urls ); } } ThirdParty/Hostings/O2Switch.php 0000644 00000006560 15174677547 0012645 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Engine\Activation\ActivationInterface; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\NullSubscriber; use WP_Rocket\ThirdParty\ReturnTypesTrait; /** * Compatibility class for O2Switch * * @since 3.6.3 */ class O2Switch extends NullSubscriber implements Subscriber_Interface, ActivationInterface { use ReturnTypesTrait; /** * Array of events this subscriber wants to listen to. * * @since 3.6.3 * * @return array */ public static function get_subscribed_events() { return [ 'do_rocket_varnish_http_purge' => 'return_true', 'rocket_varnish_field_settings' => 'varnish_addon_title', 'rocket_display_input_varnish_auto_purge' => 'return_false', 'rocket_cache_mandatory_cookies' => [ 'return_empty_array', PHP_INT_MAX ], 'rocket_htaccess_mod_expires' => [ 'remove_htaccess_html_expire', 5 ], 'rocket_varnish_purge_headers' => 'add_purge_headers', 'rocket_varnish_purge_url' => [ 'remove_regex_from_purge_url', 10, 2 ], ]; } /** * Changes the text on the Varnish one-click block. * * @since 3.6.3 * * @param array $settings Field settings data. * * @return array modified field settings data. */ public function varnish_addon_title( $settings ) { $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'O2Switch' ); return $settings; } /** * Remove expiration on HTML to prevent issue with Varnish cache. * * @since 3.6.3 * * @param string $rules htaccess rules. * * @return string Updated htaccess rules. */ public function remove_htaccess_html_expire( $rules ) { $rules = preg_replace( '@\s*#\s*Your document html@', '', $rules ); $rules = preg_replace( '@\s*ExpiresByType text/html\s*"access plus \d+ (seconds|minutes|hour|week|month|year)"@', '', $rules ); return $rules; } /** * Adjust purge request header array. * * @since 3.6.3 * * @param array $headers Headers to send. * * @return array Array for headers to be sent. */ public function add_purge_headers( $headers ) { $headers['X-VC-Purge-Key'] = rocket_get_constant( 'O2SWITCH_VARNISH_PURGE_KEY' ); if ( isset( $headers['X-Purge-Method'] ) && 'regex' === $headers['X-Purge-Method'] ) { $headers['X-Purge-Regex'] = '.*'; unset( $headers['X-Purge-Method'] ); } return $headers; } /** * Remove regex part from purge_url as it's handled via headers instead. * * @since 3.6.3 * * @param string $full_purge_url Full url for purge, regex included. * @param string $main_purge_url Main url without regex. * * @return mixed */ public function remove_regex_from_purge_url( $full_purge_url, $main_purge_url ) { return $main_purge_url; } /** * Performs these actions during the plugin activation * * @return void */ public function activate() { add_action( 'rocket_activation', [ $this, 'activate_no_htaccess_html_expire' ] ); } /** * Remove expiration on HTML on activation to prevent issue with Varnish cache. * * @since 3.6.3 * * @return void */ public function activate_no_htaccess_html_expire() { add_filter( 'rocket_htaccess_mod_expires', [ $this, 'remove_htaccess_html_expire' ] ); } } ThirdParty/Hostings/Pressidium.php 0000644 00000006467 15174677547 0013335 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use NinukisCaching; use WP_Post; use WP_Rocket\ThirdParty\ReturnTypesTrait; class Pressidium extends AbstractNoCacheHost { use ReturnTypesTrait; /** * Returns an array of events that this subscriber wants to listen to. * * @see Subscriber_Interface. * * @return array */ public static function get_subscribed_events() { $events = []; if ( defined( 'WP_NINUKIS_WP_NAME' ) ) { $events['rocket_varnish_field_settings'] = 'pressidium_varnish_field'; $events['rocket_display_input_varnish_auto_purge'] = 'return_false'; $events['rocket_cache_mandatory_cookies'] = [ 'return_empty_array', PHP_INT_MAX ]; $events['admin_init'] = 'clear_cache_after_pressidium'; } if ( class_exists( 'NinukisCaching' ) ) { $events['rocket_after_clean_domain'] = 'clean_pressidium'; $events['after_rocket_clean_file'] = [ 'purge_url', 10, 1 ]; $events['after_rocket_clean_post'] = [ 'clean_post', 10, 2 ]; } return $events; } /** * Changes the text on the Varnish one-click block. * * @since 3.0 * @author Remy Perona * * @param array $settings Field settings data. * * @return array modified field settings data. */ public function pressidium_varnish_field( $settings ) { // Translators: %s = Hosting name. $settings['varnish_auto_purge']['title'] = sprintf( __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'Pressidium' ); return $settings; } /** * Clear WP Rocket cache after purged the Varnish cache via Pressidium Hosting * * @since 2.5.11 * * @return void */ public function clear_cache_after_pressidium() { if ( isset( $_POST['purge-all'] ) && current_user_can( 'manage_options' ) && check_admin_referer( WP_NINUKIS_WP_NAME . '-caching' ) ) { // Clear all caching files. rocket_clean_domain(); } } /** * Call the cache server to purge the cache with Pressidium hosting. * * @since 2.6 * * @return void */ public function clean_pressidium() { $plugin = NinukisCaching::get_instance(); $plugin->purgeAllCaches(); } /** * Returns the path of URLs. * * @param array|string $urls Urls we want to get paths. * @return array the path. */ private function get_paths( $urls ) { if ( ! is_array( $urls ) ) { $urls = (array) $urls; } $paths = []; foreach ( $urls as $url ) { $parsed_url = get_rocket_parse_url( $url ); $paths[] = $parsed_url['path']; } return $paths; } /** * Purge the cache of Pressidium from paths. * * @param array $paths Paths of pages we are going to purge cache. * * @return void */ private function purge_cache( $paths ) { NinukisCaching::get_instance()->purge_cache( $paths ); } /** * Purge the cache for the given URL. * * @param string|array $url URL we want to purge. * * @return void */ public function purge_url( $url ) { $paths = $this->get_paths( $url ); $this->purge_cache( $paths ); } /** * Clean cache of post. * * @param WP_Post $post Post that need to be cleaned. * @param array $purge_urls URLs that need to be purged. * * @return void */ public function clean_post( $post, $purge_urls ) { // Purge related urls. $paths = $this->get_paths( $purge_urls ); $this->purge_cache( $paths ); } } ThirdParty/Hostings/WPEngine.php 0000644 00000004666 15174677547 0012664 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use WpeCommon; /** * Compatibility class for WP Engine. * * @since 3.6.1 */ class WPEngine extends AbstractNoCacheHost { /** * Array of events this subscriber wants to listen to. * * @since 3.6.1 * * @return array */ public static function get_subscribed_events() { return [ 'rocket_varnish_field_settings' => 'varnish_addon_title', 'rocket_display_input_varnish_auto_purge' => 'return_false', 'rocket_cache_mandatory_cookies' => [ 'return_empty_array', PHP_INT_MAX ], 'rocket_set_wp_cache_constant' => 'return_false', 'do_rocket_generate_caching_files' => 'return_false', 'rocket_after_clean_domain' => 'clean_wpengine', 'rocket_buffer' => [ 'add_footprint', 50 ], 'rocket_disable_htaccess' => 'return_true', 'rocket_generate_advanced_cache_file' => 'return_false', ]; } /** * Changes the text on the Varnish one-click block. * * @since 3.6.1 * * @param array $settings Field settings data. * * @return array modified field settings data. */ public function varnish_addon_title( $settings ) { $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'WP Engine' ); return $settings; } /** * Call the cache server to purge the cache with WP Engine hosting. * * @since 3.6.1 */ public function clean_wpengine() { if ( method_exists( 'WpeCommon', 'purge_memcached' ) ) { // @phpstan-ignore-line WpeCommon::purge_memcached(); } if ( method_exists( 'WpeCommon', 'purge_varnish_cache' ) ) { // @phpstan-ignore-line WpeCommon::purge_varnish_cache(); } } /** * Add WP Rocket footprint on Buffer. * * @since 3.6.1 * * @param string $buffer HTML content. * * @return string HTML with WP Rocket footprint. */ public function add_footprint( $buffer ) { if ( ! preg_match( '/<\/html>/i', $buffer ) ) { return $buffer; } $footprint = rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_FOOTPRINT' ) ? "\n" . '<!-- Optimized for great performance' : "\n" . '<!-- This website is like a Rocket, isn\'t it? Performance optimized by ' . rocket_get_constant( 'WP_ROCKET_PLUGIN_NAME' ) . '. Learn more: https://wp-rocket.me'; $footprint .= ' -->'; return $buffer . $footprint; } } ThirdParty/Hostings/ProIsp.php 0000644 00000003046 15174677547 0012413 0 ustar 00 <?php namespace WP_Rocket\ThirdParty\Hostings; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for compatibility with PRO ISP hosting. * * @since 3.13.1 */ class ProIsp implements Subscriber_Interface { /** * Array of events this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'do_rocket_varnish_http_purge' => 'is_varnish_active', 'rocket_varnish_field_settings' => 'maybe_set_varnish_addon_title', 'rocket_display_input_varnish_auto_purge' => 'should_display_varnish_auto_purge_input', ]; } /** * Purge varnish cache. * * @return boolean */ public function should_display_varnish_auto_purge_input(): bool { return ! $this->is_varnish_active(); } /** * Set varnish addon title * * @param array $settings Varnish settings field data. * @return array */ public function maybe_set_varnish_addon_title( array $settings ): array { // Bail out if varnish is disabled. if ( ! $this->is_varnish_active() ) { return $settings; } $settings['varnish_auto_purge']['title'] = sprintf( // Translators: %s = Hosting name. __( 'Your site is hosted on %s, we have enabled Varnish auto-purge for compatibility.', 'rocket' ), 'PRO ISP' ); return $settings; } /** * Check if varnish option is enabled. * * @return boolean */ public function is_varnish_active() { return rocket_get_constant( 'vcaching', false ) && rest_sanitize_boolean( get_option( 'varnish_caching_enable' ) ); } } vendors/classes/class-rocket-mobile-detect.php 0000644 00000237353 15174677547 0015543 0 ustar 00 <?php /** * Mobile Detect Library * ===================== * * Motto: "Every business should have a mobile detection script to detect mobile readers" * * Mobile_Detect is a lightweight PHP class for detecting mobile devices (including tablets). * It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment. * * @author Current authors: Serban Ghita <serbanghita@gmail.com> * Nick Ilyin <nick.ilyin@gmail.com> * * Original author: Victor Stanciu <vic.stanciu@gmail.com> * * @license Code and contributions have 'MIT License' * More details: https://github.com/serbanghita/Mobile-Detect/blob/master/LICENSE.txt * * @link Homepage: http://mobiledetect.net * GitHub Repo: https://github.com/serbanghita/Mobile-Detect * Google Code: http://code.google.com/p/php-mobile-detect/ * README: https://github.com/serbanghita/Mobile-Detect/blob/master/README.md * HOWTO: https://github.com/serbanghita/Mobile-Detect/wiki/Code-examples * * @version 2.8.27 */ class Rocket_Mobile_Detect { /** * Mobile detection type. * * @deprecated since version 2.6.9 */ const DETECTION_TYPE_MOBILE = 'mobile'; /** * Extended detection type. * * @deprecated since version 2.6.9 */ const DETECTION_TYPE_EXTENDED = 'extended'; /** * A frequently used regular expression to extract version #s. * * @deprecated since version 2.6.9 */ const VER = '([\w._\+]+)'; /** * Top-level device. */ const MOBILE_GRADE_A = 'A'; /** * Mid-level device. */ const MOBILE_GRADE_B = 'B'; /** * Low-level device. */ const MOBILE_GRADE_C = 'C'; /** * Stores the version number of the current release. */ const VERSION = '2.8.27'; /** * A type for the version() method indicating a string return value. */ const VERSION_TYPE_STRING = 'text'; /** * A type for the version() method indicating a float return value. */ const VERSION_TYPE_FLOAT = 'float'; /** * A cache for resolved matches * @var array */ protected $cache = array(); /** * The User-Agent HTTP header is stored in here. * @var string */ protected $userAgent = null; /** * HTTP headers in the PHP-flavor. So HTTP_USER_AGENT and SERVER_SOFTWARE. * @var array */ protected $httpHeaders = array(); /** * CloudFront headers. E.g. CloudFront-Is-Desktop-Viewer, CloudFront-Is-Mobile-Viewer & CloudFront-Is-Tablet-Viewer. * @var array */ protected $cloudfrontHeaders = array(); /** * The matching Regex. * This is good for debug. * @var string */ protected $matchingRegex = null; /** * The matches extracted from the regex expression. * This is good for debug. * @var string */ protected $matchesArray = null; /** * The detection type, using self::DETECTION_TYPE_MOBILE or self::DETECTION_TYPE_EXTENDED. * * @deprecated since version 2.6.9 * * @var string */ protected $detectionType = self::DETECTION_TYPE_MOBILE; /** * HTTP headers that trigger the 'isMobile' detection * to be true. * * @var array */ protected static $mobileHeaders = array( 'HTTP_ACCEPT' => array('matches' => array( // Opera Mini; @reference: http://dev.opera.com/articles/view/opera-binary-markup-language/ 'application/x-obml2d', // BlackBerry devices. 'application/vnd.rim.html', 'text/vnd.wap.wml', 'application/vnd.wap.xhtml+xml' )), 'HTTP_X_WAP_PROFILE' => null, 'HTTP_X_WAP_CLIENTID' => null, 'HTTP_WAP_CONNECTION' => null, 'HTTP_PROFILE' => null, // Reported by Opera on Nokia devices (eg. C3). 'HTTP_X_OPERAMINI_PHONE_UA' => null, 'HTTP_X_NOKIA_GATEWAY_ID' => null, 'HTTP_X_ORANGE_ID' => null, 'HTTP_X_VODAFONE_3GPDPCONTEXT' => null, 'HTTP_X_HUAWEI_USERID' => null, // Reported by Windows Smartphones. 'HTTP_UA_OS' => null, // Reported by Verizon, Vodafone proxy system. 'HTTP_X_MOBILE_GATEWAY' => null, // Seen this on HTC Sensation. SensationXE_Beats_Z715e. 'HTTP_X_ATT_DEVICEID' => null, // Seen this on a HTC. 'HTTP_UA_CPU' => array('matches' => array('ARM')), ); /** * List of mobile devices (phones). * * @var array */ protected static $phoneDevices = array( 'iPhone' => '\biPhone\b|\biPod\b', // |\biTunes 'BlackBerry' => 'BlackBerry|\bBB10\b|rim[0-9]+', 'HTC' => 'HTC|HTC.*(Sensation|Evo|Vision|Explorer|6800|8100|8900|A7272|S510e|C110e|Legend|Desire|T8282)|APX515CKT|Qtek9090|APA9292KT|HD_mini|Sensation.*Z710e|PG86100|Z715e|Desire.*(A8181|HD)|ADR6200|ADR6400L|ADR6425|001HT|Inspire 4G|Android.*\bEVO\b|T-Mobile G1|Z520m', 'Nexus' => 'Nexus One|Nexus S|Galaxy.*Nexus|Android.*Nexus.*Mobile|Nexus 4|Nexus 5|Nexus 6', // @todo: Is 'Dell Streak' a tablet or a phone? ;) 'Dell' => 'Dell[;]? (Streak|Aero|Venue|Venue Pro|Flash|Smoke|Mini 3iX)|XCD28|XCD35|\b001DL\b|\b101DL\b|\bGS01\b', 'Motorola' => 'Motorola|DROIDX|DROID BIONIC|\bDroid\b.*Build|Android.*Xoom|HRI39|MOT-|A1260|A1680|A555|A853|A855|A953|A955|A956|Motorola.*ELECTRIFY|Motorola.*i1|i867|i940|MB200|MB300|MB501|MB502|MB508|MB511|MB520|MB525|MB526|MB611|MB612|MB632|MB810|MB855|MB860|MB861|MB865|MB870|ME501|ME502|ME511|ME525|ME600|ME632|ME722|ME811|ME860|ME863|ME865|MT620|MT710|MT716|MT720|MT810|MT870|MT917|Motorola.*TITANIUM|WX435|WX445|XT300|XT301|XT311|XT316|XT317|XT319|XT320|XT390|XT502|XT530|XT531|XT532|XT535|XT603|XT610|XT611|XT615|XT681|XT701|XT702|XT711|XT720|XT800|XT806|XT860|XT862|XT875|XT882|XT883|XT894|XT901|XT907|XT909|XT910|XT912|XT928|XT926|XT915|XT919|XT925|XT1021|\bMoto E\b', 'Samsung' => '\bSamsung\b|SM-G9250|GT-19300|SGH-I337|BGT-S5230|GT-B2100|GT-B2700|GT-B2710|GT-B3210|GT-B3310|GT-B3410|GT-B3730|GT-B3740|GT-B5510|GT-B5512|GT-B5722|GT-B6520|GT-B7300|GT-B7320|GT-B7330|GT-B7350|GT-B7510|GT-B7722|GT-B7800|GT-C3010|GT-C3011|GT-C3060|GT-C3200|GT-C3212|GT-C3212I|GT-C3262|GT-C3222|GT-C3300|GT-C3300K|GT-C3303|GT-C3303K|GT-C3310|GT-C3322|GT-C3330|GT-C3350|GT-C3500|GT-C3510|GT-C3530|GT-C3630|GT-C3780|GT-C5010|GT-C5212|GT-C6620|GT-C6625|GT-C6712|GT-E1050|GT-E1070|GT-E1075|GT-E1080|GT-E1081|GT-E1085|GT-E1087|GT-E1100|GT-E1107|GT-E1110|GT-E1120|GT-E1125|GT-E1130|GT-E1160|GT-E1170|GT-E1175|GT-E1180|GT-E1182|GT-E1200|GT-E1210|GT-E1225|GT-E1230|GT-E1390|GT-E2100|GT-E2120|GT-E2121|GT-E2152|GT-E2220|GT-E2222|GT-E2230|GT-E2232|GT-E2250|GT-E2370|GT-E2550|GT-E2652|GT-E3210|GT-E3213|GT-I5500|GT-I5503|GT-I5700|GT-I5800|GT-I5801|GT-I6410|GT-I6420|GT-I7110|GT-I7410|GT-I7500|GT-I8000|GT-I8150|GT-I8160|GT-I8190|GT-I8320|GT-I8330|GT-I8350|GT-I8530|GT-I8700|GT-I8703|GT-I8910|GT-I9000|GT-I9001|GT-I9003|GT-I9010|GT-I9020|GT-I9023|GT-I9070|GT-I9082|GT-I9100|GT-I9103|GT-I9220|GT-I9250|GT-I9300|GT-I9305|GT-I9500|GT-I9505|GT-M3510|GT-M5650|GT-M7500|GT-M7600|GT-M7603|GT-M8800|GT-M8910|GT-N7000|GT-S3110|GT-S3310|GT-S3350|GT-S3353|GT-S3370|GT-S3650|GT-S3653|GT-S3770|GT-S3850|GT-S5210|GT-S5220|GT-S5229|GT-S5230|GT-S5233|GT-S5250|GT-S5253|GT-S5260|GT-S5263|GT-S5270|GT-S5300|GT-S5330|GT-S5350|GT-S5360|GT-S5363|GT-S5369|GT-S5380|GT-S5380D|GT-S5560|GT-S5570|GT-S5600|GT-S5603|GT-S5610|GT-S5620|GT-S5660|GT-S5670|GT-S5690|GT-S5750|GT-S5780|GT-S5830|GT-S5839|GT-S6102|GT-S6500|GT-S7070|GT-S7200|GT-S7220|GT-S7230|GT-S7233|GT-S7250|GT-S7500|GT-S7530|GT-S7550|GT-S7562|GT-S7710|GT-S8000|GT-S8003|GT-S8500|GT-S8530|GT-S8600|SCH-A310|SCH-A530|SCH-A570|SCH-A610|SCH-A630|SCH-A650|SCH-A790|SCH-A795|SCH-A850|SCH-A870|SCH-A890|SCH-A930|SCH-A950|SCH-A970|SCH-A990|SCH-I100|SCH-I110|SCH-I400|SCH-I405|SCH-I500|SCH-I510|SCH-I515|SCH-I600|SCH-I730|SCH-I760|SCH-I770|SCH-I830|SCH-I910|SCH-I920|SCH-I959|SCH-LC11|SCH-N150|SCH-N300|SCH-R100|SCH-R300|SCH-R351|SCH-R400|SCH-R410|SCH-T300|SCH-U310|SCH-U320|SCH-U350|SCH-U360|SCH-U365|SCH-U370|SCH-U380|SCH-U410|SCH-U430|SCH-U450|SCH-U460|SCH-U470|SCH-U490|SCH-U540|SCH-U550|SCH-U620|SCH-U640|SCH-U650|SCH-U660|SCH-U700|SCH-U740|SCH-U750|SCH-U810|SCH-U820|SCH-U900|SCH-U940|SCH-U960|SCS-26UC|SGH-A107|SGH-A117|SGH-A127|SGH-A137|SGH-A157|SGH-A167|SGH-A177|SGH-A187|SGH-A197|SGH-A227|SGH-A237|SGH-A257|SGH-A437|SGH-A517|SGH-A597|SGH-A637|SGH-A657|SGH-A667|SGH-A687|SGH-A697|SGH-A707|SGH-A717|SGH-A727|SGH-A737|SGH-A747|SGH-A767|SGH-A777|SGH-A797|SGH-A817|SGH-A827|SGH-A837|SGH-A847|SGH-A867|SGH-A877|SGH-A887|SGH-A897|SGH-A927|SGH-B100|SGH-B130|SGH-B200|SGH-B220|SGH-C100|SGH-C110|SGH-C120|SGH-C130|SGH-C140|SGH-C160|SGH-C170|SGH-C180|SGH-C200|SGH-C207|SGH-C210|SGH-C225|SGH-C230|SGH-C417|SGH-C450|SGH-D307|SGH-D347|SGH-D357|SGH-D407|SGH-D415|SGH-D780|SGH-D807|SGH-D980|SGH-E105|SGH-E200|SGH-E315|SGH-E316|SGH-E317|SGH-E335|SGH-E590|SGH-E635|SGH-E715|SGH-E890|SGH-F300|SGH-F480|SGH-I200|SGH-I300|SGH-I320|SGH-I550|SGH-I577|SGH-I600|SGH-I607|SGH-I617|SGH-I627|SGH-I637|SGH-I677|SGH-I700|SGH-I717|SGH-I727|SGH-i747M|SGH-I777|SGH-I780|SGH-I827|SGH-I847|SGH-I857|SGH-I896|SGH-I897|SGH-I900|SGH-I907|SGH-I917|SGH-I927|SGH-I937|SGH-I997|SGH-J150|SGH-J200|SGH-L170|SGH-L700|SGH-M110|SGH-M150|SGH-M200|SGH-N105|SGH-N500|SGH-N600|SGH-N620|SGH-N625|SGH-N700|SGH-N710|SGH-P107|SGH-P207|SGH-P300|SGH-P310|SGH-P520|SGH-P735|SGH-P777|SGH-Q105|SGH-R210|SGH-R220|SGH-R225|SGH-S105|SGH-S307|SGH-T109|SGH-T119|SGH-T139|SGH-T209|SGH-T219|SGH-T229|SGH-T239|SGH-T249|SGH-T259|SGH-T309|SGH-T319|SGH-T329|SGH-T339|SGH-T349|SGH-T359|SGH-T369|SGH-T379|SGH-T409|SGH-T429|SGH-T439|SGH-T459|SGH-T469|SGH-T479|SGH-T499|SGH-T509|SGH-T519|SGH-T539|SGH-T559|SGH-T589|SGH-T609|SGH-T619|SGH-T629|SGH-T639|SGH-T659|SGH-T669|SGH-T679|SGH-T709|SGH-T719|SGH-T729|SGH-T739|SGH-T746|SGH-T749|SGH-T759|SGH-T769|SGH-T809|SGH-T819|SGH-T839|SGH-T919|SGH-T929|SGH-T939|SGH-T959|SGH-T989|SGH-U100|SGH-U200|SGH-U800|SGH-V205|SGH-V206|SGH-X100|SGH-X105|SGH-X120|SGH-X140|SGH-X426|SGH-X427|SGH-X475|SGH-X495|SGH-X497|SGH-X507|SGH-X600|SGH-X610|SGH-X620|SGH-X630|SGH-X700|SGH-X820|SGH-X890|SGH-Z130|SGH-Z150|SGH-Z170|SGH-ZX10|SGH-ZX20|SHW-M110|SPH-A120|SPH-A400|SPH-A420|SPH-A460|SPH-A500|SPH-A560|SPH-A600|SPH-A620|SPH-A660|SPH-A700|SPH-A740|SPH-A760|SPH-A790|SPH-A800|SPH-A820|SPH-A840|SPH-A880|SPH-A900|SPH-A940|SPH-A960|SPH-D600|SPH-D700|SPH-D710|SPH-D720|SPH-I300|SPH-I325|SPH-I330|SPH-I350|SPH-I500|SPH-I600|SPH-I700|SPH-L700|SPH-M100|SPH-M220|SPH-M240|SPH-M300|SPH-M305|SPH-M320|SPH-M330|SPH-M350|SPH-M360|SPH-M370|SPH-M380|SPH-M510|SPH-M540|SPH-M550|SPH-M560|SPH-M570|SPH-M580|SPH-M610|SPH-M620|SPH-M630|SPH-M800|SPH-M810|SPH-M850|SPH-M900|SPH-M910|SPH-M920|SPH-M930|SPH-N100|SPH-N200|SPH-N240|SPH-N300|SPH-N400|SPH-Z400|SWC-E100|SCH-i909|GT-N7100|GT-N7105|SCH-I535|SM-N900A|SGH-I317|SGH-T999L|GT-S5360B|GT-I8262|GT-S6802|GT-S6312|GT-S6310|GT-S5312|GT-S5310|GT-I9105|GT-I8510|GT-S6790N|SM-G7105|SM-N9005|GT-S5301|GT-I9295|GT-I9195|SM-C101|GT-S7392|GT-S7560|GT-B7610|GT-I5510|GT-S7582|GT-S7530E|GT-I8750|SM-G9006V|SM-G9008V|SM-G9009D|SM-G900A|SM-G900D|SM-G900F|SM-G900H|SM-G900I|SM-G900J|SM-G900K|SM-G900L|SM-G900M|SM-G900P|SM-G900R4|SM-G900S|SM-G900T|SM-G900V|SM-G900W8|SHV-E160K|SCH-P709|SCH-P729|SM-T2558|GT-I9205|SM-G9350|SM-J120F|SM-G920F|SM-G920V|SM-G930F|SM-N910C', 'LG' => '\bLG\b;|LG[- ]?(C800|C900|E400|E610|E900|E-900|F160|F180K|F180L|F180S|730|855|L160|LS740|LS840|LS970|LU6200|MS690|MS695|MS770|MS840|MS870|MS910|P500|P700|P705|VM696|AS680|AS695|AX840|C729|E970|GS505|272|C395|E739BK|E960|L55C|L75C|LS696|LS860|P769BK|P350|P500|P509|P870|UN272|US730|VS840|VS950|LN272|LN510|LS670|LS855|LW690|MN270|MN510|P509|P769|P930|UN200|UN270|UN510|UN610|US670|US740|US760|UX265|UX840|VN271|VN530|VS660|VS700|VS740|VS750|VS910|VS920|VS930|VX9200|VX11000|AX840A|LW770|P506|P925|P999|E612|D955|D802|MS323)', 'Sony' => 'SonyST|SonyLT|SonyEricsson|SonyEricssonLT15iv|LT18i|E10i|LT28h|LT26w|SonyEricssonMT27i|C5303|C6902|C6903|C6906|C6943|D2533', 'Asus' => 'Asus.*Galaxy|PadFone.*Mobile', 'NokiaLumia' => 'Lumia [0-9]{3,4}', // http://www.micromaxinfo.com/mobiles/smartphones // Added because the codes might conflict with Acer Tablets. 'Micromax' => 'Micromax.*\b(A210|A92|A88|A72|A111|A110Q|A115|A116|A110|A90S|A26|A51|A35|A54|A25|A27|A89|A68|A65|A57|A90)\b', // @todo Complete the regex. 'Palm' => 'PalmSource|Palm', // avantgo|blazer|elaine|hiptop|plucker|xiino ; 'Vertu' => 'Vertu|Vertu.*Ltd|Vertu.*Ascent|Vertu.*Ayxta|Vertu.*Constellation(F|Quest)?|Vertu.*Monika|Vertu.*Signature', // Just for fun ;) // http://www.pantech.co.kr/en/prod/prodList.do?gbrand=VEGA (PANTECH) // Most of the VEGA devices are legacy. PANTECH seem to be newer devices based on Android. 'Pantech' => 'PANTECH|IM-A850S|IM-A840S|IM-A830L|IM-A830K|IM-A830S|IM-A820L|IM-A810K|IM-A810S|IM-A800S|IM-T100K|IM-A725L|IM-A780L|IM-A775C|IM-A770K|IM-A760S|IM-A750K|IM-A740S|IM-A730S|IM-A720L|IM-A710K|IM-A690L|IM-A690S|IM-A650S|IM-A630K|IM-A600S|VEGA PTL21|PT003|P8010|ADR910L|P6030|P6020|P9070|P4100|P9060|P5000|CDM8992|TXT8045|ADR8995|IS11PT|P2030|P6010|P8000|PT002|IS06|CDM8999|P9050|PT001|TXT8040|P2020|P9020|P2000|P7040|P7000|C790', // http://www.fly-phone.com/devices/smartphones/ ; Included only smartphones. 'Fly' => 'IQ230|IQ444|IQ450|IQ440|IQ442|IQ441|IQ245|IQ256|IQ236|IQ255|IQ235|IQ245|IQ275|IQ240|IQ285|IQ280|IQ270|IQ260|IQ250', // http://fr.wikomobile.com 'Wiko' => 'KITE 4G|HIGHWAY|GETAWAY|STAIRWAY|DARKSIDE|DARKFULL|DARKNIGHT|DARKMOON|SLIDE|WAX 4G|RAINBOW|BLOOM|SUNSET|GOA(?!nna)|LENNY|BARRY|IGGY|OZZY|CINK FIVE|CINK PEAX|CINK PEAX 2|CINK SLIM|CINK SLIM 2|CINK +|CINK KING|CINK PEAX|CINK SLIM|SUBLIM', 'iMobile' => 'i-mobile (IQ|i-STYLE|idea|ZAA|Hitz)', // Added simvalley mobile just for fun. They have some interesting devices. // http://www.simvalley.fr/telephonie---gps-_22_telephonie-mobile_telephones_.html 'SimValley' => '\b(SP-80|XT-930|SX-340|XT-930|SX-310|SP-360|SP60|SPT-800|SP-120|SPT-800|SP-140|SPX-5|SPX-8|SP-100|SPX-8|SPX-12)\b', // Wolfgang - a brand that is sold by Aldi supermarkets. // http://www.wolfgangmobile.com/ 'Wolfgang' => 'AT-B24D|AT-AS50HD|AT-AS40W|AT-AS55HD|AT-AS45q2|AT-B26D|AT-AS50Q', 'Alcatel' => 'Alcatel', 'Nintendo' => 'Nintendo 3DS', // http://en.wikipedia.org/wiki/Amoi 'Amoi' => 'Amoi', // http://en.wikipedia.org/wiki/INQ 'INQ' => 'INQ', // @Tapatalk is a mobile app; http://support.tapatalk.com/threads/smf-2-0-2-os-and-browser-detection-plugin-and-tapatalk.15565/#post-79039 'GenericPhone' => 'Tapatalk|PDA;|SAGEM|\bmmp\b|pocket|\bpsp\b|symbian|Smartphone|smartfon|treo|up.browser|up.link|vodafone|\bwap\b|nokia|Series40|Series60|S60|SonyEricsson|N900|MAUI.*WAP.*Browser', ); /** * List of tablet devices. * * @var array */ protected static $tabletDevices = array( // @todo: check for mobile friendly emails topic. 'iPad' => 'iPad|iPad.*Mobile', // Removed |^.*Android.*Nexus(?!(?:Mobile).)*$ // @see #442 'NexusTablet' => 'Android.*Nexus[\s]+(7|9|10)', 'SamsungTablet' => 'SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5105|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925|GT-I9200|GT-P5200|GT-P5210|GT-P5210X|SM-T311|SM-T310|SM-T310X|SM-T210|SM-T210R|SM-T211|SM-P600|SM-P601|SM-P605|SM-P900|SM-P901|SM-T217|SM-T217A|SM-T217S|SM-P6000|SM-T3100|SGH-I467|XE500|SM-T110|GT-P5220|GT-I9200X|GT-N5110X|GT-N5120|SM-P905|SM-T111|SM-T2105|SM-T315|SM-T320|SM-T320X|SM-T321|SM-T520|SM-T525|SM-T530NU|SM-T230NU|SM-T330NU|SM-T900|XE500T1C|SM-P605V|SM-P905V|SM-T337V|SM-T537V|SM-T707V|SM-T807V|SM-P600X|SM-P900X|SM-T210X|SM-T230|SM-T230X|SM-T325|GT-P7503|SM-T531|SM-T330|SM-T530|SM-T705|SM-T705C|SM-T535|SM-T331|SM-T800|SM-T700|SM-T537|SM-T807|SM-P907A|SM-T337A|SM-T537A|SM-T707A|SM-T807A|SM-T237|SM-T807P|SM-P607T|SM-T217T|SM-T337T|SM-T807T|SM-T116NQ|SM-T116BU|SM-P550|SM-T350|SM-T550|SM-T9000|SM-P9000|SM-T705Y|SM-T805|GT-P3113|SM-T710|SM-T810|SM-T815|SM-T360|SM-T533|SM-T113|SM-T335|SM-T715|SM-T560|SM-T670|SM-T677|SM-T377|SM-T567|SM-T357T|SM-T555|SM-T561|SM-T713|SM-T719|SM-T813|SM-T819|SM-T580|SM-T355Y|SM-T280|SM-T817A|SM-T820|SM-W700|SM-P580|SM-T587|SM-P350|SM-P555M|SM-P355M|SM-T113NU|SM-T815Y', // SCH-P709|SCH-P729|SM-T2558|GT-I9205 - Samsung Mega - treat them like a regular phone. // http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html 'Kindle' => 'Kindle|Silk.*Accelerated|Android.*\b(KFOT|KFTT|KFJWI|KFJWA|KFOTE|KFSOWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|WFJWAE|KFSAWA|KFSAWI|KFASWI|KFARWI|KFFOWI|KFGIWI|KFMEWI)\b|Android.*Silk/[0-9.]+ like Chrome/[0-9.]+ (?!Mobile)', // Only the Surface tablets with Windows RT are considered mobile. // http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx 'SurfaceTablet' => 'Windows NT [0-9.]+; ARM;.*(Tablet|ARMBJS)', // http://shopping1.hp.com/is-bin/INTERSHOP.enfinity/WFS/WW-USSMBPublicStore-Site/en_US/-/USD/ViewStandardCatalog-Browse?CatalogCategoryID=JfIQ7EN5lqMAAAEyDcJUDwMT 'HPTablet' => 'HP Slate (7|8|10)|HP ElitePad 900|hp-tablet|EliteBook.*Touch|HP 8|Slate 21|HP SlateBook 10', // Watch out for PadFone, see #132. // http://www.asus.com/de/Tablets_Mobile/Memo_Pad_Products/ 'AsusTablet' => '^.*PadFone((?!Mobile).)*$|Transformer|TF101|TF101G|TF300T|TF300TG|TF300TL|TF700T|TF700KL|TF701T|TF810C|ME171|ME301T|ME302C|ME371MG|ME370T|ME372MG|ME172V|ME173X|ME400C|Slider SL101|\bK00F\b|\bK00C\b|\bK00E\b|\bK00L\b|TX201LA|ME176C|ME102A|\bM80TA\b|ME372CL|ME560CG|ME372CG|ME302KL| K010 | K011 | K017 | K01E |ME572C|ME103K|ME170C|ME171C|\bME70C\b|ME581C|ME581CL|ME8510C|ME181C|P01Y|PO1MA|P01Z|\bP027\b', 'BlackBerryTablet' => 'PlayBook|RIM Tablet', 'HTCtablet' => 'HTC_Flyer_P512|HTC Flyer|HTC Jetstream|HTC-P715a|HTC EVO View 4G|PG41200|PG09410', 'MotorolaTablet' => 'xoom|sholest|MZ615|MZ605|MZ505|MZ601|MZ602|MZ603|MZ604|MZ606|MZ607|MZ608|MZ609|MZ615|MZ616|MZ617', 'NookTablet' => 'Android.*Nook|NookColor|nook browser|BNRV200|BNRV200A|BNTV250|BNTV250A|BNTV400|BNTV600|LogicPD Zoom2', // http://www.acer.ro/ac/ro/RO/content/drivers // http://www.packardbell.co.uk/pb/en/GB/content/download (Packard Bell is part of Acer) // http://us.acer.com/ac/en/US/content/group/tablets // http://www.acer.de/ac/de/DE/content/models/tablets/ // Can conflict with Micromax and Motorola phones codes. 'AcerTablet' => 'Android.*; \b(A100|A101|A110|A200|A210|A211|A500|A501|A510|A511|A700|A701|W500|W500P|W501|W501P|W510|W511|W700|G100|G100W|B1-A71|B1-710|B1-711|A1-810|A1-811|A1-830)\b|W3-810|\bA3-A10\b|\bA3-A11\b|\bA3-A20\b|\bA3-A30', // http://eu.computers.toshiba-europe.com/innovation/family/Tablets/1098744/banner_id/tablet_footerlink/ // http://us.toshiba.com/tablets/tablet-finder // http://www.toshiba.co.jp/regza/tablet/ 'ToshibaTablet' => 'Android.*(AT100|AT105|AT200|AT205|AT270|AT275|AT300|AT305|AT1S5|AT500|AT570|AT700|AT830)|TOSHIBA.*FOLIO', // http://www.nttdocomo.co.jp/english/service/developer/smart_phone/technical_info/spec/index.html // http://www.lg.com/us/tablets 'LGTablet' => '\bL-06C|LG-V909|LG-V900|LG-V700|LG-V510|LG-V500|LG-V410|LG-V400|LG-VK810\b', 'FujitsuTablet' => 'Android.*\b(F-01D|F-02F|F-05E|F-10D|M532|Q572)\b', // Prestigio Tablets http://www.prestigio.com/support 'PrestigioTablet' => 'PMP3170B|PMP3270B|PMP3470B|PMP7170B|PMP3370B|PMP3570C|PMP5870C|PMP3670B|PMP5570C|PMP5770D|PMP3970B|PMP3870C|PMP5580C|PMP5880D|PMP5780D|PMP5588C|PMP7280C|PMP7280C3G|PMP7280|PMP7880D|PMP5597D|PMP5597|PMP7100D|PER3464|PER3274|PER3574|PER3884|PER5274|PER5474|PMP5097CPRO|PMP5097|PMP7380D|PMP5297C|PMP5297C_QUAD|PMP812E|PMP812E3G|PMP812F|PMP810E|PMP880TD|PMT3017|PMT3037|PMT3047|PMT3057|PMT7008|PMT5887|PMT5001|PMT5002', // http://support.lenovo.com/en_GB/downloads/default.page?# 'LenovoTablet' => 'Lenovo TAB|Idea(Tab|Pad)( A1|A10| K1|)|ThinkPad([ ]+)?Tablet|YT3-850M|YT3-X90L|YT3-X90F|YT3-X90X|Lenovo.*(S2109|S2110|S5000|S6000|K3011|A3000|A3500|A1000|A2107|A2109|A1107|A5500|A7600|B6000|B8000|B8080)(-|)(FL|F|HV|H|)', // http://www.dell.com/support/home/us/en/04/Products/tab_mob/tablets 'DellTablet' => 'Venue 11|Venue 8|Venue 7|Dell Streak 10|Dell Streak 7', // http://www.yarvik.com/en/matrix/tablets/ 'YarvikTablet' => 'Android.*\b(TAB210|TAB211|TAB224|TAB250|TAB260|TAB264|TAB310|TAB360|TAB364|TAB410|TAB411|TAB420|TAB424|TAB450|TAB460|TAB461|TAB464|TAB465|TAB467|TAB468|TAB07-100|TAB07-101|TAB07-150|TAB07-151|TAB07-152|TAB07-200|TAB07-201-3G|TAB07-210|TAB07-211|TAB07-212|TAB07-214|TAB07-220|TAB07-400|TAB07-485|TAB08-150|TAB08-200|TAB08-201-3G|TAB08-201-30|TAB09-100|TAB09-211|TAB09-410|TAB10-150|TAB10-201|TAB10-211|TAB10-400|TAB10-410|TAB13-201|TAB274EUK|TAB275EUK|TAB374EUK|TAB462EUK|TAB474EUK|TAB9-200)\b', 'MedionTablet' => 'Android.*\bOYO\b|LIFE.*(P9212|P9514|P9516|S9512)|LIFETAB', 'ArnovaTablet' => '97G4|AN10G2|AN7bG3|AN7fG3|AN8G3|AN8cG3|AN7G3|AN9G3|AN7dG3|AN7dG3ST|AN7dG3ChildPad|AN10bG3|AN10bG3DT|AN9G2', // http://www.intenso.de/kategorie_en.php?kategorie=33 // @todo: http://www.nbhkdz.com/read/b8e64202f92a2df129126bff.html - investigate 'IntensoTablet' => 'INM8002KP|INM1010FP|INM805ND|Intenso Tab|TAB1004', // IRU.ru Tablets http://www.iru.ru/catalog/soho/planetable/ 'IRUTablet' => 'M702pro', 'MegafonTablet' => 'MegaFon V9|\bZTE V9\b|Android.*\bMT7A\b', // http://www.e-boda.ro/tablete-pc.html 'EbodaTablet' => 'E-Boda (Supreme|Impresspeed|Izzycomm|Essential)', // http://www.allview.ro/produse/droseries/lista-tablete-pc/ 'AllViewTablet' => 'Allview.*(Viva|Alldro|City|Speed|All TV|Frenzy|Quasar|Shine|TX1|AX1|AX2)', // http://wiki.archosfans.com/index.php?title=Main_Page // @note Rewrite the regex format after we add more UAs. 'ArchosTablet' => '\b(101G9|80G9|A101IT)\b|Qilive 97R|Archos5|\bARCHOS (70|79|80|90|97|101|FAMILYPAD|)(b|c|)(G10| Cobalt| TITANIUM(HD|)| Xenon| Neon|XSK| 2| XS 2| PLATINUM| CARBON|GAMEPAD)\b', // http://www.ainol.com/plugin.php?identifier=ainol&module=product 'AinolTablet' => 'NOVO7|NOVO8|NOVO10|Novo7Aurora|Novo7Basic|NOVO7PALADIN|novo9-Spark', 'NokiaLumiaTablet' => 'Lumia 2520', // @todo: inspect http://esupport.sony.com/US/p/select-system.pl?DIRECTOR=DRIVER // Readers http://www.atsuhiro-me.net/ebook/sony-reader/sony-reader-web-browser // http://www.sony.jp/support/tablet/ 'SonyTablet' => 'Sony.*Tablet|Xperia Tablet|Sony Tablet S|SO-03E|SGPT12|SGPT13|SGPT114|SGPT121|SGPT122|SGPT123|SGPT111|SGPT112|SGPT113|SGPT131|SGPT132|SGPT133|SGPT211|SGPT212|SGPT213|SGP311|SGP312|SGP321|EBRD1101|EBRD1102|EBRD1201|SGP351|SGP341|SGP511|SGP512|SGP521|SGP541|SGP551|SGP621|SGP612|SOT31', // http://www.support.philips.com/support/catalog/worldproducts.jsp?userLanguage=en&userCountry=cn&categoryid=3G_LTE_TABLET_SU_CN_CARE&title=3G%20tablets%20/%20LTE%20range&_dyncharset=UTF-8 'PhilipsTablet' => '\b(PI2010|PI3000|PI3100|PI3105|PI3110|PI3205|PI3210|PI3900|PI4010|PI7000|PI7100)\b', // db + http://www.cube-tablet.com/buy-products.html 'CubeTablet' => 'Android.*(K8GT|U9GT|U10GT|U16GT|U17GT|U18GT|U19GT|U20GT|U23GT|U30GT)|CUBE U8GT', // http://www.cobyusa.com/?p=pcat&pcat_id=3001 'CobyTablet' => 'MID1042|MID1045|MID1125|MID1126|MID7012|MID7014|MID7015|MID7034|MID7035|MID7036|MID7042|MID7048|MID7127|MID8042|MID8048|MID8127|MID9042|MID9740|MID9742|MID7022|MID7010', // http://www.match.net.cn/products.asp 'MIDTablet' => 'M9701|M9000|M9100|M806|M1052|M806|T703|MID701|MID713|MID710|MID727|MID760|MID830|MID728|MID933|MID125|MID810|MID732|MID120|MID930|MID800|MID731|MID900|MID100|MID820|MID735|MID980|MID130|MID833|MID737|MID960|MID135|MID860|MID736|MID140|MID930|MID835|MID733|MID4X10', // http://www.msi.com/support // @todo Research the Windows Tablets. 'MSITablet' => 'MSI \b(Primo 73K|Primo 73L|Primo 81L|Primo 77|Primo 93|Primo 75|Primo 76|Primo 73|Primo 81|Primo 91|Primo 90|Enjoy 71|Enjoy 7|Enjoy 10)\b', // @todo http://www.kyoceramobile.com/support/drivers/ // 'KyoceraTablet' => null, // @todo http://intexuae.com/index.php/category/mobile-devices/tablets-products/ // 'IntextTablet' => null, // http://pdadb.net/index.php?m=pdalist&list=SMiT (NoName Chinese Tablets) // http://www.imp3.net/14/show.php?itemid=20454 'SMiTTablet' => 'Android.*(\bMID\b|MID-560|MTV-T1200|MTV-PND531|MTV-P1101|MTV-PND530)', // http://www.rock-chips.com/index.php?do=prod&pid=2 'RockChipTablet' => 'Android.*(RK2818|RK2808A|RK2918|RK3066)|RK2738|RK2808A', // http://www.fly-phone.com/devices/tablets/ ; http://www.fly-phone.com/service/ 'FlyTablet' => 'IQ310|Fly Vision', // http://www.bqreaders.com/gb/tablets-prices-sale.html 'bqTablet' => 'Android.*(bq)?.*(Elcano|Curie|Edison|Maxwell|Kepler|Pascal|Tesla|Hypatia|Platon|Newton|Livingstone|Cervantes|Avant|Aquaris [E|M]10)|Maxwell.*Lite|Maxwell.*Plus', // http://www.huaweidevice.com/worldwide/productFamily.do?method=index&directoryId=5011&treeId=3290 // http://www.huaweidevice.com/worldwide/downloadCenter.do?method=index&directoryId=3372&treeId=0&tb=1&type=software (including legacy tablets) 'HuaweiTablet' => 'MediaPad|MediaPad 7 Youth|IDEOS S7|S7-201c|S7-202u|S7-101|S7-103|S7-104|S7-105|S7-106|S7-201|S7-Slim', // Nec or Medias Tab 'NecTablet' => '\bN-06D|\bN-08D', // Pantech Tablets: http://www.pantechusa.com/phones/ 'PantechTablet' => 'Pantech.*P4100', // Broncho Tablets: http://www.broncho.cn/ (hard to find) 'BronchoTablet' => 'Broncho.*(N701|N708|N802|a710)', // http://versusuk.com/support.html 'VersusTablet' => 'TOUCHPAD.*[78910]|\bTOUCHTAB\b', // http://www.zync.in/index.php/our-products/tablet-phablets 'ZyncTablet' => 'z1000|Z99 2G|z99|z930|z999|z990|z909|Z919|z900', // http://www.positivoinformatica.com.br/www/pessoal/tablet-ypy/ 'PositivoTablet' => 'TB07STA|TB10STA|TB07FTA|TB10FTA', // https://www.nabitablet.com/ 'NabiTablet' => 'Android.*\bNabi', 'KoboTablet' => 'Kobo Touch|\bK080\b|\bVox\b Build|\bArc\b Build', // French Danew Tablets http://www.danew.com/produits-tablette.php 'DanewTablet' => 'DSlide.*\b(700|701R|702|703R|704|802|970|971|972|973|974|1010|1012)\b', // Texet Tablets and Readers http://www.texet.ru/tablet/ 'TexetTablet' => 'NaviPad|TB-772A|TM-7045|TM-7055|TM-9750|TM-7016|TM-7024|TM-7026|TM-7041|TM-7043|TM-7047|TM-8041|TM-9741|TM-9747|TM-9748|TM-9751|TM-7022|TM-7021|TM-7020|TM-7011|TM-7010|TM-7023|TM-7025|TM-7037W|TM-7038W|TM-7027W|TM-9720|TM-9725|TM-9737W|TM-1020|TM-9738W|TM-9740|TM-9743W|TB-807A|TB-771A|TB-727A|TB-725A|TB-719A|TB-823A|TB-805A|TB-723A|TB-715A|TB-707A|TB-705A|TB-709A|TB-711A|TB-890HD|TB-880HD|TB-790HD|TB-780HD|TB-770HD|TB-721HD|TB-710HD|TB-434HD|TB-860HD|TB-840HD|TB-760HD|TB-750HD|TB-740HD|TB-730HD|TB-722HD|TB-720HD|TB-700HD|TB-500HD|TB-470HD|TB-431HD|TB-430HD|TB-506|TB-504|TB-446|TB-436|TB-416|TB-146SE|TB-126SE', // Avoid detecting 'PLAYSTATION 3' as mobile. 'PlaystationTablet' => 'Playstation.*(Portable|Vita)', // http://www.trekstor.de/surftabs.html 'TrekstorTablet' => 'ST10416-1|VT10416-1|ST70408-1|ST702xx-1|ST702xx-2|ST80208|ST97216|ST70104-2|VT10416-2|ST10216-2A|SurfTab', // http://www.pyleaudio.com/Products.aspx?%2fproducts%2fPersonal-Electronics%2fTablets 'PyleAudioTablet' => '\b(PTBL10CEU|PTBL10C|PTBL72BC|PTBL72BCEU|PTBL7CEU|PTBL7C|PTBL92BC|PTBL92BCEU|PTBL9CEU|PTBL9CUK|PTBL9C)\b', // http://www.advandigital.com/index.php?link=content-product&jns=JP001 // because of the short codenames we have to include whitespaces to reduce the possible conflicts. 'AdvanTablet' => 'Android.* \b(E3A|T3X|T5C|T5B|T3E|T3C|T3B|T1J|T1F|T2A|T1H|T1i|E1C|T1-E|T5-A|T4|E1-B|T2Ci|T1-B|T1-D|O1-A|E1-A|T1-A|T3A|T4i)\b ', // http://www.danytech.com/category/tablet-pc 'DanyTechTablet' => 'Genius Tab G3|Genius Tab S2|Genius Tab Q3|Genius Tab G4|Genius Tab Q4|Genius Tab G-II|Genius TAB GII|Genius TAB GIII|Genius Tab S1', // http://www.galapad.net/product.html 'GalapadTablet' => 'Android.*\bG1\b', // http://www.micromaxinfo.com/tablet/funbook 'MicromaxTablet' => 'Funbook|Micromax.*\b(P250|P560|P360|P362|P600|P300|P350|P500|P275)\b', // http://www.karbonnmobiles.com/products_tablet.php 'KarbonnTablet' => 'Android.*\b(A39|A37|A34|ST8|ST10|ST7|Smart Tab3|Smart Tab2)\b', // http://www.myallfine.com/Products.asp 'AllFineTablet' => 'Fine7 Genius|Fine7 Shine|Fine7 Air|Fine8 Style|Fine9 More|Fine10 Joy|Fine11 Wide', // http://www.proscanvideo.com/products-search.asp?itemClass=TABLET&itemnmbr= 'PROSCANTablet' => '\b(PEM63|PLT1023G|PLT1041|PLT1044|PLT1044G|PLT1091|PLT4311|PLT4311PL|PLT4315|PLT7030|PLT7033|PLT7033D|PLT7035|PLT7035D|PLT7044K|PLT7045K|PLT7045KB|PLT7071KG|PLT7072|PLT7223G|PLT7225G|PLT7777G|PLT7810K|PLT7849G|PLT7851G|PLT7852G|PLT8015|PLT8031|PLT8034|PLT8036|PLT8080K|PLT8082|PLT8088|PLT8223G|PLT8234G|PLT8235G|PLT8816K|PLT9011|PLT9045K|PLT9233G|PLT9735|PLT9760G|PLT9770G)\b', // http://www.yonesnav.com/products/products.php 'YONESTablet' => 'BQ1078|BC1003|BC1077|RK9702|BC9730|BC9001|IT9001|BC7008|BC7010|BC708|BC728|BC7012|BC7030|BC7027|BC7026', // http://www.cjshowroom.com/eproducts.aspx?classcode=004001001 // China manufacturer makes tablets for different small brands (eg. http://www.zeepad.net/index.html) 'ChangJiaTablet' => 'TPC7102|TPC7103|TPC7105|TPC7106|TPC7107|TPC7201|TPC7203|TPC7205|TPC7210|TPC7708|TPC7709|TPC7712|TPC7110|TPC8101|TPC8103|TPC8105|TPC8106|TPC8203|TPC8205|TPC8503|TPC9106|TPC9701|TPC97101|TPC97103|TPC97105|TPC97106|TPC97111|TPC97113|TPC97203|TPC97603|TPC97809|TPC97205|TPC10101|TPC10103|TPC10106|TPC10111|TPC10203|TPC10205|TPC10503', // http://www.gloryunion.cn/products.asp // http://www.allwinnertech.com/en/apply/mobile.html // http://www.ptcl.com.pk/pd_content.php?pd_id=284 (EVOTAB) // @todo: Softwiner tablets? // aka. Cute or Cool tablets. Not sure yet, must research to avoid collisions. 'GUTablet' => 'TX-A1301|TX-M9002|Q702|kf026', // A12R|D75A|D77|D79|R83|A95|A106C|R15|A75|A76|D71|D72|R71|R73|R77|D82|R85|D92|A97|D92|R91|A10F|A77F|W71F|A78F|W78F|W81F|A97F|W91F|W97F|R16G|C72|C73E|K72|K73|R96G // http://www.pointofview-online.com/showroom.php?shop_mode=product_listing&category_id=118 'PointOfViewTablet' => 'TAB-P506|TAB-navi-7-3G-M|TAB-P517|TAB-P-527|TAB-P701|TAB-P703|TAB-P721|TAB-P731N|TAB-P741|TAB-P825|TAB-P905|TAB-P925|TAB-PR945|TAB-PL1015|TAB-P1025|TAB-PI1045|TAB-P1325|TAB-PROTAB[0-9]+|TAB-PROTAB25|TAB-PROTAB26|TAB-PROTAB27|TAB-PROTAB26XL|TAB-PROTAB2-IPS9|TAB-PROTAB30-IPS9|TAB-PROTAB25XXL|TAB-PROTAB26-IPS10|TAB-PROTAB30-IPS10', // http://www.overmax.pl/pl/katalog-produktow,p8/tablety,c14/ // @todo: add more tests. 'OvermaxTablet' => 'OV-(SteelCore|NewBase|Basecore|Baseone|Exellen|Quattor|EduTab|Solution|ACTION|BasicTab|TeddyTab|MagicTab|Stream|TB-08|TB-09)', // http://hclmetablet.com/India/index.php 'HCLTablet' => 'HCL.*Tablet|Connect-3G-2.0|Connect-2G-2.0|ME Tablet U1|ME Tablet U2|ME Tablet G1|ME Tablet X1|ME Tablet Y2|ME Tablet Sync', // http://www.edigital.hu/Tablet_es_e-book_olvaso/Tablet-c18385.html 'DPSTablet' => 'DPS Dream 9|DPS Dual 7', // http://www.visture.com/index.asp 'VistureTablet' => 'V97 HD|i75 3G|Visture V4( HD)?|Visture V5( HD)?|Visture V10', // http://www.mijncresta.nl/tablet 'CrestaTablet' => 'CTP(-)?810|CTP(-)?818|CTP(-)?828|CTP(-)?838|CTP(-)?888|CTP(-)?978|CTP(-)?980|CTP(-)?987|CTP(-)?988|CTP(-)?989', // MediaTek - http://www.mediatek.com/_en/01_products/02_proSys.php?cata_sn=1&cata1_sn=1&cata2_sn=309 'MediatekTablet' => '\bMT8125|MT8389|MT8135|MT8377\b', // Concorde tab 'ConcordeTablet' => 'Concorde([ ]+)?Tab|ConCorde ReadMan', // GoClever Tablets - http://www.goclever.com/uk/products,c1/tablet,c5/ 'GoCleverTablet' => 'GOCLEVER TAB|A7GOCLEVER|M1042|M7841|M742|R1042BK|R1041|TAB A975|TAB A7842|TAB A741|TAB A741L|TAB M723G|TAB M721|TAB A1021|TAB I921|TAB R721|TAB I720|TAB T76|TAB R70|TAB R76.2|TAB R106|TAB R83.2|TAB M813G|TAB I721|GCTA722|TAB I70|TAB I71|TAB S73|TAB R73|TAB R74|TAB R93|TAB R75|TAB R76.1|TAB A73|TAB A93|TAB A93.2|TAB T72|TAB R83|TAB R974|TAB R973|TAB A101|TAB A103|TAB A104|TAB A104.2|R105BK|M713G|A972BK|TAB A971|TAB R974.2|TAB R104|TAB R83.3|TAB A1042', // Modecom Tablets - http://www.modecom.eu/tablets/portal/ 'ModecomTablet' => 'FreeTAB 9000|FreeTAB 7.4|FreeTAB 7004|FreeTAB 7800|FreeTAB 2096|FreeTAB 7.5|FreeTAB 1014|FreeTAB 1001 |FreeTAB 8001|FreeTAB 9706|FreeTAB 9702|FreeTAB 7003|FreeTAB 7002|FreeTAB 1002|FreeTAB 7801|FreeTAB 1331|FreeTAB 1004|FreeTAB 8002|FreeTAB 8014|FreeTAB 9704|FreeTAB 1003', // Vonino Tablets 'VoninoTablet' => '\b(Argus[ _]?S|Diamond[ _]?79HD|Emerald[ _]?78E|Luna[ _]?70C|Onyx[ _]?S|Onyx[ _]?Z|Orin[ _]?HD|Orin[ _]?S|Otis[ _]?S|SpeedStar[ _]?S|Magnet[ _]?M9|Primus[ _]?94[ _]?3G|Primus[ _]?94HD|Primus[ _]?QS|Android.*\bQ8\b|Sirius[ _]?EVO[ _]?QS|Sirius[ _]?QS|Spirit[ _]?S)\b', // ECS Tablets - http://www.ecs.com.tw/ECSWebSite/Product/Product_Tablet_List.aspx?CategoryID=14&MenuID=107&childid=M_107&LanID=0 'ECSTablet' => 'V07OT2|TM105A|S10OT1|TR10CS1', // Storex Tablets - http://storex.fr/espace_client/support.html // @note: no need to add all the tablet codes since they are guided by the first regex. 'StorexTablet' => 'eZee[_\']?(Tab|Go)[0-9]+|TabLC7|Looney Tunes Tab', // Generic Vodafone tablets. 'VodafoneTablet' => 'SmartTab([ ]+)?[0-9]+|SmartTabII10|SmartTabII7|VF-1497', // French tablets - Essentiel B http://www.boulanger.fr/tablette_tactile_e-book/tablette_tactile_essentiel_b/cl_68908.htm?multiChoiceToDelete=brand&mc_brand=essentielb // Aka: http://www.essentielb.fr/ 'EssentielBTablet' => 'Smart[ \']?TAB[ ]+?[0-9]+|Family[ \']?TAB2', // Ross & Moor - http://ross-moor.ru/ 'RossMoorTablet' => 'RM-790|RM-997|RMD-878G|RMD-974R|RMT-705A|RMT-701|RME-601|RMT-501|RMT-711', // i-mobile http://product.i-mobilephone.com/Mobile_Device 'iMobileTablet' => 'i-mobile i-note', // http://www.tolino.de/de/vergleichen/ 'TolinoTablet' => 'tolino tab [0-9.]+|tolino shine', // AudioSonic - a Kmart brand // http://www.kmart.com.au/webapp/wcs/stores/servlet/Search?langId=-1&storeId=10701&catalogId=10001&categoryId=193001&pageSize=72¤tPage=1&searchCategory=193001%2b4294965664&sortBy=p_MaxPrice%7c1 'AudioSonicTablet' => '\bC-22Q|T7-QC|T-17B|T-17P\b', // AMPE Tablets - http://www.ampe.com.my/product-category/tablets/ // @todo: add them gradually to avoid conflicts. 'AMPETablet' => 'Android.* A78 ', // Skk Mobile - http://skkmobile.com.ph/product_tablets.php 'SkkTablet' => 'Android.* (SKYPAD|PHOENIX|CYCLOPS)', // Tecno Mobile (only tablet) - http://www.tecno-mobile.com/index.php/product?filterby=smart&list_order=all&page=1 'TecnoTablet' => 'TECNO P9', // JXD (consoles & tablets) - http://jxd.hk/products.asp?selectclassid=009008&clsid=3 'JXDTablet' => 'Android.* \b(F3000|A3300|JXD5000|JXD3000|JXD2000|JXD300B|JXD300|S5800|S7800|S602b|S5110b|S7300|S5300|S602|S603|S5100|S5110|S601|S7100a|P3000F|P3000s|P101|P200s|P1000m|P200m|P9100|P1000s|S6600b|S908|P1000|P300|S18|S6600|S9100)\b', // i-Joy tablets - http://www.i-joy.es/en/cat/products/tablets/ 'iJoyTablet' => 'Tablet (Spirit 7|Essentia|Galatea|Fusion|Onix 7|Landa|Titan|Scooby|Deox|Stella|Themis|Argon|Unique 7|Sygnus|Hexen|Finity 7|Cream|Cream X2|Jade|Neon 7|Neron 7|Kandy|Scape|Saphyr 7|Rebel|Biox|Rebel|Rebel 8GB|Myst|Draco 7|Myst|Tab7-004|Myst|Tadeo Jones|Tablet Boing|Arrow|Draco Dual Cam|Aurix|Mint|Amity|Revolution|Finity 9|Neon 9|T9w|Amity 4GB Dual Cam|Stone 4GB|Stone 8GB|Andromeda|Silken|X2|Andromeda II|Halley|Flame|Saphyr 9,7|Touch 8|Planet|Triton|Unique 10|Hexen 10|Memphis 4GB|Memphis 8GB|Onix 10)', // http://www.intracon.eu/tablet 'FX2Tablet' => 'FX2 PAD7|FX2 PAD10', // http://www.xoro.de/produkte/ // @note: Might be the same brand with 'Simply tablets' 'XoroTablet' => 'KidsPAD 701|PAD[ ]?712|PAD[ ]?714|PAD[ ]?716|PAD[ ]?717|PAD[ ]?718|PAD[ ]?720|PAD[ ]?721|PAD[ ]?722|PAD[ ]?790|PAD[ ]?792|PAD[ ]?900|PAD[ ]?9715D|PAD[ ]?9716DR|PAD[ ]?9718DR|PAD[ ]?9719QR|PAD[ ]?9720QR|TelePAD1030|Telepad1032|TelePAD730|TelePAD731|TelePAD732|TelePAD735Q|TelePAD830|TelePAD9730|TelePAD795|MegaPAD 1331|MegaPAD 1851|MegaPAD 2151', // http://www1.viewsonic.com/products/computing/tablets/ 'ViewsonicTablet' => 'ViewPad 10pi|ViewPad 10e|ViewPad 10s|ViewPad E72|ViewPad7|ViewPad E100|ViewPad 7e|ViewSonic VB733|VB100a', // http://www.odys.de/web/internet-tablet_en.html 'OdysTablet' => 'LOOX|XENO10|ODYS[ -](Space|EVO|Xpress|NOON)|\bXELIO\b|Xelio10Pro|XELIO7PHONETAB|XELIO10EXTREME|XELIOPT2|NEO_QUAD10', // http://www.captiva-power.de/products.html#tablets-en 'CaptivaTablet' => 'CAPTIVA PAD', // IconBIT - http://www.iconbit.com/products/tablets/ 'IconbitTablet' => 'NetTAB|NT-3702|NT-3702S|NT-3702S|NT-3603P|NT-3603P|NT-0704S|NT-0704S|NT-3805C|NT-3805C|NT-0806C|NT-0806C|NT-0909T|NT-0909T|NT-0907S|NT-0907S|NT-0902S|NT-0902S', // http://www.teclast.com/topic.php?channelID=70&topicID=140&pid=63 'TeclastTablet' => 'T98 4G|\bP80\b|\bX90HD\b|X98 Air|X98 Air 3G|\bX89\b|P80 3G|\bX80h\b|P98 Air|\bX89HD\b|P98 3G|\bP90HD\b|P89 3G|X98 3G|\bP70h\b|P79HD 3G|G18d 3G|\bP79HD\b|\bP89s\b|\bA88\b|\bP10HD\b|\bP19HD\b|G18 3G|\bP78HD\b|\bA78\b|\bP75\b|G17s 3G|G17h 3G|\bP85t\b|\bP90\b|\bP11\b|\bP98t\b|\bP98HD\b|\bG18d\b|\bP85s\b|\bP11HD\b|\bP88s\b|\bA80HD\b|\bA80se\b|\bA10h\b|\bP89\b|\bP78s\b|\bG18\b|\bP85\b|\bA70h\b|\bA70\b|\bG17\b|\bP18\b|\bA80s\b|\bA11s\b|\bP88HD\b|\bA80h\b|\bP76s\b|\bP76h\b|\bP98\b|\bA10HD\b|\bP78\b|\bP88\b|\bA11\b|\bA10t\b|\bP76a\b|\bP76t\b|\bP76e\b|\bP85HD\b|\bP85a\b|\bP86\b|\bP75HD\b|\bP76v\b|\bA12\b|\bP75a\b|\bA15\b|\bP76Ti\b|\bP81HD\b|\bA10\b|\bT760VE\b|\bT720HD\b|\bP76\b|\bP73\b|\bP71\b|\bP72\b|\bT720SE\b|\bC520Ti\b|\bT760\b|\bT720VE\b|T720-3GE|T720-WiFi', // Onda - http://www.onda-tablet.com/buy-android-onda.html?dir=desc&limit=all&order=price 'OndaTablet' => '\b(V975i|Vi30|VX530|V701|Vi60|V701s|Vi50|V801s|V719|Vx610w|VX610W|V819i|Vi10|VX580W|Vi10|V711s|V813|V811|V820w|V820|Vi20|V711|VI30W|V712|V891w|V972|V819w|V820w|Vi60|V820w|V711|V813s|V801|V819|V975s|V801|V819|V819|V818|V811|V712|V975m|V101w|V961w|V812|V818|V971|V971s|V919|V989|V116w|V102w|V973|Vi40)\b[\s]+', 'JaytechTablet' => 'TPC-PA762', 'BlaupunktTablet' => 'Endeavour 800NG|Endeavour 1010', // http://www.digma.ru/support/download/ // @todo: Ebooks also (if requested) 'DigmaTablet' => '\b(iDx10|iDx9|iDx8|iDx7|iDxD7|iDxD8|iDsQ8|iDsQ7|iDsQ8|iDsD10|iDnD7|3TS804H|iDsQ11|iDj7|iDs10)\b', // http://www.evolioshop.com/ro/tablete-pc.html // http://www.evolio.ro/support/downloads_static.html?cat=2 // @todo: Research some more 'EvolioTablet' => 'ARIA_Mini_wifi|Aria[ _]Mini|Evolio X10|Evolio X7|Evolio X8|\bEvotab\b|\bNeura\b', // @todo http://www.lavamobiles.com/tablets-data-cards 'LavaTablet' => 'QPAD E704|\bIvoryS\b|E-TAB IVORY|\bE-TAB\b', // http://www.breezetablet.com/ 'AocTablet' => 'MW0811|MW0812|MW0922|MTK8382|MW1031|MW0831|MW0821|MW0931|MW0712', // http://www.mpmaneurope.com/en/products/internet-tablets-14/android-tablets-14/ 'MpmanTablet' => 'MP11 OCTA|MP10 OCTA|MPQC1114|MPQC1004|MPQC994|MPQC974|MPQC973|MPQC804|MPQC784|MPQC780|\bMPG7\b|MPDCG75|MPDCG71|MPDC1006|MP101DC|MPDC9000|MPDC905|MPDC706HD|MPDC706|MPDC705|MPDC110|MPDC100|MPDC99|MPDC97|MPDC88|MPDC8|MPDC77|MP709|MID701|MID711|MID170|MPDC703|MPQC1010', // https://www.celkonmobiles.com/?_a=categoryphones&sid=2 'CelkonTablet' => 'CT695|CT888|CT[\s]?910|CT7 Tab|CT9 Tab|CT3 Tab|CT2 Tab|CT1 Tab|C820|C720|\bCT-1\b', // http://www.wolderelectronics.com/productos/manuales-y-guias-rapidas/categoria-2-miTab 'WolderTablet' => 'miTab \b(DIAMOND|SPACE|BROOKLYN|NEO|FLY|MANHATTAN|FUNK|EVOLUTION|SKY|GOCAR|IRON|GENIUS|POP|MINT|EPSILON|BROADWAY|JUMP|HOP|LEGEND|NEW AGE|LINE|ADVANCE|FEEL|FOLLOW|LIKE|LINK|LIVE|THINK|FREEDOM|CHICAGO|CLEVELAND|BALTIMORE-GH|IOWA|BOSTON|SEATTLE|PHOENIX|DALLAS|IN 101|MasterChef)\b', // http://www.mi.com/en 'MiTablet' => '\bMI PAD\b|\bHM NOTE 1W\b', // http://www.nbru.cn/index.html 'NibiruTablet' => 'Nibiru M1|Nibiru Jupiter One', // http://navroad.com/products/produkty/tablety/ // http://navroad.com/products/produkty/tablety/ 'NexoTablet' => 'NEXO NOVA|NEXO 10|NEXO AVIO|NEXO FREE|NEXO GO|NEXO EVO|NEXO 3G|NEXO SMART|NEXO KIDDO|NEXO MOBI', // http://leader-online.com/new_site/product-category/tablets/ // http://www.leader-online.net.au/List/Tablet 'LeaderTablet' => 'TBLT10Q|TBLT10I|TBL-10WDKB|TBL-10WDKBO2013|TBL-W230V2|TBL-W450|TBL-W500|SV572|TBLT7I|TBA-AC7-8G|TBLT79|TBL-8W16|TBL-10W32|TBL-10WKB|TBL-W100', // http://www.datawind.com/ubislate/ 'UbislateTablet' => 'UbiSlate[\s]?7C', // http://www.pocketbook-int.com/ru/support 'PocketBookTablet' => 'Pocketbook', // http://www.kocaso.com/product_tablet.html 'KocasoTablet' => '\b(TB-1207)\b', // http://global.hisense.com/product/asia/tablet/Sero7/201412/t20141215_91832.htm 'HisenseTablet' => '\b(F5281|E2371)\b', // http://www.tesco.com/direct/hudl/ 'Hudl' => 'Hudl HT7S3|Hudl 2', // http://www.telstra.com.au/home-phone/thub-2/ 'TelstraTablet' => 'T-Hub2', 'GenericTablet' => 'Android.*\b97D\b|Tablet(?!.*PC)|BNTV250A|MID-WCDMA|LogicPD Zoom2|\bA7EB\b|CatNova8|A1_07|CT704|CT1002|\bM721\b|rk30sdk|\bEVOTAB\b|M758A|ET904|ALUMIUM10|Smartfren Tab|Endeavour 1010|Tablet-PC-4|Tagi Tab|\bM6pro\b|CT1020W|arc 10HD|\bTP750\b|\bQTAQZ3\b' ); /** * List of mobile Operating Systems. * * @var array */ protected static $operatingSystems = array( 'AndroidOS' => 'Android', 'BlackBerryOS' => 'blackberry|\bBB10\b|rim tablet os', 'PalmOS' => 'PalmOS|avantgo|blazer|elaine|hiptop|palm|plucker|xiino', 'SymbianOS' => 'Symbian|SymbOS|Series60|Series40|SYB-[0-9]+|\bS60\b', // @reference: http://en.wikipedia.org/wiki/Windows_Mobile 'WindowsMobileOS' => 'Windows CE.*(PPC|Smartphone|Mobile|[0-9]{3}x[0-9]{3})|Window Mobile|Windows Phone [0-9.]+|WCE;', // @reference: http://en.wikipedia.org/wiki/Windows_Phone // http://wifeng.cn/?r=blog&a=view&id=106 // http://nicksnettravels.builttoroam.com/post/2011/01/10/Bogus-Windows-Phone-7-User-Agent-String.aspx // http://msdn.microsoft.com/library/ms537503.aspx // https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx 'WindowsPhoneOS' => 'Windows Phone 10.0|Windows Phone 8.1|Windows Phone 8.0|Windows Phone OS|XBLWP7|ZuneWP7|Windows NT 6.[23]; ARM;', 'iOS' => '\biPhone.*Mobile|\biPod|\biPad', // http://en.wikipedia.org/wiki/MeeGo // @todo: research MeeGo in UAs 'MeeGoOS' => 'MeeGo', // http://en.wikipedia.org/wiki/Maemo // @todo: research Maemo in UAs 'MaemoOS' => 'Maemo', 'JavaOS' => 'J2ME/|\bMIDP\b|\bCLDC\b', // '|Java/' produces bug #135 'webOS' => 'webOS|hpwOS', 'badaOS' => '\bBada\b', 'BREWOS' => 'BREW', ); /** * List of mobile User Agents. * * IMPORTANT: This is a list of only mobile browsers. * Mobile Detect 2.x supports only mobile browsers, * it was never designed to detect all browsers. * The change will come in 2017 in the 3.x release for PHP7. * * @var array */ protected static $browsers = array( //'Vivaldi' => 'Vivaldi', // @reference: https://developers.google.com/chrome/mobile/docs/user-agent 'Chrome' => '\bCrMo\b|CriOS|Android.*Chrome/[.0-9]* (Mobile)?', 'Dolfin' => '\bDolfin\b', 'Opera' => 'Opera.*Mini|Opera.*Mobi|Android.*Opera|Mobile.*OPR/[0-9.]+|Coast/[0-9.]+', 'Skyfire' => 'Skyfire', 'Edge' => 'Mobile Safari/[.0-9]* Edge', 'IE' => 'IEMobile|MSIEMobile', // |Trident/[.0-9]+ 'Firefox' => 'fennec|firefox.*maemo|(Mobile|Tablet).*Firefox|Firefox.*Mobile|FxiOS', 'Bolt' => 'bolt', 'TeaShark' => 'teashark', 'Blazer' => 'Blazer', // @reference: http://developer.apple.com/library/safari/#documentation/AppleApplications/Reference/SafariWebContent/OptimizingforSafarioniPhone/OptimizingforSafarioniPhone.html#//apple_ref/doc/uid/TP40006517-SW3 'Safari' => 'Version.*Mobile.*Safari|Safari.*Mobile|MobileSafari', // http://en.wikipedia.org/wiki/Midori_(web_browser) //'Midori' => 'midori', //'Tizen' => 'Tizen', 'UCBrowser' => 'UC.*Browser|UCWEB', 'baiduboxapp' => 'baiduboxapp', 'baidubrowser' => 'baidubrowser', // https://github.com/serbanghita/Mobile-Detect/issues/7 'DiigoBrowser' => 'DiigoBrowser', // http://www.puffinbrowser.com/index.php 'Puffin' => 'Puffin', // http://mercury-browser.com/index.html 'Mercury' => '\bMercury\b', // http://en.wikipedia.org/wiki/Obigo_Browser 'ObigoBrowser' => 'Obigo', // http://en.wikipedia.org/wiki/NetFront 'NetFront' => 'NF-Browser', // @reference: http://en.wikipedia.org/wiki/Minimo // http://en.wikipedia.org/wiki/Vision_Mobile_Browser 'GenericBrowser' => 'NokiaBrowser|OviBrowser|OneBrowser|TwonkyBeamBrowser|SEMC.*Browser|FlyFlow|Minimo|NetFront|Novarra-Vision|MQQBrowser|MicroMessenger', // @reference: https://en.wikipedia.org/wiki/Pale_Moon_(web_browser) 'PaleMoon' => 'Android.*PaleMoon|Mobile.*PaleMoon', ); /** * Utilities. * * @var array */ protected static $utilities = array( // Experimental. When a mobile device wants to switch to 'Desktop Mode'. // http://scottcate.com/technology/windows-phone-8-ie10-desktop-or-mobile/ // https://github.com/serbanghita/Mobile-Detect/issues/57#issuecomment-15024011 // https://developers.facebook.com/docs/sharing/best-practices 'Bot' => 'Googlebot|facebookexternalhit|AdsBot-Google|Google Keyword Suggestion|Facebot|YandexBot|YandexMobileBot|bingbot|ia_archiver|AhrefsBot|Ezooms|GSLFbot|WBSearchBot|Twitterbot|TweetmemeBot|Twikle|PaperLiBot|Wotbox|UnwindFetchor|Exabot|MJ12bot|YandexImages|TurnitinBot|Pingdom', 'MobileBot' => 'Googlebot-Mobile|AdsBot-Google-Mobile|YahooSeeker/M1A1-R2D2', 'DesktopMode' => 'WPDesktop', 'TV' => 'SonyDTV|HbbTV', // experimental 'WebKit' => '(webkit)[ /]([\w.]+)', // @todo: Include JXD consoles. 'Console' => '\b(Nintendo|Nintendo WiiU|Nintendo 3DS|PLAYSTATION|Xbox)\b', 'Watch' => 'SM-V700', ); /** * All possible HTTP headers that represent the * User-Agent string. * * @var array */ protected static $uaHttpHeaders = array( // The default User-Agent string. 'HTTP_USER_AGENT', // Header can occur on devices using Opera Mini. 'HTTP_X_OPERAMINI_PHONE_UA', // Vodafone specific header: http://www.seoprinciple.com/mobile-web-community-still-angry-at-vodafone/24/ 'HTTP_X_DEVICE_USER_AGENT', 'HTTP_X_ORIGINAL_USER_AGENT', 'HTTP_X_SKYFIRE_PHONE', 'HTTP_X_BOLT_PHONE_UA', 'HTTP_DEVICE_STOCK_UA', 'HTTP_X_UCBROWSER_DEVICE_UA' ); /** * The individual segments that could exist in a User-Agent string. VER refers to the regular * expression defined in the constant self::VER. * * @var array */ protected static $properties = array( // Build 'Mobile' => 'Mobile/[VER]', 'Build' => 'Build/[VER]', 'Version' => 'Version/[VER]', 'VendorID' => 'VendorID/[VER]', // Devices 'iPad' => 'iPad.*CPU[a-z ]+[VER]', 'iPhone' => 'iPhone.*CPU[a-z ]+[VER]', 'iPod' => 'iPod.*CPU[a-z ]+[VER]', //'BlackBerry' => array('BlackBerry[VER]', 'BlackBerry [VER];'), 'Kindle' => 'Kindle/[VER]', // Browser 'Chrome' => array('Chrome/[VER]', 'CriOS/[VER]', 'CrMo/[VER]'), 'Coast' => array('Coast/[VER]'), 'Dolfin' => 'Dolfin/[VER]', // @reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox 'Firefox' => array('Firefox/[VER]', 'FxiOS/[VER]'), 'Fennec' => 'Fennec/[VER]', // http://msdn.microsoft.com/en-us/library/ms537503(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/ie/hh869301(v=vs.85).aspx 'Edge' => 'Edge/[VER]', 'IE' => array('IEMobile/[VER];', 'IEMobile [VER]', 'MSIE [VER];', 'Trident/[0-9.]+;.*rv:[VER]'), // http://en.wikipedia.org/wiki/NetFront 'NetFront' => 'NetFront/[VER]', 'NokiaBrowser' => 'NokiaBrowser/[VER]', 'Opera' => array( ' OPR/[VER]', 'Opera Mini/[VER]', 'Version/[VER]' ), 'Opera Mini' => 'Opera Mini/[VER]', 'Opera Mobi' => 'Version/[VER]', 'UC Browser' => 'UC Browser[VER]', 'MQQBrowser' => 'MQQBrowser/[VER]', 'MicroMessenger' => 'MicroMessenger/[VER]', 'baiduboxapp' => 'baiduboxapp/[VER]', 'baidubrowser' => 'baidubrowser/[VER]', 'SamsungBrowser' => 'SamsungBrowser/[VER]', 'Iron' => 'Iron/[VER]', // @note: Safari 7534.48.3 is actually Version 5.1. // @note: On BlackBerry the Version is overwriten by the OS. 'Safari' => array( 'Version/[VER]', 'Safari/[VER]' ), 'Skyfire' => 'Skyfire/[VER]', 'Tizen' => 'Tizen/[VER]', 'Webkit' => 'webkit[ /][VER]', 'PaleMoon' => 'PaleMoon/[VER]', // Engine 'Gecko' => 'Gecko/[VER]', 'Trident' => 'Trident/[VER]', 'Presto' => 'Presto/[VER]', 'Goanna' => 'Goanna/[VER]', // OS 'iOS' => ' \bi?OS\b [VER][ ;]{1}', 'Android' => 'Android [VER]', 'BlackBerry' => array('BlackBerry[\w]+/[VER]', 'BlackBerry.*Version/[VER]', 'Version/[VER]'), 'BREW' => 'BREW [VER]', 'Java' => 'Java/[VER]', // @reference: http://windowsteamblog.com/windows_phone/b/wpdev/archive/2011/08/29/introducing-the-ie9-on-windows-phone-mango-user-agent-string.aspx // @reference: http://en.wikipedia.org/wiki/Windows_NT#Releases 'Windows Phone OS' => array( 'Windows Phone OS [VER]', 'Windows Phone [VER]'), 'Windows Phone' => 'Windows Phone [VER]', 'Windows CE' => 'Windows CE/[VER]', // http://social.msdn.microsoft.com/Forums/en-US/windowsdeveloperpreviewgeneral/thread/6be392da-4d2f-41b4-8354-8dcee20c85cd 'Windows NT' => 'Windows NT [VER]', 'Symbian' => array('SymbianOS/[VER]', 'Symbian/[VER]'), 'webOS' => array('webOS/[VER]', 'hpwOS/[VER];'), ); /** * Construct an instance of this class. * * @param array $headers Specify the headers as injection. Should be PHP _SERVER flavored. * If left empty, will use the global _SERVER['HTTP_*'] vars instead. * @param string $userAgent Inject the User-Agent header. If null, will use HTTP_USER_AGENT * from the $headers array instead. */ public function __construct( array $headers = null, $userAgent = null ) { $this->setHttpHeaders($headers); $this->setUserAgent($userAgent); } /** * Get the current script version. * This is useful for the demo.php file, * so people can check on what version they are testing * for mobile devices. * * @return string The version number in semantic version format. */ public static function getScriptVersion() { return self::VERSION; } /** * Set the HTTP Headers. Must be PHP-flavored. This method will reset existing headers. * * @param array $httpHeaders The headers to set. If null, then using PHP's _SERVER to extract * the headers. The default null is left for backwards compatibility. */ public function setHttpHeaders($httpHeaders = null) { // use global _SERVER if $httpHeaders aren't defined if (!is_array($httpHeaders) || !count($httpHeaders)) { $httpHeaders = $_SERVER; } // clear existing headers $this->httpHeaders = array(); // Only save HTTP headers. In PHP land, that means only _SERVER vars that // start with HTTP_. foreach ($httpHeaders as $key => $value) { if (substr($key, 0, 5) === 'HTTP_') { $this->httpHeaders[$key] = $value; } } // In case we're dealing with CloudFront, we need to know. $this->setCfHeaders($httpHeaders); } /** * Retrieves the HTTP headers. * * @return array */ public function getHttpHeaders() { return $this->httpHeaders; } /** * Retrieves a particular header. If it doesn't exist, no exception/error is caused. * Simply null is returned. * * @param string $header The name of the header to retrieve. Can be HTTP compliant such as * "User-Agent" or "X-Device-User-Agent" or can be php-esque with the * all-caps, HTTP_ prefixed, underscore seperated awesomeness. * * @return string|null The value of the header. */ public function getHttpHeader($header) { // are we using PHP-flavored headers? if (strpos($header, '_') === false) { $header = str_replace('-', '_', $header); $header = strtoupper($header); } // test the alternate, too $altHeader = 'HTTP_' . $header; //Test both the regular and the HTTP_ prefix if (isset($this->httpHeaders[$header])) { return $this->httpHeaders[$header]; } elseif (isset($this->httpHeaders[$altHeader])) { return $this->httpHeaders[$altHeader]; } return null; } public function getMobileHeaders() { return self::$mobileHeaders; } /** * Get all possible HTTP headers that * can contain the User-Agent string. * * @return array List of HTTP headers. */ public function getUaHttpHeaders() { return self::$uaHttpHeaders; } /** * Set CloudFront headers * http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-device * * @param array $cfHeaders List of HTTP headers * * @return boolean If there were CloudFront headers to be set */ public function setCfHeaders($cfHeaders = null) { // use global _SERVER if $cfHeaders aren't defined if (!is_array($cfHeaders) || !count($cfHeaders)) { $cfHeaders = $_SERVER; } // clear existing headers $this->cloudfrontHeaders = array(); // Only save CLOUDFRONT headers. In PHP land, that means only _SERVER vars that // start with cloudfront-. $response = false; foreach ($cfHeaders as $key => $value) { if (substr(strtolower($key), 0, 16) === 'http_cloudfront_') { $this->cloudfrontHeaders[strtoupper($key)] = $value; $response = true; } } return $response; } /** * Retrieves the cloudfront headers. * * @return array */ public function getCfHeaders() { return $this->cloudfrontHeaders; } /** * @param string $userAgent * @return string */ private function prepareUserAgent($userAgent) { $userAgent = trim($userAgent); $userAgent = substr($userAgent, 0, 500); return $userAgent; } /** * Set the User-Agent to be used. * * @param string $userAgent The user agent string to set. * * @return string|null */ public function setUserAgent($userAgent = null) { // Invalidate cache due to #375 $this->cache = array(); if (false === empty($userAgent)) { return $this->userAgent = $this->prepareUserAgent($userAgent); } else { $this->userAgent = null; foreach ($this->getUaHttpHeaders() as $altHeader) { if (false === empty($this->httpHeaders[$altHeader])) { // @todo: should use getHttpHeader(), but it would be slow. (Serban) $this->userAgent .= $this->httpHeaders[$altHeader] . " "; } } if (!empty($this->userAgent)) { return $this->userAgent = $this->prepareUserAgent($this->userAgent); } } if (count($this->getCfHeaders()) > 0) { return $this->userAgent = 'Amazon CloudFront'; } return $this->userAgent = null; } /** * Retrieve the User-Agent. * * @return string|null The user agent if it's set. */ public function getUserAgent() { return $this->userAgent; } /** * Set the detection type. Must be one of self::DETECTION_TYPE_MOBILE or * self::DETECTION_TYPE_EXTENDED. Otherwise, nothing is set. * * @deprecated since version 2.6.9 * * @param string $type The type. Must be a self::DETECTION_TYPE_* constant. The default * parameter is null which will default to self::DETECTION_TYPE_MOBILE. */ public function setDetectionType($type = null) { if ($type === null) { $type = self::DETECTION_TYPE_MOBILE; } if ($type !== self::DETECTION_TYPE_MOBILE && $type !== self::DETECTION_TYPE_EXTENDED) { return; } $this->detectionType = $type; } public function getMatchingRegex() { return $this->matchingRegex; } public function getMatchesArray() { return $this->matchesArray; } /** * Retrieve the list of known phone devices. * * @return array List of phone devices. */ public static function getPhoneDevices() { return self::$phoneDevices; } /** * Retrieve the list of known tablet devices. * * @return array List of tablet devices. */ public static function getTabletDevices() { return self::$tabletDevices; } /** * Alias for getBrowsers() method. * * @return array List of user agents. */ public static function getUserAgents() { return self::getBrowsers(); } /** * Retrieve the list of known browsers. Specifically, the user agents. * * @return array List of browsers / user agents. */ public static function getBrowsers() { return self::$browsers; } /** * Retrieve the list of known utilities. * * @return array List of utilities. */ public static function getUtilities() { return self::$utilities; } /** * Method gets the mobile detection rules. This method is used for the magic methods $detect->is*(). * * @deprecated since version 2.6.9 * * @return array All the rules (but not extended). */ public static function getMobileDetectionRules() { static $rules; if (!$rules) { $rules = array_merge( self::$phoneDevices, self::$tabletDevices, self::$operatingSystems, self::$browsers ); } return $rules; } /** * Method gets the mobile detection rules + utilities. * The reason this is separate is because utilities rules * don't necessary imply mobile. This method is used inside * the new $detect->is('stuff') method. * * @deprecated since version 2.6.9 * * @return array All the rules + extended. */ public function getMobileDetectionRulesExtended() { static $rules; if (!$rules) { // Merge all rules together. $rules = array_merge( self::$phoneDevices, self::$tabletDevices, self::$operatingSystems, self::$browsers, self::$utilities ); } return $rules; } /** * Retrieve the current set of rules. * * @deprecated since version 2.6.9 * * @return array */ public function getRules() { if ($this->detectionType == self::DETECTION_TYPE_EXTENDED) { return self::getMobileDetectionRulesExtended(); } else { return self::getMobileDetectionRules(); } } /** * Retrieve the list of mobile operating systems. * * @return array The list of mobile operating systems. */ public static function getOperatingSystems() { return self::$operatingSystems; } /** * Check the HTTP headers for signs of mobile. * This is the fastest mobile check possible; it's used * inside isMobile() method. * * @return bool */ public function checkHttpHeadersForMobile() { foreach ($this->getMobileHeaders() as $mobileHeader => $matchType) { if (isset($this->httpHeaders[$mobileHeader])) { if (is_array($matchType['matches'])) { foreach ($matchType['matches'] as $_match) { if (strpos($this->httpHeaders[$mobileHeader], $_match) !== false) { return true; } } return false; } else { return true; } } } return false; } /** * Magic overloading method. * * @method boolean is[...]() * @param string $name * @param array $arguments * @return mixed * @throws BadMethodCallException when the method doesn't exist and doesn't start with 'is' */ public function __call($name, $arguments) { // make sure the name starts with 'is', otherwise if (substr($name, 0, 2) !== 'is') { throw new BadMethodCallException("No such method exists: $name"); } $this->setDetectionType(self::DETECTION_TYPE_MOBILE); $key = substr($name, 2); return $this->matchUAAgainstKey($key); } /** * Find a detection rule that matches the current User-agent. * * @param null $userAgent deprecated * @return boolean */ protected function matchDetectionRulesAgainstUA($userAgent = null) { // Begin general search. foreach ($this->getRules() as $_regex) { if (empty($_regex)) { continue; } if ($this->match($_regex, $userAgent)) { return true; } } return false; } /** * Search for a certain key in the rules array. * If the key is found then try to match the corresponding * regex against the User-Agent. * * @param string $key * * @return boolean */ protected function matchUAAgainstKey($key) { // Make the keys lowercase so we can match: isIphone(), isiPhone(), isiphone(), etc. $key = strtolower($key); if (false === isset($this->cache[$key])) { // change the keys to lower case $_rules = array_change_key_case($this->getRules()); if (false === empty($_rules[$key])) { $this->cache[$key] = $this->match($_rules[$key]); } if (false === isset($this->cache[$key])) { $this->cache[$key] = false; } } return $this->cache[$key]; } /** * Check if the device is mobile. * Returns true if any type of mobile device detected, including special ones * @param null $userAgent deprecated * @param null $httpHeaders deprecated * @return bool */ public function isMobile($userAgent = null, $httpHeaders = null) { if ($httpHeaders) { $this->setHttpHeaders($httpHeaders); } if ($userAgent) { $this->setUserAgent($userAgent); } // Check specifically for cloudfront headers if the useragent === 'Amazon CloudFront' if ($this->getUserAgent() === 'Amazon CloudFront') { $cfHeaders = $this->getCfHeaders(); if(array_key_exists('HTTP_CLOUDFRONT_IS_MOBILE_VIEWER', $cfHeaders) && $cfHeaders['HTTP_CLOUDFRONT_IS_MOBILE_VIEWER'] === 'true') { return true; } } $this->setDetectionType(self::DETECTION_TYPE_MOBILE); if ($this->checkHttpHeadersForMobile()) { return true; } else { return $this->matchDetectionRulesAgainstUA(); } } /** * Check if the device is a tablet. * Return true if any type of tablet device is detected. * * @param string $userAgent deprecated * @param array $httpHeaders deprecated * @return bool */ public function isTablet($userAgent = null, $httpHeaders = null) { // Check specifically for cloudfront headers if the useragent === 'Amazon CloudFront' if ($this->getUserAgent() === 'Amazon CloudFront') { $cfHeaders = $this->getCfHeaders(); if(array_key_exists('HTTP_CLOUDFRONT_IS_TABLET_VIEWER', $cfHeaders) && $cfHeaders['HTTP_CLOUDFRONT_IS_TABLET_VIEWER'] === 'true') { return true; } } $this->setDetectionType(self::DETECTION_TYPE_MOBILE); foreach (self::$tabletDevices as $_regex) { if ($this->match($_regex, $userAgent)) { return true; } } return false; } /** * This method checks for a certain property in the * userAgent. * @todo: The httpHeaders part is not yet used. * * @param string $key * @param string $userAgent deprecated * @param string $httpHeaders deprecated * @return bool|int|null */ public function is($key, $userAgent = null, $httpHeaders = null) { // Set the UA and HTTP headers only if needed (eg. batch mode). if ($httpHeaders) { $this->setHttpHeaders($httpHeaders); } if ($userAgent) { $this->setUserAgent($userAgent); } $this->setDetectionType(self::DETECTION_TYPE_EXTENDED); return $this->matchUAAgainstKey($key); } /** * Some detection rules are relative (not standard), * because of the diversity of devices, vendors and * their conventions in representing the User-Agent or * the HTTP headers. * * This method will be used to check custom regexes against * the User-Agent string. * * @param $regex * @param string $userAgent * @return bool * * @todo: search in the HTTP headers too. */ public function match($regex, $userAgent = null) { $match = (bool) preg_match(sprintf('#%s#is', $regex), (false === empty($userAgent) ? $userAgent : $this->userAgent), $matches); // If positive match is found, store the results for debug. if ($match) { $this->matchingRegex = $regex; $this->matchesArray = $matches; } return $match; } /** * Get the properties array. * * @return array */ public static function getProperties() { return self::$properties; } /** * Prepare the version number. * * @todo Remove the error supression from str_replace() call. * * @param string $ver The string version, like "2.6.21.2152"; * * @return float */ public function prepareVersionNo($ver) { $ver = str_replace(array('_', ' ', '/'), '.', $ver); $arrVer = explode('.', $ver, 2); if (isset($arrVer[1])) { $arrVer[1] = @str_replace('.', '', $arrVer[1]); // @todo: treat strings versions. } return (float) implode('.', $arrVer); } /** * Check the version of the given property in the User-Agent. * Will return a float number. (eg. 2_0 will return 2.0, 4.3.1 will return 4.31) * * @param string $propertyName The name of the property. See self::getProperties() array * keys for all possible properties. * @param string $type Either self::VERSION_TYPE_STRING to get a string value or * self::VERSION_TYPE_FLOAT indicating a float value. This parameter * is optional and defaults to self::VERSION_TYPE_STRING. Passing an * invalid parameter will default to the this type as well. * * @return string|float The version of the property we are trying to extract. */ public function version($propertyName, $type = self::VERSION_TYPE_STRING) { if (empty($propertyName)) { return false; } // set the $type to the default if we don't recognize the type if ($type !== self::VERSION_TYPE_STRING && $type !== self::VERSION_TYPE_FLOAT) { $type = self::VERSION_TYPE_STRING; } $properties = self::getProperties(); // Check if the property exists in the properties array. if (true === isset($properties[$propertyName])) { // Prepare the pattern to be matched. // Make sure we always deal with an array (string is converted). $properties[$propertyName] = (array) $properties[$propertyName]; foreach ($properties[$propertyName] as $propertyMatchString) { $propertyPattern = str_replace('[VER]', self::VER, $propertyMatchString); // Identify and extract the version. preg_match(sprintf('#%s#is', $propertyPattern), $this->userAgent, $match); if (false === empty($match[1])) { $version = ($type == self::VERSION_TYPE_FLOAT ? $this->prepareVersionNo($match[1]) : $match[1]); return $version; } } } return false; } /** * Retrieve the mobile grading, using self::MOBILE_GRADE_* constants. * * @return string One of the self::MOBILE_GRADE_* constants. */ public function mobileGrade() { $isMobile = $this->isMobile(); if ( // Apple iOS 4-7.0 – Tested on the original iPad (4.3 / 5.0), iPad 2 (4.3 / 5.1 / 6.1), iPad 3 (5.1 / 6.0), iPad Mini (6.1), iPad Retina (7.0), iPhone 3GS (4.3), iPhone 4 (4.3 / 5.1), iPhone 4S (5.1 / 6.0), iPhone 5 (6.0), and iPhone 5S (7.0) $this->is('iOS') && $this->version('iPad', self::VERSION_TYPE_FLOAT) >= 4.3 || $this->is('iOS') && $this->version('iPhone', self::VERSION_TYPE_FLOAT) >= 4.3 || $this->is('iOS') && $this->version('iPod', self::VERSION_TYPE_FLOAT) >= 4.3 || // Android 2.1-2.3 - Tested on the HTC Incredible (2.2), original Droid (2.2), HTC Aria (2.1), Google Nexus S (2.3). Functional on 1.5 & 1.6 but performance may be sluggish, tested on Google G1 (1.5) // Android 3.1 (Honeycomb) - Tested on the Samsung Galaxy Tab 10.1 and Motorola XOOM // Android 4.0 (ICS) - Tested on a Galaxy Nexus. Note: transition performance can be poor on upgraded devices // Android 4.1 (Jelly Bean) - Tested on a Galaxy Nexus and Galaxy 7 ( $this->version('Android', self::VERSION_TYPE_FLOAT)>2.1 && $this->is('Webkit') ) || // Windows Phone 7.5-8 - Tested on the HTC Surround (7.5), HTC Trophy (7.5), LG-E900 (7.5), Nokia 800 (7.8), HTC Mazaa (7.8), Nokia Lumia 520 (8), Nokia Lumia 920 (8), HTC 8x (8) $this->version('Windows Phone OS', self::VERSION_TYPE_FLOAT) >= 7.5 || // Tested on the Torch 9800 (6) and Style 9670 (6), BlackBerry® Torch 9810 (7), BlackBerry Z10 (10) $this->is('BlackBerry') && $this->version('BlackBerry', self::VERSION_TYPE_FLOAT) >= 6.0 || // Blackberry Playbook (1.0-2.0) - Tested on PlayBook $this->match('Playbook.*Tablet') || // Palm WebOS (1.4-3.0) - Tested on the Palm Pixi (1.4), Pre (1.4), Pre 2 (2.0), HP TouchPad (3.0) ( $this->version('webOS', self::VERSION_TYPE_FLOAT) >= 1.4 && $this->match('Palm|Pre|Pixi') ) || // Palm WebOS 3.0 - Tested on HP TouchPad $this->match('hp.*TouchPad') || // Firefox Mobile 18 - Tested on Android 2.3 and 4.1 devices ( $this->is('Firefox') && $this->version('Firefox', self::VERSION_TYPE_FLOAT) >= 18 ) || // Chrome for Android - Tested on Android 4.0, 4.1 device ( $this->is('Chrome') && $this->is('AndroidOS') && $this->version('Android', self::VERSION_TYPE_FLOAT) >= 4.0 ) || // Skyfire 4.1 - Tested on Android 2.3 device ( $this->is('Skyfire') && $this->version('Skyfire', self::VERSION_TYPE_FLOAT) >= 4.1 && $this->is('AndroidOS') && $this->version('Android', self::VERSION_TYPE_FLOAT) >= 2.3 ) || // Opera Mobile 11.5-12: Tested on Android 2.3 ( $this->is('Opera') && $this->version('Opera Mobi', self::VERSION_TYPE_FLOAT) >= 11.5 && $this->is('AndroidOS') ) || // Meego 1.2 - Tested on Nokia 950 and N9 $this->is('MeeGoOS') || // Tizen (pre-release) - Tested on early hardware $this->is('Tizen') || // Samsung Bada 2.0 - Tested on a Samsung Wave 3, Dolphin browser // @todo: more tests here! $this->is('Dolfin') && $this->version('Bada', self::VERSION_TYPE_FLOAT) >= 2.0 || // UC Browser - Tested on Android 2.3 device ( ($this->is('UC Browser') || $this->is('Dolfin')) && $this->version('Android', self::VERSION_TYPE_FLOAT) >= 2.3 ) || // Kindle 3 and Fire - Tested on the built-in WebKit browser for each ( $this->match('Kindle Fire') || $this->is('Kindle') && $this->version('Kindle', self::VERSION_TYPE_FLOAT) >= 3.0 ) || // Nook Color 1.4.1 - Tested on original Nook Color, not Nook Tablet $this->is('AndroidOS') && $this->is('NookTablet') || // Chrome Desktop 16-24 - Tested on OS X 10.7 and Windows 7 $this->version('Chrome', self::VERSION_TYPE_FLOAT) >= 16 && !$isMobile || // Safari Desktop 5-6 - Tested on OS X 10.7 and Windows 7 $this->version('Safari', self::VERSION_TYPE_FLOAT) >= 5.0 && !$isMobile || // Firefox Desktop 10-18 - Tested on OS X 10.7 and Windows 7 $this->version('Firefox', self::VERSION_TYPE_FLOAT) >= 10.0 && !$isMobile || // Internet Explorer 7-9 - Tested on Windows XP, Vista and 7 $this->version('IE', self::VERSION_TYPE_FLOAT) >= 7.0 && !$isMobile || // Opera Desktop 10-12 - Tested on OS X 10.7 and Windows 7 $this->version('Opera', self::VERSION_TYPE_FLOAT) >= 10 && !$isMobile ){ return self::MOBILE_GRADE_A; } if ( $this->is('iOS') && $this->version('iPad', self::VERSION_TYPE_FLOAT)<4.3 || $this->is('iOS') && $this->version('iPhone', self::VERSION_TYPE_FLOAT)<4.3 || $this->is('iOS') && $this->version('iPod', self::VERSION_TYPE_FLOAT)<4.3 || // Blackberry 5.0: Tested on the Storm 2 9550, Bold 9770 $this->is('Blackberry') && $this->version('BlackBerry', self::VERSION_TYPE_FLOAT) >= 5 && $this->version('BlackBerry', self::VERSION_TYPE_FLOAT)<6 || //Opera Mini (5.0-6.5) - Tested on iOS 3.2/4.3 and Android 2.3 ($this->version('Opera Mini', self::VERSION_TYPE_FLOAT) >= 5.0 && $this->version('Opera Mini', self::VERSION_TYPE_FLOAT) <= 7.0 && ($this->version('Android', self::VERSION_TYPE_FLOAT) >= 2.3 || $this->is('iOS')) ) || // Nokia Symbian^3 - Tested on Nokia N8 (Symbian^3), C7 (Symbian^3), also works on N97 (Symbian^1) $this->match('NokiaN8|NokiaC7|N97.*Series60|Symbian/3') || // @todo: report this (tested on Nokia N71) $this->version('Opera Mobi', self::VERSION_TYPE_FLOAT) >= 11 && $this->is('SymbianOS') ){ return self::MOBILE_GRADE_B; } if ( // Blackberry 4.x - Tested on the Curve 8330 $this->version('BlackBerry', self::VERSION_TYPE_FLOAT) <= 5.0 || // Windows Mobile - Tested on the HTC Leo (WinMo 5.2) $this->match('MSIEMobile|Windows CE.*Mobile') || $this->version('Windows Mobile', self::VERSION_TYPE_FLOAT) <= 5.2 || // Tested on original iPhone (3.1), iPhone 3 (3.2) $this->is('iOS') && $this->version('iPad', self::VERSION_TYPE_FLOAT) <= 3.2 || $this->is('iOS') && $this->version('iPhone', self::VERSION_TYPE_FLOAT) <= 3.2 || $this->is('iOS') && $this->version('iPod', self::VERSION_TYPE_FLOAT) <= 3.2 || // Internet Explorer 7 and older - Tested on Windows XP $this->version('IE', self::VERSION_TYPE_FLOAT) <= 7.0 && !$isMobile ){ return self::MOBILE_GRADE_C; } // All older smartphone platforms and featurephones - Any device that doesn't support media queries // will receive the basic, C grade experience. return self::MOBILE_GRADE_C; } } vendors/classes/class-imagify-partner.php 0000644 00000053561 15174677547 0014634 0 ustar 00 <?php /** * Tool allowing 3rd party WordPress plugins to handle partnership with Imagify. * * @package wp-media/wp-imagify-partner */ defined( 'ABSPATH' ) || exit; if ( ! class_exists( 'Imagify_Partner' ) ) : /** * Class allowing to download, install, and activate Imagify plugin. * * @author Grégory Viguier */ class Imagify_Partner { /** * Class version. * * @var string */ const VERSION = '1.0'; /** * Name of the option that stores the partner identifier. * * @var string */ const OPTION_NAME = 'imagifyp_id'; /** * Name of the transient that stores the error messages. * * @var string */ const ERROR_TRANSIENT_NAME = 'imagifyp_error'; /** * Name of the URL argument used on success. * * @var string */ const SUCCESS_ARG = 'imp-success'; /** * Name of the URL argument used to display an error notice. * * @var string */ const ERROR_ARG = 'imp-error'; /** * ID of the nonce used to install Imagify. * * @var string */ const NONCE_NAME = 'install_imagify_from_partner'; /** * Message used as fallback in get_message(). * * @var string */ const FALLBACK_MESSAGE = 'Unknown message'; /** * Partner identifier. * * @var string * @access protected */ protected $partner; /** ----------------------------------------------------------------------------------------- */ /** INSTANCE, INIT ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Class constructor: sanitize and set the partner identifier. * * @since 1.0 * @access public * @author Grégory Viguier * * @param string $partner Partner identifier. */ public function __construct( $partner ) { $this->partner = self::sanitize_partner( $partner ); } /** * Class init. * * @since 1.0 * @access public * @author Grégory Viguier */ public function init() { if ( ! $this->get_partner() ) { return; } if ( ! is_admin() ) { return; } if ( ! self::has_imagify_api_key() ) { add_action( 'wp_ajax_' . $this->get_post_action(), array( $this, 'post_callback' ) ); add_action( 'admin_post_' . $this->get_post_action(), array( $this, 'post_callback' ) ); } if ( self::is_success() || self::is_error() ) { add_action( 'all_admin_notices', array( __CLASS__, 'error_notice' ) ); add_filter( 'removable_query_args', array( __CLASS__, 'add_query_args' ) ); } } /** ----------------------------------------------------------------------------------------- */ /** MAIN PUBLIC TOOLS ======================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if Imagify's API key is set. * * @since 1.0 * @access public * @author Grégory Viguier * * @return bool */ public static function has_imagify_api_key() { static $has; if ( isset( $has ) ) { return $has; } if ( function_exists( 'get_imagify_option' ) ) { // Imagify is already installed and activated. $has = (bool) get_imagify_option( 'api_key' ); return $has; } if ( defined( 'IMAGIFY_API_KEY' ) && IMAGIFY_API_KEY ) { // It's defined in wp-config.php. $has = true; return $has; } if ( ! is_multisite() ) { // Monosite: grab the value from the options table. $options = get_option( 'imagify_settings' ); $has = ! empty( $options['api_key'] ); return $has; } $options = get_site_option( 'imagify_settings' ); if ( ! empty( $options['api_key'] ) ) { // Multisite: Imagify was activated in the network. $has = true; return $has; } // Multisite: Imagify was activated for this site. $options = get_option( 'imagify_settings' ); $has = ! empty( $options['api_key'] ); return $has; } /** * Tell if Imagify is activated. * * @since 1.0 * @access public * @author Grégory Viguier * * @return bool */ public static function is_imagify_activated() { return defined( 'IMAGIFY_VERSION' ); } /** * Tell if Imagify is installed. * * @since 1.0 * @access public * @author Grégory Viguier * * @return bool */ public static function is_imagify_installed() { if ( self::is_imagify_activated() ) { return true; } return file_exists( self::get_imagify_path() ); } /** * Tell if Imagify has been successfully installed. * * @since 1.0 * @access public * @author Grégory Viguier * * @return bool */ public static function is_success() { return ! empty( $_GET[ self::SUCCESS_ARG ] ); // WPCS: CSRF ok. } /** * Tell if Imagify install failed. * * @since 1.0 * @access public * @author Grégory Viguier * * @return bool */ public static function is_error() { return ! empty( $_GET[ self::ERROR_ARG ] ); // WPCS: CSRF ok. } /** * Get the URL to install and activate Imagify. * * @since 1.0 * @access public * @author Grégory Viguier * * @return string The URL. */ public function get_post_install_url() { if ( ! $this->get_partner() || ! self::current_user_can() ) { return ''; } $install_url = admin_url( 'admin-post.php' ); $args = array( 'action' => $this->get_post_action(), '_wpnonce' => wp_create_nonce( self::NONCE_NAME ), // To make sure we have a referrer. '_wp_http_referer' => rawurlencode( self::get_current_url() ), ); return add_query_arg( $args, $install_url ); } /** * Get the partner identifier. * * @since 1.0 * @access public * @author Grégory Viguier * * @return string Partner identifier. */ public function get_partner() { return $this->partner; } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Post callback to install and activate Imagify. * * @since 1.0 * @access public * @author Grégory Viguier */ public function post_callback() { if ( ! check_ajax_referer( self::NONCE_NAME, '_wpnonce', false ) ) { $this->error_die(); } if ( ! self::current_user_can() ) { $this->error_die( 'cant_install' ); } /** * Store the partner ID before doing anything. * If something goes wrong during the plugin installation, the partner ID will still be saved. */ self::store_partner( $this->get_partner() ); // Install Imagify. $result = $this->install_imagify(); if ( is_wp_error( $result ) ) { // Install failed. if ( self::doing_ajax() ) { $this->send_json_error( $result ); } // Redirect to the plugins search page. $this->error_redirect( $result ); } // Activate Imagify. $result = $this->activate_imagify(); if ( is_wp_error( $result ) ) { // Activation failed. if ( self::doing_ajax() ) { $this->send_json_error( $result ); } // Redirect to the plugins search page. $this->error_redirect( $result ); } if ( self::doing_ajax() ) { $this->send_json_success(); } // Redirect to the partner's page. $this->success_redirect(); } /** * Maybe print an error notice on the plugins install page. * We add the query argument we use to display an error message. * * @since 1.0 * @access public * @author Grégory Viguier */ public static function error_notice() { if ( ! self::is_error() ) { // No URL argument. return; } $screen = get_current_screen(); if ( ! $screen || 'plugin-install' !== $screen->id ) { // Not the good page. return; } $partner = self::get_stored_partner(); if ( ! $partner ) { // No partner stored in the database. return; } $errors = get_transient( self::ERROR_TRANSIENT_NAME ); if ( ! $errors ) { // No error messages. return; } if ( ! is_wp_error( $errors ) ) { // Invalid value. delete_transient( self::ERROR_TRANSIENT_NAME ); return; } $errors = $errors->get_error_messages(); if ( $errors ) { foreach ( $errors as $i => $error ) { if ( self::FALLBACK_MESSAGE === $error ) { unset( $errors[ $i ] ); } } } if ( ! $errors ) { // Add a generic message. $instance = new self( $partner ); $errors[] = $instance->get_message( 'process_failed' ); } echo '<div class="error notice is-dismissible"><p>' . implode( '<br/>', $errors ) . '</p></div>'; } /** * Filter the list of query variables to remove from admin area URLs. * We add the query arguments we use on success or error. * * @since 1.0 * @access public * @see wp_removable_query_args() * @author Grégory Viguier * * @param array $removable_query_args An array of query variables to remove from a URL. * @return array */ public static function add_query_args( $removable_query_args ) { $removable_query_args[] = self::SUCCESS_ARG; $removable_query_args[] = self::ERROR_ARG; return $removable_query_args; } /** ----------------------------------------------------------------------------------------- */ /** INFOS, INSTALL, ACTIVATE ================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Get Imagify infos from the repository. * * @since 1.0 * @access protected * @author Grégory Viguier * * @return object The plugin infos on success. A WP_Error object on failure. */ protected function get_imagify_infos() { static $infos; if ( isset( $infos ) ) { return $infos; } require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; // Get Plugin Infos. $infos = plugins_api( 'plugin_information', array( 'slug' => 'imagify', 'fields' => array( 'short_description' => false, 'sections' => false, 'rating' => false, 'ratings' => false, 'downloaded' => false, 'last_updated' => false, 'added' => false, 'tags' => false, 'homepage' => false, 'donate_link' => false, ), ) ); return $infos; } /** * Get the URL to download Imagify. * * @since 1.0 * @access protected * @author Grégory Viguier * * @return string The URL. An empty string on error. */ protected function get_download_url() { $infos = $this->get_imagify_infos(); return ! empty( $infos->download_link ) ? $infos->download_link : ''; } /** * Install Imagify. * * @since 1.0 * @access protected * @author Grégory Viguier * * @return object|null A WP_Object on failure, null on success. */ protected function install_imagify() { if ( self::is_imagify_installed() ) { // Imagify is already installed. return null; } $infos = $this->get_imagify_infos(); if ( is_wp_error( $infos ) ) { return $infos; } ob_start(); @set_time_limit( 0 ); require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() ); $result = $upgrader->install( $this->get_download_url() ); ob_end_clean(); if ( is_wp_error( $result ) ) { return $result; } clearstatcache(); if ( ! self::is_imagify_installed() ) { return new WP_Error( 'process_failed', $this->get_message( 'process_failed' ) ); } return null; } /** * Activate Imagify. * * @since 1.0 * @access protected * @author Grégory Viguier * * @return object|null A WP_Object on failure, null on success. */ protected function activate_imagify() { return activate_plugin( self::get_imagify_path(), false, is_multisite() ); } /** * Get a message used by the class. * * @since 1.0 * @access protected * @author Grégory Viguier * * @param string $message_id A message ID. * @return string A message. */ protected function get_message( $message_id ) { $messages = array( 'success' => __( 'Plugin installed successfully.' ), 'cant_install' => __( 'Sorry, you are not allowed to install plugins on this site.' ), 'not_allowed' => __( 'Sorry, you are not allowed to do that.' ), 'process_failed' => __( 'Plugin install failed.' ), 'go_back' => __( 'Go back' ), ); /** * Filter messages used everywhere in the class. * Default messages are already translated by WordPress. * * @since 1.0 * @author Grégory Viguier * * @param array $messages Messages. */ $messages = apply_filters( 'imagify_partner_messages_' . $this->get_partner(), $messages ); return ! empty( $messages[ $message_id ] ) ? $messages[ $message_id ] : self::FALLBACK_MESSAGE; } /** ----------------------------------------------------------------------------------------- */ /** HANDLE SUCCESS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Send a JSON response back to an Ajax request, indicating success. * * @since 1.0 * @access protected * @author Grégory Viguier */ protected function send_json_success() { delete_transient( self::ERROR_TRANSIENT_NAME ); wp_send_json_success( $this->get_message( 'success' ) ); } /** * Redirect the user after Imagify is successfully installed and activated. * * @since 1.0 * @access protected * @author Grégory Viguier */ protected function success_redirect() { delete_transient( self::ERROR_TRANSIENT_NAME ); wp_safe_redirect( esc_url_raw( $this->get_success_redirection_url() ) ); die(); } /** * Get the URL to redirect the user to after Imagify is successfully installed and activated: the referrer (partner's page URL). * A "success" argument is added. * * @since 1.0 * @access public * @author Grégory Viguier * * @return string */ public function get_success_redirection_url() { $success_url = add_query_arg( array( self::SUCCESS_ARG => 1, self::ERROR_ARG => false, ), wp_get_referer() ); /** * Filter the URL to redirect the user to after Imagify is successfully installed and activated. * Default is the partner's page URL. * * @since 1.0 * @author Grégory Viguier * * @param string $success_url The URL. */ return apply_filters( 'imagify_partner_success_url_' . $this->get_partner(), $success_url ); } /** ----------------------------------------------------------------------------------------- */ /** HANDLE ERROR ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Die on error. * * @since 1.0 * @access protected * @author Grégory Viguier * * @param string $message_id An error message ID. */ protected function error_die( $message_id = 'not_allowed' ) { $message = $this->get_message( $message_id ); if ( self::doing_ajax() ) { $message = new WP_Error( $message_id, $message ); $this->send_json_error( $message ); } if ( wp_get_referer() ) { $message .= '</p><p>'; $message .= sprintf( '<a href="%s">%s</a>', esc_url( remove_query_arg( 'updated', wp_get_referer() ) ), $this->get_message( 'go_back' ) ); } wp_die( $message, '', 403 ); } /** * Send a JSON response back to an Ajax request, indicating failure. * This is a backward compatible version of wp_send_json_error(): WP_Error object handling was introduced in WP 4.1. * * @since 1.0 * @access protected * @author Grégory Viguier * * @param mixed $data Data to encode as JSON, then print and die. */ protected function send_json_error( $data ) { if ( is_wp_error( $data ) ) { $result = array(); foreach ( $data->errors as $code => $messages ) { foreach ( $messages as $message ) { $result[] = array( 'code' => $code, 'message' => $message, ); } } } else { $result = $data; } wp_send_json_error( $result ); } /** * Store an error message in a transient then redirect the user. * * @since 1.0 * @access protected * @author Grégory Viguier * * @param object $error A WP_Error object. */ protected function error_redirect( $error ) { set_transient( self::ERROR_TRANSIENT_NAME, $error, 30 ); wp_safe_redirect( esc_url_raw( $this->get_error_redirection_url() ) ); die(); } /** * Get the URL to redirect the user to after Imagify installation failure: the plugins search page URL, searching for Imagify. * An "error" argument is added, to display an error notice. * * @since 1.0 * @access public * @author Grégory Viguier * * @return string */ public function get_error_redirection_url() { $error_url = 'plugin-install.php?s=imagify&tab=search&type=term&' . self::ERROR_ARG . '=1'; $error_url = is_multisite() ? network_admin_url( $error_url ) : admin_url( $error_url ); /** * Filter the URL to redirect the user to after Imagify installation failure. * Default is the plugins search page URL. * * @since 1.0 * @author Grégory Viguier * * @param string $error_url The URL. */ return apply_filters( 'imagify_partner_error_url_' . $this->get_partner(), $error_url ); } /** ----------------------------------------------------------------------------------------- */ /** STORING THE PARTNER ID IN DATABASE ====================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the partner identifier stored in the Database. * * @since 1.0 * @access public * @author Grégory Viguier * * @return string|bool The partner identifier, or false if none is stored. */ public static function get_stored_partner() { $partner = get_option( self::OPTION_NAME ); if ( $partner && is_string( $partner ) ) { $partner = self::sanitize_partner( $partner ); } return $partner ? $partner : false; } /** * Delete the partner identifier stored in the Database. * * @since 1.0 * @access public * @author Grégory Viguier */ public static function delete_stored_partner() { if ( false !== get_option( self::OPTION_NAME ) ) { delete_option( self::OPTION_NAME ); } } /** * Store the partner identifier in Database. * * @since 1.0 * @access protected * @author Grégory Viguier * * @param string $partner The partner identifier to store. */ protected static function store_partner( $partner ) { if ( false === get_option( self::OPTION_NAME ) ) { add_option( self::OPTION_NAME, $partner ); } else { update_option( self::OPTION_NAME, $partner ); } } /** * Sanitize a partner ID. * * @since 1.0 * @access protected * @author Grégory Viguier * * @param string $partner Partner identifier. * @return string */ protected static function sanitize_partner( $partner ) { return preg_replace( '@[^a-z0-9_-]@', '', strtolower( (string) $partner ) ); } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS TOOLS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the action. * * @since 1.0 * @access public * @author Grégory Viguier * * @return string Partner identifier. */ public function get_post_action() { return 'install_imagify_from_partner_' . $this->get_partner(); } /** * Determines whether the current request is a WordPress Ajax request. * This is a clone of wp_doing_ajax(), intriduced in WP 4.7. * * @since 1.0 * @access public * @author Grégory Viguier * * @return bool True if it's a WordPress Ajax request, false otherwise. */ public static function doing_ajax() { /** * Filters whether the current request is a WordPress Ajax request. * * @since 1.0 * * @param bool $wp_doing_ajax Whether the current request is a WordPress Ajax request. */ return apply_filters( 'wp_doing_ajax', defined( 'DOING_AJAX' ) && DOING_AJAX ); } /** * Get Imagify's file path. * * @since 1.0 * @access public * @author Grégory Viguier * * @return string The file path. */ public static function get_imagify_path() { if ( defined( 'IMAGIFY_FILE' ) ) { return IMAGIFY_FILE; } return WP_PLUGIN_DIR . '/imagify/imagify.php'; } /** * Tell if the current user can install and activate Imagify. * * @since 1.0 * @access public * @author Grégory Viguier * * @return bool */ public static function current_user_can() { static $can; if ( ! isset( $can ) ) { $can = is_multisite() ? 'manage_network_plugins' : 'install_plugins'; $can = current_user_can( $can ); } return $can; } /** * Get the current URL. * * @since 1.0 * @access public * @author Grégory Viguier * * @return string */ public static function get_current_url() { $port = (int) $_SERVER['SERVER_PORT']; $port = 80 !== $port && 443 !== $port ? ( ':' . $port ) : ''; $url = ! empty( $GLOBALS['HTTP_SERVER_VARS']['REQUEST_URI'] ) ? $GLOBALS['HTTP_SERVER_VARS']['REQUEST_URI'] : ( ! empty( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '' ); return 'http' . ( is_ssl() ? 's' : '' ) . '://' . $_SERVER['HTTP_HOST'] . $port . $url; } } endif; vendors/classes/class-minify-css-urirewriter.php 0000644 00000021723 15174677547 0016173 0 ustar 00 <?php /** * Class Minify_CSS_UriRewriter * * @package Minify */ /** * Rewrite file-relative URIs as root-relative in CSS files * * @package Minify * @author Stephen Clay <steve@mrclay.org> */ class Minify_CSS_UriRewriter { /** * rewrite() and rewriteRelative() append debugging information here * * @var string */ public static $debugText = ''; /** * In CSS content, rewrite file relative URIs as root relative * * @param string $css * * @param string $currentDir The directory of the current CSS file. * * @param string $docRoot The document root of the web site in which * the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']). * * @param array $symlinks (default = array()) If the CSS file is stored in * a symlink-ed directory, provide an array of link paths to * target paths, where the link paths are within the document root. Because * paths need to be normalized for this to work, use "//" to substitute * the doc root in the link paths (the array keys). E.g.: * <code> * array('//symlink' => '/real/target/path') // unix * array('//static' => 'D:\\staticStorage') // Windows * </code> * * @return string */ public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = array()) { self::$_docRoot = self::_realpath( $docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT'] ); self::$_currentDir = self::_realpath($currentDir); self::$_symlinks = array(); // normalize symlinks in order to map to link foreach ($symlinks as $link => $target) { $link = ($link === '//') ? self::$_docRoot : str_replace('//', self::$_docRoot . '/', $link); $link = strtr($link, '/', DIRECTORY_SEPARATOR); self::$_symlinks[$link] = self::_realpath($target); } self::$debugText .= "docRoot : " . self::$_docRoot . "\n" . "currentDir : " . self::$_currentDir . "\n"; if (self::$_symlinks) { self::$debugText .= "symlinks : " . var_export(self::$_symlinks, 1) . "\n"; } self::$debugText .= "\n"; $css = self::_trimUrls($css); $css = self::_owlifySvgPaths($css); // rewrite $pattern = '/@import\\s+([\'"])(.*?)[\'"]/'; $css = preg_replace_callback($pattern, array(self::$className, '_processUriCB'), $css); $pattern = '/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/'; $css = preg_replace_callback($pattern, array(self::$className, '_processUriCB'), $css); $css = self::_unOwlify($css); return $css; } /** * In CSS content, prepend a path to relative URIs * * @param string $css * * @param string $path The path to prepend. * * @return string */ public static function prepend($css, $path) { self::$_prependPath = $path; $css = self::_trimUrls($css); $css = self::_owlifySvgPaths($css); // append $pattern = '/@import\\s+([\'"])(.*?)[\'"]/'; $css = preg_replace_callback($pattern, array(self::$className, '_processUriCB'), $css); $pattern = '/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/'; $css = preg_replace_callback($pattern, array(self::$className, '_processUriCB'), $css); $css = self::_unOwlify($css); self::$_prependPath = null; return $css; } /** * Get a root relative URI from a file relative URI * * <code> * Minify_CSS_UriRewriter::rewriteRelative( * '../img/hello.gif' * , '/home/user/www/css' // path of CSS file * , '/home/user/www' // doc root * ); * // returns '/img/hello.gif' * * // example where static files are stored in a symlinked directory * Minify_CSS_UriRewriter::rewriteRelative( * 'hello.gif' * , '/var/staticFiles/theme' * , '/home/user/www' * , array('/home/user/www/static' => '/var/staticFiles') * ); * // returns '/static/theme/hello.gif' * </code> * * @param string $uri file relative URI * * @param string $realCurrentDir realpath of the current file's directory. * * @param string $realDocRoot realpath of the site document root. * * @param array $symlinks (default = array()) If the file is stored in * a symlink-ed directory, provide an array of link paths to * real target paths, where the link paths "appear" to be within the document * root. E.g.: * <code> * array('/home/foo/www/not/real/path' => '/real/target/path') // unix * array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path') // Windows * </code> * * @return string */ public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array()) { // prepend path with current dir separator (OS-independent) $path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR); $path .= DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR); self::$debugText .= "file-relative URI : {$uri}\n" . "path prepended : {$path}\n"; // "unresolve" a symlink back to doc root foreach ($symlinks as $link => $target) { if (0 === strpos($path, $target)) { // replace $target with $link $path = $link . substr($path, strlen($target)); self::$debugText .= "symlink unresolved : {$path}\n"; break; } } // strip doc root $path = substr($path, strlen($realDocRoot)); self::$debugText .= "docroot stripped : {$path}\n"; // fix to root-relative URI $uri = strtr($path, '/\\', '//'); $uri = self::removeDots($uri); self::$debugText .= "traversals removed : {$uri}\n\n"; return $uri; } /** * Remove instances of "./" and "../" where possible from a root-relative URI * * @param string $uri * * @return string */ public static function removeDots($uri) { $uri = str_replace('/./', '/', $uri); // inspired by patch from Oleg Cherniy do { $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed); } while ($changed); return $uri; } /** * Defines which class to call as part of callbacks, change this * if you extend Minify_CSS_UriRewriter * * @var string */ protected static $className = 'Minify_CSS_UriRewriter'; /** * Get realpath with any trailing slash removed. If realpath() fails, * just remove the trailing slash. * * @param string $path * * @return mixed path with no trailing slash */ protected static function _realpath($path) { $realPath = realpath($path); if ($realPath !== false) { $path = $realPath; } return rtrim($path, '/\\'); } /** * Directory of this stylesheet * * @var string */ private static $_currentDir = ''; /** * DOC_ROOT * * @var string */ private static $_docRoot = ''; /** * directory replacements to map symlink targets back to their * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath' * * @var array */ private static $_symlinks = array(); /** * Path to prepend * * @var string */ private static $_prependPath = null; /** * @param string $css * * @return string */ private static function _trimUrls($css) { $pattern = '/ url\\( # url( \\s* ([^\\)]+?) # 1 = URI (assuming does not contain ")") \\s* \\) # ) /x'; return preg_replace($pattern, 'url($1)', $css); } /** * @param array $m * * @return string */ private static function _processUriCB($m) { // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' $isImport = ($m[0][0] === '@'); // determine URI and the quote character (if any) if ($isImport) { $quoteChar = $m[1]; $uri = $m[2]; } else { // $m[1] is either quoted or not $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"') ? $m[1][0] : ''; $uri = ($quoteChar === '') ? $m[1] : substr($m[1], 1, strlen($m[1]) - 2); } if ($uri === '') { return $m[0]; } // if not root/scheme relative and not starts with scheme if (!preg_match('~^(/|[a-z]+\:)~', $uri)) { // URI is file-relative: rewrite depending on options if (self::$_prependPath === null) { $uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks); } else { $uri = self::$_prependPath . $uri; if ($uri[0] === '/') { $root = ''; $rootRelative = $uri; $uri = $root . self::removeDots($rootRelative); } elseif (preg_match('@^((https?\:)?//([^/]+))/@', $uri, $m) && (false !== strpos($m[3], '.'))) { $root = $m[1]; $rootRelative = substr($uri, strlen($root)); $uri = $root . self::removeDots($rootRelative); } } } if ($isImport) { return "@import {$quoteChar}{$uri}{$quoteChar}"; } else { return "url({$quoteChar}{$uri}{$quoteChar})"; } } /** * Mungs some inline SVG URL declarations so they won't be touched * * @link https://github.com/mrclay/minify/issues/517 * @see _unOwlify * * @param string $css * @return string */ private static function _owlifySvgPaths($css) { $pattern = '~\b((?:clip-path|mask|-webkit-mask)\s*\:\s*)url(\(\s*#\w+\s*\))~'; return preg_replace($pattern, '$1owl$2', $css); } /** * Undo work of _owlify * * @see _owlifySvgPaths * * @param string $css * @return string */ private static function _unOwlify($css) { $pattern = '~\b((?:clip-path|mask|-webkit-mask)\s*\:\s*)owl~'; return preg_replace($pattern, '$1url', $css); } } domain-mapping.php 0000644 00000004060 15174677547 0010204 0 ustar 00 <?php defined( 'ABSPATH' ) || exit; /** * Used to get compatibility between multidomain and get_rocket_parse_url() * * @since 2.2 * * @param string $url URL to modify. * @return string Modified URL */ function rocket_parse_url_domain_mapping( $url ) { $original_siteurl_host = rocket_extract_url_component( get_original_url( 'siteurl' ), PHP_URL_HOST ); $domain_mapping_siteurl_host = rocket_extract_url_component( domain_mapping_siteurl( false ), PHP_URL_HOST ); if ( false === strpos( $domain_mapping_siteurl_host, $original_siteurl_host ) ) { $url['host'] = str_replace( $original_siteurl_host, $domain_mapping_siteurl_host, $url['host'] ); } return $url; } add_filter( 'rocket_parse_url', 'rocket_parse_url_domain_mapping' ); /** * Used to get compatibility between multidomain and rocket_clean_files() & rocket_clean_domain() * * @since 2.6.5 Add compatibility with rocket_clean_domain() * @since 2.2 */ if ( function_exists( 'domain_mapping_post_content' ) ) : add_filter( 'rocket_clean_files', 'domain_mapping_post_content' ); add_filter( 'rocket_clean_domain_urls', 'domain_mapping_post_content' ); add_filter( 'rocket_post_purge_urls', 'domain_mapping_post_content' ); endif; /** * Used to get compatibility between multidomain and rocket_clean_home() * * @since 2.6.5 * * @param string $root Path to the cache for the host. * @param string $host Host value. * @param string $path Unused. * @return $root Path to the cache */ function rocket_clean_home_root_for_domain_mapping_siteurl( $root, $host, $path ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $original_siteurl_host = rocket_extract_url_component( get_original_url( 'siteurl' ), PHP_URL_HOST ); $domain_mapping_siteurl_host = rocket_extract_url_component( domain_mapping_siteurl( false ), PHP_URL_HOST ); if ( $original_siteurl_host !== $domain_mapping_siteurl_host ) { $root = WP_ROCKET_CACHE_PATH . $host . '*'; } return $root; } add_filter( 'rocket_clean_home_root', 'rocket_clean_home_root_for_domain_mapping_siteurl', 10, 3 ); Engine/Optimization/RegexTrait.php 0000644 00000013510 15174677547 0013255 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization; trait RegexTrait { /** * Array of replaced xmp tags * * @var array */ private $xmp_replace = []; /** * Array of replaced html tags. * * @var array */ private $html_replace = []; /** * Array of replaced svg tags * * @var array */ private $svg_replace = []; /** * Finds nodes matching the pattern in the HTML. * * @param string $pattern Pattern to match. * @param string $html HTML content. * @param string $modifiers Regex modifiers. * @param mixed $flag Flag to use. * @return array */ protected function find( string $pattern, string $html, string $modifiers = 'Umsi', $flag = PREG_SET_ORDER ) { preg_match_all( '/' . $pattern . '/' . $modifiers, $html, $matches, $flag ); if ( empty( $matches ) ) { return []; } return $matches; } /** * Hides unwanted blocks from the HTML to be parsed for optimization * * @since 3.1.4 * * @param string $html HTML content. * @return string */ protected function hide_comments( $html ) { $replace = preg_replace( '#<!--\s*noptimize\s*-->.*?<!--\s*/\s*noptimize\s*-->#is', '', $html ); if ( null === $replace ) { return $html; } $replace = preg_replace( '/<!--(.*)-->/Uis', '', $replace ); if ( null === $replace ) { return $html; } return $replace; } /** * Hides scripts from the HTML to be parsed when removing CSS from it * * @since 3.10.2 * * @param string $html HTML content. * * @return string */ protected function hide_scripts( $html ) { $replace = preg_replace( '#<script[^>]*>.*?<\/script\s*>#mis', '', $html ); if ( null === $replace ) { return $html; } return $replace; } /** * Hides <noscript> blocks from the HTML to be parsed. * * @param string $html HTML content. * * @return string */ protected function hide_noscripts( $html ) { $replace = preg_replace( '#<noscript[^>]*>.*?<\/noscript\s*>#mis', '', $html ); if ( null === $replace ) { return $html; } return $replace; } /** * Replace HTML comments. * * @param string $html HTML content. * * @return string */ protected function replace_html_comments( string $html ): string { $this->html_replace = []; $regex = '#<!--.*-->#iUs'; $replaced_html = preg_replace_callback( $regex, [ $this, 'replace_html_comment' ], $html ); if ( empty( $replaced_html ) ) { return $html; } return $replaced_html; } /** * Replace html with comment * * @param array $match HTML comment. * @return string */ protected function replace_html_comment( $match ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.matchFound $key = sprintf( '<!-- %s -->', uniqid( 'WPR_HTML_COMMENT_' ) ); $this->html_replace[ $key ] = $match[0]; return $key; } /** * Restore html with comment * * @param string $html HTML content. * @return string */ protected function restore_html_comments( $html ) { if ( ! is_string( $html ) || empty( $this->html_replace ) ) { // @phpstan-ignore-line For some reason, html could be null for some users, and we can't find a way to reproduce it. return $html; } return str_replace( array_keys( $this->html_replace ), array_values( $this->html_replace ), $html ); } /** * Replace <xmp> tags in the HTML with comment * * @since 3.12.3 * * @param string $html HTML content. * @return string */ protected function replace_xmp_tags( $html ) { $this->xmp_replace = []; $regex = '#<xmp.*>.*</xmp>#Uis'; $replaced_html = preg_replace_callback( $regex, [ $this, 'replace_xmp' ], $html ); if ( empty( $replaced_html ) ) { return $html; } return $replaced_html; } /** * Replace <svg> tags in the HTML with comment * * @since 3.12.5.3 * * @param string $html HTML content. * @return string */ protected function replace_svg_tags( $html ) { $this->svg_replace = []; $regex = '#<\s*svg.*>.*<\s*\\\\?/\s*svg\s*>#Uis'; $replaced_html = preg_replace_callback( $regex, [ $this, 'replace_svg' ], $html ); if ( empty( $replaced_html ) ) { return $html; } return $replaced_html; } /** * Replace svg with comment * * @since 3.12.3 * * @param array $match svg tag. * @return string */ protected function replace_svg( $match ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.matchFound $key = sprintf( '<!-- %s -->', uniqid( 'WPR_SVG_' ) ); $this->svg_replace[ $key ] = $match[0]; return $key; } /** * Replace xmp with comment * * @since 3.12.3 * * @param array $match xmp tag. * @return string */ protected function replace_xmp( $match ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.matchFound $key = sprintf( '<!-- %s -->', uniqid( 'WPR_XMP_' ) ); $this->xmp_replace[ $key ] = $match[0]; return $key; } /** * Restore <svg> tags * * @since 3.12.5.3 * * @param string $html HTML content. * @return string */ protected function restore_svg_tags( $html ) { if ( empty( $this->svg_replace ) ) { return $html; } return str_replace( array_keys( $this->svg_replace ), array_values( $this->svg_replace ), $html ); } /** * Restore <xmp> tags * * @since 3.12.3 * * @param string $html HTML content. * @return string */ protected function restore_xmp_tags( $html ) { if ( empty( $this->xmp_replace ) ) { return $html; } return str_replace( array_keys( $this->xmp_replace ), array_values( $this->xmp_replace ), $html ); } /** * Checks if the page HTML is valid or not. * Valid here means that it has a closing title tag. * * @param string $html Page HTML. * * @return bool */ private function html_has_title_tag( string $html ) { return (bool) preg_match( '#</title>#iU', $html ); } } Engine/Optimization/GoogleFonts/Subscriber.php 0000644 00000007263 15174677547 0015540 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\GoogleFonts; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Combine Google Fonts subscriber * * @since 3.1 */ class Subscriber implements Subscriber_Interface { /** * Plugin options. * * @var Options_Data */ private $options; /** * Combine instance. * * @var AbstractGFOptimization */ private $combine; /** * CombineV2 instance. * * @var AbstractGFOptimization */ private $combine_v2; /** * Instantiate the subscriber. * * @param AbstractGFOptimization $combine Combine instance. * @param AbstractGFOptimization $combine_v2 Combine V2 instance. * @param Options_Data $options Options_Data instance. */ public function __construct( AbstractGFOptimization $combine, AbstractGFOptimization $combine_v2, Options_Data $options ) { $this->combine = $combine; $this->combine_v2 = $combine_v2; $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.1 * * @return array */ public static function get_subscribed_events() { return [ 'wp_resource_hints' => [ 'preconnect', 10, 2 ], 'rocket_buffer' => [ 'process', 17 ], 'rocket_head_items' => [ [ 'insert_fonts_preload', 30 ], [ 'insert_fonts_stylesheets', 50 ], ], ]; } /** * Adds google fonts URL to preconnect * * @since 3.5.3 * * @param array $urls URLs to print for resource hints. * @param string $relation_type The relation type the URLs are printed for, e.g. 'preconnect' or 'prerender'. * @return array */ public function preconnect( array $urls, $relation_type ) { if ( ! $this->is_allowed() ) { return $urls; } if ( 'preconnect' !== $relation_type ) { return $urls; } $urls[] = [ 'href' => 'https://fonts.gstatic.com', 1 => 'crossorigin', ]; return $urls; } /** * Processes the HTML to combine found Google fonts. * Handles both Google Fonts API v2 and v1. * * @since 3.1 * * @param string $html HTML content. * @return string */ public function process( $html ) { if ( ! $this->is_allowed() ) { return $html; } // Combine Google Font API V2. $html = $this->combine_v2->optimize( $html ); // Combine Google Font API V1. $html = $this->combine->optimize( $html ); if ( ! $this->combine->has_google_fonts() && ! $this->combine_v2->has_google_fonts() ) { $html = preg_replace( '/<link\s+(?:[^>]+[\s"\'])?href\s*=\s*[\'"]https:\/\/fonts\.gstatic\.com[\'"](?:[^>]+[\s"\'])?\s?\/?>/', '', $html ); } return $html; } /** * Checks if files can combine found Google fonts. * * @since 3.1 */ protected function is_allowed() { if ( rocket_bypass() ) { return false; } if ( ! $this->options->get( 'minify_google_fonts', 0 ) ) { return false; } return ! is_user_logged_in() || (bool) $this->options->get( 'cache_logged_user', 0 ); } /** * Insert fonts link stylesheets into head elements for v1 and v2. * * @param array $items Head elements. * @return mixed */ public function insert_fonts_stylesheets( array $items ) { if ( ! $this->is_allowed() ) { return $items; } $items = $this->combine_v2->insert_font_stylesheet_into_head( $items ); return $this->combine->insert_font_stylesheet_into_head( $items ); } /** * Insert fonts preloads into head elements for v1 and v2. * * @param array $items Head elements. * @return mixed */ public function insert_fonts_preload( $items ) { if ( ! $this->is_allowed() ) { return $items; } $items = $this->combine_v2->insert_font_preload_into_head( $items ); return $this->combine->insert_font_preload_into_head( $items ); } } Engine/Optimization/GoogleFonts/Combine.php 0000644 00000010660 15174677547 0015004 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\GoogleFonts; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Logger\Logger; /** * Combine Google Fonts * * @since 3.1 */ class Combine extends AbstractGFOptimization { use RegexTrait; /** * Found fonts * * @since 3.1 * * @var string */ protected $fonts = ''; /** * Found subsets * * @since 3.1 * * @var string */ protected $subsets = ''; /** * Font urls. * * @var array */ protected $font_urls = []; /** * Combines multiple Google Fonts links into one * * @since 3.1 * * @param string $html HTML content. * * @return string */ public function optimize( $html ): string { $this->font_urls = []; Logger::info( 'GOOGLE FONTS COMBINE PROCESS STARTED.', [ 'GF combine process' ] ); $html_nocomments = $this->hide_comments( $html ); $fonts = $this->find( '<link(?:\s+(?:(?!href\s*=\s*)[^>])+)?(?:\s+href\s*=\s*([\'"])(?<url>(?:https?:)?\/\/fonts\.googleapis\.com\/css[^\d](?:(?!\1).)+)\1)(?:\s+[^>]*)?>', $html_nocomments ); if ( ! $fonts ) { Logger::debug( 'No Google Fonts found.', [ 'GF combine process' ] ); $this->has_google_fonts = false; return $html; } $this->has_google_fonts = true; $exclusions = $this->get_exclusions(); $filtered_fonts = array_filter( $fonts, function ( $font ) use ( $exclusions ) { return ! $this->is_excluded( $font[0], $exclusions ); } ); $num_fonts = count( $filtered_fonts ); Logger::debug( "Found {$num_fonts} Google Fonts after exclusions.", [ 'GF combine process', 'tags' => $filtered_fonts, ] ); $this->parse( $filtered_fonts ); if ( empty( $this->fonts ) ) { Logger::debug( 'No Google Fonts left to combine.', [ 'GF combine process' ] ); return $html; } $this->font_urls[] = $this->get_combined_url(); foreach ( $filtered_fonts as $font ) { $html = str_replace( $font[0], '', $html ); } Logger::info( 'Google Fonts successfully combined.', [ 'GF combine process', 'url' => $this->fonts . $this->subsets, ] ); return $html; } /** * Parses found matches to extract fonts and subsets. * * @since 3.1 * * @param array $matches Found matches for the pattern. * * @return void */ private function parse( array $matches ) { $fonts_array = []; $subsets_array = []; foreach ( $matches as $match ) { $url = html_entity_decode( $match[2] ); $query = wp_parse_url( $url, PHP_URL_QUERY ); if ( empty( $query ) ) { return; } $font = wp_parse_args( $query ); if ( isset( $font['family'] ) ) { $font_family = $font['family']; $font_family = rtrim( $font_family, '%7C' ); $font_family = rtrim( $font_family, '|' ); // Add font to the collection. $fonts_array[] = rawurlencode( htmlentities( $font_family ) ); } // Add subset to collection. if ( isset( $font['subset'] ) ) { $subsets_array[] = rawurlencode( htmlentities( $font['subset'] ) ); } } // Concatenate fonts tag. $this->subsets = ! empty( $subsets_array ) ? '&subset=' . implode( ',', array_filter( array_unique( $subsets_array ) ) ) : ''; $this->fonts = ! empty( $fonts_array ) ? implode( '%7C', array_filter( array_unique( $fonts_array ) ) ) : ''; } /** * Returns the combined Google fonts URL * * @since 3.9.1 * * @return string */ private function get_combined_url(): string { $display = $this->get_font_display_value(); return esc_url( "https://fonts.googleapis.com/css?family={$this->fonts}{$this->subsets}&display={$display}" ); } /** * Get font urls, getter method for font_urls property. * * @return array */ public function get_font_urls(): array { return $this->font_urls; } /** * Insert font stylesheets into head. * * @param array $items Head elements. * @return mixed */ public function insert_font_stylesheet_into_head( $items ) { $font_urls = $this->get_font_urls(); if ( empty( $font_urls ) ) { return $items; } return $this->prepare_stylesheet_fonts_to_head( $font_urls, $items ); } /** * Insert font preloads into head. * * @param array $items Head elements. * @return mixed */ public function insert_font_preload_into_head( $items ) { $font_urls = $this->get_font_urls(); if ( empty( $font_urls ) ) { return $items; } if ( ! $this->is_preload_enabled() ) { return $items; } return $this->prepare_preload_fonts_to_head( $font_urls, $items ); } } Engine/Optimization/GoogleFonts/AbstractGFOptimization.php 0000644 00000010407 15174677547 0020016 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\GoogleFonts; use WP_Rocket\Engine\Common\Head\ElementTrait; use WP_Rocket\Engine\Media\Fonts\FontsTrait; /** * Abstract Optimization Parent Class for Google Fonts Optimizers. * * @since 3.8 */ abstract class AbstractGFOptimization { use FontsTrait; use ElementTrait; /** * Allowed display values. * * @since 3.8 * * @var array */ protected $display_values = [ 'auto' => 1, 'block' => 1, 'swap' => 1, 'fallback' => 1, 'optional' => 1, ]; /** * Flag for whether google fonts have been detected (Default: true) * * @since 3.8.8 * * @var bool */ protected $has_google_fonts = true; /** * Optimize Google Fonts * * @param string $html HTML content. * * @return string */ abstract public function optimize( $html ): string; /** * Check whether the optimizer has found google fonts on the page. * * @since 3.8.8 * * @return bool Will default to true when extending classes have not set via the optimize() method. */ public function has_google_fonts() { return $this->has_google_fonts; } /** * Returns font with display value. * * @since 3.8 Moved here from GoogleFonts\Combine::class * @since 3.5.1 * * @param array $font Array containing font tag and matches. * * @return string Google Font tag with display param. */ protected function get_font_with_display( array $font ) { $font_url = html_entity_decode( $font['url'] ); $query = wp_parse_url( $font_url, PHP_URL_QUERY ); if ( empty( $query ) ) { return $font[0]; } $parsed_font = wp_parse_args( $query ); $font_url = ! empty( $parsed_font['display'] ) ? str_replace( "&display={$parsed_font['display']}", '&display=swap', $font_url ) : "{$font_url}&display=swap"; return str_replace( $font['url'], esc_url( $font_url ), $font[0] ); } /** * Get the font display value. * * @since 3.8 Moved here from GoogleFonts\Combine::class * @since 3.5.1 * * @return string font display value. */ protected function get_font_display_value(): string { /** * Filters the combined Google Fonts display parameter value * * @since 3.8 Moved here from GoogleFonts\Combine::class * @since 3.3.5 * * @param string $display Display value. Can be either auto, block, swap, fallback or optional. */ $display = wpm_apply_filters_typed( 'string', 'rocket_combined_google_fonts_display', 'swap' ); return isset( $this->display_values[ $display ] ) ? $display : 'swap'; } /** * Check if preload google fonts is enabled or not using filter. * * @return bool */ protected function is_preload_enabled() { return ! wpm_apply_filters_typed( 'boolean', 'rocket_disable_google_fonts_preload', false ); } /** * Prepare preload fonts to the head items. * * @param array $fonts Fonts list. * @param array $items Head items. * @return array */ protected function prepare_preload_fonts_to_head( array $fonts, array $items ): array { foreach ( $fonts as $font_url ) { $items[] = $this->preload_link( [ 'href' => $font_url, 'as' => 'style', ] ); } return $items; } /** * Prepare stylesheets to the head. * * @param array $fonts Fonts list. * @param array $items Head items. * @return array */ protected function prepare_stylesheet_fonts_to_head( array $fonts, array $items ): array { $preload_enabled = $this->is_preload_enabled(); foreach ( $fonts as $font_url ) { $item = $this->stylesheet_link( [ 'href' => $font_url, ] ); if ( ! $preload_enabled ) { $items[] = $item; continue; } $item['media'] = 'print'; $item['onload'] = "this.media='all'"; $items[] = $item; $items[] = $this->noscript_tag( sprintf( '<link rel="stylesheet" href="%1$s">', $font_url ) // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet ); } return $items; } /** * Insert font stylesheets into head. * * @param array $items Head elements. * @return mixed */ public function insert_font_stylesheet_into_head( $items ) { return $items; } /** * Insert font preloads into head. * * @param array $items Head elements. * @return mixed */ public function insert_font_preload_into_head( $items ) { return $items; } } Engine/Optimization/GoogleFonts/CombineV2.php 0000644 00000011524 15174677547 0015214 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\GoogleFonts; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Logger\Logger; /** * Combine v2 Google Fonts * * @since 3.8 */ class CombineV2 extends AbstractGFOptimization { use RegexTrait; /** * Font urls. * * @var array */ protected $font_urls = []; /** * Combines multiple Google Fonts (API v2) links into one * * @since 3.8 * * @param string $html HTML content. * * @return string */ public function optimize( $html ): string { $this->font_urls = []; Logger::info( 'GOOGLE FONTS COMBINE-V2 PROCESS STARTED.', [ 'GF combine process' ] ); $processed_tags = []; $html_nocomments = $this->hide_comments( $html ); $font_tags = $this->find( '<link(?:\s+(?:(?!href\s*=\s*)[^>])+)?(?:\s+href\s*=\s*([\'"])(?<url>(?:https?:)?\/\/fonts\.googleapis\.com\/css2(?:(?!\1).)+)\1)(?:\s+[^>]*)?>', $html_nocomments ); if ( ! $font_tags ) { Logger::debug( 'No v2 Google Fonts found.', [ 'GF combine process' ] ); $this->has_google_fonts = false; return $html; } $this->has_google_fonts = true; $exclusions = $this->get_exclusions(); $filtered_tags = array_filter( $font_tags, function ( $tag ) use ( $exclusions ) { return ! $this->is_excluded( $tag[0], $exclusions ); } ); $num_tags = count( $filtered_tags ); Logger::debug( "Found {$num_tags} v2 Google Fonts after exclusions.", [ 'GF combine process', 'tags' => $filtered_tags, ] ); $families = []; foreach ( $filtered_tags as $tag ) { $parsed_families = $this->parse( $tag ); if ( ! empty( $parsed_families ) ) { $processed_tags[] = $tag; $families = array_merge( $families, $parsed_families ); } } if ( empty( $families ) ) { Logger::debug( 'No v2 Google Fonts left to combine.', [ 'GF combine process' ] ); return $html; } $families = array_unique( $families ); $combined_url = $this->get_combined_url( $families ); $this->font_urls[] = $combined_url; foreach ( $processed_tags as $font ) { $html = str_replace( $font[0], '', $html ); } Logger::info( 'V2 Google Fonts successfully combined.', [ 'GF combine process', 'url' => $combined_url, ] ); return $html; } /** * Parses found matches to extract fonts and subsets. * * @since 3.8 * * @param array $tag A Google Font v2 url. * * @return array */ private function parse( array $tag ): array { if ( false !== strpos( $tag['url'], 'text=' ) ) { Logger::debug( 'GOOGLEFONTS V2 COMBINE: ' . $tag['url'] . ' SKIPPED TO PRESERVE "text" ATTRIBUTE.' ); return []; } $url_pattern = '#(family=[A-Za-z0-9;:,=%&\+\@\.]+)$#'; $display_pattern = '#&display=(?:swap|auto|block|fallback|optional)#'; $decoded_url = html_entity_decode( $tag['url'] ); $query = wp_parse_url( $decoded_url, PHP_URL_QUERY ); if ( empty( $query ) ) { return []; } if ( ! preg_match_all( $url_pattern, $query, $matches, PREG_PATTERN_ORDER ) ) { return []; } $families = []; foreach ( $matches[1] as $family ) { $families[] = preg_replace( $display_pattern, '', $family ); } return $families; } /** * Returns the combined Google fonts URL * * @since 3.9.1 * * @param array $families Array with all Google V2 families. * * @return string */ private function get_combined_url( array $families ): string { $display = $this->get_font_display_value(); return esc_url( "https://fonts.googleapis.com/css2{$this->get_concatenated_families( $families )}&display={$display}" ); } /** * Get a string of the concatenated font family queries. * * @since 3.8 * * @param array $families Array with all Google V2 families. * * @return string */ private function get_concatenated_families( array $families ): string { $families_string = '?'; foreach ( $families as $family ) { $families_string .= $family . '&'; } return rtrim( $families_string, '&?' ); } /** * Get font urls, getter method for font_urls property. * * @return array */ public function get_font_urls(): array { return $this->font_urls; } /** * Insert font stylesheets into head. * * @param array $items Head elements. * @return mixed */ public function insert_font_stylesheet_into_head( $items ) { $font_urls = $this->get_font_urls(); if ( empty( $font_urls ) ) { return $items; } return $this->prepare_stylesheet_fonts_to_head( $font_urls, $items ); } /** * Insert font preloads into head. * * @param array $items Head elements. * @return mixed */ public function insert_font_preload_into_head( $items ) { $font_urls = $this->get_font_urls(); if ( empty( $font_urls ) ) { return $items; } if ( ! $this->is_preload_enabled() ) { return $items; } return $this->prepare_preload_fonts_to_head( $font_urls, $items ); } } Engine/Optimization/GoogleFonts/Admin/Subscriber.php 0000644 00000002305 15174677547 0016560 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\GoogleFonts\Admin; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Google Fonts Settings instance * * @var Settings */ private $settings; /** * Instantiate the class * * @param Settings $settings Google Fonts Settings instance. */ public function __construct( Settings $settings ) { $this->settings = $settings; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.7 * * @return array */ public static function get_subscribed_events() { return [ 'rocket_settings_tools_content' => 'display_google_fonts_enabler', 'wp_ajax_rocket_enable_google_fonts' => 'enable_google_fonts', ]; } /** * Displays the Google Fonts Optimization section in the tools tab * * @since 3.7 * * @return void */ public function display_google_fonts_enabler() { $this->settings->display_google_fonts_enabler(); } /** * Callback method for the AJAX request to enable Google Fonts Optimization * * @since 3.7 * * @return void */ public function enable_google_fonts() { $this->settings->enable_google_fonts(); } } Engine/Optimization/GoogleFonts/Admin/Settings.php 0000644 00000003726 15174677547 0016265 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\GoogleFonts\Admin; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Beacon\Beacon; class Settings extends Abstract_Render { /** * WP Rocket options instance * * @var Options_Data */ private $options; /** * Beacon instance * * @var Beacon */ private $beacon; /** * Instantiate the class * * @param Options_Data $options WP Rocket options instance. * @param Beacon $beacon Beacon instance. * @param string $template_path Path to template files. */ public function __construct( Options_Data $options, Beacon $beacon, $template_path ) { parent::__construct( $template_path ); $this->options = $options; $this->beacon = $beacon; } /** * Displays the Google Fonts Optimization section in the tools tab * * @since 3.7 * * @return void */ public function display_google_fonts_enabler() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( ! apply_filters( 'pre_get_rocket_option_minify_google_fonts', true ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound return; } if ( $this->options->get( 'minify_google_fonts', 0 ) ) { return; } $data = [ 'beacon' => $this->beacon->get_suggest( 'google_fonts' ), ]; echo $this->generate( 'settings/enable-google-fonts', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Callback method for the AJAX request to enable Google Fonts Optimization * * @since 3.7 * * @return void */ public function enable_google_fonts() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_send_json_error(); } $this->options->set( 'minify_google_fonts', 1 ); update_option( rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ), $this->options->get_options() ); wp_send_json_success(); } } Engine/Optimization/IEConditionalSubscriber.php 0000644 00000006361 15174677547 0015712 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Handles IE conditionals comments in the HTML to prevent their content from being processed during optimization. * * @since 3.6 Changes template tag to be an HTML comment. * @since 3.1 */ class IEConditionalSubscriber implements Subscriber_Interface { /** * Stores IE conditionals. * * @since 3.1 * * @var array */ private $conditionals = []; /** * HTML IE conditional pattern. * * @since 3.6.2 * * @var string */ const IE_PATTERN = '/<!--\[if[^\]]*?\]>.*?<!\[endif\]-->/is'; /** * HTML IE conditional template tag. * * @since 3.6.2 * * @var string */ const WP_ROCKET_CONDITIONAL = '<!--{{WP_ROCKET_CONDITIONAL}}-->'; /** * Return an array of events that this subscriber listens to. * * @since 3.1 * * @return array */ public static function get_subscribed_events() { return [ 'rocket_buffer' => [ [ 'extract_ie_conditionals', 1 ], [ 'inject_ie_conditionals', 34 ], ], ]; } /** * Extracts IE conditionals tags and replace them with placeholders. * * @since 3.1 * * @param string $html HTML content. * * @return string */ public function extract_ie_conditionals( $html ) { preg_match_all( self::IE_PATTERN, $html, $conditionals_match ); if ( count( $conditionals_match[0] ) === 0 ) { return $html; } foreach ( $conditionals_match[0] as $conditional ) { $this->conditionals[] = $conditional; } return preg_replace( self::IE_PATTERN, self::WP_ROCKET_CONDITIONAL, $html ); } /** * Replaces WP Rocket placeholders with IE conditional tags. * * @since 3.1 * * @param string $html HTML content. * * @return string */ public function inject_ie_conditionals( $html ) { if ( ! $this->has_conditional_tag( $html ) ) { return $html; } foreach ( $this->conditionals as $conditional ) { // Prevent scripts containing things like "\\s" to be striped of a backslash when put back in content. if ( preg_match( '@^(?<opening><!--\[if[^\]]*?\]>\s*?(?:<!-->)?\s*<script(?:\s[^>]*?>))\s*(?<content>.*?)\s*(?<closing></script>\s*(?:<!--)?\s*?<!\[endif\]-->)$@is', $conditional, $matches ) ) { $conditional = $matches['opening'] . preg_replace( '#(?<!\\\\)(\\$|\\\\)#', '\\\\$1', $matches['content'] ) . $matches['closing']; } $html = $this->replace_conditional_tag( $html, $conditional ); } return $html; } /** * Checks if the template tag for the IE conditional exists in the given HTML string. * * @since 3.6.2 * * @param string $html HTML content. * * @return bool true if at least one exists; else false. */ private function has_conditional_tag( $html ) { return ( false !== strpos( $html, self::WP_ROCKET_CONDITIONAL ) ); } /** * Replaces the template tag with the original IE conditional HTML. * * @since 3.6.2 * * @param string $html HTML content. * @param string $original Original IE conditional HTML. * * @return string */ private function replace_conditional_tag( $html, $original ) { $template_tag_position = strpos( $html, self::WP_ROCKET_CONDITIONAL ); return substr_replace( $html, $original, $template_tag_position, strlen( self::WP_ROCKET_CONDITIONAL ) ); } } Engine/Optimization/DelayJS/Subscriber.php 0000644 00000006072 15174677547 0014602 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DelayJS; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * HTML instance. * * @since 3.7 * * @var HTML */ private $html; /** * WP_Filesystem_Direct instance. * * @since 3.7 * * @var \WP_Filesystem_Direct */ private $filesystem; /** * Subscriber constructor. * * @param HTML $html HTML Instance. * @param \WP_Filesystem_Direct $filesystem The Filesystem object. */ public function __construct( HTML $html, $filesystem ) { $this->html = $html; $this->filesystem = $filesystem; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.7 * * @return array */ public static function get_subscribed_events() { return [ 'rocket_buffer' => [ [ 'delay_js', 26 ], [ 'add_delay_js_script', 26 ], ], 'pre_get_rocket_option_minify_concatenate_js' => 'maybe_disable_option', ]; } /** * Modifies scripts HTML to apply delay JS attribute * * @since 3.7 * * @param string $buffer_html Html for the page. * * @return string */ public function delay_js( $buffer_html ) { return $this->html->delay_js( $buffer_html ); } /** * Displays the inline script to the head when the option is enabled. * * @since 3.9.4 Move meta charset to head. * @since 3.9 Hooked on rocket_buffer, display the script right after <head> * @since 3.7 * * @param string $html HTML content. * * @return string */ public function add_delay_js_script( $html ): string { if ( ! $this->html->is_allowed() ) { return $html; } $pattern = '/<head[^>]*>/i'; /** * Select the version of the JS script used for delay js. * * @param string $version Version of the script. */ $version = wpm_apply_filters_typesafe( 'rocket_delay_js_version_js_script', '' ); $path_script = rocket_get_constant( 'WP_ROCKET_PATH' ) . "assets/js/lazyload-scripts$version.min.js"; if ( ! $this->filesystem->exists( $path_script ) ) { $path_script = rocket_get_constant( 'WP_ROCKET_PATH' ) . 'assets/js/lazyload-scripts.min.js'; } $lazyload_script = $this->filesystem->get_contents( $path_script ); $replaced_html = $html; if ( false !== $lazyload_script ) { $replaced_html = preg_replace( $pattern, "$0<script>{$lazyload_script}</script>", $replaced_html, 1 ); if ( empty( $replaced_html ) ) { return $html; } } $replaced_html = preg_replace( $pattern, '$0<script>' . $this->html->get_ie_fallback() . '</script>', $replaced_html, 1 ); if ( empty( $replaced_html ) ) { return $html; } return $this->html->move_meta_charset_to_head( $replaced_html ); } /** * Disables defer JS if delay JS is enabled * * @since 3.9 * * @param null $value Original value. Should be always null. * * @return null|false */ public function maybe_disable_option( $value ) { if ( $this->html->is_allowed() ) { return false; } return $value; } } Engine/Optimization/DelayJS/ServiceProvider.php 0000644 00000003353 15174677547 0015611 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DelayJS; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Optimization\DelayJS\Admin\{ Settings, SiteList, Subscriber as AdminSubscriber }; /** * Service provider for the WP Rocket Delay JS */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'delay_js_settings', 'delay_js_admin_subscriber', 'delay_js_html', 'delay_js_subscriber', 'delay_js_sitelist', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'delay_js_sitelist', SiteList::class ) ->addArguments( [ 'dynamic_lists', 'options', 'options_api', ] ); $this->getContainer()->add( 'delay_js_settings', Settings::class ) ->addArgument( 'options_api' ); $this->getContainer()->addShared( 'delay_js_admin_subscriber', AdminSubscriber::class ) ->addArguments( [ 'delay_js_settings', 'delay_js_sitelist', ] ); $this->getContainer()->add( 'delay_js_html', HTML::class ) ->addArguments( [ 'options', 'dynamic_lists_defaultlists_data_manager', 'logger', ] ); $this->getContainer()->addShared( 'delay_js_subscriber', Subscriber::class ) ->addArguments( [ 'delay_js_html', rocket_direct_filesystem(), ] ); } } Engine/Optimization/DelayJS/Admin/SiteList.php 0000644 00000040521 15174677547 0015264 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DelayJS\Admin; use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\DynamicLists\DynamicLists; use WP_Theme; class SiteList { /** * Delay JS data manager. * * @var DynamicLists */ protected $dynamic_lists; /** * Options instance. * * @var Options_Data */ protected $options; /** * Options API instance. * * @var Options */ private $options_api; /** * SiteList Constructor. * * @param DynamicLists $dynamic_lists Dynamic Lists instance. * @param Options_Data $options Options instance. * @param Options $options_api Options API instance. */ public function __construct( DynamicLists $dynamic_lists, Options_Data $options, Options $options_api ) { $this->dynamic_lists = $dynamic_lists; $this->options = $options; $this->options_api = $options_api; } /** * Check if plugin is in the list and return it if found. * * @param string $item_id Plugin ID like wp-rocket/wp-rocket.php. * * @return array */ private function get_plugin_in_list( string $item_id ) { $list = $this->dynamic_lists->get_delayjs_list(); return ! empty( $list->plugins->$item_id ) ? (array) $list->plugins->$item_id : []; } /** * Check if theme is in the list and return it if found. * * @param string $item_id Theme ID (directory name) like twentytwenty. * * @return array */ private function get_theme_in_list( string $item_id ) { $list = $this->dynamic_lists->get_delayjs_list(); return ! empty( $list->themes->$item_id ) ? (array) $list->themes->$item_id : []; } /** * Check if script is in the list and return it if found. * * @param string $item_id Script ID. * @param string $script_type Script Type. * * @return array */ private function get_script_in_list( string $item_id, string $script_type ) { $list = $this->dynamic_lists->get_delayjs_list(); $scripts = ! empty( $list->scripts->$script_type ) ? (array) $list->scripts->$script_type : []; return ! empty( $scripts[ $item_id ] ) ? (array) $scripts[ $item_id ] : []; } /** * Get Analytics scripts from the list. * * @return array */ private function get_analytics_from_list() { $list = $this->dynamic_lists->get_delayjs_list(); return ! empty( $list->scripts->analytics ) ? (array) $list->scripts->analytics : []; } /** * Get Ad Networks from the list. * * @return array */ private function get_ad_networks_from_list() { $list = $this->dynamic_lists->get_delayjs_list(); return ! empty( $list->scripts->ad_networks ) ? (array) $list->scripts->ad_networks : []; } /** * Get Payment Processors from the list. * * @return array */ private function get_payment_processors_from_list() { $list = $this->dynamic_lists->get_delayjs_list(); return ! empty( $list->scripts->payment_processors ) ? (array) $list->scripts->payment_processors : []; } /** * Get Other Services from the list. * * @return array */ private function get_other_services_from_list() { $list = $this->dynamic_lists->get_delayjs_list(); return ! empty( $list->scripts->other_services ) ? (array) $list->scripts->other_services : []; } /** * Get all plugins from the list. * * @return array */ private function get_plugins_from_list() { $list = $this->dynamic_lists->get_delayjs_list(); return ! empty( $list->plugins ) ? (array) $list->plugins : []; } /** * Get all themes from the list. * * @return array */ private function get_themes_from_list() { $list = $this->dynamic_lists->get_delayjs_list(); return ! empty( $list->themes ) ? (array) $list->themes : []; } /** * Get list of exclusions from the API list. * * @param string $item_id Item ID to get exclusions for (plugin slug, theme slug, script ID). * * @return array */ public function get_delayjs_exclusions_by_id( string $item_id ) { $item = $this->get_script_in_list( $item_id, 'analytics' ); if ( $item ) { return $item['exclusions']; } $item = $this->get_script_in_list( $item_id, 'ad_networks' ); if ( $item ) { return $item['exclusions']; } $item = $this->get_script_in_list( $item_id, 'payment_processors' ); if ( $item ) { return $item['exclusions']; } $item = $this->get_script_in_list( $item_id, 'other_services' ); if ( $item ) { return $item['exclusions']; } $item = $this->get_plugin_in_list( $item_id ); if ( $item ) { return $item['exclusions']; } $item = $this->get_theme_in_list( $item_id ); if ( $item ) { return $item['exclusions']; } return []; } /** * Get all exclusions (merged together) for a list of item IDs. * * @param array $items List of items. * * @return array */ public function get_delayjs_items_exclusions( array $items ) { if ( empty( $items ) ) { return []; } $exclusions = []; foreach ( $items as $item ) { $exclusions = array_merge( $exclusions, $this->get_delayjs_exclusions_by_id( $item ) ); } return $exclusions; } /** * Get active theme ID. * * @return string */ public function get_active_theme() { return $this->get_theme_name( wp_get_theme() ); } /** * Get active plugins (list of IDs). * * @return array */ public function get_active_plugins() { $plugins = (array) get_option( 'active_plugins', [] ); if ( ! is_multisite() ) { return $plugins; } return array_merge( $plugins, array_keys( (array) get_site_option( 'active_sitewide_plugins', [] ) ) ); } /** * Prepare the list of scripts, plugins and theme for the view. * * @return array|array[] */ public function prepare_delayjs_ui_list() { $full_list = [ 'third_parties' => [ 'analytics' => [ 'title' => __( 'Analytics & Trackers', 'rocket' ), 'items' => [], 'svg-icon' => 'analytics', ], 'ad_networks' => [ 'title' => __( 'Ad Networks', 'rocket' ), 'items' => [], 'svg-icon' => 'ad_network', ], 'payment_processors' => [ 'title' => __( 'Payment Processors', 'rocket' ), 'items' => [], 'svg-icon' => 'payment', ], 'other_services' => [ 'title' => __( 'Other Services', 'rocket' ), 'items' => [], 'svg-icon' => 'others', ], 'has_subcats' => false, ], 'wordpress' => [ 'themes' => [ 'title' => __( 'Themes', 'rocket' ), 'items' => [], 'svg-icon' => 'themes', ], 'plugins' => [ 'title' => __( 'Plugins', 'rocket' ), 'items' => [], 'svg-icon' => 'plugins', ], 'has_subcats' => false, ], ]; $has_subcats = false; // Scripts. $scripts = $this->get_analytics_from_list(); foreach ( $scripts as $script_key => $script ) { $full_list['third_parties']['analytics']['items'][] = [ 'id' => $script_key, 'title' => $script->title, 'icon' => $this->get_icon( $script ), ]; $has_subcats = ! $has_subcats ? true : $has_subcats; } $scripts = $this->get_ad_networks_from_list(); foreach ( $scripts as $script_key => $script ) { $full_list['third_parties']['ad_networks']['items'][] = [ 'id' => $script_key, 'title' => $script->title, 'icon' => $this->get_icon( $script ), ]; $has_subcats = ! $has_subcats ? true : $has_subcats; } $scripts = $this->get_payment_processors_from_list(); foreach ( $scripts as $script_key => $script ) { $full_list['third_parties']['payment_processors']['items'][] = [ 'id' => $script_key, 'title' => $script->title, 'icon' => $this->get_icon( $script ), ]; $has_subcats = ! $has_subcats ? true : $has_subcats; } $scripts = $this->get_other_services_from_list(); foreach ( $scripts as $script_key => $script ) { $full_list['third_parties']['other_services']['items'][] = [ 'id' => $script_key, 'title' => $script->title, 'icon' => $this->get_icon( $script ), ]; $has_subcats = ! $has_subcats ? true : $has_subcats; } $full_list['third_parties']['has_subcats'] = $has_subcats; $has_subcats = false; $active_theme = $this->get_active_theme(); foreach ( $this->get_themes_from_list() as $theme_key => $theme ) { if ( $theme->condition !== $active_theme ) { continue; } $full_list['wordpress']['themes']['items'][] = [ 'id' => $theme_key, 'title' => $theme->title, 'icon' => $this->get_icon( $theme ), ]; $has_subcats = ! $has_subcats ? true : $has_subcats; } $active_plugins = $this->get_active_plugins(); foreach ( $this->get_plugins_from_list() as $plugin_key => $plugin ) { if ( ! in_array( $plugin->condition, $active_plugins, true ) ) { continue; } $full_list['wordpress']['plugins']['items'][] = [ 'id' => $plugin_key, 'title' => $plugin->title, 'icon' => $this->get_icon( $plugin ), ]; $has_subcats = ! $has_subcats ? true : $has_subcats; } $full_list['wordpress']['has_subcats'] = $has_subcats; $has_subcats = false; return $full_list; } /** * Fetch the icon. * * @param object $item item from the list. * @return string */ private function get_icon( $item ) { if ( empty( $item->icon_url ) ) { return ''; } return $item->icon_url; } /** * Sanitizes delay JS options when saving the settings * * @since 3.13 * * @param array $input Array of values submitted from the form. * * @return array */ public function sanitize_options( $input ): array { if ( empty( $input['delay_js_exclusions_selected'] ) ) { $input['delay_js_exclusions_selected'] = []; $input['delay_js_exclusions_selected_exclusions'] = []; return $input; } $input['delay_js_exclusions_selected_exclusions'] = $this->get_delayjs_items_exclusions( $input['delay_js_exclusions_selected'] ); return $input; } /** * Refresh exclusions option based on selected items option. * * @return void */ public function refresh_exclusions_option() { $slug = rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ); $options = get_option( $slug, [] ); if ( empty( $options ) ) { return; } $options['delay_js_exclusions_selected_exclusions'] = $this->get_delayjs_items_exclusions( $options['delay_js_exclusions_selected'] ?? [] ); update_option( $slug, $options ); } /** * Get plugin item ids from the list using plugin base. * * @param string $plugin_base Plugin basename. * * @return string[] */ private function get_plugin_item_ids( string $plugin_base ) { $item_ids = []; foreach ( $this->get_plugins_from_list() as $plugin_key => $plugin ) { if ( $plugin_base !== $plugin->condition ) { continue; } $item_ids[ $plugin_key ] = $plugin->is_default; } return $item_ids; } /** * Add plugin exclusions only if plugin is default is checked in backend. * * @param string $plugin_base Plugin basename. * * @return void */ public function add_default_plugin_exclusions( string $plugin_base ) { $plugin_item_ids = $this->get_plugin_item_ids( $plugin_base ); if ( empty( $plugin_item_ids ) ) { return; } $selected_items = $this->options->get( 'delay_js_exclusions_selected', [] ); if ( empty( $selected_items ) ) { return; } $current_selected_items = $selected_items; foreach ( $plugin_item_ids as $plugin_item_id => $plugin_is_default ) { if ( ! $plugin_is_default ) { continue; } $selected_items[] = $plugin_item_id; } if ( $current_selected_items === $selected_items ) { return; } $this->options->set( 'delay_js_exclusions_selected', $selected_items ); $this->options->set( 'delay_js_exclusions_selected_exclusions', $this->get_delayjs_items_exclusions( $selected_items ) ); $this->options_api->set( 'settings', $this->options->get_options() ); } /** * Remove plugin selections from settings. * * @param string $plugin_base Plugin basename. * * @return void */ public function remove_plugin_selection( $plugin_base ) { $plugin_item_ids = $this->get_plugin_item_ids( $plugin_base ); if ( empty( $plugin_item_ids ) ) { return; } $selected_items = $this->options->get( 'delay_js_exclusions_selected', [] ); if ( empty( $selected_items ) ) { return; } $current_selected_items = $selected_items; foreach ( $plugin_item_ids as $plugin_item_id => $plugin_is_default ) { $selected_item_key = array_search( $plugin_item_id, $selected_items, true ); if ( false === $selected_item_key ) { continue; } unset( $selected_items[ $selected_item_key ] ); } if ( $current_selected_items === $selected_items ) { return; } $this->options->set( 'delay_js_exclusions_selected', $selected_items ); $this->options->set( 'delay_js_exclusions_selected_exclusions', $this->get_delayjs_items_exclusions( $selected_items ) ); $this->options_api->set( 'settings', $this->options->get_options() ); } /** * Get theme name from theme object. * * @param WP_Theme $theme Theme to get its name. * * @return string */ private function get_theme_name( WP_Theme $theme ) { $parent = $theme->get_template(); if ( ! empty( $parent ) ) { return $parent; } return $theme->get( 'Name' ); } /** * Get Theme item ids from the list using theme name. * * @param string $theme_name Theme name. * * @return string[] */ private function get_theme_item_ids( $theme_name ) { $item_ids = []; foreach ( $this->get_themes_from_list() as $theme_key => $theme ) { if ( $theme_name !== $theme->condition ) { continue; } $item_ids[] = $theme_key; } return $item_ids; } /** * Replace the old theme with the new theme exclusions. * * @param WP_Theme $new_theme WP_Theme instance of the new theme. * @param WP_Theme $old_theme WP_Theme instance of the old theme. * * @return void */ public function replace_theme_selection( $new_theme, $old_theme ) { $new_theme_ids = $this->get_theme_item_ids( $this->get_theme_name( $new_theme ) ); $old_theme_ids = $this->get_theme_item_ids( $this->get_theme_name( $old_theme ) ); if ( empty( $new_theme_ids ) && empty( $old_theme_ids ) ) { return; } $selected_items = $this->options->get( 'delay_js_exclusions_selected', [] ); if ( empty( $selected_items ) ) { return; } $current_selected_items = $selected_items; if ( ! empty( $old_theme_ids ) ) { foreach ( $old_theme_ids as $old_theme_id ) { $selected_item_key = array_search( $old_theme_id, $selected_items, true ); if ( false === $selected_item_key ) { continue; } unset( $selected_items[ $selected_item_key ] ); } } if ( ! empty( $new_theme_ids ) ) { $themes = $this->get_themes_from_list(); foreach ( $new_theme_ids as $new_theme_id ) { if ( ! $themes[ $new_theme_id ]->is_default ) { continue; } $selected_items[] = $new_theme_id; } } if ( $current_selected_items === $selected_items ) { return; } $this->options->set( 'delay_js_exclusions_selected', $selected_items ); $this->options->set( 'delay_js_exclusions_selected_exclusions', $this->get_delayjs_items_exclusions( $selected_items ) ); $this->options_api->set( 'settings', $this->options->get_options() ); } /** * Get default items from the list with their exclusions. * * @return array */ public function get_default_exclusions() { $items = []; $scripts = array_merge( $this->get_analytics_from_list(), $this->get_ad_networks_from_list(), $this->get_payment_processors_from_list(), $this->get_other_services_from_list() ); foreach ( $scripts as $script_key => $script ) { if ( ! $script->is_default ) { continue; } $items[ $script_key ] = $script->exclusions; } $active_plugins = $this->get_active_plugins(); foreach ( $this->get_plugins_from_list() as $plugin_key => $plugin ) { if ( ! in_array( $plugin->condition, $active_plugins, true ) || ! $plugin->is_default ) { continue; } $items[ $plugin_key ] = $plugin->exclusions; } $active_theme = $this->get_active_theme(); foreach ( $this->get_themes_from_list() as $theme_key => $theme ) { if ( $theme->condition !== $active_theme || ! $theme->is_default ) { continue; } $items[ $theme_key ] = $theme->exclusions; } /** * Filters the delay JS default exclusions list. * Key is the plugin/theme/script unique ID and value is array of exclusions * * @since 3.13 * * @param array $items Array of default excluded items. */ return apply_filters( 'rocket_delay_js_default_exclusions', $items ); } } Engine/Optimization/DelayJS/Admin/Subscriber.php 0000644 00000013472 15174677547 0015634 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DelayJS\Admin; use WP_Rocket\Engine\Admin\Settings\Settings as AdminSettings; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Theme; class Subscriber implements Subscriber_Interface { /** * Settings instance * * @var Settings */ private $settings; /** * Site List instance. * * @var SiteList */ private $site_list; /** * Instantiate the class * * @param Settings $settings Settings instance. * @param SiteList $site_list DelayJS Site List instance. */ public function __construct( Settings $settings, SiteList $site_list ) { $this->settings = $settings; $this->site_list = $site_list; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_first_install_options' => [ [ 'add_options' ], [ 'add_default_exclusions_options' ], ], 'wp_rocket_upgrade' => [ 'set_option_on_update', 13, 2 ], 'rocket_input_sanitize' => [ [ 'sanitize_options', 13, 2 ], [ 'sanitize_selected_exclusions', 14 ], ], 'pre_update_option_wp_rocket_settings' => [ 'maybe_disable_combine_js', 11, 2 ], 'rocket_after_save_dynamic_lists' => 'refresh_exclusions_option', 'activate_plugin' => 'add_plugin_exclusions', 'deactivate_plugin' => 'remove_plugin_exclusions', 'switch_theme' => [ 'handle_switch_theme_exclusions', 10, 3 ], 'rocket_meta_boxes_fields' => [ 'add_meta_box', 6 ], ]; } /** * Add the delay JS options to the WP Rocket options array * * @param array $options WP Rocket options array. * * @return array * @since 3.7 */ public function add_options( $options ): array { return $this->settings->add_options( $options ); } /** * Add the delay JS exclusions options to the WP Rocket options array * based on the default items in the list. * * @param array $options WP Rocket options array. * * @return array * @since 3.13 */ public function add_default_exclusions_options( $options ): array { $default_exclusions = $this->site_list->get_default_exclusions(); if ( empty( $default_exclusions ) ) { $options['delay_js_exclusions_selected'] = []; $options['delay_js_exclusions_selected_exclusions'] = []; return $options; } $options['delay_js_exclusions_selected'] = array_keys( $default_exclusions ); $options['delay_js_exclusions_selected_exclusions'] = array_merge( ...array_values( $default_exclusions ) ); return $options; } /** * Sets the delay_js_exclusions default value for users with delay JS enabled on upgrade * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void * @since 3.7 * * @since 3.9 Sets the delay_js_exclusions default value if delay_js is 1 */ public function set_option_on_update( $new_version, $old_version ) { $this->settings->set_option_on_update( $old_version ); } /** * Sanitizes Delay JS options values when the settings form is submitted * * @param array $input Array of values submitted from the form. * @param AdminSettings $settings Settings class instance. * * @return array * @since 3.9 */ public function sanitize_options( $input, AdminSettings $settings ): array { return $this->settings->sanitize_options( $input, $settings ); } /** * Sanitizes delay JS selected exclusions options when saving the settings. * * @since 3.13 * * @param array $input Array of values submitted from the form. * * @return array */ public function sanitize_selected_exclusions( $input ) { return $this->site_list->sanitize_options( $input ); } /** * Disable combine JS option when delay JS is enabled * * @param array $value The new, unserialized option value. * @param array $old_value The old option value. * * @return array * @since 3.9 */ public function maybe_disable_combine_js( $value, $old_value ): array { return $this->settings->maybe_disable_combine_js( $value, $old_value ); } /** * Refresh exclusions option when the dynamic list is updated weekly or manually. * * @return void */ public function refresh_exclusions_option() { $this->site_list->refresh_exclusions_option(); } /** * Remove plugin from exclusions list once deactivated. * * @param string $plugin Plugin basename. * * @return void */ public function remove_plugin_exclusions( string $plugin ) { if ( plugin_basename( WP_ROCKET_FILE ) === $plugin ) { return; } $this->site_list->remove_plugin_selection( $plugin ); } /** * Handle switch theme exclusions, remove the old theme exclusions and add the new one. * * @param string $new_name Name of the new theme. * @param WP_Theme $new_theme WP_Theme instance of the new theme. * @param WP_Theme $old_theme WP_Theme instance of the old theme. * * @return void */ public function handle_switch_theme_exclusions( string $new_name, WP_Theme $new_theme, WP_Theme $old_theme ) { $this->site_list->replace_theme_selection( $new_theme, $old_theme ); } /** * Add plugin exclusions with plugin activation for default checked plugins. * * @param string $plugin Plugin basename. * * @return void */ public function add_plugin_exclusions( string $plugin ) { if ( plugin_basename( WP_ROCKET_FILE ) === $plugin ) { return; } $this->site_list->add_default_plugin_exclusions( $plugin ); } /** * Add the field to the WP Rocket metabox on the post edit page. * * @param string[] $fields Metaboxes fields. * * @return string[] */ public function add_meta_box( array $fields ) { $fields['delay_js'] = __( 'Delay JavaScript execution', 'rocket' ); return $fields; } } Engine/Optimization/DelayJS/Admin/Settings.php 0000644 00000012644 15174677547 0015331 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DelayJS\Admin; use WP_Rocket\Admin\Options; use WP_Rocket\Engine\Admin\Settings\Settings as AdminSettings; class Settings { /** * Options instance. * * @var Options */ protected $options_api; /** * Constructor. * * @param Options $options_api Options instance. */ public function __construct( Options $options_api ) { $this->options_api = $options_api; } /** * Add the delay JS options to the WP Rocket options array * * @since 3.18 Added delay_js_execution_safe_mode. * @since 3.9 Removed delay_js_scripts key, added delay_js_exclusions. * @since 3.7 * * @param array $options WP Rocket options array. * * @return array */ public function add_options( $options ): array { $options = (array) $options; $options['delay_js'] = 0; $options['delay_js_exclusions'] = []; $options['delay_js_execution_safe_mode'] = 0; return $options; } /** * Sets the delay_js_exclusions default value for users with delay JS enabled on upgrade * * @since 3.18 Keep the custom delay js exclusions if there were before. * @since 3.9 Sets the delay_js_exclusions default value if delay_js is 1 * @since 3.7 * * @param string $old_version Previous plugin version. * * @return void */ public function set_option_on_update( $old_version ) { if ( version_compare( $old_version, '3.9', '>=' ) ) { return; } $options = $this->options_api->get( 'settings', [] ); $options['delay_js_exclusions'] = []; if ( isset( $options['delay_js'] ) && 1 === (int) $options['delay_js'] ) { $options['minify_concatenate_js'] = 0; } $this->options_api->set( 'settings', $options ); } /** * Sanitizes delay JS options when saving the settings * * @since 3.9 * @since 3.18 Deletes safe mode exclusions from `delay_js_exclusions`. * * @param array $input Array of values submitted from the form. * @param AdminSettings $settings Settings class instance. * * @return array */ public function sanitize_options( $input, $settings ): array { $input['delay_js'] = $settings->sanitize_checkbox( $input, 'delay_js' ); $input['delay_js_execution_safe_mode'] = $settings->sanitize_checkbox( $input, 'delay_js_execution_safe_mode' ); $input['delay_js_exclusions'] = ! empty( $input['delay_js_exclusions'] ) ? rocket_sanitize_textarea_field( 'delay_js_exclusions', $input['delay_js_exclusions'] ) : []; if ( ! empty( $input['delay_js_execution_safe_mode'] ) ) { $default_exclusions = self::get_safe_mode_exclusions(); $input['delay_js_exclusions'] = array_diff( $input['delay_js_exclusions'], $default_exclusions ); } return $input; } /** * Disable combine JS option when delay JS is enabled * * @since 3.9 * * @param array $value The new, unserialized option value. * @param array $old_value The old option value. * * @return array */ public function maybe_disable_combine_js( $value, $old_value ): array { if ( ! isset( $value['delay_js'], $value['minify_concatenate_js'] ) ) { return $value; } if ( 0 === $value['minify_concatenate_js'] || 0 === $value['delay_js'] ) { return $value; } if ( isset( $old_value['delay_js'], $old_value['minify_concatenate_js'] ) && $value['delay_js'] === $old_value['delay_js'] && 1 === $value['delay_js'] && 0 === $old_value['minify_concatenate_js'] ) { return $value; } $value['minify_concatenate_js'] = 0; return $value; } /** * Get the list of default exclusions for the safe mode. * * This method returns an array of regular expressions that match JavaScript files * and patterns which should be excluded from the delay JavaScript execution feature * when the safe mode is enabled. * * @since 3.18 * * @return array An array of regular expressions for safe mode exclusions. */ public static function get_safe_mode_exclusions(): array { return [ '\/jquery(-migrate)?-?([0-9.]+)?(.min|.slim|.slim.min)?.js(\?(.*))?( |\'|"|>)', 'js-(before|after)', '(?:/wp-content/|/wp-includes/)(.*)', ]; } /** * Get default exclusion list. * * @since 3.9.1 * * @return string[] */ public static function get_delay_js_default_exclusions(): array { $exclusions = self::get_safe_mode_exclusions(); $wp_content = wp_parse_url( content_url( '/' ), PHP_URL_PATH ); $wp_includes = wp_parse_url( includes_url( '/' ), PHP_URL_PATH ); $pattern = '(?:placeholder)(.*)'; $paths = []; if ( ! $wp_content && ! $wp_includes ) { return $exclusions; } if ( $wp_content ) { $paths[] = $wp_content; } if ( $wp_includes ) { $paths[] = $wp_includes; } $exclusions[] = str_replace( 'placeholder', implode( '|', $paths ), $pattern ); return $exclusions; } /** * Check if current exclusion list has the default list. * * @since 3.9.1 * * @return bool */ public static function exclusion_list_has_default(): bool { $current_list = get_rocket_option( 'delay_js_exclusions', [] ); if ( empty( $current_list ) ) { return false; } $default_list = self::get_delay_js_default_exclusions(); if ( count( $current_list ) < count( $default_list ) ) { return false; } $current_list = array_flip( $current_list ); foreach ( $default_list as $item ) { if ( ! isset( $current_list[ $item ] ) ) { return false; } } return true; } } Engine/Optimization/DelayJS/HTML.php 0000644 00000017503 15174677547 0013244 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Optimization\DelayJS; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\DelayJS\Admin\Settings; use WP_Rocket\Engine\Optimization\DynamicLists\DefaultLists\DataManager; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Engine\Support\CommentTrait; use WP_Rocket\Logger\Logger; class HTML { use RegexTrait; use CommentTrait; /** * Plugin options instance. * * @var Options_Data */ protected $options; /** * DataManager instance * * @var DataManager */ private $data_manager; /** * Array of excluded patterns from delay JS * * @since 3.9 * * @var array */ protected $excluded = []; /** * Allowed type attributes. * * @var array Array of allowed type attributes. */ private $allowed_types = [ 'text/javascript', 'module', 'application/javascript', 'application/ecmascript', 'application/x-ecmascript', 'application/x-javascript', 'text/ecmascript', 'text/javascript1.0', 'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4', 'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript', ]; /** * Logger instance. * * @var Logger */ protected $logger; /** * Creates an instance of HTML. * * @param Options_Data $options Plugin options instance. * @param DataManager $data_manager DataManager instance. * @param Logger $logger Logger instance. */ public function __construct( Options_Data $options, DataManager $data_manager, Logger $logger ) { $this->options = $options; $this->data_manager = $data_manager; $this->logger = $logger; } /** * Adjust HTML to have delay js structure. * * @since 3.9 Updated to use exclusions list instead of inclusions list. * @since 3.7 * * @param string $html Buffer html for the page. * * @return string */ public function delay_js( $html ): string { if ( ! $this->is_allowed() ) { return $html; } $this->set_exclusions(); $this->excluded = array_merge( $this->excluded, $this->options->get( 'delay_js_exclusions', [] ) ); $this->excluded = array_merge( $this->excluded, $this->options->get( 'delay_js_exclusions_selected_exclusions', [] ) ); if ( $this->options->get( 'delay_js_execution_safe_mode', 0 ) ) { $this->excluded = array_merge( $this->excluded, Settings::get_safe_mode_exclusions() ); } /** * Filters the delay JS exclusions array * * @since 3.9 * * @param array $excluded Array of excluded patterns. */ $this->excluded = wpm_apply_filters_typed( 'string[]', 'rocket_delay_js_exclusions', $this->excluded ); $this->excluded = array_map( function ( $value ) { if ( ! is_string( $value ) ) { $value = (string) $value; } return str_replace( [ '+', '?ver', '#' ], [ '\+', '\?ver', '\#' ], $value ); }, $this->excluded ); $html = $this->parse( $html ); return $this->add_meta_comment( 'delay_js', $html ); } /** * Checks if is allowed to Delay JS. * * @since 3.7 * * @return bool */ public function is_allowed(): bool { if ( rocket_bypass() ) { return false; } if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return false; } if ( is_rocket_post_excluded_option( 'delay_js' ) ) { return false; } return (bool) $this->options->get( 'delay_js', 0 ); } /** * Gets Javascript to redirect IE visitors to the uncached page * * @since 3.9 * * @return string */ public function get_ie_fallback(): string { return 'if(navigator.userAgent.match(/MSIE|Internet Explorer/i)||navigator.userAgent.match(/Trident\/7\..*?rv:11/i)){var href=document.location.href;if(!href.match(/[?&]nowprocket/)){if(href.indexOf("?")==-1){if(href.indexOf("#")==-1){document.location.href=href+"?nowprocket=1"}else{document.location.href=href.replace("#","?nowprocket=1#")}}else{if(href.indexOf("#")==-1){document.location.href=href+"&nowprocket=1"}else{document.location.href=href.replace("#","&nowprocket=1#")}}}}'; } /** * Parse the html and add/remove attributes from specific scripts. * * @since 3.7 * * @param string $html Buffer html for the page. * * @return string */ private function parse( $html ): string { if ( empty( $html ) ) { return $html; } $result = $this->replace_xmp_tags( $html ); $result = $this->replace_svg_tags( $result ); $replaced_html = preg_replace_callback( '/<\s*script(?<attr>\s*[^>]*?)?>(?<content>.*?)?<\s*\/\s*script\s*>/ims', [ $this, 'replace_scripts', ], $result ); if ( empty( $replaced_html ) ) { return $html; } $replaced_html = $this->restore_xmp_tags( $replaced_html ); return $this->restore_svg_tags( $replaced_html ); } /** * Callback method for preg_replace_callback that is used to adjust attributes for scripts. * * @since 3.9 Use exclusions list & fake type attribute. * @since 3.7 * * @param array $matches Matches array for scripts regex. * * @return string */ public function replace_scripts( $matches ): string { foreach ( $this->excluded as $pattern ) { if ( preg_match( "#{$pattern}#i", $matches[0] ) ) { $this->logger->debug( "DelayJS: Script {$matches[0]} excluded by $pattern" ); return $matches[0]; } } if ( empty( $matches['attr'] ) ) { return '<script type="rocketlazyloadscript">' . $matches['content'] . '</script>'; } $type_regex = '/type\s*=\s*(["\'])(?<type>.*)\1/i'; preg_match( $type_regex . 'U', $matches['attr'], $type_matches ); if ( ! empty( $type_matches ) && ! empty( trim( $type_matches['type'] ) ) && ! in_array( trim( $type_matches['type'] ), $this->allowed_types, true ) ) { return $matches[0]; } $matches['attr'] = preg_replace( $type_regex, 'data-rocket-type=$1$2$1', $matches['attr'] ); // To remove type attribute without any value. $matches['attr'] = preg_replace( '/(\s+type\s+)|(^type\s+)|(\s+type$)/i', '', $matches['attr'] ); // Checks if script has src attribute so then treat as external script and replace src with data-rocket-src. $src_regex = '/src\s*=\s*(["\'])(.*)\1/i'; $matches['attr'] = preg_replace( $src_regex, 'data-rocket-src=$1$2$1', $matches['attr'] ); return '<script type="rocketlazyloadscript" ' . trim( $matches['attr'] ) . '>' . $matches['content'] . '</script>'; } /** * Move meta charset to head if not found to the top of page content. * * @since 3.9.4 * * @param string $html Html content. * * @return string */ public function move_meta_charset_to_head( $html ): string { $meta_pattern = "#<meta[^h]*(http-equiv[^=]*=[^\'\"]*[\'\" ]Content-Type[\'\"][ ]*[^>]*|)(charset[^=]*=[ ]*[\'\" ]*[^\'\"> ][^\'\">]+[^\'\"> ][\'\" ]*|charset[^=]*=*[^\'\"> ][^\'\">]+[^\'\"> ])([^>]*|)>(?=.*</head>)#Usmi"; if ( ! preg_match( $meta_pattern, $html, $matches ) ) { return $html; } $replaced_html = preg_replace( "$meta_pattern", '', $html ); if ( empty( $replaced_html ) ) { return $html; } if ( preg_match( '/<head\b/i', $replaced_html ) ) { $replaced_html = preg_replace( '/(<head\b[^>]*?>)/i', "\${1}{$matches[0]}", $replaced_html, 1 ); if ( empty( $replaced_html ) ) { return $html; } return $replaced_html; } if ( preg_match( '/<html\b/i', $replaced_html ) ) { $replaced_html = preg_replace( '/(<html\b[^>]*?>)/i', "\${1}{$matches[0]}", $replaced_html, 1 ); if ( empty( $replaced_html ) ) { return $html; } return $replaced_html; } $replaced_html = preg_replace( '/(<\w+)/', "{$matches[0]}\${1}", $replaced_html, 1 ); if ( empty( $replaced_html ) ) { return $html; } return $replaced_html; } /** * Get exclusions * * @return void */ private function set_exclusions() { $lists = $this->data_manager->get_lists(); $this->excluded = isset( $lists->delay_js_exclusions ) ? $lists->delay_js_exclusions : []; } } Engine/Optimization/DynamicLists/AbstractAPIClient.php 0000644 00000005705 15174677547 0017045 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists; use WP_Error; use WP_Rocket\Admin\Options_Data; abstract class AbstractAPIClient { /** * API URL. */ const API_URL = 'https://b.rucss.wp-rocket.me/api/v2/'; /** * Response Code. * * @var int */ private $response_code = 200; /** * Error message. * * @var string */ private $error_message = ''; /** * Response Body. * * @var string */ private $response_body = ''; /** * Plugin options instance. * * @var Options_Data */ private $options; /** * Instantiate the class. * * @param Options_Data $options Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Specify API endpoint path. * * @return string */ abstract protected function get_api_path(); /** * Get exclusions list. * * @param string $hash Hash of lists content to compare. * * @return array */ public function get_exclusions_list( $hash ) { $args = [ 'body' => [ 'hash' => $hash, ], 'timeout' => 5, ]; if ( ! $this->handle_request( $args ) ) { return [ 'code' => $this->response_code, 'message' => $this->error_message, ]; } return [ 'code' => $this->response_code, 'body' => $this->response_body, ]; } /** * Handle the request. * * @param array $args Passed arguments. * * @return bool */ private function handle_request( array $args ) { $use_old_api_url = false; // Handle logic to set API Url to old version when rolling back to versions < 3.18. if ( version_compare( rocket_get_constant( 'WP_ROCKET_LASTVERSION' ), '3.18', '<' ) && 'rocket_before_rollback' === current_action() ) { $use_old_api_url = true; } $api_url = rocket_get_constant( 'WP_ROCKET_EXCLUSIONS_API_URL', false ) ? rocket_get_constant( 'WP_ROCKET_EXCLUSIONS_API_URL', false ) : self::API_URL; $api_url = $use_old_api_url ? str_replace( 'v2/', '', $api_url ) : $api_url; if ( empty( $args['body'] ) ) { $args['body'] = []; } $args['body']['credentials'] = [ 'wpr_email' => $this->options->get( 'consumer_email', '' ), 'wpr_key' => $this->options->get( 'consumer_key', '' ), ]; $response = wp_remote_get( $api_url . $this->get_api_path(), $args ); return $this->check_response( $response ); } /** * Handle SaaS request error. * * @param array|WP_Error $response WP Remote request. * * @return bool */ private function check_response( $response ): bool { $this->response_code = is_array( $response ) ? wp_remote_retrieve_response_code( $response ) : $response->get_error_code(); if ( 200 !== $this->response_code && 206 !== $this->response_code ) { $this->error_message = is_array( $response ) ? wp_remote_retrieve_response_message( $response ) : $response->get_error_message(); return false; } $this->response_body = wp_remote_retrieve_body( $response ); return true; } } Engine/Optimization/DynamicLists/Subscriber.php 0000644 00000015617 15174677547 0015717 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * DynamicLists instance * * @var DynamicLists */ private $dynamic_lists; /** * Instantiate the class * * @param DynamicLists $dynamic_lists DynamicLists instance. */ public function __construct( DynamicLists $dynamic_lists ) { $this->dynamic_lists = $dynamic_lists; } /** * Events this subscriber listens to. * * @return array */ public static function get_subscribed_events() { return [ 'rest_api_init' => 'register_rest_route', 'rocket_localize_admin_script' => [ 'add_dynamic_lists_script', 11 ], 'init' => 'schedule_lists_update', 'rocket_update_dynamic_lists' => 'update_lists', 'rocket_deactivation' => 'clear_schedule_lists_update', 'rocket_settings_tools_content' => 'display_update_lists_section', 'rocket_cache_ignored_parameters' => 'add_cache_ignored_parameters', 'rocket_minify_excluded_external_js' => 'add_minify_excluded_external_js', 'rocket_move_after_combine_js' => 'add_move_after_combine_js', 'rocket_excluded_inline_js_content' => 'add_combine_js_excluded_inline', 'rocket_preload_exclude_urls' => 'add_preload_exclusions', 'rocket_exclude_js' => 'add_js_exclude_files', 'rocket_plugins_to_deactivate' => 'add_incompatible_plugins_to_deactivate', 'rocket_staging_list' => 'add_staging_exclusions', 'rocket_lrc_exclusions' => 'add_lrc_exclusions', 'rocket_mixpanel_tracked_options' => 'add_mixpanel_tracked_options', 'wp_rocket_upgrade' => 'update_lists_from_files', 'rocket_before_rollback' => 'maybe_update_lists', 'rocket_exclude_locally_host_fonts' => 'add_media_fonts_exclusions', ]; } /** * Registers the REST dynamic lists update route * * @return void */ public function register_rest_route() { $this->dynamic_lists->register_rest_route(); } /** * Add REST data to our localize script data. * * @param array $data Localize script data. * @return array */ public function add_dynamic_lists_script( $data ) { $data['rest_url'] = rest_url( 'wp-rocket/v1/dynamic_lists/update/?_locale=user' ); $data['rest_nonce'] = wp_create_nonce( 'wp_rest' ); return $data; } /** * Scheduling the dynamic lists update cron event. */ public function schedule_lists_update() { $this->dynamic_lists->schedule_lists_update(); } /** * Clear the dynamic lists update cron event. * * @return void */ public function clear_schedule_lists_update() { $this->dynamic_lists->clear_schedule_lists_update(); } /** * Update dynamic lists from API. * * * @return void */ public function update_lists() { $this->dynamic_lists->update_lists_from_remote(); } /** * Displays the dynamic lists update section on tools tab * * @return void */ public function display_update_lists_section() { $this->dynamic_lists->display_update_lists_section(); } /** * Add the cached ignored parameters to the array * * @param array $params Array of ignored parameters. * * @return array */ public function add_cache_ignored_parameters( array $params = [] ): array { return array_merge( $params, $this->dynamic_lists->get_cache_ignored_parameters() ); } /** * Add the excluded external JS patterns to the array * * @param array $excluded Array of excluded patterns. * * @return array */ public function add_minify_excluded_external_js( array $excluded = [] ): array { return array_merge( $excluded, $this->dynamic_lists->get_js_minify_excluded_external() ); } /** * Add the JS patterns to move after the combine JS file to the array * * @param array $excluded Array of patterns to move. * * @return array */ public function add_move_after_combine_js( array $excluded = [] ): array { return array_merge( $excluded, $this->dynamic_lists->get_js_move_after_combine() ); } /** * Add the excluded inline JS patterns to the array * * @param array $excluded Array of excluded patterns. * * @return array */ public function add_combine_js_excluded_inline( array $excluded = [] ): array { return array_merge( $excluded, $this->dynamic_lists->get_combine_js_excluded_inline() ); } /** * Add the preload exclusions to the array * * @param array $excluded Array of ignored URL regex. * * @return array */ public function add_preload_exclusions( array $excluded = [] ): array { return array_merge( $excluded, $this->dynamic_lists->get_preload_exclusions() ); } /** * Add the js files exclusions to the array * * @param array $js_files Array of files. * * @return array */ public function add_js_exclude_files( array $js_files = [] ): array { return array_merge( $js_files, $this->dynamic_lists->get_js_exclude_files() ); } /** * Add incompatible plugins to the array * * @param array $plugins Array of $plugins. * * @return array */ public function add_incompatible_plugins_to_deactivate( $plugins = [] ): array { return array_merge( (array) $plugins, $this->dynamic_lists->get_incompatible_plugins() ); } /** * Add the staging exclusions to the array * * @param array $stagings Array of staging urls. * * @return array */ public function add_staging_exclusions( $stagings = [] ): array { return array_merge( (array) $stagings, (array) $this->dynamic_lists->get_stagings() ); } /** * Add the LRC exclusions to the array * * @param array $exclusions Array of LRC exclusions. * * @return array */ public function add_lrc_exclusions( $exclusions ): array { return array_merge( (array) $exclusions, $this->dynamic_lists->get_lrc_exclusions() ); } /** * Update dynamic lists from JSON files. * * @return void */ public function update_lists_from_files() { $this->dynamic_lists->update_lists_from_files(); } /** * Update dynamic lists during rollback to versions < 3.18. * * @return void */ public function maybe_update_lists(): void { if ( version_compare( rocket_get_constant( 'WP_ROCKET_LASTVERSION' ), '3.18', '>=' ) ) { return; } $this->dynamic_lists->update_lists_from_remote(); } /** * Add the media fonts exclusion to the array * * @param array $exclusions Array of Media fonts exclusions. * * @return array */ public function add_media_fonts_exclusions( array $exclusions ): array { return array_merge( (array) $exclusions, $this->dynamic_lists->get_exclude_media_fonts() ); } /** * Add the MixPanel tracked options to the array * * @param array $options Array of tracked options. * * @return array */ public function add_mixpanel_tracked_options( array $options ): array { return array_unique( array_merge( (array) $options, $this->dynamic_lists->get_mixpanel_tracked_options() ) ); } } Engine/Optimization/DynamicLists/ServiceProvider.php 0000644 00000006617 15174677547 0016727 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Optimization\DynamicLists\DefaultLists\{ APIClient as DefaultListsAPIClient, DataManager as DefaultListsDataManager }; use WP_Rocket\Engine\Optimization\DynamicLists\DelayJSLists\{ APIClient as DelayJSListsAPIClient, DataManager as DelayJSListsDataManager }; use WP_Rocket\Engine\Optimization\DynamicLists\IncompatiblePluginsLists\{ APIClient as IncompatiblePluginsListsAPIClient, DataManager as IncompatiblePluginsListsDataManager }; /** * Service provider for the WP Rocket DynamicLists */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'dynamic_lists_defaultlists_data_manager', 'dynamic_lists_defaultlists_api_client', 'dynamic_lists_delayjslists_data_manager', 'dynamic_lists_delayjslists_api_client', 'dynamic_lists_incompatible_plugins_lists_data_manager', 'dynamic_lists_incompatible_plugins_lists_api_client', 'dynamic_lists', 'dynamic_lists_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the option array in the container * * @return void */ public function register(): void { $this->getContainer()->add( 'dynamic_lists_defaultlists_data_manager', DefaultListsDataManager::class ); $this->getContainer()->add( 'dynamic_lists_defaultlists_api_client', DefaultListsAPIClient::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'dynamic_lists_delayjslists_data_manager', DelayJSListsDataManager::class ); $this->getContainer()->add( 'dynamic_lists_delayjslists_api_client', DelayJSListsAPIClient::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'dynamic_lists_incompatible_plugins_lists_data_manager', IncompatiblePluginsListsDataManager::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'dynamic_lists_incompatible_plugins_lists_api_client', IncompatiblePluginsListsAPIClient::class ) ->addArgument( 'options' ); $providers = [ 'defaultlists' => (object) [ 'api_client' => $this->getContainer()->get( 'dynamic_lists_defaultlists_api_client' ), 'data_manager' => $this->getContainer()->get( 'dynamic_lists_defaultlists_data_manager' ), ], 'delayjslists' => (object) [ 'api_client' => $this->getContainer()->get( 'dynamic_lists_delayjslists_api_client' ), 'data_manager' => $this->getContainer()->get( 'dynamic_lists_delayjslists_data_manager' ), ], 'incompatible_plugins' => (object) [ 'api_client' => $this->getContainer()->get( 'dynamic_lists_incompatible_plugins_lists_api_client' ), 'data_manager' => $this->getContainer()->get( 'dynamic_lists_incompatible_plugins_lists_data_manager' ), 'clear_cache' => false, ], ]; $this->getContainer()->add( 'dynamic_lists', DynamicLists::class ) ->addArguments( [ $providers, 'user', 'template_path', 'beacon', ] ); $this->getContainer()->addShared( 'dynamic_lists_subscriber', Subscriber::class ) ->addArgument( 'dynamic_lists' ); } } Engine/Optimization/DynamicLists/DynamicLists.php 0000644 00000020560 15174677547 0016210 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists; use WP_Rocket\Abstract_Render; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Engine\License\API\User; use WP_REST_Response; use WP_Error; class DynamicLists extends Abstract_Render { /** * Providers array. * Array of objects with keys: api_client and data_manager. * * @var array */ private $providers; /** * User instance * * @var User */ private $user; /** * Beacon instance * * @var Beacon */ private $beacon; /** * Route Rest API namespace. */ const ROUTE_NAMESPACE = 'wp-rocket/v1'; /** * Instantiate the class. * * @param array $providers Lists providers. * @param User $user User instance. * @param string $template_path Path to views. * @param Beacon $beacon Beacon instance. */ public function __construct( array $providers, User $user, $template_path, Beacon $beacon ) { parent::__construct( $template_path ); $this->providers = $providers; $this->user = $user; $this->beacon = $beacon; } /** * Registers the dynamic lists update route * * @return void */ public function register_rest_route() { register_rest_route( self::ROUTE_NAMESPACE, 'dynamic_lists/update', [ 'methods' => 'PUT', 'callback' => [ $this, 'rest_update_response' ], 'permission_callback' => [ $this, 'check_permissions' ], ] ); } /** * Checks user's permissions. This is a callback registered to REST route's "permission_callback" parameter. * * @return bool true if the user has permission; else false. */ public function check_permissions() { return current_user_can( 'rocket_manage_options' ); } /** * Returns the update response * * @return WP_REST_Response|WP_Error */ public function rest_update_response() { return rest_ensure_response( $this->update_lists_from_remote() ); } /** * Updates the lists from remote * * @return array */ public function update_lists_from_remote() { if ( $this->user->is_license_expired() ) { return [ 'success' => false, 'data' => '', 'message' => __( 'You need an active license to get the latest version of the lists from our server.', 'rocket' ), ]; } $response = []; $success = false; $should_purge = false; $titles = [ 'defaultlists' => __( 'Default Lists', 'rocket' ), 'delayjslists' => __( 'Delay JavaScript Execution Exclusion Lists', 'rocket' ), 'incompatible_plugins' => __( 'Incompatible plugins Lists', 'rocket' ), ]; foreach ( $this->providers as $provider_id => $provider ) { $result = $provider->api_client->get_exclusions_list( $provider->data_manager->get_lists_hash() ); if ( empty( $result['code'] ) || empty( $result['body'] ) ) { $response[ $titles[ $provider_id ] ] = [ 'success' => false, 'data' => '', 'message' => __( 'Could not get updated lists from server.', 'rocket' ), ]; continue; } if ( 206 === $result['code'] ) { $response[ $titles[ $provider_id ] ] = [ 'success' => true, 'data' => '', 'message' => __( 'Lists are up to date.', 'rocket' ), ]; continue; } if ( ! $provider->data_manager->save_dynamic_lists( $result['body'] ) ) { $response[ $titles[ $provider_id ] ] = [ 'success' => false, 'data' => '', 'message' => __( 'Could not update lists.', 'rocket' ), ]; continue; } $success = true; $response[ $titles[ $provider_id ] ] = [ 'success' => true, 'data' => '', 'message' => __( 'Lists are successfully updated.', 'rocket' ), ]; $should_purge |= $provider->clear_cache ?? true; } if ( $success ) { /** * Fires after saving all dynamic lists files. * * @since 3.12.1 * * @param bool $should_purge Should purge status based on the updated providers. */ do_action( 'rocket_after_save_dynamic_lists', $should_purge ); } return $response; } /** * Schedule cron to update dynamic lists weekly. * * @return void */ public function schedule_lists_update() { if ( ! wp_next_scheduled( 'rocket_update_dynamic_lists' ) ) { wp_schedule_event( time(), 'weekly', 'rocket_update_dynamic_lists' ); } } /** * Clear dynamic lists update event. */ public function clear_schedule_lists_update() { wp_clear_scheduled_hook( 'rocket_update_dynamic_lists' ); } /** * Displays the dynamic lists update section on tools tab * * @return void */ public function display_update_lists_section() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } $data = [ 'beacon' => $this->beacon->get_suggest( 'dynamic_lists' ), ]; echo $this->generate( 'settings/dynamic-lists-update', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Get the cached ignored parameters * * @return array */ public function get_cache_ignored_parameters(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return isset( $lists->cache_ignored_parameters ) ? array_flip( $lists->cache_ignored_parameters ) : []; } /** * Get the JS minify excluded external paths * * @return array */ public function get_js_minify_excluded_external(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return isset( $lists->js_minify_external ) ? $lists->js_minify_external : []; } /** * Get the patterns to move after the combine JS file * * @return array */ public function get_js_move_after_combine(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return isset( $lists->js_move_after_combine ) ? $lists->js_move_after_combine : []; } /** * Get the inline JS excluded from combine JS * * @return array */ public function get_combine_js_excluded_inline(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return isset( $lists->js_excluded_inline ) ? $lists->js_excluded_inline : []; } /** * Get the preload exclusions * * @return array */ public function get_preload_exclusions(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return isset( $lists->preload_exclusions ) ? $lists->preload_exclusions : []; } /** * Get Delay JS dynamic list. * * @return object */ public function get_delayjs_list() { return $this->providers['delayjslists']->data_manager->get_lists(); } /** * Get the JS minify excluded files * * @return array */ public function get_js_exclude_files(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return isset( $lists->exclude_js_files ) ? $lists->exclude_js_files : []; } /** * Get the incompatible plugins list * * @return array */ public function get_incompatible_plugins() { $lists = $this->providers['incompatible_plugins']->data_manager->get_plugins_list(); return isset( $lists ) ? $lists : []; } /** * Get the staging list * * @return array */ public function get_stagings(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return isset( $lists->staging_domains ) ? $lists->staging_domains : []; } /** * Get the JS minify excluded templates * * @return array */ public function get_exclude_js_templates(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return $lists->exclude_js_template ?? []; } /** * Get the lazy rendered exclusions. * * @return array */ public function get_lrc_exclusions(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return $lists->lazy_rendering_exclusions ?? []; } /** * Updates the lists from JSON files * * @return void */ public function update_lists_from_files() { foreach ( $this->providers as $provider ) { $provider->data_manager->remove_lists_cache(); $provider->data_manager->get_lists(); } } /** * Get the host fonts excluded templates * * @return array */ public function get_exclude_media_fonts(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return $lists->host_fonts ?? []; } /** * Get the MixPanel tracked options * * @return array */ public function get_mixpanel_tracked_options(): array { $lists = $this->providers['defaultlists']->data_manager->get_lists(); return $lists->mixpanel_tracked_settings ?? []; } } Engine/Optimization/DynamicLists/DefaultLists/APIClient.php 0000644 00000000532 15174677547 0017755 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists\DefaultLists; use WP_Rocket\Engine\Optimization\DynamicLists\AbstractAPIClient; class APIClient extends AbstractAPIClient { /** * Specify API endpoint path. * * @return string */ protected function get_api_path() { return 'exclusions/list'; } } Engine/Optimization/DynamicLists/DefaultLists/DataManager.php 0000644 00000000763 15174677547 0020357 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists\DefaultLists; use WP_Rocket\Engine\Optimization\DynamicLists\AbstractDataManager; class DataManager extends AbstractDataManager { /** * Get cache transient name. * * @return string */ protected function get_cache_transient_name() { return 'wpr_dynamic_lists'; } /** * Get lists json filename. * * @return string */ protected function get_json_filename() { return 'dynamic-lists'; } } Engine/Optimization/DynamicLists/DelayJSLists/APIClient.php 0000644 00000000543 15174677547 0017666 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists\DelayJSLists; use WP_Rocket\Engine\Optimization\DynamicLists\AbstractAPIClient; class APIClient extends AbstractAPIClient { /** * Specify API endpoint path. * * @return string */ protected function get_api_path() { return 'delay-js-exclusions/list'; } } Engine/Optimization/DynamicLists/DelayJSLists/DataManager.php 0000644 00000001003 15174677547 0020252 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists\DelayJSLists; use WP_Rocket\Engine\Optimization\DynamicLists\AbstractDataManager; class DataManager extends AbstractDataManager { /** * Get cache transient name. * * @return string */ protected function get_cache_transient_name() { return 'wpr_dynamic_lists_delayjs'; } /** * Get lists json filename. * * @return string */ protected function get_json_filename() { return 'dynamic-lists-delayjs'; } } Engine/Optimization/DynamicLists/AbstractDataManager.php 0000644 00000006777 15174677547 0017453 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists; use WP_Filesystem_Direct; use StdClass; abstract class AbstractDataManager { /** * Filesystem instance * * @var WP_Filesystem_Direct */ private $filesystem; /** * Cache ttl. * * @var int */ protected $cache_duration = WEEK_IN_SECONDS; /** * Instantiate the class * * @param WP_Filesystem_Direct $filesystem Filesystem instance. */ public function __construct( $filesystem = null ) { $this->filesystem = is_null( $filesystem ) ? rocket_direct_filesystem() : $filesystem; } /** * Get cache transient name. * * @return string */ abstract protected function get_cache_transient_name(); /** * Get lists json filename. * * @return string */ abstract protected function get_json_filename(); /** * Gets the lists content * * @return object */ public function get_lists() { $transient = get_transient( $this->get_cache_transient_name() ); if ( false !== $transient ) { return $transient; } $json = $this->get_lists_from_file(); $lists = json_decode( $json ); if ( empty( $lists ) ) { return new StdClass(); } $this->set_lists_cache( $lists ); return $lists; } /** * Returns the hash of the current JSON * * @return string */ public function get_lists_hash() { return md5( $this->get_lists_from_file() ); } /** * Save dynamic lists on file & transient * * @param string $content Lists content. * * @return boolean */ public function save_dynamic_lists( string $content ) { $result = $this->put_lists_to_file( $content ); $lists = json_decode( $content ); $this->set_lists_cache( $lists ); return $result; } /** * Gets the path to the dynamic lists JSON file * * @return string */ private function get_json_filepath(): string { return rocket_get_constant( 'WP_ROCKET_CONFIG_PATH', '' ) . $this->get_json_filename() . '.json'; } /** * Gets lists JSON content from file * * @return string */ private function get_lists_from_file(): string { $content = ''; $lists_filepath = $this->get_json_filepath(); if ( $this->filesystem->exists( $lists_filepath ) ) { $content = $this->filesystem->get_contents( $lists_filepath ); } if ( ! empty( $content ) ) { return $content; } $fallback_filepath = rocket_get_constant( 'WP_ROCKET_PATH', '' ) . $this->get_json_filename() . '.json'; if ( $this->filesystem->exists( $fallback_filepath ) ) { $content = $this->filesystem->get_contents( $fallback_filepath ); } if ( ! empty( $content ) ) { $this->put_lists_to_file( $content ); return $content; } return $content; } /** * Write lists content to JSON file * * @param string $content JSON content. * * @return bool */ private function put_lists_to_file( string $content ): bool { return $this->filesystem->put_contents( $this->get_json_filepath(), $content, rocket_get_filesystem_perms( 'file' ) ); } /** * Sets transient for lists content * * @param object $content Lists content. * * @return void */ private function set_lists_cache( $content ) { set_transient( $this->get_cache_transient_name(), $content, $this->cache_duration ); } /** * Removes the lists cache * * @return void */ public function remove_lists_cache() { delete_transient( $this->get_cache_transient_name() ); $lists_filepath = $this->get_json_filepath(); if ( ! $this->filesystem->exists( $lists_filepath ) ) { return; } $this->filesystem->delete( $lists_filepath ); } } Engine/Optimization/DynamicLists/IncompatiblePluginsLists/APIClient.php 0000644 00000000560 15174677547 0022342 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists\IncompatiblePluginsLists; use WP_Rocket\Engine\Optimization\DynamicLists\AbstractAPIClient; class APIClient extends AbstractAPIClient { /** * Specify API endpoint path. * * @return string */ protected function get_api_path() { return 'incompatible-plugins/list'; } } Engine/Optimization/DynamicLists/IncompatiblePluginsLists/DataManager.php 0000644 00000003404 15174677547 0022736 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DynamicLists\IncompatiblePluginsLists; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\DynamicLists\AbstractDataManager; class DataManager extends AbstractDataManager { /** * Plugin options instance. * * @var Options_Data */ private $options; /** * Instantiate the class. * * @param Options_Data $options Options instance. */ public function __construct( Options_Data $options ) { parent::__construct(); $this->options = $options; } /** * Get cache transient name. * * @return string */ protected function get_cache_transient_name() { return 'wpr_dynamic_lists_incompatible_plugins'; } /** * Get lists json filename. * * @return string */ protected function get_json_filename() { return 'dynamic-lists-incompatible-plugins'; } /** * Gets the plugins list content * * @return array */ public function get_plugins_list() { $lists = []; $list_from_json = $this->get_lists(); foreach ( $list_from_json as $conditions => $list ) { if ( $this->meet_conditions( $conditions ) ) { $list = array_column( $list, 'file', 'slug' ); $lists = array_merge( $lists, $list ); } } return $lists; } /** * Check if the condition is meet based on plugin option and condition string. * If $conditions contain "||" split and treat it like or * * @param string $conditions condition. * * @return bool */ private function meet_conditions( $conditions = '' ) { if ( empty( $conditions ) ) { return true; } $conditions = explode( '||', $conditions ); foreach ( $conditions as $condition ) { if ( $this->options->get( trim( $condition ), false ) ) { return true; } } return false; } } Engine/Optimization/ServiceProvider.php 0000644 00000005144 15174677547 0014316 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Optimization\Buffer\Optimization; use WP_Rocket\Engine\Optimization\Buffer\Subscriber as BufferSubscriber; use WP_Rocket\Engine\Optimization\GoogleFonts\{Combine, CombineV2, Subscriber}; /** * Service provider for the WP Rocket optimizations */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'buffer_optimization', 'buffer_subscriber', 'cache_dynamic_resource', 'ie_conditionals_subscriber', 'optimize_google_fonts', 'optimize_google_fonts_v2', 'combine_google_fonts_subscriber', 'minify_css_subscriber', 'minify_js_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $filesystem = rocket_direct_filesystem(); $this->getContainer()->add( 'buffer_optimization', Optimization::class ) ->addArgument( 'tests' ); $this->getContainer()->addShared( 'buffer_subscriber', BufferSubscriber::class ) ->addArgument( 'buffer_optimization' ); $this->getContainer()->addShared( 'cache_dynamic_resource', CacheDynamicResource::class ) ->addArguments( [ 'options', new StringArgument( rocket_get_constant( 'WP_ROCKET_CACHE_BUSTING_PATH', '' ) ), new StringArgument( rocket_get_constant( 'WP_ROCKET_CACHE_BUSTING_URL', '' ) ), ] ); $this->getContainer()->add( 'optimize_google_fonts', Combine::class ); $this->getContainer()->add( 'optimize_google_fonts_v2', CombineV2::class ); $this->getContainer()->addShared( 'combine_google_fonts_subscriber', Subscriber::class ) ->addArguments( [ 'optimize_google_fonts', 'optimize_google_fonts_v2', 'options', ] ); $this->getContainer()->addShared( 'minify_css_subscriber', Minify\CSS\Subscriber::class ) ->addArguments( [ 'options', $filesystem, ] ); $this->getContainer()->addShared( 'minify_js_subscriber', Minify\JS\Subscriber::class ) ->addArguments( [ 'options', $filesystem, ] ); $this->getContainer()->addShared( 'ie_conditionals_subscriber', IEConditionalSubscriber::class ); } } Engine/Optimization/LazyRenderContent/ServiceProvider.php 0000644 00000005115 15174677547 0017726 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Optimization\LazyRenderContent\Context\Context; use WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Table\LazyRenderContent as LRCTable; use WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Queries\LazyRenderContent as LRCQuery; use WP_Rocket\Engine\Optimization\LazyRenderContent\AJAX\Controller as AJAXController; use WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend\{Controller as FrontController, Subscriber as FrontSubscriber}; use WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend\Processor\Processor; class ServiceProvider extends AbstractServiceProvider { /** * The provides array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ 'lrc_context', 'lrc_factory', 'lrc_table', 'lrc_query', 'lrc_ajax_controller', 'lrc_frontend_processor', 'lrc_frontend_controller', 'lrc_frontend_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the classes in the container * * @return void */ public function register(): void { $this->getContainer()->add( 'lrc_context', Context::class ); $this->getContainer()->addShared( 'lrc_table', LRCTable::class ); $this->getContainer()->add( 'lrc_query', LRCQuery::class ); $this->getContainer()->add( 'lrc_ajax_controller', AJAXController::class ) ->addArguments( [ 'lrc_query', 'lrc_context', ] ); $this->getContainer()->add( 'lrc_frontend_processor', Processor::class ); $this->getContainer()->add( 'lrc_frontend_controller', FrontController::class ) ->addArguments( [ 'lrc_frontend_processor', 'lrc_context', ] ); $this->getContainer()->addShared( 'lrc_frontend_subscriber', FrontSubscriber::class ) ->addArguments( [ $this->getContainer()->get( 'lrc_frontend_controller' ), ] ); $this->getContainer()->addShared( 'lrc_factory', Factory::class ) ->addArguments( [ 'lrc_context', 'lrc_table', 'lrc_query', 'lrc_ajax_controller', 'lrc_frontend_controller', ] ); } } Engine/Optimization/LazyRenderContent/AJAX/Controller.php 0000644 00000010342 15174677547 0017457 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\AJAX; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\AJAXControllerTrait; use WP_Rocket\Engine\Optimization\UrlTrait; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\ControllerInterface; use WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Queries\LazyRenderContent as LRCQuery; class Controller implements ControllerInterface { use UrlTrait; use AJAXControllerTrait; /** * LRCQuery instance * * @var LRCQuery */ private $query; /** * LRC Context. * * @var ContextInterface */ protected $context; /** * Constructor * * @param LRCQuery $query LRCQuery instance. * @param ContextInterface $context Context interface. */ public function __construct( LRCQuery $query, ContextInterface $context ) { $this->query = $query; $this->context = $context; } /** * Add LRC data to the database * * @return array */ public function add_data(): array { $payload = []; check_ajax_referer( 'rocket_beacon', 'rocket_beacon_nonce' ); if ( ! $this->context->is_allowed() ) { $payload['lrc'] = 'not allowed'; return $payload; } $url = isset( $_POST['url'] ) ? untrailingslashit( esc_url_raw( wp_unslash( $_POST['url'] ) ) ) : ''; $is_mobile = isset( $_POST['is_mobile'] ) ? filter_var( wp_unslash( $_POST['is_mobile'] ), FILTER_VALIDATE_BOOLEAN ) : false; $results = isset( $_POST['results'] ) ? json_decode( wp_unslash( $_POST['results'] ) ) : (object) [ 'lrc' => [] ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $hashes = $results->lrc ?? []; $below_the_fold = []; /** * Filters the maximum number of LRC hashes being saved into the database. * * @param int $max_number Maximum number to allow. * @param string $url Current page url. * @param string[]|array $hashes Current list of LRC hashes. */ $max_lrc_hashes_number = wpm_apply_filters_typed( 'integer', 'rocket_lrc_hashes_number', 20, $url, $hashes ); if ( 0 >= $max_lrc_hashes_number ) { $max_lrc_hashes_number = 1; } foreach ( (array) $hashes as $hash ) { $below_the_fold[] = sanitize_text_field( wp_unslash( $hash ) ); --$max_lrc_hashes_number; } $row = $this->query->get_row( $url, $is_mobile ); if ( ! empty( $row ) ) { $payload['lrc'] = 'item already in the database'; return $payload; } $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : ''; list( $status_code, $status_message ) = $this->get_status_code_message( $status ); $item = [ 'url' => $url, 'is_mobile' => $is_mobile, 'status' => $status_code, 'error_message' => $status_message, 'below_the_fold' => wp_json_encode( $below_the_fold ), 'last_accessed' => current_time( 'mysql', true ), 'created_at' => current_time( 'mysql', true ), ]; $result = $this->query->add_item( $item ); $payload['lrc'] = $item; if ( ! $result ) { $payload['lrc'] = 'error when adding the entry to the database'; } return $payload; } /** * Checks if there is existing data for the current URL and device type from the beacon script. * * This method is called via AJAX. It checks if there is existing LRC data for the current URL and device type. * If the data exists, it returns a JSON success response with true. If the data does not exist, it returns a JSON success response with false. * If the context is not allowed, it returns a JSON error response with false. * * @return array */ public function check_data(): array { $payload = [ 'lrc' => false, ]; check_ajax_referer( 'rocket_beacon', 'rocket_beacon_nonce' ); if ( ! $this->context->is_allowed() ) { $payload['lrc'] = true; return $payload; } $url = isset( $_POST['url'] ) ? untrailingslashit( esc_url_raw( wp_unslash( $_POST['url'] ) ) ) : ''; $is_mobile = isset( $_POST['is_mobile'] ) ? filter_var( wp_unslash( $_POST['is_mobile'] ), FILTER_VALIDATE_BOOLEAN ) : false; $row = $this->query->get_row( $url, $is_mobile ); if ( ! empty( $row ) ) { $payload['lrc'] = true; } return $payload; } } Engine/Optimization/LazyRenderContent/Database/Queries/LazyRenderContent.php 0000644 00000004571 15174677547 0023353 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Queries; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\AbstractQueries; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\QueriesInterface; use WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Schema\LazyRenderContent as LRCSchema; use WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Rows\LazyRenderContent as LRCRow; class LazyRenderContent extends AbstractQueries implements QueriesInterface { /** * Name of the database table to query. * * @var string */ protected $table_name = 'wpr_lazy_render_content'; /** * String used to alias the database table in MySQL statement. * * Keep this short, but descriptive. I.E. "tr" for term relationships. * * This is used to avoid collisions with JOINs. * * @var string */ protected $table_alias = 'wpr_lrc'; /** * Name of class used to setup the database schema. * * @var string */ protected $table_schema = LRCSchema::class; /** Item ******************************************************************/ /** * Name for a single item. * * Use underscores between words. I.E. "term_relationship" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name = 'lazy_render_content'; /** * Plural version for a group of items. * * Use underscores between words. I.E. "term_relationships" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name_plural = 'lazy_render_content'; /** * Name of class used to turn IDs into first-class objects. * * This is used when looping through return values to guarantee their shape. * * @var mixed */ protected $item_shape = LRCRow::class; /** * Delete all rows which were not accessed in the last month. * * @return bool|int */ public function delete_old_rows() { // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $delete_interval = $this->cleanup_interval; $prefixed_table_name = $db->prefix . $this->table_name; $query = "DELETE FROM `$prefixed_table_name` WHERE status = 'failed' OR `last_accessed` <= date_sub(now(), interval $delete_interval month)"; return $db->query( $query ); } } Engine/Optimization/LazyRenderContent/Database/Rows/LazyRenderContent.php 0000644 00000003773 15174677547 0022673 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Rows; use WP_Rocket\Dependencies\BerlinDB\Database\Row; class LazyRenderContent extends Row { /** * Row ID * * @var int */ public $id; /** * URL * * @var string */ public $url; /** * Is for mobile * * @var bool */ public $is_mobile; /** * Below the fold * * @var string */ public $below_the_fold; /** * Error message * * @var string */ public $error_message; /** * Status * * @var string */ public $status; /** * Last modified time * * @var int */ public $modified; /** * Last accessed time * * @var int */ public $last_accessed; /** * Created time * * @var int */ public $created_at; /** * Constructor. * * @param mixed $item Object Row. */ public function __construct( $item ) { parent::__construct( $item ); // Set the type of each column, and prepare. $this->id = (int) $this->id; $this->url = (string) $this->url; $this->is_mobile = (bool) $this->is_mobile; $this->below_the_fold = (string) $this->below_the_fold; $this->error_message = (string) $this->error_message; $this->status = (string) $this->status; $this->modified = empty( $this->modified ) ? 0 : strtotime( (string) $this->modified ); $this->last_accessed = empty( $this->last_accessed ) ? 0 : strtotime( (string) $this->last_accessed ); $this->created_at = empty( $this->created_at ) ? 0 : strtotime( (string) $this->created_at ); } /** * Checks if the object has a valid LRC (Lazy Render Content) value. * * @return bool Returns true if the object's status is 'completed' and the Below the fold value is not empty or '[]', false otherwise. */ public function has_lrc() { if ( 'completed' !== $this->status ) { return false; } if ( empty( $this->below_the_fold ) ) { return false; } if ( '[]' === $this->below_the_fold ) { return false; } return true; } } Engine/Optimization/LazyRenderContent/Database/Schema/LazyRenderContent.php 0000644 00000004206 15174677547 0023131 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Schema; use WP_Rocket\Dependencies\BerlinDB\Database\Schema; class LazyRenderContent extends Schema { /** * Array of database column objects * * @var array */ public $columns = [ // ID column. [ 'name' => 'id', 'type' => 'bigint', 'length' => '20', 'unsigned' => true, 'extra' => 'auto_increment', 'primary' => true, 'sortable' => true, ], // URL column. [ 'name' => 'url', 'type' => 'varchar', 'length' => '2000', 'default' => '', 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // Below the fold column. [ 'name' => 'below_the_fold', 'type' => 'longtext', 'default' => '', 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // IS_MOBILE column. [ 'name' => 'is_mobile', 'type' => 'tinyint', 'length' => '1', 'default' => 0, 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // error_message column. [ 'name' => 'error_message', 'type' => 'longtext', 'default' => null, 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // STATUS column. [ 'name' => 'status', 'type' => 'varchar', 'length' => '255', 'default' => null, 'cache_key' => true, 'searchable' => true, 'sortable' => false, ], // MODIFIED column. [ 'name' => 'modified', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], // LAST_ACCESSED column. [ 'name' => 'last_accessed', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], // CREATED_AT column. [ 'name' => 'created_at', 'type' => 'timestamp', 'default' => null, 'created' => true, 'date_query' => true, 'sortable' => true, ], ]; } Engine/Optimization/LazyRenderContent/Database/Table/LazyRenderContent.php 0000644 00000002672 15174677547 0022765 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Table; use WP_Rocket\Engine\Common\PerformanceHints\Database\Table\AbstractTable; class LazyRenderContent extends AbstractTable { /** * Table name * * @var string */ protected $name = 'wpr_lazy_render_content'; /** * Database version key (saved in _options or _sitemeta) * * @var string */ protected $db_version_key = 'wpr_lazy_render_content_version'; /** * Database version * * @var int */ protected $version = 20240812; /** * Key => value array of versions => methods. * * @var array */ protected $upgrades = []; /** * Table schema data. * * @var string */ protected $schema_data = " id bigint(20) unsigned NOT NULL AUTO_INCREMENT, url varchar(2000) NOT NULL default '', is_mobile tinyint(1) NOT NULL default 0, below_the_fold longtext default '', error_message longtext default '', status varchar(255) NOT NULL default '', modified timestamp NOT NULL default '0000-00-00 00:00:00', last_accessed timestamp NOT NULL default '0000-00-00 00:00:00', created_at timestamp NULL, PRIMARY KEY (id), KEY url (url(150), is_mobile), KEY modified (modified), KEY last_accessed (last_accessed), INDEX `status_index` (`status`(191))"; } Engine/Optimization/LazyRenderContent/Activation/ActivationFactory.php 0000644 00000001330 15174677547 0022340 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Activation; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Common\PerformanceHints\ActivationFactoryInterface; class ActivationFactory implements ActivationFactoryInterface { /** * Context instance. * * @var ContextInterface */ protected $context; /** * Instantiate the class. * * @param ContextInterface $context LRC Context instance. */ public function __construct( ContextInterface $context ) { $this->context = $context; } /** * Provides a Context object. * * @return ContextInterface */ public function get_context(): ContextInterface { return $this->context; } } Engine/Optimization/LazyRenderContent/Frontend/Subscriber.php 0000644 00000002365 15174677547 0020501 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * LazyRenderContent controller. * * @var Controller */ private $controller; /** * Subscriber constructor. * * @param Controller $controller LazyRenderContent controller. */ public function __construct( Controller $controller ) { $this->controller = $controller; } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_buffer' => [ 'add_hashes_when_allowed', 16 ], 'rocket_performance_hints_buffer' => [ 'add_hashes', 16 ], ]; } /** * Add hashes to the HTML elements if allowed * * @param string $html The HTML content. * * @return string */ public function add_hashes_when_allowed( $html ) { return $this->controller->add_hashes_when_allowed( $html ); } /** * Add hashes to the HTML elements * * @param string $html The HTML content. * * @return string */ public function add_hashes( $html ) { return $this->controller->add_hashes( $html ); } } Engine/Optimization/LazyRenderContent/Frontend/Processor/Processor.php 0000644 00000001473 15174677547 0022333 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend\Processor; class Processor { /** * The processor to use. * * @var ProcessorInterface */ private $processor; /** * Set the processor to use. * * @param string $processor The processor to use. * * @return void */ public function set_processor( $processor ): void { switch ( $processor ) { case 'dom': $this->processor = new Dom(); break; case 'simplehtmldom': $this->processor = new SimpleHtmlDom(); break; case 'regex': $this->processor = new Regex(); break; default: $this->processor = new Dom(); } } /** * Get the processor. * * @return ProcessorInterface */ public function get_processor(): ProcessorInterface { return $this->processor; } } Engine/Optimization/LazyRenderContent/Frontend/Processor/ProcessorInterface.php 0000644 00000000773 15174677547 0024156 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend\Processor; interface ProcessorInterface { /** * Add hashes to the HTML elements * * @param string $html The HTML content. * * @return string */ public function add_hashes( $html ); /** * Sets the exclusions list * * @param string[] $exclusions The list of patterns to exclude from hash injection. * * @return void */ public function set_exclusions( array $exclusions ): void; } Engine/Optimization/LazyRenderContent/Frontend/Processor/Regex.php 0000644 00000004757 15174677547 0021436 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend\Processor; use WP_Rocket\Logger\Logger; class Regex implements ProcessorInterface { use HelperTrait; /** * Number of injects hashes. * * @since 3.17 * * @var int */ private $count; /** * Maximum number of hashes to inject. * * @since 3.17 * * @var int */ private $max_hashes; /** * Array of patterns to exclude from hash injection. * * @since 3.17.0.2 * * @var array */ private $exclusions; // @phpstan-ignore-line /** * Sets the exclusions list * * @param string[] $exclusions The list of patterns to exclude from hash injection. * * @return void */ public function set_exclusions( $exclusions ): void { $this->exclusions = $exclusions; } /** * Add hashes to the HTML elements * * @param string $html The HTML content. * * @return string */ public function add_hashes( $html ) { $result = preg_match( '/(?><body[^>]*>)(?>.*?<\/body>)/is', $html, $matches ); if ( ! $result ) { Logger::error( 'Body element not found in the HTML content.', [ 'LazyRenderContent' ] ); return $html; } $this->max_hashes = $this->get_max_tags(); $this->count = 0; return $this->add_hash_to_element( $html, $matches[0] ); } /** * Add a hash to the element and its children. * * @param string $html The HTML content. * @param string $element The element to add the hash to. * * @return string */ private function add_hash_to_element( $html, $element ) { $processed_tags = $this->get_processed_tags(); $result = preg_match_all( '/(?><(' . implode( '|', $processed_tags ) . ')[^>]*>)/is', $element, $matches, PREG_SET_ORDER ); if ( ! $result ) { Logger::error( 'No elements found in the HTML content.', [ 'LazyRenderContent' ] ); return $html; } foreach ( $matches as $child ) { if ( $this->count >= $this->max_hashes ) { Logger::warning( 'Stopping LRC hash injection as max_hashes is reached.', [ 'LazyRenderContent' ] ); return $html; } // Calculate the hash of the opening tag. $opening_tag_html = strstr( $child[0], '>', true ) . '>'; $hash = md5( $opening_tag_html . $this->count ); ++$this->count; // Add the data-rocket-location-hash attribute. $replace = preg_replace( '/' . $child[1] . '/is', '$0 data-rocket-location-hash="' . $hash . '"', $child[0], 1 ); $html = preg_replace( '/' . preg_quote( $child[0], '/' ) . '/', $replace, $html, 1 ); } return $html; } } Engine/Optimization/LazyRenderContent/Frontend/Processor/HelperTrait.php 0000644 00000002752 15174677547 0022600 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend\Processor; trait HelperTrait { /** * Get filtered elements depth. * * @since 3.17 * * @return int */ protected function get_depth() { /** * Filter the depth integer value. * The actual applied depth in the source is the default + 1 after the body or rocket_lrc_depth+1 after the body if the filter is set * * @param int $depth Depth value. */ return wpm_apply_filters_typed( 'integer', 'rocket_lrc_depth', 2 ); } /** * Get filtered element maximum count. * * @since 3.17 * * @return int */ protected function get_max_tags() { /** * Filter the maximal number of processed tags. * High values allow to process more elements but expose to a risk of performance issue because of the regex replacement process. * * @param int $depth Depth value. */ return wpm_apply_filters_typed( 'integer', 'rocket_lrc_max_hashes', 200 ); } /** * Get processed tags. * * @return array|string[] */ protected function get_processed_tags() { /** * Filter the processed element tags. * * @since 3.17 * * @param array|string[] $tags Tags to be processed. */ $tags = wpm_apply_filters_typed( 'array', 'rocket_lrc_processed_tags', [ 'DIV', 'MAIN', 'FOOTER', 'SECTION', 'ARTICLE', 'HEADER', ] ); /** * Convert tags to upper case here before */ return array_map( 'strtoupper', $tags ); } } Engine/Optimization/LazyRenderContent/Frontend/Processor/Dom.php 0000644 00000012165 15174677547 0021073 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend\Processor; use DOMDocument; use WP_Rocket\Logger\Logger; class Dom implements ProcessorInterface { use HelperTrait; /** * Number of injects hashes. * * @since 3.17 * * @var int */ private $count; /** * Maximum number of hashes to inject. * * @since 3.17 * * @var int */ private $max_hashes; /** * Array of patterns to exclude from hash injection. * * @since 3.17.0.2 * * @var array */ private $exclusions = []; /** * Sets the exclusions list * * @param string[] $exclusions The list of patterns to exclude from hash injection. * * @return void */ public function set_exclusions( $exclusions ): void { $this->exclusions = $exclusions; } /** * Gets the exclusions pattern * * @return string */ private function get_exclusions_pattern(): string { if ( empty( $this->exclusions ) ) { return ''; } $exclusions = array_map( function ( $exclusion ) { return preg_quote( $exclusion, '/' ); }, $this->exclusions ); return implode( '|', $exclusions ); } /** * Add hashes to the HTML elements * * @param string $html The HTML content. * * @return string */ public function add_hashes( $html ) { $internal_errors = libxml_use_internal_errors( true ); // Load HTML into DOMDocument. $dom = new DOMDocument(); if ( ! $dom->loadHTML( $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ) ) { foreach ( libxml_get_errors() as $error ) { Logger::error( $error->message, [ 'LazyRenderContent' ] ); } libxml_clear_errors(); return $html; } libxml_use_internal_errors( $internal_errors ); // Get the body element. $body = $dom->getElementsByTagName( 'body' )->item( 0 ); if ( ! $body ) { Logger::error( 'Body element not found in the HTML content.', [ 'LazyRenderContent' ] ); return $html; } $this->max_hashes = $this->get_max_tags(); $this->count = 0; return $this->add_hash_to_element( $body, $this->get_depth(), $html ); } /** * Add a hash to the element and its children. * * @param \DOMElement $element The element to add the hash to. * @param int $depth The depth of the recursion. * @param string $html The HTML content. * * @return string */ private function add_hash_to_element( $element, $depth, $html ) { if ( $depth < 0 ) { return $html; } $processed_tags = $this->get_processed_tags(); foreach ( $element->childNodes as $child ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( $this->count >= $this->max_hashes ) { Logger::warning( 'Stopping LRC hash injection as max_hashes is reached.', [ 'LazyRenderContent' ] ); return $html; } if ( ! $child instanceof \DOMElement ) { continue; } // Check if the element contains an SVG use element and skip it. if ( $child->getElementsByTagName( 'use' )->length > 0 ) { continue; } if ( XML_ELEMENT_NODE !== $child->nodeType // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase || ! $child->tagName // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase || ! in_array( strtoupper( $child->tagName ), $processed_tags, true ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ) { continue; } // Calculate the hash of the opening tag. $child_html = $child->ownerDocument->saveHTML( $child ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $opening_tag_html = strstr( $child_html, '>', true ) . '>'; $exclusions_pattern = $this->get_exclusions_pattern(); if ( ! empty( $exclusions_pattern ) && preg_match( '/(' . $exclusions_pattern . ')/i', $opening_tag_html ) ) { continue; } $hash = md5( $opening_tag_html . $this->count ); ++$this->count; // Inject the hash as an attribute in the opening tag. $replace = preg_replace( '/' . $child->tagName . '/is', '$0 data-rocket-location-hash="' . $hash . '"', $opening_tag_html, 1 ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( is_null( $replace ) ) { continue; } // Replace the opening tag in the HTML by the manipulated one // If DOMDocument automatically modified the original element, we might not find it in the HTML. // Known issue: if there is an element with the exact same opening tag before in the HTML that did not receive a hash, it will replaced instead of the correct element in the HTML. $element_replacements = 0; $modified_html = preg_replace( '/' . preg_quote( $opening_tag_html, '/' ) . '/', $replace, $html, 1, $element_replacements ); if ( $element_replacements < 1 ) { Logger::warning( 'Opening tag from DOMDocument not found in original HTML.', [ 'LazyRenderContent' ] ); } if ( is_null( $modified_html ) ) { continue; } $html = $modified_html; // Recursively process child elements. $html = $this->add_hash_to_element( $child, $depth - 1, $html ); } return $html; } } Engine/Optimization/LazyRenderContent/Frontend/Processor/SimpleHtmlDom.php 0000644 00000005057 15174677547 0023074 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend\Processor; use voku\helper\HtmlDomParser; use voku\helper\SimpleHtmlDomBlank; use voku\helper\SimpleHtmlDomInterface; use WP_Rocket\Logger\Logger; class SimpleHtmlDom implements ProcessorInterface { use HelperTrait; /** * Number of injects hashes. * * @since 3.17 * * @var int */ private $count; /** * Maximum number of hashes to inject. * * @since 3.17 * * @var int */ private $max_hashes; /** * Array of patterns to exclude from hash injection. * * @since 3.17.0.2 * * @var array */ private $exclusions; // @phpstan-ignore-line /** * Sets the exclusions list * * @param string[] $exclusions The list of patterns to exclude from hash injection. * * @return void */ public function set_exclusions( $exclusions ): void { $this->exclusions = $exclusions; } /** * Add hashes to the HTML elements * * @param string $html The HTML content. * * @return string */ public function add_hashes( $html ) { $dom = HtmlDomParser::str_get_html( $html ); $body = $dom->getElementByTagName( 'body' ); if ( $body instanceof SimpleHtmlDomBlank ) { Logger::error( 'Body element not found in the HTML content.', [ 'LazyRenderContent' ] ); return $html; } $this->max_hashes = $this->get_max_tags(); $this->count = 0; $this->add_hash_to_element( $body, $this->get_depth() ); return $dom->save(); } /** * Add a hash to the element and its children. * * @param SimpleHtmlDomInterface $element The element to add the hash to. * @param int $depth The depth of the recursion. */ private function add_hash_to_element( $element, $depth ) { if ( $depth < 0 ) { return; } $processed_tags = $this->get_processed_tags(); foreach ( $element->childNodes() as $child ) { if ( $this->count >= $this->max_hashes ) { Logger::warning( 'Stopping LRC hash injection as max_hashes is reached.', [ 'LazyRenderContent' ] ); return; } if ( ! in_array( strtoupper( $child->getTag() ), $processed_tags, true ) ) { continue; } // Calculate the hash of the opening tag. $child_html = $child->html(); $opening_tag_html = strstr( $child_html, '>', true ) . '>'; $hash = md5( $opening_tag_html . $this->count ); ++$this->count; // Add the data-rocket-location-hash attribute. $child->setAttribute( 'data-rocket-location-hash', $hash ); // Recursively process child elements. $this->add_hash_to_element( $child, $depth - 1 ); } } } Engine/Optimization/LazyRenderContent/Frontend/Controller.php 0000644 00000010521 15174677547 0020512 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface; use WP_Rocket\Engine\Optimization\LazyRenderContent\Frontend\Processor\Processor; use WP_Rocket\Engine\Support\CommentTrait; class Controller implements ControllerInterface { use CommentTrait; /** * Processor instance * * @var Processor */ private $processor; /** * Context instance * * @var ContextInterface */ private $context; /** * Constructor * * @param Processor $processor Processor instance. * @param ContextInterface $context Context instance. */ public function __construct( Processor $processor, ContextInterface $context ) { $this->processor = $processor; $this->context = $context; } /** * Applies optimization. * * @param string $html HTML content. * @param object $row Database Row. * * @return string */ public function optimize( string $html, $row ): string { if ( rocket_bypass() && $row->has_lrc() ) { return $this->remove_hashes( $html ); } if ( ! $row->has_lrc() ) { return $this->remove_hashes( $html ); } $hashes = json_decode( $row->below_the_fold ); if ( ! is_array( $hashes ) ) { return $this->remove_hashes( $html ); } $result = preg_replace( '/data-rocket-location-hash="(?:' . implode( '|', $hashes ) . ')"/i', 'data-wpr-lazyrender="1"', $html, -1, $count ); if ( null === $result || 0 === $count ) { return $this->remove_hashes( $html ); } $html = $result; $html = $this->remove_hashes( $html ); $html = $this->add_css( $html ); return $this->add_meta_comment( 'automatic_lazy_rendering', $html ); } /** * Remove hashes from the HTML content. * * @param string $html The HTML content. * * @return string */ private function remove_hashes( $html ) { $result = preg_replace( '/data-rocket-location-hash="[^"]*"/i', '', $html ); if ( null === $result ) { return $html; } return $result; } /** * Add CSS to the HTML content. * * @param string $html The HTML content. * * @return string */ private function add_css( $html ) { $css = '<style id="rocket-lazyrender-inline-css">[data-wpr-lazyrender] {content-visibility: auto;}</style>'; $result = preg_replace( '/<\/head>/i', $css . '</head>', $html, 1 ); if ( null === $result ) { return $html; } return $result; } /** * Add hashes to the HTML elements if allowed * * @param string $html The HTML content. * * @return string */ public function add_hashes_when_allowed( $html ) { if ( ! $this->context->is_allowed() ) { return $html; } return $this->add_hashes( $html ); } /** * Add hashes to the HTML elements * * @param string $html The HTML content. * * @return string */ public function add_hashes( $html ) { if ( empty( $html ) ) { return $html; } /** * Filters the Lazy Render Content processor to use. * * @since 3.17 * * @param string $processor The processor to use. */ $processor = wpm_apply_filters_typed( 'string', 'rocket_lrc_processor', extension_loaded( 'dom' ) ? 'dom' : 'regex' ); $this->processor->set_processor( $processor ); $this->processor->get_processor()->set_exclusions( $this->get_exclusions() ); return $this->processor->get_processor()->add_hashes( $html ); } /** * Get the list of patterns to exclude from hash injection. * * @return string[] */ private function get_exclusions(): array { /** * Filters the list of patterns to exclude from hash injection. * * @since 3.17.0.2 * * @param string[] $exclusions The list of patterns to exclude from hash injection. */ $exclusions = wpm_apply_filters_typed( 'string[]', 'rocket_lrc_exclusions', [] ); return $exclusions; } /** * Add custom data like the List of elements to be considered for optimization. * * @param array $data Array of data passed in beacon. * * @return array */ public function add_custom_data( array $data ): array { $data['status']['lrc'] = $this->context->is_allowed(); /** * Filters the LRC threshold * * @since 3.17 * * @param int $threshold The LRC threshold value. */ $data['lrc_threshold'] = wpm_apply_filters_typed( 'integer', 'rocket_lrc_threshold', 1800 ); return $data; } } Engine/Optimization/LazyRenderContent/Factory.php 0000644 00000005632 15174677547 0016226 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent; use WP_Rocket\Engine\Common\PerformanceHints\Cron\CronTrait; use WP_Rocket\Engine\Common\PerformanceHints\FactoryInterface; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\ControllerInterface as AjaxControllerInterface; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface as FrontendControllerInterface; use WP_Rocket\Engine\Common\PerformanceHints\Database\Table\TableInterface; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\QueriesInterface; use WP_Rocket\Engine\Common\Context\ContextInterface; class Factory implements FactoryInterface { use CronTrait; /** * Ajax Controller instance. * * @var AjaxControllerInterface */ protected $ajax_controller; /** * Frontend Controller instance. * * @var FrontendControllerInterface */ protected $frontend_controller; /** * Table instance. * * @var TableInterface */ protected $table; /** * Queries instance. * * @var QueriesInterface */ protected $queries; /** * Context instance. * * @var ContextInterface */ protected $context; /** * Instantiate the class. * * @param ContextInterface $context LRC Context instance. * @param TableInterface $table LRC Table instance. * @param QueriesInterface $queries LRC Queries instance. * @param AjaxControllerInterface $ajax_controller LRC AJAX Controller instance. * @param FrontendControllerInterface $frontend_controller LRC Frontend Controller instance. */ public function __construct( ContextInterface $context, TableInterface $table, QueriesInterface $queries, AjaxControllerInterface $ajax_controller, FrontendControllerInterface $frontend_controller ) { $this->context = $context; $this->table = $table; $this->queries = $queries; $this->ajax_controller = $ajax_controller; $this->frontend_controller = $frontend_controller; } /** * Provides an Ajax controller object. * * @return AjaxControllerInterface */ public function get_ajax_controller(): AjaxControllerInterface { return $this->ajax_controller; } /** * Provides a Frontend object. * * @return FrontendControllerInterface */ public function get_frontend_controller(): FrontendControllerInterface { return $this->frontend_controller; } /** * Provides a Table object. * * @return TableInterface */ public function table(): TableInterface { return $this->table; } /** * Provides a Queries object. * * @return QueriesInterface */ public function queries(): QueriesInterface { // Defines the interval for deletion and returns Queries object. return $this->deletion_interval( 'rocket_lrc_cleanup_interval' ); } /** * Provides a Context object. * * @return ContextInterface */ public function get_context(): ContextInterface { return $this->context; } } Engine/Optimization/LazyRenderContent/Context/Context.php 0000644 00000001226 15174677547 0017662 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\LazyRenderContent\Context; use WP_Rocket\Engine\Common\Context\ContextInterface; class Context implements ContextInterface { /** * Determine if the action is allowed. * * @param array $data Data to pass to the context. * @return bool */ public function is_allowed( array $data = [] ): bool { if ( get_option( 'wp_rocket_no_licence' ) ) { return false; } /** * Filters to manage lazy render content optimization * * @param bool $allow True to allow, false otherwise. */ return wpm_apply_filters_typed( 'boolean', 'rocket_lrc_optimization', true ); } } Engine/Optimization/DeferJS/Subscriber.php 0000644 00000003171 15174677547 0014566 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DeferJS; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * DeferJS instance * * @var DeferJS */ private $defer_js; /** * Instantiate the class * * @param DeferJS $defer_js DeferJS instance. */ public function __construct( DeferJS $defer_js ) { $this->defer_js = $defer_js; } /** * Returns array of events this listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_buffer' => [ [ 'defer_js', 24 ], [ 'defer_inline_js', 25 ], ], 'rocket_exclude_js' => 'exclude_jquery_combine', 'rocket_minify_excluded_external_js' => 'exclude_jquery_combine', ]; } /** * Adds the defer attribute to JS files * * @since 3.8 * * @param string $html HTML content. * @return string */ public function defer_js( string $html ): string { return $this->defer_js->defer_js( $html ); } /** * Defers inline JS containing jQuery calls * * @since 3.8 * * @param string $html HTML content. * @return string */ public function defer_inline_js( string $html ): string { return $this->defer_js->defer_inline_js( $html ); } /** * Excludes jQuery from combine JS when defer and combine are enabled * * @since 3.8 * * @param array $excluded_files Array of excluded files from combine JS. * @return array */ public function exclude_jquery_combine( array $excluded_files ): array { return $this->defer_js->exclude_jquery_combine( $excluded_files ); } } Engine/Optimization/DeferJS/DeferJS.php 0000644 00000017403 15174677547 0013750 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DeferJS; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\DynamicLists\DefaultLists\DataManager; use WP_Rocket\Engine\Support\CommentTrait; class DeferJS { use CommentTrait; /** * Options instance * * @var Options_Data */ private $options; /** * DataManager instance * * @var DataManager */ private $data_manager; /** * Instantiate the class * * @param Options_Data $options Options instance. * @param DataManager $data_manager DataManager instance. */ public function __construct( Options_Data $options, DataManager $data_manager ) { $this->options = $options; $this->data_manager = $data_manager; } /** * Add the exclude defer JS option in WP Rocket options array * * @since 3.8 * * @param array $options WP Rocket options array. * @return array */ public function add_option( array $options ): array { $options['exclude_defer_js'] = []; return $options; } /** * Defer all JS files. * * @since 3.8 * * @param string $html HTML content. * @return string */ public function defer_js( string $html ): string { if ( ! $this->can_defer_js() ) { return $html; } $buffer_nocomments = preg_replace( '/<!--(.*)-->/Uis', '', $html ); preg_match_all( '#<script\s+(?:[^>]+[\s\'"])?src\s*=\s*[\'"]\s*?(?<url>[^\'"]+)\s*?[\'"](?:[^>]+)?\/?>#i', $buffer_nocomments, $matches, PREG_SET_ORDER ); if ( empty( $matches ) ) { return $html; } $exclude_defer_js = implode( '|', $this->get_excluded() ); if ( ! @preg_replace( '#(' . $exclude_defer_js . ')#i', '', 'dummy-string' ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged return $html; } foreach ( $matches as $tag ) { if ( preg_match( '#(' . $exclude_defer_js . ')#i', $tag['url'] ) ) { continue; } if ( preg_match( '/\s+(?:async|defer)/i', $tag[0] ) ) { continue; } $deferred_tag = str_replace( '>', ' data-rocket-defer defer>', $tag[0] ); $html = str_replace( $tag[0], $deferred_tag, $html ); } return $this->add_meta_comment( 'defer_js', $html ); } /** * Defers inline JS containing jQuery calls * * @since 3.8 * * @param string $html HTML content. * @return string */ public function defer_inline_js( string $html ): string { if ( ! $this->can_defer_js() ) { return $html; } $exclude_defer_js = implode( '|', $this->get_excluded() ); if ( preg_match( '/(jquery(?:.*)?\.js)/i', $exclude_defer_js ) ) { return $html; } $buffer_nocomments = preg_replace( '/<!--(.*)-->/Uis', '', $html ); preg_match_all( '#<script(?:[^>]*)>(?<content>[\s\S]*?)</script>#msi', $buffer_nocomments, $matches, PREG_SET_ORDER ); if ( empty( $matches ) ) { return $html; } /** * Filters the patterns used to find jQuery in inline JS * * @since 3.8 * * @param string $jquery_patterns A RegEx pattern to find jQuery inline JS. */ $jquery_patterns = apply_filters( 'rocket_defer_jquery_patterns', 'jQuery|\$\.\(|\$\(' ); $inline_exclusions = $this->get_inline_exclusions_list_pattern(); foreach ( $matches as $inline_js ) { if ( empty( $inline_js['content'] ) ) { continue; } if ( preg_match( '/(application\/ld\+json)/i', $inline_js[0] ) ) { continue; } if ( empty( $inline_exclusions ) || preg_match( "/({$inline_exclusions})/msi", $inline_js['content'] ) ) { continue; } if ( ! empty( $jquery_patterns ) && ! preg_match( "/({$jquery_patterns})/msi", $inline_js['content'] ) ) { continue; } $tag = str_replace( $inline_js['content'], $this->inline_js_wrapper( $inline_js['content'] ), $inline_js[0] ); $html = str_replace( $inline_js[0], $tag, $html ); } return $html; } /** * Adds the JS wrapper for inline jQuery code * * @since 3.8 * * @param string $content Inline JS content. * @return string */ private function inline_js_wrapper( string $content ): string { return "window.addEventListener('DOMContentLoaded', function() {" . $content . '});'; } /** * Checks if we can defer JS * * @since 3.8 * * @return boolean */ private function can_defer_js(): bool { if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return false; } if ( ! $this->options->get( 'defer_all_js', 0 ) ) { return false; } return ! is_rocket_post_excluded_option( 'defer_all_js' ); } /** * Get list of JS files to be excluded from defer JS. * * @since 3.8 * * @return array */ public function get_excluded(): array { if ( ! $this->can_defer_js() ) { return []; } $exclude_defer_js = array_unique( array_merge( $this->get_external_exclusions(), $this->options->get( 'exclude_defer_js', [] ) ) ); /** * Filter list of Deferred JavaScript files * * @since 2.10 * * @param array $exclude_defer_js An array of URLs for the JS files to be excluded. */ $exclude_defer_js = apply_filters( 'rocket_exclude_defer_js', $exclude_defer_js ); foreach ( $exclude_defer_js as $i => $exclude ) { $exclude_defer_js[ $i ] = str_replace( '#', '\#', $exclude ); } return $exclude_defer_js; } /** * Excludes jQuery from combine JS when defer and combine are enabled * * @since 3.8 * * @param array $excluded_files Array of excluded files from combine JS. * @return array */ public function exclude_jquery_combine( array $excluded_files ): array { if ( ! $this->can_defer_js() ) { return $excluded_files; } if ( ! (bool) $this->options->get( 'minify_concatenate_js', 0 ) ) { return $excluded_files; } $excluded_files[] = '/jquery-?[0-9.]*(.min|.slim|.slim.min)?.js'; return $excluded_files; } /** * Adds jQuery to defer JS exclusion field if safe mode was enabled before 3.8 * * @since 3.8 * * @return void */ public function exclude_jquery_upgrade() { $options = get_option( 'wp_rocket_settings' ); if ( ! isset( $options['defer_all_js_safe'] ) ) { return; } if ( ! (bool) $options['defer_all_js_safe'] ) { return; } $options['exclude_defer_js'][] = '/jquery-?[0-9.]*(.min|.slim|.slim.min)?.js'; update_option( 'wp_rocket_settings', $options ); } /** * Get exclusion list pattern. * * @return string */ private function get_inline_exclusions_list_pattern() { $inline_exclusions_list = $this->get_inline_exclusions(); /** * Filters the patterns used to find inline JS that should not be deferred * * @since 3.8 * * @param array $inline_exclusions_list Array of inline JS that should not be deferred. */ $additional_inline_exclusions_list = apply_filters( 'rocket_defer_inline_exclusions', [] ); $inline_exclusions = ''; // Check if filter return is string so convert it to array for backward compatibility. // @phpstan-ignore-next-line - Ignoring the safeguard as the result could be mixed. if ( is_string( $additional_inline_exclusions_list ) ) { $additional_inline_exclusions_list = explode( '|', $additional_inline_exclusions_list ); } // Cast filter return to array. $inline_exclusions_list = array_merge( $inline_exclusions_list, (array) $additional_inline_exclusions_list ); foreach ( $inline_exclusions_list as $inline_exclusions_item ) { $inline_exclusions .= preg_quote( (string) $inline_exclusions_item, '#' ) . '|'; } return rtrim( $inline_exclusions, '|' ); } /** * Get inline exclusions list * * @return array */ private function get_inline_exclusions(): array { $lists = $this->data_manager->get_lists(); return isset( $lists->defer_js_inline_exclusions ) ? $lists->defer_js_inline_exclusions : []; } /** * Get external exclusions list * * @return array */ private function get_external_exclusions(): array { $lists = $this->data_manager->get_lists(); return isset( $lists->defer_js_external_exclusions ) ? $lists->defer_js_external_exclusions : []; } } Engine/Optimization/DeferJS/ServiceProvider.php 0000644 00000002274 15174677547 0015601 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DeferJS; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for the WP Rocket Defer JS */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'defer_js', 'defer_js_admin_subscriber', 'defer_js_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'defer_js', DeferJS::class ) ->addArguments( [ 'options', 'dynamic_lists_defaultlists_data_manager', ] ); $this->getContainer()->addShared( 'defer_js_admin_subscriber', AdminSubscriber::class ) ->addArgument( 'defer_js' ); $this->getContainer()->addShared( 'defer_js_subscriber', Subscriber::class ) ->addArgument( 'defer_js' ); } } Engine/Optimization/DeferJS/AdminSubscriber.php 0000644 00000003367 15174677547 0015546 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\DeferJS; use WP_Rocket\Event_Management\Subscriber_Interface; class AdminSubscriber implements Subscriber_Interface { /** * DeferJS instance * * @var DeferJS */ private $defer_js; /** * Instantiate the class * * @param DeferJS $defer_js DeferJS instance. */ public function __construct( DeferJS $defer_js ) { $this->defer_js = $defer_js; } /** * Returns array of events this listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_first_install_options' => 'add_defer_js_option', 'wp_rocket_upgrade' => [ 'exclude_jquery_defer', 14, 2 ], 'rocket_meta_boxes_fields' => [ 'add_meta_box', 5 ], ]; } /** * Adds defer js option to WP Rocket options array * * @since 3.8 * * @param array $options WP Rocket options array. * @return array */ public function add_defer_js_option( array $options ): array { return $this->defer_js->add_option( $options ); } /** * Adds jQuery to defer JS exclusion field if safe mode was enabled before 3.8 * * @since 3.8 * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function exclude_jquery_defer( $new_version, $old_version ) { if ( version_compare( $old_version, '3.8', '>' ) ) { return; } $this->defer_js->exclude_jquery_upgrade(); } /** * Add the field to the WP Rocket metabox on the post edit page. * * @param string[] $fields Metaboxes fields. * * @return string[] */ public function add_meta_box( array $fields ) { $fields['defer_all_js'] = __( 'Load JavaScript deferred', 'rocket' ); return $fields; } } Engine/Optimization/ContentTrait.php 0000644 00000005106 15174677547 0013617 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization; trait ContentTrait { /** * Gets all public post types. * * @since 3.9 moved into trait * @since 2.11 */ private function get_public_post_types() { global $wpdb; $post_types = get_post_types( [ 'public' => true, 'publicly_queryable' => true, ] ); $post_types[] = 'page'; /** * Filters the post types excluded from critical CSS generation. * * @since 2.11 * * @param array $excluded_post_types An array of post types names. * * @return array */ $excluded_post_types = (array) apply_filters( 'rocket_cpcss_excluded_post_types', [ 'elementor_library', 'oceanwp_library', 'tbuilder_layout', 'tbuilder_layout_part', 'slider', 'karma-slider', 'tt-gallery', 'xlwcty_thankyou', 'fusion_template', 'blocks', 'jet-woo-builder', 'fl-builder-template', 'cms_block', 'web-story', ] ); $post_types = array_diff( $post_types, $excluded_post_types ); $post_types = esc_sql( $post_types ); $post_types = "'" . implode( "','", $post_types ) . "'"; return $wpdb->get_results( "SELECT MAX(ID) as ID, post_type FROM ( SELECT ID, post_type FROM $wpdb->posts WHERE post_type IN ( $post_types ) AND post_status = 'publish' ORDER BY post_date DESC ) AS posts GROUP BY post_type" ); } /** * Gets all public taxonomies. * * @since 3.9 moved into trait * @since 2.11 */ private function get_public_taxonomies() { global $wpdb; $taxonomies = get_taxonomies( [ 'public' => true, 'publicly_queryable' => true, ] ); /** * Filters the taxonomies excluded from critical CSS generation. * * @since 2.11 * * @param array $excluded_taxonomies An array of taxonomies names. * * @return array */ $excluded_taxonomies = (array) apply_filters( 'rocket_cpcss_excluded_taxonomies', [ 'post_format', 'product_shipping_class', 'karma-slider-category', 'truethemes-gallery-category', 'coupon_campaign', 'element_category', 'mediamatic_wpfolder', 'attachment_category', ] ); $taxonomies = array_diff( $taxonomies, $excluded_taxonomies ); $taxonomies = esc_sql( $taxonomies ); $taxonomies = "'" . implode( "','", $taxonomies ) . "'"; return $wpdb->get_results( "SELECT MAX( term_id ) AS ID, taxonomy FROM ( SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE taxonomy IN ( $taxonomies ) AND count > 0 ) AS taxonomies GROUP BY taxonomy" ); } } Engine/Optimization/Buffer/Subscriber.php 0000644 00000001572 15174677547 0014520 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\Buffer; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Event subscriber to buffer and process a page content. * * @since 3.3 */ class Subscriber implements Subscriber_Interface { /** * Optimization instance * * @var Optimization */ private $optimizer; /** * Constructor * * @param Optimization $optimizer Optimization instance. */ public function __construct( Optimization $optimizer ) { $this->optimizer = $optimizer; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'template_redirect' => [ 'start_content_process', 2 ], ]; } /** * Start buffering the page content and apply optimizations if we can. * * @since 3.3 */ public function start_content_process() { return $this->optimizer->maybe_init_process(); } } Engine/Optimization/Buffer/Optimization.php 0000644 00000005434 15174677547 0015104 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\Buffer; use WP_Rocket\Buffer\Abstract_Buffer; use WP_Rocket\Buffer\Tests; /** * Handle page optimizations. * * @since 3.3 */ class Optimization extends Abstract_Buffer { /** * Process identifier used by the logger. * * @var string * @since 3.3 */ protected $process_id = 'optimization process'; /** * Tests instance * * @var Tests */ protected $tests; /** * Constructor. * * @since 3.3 * * @param Tests $tests Tests instance. */ public function __construct( Tests $tests ) { parent::__construct( $tests ); $this->log( 'OPTIMIZATION PROCESS STARTED.', [], 'info' ); } /** ----------------------------------------------------------------------------------------- */ /** CACHE =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Do preliminary tests and maybe launch the buffer process. * * @since 3.3 */ public function maybe_init_process() { if ( ! $this->tests->can_init_process() ) { $this->log_last_test_error(); return; } ob_start( [ $this, 'maybe_process_buffer' ] ); } /** * Maybe optimize the page content. * * @since 3.3 * * @param string $buffer The buffer content. * @return string The buffered content. */ public function maybe_process_buffer( $buffer ) { /** * Triggered before WP Rocket starts the optimization process. * * @since 3.4.2 * @author Soponar Cristina * * @param string $buffer HTML content. */ do_action( 'rocket_before_maybe_process_buffer', $buffer ); if ( ! $this->is_feed_uri() && ! $this->is_html( $buffer ) ) { return $buffer; } if ( ! $this->tests->can_process_buffer( $buffer ) ) { $this->log_last_test_error(); return $buffer; } /** * This hook is used for: * - Async CSS files * - Defer JavaScript files * - Minify/Combine HTML/CSS/JavaScript * - CDN * - LazyLoad * * @param string $buffer The page content. */ $filtered_buffer = (string) apply_filters( 'rocket_buffer', $buffer ); if ( empty( $filtered_buffer ) ) { $this->log_last_test_error(); $this->log( 'Empty buffer.', [], 'error' ); return $buffer; } $this->log( 'Page optimized.', [], 'info' ); /** * Fires after processing the buffer * * @since 3.12 */ do_action( 'rocket_after_process_buffer' ); return $filtered_buffer; } /** * Tell if the current url is a feed. * * @return bool */ public function is_feed_uri() { global $wp_rewrite, $wp; $feed_uri = '/(?:.+/)?' . $wp_rewrite->feed_base . '(?:/(?:.+/?)?)?$'; return (bool) preg_match( '#^(' . $feed_uri . ')$#i', '/' . $wp->request ); } } Engine/Optimization/Minify/JS/Subscriber.php 0000644 00000004370 15174677547 0015055 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\Minify\JS; use WP_Rocket\Dependencies\Minify\JS as MinifyJS; use WP_Rocket\Engine\Optimization\AssetsLocalCache; use WP_Rocket\Engine\Optimization\DeferJS\DeferJS; use WP_Rocket\Engine\Optimization\DynamicLists\DynamicLists; use WP_Rocket\Engine\Optimization\Minify\AbstractMinifySubscriber; /** * Minify/Combine JS subscriber * * @since 3.1 */ class Subscriber extends AbstractMinifySubscriber { /** * Return an array of events that this subscriber wants to listen to. * * @since 3.1 * * @return array */ public static function get_subscribed_events() { $events = [ 'rocket_js_url' => [ [ 'fix_ssl_minify' ], [ 'i18n_multidomain_url' ], ], 'rocket_buffer' => [ 'process', 22 ], ]; return $events; } /** * Processes the HTML to Minify/Combine JS. * * @since 3.1 * * @param string $html HTML content. * @return string */ public function process( $html ) { if ( ! $this->is_allowed() ) { return $html; } $assets_local_cache = new AssetsLocalCache( rocket_get_constant( 'WP_ROCKET_MINIFY_CACHE_PATH' ), $this->filesystem ); $container = apply_filters( 'rocket_container', null ); $dynamic_lists = $container->get( 'dynamic_lists' ); if ( $this->options->get( 'minify_js' ) && $this->options->get( 'minify_concatenate_js' ) ) { $this->set_processor_type( new Combine( $this->options, new MinifyJS(), $assets_local_cache, $container->get( 'defer_js' ), $dynamic_lists ) ); } elseif ( $this->options->get( 'minify_js' ) && ! $this->options->get( 'minify_concatenate_js' ) ) { $this->set_processor_type( new Minify( $this->options, $assets_local_cache, $dynamic_lists ) ); } return $this->processor->optimize( $html ); } /** * Checks if is allowed to Minify/Combine JS. * * @since 3.1 * * @return bool */ protected function is_allowed() { if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return false; } if ( ! (bool) $this->options->get( 'minify_js', 0 ) ) { return false; } return ! is_rocket_post_excluded_option( 'minify_js' ); } /** * Returns an array of CDN zones for JS files. * * @since 3.1 * * @return array */ public function get_zones() { return [ 'all', 'css_and_js', 'js' ]; } } Engine/Optimization/Minify/JS/Minify.php 0000644 00000020161 15174677547 0014201 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\Minify\JS; use WP_Rocket\Dependencies\Minify as Minifier; use WP_Rocket\Engine\Optimization\Minify\ProcessorInterface; use WP_Rocket\Engine\Support\CommentTrait; use WP_Rocket\Logger\Logger; /** * Minify JS files * * @since 3.1 */ class Minify extends AbstractJSOptimization implements ProcessorInterface { use CommentTrait; /** * Minifies JS files * * @since 3.1 * * @param string $html HTML content. * @return string */ public function optimize( $html ) { Logger::info( 'JS MINIFICATION PROCESS STARTED.', [ 'js minification process' ] ); $scripts = $this->get_scripts( $html ); if ( empty( $scripts ) ) { return $html; } foreach ( $scripts as $script ) { global $wp_scripts; $is_external_url = $this->is_external_file( $script['url'] ); if ( ! $is_external_url && preg_match( '/[-.]min\.js/iU', $script['url'] ) ) { Logger::debug( 'Script is already minified.', [ 'js minification process', 'tag' => $script[0], ] ); continue; } if ( $is_external_url && $this->is_excluded_external( $script['url'] ) ) { continue; } if ( $this->is_minify_excluded_file( $script ) ) { Logger::debug( 'Script is excluded.', [ 'js minification process', 'tag' => $script[0], ] ); continue; } // Don't minify jQuery included in WP core since it's already minified but without .min in the filename. if ( ! empty( $wp_scripts->registered['jquery-core']->src ) && false !== strpos( $script['url'], $wp_scripts->registered['jquery-core']->src ) ) { Logger::debug( 'jQuery script is already minified.', [ 'js minification process', 'tag' => $script[0], ] ); continue; } $integrity_validated = $this->local_cache->validate_integrity( $script ); if ( false === $integrity_validated ) { Logger::debug( 'Script integrity attribute not valid.', [ 'js minification process', 'tag' => $script[0], ] ); continue; } $script['final'] = $integrity_validated; $minify_url = $this->replace_url( strtok( $script['url'], '?' ) ); if ( ! $minify_url ) { Logger::error( 'Script minification failed.', [ 'js minification process', 'tag' => $script[0], ] ); continue; } $html = $this->replace_script( $script, $minify_url, $html ); } return $this->add_meta_comment( 'minify_js', $html ); } /** * Get all script tags from HTML. * * @param string $html HTML content. * @return array Array with script tags, empty array if no script tags found. */ private function get_scripts( $html ) { $html_nocomments = $this->hide_comments( $html ); $scripts = $this->find( '<script\s+([^>]+[\s\'"])?src\s*=\s*[\'"]\s*?(?<url>[^\'"]+\.js(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>', $html_nocomments ); if ( ! $scripts ) { Logger::debug( 'No `<script>` tags found.', [ 'js minification process' ] ); return []; } Logger::debug( 'Found ' . count( $scripts ) . ' <link> tags.', [ 'js minification process', 'tags' => $scripts, ] ); return $scripts; } /** * Checks if the provided external URL is excluded from minify * * @since 3.7 * * @param string $url External URL to check. * @return boolean */ private function is_excluded_external( $url ) { $excluded_externals = $this->get_excluded_external_file_path(); $excluded_externals[] = 'google-analytics.com/analytics.js'; foreach ( $excluded_externals as $excluded ) { if ( false !== strpos( $url, $excluded ) ) { Logger::debug( 'Script is external.', [ 'js combine process', 'url' => $url, ] ); return true; } } return false; } /** * Creates the minify URL if the minification is successful * * @since 2.11 * * @param string $url Original file URL. * @return string|bool The minify URL if successful, false otherwise */ protected function replace_url( $url ) { if ( empty( $url ) ) { return false; } // This filter is documented in /inc/classes/optimization/class-abstract-optimization.php. $url = apply_filters( 'rocket_asset_url', $url, $this->get_zones() ); $parsed_url = wp_parse_url( $url ); if ( empty( $parsed_url['path'] ) ) { return false; } if ( ! empty( $parsed_url['host'] ) ) { $url = rocket_add_url_protocol( $url ); } $filename = ltrim( rocket_realpath( $parsed_url['path'] ), '/' ); $minified_file = rawurldecode( $this->minify_base_path . $filename ); if ( rocket_direct_filesystem()->exists( $minified_file ) ) { Logger::debug( 'Minified JS file already exists.', [ 'js minification process', 'path' => $minified_file, ] ); return $this->get_full_minified_url( $minified_file, $this->get_minify_url( $filename, $url ) ); } $is_external_url = $this->is_external_file( $url ); $file_path = $is_external_url ? $this->local_cache->get_filepath( rocket_add_url_protocol( $url ) ) : $this->get_file_path( $url ); if ( ! $file_path ) { Logger::error( 'Couldn’t get the file path from the URL.', [ 'js minification process', 'url' => $url, ] ); return false; } $file_content = $is_external_url ? $this->local_cache->get_content( rocket_add_url_protocol( $url ) ) : $this->get_file_content( $file_path ); if ( empty( $file_content ) ) { Logger::error( 'No file content.', [ 'js minification process', 'path' => $file_path, ] ); return false; } $minified_content = $this->minify( $file_content ); if ( empty( $minified_content ) ) { Logger::error( 'No minified content.', [ 'js minification process', 'path' => $minified_file, ] ); return false; } $save_minify_file = $this->save_minify_file( $minified_file, $minified_content ); if ( ! $save_minify_file ) { return false; } return $this->get_full_minified_url( $minified_file, $this->get_minify_url( $filename, $url ) ); } /** * Replace old script tag with the minified tag. * * @param array $script Script matched data. * @param string $minify_url Minified URL. * @param string $html HTML content. * * @return string */ private function replace_script( $script, $minify_url, $html ) { $replace_script = str_replace( $script['url'], $minify_url, $script['final'] ); $replace_script = str_replace( '<script', '<script data-minify="1"', $replace_script ); $html = str_replace( $script[0], $replace_script, $html ); Logger::info( 'Script minification succeeded.', [ 'js minification process', 'url' => $minify_url, ] ); return $html; } /** * Save minified JS file. * * @since 3.7 * * @param string $minified_file Minified file path. * @param string $minified_content Minified HTML content. * * @return bool */ protected function save_minify_file( $minified_file, $minified_content ) { $save_minify_file = $this->write_file( $minified_content, $minified_file ); if ( ! $save_minify_file ) { Logger::error( 'Minified JS file could not be created.', [ 'js minification process', 'path' => $minified_file, ] ); return false; } Logger::debug( 'Minified JS file successfully created.', [ 'js minification process', 'path' => $minified_file, ] ); return true; } /** * Minifies the content * * @since 2.11 * * @param string $file_content Content to minify. * @return string */ protected function minify( $file_content ) { $minifier = $this->get_minifier( $file_content ); $minified_content = $minifier->minify(); if ( empty( $minified_content ) ) { return ''; // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound } return $minified_content; // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound } /** * Returns a new minifier instance * * @since 3.1 * * @param string $file_content Content to minify. * @return Minifier\JS */ protected function get_minifier( $file_content ) { return new Minifier\JS( $file_content ); } } Engine/Optimization/Minify/JS/Combine.php 0000644 00000030353 15174677547 0014326 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\Minify\JS; use WP_Rocket\Dependencies\Minify\JS as MinifyJS; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\AssetsLocalCache; use WP_Rocket\Engine\Optimization\DeferJS\DeferJS; use WP_Rocket\Engine\Optimization\DynamicLists\DynamicLists; use WP_Rocket\Engine\Optimization\Minify\ProcessorInterface; use WP_Rocket\Engine\Support\CommentTrait; use WP_Rocket\Logger\Logger; /** * Combines JS files * * @since 3.1 */ class Combine extends AbstractJSOptimization implements ProcessorInterface { use CommentTrait; /** * Minifier instance * * @since 3.1 * * @var MinifyJS */ private $minifier; /** * Excluded defer JS pattern * * @since 3.8 * * @var string */ private $excluded_defer_js; /** * Scripts to combine * * @since 3.1 * * @var array */ private $scripts = []; /** * Inline scripts excluded from combined and moved after the combined file * * @since 3.1.4 * * @var array */ private $move_after = []; /** * Constructor * * @since 3.1 * * @param Options_Data $options Plugin options instance. * @param MinifyJS $minifier Minifier instance. * @param AssetsLocalCache $local_cache Assets local cache instance. * @param DeferJS $defer_js Defer JS instance. * @param DynamicLists $dynamic_lists Dynamic list instance. */ public function __construct( Options_Data $options, MinifyJS $minifier, AssetsLocalCache $local_cache, DeferJS $defer_js, DynamicLists $dynamic_lists ) { parent::__construct( $options, $local_cache, $dynamic_lists ); $this->minifier = $minifier; $this->excluded_defer_js = implode( '|', $defer_js->get_excluded() ); } /** * Minifies and combines JavaScripts into one * * @since 3.1 * * @param string $html HTML content. * @return string */ public function optimize( $html ) { Logger::info( 'JS COMBINE PROCESS STARTED.', [ 'js combine process' ] ); $html_nocomments = $this->hide_comments( $html ); $scripts = $this->find( '<script.*<\/script>', $html_nocomments ); if ( ! $scripts ) { Logger::debug( 'No `<script>` tags found.', [ 'js combine process' ] ); return $html; } Logger::debug( 'Found ' . count( $scripts ) . ' `<script>` tag(s).', [ 'js combine process', 'tags' => $scripts, ] ); $combine_scripts = $this->parse( $scripts ); if ( empty( $combine_scripts ) ) { Logger::debug( 'No `<script>` tags to optimize.', [ 'js combine process' ] ); return $html; } Logger::debug( count( $combine_scripts ) . ' `<script>` tag(s) remaining.', [ 'js combine process', 'tags' => $combine_scripts, ] ); $content = $this->get_content(); if ( empty( $content ) ) { Logger::debug( 'No JS content.', [ 'js combine process' ] ); return $html; } $minify_url = $this->combine( $content ); if ( ! $minify_url ) { Logger::error( 'JS combine process failed.', [ 'js combine process' ] ); return $html; } $move_after = ''; if ( ! empty( $this->move_after ) ) { foreach ( $this->move_after as $script ) { $move_after .= $script; $html = str_replace( $script, '', $html ); } } // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript $html = str_replace( '</body>', '<script src="' . esc_url( $minify_url ) . '" data-minify="1"></script>' . $move_after . '</body>', $html ); foreach ( $combine_scripts as $script ) { $html = str_replace( $script[0], '', $html ); } Logger::info( 'Combined JS file successfully added.', [ 'js combine process', 'url' => $minify_url, ] ); return $this->add_meta_comment( 'minify_concatenate_js', $html ); } /** * Parses found nodes to keep only the ones to combine * * @since 3.1 * * @param Array $scripts scripts corresponding to JS file or content. * @return array */ protected function parse( $scripts ) { $excluded_externals = implode( '|', $this->get_excluded_external_file_path() ); $scripts = array_map( function ( $script ) use ( $excluded_externals ) { preg_match( '/<script\s+([^>]+[\s\'"])?src\s*=\s*[\'"]\s*?(?<url>[^\'"]+\.js(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>/Umsi', $script[0], $matches ); if ( isset( $matches['url'] ) ) { if ( $this->is_external_file( $matches['url'] ) ) { if ( preg_match( '#(' . $excluded_externals . ')#', $matches['url'] ) ) { Logger::debug( 'Script is external.', [ 'js combine process', 'tag' => $matches[0], ] ); return; } if ( $this->is_defer_excluded( $matches['url'] ) ) { return; } $this->scripts[] = [ 'type' => 'url', 'content' => $matches['url'], ]; return $script; } if ( $this->is_minify_excluded_file( $matches ) ) { Logger::debug( 'Script is excluded.', [ 'js combine process', 'tag' => $matches[0], ] ); return; } if ( $this->is_defer_excluded( $matches['url'] ) ) { return; } $file_path = $this->get_file_path( strtok( $matches['url'], '?' ) ); if ( ! $file_path ) { return; } $this->scripts[] = [ 'type' => 'file', 'content' => $file_path, ]; } else { preg_match( '/<script\b(?<attrs>[^>]*)>(?:\/\*\s*<!\[CDATA\[\s*\*\/)?\s*(?<content>[\s\S]*?)\s*(?:\/\*\s*\]\]>\s*\*\/)?<\/script>/msi', $script[0], $matches_inline ); $matches_inline = array_merge( [ 'attrs' => '', 'content' => '', ], $matches_inline ); if ( preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR ) { Logger::debug( 'PCRE regex execution Catastrophic Backtracking', [ 'inline JS backtracking error', 'content' => $matches_inline['content'], ] ); return; } if ( strpos( $matches_inline['attrs'], 'type' ) !== false && ! preg_match( '/type\s*=\s*["\']?(?:text|application)\/(?:(?:x\-)?javascript|ecmascript)["\']?/i', $matches_inline['attrs'] ) ) { Logger::debug( 'Inline script is not JS.', [ 'js combine process', 'attributes' => $matches_inline['attrs'], ] ); return; } if ( false !== strpos( $matches_inline['attrs'], 'src=' ) ) { Logger::debug( 'Inline script has a `src` attribute.', [ 'js combine process', 'attributes' => $matches_inline['attrs'], ] ); return; } if ( in_array( $matches_inline['content'], $this->get_localized_scripts(), true ) ) { Logger::debug( 'Inline script is a localize script', [ 'js combine process', 'excluded_content' => $matches_inline['content'], ] ); return; } if ( $this->is_delayed_script( $matches_inline['attrs'] ) ) { return; } foreach ( $this->get_excluded_inline_content() as $excluded_content ) { if ( false !== strpos( $matches_inline['content'], $excluded_content ) ) { Logger::debug( 'Inline script has excluded content.', [ 'js combine process', 'excluded_content' => $excluded_content, ] ); return; } } foreach ( $this->get_move_after_inline_scripts() as $move_after_script ) { if ( false !== strpos( $matches_inline['content'], $move_after_script ) ) { $this->move_after[] = $script[0]; return; } } $this->scripts[] = [ 'type' => 'inline', 'content' => $matches_inline['content'], ]; } return $script; }, $scripts ); return array_filter( $scripts ); } /** * Gets content for each script either from inline or from src * * @since 3.1 * * @return string */ protected function get_content() { $content = ''; foreach ( $this->scripts as $script ) { if ( 'file' === $script['type'] ) { $file_content = $this->get_file_content( $script['content'] ); $content .= $file_content; $this->add_to_minify( $file_content ); } elseif ( 'url' === $script['type'] ) { $file_content = $this->local_cache->get_content( rocket_add_url_protocol( $script['content'] ) ); $content .= $file_content; $this->add_to_minify( $file_content ); } elseif ( 'inline' === $script['type'] ) { $inline_js = rtrim( $script['content'], ";\n\t\r" ) . ';'; $content .= $inline_js; $this->add_to_minify( $inline_js ); } } return $content; } /** * Creates the minify URL if the minification is successful * * @since 2.11 * * @param string $content Content to minify & combine. * @return string|bool The minify URL if successful, false otherwise */ protected function combine( $content ) { if ( empty( $content ) ) { return false; // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound } $filename = md5( $content . $this->minify_key ) . '.js'; $minified_file = $this->minify_base_path . $filename; if ( ! rocket_direct_filesystem()->is_readable( $minified_file ) ) { $minified_content = $this->minify(); if ( ! $minified_content ) { return false; // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound } $minify_filepath = $this->write_file( $minified_content, $minified_file ); if ( ! $minify_filepath ) { return false; // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound } } return $this->get_minify_url( $filename ); // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound } /** * Minifies the content * * @since 2.11 * * @return string|bool Minified content, false if empty */ protected function minify() { $minified_content = $this->minifier->minify(); if ( empty( $minified_content ) ) { return false; } return $minified_content; } /** * Adds content to the minifier * * @since 3.1 * * @param string $content Content to minify/combine. * @return void */ protected function add_to_minify( $content ) { $this->minifier->add( $content ); } /** * Patterns in content excluded from being combined * * @since 3.1 * * @return array */ protected function get_excluded_inline_content() { $excluded_inline = $this->options->get( 'exclude_inline_js', [] ); /** * Filters inline JS excluded from being combined * * @since 3.1 * * @param array $pattern Patterns to match. */ return apply_filters( 'rocket_excluded_inline_js_content', $excluded_inline ); } /** * Patterns of inline JS to move after the combined JS file * * @since 3.1.4 * * @return array */ protected function get_move_after_inline_scripts() { /** * Filters inline JS to move after the combined JS file * * @since 3.1.4 * * @param array $move_after_scripts Patterns to match. */ return apply_filters( 'rocket_move_after_combine_js', [] ); } /** * Gets all localized scripts data to exclude them from combine. * * @since 3.1.3 * * @return array */ protected function get_localized_scripts() { static $localized_scripts; if ( isset( $localized_scripts ) ) { return $localized_scripts; } $localized_scripts = []; foreach ( array_unique( wp_scripts()->queue ) as $item ) { $data = wp_scripts()->print_extra_script( $item, false ); if ( empty( $data ) ) { continue; } $localized_scripts[] = $data; } return $localized_scripts; } /** * Is this script a delayed script or not. * * @since 3.7 * * @param string $script_attributes Attributes beside the opening of script tag. * * @return bool True if it's a delayed script and false if not. */ private function is_delayed_script( $script_attributes ) { return false !== strpos( $script_attributes, 'data-rocketlazyloadscript=' ); } /** * Checks if the current URL is excluded from defer JS * * @since 3.8 * * @param string $url URL to check. * @return boolean */ private function is_defer_excluded( string $url ): bool { if ( ! empty( $this->excluded_defer_js ) && preg_match( '#(' . $this->excluded_defer_js . ')#i', $url ) ) { Logger::debug( 'Script is excluded from defer JS.', [ 'js combine process', 'url' => $url, ] ); return true; } return false; } } Engine/Optimization/Minify/JS/AbstractJSOptimization.php 0000644 00000011103 15174677547 0017351 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\Minify\JS; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\AbstractOptimization; use WP_Rocket\Engine\Optimization\AssetsLocalCache; use WP_Rocket\Engine\Optimization\DynamicLists\DynamicLists; /** * Abstract class for JS optimization * * @since 3.1 */ abstract class AbstractJSOptimization extends AbstractOptimization { const FILE_TYPE = 'js'; /** * Assets local cache instance * * @since 3.1 * * @var AssetsLocalCache */ protected $local_cache; /** * DynamicLists instance * * @var DynamicLists */ private $dynamic_lists; /** * Creates an instance of inheriting class. * * @since 3.1 * * @param Options_Data $options Options instance. * @param AssetsLocalCache $local_cache Assets local cache instance. * @param DynamicLists $dynamic_lists DynamicLists instance. */ public function __construct( Options_Data $options, AssetsLocalCache $local_cache, DynamicLists $dynamic_lists ) { $this->options = $options; $this->local_cache = $local_cache; $this->minify_key = $this->options->get( 'minify_js_key', create_rocket_uniqid() ); $this->excluded_files = $this->get_excluded_files(); $this->dynamic_lists = $dynamic_lists; $this->init_base_path_and_url(); } /** * Get all files to exclude from minification/concatenation. * * @since 2.11 * * @return string A list of files to exclude, ready to be used in a regex pattern. */ protected function get_excluded_files() { $excluded_files = $this->options->get( 'exclude_js', [] ); /** * Filter JS files to exclude from minification/concatenation. * * @since 2.6 * * @param array $js_files List of excluded JS files. */ $excluded_files = (array) apply_filters( 'rocket_exclude_js', $excluded_files ); if ( empty( $excluded_files ) ) { return ''; } foreach ( $excluded_files as $i => $excluded_file ) { // Escape characters for future use in regex pattern. $excluded_files[ $i ] = str_replace( '#', '\#', $excluded_file ); } return implode( '|', $excluded_files ); } /** * Returns the CDN zones. * * @since 3.1 * * @return array */ public function get_zones() { return [ 'all', 'css_and_js', self::FILE_TYPE ]; } /** * Determines if it is a file excluded from minification. * * @since 2.11 * * @param array $tag Tag corresponding to a JS file. * * @return bool True if it is a file excluded, false otherwise */ protected function is_minify_excluded_file( array $tag ) { if ( ! isset( $tag[0], $tag['url'] ) ) { return true; } // Type casting to array, cause json can be bummer sometimes. $exclude_js_templates = array_filter( (array) $this->dynamic_lists->get_exclude_js_templates() ); if ( empty( $exclude_js_templates ) ) { return false; } $escaped_js_template_array = array_map( function ( $item ) { return preg_quote( $item, '/' ); }, $exclude_js_templates ); $js_template_pattern = '/' . implode( '|', $escaped_js_template_array ) . '/'; // File should not be minified. if ( preg_match( $js_template_pattern, $tag[0] ) ) { return true; } $file_path = wp_parse_url( $tag['url'], PHP_URL_PATH ); // File extension is not js. if ( pathinfo( $file_path, PATHINFO_EXTENSION ) !== self::FILE_TYPE ) { return true; } if ( ! empty( $this->excluded_files ) ) { // File is excluded from minification/concatenation. if ( preg_match( '#(' . $this->excluded_files . ')#', $file_path ) ) { return true; } } return false; } /** * Gets the minify URL. * * @since 3.1 * * @param string $filename Minified filename. * @param string $original_url Original URL for this file. Optional. * * @return string */ protected function get_minify_url( $filename, $original_url = '' ) { $minify_url = $this->minify_base_url . $filename; /** * Filters JS file URL with CDN hostname * * @since 2.1 * * @param string $minify_url Minified file URL. * @param string $original_url Original URL for this file. */ return apply_filters( 'rocket_js_url', $minify_url, $original_url ); } /** * Patterns in URL excluded from being combined * * @since 3.1 * * @return array */ protected function get_excluded_external_file_path() { $excluded_external = $this->options->get( 'exclude_js', [] ); /** * Filters JS externals files to exclude from the combine process * * @since 2.2 * * @param array $pattern Patterns to match. */ return apply_filters( 'rocket_minify_excluded_external_js', $excluded_external ); } } Engine/Optimization/Minify/ProcessorInterface.php 0000644 00000000374 15174677547 0016236 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\Minify; interface ProcessorInterface { /** * Performs the optimization process on the given HTML * * @param string $html HTML content. * @return string */ public function optimize( $html ); } Engine/Optimization/Minify/CSS/Subscriber.php 0000644 00000003245 15174677547 0015171 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\Minify\CSS; use WP_Rocket\Engine\Optimization\AssetsLocalCache; use WP_Rocket\Engine\Optimization\Minify\AbstractMinifySubscriber; /** * Minify/Combine CSS subscriber * * @since 3.1 */ class Subscriber extends AbstractMinifySubscriber { /** * Return an array of events that this subscriber wants to listen to. * * @since 3.1 * * @return array */ public static function get_subscribed_events() { $events = [ 'rocket_css_url' => [ [ 'fix_ssl_minify' ], [ 'i18n_multidomain_url' ], ], 'rocket_buffer' => [ 'process', 16 ], ]; return $events; } /** * Processes the HTML to Minify/Combine CSS. * * @since 3.1 * * @param string $html HTML content. * @return string */ public function process( $html ) { if ( ! $this->is_allowed() ) { return $html; } $assets_local_cache = new AssetsLocalCache( rocket_get_constant( 'WP_ROCKET_MINIFY_CACHE_PATH' ), $this->filesystem ); if ( $this->options->get( 'minify_css' ) ) { $this->set_processor_type( new Minify( $this->options, $assets_local_cache ) ); } return $this->processor->optimize( $html ); } /** * Checks if is allowed to Minify/Combine CSS. * * @since 3.1 * * @return bool */ protected function is_allowed() { if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return false; } if ( ! (bool) $this->options->get( 'minify_css', 0 ) ) { return false; } return ! is_rocket_post_excluded_option( 'minify_css' ); } /** * Returns an array of CDN zones for CSS files. * * @since 3.1 * * @return array */ public function get_zones() { return [ 'all', 'css_and_js', 'css' ]; } } Engine/Optimization/Minify/CSS/Minify.php 0000644 00000017604 15174677547 0014325 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\Minify\CSS; use WP_Rocket\Dependencies\Minify as Minifier; use WP_Rocket\Engine\Optimization\CSSTrait; use WP_Rocket\Engine\Optimization\Minify\ProcessorInterface; use WP_Rocket\Engine\Support\CommentTrait; use WP_Rocket\Logger\Logger; /** * Minify CSS files * * @since 3.1 */ class Minify extends AbstractCSSOptimization implements ProcessorInterface { use CSSTrait; use CommentTrait; /** * Minifies CSS files * * @since 3.1 * * @param string $html HTML content. * * @return string */ public function optimize( $html ) { Logger::info( 'CSS MINIFICATION PROCESS STARTED.', [ 'css minification process' ] ); $styles = $this->get_styles( $html ); if ( empty( $styles ) ) { return $html; } foreach ( $styles as $style ) { if ( $this->is_minify_excluded_file( $style ) ) { Logger::debug( 'Style is excluded.', [ 'css minification process', 'tag' => $style[0], ] ); continue; } $integrity_validated = $this->local_cache->validate_integrity( $style ); if ( false === $integrity_validated ) { Logger::debug( 'Style integrity attribute not valid.', [ 'css minification process', 'tag' => $style[0], ] ); continue; } $style['final'] = $integrity_validated; $minify_url = $this->replace_url( strtok( $style['url'], '?' ) ); if ( ! $minify_url ) { Logger::error( 'Style minification failed.', [ 'css minification process', 'tag' => $style[0], ] ); continue; } $html = $this->replace_style( $style, $minify_url, $html ); } return $this->add_meta_comment( 'minify_css', $html ); } /** * Get all style tags from HTML. * * @param string $html HTML content. * * @return array Array with style tags, empty array if no style tags found. */ protected function get_styles( $html ) { $html_nocomments = $this->hide_comments( $html ); $styles = $this->find( '<link\s+([^>]+[\s"\'])?href\s*=\s*[\'"]\s*?(?<url>[^\'"]+\.css(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>', $html_nocomments ); if ( ! $styles ) { Logger::debug( 'No `<link>` tags found.', [ 'css minification process' ] ); return []; } Logger::debug( 'Found ' . count( $styles ) . ' `<link>` tags.', [ 'css minification process', 'tags' => $styles, ] ); return $styles; } /** * Creates the minify URL if the minification is successful * * @since 2.11 * * @param string $url Original file URL. * * @return string|bool The minify URL if successful, false otherwise */ private function replace_url( $url ) { if ( empty( $url ) ) { return false; } // This filter is documented in /inc/classes/optimization/class-abstract-optimization.php. $url = apply_filters( 'rocket_asset_url', $url, $this->get_zones() ); $parsed_url = wp_parse_url( $url ); if ( empty( $parsed_url['path'] ) ) { return false; } if ( ! empty( $parsed_url['host'] ) ) { $url = rocket_add_url_protocol( $url ); } $filename = ltrim( rocket_realpath( $parsed_url['path'] ), '/' ); $minified_file = rawurldecode( $this->minify_base_path . $filename ); if ( rocket_direct_filesystem()->exists( $minified_file ) ) { Logger::debug( 'Minified CSS file already exists.', [ 'css minification process', 'path' => $minified_file, ] ); return $this->get_full_minified_url( $minified_file, $this->get_minify_url( $filename, $url ) ); } $external_url = $this->is_external_file( $url ); $file_path = $external_url ? $this->local_cache->get_filepath( $url ) : $this->get_file_path( $url ); if ( empty( $file_path ) ) { Logger::error( 'Couldn’t get the file path from the URL.', [ 'css minification process', 'url' => $url, ] ); return false; } $file_content = $external_url ? $this->local_cache->get_content( $url ) : $this->get_file_content( $file_path ); if ( ! $file_content ) { Logger::error( 'No file content.', [ 'css minification process', 'path' => $file_path, ] ); return false; } $minified_content = $external_url ? $this->minify( $url, $minified_file, $file_content ) : $this->minify( $file_path, $minified_file, $file_content ); if ( empty( $minified_content ) ) { return false; } $minified_content = $this->font_display_swap( $url, $minified_file, $minified_content ); if ( empty( $minified_content ) ) { return false; } $save_minify_file = $this->save_minify_file( $minified_file, $minified_content ); if ( ! $save_minify_file ) { return false; } return $this->get_full_minified_url( $minified_file, $this->get_minify_url( $filename, $url ) ); } /** * Replace old style tag with the minified tag. * * @param array $style Style matched data. * @param string $minify_url Minified URL. * @param string $html HTML content. * * @return string */ protected function replace_style( $style, $minify_url, $html ) { $replace_style = str_replace( $style['url'], $minify_url, $style['final'] ); $replace_style = str_replace( '<link', '<link data-minify="1"', $replace_style ); $html = str_replace( $style[0], $replace_style, $html ); Logger::info( 'Style minification succeeded.', [ 'css minification process', 'url' => $minify_url, ] ); return $html; } /** * Save minified CSS file. * * @since 3.7 * * @param string $minified_file Minified file path. * @param string $minified_content Minified HTML content. * * @return bool */ protected function save_minify_file( $minified_file, $minified_content ) { $save_minify_file = $this->write_file( $minified_content, $minified_file ); if ( ! $save_minify_file ) { Logger::error( 'Minified CSS file could not be created.', [ 'css minification process', 'path' => $minified_file, ] ); return false; } Logger::debug( 'Minified CSS file successfully created.', [ 'css minification process', 'path' => $minified_file, ] ); return true; } /** * Applies font display swap if the file contains @font-face. * * @since 3.7 * * @param string $url File Url. * @param string $minified_file Minified file path. * @param string $content CSS file content. * * @return string */ protected function font_display_swap( $url, $minified_file, $content ) { if ( preg_match( '/(?:-|\.)min.css/iU', $url ) && false === stripos( $content, '@font-face' ) ) { Logger::error( 'Do not apply font display swap on min.css files without font-face.', [ 'css minification process', 'path' => $minified_file, ] ); if ( ! $this->is_external_file( $url ) ) { return ''; } return $content; } return $this->apply_font_display_swap( $content ); } /** * Minifies the content * * @since 2.11 * * @param string $file_path Source filepath. * @param string $minified_file Target filepath. * @param string $file_content Content to minify. * * @return string */ protected function minify( $file_path, $minified_file, $file_content ) { $file_content = $this->rewrite_paths( $file_path, $minified_file, $file_content ); $minifier = $this->get_minifier( $file_content ); $minified_content = $minifier->minify(); if ( empty( $minified_content ) ) { Logger::error( 'No minified content.', [ 'css minification process', 'path' => $minified_file, ] ); return ''; // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound } return $minified_content; // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound } /** * Returns a new minifier instance * * @since 3.1 * * @param string $file_content Content to minify. * * @return Minifier\CSS */ protected function get_minifier( $file_content ) { return new Minifier\CSS( $file_content ); } } Engine/Optimization/Minify/CSS/AbstractCSSOptimization.php 0000644 00000007424 15174677547 0017614 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\Minify\CSS; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\AbstractOptimization; use WP_Rocket\Engine\Optimization\AssetsLocalCache; /** * Abstract class for CSS Optimization * * @since 3.1 */ abstract class AbstractCSSOptimization extends AbstractOptimization { const FILE_TYPE = 'css'; /** * Assets local cache instance * * @var AssetsLocalCache */ protected $local_cache; /** * Creates an instance of inheriting class. * * @since 3.1 * * @param Options_Data $options Options instance. * @param AssetsLocalCache $local_cache AssetsLocalCache instance. */ public function __construct( Options_Data $options, AssetsLocalCache $local_cache ) { $this->options = $options; $this->local_cache = $local_cache; $this->minify_key = $this->options->get( 'minify_css_key', create_rocket_uniqid() ); $this->excluded_files = $this->get_excluded_files(); $this->init_base_path_and_url(); } /** * Get all files to exclude from minification/concatenation. * * @since 2.11 * * @return string */ protected function get_excluded_files() { $excluded_files = $this->options->get( 'exclude_css', [] ); /** * Filters CSS files to exclude from minification/concatenation. * * @since 2.6 * * @param array $excluded_files List of excluded CSS files. */ $excluded_files = (array) apply_filters( 'rocket_exclude_css', $excluded_files ); if ( empty( $excluded_files ) ) { return ''; } foreach ( $excluded_files as $i => $excluded_file ) { $excluded_files[ $i ] = str_replace( '#', '\#', $excluded_file ); } return implode( '|', $excluded_files ); } /** * Returns the CDN zones. * * @since 3.1 * * @return array */ public function get_zones() { return [ 'all', 'css_and_js', self::FILE_TYPE ]; } /** * Gets the minify URL * * @since 3.1 * * @param string $filename Minified filename. * @param string $original_url Original URL for this file. Optional. * * @return string */ protected function get_minify_url( $filename, $original_url = '' ) { $minify_url = $this->minify_base_url . $filename; /** * Filters CSS file URL with CDN hostname * * @since 2.1 * * @param string $minify_url Minified file URL. * @param string $original_url Original URL for this file. */ return apply_filters( 'rocket_css_url', $minify_url, $original_url ); } /** * Determines if it is a file excluded from minification * * @since 2.11 * * @param array $tag Tag corresponding to a CSS file. * * @return bool True if it is a file excluded, false otherwise */ protected function is_minify_excluded_file( array $tag ) { if ( ! isset( $tag[0], $tag['url'] ) ) { return true; } // File should not be minified. if ( false !== strpos( $tag[0], 'data-minify=' ) || false !== strpos( $tag[0], 'data-no-minify=' ) ) { return true; } if ( false !== strpos( $tag[0], 'media=' ) && ! preg_match( '/media=["\'](?:\s*|[^"\']*?\b(all|screen)\b[^"\']*?)["\']/i', $tag[0] ) ) { return true; } $file = wp_parse_url( $tag['url'] ); $file_path = isset( $file['path'] ) ? $file['path'] : ''; $host = isset( $file['host'] ) ? $file['host'] : ''; // File extension is not css. if ( pathinfo( $file_path, PATHINFO_EXTENSION ) !== self::FILE_TYPE ) { return true; } if ( ! empty( $this->excluded_files ) ) { // File is excluded from minification/concatenation. if ( preg_match( '#^(' . $this->excluded_files . ')$#', $file_path ) ) { return true; } if ( preg_match( '#^(' . $this->excluded_files . ')$#', $host ) ) { return true; } if ( preg_match( '#^(' . $this->excluded_files . ')$#', $host . $file_path ) ) { return true; } } return false; } } Engine/Optimization/Minify/CSS/AdminSubscriber.php 0000644 00000007622 15174677547 0016145 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\Minify\CSS; use WP_Rocket\Event_Management\Subscriber_Interface; class AdminSubscriber implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @since 3.5.4 * * @return array */ public static function get_subscribed_events() { $slug = rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ); return [ "update_option_{$slug}" => [ 'clean_minify', 10, 2 ], "pre_update_option_{$slug}" => [ 'regenerate_minify_css_key', 10, 2 ], 'wp_rocket_upgrade' => [ 'on_update', 16, 2 ], 'rocket_meta_boxes_fields' => [ 'add_meta_box', 1 ], ]; } /** * Clean minify CSS files when options change. * * @since 3.5.4 * * @param array $old An array of previous settings. * @param array $new An array of submitted settings. * * @return void */ public function clean_minify( array $old, array $new ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.newFound if ( ! $this->maybe_minify_regenerate( $new, $old ) ) { return; } // Purge all minify cache files. rocket_clean_minify( 'css' ); } /** * Regenerate the minify key if CSS files have been modified. * * @since 3.5.4 * * @param array $new An array of submitted settings. * @param mixed $old An array of previous settings or false. * * @return array Updates 'minify_css_key' setting when regenerated; else, original submitted settings. */ public function regenerate_minify_css_key( array $new, $old ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.newFound $old = is_array( $old ) ? $old : []; if ( ! $this->maybe_minify_regenerate( $new, $old ) ) { return $new; } $new['minify_css_key'] = create_rocket_uniqid(); return $new; } /** * Checks minify CSS condition when options change. * * @since 3.5.4 * * @param array $new An array of submitted settings. * @param array $old An array of previous settings. * * @return bool true when should regenerate; else false. */ protected function maybe_minify_regenerate( array $new, array $old ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.newFound $settings_to_check = [ 'minify_css', 'exclude_css', 'cdn', 'host_fonts_locally', ]; foreach ( $settings_to_check as $setting ) { if ( $this->did_setting_change( $setting, $new, $old ) ) { return true; } } return ( array_key_exists( 'cdn', $new ) && 1 === (int) $new['cdn'] && $this->did_setting_change( 'cdn_cnames', $new, $old ) ); } /** * Checks if the given setting's value changed. * * @since 3.5.4 * * @param string $setting The settings's value to check in the old and new values. * @param array $new An array of submitted settings. * @param array $old An array of previous settings. * * @return bool */ protected function did_setting_change( $setting, array $new, array $old ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.newFound return ( array_key_exists( $setting, $old ) && array_key_exists( $setting, $new ) && // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual $old[ $setting ] != $new[ $setting ] ); } /** * Clean cache on update. * * @param string $new_version new version from the plugin. * @param string $old_version old version from the plugin. * * @return void */ public function on_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.15', '>=' ) ) { return; } rocket_clean_domain(); } /** * Add the field to the WP Rocket metabox on the post edit page. * * @param string[] $fields Metaboxes fields. * * @return string[] */ public function add_meta_box( array $fields ) { $fields['minify_css'] = __( 'Minify CSS', 'rocket' ); return $fields; } } Engine/Optimization/Minify/AbstractMinifySubscriber.php 0000644 00000006342 15174677547 0017402 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\Minify; use WP_Filesystem_Direct; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Parent class for minify subscribers. */ abstract class AbstractMinifySubscriber implements Subscriber_Interface { /** * Plugin options. * * @var Options_Data */ protected $options; /** * Processor instance. * * @var ProcessorInterface */ protected $processor; /** * Filesystem instance * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Creates an instance of inheriting class. * * @since 3.1 * * @param Options_Data $options Plugin options. * @param WP_Filesystem_Direct $filesystem Filesystem instance. */ public function __construct( Options_Data $options, $filesystem ) { $this->options = $options; $this->filesystem = $filesystem; } /** * Sets the type of processor to use * * @since 3.1 * * @param ProcessorInterface $processor Processor instance. * @return void */ protected function set_processor_type( ProcessorInterface $processor ) { $this->processor = $processor; } /** * Processes the HTML to perform an optimization and return the new content * * @since 3.1 * * @param string $html HTML content. * @return string */ abstract public function process( $html ); /** * Checks if files can be optimized * * @since 3.1 */ abstract protected function is_allowed(); /** * Fix issue with SSL and minification * * @since 2.3 * * @param string $url An url to filter to set the scheme to https if needed. * @return string */ public function fix_ssl_minify( $url ) { if ( ! is_ssl() ) { return $url; } if ( 0 === strpos( $url, 'https://' ) ) { return $url; } // This filter is documented in inc/Engine/Admin/Settings/Settings.php. if ( in_array( wp_parse_url( $url, PHP_URL_HOST ), apply_filters( 'rocket_cdn_hosts', [], ( $this->get_zones() ) ), true ) ) { return $url; } return str_replace( 'http://', 'https://', $url ); } /** * Compatibility with multilingual plugins & multidomain configuration * * @since 2.6.13 Regression Fix: Apply CDN on minified CSS and JS files by checking the CNAME host * @since 2.6.8 * * @param string $url Minified file URL. * @return string Updated minified file URL */ public function i18n_multidomain_url( $url ) { if ( ! rocket_has_i18n() ) { return $url; } $url_host = wp_parse_url( $url, PHP_URL_HOST ); if ( isset( $_SERVER['HTTP_HOST'] ) && $url_host === $_SERVER['HTTP_HOST'] ) { return $url; } if ( ! in_array( $_SERVER['HTTP_HOST'], get_rocket_i18n_host(), true ) ) { return $url; } // This filter is documented in inc/Engine/Admin/Settings/Settings.php. $cdn_hosts = apply_filters( 'rocket_cdn_hosts', [], ( $this->get_zones() ) ); if ( in_array( $url_host, $cdn_hosts, true ) ) { return $url; } return preg_replace( '/' . preg_quote( $url_host, '/' ) . '/', sanitize_text_field( $_SERVER['HTTP_HOST'] ), $url, 1 ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash } /** * Returns an array of CDN zones for CSS files. * * @since 3.1 * * @return array */ public function get_zones() { return []; } } Engine/Optimization/Minify/AdminSubscriber.php 0000644 00000002627 15174677547 0015515 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\Minify; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class AdminSubscriber implements Subscriber_Interface { /** * WP Rocket Options * * @var Options_Data */ private $options; /** * Instantiate the class * * @param Options_Data $options WP Rocket Options Instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'switch_theme' => 'clean_minify_all', 'rocket_meta_boxes_fields' => [ 'add_meta_box', 4 ], ]; } /** * Delete all minified cache files * * @return void */ public function clean_minify_all() { // Bail out if minify_js or minify_css is not enabled. if ( ! (bool) $this->options->get( 'minify_js', 0 ) && ! (bool) $this->options->get( 'minify_css', 0 ) ) { return; } // Delete all minify cache files. rocket_clean_minify(); } /** * Add the field to the WP Rocket metabox on the post edit page. * * @param string[] $fields Metaboxes fields. * * @return string[] */ public function add_meta_box( array $fields ) { $fields['minify_js'] = __( 'Minify/combine JavaScript', 'rocket' ); return $fields; } } Engine/Optimization/AssetsLocalCache.php 0000644 00000010041 15174677547 0014334 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization; use WP_Filesystem_Direct; /** * Cache locally 3rd party assets. * * @since 3.1 */ class AssetsLocalCache { /** * 3rd party assets cache folder path * * @since 3.1 * * @var string */ protected $cache_path; /** * Filesystem instance. * * @var WP_Filesystem_Direct */ private $filesystem; /** * Cache contents with url. * * @var array */ private $url_cache = []; /** * Constructor * * @since 3.1 * * @param string $cache_path 3rd party assets cache folder path. * @param WP_Filesystem_Direct $filesystem Filesysten instance. */ public function __construct( $cache_path, $filesystem ) { $this->cache_path = "{$cache_path}3rd-party/"; $this->filesystem = $filesystem; } /** * Get remote file contents. * * @param string $url Url of the file to get contents for. * * @return string Raw file contents. */ private function get_raw_content( $url ) { $cache_key = md5( $url ); if ( ! isset( $this->url_cache[ $cache_key ] ) ) { $this->url_cache[ $cache_key ] = wp_remote_retrieve_body( wp_remote_get( $url ) ); } return $this->url_cache[ $cache_key ]; } /** * Gets content for the provided URL. * Use the local cache file if it exists, else get it from the 3rd party URL and save it locally for future use. * * @since 3.1 * * @param string $url URL to get the content from. * * @return string */ public function get_content( $url ) { $filepath = $this->get_filepath( $url ); if ( empty( $filepath ) ) { return ''; } if ( $this->filesystem->is_readable( $filepath ) ) { return $this->filesystem->get_contents( $filepath ); } $content = $this->get_raw_content( $url ); if ( empty( $content ) ) { return ''; } $this->write_file( $content, $filepath ); return $content; } /** * Gets the filepath of the local copy for the given URL * * @since 3.7 * * @param string $url URL to get filepath for. * @return string */ public function get_filepath( $url ) { $parts = wp_parse_url( $url ); if ( empty( $parts['path'] ) ) { return ''; } $filename = $parts['host'] . str_replace( '/', '-', $parts['path'] ); return $this->cache_path . $filename; } /** * Writes the content to a file * * @since 3.1 * * @param string $content Content to write. * @param string $file Path to the file to write in. * @return bool */ protected function write_file( $content, $file ) { if ( $this->filesystem->is_readable( $file ) ) { return true; } if ( ! rocket_mkdir_p( dirname( $file ) ) ) { return false; } return rocket_put_content( $file, $content ); } /** * Check if this link HTML has integrity attribute or not? * * @since 3.7.5 * * @param string $asset Link HTML to be tested. * * @return array|false Matched array with integrityhashmethod, integrityhash keys. */ private function has_integrity( $asset ) { if ( ! preg_match( '#\s*integrity\s*=[\'"](?<integrityhashmethod>.*)-(?<integrityhash>.*)[\'"]#Ui', $asset, $integrity_matches ) ) { return false; } return $integrity_matches; } /** * Validate the integrity attribute if the content matches with the hashed integrity value. * * @param array $asset_matched the matched array which has 0, url keys. * * @return bool|string */ public function validate_integrity( $asset_matched ) { $integrity_matches = $this->has_integrity( $asset_matched[0] ); if ( false === $integrity_matches ) { return $asset_matched[0]; } // validate the hash algorithm. if ( ! in_array( $integrity_matches['integrityhashmethod'], hash_algos(), true ) ) { return false; } $content = $this->get_raw_content( $asset_matched['url'] ); $content_hash = base64_encode( hash( $integrity_matches['integrityhashmethod'], $content, true ) );// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode if ( $integrity_matches['integrityhash'] !== $content_hash ) { return false; } return str_replace( $integrity_matches[0], '', $asset_matched[0] ); } } Engine/Optimization/RUCSS/Cron/Subscriber.php 0000644 00000002672 15174677547 0015111 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\RUCSS\Cron; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Engine\Common\JobManager\JobProcessor; use WP_Rocket\Engine\Optimization\RUCSS\Database\Queries\UsedCSS as UsedCSS_Query; class Subscriber implements Subscriber_Interface { /** * JobProcessor instance * * @var JobProcessor */ private $job_processor; /** * UsedCss Query instance. * * @var UsedCSS_Query */ private $used_css_query; /** * Instantiate the class * * @param JobProcessor $job_processor JobProcessor instance. * @param UsedCSS_Query $used_css_query Usedcss Query instance. */ public function __construct( JobProcessor $job_processor, UsedCSS_Query $used_css_query ) { $this->job_processor = $job_processor; $this->used_css_query = $used_css_query; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_rucss_job_check_status' => 'check_job_status', ]; } /** * Handle old rucss job during upgrade from versions < 3.16. * * @param integer $row_id DB Row ID. * @return void */ public function check_job_status( int $row_id ): void { $row = $this->used_css_query->get_row_by_id( $row_id ); if ( ! is_object( $row ) ) { return; } $this->job_processor->check_job_status( $row->url, $row->is_mobile, 'rucss' ); } } Engine/Optimization/RUCSS/ServiceProvider.php 0000644 00000011017 15174677547 0015211 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Optimization\RUCSS; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Optimization\RUCSS\Admin\{Database, OptionSubscriber, Settings}; use WP_Rocket\Engine\Optimization\RUCSS\Admin\Subscriber as AdminSubscriber; use WP_Rocket\Engine\Optimization\RUCSS\Context\RUCSSContext; use WP_Rocket\Engine\Optimization\RUCSS\Context\RUCSSOptimizeContext; use WP_Rocket\Engine\Optimization\RUCSS\Controller\Filesystem; use WP_Rocket\Engine\Common\JobManager\Queue\Queue; use WP_Rocket\Engine\Optimization\RUCSS\Controller\UsedCSS as UsedCSSController; use WP_Rocket\Engine\Optimization\RUCSS\Database\Queries\UsedCSS as UsedCSSQuery; use WP_Rocket\Engine\Optimization\RUCSS\Database\Tables\UsedCSS as UsedCSSTable; use WP_Rocket\Engine\Optimization\RUCSS\Frontend\Subscriber as FrontendSubscriber; use WP_Rocket\Engine\Optimization\RUCSS\Jobs\{Manager, Factory}; use WP_Rocket\Engine\Optimization\RUCSS\Context\RUCSSContextSaas; use WP_Rocket\Engine\Optimization\RUCSS\Cron\Subscriber as CronSubscriber; /** * Service provider for the WP Rocket RUCSS */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'rucss_usedcss_table', 'rucss_settings', 'rucss_database', 'rucss_option_subscriber', 'rucss_admin_subscriber', 'rucss_used_css', 'rucss_used_css_query', 'rucss_frontend_subscriber', 'rucss_queue', 'rucss_filesystem', 'rucss_used_css_controller', 'rucss_manager', 'rucss_context_saas', 'rucss_factory', 'rucss_cron_subscriber', 'rucss_context', 'rucss_optimize_context', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the option array in the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'rucss_usedcss_table', UsedCSSTable::class ); $this->getContainer()->add( 'rucss_database', Database::class ) ->addArgument( 'rucss_usedcss_table' ); $this->getContainer()->add( 'rucss_settings', Settings::class ) ->addArguments( [ 'options', 'beacon', 'rucss_usedcss_table', ] ); $this->getContainer()->add( 'rucss_used_css_query', UsedCSSQuery::class ); $this->getContainer()->add( 'rucss_queue', Queue::class ); $this->getContainer()->add( 'rucss_filesystem', Filesystem::class ) ->addArguments( [ new StringArgument( rocket_get_constant( 'WP_ROCKET_USED_CSS_PATH', '' ) ), rocket_direct_filesystem(), ] ); $this->getContainer()->add( 'rucss_context', RUCSSContext::class ) ->addArguments( [ 'options', 'rucss_filesystem', ] ); $this->getContainer()->add( 'rucss_optimize_context', RUCSSOptimizeContext::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'rucss_context_saas', RUCSSContextSaas::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'rucss_manager', Manager::class ) ->addArguments( [ 'rucss_used_css_query', 'rucss_filesystem', 'rucss_context_saas', 'options', ] ); $this->getContainer()->addShared( 'rucss_factory', Factory::class ) ->addArguments( [ 'rucss_manager', 'rucss_usedcss_table', ] ); $this->getContainer()->add( 'rucss_used_css_controller', UsedCSSController::class ) ->addArguments( [ 'options', 'rucss_used_css_query', 'dynamic_lists_defaultlists_data_manager', 'rucss_filesystem', 'rucss_context', 'rucss_manager', ] ); $this->getContainer()->addShared( 'rucss_option_subscriber', OptionSubscriber::class ) ->addArgument( 'rucss_settings' ); $this->getContainer()->addShared( 'rucss_admin_subscriber', AdminSubscriber::class ) ->addArguments( [ 'rucss_settings', 'rucss_database', 'rucss_used_css_controller', 'rucss_queue', ] ); $this->getContainer()->addShared( 'rucss_frontend_subscriber', FrontendSubscriber::class ) ->addArguments( [ 'rucss_used_css_controller', 'rucss_context', ] ); $this->getContainer()->addShared( 'rucss_cron_subscriber', CronSubscriber::class ) ->addArguments( [ 'job_processor', 'rucss_used_css_query', ] ); } } Engine/Optimization/RUCSS/Controller/Filesystem.php 0000644 00000006023 15174677547 0016346 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\RUCSS\Controller; use WP_Filesystem_Direct; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use WP_Rocket\Engine\Common\AbstractFileSystem; class Filesystem extends AbstractFileSystem { /** * WP Filesystem instance * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Path to the used CSS storage * * @var string */ private $path; /** * Instantiate the class * * @param string $base_path Base path to the used CSS storage. * @param WP_Filesystem_Direct $filesystem WP Filesystem instance. */ public function __construct( $base_path, $filesystem = null ) { parent::__construct( is_null( $filesystem ) ? rocket_direct_filesystem() : $filesystem ); $this->path = $base_path . get_current_blog_id() . '/'; } /** * Get the file path for the used CSS file. * * @param string $hash Hash of the file contents. * * @return string Path for the used CSS file. */ private function get_usedcss_full_path( string $hash ) { return $this->path . $this->hash_to_path( $hash ) . '.css' . ( function_exists( 'gzdecode' ) ? '.gz' : '' ); } /** * Gets the used CSS content corresponding to the provided hash * * @param string $hash Hash of the corresponding used CSS. * * @return string */ public function get_used_css( string $hash ): string { $file = $this->get_usedcss_full_path( $hash ); if ( ! $this->filesystem->exists( $file ) ) { return ''; } $file_contents = $this->get_file_content( $file ); $css = function_exists( 'gzdecode' ) ? gzdecode( $file_contents ) : $file_contents; if ( ! $css ) { return ''; } return $css; } /** * Writes the used CSS to the filesystem * * @param string $hash Hash to use for the filename. * @param string $used_css Used CSS content. * * @return bool */ public function write_used_css( string $hash, string $used_css ): bool { $file = $this->get_usedcss_full_path( $hash ); if ( ! rocket_mkdir_p( dirname( $file ) ) ) { return false; } // This filter is documented in inc/classes/Buffer/class-cache.php. $css = function_exists( 'gzencode' ) ? gzencode( $used_css, apply_filters( 'rocket_gzencode_level_compression', 6 ) ) : $used_css; if ( ! $css ) { return false; } return $this->write_file( $file, $css ); } /** * Deletes the used CSS files for the corresponding hash * * @since 3.11.4 * * @param string $hash md5 hash string. * * @return bool */ public function delete_used_css( string $hash ): bool { $file = $this->get_usedcss_full_path( $hash ); return $this->delete_file( $file ); } /** * Deletes all the used CSS files * * @since 3.11.4 * * @return void */ public function delete_all_used_css() { $this->delete_all_files_from_directory( $this->path ); } /** * Checks if the used CSS storage folder is writable * * @since 3.11.4 * * @return bool */ public function is_writable_folder() { return $this->is_folder_writable( $this->path ); } } Engine/Optimization/RUCSS/Controller/UsedCSS.php 0000644 00000042456 15174677547 0015505 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Optimization\RUCSS\Controller; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Common\Head\ElementTrait; use WP_Rocket\Engine\Optimization\CSSTrait; use WP_Rocket\Engine\Optimization\DynamicLists\DefaultLists\DataManager; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Engine\Optimization\RUCSS\Database\Queries\UsedCSS as UsedCSS_Query; use WP_Rocket\Engine\Optimization\RUCSS\Jobs\Manager; use WP_Rocket\Engine\Support\CommentTrait; class UsedCSS { use RegexTrait; use CSSTrait; use CommentTrait; use ElementTrait; /** * UsedCss Query instance. * * @var UsedCSS_Query */ private $used_css_query; /** * Plugin options instance. * * @var Options_Data */ protected $options; /** * DataManager instance * * @var DataManager */ private $data_manager; /** * Filesystem instance * * @var Filesystem */ private $filesystem; /** * RUCSS context. * * @var ContextInterface */ protected $context; /** * External exclusions list, can be urls or attributes. * * @var array */ private $external_exclusions = []; /** * Inline CSS attributes exclusions patterns to be preserved on the page after treeshaking. * * @var string[] */ private $inline_atts_exclusions = []; /** * Inline CSS content exclusions patterns to be preserved on the page after treeshaking. * * @var string[] */ private $inline_content_exclusions = []; /** * Above the fold Job Manager. * * @var Manager */ private $manager; /** * Used CSS contents. * * @var string */ private $used_css_content = ''; /** * Preloaded font urls. * * @var array */ private $preloaded_fonts = []; /** * Instantiate the class. * * @param Options_Data $options Options instance. * @param UsedCSS_Query $used_css_query Usedcss Query instance. * @param DataManager $data_manager DataManager instance. * @param Filesystem $filesystem Filesystem instance. * @param ContextInterface $context RUCSS context. * @param Manager $manager RUCSS manager. */ public function __construct( Options_Data $options, UsedCSS_Query $used_css_query, DataManager $data_manager, Filesystem $filesystem, ContextInterface $context, Manager $manager ) { $this->options = $options; $this->used_css_query = $used_css_query; $this->data_manager = $data_manager; $this->filesystem = $filesystem; $this->context = $context; $this->manager = $manager; } /** * Check if RUCSS option is enabled. * * Used inside the CRON so post object isn't there. * * @return bool */ public function is_enabled() { return (bool) $this->options->get( 'remove_unused_css', 0 ); } /** * Start treeshaking the current page. * * @param string $html Buffet HTML for current page. * * @return string */ public function treeshake( string $html ): string { if ( ! $this->context->is_allowed() ) { return $html; } $clean_html = $this->hide_comments( $html ); $clean_html = $this->hide_noscripts( $clean_html ); $clean_html = $this->hide_scripts( $clean_html ); if ( ! $this->html_has_title_tag( $clean_html ) ) { return $html; } global $wp; $url = untrailingslashit( home_url( add_query_arg( [], $wp->request ) ) ); $is_mobile = $this->is_mobile(); $used_css = $this->used_css_query->get_row( $url, $is_mobile ); if ( ! is_object( $used_css ) ) { $this->manager->add_url_to_the_queue( $url, $is_mobile ); return $html; } if ( 'completed' !== $used_css->status || empty( $used_css->hash ) ) { return $html; } $used_css_content = $this->filesystem->get_used_css( $used_css->hash ); if ( empty( $used_css_content ) ) { $this->used_css_query->delete_by_url( $url ); return $html; } $this->used_css_content = $used_css_content; $html = $this->remove_used_css_from_html( $clean_html, $html ); $this->add_used_fonts_preload( $used_css_content ); $html = $this->remove_google_font_preconnect( $html ); if ( ! empty( $used_css->id ) ) { $this->used_css_query->update_last_accessed( (int) $used_css->id ); } return $this->add_meta_comment( 'remove_unused_css', $html ); } /** * Delete used css based on URL. * * @param string $url The page URL. * * @return boolean */ public function delete_used_css( string $url ): bool { $used_css_arr = $this->used_css_query->get_rows_by_url( $url ); if ( empty( $used_css_arr ) ) { return false; } $deleted = true; foreach ( $used_css_arr as $used_css ) { if ( ! is_object( $used_css ) || empty( $used_css->id ) ) { continue; } $deleted = $deleted && $this->used_css_query->delete_item( $used_css->id ); if ( ! empty( $used_css->hash ) ) { $count = $this->used_css_query->count_rows_by_hash( $used_css->hash ); if ( 0 === $count ) { $this->filesystem->delete_used_css( $used_css->hash ); } } } return $deleted; } /** * Deletes all the used CSS files * * @since 3.11.4 * * @return void */ public function delete_all_used_css() { $this->filesystem->delete_all_used_css(); } /** * Alter HTML and remove all CSS which was processed from HTML page. * * @param string $clean_html Cleaned HTML after removing comments, noscripts and scripts. * @param string $html HTML content. * * @return string HTML content. */ private function remove_used_css_from_html( string $clean_html, string $html ): string { $this->set_inline_exclusions_lists(); $html = $this->remove_external_styles_from_html( $clean_html, $html ); return $this->remove_internal_styles_from_html( $clean_html, $html ); } /** * Remove external styles from the page's HTML. * * @param string $clean_html Cleaned HTML after removing comments, noscripts and scripts. * @param string $html Actual page's HTML. * * @return string */ private function remove_external_styles_from_html( string $clean_html, string $html ) { $link_styles = $this->find( '<link\s+([^>]+[\s"\'])?href\s*=\s*[\'"]\s*?(?<url>[^\'"]+(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>', $clean_html, 'Uis' ); $preserve_google_font = apply_filters( 'rocket_rucss_preserve_google_font', false ); $external_exclusions = $this->validate_array_and_quote( /** * Filters the array of external exclusions. * * @since 3.11.4 * * @param array $external_exclusions Array of patterns used to match against the external style tag. */ (array) apply_filters( 'rocket_rucss_external_exclusions', $this->external_exclusions ) ); foreach ( $link_styles as $style ) { if ( ( ! (bool) preg_match( '/rel=[\'"]?stylesheet[\'"]?/is', $style[0] ) && ! ( (bool) preg_match( '/rel=[\'"]?preload[\'"]?/is', $style[0] ) && (bool) preg_match( '/as=[\'"]?style[\'"]?/is', $style[0] ) ) ) || ( $preserve_google_font && strstr( $style['url'], '//fonts.googleapis.com/css' ) ) ) { continue; } if ( ! empty( $external_exclusions ) && $this->find( implode( '|', $external_exclusions ), $style[0] ) ) { continue; } $html = str_replace( $style[0], '', $html ); } return (string) $html; } /** * Remove internal styles from the page's HTML. * * @param string $clean_html Cleaned HTML after removing comments, noscripts and scripts. * @param string $html Actual page's HTML. * * @return string */ private function remove_internal_styles_from_html( string $clean_html, string $html ) { $inline_styles = $this->find( '<style(?<atts>.*)>(?<content>.*)<\/style\s*>', $clean_html ); $inline_atts_exclusions = $this->validate_array_and_quote( /** * Filters the array of inline CSS attributes patterns to preserve * * @since 3.11 * * @param array $inline_atts_exclusions Array of patterns used to match against the inline CSS attributes. */ (array) apply_filters( 'rocket_rucss_inline_atts_exclusions', $this->inline_atts_exclusions ) ); $inline_content_exclusions = $this->validate_array_and_quote( /** * Filters the array of inline CSS content patterns to preserve * * @since 3.11 * * @param array $inline_atts_exclusions Array of patterns used to match against the inline CSS content. */ (array) apply_filters( 'rocket_rucss_inline_content_exclusions', $this->inline_content_exclusions ) ); foreach ( $inline_styles as $style ) { if ( ! empty( $inline_atts_exclusions ) && $this->find( implode( '|', $inline_atts_exclusions ), $style['atts'] ) ) { continue; } if ( ! empty( $inline_content_exclusions ) && $this->find( implode( '|', $inline_content_exclusions ), $style['content'] ) ) { continue; } /** * Filters the status of preserving inline style tags. * * @since 3.11.4 * * @param bool $preserve_status Status of preserve. * @param array $style Full match style tag. */ if ( apply_filters( 'rocket_rucss_preserve_inline_style_tags', true, $style ) ) { $content = trim( $style['content'] ); if ( empty( $content ) ) { continue; } $empty_tag = str_replace( $style['content'], '', $style[0] ); $html = str_replace( $style[0], $empty_tag, $html ); continue; } $html = str_replace( $style[0], '', $html ); } return $html; } /** * Add the used CSS style in <head> tag, * * @param array $items Head items. * * @return array Filtered head items. */ public function add_used_css_to_html( array $items ): array { $used_css = $this->get_used_css_content(); if ( empty( $used_css ) ) { return $items; } // Remove locally hosted google fonts. $items = array_filter( $items, function ( $item ) { return ! isset( $item['data-wpr-hosted-gf-parameters'] ); } ); $items[] = $this->style_tag( $this->get_used_css_markup( $used_css ), [ 'id' => 'wpr-usedcss', ] ); return $items; } /** * Insert preload fonts into page head. * * @param array $items Head elements. * @return mixed */ public function insert_preload_fonts( $items ) { if ( ! $this->context->is_allowed() ) { return $items; } foreach ( $this->preloaded_fonts as $font ) { $items[] = $this->preload_link( [ 'href' => esc_url( $font ), 'as' => 'font', 1 => 'crossorigin', ] ); } return $items; } /** * Return Markup for used_css into the page. * * @param string $used_css Used CSS content. * * @return string */ private function get_used_css_markup( string $used_css ): string { /** * Filters Used CSS content before output. * * @since 3.9.0.2 * * @param string $used_css Used CSS content. */ $used_css = apply_filters( 'rocket_usedcss_content', $used_css ); $used_css = str_replace( '\\', '\\\\', $used_css );// Guard the backslashes before passing the content to preg_replace. return $this->handle_charsets( $used_css, false ); } /** * Determines if the page is mobile and separate cache for mobile files is enabled. * * @return boolean */ private function is_mobile(): bool { return $this->options->get( 'cache_mobile', 0 ) && $this->options->get( 'do_caching_mobile_files', 0 ) && wp_is_mobile(); } /** * Clear specific url. * * @param string $url Page url. * * @return void */ public function clear_url_usedcss( string $url ) { $this->delete_used_css( $url ); /** * Fires after clearing usedcss for specific url. * * @since 3.11 * * @param string $url Current page URL. */ do_action( 'rocket_rucss_after_clearing_usedcss', $url ); } /** * Get the count of not completed rows. * * @return int */ public function get_not_completed_count() { return $this->used_css_query->get_not_completed_count(); } /** * Add preload links for the fonts in the used CSS * * @param string $used_css Used CSS content. * * @return void */ private function add_used_fonts_preload( string $used_css ): void { /** * Filters the fonts preload from the used CSS * * @since 3.11 * * @param bool $enable True to enable, false to disable. */ if ( ! apply_filters( 'rocket_enable_rucss_fonts_preload', true ) ) { return; } if ( ! preg_match_all( '/@font-face\s*{\s*(?<content>[^}]+)}/is', $used_css, $font_faces, PREG_SET_ORDER ) ) { return; } if ( empty( $font_faces ) ) { return; } /** * Filters the list of fonts to exclude from preload * * @since 3.15.10 * * @param array $excluded_fonts_preload List of fonts to exclude from preload */ $exclude_fonts_preload = wpm_apply_filters_typed( 'array', 'rocket_exclude_rucss_fonts_preload', [] ); $urls = []; foreach ( $font_faces as $font_face ) { if ( empty( $font_face['content'] ) ) { continue; } $font_url = $this->extract_first_font( $font_face['content'] ); /** * Filters font URL with CDN hostname * * @since 3.11.4 * * @param string $url url to be rewritten. */ $font_url = apply_filters( 'rocket_font_url', $font_url ); if ( empty( $font_url ) ) { continue; } // Making sure the excluded fonts array isn't empty to avoid excluding all fonts. if ( ! empty( $exclude_fonts_preload ) ) { $exclude_fonts_preload = array_filter( $exclude_fonts_preload ); // Combine the array elements into a single string with | as a separator and returning a pattern. $exclude_fonts_preload_pattern = implode( '|', array_map( function ( $item ) { return is_string( $item ) ? preg_quote( $item, '/' ) : ''; }, $exclude_fonts_preload ) ); // Check if the font URL matches any part of the exclude_fonts_preload array. if ( ! empty( $exclude_fonts_preload_pattern ) && preg_match( '/' . $exclude_fonts_preload_pattern . '/i', $font_url ) ) { continue; // Skip this iteration as the font URL is in the exclusion list. } } $urls[] = $font_url; } if ( empty( $urls ) ) { return; } $this->preloaded_fonts = array_unique( $urls ); } /** * Remove preconnect tag for google api. * * @param string $html html content. * * @return string */ protected function remove_google_font_preconnect( string $html ): string { $clean_html = $this->hide_comments( $html ); $clean_html = $this->hide_noscripts( $clean_html ); $clean_html = $this->hide_scripts( $clean_html ); $links = $this->find( '<link\s+([^>]+[\s"\'])?rel\s*=\s*[\'"]((preconnect)|(dns-prefetch))[\'"]([^>]+)?\/?>', $clean_html, 'Uis' ); foreach ( $links as $link ) { if ( preg_match( '/href=[\'"](https:)?\/\/fonts.googleapis.com\/?[\'"]/', $link[0] ) ) { $html = str_replace( $link[0], '', $html ); } } return $html; } /** * Extracts the first font URL from the font-face declaration * * Skips .eot fonts if it exists * * @since 3.11 * * @param string $font_face Font-face declaration content. * * @return string */ private function extract_first_font( string $font_face ): string { if ( ! preg_match_all( '/src:\s*(?<urls>[^;}]*)/is', $font_face, $sources, PREG_SET_ORDER ) ) { return ''; } foreach ( $sources as $src ) { if ( empty( $src['urls'] ) ) { continue; } $urls = explode( ',', $src['urls'] ); foreach ( $urls as $url ) { if ( false !== strpos( $url, '.eot' ) ) { continue; } if ( ! preg_match( '/url\(\s*[\'"]?(?<url>[^\'")]+)[\'"]?\)/is', $url, $matches ) ) { continue; } return trim( $matches['url'] ); } } return ''; } /** * Set Rucss inline attr exclusions * * @return void */ private function set_inline_exclusions_lists() { $wpr_dynamic_lists = $this->data_manager->get_lists(); $this->inline_atts_exclusions = isset( $wpr_dynamic_lists->rucss_inline_atts_exclusions ) ? $wpr_dynamic_lists->rucss_inline_atts_exclusions : []; $this->inline_content_exclusions = isset( $wpr_dynamic_lists->rucss_inline_content_exclusions ) ? $wpr_dynamic_lists->rucss_inline_content_exclusions : []; } /** * Displays a notice if the used CSS folder is not writable * * @since 3.11.4 * * @return void */ public function notice_write_permissions() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( ! $this->is_enabled() ) { return; } if ( $this->filesystem->is_writable_folder() ) { return; } $message = rocket_notice_writing_permissions( trim( str_replace( rocket_get_constant( 'ABSPATH', '' ), '', rocket_get_constant( 'WP_ROCKET_USED_CSS_PATH', '' ) ), '/' ) ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, ] ); } /** * Validate the items in array to be strings only and preg_quote them. * * @param array $items Array to be validated and quoted. * * @return array|string[] */ private function validate_array_and_quote( array $items ) { $items_array = array_filter( $items, 'is_string' ); return array_map( static function ( $item ) { return preg_quote( $item, '/' ); }, $items_array ); } /** * Check if database has at least one completed row. * * @return bool */ public function has_one_completed_row_at_least() { return $this->used_css_query->get_completed_count() > 0; } /** * Get generated used CSS, getter method for used_css_content property. * * @return string */ public function get_used_css_content() { if ( ! $this->context->is_allowed() ) { return ''; } return $this->used_css_content; } } Engine/Optimization/RUCSS/Jobs/Factory.php 0000644 00000001774 15174677547 0014413 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\RUCSS\Jobs; use WP_Rocket\Engine\Common\JobManager\AbstractFactory\SaasFactory; use WP_Rocket\Engine\Common\JobManager\Managers\ManagerInterface; use WP_Rocket\Engine\Common\Database\TableInterface; class Factory implements SaasFactory { /** * RUCSS Manager. * * @var ManagerInterface */ private $manager; /** * RUCSS Table. * * @var TableInterface */ private $table; /** * Instantiate the class. * * @param ManagerInterface $manager RUCSS Manager. * @param TableInterface $table RUCSS Table. */ public function __construct( ManagerInterface $manager, TableInterface $table ) { $this->manager = $manager; $this->table = $table; } /** * RUCSS job manager. * * @return ManagerInterface */ public function manager(): ManagerInterface { return $this->manager; } /** * RUCSS Table. * * @return TableInterface */ public function table(): TableInterface { return $this->table; } } Engine/Optimization/RUCSS/Jobs/Manager.php 0000644 00000013236 15174677547 0014352 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\RUCSS\Jobs; use WP_Rocket\Logger\LoggerAware; use WP_Rocket\Logger\LoggerAwareInterface; use WP_Rocket\Engine\Optimization\RUCSS\Database\Queries\UsedCSS as UsedCSS_Query; use WP_Rocket\Engine\Optimization\RUCSS\Controller\Filesystem; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Optimization\CSSTrait; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\JobManager\Managers\AbstractManager; use WP_Rocket\Engine\Common\JobManager\Managers\ManagerInterface; class Manager implements ManagerInterface, LoggerAwareInterface { use LoggerAware; use CSSTrait; use AbstractManager; /** * UsedCss Query instance. * * @var UsedCSS_Query */ protected $query; /** * Filesystem instance * * @var Filesystem */ private $filesystem; /** * RUCSS Context. * * @var ContextInterface */ protected $context; /** * The type of optimization applied for the current job. * * @var string */ protected $optimization_type = 'rucss'; /** * Check if manager can process. * * @var boolean */ protected $can_process = true; /** * Plugin options instance. * * @var Options_Data */ protected $options; /** * Instantiate the class. * * @param UsedCSS_Query $query Usedcss Query instance. * @param Filesystem $filesystem Filesystem instance. * @param ContextInterface $context RUCSS Context. * @param Options_Data $options Options instance. */ public function __construct( UsedCSS_Query $query, Filesystem $filesystem, ContextInterface $context, Options_Data $options ) { $this->query = $query; $this->filesystem = $filesystem; $this->context = $context; $this->options = $options; } /** * Get pending jobs from db. * * @param integer $num_rows Number of rows to grab. * @return array */ public function get_pending_jobs( int $num_rows ): array { $this->logger::debug( "RUCSS: Start getting number of {$num_rows} pending jobs." ); $pending_jobs = $this->query->get_pending_jobs( $num_rows ); if ( ! $pending_jobs ) { $this->logger::debug( 'RUCSS: No pending jobs are there.' ); return []; } return $pending_jobs; } /** * Validate SaaS response and fail job. * * @param array $job_details Details related to the job.. * @param object $row_details Details related to the row. * @param string $optimization_type The type of optimization applied for the current job. * * @return void */ public function validate_and_fail( array $job_details, $row_details, string $optimization_type ): void { if ( 'all' !== $optimization_type && $this->optimization_type !== $optimization_type ) { return; } /** * Filters the rocket min rucss css result size. * * @since 3.13.3 * * @param int $min_rucss_size min size. */ $min_rucss_size = wpm_apply_filters_typed( 'integer', 'rocket_min_rucss_size', 150 ); if ( isset( $job_details['contents']['shakedCSS_size'] ) && intval( $job_details['contents']['shakedCSS_size'] ) < $min_rucss_size ) { $message = 'RUCSS: shakedCSS size is less than ' . $min_rucss_size; $this->logger::error( $message ); $this->make_status_failed( $row_details->url, $row_details->is_mobile, '500', $message ); $this->can_process = false; } } /** * Process SaaS response. * * @param array $job_details Details related to the job.. * @param object $row_details Details related to the row. * @param string $optimization_type The type of optimization applied for the current job. * * @return void */ public function process( array $job_details, $row_details, string $optimization_type ): void { if ( ! $this->is_allowed( $optimization_type ) || ! $this->can_process ) { return; } $css = $this->apply_font_display_swap( $job_details['contents']['shakedCSS'] ); /** * RUCSS hash. * * @param string $hash RUCSS hash. * @param string $css RUCSS content. * @param UsedCSSRow $row_details Job details. */ $hash = (string) apply_filters( 'rocket_rucss_hash', md5( $css ), $css, $row_details ); if ( ! $this->filesystem->write_used_css( $hash, $css ) ) { $message = 'RUCSS: Could not write used CSS to the filesystem: ' . $row_details->url; $this->logger::error( $message ); $this->query->make_status_failed( $row_details->url, $row_details->is_mobile, '', $job_details['message'] ); return; } // Everything is fine, save the usedcss into DB, change status to completed and reset queue_name and job_id. $this->logger::debug( 'RUCSS: Save used CSS for url: ' . $row_details->url ); $this->query->make_status_completed( $row_details->url, $row_details->is_mobile, $hash ); } /** * Set the request parameter to be sent to the SaaS * * @return array */ public function set_request_param(): array { /** * Filters the RUCSS safelist * * @since 3.11 * * @param array $safelist Array of safelist values. */ $safelist = apply_filters( 'rocket_rucss_safelist', $this->options->get( 'remove_unused_css_safelist', [] ) ); /** * Filters the styles attributes to be skipped (blocked) by RUCSS. * * @since 3.14 * * @param array $skipped_attr Array of safelist values. */ $skipped_attr = wpm_apply_filters_typed( 'array', 'rocket_rucss_skip_styles_with_attr', [] ); return [ 'rucss_safelist' => $safelist, 'skip_attr' => $skipped_attr, 'optimization_list' => [ 'rucss', ], ]; } /** * Get the optimization type from the DB Row. * * @param object $row DB Row Object. * @return boolean|string */ public function get_optimization_type_from_row( $row ) { if ( ! isset( $row->css ) ) { return false; } return $this->optimization_type; } } Engine/Optimization/RUCSS/Database/Queries/UsedCSS.php 0000644 00000005322 15174677547 0016472 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\RUCSS\Database\Queries; use WP_Rocket\Engine\Common\Database\Queries\AbstractQuery; use WP_Rocket\Engine\Optimization\RUCSS\Database\Row\UsedCSS as UsedCSSRow; use WP_Rocket\Engine\Optimization\RUCSS\Database\Schemas\UsedCSS as UsedCSSSchema; /** * RUCSS UsedCSS Query. */ class UsedCSS extends AbstractQuery { /** * Name of the database table to query. * * @var string */ protected $table_name = 'wpr_rucss_used_css'; /** * String used to alias the database table in MySQL statement. * * Keep this short, but descriptive. I.E. "tr" for term relationships. * * This is used to avoid collisions with JOINs. * * @var string */ protected $table_alias = 'wpr_rucss'; /** * Name of class used to setup the database schema. * * @var string */ protected $table_schema = UsedCSSSchema::class; /** Item ******************************************************************/ /** * Name for a single item. * * Use underscores between words. I.E. "term_relationship" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name = 'used_css'; /** * Plural version for a group of items. * * Use underscores between words. I.E. "term_relationships" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name_plural = 'used_css'; /** * Name of class used to turn IDs into first-class objects. * * This is used when looping through return values to guarantee their shape. * * @var mixed */ protected $item_shape = UsedCSSRow::class; /** * Complete a job. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $hash Hash. * * @return bool */ public function make_status_completed( string $url, bool $is_mobile, string $hash = '' ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $prefixed_table_name = $db->prefix . $this->table_name; $data = [ 'hash' => $hash, 'status' => 'completed', ]; $where = [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, ]; return $db->update( $prefixed_table_name, $data, $where ); } /** * Get number of rows with the same hash value. * * @param string $hash Hash. * * @return int */ public function count_rows_by_hash( string $hash ): int { if ( ! self::$table_exists && ! $this->table_exists() ) { return 0; } return $this->query( [ 'hash' => $hash, 'count' => true, ] ); } } Engine/Optimization/RUCSS/Database/Schemas/UsedCSS.php 0000644 00000006570 15174677547 0016446 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\RUCSS\Database\Schemas; use WP_Rocket\Dependencies\BerlinDB\Database\Schema; /** * RUCSS UsedCSS Schema. */ class UsedCSS extends Schema { /** * Array of database column objects * * @var array */ public $columns = [ // ID column. [ 'name' => 'id', 'type' => 'bigint', 'length' => '20', 'unsigned' => true, 'extra' => 'auto_increment', 'primary' => true, 'sortable' => true, ], // URL column. [ 'name' => 'url', 'type' => 'varchar', 'length' => '2000', 'default' => '', 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // CSS column. [ 'name' => 'css', 'type' => 'longtext', 'default' => null, 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // Hash column. [ 'name' => 'hash', 'type' => 'varchar', 'length' => '32', 'default' => '', 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // error_code column. [ 'name' => 'error_code', 'type' => 'varchar', 'length' => '32', 'default' => null, 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // error_message column. [ 'name' => 'error_message', 'type' => 'longtext', 'default' => null, 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // RETRIES column. [ 'name' => 'retries', 'type' => 'tinyint', 'length' => '1', 'default' => 1, 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // IS_MOBILE column. [ 'name' => 'is_mobile', 'type' => 'tinyint', 'length' => '1', 'default' => 0, 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // JOB_ID column. [ 'name' => 'job_id', 'type' => 'varchar', 'length' => '255', 'default' => null, 'cache_key' => true, 'searchable' => false, 'sortable' => false, ], // QUEUE_NAME column. [ 'name' => 'queue_name', 'type' => 'varchar', 'length' => '255', 'default' => null, 'cache_key' => true, 'searchable' => false, 'sortable' => false, ], // STATUS column. [ 'name' => 'status', 'type' => 'varchar', 'length' => '255', 'default' => null, 'cache_key' => true, 'searchable' => true, 'sortable' => false, ], // MODIFIED column. [ 'name' => 'modified', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'modified' => true, 'date_query' => true, 'sortable' => true, ], // LAST_ACCESSED column. [ 'name' => 'last_accessed', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'date_query' => true, 'sortable' => true, ], // SUBMITTED_AT column. [ 'name' => 'submitted_at', 'type' => 'timestamp', 'default' => null, 'created' => true, 'date_query' => true, 'sortable' => true, ], // NEXT_RETRY_TIME column. [ 'name' => 'next_retry_time', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], ]; } Engine/Optimization/RUCSS/Database/Tables/UsedCSS.php 0000644 00000016367 15174677547 0016302 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\RUCSS\Database\Tables; use WP_Rocket\Engine\Common\Database\Tables\AbstractTable; /** * RUCSS UsedCSS Table. */ class UsedCSS extends AbstractTable { /** * Table name * * @var string */ protected $name = 'wpr_rucss_used_css'; /** * Database version key (saved in _options or _sitemeta) * * @var string */ protected $db_version_key = 'wpr_rucss_used_css_version'; /** * Database version * * @var int */ protected $version = 20231031; /** * Key => value array of versions => methods. * * @var array */ protected $upgrades = [ 20220121 => 'add_async_rucss_columns', 20220131 => 'make_status_column_index', 20220513 => 'add_hash_column', 20220920 => 'make_status_column_index_instead_queue_name', 20221104 => 'add_error_columns', 20231010 => 'add_submitted_at_column', 20231031 => 'add_next_retry_time_column', ]; /** * Table schema data. * * @var string */ protected $schema_data = " id bigint(20) unsigned NOT NULL AUTO_INCREMENT, url varchar(2000) NOT NULL default '', css longtext default NULL, hash varchar(32) default '', error_code varchar(32) NULL default NULL, error_message longtext NULL default NULL, unprocessedcss longtext NULL, retries tinyint(1) NOT NULL default 1, is_mobile tinyint(1) NOT NULL default 0, job_id varchar(255) NOT NULL default '', queue_name varchar(255) NOT NULL default '', status varchar(255) NOT NULL default '', modified timestamp NOT NULL default '0000-00-00 00:00:00', last_accessed timestamp NOT NULL default '0000-00-00 00:00:00', submitted_at timestamp NULL, next_retry_time timestamp NOT NULL default '0000-00-00 00:00:00', PRIMARY KEY (id), KEY url (url(150), is_mobile), KEY modified (modified), KEY last_accessed (last_accessed), INDEX `status_index` (`status`(191)), INDEX `error_code_index` (`error_code`(32)), KEY hash (hash)"; /** * Add queue columns. * * @return bool */ protected function add_async_rucss_columns() { $jobid_column_exists = $this->column_exists( 'job_id' ); $queuename_column_exists = $this->column_exists( 'queue_name' ); $status_column_exists = $this->column_exists( 'status' ); $created = true; if ( ! $jobid_column_exists ) { $created &= $this->get_db()->query( "ALTER TABLE {$this->table_name} ADD COLUMN job_id VARCHAR(255) NULL default '' AFTER is_mobile " ); } if ( ! $queuename_column_exists ) { $created &= $this->get_db()->query( "ALTER TABLE {$this->table_name} ADD COLUMN queue_name VARCHAR(255) NULL default '' AFTER job_id " ); } if ( ! $status_column_exists ) { $created &= $this->get_db()->query( "ALTER TABLE {$this->table_name} ADD COLUMN status VARCHAR(255) NULL default '' AFTER queue_name " ); } return $this->is_success( $created ); } /** * Make status column as index. * * @return bool */ protected function make_status_column_index() { $queuename_column_exists = $this->column_exists( 'queue_name' ); if ( ! $queuename_column_exists ) { return $this->is_success( false ); } if ( $this->index_exists( 'queue_name_index' ) ) { return $this->is_success( true ); } $index_added = $this->get_db()->query( "ALTER TABLE {$this->table_name} ADD INDEX `queue_name_index` (`queue_name`) " ); return $this->is_success( $index_added ); } /** * Add hash column and index * * @return bool */ protected function add_hash_column() { $hash_column_exists = $this->column_exists( 'hash' ); $created = true; if ( ! $hash_column_exists ) { $created &= $this->get_db()->query( "ALTER TABLE {$this->table_name} ADD COLUMN hash VARCHAR(32) NULL default '' AFTER css, ADD KEY hash (hash) " ); } return $this->is_success( $created ); } /** * Make status column as index. * * @return bool */ protected function make_status_column_index_instead_queue_name() { $queuename_column_exists = $this->column_exists( 'status' ); if ( ! $queuename_column_exists ) { return $this->is_success( false ); } if ( $this->index_exists( 'status_index' ) ) { return $this->is_success( true ); } if ( $this->index_exists( 'queue_name_index' ) ) { if ( ! $this->get_db()->query( "ALTER TABLE {$this->table_name} DROP INDEX `queue_name_index`" ) ) { return $this->is_success( false ); } } $index_added = $this->get_db()->query( "ALTER TABLE {$this->table_name} ADD INDEX `status_index` (`status`(191)) " ); return $this->is_success( $index_added ); } /** * Add error columns * * @return bool */ protected function add_error_columns() { return $this->add_error_message_column() && $this->add_error_code_column() && $this->make_error_code_column_index(); } /** * Add error_message column and index * * @return bool */ private function add_error_message_column() { $error_message_column_exists = $this->column_exists( 'error_message' ); $created = true; if ( ! $error_message_column_exists ) { $created &= $this->get_db()->query( "ALTER TABLE `{$this->table_name}` ADD COLUMN error_message longtext NULL default NULL AFTER hash" ); } return $this->is_success( $created ); } /** * Add error_code column and index * * @return bool */ private function add_error_code_column() { $error_code_column_exists = $this->column_exists( 'error_code' ); $created = true; if ( ! $error_code_column_exists ) { $created &= $this->get_db()->query( "ALTER TABLE `{$this->table_name}` ADD COLUMN error_code VARCHAR(32) NULL default NULL AFTER hash" ); } return $this->is_success( $created ); } /** * Make status column as index. * * @return bool */ private function make_error_code_column_index() { $error_code_column_exists = $this->column_exists( 'error_code' ); if ( ! $error_code_column_exists ) { return $this->is_success( false ); } if ( $this->index_exists( 'error_code_index' ) ) { return $this->is_success( true ); } $index_added = $this->get_db()->query( "ALTER TABLE {$this->table_name} ADD INDEX `error_code_index` (`error_code`) " ); return $this->is_success( $index_added ); } /** * Adds the submitted_at column * * @return bool */ protected function add_submitted_at_column() { $submitted_at_column_exists = $this->column_exists( 'submitted_at' ); $created = true; if ( ! $submitted_at_column_exists ) { $created &= $this->get_db()->query( "ALTER TABLE `{$this->table_name}` ADD COLUMN submitted_at timestamp NULL AFTER last_accessed" ); } return $this->is_success( $created ); } /** * Adds the next_retry_time column * * @return bool */ protected function add_next_retry_time_column() { $next_retry_time_exists = $this->column_exists( 'next_retry_time' ); $created = true; if ( ! $next_retry_time_exists ) { $created &= $this->get_db()->query( "ALTER TABLE `{$this->table_name}` ADD COLUMN next_retry_time timestamp NOT NULL default '0000-00-00 00:00:00' AFTER submitted_at" ); } return $this->is_success( $created ); } } Engine/Optimization/RUCSS/Database/Row/UsedCSS.php 0000644 00000004545 15174677547 0015632 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\RUCSS\Database\Row; use WP_Rocket\Dependencies\BerlinDB\Database\Row; /** * RUCSS UsedCSS Row. */ class UsedCSS extends Row { /** * Row ID * * @var int */ public $id; /** * URL * * @var string */ public $url; /** * CSS * * @var string */ public $css; /** * Hash storage value * * @var string */ public $hash; /** * Error code * * @var string */ public $error_code; /** * Error message * * @var string */ public $error_message; /** * Number of retries * * @var int */ public $retries; /** * Is CSS for mobile * * @var bool */ public $is_mobile; /** * Job ID * * @var string */ public $job_id; /** * Job queue name * * @var string */ public $queue_name; /** * Status * * @var string */ public $status; /** * Last modified time * * @var int */ public $modified; /** * Last accessed time * * @var int */ public $last_accessed; /** * Unused variable * * @var string */ public $unprocessedcss; /** * Submitted date * * @var int */ public $submitted_at; /** * Tells when the retry has to be processed * * @var int */ public $next_retry_time; /** * UsedCSS constructor. * * @param mixed $item Object Row. */ public function __construct( $item ) { parent::__construct( $item ); // Set the type of each column, and prepare. $this->id = (int) $this->id; $this->url = (string) $this->url; $this->css = (string) $this->css; $this->hash = (string) $this->hash; $this->error_code = (string) $this->error_code; $this->error_message = (string) $this->error_message; $this->retries = (int) $this->retries; $this->is_mobile = (bool) $this->is_mobile; $this->job_id = (string) $this->job_id; $this->queue_name = (string) $this->queue_name; $this->status = (string) $this->status; $this->modified = empty( $this->modified ) ? 0 : strtotime( $this->modified ); $this->last_accessed = empty( $this->last_accessed ) ? 0 : strtotime( $this->last_accessed ); $this->submitted_at = empty( $this->submitted_at ) ? 0 : strtotime( $this->submitted_at ); $this->next_retry_time = empty( $this->next_retry_time ) ? 0 : strtotime( $this->next_retry_time ); } } Engine/Optimization/RUCSS/Frontend/Subscriber.php 0000644 00000005241 15174677547 0015762 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\RUCSS\Frontend; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Optimization\RUCSS\Context\RUCSSContext; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Engine\Optimization\RUCSS\Controller\UsedCSS; class Subscriber implements Subscriber_Interface { /** * UsedCss instance * * @var UsedCSS */ private $used_css; /** * RUCSS context. * * @var ContextInterface */ protected $context; /** * Instantiate the class * * @param UsedCSS $used_css UsedCSS instance. * @param ContextInterface $context RUCSS context. */ public function __construct( UsedCSS $used_css, ContextInterface $context ) { $this->used_css = $used_css; $this->context = $context; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_buffer' => [ 'treeshake', 1000 ], 'rocket_first_install_options' => 'on_install', 'wp_rocket_upgrade' => [ 'on_update', 10, 2 ], 'rocket_head_items' => [ [ 'insert_preload_fonts', 1100 ], [ 'insert_css_in_head', 1200 ], ], ]; } /** * Apply TreeShaked CSS to the current HTML page. * * @param string $html HTML content. * * @return string HTML content. */ public function treeshake( string $html ): string { return $this->used_css->treeshake( $html ); } /** * Add option on update. * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function on_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.15', '>=' ) ) { return; } $default = 0; if ( get_transient( 'wp_rocket_no_licence' ) ) { $default = get_transient( 'wp_rocket_no_licence' ); delete_transient( 'wp_rocket_no_licence' ); } update_option( 'wp_rocket_no_licence', $default ); } /** * Add option on installation. * * @param array $options WP Rocket options array. * * @return array */ public function on_install( $options ) { update_option( 'wp_rocket_no_licence', 0 ); return $options; } /** * Insert used CSS into head. * * @param array $items Head elements. * @return mixed */ public function insert_css_in_head( $items ) { return $this->used_css->add_used_css_to_html( $items ); } /** * Insert font preloads into head. * * @param array $items Head elements. * @return mixed */ public function insert_preload_fonts( $items ) { return $this->used_css->insert_preload_fonts( $items ); } } Engine/Optimization/RUCSS/Admin/OptionSubscriber.php 0000644 00000003503 15174677547 0016423 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\RUCSS\Admin; use WP_Rocket\Engine\Admin\Settings\Settings as AdminSettings; use WP_Rocket\Event_Management\Subscriber_Interface; class OptionSubscriber implements Subscriber_Interface { /** * Settings instance * * @var Settings */ private $settings; /** * Instantiate the class * * @param Settings $settings Settings instance. */ public function __construct( Settings $settings ) { $this->settings = $settings; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_first_install_options' => 'add_options_first_time', 'rocket_input_sanitize' => [ 'sanitize_options', 14, 2 ], 'rocket_meta_boxes_fields' => [ 'add_meta_box', 2 ], ]; } /** * Add the RUCSS options to the WP Rocket options array. * * @since 3.9 * * @param array $options WP Rocket options array. * * @return array */ public function add_options_first_time( $options ): array { return $this->settings->add_options( $options ); } /** * Sanitizes RUCSS options values when the settings form is submitted * * @since 3.9 * * @param array $input Array of values submitted from the form. * @param AdminSettings $settings Settings class instance. * * @return array */ public function sanitize_options( $input, AdminSettings $settings ): array { return $this->settings->sanitize_options( $input, $settings ); } /** * Add the field to the WP Rocket metabox on the post edit page. * * @param string[] $fields Metaboxes fields. * * @return string[] */ public function add_meta_box( array $fields ) { $fields['remove_unused_css'] = __( 'Remove Unused CSS', 'rocket' ); return $fields; } } Engine/Optimization/RUCSS/Admin/Subscriber.php 0000644 00000033477 15174677547 0015247 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\RUCSS\Admin; use WP_Rocket\Engine\Common\JobManager\Queue\Queue; use WP_Rocket\Engine\Common\Queue\RUCSSQueueRunner; use WP_Rocket\Engine\Optimization\RUCSS\Controller\UsedCSS; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Settings instance * * @var Settings */ private $settings; /** * Database instance * * @var Database */ private $database; /** * UsedCSS instance * * @var UsedCSS */ private $used_css; /** * Queue instance * * @var Queue */ private $queue; /** * Instantiate the class * * @param Settings $settings Settings instance. * @param Database $database Database instance. * @param UsedCSS $used_css UsedCSS instance. * @param Queue $queue Queue instance. */ public function __construct( Settings $settings, Database $database, UsedCSS $used_css, Queue $queue ) { $this->settings = $settings; $this->database = $database; $this->used_css = $used_css; $this->queue = $queue; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events(): array { $slug = rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ); return [ 'update_option_' . $slug => [ [ 'clean_used_css_and_cache', 9, 2 ], [ 'maybe_set_processing_transient', 50, 2 ], [ 'maybe_unlock_preload', 9, 2 ], [ 'maybe_delete_transient', 10, 2 ], ], 'switch_theme' => 'truncate_used_css', 'permalink_structure_changed' => 'truncate_used_css', 'rocket_domain_options_changed' => 'truncate_used_css', 'rocket_host_fonts_locally_changed' => 'delete_used_css_rows', 'wp_trash_post' => 'delete_used_css_on_update_or_delete', 'delete_post' => 'delete_used_css_on_update_or_delete', 'clean_post_cache' => 'delete_used_css_on_update_or_delete', 'wp_update_comment_count' => 'delete_used_css_on_update_or_delete', 'edit_term' => 'delete_term_used_css', 'pre_delete_term' => 'delete_term_used_css', 'admin_notices' => [ [ 'display_no_table_notice' ], [ 'notice_write_permissions' ], ], 'rocket_before_add_field_to_settings' => [ [ 'set_optimize_css_delivery_value', 10, 1 ], [ 'set_optimize_css_delivery_method_value', 10, 1 ], ], 'wp_rocket_upgrade' => [ [ 'set_option_on_update', 14, 2 ], [ 'update_safelist_items', 15, 2 ], [ 'delete_used_css', 16, 2 ], [ 'cancel_pending_jobs_as', 16, 2 ], [ 'drop_resources_table', 18, 2 ], ], 'wp_ajax_rocket_spawn_cron' => 'spawn_cron', 'rocket_deactivation' => 'cancel_queues', 'admin_head-tools_page_action-scheduler' => 'delete_as_tables_transient_on_tools_page', 'pre_get_rocket_option_remove_unused_css' => 'disable_russ_on_wrong_license', 'rocket_before_rollback' => 'cancel_queues', 'rocket_saas_clean_all' => [ 'truncate', 11 ], 'rocket_saas_clean_url' => [ 'clean_url', 11 ], ]; } /** * Delete used_css on Update Post or Delete post. * * @since 3.9 * * @param int $post_id The post ID. * * @return void */ public function delete_used_css_on_update_or_delete( $post_id ) { if ( ! $this->settings->is_enabled() ) { return; } if ( ! $this->is_deletion_enabled() ) { return; } if ( 'attachment' === get_post_type( $post_id ) ) { return; } $url = get_permalink( $post_id ); if ( false === $url ) { return; } $this->used_css->delete_used_css( untrailingslashit( $url ) ); } /** * Maybe unlock all locked preload urls. * * @param array $old_value An array of submitted values for the settings. * @param array $value An array of previous values for the settings. * * @return void */ public function maybe_unlock_preload( $old_value, $value ) { if ( ! isset( $value['remove_unused_css'], $old_value['remove_unused_css'] ) ) { return; } if ( $value['remove_unused_css'] === $old_value['remove_unused_css'] ) { return; } if ( $value['remove_unused_css'] ) { return; } do_action( 'rocket_preload_unlock_all_urls' ); } /** * Deletes the used CSS when updating a term * * @since 3.10.2 * * @param int $term_id the term ID. * * @return void */ public function delete_term_used_css( $term_id ) { if ( ! $this->settings->is_enabled() ) { return; } if ( ! $this->is_deletion_enabled() ) { return; } $url = get_term_link( (int) $term_id ); if ( is_wp_error( $url ) ) { return; } $this->used_css->delete_used_css( untrailingslashit( $url ) ); } /** * Truncate RUCSS used_css DB table. * * @since 3.9 * * @return void */ public function truncate_used_css() { if ( ! $this->settings->is_enabled() ) { return; } if ( ! $this->is_deletion_enabled() ) { return; } $this->delete_used_css_rows(); $this->set_notice_transient(); } /** * Deletes the used CSS from the table * * @since 3.11 * * @return void */ public function delete_used_css_rows() { $this->used_css->delete_all_used_css(); if ( 0 < $this->used_css->get_not_completed_count() ) { $this->database->remove_all_completed_rows(); } else { $this->database->truncate_used_css_table(); } /** * Fires after the used CSS has been cleaned in the database * * @since 3.11 */ do_action( 'rocket_after_clean_used_css' ); } /** * Truncate UsedCSS DB Table when `remove_unused_css_safelist` is changed. * * @since 3.9 * * @param array $old_value An array of submitted values for the settings. * @param array $value An array of previous values for the settings. * * @return void */ public function clean_used_css_and_cache( $old_value, $value ) { if ( ! isset( $value['remove_unused_css_safelist'], $old_value['remove_unused_css_safelist'] ) ) { return; } if ( $value['remove_unused_css_safelist'] === $old_value['remove_unused_css_safelist'] ) { return; } $this->delete_used_css_rows(); $this->set_notice_transient(); } /** * Deletes rows when triggering clean from admin * * @param array $clean An array containing the status and message. * * @return array */ public function truncate( $clean ) { if ( ! $this->settings->is_enabled() ) { return $clean; } if ( ! current_user_can( 'rocket_remove_unused_css' ) ) { return [ 'status' => 'die', ]; } $this->delete_used_css_rows(); return [ 'status' => 'success', 'message' => sprintf( // translators: %1$s = plugin name. __( '%1$s: Used CSS cache cleared!', 'rocket' ), '<strong>WP Rocket</strong>' ), ]; } /** * Set optimize css delivery value * * @since 3.10 * * @param array $field_args Array of field to be added to settings page. * * @return array */ public function set_optimize_css_delivery_value( $field_args ): array { return $this->settings->set_optimize_css_delivery_value( $field_args ); } /** * Set optimize css delivery method value * * @since 3.10 * * @param array $field_args Array of field to be added to settings page. * * @return array */ public function set_optimize_css_delivery_method_value( $field_args ): array { return $this->settings->set_optimize_css_delivery_method_value( $field_args ); } /** * Clean UsedCSS for the current URL. * * @return void */ public function clean_url() { if ( ! current_user_can( 'rocket_remove_unused_css' ) ) { wp_nonce_ays( '' ); } $url = wp_get_referer(); if ( 0 !== strpos( $url, 'http' ) ) { $parse_url = get_rocket_parse_url( untrailingslashit( home_url() ) ); $url = $parse_url['scheme'] . '://' . $parse_url['host'] . $url; } $this->used_css->clear_url_usedcss( $url ); } /** * Disables combine CSS if RUCSS is enabled when updating to 3.11 * * @since 3.11 * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function set_option_on_update( $new_version, $old_version ) { $this->settings->set_option_on_update( $old_version ); if ( version_compare( $old_version, '3.11', '>=' ) ) { return; } $this->database->truncate_used_css_table(); rocket_clean_domain(); $this->set_notice_transient(); } /** * Updates safelist items for new SaaS compatibility * * @since 3.11.0.2 * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function update_safelist_items( $new_version, $old_version ) { $this->settings->update_safelist_items( $old_version ); } /** * Cancel pending jobs actions in Action Scheduler on update to 3.11.3 * * @since 3.11.3 * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function cancel_pending_jobs_as( $new_version, $old_version ) { if ( version_compare( $old_version, '3.11.3', '>=' ) ) { return; } try { $this->queue->cancel_pending_jobs_cron(); } catch ( \InvalidArgumentException $e ) { // nothing to do. return; } } /** * Sets the processing transient if RUCSS is enabled * * @since 3.11 * * @param mixed $old_value Option old value. * @param mixed $value Option new value. * * @return void */ public function maybe_set_processing_transient( $old_value, $value ) { if ( ! isset( $old_value['remove_unused_css'], $value['remove_unused_css'] ) ) { return; } if ( 0 === (int) $value['remove_unused_css'] ) { return; } if ( $old_value['remove_unused_css'] === $value['remove_unused_css'] ) { return; } $this->set_notice_transient(); } /** * Sets the transient for the processing notice * * @since 3.11 * * @return void */ private function set_notice_transient() { set_transient( 'rocket_saas_processing', time() + 90, 1.5 * MINUTE_IN_SECONDS ); rocket_renew_box( 'saas_success_notice' ); } /** * Sends a request to run cron when switching to RUCSS completed notice * * @since 3.11 * * @return void */ public function spawn_cron() { if ( rocket_get_constant( 'DISABLE_WP_CRON', false ) ) { return;// Bailout and don't fire the CRON. } check_ajax_referer( 'rocket-ajax', 'nonce' ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_send_json_error(); } spawn_cron(); wp_send_json_success(); } /** * Cancel queues and crons for RUCSS. * * @return void */ public function cancel_queues() { // Will unhook check for dispatching an async request without RUCSS process running. \ActionScheduler_QueueRunner::instance()->unhook_dispatch_async_request(); // Will unhook check for dispatching an async request when RUCSS process is already running. RUCSSQueueRunner::instance()->unhook_dispatch_async_request(); $this->queue->cancel_pending_jobs_cron(); if ( ! wp_next_scheduled( 'rocket_saas_clean_rows_time_event' ) ) { return; } wp_clear_scheduled_hook( 'rocket_saas_clean_rows_time_event' ); } /** * Delete the transient for Action scheduler once admin visits the AS tools page. * * @return void */ public function delete_as_tables_transient_on_tools_page() { delete_transient( 'rocket_rucss_as_tables_count' ); } /** * Deletes the used CSS on update to 3.11.4 for new storage method * * @since 3.11.4 * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function delete_used_css( $new_version, $old_version ) { if ( version_compare( $old_version, '3.11.4', '>=' ) ) { return; } $this->database->truncate_used_css_table(); } /** * Disable RUCSS on wrong license. * * @return null|false */ public function disable_russ_on_wrong_license() { if ( false !== (bool) get_option( 'wp_rocket_no_licence' ) ) { return false; } return null; } /** * Remove the resources table & version stored in options table on update to 3.12 * * @since 3.12 * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function drop_resources_table( $new_version, $old_version ) { if ( version_compare( $old_version, '3.12', '>=' ) ) { return; } $this->database->drop_resources_table(); } /** * Displays a notice if the used CSS folder is not writable * * @since 3.11.4 * * @return void */ public function notice_write_permissions() { $this->used_css->notice_write_permissions(); } /** * Display a notice on table missing. * * @return void */ public function display_no_table_notice() { $this->settings->display_no_table_notice(); } /** * Maybe delete transient. * * @param mixed $old_value Option old value. * @param mixed $value Option new value. * * @return void */ public function maybe_delete_transient( $old_value, $value ) { if ( ! isset( $old_value['remove_unused_css'], $value['remove_unused_css'] ) ) { return; } if ( 1 === (int) $value['remove_unused_css'] ) { return; } if ( $old_value['remove_unused_css'] === $value['remove_unused_css'] ) { return; } delete_transient( 'wp_rocket_no_licence' ); } /** * Checks if the SaaS deletion is enabled. * * @return bool */ protected function is_deletion_enabled(): bool { /** * Filters the enable SaaS deletion value * * @param bool $delete_saas_jobs True to enable deletion, false otherwise. */ return (bool) rocket_apply_filter_and_deprecated( 'rocket_saas_deletion_enabled', [ true ], '3.16', 'rocket_rucss_deletion_enabled' ); } } Engine/Optimization/RUCSS/Admin/Settings.php 0000644 00000014662 15174677547 0014737 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\RUCSS\Admin; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Engine\Admin\Settings\Settings as AdminSettings; use WP_Rocket\Engine\Optimization\RUCSS\Database\Tables\UsedCSS; class Settings { /** * Instance of options handler. * * @var Options_Data */ private $options; /** * Instance of Beacon class. * * @var Beacon */ private $beacon; /** * Used CSS table. * * @var UsedCSS */ private $used_css; /** * Creates an instance of the class. * * @param Options_Data $options WP Rocket Options instance. * @param Beacon $beacon Beacon instance. * @param UsedCSS $used_css Used CSS table. */ public function __construct( Options_Data $options, Beacon $beacon, UsedCSS $used_css ) { $this->options = $options; $this->beacon = $beacon; $this->used_css = $used_css; } /** * Add the RUCSS options to the WP Rocket options array * * @since 3.9 * * @param array $options WP Rocket options array. * * @return array */ public function add_options( $options ): array { $options = (array) $options; $options['remove_unused_css'] = 0; $options['remove_unused_css_safelist'] = []; return $options; } /** * Determines if Remove Unused CSS option is enabled. * * @since 3.9 * * @return boolean */ public function is_enabled(): bool { return (bool) $this->options->get( 'remove_unused_css', 0 ); } /** * Sanitizes RUCSS options values when the settings form is submitted * * @since 3.9 * * @param array $input Array of values submitted from the form. * @param AdminSettings $settings Settings class instance. * * @return array */ public function sanitize_options( array $input, AdminSettings $settings ): array { $input['remove_unused_css'] = $settings->sanitize_checkbox( $input, 'remove_unused_css' ); $input['remove_unused_css_safelist'] = ! empty( $input['remove_unused_css_safelist'] ) ? rocket_sanitize_textarea_field( 'remove_unused_css_safelist', $input['remove_unused_css_safelist'] ) : []; return $input; } /** * Set optimize css delivery value * * @since 3.10 * * @param array $field_args Array of field to be added to settings page. * * @return array */ public function set_optimize_css_delivery_value( $field_args ): array { if ( 'optimize_css_delivery' !== $field_args['id'] ) { return $field_args; } $async_css_value = (bool) $this->options->get( 'async_css', 0 ); $remove_unused_css_value = (bool) $this->options->get( 'remove_unused_css', 0 ); $field_args['value'] = ( $remove_unused_css_value || $async_css_value ); return $field_args; } /** * Set optimize css delivery method value * * @since 3.10 * * @param array $field_args Array of field to be added to settings page. * * @return array */ public function set_optimize_css_delivery_method_value( $field_args ): array { if ( 'optimize_css_delivery_method' !== $field_args['id'] ) { return $field_args; } $value = ''; if ( (bool) $this->options->get( 'async_css', 0 ) ) { $value = 'async_css'; } if ( (bool) $this->options->get( 'remove_unused_css', 0 ) ) { $value = 'remove_unused_css'; } $field_args['value'] = $value; return $field_args; } /** * Checks if we can display the RUCSS notices * * @param bool $check_enabled check if RUCSS is enabled. * * @since 3.11 * * @return bool */ private function can_display_notice( $check_enabled = true ): bool { $screen = get_current_screen(); if ( ! rocket_direct_filesystem()->is_writable( rocket_get_constant( 'WP_ROCKET_USED_CSS_PATH' ) ) ) { return false; } if ( isset( $screen->id ) && 'settings_page_wprocket' !== $screen->id ) { return false; } if ( ! current_user_can( 'rocket_manage_options' ) ) { return false; } if ( $check_enabled && ! $this->is_enabled() ) { return false; } return true; } /** * Disables combine CSS if RUCSS is enabled when updating to 3.11 * * @since 3.11 * * @param string $old_version Previous plugin version. * * @return void */ public function set_option_on_update( $old_version ) { if ( version_compare( $old_version, '3.11', '>=' ) ) { return; } $options = get_option( 'wp_rocket_settings', [] ); if ( 'local' === wp_get_environment_type() ) { $options['optimize_css_delivery'] = 0; $options['remove_unused_css'] = 0; $options['async_css'] = 0; } update_option( 'wp_rocket_settings', $options ); } /** * Updates safelist items for new SaaS compatibility * * @since 3.11.0.2 * * @param string $old_version Previous plugin version. * * @return void */ public function update_safelist_items( $old_version ) { if ( version_compare( $old_version, '3.11.0.2', '>=' ) ) { return; } $options = get_option( 'wp_rocket_settings', [] ); if ( empty( $options['remove_unused_css_safelist'] ) ) { return; } foreach ( $options['remove_unused_css_safelist'] as $key => $value ) { if ( str_contains( $value, '.css' ) ) { continue; } if ( str_starts_with( $value, '(' ) ) { continue; } $options['remove_unused_css_safelist'][ $key ] = '(.*)' . $value; } update_option( 'wp_rocket_settings', $options ); } /** * Display a notice on table missing. * * @return void */ public function display_no_table_notice() { if ( ! $this->can_display_notice() ) { return; } if ( $this->used_css->exists() ) { return; } // translators: %1$s = plugin name, %2$s = table name, %3$s = <a> open tag, %4$s = </a> closing tag. $main_message = esc_html__( '%1$s: Could not create the %2$s table in the database which is necessary for the Remove Unused CSS feature to work. Please check our %3$sdocumentation%4$s.', 'rocket' ); $rucss_database = $this->beacon->get_suggest( 'rucss_database' ); $message = sprintf( // translators: %1$s = plugin name, %2$s = table name, %3$s = <a> open tag, %4$s = </a> closing tag. $main_message, '<strong>WP Rocket</strong>', $this->used_css->get_name(), '<a href="' . esc_url( $rucss_database['url'] ) . '" data-beacon-article="' . esc_attr( $rucss_database['id'] ) . '" target="_blank" rel="noopener">', '</a>' ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, 'id' => 'rocket-notice-rucss-missing-table', ] ); } } Engine/Optimization/RUCSS/Admin/Database.php 0000644 00000004374 15174677547 0014642 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization\RUCSS\Admin; use WP_Rocket\Engine\Optimization\RUCSS\Database\Tables\UsedCSS; class Database { /** * Instance of RUCSS used_css table. * * @var UsedCSS */ private $rucss_usedcss_table; /** * Creates an instance of the class. * * @param UsedCSS $rucss_usedcss_table RUCSS UsedCSS Database Table. */ public function __construct( UsedCSS $rucss_usedcss_table ) { $this->rucss_usedcss_table = $rucss_usedcss_table; } /** * Drop RUCSS Database Tables. * * @return void */ public function drop_rucss_database_tables() { // If the table exist, then drop the table. if ( $this->rucss_usedcss_table->exists() ) { $this->rucss_usedcss_table->uninstall(); } } /** * Truncate RUCSS used_css DB table. * * @return bool */ public function truncate_used_css_table(): bool { if ( ! $this->rucss_usedcss_table->exists() ) { return false; } return $this->rucss_usedcss_table->truncate(); } /** * Delete old used css based on last accessed date. * * @return void */ public function delete_old_used_css() { if ( ! $this->rucss_usedcss_table->exists() ) { return; } $this->rucss_usedcss_table->delete_old_rows(); } /** * Get old used css based on last accessed date. * * @return array */ public function get_old_used_css(): array { if ( ! $this->rucss_usedcss_table->exists() ) { return []; } return $this->rucss_usedcss_table->get_old_rows(); } /** * Remove all completed rows. * * @return bool|int */ public function remove_all_completed_rows() { if ( ! $this->rucss_usedcss_table->exists() ) { return false; } return $this->rucss_usedcss_table->remove_all_completed_rows(); } /** * Remove the resources table & version stored in options table * * @since 3.12 * * @return bool */ public function drop_resources_table(): bool { global $wpdb; $result = $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wpr_rucss_resources" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange if ( false === $result ) { return false; } return delete_option( 'wpr_rucss_resources_version' ); } } Engine/Optimization/RUCSS/Context/RUCSSContextSaas.php 0000644 00000000760 15174677547 0016601 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\RUCSS\Context; use WP_Rocket\Engine\Common\Context\AbstractContext; class RUCSSContextSaas extends AbstractContext { /** * Check if the operation is allowed. * * @param array $data Data to provide to the context. * @return bool */ public function is_allowed( array $data = [] ): bool { $is_allowed = $this->run_common_checks( [ 'option' => 'remove_unused_css' ] ); if ( ! $is_allowed ) { return false; } return true; } } Engine/Optimization/RUCSS/Context/RUCSSOptimizeContext.php 0000644 00000001351 15174677547 0017507 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\RUCSS\Context; use WP_Rocket\Engine\Common\Context\AbstractContext; class RUCSSOptimizeContext extends AbstractContext { /** * Check if the operation is allowed. * * @param array $data Data to provide to the context. * * @return bool */ public function is_allowed( array $data = [] ): bool { $is_allowed = $this->run_common_checks( [ 'bypass' => false, 'option' => 'remove_unused_css', 'post_excluded' => 'remove_unused_css', ] ); if ( ! current_user_can( 'rocket_remove_unused_css' ) ) { return false; } if ( ! $is_allowed ) { return false; } if ( ! rocket_can_display_options() ) { return false; } return true; } } Engine/Optimization/RUCSS/Context/RUCSSContext.php 0000644 00000002303 15174677547 0015764 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization\RUCSS\Context; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\Context\AbstractContext; use WP_Rocket\Engine\Optimization\RUCSS\Controller\Filesystem; class RUCSSContext extends AbstractContext { /** * Filesystem instance * * @var Filesystem */ protected $filesystem; /** * Instantiate the class. * * @param Options_Data $options Options. * @param Filesystem $filesystem Filesystem instance. */ public function __construct( Options_Data $options, Filesystem $filesystem ) { parent::__construct( $options ); $this->filesystem = $filesystem; } /** * Check if the operation is allowed. * * @param array $data Data to provide to the context. * @return bool */ public function is_allowed( array $data = [] ): bool { $is_allowed = $this->run_common_checks( [ 'do_not_optimize' => false, 'bypass' => false, 'option' => 'remove_unused_css', 'password_protected' => false, 'post_excluded' => 'remove_unused_css', 'logged_in' => false, ] ); if ( ! $is_allowed ) { return false; } return $this->filesystem->is_writable_folder(); } } Engine/Optimization/AdminServiceProvider.php 0000644 00000003166 15174677547 0015271 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Optimization\GoogleFonts\Admin\{Settings, Subscriber}; use WP_Rocket\Engine\Optimization\Minify\AdminSubscriber as MinifyAdminSubscriber; use WP_Rocket\Engine\Optimization\Minify\CSS\AdminSubscriber as MinifyCssAdminSubscriber; /** * Service provider for the WP Rocket optimizations */ class AdminServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'minify_css_admin_subscriber', 'google_fonts_settings', 'google_fonts_admin_subscriber', 'minify_admin_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'minify_css_admin_subscriber', MinifyCssAdminSubscriber::class ); $this->getContainer()->add( 'google_fonts_settings', Settings::class ) ->addArgument( 'options' ) ->addArgument( 'beacon' ) ->addArgument( 'template_path' ); $this->getContainer()->addShared( 'google_fonts_admin_subscriber', Subscriber::class ) ->addArgument( 'google_fonts_settings' ); $this->getContainer()->addShared( 'minify_admin_subscriber', MinifyAdminSubscriber::class ) ->addArgument( 'options' ); } } Engine/Optimization/AbstractOptimization.php 0000644 00000012357 15174677547 0015361 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization; use WP_Rocket\Admin\Options_Data; /** * Base abstract class for files optimization * * @since 3.1 * @author Remy Perona */ abstract class AbstractOptimization { use RegexTrait; /** * Plugin options. * * @var Options_Data */ protected $options; /** * Minify key. * * @var mixed */ protected $minify_key; /** * Concatenated list of excluded files. * * @var string */ protected $excluded_files; /** * Minify base path. * * @var string */ protected $minify_base_path; /** * Minify base URL. * * @var string */ protected $minify_base_url; /** * Initializes the minify base path and URL. */ protected function init_base_path_and_url() { $site_id = get_current_blog_id() . '/'; $this->minify_base_path = rocket_get_constant( 'WP_ROCKET_MINIFY_CACHE_PATH' ) . $site_id; $this->minify_base_url = rocket_get_constant( 'WP_ROCKET_MINIFY_CACHE_URL' ) . $site_id; } /** * Finds nodes matching the pattern in the HTML. * * @since 3.1 * @author Remy Perona * * @param string $pattern Pattern to match. * @param string $html HTML content. * @return bool|array */ protected function find( string $pattern, string $html ) { preg_match_all( '/' . $pattern . '/Umsi', $html, $matches, PREG_SET_ORDER ); if ( empty( $matches ) ) { return false; } return $matches; } /** * Determines if the file is external. * * @since 2.11 * @author Remy Perona * * @param string $url URL of the file. * @return bool True if external, false otherwise */ protected function is_external_file( $url ) { $file = get_rocket_parse_url( $url ); if ( empty( $file['path'] ) ) { return true; } $wp_content = wp_parse_url( content_url() ); if ( empty( $wp_content['path'] ) ) { return true; } /** * Filters the allowed hosts for optimization * * @since 3.4 * @author Remy Perona * * @param array $hosts Allowed hosts. * @param array $zones Zones to check available hosts. */ $hosts = (array) apply_filters( 'rocket_cdn_hosts', [], $this->get_zones() ); if ( ! empty( $wp_content['host'] ) ) { $hosts[] = $wp_content['host']; } $langs = get_rocket_i18n_uri(); // Get host for all langs. foreach ( $langs as $lang ) { $url_host = wp_parse_url( $lang, PHP_URL_HOST ); if ( ! isset( $url_host ) ) { continue; } $hosts[] = $url_host; } $hosts = array_unique( $hosts ); if ( empty( $hosts ) ) { return true; } // URL has domain and domain is part of the internal domains. if ( ! empty( $file['host'] ) ) { foreach ( $hosts as $host ) { $check_url = strtok( $url, '?' ); if ( false !== strpos( $check_url, $host ) ) { return false; } } return true; } if ( empty( $file['host'] ) ) { return false; } // URL has no domain and doesn't contain the WP_CONTENT path or wp-includes. return ! preg_match( '#(' . $wp_content['path'] . '|wp-includes)#', $file['path'] ); } /** * Writes the content to a file * * @since 3.1 * @author Remy Perona * * @param string $content Content to write. * @param string $file Path to the file to write in. * @return bool */ protected function write_file( $content, $file ) { if ( rocket_direct_filesystem()->is_readable( $file ) ) { return true; } if ( ! rocket_mkdir_p( dirname( $file ) ) ) { return false; } if ( function_exists( 'gzencode' ) ) { // This filter is documented in inc/classes/Buffer/class-cache.php. $gzip_content = gzencode( $content, apply_filters( 'rocket_gzencode_level_compression', 6 ) ); if ( $gzip_content ) { rocket_put_content( $file . '.gz', $gzip_content ); } } return rocket_put_content( $file, $content ); } /** * Gets the file path from an URL * * @since 3.1 * @author Remy Perona * * @param string $url File URL. * @return bool|string */ protected function get_file_path( $url ) { return rocket_url_to_path( strtok( $url, '?' ), $this->get_zones() ); } /** * Gets content of a file * * @since 3.1 * @author Remy Perona * * @param string $file File path. * @return string */ protected function get_file_content( $file ) { return rocket_direct_filesystem()->get_contents( $file ); } /** * Hides unwanted blocks from the HTML to be parsed for optimization * * @since 3.1.4 * @author Remy Perona * * @param string $html HTML content. * @return string */ protected function hide_comments( $html ) { $html = preg_replace( '#<!--\s*noptimize\s*-->.*?<!--\s*/\s*noptimize\s*-->#is', '', $html ); $html = preg_replace( '/<!--(.*)-->/Uis', '', $html ); return $html; } /** * Get full minified url with ?ver query string. * * @param string $minified_path Path of minified file. * @param string $minified_url Url of minified file. * * @return string */ protected function get_full_minified_url( $minified_path, $minified_url ) { $file_mtime = rocket_direct_filesystem()->mtime( $minified_path ); $version = $file_mtime ? $file_mtime : md5( $minified_url . $this->minify_key ); return add_query_arg( 'ver', $version, $minified_url ); } /** * Gets the CDN zones. * * @since 3.1 * * @return array */ abstract public function get_zones(); } Engine/Optimization/CSSTrait.php 0000644 00000034742 15174677547 0012645 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization; use WP_Rocket\Dependencies\PathConverter\ConverterInterface; use WP_Rocket\Dependencies\PathConverter\Converter; trait CSSTrait { /** * Currently imported files. * * @var array */ private $imports = []; /** * Found charset on CSS. * * @var null|string */ private $found_charset = null; /** * Rewrites the paths inside the CSS file content * * @since 3.1 * * @param string $source Source filepath. * @param string $target Target filepath. * @param string $content File content. * @return string */ public function rewrite_paths( $source, $target, $content ) { /** * Filters the source path for an asset inside a CSS file * * @since 3.3.1 * * @param string $source Source filepath. */ $source = apply_filters( 'rocket_css_asset_source_path', $source ); /** * Filters the target path for an asset inside a CSS file * * @since 3.3.1 * * @param string $target Target filepath. */ $target = apply_filters( 'rocket_css_asset_target_path', $target ); $content = $this->move( $this->get_converter( $source, $target ), $content, $source ); $this->set_cached_import( $source ); $content = $this->combine_imports( $content, $target ); /** * Filters the content of a CSS file * * @since 3.4 * * @param string $content CSS content. * @param string $source Source filepath. * @param string $target Target filepath. */ return apply_filters( 'rocket_css_content', $content, $source, $target ); } /** * Get an instance of the Converter class * * @param string $source Source filepath. * @param string $target Destination filepath. * @return Converter */ protected function get_converter( $source, $target ) { return new Converter( $source, $target ); } /** * Moving a css file should update all relative urls. * Relative references (e.g. ../images/image.gif) in a certain css file, * will have to be updated when a file is being saved at another location * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper). * * Method copied from WP_Rocket\Dependencies\Minify\CSS; * * @param ConverterInterface $converter Relative path converter. * @param string $content The CSS content to update relative urls for. * @param string $source The source path or URL for the CSS file. * * @return string */ protected function move( ConverterInterface $converter, $content, $source ) { /* * Relative path references will usually be enclosed by url(). @import * is an exception, where url() is not necessary around the path (but is * allowed). * This *could* be 1 regular expression, where both regular expressions * in this array are on different sides of a |. But we're using named * patterns in both regexes, the same name on both regexes. This is only * possible with a (?J) modifier, but that only works after a fairly * recent PCRE version. That's why I'm doing 2 separate regular * expressions & combining the matches after executing of both. */ $relative_regexes = [ // url(xxx). '/ # open url() url\( \s* # open path enclosure (?P<quotes>["\'])? # fetch path (?P<path>.+?) # close path enclosure (?(quotes)(?P=quotes)) \s* # close url() \) /ix', // @import "xxx" '/ # import statement @import # whitespace \s+ # we don\'t have to check for @import url(), because the # condition above will already catch these # open path enclosure (?P<quotes>["\']) # fetch path (?P<path>.+?) # close path enclosure (?P=quotes) /ix', ]; // find all relative urls in css. $matches = []; foreach ( $relative_regexes as $relative_regex ) { if ( preg_match_all( $relative_regex, $content, $regex_matches, PREG_SET_ORDER ) ) { $matches = array_merge( $matches, $regex_matches ); } } $search = []; $replace = []; // loop all urls. foreach ( $matches as $match ) { // determine if it's a url() or an @import match. $type = ( strpos( $match[0], '@import' ) === 0 ? 'import' : 'url' ); $url = $match['path']; if ( preg_match( '/^#/', $url ) ) { continue; } if ( ! preg_match( '/^(data:|https?:|\\/)/', $url ) ) { // attempting to interpret GET-params makes no sense, so let's discard them for awhile. $params = strrchr( $url, '?' ); $url = $params ? substr( $url, 0, -strlen( $params ) ) : $url; // fix relative url. $url = filter_var( $source, FILTER_VALIDATE_URL ) ? dirname( $source ) . '/' . ltrim( $url, '/' ) : $converter->convert( $url ); // now that the path has been converted, re-apply GET-params. $url .= $params; } /* * Urls with control characters above 0x7e should be quoted. * According to Mozilla's parser, whitespace is only allowed at the * end of unquoted urls. * Urls with `)` (as could happen with data: uris) should also be * quoted to avoid being confused for the url() closing parentheses. * And urls with a # have also been reported to cause issues. * Urls with quotes inside should also remain escaped. * * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378 * @see https://github.com/matthiasmullie/minify/issues/193 */ $url = trim( $url ); if ( preg_match( '/[\s\)\'"#\x{7f}-\x{9f}]/u', $url ) ) { $url = $match['quotes'] . $url . $match['quotes']; } // build replacement. $search[] = $match[0]; if ( 'url' === $type ) { $replace[] = 'url(' . $url . ')'; } elseif ( 'import' === $type ) { $replace[] = '@import "' . $url . '"'; } } // replace urls. return str_replace( $search, $replace, $content ); } /** * Replace local imports with their contents recursively. * * @since 3.8.6 * * @param string $content CSS Content. * @param string $target Target CSS file path. * * @return string */ protected function combine_imports( $content, $target ) { $import_regexes = [ // @import url(xxx) '/ # import statement @import # whitespace \s+ # open url() url\( # (optional) open path enclosure (?P<quotes>["\']?) # fetch path (?P<path>.+?) # (optional) close path enclosure (?P=quotes) # close url() \) # (optional) trailing whitespace \s* # (optional) media statement(s) (?P<media>[^;]*) # (optional) trailing whitespace \s* # (optional) closing semi-colon ;? /ix', // @import 'xxx' '/ # import statement @import # whitespace \s+ # open path enclosure (?P<quotes>["\']) # fetch path (?P<path>.+?) # close path enclosure (?P=quotes) # (optional) trailing whitespace \s* # (optional) media statement(s) (?P<media>[^;]*) # (optional) trailing whitespace \s* # (optional) closing semi-colon ;? /ix', ]; // find all relative imports in css. $matches = []; foreach ( $import_regexes as $import_regexe ) { if ( preg_match_all( $import_regexe, $content, $regex_matches, PREG_SET_ORDER ) ) { $matches = array_merge( $matches, $regex_matches ); } } if ( empty( $matches ) ) { return $content; } $search = []; $replace = []; // loop the matches. foreach ( $matches as $match ) { /** * Filter Skip import replacement for one file. * * @since 3.8.6 * * @param bool $skip_import Skipped or not (Default not skipped). * @param string $file_path Matched import path. * @param array $import_match Full import match. */ if ( apply_filters( 'rocket_skip_import_replacement', false, $match['path'], $match ) ) { continue; } list( $import_path, $import_content ) = $this->get_internal_file_contents( $match['path'], dirname( $target ) ); if ( empty( $import_content ) ) { continue; } if ( $this->check_cached_import( $import_path ) ) { $search[] = $match[0]; $replace[] = ''; continue; } $this->set_cached_import( $import_path ); // check if this is only valid for certain media. if ( ! empty( $match['media'] ) ) { $import_content = '@media ' . $match['media'] . '{' . $import_content . '}'; } // Use recursion to rewrite paths and combine imports again for imported content. $import_content = $this->rewrite_paths( $import_path, $target, $import_content ); // add to replacement array. $search[] = $match[0]; $replace[] = $import_content; } // replace the import statements. return str_replace( $search, $replace, $content ); } /** * Applies font-display:swap to all font-family rules without a previously set font-display property. * * @since 3.7 * * @param string $css_file_content CSS file content to modify. * * @return string Modified CSS content. */ private function apply_font_display_swap( $css_file_content ) { $css_file_content = (string) $css_file_content; return preg_replace_callback( '/(?:@font-face)\s*{(?<value>[^}]+)}/i', function ( $matches ) { if ( preg_match( '/font-display:\s*(?<swap_value>\w*);?/i', $matches['value'], $attribute ) ) { if ( 'swap' === strtolower( $attribute['swap_value'] ) ) { return $matches[0]; } $swap = str_replace( $attribute['swap_value'], 'swap', $attribute[0] ); return preg_replace( '/font-display:\s*(?<swap_value>\w*);?/i', $swap, $matches[0] ); } return str_replace( $matches['value'], "font-display:swap;{$matches['value']}", $matches[0] ); }, $css_file_content ); } /** * Get internal file full path and contents. * * @since 3.8.6 * * @param string $file Internal file path (maybe external url or relative path). * @param string $base_path Base path as reference for relative paths. * * @return array Array of two values ( full path, contents ) */ private function get_internal_file_contents( $file, $base_path ) { if ( $this->is_external_path( $file ) && wp_http_validate_url( $file ) ) { return [ $file, false ]; } // Remove query strings. $file = str_replace( '?' . wp_parse_url( $file, PHP_URL_QUERY ), '', $file ); // Check if this file is readable or it's relative path so we add base_path at it's start. if ( ! rocket_direct_filesystem()->is_readable( $this->get_local_path( $file ) ) ) { $ds = rocket_get_constant( 'DIRECTORY_SEPARATOR' ); $file = $base_path . $ds . str_replace( '/', $ds, $file ); }else { $file = $this->get_local_path( $file ); } $file_type = wp_check_filetype( $file, [ 'css' => 'text/css' ] ); if ( 'css' !== $file_type['ext'] ) { return [ $file, null ]; } $import_content = rocket_direct_filesystem()->get_contents( $file ); return [ $file, $import_content ]; } /** * Determines if the file is external. * * @since 3.8.6 * * @param string $url URL of the file. * @return bool True if external, false otherwise. */ protected function is_external_path( $url ) { $file = get_rocket_parse_url( $url ); if ( empty( $file['path'] ) ) { return true; } $parsed_site_url = wp_parse_url( site_url() ); if ( empty( $parsed_site_url['host'] ) ) { return true; } // This filter is documented in inc/Engine/Admin/Settings/Settings.php. $hosts = (array) apply_filters( 'rocket_cdn_hosts', [], [ 'all' ] ); $hosts[] = $parsed_site_url['host']; $langs = get_rocket_i18n_uri(); // Get host for all langs. foreach ( $langs as $lang ) { $url_host = wp_parse_url( $lang, PHP_URL_HOST ); if ( ! isset( $url_host ) ) { continue; } $hosts[] = $url_host; } $hosts = array_unique( $hosts ); // URL has domain and domain is part of the internal domains. if ( ! empty( $file['host'] ) ) { foreach ( $hosts as $host ) { if ( false !== strpos( $url, $host ) ) { return false; } } return true; } return false; } /** * Get local absolute path for image. * * @since 3.8.6 * * @param string $url Image url. * * @return string Image absolute local path. */ private function get_local_path( $url ) { $url = $this->normalize_url( $url ); $path = rocket_url_to_path( $url ); if ( $path ) { return $path; } $relative_url = ltrim( wp_make_link_relative( $url ), '/' ); $ds = rocket_get_constant( 'DIRECTORY_SEPARATOR' ); $base_path = isset( $_SERVER['DOCUMENT_ROOT'] ) ? ( sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $ds ) : ''; return $base_path . str_replace( '/', $ds, $relative_url ); } /** * Normalize relative url to full url. * * @since 3.8.6 * * @param string $url Url to be normalized. * * @return string Normalized url. */ private function normalize_url( $url ) { $url_host = wp_parse_url( $url, PHP_URL_HOST ); if ( ! empty( $url_host ) ) { return $url; } $relative_url = ltrim( wp_make_link_relative( $url ), '/' ); $site_url_components = wp_parse_url( site_url( '/' ) ); return $site_url_components['scheme'] . '://' . $site_url_components['host'] . '/' . $relative_url; } /** * Set cached import locally not to imported again. * * @param string $path Path to be cached. */ private function set_cached_import( string $path ) { $real_path = rocket_realpath( $path ); $this->imports[ md5( $real_path ) ] = $real_path; } /** * Check if path imported before. * * @param string $path Path to be checked. * * @return bool */ private function check_cached_import( string $path ): bool { return isset( $this->imports[ md5( rocket_realpath( $path ) ) ] ); } /** * Move charset to top of CSS file OR remove all charsets for internal CSS. * * @param string $content CSS content. * @param bool $keep_first_charset Keep first charset if true, otherwise remove all charsets. * * @return string */ public function handle_charsets( string $content, bool $keep_first_charset = true ): string { $new_content = preg_replace_callback( '/@charset\s+["|\'](.*?)["|\'];?/i', [ $this, 'match_charsets' ], $content ); if ( ! $keep_first_charset ) { return $new_content; } if ( is_null( $this->found_charset ) ) { return $content; } return "@charset \"{$this->found_charset}\";" . $new_content; } /** * Match each charset on the CSS file. * * @param array $match Match array. * * @return string */ private function match_charsets( array $match ): string { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.matchFound if ( is_null( $this->found_charset ) ) { $this->found_charset = $match[1]; } return ''; } } Engine/Optimization/UrlTrait.php 0000644 00000006274 15174677547 0012756 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Optimization; trait UrlTrait { /** * Determines if the file is external. * * @since 3.8 * * @param string $url URL of the file. * @return bool True if external, false otherwise. */ private function is_external_file( $url ) { $file = get_rocket_parse_url( $url ); if ( empty( $file['path'] ) ) { return true; } $parsed_site_url = wp_parse_url( site_url() ); if ( empty( $parsed_site_url['host'] ) ) { return true; } /** * Filters the allowed hosts for optimization * * @since 3.4 * * @param array $hosts Allowed hosts. * @param array $zones Zones to check available hosts. */ $hosts = (array) apply_filters( 'rocket_cdn_hosts', [], [ 'all' ] ); $hosts[] = $parsed_site_url['host']; $langs = get_rocket_i18n_uri(); // Get host for all langs. foreach ( $langs as $lang ) { $url_host = wp_parse_url( $lang, PHP_URL_HOST ); if ( ! isset( $url_host ) ) { continue; } $hosts[] = $url_host; } $hosts = array_unique( $hosts ); // URL has domain and domain is part of the internal domains. if ( ! empty( $file['host'] ) ) { foreach ( $hosts as $host ) { if ( false !== strpos( $url, $host ) ) { return false; } } return true; } return false; } /** * Gets the file path from an URL * * @param string $url File URL. * @return bool|string */ protected function get_file_path( $url ) { $url = $this->normalize_fullurl( $url ); $path = rocket_url_to_path( $url ); if ( $path ) { return $path; } $relative_url = ltrim( wp_make_link_relative( $url ), '/' ); $ds = rocket_get_constant( 'DIRECTORY_SEPARATOR' ); $base_path = isset( $_SERVER['DOCUMENT_ROOT'] ) ? ( sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $ds ) : ''; return $base_path . str_replace( '/', $ds, $relative_url ); } /** * Normalize relative url to full url. * * @param string $url Url to be normalized. * @param bool $remove_query Remove Query string or not. * * @return string Normalized url. */ public function normalize_fullurl( string $url, bool $remove_query = true ) { $url = htmlspecialchars_decode( $url ); $parsed_url = wp_parse_url( $url ); if ( $remove_query && ! empty( $parsed_url['query'] ) ) { $url = str_replace( '?' . $parsed_url['query'], '', $url ); } if ( empty( $parsed_url['host'] ) ) { $relative_url = ltrim( wp_make_link_relative( $url ), '/' ); $site_url_components = wp_parse_url( site_url( '/' ) ); return $site_url_components['scheme'] . '://' . $site_url_components['host'] . '/' . $relative_url; } return rocket_add_url_protocol( $url ); } /** * Gets content of a file * * @param string $file File path. * * @return string */ protected function get_file_content( $file ) { if ( empty( $file ) ) { return ''; } return rocket_direct_filesystem()->get_contents( $file ); } /** * Check if the URL is relative. * * @param string $url URL to check. * @return bool */ protected function is_relative( string $url ): bool { return ! empty( preg_match( '/^\./', $url ) ) || empty( wp_parse_url( $url, PHP_URL_HOST ) ); } } Engine/Optimization/CacheDynamicResource.php 0000644 00000014756 15174677547 0015234 0 ustar 00 <?php namespace WP_Rocket\Engine\Optimization; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Create a static file for CSS/JS generated by a PHP file * * @since 3.1 */ class CacheDynamicResource extends AbstractOptimization implements Subscriber_Interface { use CSSTrait; /** * Plugin options instance. * * @since 3.1 * * @var Options_Data */ protected $options; /** * Cache busting base path * * @since 3.1 * * @var string */ protected $busting_path; /** * Cache busting base URL * * @since 3.1 * * @var string */ protected $busting_url; /** * Excluded files from optimization * * @since 3.1 * * @var string */ protected $excluded_files; /** * Extension to use for the CDN zones selection * * @var string */ protected $extension = ''; /** * Creates an instance of CacheDynamicResource. * * @since 3.1 * * @param Options_Data $options Plugin options instance. * @param string $busting_path Base cache busting files path. * @param string $busting_url Base cache busting files URL. */ public function __construct( Options_Data $options, $busting_path, $busting_url ) { $site_id = get_current_blog_id(); $this->options = $options; $this->busting_path = "{$busting_path}{$site_id}/"; $this->busting_url = "{$busting_url}{$site_id}/"; /** * Filters files to exclude from static dynamic resources * * @since 2.9.3 * @author Remy Perona * * @param array $excluded_files An array of filepath to exclude. */ $excluded_files = (array) apply_filters( 'rocket_exclude_static_dynamic_resources', [] ); $excluded_files[] = '/wp-admin/admin-ajax.php'; foreach ( $excluded_files as $i => $excluded_file ) { // Escape character for future use in regex pattern. $excluded_files[ $i ] = str_replace( '#', '\#', $excluded_file ); } $this->excluded_files = implode( '|', $excluded_files ); } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.1 * * @return array */ public static function get_subscribed_events() { return [ 'style_loader_src' => [ 'cache_dynamic_resource', 16 ], 'script_loader_src' => [ 'cache_dynamic_resource', 16 ], ]; } /** * Filters the source dynamic php file to replace it with a static file * * @since 3.1 * * @param string $src source URL. * * @return string */ public function cache_dynamic_resource( $src ) { if ( ! $this->is_allowed() ) { return $src; } switch ( current_filter() ) { case 'script_loader_src': $this->set_extension( 'js' ); break; case 'style_loader_src': $this->set_extension( 'css' ); break; } if ( $this->is_excluded_file( $src ) ) { return $src; } return $this->replace_url( $src ); } /** * Replaces the dynamic URL by the static file URL * * @since 3.1 * * @param string $src Source URL. * * @return string */ public function replace_url( $src ) { $path = ltrim( rocket_extract_url_component( $src, PHP_URL_PATH ), '/' ); /** * Filters the dynamic resource cache filename * * @since 2.9 * * @param string $filename filename for the cache file */ $filename = apply_filters( 'rocket_dynamic_resource_cache_filename', preg_replace( '/\.php$/', '.' . $this->extension, $path ) ); $filename = ltrim( rocket_realpath( rtrim( str_replace( [ ' ', '%20' ], '-', $filename ) ) ), '/' ); $filepath = $this->busting_path . $filename; if ( ! rocket_direct_filesystem()->is_readable( $filepath ) ) { $content = $this->get_url_content( $src ); if ( ! $content ) { return $src; } if ( 'css' === $this->extension ) { $content = $this->rewrite_paths( $this->get_file_path( $src ), $filepath, $content ); $content = $this->apply_font_display_swap( $content ); } if ( ! $this->write_file( $content, $filepath ) ) { return $src; } } return $this->get_cache_url( $filename ); } /** * Determines if we can optimize * * @since 3.1 * * @return bool */ public function is_allowed() { global $pagenow; if ( rocket_bypass() ) { return false; } if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return false; } if ( is_user_logged_in() && ! $this->options->get( 'cache_logged_user' ) ) { return false; } if ( 'wp-login.php' === $pagenow ) { return false; } return true; } /** * Determines if the file is excluded from optimization * * @since 3.1 * * @param string $src source URL. * * @return bool */ public function is_excluded_file( $src ) { $file = get_rocket_parse_url( $src ); if ( isset( $file['path'] ) && ! preg_match( '#\.php$#', $file['path'] ) ) { return true; } if ( $this->is_external_file( $src ) ) { return true; } if ( preg_match( '#^' . $this->excluded_files . '$#', $file['path'] ) ) { return true; } if ( ! isset( $file['query'] ) ) { return false; } $file['query'] = remove_query_arg( 'ver', $file['query'] ); return (bool) $file['query']; } /** * Sets the current file extension and minify key * * @since 3.1 * * @param string $extension Current file extension. */ public function set_extension( $extension ) { $this->extension = $extension; $this->minify_key = $this->options->get( 'minify_' . $this->extension . '_key' ); } /** * Gets the CDN zones. * * @since 3.1 * * @return array */ public function get_zones() { return [ 'all', 'css_and_js', $this->extension ]; } /** * Gets the cache URL for the static file * * @since 3.1 * * @param string $filename Filename for the static file. * * @return string */ protected function get_cache_url( $filename ) { $cache_url = $this->busting_url . $filename; switch ( $this->extension ) { case 'css': // This filter is documented in inc/classes/optimization/css/class-abstract-css-optimization.php. $cache_url = apply_filters( 'rocket_css_url', $cache_url ); break; case 'js': // This filter is documented in inc/classes/optimization/css/class-abstract-js-optimization.php. $cache_url = apply_filters( 'rocket_js_url', $cache_url ); break; } return $cache_url; } /** * Gets content from an URL * * @since 3.1 * * @param string $url URL to get the content from. * * @return string|bool */ protected function get_url_content( $url ) { $content = wp_remote_retrieve_body( wp_remote_get( $url ) ); if ( ! $content ) { return false; } return $content; } } Engine/Cache/WPCache.php 0000644 00000021720 15174677547 0010770 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache; use WP_Filesystem_Direct; use WP_Rocket\Engine\Activation\ActivationInterface; use WP_Rocket\Engine\Deactivation\DeactivationInterface; class WPCache implements ActivationInterface, DeactivationInterface { /** * Filesystem instance. * * @var WP_Filesystem_Direct */ private $filesystem; /** * Instantiate the class * * @param WP_Filesystem_Direct $filesystem Filesystem instance. */ public function __construct( $filesystem ) { $this->filesystem = $filesystem; } /** * Performs these actions during the plugin activation * * @return void */ public function activate() { add_action( 'rocket_activation', [ $this, 'update_wp_cache' ] ); } /** * Performs these actions during the plugin deactivation * * @return void */ public function deactivate() { add_action( 'rocket_deactivation', [ $this, 'update_wp_cache' ] ); add_filter( 'rocket_prevent_deactivation', [ $this, 'maybe_prevent_deactivation' ] ); } /** * Sets the WP_CACHE constant on (de)activation * * @since 3.6.3 * * @param int $sites_number Number of WP Rocket config files found. * @return void */ public function update_wp_cache( $sites_number = 0 ) { if ( ! rocket_valid_key() ) { return; } $value = true; if ( 'rocket_deactivation' === current_filter() ) { if ( is_multisite() && 0 !== $sites_number ) { return; } $value = false; } $this->set_wp_cache_constant( $value ); } /** * Updates the causes array on deactivation if needed * * @since 3.6.3 * * @param array $causes Array of causes to pass to the notice. */ public function maybe_prevent_deactivation( $causes ) { if ( $this->find_wpconfig_path() || // This filter is documented in inc/Engine/Cache/WPCache.php. ! (bool) apply_filters( 'rocket_set_wp_cache_constant', true ) ) { return $causes; } $causes[] = 'wpconfig'; return $causes; } /** * Set WP_CACHE constant to true if needed * * @since 3.6.1 * * @return void */ public function maybe_set_wp_cache() { if ( rocket_get_constant( 'DOING_AJAX' ) || rocket_get_constant( 'DOING_AUTOSAVE' ) ) { return; } if ( rocket_get_constant( 'WP_CACHE' ) ) { return; } $this->set_wp_cache_constant( true ); } /** * Sets the value of the WP_CACHE constant in wp-config.php * * @since 3.6.1 * * @param bool $value The value to set for WP_CACHE constant. * @return bool true on success, false otherwise. */ public function set_wp_cache_constant( $value ) { if ( ! $this->should_set_wp_cache_constant( $value ) ) { return false; } $config_file_path = $this->find_wpconfig_path(); if ( ! $config_file_path ) { return false; } $config_file_contents = $this->filesystem->get_contents( $config_file_path ); $value = $value ? 'true' : 'false'; /** * Filter allow to change the value of WP_CACHE constant * * @since 2.1 * * @param string $value The value of WP_CACHE constant. */ $value = apply_filters( 'set_rocket_wp_cache_define', $value ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals $wp_cache_found = preg_match( '/^\s*define\(\s*\'WP_CACHE\'\s*,\s*(?<value>[^\s\)]*)\s*\)/m', $config_file_contents, $matches ); if ( ! empty( $matches['value'] ) && $matches['value'] === $value ) { return false; } $constant = $this->get_wp_cache_content( $value ); if ( ! $wp_cache_found ) { $config_file_contents = preg_replace( '/(<\?php)/i', "<?php\r\n{$constant}\r\n", $config_file_contents, 1 ); } elseif ( ! empty( $matches['value'] ) && $matches['value'] !== $value ) { $config_file_contents = preg_replace( '/^\s*define\(\s*\'WP_CACHE\'\s*,\s*([^\s\)]*)\s*\).+/m', $constant, $config_file_contents ); } return $this->filesystem->put_contents( $config_file_path, $config_file_contents, rocket_get_filesystem_perms( 'file' ) ); } /** * Checks if we should set the WP_CACHE constant * * @since 3.6.1 * * @param bool $value The value to set for WP_CACHE constant. * @return bool */ private function should_set_wp_cache_constant( $value ) { if ( ! $this->is_user_allowed() ) { return false; } if ( true === $value && rocket_get_constant( 'WP_CACHE' ) ) { return false; } /** * Filters the writing of the WP_CACHE constant in wp-config.php * * @since 3.6.1 * @param bool $set True to allow writing, false otherwise. */ return (bool) apply_filters( 'rocket_set_wp_cache_constant', true ); } /** * Try to find the correct wp-config.php file, support one level up in file tree. * * @since 3.6.1 * * @return string|bool The path of wp-config.php file or false if not found. */ private function find_wpconfig_path() { /** * Filter the wp-config's filename. * * @since 2.11 * * @param string $filename The WP Config filename, without the extension. */ $config_file_name = apply_filters( 'rocket_wp_config_name', 'wp-config' ); $abspath = rocket_get_constant( 'ABSPATH' ); $config_file = "{$abspath}{$config_file_name}.php"; if ( $this->filesystem->is_writable( $config_file ) ) { return $config_file; } $abspath_parent = dirname( $abspath ) . DIRECTORY_SEPARATOR; $config_file_alt = "{$abspath_parent}{$config_file_name}.php"; if ( $this->filesystem->exists( $config_file_alt ) && $this->filesystem->is_writable( $config_file_alt ) && ! $this->filesystem->exists( "{$abspath_parent}wp-settings.php" ) ) { return $config_file_alt; } // No writable file found. return false; } /** * This warning is displayed when the wp-config.php file isn't writable * * @since 3.6.1 * * @return void */ public function notice_wp_config_permissions() { global $pagenow; if ( 'plugins.php' === $pagenow || isset( $_GET['activate'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended ) { return; } if ( ! $this->is_user_allowed() ) { return; } if ( rocket_get_constant( 'WP_CACHE' ) ) { return; } // This filter is documented in inc/Engine/Cache/WPCache.php. if ( ! (bool) apply_filters( 'rocket_set_wp_cache_constant', true ) ) { return; } if ( $this->find_wpconfig_path() ) { return; } $notice_name = 'rocket_warning_wp_config_permissions'; if ( in_array( $notice_name, (array) get_user_meta( get_current_user_id(), 'rocket_boxes', true ), true ) ) { return; } rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => rocket_notice_writing_permissions( 'wp-config.php' ), 'dismiss_button' => $notice_name, 'readonly_content' => $this->get_wp_cache_content(), ] ); } /** * Checks if current user can perform the action * * @since 3.6.1 * * @return bool */ private function is_user_allowed() { return ( rocket_get_constant( 'WP_CLI', false ) || current_user_can( 'rocket_manage_options' ) ) && rocket_valid_key(); } /** * Gets the content to add to the wp-config.php file * * @since 3.6.1 * * @param string $value Value for the WP_CACHE constant. * @return string */ private function get_wp_cache_content( $value = 'true' ) { $plugin_name = rocket_get_constant( 'WP_ROCKET_PLUGIN_NAME' ); return "define( 'WP_CACHE', {$value} ); // Added by {$plugin_name}"; } /** * Adds a Site Health check for the WP_CACHE constant value * * @since 3.6.1 * * @param array $tests An array of tests to perform. * @return array */ public function add_wp_cache_status_test( $tests ) { // This filter is documented in inc/Engine/Cache/WPCache.php. if ( ! (bool) apply_filters( 'rocket_set_wp_cache_constant', true ) ) { return $tests; } $tests['direct']['wp_cache_status'] = [ 'label' => __( 'WP_CACHE value', 'rocket' ), 'test' => [ $this, 'check_wp_cache_value' ], ]; return $tests; } /** * Checks the WP_CACHE constant value and return the result for Site Health * * @since 3.6.1 * * @return array */ public function check_wp_cache_value() { $result = [ 'badge' => [ 'label' => __( 'Cache', 'rocket' ), ], 'description' => sprintf( '<p>%s</p>', __( 'The WP_CACHE constant needs to be set to true for WP Rocket cache to work properly', 'rocket' ) ), 'actions' => '', 'test' => 'wp_cache_status', ]; $value = rocket_get_constant( 'WP_CACHE' ); if ( true === $value ) { $result['label'] = __( 'WP_CACHE is set to true', 'rocket' ); $result['status'] = 'good'; $result['badge']['color'] = 'green'; return $result; } if ( null === $value ) { $result['label'] = __( 'WP_CACHE is not set', 'rocket' ); $result['status'] = 'critical'; $result['badge']['color'] = 'red'; return $result; } if ( false === $value ) { $result['label'] = __( 'WP_CACHE is set to false', 'rocket' ); $result['status'] = 'critical'; $result['badge']['color'] = 'red'; return $result; } return $result; } } Engine/Cache/Config/Subscriber.php 0000644 00000001075 15174677547 0013027 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache\Config; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the Cache Config */ class Subscriber implements Subscriber_Interface { /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'permalink_structure_changed' => 'regenerate_config_file', ]; } /** * Regenerate config file. * * @return void */ public function regenerate_config_file() { rocket_generate_config_file(); } } Engine/Cache/Config/ConfigSubscriber.php 0000644 00000005133 15174677547 0014154 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache\Config; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Admin\Options; /** * Subscriber for the Cache Config */ class ConfigSubscriber implements Subscriber_Interface { /** * Options Data instance. * * @var Options_Data */ private $options; /** * Options instance. * * @var Options */ private $options_api; /** * Creates an instance of the Cache Config Subscriber. * * @param Options_Data $options WP Rocket options instance. * @param Options $options_api Options instance. */ public function __construct( Options_Data $options, Options $options_api ) { $this->options = $options; $this->options_api = $options_api; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'permalink_structure_changed' => 'regenerate_config_file', 'pre_update_option_' . rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ) => [ 'change_cache_reject_uri_with_permalink', 10, 2 ], ]; } /** * Returns a matching user added patterns with permalink structure * * @param array $patterns user pattern. * @return array */ private function match_pattern_with_permalink_structure( array $patterns ): array { if ( empty( $patterns ) ) { return $patterns; } $patterns = array_map( function ( $uri ) { if ( false !== strpos( $uri, 'index.php' ) || '/' === $uri ) { return $uri; } return user_trailingslashit( $uri ); }, $patterns ); return $patterns; } /** * Regenerate config file. * * @return void */ public function regenerate_config_file() { $cache_reject_uri = $this->match_pattern_with_permalink_structure( $this->options->get( 'cache_reject_uri', [] ) ); $this->options->set( 'cache_reject_uri', $cache_reject_uri ); $this->options_api->set( 'settings', $this->options->get_options() ); rocket_generate_config_file(); } /** * Modify cache_reject_uri values. * * @param mixed $value The new, unserialized option value. * @param mixed $old_value The old option value. * @return array */ public function change_cache_reject_uri_with_permalink( $value, $old_value ): array { if ( ! isset( $old_value['cache_reject_uri'], $value['cache_reject_uri'] ) ) { return $value; } if ( $old_value['cache_reject_uri'] === $value['cache_reject_uri'] ) { return $value; } $value['cache_reject_uri'] = $this->match_pattern_with_permalink_structure( $value['cache_reject_uri'] ); return $value; } } Engine/Cache/ServiceProvider.php 0000644 00000005611 15174677547 0012632 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Cache; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Cache\PurgeExpired\PurgeExpiredCache; use WP_Rocket\Engine\Cache\PurgeExpired\Subscriber; use WP_Rocket\Engine\Preload\Database\Queries\Cache as CacheQuery; use WP_Rocket\Engine\Cache\Config\ConfigSubscriber; use WP_Rocket\Engine\Cache\UrlValidation\{ TaxonomySubscriber, PostSubscriber }; /** * Service Provider for cache subscribers */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'advanced_cache', 'wp_cache', 'purge', 'purge_actions_subscriber', 'admin_cache_subscriber', 'expired_cache_purge', 'expired_cache_purge_subscriber', 'preload_caches_query', 'cache_config', 'taxonomy_subscriber', 'post_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $filesystem = rocket_direct_filesystem(); $this->getContainer()->add( 'preload_caches_query', CacheQuery::class ) ->addArgument( 'logger' ); $this->getContainer()->add( 'advanced_cache', AdvancedCache::class ) ->addArguments( [ new StringArgument( $this->getContainer()->get( 'template_path' ) . '/cache/' ), $filesystem, ] ); $this->getContainer()->add( 'wp_cache', WPCache::class ) ->addArgument( $filesystem ); $this->getContainer()->add( 'purge', Purge::class ) ->addArguments( [ $filesystem, 'preload_caches_query', ] ); $this->getContainer()->addShared( 'purge_actions_subscriber', PurgeActionsSubscriber::class ) ->addArguments( [ 'options', 'purge', ] ); $this->getContainer()->addShared( 'admin_cache_subscriber', AdminSubscriber::class ) ->addArguments( [ 'advanced_cache', 'wp_cache', ] ); $this->getContainer()->add( 'expired_cache_purge', PurgeExpiredCache::class ) ->addArgument( new StringArgument( rocket_get_constant( 'WP_ROCKET_CACHE_PATH', '' ) ) ); $this->getContainer()->addShared( 'expired_cache_purge_subscriber', Subscriber::class ) ->addArguments( [ 'options', 'expired_cache_purge', ] ); $this->getContainer()->addShared( 'cache_config', ConfigSubscriber::class ) ->addArguments( [ 'options', 'options_api', ] ); $this->getContainer()->addShared( 'taxonomy_subscriber', TaxonomySubscriber::class ); $this->getContainer()->addShared( 'post_subscriber', PostSubscriber::class ); } } Engine/Cache/Purge.php 0000644 00000016227 15174677547 0010606 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache; use WP_Rocket\Engine\Preload\Database\Queries\Cache; use DirectoryIterator; use Exception; use WP_Filesystem_Direct; use WP_Term; use WP_Post; /** * Cache Purge handling class */ class Purge { /** * Filesystem instance * * @var WP_Filesystem_Direct */ private $filesystem; /** * Cache Query * * @var Cache */ protected $query; /** * Initialize the class. * * @param WP_Filesystem_Direct $filesystem Filesystem instance. * @param Cache $query Cache query instance. */ public function __construct( $filesystem, Cache $query ) { $this->filesystem = $filesystem; $this->query = $query; } /** * Purges cache for the dates archives of a post * * @param WP_Post $post Post object. * @return void */ public function purge_dates_archives( $post ) { foreach ( $this->get_dates_archives( $post ) as $url ) { $this->purge_url( $url, true ); } } /** * Purge URL cache. * * @param string $url URL to be purged. * @param boolean $pagination Purge also pagination. * @return void */ public function purge_url( string $url, $pagination = false ) { global $wp_rewrite; $parsed_url = $this->parse_url( $url ); foreach ( _rocket_get_cache_dirs( $parsed_url['host'] ) as $dir ) { $path = $dir . $parsed_url['path']; if ( ! $this->filesystem->exists( $path ) ) { continue; } foreach ( $this->get_iterator( $path ) as $item ) { if ( $item->isFile() ) { $this->filesystem->delete( $item->getPathname() ); continue; } if ( str_contains( $item->getPathname(), '#' ) ) { $this->filesystem->delete( $item->getPathname(), true ); } } if ( $pagination ) { $this->maybe_remove_dir( $path . DIRECTORY_SEPARATOR . $wp_rewrite->pagination_base ); } } } /** * Gets the dates archives URLs for the provided post * * @param WP_Post $post Post object. * @return array */ private function get_dates_archives( $post ) { $time = get_the_time( 'Y-m-d', $post ); if ( empty( $time ) ) { return []; } $date = explode( '-', $time ); $urls = [ get_year_link( $date[0] ), get_month_link( $date[0], $date[1] ), get_day_link( $date[0], $date[1], $date[2] ), ]; /** * Filter the list of dates URLs. * * @since 1.1.0 * * @param array $urls List of dates URLs. */ return (array) apply_filters( 'rocket_post_dates_urls', $urls ); } /** * Parses URL and return the parts array * * @since 3.6.1 * * @param string $url URL to parse. * @return array */ private function parse_url( $url ) { $parsed_url = get_rocket_parse_url( $url ); /** This filter is documented in inc/front/htaccess.php */ if ( apply_filters( 'rocket_url_no_dots', false ) ) { $parsed_url['host'] = str_replace( '.', '_', $parsed_url['host'] ); } return $parsed_url; } /** * Gets the iterator for the given path * * @since 3.6.1 * * @param string $path Absolute path. * @return DirectoryIterator|array */ private function get_iterator( $path ) { try { $iterator = new DirectoryIterator( $path ); } catch ( Exception $e ) { // No action required, as logging not enabled. $iterator = []; } return $iterator; } /** * Recursively remove the provided directory and its content * * @since 3.6.1 * * @param string $dir Absolute path for the directory. * @return void */ private function maybe_remove_dir( $dir ) { if ( $this->filesystem->is_dir( $dir ) ) { rocket_rrmdir( $dir, [], $this->filesystem ); } } /** * Purge all terms archives urls associated to a specific post. * * @since 3.6.1 * * @param WP_Post $post Post object. */ public function purge_post_terms_urls( WP_Post $post ) { $urls = $this->get_post_terms_urls( $post ); foreach ( $urls as $url ) { $this->purge_url( $url, true ); } /** * Action to preload urls after cleaning cache. * * @param array $urls urls to preload. */ do_action( 'rocket_after_clean_terms', $urls ); } /** * Get all terms archives urls associated to a specific post. * * @since 3.6.1 * * @param WP_Post $post Post object. * * @return array $urls List of taxonomies URLs */ private function get_post_terms_urls( WP_Post $post ) { $urls = []; $taxonomies = get_object_taxonomies( get_post_type( $post->ID ), 'objects' ); /** * Filters the taxonomies excluded from post purge * * @since 3.9.1 * * @param array $excluded_taxonomies Array of excluded taxonomies names. */ $excluded_taxonomies = apply_filters( 'rocket_exclude_post_taxonomy', [] ); foreach ( $taxonomies as $taxonomy ) { if ( ! $taxonomy->public || in_array( $taxonomy->name, $excluded_taxonomies, true ) ) { continue; } // Get the terms related to post. $terms = get_the_terms( $post->ID, $taxonomy->name ); if ( empty( $terms ) || is_wp_error( $terms ) ) { continue; } foreach ( $terms as $term ) { $term_url = get_term_link( $term->slug, $taxonomy->name ); if ( ! is_wp_error( $term_url ) ) { $urls[] = $term_url; } if ( ! is_taxonomy_hierarchical( $taxonomy->name ) ) { continue; } $ancestors = (array) get_ancestors( $term->term_id, $taxonomy->name ); foreach ( $ancestors as $ancestor ) { $ancestor_object = get_term( $ancestor, $taxonomy->name ); if ( ! $ancestor_object instanceof WP_Term ) { continue; } $ancestor_term_url = get_term_link( $ancestor_object->slug, $taxonomy->name ); if ( ! is_wp_error( $ancestor_term_url ) ) { $urls[] = $ancestor_term_url; } } } } // Remove entries with empty values in array. $urls = array_filter( $urls, 'is_string' ); /** * Filter the list of taxonomies URLs * * @since 1.1.0 * * @param array $urls List of taxonomies URLs */ return apply_filters( 'rocket_post_terms_urls', $urls ); } /** * Purge single cache file(s) added in the Never Cache URL(s). * * @param array $old_value An array of previous values for the settings. * @param array $value An array of submitted values for the settings. * @return void */ public function purge_cache_reject_uri_partially( array $old_value, array $value ): void { // Bail out if cache_reject_uri key is not in the settings array. if ( ! array_key_exists( 'cache_reject_uri', $old_value ) || ! array_key_exists( 'cache_reject_uri', $value ) ) { return; } // Get change in uris. $diff = array_diff( $value['cache_reject_uri'], $old_value['cache_reject_uri'] ); // Bail out if values has not changed. if ( empty( $diff ) ) { return; } $urls = []; $wildcard = '(.*)'; foreach ( $diff as $path ) { // Check if string is a path or pattern. if ( strpos( $path, $wildcard ) !== false ) { $pattern = preg_replace( '#\(\.\*\).*#', '*', $path ); $results = $this->query->query( [ 'search' => $pattern, 'search_columns' => [ 'url' ], 'status' => 'completed', ] ); foreach ( $results as $result ) { $urls[] = $result->url; } continue; } // Get full url of never cache path. $urls[] = home_url( $path ); } rocket_clean_files( array_unique( $urls ) ); } } Engine/Cache/PurgeActionsSubscriber.php 0000644 00000012425 15174677547 0014147 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache; use WP_Post; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Logger\Logger; /** * Subscriber for the cache purge actions * * @since 3.5 */ class PurgeActionsSubscriber implements Subscriber_Interface { /** * WP Rocket options instance * * @var Options_Data */ private $options; /** * Purge instance * * @var Purge */ private $purge; /** * Constructor * * @param Options_Data $options WP Rocket options instance. * @param Purge $purge Purge instance. */ public function __construct( Options_Data $options, Purge $purge ) { $this->options = $options; $this->purge = $purge; } /** * {@inheritdoc} */ public static function get_subscribed_events() { $slug = rocket_get_constant( 'WP_ROCKET_SLUG' ); return [ 'profile_update' => 'purge_user_cache', 'delete_user' => 'purge_user_cache', 'create_term' => [ 'maybe_purge_cache_on_term_change', 10, 3 ], 'edit_term' => [ 'maybe_purge_cache_on_term_change', 10, 3 ], 'delete_term' => [ 'maybe_purge_cache_on_term_change', 10, 3 ], 'after_rocket_clean_post' => [ [ 'purge_dates_archives' ], [ 'purge_post_terms_urls' ], ], 'rocket_saas_complete_job_status' => [ 'purge_url_cache', 100 ], 'rocket_rucss_after_clearing_usedcss' => 'purge_url_cache', 'rocket_performance_hints_data_after_clearing' => 'purge_url_cache', 'rocket_after_save_dynamic_lists' => 'purge_cache_after_saving_dynamic_lists', 'update_option_' . $slug => [ 'purge_cache_reject_uri_partially', 10, 2 ], 'update_option_blog_public' => 'purge_cache', 'wp_rocket_upgrade' => [ 'on_update', 10, 2 ], ]; } /** * Purges the cache of the corresponding user * * @since 3.5 * * @param int $user_id User ID. * @return void */ public function purge_user_cache( $user_id ) { if ( ! $this->should_purge_user_cache() ) { return; } rocket_clean_user( $user_id ); } /** * Purges the cache when a public term is created|updated|deleted * * @since 3.5.5 * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @return void */ public function maybe_purge_cache_on_term_change( $term_id, $tt_id, $taxonomy ) { if ( rocket_is_importing() ) { return; } if ( ! $this->is_taxonomy_public( $taxonomy ) ) { return; } rocket_clean_domain(); } /** * Purges cache for the dates archives of a post after cleaning the post * * @param WP_Post $post Post object. * @return void */ public function purge_dates_archives( $post ) { $this->purge->purge_dates_archives( $post ); } /** * Purge all terms archives urls associated to a specific post. * * @param WP_Post $post Post object. * @return void */ public function purge_post_terms_urls( $post ) { $this->purge->purge_post_terms_urls( $post ); } /** * Checks if the given taxonomy is public * * @param string $name Taxonomy name. * @return bool */ private function is_taxonomy_public( $name ) { $taxonomy = get_taxonomy( $name ); if ( false === $taxonomy ) { return false; } return ( $taxonomy->public && $taxonomy->publicly_queryable ); } /** * Checks if the user cache should be purged * * @since 3.5 * * @return boolean */ private function should_purge_user_cache() { if ( ! $this->options->get( 'cache_logged_user', 0 ) ) { return false; } // This filter is documented in /inc/functions/files.php. return ! (bool) apply_filters( 'rocket_common_cache_logged_users', false ); } /** * Purge cache after RUCSS * * @param string $url URL to be purged. * * @return void */ public function purge_url_cache( string $url ) { // Flush cache for this url. Logger::debug( 'RUCSS: Purge the cache for url: ' . $url ); $this->purge->purge_url( $url ); } /** * Clean the whole cache * * @return void */ public function purge_cache() { rocket_clean_domain(); } /** * Purge single cache file(s) added in the Never Cache URL(s). * * @param array $old_value An array of previous values for the settings. * @param array $value An array of submitted values for the settings. * @return void */ public function purge_cache_reject_uri_partially( array $old_value, array $value ): void { $this->purge->purge_cache_reject_uri_partially( $old_value, $value ); } /** * Purge cache after saving dynamic lists. * * @param bool $should_purge Should purge or not. * * @return void */ public function purge_cache_after_saving_dynamic_lists( $should_purge = true ) { if ( ! $should_purge ) { return; } $this->purge_cache(); } /** * Regenerate the advanced cache file on update * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function on_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.15.3', '>=' ) ) { return; } rocket_generate_advanced_cache_file(); } } Engine/Cache/UrlValidation/TaxonomySubscriber.php 0000644 00000003130 15174677547 0016130 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache\UrlValidation; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the taxonomy frontend pages. */ class TaxonomySubscriber extends AbstractUrlValidation implements Subscriber_Interface { /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'do_rocket_generate_caching_files' => 'disable_cache_on_not_valid_url', 'rocket_buffer' => [ 'stop_optimizations_for_not_valid_url', 1 ], ]; } /** * Check if we are on the taxonomy frontend page, but it's not valid url query. * * @return bool (True when not valid taxonomy page, False if it's a valid one) */ protected function is_not_valid_url(): bool { if ( ! is_category() && ! is_tag() && ! is_tax() ) { return false; } $term_id = get_queried_object_id(); if ( empty( $term_id ) ) { return false; } global $wp; $term_link = get_term_link( $term_id ); if ( is_wp_error( $term_link ) ) { return false; } $current_link = home_url( add_query_arg( [], $wp->request ?? '' ) ); if ( is_paged() ) { $term_link = trailingslashit( $term_link ) . 'page/' . get_query_var( 'paged' ); } $term_link = urldecode( untrailingslashit( $term_link ) ); if ( urldecode( untrailingslashit( $current_link ) ) !== $term_link && ! empty( $_SERVER['REQUEST_URI'] ) ) { $current_link = home_url( add_query_arg( [], wp_unslash( $_SERVER['REQUEST_URI'] ) ) );// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } return urldecode( untrailingslashit( $current_link ) ) !== $term_link; } } Engine/Cache/UrlValidation/AbstractUrlValidation.php 0000644 00000003320 15174677547 0016530 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Cache\UrlValidation; abstract class AbstractUrlValidation { /** * Disable caching invalid page urls. * * @param bool $can_cache Filter callback passed value. * * @return bool */ public function disable_cache_on_not_valid_url( $can_cache ) { if ( $this->is_disabled() ) { return $can_cache; } if ( $this->is_not_valid_url() ) { return false; } return $can_cache; } /** * Stop optimizing those invalid pages by returning empty html string, * So it fall back to the normal page's HTML. * * @param string $html Page's buffer HTML. * * @return string */ public function stop_optimizations_for_not_valid_url( $html ) { if ( $this->is_disabled() ) { return $html; } return $this->is_not_valid_url() ? '' : $html; } /** * Check if url validation is disabled by filter * * @return bool */ protected function is_disabled(): bool { /** * Filters whether to disable URL validation. * * @param bool $disable True to disable URL validation, false to enable it. */ return wpm_apply_filters_typed( 'boolean', 'rocket_disable_url_validation', false ); } /** * Check if current url is not valid * * @return bool */ abstract protected function is_not_valid_url(): bool; /** * Retrieves the current URL for validation purposes. * * @return string The current URL. */ protected function get_current_url() { global $wp; $current_url = home_url( add_query_arg( [], $wp->request ?? '' ) ); /** * Filters the current URL used for validation. * * @param string $current_url The current URL. */ return wpm_apply_filters_typed( 'string', 'rocket_current_url', $current_url ); } } Engine/Cache/UrlValidation/PostSubscriber.php 0000644 00000002777 15174677547 0015257 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache\UrlValidation; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the post/page frontend pages. */ class PostSubscriber extends AbstractUrlValidation implements Subscriber_Interface { /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'do_rocket_generate_caching_files' => 'disable_cache_on_not_valid_url', 'rocket_buffer' => [ 'stop_optimizations_for_not_valid_url', 1 ], ]; } /** * Check if we are on the post frontend page, but it's not valid url query. * * @return bool (True when not valid post url, False if it's a valid one) */ protected function is_not_valid_url(): bool { if ( ! is_singular() ) { return false; } $post_id = get_queried_object_id(); if ( empty( $post_id ) ) { return false; } $post_link = get_permalink( $post_id ); if ( ! $post_link ) { return false; } $current_link = $this->get_current_url(); if ( is_paged() ) { $post_link = trailingslashit( $post_link ) . 'page/' . get_query_var( 'paged' ); } if ( urldecode( untrailingslashit( $current_link ) ) !== urldecode( untrailingslashit( $post_link ) ) && ! empty( $_SERVER['REQUEST_URI'] ) ) { $current_link = home_url( add_query_arg( [], wp_unslash( $_SERVER['REQUEST_URI'] ) ) );// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } return urldecode( untrailingslashit( $post_link ) ) !== urldecode( untrailingslashit( $current_link ) ); } } Engine/Cache/PurgeExpired/Subscriber.php 0000644 00000010674 15174677547 0014232 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache\PurgeExpired; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Event subscriber to clear cached files after lifespan. * * @since 3.4 */ class Subscriber implements Subscriber_Interface { /** * Cron name. * * @since 3.4 * * @var string */ const EVENT_NAME = 'rocket_purge_time_event'; /** * WP Rocket Options instance. * * @since 3.4 * * @var Options_Data */ private $options; /** * Expired Cache Purge instance. * * @since 3.4 * * @var PurgeExpiredCache */ private $purge; /** * Constructor. * * @param Options_Data $options Options instance. * @param PurgeExpiredCache $purge Purge instance. */ public function __construct( Options_Data $options, PurgeExpiredCache $purge ) { $this->options = $options; $this->purge = $purge; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'init' => 'schedule_event', 'rocket_deactivation' => 'unschedule_event', static::EVENT_NAME => 'purge_expired_files', 'cron_schedules' => 'custom_cron_schedule', 'wp_rocket_upgrade' => [ 'update_lifespan_option_on_update', 13, 2 ], ]; } /** * Adds a custom cron schedule based on purge lifespan interval. * * @since 3.4.3 * * @param array $schedules An array of non-default cron schedules. */ public function custom_cron_schedule( $schedules ) { $schedules['rocket_expired_cache_cron_interval'] = [ 'interval' => $this->get_interval(), 'display' => __( 'WP Rocket Expired Cache Interval', 'rocket' ), ]; return $schedules; } /** ----------------------------------------------------------------------------------------- */ /** HOOK CALLBACKS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Scheduling the cron event. * If the task is not programmed, it is automatically added. * * @since 3.4 */ public function schedule_event() { if ( $this->get_cache_lifespan() && ! wp_next_scheduled( static::EVENT_NAME ) ) { $interval = $this->get_interval(); wp_schedule_event( time() + $interval, 'rocket_expired_cache_cron_interval', static::EVENT_NAME ); } } /** * Gets the interval when the scheduled clean cache purge needs to run. * If Minutes option is selected, then the interval will be set to minutes. * If Hours / Days options are selected, then it will be set to 1 hour. * * @since 3.4.3 * * @return int $interval Interval time in seconds. */ private function get_interval() { $unit = $this->options->get( 'purge_cron_unit' ); $lifespan = $this->options->get( 'purge_cron_interval', 10 ); $interval = HOUR_IN_SECONDS; if ( ! $unit || ! defined( $unit ) ) { $unit = 'HOUR_IN_SECONDS'; } if ( 'MINUTE_IN_SECONDS' === $unit ) { $interval = $lifespan * MINUTE_IN_SECONDS; } return $interval; } /** * Unschedule the event. * * @since 3.4 */ public function unschedule_event() { wp_clear_scheduled_hook( static::EVENT_NAME ); } /** * Perform the event action. * * @since 3.4 */ public function purge_expired_files() { $this->purge->purge_expired_files( $this->get_cache_lifespan() ); } /** * Get the cache lifespan in seconds. * If no value is filled in the settings, return 0. It means the purge is disabled. * If the value from the settings is filled but invalid, fallback to the initial value (10 hours). * * @since 3.4 * * @return int The cache lifespan in seconds. */ public function get_cache_lifespan() { $lifespan = $this->options->get( 'purge_cron_interval' ); if ( ! $lifespan ) { return 0; } $unit = $this->options->get( 'purge_cron_unit' ); if ( $lifespan < 0 || ! $unit || ! defined( $unit ) ) { return 10 * HOUR_IN_SECONDS; } return $lifespan * constant( $unit ); } /** * Update lifespan option to remove minutes with WP Rocket Update. * * @since 3.8 * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function update_lifespan_option_on_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.8', '>' ) ) { return; } $this->purge->update_lifespan_value( $this->options->get( 'purge_cron_interval' ), $this->options->get( 'purge_cron_unit' ) ); } } Engine/Cache/PurgeExpired/PurgeExpiredCache.php 0000644 00000027020 15174677547 0015447 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache\PurgeExpired; use WP_Rocket\Buffer\Cache; /** * Purge expired cache files based on the defined lifespan * * @since 3.4 */ class PurgeExpiredCache { /** * Path to the global cache folder. * * @since 3.4 * * @var string */ private $cache_path; /** * Filesystem object. * * @since 3.4 * * @var null|\WP_Filesystem_Direct */ private $filesystem; /** * Constructor * * @param string $cache_path Path to the global cache folder. */ public function __construct( $cache_path ) { $this->cache_path = $cache_path; } /** * Perform the event action. * * @since 3.4 * * @param int $lifespan The cache lifespan in seconds. */ public function purge_expired_files( $lifespan ) { if ( ! $lifespan ) { // Uh? return; } $urls = get_rocket_i18n_uri(); $file_age_limit = time() - $lifespan; /** * Filter home URLs that will be searched for old cache files. * * @since 3.4 * * @param array $urls URLs that will be searched for old cache files. * @param int $file_age_limit Timestamp of the maximum age files must have. */ $urls = wpm_apply_filters_typed( 'array', 'rocket_automatic_cache_purge_urls', $urls, $file_age_limit ); if ( ! is_array( $urls ) ) { // I saw what you did ಠ_ಠ. $urls = get_rocket_i18n_uri(); } $urls = array_filter( $urls, 'is_string' ); $urls = array_filter( $urls ); if ( ! $urls ) { return; } $urls = array_unique( $urls ); if ( empty( $this->filesystem ) ) { $this->filesystem = rocket_direct_filesystem(); } $deleted = []; $cache_enabled = Cache::can_generate_caching_files(); foreach ( $urls as $url ) { /** * Fires before purging a cache directory. * * @since 3.4 * * @param string $url The home url. * @param int $file_age_limit Timestamp of the maximum age files must have. */ do_action( 'rocket_before_automatic_cache_purge_dir', $url, $file_age_limit ); $url_deleted = []; if ( $cache_enabled ) { // Get the directory names. $file = get_rocket_parse_url( $url ); /** This filter is documented in inc/front/htaccess.php */ if ( apply_filters( 'rocket_url_no_dots', false ) ) { $file['host'] = str_replace( '.', '_', $file['host'] ); } $sub_dir = rtrim( $file['path'], '/' ); $files = $this->get_cache_files_in_dir( $file ); foreach ( $files as $item ) { $dir_path = $item->getPathname(); $sub_dir_path = $dir_path . $sub_dir; // Time to cut old leaves. $item_paths = $this->purge_dir( $sub_dir_path, $file_age_limit ); if ( $item_paths ) { $url_deleted[] = [ 'home_url' => $url, 'home_path' => $sub_dir_path, 'logged_in' => $dir_path !== $this->cache_path . $file['host'], 'files' => $item_paths, ]; } if ( $this->is_dir_empty( $dir_path ) ) { // If the folder is empty, remove it. $this->filesystem->delete( $dir_path ); } } if ( $url_deleted ) { $deleted = array_merge( $deleted, $url_deleted ); } } $args = [ 'url' => $url, 'lifespan' => $lifespan, 'file_age_limit' => $file_age_limit, ]; /** * Fires after a cache directory is purged. * * @since 3.4 * * @param array $deleted { * An array of arrays sharing the same home URL, described like: { * @type string $home_url The home URL. This is the same as $args['url']. * @type string $home_path Path to home. * @type bool $logged_in True if the home path corresponds to a logged in user’s folder. * @type array $files A list of paths of files that have been deleted. * } * Ex: * [ * [ * 'home_url' => 'http://example.com/home1', * 'home_path' => '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1', * 'logged_in' => false, * 'files' => [ * '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1/deleted-page', * '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1/very-dead-page', * ], * ], * [ * 'home_url' => 'http://example.com/home1', * 'home_path' => '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1', * 'logged_in' => true, * 'files' => [ * '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1/how-to-prank-your-coworkers', * '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1/best-source-of-gifs', * ], * ], * ] * @param array $args { * @type string $url The home url. * @type int $lifespan Files lifespan in seconds. * @type int $file_age_limit Timestamp of the maximum age files must have. This is basically `time() - $lifespan`. * } */ do_action( 'rocket_after_automatic_cache_purge_dir', $url_deleted, $args ); } $args = [ 'lifespan' => $lifespan, 'file_age_limit' => $file_age_limit, ]; /** * Fires after cache directories are purged. * * @since 3.4 * * @param array $deleted { * An array of arrays, described like: { * @type string $home_url The home URL. * @type string $home_path Path to home. * @type bool $logged_in True if the home path corresponds to a logged in user’s folder. * @type array $files A list of paths of files that have been deleted. * } * Ex: * [ * [ * 'home_url' => 'http://example.com/home1', * 'home_path' => '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1', * 'logged_in' => false, * 'files' => [ * '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1/deleted-page', * '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1/very-dead-page', * ], * ], * [ * 'home_url' => 'http://example.com/home1', * 'home_path' => '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1', * 'logged_in' => true, * 'files' => [ * '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1/how-to-prank-your-coworkers', * '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1/best-source-of-gifs', * ], * ], * [ * 'home_url' => 'http://example.com/home4', * 'home_path' => '/path-to/home4/wp-content/cache/wp-rocket/example.com-Greg-71edg8d6af865569979569/home4', * 'logged_in' => true, * 'files' => [ * '/path-to/home4/wp-content/cache/wp-rocket/example.com-Greg-71edg8d6af865569979569/home4/easter-eggs-in-code-your-best-opportunities', * ], * ], * ] * } * @param array $args { * @type int $lifespan Files lifespan in seconds. * @type int $file_age_limit Timestamp of the maximum age files must have. This is basically `time() - $lifespan`. * } */ do_action( 'rocket_after_automatic_cache_purge', $deleted, $args ); } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get all cache files for the provided URL * * @since 3.4 * * @param array $file An array of the parsed URL parts. * @return array|\CallbackFilterIterator */ private function get_cache_files_in_dir( $file ) { // Grab cache folders. $host_pattern = '@^' . preg_quote( $file['host'], '@' ) . '@'; $sub_dir = rtrim( $file['path'], '/' ); try { $iterator = new \DirectoryIterator( $this->cache_path ); } catch ( \Exception $e ) { return []; } return new \CallbackFilterIterator( $iterator, function ( $current ) use ( $host_pattern, $sub_dir ) { if ( ! $current->isDir() || $current->isDot() ) { // We look for folders only, and don't want '.' nor '..'. return false; } if ( ! preg_match( $host_pattern, $current->getFilename() ) ) { // Not the right host. return false; } if ( '' !== $sub_dir && ! $this->filesystem->exists( $current->getPathname() . $sub_dir ) ) { // Not the right path. return false; } return true; } ); } /** * Purge a folder from old files. * * @since 3.4 * * @param string $dir_path Path to the folder to purge. * @param int $file_age_limit Timestamp of the maximum age files must have. * @return array A list of files that have been deleted. */ private function purge_dir( $dir_path, $file_age_limit ) { $deleted = []; try { $iterator = new \DirectoryIterator( $dir_path ); } catch ( \Exception $e ) { return []; } foreach ( $iterator as $item ) { if ( $item->isDot() ) { continue; } if ( $item->isDir() ) { /** * A folder, let’s see what’s in there. * Maybe there’s a dinosaur fossil or a hidden treasure. */ $dir_deleted = $this->purge_dir( $item->getPathname(), $file_age_limit ); $deleted = array_merge( $deleted, $dir_deleted ); } elseif ( $item->isFile() && $item->getMTime() < $file_age_limit ) { $file_path = $item->getPathname(); /** * The file is older than our limit. * This will also delete the file if `$item->getMTime()` fails. */ if ( ! $this->filesystem->delete( $file_path ) ) { continue; } /** * A page can have multiple cache files: * index(-mobile)(-https)(-dynamic-cookie-key){0,*}.html(_gzip). */ $dir_path = dirname( $file_path ); if ( ! in_array( $dir_path, $deleted, true ) ) { $deleted[] = $dir_path; } } } if ( $this->is_dir_empty( $dir_path ) ) { // If the folder is empty, remove it. $this->filesystem->delete( $dir_path ); } return $deleted; } /** * Tell if a folder is empty. * * @since 3.4 * * @param string $dir_path Path to the folder to purge. * @return bool True if empty. False if it contains files. */ private function is_dir_empty( $dir_path ) { try { $iterator = new \DirectoryIterator( $dir_path ); } catch ( \Exception $e ) { return true; } foreach ( $iterator as $item ) { if ( $item->isDot() ) { continue; } return false; } return true; } /** * Update lifespan option to convert old minutes to hours. * * @since 3.8 * * @param int $old_lifespan Old value in minutes. * @param string $old_lifespan_unit Old value of unit. * * @return void */ public function update_lifespan_value( $old_lifespan, $old_lifespan_unit ) { if ( 'MINUTE_IN_SECONDS' !== $old_lifespan_unit ) { return; } $options = get_option( 'wp_rocket_settings', [] ); if ( $old_lifespan > 0 && $old_lifespan < 60 ) { $old_lifespan = 60; } $options['purge_cron_unit'] = 'HOUR_IN_SECONDS'; $options['purge_cron_interval'] = round( $old_lifespan / 60 ); update_option( 'wp_rocket_settings', $options ); } } Engine/Cache/AdvancedCache.php 0000644 00000011461 15174677547 0012150 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache; use WP_Filesystem_Direct; use WP_Rocket\Engine\Activation\ActivationInterface; use WP_Rocket\Engine\Deactivation\DeactivationInterface; class AdvancedCache implements ActivationInterface, DeactivationInterface { /** * Absolute path to template files * * @var string */ private $template_path; /** * WP Content directory path * * @var string */ private $content_dir; /** * Instance of the filesystem handler. * * @var WP_Filesystem_Direct */ private $filesystem; /** * Instantiate of the class. * * @param string $template_path Absolute path to template files. * @param WP_Filesystem_Direct $filesystem Instance of the filesystem handler. */ public function __construct( $template_path, $filesystem ) { $this->template_path = $template_path; $this->content_dir = rocket_get_constant( 'WP_CONTENT_DIR' ); $this->filesystem = $filesystem; } /** * Actions to perform on plugin activation * * @since 3.6.3 * * @return void */ public function activate() { add_action( 'rocket_activation', [ $this, 'update_advanced_cache' ] ); } /** * Actions to perform on plugin deactivation * * @since 3.6.3 * * @return void */ public function deactivate() { add_action( 'rocket_deactivation', [ $this, 'update_advanced_cache' ] ); } /** * Generates the advanced-cache.php file with its content * * @since 3.6.3 * * @param int $sites_number Number of WP Rocket config files found. * @return void */ public function update_advanced_cache( $sites_number = 0 ) { /** * Filters whether to generate the advanced-cache.php file. * * @since 3.6.3 * * @param bool $generate True (default) to go ahead with advanced cache file generation; false to stop generation. */ if ( ! (bool) apply_filters( 'rocket_generate_advanced_cache_file', true ) ) { return; } $content = $this->get_advanced_cache_content(); if ( 'rocket_deactivation' === current_filter() ) { if ( is_multisite() && 0 !== $sites_number ) { return; } $content = ''; } $this->filesystem->put_contents( "{$this->content_dir}/advanced-cache.php", $content, rocket_get_filesystem_perms( 'file' ) ); } /** * Gets the content for the advanced-cache.php file * * @since 3.6 * * @return string */ public function get_advanced_cache_content() { $content = $this->filesystem->get_contents( $this->template_path . 'advanced-cache.php' ); $mobile = is_rocket_generate_caching_mobile_files() ? '$1' : ''; $content = preg_replace( "/'{{MOBILE_CACHE}}';(\X*)'{{\/MOBILE_CACHE}}';/", $mobile, $content ); $replacements = [ '{{WP_ROCKET_PHP_VERSION}}' => rocket_get_constant( 'WP_ROCKET_PHP_VERSION' ), '{{WP_ROCKET_PATH}}' => rocket_get_constant( 'WP_ROCKET_PATH' ), '{{WP_ROCKET_CONFIG_PATH}}' => rocket_get_constant( 'WP_ROCKET_CONFIG_PATH' ), '{{WP_ROCKET_CACHE_PATH}}' => rocket_get_constant( 'WP_ROCKET_CACHE_PATH' ), ]; foreach ( $replacements as $key => $value ) { $content = str_replace( $key, $value, $content ); } /** * Filter the content of advanced-cache.php file. * * @since 2.1 * * @param string $content The content that will be printed in advanced-cache.php. */ return (string) apply_filters( 'rocket_advanced_cache_file', $content ); } /** * This warning is displayed when the advanced-cache.php file isn't writeable * * @since 3.6 Moved to a method in AdvancedCache * @since 2.0 * * @return void */ public function notice_permissions() { if ( ! $this->is_user_allowed() ) { return; } // This filter is documented in inc/functions/files.php. if ( ! (bool) apply_filters( 'rocket_generate_advanced_cache_file', true ) ) { return; } if ( $this->filesystem->is_writable( "{$this->content_dir}/advanced-cache.php" ) || rocket_get_constant( 'WP_ROCKET_ADVANCED_CACHE' ) ) { return; } $notice_name = 'rocket_warning_advanced_cache_permissions'; if ( in_array( $notice_name, (array) get_user_meta( get_current_user_id(), 'rocket_boxes', true ), true ) ) { return; } rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $this->get_notice_message(), 'dismiss_button' => $notice_name, 'readonly_content' => $this->get_advanced_cache_content(), ] ); } /** * Checks if current user can see the notices * * @since 3.6 * * @return bool */ private function is_user_allowed() { return current_user_can( 'rocket_manage_options' ) && rocket_valid_key(); } /** * Gets the message to display in the notice * * @since 3.6 * * @return string */ private function get_notice_message() { return rocket_notice_writing_permissions( basename( $this->content_dir ) . '/advanced-cache.php' ); } } Engine/Cache/AdminSubscriber.php 0000644 00000015316 15174677547 0012576 0 ustar 00 <?php namespace WP_Rocket\Engine\Cache; use WP_Filesystem_Direct; use WP_Rocket\Event_Management\Event_Manager; use WP_Rocket\Event_Management\Event_Manager_Aware_Subscriber_Interface; use WP_Term; /** * Subscriber for the cache admin events * * @since 3.5.5 */ class AdminSubscriber implements Event_Manager_Aware_Subscriber_Interface { /** * Event Manager instance * * @var Event_Manager */ protected $event_manager; /** * AdvancedCache instance * * @var AdvancedCache */ private $advanced_cache; /** * WPCache instance * * @var WPCache */ private $wp_cache; /** * WordPress filesystem. * * @var WP_Filesystem_Direct */ private $filesystem; /** * Instantiate the class * * @param AdvancedCache $advanced_cache AdvancedCache instance. * @param WPCache $wp_cache WPCache instance. * @param WP_Filesystem_Direct|null $filesystem WordPress filesystem. */ public function __construct( AdvancedCache $advanced_cache, WPCache $wp_cache, $filesystem = null ) { $this->advanced_cache = $advanced_cache; $this->wp_cache = $wp_cache; $this->filesystem = ! empty( $filesystem ) ? $filesystem : rocket_direct_filesystem(); } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { $slug = rocket_get_constant( 'WP_ROCKET_SLUG' ); return [ 'admin_init' => [ [ 'register_terms_row_action' ], [ 'maybe_set_wp_cache' ], ], 'admin_notices' => [ [ 'notice_advanced_cache_permissions' ], [ 'notice_wp_config_permissions' ], ], "update_option_{$slug}" => [ 'maybe_set_wp_cache', 12 ], 'site_status_tests' => 'add_wp_cache_status_test', 'wp_rocket_upgrade' => [ 'on_update', 10, 2 ], 'rocket_domain_changed' => [ [ 'regenerate_configs' ], [ 'delete_old_configs' ], [ 'clear_cache', 10, 2 ], ], 'rocket_after_save_import' => 'maybe_regenerate_advanced_cache', ]; } /** * Sets the event manager for the subscriber. * * @param Event_Manager $event_manager Event Manager instance. */ public function set_event_manager( Event_Manager $event_manager ) { $this->event_manager = $event_manager; } /** * Registers the action for each public taxonomy * * @since 3.5.5 * * @return void */ public function register_terms_row_action() { $taxonomies = get_taxonomies( [ 'public' => true, 'publicly_queryable' => true, ] ); foreach ( $taxonomies as $taxonomy ) { $this->event_manager->add_callback( "{$taxonomy}_row_actions", [ $this, 'add_purge_term_link' ], 10, 2 ); } } /** * Adds a link "Purge this cache" in the terms list table * * @param array $actions An array of action links to be displayed. * @param WP_Term $term Term object. * * @return array */ public function add_purge_term_link( $actions, $term ) { if ( ! current_user_can( 'rocket_purge_terms' ) ) { return $actions; } $url = wp_nonce_url( admin_url( "admin-post.php?action=purge_cache&type=term-{$term->term_id}&taxonomy={$term->taxonomy}" ), "purge_cache_term-{$term->term_id}" ); $actions['rocket_purge'] = sprintf( '<a href="%1$s">%2$s</a>', $url, __( 'Clear this cache', 'rocket' ) ); return $actions; } /** * Displays the notice for advanced-cache.php permissions * * @since 3.6 * * @return void */ public function notice_advanced_cache_permissions() { $this->advanced_cache->notice_permissions(); } /** * Set WP_CACHE constant to true if needed * * @since 3.6.1 * * @return void */ public function maybe_set_wp_cache() { $this->wp_cache->maybe_set_wp_cache(); } /** * Displays the notice for wp-config.php permissions * * @since 3.6.1 * * @return void */ public function notice_wp_config_permissions() { $this->wp_cache->notice_wp_config_permissions(); } /** * Adds a Site Health check for the WP_CACHE constant value * * @since 3.6.1 * * @param array $tests An array of tests to perform. * @return array */ public function add_wp_cache_status_test( $tests ) { return $this->wp_cache->add_wp_cache_status_test( $tests ); } /** * Regenerate configs. * * @return void */ public function regenerate_configs() { rocket_generate_advanced_cache_file(); flush_rocket_htaccess(); rocket_generate_config_file(); } /** * Delete old config files. * * @return void */ public function delete_old_configs() { $configs = []; if ( is_multisite() ) { foreach ( get_sites( [ 'fields' => 'ids' ] ) as $site_id ) { switch_to_blog( $site_id ); $configs[] = $this->generate_config_path(); restore_current_blog(); } } else { $configs[] = $this->generate_config_path(); } $contents = $this->filesystem->dirlist( WP_ROCKET_CONFIG_PATH ); foreach ( $contents as $content ) { $content = WP_ROCKET_CONFIG_PATH . $content['name']; if ( ! preg_match( '#\.php$#', $content ) || ! $this->filesystem->is_file( $content ) || in_array( $content, $configs, true ) ) { continue; } if ( false === strpos( $this->filesystem->get_contents( $content ), '$rocket_cookie_hash' ) ) { continue; } $this->filesystem->delete( $content ); } } /** * Generate the path to the config for the current website. * * @return string */ protected function generate_config_path() { $file = get_rocket_parse_url( untrailingslashit( home_url() ) ); $file['path'] = ( ! empty( $file['path'] ) ) ? str_replace( '/', '.', untrailingslashit( $file['path'] ) ) : ''; return WP_ROCKET_CONFIG_PATH . strtolower( $file['host'] ) . $file['path'] . '.php'; } /** * Clear cache. * * @param string $current_url current URL from the website. * @param string $old_url old URL from the website. * * @return void */ public function clear_cache( string $current_url, string $old_url ) { rocket_clean_files( [ $old_url, $current_url ], null, false ); } /** * Regenerate the advanced cache file on update * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function on_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.16', '>=' ) ) { return; } rocket_generate_advanced_cache_file(); } /** * Regenerate config files after importing settings when do_caching_mobile_files is disabled and cache_mobile is enabled. * * @param boolean $regenerate_configs Returns whether to regenerate config. * @return void */ public function maybe_regenerate_advanced_cache( bool $regenerate_configs ): void { if ( ! $regenerate_configs ) { return; } $this->regenerate_configs(); } } Engine/Saas/ServiceProvider.php 0000644 00000003256 15174677547 0012521 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Saas; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Saas\Admin\{AdminBar, Clean, Notices, Subscriber}; class ServiceProvider extends AbstractServiceProvider { /** * The provides array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ 'saas_admin_bar', 'saas_clean', 'saas_notices', 'saas_admin_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'saas_admin_bar', Adminbar::class ) ->addArguments( [ 'options', 'rucss_optimize_context', new StringArgument( $this->getContainer()->get( 'template_path' ) . '/settings' ), ] ); $this->getContainer()->add( 'saas_clean', Clean::class ); $this->getContainer()->add( 'saas_notices', Notices::class ) ->addArguments( [ 'options', 'beacon', ] ); $this->getContainer()->addShared( 'saas_admin_subscriber', Subscriber::class ) ->addArguments( [ 'saas_admin_bar', 'saas_clean', 'saas_notices', ] ); } } Engine/Saas/Admin/Subscriber.php 0000644 00000007245 15174677547 0012543 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Saas\Admin; use WP_Admin_Bar; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * AdminBar instance * * @var AdminBar */ private $admin_bar; /** * Clean instance * * @var Clean */ private $clean; /** * Notices instance * * @var Notices */ private $notices; /** * Constructor * * @param AdminBar $admin_bar AdminBar instance. * @param Clean $clean Clean instance. * @param Notices $notices Notices instance. */ public function __construct( $admin_bar, $clean, $notices ) { $this->admin_bar = $admin_bar; $this->clean = $clean; $this->notices = $notices; } /** * Array of events this subscriber listens to * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_admin_bar_items' => [ [ 'add_clean_saas_menu_item' ], [ 'add_clean_url_menu_item' ], ], 'admin_post_rocket_clean_saas' => 'clean_saas', 'admin_post_rocket_clean_saas_url' => 'clean_url_saas', 'admin_notices' => [ [ 'clean_saas_result' ], [ 'display_processing_notice' ], [ 'display_success_notice' ], [ 'display_wrong_license_notice' ], [ 'display_saas_error_notice' ], ], 'rocket_localize_admin_script' => 'add_localize_script_data', 'rocket_dashboard_actions' => 'display_dashboard_button', ]; } /** * Add clean SaaS data to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * * @return void */ public function add_clean_saas_menu_item( WP_Admin_Bar $wp_admin_bar ) { $this->admin_bar->add_clean_saas_menu_item( $wp_admin_bar ); } /** * Add clean SaaS URL data to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * * @return void */ public function add_clean_url_menu_item( WP_Admin_Bar $wp_admin_bar ) { $this->admin_bar->add_clean_url_menu_item( $wp_admin_bar ); } /** * Truncate SaaS tables when clicking on the dashboard button * * @return void */ public function clean_saas() { $this->clean->clean_saas(); } /** * Clean SaaS for the current URL. * * @return void */ public function clean_url_saas() { $this->clean->clean_url_saas(); } /** * Show admin notice after clearing SaaS tables. * * @return void */ public function clean_saas_result() { $this->notices->clean_saas_result(); } /** * Displays the SaaS currently processing notice * * @return void */ public function display_processing_notice() { $this->notices->display_processing_notice(); } /** * Displays the SaaS success notice * * @return void */ public function display_success_notice() { $this->notices->display_success_notice(); } /** * Display a notification on wrong license. * * @return void */ public function display_wrong_license_notice() { $this->notices->display_wrong_license_notice(); } /** * Display an error notice when the connection to the server fails * * @return void */ public function display_saas_error_notice() { $this->notices->display_saas_error_notice(); } /** * Adds the notice end time to WP Rocket localize script data * * @param array $data Localize script data. * * @return array */ public function add_localize_script_data( $data ): array { return $this->notices->add_localize_script_data( $data ); } /** * Display the dashboard button to clean SaaS features * * @return void */ public function display_dashboard_button() { $this->admin_bar->display_dashboard_button(); } } Engine/Saas/Admin/Clean.php 0000644 00000002232 15174677547 0011451 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Saas\Admin; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Settings\DataClearingTrait; class Clean { use DataClearingTrait; /** * Truncate SaaS tables when clicking on the dashboard button * * @return void */ public function clean_saas() { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_clean_saas' ) ) { wp_nonce_ays( '' ); } /** * Filters the value of the SaaS clean * * @since 3.16 * * @param array $clean An array containing the status and message. */ $clean = wpm_apply_filters_typed( 'array', 'rocket_saas_clean_all', [] ); $this->clean_data( $clean, 'rocket_saas_clean_message' ); } /** * Clean SaaS for the current URL. * * @return void */ public function clean_url_saas() { check_admin_referer( 'rocket_clean_saas_url' ); /** * Fires when cleaning a single URL for the Saas * * @since 3.16 */ do_action( 'rocket_saas_clean_url' ); wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } } Engine/Saas/Admin/AdminBar.php 0000644 00000006135 15174677547 0012112 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Saas\Admin; use WP_Admin_Bar; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Settings\AdminBarMenuTrait; use WP_Rocket\Engine\Common\Context\ContextInterface; class AdminBar extends Abstract_Render { use AdminBarMenuTrait; /** * Options data instance. * * @var Options_data */ private $options; /** * RUCSS optimize url context. * * @var ContextInterface */ private $rucss_url_context; /** * Constructor * * @param Options_Data $options Options data instance. * @param ContextInterface $rucss_url_context RUCSS optimize url context. * @param string $template_path Template path. */ public function __construct( Options_Data $options, ContextInterface $rucss_url_context, $template_path ) { parent::__construct( $template_path ); $this->options = $options; $this->rucss_url_context = $rucss_url_context; } /** * Add clean SaaS data to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * * @return void */ public function add_clean_saas_menu_item( $wp_admin_bar ) { $title = __( 'Clear Used CSS', 'rocket' ); $action = 'rocket_clean_saas'; if ( 'local' === wp_get_environment_type() ) { return; } if ( 'local' === wp_get_environment_type() && (bool) $this->options->get( 'remove_unused_css', 0 ) ) { return; } if ( ! (bool) $this->options->get( 'remove_unused_css', 0 ) ) { return; } $this->add_menu_to_admin_bar( $wp_admin_bar, 'clean-saas', $title, $action ); } /** * Add clean SaaS URL data to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * * @return void */ public function add_clean_url_menu_item( WP_Admin_Bar $wp_admin_bar ) { if ( 'local' === wp_get_environment_type() && $this->rucss_url_context->is_allowed() ) { return; } global $post; /** * Filters the rocket `clear used css of this url` option on admin bar menu. * * @since 3.12.1 * * @param bool $should_skip Should skip adding `clear used css of this url` option in admin bar. * @param type $post Post object. */ if ( wpm_apply_filters_typed( 'boolean', 'rocket_skip_admin_bar_clear_used_css_option', false, $post ) ) { return; } $action = 'rocket_clean_saas_url'; $title = __( 'Clear Used CSS of this URL', 'rocket' ); $this->add_url_menu_item_to_admin_bar( $wp_admin_bar, 'clear-saas-url', $title, $action, $this->rucss_url_context->is_allowed() ); } /** * Display the dashboard button to clean SaaS features * * @return void */ public function display_dashboard_button() { if ( 'local' === wp_get_environment_type() && $this->rucss_url_context->is_allowed() ) { return; } $this->dashboard_button( $this->rucss_url_context->is_allowed(), __( 'Used CSS', 'rocket' ), esc_html__( 'Clear', 'rocket' ), 'rocket_clean_saas', esc_html__( 'This action will clear the used CSS files.', 'rocket' ) ); } } Engine/Saas/Admin/Notices.php 0000644 00000020646 15174677547 0012044 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Saas\Admin; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Engine\Common\Context\ContextInterface; class Notices { /** * Options Data instance * * @var Options_Data */ private $options; /** * Beacon instance. * * @var Beacon */ private $beacon; /** * Constructor * * @param Options_Data $options Options_Data instance. * @param Beacon $beacon Beacon instance. */ public function __construct( Options_Data $options, Beacon $beacon ) { $this->options = $options; $this->beacon = $beacon; } /** * Show admin notice after clearing SaaS tables. * * @return void */ public function clean_saas_result() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( ! $this->options->get( 'remove_unused_css', 0 ) ) { return; } $response = get_transient( 'rocket_saas_clean_message' ); if ( ! $response ) { return; } delete_transient( 'rocket_saas_clean_message' ); rocket_notice_html( $response ); } /** * Displays the SaaS currently processing notice * * @return void */ public function display_processing_notice() { if ( $this->has_saas_error_notice() ) { return; } if ( ! $this->can_display_notice() ) { return; } $transient = get_transient( 'rocket_saas_processing' ); if ( false === $transient ) { return; } $current_time = time(); if ( $transient < $current_time ) { return; } $remaining = $transient - $current_time; $message = sprintf( // translators: %1$s = plugin name, %2$s = number of seconds. __( '%1$s: Please wait %2$s seconds. The Remove Unused CSS service is processing your pages, the plugin is optimizing LCP and the images above the fold.', 'rocket' ), '<strong>WP Rocket</strong>', '<span id="rocket-rucss-timer">' . $remaining . '</span>' ); rocket_notice_html( [ 'status' => 'info', 'message' => $message, 'id' => 'rocket-notice-saas-processing', ] ); } /** * Displays the SaaS success notice * * @return void */ public function display_success_notice() { if ( ! $this->can_display_notice() ) { return; } if ( $this->has_saas_error_notice() ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( 'saas_success_notice', (array) $boxes, true ) ) { return; } $transient = get_transient( 'rocket_saas_processing' ); $class = ''; if ( false !== $transient || ( ! $this->options->get( 'remove_unused_css', 0 ) ) ) { $class = 'hidden'; } $message = sprintf( // translators: %1$s = plugin name, %2$s = number of URLs, %3$s = number of seconds. __( '%1$s: The LCP element has been optimized, and the images above the fold were excluded from lazyload. The Used CSS of your homepage has been processed. WP Rocket will continue to generate Used CSS for up to %2$s URLs per %3$s second(s).', 'rocket' ), '<strong>WP Rocket</strong>', rocket_apply_filter_and_deprecated( 'rocket_saas_pending_jobs_cron_rows_count', [ 100 ], '3.16', 'rocket_rucss_pending_jobs_cron_rows_count' ), rocket_apply_filter_and_deprecated( 'rocket_saas_pending_jobs_cron_interval', [ MINUTE_IN_SECONDS ], '3.16', 'rocket_rucss_pending_jobs_cron_interval' ) ); if ( ! $this->options->get( 'manual_preload', 0 ) ) { $message .= ' ' . sprintf( // translators: %1$s = opening link tag, %2$s = closing link tag. __( 'We suggest enabling %1$sPreload%2$s for the fastest results.', 'rocket' ), '<a href="#preload">', '</a>' ); } $beacon = $this->beacon->get_suggest( 'async_opti' ); $message .= '<br>' . sprintf( // translators: %1$s = opening link tag, %2$s = closing link tag. __( 'To learn more about the process check our %1$sdocumentation%2$s.', 'rocket' ), '<a href="' . esc_url( $beacon['url'] ) . '" data-beacon-article="' . esc_attr( $beacon['id'] ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); rocket_notice_html( [ 'message' => $message, 'dismissible' => $class, 'id' => 'rocket-notice-saas-success', 'dismiss_button' => 'saas_success_notice', 'dismiss_button_class' => 'button-primary', ] ); } /** * Adds the notice end time to WP Rocket localize script data * * @since 3.11 * * @param array $data Localize script data. * * @return array */ public function add_localize_script_data( array $data ): array { if ( ! $this->options->get( 'remove_unused_css', 0 ) ) { return $data; } $transient = get_transient( 'rocket_saas_processing' ); if ( false === $transient ) { return $data; } $data['notice_end_time'] = $transient; $data['cron_disabled'] = rocket_get_constant( 'DISABLE_WP_CRON', false ); return $data; } /** * Display a notification on wrong license. * * @return void */ public function display_wrong_license_notice() { $transient = get_option( 'wp_rocket_no_licence' ); if ( ! $transient ) { return; } if ( ! $this->can_display_notice() ) { return; } $main_message = __( "We couldn't generate the used CSS because you're using a nulled version of WP Rocket. You need an active license to use the Remove Unused CSS feature and further improve your website's performance.", 'rocket' ); $cta_message = sprintf( // translators: %1$s = promo percentage. __( 'Click here to get a WP Rocket single license at %1$s off!', 'rocket' ), '10%%' ); $message = sprintf( // translators: %1$s = plugin name, %2$s = opening anchor tag, %3$s = closing anchor tag. "%1\$s: <p>$main_message</p>%2\$s$cta_message%3\$s", '<strong>WP Rocket</strong>', '<a href="https://wp-rocket.me/?add-to-cart=191&coupon_code=iamnotapirate10" class="button button-primary" rel="noopener noreferrer" target="_blank">', '</a>' ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, 'id' => 'rocket-notice-rucss-wrong-licence', ] ); } /** * Display an error notice when the connection to the server fails * * @return void */ public function display_saas_error_notice() { if ( ! $this->has_saas_error_notice() ) { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( 'rucss_saas_error_notice', (array) $boxes, true ) ) { unset( $boxes['rucss_saas_error_notice'] ); update_user_meta( get_current_user_id(), 'rocket_boxes', $boxes ); } return; } if ( ! $this->can_display_notice() ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( 'saas_error_notice', (array) $boxes, true ) ) { return; } $firewall_beacon = $this->beacon->get_suggest( 'rucss_firewall_ips' ); $main_message = sprintf( // translators: %1$s = <a> open tag, %2$s = </a> closing tag. __( 'It seems a security plugin or the server\'s firewall prevents WP Rocket from accessing the SaaS features. IPs listed %1$shere in our documentation%2$s should be added to your allowlists:', 'rocket' ), '<a href="' . esc_url( $firewall_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $firewall_beacon['id'] ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); $security_message = __( '- In the security plugin, if you are using one', 'rocket' ); $firewall_message = __( "- In the server's firewall. Your host can help you with this", 'rocket' ); $message = "<strong>WP Rocket</strong>: $main_message<ul><li>$security_message</li><li>$firewall_message</li></ul>"; rocket_notice_html( [ 'status' => 'error', 'message' => $message, 'dismissible' => '', 'id' => 'rocket-notice-rucss-error-http', 'dismiss_button' => 'saas_error_notice', 'dismiss_button_class' => 'button-primary', ] ); } /** * Checks if we can display the SaaS notices * * @return bool */ private function can_display_notice(): bool { $screen = get_current_screen(); if ( isset( $screen->id ) && 'settings_page_wprocket' !== $screen->id ) { return false; } if ( ! current_user_can( 'rocket_manage_options' ) ) { return false; } return true; } /** * Is the error notice present. * * @return bool */ private function has_saas_error_notice() { return (bool) get_transient( 'wp_rocket_rucss_errors_count' ); } } Engine/Plugin/InformationSubscriber.php 0000644 00000014533 15174677547 0014266 0 ustar 00 <?php namespace WP_Rocket\Engine\Plugin; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Error; /** * Manages the plugin information. */ class InformationSubscriber implements Subscriber_Interface { use UpdaterApiTools; const INFORMATION_ENDPOINT = 'https://api.wp-rocket.me/plugin_information.php'; /** * Plugin slug. * * @var string */ private $plugin_slug; /** * An ID to use when a API request fails. * * @var string */ protected $request_error_id = 'plugins_api_failed'; /** * Constructor * * @param array $args { Required arguments to populate the class properties. * @type string $plugin_file Full path to the plugin. * } */ public function __construct( $args ) { if ( isset( $args['plugin_file'] ) ) { $this->plugin_slug = $this->get_plugin_slug( $args['plugin_file'] ); } } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'plugins_api' => [ 'exclude_rocket_from_wp_info', 10, 3 ], 'plugins_api_result' => [ [ 'add_rocket_info', 10, 3 ], [ 'add_plugins_to_result', 11, 3 ], ], 'rocket_wp_tested_version' => 'add_wp_tested_version', ]; } /** * Don’t ask for plugin info to the repository. * * @param false|object|array $bool The result object or array. Default false. * @param string $action The type of information being requested from the Plugin Install API. * @param object $args Plugin API arguments. * @return false|object|array Empty object if slug is WP Rocket, default value otherwise. */ public function exclude_rocket_from_wp_info( $bool, $action, $args ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.boolFound if ( ! $this->is_requesting_rocket_info( $action, $args ) ) { return $bool; } return new \stdClass(); } /** * Insert WP Rocket plugin info. * * @param object|WP_Error $res Response object or WP_Error. * @param string $action The type of information being requested from the Plugin Install API. * @param object $args Plugin API arguments. * @return object|WP_Error Updated response object or WP_Error. */ public function add_rocket_info( $res, $action, $args ) { if ( ! $this->is_requesting_rocket_info( $action, $args ) || empty( $res->external ) ) { return $res; } return $this->get_plugin_information(); } /** * Adds the WP tested version value from our API * * @param string $wp_tested_version WP tested version. * * @return string */ public function add_wp_tested_version( $wp_tested_version ): string { $info = $this->get_plugin_information(); if ( empty( $info->tested ) ) { return $wp_tested_version; } return $info->tested; } /** * Tell if requesting WP Rocket plugin info. * * @param string $action The type of information being requested from the Plugin Install API. * @param object $args Plugin API arguments. * @return bool */ private function is_requesting_rocket_info( $action, $args ) { return ( 'query_plugins' === $action || 'plugin_information' === $action ) && isset( $args->slug ) && $args->slug === $this->plugin_slug; } /** * Gets the plugin information data * * @return object|WP_Error */ private function get_plugin_information() { $response = wp_remote_get( self::INFORMATION_ENDPOINT ); if ( is_wp_error( $response ) ) { return $this->get_request_error( $response->get_error_message() ); } $res = maybe_unserialize( wp_remote_retrieve_body( $response ) ); $code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $code || ! ( is_object( $res ) || is_array( $res ) ) ) { return $this->get_request_error( wp_remote_retrieve_body( $response ) ); } return $res; } /** * Filter plugin fetching API results to inject our plugins * * @param object|WP_Error $result Response object or WP_Error. * @param string $action The type of information being requested from the Plugin Install API. * @param object $args Plugin API arguments. * * @return object|WP_Error */ public function add_plugins_to_result( $result, $action, $args ) { if ( ! $this->can_add_plugins( $result, $args ) ) { return $result; } $plugins = [ 'uk-cookie-consent' => 'uk-cookie-consent/uk-cookie-consent.php', 'backwpup' => 'backwpup/backwpup.php', 'seo-by-rank-math' => 'seo-by-rank-math/rank-math.php', 'imagify' => 'imagify/imagify.php', ]; // grab all slugs from the api results. $result_slugs = wp_list_pluck( $result->plugins, 'slug' ); foreach ( $plugins as $slug => $path ) { if ( is_plugin_active( $path ) || is_plugin_active_for_network( $path ) ) { continue; } if ( in_array( $slug, $result_slugs, true ) ) { foreach ( $result->plugins as $index => $plugin ) { if ( is_object( $plugin ) ) { $plugin = (array) $plugin; } if ( $slug === $plugin['slug'] ) { $move = $plugin; unset( $result->plugins[ $index ] ); array_unshift( $result->plugins, $move ); } } continue; } $plugin_data = $this->get_plugin_data( $slug ); if ( empty( $plugin_data ) ) { continue; } array_unshift( $result->plugins, $plugin_data ); } return $result; } /** * Checks if we can add plugins to the results * * @param object|WP_error $result Response object or WP_Error. * @param object $args Plugin API arguments. * * @return bool */ private function can_add_plugins( $result, $args ) { if ( is_wp_error( $result ) ) { return false; } if ( empty( $args->browse ) ) { return false; } if ( 'featured' !== $args->browse && 'recommended' !== $args->browse && 'popular' !== $args->browse ) { return false; } if ( ! isset( $result->info['page'] ) || 1 < $result->info['page'] ) { return false; } return true; } /** * Returns plugin data * * @param string $slug Plugin slug. * * @return array|object */ private function get_plugin_data( string $slug ) { $query_args = [ 'slug' => $slug, 'fields' => [ 'icons' => true, 'active_installs' => true, 'short_description' => true, 'group' => true, ], ]; $plugin_data = plugins_api( 'plugin_information', $query_args ); if ( is_wp_error( $plugin_data ) ) { return []; } return $plugin_data; } } Engine/Plugin/ServiceProvider.php 0000644 00000004521 15174677547 0013064 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Plugin; use WP_Rocket\Dependencies\League\Container\Argument\Literal\ArrayArgument; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for the WP Rocket updates. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'plugin_renewal_notice', 'plugin_updater_common_subscriber', 'plugin_information_subscriber', 'plugin_updater_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'plugin_renewal_notice', RenewalNotice::class ) ->addArguments( [ 'user', new StringArgument( $this->getContainer()->get( 'template_path' ) . '/plugins/' ), ] ); $this->getContainer()->addShared( 'plugin_updater_common_subscriber', UpdaterApiCommonSubscriber::class ) ->addArgument( new ArrayArgument( [ 'site_url' => home_url(), 'plugin_version' => WP_ROCKET_VERSION, 'settings_slug' => WP_ROCKET_SLUG, 'settings_nonce_key' => WP_ROCKET_PLUGIN_SLUG, 'plugin_options' => $this->getContainer()->get( 'options' ), ] ) ); $this->getContainer()->addShared( 'plugin_information_subscriber', InformationSubscriber::class ) ->addArgument( new ArrayArgument( [ 'plugin_file' => WP_ROCKET_FILE, ] ) ); $this->getContainer()->addShared( 'plugin_updater_subscriber', UpdaterSubscriber::class ) ->addArguments( [ 'plugin_renewal_notice', new ArrayArgument( [ 'plugin_file' => WP_ROCKET_FILE, 'plugin_version' => WP_ROCKET_VERSION, 'vendor_url' => WP_ROCKET_WEB_MAIN, 'icons' => [ '2x' => WP_ROCKET_ASSETS_IMG_URL . 'icon-256x256.png', '1x' => WP_ROCKET_ASSETS_IMG_URL . 'icon-128x128.png', ], ] ), ] ); } } Engine/Plugin/UpdaterApiCommonSubscriber.php 0000644 00000007432 15174677547 0015210 0 ustar 00 <?php namespace WP_Rocket\Engine\Plugin; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Manages common hooks for the plugin updater. */ class UpdaterApiCommonSubscriber implements Subscriber_Interface { const API_HOST = 'api.wp-rocket.me'; /** * URL to the site’s home. * * @var string */ private $site_url; /** * Current version of the plugin. * * @var string */ private $plugin_version; /** * Key slug used when submitting new settings (POST). * * @var string */ private $settings_slug; /** * The key (1st part of the action) used for the nonce field used on the settings page. It is also used in the page URL. * * @var string */ private $settings_nonce_key; /** * Options instance. * * @var \WP_Rocket\Admin\Options */ private $plugin_options; /** * Constructor * * @param array $args { * Required arguments to populate the class properties. * * @type string $site_url URL to the site’s home. * @type string $plugin_version Current version of the plugin. * @type string $settings_slug Key slug used when submitting new settings (POST). * @type string $settings_nonce_key The key (1st part of the action) used for the nonce field used on the settings page. It is also used in the page URL. * @type Options $plugin_options Options instance. * } */ public function __construct( $args ) { foreach ( [ 'site_url', 'plugin_version', 'settings_slug', 'settings_nonce_key', 'plugin_options' ] as $setting ) { if ( isset( $args[ $setting ] ) ) { $this->$setting = $args[ $setting ]; } } } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'http_request_args' => [ 'maybe_set_rocket_user_agent', 10, 2 ], ]; } /** * Force our user agent header when we hit our URLs. * * @param array $request An array of request arguments. * @param string $url Requested URL. * @return array An array of requested arguments */ public function maybe_set_rocket_user_agent( $request, $url ) { if ( ! is_string( $url ) ) { // @phpstan-ignore-line GH #7042 - $url variable may be change by other plugins to something else than string. return $request; } if ( strpos( $url, self::API_HOST ) !== false ) { $request['user-agent'] = sprintf( '%s;%s', $request['user-agent'], $this->get_rocket_user_agent() ); } return $request; } /** * Get the user agent to use when requesting the API. * * @return string WP Rocket user agent */ public function get_rocket_user_agent() { $consumer_key = $this->get_current_option( 'consumer_key' ); $consumer_email = $this->get_current_option( 'consumer_email' ); $php_version = preg_replace( '@^(\d+\.\d+).*@', '\1', phpversion() ); return sprintf( 'WP-Rocket|%s|%s|%s|%s|%s;', $this->plugin_version, $consumer_key, $consumer_email, esc_url( $this->site_url ), $php_version ); } /** * Get a plugin option. If the value is currently being posted through the settings page, it is returned instead of the one stored in the database. * * @param string $field_name Name of a plugin option. * @return string */ protected function get_current_option( $field_name ) { if ( current_user_can( 'rocket_manage_options' ) && wp_verify_nonce( filter_input( INPUT_POST, '_wpnonce' ), $this->settings_nonce_key . '-options' ) ) { $posted = filter_input( INPUT_POST, $this->settings_slug, FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); if ( ! empty( $posted[ $field_name ] ) ) { // The value has been posted through the settings page. return sanitize_text_field( $posted[ $field_name ] ); } } $option_value = $this->plugin_options->get( $field_name ); if ( $option_value && is_string( $option_value ) ) { return $option_value; } return ''; } } Engine/Plugin/RenewalNotice.php 0000644 00000005473 15174677547 0012517 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Plugin; use WP_Rocket\Abstract_Render; use WP_Rocket\Engine\License\API\User; class RenewalNotice extends Abstract_Render { /** * User instance * * @var User */ private $user; /** * Constructor * * @param User $user User instance. * @param string $template_path Template path. */ public function __construct( User $user, string $template_path ) { parent::__construct( $template_path ); $this->user = $user; } /** * Display the renewal notice on plugins page * * @param string $version Latest version number. * * @return void */ public function renewal_notice( $version ) { if ( ! $this->user->is_license_expired() ) { return; } if ( ! $this->is_major_version_available( $version ) ) { return; } $major = $this->extract_major( $version ); $data = [ 'version' => $major, 'release_url' => 'https://wp-rocket.me/blog/wp-rocket-' . str_replace( '.', '-', $major ) . '/', 'renew_url' => $this->user->get_renewal_url(), ]; echo $this->generate( 'update-renewal-expired-notice', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Adds styles for expired banner * * @param string $version Latest version number. * * @return void */ public function add_expired_styles( $version ) { if ( ! $this->user->is_license_expired() ) { return; } if ( ! $this->is_major_version_available( $version ) ) { return; } echo '<style>.plugins tr[data-slug=wp-rocket] th, .plugins tr[data-slug=wp-rocket] td {box-shadow: none !important;}.notice.wp-rocket-update{border-color:#d63638;background-color:#fbf9e8;}.plugin-update .notice.wp-rocket-update a{color:#2782ad;}.wp-rocket-update p::before{display: inline-block;font: normal 20px/1 dashicons;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;vertical-align: top;color: #d63638;content: "\f534";}@media screen and (max-width: 782px){.plugins tr[data-slug=wp-rocket].active + .plugin-update-tr::before {background-color: #f0f6fc;border-left: 4px solid #72aee6;}}</style>'; } /** * Checks if a new major version is available * * @param string $version Version available from the API. * * @return bool */ private function is_major_version_available( $version ): bool { $current_version = rocket_get_constant( 'WP_ROCKET_VERSION', '' ); $current_major = $this->extract_major( $current_version ); $version_major = $this->extract_major( $version ); return version_compare( $current_major, $version_major, '<' ); } /** * Extracts the major version number from the provided version * * @param string $version Version number. * * @return string */ private function extract_major( $version ): string { $parts = explode( '.', $version ); return $parts[0] . '.' . $parts[1]; } } Engine/Plugin/UpdaterSubscriber.php 0000644 00000043060 15174677547 0013402 0 ustar 00 <?php namespace WP_Rocket\Engine\Plugin; use Plugin_Upgrader; use Plugin_Upgrader_Skin; use WP_Error; use WP_Rocket\Event_Management\{Event_Manager, Event_Manager_Aware_Subscriber_Interface}; /** * Manages the plugin updates. */ class UpdaterSubscriber implements Event_Manager_Aware_Subscriber_Interface { use UpdaterApiTools; const UPDATE_ENDPOINT = 'https://api.wp-rocket.me/check_update.php'; /** * Full path to the plugin. * * @var string */ private $plugin_file; /** * Current version of the plugin. * * @var string */ private $plugin_version; /** * URL to the plugin provider. * * @var string */ private $vendor_url; /** * A list of plugin’s icon URLs. * * @var array { * @type string $2x URL to the High-DPI size (png or jpg). Optional. * @type string $1x URL to the normal icon size (png or jpg). Mandatory. * @type string $svg URL to the svg version of the icon. Optional. * } * @see https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons */ private $icons; /** * An ID to use when a API request fails. * * @var string */ protected $request_error_id = 'rocket_update_failed'; /** * Name of the transient that caches the update data. * * @var string */ protected $cache_transient_name = 'wp_rocket_update_data'; /** * The WordPress Event Manager * * @var Event_Manager */ protected $event_manager; /** * RenewalNotice instance * * @var RenewalNotice */ private $renewal_notice; /** * Constructor * * @param RenewalNotice $renewal_notice RenewalNotice instance. * * @param array $args { Required arguments to populate the class properties. * @type string $plugin_file Full path to the plugin. * @type string $plugin_version Current version of the plugin. * @type string $vendor_url URL to the plugin provider. * } */ public function __construct( RenewalNotice $renewal_notice, $args ) { foreach ( [ 'plugin_file', 'plugin_version', 'vendor_url', 'icons' ] as $setting ) { if ( isset( $args[ $setting ] ) ) { $this->$setting = $args[ $setting ]; } } $this->renewal_notice = $renewal_notice; } /** * {@inheritdoc} * * @param Event_Manager $event_manager The WordPress Event Manager. */ public function set_event_manager( Event_Manager $event_manager ) { $this->event_manager = $event_manager; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'http_request_args' => [ 'exclude_rocket_from_wp_updates', 5, 2 ], 'pre_set_site_transient_update_plugins' => 'maybe_add_rocket_update_data', 'deleted_site_transient' => 'maybe_delete_rocket_update_data_cache', 'wp_rocket_loaded' => 'maybe_force_check', 'auto_update_plugin' => [ 'disable_auto_updates', 10, 2 ], 'admin_post_rocket_rollback' => 'rollback', 'upgrader_pre_install' => [ 'upgrade_pre_install_option', 10, 2 ], 'upgrader_post_install' => [ 'upgrade_post_install_option', 10, 2 ], 'after_plugin_row_wp-rocket/wp-rocket.php' => 'display_renewal_notice', 'admin_print_styles-plugins.php' => 'add_expired_styles', ]; } /** * When WP checks plugin versions against the latest versions hosted on WordPress.org, remove WPR from the list. * * @see wp_update_plugins() * * @param array $request An array of HTTP request arguments. * @param string $url The request URL. * @return array Updated array of HTTP request arguments. */ public function exclude_rocket_from_wp_updates( $request, $url ) { if ( ! is_string( $url ) ) { // @phpstan-ignore-line GH #7042 - $url variable may be change by other plugins to something else than string. return $request; } if ( ! preg_match( '@^https?://api.wordpress.org/plugins/update-check(/|\?|$)@', $url ) || empty( $request['body']['plugins'] ) ) { // Not a plugin update request. Stop immediately. return $request; } /** * Depending on the API version, the data can have several forms: * - Can be serialized or JSON encoded, * - Can be an object of arrays or an object of objects. */ $is_serialized = is_serialized( $request['body']['plugins'] ); $basename = plugin_basename( $this->plugin_file ); $edited = false; if ( $is_serialized ) { $plugins = maybe_unserialize( $request['body']['plugins'] ); } else { $plugins = json_decode( $request['body']['plugins'] ); } if ( ! empty( $plugins->plugins ) ) { if ( is_object( $plugins->plugins ) ) { if ( isset( $plugins->plugins->$basename ) ) { unset( $plugins->plugins->$basename ); $edited = true; } } elseif ( is_array( $plugins->plugins ) ) { if ( isset( $plugins->plugins[ $basename ] ) ) { unset( $plugins->plugins[ $basename ] ); $edited = true; } } } if ( ! empty( $plugins->active ) ) { $active_is_object = is_object( $plugins->active ); if ( $active_is_object || is_array( $plugins->active ) ) { foreach ( $plugins->active as $key => $plugin_basename ) { if ( $plugin_basename !== $basename ) { continue; } if ( $active_is_object ) { unset( $plugins->active->$key ); } else { unset( $plugins->active[ $key ] ); } $edited = true; break; } } } if ( $edited ) { if ( $is_serialized ) { $request['body']['plugins'] = maybe_serialize( $plugins ); } else { $request['body']['plugins'] = wp_json_encode( $plugins ); } } return $request; } /** * Add WPR update data to the "WP update" transient. * * @param \stdClass|array $transient_value New value of site transient. * @return \stdClass */ public function maybe_add_rocket_update_data( $transient_value ) { if ( defined( 'WP_INSTALLING' ) ) { return $transient_value; } // Get the remote version data. $remote_data = $this->get_cached_latest_version_data(); if ( is_wp_error( $remote_data ) ) { return $transient_value; } // Make sure the transient value is well formed. if ( ! is_object( $transient_value ) ) { $transient_value = new \stdClass(); } if ( empty( $transient_value->response ) ) { $transient_value->response = []; } if ( empty( $transient_value->checked ) ) { $transient_value->checked = []; } // If a newer version is available, add the update. if ( version_compare( $this->plugin_version, $remote_data->new_version, '<' ) ) { $transient_value->response[ $remote_data->plugin ] = $remote_data; } $transient_value->checked[ $remote_data->plugin ] = $this->plugin_version; return $transient_value; } /** * Delete WPR update data cache when the "WP update" transient is deleted. * * @param string $transient_name Deleted transient name. */ public function maybe_delete_rocket_update_data_cache( $transient_name ) { if ( 'update_plugins' === $transient_name ) { $this->delete_rocket_update_data_cache(); } } /** * If the `rocket_force_update` query arg is set, force WP to refresh the list of plugins to update. */ public function maybe_force_check() { if ( is_string( filter_input( INPUT_GET, 'rocket_force_update' ) ) ) { delete_site_transient( 'update_plugins' ); } } /** * Disable auto-updates for WP Rocket * * @param bool|null $update Whether to update. The value of null is internally used to detect whether nothing has hooked into this filter. * @param object $item The update offer. * @return bool|null */ public function disable_auto_updates( $update, $item ) { if ( isset( $item->plugin ) && ( 'wp-rocket/wp-rocket.php' === $item->plugin ) ) { return false; } return $update; } /** * Get the latest WPR update data from our server. * * @return \stdClass|\WP_Error { * A \WP_Error object on failure. An object on success: * * @type string $slug The plugin slug. * @type string $plugin The plugin base name. * @type string $new_version The plugin new version. * @type string $url URL to the plugin provider. * @type string $package URL to the zip file of the new version. * @type array $icons { * A list of plugin’s icon URLs. * * @type string $2x URL to the High-DPI size (png or jpg). Optional. * @type string $1x URL to the normal icon size (png or jpg). Mandatory. * @type string $svg URL to the svg version of the icon. Optional. * } * } */ public function get_latest_version_data() { $request = wp_remote_get( self::UPDATE_ENDPOINT, [ 'timeout' => 30, ] ); if ( is_wp_error( $request ) ) { return $this->get_request_error( [ 'error_code' => $request->get_error_code(), 'response' => $request->get_error_message(), ] ); } $res = trim( wp_remote_retrieve_body( $request ) ); $code = wp_remote_retrieve_response_code( $request ); if ( 200 !== $code ) { /** * If the response doesn’t have a status 200: it is an error, or there is no new update. */ return $this->get_request_error( [ 'http_code' => $code, 'response' => $res, ] ); } /** * This will match: * - `2.3.4.5-beta1||1.2.3.4-beta2||||||||||||||||||||||||||||||||`: expired license. * - `2.3.4.5-beta1|https://wp-rocket.me/i-should-write-a-funny-thing-here/wp-rocket_1.2.3.4-beta2.zip|1.2.3.4-beta2`: valid license. */ if ( ! preg_match( '@^(?<stable_version>\d+(?:\.\d+){1,3}[^|]*)\|(?<package>(?:http.+\.zip)?)\|(?<user_version>\d+(?:\.\d+){1,3}[^|]*)(?:\|+)?$@', $res, $match ) ) { /** * If the response doesn’t have the right format, it is an error. */ return $this->get_request_error( $res ); } $obj = new \stdClass(); $obj->slug = $this->get_plugin_slug( $this->plugin_file ); $obj->plugin = plugin_basename( $this->plugin_file ); $obj->new_version = $match['user_version']; $obj->url = $this->vendor_url; $obj->package = $match['package']; $obj->stable_version = $match['stable_version']; /** * Filters the WP tested version value * * @since 3.10.7 * * @param string $wp_tested_version WP tested version value. */ $obj->tested = apply_filters( 'rocket_wp_tested_version', WP_ROCKET_WP_VERSION_TESTED ); if ( $this->icons && ! empty( $this->icons['1x'] ) ) { $obj->icons = $this->icons; } return $obj; } /** * Get the cached version of the latest WPR update data. * * @return \stdClass|\WP_Error { * A \WP_Error object on failure. An object on success: * * @type string $slug The plugin slug. * @type string $plugin The plugin base name. * @type string $new_version The plugin new version. * @type string $url URL to the plugin provider. * @type string $package URL to the zip file of the new version. * @type array $icons { * A list of plugin’s icon URLs. * * @type string $2x URL to the High-DPI size (png or jpg). Optional. * @type string $1x URL to the normal icon size (png or jpg). Mandatory. * @type string $svg URL to the svg version of the icon. Optional. * } * } */ public function get_cached_latest_version_data() { static $response; if ( isset( $response ) ) { // "force update" won’t bypass the static cache: only one http request by page load. return $response; } $force_update = is_string( filter_input( INPUT_GET, 'rocket_force_update' ) ); if ( ! $force_update ) { // No "force update": try to get the result from a transient. $response = get_site_transient( $this->cache_transient_name ); if ( $response && is_object( $response ) ) { // Got something in cache. return $response; } } // Get fresh data. $response = $this->get_latest_version_data(); $cache_duration = 12 * HOUR_IN_SECONDS; if ( is_wp_error( $response ) ) { $error_data = $response->get_error_data(); if ( ! empty( $error_data['error_code'] ) ) { // `wp_remote_get()` returned an internal error ('error_code' contains a WP_Error code ). $cache_duration = HOUR_IN_SECONDS; } elseif ( ! empty( $error_data['http_code'] ) && $error_data['http_code'] >= 400 ) { // We got a 4xx or 5xx HTTP error. $cache_duration = 2 * HOUR_IN_SECONDS; } } set_site_transient( $this->cache_transient_name, $response, $cache_duration ); return $response; } /** * Delete WP Rocket update data cache. */ public function delete_rocket_update_data_cache() { delete_site_transient( $this->cache_transient_name ); } /** * Do the rollback * * @since 2.4 */ public function rollback() { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_rollback' ) ) { wp_nonce_ays( '' ); } if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_nonce_ays( '' ); } /** * Fires before doing the rollback */ do_action( 'rocket_before_rollback' ); $plugin_transient = get_site_transient( 'update_plugins' ); $plugin_folder = plugin_basename( dirname( WP_ROCKET_FILE ) ); $plugin = $plugin_folder . '/' . basename( WP_ROCKET_FILE ); $plugin_transient->response[ $plugin ] = (object) [ 'slug' => $plugin_folder, 'new_version' => WP_ROCKET_LASTVERSION, 'url' => 'https://wp-rocket.me', 'package' => sprintf( 'https://api.wp-rocket.me/%s/wp-rocket_%s.zip', get_rocket_option( 'consumer_key' ), WP_ROCKET_LASTVERSION ), ]; $this->event_manager->remove_callback( 'pre_set_site_transient_update_plugins', [ $this, 'maybe_add_rocket_update_data' ] ); set_site_transient( 'update_plugins', $plugin_transient ); // @phpstan-ignore-next-line require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // translators: %s is the plugin name. $title = sprintf( __( '%s Update Rollback', 'rocket' ), WP_ROCKET_PLUGIN_NAME ); $nonce = 'upgrade-plugin_' . $plugin; $url = 'update.php?action=upgrade-plugin&plugin=' . rawurlencode( $plugin ); $upgrader_skin = new Plugin_Upgrader_Skin( compact( 'title', 'nonce', 'url', 'plugin' ) ); $upgrader = new Plugin_Upgrader( $upgrader_skin ); add_filter( 'update_plugin_complete_actions', [ $this, 'rollback_add_return_link' ] ); rocket_put_content( WP_CONTENT_DIR . '/advanced-cache.php', '' ); $upgrader->upgrade( $plugin ); wp_die( '', // translators: %s is the plugin name. esc_html( sprintf( __( '%s Update Rollback', 'rocket' ), WP_ROCKET_PLUGIN_NAME ) ), [ 'response' => 200, ] ); } /** * After a rollback has been done, replace the "return to" link by a link pointing to WP Rocket's tools page. * A link to the plugins page is kept in case the plugin is not reactivated correctly. * * @since 3.2.4 * * @param array $update_actions Array of plugin action links. * @return array The array of links where the "return to" link has been replaced. */ public function rollback_add_return_link( $update_actions ) { if ( ! isset( $update_actions['plugins_page'] ) ) { return $update_actions; } $update_actions['plugins_page'] = sprintf( /* translators: 1 and 3 are link openings, 2 is a link closing. */ __( '%1$sReturn to WP Rocket%2$s or %3$sgo to Plugins page%2$s', 'rocket' ), '<a href="' . esc_url( admin_url( 'options-general.php?page=' . WP_ROCKET_PLUGIN_SLUG ) . '#tools' ) . '" target="_parent">', '</a>', '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '" target="_parent">' ); return $update_actions; } /** * Set plugin option before upgrade. * * @param mixed $return The result of the upgrade process. * @param array $plugin The plugin data. * * @return mixed|WP_Error */ public function upgrade_pre_install_option( $return, $plugin = [] ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.returnFound if ( is_wp_error( $return ) || ! $plugin ) { return $return; } $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; if ( empty( $plugin ) || 'wp-rocket/wp-rocket.php' !== $plugin ) { return $return; } set_transient( 'wp_rocket_updating', true, MINUTE_IN_SECONDS ); return $return; } /** * Update plugin option after upgrade. * * @param mixed $return The result of the upgrade process. * @param array $plugin The plugin data. * * @return mixed|string|WP_Error */ public function upgrade_post_install_option( $return, $plugin = [] ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.returnFound if ( is_wp_error( $return ) || ! $plugin ) { return $return; } $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; if ( empty( $plugin ) || 'wp-rocket/wp-rocket.php' !== $plugin ) { return $return; } delete_transient( 'wp_rocket_updating' ); return $return; } /** * Displays Renewal notice on the plugins page * * @return void */ public function display_renewal_notice() { $latest_version_data = $this->get_cached_latest_version_data(); if ( is_wp_error( $latest_version_data ) ) { return; } if ( ! isset( $latest_version_data->stable_version ) ) { return; } $this->renewal_notice->renewal_notice( $latest_version_data->stable_version ); } /** * Adds styles for expired banner * * @return void */ public function add_expired_styles() { $latest_version_data = $this->get_cached_latest_version_data(); if ( is_wp_error( $latest_version_data ) ) { return; } if ( ! isset( $latest_version_data->stable_version ) ) { return; } $this->renewal_notice->add_expired_styles( $latest_version_data->stable_version ); } } Engine/Plugin/UpdaterApiTools.php 0000644 00000003021 15174677547 0013022 0 ustar 00 <?php namespace WP_Rocket\Engine\Plugin; use WP_Rocket\Logger\Logger; /** * Trait for the plugin updater. */ trait UpdaterApiTools { /** * Get a \WP_Error object to use when the request to WP Rocket’s server fails. * * @param mixed $data Error data to pass along the \WP_Error object. * @return \WP_Error */ protected function get_request_error( $data = [] ) { if ( ! is_array( $data ) ) { $data = [ 'response' => $data, ]; } Logger::debug( 'Error when contacting the API.', array_merge( [ 'Plugin Information' ], $data ) ); return new \WP_Error( $this->request_error_id, sprintf( // translators: %s is an URL. __( 'An unexpected error occurred. Something may be wrong with WP-Rocket.me or this server’s configuration. If you continue to have problems, <a href="%s">contact support</a>.', 'rocket' ), $this->get_support_url() ), $data ); } /** * Get support URL. * * @return string */ protected function get_support_url() { return rocket_get_external_url( 'support', [ 'utm_source' => 'wp_plugin', 'utm_medium' => 'wp_rocket', ] ); } /** * Get a plugin slug, given its full path. * * @param string $plugin_file Full path to the plugin. * @return string */ protected function get_plugin_slug( $plugin_file ) { $plugin_file = trim( $plugin_file, '/' ); $plugin_slug = explode( '/', $plugin_file ); $plugin_slug = end( $plugin_slug ); $plugin_slug = str_replace( '.php', '', $plugin_slug ); return $plugin_slug; } } Engine/WPRocketUninstall.php 0000644 00000016114 15174677547 0012104 0 ustar 00 <?php use WP_Rocket\Dependencies\BerlinDB\Database\Table; use WP_Rocket\Engine\Optimization\RUCSS\Database\Tables\UsedCSS; use WP_Rocket\Engine\Preload\Database\Tables\Cache; use WP_Rocket\Engine\Media\AboveTheFold\Database\Tables\AboveTheFold; use WP_Rocket\Engine\Optimization\LazyRenderContent\Database\Table\LazyRenderContent; use WP_Rocket\Engine\Media\PreloadFonts\Database\Table\PreloadFonts; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Table\PreconnectExternalDomains; /** * Manages the deletion of WP Rocket data and files on uninstall. */ class WPRocketUninstall { /** * Path to the cache folder. * * @var string */ private $cache_path; /** * Path to the config folder. * * @var string */ private $config_path; /** * WP Rocket options. * * @var array */ private $options = [ 'wp_rocket_settings', 'rocket_analytics_notice_displayed', 'rocketcdn_user_token', 'rocketcdn_process', 'wp_rocket_hide_deactivation_form', 'wp_rocket_last_base_url', 'wp_rocket_no_licence', 'wp_rocket_last_option_hash', 'wp_rocket_debug', 'wp_rocket_rocketcdn_old_url', 'plugin_family_dismiss_promote_imagify', ]; /** * WP Rocket transients. * * @var array */ private $transients = [ 'wp_rocket_customer_data', 'rocket_notice_missing_tags', 'rocket_clear_cache', 'rocket_check_key_errors', 'rocket_send_analytics_data', 'rocket_critical_css_generation_process_running', 'rocket_critical_css_generation_process_complete', 'rocket_critical_css_generation_triggered', 'rocketcdn_status', 'rocketcdn_pricing', 'rocketcdn_purge_cache_response', 'rocket_cloudflare_ips', 'rocket_cloudflare_is_api_keys_valid', 'rocket_preload_triggered', 'rocket_preload_complete', 'rocket_preload_complete_time', 'rocket_preload_errors', 'rocket_database_optimization_process', 'rocket_database_optimization_process_complete', 'rocket_hide_deactivation_form', 'wpr_preload_running', 'rocket_preload_as_tables_count', 'wpr_dynamic_lists', 'wpr_dynamic_lists_delayjs', 'rocket_domain_changed', 'wp_rocket_rucss_errors_count', 'wpr_dynamic_lists_incompatible_plugins', 'rocket_divi_notice', 'rocket_saas_processing', 'rocket_mod_pagespeed_enabled', 'wp_rocket_pricing', 'wp_rocket_pricing_timeout', 'wp_rocket_pricing_timeout_active', 'rocket_get_refreshed_fragments_cache', 'rocket_preload_previous_requests_durations', 'rocket_preload_check_duration', 'wpr_user_information_timeout_active', 'wpr_user_information_timeout', 'rocket_fonts_data_collection', ]; /** * WP Rocket scheduled events. * * @var array */ private $events = [ 'rocket_purge_time_event', 'rocket_database_optimization_time_event', 'rocket_cache_dir_size_check', 'rocketcdn_check_subscription_status_event', 'rocket_cron_deactivate_cloudflare_devmode', ]; /** * WP Rocket cache directories. * * @var array */ private $cache_dirs = [ 'wp-rocket', 'min', 'busting', 'critical-css', 'used-css', 'fonts', 'background-css', ]; /** * WP Rocket Post MetaData Entries * * @var array */ private $post_meta = [ 'minify_css', 'minify_js', 'cdn', 'lazyload', 'lazyload_iframes', 'async_css', 'defer_all_js', 'delay_js', 'remove_unused_css', 'lazyload_css_bg_img', ]; /** * Tables instances * * @var array */ private $tables; /** * Constructor. * * @param string $cache_path Path to the cache folder. * @param string $config_path Path to the config folder. * @param UsedCSS $rucss_usedcss_table RUCSS used_css table. * @param Cache $rocket_cache Preload rocket_cache table. * @param AboveTheFold $atf_table Above the fold table. * @param LazyRenderContent $lrc_table Lazy Render content table. * @param PreloadFonts $preload_fonts_table Preload fonts table. * @param PreconnectExternalDomains $preload_domains_table Preload External Domains content table. */ public function __construct( $cache_path, $config_path, $rucss_usedcss_table, $rocket_cache, $atf_table, $lrc_table, $preload_fonts_table, $preload_domains_table ) { $this->cache_path = trailingslashit( $cache_path ); $this->config_path = $config_path; $this->tables = [ $rucss_usedcss_table, $rocket_cache, $atf_table, $lrc_table, $preload_fonts_table, $preload_domains_table, ]; } /** * Deletes all plugin data and files on uninstall. * * @since 3.5.2 * * @return void */ public function uninstall() { $this->delete_plugin_data(); $this->delete_cache_files(); $this->delete_config_files(); foreach ( $this->tables as $table ) { $this->delete_table( $table ); } } /** * Deletes a table * * @param Table $table Table instance. * * @return void */ private function delete_table( $table ) { if ( $table->exists() ) { $table->uninstall(); } if ( ! is_multisite() ) { return; } foreach ( get_sites( [ 'fields' => 'ids' ] ) as $site_id ) { switch_to_blog( $site_id ); if ( $table->exists() ) { $table->uninstall(); } restore_current_blog(); } } /** * Deletes WP Rocket options, transients and events. * * @since 3.5.2 * * @return void */ private function delete_plugin_data() { delete_site_transient( 'wp_rocket_update_data' ); // Delete all user meta related to WP Rocket. delete_metadata( 'user', '', 'rocket_boxes', '', true ); // Delete all post meta related to WP Rocket. foreach ( $this->post_meta as $post_meta ) { delete_post_meta_by_key( "_rocket_exclude_{$post_meta}" ); } array_walk( $this->transients, 'delete_transient' ); array_walk( $this->options, 'delete_option' ); foreach ( $this->events as $event ) { wp_clear_scheduled_hook( $event ); } } /** * Deletes all WP Rocket cache files. * * @since 3.5.2 * * @return void */ private function delete_cache_files() { foreach ( $this->cache_dirs as $dir ) { $this->delete( $this->cache_path . $dir ); } } /** * Deletes all WP Rocket config files. * * @since 3.5.2 * * @return void */ private function delete_config_files() { $this->delete( $this->config_path ); } /** * Recursively deletes files and directories. * * @since 3.5.2 * * @param string $file Path to file or directory. */ private function delete( $file ) { if ( ! is_dir( $file ) ) { wp_delete_file( $file ); return; } try { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $file, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST ); } catch ( UnexpectedValueException $e ) { return; } foreach ( $iterator as $item ) { if ( $item->isDir() ) { @rmdir( $item ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_rmdir continue; } wp_delete_file( $item ); } @rmdir( $file ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_rmdir } } Engine/Common/Head/Subscriber.php 0000644 00000010401 15174677547 0012701 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Head; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Head subscriber class. */ class Subscriber implements Subscriber_Interface { /** * Head elements array. * * @var array */ private $head_items = []; /** * Returns an array of events that this subscriber wants to listen to. * * The array key is the event name. The value can be: * * * The method name * * An array with the method name and priority * * An array with the method name, priority and number of accepted arguments * * For instance: * * * array('hook_name' => 'method_name') * * array('hook_name' => array('method_name', $priority)) * * array('hook_name' => array('method_name', $priority, $accepted_args)) * * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2))) * * @return array */ public static function get_subscribed_events() { return [ 'rocket_buffer' => [ 'insert_rocket_head', 100000 ], 'rocket_head' => 'print_head_elements', ]; } /** * Print all head elements. * * @param string $content Head elements HTML. * @return string */ public function print_head_elements( $content ) { /** * Filter Head elements array. * * @param array $head_items Elements to be added to head after closing of title tag. * * Priority 10: preconnect * Priority 30: preload * Priority 50: styles * @returns array */ $items = wpm_apply_filters_typed( 'array', 'rocket_head_items', [] ); if ( empty( $items ) ) { return $content; } $this->head_items = []; // Combine elements. $elements = ''; foreach ( $items as $item ) { // Make sure that we don't have duplication based on `href` inside each `rel`. if ( $this->is_duplicate( $item ) ) { continue; } $elements .= "\n" . $this->prepare_element( $item ); } return $content . $elements; } /** * Check if the item is duplicate. * * @param array $item Item to check. * @return bool */ private function is_duplicate( $item ) { if ( empty( $item['rel'] ) || empty( $item['href'] ) ) { return false; } if ( ! isset( $this->head_items[ $item['rel'] ] ) ) { $this->head_items[ $item['rel'] ] = []; } if ( ! isset( $this->head_items[ $item['rel'] ][ $item['href'] ] ) ) { $this->head_items[ $item['rel'] ][ $item['href'] ] = true; return false; } return true; } /** * Prepare element HTML from the item array. * * @param array $element Item element. * @return string */ private function prepare_element( $element ) { $open_tag = ''; if ( ! empty( $element['open_tag'] ) ) { $open_tag = $element['open_tag']; unset( $element['open_tag'] ); } $close_tag = ''; if ( ! empty( $element['close_tag'] ) ) { $close_tag = $element['close_tag']; unset( $element['close_tag'] ); } $inner_content = ''; if ( ! empty( $element['inner_content'] ) ) { $inner_content = $element['inner_content']; unset( $element['inner_content'] ); } $attributes = []; ksort( $element, SORT_NATURAL ); foreach ( $element as $key => $value ) { if ( is_int( $key ) ) { $attributes[] = $value; continue; } $attributes[] = $key . '="' . $this->esc_attribute( $key, $value ) . '"'; } $attributes_html = ! empty( $attributes ) ? ' ' . implode( ' ', $attributes ) : ''; return $open_tag . $attributes_html . '>' . $inner_content . $close_tag; } /** * Insert rocket_head into the buffer HTML * * @param string $html Buffer HTML. * @return string */ public function insert_rocket_head( $html ) { if ( empty( $html ) ) { return $html; } $filtered_buffer = preg_replace( '#</title>#iU', '</title>' . wpm_apply_filters_typed( 'string', 'rocket_head', '' ), $html, 1 ); if ( empty( $filtered_buffer ) ) { return $html; } return $filtered_buffer; } /** * Escape attribute value before printing it. * * @param string $attribute_name Attribute name. * @param string $attribute_value Attribute value. * @return string */ private function esc_attribute( $attribute_name, $attribute_value ) { if ( 'data-wpr-hosted-gf-parameters' === $attribute_name ) { return $attribute_value; } return esc_attr( $attribute_value ); } } Engine/Common/Head/ServiceProvider.php 0000644 00000001440 15174677547 0013714 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Head; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'common_head_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'common_head_subscriber', Subscriber::class ); } } Engine/Common/Head/ElementTrait.php 0000644 00000005116 15174677547 0013202 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\Head; /** * Element trait. */ trait ElementTrait { /** * Preload link. * * @param array $args Element args. * @return array|string[] */ protected function preload_link( array $args = [] ) { $args['rel'] = 'preload'; $args[] = 'data-rocket-preload'; return $this->link( $args ); } /** * Preconnect link. * * @param array $args Element args. * @return array|string[] */ protected function preconnect_link( array $args = [] ) { $args['rel'] = 'preconnect'; return $this->link( $args ); } /** * Dns_prefetch link. * * @param array $args Element args. * @return array|string[] */ protected function dns_prefetch_link( array $args = [] ) { $args['rel'] = 'dns-prefetch'; return $this->link( $args ); } /** * Prefetch link. * * @param array $args Element args. * @return array|string[] */ protected function prefetch_link( array $args = [] ) { $args['rel'] = 'prefetch'; return $this->link( $args ); } /** * Prerender link. * * @param array $args Element args. * @return array|string[] */ protected function prerender_link( array $args = [] ) { $args['rel'] = 'prerender'; return $this->link( $args ); } /** * Stylesheet link. * * @param array $args Element args. * @return array|string[] */ protected function stylesheet_link( array $args = [] ) { $args['rel'] = 'stylesheet'; return $this->link( $args ); } /** * Style tag. * * @param string $css CSS content. * @param array $args Element args. * @return array|string[] */ protected function style_tag( string $css = '', array $args = [] ) { $element = [ 'open_tag' => '<style', ]; $element += wp_parse_args( $args, [ 'inner_content' => $css, ] ); $element['close_tag'] = '</style>'; return $element; } /** * Noscript tag. * * @param string $content Element contents. * @param array $args Element args. * @return array|string[] */ protected function noscript_tag( string $content = '', array $args = [] ) { $element = [ 'open_tag' => '<noscript', ]; $element += wp_parse_args( $args, [ 'inner_content' => $content, ] ); $element['close_tag'] = '</noscript>'; return $element; } /** * Generic link tag. * * @param array $args Element args. * @return array|string[] */ private function link( array $args = [] ) { $element = [ 'open_tag' => '<link', ]; $element += wp_parse_args( $args, [ 'href' => '', ] ); return $element; } } Engine/Common/Cache/FilesystemCache.php 0000644 00000020076 15174677547 0014021 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Cache; use WP_Rocket\Dependencies\Psr\SimpleCache\InvalidArgumentException; use WP_Filesystem_Direct; class FilesystemCache implements CacheInterface { /** * Root folder from the path. * * @var string */ protected $root_folder; /** * WordPress filesystem. * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Class instantiation. * * @param string $root_folder Root folder from the path. * @param WP_Filesystem_Direct|null $filesystem WordPress filesystem. */ public function __construct( string $root_folder, ?WP_Filesystem_Direct $filesystem = null ) { $this->root_folder = $root_folder; $this->filesystem = $filesystem ?: rocket_direct_filesystem(); } /** * Fetches a value from the cache. * * @param string $key The unique key of this item in the cache. * @param mixed $default Default value to return if the key does not exist. * * @return mixed The value of the item from the cache, or $default in case of cache miss. */ public function get( string $key, $default = null ) { $path = $this->generate_path( $key ); if ( ! $this->filesystem->exists( $path ) ) { return $default; } return $this->filesystem->get_contents( $path ); } /** * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. * * @param string $key The key of the item to store. * @param mixed $value The value of the item to store, must be serializable. * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and * the driver supports TTL then the library may set a default value * for it or let the driver take care of that. * * @return bool True on success and false on failure. */ public function set( string $key, $value, $ttl = null ): bool { $path = $this->generate_path( $key ); $directory = dirname( $path ); rocket_mkdir_p( $directory, $this->filesystem ); return $this->filesystem->put_contents( $path, $value, rocket_get_filesystem_perms( 'file' ) ); } /** * Delete an item from the cache by its unique key. * * @param string $key The unique cache key of the item to delete. * * @return bool True if the item was successfully removed. False if there was an error. */ public function delete( string $key ): bool { $path = $this->generate_path( $key ); if ( ! $this->filesystem->exists( $path ) ) { return false; } if ( $this->filesystem->is_dir( $path ) ) { rocket_rrmdir( $path, [], $this->filesystem ); return true; } return $this->filesystem->delete( $path ); } /** * Wipes clean the entire cache's keys. * * @return bool True on success and false on failure. */ public function clear(): bool { $root_path = $this->get_root_path(); if ( ! $this->filesystem->exists( $root_path ) ) { return false; } rocket_rrmdir( $root_path, [], $this->filesystem ); return true; } /** * Clear the whole background-css directory. * * @param array $preserve_dirs List of directories to be preserved. * * @return bool True on success and false on failure. */ public function full_clear( array $preserve_dirs = [] ): bool { $base_path = $this->get_base_path(); if ( ! $this->filesystem->exists( $base_path ) ) { return false; } if ( ! empty( $preserve_dirs ) ) { $preserve_dirs = array_map( function ( $dir ) use ( $base_path ) { return $base_path . $dir; }, $preserve_dirs ); } rocket_rrmdir( $base_path, $preserve_dirs, $this->filesystem ); return true; } /** * Obtains multiple cache items by their unique keys. * * @param iterable $keys A list of keys that can obtained in a single operation. * @param mixed $default Default value to return for keys that do not exist. * * @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value. */ public function getMultiple( iterable $keys, $default = null ): iterable { $results = []; foreach ( $keys as $key ) { $results[ $key ] = $this->get( $key, $default ); } return $results; } /** * Persists a set of key => value pairs in the cache, with an optional TTL. * * @param iterable $values A list of key => value pairs for a multiple-set operation. * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and * the driver supports TTL then the library may set a default value * for it or let the driver take care of that. * * @return bool True on success and false on failure. */ public function setMultiple( iterable $values, $ttl = null ): bool { $result = true; foreach ( $values as $key => $value ) { $result &= $this->set( $key, $value, $ttl ); } return (bool) $result; } /** * Deletes multiple cache items in a single operation. * * @param iterable $keys A list of string-based keys to be deleted. * * @return bool True if the items were successfully removed. False if there was an error. */ public function deleteMultiple( iterable $keys ): bool { $result = true; foreach ( $keys as $key ) { $result &= $this->delete( $key ); } return (bool) $result; } /** * Determines whether an item is present in the cache. * * NOTE: It is recommended that has() is only to be used for cache warming type purposes * and not to be used within your live applications operations for get/set, as this method * is subject to a race condition where your has() will return true and immediately after, * another script can remove it making the state of your app out of date. * * @param string $key The cache item key. * * @return bool */ public function has( string $key ): bool { $path = $this->generate_path( $key ); return $this->filesystem->exists( $path ); } /** * Generate the real URL. * * @param string $url original URL. * @return string */ public function generate_url( string $url ): string { $path = $this->generate_path( $url ); $wp_content_dir = rocket_get_constant( 'WP_CONTENT_DIR' ); $wp_content_url = rocket_get_constant( 'WP_CONTENT_URL' ); $relative_path = str_replace( $wp_content_dir, '', $path ); $generated_url = $wp_content_url . $relative_path; return (string) apply_filters( 'rocket_css_url', $generated_url ); } /** * Generate a path from the URL. * * @param string $url URL to change to a path. * @return string */ public function generate_path( string $url ): string { $root_path = $this->get_root_path(); $root_path = rtrim( $root_path, '/' ); $parsed_url = get_rocket_parse_url( $url ); $parsed_url_path = trim( $parsed_url['path'], '/' ); $home_url = home_url(); $home_parsed_url = get_rocket_parse_url( $home_url ); $host = '' === $parsed_url['host'] || null === $parsed_url['host'] ? $home_parsed_url['host'] : $parsed_url['host']; $parsed_url_host = '/' . $host; return $root_path . $parsed_url_host . '/' . $parsed_url_path; } /** * Is the root path available. * * @return bool */ public function is_accessible(): bool { $base_path = $this->get_base_path(); if ( ! $this->filesystem->exists( $base_path ) ) { rocket_mkdir_p( $base_path, $this->filesystem ); } $root_path = $this->get_root_path(); if ( ! $this->filesystem->exists( $root_path ) ) { rocket_mkdir_p( $root_path, $this->filesystem ); } return $this->filesystem->is_writable( $root_path ); } /** * Get root path from the cache (including current blog ID) with trailing slash. * * @return string */ public function get_root_path(): string { return $this->get_base_path() . get_current_blog_id() . '/'; } /** * Get base path from the cache (/cache/background-css/) with trailing slash. * * @return string */ private function get_base_path(): string { return rtrim( _rocket_normalize_path( rocket_get_constant( 'WP_ROCKET_CACHE_ROOT_PATH' ) ) . $this->root_folder, '/' ) . '/'; } } Engine/Common/Cache/CacheInterface.php 0000644 00000001632 15174677547 0013572 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Cache; interface CacheInterface extends \WP_Rocket\Dependencies\Psr\SimpleCache\CacheInterface { /** * Generate the real URL. * * @param string $url original URL. * @return string */ public function generate_url( string $url ): string; /** * Is the root path available. * * @return bool */ public function is_accessible(): bool; /** * Get root path from the cache. * * @return string */ public function get_root_path(): string; /** * Generate a path from the URL. * * @param string $url URL to change to a path. * @return string */ public function generate_path( string $url ): string; /** * Wipes the whole cache directory. * * @param array $preserve_dirs List of directories to be preserved. * * @return bool True on success and false on failure. */ public function full_clear( array $preserve_dirs = [] ): bool; } Engine/Common/Clock/ClockInterface.php 0000644 00000002102 15174677547 0013643 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Clock; interface ClockInterface { /** * Retrieves the current time based on specified type. * * - The 'mysql' type will return the time in the format for MySQL DATETIME field. * - The 'timestamp' or 'U' types will return the current timestamp or a sum of timestamp * and timezone offset, depending on `$gmt`. * - Other strings will be interpreted as PHP date formats (e.g. 'Y-m-d'). * * If `$gmt` is a truthy value then both types will use GMT time, otherwise the * output is adjusted with the GMT offset for the site. * * @since 1.0.0 * @since 5.3.0 Now returns an integer if `$type` is 'U'. Previously a string was returned. * * @param string $type Type of time to retrieve. Accepts 'mysql', 'timestamp', 'U', * or PHP date format string (e.g. 'Y-m-d'). * @param int|bool $gmt Optional. Whether to use GMT timezone. Default false. * * @return int|string Integer if `$type` is 'timestamp' or 'U', string otherwise. */ public function current_time( string $type, $gmt = 0 ); } Engine/Common/Clock/WPRClock.php 0000644 00000002573 15174677547 0012427 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Clock; class WPRClock implements ClockInterface { /** * Retrieves the current time based on specified type. * * - The 'mysql' type will return the time in the format for MySQL DATETIME field. * - The 'timestamp' or 'U' types will return the current timestamp or a sum of timestamp * and timezone offset, depending on `$gmt`. * - Other strings will be interpreted as PHP date formats (e.g. 'Y-m-d'). * * If `$gmt` is a truthy value then both types will use GMT time, otherwise the * output is adjusted with the GMT offset for the site. * * @since 1.0.0 * @since 5.3.0 Now returns an integer if `$type` is 'U'. Previously a string was returned. * * @param string $type Type of time to retrieve. Accepts 'mysql', 'timestamp', 'U', * or PHP date format string (e.g. 'Y-m-d'). * @param int|bool $gmt Optional. Whether to use GMT timezone. Default false. * * @return int|string Integer if `$type` is 'timestamp' or 'U', string otherwise. */ public function current_time( string $type, $gmt = 0 ) { $current_time = current_time( $type, $gmt ); $output = apply_filters( 'rocket_current_time', $current_time ); if ( ( is_string( $current_time ) && strtotime( $current_time ) ) || ( is_int( $current_time ) && $current_time >= 0 ) ) { return $output; } return $current_time; } } Engine/Common/Utils.php 0000644 00000002054 15174677547 0011042 0 ustar 00 <?php namespace WP_Rocket\Engine\Common; class Utils { /** * Check if current page is the home page. * * @param string $url Current page url. * * @return bool */ public static function is_home( string $url ): bool { /** * Filters the home url. * * @since 3.11.4 * * @param string $home_url home url. * @param string $url url of current page. */ $home_url = rocket_apply_filter_and_deprecated( 'rocket_saas_is_home_url', [ home_url(), $url ], '3.16', 'rocket_rucss_is_home_url' ); return untrailingslashit( $url ) === untrailingslashit( $home_url ); } /** * Checks if current request is coming from our SaaS. * * @return bool */ public static function is_saas_visit(): bool { return isset( $_SERVER['HTTP_WPR_OPT_LIST'] ); } /** * Checks if current request is coming from our inspector tool. * * @return bool */ public static function is_inspector_visit(): bool { return isset( $_GET['wpr_lazyrendercontent'] );// phpcs:ignore WordPress.Security.NonceVerification.Recommended } } Engine/Common/AbstractFileSystem.php 0000644 00000005675 15174677547 0013526 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use WP_Filesystem_Direct; abstract class AbstractFileSystem { /** * WP Filesystem instance. * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Constructor method. * Initializes a new instance of the Controller class. * * @param WP_Filesystem_Direct $filesystem Filesystem class. */ public function __construct( $filesystem = null ) { $this->filesystem = $filesystem ?? rocket_direct_filesystem(); } /** * Write to file. * * @param string $file_path File path to store the file. * @param string $content File content(data). * * @return bool */ protected function write_file( string $file_path, string $content ): bool { return $this->filesystem->put_contents( $file_path, $content, rocket_get_filesystem_perms( 'file' ) ); } /** * Get the content of a file * * @param string $file The file content to get. * * @return string */ public function get_file_content( string $file ): string { if ( ! $this->filesystem->exists( $file ) ) { return ''; } return $this->filesystem->get_contents( $file ); } /** * Delete file from a directory * * @param string $file_path Path to file that would be deleted. * * @return bool */ protected function delete_file( string $file_path ): bool { return $this->filesystem->delete( $file_path, false, 'f' ); } /** * Checks if the dir path is writable and create dir if it doesn't exist. * * @param string $dir_path The directory to check. * * @return bool */ protected function is_folder_writable( string $dir_path ): bool { if ( ! $this->filesystem->exists( $dir_path ) ) { rocket_mkdir_p( $dir_path ); } return $this->filesystem->is_writable( $dir_path ); } /** * Deletes all files in a given directory * * @param string $dir_path The directory path. * * @return void */ public function delete_all_files_from_directory( $dir_path ): void { try { $dir = new RecursiveDirectoryIterator( $dir_path, \FilesystemIterator::SKIP_DOTS ); $items = new RecursiveIteratorIterator( $dir, RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $items as $item ) { $this->filesystem->delete( $item ); } } catch ( \Exception $e ) { return; } } /** * Converts hash to path with filtered number of levels * * @since 3.11.4 * * @param string $hash md5 hash string. * * @return string */ public function hash_to_path( string $hash ): string { /** * Filters the number of sub-folders level to create for used CSS storage * * @since 3.11.4 * * @param int $levels Number of levels. */ $levels = wpm_apply_filters_typed( 'integer', 'rocket_used_css_dir_level', 3 ); $base = substr( $hash, 0, $levels ); $remain = substr( $hash, $levels ); $path_array = str_split( $base ); $path_array[] = $remain; return implode( '/', $path_array ); } } Engine/Common/Ajax/AjaxHandler.php 0000644 00000001407 15174677547 0013007 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Ajax; class AjaxHandler { /** * Validate the referer before going further. * * @param string $action Action to validate. * @param string $capacities User capacity to verify. * @return bool */ public function validate_referer( string $action, string $capacities ) { check_admin_referer( $action ); if ( ! current_user_can( $capacities ) ) { return false; } return true; } /** * Redirect the page at the end of the logic. * * @param string $url URl to redirect to (by default referrer). * @return void */ public function redirect( string $url = '' ) { wp_safe_redirect( '' === $url ? wp_get_referer() : $url ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } } Engine/Common/Database/Queries/AbstractQuery.php 0000644 00000034156 15174677547 0015664 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\Database\Queries; use WP_Rocket\Dependencies\BerlinDB\Database\Query; class AbstractQuery extends Query { /** * Table status. * * @var boolean */ public static $table_exists = false; /** * Get row for specific url. * * @param string $url Page Url. * @param bool $is_mobile if the request is for mobile page. * * @return false|mixed */ public function get_row( string $url, bool $is_mobile = false ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $query = $this->query( [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, ] ); if ( empty( $query[0] ) ) { return false; } return $query[0]; } /** * Get single row by ID. * * @param int $row_id DB Row ID. * * @return object|array|false false if no row found, array or object if row found. */ public function get_row_by_id( int $row_id ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $query = $this->query( [ 'id' => $row_id, ] ); if ( is_array( $query ) ) { $query = array_pop( $query ); } if ( empty( $query ) ) { return false; } return $query; } /** * Get all rows with the same url (desktop and mobile versions). * * @param string $url Page url. * * @return array|false */ public function get_rows_by_url( string $url ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $query = $this->query( [ 'url' => untrailingslashit( $url ), ] ); if ( empty( $query ) ) { return false; } return $query; } /** * Fetch on submit jobs. * * @param int $count Number of jobs to fetch. * @return array|int */ public function get_on_submit_jobs( int $count = 100 ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return []; } $in_progress_count = (int) $this->query( [ 'count' => true, 'status' => [ 'in-progress' ], ] ); $pending_count = (int) $this->query( [ 'count' => true, 'status' => [ 'pending' ], ] ); $processing_count = $in_progress_count + $pending_count; if ( 0 !== $count && $processing_count >= $count ) { return []; } $query_params = [ 'status' => 'to-submit', 'orderby' => 'modified', 'order' => 'asc', ]; if ( 0 !== $count ) { $query_params['number'] = ( $count - $processing_count ); } return $this->query( $query_params ); } /** * Create new DB row for specific url. * * @param string $url Current page url. * @param string $job_id API job_id. * @param string $queue_name API Queue name. * @param bool $is_mobile if the request is for mobile page. * * @return bool */ public function create_new_job( string $url, string $job_id = '', string $queue_name = '', bool $is_mobile = false ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $item = [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, 'job_id' => $job_id, 'queue_name' => $queue_name, 'status' => 'to-submit', 'retries' => 0, 'last_accessed' => current_time( 'mysql', true ), ]; $result = $this->add_item( $item ); /** * Fires after a new job has been added. * * @param mixed $is_success New job status: ID of inserted row if successfully added; false otherwise. * @param string $timestamp Current timestamp. */ rocket_do_action_and_deprecated( 'rocket_last_saas_job_added_time', [ $result, current_time( 'mysql', true ) ], '3.16', 'rocket_last_rucss_job_added_time' ); return $result; } /** * Get pending jobs. * * @param int $count Number of rows. * * @return array */ public function get_pending_jobs( int $count = 100 ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return []; } $inprogress_count = (int) $this->query( [ 'count' => true, 'status' => 'in-progress', ] ); if ( $inprogress_count >= $count ) { return []; } return $this->query( [ 'number' => ( $count - $inprogress_count ), 'status' => 'pending', 'job_id__not_in' => [ 'not_in' => '', ], 'orderby' => 'modified', 'order' => 'asc', ] ); } /** * Increment retries number and change status back to pending. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $error_code error code. * @param string $error_message error message. * * @return bool|int */ public function increment_retries( string $url, bool $is_mobile, string $error_code, string $error_message ) { if ( ! $this->is_allowed() ) { return false; } $db = $this->get_db(); $prefixed_table_name = $db->prefix . $this->table_name; $old = $this->get_row( $url, $is_mobile ); $retries = 0; $previous_message = ''; if ( $old ) { $retries = $old->retries; $previous_message = $old->error_message; } $data = [ 'retries' => $retries + 1, 'status' => 'pending', 'error_message' => $previous_message . ' - ' . current_time( 'mysql', true ) . " {$error_code}: {$error_message}", ]; $where = [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, ]; return $db->update( $prefixed_table_name, $data, $where ); } /** * Update Job ID. * * @param int $id DB row ID. * @param int $new_job_id new job id. * * @return bool */ public function update_job_id( $id, $new_job_id ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $update_data['job_id'] = $new_job_id; return $this->update_item( $id, $update_data ); } /** * Change the status to be in-progress. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @return bool|int */ public function make_status_inprogress( string $url, bool $is_mobile ) { if ( ! $this->is_allowed() ) { return false; } $db = $this->get_db(); $prefixed_table_name = $db->prefix . $this->table_name; $where = [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, ]; return $db->update( $prefixed_table_name, [ 'status' => 'in-progress' ], $where ); } /** * Reset the job and add new job_id pending. * * @param int $id DB row ID. * @param string $job_id API job_id. * * @return bool */ public function reset_job( int $id, string $job_id = '' ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } return $this->update_item( $id, [ 'job_id' => $job_id, 'status' => 'to-submit', 'error_code' => '', 'error_message' => '', 'retries' => 0, 'modified' => current_time( 'mysql', true ), 'submitted_at' => current_time( 'mysql', true ), ] ); } /** * Change the status to be failed. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $error_code error code. * @param string $error_message error message. * * @return bool|int */ public function make_status_failed( string $url, bool $is_mobile, string $error_code, string $error_message ) { if ( ! $this->is_allowed() ) { return false; } $db = $this->get_db(); $prefixed_table_name = $db->prefix . $this->table_name; $old = $this->get_row( $url, $is_mobile ); $previous_message = $old ? $old->error_message : ''; $data = [ 'status' => 'failed', 'error_code' => $error_code, 'error_message' => $previous_message . ' - ' . current_time( 'mysql', true ) . " {$error_code}: {$error_message}", ]; $where = [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, ]; return $db->update( $prefixed_table_name, $data, $where ); } /** * Update row last_accessed date to current date. * * @param int $id Used CSS id. * * @return bool */ public function update_last_accessed( int $id ): bool { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } return (bool) $this->update_item( $id, [ 'last_accessed' => current_time( 'mysql', true ), ] ); } /** * Delete DB row by url. * * @param string $url Page url to be deleted. * * @return bool */ public function delete_by_url( string $url ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $items = $this->get_rows_by_url( $url ); if ( ! $items ) { return false; } $deleted = true; foreach ( $items as $item ) { $deleted = $deleted && $this->delete_item( $item->id ); } return $deleted; } /** * Get the count of not completed rows. * * @return int */ public function get_not_completed_count() { if ( ! self::$table_exists && ! $this->table_exists() ) { return 0; } return $this->query( [ 'count' => true, 'status__in' => [ 'pending', 'in-progress' ], ] ); } /** * Get the count of completed rows. * * @return int */ public function get_completed_count() { if ( ! self::$table_exists && ! $this->table_exists() ) { return 0; } return $this->query( [ 'count' => true, 'status' => 'completed', ] ); } /** * Get all failed rows. * * @param float $delay delay before the urls are deleted. * @param string $unit unit from the delay. * @return array|false */ public function get_failed_rows( float $delay = 3, string $unit = 'days' ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $query = $this->query( [ 'status' => 'failed', 'date_query' => [ [ 'column' => 'modified', 'before' => "$delay $unit ago", 'inclusive' => true, ], ], ], false ); if ( empty( $query ) ) { return false; } return $query; } /** * Revert status to pending. * * @param integer $id Used CSS id. * @return boolean */ public function revert_to_pending( int $id ): bool { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } return (bool) $this->update_item( $id, [ 'error_code' => '', 'error_message' => '', 'retries' => 0, 'status' => 'pending', 'modified' => current_time( 'mysql', true ), ] ); } /** * Returns the current status of the table; true if it exists, false otherwise. * * @return boolean */ protected function table_exists(): bool { if ( self::$table_exists ) { return true; } // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } // Query statement. $query = 'SELECT table_name FROM information_schema.tables WHERE table_schema = %s AND table_name = %s LIMIT 1'; $prepared = $db->prepare( $query, $db->__get( 'dbname' ), $db->{$this->table_name} ); $result = $db->get_var( $prepared ); // Does the table exist? $exists = $this->is_success( $result ); if ( $exists ) { self::$table_exists = $exists; } return $exists; } /** * Change the status to be pending. * * @param string $url DB row url. * @param string $job_id API job_id. * @param string $queue_name API Queue name. * @param bool $is_mobile if the request is for mobile page. * @return bool|int */ public function make_status_pending( string $url, string $job_id = '', string $queue_name = '', bool $is_mobile = false ) { if ( ! $this->is_allowed() ) { return false; } $db = $this->get_db(); $prefixed_table_name = $db->prefix . $this->table_name; $data = [ 'job_id' => $job_id, 'queue_name' => $queue_name, 'status' => 'pending', 'is_mobile' => $is_mobile, 'submitted_at' => current_time( 'mysql', true ), ]; $where = [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, ]; return $db->update( $prefixed_table_name, $data, $where ); } /** * Update the error message. * * @param string $url DB row url. * @param boolean $is_mobile Is mobile from DB row. * @param int $code Response code. * @param string $message Response message. * @param string $previous_message Previous saved message. * * @return bool|int */ public function update_message( string $url, bool $is_mobile, int $code, string $message, string $previous_message = '' ) { if ( ! $this->is_allowed() ) { return false; } $db = $this->get_db(); $prefixed_table_name = $db->prefix . $this->table_name; $data = [ 'error_message' => $previous_message . ' - ' . current_time( 'mysql', true ) . " {$code}: {$message}" ]; $where = [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, ]; return $db->update( $prefixed_table_name, $data, $where ); } /** * Updates the next_retry_time field * * @param string $url DB row url. * @param boolean $is_mobile Is mobile from DB row. * @param string|int $next_retry_time timestamp or mysql format date. * * @return bool|int either it is saved or not. */ public function update_next_retry_time( string $url, bool $is_mobile, $next_retry_time ) { if ( ! $this->is_allowed() ) { return false; } $db = $this->get_db(); $prefixed_table_name = $db->prefix . $this->table_name; if ( is_string( $next_retry_time ) && strtotime( $next_retry_time ) ) { // If $next_retry_time is a valid date string, convert it to a timestamp. $next_retry_time = strtotime( $next_retry_time ); } elseif ( ! is_numeric( $next_retry_time ) ) { // If it's not numeric and not a valid date string, return false. return false; } $data = [ 'next_retry_time' => gmdate( 'Y-m-d H:i:s', $next_retry_time ) ]; $where = [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, ]; return $db->update( $prefixed_table_name, $data, $where ); } /** * Check if db action can be processed. * * @return boolean */ private function is_allowed() { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } // Bail if no database interface is available. if ( empty( $this->get_db() ) ) { return false; } return true; } } Engine/Common/Database/Tables/AbstractTable.php 0000644 00000006102 15174677547 0015371 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Database\Tables; use WP_Rocket\Dependencies\BerlinDB\Database\Table; use WP_Rocket\Engine\Common\Database\TableInterface; class AbstractTable extends Table implements TableInterface { /** * Table schema data. * * @var string */ protected $schema_data; /** * Instantiate class. */ public function __construct() { parent::__construct(); add_action( 'admin_init', [ $this, 'maybe_trigger_recreate_table' ], 9 ); add_action( 'init', [ $this, 'maybe_upgrade' ] ); } /** * Setup the database schema * * @return void */ protected function set_schema() { if ( ! $this->schema_data ) { return; } $this->schema = $this->schema_data; } /** * Delete all rows which were not accessed in the last month. * * @return bool|int */ public function delete_old_rows() { if ( ! $this->exists() ) { return false; } // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } /** * Filters the old SaaS data deletion interval * * @param int $delete_interval Old Saas data deletion interval in months */ $delete_interval = (int) rocket_apply_filter_and_deprecated( 'rocket_saas_delete_interval', [ 1 ], '3.16', 'rocket_rucss_delete_interval' ); if ( $delete_interval <= 0 ) { return false; } $prefixed_table_name = $this->apply_prefix( $this->table_name ); $query = "DELETE FROM `$prefixed_table_name` WHERE `last_accessed` <= date_sub(now(), interval $delete_interval month)"; $rows_affected = $db->query( $query ); return $rows_affected; } /** * Get all rows which were not accessed in the last month. * * @return array */ public function get_old_rows(): array { if ( ! $this->exists() ) { return []; } // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return []; } $prefixed_table_name = $this->apply_prefix( $this->table_name ); $query = "SELECT * FROM `$prefixed_table_name` WHERE `last_accessed` <= date_sub(now(), interval 1 month)"; $rows_affected = $db->get_results( $query ); return $rows_affected; } /** * Remove all completed rows. * * @return bool|int */ public function remove_all_completed_rows() { if ( ! $this->exists() ) { return false; } // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $prefixed_table_name = $this->apply_prefix( $this->table_name ); return $db->query( "DELETE FROM `$prefixed_table_name` WHERE status IN ( 'failed', 'completed' )" ); } /** * Returns name from table. * * @return string */ public function get_name() { return $this->apply_prefix( $this->table_name ); } /** * Trigger recreation of cache table if not exist. * * @return void */ public function maybe_trigger_recreate_table() { if ( $this->exists() ) { return; } delete_option( $this->db_version_key ); } } Engine/Common/Database/TableInterface.php 0000644 00000001256 15174677547 0014321 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Database; interface TableInterface { /** * Delete all rows which were not accessed in the last month. * * @return bool|int */ public function delete_old_rows(); /** * Get all rows which were not accessed in the last month. * * @return array */ public function get_old_rows(): array; /** * Remove all completed rows. * * @return bool|int */ public function remove_all_completed_rows(); /** * Returns name from table. * * @return string */ public function get_name(); /** * Trigger recreation of cache table if not exist. * * @return void */ public function maybe_trigger_recreate_table(); } Engine/Common/ExtractCSS/Subscriber.php 0000644 00000007376 15174677547 0014044 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\ExtractCSS; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Logger\LoggerAware; use WP_Rocket\Logger\LoggerAwareInterface; class Subscriber implements Subscriber_Interface, LoggerAwareInterface { use RegexTrait; use LoggerAware; /** * Returns an array of events that this subscriber wants to listen to. * * The array key is the event name. The value can be: * * * The method name * * An array with the method name and priority * * An array with the method name, priority and number of accepted arguments * * For instance: * * * array('hook_name' => 'method_name') * * array('hook_name' => array('method_name', $priority)) * * array('hook_name' => array('method_name', $priority, $accepted_args)) * * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2))) * * @return array */ public static function get_subscribed_events() { return [ 'rocket_generate_lazyloaded_css' => [ [ 'extract_css_files_from_html', 11 ], [ 'extract_inline_css_from_html', 14 ], ], ]; } /** * Extract CSS files from the HTML. * * @param array $data Data sent. * @return array */ public function extract_css_files_from_html( array $data ): array { if ( ! key_exists( 'html', $data ) ) { $this->logger::notice( 'Extract CSS files bailed out', [ 'type' => 'extract_css', 'data' => $data, ] ); return $data; } if ( ! key_exists( 'css_files', $data ) ) { $data['css_files'] = []; } $css_links = []; $html = $this->hide_comments( $data['html'] ); $link_styles = $this->find( '<link\s+([^>]+[\s"\'])?href\s*=\s*[\'"]\s*?(?<url>[^\'"]+(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>', $html, 'Uis' ); foreach ( $link_styles as $style ) { if ( ( ! (bool) preg_match( '/rel=[\'"]?stylesheet[\'"]?/is', $style[0] ) && ! ( (bool) preg_match( '/rel=[\'"]?preload[\'"]?/is', $style[0] ) && (bool) preg_match( '/as=[\'"]?style[\'"]?/is', $style[0] ) ) ) || strstr( $style['url'], '//fonts.googleapis.com/css' ) ) { $this->logger::notice( "Skipped URL: {$style['url']}", [ 'type' => 'extract_css', 'data' => $style[0], ] ); continue; } $css_links [] = $style['url']; $this->logger::notice( "Extracted URL: {$style['url']}", [ 'type' => 'extract_css', 'data' => $style[0], ] ); } $data['css_files'] = array_merge( $data['css_files'], $css_links ); return $data; } /** * Extract inline CSS from the HTML. * * @param array $data Data sent. * @return array */ public function extract_inline_css_from_html( array $data ): array { if ( ! key_exists( 'html', $data ) ) { $this->logger::notice( 'Extract CSS inline bailed out', [ 'type' => 'extract_css', 'data' => $data, ] ); return $data; } if ( ! key_exists( 'css_inline', $data ) ) { $data['css_inline'] = []; } $css_links = []; $html = $this->hide_comments( $data['html'] ); $inline_styles = $this->find( '<style(?<atts>.*)>(?<content>.*)<\/style\s*>', $html ); foreach ( $inline_styles as $style ) { $content = trim( $style['content'] ); if ( empty( $content ) ) { $this->logger::notice( "Skipped Content: {$style['content']}", [ 'type' => 'extract_css', 'data' => $style[0], ] ); continue; } $css_links [] = $content; $this->logger::notice( "Extracted Content: $content", [ 'type' => 'extract_css', 'data' => $style[0], ] ); } $data['css_inline'] = array_merge( $data['css_inline'], $css_links ); return $data; } } Engine/Common/ExtractCSS/ServiceProvider.php 0000644 00000002432 15174677547 0015040 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\ExtractCSS; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Common\Cache\FilesystemCache; /** * Service provider. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'lazyload_css_cache', 'common_extractcss_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { /** * Background CSS cache folder. * * @param string $root Background CSS cache folder. */ $root = apply_filters( 'rocket_lazyload_css_cache_root', 'background-css/' ); $this->getContainer()->add( 'lazyload_css_cache', FilesystemCache::class ) ->addArgument( new StringArgument( $root ) ); $this->getContainer()->addShared( 'common_extractcss_subscriber', Subscriber::class ); } } Engine/Common/PerformanceHints/WarmUp/Subscriber.php 0000644 00000004156 15174677547 0016534 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\WarmUp; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * WarmUp controller instance * * @var Controller */ private $controller; /** * Constructor * * @param Controller $controller WarmUp controller instance. */ public function __construct( Controller $controller ) { $this->controller = $controller; } /** * Array of events this subscriber listens to * * @return array */ public static function get_subscribed_events(): array { return [ 'wp_rocket_upgrade' => [ 'warm_up_on_update', 10, 2 ], 'rocket_after_clear_performance_hints_data' => 'warm_up_home', 'rocket_job_warmup' => 'warm_up', 'rocket_job_warmup_url' => 'send_to_saas', 'rocket_saas_api_queued_url' => 'add_wpr_imagedimensions_query_arg', ]; } /** * Send home to warmup and start async fetch links * * @return void */ public function warm_up_home(): void { $this->controller->warm_up_home(); } /** * Fetch links for warmup and create async tasks * * @return void */ public function warm_up(): void { $this->controller->warm_up(); } /** * Send url to SaaS for warmup * * @param string $url URL to be sent. * * @return void */ public function send_to_saas( string $url ): void { $this->controller->send_to_saas( $url ); } /** * Process links fetched from homepage on update. * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function warm_up_on_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.17', '>=' ) ) { return; } $this->controller->warm_up(); } /** * Add image dimensions query parameter to URL. * * @param string $url URL to be sent. * * @return string */ public function add_wpr_imagedimensions_query_arg( string $url ): string { return $this->controller->add_wpr_imagedimensions_query_arg( $url ); } } Engine/Common/PerformanceHints/WarmUp/APIClient.php 0000644 00000001406 15174677547 0016174 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\WarmUp; use WP_Rocket\Engine\Common\JobManager\APIHandler\APIClient as BaseAPIClient; use WP_Rocket\Engine\Common\Utils; class APIClient extends BaseAPIClient { /** * Send the link to SaaS. * * @param string $url Url to be sent. * @param string $device Device type. * * @return array */ public function add_to_performance_hints_queue( string $url, $device = 'desktop' ): array { $is_home = Utils::is_home( $url ); $config = [ 'optimization_list' => [ 'performance_hints', // performance_hints represent atf,lrc. ], 'is_home' => $is_home, 'is_mobile' => 'mobile' === $device, ]; return $this->add_to_queue( $url, $config ); } } Engine/Common/PerformanceHints/WarmUp/Controller.php 0000644 00000012650 15174677547 0016552 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\WarmUp; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\Utils; use WP_Rocket\Engine\License\API\User; class Controller { /** * Array of Factories. * * @var array */ private $factories; /** * Plugin options instance. * * @var Options_Data */ protected $options; /** * APIClient Instance. * * @var APIClient */ private $api_client; /** * User instance * * @var User */ private $user; /** * Queue instance. * * @var Queue */ private $queue; /** * Constructor * * @param array $factories Array of factories. * @param Options_Data $options Options instance. * @param APIClient $api_client APIClient instance. * @param User $user User instance. * @param Queue $queue Queue instance. */ public function __construct( array $factories, Options_Data $options, APIClient $api_client, User $user, Queue $queue ) { $this->factories = $factories; $this->options = $options; $this->api_client = $api_client; $this->user = $user; $this->queue = $queue; } /** * Should terminate early if true. * * @return bool */ private function is_allowed(): bool { return ! ( 'local' === wp_get_environment_type() || $this->user->is_license_expired_grace_period() || (bool) $this->options->get( 'remove_unused_css', 0 ) ); } /** * Send home URL for warm up. * * @return void */ public function warm_up_home(): void { if ( ! $this->is_allowed() ) { return; } if ( empty( $this->factories ) ) { return; } $this->send_to_saas( home_url() ); $this->queue->add_job_warmup(); } /** * Fetch links and send them to SaaS for warm up. * * @return void */ public function warm_up(): void { if ( ! $this->is_allowed() ) { return; } if ( empty( $this->factories ) ) { return; } $links = $this->fetch_links(); foreach ( $links as $link ) { $this->queue->add_job_warmup_url( $link ); } } /** * Fetch links from homepage. * * @return array */ public function fetch_links(): array { $user_agent = 'WP Rocket/Pre-fetch Home Links Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1'; $home_url = home_url(); $args = [ 'user-agent' => $user_agent, 'timeout' => 60, ]; $response = wp_safe_remote_get( $home_url, $args ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return []; } $html = wp_remote_retrieve_body( $response ); if ( ! preg_match_all( '/<a\s+(?:[^>]*?\s+)?href=(["\'])(.*?)\1/', $html, $matches ) ) { return []; } $links = $matches[2]; // Cater for relative urls. $links = array_map( function ( $link ) { $link_path = wp_parse_url( $link ); // Return if absolute url. if ( isset( $link_path['path'], $link_path['scheme'] ) ) { return $link; } // Transform to absolute url if relative. if ( isset( $link_path['path'] ) ) { return home_url( $link ); } return $link; }, $links ); $reject_uri_pattern = '/(?:.+/)?feed(?:/(?:.+/?)?)?$|/(?:.+/)?embed/|/wc-api/v(.*)|/(index.php/)?(.*)wp-json(/.*|$)'; // Filter links. $links = array_filter( $links, function ( $link ) use ( $home_url, $reject_uri_pattern ) { $link_host = wp_parse_url( $link ); $site_host = wp_parse_url( $home_url ); /** * Check for valid link. * Check that no external link. * Check that it's not home. */ $is_valid_url = wp_http_validate_url( $link ); $is_same_host = isset( $link_host['host'] ) ? $link_host['host'] === $site_host['host'] : false; $is_not_home = ! Utils::is_home( $link ); $is_not_excluded_uri = ! (bool) preg_match( '#' . $reject_uri_pattern . '#i', $link ); return $is_valid_url && $is_same_host && $is_not_home && $is_not_excluded_uri; } ); // Remove duplicate links. $links = array_unique( $links ); $default_limit = 10; /** * Filters the number of links to return from the homepage. * * @param int $links_limit number of links to return. */ $links_limit = rocket_apply_filter_and_deprecated( 'rocket_performance_hints_warmup_links_number', [ $default_limit ], '3.16.4', 'rocket_atf_warmup_links_number' ); if ( ! is_int( $links_limit ) || $links_limit < 1 ) { $links_limit = $default_limit; } $links = array_slice( $links, 0, $links_limit ); return $links; } /** * Send link to SaaS to do the warmup. * * @param string $url Url to send. * * @return void */ public function send_to_saas( string $url ) { $this->api_client->add_to_performance_hints_queue( $url ); if ( $this->is_mobile() ) { $this->api_client->add_to_performance_hints_queue( $url, 'mobile' ); } } /** * Check if the mobile cache is set. * * @return bool */ private function is_mobile(): bool { return $this->options->get( 'cache_mobile', 1 ) && $this->options->get( 'do_caching_mobile_files', 1 ); } /** * Add wpr_imagedimensions to URL query. * * @param string $url URL to be sent. * * @return string */ public function add_wpr_imagedimensions_query_arg( string $url ): string { if ( empty( $this->factories ) && ! $this->options->get( 'remove_unused_css', 0 ) ) { return $url; } return add_query_arg( [ 'wpr_imagedimensions' => 1, ], $url ); } } Engine/Common/PerformanceHints/WarmUp/Queue.php 0000644 00000001321 15174677547 0015504 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\WarmUp; use WP_Rocket\Engine\Common\Queue\AbstractASQueue; class Queue extends AbstractASQueue { /** * PerformanceHints queue group * * @var string */ protected $group = 'rocket-performance-hints-warmup'; /** * Add an async job to warm up home links * * @return int */ public function add_job_warmup(): int { return $this->add_async( 'rocket_job_warmup' ); } /** * Add an async job to send URL to SaaS for warmup * * @param string $url URL to warm up. * * @return int */ public function add_job_warmup_url( string $url ): int { return $this->add_async( 'rocket_job_warmup_url', [ $url ] ); } } Engine/Common/PerformanceHints/Cron/CronTrait.php 0000644 00000001635 15174677547 0016023 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Cron; trait CronTrait { /** * Performance Hints Deletion interval filter. * * @param string $filter_name The filter name. * * @return object */ public function deletion_interval( string $filter_name ): object { /** * Filters the interval (in months) to determine when a performance data entry is considered 'old'. * Old performance entries are eligible for deletion. By default, a performance entry is considered old if it hasn't been accessed in the last month. * * @param int $delete_interval The interval in months after which a performance data entry is considered old. Default is 1 month. */ $delete_interval = wpm_apply_filters_typed( 'integer', $filter_name, 1 ); if ( $delete_interval <= 0 ) { return $this->queries; } return $this->queries->set_cleanup_interval( $delete_interval ); } } Engine/Common/PerformanceHints/Cron/Subscriber.php 0000644 00000002764 15174677547 0016225 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Cron; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Cron Controller Instance. * * @var Controller */ private $controller; /** * Instantiate the cron controller class * * @param Controller $controller Cron controller instance. */ public function __construct( Controller $controller ) { $this->controller = $controller; } /** * Returns an array of events that this class subscribes to. * * @return array An associative array where the keys are the event names and the values are the method names to call when the event is triggered. */ public static function get_subscribed_events(): array { return [ 'rocket_performance_hints_cleanup' => 'cleanup', 'init' => 'schedule_cleanup', 'rocket_deactivation' => 'unscheduled_cleanup', ]; } /** * Executes the performance hints cleanup. * * @return void */ public function cleanup() { $this->controller->cleanup(); } /** * Schedules the performance hints cleanup to run at a later time. * * @return void */ public function schedule_cleanup() { $this->controller->schedule_cleanup(); } /** * Unscheduled the performance hints cleanup, preventing it from running at the previously scheduled time. * * @return void */ public function unscheduled_cleanup() { $this->controller->unscheduled_cleanup(); } } Engine/Common/PerformanceHints/Cron/Controller.php 0000644 00000003044 15174677547 0016235 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Cron; /** * The Controller Class is responsible for scheduling, executing, and unschedulin performance hints optimization cleanup. */ class Controller { /** * Array of factories. * * @var array */ private $factories; /** * Constructor method. * Initializes a new instance of the Controller class. * * @param array $factories Array of factories. */ public function __construct( array $factories ) { $this->factories = $factories; } /** * Schedules the performance cleanup to run daily if it's not already scheduled. */ public function schedule_cleanup() { if ( ! wp_next_scheduled( 'rocket_performance_hints_cleanup' ) ) { wp_schedule_event( time(), 'daily', 'rocket_performance_hints_cleanup' ); } } /** * Executes the performance hints cleanup. * It gets the current date and subtracts the interval (default to 1 month) from it. * Then it deletes the rows with 'failed' status or not accessed since the interval. */ public function cleanup() { // Delete the rows with failed status or not accessed. foreach ( $this->factories as $factory ) { $factory->queries()->delete_old_rows(); } } /** * Unscheduled the performance hints cleanup, preventing it from running at the previously scheduled time. */ public function unscheduled_cleanup() { $timestamp = wp_next_scheduled( 'rocket_performance_hints_cleanup' ); if ( $timestamp ) { wp_unschedule_event( $timestamp, 'rocket_performance_hints_cleanup' ); } } } Engine/Common/PerformanceHints/FactoryInterface.php 0000644 00000002272 15174677547 0016443 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\PerformanceHints; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\ControllerInterface as AjaxControllerInterface; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface as FrontendControllerInterface; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\QueriesInterface; use WP_Rocket\Engine\Common\PerformanceHints\Database\Table\TableInterface; interface FactoryInterface { /** * Provides an Ajax interface. * * @return AjaxControllerInterface */ public function get_ajax_controller(): AjaxControllerInterface; /** * Provides a Frontend interface. * * @return FrontendControllerInterface */ public function get_frontend_controller(): FrontendControllerInterface; /** * Provides a Table interface. * * @return TableInterface */ public function table(): TableInterface; /** * Provides a Queries interface. * * @return QueriesInterface */ public function queries(): QueriesInterface; /** * Provides a Context interface * * @return ContextInterface */ public function get_context(): ContextInterface; } Engine/Common/PerformanceHints/ServiceProvider.php 0000644 00000012156 15174677547 0016330 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints; use WP_Rocket\Buffer\{Config, Tests}; use WP_Rocket\Dependencies\League\Container\Argument\Literal\{ArrayArgument, StringArgument}; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Common\PerformanceHints\Admin\{ Controller as AdminController, Subscriber as AdminSubscriber, AdminBar, Clean, Notices }; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\{Processor as AjaxProcessor, Subscriber as AjaxSubscriber}; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\{Processor as FrontendProcessor, Subscriber as FrontendSubscriber }; use WP_Rocket\Engine\Common\PerformanceHints\Cron\{Controller as CronController, Subscriber as CronSubscriber}; use WP_Rocket\Engine\Common\PerformanceHints\WarmUp\{ APIClient, Controller as WarmUpController, Subscriber as WarmUpSubscriber, Queue }; class ServiceProvider extends AbstractServiceProvider { /** * The provides array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ 'config', 'tests', 'ajax_processor', 'performance_hints_ajax_subscriber', 'frontend_processor', 'performance_hints_frontend_subscriber', 'performance_hints_admin_subscriber', 'performance_hints_admin_controller', 'performance_hints_cron_subscriber', 'cron_controller', 'performance_hints_warmup_apiclient', 'performance_hints_warmup_queue', 'performance_hints_warmup_controller', 'performance_hints_warmup_subscriber', 'performance_hints_admin_bar', 'performance_hints_clean', 'performance_hints_notices', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the classes in the container * * @return void */ public function register(): void { $factories = []; $factory_array = [ $this->getContainer()->get( 'atf_factory' ), $this->getContainer()->get( 'lrc_factory' ), $this->getContainer()->get( 'preload_fonts_factory' ), $this->getContainer()->get( 'preconnect_external_domains_factory' ), ]; foreach ( $factory_array as $factory ) { if ( ! $factory->get_context()->is_allowed() ) { continue; } $factories[] = $factory; } $this->getContainer()->addShared( 'ajax_processor', AjaxProcessor::class ) ->addArguments( [ $factories, ] ); $this->getContainer()->addShared( 'performance_hints_ajax_subscriber', AjaxSubscriber::class ) ->addArgument( 'ajax_processor' ); $this->getContainer()->add( 'frontend_processor', FrontendProcessor::class ) ->addArguments( [ $factories, 'options', ] ); $this->getContainer()->add( 'config', Config::class ) ->addArgument( new ArrayArgument( [ 'config_dir_path' => rocket_get_constant( 'WP_ROCKET_CONFIG_PATH', '' ), ] ) ); $this->getContainer()->add( 'tests', Tests::class ) ->addArgument( 'config' ); $this->getContainer()->addShared( 'performance_hints_frontend_subscriber', FrontendSubscriber::class ) ->addArguments( [ 'frontend_processor', 'tests', ] ); $this->getContainer()->add( 'performance_hints_admin_controller', AdminController::class ) ->addArguments( [ $factory_array, ] ); $this->getContainer()->add( 'performance_hints_notices', Notices::class ) ->addArguments( [ $factories, ] ); $this->getContainer()->add( 'performance_hints_admin_bar', Adminbar::class ) ->addArguments( [ $factories, new StringArgument( $this->getContainer()->get( 'template_path' ) . '/settings' ), ] ); $this->getContainer()->add( 'performance_hints_clean', Clean::class ); $this->getContainer()->addShared( 'performance_hints_admin_subscriber', AdminSubscriber::class ) ->addArguments( [ 'performance_hints_admin_controller', 'performance_hints_admin_bar', 'performance_hints_clean', 'performance_hints_notices', ] ); $this->getContainer()->add( 'cron_controller', CronController::class ) ->addArgument( $factory_array ); $this->getContainer()->addShared( 'performance_hints_cron_subscriber', CronSubscriber::class ) ->addArgument( 'cron_controller' ); $this->getContainer()->add( 'performance_hints_warmup_apiclient', APIClient::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'performance_hints_warmup_queue', Queue::class ); $this->getContainer()->add( 'performance_hints_warmup_controller', WarmUpController::class ) ->addArguments( [ $factories, 'options', 'performance_hints_warmup_apiclient', 'user', 'performance_hints_warmup_queue', ] ); $this->getContainer()->addShared( 'performance_hints_warmup_subscriber', WarmUpSubscriber::class ) ->addArgument( 'performance_hints_warmup_controller' ); } } Engine/Common/PerformanceHints/AJAX/Processor.php 0000644 00000002417 15174677547 0015716 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\PerformanceHints\AJAX; class Processor { /** * Array of Factories. * * @var array */ private $factories; /** * Instantiate the class * * @param array $factories Array of factories. */ public function __construct( array $factories ) { $this->factories = $factories; } /** * Checks existing data for various performance hints feature using their factories, * then encodes the result in a single instance. * * @return void */ public function check_data(): void { $payload = $this->get_payload( $this->factories, 'check_data' ); wp_send_json_success( $payload ); } /** * Adds performance hints data to DB. * * @return void */ public function add_data() { $payload = $this->get_payload( $this->factories, 'add_data' ); wp_send_json_success( $payload ); } /** * Gets the response for ajax request. * * @param array $factories Array of factories. * @param string $method Ajax product method name. * @return array */ private function get_payload( array $factories, string $method ): array { $payload = []; foreach ( $factories as $factory ) { $payload = array_merge( $payload, $factory->get_ajax_controller()->$method() ); } return $payload; } } Engine/Common/PerformanceHints/AJAX/ControllerInterface.php 0000644 00000000521 15174677547 0017675 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\PerformanceHints\AJAX; interface ControllerInterface { /** * Initiates the addition of data. * * @return array */ public function add_data(): array; /** * Initiates the checking of data. * * @return array */ public function check_data(): array; } Engine/Common/PerformanceHints/AJAX/Subscriber.php 0000644 00000002157 15174677547 0016043 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\AJAX; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Processor Instance. * * @var Processor */ private $processor; /** * Instantiate the class * * @param Processor $processor Processor Instance. */ public function __construct( Processor $processor ) { $this->processor = $processor; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events(): array { return [ 'wp_ajax_rocket_beacon' => 'add_data', 'wp_ajax_nopriv_rocket_beacon' => 'add_data', 'wp_ajax_rocket_check_beacon' => 'check_data', 'wp_ajax_nopriv_rocket_check_beacon' => 'check_data', ]; } /** * Callback for data received from beacon script * * @return void */ public function add_data() { $this->processor->add_data(); } /** * Callback for checking data * * @return void */ public function check_data() { $this->processor->check_data(); } } Engine/Common/PerformanceHints/AJAX/AJAXControllerTrait.php 0000644 00000001310 15174677547 0017521 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\AJAX; trait AJAXControllerTrait { /** * Get status code and message to be saved into the database * * @param string $status Current status code from $_POST. * @return array */ protected function get_status_code_message( string $status ): array { $status_code = 'success' !== $status ? 'failed' : 'completed'; $status_message = ''; switch ( $status ) { case 'script_error': $status_message = esc_html__( 'Script error', 'rocket' ); break; case 'timeout': $status_message = esc_html__( 'Script timeout', 'rocket' ); break; } return [ $status_code, $status_message, ]; } } Engine/Common/PerformanceHints/Database/Queries/AbstractQueries.php 0000644 00000006603 15174677547 0021437 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Database\Queries; use WP_Rocket\Dependencies\BerlinDB\Database\Query; class AbstractQueries extends Query { /** * Cleanup interval. * * @var int */ public $cleanup_interval; /** * Table status. * * @var boolean */ public static $table_exists = false; /** * Get row for specific url. * * @param string $url Page Url. * @param bool $is_mobile if the request is for mobile page. * * @return false|mixed */ public function get_row( string $url, bool $is_mobile = false ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $query = $this->query( [ 'url' => untrailingslashit( $url ), 'is_mobile' => $is_mobile, ] ); if ( empty( $query[0] ) ) { return false; } return $query[0]; } /** * Delete DB row by url. * * @param string $url Page url to be deleted. * * @return bool */ public function delete_by_url( string $url ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $items = $this->get_rows_by_url( $url ); if ( ! $items ) { return false; } $deleted = true; foreach ( $items as $item ) { if ( ! is_object( $item ) || ! isset( $item->id ) ) { continue; } $deleted = $deleted && $this->delete_item( $item->id ); } return $deleted; } /** * Get the count of not completed rows. * * @return int */ public function get_not_completed_count() { if ( ! self::$table_exists && ! $this->table_exists() ) { return 0; } return $this->query( [ 'count' => true, 'status__in' => [ 'pending', 'in-progress' ], ] ); } /** * Get the count of completed rows. * * @return int */ public function get_completed_count() { if ( ! self::$table_exists && ! $this->table_exists() ) { return 0; } return $this->query( [ 'count' => true, 'status' => 'completed', ] ); } /** * Returns the current status of the table; true if it exists, false otherwise. * * @return boolean */ protected function table_exists(): bool { if ( self::$table_exists ) { return true; } // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } // Query statement. $query = 'SELECT table_name FROM information_schema.tables WHERE table_schema = %s AND table_name = %s LIMIT 1'; $prepared = $db->prepare( $query, $db->__get( 'dbname' ), $db->{$this->table_name} ); $result = $db->get_var( $prepared ); // Does the table exist? $exists = $this->is_success( $result ); if ( $exists ) { self::$table_exists = $exists; } return $exists; } /** * Get all rows with the same url (desktop and mobile versions). * * @param string $url Page url. * * @return array|false */ public function get_rows_by_url( string $url ) { if ( ! self::$table_exists && ! $this->table_exists() ) { return false; } $query = $this->query( [ 'url' => untrailingslashit( $url ), ] ); if ( empty( $query ) ) { return false; } return $query; } /** * Set cleanup interval * * @param int $interval The interval duration, usually default to 1. * * @return object */ public function set_cleanup_interval( int $interval ): object { $this->cleanup_interval = $interval; return $this; } } Engine/Common/PerformanceHints/Database/Queries/QueriesInterface.php 0000644 00000001335 15174677547 0021571 0 ustar 00 <?php /** * The Queries interface defines the contract for database query operations. */ namespace WP_Rocket\Engine\Common\PerformanceHints\Database\Queries; interface QueriesInterface { /** * Deletes old rows from the database. * * This method is used to delete rows from the database that have not been accessed in the last month. * * @return bool|int Returns a boolean or integer value. The exact return value depends on the implementation. */ public function delete_old_rows(); /** * Sets the cleanup interval. * * This method sets the interval at which the cleanup process should run. * * @param int $interval The interval in seconds. */ public function set_cleanup_interval( int $interval ); } Engine/Common/PerformanceHints/Database/Table/AbstractTable.php 0000644 00000003263 15174677547 0020462 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\PerformanceHints\Database\Table; use WP_Rocket\Dependencies\BerlinDB\Database\Table; class AbstractTable extends Table implements TableInterface { /** * Table schema data. * * @var string */ protected $schema_data; /** * Instantiate class. */ public function __construct() { parent::__construct(); add_action( 'admin_init', [ $this, 'maybe_trigger_recreate_table' ], 9 ); add_action( 'init', [ $this, 'maybe_upgrade' ] ); } /** * Setup the database schema * * @return void */ protected function set_schema() { if ( ! $this->schema_data ) { return; } $this->schema = $this->schema_data; } /** * Remove all completed rows. * * @return bool|int */ public function remove_all_completed_rows() { if ( ! $this->exists() ) { return false; } // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $prefixed_table_name = $this->apply_prefix( $this->table_name ); return $db->query( "DELETE FROM `$prefixed_table_name` WHERE status IN ( 'failed', 'completed' )" ); } /** * Truncate DB table. * * @return bool */ public function truncate_table(): bool { if ( ! $this->exists() ) { return false; } return $this->truncate(); } /** * Returns name from table. * * @return string */ public function get_name() { return $this->apply_prefix( $this->table_name ); } /** * Trigger recreation of cache table if not exist. * * @return void */ public function maybe_trigger_recreate_table() { if ( $this->exists() ) { return; } delete_option( $this->db_version_key ); } } Engine/Common/PerformanceHints/Database/Table/TableInterface.php 0000644 00000001053 15174677547 0020612 0 ustar 00 <?php /** * The Table interface defines the contract for database table operations. */ namespace WP_Rocket\Engine\Common\PerformanceHints\Database\Table; interface TableInterface { /** * Truncates the database table. * * This method is used to delete all rows from the database table. * * @return bool Returns a boolean value indicating the success or failure of the operation. */ public function truncate_table(): bool; /** * Remove all completed rows. * * @return bool|int */ public function remove_all_completed_rows(); } Engine/Common/PerformanceHints/Activation/ServiceProvider.php 0000644 00000006741 15174677547 0020434 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Activation; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Common\PerformanceHints\WarmUp\{APIClient, Controller as WarmUpController, Subscriber as WarmUpSubscriber, Queue}; use WP_Rocket\Engine\Media\AboveTheFold\Context\Context as ATFContext; use WP_Rocket\Engine\Media\AboveTheFold\Activation\ActivationFactory as ATFActivationFactory; use WP_Rocket\Engine\Optimization\LazyRenderContent\Activation\ActivationFactory as LRCActivationFactory; use WP_Rocket\Engine\Optimization\LazyRenderContent\Context\Context as LRCContext; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Context\Context as PreconnectContext; class ServiceProvider extends AbstractServiceProvider { /** * The provides array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ 'performance_hints_warmup_apiclient', 'performance_hints_warmup_queue', 'performance_hints_warmup_controller', 'performance_hints_activation', 'performance_hints_warmup_subscriber', 'atf_context', 'atf_activation_factory', 'lrc_context', 'lrc_activation_factory', 'preconnect_external_domains_context', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'atf_context', ATFContext::class ); $this->getContainer()->addShared( 'atf_activation_factory', ATFActivationFactory::class ) ->addArguments( [ 'atf_context', ] ); $this->getContainer()->add( 'lrc_context', LRCContext::class ); $this->getContainer()->addShared( 'lrc_activation_factory', LRCActivationFactory::class ) ->addArguments( [ 'lrc_context', ] ); $this->getContainer()->add( 'preconnect_external_domains_context', PreconnectContext::class ); $factories = []; $atf_activation_factory = $this->getContainer()->get( 'atf_activation_factory' ); if ( $atf_activation_factory->get_context()->is_allowed() ) { $factories[] = $atf_activation_factory; } $lrc_activation_factory = $this->getContainer()->get( 'lrc_activation_factory' ); if ( $lrc_activation_factory->get_context()->is_allowed() ) { $factories[] = $lrc_activation_factory; } $this->getContainer()->add( 'performance_hints_warmup_apiclient', APIClient::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'performance_hints_warmup_queue', Queue::class ); $this->getContainer()->add( 'performance_hints_warmup_controller', WarmUpController::class ) ->addArguments( [ $factories, 'options', 'performance_hints_warmup_apiclient', 'user', 'performance_hints_warmup_queue', ] ); $this->getContainer()->addShared( 'performance_hints_warmup_subscriber', WarmUpSubscriber::class ) ->addArgument( 'performance_hints_warmup_controller' ); $this->getContainer()->add( 'performance_hints_activation', Activation::class ) ->addArguments( [ 'performance_hints_warmup_controller', $factories, ] ); } } Engine/Common/PerformanceHints/Activation/Activation.php 0000644 00000002001 15174677547 0017403 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Activation; use WP_Rocket\Engine\Activation\ActivationInterface; use WP_Rocket\Engine\Common\PerformanceHints\WarmUp\Controller; class Activation implements ActivationInterface { /** * WarmUp controller * * @var Controller */ private $controller; /** * Array of factories. * * @var array */ private $factories; /** * Instantiate class. * * @param Controller $controller Controller instance. * @param array $factories Array of factories. */ public function __construct( Controller $controller, array $factories ) { $this->controller = $controller; $this->factories = $factories; } /** * Add actions on activation. */ public function activate() { add_action( 'rocket_after_activation', [ $this, 'warm_up' ] ); } /** * Start warm up. * * @return void */ public function warm_up() { if ( empty( $this->factories ) ) { return; } $this->controller->warm_up_home(); } } Engine/Common/PerformanceHints/Frontend/Processor.php 0000644 00000014502 15174677547 0016750 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Frontend; use WP_Rocket\Admin\Options_Data; use WP_Filesystem_Direct; use WP_Rocket\Engine\Common\Utils; class Processor { /** * Array of Factories. * * @var array */ private $factories; /** * Options instance * * @var Options_Data */ private $options; /** * WordPress filesystem. * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Instantiate the class * * @param array $factories Array of factories. * @param Options_Data $options Options instance. * @param WP_Filesystem_Direct|null $filesystem WordPress filesystem. */ public function __construct( array $factories, Options_Data $options, ?WP_Filesystem_Direct $filesystem = null ) { $this->factories = $factories; $this->options = $options; $this->filesystem = $filesystem ?: rocket_direct_filesystem(); } /** * Apply Performance Hints Optimizations. * * @param string $html HTML content. * @return string */ public function maybe_apply_optimizations( string $html ): string { if ( empty( $this->factories ) ) { return $html; } if ( is_user_logged_in() && $this->options->get( 'cache_logged_user', 0 ) ) { return $html; } global $wp; $url = untrailingslashit( home_url( add_query_arg( [], $wp->request ) ) ); $is_mobile = $this->is_mobile(); // Set flag as true by default. $optimization_applied = true; foreach ( $this->factories as $factory ) { $row = $factory->queries()->get_row( $url, $is_mobile ); if ( empty( $row ) ) { // Flag false if optimization has not been applied. $optimization_applied = false; continue; } $html = $factory->get_frontend_controller()->optimize( $html, $row ); } // Check if all optimizations were applied: if not, inject beacon. if ( ! $optimization_applied ) { $html = $this->inject_beacon( $html, $url, $is_mobile ); } return $html; } /** * The `inject_beacon` function is used to inject a JavaScript beacon into the HTML content * * @param string $html The HTML content where the beacon will be injected. * @param string $url The current URL. * @param bool $is_mobile True for mobile device, false otherwise. * * @return string The modified HTML content with the beacon script injected just before the closing body tag. */ private function inject_beacon( $html, $url, $is_mobile ): string { if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) && ! Utils::is_saas_visit() ) { return $html; } $min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; if ( ! $this->filesystem->exists( rocket_get_constant( 'WP_ROCKET_ASSETS_JS_PATH' ) . 'wpr-beacon' . $min . '.js' ) ) { return $html; } $default_width_threshold = $is_mobile ? 393 : 1600; $default_height_threshold = $is_mobile ? 830 : 700; /** * Filters the width threshold for the beacon. * * @param int $width_threshold The width threshold. Default is 393 for mobile and 1920 for others. * @param bool $is_mobile True if the current device is mobile, false otherwise. * @param string $url The current URL. * * @return int The filtered width threshold. */ $width_threshold = rocket_apply_filter_and_deprecated( 'rocket_performance_hints_optimization_width_threshold', [ $default_width_threshold, $is_mobile, $url ], '3.16.4', 'rocket_lcp_width_threshold' ); /** * Filters the height threshold for the beacon. * * @param int $height_threshold The height threshold. Default is 830 for mobile and 1080 for others. * @param bool $is_mobile True if the current device is mobile, false otherwise. * @param string $url The current URL. * * @return int The filtered height threshold. */ $height_threshold = rocket_apply_filter_and_deprecated( 'rocket_performance_hints_optimization_height_threshold', [ $default_height_threshold, $is_mobile, $url ], '3.16.4', 'rocket_lcp_height_threshold' ); if ( ! is_int( $width_threshold ) ) { $width_threshold = $default_width_threshold; } if ( ! is_int( $height_threshold ) ) { $height_threshold = $default_height_threshold; } $default_delay = 500; /** * Filters the delay before the beacon is triggered. * * @param int $delay The delay in milliseconds. Default is 500. */ $delay = rocket_apply_filter_and_deprecated( 'rocket_performance_hints_optimization_delay', [ $default_delay ], '3.16.4', 'rocket_lcp_delay' ); if ( ! is_int( $delay ) ) { $delay = $default_delay; } $data = [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'rocket_beacon' ), 'url' => $url, 'is_mobile' => $is_mobile, 'width_threshold' => $width_threshold, 'height_threshold' => $height_threshold, 'delay' => $delay, 'debug' => rocket_get_constant( 'WP_ROCKET_DEBUG' ), 'status' => [], ]; $data_modified = null; foreach ( $this->factories as $factory ) { $data = $data_modified ?? $data; $data_modified = $factory->get_frontend_controller()->add_custom_data( $data ); } $inline_script = '<script>var rocket_beacon_data = ' . wp_json_encode( $data_modified ) . '</script>'; // Get the URL of the script. $script_url = rocket_get_constant( 'WP_ROCKET_ASSETS_JS_URL' ) . 'wpr-beacon' . $min . '.js'; // Create the script tag. $script_tag = "<script data-name=\"wpr-wpr-beacon\" src='{$script_url}' async></script>"; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript $last_body_tag_position = strrpos( $html, '</body>' ); if ( false !== $last_body_tag_position ) { // Append the script tag just before the last closing body tag especially in cases where there's an iframe. $html = substr_replace( $html, $inline_script . $script_tag . '</body>', $last_body_tag_position, 7 ); } else { // Append to the end of html if </body> is not found. $html .= $inline_script . $script_tag; } return $html; } /** * Determines if the page is mobile and separate cache for mobile files is enabled. * * @return bool */ private function is_mobile(): bool { return $this->options->get( 'cache_mobile', 0 ) && $this->options->get( 'do_caching_mobile_files', 0 ) && wp_is_mobile(); } } Engine/Common/PerformanceHints/Frontend/ControllerInterface.php 0000644 00000001052 15174677547 0020731 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\PerformanceHints\Frontend; interface ControllerInterface { /** * Applies optimization. * * @param string $html HTML content. * @param object $row Database Row. * * @return string */ public function optimize( string $html, $row ): string; /** * Add custom data like the List of elements to be considered for optimization. * * @param array $data Array of data passed in beacon. * * @return array */ public function add_custom_data( array $data ): array; } Engine/Common/PerformanceHints/Frontend/Subscriber.php 0000644 00000004500 15174677547 0017071 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Frontend; use WP_Rocket\Buffer\Tests; use WP_Rocket\Engine\Common\Utils; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Processor Instance. * * @var Processor */ private $processor; /** * Buffer tests to run against current page, to decide if we can start the buffer or not. * * @var Tests */ private $buffer_tests; /** * Instantiate the class * * @param Processor $processor Processor Instance. * @param Tests $buffer_tests Buffer tests instance. */ public function __construct( Processor $processor, Tests $buffer_tests ) { $this->processor = $processor; $this->buffer_tests = $buffer_tests; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_buffer' => [ 'maybe_apply_optimizations', 17 ], 'rocket_performance_hints_buffer' => [ 'maybe_apply_optimizations', 17 ], 'template_redirect' => [ 'start_performance_hints_buffer', 3 ], ]; } /** * Apply performance hints optimizations. * * @param string $html HTML content. * * @return string */ public function maybe_apply_optimizations( $html ): string { if ( empty( $html ) || ( ! Utils::is_saas_visit() && Utils::is_inspector_visit() ) ) { return $html; } return $this->processor->maybe_apply_optimizations( $html ); } /** * Start performance hints buffer * * @return void */ public function start_performance_hints_buffer() { if ( ! Utils::is_saas_visit() && ! Utils::is_inspector_visit() ) { return; } if ( ! $this->buffer_tests->can_process_any_buffer() ) { return; } ob_start( [ $this, 'performance_hints_buffer' ] ); } /** * Update images that have no width/height with real dimensions for the SaaS * * @param string $buffer Page HTML content. * * @return string Page HTML content after update. */ public function performance_hints_buffer( $buffer ) { /** * Filters the buffer content for performance hints. * * @since 3.17 * * @param $buffer string HTML content. */ return wpm_apply_filters_typed( 'string', 'rocket_performance_hints_buffer', $buffer ); } } Engine/Common/PerformanceHints/ActivationFactoryInterface.php 0000644 00000000465 15174677547 0020467 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\PerformanceHints; use WP_Rocket\Engine\Common\Context\ContextInterface; interface ActivationFactoryInterface { /** * Provides a Context interface * * @return ContextInterface */ public function get_context(): ContextInterface; } Engine/Common/PerformanceHints/Admin/Subscriber.php 0000644 00000012506 15174677547 0016347 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Admin; use WP_Admin_Bar; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Controller instance. * * @var Controller */ private $controller; /** * AdminBar instance. * * @var AdminBar */ private $admin_bar; /** * Clean Instance. * * @var Clean */ private $clean; /** * Notices Instance. * * @var Notices */ private $notices; /** * Instantiate the class * * @param Controller $controller Controller instance. * @param AdminBar $admin_bar Admin bar instance. * @param Clean $clean Clean instance. * @param Notices $notices Notices instance. */ public function __construct( Controller $controller, AdminBar $admin_bar, Clean $clean, Notices $notices ) { $this->controller = $controller; $this->admin_bar = $admin_bar; $this->clean = $clean; $this->notices = $notices; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events(): array { return [ 'switch_theme' => 'truncate_tables', 'permalink_structure_changed' => 'truncate_tables', 'rocket_domain_options_changed' => 'truncate_tables', 'wp_trash_post' => 'delete_post', 'delete_post' => 'delete_post', 'clean_post_cache' => 'delete_post', 'wp_update_comment_count' => 'delete_post', 'edit_term' => 'delete_term', 'pre_delete_term' => 'delete_term', 'rocket_performance_hints_clean_all' => 'truncate_from_admin', 'rocket_performance_hints_clean_url' => 'clean_url', 'wp_rocket_upgrade' => [ 'truncate_on_update', 10, 2 ], 'rocket_admin_bar_items' => [ [ 'add_clear_performance_hints_menu_item' ], [ 'add_clear_url_performance_hints_menu_item' ], ], 'admin_notices' => [ [ 'clean_performance_hint_result' ], ], 'rocket_dashboard_actions' => 'display_dashboard_button', 'admin_post_rocket_clean_performance_hints' => 'clean_performance_hints', 'admin_post_rocket_clean_performance_hints_url' => 'clean_url_performance_hints', ]; } /** * Callback for truncating performance hints tables * * @return void */ public function truncate_tables(): void { $this->controller->truncate_tables(); } /** * Callback for deleting row or update post. * * @param int $post_id The post ID. * * @return void */ public function delete_post( int $post_id ): void { $this->controller->delete_post( $post_id ); } /** * Callback for Deleting Performance hints optimization row on update or delete term. * * @param int $term_id The term ID. * * @return void */ public function delete_term( int $term_id ): void { $this->controller->delete_term( $term_id ); } /** * Deletes rows when triggering clean from admin * * @param array $clean An array containing the status and message. * * @return array */ public function truncate_from_admin( $clean ): array { return $this->controller->truncate_from_admin( $clean ); } /** * Cleans rows for the current URL. * * @return void */ public function clean_url() { $this->controller->clean_url(); } /** * Truncate Performance hints optimization tables on update to 3.16.1 and higher * * @param string $new_version New plugin version. * @param string $old_version Old plugin version. * * @return void */ public function truncate_on_update( $new_version, $old_version ) { $this->controller->truncate_on_update( $new_version, $old_version ); } /** * Add clear performance hints data to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * * @return void */ public function add_clear_performance_hints_menu_item( WP_Admin_Bar $wp_admin_bar ): void { $this->admin_bar->add_clear_performance_menu_item( $wp_admin_bar ); } /** * Add clear performance data hints for current url to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * * @return void */ public function add_clear_url_performance_hints_menu_item( WP_Admin_Bar $wp_admin_bar ): void { $this->admin_bar->add_clear_url_performance_hints_menu_item( $wp_admin_bar ); } /** * Display the dashboard button to clear performance data hints features * * @return void */ public function display_dashboard_button() { $this->admin_bar->display_dashboard_button(); } /** * Truncate Performance Hints tables when clicking on the dashboard button/menu * * @return void */ public function clean_performance_hints(): void { $this->clean->clean_performance_hints(); } /** * Truncate performance hints the current URL. * * @return void */ public function clean_url_performance_hints(): void { $this->clean->clean_url_performance_hints(); } /** * Show admin notice after clearing Performance Hints tables. * * @return void */ public function clean_performance_hint_result(): void { $this->notices->clean_performance_hint_result(); } } Engine/Common/PerformanceHints/Admin/Clean.php 0000644 00000002565 15174677547 0015272 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Admin; use WP_Rocket\Engine\Admin\Settings\DataClearingTrait; class Clean { use DataClearingTrait; /** * Truncate performance hints tables when clicking on the dashboard button * * @return void */ public function clean_performance_hints(): void { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_clean_performance_hints' ) ) { wp_nonce_ays( '' ); } /** * Filters the value of the Performance hints clean * * @since 3.17 * * @param array $clean An array containing the status and message. */ $clean = wpm_apply_filters_typed( 'array', 'rocket_performance_hints_clean_all', [] ); $this->clean_data( $clean, 'rocket_performance_hints_clear_message' ); } /** * Clean performance hints data for current url. * * @return void */ public function clean_url_performance_hints(): void { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } check_admin_referer( 'rocket_clean_performance_hints_url' ); /** * Fires when cleaning a single URL for the performance hints data * * @since 3.17 */ do_action( 'rocket_performance_hints_clean_url' ); wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } } Engine/Common/PerformanceHints/Admin/AdminBar.php 0000644 00000005100 15174677547 0015711 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Admin; use WP_Admin_Bar; use WP_Rocket\Abstract_Render; use WP_Rocket\Engine\Admin\Settings\AdminBarMenuTrait; class AdminBar extends Abstract_Render { use AdminBarMenuTrait; /** * Array of factories * * @var array */ private $factories; /** * Constructor * * @param array $factories Array of factories. * @param string $template_path Template path. */ public function __construct( array $factories, $template_path ) { parent::__construct( $template_path ); $this->factories = $factories; } /** * Add performance hints data to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * * @return void */ public function add_clear_performance_menu_item( WP_Admin_Bar $wp_admin_bar ): void { if ( empty( $this->factories ) ) { return; } $title = __( 'Clear Priority Elements', 'rocket' ); $action = 'rocket_clean_performance_hints'; $this->add_menu_to_admin_bar( $wp_admin_bar, 'clear-performance-hints', $title, $action ); } /** * Add clear performance hints URL data to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * * @return void */ public function add_clear_url_performance_hints_menu_item( WP_Admin_Bar $wp_admin_bar ) { global $post; /** * Filters the rocket `clear performance hints data of this url` option on admin bar menu. * * @since 3.17 * * @param bool $should_skip Should skip adding `clear performance hints of this url` option in admin bar. * @param type $post Post object. */ if ( wpm_apply_filters_typed( 'boolean', 'rocket_skip_admin_bar_clean_performance_hints_option', false, $post ) ) { return; } $action = 'rocket_clean_performance_hints_url'; $title = __( 'Clear Priority Elements of this URL', 'rocket' ); $this->add_url_menu_item_to_admin_bar( $wp_admin_bar, 'clear-performance-hints-data-url', $title, $action, ! empty( $this->factories ) ); } /** * Display the dashboard button to clear performance hints data * * @return void */ public function display_dashboard_button() { $context = ! empty( $this->factories ); $this->dashboard_button( $context, __( 'Priority Elements', 'rocket' ), esc_html__( 'Clear', 'rocket' ), 'rocket_clean_performance_hints', __( 'Reset stored optimizations related to Automatic Lazy Rendering, Critical Images, Preconnect to External Domains, and Preload Fonts.', 'rocket' ) ); } } Engine/Common/PerformanceHints/Admin/Notices.php 0000644 00000001707 15174677547 0015651 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Admin; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Engine\Common\Context\ContextInterface; class Notices { /** * Array of factories * * @var array */ private $factories; /** * Constructor * * @param array $factories Array of factories. */ public function __construct( array $factories ) { $this->factories = $factories; } /** * Show admin notice after clearing performance hints tables. * * @return void */ public function clean_performance_hint_result() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( empty( $this->factories ) ) { return; } $response = get_transient( 'rocket_performance_hints_clear_message' ); if ( ! $response ) { return; } delete_transient( 'rocket_performance_hints_clear_message' ); rocket_notice_html( $response ); } } Engine/Common/PerformanceHints/Admin/Controller.php 0000644 00000010101 15174677547 0016354 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\PerformanceHints\Admin; class Controller { /** * Array of factories * * @var array */ private $factories; /** * Constructor * * @param array $factories Array of factories. */ public function __construct( array $factories ) { $this->factories = $factories; } /** * Truncate performance hints optimization tables. * * @return void */ public function truncate_tables() { $this->delete_rows(); } /** * Deletes the rows from the table * * @return void */ private function delete_rows() { foreach ( $this->factories as $factory ) { if ( 0 < $factory->queries()->get_not_completed_count() ) { $factory->table()->remove_all_completed_rows(); continue; } $factory->table()->truncate_table(); } /** * Fires after clearing performance hints optimization data. */ rocket_do_action_and_deprecated( 'rocket_after_clear_performance_hints_data', [], '3.16.4', 'rocket_after_clear_atf' ); } /** * Delete row on update Post or delete post. * * @param int $post_id The post ID. * * @return void */ public function delete_post( $post_id ) { if ( 'attachment' === get_post_type( $post_id ) ) { return; } $url = get_permalink( $post_id ); // get_permalink should return false or string, but some plugins return null. if ( ! is_string( $url ) ) { return; } $this->delete_by_url( $url ); } /** * Deletes performance hints optimizations when updating a term * * @param int $term_id the term ID. * * @return void */ public function delete_term( $term_id ) { $url = get_term_link( (int) $term_id ); if ( is_wp_error( $url ) ) { return; } $this->delete_by_url( $url ); } /** * Should allow early if true. * * @return bool */ private function is_allowed(): bool { $allowed = false; foreach ( $this->factories as $factory ) { if ( $factory->get_context()->is_allowed() ) { $allowed = true; break; } } return $allowed; } /** * Deletes rows when triggering clean from admin * * @param array $clean An array containing the status and message. * * @return array */ public function truncate_from_admin( $clean ) { if ( ! $this->is_allowed() ) { return $clean; } if ( ! current_user_can( 'rocket_manage_options' ) ) { return [ 'status' => 'die', ]; } $this->delete_rows(); return [ 'status' => 'success', 'message' => sprintf( // translators: %1$s = plugin name. __( '%1$s: Stored optimization data for Automatic Lazy Rendering, Critical Images, Preconnect to External Domains, and Preload Fonts has been cleared! New data will be generated as needed.', 'rocket' ), '<strong>WP Rocket</strong>' ), ]; } /** * Cleans rows for the current URL. * * @return void */ public function clean_url() { if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_nonce_ays( '' ); } $url = wp_get_referer(); if ( 0 !== strpos( $url, 'http' ) ) { $parse_url = get_rocket_parse_url( untrailingslashit( home_url() ) ); $url = $parse_url['scheme'] . '://' . $parse_url['host'] . $url; } /** * Fires after clearing performance hints data for specific url. * * @param string $url Current page URL. */ do_action( 'rocket_performance_hints_data_after_clearing', $url ); $this->delete_by_url( $url ); } /** * Truncate Performance hints optimization tables on update to 3.16.1.1 and higher * * @param string $new_version New plugin version. * @param string $old_version Old plugin version. * * @return void */ public function truncate_on_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.16.1', '>=' ) ) { return; } if ( ! $this->is_allowed() ) { return; } $this->truncate_tables(); } /** * Deletes row by url from table. * * @param string $url Url to delete. * @return void */ private function delete_by_url( string $url ) { foreach ( $this->factories as $factory ) { $factory->queries()->delete_by_url( untrailingslashit( $url ) ); } } } Engine/Common/Queue/RUCSSQueueRunner.php 0000644 00000023131 15174677547 0014123 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\Queue; use WP_Rocket\Logger\Logger; use ActionScheduler_Abstract_QueueRunner; use ActionScheduler_Store; use ActionScheduler_FatalErrorMonitor; use ActionScheduler_AsyncRequest_QueueRunner; class RUCSSQueueRunner extends ActionScheduler_Abstract_QueueRunner { /** * Cron hook name. */ const WP_CRON_HOOK = 'action_scheduler_run_queue_rucss'; /** * Cron schedule interval. */ const WP_CRON_SCHEDULE = 'every_minute'; /** * Async Request Queue Runner instance. * We used the default one from AS. * * @var \ActionScheduler_AsyncRequest_QueueRunner Instance. */ protected $async_request; /** * Current runner instance. * * @var RUCSSQueueRunner Instance. */ private static $runner = null; /** * Current queue group. * * @var string */ private $group = 'rocket-rucss'; /** * Get singleton instance. * * @return RUCSSQueueRunner Instance. */ public static function instance() { if ( null === self::$runner ) { self::$runner = new RUCSSQueueRunner(); } return self::$runner; } /** * ActionScheduler_QueueRunner constructor. * * @param ActionScheduler_Store|null $store Store Instance. * @param ActionScheduler_FatalErrorMonitor|null $monitor Fatal Error monitor instance. * @param Cleaner|null $cleaner Cleaner instance. * @param ActionScheduler_AsyncRequest_QueueRunner|null $async_request Async Request Queue Runner instance. */ public function __construct( ?ActionScheduler_Store $store = null, ?ActionScheduler_FatalErrorMonitor $monitor = null, ?Cleaner $cleaner = null, ?ActionScheduler_AsyncRequest_QueueRunner $async_request = null ) { if ( is_null( $cleaner ) ) { /** * Filters the clean batch size. * * @since 3.11.0.5 * * @param int $batch_size Batch size. * @param string $group The group name. * * @return int */ $batch_size = (int) apply_filters( 'rocket_action_scheduler_clean_batch_size', 100, $this->group ); $cleaner = new Cleaner( $store, $batch_size, $this->group ); } parent::__construct( $store, $monitor, $cleaner ); if ( is_null( $async_request ) ) { $async_request = new \ActionScheduler_AsyncRequest_QueueRunner( $this->store ); } $this->async_request = $async_request; } /** * Initialize the queue runner. */ public function init() { // phpcs:ignore WordPress.WP.CronInterval.CronSchedulesInterval add_filter( 'cron_schedules', [ self::instance(), 'add_wp_cron_schedule' ] ); // Check for and remove any WP Cron hook scheduled by Action Scheduler < 3.0.0, which didn't include the $context param. $next_timestamp = wp_next_scheduled( self::WP_CRON_HOOK ); if ( $next_timestamp ) { wp_unschedule_event( $next_timestamp, self::WP_CRON_HOOK ); } $cron_context = [ 'WP Cron' ]; if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) { $schedule = apply_filters( 'rocket_action_scheduler_run_schedule', self::WP_CRON_SCHEDULE ); wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK, $cron_context ); } // @phpstan-ignore-next-line Action callback returns int but should not return anything. add_action( self::WP_CRON_HOOK, [ self::instance(), 'run' ] ); $this->hook_dispatch_async_request(); } /** * Hook check for dispatching an async request. */ public function hook_dispatch_async_request() { add_action( 'shutdown', [ $this, 'maybe_dispatch_async_request' ] ); } /** * Unhook check for dispatching an async request. */ public function unhook_dispatch_async_request() { remove_action( 'shutdown', [ $this, 'maybe_dispatch_async_request' ] ); } /** * Check if we should dispatch an async request to process actions. * * This method is attached to 'shutdown', so is called frequently. To avoid slowing down * the site, it mitigates the work performed in each request by: * 1. checking if it's in the admin context and then * 2. haven't run on the 'shutdown' hook within the lock time (60 seconds by default) * 3. haven't exceeded the number of allowed batches. * * The order of these checks is important, because they run from a check on a value: * 1. in memory - is_admin() maps to $GLOBALS or the WP_ADMIN constant * 2. in memory - transients use autoloaded options by default * 3. from a database query - has_maximum_concurrent_batches() run the query * $this->store->get_claim_count() to find the current number of claims in the DB. * * If all of these conditions are met, then we request an async runner check whether it * should dispatch a request to process pending actions. */ public function maybe_dispatch_async_request() { if ( is_admin() && ! \ActionScheduler::lock()->is_locked( 'async-request-runner' ) ) { // Only start an async queue at most once every 60 seconds. \ActionScheduler::lock()->set( 'async-request-runner' ); $this->async_request->maybe_dispatch(); } } /** * Process actions in the queue. Attached to self::WP_CRON_HOOK i.e. 'action_scheduler_run_queue' * * The $context param of this method defaults to 'WP Cron', because prior to Action Scheduler 3.0.0 * that was the only context in which this method was run, and the self::WP_CRON_HOOK hook had no context * passed along with it. New code calling this method directly, or by triggering the self::WP_CRON_HOOK, * should set a context as the first parameter. For an example of this, refer to the code seen in * * @see ActionScheduler_AsyncRequest_QueueRunner::handle() * * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * * @return int */ public function run( $context = 'WP Cron' ) { \ActionScheduler_Compatibility::raise_memory_limit(); \ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() ); do_action( 'action_scheduler_before_process_queue' );// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $this->run_cleanup(); $processed_actions = 0; if ( false === $this->has_maximum_concurrent_batches() ) { $batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 );// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound do { $processed_actions_in_batch = $this->do_batch( $batch_size, $context ); $processed_actions += $processed_actions_in_batch; } while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $processed_actions ) ); // keep going until we run out of actions, time, or memory. } do_action( 'action_scheduler_after_process_queue' );// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound return $processed_actions; } /** * Process a batch of actions pending in the queue. * * Actions are processed by claiming a set of pending actions then processing each one until either the batch * size is completed, or memory or time limits are reached, defined by @see $this->batch_limits_exceeded(). * * @param int $size The maximum number of actions to process in the batch. * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' * Generally, this should be capitalised and not localised as it's a proper noun. * @return int The number of actions processed. */ protected function do_batch( $size = 100, $context = '' ) { try { $claim = $this->store->stake_claim( $size, null, [], $this->group ); $this->monitor->attach( $claim ); $processed_actions = 0; foreach ( $claim->get_actions() as $action_id ) { // bail if we lost the claim. if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $claim->get_id() ), true ) ) { break; } $this->process_action( $action_id, $context ); ++$processed_actions; if ( $this->batch_limits_exceeded( $processed_actions ) ) { break; } } $this->store->release_claim( $claim ); $this->monitor->detach(); $this->clear_caches(); $this->reset_group(); return $processed_actions; } catch ( \Exception $exception ) { Logger::debug( $exception->getMessage() ); $this->reset_group(); return 0; } } /** * Reset group in store's claim filter. * * @return void */ private function reset_group() { if ( ! method_exists( $this->store, 'set_claim_filter' ) ) { return; } $this->store->set_claim_filter( 'group', '' ); } /** * Running large batches can eat up memory, as WP adds data to its object cache. * * If using a persistent object store, this has the side effect of flushing that * as well, so this is disabled by default. To enable: * * add_filter( 'action_scheduler_queue_runner_flush_cache', '__return_true' ); */ protected function clear_caches() { if ( ! wp_using_ext_object_cache() || apply_filters( 'action_scheduler_queue_runner_flush_cache', false ) ) {// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound wp_cache_flush(); } } /** * Add the cron schedule. * * @param array $schedules Array of current schedules. * * @return array */ public function add_wp_cron_schedule( $schedules ) { if ( isset( $schedules['every_minute'] ) ) { return $schedules; } $schedules['every_minute'] = [ 'interval' => 60, // in seconds. 'display' => __( 'Every minute', 'rocket' ), ]; return $schedules; } /** * Get the number of concurrent batches a runner allows. * * @return int */ public function get_allowed_concurrent_batches() { return 2; } } Engine/Common/Queue/QueueInterface.php 0000644 00000011625 15174677547 0013737 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\Queue; interface QueueInterface { /** * Enqueue an action to run one time, as soon as possible * * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @return int The action ID */ public function add_async( $hook, $args = [] ); /** * Schedule an action to run once at some time in the future * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @return int The action ID */ public function schedule_single( $timestamp, $hook, $args = [] ); /** * Schedule a recurring action * * @param int $timestamp When the first instance of the job will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @return int The action ID */ public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = [] ); /** * Checks if the hook is scheduled. * * @param string $hook The hook to check. * @param array $args Passed arguments. * * @return bool */ public function is_scheduled( $hook, $args = [] ); /** * Schedule an action that recurs on a cron-like schedule. * * @param int $timestamp The schedule will start on or after this time. * @param string $cron_schedule A cron-link schedule string. * @see http://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ * | | | | | | * | | | | | + year [optional] * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) * | | | +---------- month (1 - 12) * | | +--------------- day of month (1 - 31) * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @return int The action ID */ public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = [] ); /** * Dequeue the next scheduled instance of an action with a matching hook (and optionally matching args and group). * * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. */ public function cancel( $hook, $args = [] ); /** * Dequeue all actions with a matching hook (and optionally matching args and group) so no matching actions are ever run. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. */ public function cancel_all( $hook, $args = [] ); /** * Get the date and time for the next scheduled occurrence of an action with a given hook * (an optionally that matches certain args and group), if any. * * @param string $hook The hook that the job will trigger. * @param array $args Filter to a hook with matching args that will be passed to the job when it runs. * @return int|null The date and time for the next occurrence, or null if there is no pending, scheduled action for the given hook */ public function get_next( $hook, $args = null ); /** * Find scheduled actions. * * @param array $args Possible arguments, with their default values. * 'hook' => '' - the name of the action that will be triggered. * 'args' => null - the args array that will be passed with the action. * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='. * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='. * 'group' => '' - the group the action belongs to. * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING. * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID. * 'per_page' => 5 - Number of results to return. * 'offset' => 0. * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date'. * 'order' => 'ASC'. * @param string $return_format OBJECT, ARRAY_A, or ids. * @return array */ public function search( $args = [], $return_format = OBJECT ); } Engine/Common/Queue/Cleaner.php 0000644 00000012710 15174677547 0012377 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\Queue; use ActionScheduler_QueueCleaner; use ActionScheduler_Store; class Cleaner extends ActionScheduler_QueueCleaner { /** * The duration of clean Hour In seconds. * * @var int */ protected $hour_in_seconds = 60 * 60; /** * Default list of statuses purged by the cleaner process. * * @var string[] */ private $default_statuses_to_purge = [ ActionScheduler_Store::STATUS_COMPLETE, ActionScheduler_Store::STATUS_CANCELED, ]; /** * Store instance. * * @var \ActionScheduler_Store */ private $store = null; /** * Group name to be cleaned. * * @var string */ private $group; /** * Cleaner constructor. * * @param ActionScheduler_Store|null $store The store instance. * @param int $batch_size The batch size. * @param string $group Current queue group. */ public function __construct( ?ActionScheduler_Store $store = null, $batch_size = 20, $group = '' ) { parent::__construct( $store, $batch_size ); $this->store = $store ? $store : ActionScheduler_Store::instance(); $this->group = $group; } /** * Overrides the base method of action scheduler to do the clean process for our actions only. * * @return array */ public function delete_old_actions() { /** * Filter the minimum scheduled date age for action deletion. * * @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted. */ $lifespan = (int) apply_filters( 'action_scheduler_retention_period', $this->hour_in_seconds ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound /** * Filters the retention period for our tasks only. * * @since 3.11.0.5 * * @param int $lifespan Lifespan in seconds. * @param string $group The group name. * * @return int */ $lifespan = (int) apply_filters( 'rocket_action_scheduler_retention_period', $lifespan, $this->group ); try { $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' ); } catch ( \Exception $e ) { _doing_it_wrong( __METHOD__, sprintf( /* Translators: %s is the exception message. */ esc_html__( 'It was not possible to determine a valid cut-off time: %s.', 'rocket' ), esc_html( $e->getMessage() ) ), '3.5.5' ); return []; } $statuses_to_purge = [ \ActionScheduler_Store::STATUS_COMPLETE, \ActionScheduler_Store::STATUS_CANCELED, \ActionScheduler_Store::STATUS_FAILED, ]; return $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() ); } /** * Delete selected actions limited by status and date. * * @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete. * @param \DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago. * @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20. * @param string $context Calling process context. Defaults to `old`. * @return array Actions deleted. */ public function clean_actions( array $statuses_to_purge, \DateTime $cutoff_date, $batch_size = null, $context = 'old' ) { $batch_size = null !== $batch_size ? $batch_size : $this->batch_size; $cutoff = $cutoff_date; $lifespan = time() - $cutoff->getTimestamp(); if ( empty( $statuses_to_purge ) ) { $statuses_to_purge = $this->default_statuses_to_purge; } $deleted_actions = []; foreach ( $statuses_to_purge as $status ) { $actions_to_delete = $this->store->query_actions( [ 'status' => $status, 'modified' => $cutoff, 'modified_compare' => '<=', 'per_page' => $batch_size, 'orderby' => 'none', 'group' => $this->group, ] ); $deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) ); } return $deleted_actions; } /** * Delete actions * * @param int[] $actions_to_delete List of action IDs to delete. * @param int $lifespan Minimum scheduled age in seconds of the actions being deleted. * @param string $context Context of the delete request. * @return array Deleted action IDs. */ private function delete_actions( array $actions_to_delete, $lifespan = null, $context = 'old' ) { $deleted_actions = []; if ( null === $lifespan ) { $lifespan = $this->hour_in_seconds; } foreach ( $actions_to_delete as $action_id ) { try { $this->store->delete_action( $action_id ); $deleted_actions[] = $action_id; } catch ( \Exception $e ) { /** * Notify 3rd party code of exceptions when deleting a completed action older than the retention period * * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their * actions. * * @param int $action_id The scheduled actions ID in the data store * @param \Exception $e The exception thrown when attempting to delete the action from the data store * @param int $lifespan The retention period, in seconds, for old actions * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch * @since 2.0.0 */ do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } } return $deleted_actions; } } Engine/Common/Queue/AbstractASQueue.php 0000644 00000021262 15174677547 0014024 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\Queue; use ActionScheduler_Store; use Exception; use WP_Rocket\Logger\Logger; abstract class AbstractASQueue implements QueueInterface { /** * Queue shared Group. * * @var string */ protected $group = 'rocket'; /** * Enqueue an action to run one time, as soon as possible * * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @return int The action ID. */ public function add_async( $hook, $args = [] ) { try { if ( function_exists( 'as_enqueue_async_action' ) ) { return as_enqueue_async_action( $hook, $args, $this->group ); } return $this->schedule_single( time() + MINUTE_IN_SECONDS, $hook, $args ); } catch ( Exception $exception ) { Logger::error( $exception->getMessage(), [ 'Action Scheduler Queue' ] ); return 0; } } /** * Schedule an action to run once at some time in the future * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @return int The action ID. */ public function schedule_single( $timestamp, $hook, $args = [] ) { try { return as_schedule_single_action( $timestamp, $hook, $args, $this->group ); } catch ( Exception $exception ) { Logger::error( $exception->getMessage(), [ 'Action Scheduler Queue' ] ); return 0; } } /** * Schedule a recurring action * * @param int $timestamp When the first instance of the job will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @return int The action ID. */ public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = [] ) { if ( $this->is_scheduled( $hook, $args ) ) { // TODO: When v3.3.0 from Action Scheduler is commonly used use the array notation for status to reduce search queries to one. $pending_actions = $this->search( [ 'hook' => $hook, 'status' => ActionScheduler_Store::STATUS_PENDING, ], 'ids' ); if ( 1 < count( $pending_actions ) ) { $this->cancel_all( $hook, $args ); return 0; } $running_actions = $this->search( [ 'hook' => $hook, 'status' => ActionScheduler_Store::STATUS_RUNNING, ], 'ids' ); if ( 1 === count( $pending_actions ) + count( $running_actions ) ) { return 0; } $this->cancel_all( $hook, $args ); } try { return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $this->group ); } catch ( Exception $exception ) { Logger::error( $exception->getMessage(), [ 'Action Scheduler Queue' ] ); return 0; } } /** * Checks if the hook is scheduled. * * @param string $hook The hook to check. * @param array $args Passed arguments. * * @return bool */ public function is_scheduled( $hook, $args = [] ) { if ( ! function_exists( 'as_has_scheduled_action' ) ) { return ! is_null( $this->get_next( $hook, $args ) ); } try { return as_has_scheduled_action( $hook, $args, $this->group ); } catch ( Exception $exception ) { Logger::error( $exception->getMessage(), [ 'Action Scheduler Queue' ] ); return false; } } /** * Schedule an action that recurs on a cron-like schedule. * * @param int $timestamp The schedule will start on or after this time. * @param string $cron_schedule A cron-link schedule string. * @see http://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ * | | | | | | * | | | | | + year [optional] * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) * | | | +---------- month (1 - 12) * | | +--------------- day of month (1 - 31) * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @return int The action ID */ public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = [] ) { if ( $this->is_scheduled( $hook, $args ) ) { return 0; } try { return as_schedule_cron_action( $timestamp, $cron_schedule, $hook, $args, $this->group ); } catch ( Exception $exception ) { Logger::error( $exception->getMessage(), [ 'Action Scheduler Queue' ] ); return 0; } } /** * Dequeue the next scheduled instance of an action with a matching hook (and optionally matching args and group). * * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action. * * While technically only the next instance of a recurring or cron action is unscheduled by this method, that will also * prevent all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled * in a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled * only after the former action is run. As the next instance is never run, because it's unscheduled by this function, * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled * by this method also. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. */ public function cancel( $hook, $args = [] ) { try { as_unschedule_action( $hook, $args, $this->group ); } catch ( Exception $exception ) { Logger::error( $exception->getMessage(), [ 'Action Scheduler Queue' ] ); } } /** * Dequeue all actions with a matching hook (and optionally matching args and group) so no matching actions are ever run. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. */ public function cancel_all( $hook, $args = [] ) { try { as_unschedule_all_actions( $hook, $args, $this->group ); } catch ( Exception $exception ) { Logger::error( $exception->getMessage(), [ 'Action Scheduler Queue' ] ); } } /** * Get the date and time for the next scheduled occurrence of an action with a given hook * (an optionally that matches certain args and group), if any. * * @param string $hook The hook that the job will trigger. * @param array $args Filter to a hook with matching args that will be passed to the job when it runs. * @return int|null The date and time for the next occurrence, or null if there is no pending, scheduled action for the given hook. */ public function get_next( $hook, $args = null ) { try { $next_timestamp = as_next_scheduled_action( $hook, $args, $this->group ); if ( is_numeric( $next_timestamp ) ) { return $next_timestamp; } return null; } catch ( Exception $exception ) { Logger::error( $exception->getMessage(), [ 'Action Scheduler Queue' ] ); return null; } } /** * Find scheduled actions * * @param array $args Possible arguments, with their default values: * 'hook' => '' - the name of the action that will be triggered * 'args' => null - the args array that will be passed with the action * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' * 'group' => '' - the group the action belongs to * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID * 'per_page' => 5 - Number of results to return * 'offset' => 0 * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' * 'order' => 'ASC'. * * @param string $return_format OBJECT, ARRAY_A, or ids. * @return array */ public function search( $args = [], $return_format = OBJECT ) { try { return as_get_scheduled_actions( $args, $return_format ); } catch ( Exception $exception ) { Logger::error( $exception->getMessage(), [ 'Action Scheduler Queue' ] ); return []; } } } Engine/Common/JobManager/AbstractFactory/SaasFactory.php 0000644 00000000705 15174677547 0017262 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\JobManager\AbstractFactory; use WP_Rocket\Engine\Common\JobManager\Managers\ManagerInterface; use WP_Rocket\Engine\Common\Database\TableInterface; interface SaasFactory { /** * SaaS job manager. * * @return ManagerInterface */ public function manager(): ManagerInterface; /** * Job table. * * @return TableInterface */ public function table(): TableInterface; } Engine/Common/JobManager/APIHandler/AbstractAPIClient.php 0000644 00000005746 15174677547 0017125 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Common\JobManager\APIHandler; use WP_Error; use WP_Rocket\Admin\Options_Data; abstract class AbstractAPIClient { /** * API URL. */ const API_URL = 'https://saas.wp-rocket.me/'; /** * Part of request Url after the main API_URL. * * @var string */ protected $request_path; /** * Response Code. * * @var int */ protected $response_code = 200; /** * Error message. * * @var string */ protected $error_message = ''; /** * Response Body. * * @var string */ protected $response_body; /** * Plugin options instance. * * @var Options_Data */ protected $options; /** * Instantiate the class. * * @param Options_Data $options Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Handle the request. * * @param array $args Passed arguments. * @param string $type GET or POST. * * @return bool */ private function handle_request( array $args, string $type = 'post' ) { $api_url = rocket_get_constant( 'WP_ROCKET_SAAS_API_URL', false ) ? rocket_get_constant( 'WP_ROCKET_SAAS_API_URL', false ) : self::API_URL; if ( empty( $args['body'] ) ) { $args['body'] = []; } $args['body']['credentials'] = [ 'wpr_email' => $this->options->get( 'consumer_email', '' ), 'wpr_key' => $this->options->get( 'consumer_key', '' ), ]; $args['method'] = strtoupper( $type ); $response = wp_remote_request( $api_url . $this->request_path, $args ); return $this->check_response( $response ); } /** * Handle remote POST. * * @param array $args Array with options sent to Saas API. * * @return bool WP Remote request status. */ protected function handle_post( array $args ): bool { return $this->handle_request( $args ); } /** * Handle remote GET. * * @param array $args Array with options sent to Saas API. * * @return bool WP Remote request status. */ protected function handle_get( array $args ): bool { return $this->handle_request( $args, 'get' ); } /** * Handle SaaS request error. * * @param array|WP_Error $response WP Remote request. * * @return bool */ private function check_response( $response ): bool { $this->response_code = is_array( $response ) ? wp_remote_retrieve_response_code( $response ) : $response->get_error_code(); if ( 200 !== $this->response_code ) { $previous_errors = (int) get_transient( 'wp_rocket_rucss_errors_count' ); set_transient( 'wp_rocket_rucss_errors_count', $previous_errors + 1, 5 * MINUTE_IN_SECONDS ); if ( empty( $response ) ) { $this->error_message = 'API Client Error'; return false; } $this->error_message = is_array( $response ) ? wp_remote_retrieve_response_message( $response ) : $response->get_error_message(); return false; } delete_transient( 'wp_rocket_rucss_errors_count' ); $this->response_body = wp_remote_retrieve_body( $response ); return true; } } Engine/Common/JobManager/APIHandler/APIClient.php 0000644 00000006012 15174677547 0015424 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\JobManager\APIHandler; use WP_Rocket\Logger\LoggerAware; use WP_Rocket\Logger\LoggerAwareInterface; class APIClient extends AbstractAPIClient implements LoggerAwareInterface { use LoggerAware; /** * SaaS main API path. * * @var string */ protected $request_path = 'rucss-job'; /** * Array of Factories. * * @var array */ protected $factories; /** * Calls Central SaaS API. * * @param string $url Page url. * @param array $options Array with options sent to Saas API. * * @return array */ public function add_to_queue( string $url, array $options ): array { $url = add_query_arg( [ 'nowprocket' => 1, 'no_optimize' => 1, ], user_trailingslashit( $url ) ); /** * Filter the url that is sent to Saas when RUCSS and LCP/ATF is on. * * @param string $url contains the URL that is sent to Saas. */ $url = apply_filters( 'rocket_saas_api_queued_url', $url ); $args = [ 'body' => [ 'url' => $url, 'config' => $options, ], 'timeout' => 5, ]; $this->logger::debug( 'Add to queue request arguments', $args ); $sent = $this->handle_post( $args ); if ( ! $sent ) { $output = [ 'code' => $this->response_code, 'message' => $this->error_message, ]; $this->logger::error( 'Add to queue request failure', $output ); return $output; } $default = [ 'code' => 400, 'message' => 'No message. Defaulted in add_to_queue', 'contents' => [ 'jobId' => '0', 'queueName' => '', ], ]; $result = json_decode( $this->response_body, true ); $this->logger::debug( $url . ' - Add to queue response body', $result ); if ( key_exists( 'code', $result ) && 401 === $result['code'] ) { update_option( 'wp_rocket_no_licence', true ); update_rocket_option( 'remove_unused_css', 0 ); } return wp_parse_args( (array) $result, $default ); } /** * Get job status from RUCSS queue. * * @param string $job_id Job ID. * @param string $queue_name Queue Name. * @param bool $is_home Is home or not. * * @return array */ public function get_queue_job_status( $job_id, $queue_name, $is_home = false ) { $args = [ 'body' => [ 'id' => $job_id, 'force_queue' => $queue_name, 'is_home' => $is_home, ], 'timeout' => 5, ]; if ( ! $this->handle_get( $args ) ) { return [ 'code' => $this->response_code, 'message' => $this->error_message, ]; } $default = [ 'code' => 400, 'status' => 'failed', 'message' => 'No message. Defaulted in get_queue_job_status', 'contents' => [ 'success' => false, 'shakedCSS' => '', 'above_the_fold_result' => [ 'lcp' => [], 'images_above_fold' => [], ], ], ]; $result = json_decode( $this->response_body, true ); return (array) wp_parse_args( ( $result && $result['returnvalue'] ) ? (array) $result['returnvalue'] : [], $default ); } } Engine/Common/JobManager/APIHandler/AbstractSafeAPIClient.php 0000644 00000011131 15174677547 0017705 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager\APIHandler; use WP_Error; /** * Class AbstractSafeAPIClient * * This abstract class provides a base for making API requests. * It includes methods for sending GET and POST requests, and handles transient caching to mitigate the impact of API failures. * * @package WP_Rocket\Engine\Common\JobManager\APIHandler */ abstract class AbstractSafeAPIClient { /** * Get the transient key. * * @return string The transient key. */ abstract protected function get_transient_key(); /** * Get the API URL. * * @return string The API URL. */ abstract protected function get_api_url(); /** * Send a GET request. * * @param array $params The request parameters. * @param bool $safe Send safe request WP functions or not, default to not. * * @return mixed The response from the API. */ public function send_get_request( $params = [], $safe = false ) { return $this->send_request( 'GET', $params, $safe ); } /** * Send a POST request. * * @param array $params The request parameters. * @param bool $safe Send safe request WP functions or not, default to not. * * @return mixed The response from the API. */ public function send_post_request( $params = [], $safe = false ) { return $this->send_request( 'POST', $params, $safe ); } /** * Send a request to the API. * * @param string $method The HTTP method (GET or POST). * @param array $params The request parameters. * @param bool $safe Send safe request WP functions or not, default to not. * @return mixed The response from the API, or WP_Error if a timeout is active. */ private function send_request( $method, $params = [], $safe = false ) { $api_url = $this->get_api_url(); $transient_key = $this->get_transient_key(); if ( get_transient( $transient_key . '_timeout_active' ) ) { return new WP_Error( 429, 'Too many requests.' ); } // Get previous_expiration early to avoid multiple parallel requests increasing the expiration multiple times. $previous_expiration = (int) get_transient( $transient_key . '_timeout' ); $params['method'] = strtoupper( $method ); $response = $this->send_remote_request( $api_url, $method, $params, $safe ); if ( is_wp_error( $response ) ) { $this->set_timeout_transients( $previous_expiration ); return $response; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) || ( ! empty( $response['response']['code'] ) && 200 !== $response['response']['code'] ) ) { $this->set_timeout_transients( $previous_expiration ); return new WP_Error( 500, 'Not valid response.' ); } $this->delete_timeout_transients(); return $response; } /** * Set the timeout transients. * * @param string $previous_expiration The previous value of _timeout_active transient. */ private function set_timeout_transients( $previous_expiration ) { $transient_key = $this->get_transient_key(); $expiration = ( 0 === (int) $previous_expiration ) ? 300 : ( 2 * (int) $previous_expiration <= DAY_IN_SECONDS ? 2 * (int) $previous_expiration : DAY_IN_SECONDS ); set_transient( $transient_key . '_timeout', $expiration, WEEK_IN_SECONDS ); set_transient( $transient_key . '_timeout_active', true, $expiration ); } /** * Delete the timeout transients. * * This method deletes the timeout transients for the API requests. It uses the transient key obtained from the `get_transient_key` method. * The transients deleted are: * - `{transient_key}_timeout_active`: This transient indicates if a timeout is currently active. * - `{transient_key}_timeout`: This transient stores the timeout duration. * * @return void */ private function delete_timeout_transients() { $transient_key = $this->get_transient_key(); delete_transient( $transient_key . '_timeout_active' ); delete_transient( $transient_key . '_timeout' ); } /** * Decide which WP core function will be used to send the request based on the params. * * @param string $api_url API Url. * @param string $method Request method (GET or POST). * @param array $params Parameters being sent with the request. * @param bool $safe Send safe request WP functions or not, default to not. * @return array|WP_Error */ private function send_remote_request( $api_url, $method, $params, $safe ) { if ( ! $safe ) { return wp_remote_request( $api_url, $params ); } unset( $params['method'] ); switch ( $method ) { case 'GET': return wp_safe_remote_get( $api_url, $params ); case 'POST': return wp_safe_remote_post( $api_url, $params ); } return new WP_Error( 400, 'Not valid request type.' ); } } Engine/Common/JobManager/Cron/Subscriber.php 0000644 00000020461 15174677547 0014755 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\JobManager\Cron; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Engine\Common\Queue\RUCSSQueueRunner; use WP_Rocket\Engine\Common\JobManager\JobProcessor; class Subscriber implements Subscriber_Interface { /** * JobProcessor instance * * @var JobProcessor */ private $job_processor; /** * Array of Factories. * * @var array */ private $factories; /** * Instantiate the class * * @param JobProcessor $job_processor JobProcessor instance. * @param array $factories Array of factories. */ public function __construct( JobProcessor $job_processor, array $factories ) { $this->job_processor = $job_processor; $this->factories = $factories; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_saas_pending_jobs' => 'process_pending_jobs', 'rocket_saas_on_submit_jobs' => 'process_on_submit_jobs', 'rocket_saas_job_check_status' => [ 'check_job_status', 10, 3 ], 'rocket_saas_clean_rows_time_event' => 'cron_clean_rows', 'cron_schedules' => 'add_interval', 'rocket_deactivation' => 'on_deactivation', 'rocket_remove_saas_failed_jobs' => 'cron_remove_failed_jobs', 'init' => [ [ 'schedule_clean_not_commonly_used_rows' ], [ 'schedule_pending_jobs' ], [ 'initialize_rucss_queue_runner' ], [ 'schedule_removing_failed_jobs' ], [ 'schedule_on_submit_jobs' ], ], 'wp_rocket_upgrade' => [ 'unschedule_rucss_cron', 13, 2 ], ]; } /** * Schedules cron to clean not commonly used rows. * * @since 3.9 * * @return void */ public function schedule_clean_not_commonly_used_rows() { if ( ! $this->job_processor->is_allowed() ) { return; } if ( wp_next_scheduled( 'rocket_saas_clean_rows_time_event' ) ) { return; } wp_schedule_event( time(), 'weekly', 'rocket_saas_clean_rows_time_event' ); } /** * Initialize the queue runner for our SaaS. * * @return void */ public function initialize_rucss_queue_runner() { if ( ! $this->job_processor->is_allowed() ) { return; } RUCSSQueueRunner::instance()->init(); } /** * Process pending jobs with Cron iteration. * * @return void */ public function process_pending_jobs() { $this->job_processor->process_pending_jobs(); } /** * Process on submit jobs with Cron iteration. * * @return void */ public function process_on_submit_jobs() { $this->job_processor->process_on_submit_jobs(); } /** * Cron callback for deleting old rows in both table databases. * * @since 3.9 * * @return void */ public function cron_clean_rows() { if ( ! $this->is_deletion_enabled() ) { return; } foreach ( $this->factories as $factory ) { if ( $factory->manager()->is_allowed() ) { $factory->table()->delete_old_rows(); } } } /** * Cron callback for removing failed jobs. * * @return void */ public function cron_remove_failed_jobs() { $this->job_processor->clear_failed_urls(); } /** * Handle job status by DB url and is_mobile. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $optimization_type The type of optimization request to send. * * @return void */ public function check_job_status( string $url, bool $is_mobile, string $optimization_type ) { $this->job_processor->check_job_status( $url, $is_mobile, $optimization_type ); } /** * Adds new interval for SaaS pending jobs cron * * @since 3.11.3 * * @param array[] $schedules An array of non-default cron schedule arrays. * * @return array */ public function add_interval( $schedules ) { if ( ! $this->job_processor->is_allowed() ) { return $schedules; } /** * Filters the cron interval. * * @since 3.11 * * @param int $interval Interval in seconds. */ $interval = rocket_apply_filter_and_deprecated( 'rocket_saas_pending_jobs_cron_interval', [ 1 * rocket_get_constant( 'MINUTE_IN_SECONDS', 60 ) ], '3.16', 'rocket_rucss_pending_jobs_cron_interval' ); $schedules['rocket_saas_pending_jobs'] = [ 'interval' => $interval, 'display' => esc_html__( 'WP Rocket process pending jobs', 'rocket' ), ]; $default_interval = 3 * rocket_get_constant( 'DAY_IN_SECONDS', 86400 ); /** * Filters the cron interval for clearing failed jobs. * * @param int $interval Interval in seconds. */ $interval = rocket_apply_filter_and_deprecated( 'rocket_remove_saas_failed_jobs_cron_interval', [ $default_interval ], '3.16', 'rocket_remove_rucss_failed_jobs_cron_interval' ); $interval = (bool) $interval ? $interval : $default_interval; $schedules['rocket_remove_saas_failed_jobs'] = [ 'interval' => $interval, 'display' => esc_html__( 'WP Rocket clear failed jobs', 'rocket' ), ]; /** * Filters the cron interval for processing on submit jobs. * * @param int $interval Interval in seconds. */ $interval = (int) rocket_apply_filter_and_deprecated( 'rocket_remove_saas_on_submit_jobs_cron_interval', [ 1 * rocket_get_constant( 'MINUTE_IN_SECONDS', 60 ) ], '3.16', 'rocket_remove_rucss_on_submit_jobs_cron_interval' ); $schedules['rocket_saas_on_submit_jobs'] = [ 'interval' => $interval, 'display' => esc_html__( 'WP Rocket process on submit jobs', 'rocket' ), ]; return $schedules; } /** * Schedule on submit jobs. * * @return void */ public function schedule_on_submit_jobs() { if ( ! $this->job_processor->is_allowed() && wp_next_scheduled( 'rocket_saas_on_submit_jobs' ) ) { wp_clear_scheduled_hook( 'rocket_saas_on_submit_jobs' ); return; } if ( ! $this->job_processor->is_allowed() ) { return; } if ( wp_next_scheduled( 'rocket_saas_on_submit_jobs' ) ) { return; } wp_schedule_event( time(), 'rocket_saas_on_submit_jobs', 'rocket_saas_on_submit_jobs' ); } /** * Schedules cron to get SaaS pendings jobs. * * @since 3.11.3 * * @return void */ public function schedule_pending_jobs() { if ( ! $this->job_processor->is_allowed() && wp_next_scheduled( 'rocket_saas_pending_jobs' ) ) { wp_clear_scheduled_hook( 'rocket_saas_pending_jobs' ); return; } if ( ! $this->job_processor->is_allowed() ) { return; } if ( wp_next_scheduled( 'rocket_saas_pending_jobs' ) ) { return; } wp_schedule_event( time(), 'rocket_saas_pending_jobs', 'rocket_saas_pending_jobs' ); } /** * Schedules cron to remove failed jobs. * * @return void */ public function schedule_removing_failed_jobs() { if ( ! $this->job_processor->is_allowed() && wp_next_scheduled( 'rocket_remove_saas_failed_jobs' ) ) { wp_clear_scheduled_hook( 'rocket_remove_saas_failed_jobs' ); return; } if ( ! $this->job_processor->is_allowed() ) { return; } if ( wp_next_scheduled( 'rocket_remove_saas_failed_jobs' ) ) { return; } wp_schedule_event( time(), 'rocket_remove_saas_failed_jobs', 'rocket_remove_saas_failed_jobs' ); } /** * Clear schedule of SaaS CRONs on deactivation. * * @return void */ public function on_deactivation() { wp_clear_scheduled_hook( 'action_scheduler_run_queue_rucss', [ 'WP Cron' ] ); } /** * Checks if the SaaS deletion is enabled. * * @return bool */ protected function is_deletion_enabled(): bool { /** * Filters the enable SaaS job deletion value * * @param bool $delete_saas_jobs True to enable deletion, false otherwise. */ return (bool) rocket_apply_filter_and_deprecated( 'rocket_saas_deletion_enabled', [ true ], '3.16', 'rocket_rucss_deletion_enabled' ); } /** * Unschedule old rucss crons. * * @since 3.16 * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function unschedule_rucss_cron( $new_version, $old_version ) { if ( version_compare( $old_version, '3.16', '>=' ) ) { return; } wp_clear_scheduled_hook( 'rocket_rucss_on_submit_jobs' ); wp_clear_scheduled_hook( 'rocket_rucss_pending_jobs' ); wp_clear_scheduled_hook( 'rocket_remove_rucss_failed_jobs' ); wp_clear_scheduled_hook( 'rocket_rucss_clean_rows_time_event' ); } } Engine/Common/JobManager/ServiceProvider.php 0000644 00000004304 15174677547 0015062 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\JobManager; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Common\Clock\WPRClock; use WP_Rocket\Engine\Common\JobManager\APIHandler\APIClient; use WP_Rocket\Engine\Common\JobManager\Cron\Subscriber as CronSubscriber; use WP_Rocket\Engine\Common\JobManager\Queue\Queue; use WP_Rocket\Engine\Common\JobManager\Strategy\Context\RetryContext; use WP_Rocket\Engine\Common\JobManager\Strategy\Factory\StrategyFactory; class ServiceProvider extends AbstractServiceProvider { /** * The provides array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ 'wpr_clock', 'retry_strategy_factory', 'retry_strategy_context', 'job_processor', 'queue', 'api_client', 'cron_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the classes in the container * * @return void */ public function register(): void { $factories = [ $this->getContainer()->get( 'rucss_factory' ), ]; $this->getContainer()->add( 'wpr_clock', WPRClock::class ); $this->getContainer()->add( 'retry_strategy_context', RetryContext::class ); $this->getContainer()->add( 'retry_strategy_factory', StrategyFactory::class ) ->addArgument( 'wpr_clock' ); $this->getContainer()->add( 'queue', Queue::class ); $this->getContainer()->add( 'api_client', APIClient::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'job_processor', JobProcessor::class ) ->addArguments( [ $factories, 'queue', 'retry_strategy_factory', 'api_client', 'wpr_clock', ] ); $this->getContainer()->addShared( 'cron_subscriber', CronSubscriber::class ) ->addArguments( [ 'job_processor', $factories, ] ); } } Engine/Common/JobManager/Managers/AbstractManager.php 0000644 00000013337 15174677547 0016550 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager\Managers; trait AbstractManager { /** * Determine if the action is allowed. * * @param string $optimization_type The type of optimization applied for the current job. * * @return boolean */ public function is_allowed( $optimization_type = '' ): bool { if ( ! $this->context->is_allowed() ) { return false; } if ( ! $optimization_type ) { return true; } return in_array( $optimization_type, [ 'all', $this->optimization_type ], true ); } /** * Query object. * * @return object */ public function query() { return $this->query; } /** * Return type of optimization. * * @return string */ public function get_optimization_type(): string { return $this->optimization_type; } /** * Send the request to add url into the queue. * * @param string $url page URL. * @param bool $is_mobile page is for mobile. * * @return void */ public function add_url_to_the_queue( string $url, bool $is_mobile ): void { if ( ! $this->is_allowed() ) { return; } $row = $this->query->get_row( $url, (bool) $is_mobile ); if ( empty( $row ) ) { $this->query->create_new_job( $url, '', '', $is_mobile ); return; } $this->query->reset_job( (int) $row->id ); } /** * Clear failed jobs. * * @param float $delay delay before the urls are deleted. * @param string $unit unit from the delay. * @return array */ public function clear_failed_jobs( float $delay, string $unit ): array { $rows = $this->query->get_failed_rows( $delay, $unit ); if ( empty( $rows ) ) { return []; } $failed_urls = []; foreach ( $rows as $row ) { $failed_urls[] = $row->url; $id = (int) $row->id; if ( empty( $id ) ) { continue; } $this->add_url_to_the_queue( $row->url, (bool) $row->is_mobile ); } return $failed_urls; } /** * Change the status to be in-progress. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $optimization_type The type of optimization applied for the current job. * @return void */ public function make_status_inprogress( string $url, bool $is_mobile, string $optimization_type ): void { if ( ! $this->is_allowed( $optimization_type ) ) { return; } $this->query->make_status_inprogress( $url, $is_mobile ); } /** * Get single job. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @return bool|object */ public function get_single_job( string $url, bool $is_mobile ) { return $this->query->get_row( $url, $is_mobile ); } /** * Get on submit jobs based on enabled option. * * @param integer $num_rows Number of rows to grab with each CRON iteration. * @return array|int */ public function get_on_submit_jobs( int $num_rows ) { return $this->query->get_on_submit_jobs( $num_rows ); } /** * Change the job status to be failed. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $error_code error code. * @param string $error_message error message. * @param string $optimization_type The type of optimization applied for the current job. * @return void */ public function make_status_failed( string $url, bool $is_mobile, string $error_code, string $error_message, string $optimization_type = '' ): void { if ( ! $this->is_allowed( $optimization_type ) ) { return; } $this->query->make_status_failed( $url, $is_mobile, $error_code, $error_message ); } /** * Change the job status to be pending. * * @param string $url Url from DB row. * @param string $job_id API job_id. * @param string $queue_name API Queue name. * @param boolean $is_mobile if the request is for mobile page. * @param string $optimization_type The type of optimization applied for the current job. * @return void */ public function make_status_pending( string $url, string $job_id, string $queue_name, bool $is_mobile, string $optimization_type ): void { if ( ! $this->is_allowed( $optimization_type ) ) { return; } $this->query->make_status_pending( $url, $job_id, $queue_name, $is_mobile ); } /** * Increment retries number and change status back to pending. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $error_code error code. * @param string $error_message error message. * * @return void */ public function increment_retries( string $url, bool $is_mobile, string $error_code, string $error_message ): void { if ( ! $this->is_allowed() ) { return; } $this->query->increment_retries( $url, $is_mobile, $error_code, $error_message ); } /** * Update the error message. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param int $error_code error code. * @param string $error_message error message. * @param string $previous_message Previous saved message. * * @return void */ public function update_message( string $url, bool $is_mobile, int $error_code, string $error_message, string $previous_message ): void { if ( ! $this->is_allowed() ) { return; } $this->query->update_message( $url, $is_mobile, $error_code, $error_message, $previous_message ); } /** * Updates the next_retry_time field * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string|int $next_retry_time timestamp or mysql format date. * * @return void */ public function update_next_retry_time( string $url, bool $is_mobile, $next_retry_time ): void { if ( ! $this->is_allowed() ) { return; } $this->query->update_next_retry_time( $url, $is_mobile, $next_retry_time ); } } Engine/Common/JobManager/Managers/ManagerInterface.php 0000644 00000002470 15174677547 0016701 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager\Managers; interface ManagerInterface { /** * Get pending jobs from db. * * @param integer $num_rows Number of rows to grab. * @return array */ public function get_pending_jobs( int $num_rows ): array; /** * Validate SaaS response and fail job. * * @param array $job_details Details related to the job.. * @param object $row_details Details related to the row. * @param string $optimization_type The type of optimization applied for the current job. * @return void */ public function validate_and_fail( array $job_details, $row_details, string $optimization_type ): void; /** * Process SaaS response. * * @param array $job_details Details related to the job.. * @param object $row_details Details related to the row. * @param string $optimization_type The type of optimization applied for the current job. * @return void */ public function process( array $job_details, $row_details, string $optimization_type ): void; /** * Set the request parameter to be sent to the SaaS * * @return array */ public function set_request_param(): array; /** * Get the optimization type from the DB Row. * * @param object $row DB Row Object. * @return boolean|string */ public function get_optimization_type_from_row( $row ); } Engine/Common/JobManager/Queue/Queue.php 0000644 00000003024 15174677547 0014115 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Common\JobManager\Queue; use WP_Rocket\Engine\Common\Queue\AbstractASQueue; /** * Queue * * A job queue using WordPress actions. * * @version 3.11.0 */ class Queue extends AbstractASQueue { /** * Queue group. * * @var string */ protected $group = 'rocket-rucss'; /** * Pending jobs cron hook. * * @var string */ private $pending_job_cron = 'rocket_saas_pending_jobs_cron'; /** * Check if pending jobs cron is scheduled. * * @return bool */ public function is_pending_jobs_cron_scheduled() { return $this->is_scheduled( $this->pending_job_cron ); } /** * Cancel pending jobs cron. * * @return void */ public function cancel_pending_jobs_cron() { $this->cancel_all( $this->pending_job_cron ); } /** * Schedule pending jobs cron. * * @param int $interval Cron interval in seconds. * * @return int */ public function schedule_pending_jobs_cron( int $interval ) { return $this->schedule_recurring( time(), $interval, $this->pending_job_cron ); } /** * Add Async job with DB row ID. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $optimization_type The type of optimization request to send. * * @return int */ public function add_job_status_check_async( string $url, bool $is_mobile, string $optimization_type ) { return $this->add_async( 'rocket_saas_job_check_status', [ $url, $is_mobile, $optimization_type, ] ); } } Engine/Common/JobManager/Strategy/Strategies/StrategyInterface.php 0000644 00000000556 15174677547 0021313 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager\Strategy\Strategies; interface StrategyInterface { /** * Execute the retry process of a RUCSS job. * * @param object $row_details DB Row of a job. * @param array $job_details Job information from the API. * * @return mixed */ public function execute( object $row_details, array $job_details ); } Engine/Common/JobManager/Strategy/Strategies/JobSetFail.php 0000644 00000002021 15174677547 0017637 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager\Strategy\Strategies; use WP_Rocket\Engine\Optimization\RUCSS\Jobs\Manager; /** * Class managing the retry process whenever a job isn't found in the SaaS. */ class JobSetFail implements StrategyInterface { /** * Job Manager. * * @var Manager */ private $manager; /** * Strategy Constructor. * * @param Manager $manager Job Manager. */ public function __construct( Manager $manager ) { $this->manager = $manager; } /** * Execute the strategy process. * * @param object $row_details Row details of the job. * @param array $job_details Job details from the API. * * @return void */ public function execute( object $row_details, array $job_details ): void { /** * Unlock preload URL. * * @param string $url URL to unlock */ do_action( 'rocket_preload_unlock_url', $row_details->url ); $this->manager->make_status_failed( $row_details->url, $row_details->is_mobile, strval( $job_details['code'] ), $job_details['message'] ); } } Engine/Common/JobManager/Strategy/Strategies/DefaultProcess.php 0000644 00000006323 15174677547 0020611 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager\Strategy\Strategies; use WP_Rocket\Engine\Common\Clock\WPRClock; use WP_Rocket\Engine\Optimization\RUCSS\Jobs\Manager; /** * Class managing the default error for retry process */ class DefaultProcess implements StrategyInterface { /** * Job Manager. * * @var Manager */ private $manager; /** * Clock Object. * * @var WPRClock */ protected $clock; /** * Represents a timetable which shows how long to wait after for a new retry depending on how many retries have been made already. * * @var int[] */ private $time_table_retry = [ 0 => 60, // 1 minutes 1 => 120, // 2 minutes 2 => 300, // 5 minutes 3 => 600, // 10 minutes. 4 => 1200, // 20 minutes. 5 => 1500, // 25 minutes. ]; /** * Default value to wait before a retry. * * @var int */ private $default_waiting_retry = 1500; /** * Strategy Constructor. * * @param Manager $manager Job Manager. * @param WPRClock $clock Clock object. */ public function __construct( Manager $manager, WPRClock $clock ) { $this->manager = $manager; $this->clock = $clock; /** * Filter the array containing the time needed to wait for each retry. * * @param array $time_table_entry contains the number of retry and how long we have to wait. */ $time_table_retry = rocket_apply_filter_and_deprecated( 'rocket_saas_retry_table', [ $this->time_table_retry ], '3.16', 'rocket_rucss_retry_table' ); if ( is_array( $time_table_retry ) ) { $this->time_table_retry = $time_table_retry; } } /** * Execute the strategy process. * * @param object $row_details Row details of the job. * @param array $job_details Job details from the API. * * @return void */ public function execute( object $row_details, array $job_details ): void { if ( $row_details->retries >= count( $this->time_table_retry ) ) { /** * Unlock preload URL. * * @param string $url URL to unlock */ do_action( 'rocket_preload_unlock_url', $row_details->url ); $this->manager->make_status_failed( $row_details->url, $row_details->is_mobile, $job_details['code'], $job_details['message'] ); return; } $this->manager->increment_retries( $row_details->url, $row_details->is_mobile, $job_details['code'], $job_details['message'] ); $saas_retry_duration = $this->time_table_retry[ $row_details->retries ] ?? $this->default_waiting_retry; // Default to 30 minutes. /** * Filter SaaS retry duration. * * @param int $duration Duration between each retry in seconds. */ $saas_retry_duration = (int) rocket_apply_filter_and_deprecated( 'rocket_saas_retry_duration', [ $saas_retry_duration ], '3.16', 'rocket_rucss_retry_duration' ); if ( $saas_retry_duration < 0 ) { $saas_retry_duration = $this->default_waiting_retry; } // update the `next_retry_time` column. $next_retry_time = $this->clock->current_time( 'timestamp', true ) + $saas_retry_duration; $this->manager->update_message( $row_details->url, $row_details->is_mobile, (int) $job_details['code'], $job_details['message'], $row_details->error_message ); $this->manager->update_next_retry_time( $row_details->url, $row_details->is_mobile, $next_retry_time ); } } Engine/Common/JobManager/Strategy/Strategies/ResetRetryProcess.php 0000644 00000001521 15174677547 0021330 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager\Strategy\Strategies; use WP_Rocket\Engine\Optimization\RUCSS\Jobs\Manager; /** * Class managing the retry process whenever a job isn't found in the SaaS. */ class ResetRetryProcess implements StrategyInterface { /** * Job Manager. * * @var Manager */ private $manager; /** * Strategy Constructor. * * @param Manager $manager Job Manager. */ public function __construct( Manager $manager ) { $this->manager = $manager; } /** * Execute the strategy process. * * @param object $row_details Row details of the job. * @param array $job_details Job details from the API. * * @return void */ public function execute( object $row_details, array $job_details ): void { $this->manager->add_url_to_the_queue( $row_details->url, $row_details->is_mobile ); } } Engine/Common/JobManager/Strategy/Factory/StrategyFactory.php 0000644 00000003177 15174677547 0020321 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager\Strategy\Factory; use WP_Rocket\Engine\Common\Clock\WPRClock; use WP_Rocket\Engine\Common\JobManager\Strategy\Context\RetryContext; use WP_Rocket\Engine\Common\JobManager\Strategy\Strategies\JobSetFail; use WP_Rocket\Engine\Common\JobManager\Strategy\Strategies\ResetRetryProcess; use WP_Rocket\Engine\Common\JobManager\Strategy\Strategies\DefaultProcess; use WP_Rocket\Logger\LoggerAware; use WP_Rocket\Logger\LoggerAwareInterface; use WP_Rocket\Engine\Common\JobManager\Managers\ManagerInterface; class StrategyFactory implements LoggerAwareInterface { use LoggerAware; /** * Clock instance. * * @var WPRClock */ protected $clock; /** * Constructor. * * @param WPRClock $clock Clock instance. */ public function __construct( WPRClock $clock ) { $this->clock = $clock; } /** * Manage the whole process, to determine which strategy to adopt.. * * @param object $row_details DB Row of a job. * @param array $job_details Job information from the API. * @param ManagerInterface $manager Job Manager. * * @return void */ public function manage( $row_details, $job_details, ManagerInterface $manager ): void { switch ( $job_details['code'] ) { case 408: $strategy = new ResetRetryProcess( $manager ); break; case 500: case 422: case 404: case 401: $strategy = new JobSetFail( $manager ); break; default: $strategy = new DefaultProcess( $manager, $this->clock ); break; } $context = new RetryContext(); $context->set_strategy( $strategy ); $context->execute( $row_details, $job_details ); } } Engine/Common/JobManager/Strategy/Context/RetryContext.php 0000644 00000001364 15174677547 0017652 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager\Strategy\Context; use WP_Rocket\Engine\Common\JobManager\Strategy\Strategies\StrategyInterface; class RetryContext { /** * Strategy Interface. * * @var StrategyInterface */ protected $strategy; /** * Set the strategy property * * @param StrategyInterface $strategy Strategy. * * @return void */ public function set_strategy( StrategyInterface $strategy ) { $this->strategy = $strategy; } /** * Execute the strategy. * * @param object $row_details row from the database. * @param array $job_details job details. * * @return void */ public function execute( $row_details, $job_details ): void { $this->strategy->execute( $row_details, $job_details ); } } Engine/Common/JobManager/JobProcessor.php 0000644 00000041656 15174677547 0014374 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\JobManager; use WP_Rocket\Logger\LoggerAware; use WP_Rocket\Logger\LoggerAwareInterface; use WP_Rocket\Engine\Common\JobManager\Queue\Queue; use WP_Rocket\Engine\Common\JobManager\Strategy\Factory\StrategyFactory; use WP_Rocket\Engine\Common\JobManager\APIHandler\APIClient; use WP_Rocket\Engine\Common\Clock\WPRClock; use WP_Rocket\Engine\Common\Utils; class JobProcessor implements LoggerAwareInterface { use LoggerAware; /** * Array of Factories. * * @var array */ private $factories; /** * Queue instance. * * @var Queue */ private $queue; /** * Retry Strategy Factory * * @var StrategyFactory */ protected $strategy_factory; /** * APIClient instance * * @var APIClient */ private $api; /** * Clock instance. * * @var WPRClock */ protected $wpr_clock; /** * Instantiate the class. * * @param array $factories Array of factories. * @param Queue $queue Queue instance. * @param StrategyFactory $strategy_factory Strategy Factory. * @param APIClient $api APIClient instance. * @param WPRClock $clock Clock object instance. */ public function __construct( array $factories, Queue $queue, StrategyFactory $strategy_factory, APIClient $api, WPRClock $clock ) { $this->factories = $factories; $this->queue = $queue; $this->strategy_factory = $strategy_factory; $this->api = $api; $this->wpr_clock = $clock; } /** * Determine if action is allowed. * * @return boolean */ public function is_allowed(): bool { if ( ! $this->factories ) { return false; } $is_allowed = []; foreach ( $this->factories as $factory ) { $is_allowed[] = $factory->manager()->is_allowed(); } return (bool) array_sum( $is_allowed ); } /** * Process pending jobs inside cron iteration. * * @return void */ public function process_pending_jobs() { /** * Fires at the start of the process pending jobs. * * @param string $current_time Current time. */ rocket_do_action_and_deprecated( 'rocket_saas_process_pending_jobs_start', [ $this->wpr_clock->current_time( 'mysql', true ) ], '3.16', 'rocket_rucss_process_pending_jobs_start' ); $this->logger::debug( 'RUCSS: Start processing pending jobs inside cron.' ); if ( ! $this->is_allowed() ) { $this->logger::debug( 'Stop processing cron iteration for pending jobs.' ); return; } $this->logger::debug( 'Start processing pending jobs inside cron.' ); // Get some items from the DB with status=pending & job_id isn't empty. /** * Filters the pending jobs count. * * @since 3.11 * * @param int $rows Number of rows to grab with each CRON iteration. */ $rows = rocket_apply_filter_and_deprecated( 'rocket_saas_pending_jobs_cron_rows_count', [ 100 ], '3.16', 'rocket_rucss_pending_jobs_cron_rows_count' ); $pending_jobs = $this->get_jobs( $rows, 'pending' ); if ( ! $pending_jobs ) { return; } foreach ( $pending_jobs as $row ) { $current_time = $this->wpr_clock->current_time( 'timestamp', true ); if ( $row->next_retry_time < $current_time ) { $optimization_type = $this->get_optimization_type( $row ); // Change status to in-progress. $this->make_status_inprogress( $row->url, $row->is_mobile, $optimization_type ); $this->queue->add_job_status_check_async( $row->url, $row->is_mobile, $optimization_type ); } } /** * Fires at the end of the process pending jobs. * * @param string $current_time Current time. */ rocket_do_action_and_deprecated( 'rocket_saas_process_pending_jobs_end', [ $this->wpr_clock->current_time( 'mysql', true ) ], '3.16', 'rocket_rucss_process_pending_jobs_end' ); } /** * Check job status by DB row ID. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $optimization_type The type of optimization request to send. * * @return void */ public function check_job_status( string $url, bool $is_mobile, string $optimization_type ) { $row_details = $this->get_single_job( $url, $is_mobile, $optimization_type ); if ( ! is_object( $row_details ) ) { $this->logger::debug( 'Url - ' . $url . ' not found for is_mobile - ' . (int) $is_mobile ); // Nothing in DB, bailout. return; } // Send the request to get the job status from SaaS. $job_details = $this->api->get_queue_job_status( $row_details->job_id, $row_details->queue_name, Utils::is_home( $row_details->url ) ); foreach ( $this->factories as $factory ) { $factory->manager()->validate_and_fail( $job_details, $row_details, $optimization_type ); } if ( 200 !== (int) $job_details['code'] ) { $this->logger::debug( 'Job status failed for url: ' . $row_details->url, $job_details ); $this->decide_strategy( $row_details, $job_details, $optimization_type ); return; } /** * Unlock preload URL. * * @param string $url URL to unlock */ do_action( 'rocket_preload_unlock_url', $row_details->url ); foreach ( $this->factories as $factory ) { $factory->manager()->process( $job_details, $row_details, $optimization_type ); } /** * Fires after successfully Processing the SaaS jobs. * * @param string $current_time Current time. */ rocket_do_action_and_deprecated( 'rocket_saas_check_job_status_end', [ $this->wpr_clock->current_time( 'mysql', true ) ], '3.16', 'rocket_rucss_check_job_status_end' ); /** * Fires after successfully processing the SaaS jobs. * * @param string $url Optimized Url. * @param array $job_details Result of the request to get the job status from SaaS. */ rocket_do_action_and_deprecated( 'rocket_saas_complete_job_status', [ $row_details->url, $job_details ], '3.16', 'rocket_rucss_complete_job_status' ); } /** * Process on submit jobs. * * @return void */ public function process_on_submit_jobs() { $this->logger::debug( 'Start processing on submit jobs for adding jobs to queue.' ); /** * Fires at the start of the process on submit jobs. * * @param string $current_time Current time. */ rocket_do_action_and_deprecated( 'rocket_saas_process_on_submit_jobs_start', [ $this->wpr_clock->current_time( 'mysql', true ) ], '3.16', 'rocket_rucss_process_on_submit_jobs_start' ); if ( ! $this->is_allowed() ) { $this->logger::debug( 'Stop processing cron iteration for to-submit jobs.' ); return; } /** * Pending rows cont. * * @param int $count Number of rows. */ $pending_job = rocket_apply_filter_and_deprecated( 'rocket_saas_pending_jobs_cron_rows_count', [ 100 ], '3.16', 'rocket_rucss_pending_jobs_cron_rows_count' ); /** * Maximum processing rows. * * @param int $max Max processing rows. */ $max_pending_rows = (int) rocket_apply_filter_and_deprecated( 'rocket_saas_max_pending_jobs', [ 3 * $pending_job, $pending_job ], '3.16', 'rocket_rucss_max_pending_jobs' ); $rows = $this->get_jobs( $max_pending_rows, 'submit' ); if ( ! $rows ) { return; } foreach ( $rows as $row ) { $optimization_type = $this->get_optimization_type( $row ); $response = $this->send_api( $row->url, (bool) $row->is_mobile, $optimization_type ); if ( false === $response || ! isset( $response['contents'], $response['contents']['jobId'], $response['contents']['queueName'] ) ) { $this->make_status_failed( $row->url, $row->is_mobile, '', '', $optimization_type ); continue; } /** * Lock preload URL. * * @param string $url URL to lock */ do_action( 'rocket_preload_lock_url', $row->url ); $this->make_status_pending( $row->url, $response['contents']['jobId'], $response['contents']['queueName'], (bool) $row->is_mobile, $optimization_type ); } $this->logger::debug( 'End processing on submit jobs for adding jobs to queue.' ); /** * Fires at the end of the process pending jobs. * * @param string $current_time Current time. */ rocket_do_action_and_deprecated( 'rocket_saas_process_on_submit_jobs_end', [ $this->wpr_clock->current_time( 'mysql', true ) ], '3.16', 'rocket_rucss_process_on_submit_jobs_end' ); } /** * Send the job to the API. * * @param string $url URL to work on. * @param bool $is_mobile Is the page for mobile. * @param string $optimization_type The type of optimization request to send. * @return array|false */ protected function send_api( string $url, bool $is_mobile, string $optimization_type ) { $config = [ 'is_mobile' => $is_mobile, 'is_home' => Utils::is_home( $url ), ]; $config = $this->set_request_params( $config, $optimization_type ); $add_to_queue_response = $this->api->add_to_queue( $url, $config ); if ( 200 !== $add_to_queue_response['code'] ) { $this->logger::error( 'Error when contacting the SaaS API.', [ 'SaaS error', 'url' => $url, 'code' => $add_to_queue_response['code'], 'message' => $add_to_queue_response['message'], ] ); return false; } return $add_to_queue_response; } /** * Set request parameters * * @param array $config Array of request parameters. * @param string $optimization_type The type of optimization applied for the current job. * @return array */ public function set_request_params( array $config, string $optimization_type ): array { list($updated_config, $optimization_list, $request_param) = [ [], [], [] ]; foreach ( $this->factories as $factory ) { if ( $optimization_type === $factory->manager()->get_optimization_type() ) { $config = array_merge( $config, $factory->manager()->set_request_param() ); return $config; } $request_param = $factory->manager()->set_request_param(); $optimization_list = array_merge( $optimization_list, $request_param['optimization_list'] ); $updated_config = array_merge( $request_param, $updated_config ); } if ( ! $updated_config ) { $updated_config['optimization_list'] = $optimization_list; } return $updated_config; } /** * Clear failed urls. * * @return void */ public function clear_failed_urls(): void { /** * Delay before failed saas jobs are deleted. * * @param string $delay delay before failed saas jobs are deleted. */ $delay = (string) rocket_apply_filter_and_deprecated( 'rocket_delay_remove_saas_failed_jobs', [ '3 days' ], '3.16', 'rocket_delay_remove_rucss_failed_jobs' ); if ( '' === $delay || '0' === $delay ) { $delay = '3 days'; } $parts = explode( ' ', $delay ); $value = 3; $unit = 'days'; if ( count( $parts ) === 2 && $parts[0] >= 0 ) { $value = (float) $parts[0]; $unit = $parts[1]; } foreach ( $this->factories as $factory ) { if ( $factory->manager()->is_allowed() ) { $failed_urls = $factory->manager()->clear_failed_jobs( $value, $unit ); $hook = 'rocket_' . $factory->manager()->get_optimization_type() . '_after_clearing_failed_url'; /** * Fires after clearing failed urls. * * @param array $urls Failed urls. */ do_action( $hook, $failed_urls ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } } } /** * Change the status to be in-progress. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $optimization_type The type of optimization applied for the current job. * @return void */ private function make_status_inprogress( string $url, bool $is_mobile, string $optimization_type ): void { foreach ( $this->factories as $factory ) { $factory->manager()->make_status_inprogress( $url, $is_mobile, $optimization_type ); } } /** * Get single job. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $optimization_type The type of optimization applied for the current job. * * @return bool|object */ private function get_single_job( string $url, bool $is_mobile, string $optimization_type ) { $job = []; foreach ( $this->factories as $factory ) { if ( $optimization_type === $factory->manager()->get_optimization_type() ) { return $factory->manager()->get_single_job( $url, $is_mobile ); } } $job = $this->factories[0]->manager()->get_single_job( $url, $is_mobile ); return ( ! $job ? [] : $job ); } /** * Decide jobs to get. * * @param integer $num_rows Number of rows to grab with each CRON iteration. * @param string $type Type of job to get. * @return array */ public function get_jobs( int $num_rows, string $type ): array { $allowed_types = [ 'pending', 'submit' ]; if ( ! in_array( $type, $allowed_types, true ) ) { return []; } $rows = []; switch ( $type ) { case 'pending': foreach ( $this->factories as $factory ) { $rows = array_merge( $rows, $factory->manager()->get_pending_jobs( $num_rows ) ); } break; case 'submit': foreach ( $this->factories as $factory ) { $rows = array_merge( $rows, $factory->manager()->get_on_submit_jobs( $num_rows ) ); } break; } if ( ! $rows ) { return []; } // Get distinct rows. return $this->get_distinct( $rows ); } /** * Get rows common to jobs. * * @param array $rows Merged DB Rows of jobs. * @return array */ private function get_common_jobs( array $rows ): array { list($occurrences, $duplicates) = [ [], [] ]; foreach ( $rows as $row ) { $key = $row->url . '|' . ( isset( $row->is_mobile ) ? (bool) $row->is_mobile : 'null' ); if ( ! isset( $occurrences[ $key ] ) ) { $occurrences[ $key ] = 1; continue; } ++$occurrences[ $key ]; if ( 2 === $occurrences[ $key ] ) { // Add new is_common property to the object and add object to duplicate. $row->is_common = true; $duplicates[] = $row; } } return $duplicates; } /** * Get distinct rows merged from both jobs. * * @param array $rows Merged DB Rows of jobs. * @return array */ private function get_distinct( array $rows ): array { // Get jobs common to both optimizations. $common_rows = $this->get_common_jobs( $rows ); if ( ! $common_rows ) { return $rows; } $index = 0; foreach ( $rows as $row ) { foreach ( $common_rows as $common_row ) { if ( $row->url === $common_row->url && (bool) $row->is_mobile === (bool) $common_row->is_mobile ) { // Remove the common row that is without the new is_common property. unset( $rows[ $index ] ); } } ++$index; } return array_merge( $rows, $common_rows ); } /** * Get the optimization type requested. * * @param object $row DB Row. * @return string */ public function get_optimization_type( $row ): string { $optimization_type = 'all'; if ( isset( $row->is_common ) ) { return $optimization_type; } foreach ( $this->factories as $factory ) { $type = $factory->manager()->get_optimization_type_from_row( $row ); if ( is_string( $type ) ) { $optimization_type = $type; break; } } return $optimization_type; } /** * Decide with job strategy to apply based on the optimization type. * * @param object $row_details DB Row of job. * @param array $job_details Job details from the API. * @param string $optimization_type The type of optimization applied for the current job. * @return void */ private function decide_strategy( $row_details, array $job_details, string $optimization_type ): void { foreach ( $this->factories as $factory ) { if ( $optimization_type === $factory->manager()->get_optimization_type() ) { $this->strategy_factory->manage( $row_details, $job_details, $factory->manager() ); break; } $this->strategy_factory->manage( $row_details, $job_details, $factory->manager() ); } } /** * Change the job status to be failed. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param string $error_code error code. * @param string $error_message error message. * @param string $optimization_type The type of optimization applied for the current job. * @return void */ private function make_status_failed( string $url, bool $is_mobile, string $error_code, string $error_message, $optimization_type ): void { foreach ( $this->factories as $factory ) { $factory->manager()->make_status_failed( $url, $is_mobile, $error_code, $error_message, $optimization_type ); } } /** * Change the job status to be pending. * * @param string $url Url from DB row. * @param string $job_id API job_id. * @param string $queue_name API Queue name. * @param boolean $is_mobile if the request is for mobile page. * @param string $optimization_type The type of optimization applied for the current job. * @return void */ private function make_status_pending( string $url, string $job_id, string $queue_name, bool $is_mobile, string $optimization_type ): void { foreach ( $this->factories as $factory ) { $factory->manager()->make_status_pending( $url, $job_id, $queue_name, $is_mobile, $optimization_type ); } } } Engine/Common/Context/AbstractContext.php 0000644 00000003317 15174677547 0014501 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Context; use WP_Rocket\Admin\Options_Data; abstract class AbstractContext implements ContextInterface { /** * WPR options. * * @var Options_Data */ protected $options; /** * Instantiate the class. * * @param Options_Data $options WPR options. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Run common checks. * * @param array $args Arguments to configure the checks. * * @return bool */ public function run_common_checks( array $args = [] ): bool { if ( key_exists( 'do_not_optimize', $args ) && (bool) rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) !== (bool) $args['do_not_optimize'] ) { return false; } if ( key_exists( 'bypass', $args ) && rocket_bypass() !== (bool) $args['bypass'] ) { return false; } if ( key_exists( 'option', $args ) && is_string( $args['option'] ) && ! (bool) $this->options->get( $args['option'], 0 ) ) { return false; } if ( key_exists( 'password_protected', $args ) && $this->is_password_protected() !== (bool) $args['password_protected'] ) { return false; } if ( key_exists( 'post_excluded', $args ) && is_string( $args['post_excluded'] ) && is_rocket_post_excluded_option( $args['post_excluded'] ) ) { return false; } // Bailout if user is logged in. if ( key_exists( 'logged_in', $args ) && is_user_logged_in() !== (bool) $args['logged_in'] ) { return false; } return true; } /** * Checks if on a single post and if it is password protected * * @since 3.11 * * @return bool */ private function is_password_protected(): bool { if ( ! is_singular() ) { return false; } return post_password_required(); } } Engine/Common/Context/ContextInterface.php 0000644 00000000405 15174677547 0014631 0 ustar 00 <?php namespace WP_Rocket\Engine\Common\Context; interface ContextInterface { /** * Determine if the action is allowed. * * @param array $data Data to pass to the context. * @return bool */ public function is_allowed( array $data = [] ): bool; } Engine/CriticalPath/CriticalCSS.php 0000644 00000025650 15174677547 0013173 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use FilesystemIterator; use UnexpectedValueException; use WP_Filesystem_Direct; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\ContentTrait; /** * Handles the critical CSS generation process. * * @since 2.11 */ class CriticalCSS { use ContentTrait; /** * Background Process instance. * * @var CriticalCSSGeneration */ public $process; /** * WP Rocket options instance. * * @var Options_Data */ private $options; /** * Items for which we generate a critical CSS. * * @var array */ public $items = []; /** * Path to the critical CSS directory. * * @var string */ private $critical_css_path; /** * Instance of the filesystem handler. * * @var WP_Filesystem_Direct */ private $filesystem; /** * Creates an instance of CriticalCSS. * * @param CriticalCSSGeneration $process Background process instance. * @param Options_Data $options Instance of options data handler. * @param WP_Filesystem_Direct $filesystem Instance of the filesystem handler. */ public function __construct( CriticalCSSGeneration $process, Options_Data $options, $filesystem ) { $this->process = $process; $this->options = $options; $this->critical_css_path = rocket_get_constant( 'WP_ROCKET_CRITICAL_CSS_PATH' ) . get_current_blog_id() . '/'; $this->filesystem = $filesystem; $this->items['front_page'] = [ 'type' => 'front_page', 'url' => home_url( '/' ), 'path' => 'front_page.css', 'check' => 0, ]; } /** * Returns the current site critical CSS path. * * @since 3.3.5 * * @return string */ public function get_critical_css_path() { return $this->critical_css_path; } /** * Performs the critical CSS generation. * * @since 3.13.2 Always clear all CPCSS files. * @since 3.6 Added the $version parameter. * @since 2.11 * * @param string $version Optional. Version of the CPCSS files to generate. Possible values: default, mobile, all. * @param string $clean_version Optional: Version of the CPCSS files to clean. Possible values: default, mobile, all. */ public function process_handler( $version = 'default', $clean_version = '' ) { /** * Filters the critical CSS generation process. * * Use this filter to prevent the automatic critical CSS generation. * * @since 2.11.5 * * @param bool $do_rocket_critical_css_generation True to activate the automatic generation, false to prevent it. */ if ( ! apply_filters( 'do_rocket_critical_css_generation', true ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound return; } if ( get_transient( 'rocket_critical_css_generation_process_running' ) ) { return; } if ( empty( $clean_version ) ) { $clean_version = $version; } $this->clean_critical_css( $clean_version ); $this->stop_generation(); $this->set_items( $version ); array_map( [ $this->process, 'push_to_queue' ], $this->items ); $this->update_process_running_transient(); $this->process->save()->dispatch(); } /** * Stop the critical CSS generation process. * * @since 3.3 */ public function stop_generation() { // @phpstan-ignore-next-line if ( method_exists( $this->process, 'cancel_process' ) ) { $this->process->cancel_process(); } } /** * Launches when the CPCSS generation is complete. * * @since 3.6 */ protected function generation_complete() { /** * Fires when the critical CSS generation process is complete. * * @since 2.11 */ do_action( 'rocket_critical_css_generation_process_complete' ); set_transient( 'rocket_critical_css_generation_process_complete', get_transient( 'rocket_critical_css_generation_process_running' ), HOUR_IN_SECONDS ); delete_transient( 'rocket_critical_css_generation_process_running' ); } /** * Deletes critical CSS files. * * @since 3.6 Replaced glob(). * @since 3.6 Added $version parameter. * @since 2.11 * * @param string $version Optional. Version of the CPCSS files to delete. Possible values: default, mobile, all. */ public function clean_critical_css( $version = 'default' ) { foreach ( $this->get_critical_css_iterator() as $file ) { if ( ! $this->filesystem->is_file( $file ) ) { continue; } if ( 'mobile' === $version && false === strpos( $file, '-mobile' ) ) { continue; } elseif ( 'default' === $version && false !== strpos( $file, '-mobile' ) ) { continue; } $this->filesystem->delete( $file ); } } /** * Gets the Critical CSS Filesystem Iterator. * * @since 3.6 * * @return FilesystemIterator|array Returns iterator on success; else an empty array. */ private function get_critical_css_iterator() { try { return new FilesystemIterator( $this->critical_css_path ); } catch ( UnexpectedValueException $e ) { // No logging yet. return []; } } /** * Sets the items for which we generate critical CSS. * * @since 2.11 * * @param string $version Optional. Version of the CPCSS files to generate. Possible values: default, mobile, all. */ private function set_items( $version = 'default' ) { $page_for_posts = get_option( 'page_for_posts' ); if ( 'page' === get_option( 'show_on_front' ) && ! empty( $page_for_posts ) ) { $this->items['home'] = [ 'type' => 'home', 'url' => get_permalink( get_option( 'page_for_posts' ) ), 'path' => 'home.css', 'check' => 0, ]; } $post_types = $this->get_public_post_types(); foreach ( $post_types as $post_type ) { $this->items[ $post_type->post_type ] = [ 'type' => $post_type->post_type, 'url' => get_permalink( $post_type->ID ), 'path' => "{$post_type->post_type}.css", 'check' => 0, ]; } $taxonomies = $this->get_public_taxonomies(); foreach ( $taxonomies as $taxonomy ) { $this->items[ $taxonomy->taxonomy ] = [ 'type' => $taxonomy->taxonomy, 'url' => get_term_link( (int) $taxonomy->ID, $taxonomy->taxonomy ), 'path' => "{$taxonomy->taxonomy}.css", 'check' => 0, ]; } if ( in_array( $version, [ 'all', 'mobile' ], true ) ) { $mobile_items = []; foreach ( $this->items as $key => $value ) { $value['mobile'] = 1; $value['path'] = str_replace( '.css', '-mobile.css', $value['path'] ); $mobile_items[ "{$key}-mobile" ] = $value; } if ( 'mobile' === $version ) { $this->items = $mobile_items; } elseif ( 'all' === $version ) { $this->items = array_merge( $this->items, $mobile_items ); } } /** * Filters the array containing the items to send to the critical CSS generator. * * @since 2.11.4 * * @param array $items Array containing the type/url pair for each item to send. */ $this->items = (array) apply_filters( 'rocket_cpcss_items', $this->items ); } /** * Updates the "rocket_critical_css_generation_process_running" transient. * * @since 3.6 */ private function update_process_running_transient() { $total = 0; foreach ( $this->items as $item ) { if ( ! isset( $item['mobile'] ) ) { ++$total; continue; } if ( 1 === $item['mobile'] ) { continue; } ++$total; } $transient = [ 'total' => $total, 'items' => [], ]; set_transient( 'rocket_critical_css_generation_process_running', $transient, HOUR_IN_SECONDS ); } /** * Gets the CPCSS content to use on the current page. * * @since 3.6 * * @return bool|string */ public function get_critical_css_content() { $filename = $this->get_current_page_critical_css(); if ( empty( $filename ) ) { return $this->options->get( 'critical_css', '' ); } return $this->filesystem->get_contents( $filename ); } /** * Gets the CPCSS filepath for the current page. * * @since 2.11 * * @return string Filepath if the file exists, empty string otherwise. */ public function get_current_page_critical_css() { $files = $this->get_critical_css_filenames(); if ( $this->is_async_css_mobile() && wp_is_mobile() && $this->filesystem->is_readable( $this->critical_css_path . $files['mobile'] ) ) { return $this->critical_css_path . $files['mobile']; } if ( $this->filesystem->is_readable( $this->critical_css_path . $files['default'] ) ) { return $this->critical_css_path . $files['default']; } return ''; } /** * Gets the CPCSS filenames for the current URL type. * * @since 3.6 * * @return array */ private function get_critical_css_filenames() { $default = [ 'default' => 'front_page.css', 'mobile' => 'front_page-mobile.css', ]; if ( is_home() && 'page' === get_option( 'show_on_front' ) ) { return [ 'default' => 'home.css', 'mobile' => 'home-mobile.css', ]; } if ( is_front_page() ) { return $default; } if ( is_category() ) { return [ 'default' => 'category.css', 'mobile' => 'category-mobile.css', ]; } if ( is_tag() ) { return [ 'default' => 'post_tag.css', 'mobile' => 'post_tag-mobile.css', ]; } if ( is_tax() ) { $taxonomy = get_queried_object()->taxonomy; return [ 'default' => "{$taxonomy}.css", 'mobile' => "{$taxonomy}-mobile.css", ]; } if ( is_singular() ) { return $this->get_singular_cpcss_filenames(); } return $default; } /** * Gets the filenames for a singular content. * * @since 3.6 * * @return array */ private function get_singular_cpcss_filenames() { $post_type = get_post_type(); $post_id = get_the_ID(); $post_cpcss = [ 'default' => "posts/{$post_type}-{$post_id}.css", 'mobile' => "posts/{$post_type}-{$post_id}-mobile.css", ]; if ( $this->is_async_css_mobile() && ! $this->filesystem->exists( $this->critical_css_path . $post_cpcss['mobile'] ) ) { $post_cpcss['mobile'] = $post_cpcss['default']; } if ( $this->filesystem->exists( $this->critical_css_path . $post_cpcss['default'] ) ) { return $post_cpcss; } return [ 'default' => "{$post_type}.css", 'mobile' => "{$post_type}-mobile.css", ]; } /** * Checks if we are in a situation where we need the mobile CPCSS. * * @since 3.6 * * @return bool */ public function is_async_css_mobile() { if ( ! (bool) $this->options->get( 'do_caching_mobile_files', 0 ) ) { return false; } return (bool) $this->options->get( 'async_css_mobile', 0 ); } /** * Get list of CSS files to be excluded from async CSS. * * @since 3.6.2 * * @return array An array of URLs for the CSS files to be excluded. */ public function get_exclude_async_css() { /** * Filter list of async CSS files * * @since 2.10 * * @param array $exclude_async_css An array of URLs for the CSS files to be excluded. */ $exclude_async_css = (array) apply_filters( 'rocket_exclude_async_css', [] ); if ( empty( $exclude_async_css ) ) { return $exclude_async_css; } $exclude_async_css = array_filter( $exclude_async_css ); return array_flip( array_flip( $exclude_async_css ) ); } } Engine/CriticalPath/RESTCSSSubscriber.php 0000644 00000002042 15174677547 0014230 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Class RESTCSSSubscriber * * @package WP_Rocket\Engine\CriticalPath */ class RESTCSSSubscriber implements Subscriber_Interface { /** * REST manager that has generate and delete methods. * * @var RESTWPInterface */ private $rest_manager; /** * RESTCSSSubscriber constructor. * * @param RESTWPInterface $rest_manager REST manager instance. */ public function __construct( RESTWPInterface $rest_manager ) { $this->rest_manager = $rest_manager; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.6 * * @return array */ public static function get_subscribed_events() { return [ 'rest_api_init' => [ 'register_routes' ], ]; } /** * Registers generate/delete routes in the API. * * @since 3.6 * * @return void */ public function register_routes() { $this->rest_manager->register_generate_route(); $this->rest_manager->register_delete_route(); } } Engine/CriticalPath/CriticalCSSSubscriber.php 0000644 00000057662 15174677547 0015227 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\Head\ElementTrait; use WP_Rocket\Engine\License\API\User; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Filesystem_Direct; use WP_Rocket\Engine\Support\CommentTrait; /** * Critical CSS Subscriber. * * @since 3.3 */ class CriticalCSSSubscriber implements Subscriber_Interface { use RegexTrait; use CommentTrait; use ElementTrait; /** * Instance of Critical CSS. * * @var CriticalCSS */ protected $critical_css; /** * Instance of options. * * @var Options_Data */ protected $options; /** * WordPress options. * * @var Options */ protected $options_api; /** * Instance of the filesystem handler. * * @var WP_Filesystem_Direct */ private $filesystem; /** * CPCSS generation and deletion service. * * @var ProcessorService instance for this service. */ private $cpcss_service; /** * User instance. * * @var User */ protected $user; /** * Critical CSS contents. * * @var string */ private $critical_css_content = ''; /** * Creates an instance of the Critical CSS Subscriber. * * @param CriticalCSS $critical_css Critical CSS instance. * @param ProcessorService $cpcss_service Has the logic for cpcss generation and deletion. * @param Options_Data $options WP Rocket options. * @param Options $options_api WordPress options. * @param User $user User instance. * @param WP_Filesystem_Direct $filesystem Instance of the filesystem handler. */ public function __construct( CriticalCSS $critical_css, ProcessorService $cpcss_service, Options_Data $options, Options $options_api, User $user, $filesystem ) { $this->critical_css = $critical_css; $this->cpcss_service = $cpcss_service; $this->options = $options; $this->options_api = $options_api; $this->user = $user; $this->filesystem = $filesystem; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3 * * @return array */ public static function get_subscribed_events() { // phpcs:disable WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned return [ 'admin_post_rocket_generate_critical_css' => 'init_critical_css_generation', 'update_option_' . rocket_get_constant( 'WP_ROCKET_SLUG' ) => [ [ 'generate_critical_css_on_activation', 11, 2 ], [ 'stop_process_on_deactivation', 11, 2 ], [ 'maybe_generate_cpcss_mobile', 12, 2 ], ], 'admin_notices' => [ [ 'notice_critical_css_generation_triggered' ], [ 'critical_css_generation_running_notice' ], [ 'critical_css_generation_complete_notice' ], [ 'warning_critical_css_dir_permissions' ], [ 'switch_to_rucss_notice', 9 ], ], 'wp_head' => [ 'insert_load_css', PHP_INT_MAX ], 'rocket_buffer' => [ [ 'insert_critical_css_buffer', 19 ], [ 'async_css', 32 ], ], 'rocket_head_items' => [ 'insert_css_in_head', 50 ], 'switch_theme' => 'maybe_regenerate_cpcss', 'rocket_excluded_inline_js_content' => 'exclude_inline_js', 'before_delete_post' => 'delete_cpcss', 'rocket_before_rollback' => [ 'stop_critical_css_generation', 9 ], 'wp_rocket_upgrade' => [ 'stop_critical_css_generation', 9 ], 'admin_post_switch_to_rucss' => 'switch_to_rucss', ]; // phpcs:enable WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned } /** * Deletes the custom CPCSS files from /posts/ folder. * * @since 3.6 * * @param int $post_id Deleted post id. */ public function delete_cpcss( $post_id ) { if ( ! current_user_can( 'rocket_regenerate_critical_css' ) ) { return; } if ( ! $this->options->get( 'async_css', 0 ) ) { return; } $post_type = get_post_type( $post_id ); $item_path = 'posts' . DIRECTORY_SEPARATOR . "{$post_type}-{$post_id}.css"; $this->cpcss_service->process_delete( $item_path ); if ( $this->options->get( 'async_css_mobile', 0 ) ) { $mobile_item_path = 'posts' . DIRECTORY_SEPARATOR . "{$post_type}-{$post_id}-mobile.css"; $this->cpcss_service->process_delete( $mobile_item_path ); } } /** * This notice is displayed when the Critical CSS Generation is triggered from a different page than * WP Rocket settings page. * * @since 3.4.1 */ public function notice_critical_css_generation_triggered() { if ( ! current_user_can( 'rocket_regenerate_critical_css' ) ) { return; } $screen = get_current_screen(); if ( 'settings_page_wprocket' === $screen->id ) { return; } if ( ! $this->options->get( 'async_css', 0 ) ) { return; } if ( false === get_transient( 'rocket_critical_css_generation_triggered' ) ) { return; } delete_transient( 'rocket_critical_css_generation_triggered' ); $message = __( 'Critical CSS generation is currently running.', 'rocket' ); if ( current_user_can( 'rocket_manage_options' ) ) { $message .= ' ' . sprintf( // Translators: %1$s = opening link tag, %2$s = closing link tag. __( 'Go to the %1$sWP Rocket settings%2$s page to track progress.', 'rocket' ), '<a href="' . esc_url( admin_url( 'options-general.php?page=' . WP_ROCKET_PLUGIN_SLUG ) ) . '">', '</a>' ); } rocket_notice_html( [ 'status' => 'info', 'message' => $message, ] ); } /** * Launches the critical CSS generation from admin. * * @since 2.11 * * @see CriticalCSS::process_handler() */ public function init_critical_css_generation() { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_generate_critical_css' ) ) { wp_nonce_ays( '' ); } if ( ! current_user_can( 'rocket_regenerate_critical_css' ) ) { wp_die(); } $version = 'default'; if ( $this->critical_css->is_async_css_mobile() ) { $version = 'all'; } $this->critical_css->process_handler( $version ); if ( ! strpos( wp_get_referer(), 'wprocket' ) ) { set_transient( 'rocket_critical_css_generation_triggered', 1 ); } wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } /** * Launches the critical CSS generation when activating the async CSS option. * * @since 2.11 * * @param array $old_value Previous values for WP Rocket settings. * @param array $value New values for WP Rocket settings. * * @see CriticalCSS::process_handler() */ public function generate_critical_css_on_activation( $old_value, $value ) { if ( ! isset( $old_value['async_css'], $value['async_css'] ) || ( $old_value['async_css'] === $value['async_css'] ) || 1 !== (int) $value['async_css'] ) { return; } $critical_css_path = $this->critical_css->get_critical_css_path(); // Check if the CPCSS path exists and create it. if ( ! $this->filesystem->is_dir( $critical_css_path ) ) { rocket_mkdir_p( $critical_css_path ); } $version = 'default'; if ( isset( $value['do_caching_mobile_files'], $value['async_css_mobile'] ) && ( 1 === (int) $value['do_caching_mobile_files'] && 1 === (int) $value['async_css_mobile'] ) ) { $version = 'all'; } // Generate the CPCSS files. $this->critical_css->process_handler( $version ); } /** * Maybe generate the CPCSS for Mobile. * * @since 3.6 * * @param array $old_value Array of original values. * @param array $value Array of new values. */ public function maybe_generate_cpcss_mobile( $old_value, $value ) { if ( ! isset( $value['async_css_mobile'] ) || 1 !== (int) $value['async_css_mobile'] ) { return; } if ( ! isset( $value['do_caching_mobile_files'] ) || 1 !== (int) $value['do_caching_mobile_files'] ) { return; } if ( ! isset( $old_value['async_css'], $value['async_css'] ) || ( ( $old_value['async_css'] !== $value['async_css'] ) && 1 === (int) $value['async_css'] ) || 1 !== (int) $value['async_css'] ) { return; } $this->critical_css->process_handler( 'mobile' ); } /** * Stops the critical CSS generation when deactivating the async CSS option and remove the notices. * * @since 2.11 * * @param array $old_value Previous values for WP Rocket settings. * @param array $value New values for WP Rocket settings. */ public function stop_process_on_deactivation( $old_value, $value ) { if ( ! empty( $_POST[ WP_ROCKET_SLUG ] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing && isset( $old_value['async_css'], $value['async_css'] ) && ( $old_value['async_css'] !== $value['async_css'] ) && 0 === (int) $value['async_css'] ) { $this->stop_critical_css_generation(); } } /** * This notice is displayed when the critical CSS generation is running. * * @since 2.11 */ public function critical_css_generation_running_notice() { if ( ! current_user_can( 'rocket_regenerate_critical_css' ) ) { return; } $screen = get_current_screen(); if ( 'settings_page_wprocket' !== $screen->id ) { return; } if ( ! $this->options->get( 'async_css', 0 ) ) { return; } $transient = get_transient( 'rocket_critical_css_generation_process_running' ); if ( ! $transient ) { return; } $success_counter = 0; $items_message = ''; if ( ! empty( $transient['items'] ) ) { $items_message .= '<ul>'; foreach ( $transient['items'] as $item ) { $status_nonmobile = isset( $item['status']['nonmobile'] ); $status_mobile = $this->is_mobile_cpcss_active() ? isset( $item['status']['mobile'] ) : true; if ( $status_nonmobile && $status_mobile ) { $items_message .= '<li>' . $item['status']['nonmobile']['message'] . '</li>'; if ( $item['status']['nonmobile']['success'] ) { ++$success_counter; } } } $items_message .= '</ul>'; } if ( ! isset( $transient['total'] ) ) { return; } if ( 0 === $success_counter && 0 === $transient['total'] ) { return; } $message = '<p>' . sprintf( // Translators: %1$d = number of critical CSS generated, %2$d = total number of critical CSS to generate. __( 'Critical CSS generation is currently running: %1$d of %2$d page types completed. (Refresh this page to view progress)', 'rocket' ), $success_counter, $transient['total'] ) . '</p>' . $items_message; rocket_notice_html( [ 'status' => 'info', 'message' => $message, ] ); } /** * This notice is displayed when the critical CSS generation is complete. * * @since 2.11 */ public function critical_css_generation_complete_notice() { if ( ! current_user_can( 'rocket_regenerate_critical_css' ) ) { return; } $screen = get_current_screen(); if ( 'settings_page_wprocket' !== $screen->id ) { return; } if ( ! $this->options->get( 'async_css', 0 ) ) { return; } $transient = get_transient( 'rocket_critical_css_generation_process_complete' ); if ( ! $transient ) { return; } $status = 'success'; $success_counter = 0; $items_message = ''; $desktop = false; if ( ! empty( $transient['items'] ) ) { $items_message .= '<ul>'; foreach ( $transient['items'] as $item ) { $status_nonmobile = isset( $item['status']['nonmobile'] ); $status_mobile = $this->is_mobile_cpcss_active() ? isset( $item['status']['mobile'] ) : true; if ( ! $status_nonmobile || ! $status_mobile ) { continue; } if ( isset( $item['status']['nonmobile']['message'] ) ) { $desktop = true; } $items_message .= '<li>' . $item['status']['nonmobile']['message'] . '</li>'; if ( $item['status']['nonmobile']['success'] ) { ++$success_counter; } } $items_message .= '</ul>'; } if ( ! $desktop || ( 0 === $success_counter && 0 === $transient['total'] ) ) { return; } if ( 0 === $success_counter ) { $status = 'error'; } elseif ( $success_counter < $transient['total'] ) { $status = 'warning'; } $message = '<p>' . sprintf( // Translators: %1$d = number of critical CSS generated, %2$d = total number of critical CSS to generate. __( 'Critical CSS generation finished for %1$d of %2$d page types.', 'rocket' ), $success_counter, $transient['total'] ); $message .= ' <em> (' . date_i18n( get_option( 'date_format' ) ) . ' @ ' . date_i18n( get_option( 'time_format' ) ) . ') </em></p>' . $items_message; if ( 'error' === $status || 'warning' === $status ) { $message .= '<p>' . __( 'Critical CSS generation encountered one or more errors.', 'rocket' ) . ' <a href="https://docs.wp-rocket.me/article/1267-troubleshooting-critical-css-generation-issues" data-beacon-article="5d5214d10428631e94f94ae6" target="_blank" rel="noreferer noopener">' . __( 'Learn more.', 'rocket' ) . '</a>'; } rocket_notice_html( [ 'status' => $status, 'message' => $message, ] ); delete_transient( 'rocket_critical_css_generation_process_complete' ); } /** * This warning is displayed when the critical CSS dir isn't writeable. * * @since 2.11 */ public function warning_critical_css_dir_permissions() { if ( current_user_can( 'rocket_manage_options' ) && ( ! $this->filesystem->is_writable( WP_ROCKET_CRITICAL_CSS_PATH ) ) && ( $this->options->get( 'async_css', false ) ) && rocket_valid_key() ) { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } $message = rocket_notice_writing_permissions( trim( str_replace( ABSPATH, '', WP_ROCKET_CRITICAL_CSS_PATH ), '/' ) ); rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => $message, ] ); } } /** * Insert loadCSS script in <head>. * * @since 2.11.2 Updated loadCSS rel=preload polyfill to version 2.0.1 * @since 2.10 */ public function insert_load_css() { if ( ! $this->should_async_css() ) { return; } // This filter is documented in inc/classes/Buffer/class-tests.php. $rocket_cache_search = apply_filters( 'rocket_cache_search', false ); // Don't apply on search page. if ( is_search() && ! $rocket_cache_search ) { return; } // Don't apply on 404 page. if ( is_404() ) { return; } if ( empty( $this->critical_css->get_current_page_critical_css() ) && empty( $this->options->get( 'critical_css', '' ) ) ) { return; } echo /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. */ <<<JS <script> /*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */ (function(w){"use strict";if(!w.loadCSS){w.loadCSS=function(){}} var rp=loadCSS.relpreload={};rp.support=(function(){var ret;try{ret=w.document.createElement("link").relList.supports("preload")}catch(e){ret=!1} return function(){return ret}})();rp.bindMediaToggle=function(link){var finalMedia=link.media||"all";function enableStylesheet(){link.media=finalMedia} if(link.addEventListener){link.addEventListener("load",enableStylesheet)}else if(link.attachEvent){link.attachEvent("onload",enableStylesheet)} setTimeout(function(){link.rel="stylesheet";link.media="only x"});setTimeout(enableStylesheet,3000)};rp.poly=function(){if(rp.support()){return} var links=w.document.getElementsByTagName("link");for(var i=0;i<links.length;i++){var link=links[i];if(link.rel==="preload"&&link.getAttribute("as")==="style"&&!link.getAttribute("data-loadcss")){link.setAttribute("data-loadcss",!0);rp.bindMediaToggle(link)}}};if(!rp.support()){rp.poly();var run=w.setInterval(rp.poly,500);if(w.addEventListener){w.addEventListener("load",function(){rp.poly();w.clearInterval(run)})}else if(w.attachEvent){w.attachEvent("onload",function(){rp.poly();w.clearInterval(run)})}} if(typeof exports!=="undefined"){exports.loadCSS=loadCSS} else{w.loadCSS=loadCSS}}(typeof global!=="undefined"?global:this)) </script> JS; } /** * Insert critical CSS before combined CSS when option is active. * * @since 2.11.5 * * @param string $buffer HTML output of the page. * * @return string Updated HTML output */ public function insert_critical_css_buffer( $buffer ) { if ( ! $this->should_async_css() ) { return $buffer; } $critical_css_content = $this->critical_css->get_critical_css_content(); if ( empty( $critical_css_content ) ) { return $buffer; } $this->critical_css_content = str_replace( '\\', '\\\\', $critical_css_content ); $buffer = preg_replace( '#</body>#iU', $this->return_remove_cpcss_script() . '</body>', $buffer, 1 ); return $this->add_meta_comment( 'async_css', $buffer ); } /** * Insert critical CSS into head. * * @param array $items Head elements. * @return mixed */ public function insert_css_in_head( $items ) { $css = $this->get_critical_css_content(); if ( empty( $css ) ) { return $items; } $items[] = $this->style_tag( $css, [ 'id' => 'rocket-critical-css', ] ); return $items; } /** * Get critical CSS content, getter method for critical_css_content property. * * @return string */ public function get_critical_css_content() { return $this->should_async_css() ? $this->critical_css_content : ''; } /** * Returns JS script to remove the critical css style from frontend. * * @since 3.6 * * @return string */ protected function return_remove_cpcss_script() { $filename = rocket_get_constant( 'SCRIPT_DEBUG' ) ? 'cpcss-removal.js' : 'cpcss-removal.min.js'; $script = rocket_get_constant( 'WP_ROCKET_PATH' ) . "assets/js/{$filename}"; if ( ! is_readable( $script ) ) { return ''; } return sprintf( '<script>%s</script>', $this->filesystem->get_contents( $script ) ); } /** * Adds wprRemoveCPCSS to excluded inline JS array. * * @since 3.6 * * @param array $excluded_inline Array of inline JS excluded from being combined. * * @return array */ public function exclude_inline_js( array $excluded_inline ) { $excluded_inline[] = 'wprRemoveCPCSS'; return $excluded_inline; } /** * Defer loading of CSS files. * * @since 2.10 * * @param string $buffer HTML code. * * @return string Updated HTML code */ public function async_css( $buffer ) { if ( ! $this->should_async_css() ) { return $buffer; } if ( empty( $this->critical_css->get_current_page_critical_css() ) && empty( $this->options->get( 'critical_css', '' ) ) ) { return $buffer; } $excluded_css = array_flip( $this->critical_css->get_exclude_async_css() ); /** * Filters the pattern used to get all stylesheets in the HTML. * * @since 2.10 * * @param string $css_pattern Regex pattern to get all stylesheets in the HTML. */ $css_pattern = apply_filters( 'rocket_async_css_regex_pattern', '/(?=<link[^>]*\s(rel\s*=\s*[\'"]stylesheet["\']))<link[^>]*\shref\s*=\s*[\'"]([^\'"]+)[\'"](.*)>/iU' ); // Remove comments from the buffer. $clean_buffer = $this->hide_comments( $buffer ); $clean_buffer = $this->hide_noscripts( $clean_buffer ); // Get all css files with this regex. preg_match_all( $css_pattern, $clean_buffer, $tags_match ); if ( ! isset( $tags_match[0] ) ) { return $buffer; } $noscripts = '<noscript>'; foreach ( $tags_match[0] as $i => $tag ) { // Strip query args. $path = wp_parse_url( $tags_match[2][ $i ], PHP_URL_PATH ); // Check if this file should be deferred. if ( isset( $excluded_css[ $path ] ) ) { continue; } if ( preg_match( '/media\s*=\s*[\'"]print[\'"]/i', $tags_match[0][ $i ] ) ) { continue; } $preload = str_replace( 'stylesheet', 'preload', $tags_match[1][ $i ] ); $onload = preg_replace( '~' . preg_quote( $tags_match[3][ $i ], '~' ) . '~iU', ' data-rocket-async="style" as="style" onload="" onerror="this.removeAttribute(\'data-rocket-async\')" ' . $tags_match[3][ $i ] . '>', $tags_match[3][ $i ] ); $tag = str_replace( $tags_match[3][ $i ] . '>', $onload, $tag ); $tag = str_replace( $tags_match[1][ $i ], $preload, $tag ); $tag = str_replace( 'onload=""', 'onload="this.onload=null;this.rel=\'stylesheet\'"', $tag ); $tag = preg_replace( '/(id\s*=\s*[\"\'](?:[^\"\']*)*[\"\'])/i', '', $tag ); $buffer = str_replace( $tags_match[0][ $i ], $tag, $buffer ); $noscripts .= $tags_match[0][ $i ]; } $noscripts .= '</noscript>'; return str_replace( '</body>', $noscripts . '</body>', $buffer ); } /** * Regenerates the CPCSS when switching theme if the option is active. * * @since 3.3 */ public function maybe_regenerate_cpcss() { if ( ! $this->options->get( 'async_css' ) ) { return; } if ( ! $this->is_mobile_cpcss_active() ) { $this->critical_css->process_handler( 'default', 'all' ); return; } $this->critical_css->process_handler( 'all' ); } /** * Checks if mobile CPCSS is active. * * @since 3.6 * * @return boolean CPCSS active or not. */ private function is_mobile_cpcss_active() { return ( $this->options->get( 'async_css', 0 ) && $this->options->get( 'cache_mobile', 0 ) && $this->options->get( 'do_caching_mobile_files', 0 ) ) && $this->options->get( 'async_css_mobile', 0 ); } /** * Checks if we should async CSS * * @since 3.6.2.1 * * @return boolean */ private function should_async_css() { if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return false; } if ( ! $this->options->get( 'async_css', 0 ) ) { return false; } return ! is_rocket_post_excluded_option( 'async_css' ); } /** * Stops the critical CSS generation. * * @since 3.10 * * @return void */ public function stop_critical_css_generation() { $this->critical_css->stop_generation(); delete_transient( 'rocket_critical_css_generation_process_running' ); delete_transient( 'rocket_critical_css_generation_process_complete' ); } /** * Display a notice to pass from CPCSS to RUCSS. * * @return void */ public function switch_to_rucss_notice() { $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( __FUNCTION__, (array) $boxes, true ) ) { return; } if ( ! $this->options->get( 'async_css', 0 ) ) { return; } if ( $this->user->is_license_expired() ) { return; } $screen = get_current_screen(); if ( isset( $screen->id ) && 'settings_page_wprocket' !== $screen->id ) { return; } /** * Filters the status of the RUCSS option. * * @param array $should_disable will return array with disable status and text. */ $rucss_status = wpm_apply_filters_typed( 'array', 'rocket_disable_rucss_setting', [ 'disable' => false, 'text' => '', ] ); if ( key_exists( 'disable', $rucss_status ) && $rucss_status['disable'] ) { return; } rocket_notice_html( [ 'status' => 'wpr-warning', 'dismissible' => '', 'dismiss_button' => __FUNCTION__, 'message' => sprintf( // translators: %1$ = opening bold tag, %2$ = closing bold tag. __( 'We highly recommend the %1$supdated Remove Unused CSS%2$s for a better CSS optimization. Load CSS Asynchronously is always available as a back-up.', 'rocket' ), '<b>', '</b>' ), 'action' => 'switch_to_rucss', 'dismiss_button_message' => __( 'Stay with the old option', 'rocket' ), ] ); } /** * Switch to RUCSS. * * @return void */ public function switch_to_rucss() { check_admin_referer( 'rucss_switch' ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_safe_redirect( wp_get_referer() ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; // @phpstan-ignore-next-line return; // phpcs:ignore Squiz.PHP.NonExecutableCode.Unreachable } $this->options->set( 'async_css', false ); $this->options->set( 'remove_unused_css', true ); $this->options_api->set( 'settings', $this->options->get_options() ); rocket_dismiss_box( 'switch_to_rucss_notice' ); wp_safe_redirect( wp_get_referer() ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } } Engine/CriticalPath/APIClient.php 0000644 00000017021 15174677547 0012631 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use stdClass; use WP_Error; class APIClient { /** * Constant url for Critical Path API job. */ const API_URL = 'https://cpcss.wp-rocket.me/api/job/'; /** * Sends a generation request to the Critical Path API. * * @since 3.6 * * @param string $url The URL to send a CPCSS generation request for. * @param array $params Optional. Parameters needed to be sent in the body. Default: []. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * @return object|array|WP_Error */ public function send_generation_request( $url, $params = [], $item_type = 'custom' ) { $params['url'] = $url; $is_mobile = isset( $params['mobile'] ) && $params['mobile']; $response = wp_remote_post( self::API_URL, [ /** * Filters the parameters sent to the Critical CSS generator API. * * @since 2.11 * * @param array $params An array of parameters to send to the API. */ 'body' => apply_filters( 'rocket_cpcss_job_request', $params ), ] ); return $this->prepare_response( $response, $url, $is_mobile, $item_type ); } /** * Prepare the response to be returned. * * @since 3.6 * * @param array|WP_Error $response The response or WP_Error on failure. * @param string $url Url to be checked. * @param bool $is_mobile Optional. Flag for if this is cpcss for mobile or not. Default: false. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * * @return array|WP_Error */ private function prepare_response( $response, $url, $is_mobile = false, $item_type = 'custom' ) { if ( is_wp_error( $response ) ) { return new WP_Error( $this->get_response_code( $response ), sprintf( // translators: %1$s = type of content, %2$s = error message. __( 'Critical CSS for %1$s not generated. Error: %2$s', 'rocket' ), ( 'custom' === $item_type ) ? $url : $item_type, $response->get_error_message() ), [ 'status' => 400, ] ); } $response_data = $this->get_response_data( $response ); $response_status_code = $this->get_response_status( $response, ( isset( $response_data->status ) ) ? $response_data->status : null ); $succeeded = $this->get_response_success( $response_status_code, $response_data ); if ( $succeeded ) { return $response_data; } $response_message = $this->get_response_message( $response_status_code, $response_data, $url, $is_mobile, $item_type ); if ( 200 === $response_status_code ) { $response_status_code = 400; } return new WP_Error( $this->get_response_code( $response ), $response_message, [ 'status' => $response_status_code, ] ); } /** * Get the status of response. * * @since 3.6 * * @param int $response_code Response code to check success or failure. * @param stdClass $response_data Object of data returned from request. * * @return bool success or failed. */ private function get_response_success( int $response_code, $response_data ) { return ( 200 === $response_code && ( ! empty( (array) $response_data ) ) && ( ( isset( $response_data->status ) && 200 === $response_data->status ) || ( isset( $response_data->data ) && isset( $response_data->data->id ) ) ) ); } /** * Get response status code/number. * * @since 3.6 * * @param array|WP_Error $response The response or WP_Error on failure. * @param null|int $status Optional. Status code to overwrite the response status. Default: null. * * @return int status code|number of response. */ private function get_response_status( $response, $status = null ) { if ( ! is_null( $status ) ) { return (int) $status; } return (int) wp_remote_retrieve_response_code( $response ); } /** * Get response message. * * @since 3.6 * * @param int $response_status_code Response status code. * @param stdClass $response_data Object of data returned from request. * @param string $url Url for the web page to be checked. * @param bool $is_mobile Optional. Flag for if this is cpcss for mobile or not. Default: false. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * * @return string */ private function get_response_message( $response_status_code, $response_data, $url, $is_mobile = false, $item_type = 'custom' ) { $message = ''; switch ( $response_status_code ) { case 200: if ( ! isset( $response_data->data->id ) ) { $message .= sprintf( $is_mobile ? // translators: %s = item URL. __( 'Critical CSS for %1$s on mobile not generated. Error: The API returned an empty response.', 'rocket' ) : // translators: %s = item URL. __( 'Critical CSS for %1$s not generated. Error: The API returned an empty response.', 'rocket' ), ( 'custom' === $item_type ) ? $url : $item_type ); } break; case 400: case 440: case 404: // translators: %s = item URL. $message .= sprintf( $is_mobile // translators: %s = item URL. ? __( 'Critical CSS for %1$s on mobile not generated.', 'rocket' ) // translators: %s = item URL. : __( 'Critical CSS for %1$s not generated.', 'rocket' ), ( 'custom' === $item_type ) ? $url : $item_type ); break; default: $message .= sprintf( $is_mobile // translators: %s = URL. ? __( 'Critical CSS for %1$s on mobile not generated. Error: The API returned an invalid response code.', 'rocket' ) // translators: %s = URL. : __( 'Critical CSS for %1$s not generated. Error: The API returned an invalid response code.', 'rocket' ), ( 'custom' === $item_type ) ? $url : $item_type ); break; } if ( isset( $response_data->message ) ) { // translators: %1$s = error message. $message .= ' ' . sprintf( __( 'Error: %1$s', 'rocket' ), $response_data->message ); } return $message; } /** * Get response data from the API. * * @since 3.6 * * @param array|WP_Error $response The response or WP_Error on failure. * * @return mixed response of API. */ private function get_response_data( $response ) { return json_decode( wp_remote_retrieve_body( $response ) ); } /** * Get our internal response code [Not the standard HTTP codes]. * * @since 3.6 * * @param array|WP_Error $response The response or WP_Error on failure. * * @return string response code. */ private function get_response_code( $response ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found // Todo: we can return code based on the response status number, for example 404 not_found. return 'cpcss_generation_failed'; } /** * Get job details by calling API with job ID. * * @since 3.6 * * @param string $job_id ID for the job to get details. * @param string $url URL to be used in error messages. * @param bool $is_mobile Optional. Flag for if this is cpcss for mobile or not. Default: false. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * * @return mixed|WP_Error Details for job. */ public function get_job_details( $job_id, $url, $is_mobile = false, $item_type = 'custom' ) { $response = wp_remote_get( self::API_URL . "{$job_id}/" ); return $this->prepare_response( $response, $url, $is_mobile, $item_type ); } } Engine/CriticalPath/RESTWP.php 0000644 00000016577 15174677547 0012124 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use WP_REST_Request; use WP_REST_Response; use WP_Error; use WP_Rocket\Admin\Options_Data; /** * Class RESTWP * * @package WP_Rocket\Engine\CriticalPath */ abstract class RESTWP implements RESTWPInterface { /** * Namespace for REST Route. */ const ROUTE_NAMESPACE = 'wp-rocket/v1'; /** * Part of route namespace for this inherited class item type. * * @var string $route_namespace to be set with like post, term. */ protected $route_namespace; /** * CPCSS generation and deletion service. * * @var ProcessorService instance for this service. */ private $cpcss_service; /** * WP Rocket options instance. * * @var Options_Data */ private $options; /** * RESTWP constructor. * * @since 3.6 * * @param ProcessorService $cpcss_service Has the logic for cpcss generation and deletion. * @param Options_Data $options Instance of options data handler. */ public function __construct( ProcessorService $cpcss_service, Options_Data $options ) { $this->cpcss_service = $cpcss_service; $this->options = $options; } /** * Registers the generate route in the WP REST API * * @since 3.6 * * @return void */ public function register_generate_route() { register_rest_route( self::ROUTE_NAMESPACE, 'cpcss/' . $this->route_namespace . '/(?P<id>[\d]+)', [ 'methods' => 'POST', 'callback' => [ $this, 'generate' ], 'permission_callback' => [ $this, 'check_permissions' ], ] ); } /** * Register Delete CPCSS route in the WP REST API. * * @since 3.6 */ public function register_delete_route() { register_rest_route( self::ROUTE_NAMESPACE, 'cpcss/' . $this->route_namespace . '/(?P<id>[\d]+)', [ 'methods' => 'DELETE', 'callback' => [ $this, 'delete' ], 'permission_callback' => [ $this, 'check_permissions' ], ] ); } /** * Checks user's permissions. This is a callback registered to REST route's "permission_callback" parameter. * * @since 3.6 * * @return bool true if the user has permission; else false. */ public function check_permissions() { return current_user_can( 'rocket_regenerate_critical_css' ); } /** * Clean post cache files on CPCSS generation or deletion. * * @since 3.6.1 * * @param int $item_id ID for this item to get Url for. */ private function clean_post_cache( $item_id ) { rocket_clean_files( $this->get_url( $item_id ) ); } /** * Generates the CPCSS for the requested post ID. * * @since 3.6 * * @param WP_REST_Request $request WP REST request response. * * @return WP_REST_Response */ public function generate( WP_REST_Request $request ) { $item_id = (int) $request->get_param( 'id' ); $is_mobile = (bool) $request->get_param( 'is_mobile' ); // Bailout in case mobile CPCSS generation is called but this option is disabled. if ( $is_mobile && ( ! $this->options->get( 'async_css_mobile', 0 ) || ! $this->options->get( 'do_caching_mobile_files', 0 ) ) ) { return rest_ensure_response( $this->return_error( new WP_Error( 'mobile_cpcss_not_enabled', __( 'Mobile CPCSS generation not enabled.', 'rocket' ), [ 'status' => 400, ] ) ) ); } // validate item. $validated = $this->validate_item_for_generate( $item_id ); if ( is_wp_error( $validated ) ) { return rest_ensure_response( $this->return_error( $validated ) ); } // get item url. $item_url = $this->get_url( $item_id ); $timeout = ( isset( $request['timeout'] ) && ! empty( $request['timeout'] ) ); $item_path = $this->get_path( $item_id, $is_mobile ); $additional_params = [ 'timeout' => $timeout, 'is_mobile' => $is_mobile, 'item_type' => 'custom', ]; $generated = $this->cpcss_service->process_generate( $item_url, $item_path, $additional_params ); if ( is_wp_error( $generated ) ) { return rest_ensure_response( $this->return_error( $generated ) ); } $this->clean_post_cache( $item_id ); return rest_ensure_response( $this->return_success( $generated ) ); } /** * Validate the item to be sent to generate CPCSS. * * @since 3.6 * * @param int $item_id ID for this item to be validated. * * @return true|WP_Error */ abstract protected function validate_item_for_generate( $item_id ); /** * Validate the item to be sent to Delete CPCSS. * * @since 3.6 * * @param int $item_id ID for this item to be validated. * * @return true|WP_Error */ abstract protected function validate_item_for_delete( $item_id ); /** * Get url for this item. * * @since 3.6 * * @param int $item_id ID for this item to get Url for. * * @return false|string */ abstract protected function get_url( $item_id ); /** * Get CPCSS file path to save CPCSS code into. * * @since 3.6 * * @param int $item_id ID for this item to get the path for. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * * @return string */ abstract protected function get_path( $item_id, $is_mobile = false ); /** * Delete Post ID CPCSS file. * * @since 3.6 * * @param WP_REST_Request $request the WP Rest Request object. * * @return WP_REST_Response */ public function delete( WP_REST_Request $request ) { $item_id = (int) $request->get_param( 'id' ); // validate item. $validated = $this->validate_item_for_delete( $item_id ); if ( is_wp_error( $validated ) ) { return rest_ensure_response( $this->return_error( $validated ) ); } if ( $this->options->get( 'async_css_mobile', 0 ) ) { $mobile_item_path = $this->get_path( $item_id, true ); $this->cpcss_service->process_delete( $mobile_item_path ); } $item_path = $this->get_path( $item_id ); $deleted = $this->cpcss_service->process_delete( $item_path ); if ( is_wp_error( $deleted ) ) { return rest_ensure_response( $this->return_error( $deleted ) ); } $this->clean_post_cache( $item_id ); return rest_ensure_response( $this->return_success( $deleted ) ); } /** * Returns the formatted array response * * @since 3.6 * * @param bool $success True for success, false otherwise. * @param string $code The code to use for the response. * @param string $message The message to send in the response. * @param int $status The status code to send for the response. * * @return array */ protected function return_array_response( $success = false, $code = '', $message = '', $status = 200 ) { return [ 'success' => $success, 'code' => $code, 'message' => $message, 'data' => [ 'status' => $status, ], ]; } /** * Convert WP_Error into array to be used in response. * * @since 3.6 * * @param WP_Error $error Error that will be converted to array. * * @return array */ protected function return_error( $error ) { $error_data = $error->get_error_data(); return $this->return_array_response( false, $error->get_error_code(), $error->get_error_message(), isset( $error_data['status'] ) ? $error_data['status'] : 400 ); } /** * Return success to be used in response. * * @since 3.6 * * @param array $data which has success parameters with two keys: code and message. * * @return array */ protected function return_success( $data ) { return $this->return_array_response( true, $data['code'], $data['message'], 200 ); } } Engine/CriticalPath/RESTWPPost.php 0000644 00000004141 15174677547 0012752 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use WP_Error; /** * Class RESTWPPost * * @package WP_Rocket\Engine\CriticalPath */ class RESTWPPost extends RESTWP { /** * Part of route namespace for this inherited class item type. * * @var string $route_namespace to be set with like post, term. */ protected $route_namespace = 'post'; /** * Validate the item to be sent to generate CPCSS. * * @since 3.6 * * @param int $post_id ID for this post to be validated. * * @return true|WP_Error */ protected function validate_item_for_generate( $post_id ) { $status = get_post_status( $post_id ); if ( ! $status ) { return new WP_Error( 'post_not_exists', __( 'Requested post does not exist.', 'rocket' ), [ 'status' => 400, ] ); } if ( 'publish' !== $status ) { return new WP_Error( 'post_not_published', __( 'Cannot generate CPCSS for unpublished post.', 'rocket' ), [ 'status' => 400, ] ); } return true; } /** * Validate the item to be sent to delete CPCSS. * * @since 3.6 * * @param int $post_id ID for this post to be validated. * * @return true|WP_Error */ protected function validate_item_for_delete( $post_id ) { if ( empty( get_permalink( $post_id ) ) ) { return new WP_Error( 'post_not_exists', __( 'Requested post does not exist.', 'rocket' ), [ 'status' => 400, ] ); } return true; } /** * Get url for this item. * * @since 3.6 * * @param int $post_id ID for this post to be validated. * * @return false|string */ protected function get_url( $post_id ) { return get_permalink( $post_id ); } /** * Get CPCSS file path to save CPCSS code into. * * @since 3.6 * * @param int $post_id ID for this post to be validated. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * * @return string */ protected function get_path( $post_id, $is_mobile = false ) { $post_type = get_post_type( $post_id ); return 'posts' . DIRECTORY_SEPARATOR . "{$post_type}-{$post_id}" . ( $is_mobile ? '-mobile' : '' ) . '.css'; } } Engine/CriticalPath/ServiceProvider.php 0000644 00000006422 15174677547 0014177 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\CriticalPath; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\CriticalPath\Admin\{Admin, Post, Settings, Subscriber}; /** * Service provider for the Critical CSS classes */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'critical_css_generation', 'critical_css', 'critical_css_subscriber', 'cpcss_api_client', 'cpcss_data_manager', 'cpcss_service', 'rest_cpcss_wp_post', 'rest_cpcss_subscriber', 'cpcss_settings', 'cpcss_post', 'cpcss_admin', 'critical_css_admin_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $filesystem = rocket_direct_filesystem(); $critical_css_path = new StringArgument( rocket_get_constant( 'WP_ROCKET_CRITICAL_CSS_PATH' ) ); $template_path = new StringArgument( $this->getContainer()->get( 'template_path' ) . '/cpcss' ); $this->getContainer()->add( 'cpcss_api_client', APIClient::class ); $this->getContainer()->add( 'cpcss_data_manager', DataManager::class ) ->addArguments( [ $critical_css_path, $filesystem, ] ); $this->getContainer()->add( 'cpcss_service', ProcessorService::class ) ->addArguments( [ 'cpcss_data_manager', 'cpcss_api_client', ] ); // REST CPCSS START. $this->getContainer()->add( 'rest_cpcss_wp_post', RESTWPPost::class ) ->addArguments( [ 'cpcss_service', 'options', ] ); $this->getContainer()->addShared( 'rest_cpcss_subscriber', RESTCSSSubscriber::class ) ->addArgument( 'rest_cpcss_wp_post' ); // REST CPCSS END. $this->getContainer()->add( 'critical_css_generation', CriticalCSSGeneration::class ) ->addArgument( 'cpcss_service' ); $this->getContainer()->add( 'critical_css', CriticalCSS::class ) ->addArguments( [ 'critical_css_generation', 'options', $filesystem, ] ); $this->getContainer()->addShared( 'critical_css_subscriber', CriticalCSSSubscriber::class ) ->addArguments( [ 'critical_css', 'cpcss_service', 'options', 'options_api', 'user', $filesystem, ] ); $this->getContainer()->add( 'cpcss_post', Post::class ) ->addArguments( [ 'options', 'beacon', $critical_css_path, $template_path, ] ); $this->getContainer()->add( 'cpcss_settings', Settings::class ) ->addArguments( [ 'options', 'beacon', 'critical_css', $template_path, ] ); $this->getContainer()->add( 'cpcss_admin', Admin::class ) ->addArguments( [ 'options', 'cpcss_service', ] ); $this->getContainer()->addShared( 'critical_css_admin_subscriber', Subscriber::class ) ->addArguments( [ 'cpcss_post', 'cpcss_settings', 'cpcss_admin', ] ); } } Engine/CriticalPath/RESTWPInterface.php 0000644 00000002125 15174677547 0013725 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use WP_REST_Request; use WP_REST_Response; interface RESTWPInterface { /** * Registers the generate route in the WP REST API * * @since 3.6 * * @return void */ public function register_generate_route(); /** * Register Delete CPCSS route in the WP REST API. * * @since 3.6 * * @return void */ public function register_delete_route(); /** * Checks user's permissions. This is a callback registered to REST route's "permission_callback" parameter. * * @since 3.6 * * @return bool true if the user has permission; else false. */ public function check_permissions(); /** * Generates the CPCSS for the requested post ID. * * @since 3.6 * * @param WP_REST_Request $request WP REST request response. * * @return WP_REST_Response */ public function generate( WP_REST_Request $request ); /** * Delete Post ID CPCSS file. * * @since 3.6 * * @param WP_REST_Request $request the WP Rest Request object. * * @return WP_REST_Response */ public function delete( WP_REST_Request $request ); } Engine/CriticalPath/DataManager.php 0000644 00000012162 15174677547 0013226 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use WP_Error; use WP_Filesystem_Direct; use WP_Rocket\Engine\Optimization\CSSTrait; /** * Class DataManager * * @package WP_Rocket\Engine\CriticalPath */ class DataManager { use CSSTrait; /** * Base critical CSS path for posts. * * @var string */ private $critical_css_path; /** * Instance of the filesystem handler. * * @var WP_Filesystem_Direct */ private $filesystem; /** * DataManager constructor, adjust the critical css path for posts. * * @param string $critical_css_path path for main critical css folder. * @param WP_Filesystem_Direct $filesystem Instance of the filesystem handler. */ public function __construct( $critical_css_path, $filesystem ) { $this->critical_css_path = $critical_css_path . get_current_blog_id() . DIRECTORY_SEPARATOR; $this->filesystem = $filesystem; } /** * Save CPCSS into file. * * @since 3.6 * * @param string $path Path for cpcss file related to this web page. * @param string $cpcss CPCSS code to be saved. * @param string $url URL for item to be used in error messages. * @param bool $is_mobile If this is cpcss for mobile or not. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * * @return bool|WP_Error */ public function save_cpcss( $path, $cpcss, $url, $is_mobile = false, $item_type = 'custom' ) { $file_path_directory = dirname( $this->critical_css_path . $path ); if ( ! $this->filesystem->is_dir( $file_path_directory ) ) { if ( ! rocket_mkdir_p( $file_path_directory ) ) { return new WP_Error( 'cpcss_generation_failed', // translators: %s = item URL. sprintf( $is_mobile ? // translators: %s = item URL. __( 'Critical CSS for %1$s on mobile not generated. Error: The destination folder could not be created.', 'rocket' ) : // translators: %s = item URL. __( 'Critical CSS for %1$s not generated. Error: The destination folder could not be created.', 'rocket' ), ( 'custom' === $item_type ) ? $url : $item_type ), [ 'status' => 400, ] ); } } $cpcss = $this->apply_font_display_swap( $cpcss ); return rocket_put_content( $this->critical_css_path . $path, wp_strip_all_tags( $cpcss, true ) ); } /** * Delete critical css file by path. * * @param string $path Critical css file path to be deleted. * @param bool $is_mobile If this is cpcss for mobile or not. * * @return bool|WP_Error */ public function delete_cpcss( $path, $is_mobile = false ) { $full_path = $this->critical_css_path . $path; if ( ! $this->filesystem->exists( $full_path ) ) { return new WP_Error( 'cpcss_not_exists', $is_mobile ? __( 'Critical CSS file for mobile does not exist', 'rocket' ) : __( 'Critical CSS file does not exist', 'rocket' ), [ 'status' => 400, ] ); } if ( ! $this->filesystem->delete( $full_path ) ) { return new WP_Error( 'cpcss_deleted_failed', $is_mobile ? __( 'Critical CSS file for mobile cannot be deleted', 'rocket' ) : __( 'Critical CSS file cannot be deleted', 'rocket' ), [ 'status' => 400, ] ); } return true; } /** * Get job_id from cache based on item_url. * * @since 3.6 * * @param string $item_url URL for item to be used in error messages. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * * @return mixed */ public function get_cache_job_id( $item_url, $is_mobile = false ) { $cache_key = $this->get_cache_key_from_url( $item_url, $is_mobile ); return get_transient( $cache_key ); } /** * Set Job_id for Item_url into cache. * * @since 3.6 * * @param string $item_url URL for item to be used in error messages. * @param string $job_id ID for the job to get details. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * * @return bool */ public function set_cache_job_id( $item_url, $job_id, $is_mobile = false ) { $cache_key = $this->get_cache_key_from_url( $item_url, $is_mobile ); return set_transient( $cache_key, $job_id, HOUR_IN_SECONDS ); } /** * Delete job_id from cache based on item_url. * * @since 3.6 * * @param string $item_url URL for item to be used in error messages. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * * @return bool */ public function delete_cache_job_id( $item_url, $is_mobile = false ) { $cache_key = $this->get_cache_key_from_url( $item_url, $is_mobile ); return delete_transient( $cache_key ); } /** * Get cache key from url to be used in caching job_id. * * @since 3.6 * * @param string $item_url URL for item to be used in error messages. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * * @return string */ private function get_cache_key_from_url( $item_url, $is_mobile = false ) { $encoded_url = md5( $item_url ); if ( $is_mobile ) { $encoded_url .= '_mobile'; } return 'rocket_specific_cpcss_job_' . $encoded_url; } } Engine/CriticalPath/TransientTrait.php 0000644 00000001745 15174677547 0014042 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; trait TransientTrait { /** * Updates CPCSS running transient with status notices. * * @since 3.6 * * @param array $transient Transient to be updated. * @param string $item_path Path for processed item. * @param bool $mobile If this request is for mobile cpcss. * @param string $message CPCSS reply message. * @param bool|string $success CPCSS success or failure. * @return void */ private function update_running_transient( $transient, $item_path, $mobile, $message, $success ) { $path = ! (bool) $mobile ? $item_path : str_replace( '-mobile.css', '.css', $item_path ); $transient['items'][ $path ]['status'][ ! (bool) $mobile ? 'nonmobile' : 'mobile' ]['message'] = $message; $transient['items'][ $path ]['status'][ ! (bool) $mobile ? 'nonmobile' : 'mobile' ]['success'] = $success; set_transient( 'rocket_critical_css_generation_process_running', $transient, HOUR_IN_SECONDS ); } } Engine/CriticalPath/CriticalCSSGeneration.php 0000644 00000004315 15174677547 0015202 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use WP_Rocket_WP_Background_Process; /** * Extends the background process class for the critical CSS generation process. * * @since 2.11 * * @see WP_Background_Process */ class CriticalCSSGeneration extends WP_Rocket_WP_Background_Process { use TransientTrait; /** * Process prefix. * * @var string */ protected $prefix = 'rocket'; /** * Specific action identifier for sitemap preload. * * @var string Action identifier */ protected $action = 'critical_css_generation'; /** * ProcessorService instance. * * @var ProcessorService */ protected $processor; /** * Instantiate the class * * @param ProcessorService $processor ProcessorService instance. */ public function __construct( ProcessorService $processor ) { parent::__construct(); $this->processor = $processor; } /** * Perform the optimization corresponding to $item. * * @since 2.11 * * @param mixed $item Queue item to iterate over. * * @return bool false if task performed successfully, true otherwise to re-queue the item. */ protected function task( $item ) { if ( ! is_array( $item ) ) { return false; } $transient = get_transient( 'rocket_critical_css_generation_process_running' ); $mobile = isset( $item['mobile'] ) ? $item['mobile'] : 0; $generation_params = [ 'is_mobile' => $mobile, 'item_type' => $item['type'], ]; $generated = $this->processor->process_generate( $item['url'], $item['path'], $generation_params ); if ( is_wp_error( $generated ) ) { $this->update_running_transient( $transient, $item['path'], $mobile, $generated->get_error_message(), false ); return false; } if ( isset( $generated['code'] ) && 'cpcss_generation_pending' === $generated['code'] ) { $pending = get_transient( 'rocket_cpcss_generation_pending' ); if ( false === $pending ) { $pending = []; } $pending[ $item['path'] ] = $item; set_transient( 'rocket_cpcss_generation_pending', $pending, HOUR_IN_SECONDS ); return false; } $this->update_running_transient( $transient, $item['path'], $mobile, $generated['message'], ( 'cpcss_generation_successful' === $generated['code'] ) ); return false; } } Engine/CriticalPath/ProcessorService.php 0000644 00000023107 15174677547 0014363 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath; use WP_Error; class ProcessorService { /** * Responsible for dealing with data/database. * * @var DataManager datamanager instance. */ private $data_manager; /** * Responsible for dealing with CPCSS APIs. * * @var APIClient api_client instance. */ private $api_client; /** * RESTWP constructor. * * @since 3.6 * * @param DataManager $data_manager Data manager instance, responsible for dealing with data/database. * @param APIClient $api_client API Client instance to deal with CPCSS APIs. */ public function __construct( DataManager $data_manager, APIClient $api_client ) { $this->data_manager = $data_manager; $this->api_client = $api_client; } /** * Process CPCSS generation, Check timeout and send the generation request. * * @since 3.6 * * @param string $item_url URL for item to be used in error messages. * @param string $item_path Path for item to be processed. * @param array $additional_parameters additional parameters for generation. * * @return array|WP_Error */ public function process_generate( $item_url, $item_path, $additional_parameters = [] ) { $defaults = [ 'timeout' => false, 'is_mobile' => false, 'item_type' => 'custom', ]; $args = array_merge( $defaults, $additional_parameters ); // Ajax call requested a timeout. if ( $args['timeout'] ) { return $this->process_timeout( $item_url, $args['is_mobile'], $args['item_type'] ); } $cpcss_job_id = $this->data_manager->get_cache_job_id( $item_url, $args['is_mobile'] ); if ( false === $cpcss_job_id ) { return $this->send_generation_request( $item_url, $item_path, $args['is_mobile'], $args['item_type'] ); } // job_id is found and we need to check status for it. return $this->check_cpcss_job_status( $cpcss_job_id, $item_path, $item_url, $args['is_mobile'], $args['item_type'] ); } /** * Send Generation first request. * * @since 3.6 * * @param string $item_url Url for item to send the generation request for. * @param string $item_path Path for item to send the generation request for. * @param bool $is_mobile If this request is for mobile cpcss. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * * @return array */ private function send_generation_request( $item_url, $item_path, $is_mobile = false, $item_type = 'custom' ) { // call send generation request from APIClient for the first time. $params = [ 'mobile' => (int) $is_mobile, 'nofontface' => false, ]; $generated_job = $this->api_client->send_generation_request( $item_url, $params, $item_type ); // validate generate response. if ( is_wp_error( $generated_job ) || ! is_object( $generated_job ) ) { // Failed so return back the data. return $generated_job; } // Send generation request succeeded. // Save job_id into cache. $this->data_manager->set_cache_job_id( $item_url, $generated_job->data->id, $is_mobile ); return $this->check_cpcss_job_status( $generated_job->data->id, $item_path, $item_url, $is_mobile, $item_type ); } /** * Check status and process the output for a job. * * @since 3.6 * * @param string $job_id ID for the job to get details. * @param string $item_path Path for this item to be validated. * @param string $item_url URL for item to be used in error messages. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * * @return array|WP_Error Response in case of success, failure or pending. */ private function check_cpcss_job_status( $job_id, $item_path, $item_url, $is_mobile = false, $item_type = 'custom' ) { $job_details = $this->api_client->get_job_details( $job_id, $item_url, $is_mobile, $item_type ); if ( is_wp_error( $job_details ) ) { $this->data_manager->delete_cache_job_id( $item_url, $is_mobile ); return $job_details; } if ( 200 !== $job_details->status ) { // On job error. return $this->on_job_error( $job_details, $item_url, $is_mobile, $item_type ); } // On job status 200. $job_state = $job_details->data->state; // For pending job status. if ( isset( $job_state ) && 'complete' !== $job_state ) { return $this->on_job_pending( $item_url, $item_type ); } // For successful job status. if ( isset( $job_state, $job_details->data->critical_path ) ) { return $this->on_job_success( $item_path, $item_url, $job_details->data->critical_path, $is_mobile, $item_type ); } return $this->on_job_error( $job_details, $item_url, $is_mobile, $item_type ); } /** * Process logic for job error. * * @since 3.6 * * @param object $job_details Job details object. * @param string $item_url Url for web page to be processed, used for error messages. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * * @return WP_Error */ private function on_job_error( $job_details, $item_url, $is_mobile = false, $item_type = 'custom' ) { $this->data_manager->delete_cache_job_id( $item_url, $is_mobile ); if ( $is_mobile ) { $error = sprintf( // translators: %1$s = item URL or item type. __( 'Mobile Critical CSS for %1$s not generated.', 'rocket' ), ( 'custom' === $item_type ) ? $item_url : $item_type ); } else { $error = sprintf( // translators: %1$s = item URL or item type. __( 'Critical CSS for %1$s not generated.', 'rocket' ), ( 'custom' === $item_type ) ? $item_url : $item_type ); } if ( isset( $job_details->message ) ) { // translators: %1$s = error message. $error .= ' ' . sprintf( __( 'Error: %1$s', 'rocket' ), $job_details->message ); } return new WP_Error( 'cpcss_generation_failed', $error, [ 'status' => 400, ] ); } /** * Process logic for job pending status. * * @since 3.6 * * @param string $item_url Url for web page to be processed, used for error messages. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * * @return array */ private function on_job_pending( $item_url, $item_type = 'custom' ) { return [ 'code' => 'cpcss_generation_pending', 'message' => sprintf( // translators: %1$s = Item URL or item type. __( 'Critical CSS for %s in progress.', 'rocket' ), ( 'custom' === $item_type ) ? $item_url : $item_type ), ]; } /** * Process logic for job success status. * * @since 3.6 * * @param string $item_path Item Path for web page to be processed. * @param string $item_url Item Url for web page to be processed. * @param string $cpcss_code CPCSS Code to be saved. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * * @return array|WP_Error */ private function on_job_success( $item_path, $item_url, $cpcss_code, $is_mobile = false, $item_type = 'custom' ) { // delete cache job_id for this item. $this->data_manager->delete_cache_job_id( $item_url, $is_mobile ); // save the generated CPCSS code into file. $saved = $this->data_manager->save_cpcss( $item_path, $cpcss_code, $item_url, $is_mobile, $item_type ); if ( is_wp_error( $saved ) ) { return $saved; } if ( $is_mobile ) { return [ 'code' => 'cpcss_generation_successful', 'message' => sprintf( // translators: %1$s = Item URL or item type. __( 'Mobile Critical CSS for %s generated.', 'rocket' ), ( 'custom' === $item_type ) ? $item_url : $item_type ), ]; } // Send the current status of job. return [ 'code' => 'cpcss_generation_successful', 'message' => sprintf( // translators: %1$s = Item URL or item type. __( 'Critical CSS for %s generated.', 'rocket' ), ( 'custom' === $item_type ) ? $item_url : $item_type ), ]; } /** * Process the login for CPCSS deletion. * * @param string $item_path Path for item to delete CPCSS code. * * @return array|WP_Error */ public function process_delete( $item_path ) { $deleted = $this->data_manager->delete_cpcss( $item_path ); if ( is_wp_error( $deleted ) ) { return $deleted; } return [ 'code' => 'success', 'message' => __( 'Critical CSS file deleted successfully.', 'rocket' ), ]; } /** * Process timeout action for CPCSS generation. * * @since 3.6 * * @param string $item_url URL for item to be used in error messages. * @param bool $is_mobile Bool identifier for is_mobile CPCSS generation. * @param string $item_type Optional. Type for this item if it's custom or specific type. Default: custom. * @return WP_Error */ private function process_timeout( $item_url, $is_mobile = false, $item_type = 'custom' ) { $this->data_manager->delete_cache_job_id( $item_url, $is_mobile ); if ( $is_mobile ) { return new WP_Error( 'cpcss_generation_timeout', sprintf( // translators: %1$s = Item URL or item type. __( 'Mobile Critical CSS for %1$s timeout. Please retry a little later.', 'rocket' ), ( 'custom' === $item_type ) ? $item_url : $item_type ), [ 'status' => 400, ] ); } return new WP_Error( 'cpcss_generation_timeout', sprintf( // translators: %1$s = Item URL or item type. __( 'Critical CSS for %1$s timeout. Please retry a little later.', 'rocket' ), ( 'custom' === $item_type ) ? $item_url : $item_type ), [ 'status' => 400, ] ); } } Engine/CriticalPath/Admin/Post.php 0000644 00000013217 15174677547 0013041 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath\Admin; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Beacon\Beacon; class Post extends Abstract_Render { /** * Instance of the Beacon handler. * * @var Beacon */ private $beacon; /** * Instance of options handler. * * @var Options_Data */ private $options; /** * Path to the critical-css directory. * * @var string */ private $critical_css_path; /** * Array of reasons to disable actions. * * @var null|array */ private $disabled_data; /** * Creates an instance of the subscriber. * * @param Options_Data $options WP Rocket Options instance. * @param Beacon $beacon Beacon instance. * @param string $critical_path Path to the critical CSS base folder. * @param string $template_path Path to the templates folder. */ public function __construct( Options_Data $options, Beacon $beacon, $critical_path, $template_path ) { parent::__construct( $template_path ); $this->beacon = $beacon; $this->options = $options; $this->critical_css_path = $critical_path . get_current_blog_id() . '/posts/'; } /** * Displays the critical CSS block in WP Rocket options metabox. * * @since 3.6 * * @return void */ public function cpcss_section() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } $data = [ 'disabled_description' => $this->get_disabled_description(), ]; echo $this->generate( 'metabox/container', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Displays the content inside the critical CSS block. * * @since 3.6 * * @return void */ public function cpcss_actions() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } $data = [ 'disabled' => $this->is_enabled(), 'beacon' => $this->beacon->get_suggest( 'async' ), 'cpcss_exists' => $this->cpcss_exists(), ]; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->generate( 'metabox/generate', $data // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Enqueue CPCSS generation / deletion script on edit.php page. * * @since 3.6 * * @param string $page The current admin page. * * @return void */ public function enqueue_admin_edit_script( $page ) { global $post, $pagenow; if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } // Bailout if the page is not Post / Page. if ( ! in_array( $page, [ 'edit.php', 'post.php' ], true ) ) { return; } if ( ! in_array( $pagenow, [ 'post-new.php', 'post.php' ], true ) ) { return; } // Bailout if the CPCSS is not enabled for this Post / Page. if ( $this->is_enabled() ) { return; } $post_id = ( 'post-new.php' === $pagenow ) ? '' : $post->ID; wp_enqueue_script( 'wpr-edit-cpcss-script', rocket_get_constant( 'WP_ROCKET_ASSETS_JS_URL' ) . 'wpr-cpcss.js', [], rocket_get_constant( 'WP_ROCKET_VERSION' ), true ); wp_localize_script( 'wpr-edit-cpcss-script', 'rocket_cpcss', [ 'rest_url' => rest_url( "wp-rocket/v1/cpcss/post/{$post_id}" ), 'rest_nonce' => wp_create_nonce( 'wp_rest' ), 'generate_btn' => __( 'Generate Specific CPCSS', 'rocket' ), 'regenerate_btn' => __( 'Regenerate specific CPCSS', 'rocket' ), 'wprMobileCpcssEnabled' => $this->options->get( 'async_css_mobile', 0 ), ] ); } /** * Gets data for the disabled checks. * * @since 3.6 * * @return null|array */ private function get_disabled_data() { global $post; if ( rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ) { $this->disabled_data = null; } if ( isset( $this->disabled_data ) ) { return $this->disabled_data; } if ( 'publish' !== $post->post_status ) { $this->disabled_data['not_published'] = 1; } if ( ! $this->options->get( 'async_css', 0 ) ) { $this->disabled_data['option_disabled'] = 1; } if ( get_post_meta( $post->ID, '_rocket_exclude_async_css', true ) ) { $this->disabled_data['option_excluded'] = 1; } if ( ! is_post_type_viewable( get_post_type( $post ) ) ) { $this->disabled_data['not_viewable'] = 1; } return $this->disabled_data; } /** * Checks if critical CSS generation is enabled for the current post. * * @since 3.6 * * @return bool */ private function is_enabled() { return ! empty( $this->get_disabled_data() ); } /** * Returns the reason why actions are disabled. * * @since 3.6 * * @return string */ private function get_disabled_description() { global $post; $disabled_data = $this->get_disabled_data(); if ( empty( $disabled_data ) ) { return ''; } if ( isset( $disabled_data['not_viewable'] ) ) { return __( 'This feature is not available for non-public post types.', 'rocket' ); } $notice = __( '%l to use this feature.', 'rocket' ); $list = [ // translators: %s = post type. 'not_published' => sprintf( __( 'Publish the %s', 'rocket' ), $post->post_type ), 'option_disabled' => __( 'Enable Load CSS asynchronously in WP Rocket settings', 'rocket' ), 'option_excluded' => __( 'Enable Load CSS asynchronously in the options above', 'rocket' ), ]; return wp_sprintf_l( $notice, array_intersect_key( $list, $disabled_data ) ); } /** * Checks if a specific critical css file exists for the current post. * * @since 3.6 * * @return bool */ private function cpcss_exists() { global $post; $post_cpcss = "{$this->critical_css_path}{$post->post_type}-{$post->ID}.css"; return rocket_direct_filesystem()->exists( $post_cpcss ); } } Engine/CriticalPath/Admin/Subscriber.php 0000644 00000011322 15174677547 0014212 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\CriticalPath\Admin; use WP_Admin_Bar; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Instance of the CPCSS Settings handler. * * @var Settings */ private $settings; /** * Instance of the Post handler * * @var Post */ private $post; /** * Instance of the Admin handler * * @var Admin */ private $admin; /** * Creates an instance of the subscriber. * * @param Post $post Post instance. * @param Settings $settings CPCSS Settings instance. * @param Admin $admin Admin instance. */ public function __construct( Post $post, Settings $settings, Admin $admin ) { $this->post = $post; $this->settings = $settings; $this->admin = $admin; } /** * Events this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_after_options_metabox' => 'cpcss_section', 'rocket_metabox_cpcss_content' => 'cpcss_actions', 'rocket_first_install_options' => 'add_async_css_mobile_option', 'wp_rocket_upgrade' => [ 'set_async_css_mobile_default_value', 12, 2 ], 'rocket_hidden_settings_fields' => 'add_hidden_async_css_mobile', 'rocket_settings_tools_content' => 'display_cpcss_mobile_section', 'wp_ajax_rocket_enable_mobile_cpcss' => 'enable_mobile_cpcss', 'wp_ajax_rocket_cpcss_heartbeat' => 'cpcss_heartbeat', 'admin_enqueue_scripts' => [ [ 'enqueue_admin_edit_script' ], [ 'enqueue_admin_cpcss_heartbeat_script' ], ], 'rocket_admin_bar_items' => 'add_regenerate_menu_item', 'rocket_meta_boxes_fields' => [ 'add_meta_box', 3 ], ]; } /** * Enable CPCSS mobile. * * @since 3.6 * * @return void */ public function enable_mobile_cpcss() { $this->settings->enable_mobile_cpcss(); } /** * Display CPCSS mobile section tool admin view. * * @since 3.6 * * @return void */ public function display_cpcss_mobile_section() { $this->settings->display_cpcss_mobile_section(); } /** * Enqueue CPCSS generation / deletion script on edit.php page. * * @since 3.6 * * @param string $page The current admin page. * * @return void */ public function enqueue_admin_edit_script( $page ) { $this->post->enqueue_admin_edit_script( $page ); } /** * Displays the critical CSS block in WP Rocket options metabox. * * @since 3.6 * * @return void */ public function cpcss_section() { $this->post->cpcss_section(); } /** * Displays the content inside the critical CSS block. * * @since 3.6 * * @return void */ public function cpcss_actions() { $this->post->cpcss_actions(); } /** * Adds async_css_mobile option to WP Rocket options. * * @since 3.6 * * @param array $options WP Rocket options array. * * @return array */ public function add_async_css_mobile_option( $options ) { return $this->settings->add_async_css_mobile_option( $options ); } /** * Sets the default value of async_css_mobile to 0 when upgrading from < 3.6. * * @since 3.6 * * @param string $new_version New WP Rocket version. * @param string $old_version Previous WP Rocket version. */ public function set_async_css_mobile_default_value( $new_version, $old_version ) { $this->settings->set_async_css_mobile_default_value( $new_version, $old_version ); } /** * Adds async_css_mobile to the hidden settings fields. * * @since 3.6 * * @param array $hidden_settings_fields An array of hidden settings fields ID. * * @return array */ public function add_hidden_async_css_mobile( $hidden_settings_fields ) { return $this->settings->add_hidden_async_css_mobile( $hidden_settings_fields ); } /** * Check the CPCSS heartbeat. * * @since 3.6 */ public function cpcss_heartbeat() { $this->admin->cpcss_heartbeat(); } /** * Enqueue CPCSS heartbeat script on all admin pages. * * @since 3.6 */ public function enqueue_admin_cpcss_heartbeat_script() { $this->admin->enqueue_admin_cpcss_heartbeat_script(); } /** * Add Regenerate Critical CSS link to WP Rocket admin bar item * * @since 3.6 * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * @return void */ public function add_regenerate_menu_item( $wp_admin_bar ) { $this->admin->add_regenerate_menu_item( $wp_admin_bar ); } /** * Add the field to the WP Rocket metabox on the post edit page. * * @param string[] $fields Metaboxes fields. * * @return string[] */ public function add_meta_box( array $fields ) { $fields['async_css'] = __( 'Load CSS asynchronously', 'rocket' ); return $fields; } } Engine/CriticalPath/Admin/Settings.php 0000644 00000007242 15174677547 0013715 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath\Admin; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Engine\CriticalPath\CriticalCSS; class Settings extends Abstract_Render { /** * Instance of the Beacon handler. * * @var Beacon */ private $beacon; /** * Instance of options handler. * * @var Options_Data */ private $options; /** * Instance of CriticalCSS. * * @var CriticalCSS */ private $critical_css; /** * Creates an instance of the subscriber. * * @param Options_Data $options WP Rocket Options instance. * @param Beacon $beacon Beacon instance. * @param CriticalCSS $critical_css CriticalCSS instance. * @param string $template_path Path to the templates folder. */ public function __construct( Options_Data $options, Beacon $beacon, CriticalCSS $critical_css, $template_path ) { parent::__construct( $template_path ); $this->beacon = $beacon; $this->options = $options; $this->critical_css = $critical_css; } /** * Display CPCSS mobile section tool admin view. * * @since 3.6 * * @return void */ public function display_cpcss_mobile_section() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } // Bailout if CPCSS is not enabled & separate cache for mobiles is not enabled. // Or bailout if CPCSS mobile option is false. if ( ! ( $this->options->get( 'async_css', 0 ) && $this->options->get( 'cache_mobile', 0 ) && $this->options->get( 'do_caching_mobile_files', 0 ) ) || $this->options->get( 'async_css_mobile', 0 ) ) { return; } $data = [ 'beacon' => $this->beacon->get_suggest( 'async' ), ]; echo $this->generate( 'activate-cpcss-mobile', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Enable CPCSS mobile. * * @since 3.6 * * @return void */ public function enable_mobile_cpcss() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) || ! current_user_can( 'rocket_regenerate_critical_css' ) ) { wp_send_json_error(); } $this->options->set( 'async_css_mobile', 1 ); update_option( rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ), $this->options->get_options() ); // Start Mobile CPCSS process. $this->critical_css->process_handler( 'mobile' ); wp_send_json_success(); } /** * Adds async_css_mobile option to WP Rocket options. * * @since 3.6 * * @param array $options WP Rocket options array. * * @return array */ public function add_async_css_mobile_option( $options ) { $options = (array) $options; $options['async_css_mobile'] = 1; return $options; } /** * Sets the default value of async_css_mobile to 0 when upgrading from < 3.6. * * @since 3.6 * * @param string $new_version New WP Rocket version. * @param string $old_version Previous WP Rocket version. */ public function set_async_css_mobile_default_value( $new_version, $old_version ) { if ( version_compare( $old_version, '3.6', '>' ) ) { return; } $options = get_option( 'wp_rocket_settings', [] ); $options['async_css_mobile'] = 0; update_option( 'wp_rocket_settings', $options ); } /** * Adds async_css_mobile to the hidden settings fields. * * @since 3.6 * * @param array $hidden_settings_fields An array of hidden settings fields ID. * * @return array */ public function add_hidden_async_css_mobile( $hidden_settings_fields ) { $hidden_settings_fields = (array) $hidden_settings_fields; $hidden_settings_fields[] = 'async_css_mobile'; return $hidden_settings_fields; } } Engine/CriticalPath/Admin/Admin.php 0000644 00000015752 15174677547 0013152 0 ustar 00 <?php namespace WP_Rocket\Engine\CriticalPath\Admin; use WP_Admin_Bar; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\CriticalPath\ProcessorService; use WP_Rocket\Engine\CriticalPath\TransientTrait; class Admin { use TransientTrait; /** * Instance of options handler. * * @var Options_Data */ private $options; /** * Instance of ProcessorService. * * @var ProcessorService */ private $processor; /** * Creates an instance of the class. * * @param Options_Data $options Options instance. * @param ProcessorService $processor ProcessorService instance. */ public function __construct( Options_Data $options, ProcessorService $processor ) { $this->options = $options; $this->processor = $processor; } /** * Check the CPCSS heartbeat. * * @since 3.6 */ public function cpcss_heartbeat() { check_ajax_referer( 'cpcss_heartbeat_nonce', '_nonce', true ); if ( ! $this->is_async_css_enabled() || ! current_user_can( 'rocket_manage_options' ) || ! current_user_can( 'rocket_regenerate_critical_css' ) ) { wp_send_json_error(); } $cpcss_pending = get_transient( 'rocket_cpcss_generation_pending' ); if ( ! empty( $cpcss_pending ) ) { $cpcss_pending = $this->process_cpcss_pending_queue( (array) $cpcss_pending ); } if ( false !== $cpcss_pending && empty( $cpcss_pending ) ) { delete_transient( 'rocket_cpcss_generation_pending' ); } if ( empty( $cpcss_pending ) ) { $this->generation_complete(); wp_send_json_success( [ 'status' => 'cpcss_complete' ] ); } set_transient( 'rocket_cpcss_generation_pending', $cpcss_pending, HOUR_IN_SECONDS ); wp_send_json_success( [ 'status' => 'cpcss_running' ] ); } /** * Pull one item off of the CPCSS Pending Queue and process it. * * @since 3.6 * * @param array $cpcss_pending CPCSS Pending Queue. * * @return array remaining queue. */ private function process_cpcss_pending_queue( array $cpcss_pending ) { $cpcss_item = reset( $cpcss_pending ); if ( empty( $cpcss_item ) ) { return $cpcss_pending; } // Threshold 'check' > 10 = timed out. $timeout = ( $cpcss_item['check'] > 10 ); $additional_params = [ 'timeout' => $timeout, 'is_mobile' => ! empty( $cpcss_item['mobile'] ) ? (bool) $cpcss_item['mobile'] : false, 'item_type' => $cpcss_item['type'], ]; $cpcss_generation = $this->processor->process_generate( $cpcss_item['url'], $cpcss_item['path'], $additional_params ); // Increment this item's threshold count. ++$cpcss_pending[ $cpcss_item['path'] ]['check']; $this->cpcss_heartbeat_notices( $cpcss_generation, $cpcss_item ); // Remove the item from the queue when (a) the CPCSS API returns success or error or (b) timeouts. if ( is_wp_error( $cpcss_generation ) || 'cpcss_generation_successful' === $cpcss_generation['code'] || 'cpcss_generation_failed' === $cpcss_generation['code'] || $timeout ) { unset( $cpcss_pending[ $cpcss_item['path'] ] ); } return $cpcss_pending; } /** * CPCSS heartbeat update notices transients. * * @since 3.6 * * @param array|\WP_Error $cpcss_generation CPCSS regeneration reply. * @param array $cpcss_item Item processed. */ private function cpcss_heartbeat_notices( $cpcss_generation, $cpcss_item ) { $mobile = isset( $cpcss_item['mobile'] ) ? $cpcss_item['mobile'] : 0; $transient = (array) get_transient( 'rocket_critical_css_generation_process_running' ); // Initializes the transient. if ( ! isset( $transient['items'] ) ) { $transient['items'] = []; } if ( is_wp_error( $cpcss_generation ) ) { $this->update_running_transient( $transient, $cpcss_item['path'], $mobile, $cpcss_generation->get_error_message(), false ); return; } if ( isset( $cpcss_generation['code'] ) && ( 'cpcss_generation_successful' === $cpcss_generation['code'] || 'cpcss_generation_failed' === $cpcss_generation['code'] ) ) { $this->update_running_transient( $transient, $cpcss_item['path'], $mobile, $cpcss_generation['message'], ( 'cpcss_generation_successful' === $cpcss_generation['code'] ) ); } } /** * Launches when the CPCSS generation is complete. * * @since 3.6 */ private function generation_complete() { $running = get_transient( 'rocket_critical_css_generation_process_running' ); if ( false === $running ) { return; } if ( ! isset( $running['total'], $running['items'] ) ) { return; } if ( $running['total'] > count( $running['items'] ) ) { return; } /** * Fires when the critical CSS generation process is complete. * * @since 2.11 */ do_action( 'rocket_critical_css_generation_process_complete' ); rocket_clean_domain(); set_transient( 'rocket_critical_css_generation_process_complete', get_transient( 'rocket_critical_css_generation_process_running' ), HOUR_IN_SECONDS ); delete_transient( 'rocket_critical_css_generation_process_running' ); } /** * Enqueue CPCSS heartbeat script on all admin pages. * * @since 3.6 */ public function enqueue_admin_cpcss_heartbeat_script() { if ( ! $this->is_async_css_enabled() ) { return; } wp_enqueue_script( 'wpr-heartbeat-cpcss-script', rocket_get_constant( 'WP_ROCKET_ASSETS_JS_URL' ) . 'wpr-cpcss-heartbeat.js', [], rocket_get_constant( 'WP_ROCKET_VERSION' ), true ); wp_localize_script( 'wpr-heartbeat-cpcss-script', 'rocket_cpcss_heartbeat', [ 'nonce' => wp_create_nonce( 'cpcss_heartbeat_nonce' ), ] ); } /** * Add Regenerate Critical CSS link to WP Rocket admin bar item * * @since 3.6 * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. * * @return void */ public function add_regenerate_menu_item( $wp_admin_bar ) { if ( 'local' === wp_get_environment_type() ) { return; } if ( ! current_user_can( 'rocket_regenerate_critical_css' ) ) { return; } if ( ! is_admin() ) { return; } if ( ! $this->is_async_css_enabled() ) { return; } // This filter is documented in inc/Engine/CriticalPath/CriticalCSS.php. if ( ! apply_filters( 'do_rocket_critical_css_generation', true ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals return; } $referer = ''; $action = 'rocket_generate_critical_css'; if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { $referer_url = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ); $referer = '&_wp_http_referer=' . rawurlencode( remove_query_arg( 'fl_builder', $referer_url ) ); } $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => 'regenerate-critical-path', 'title' => __( 'Regenerate Critical Path CSS', 'rocket' ), 'href' => wp_nonce_url( admin_url( "admin-post.php?action={$action}{$referer}" ), $action ), ] ); } /** * Checks if the "async_css" option is enabled. * * @since 3.6 * * @return bool true when "async_css" option is enabled. */ private function is_async_css_enabled() { return (bool) $this->options->get( 'async_css', 0 ); } } Engine/Support/Subscriber.php 0000644 00000002234 15174677547 0012271 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Support; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Rest instance * * @var Rest */ private $rest; /** * Meta instance * * @var Meta */ private $meta; /** * Instantiate the class * * @param Rest $rest Rest instance. * @param Meta $meta Meta instance. */ public function __construct( Rest $rest, Meta $meta ) { $this->rest = $rest; $this->meta = $meta; } /** * Events this subscriber listens to. * * @return array */ public static function get_subscribed_events() { return [ 'rest_api_init' => 'register_support_route', 'rocket_buffer' => [ 'add_meta_generator', PHP_INT_MAX ], ]; } /** * Registers the rest support route * * @since 3.7.5 * * @return void */ public function register_support_route() { $this->rest->register_route(); } /** * Add the WP Rocket meta generator tag to the HTML * * @param string $html The HTML content. * @return string */ public function add_meta_generator( $html ): string { return $this->meta->add_meta_generator( $html ); } } Engine/Support/Meta.php 0000644 00000007022 15174677547 0011054 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Support; use WP_Rocket\Admin\Options_Data; use WP_Rocket_Mobile_Detect; class Meta { /** * Mobile Detect instance * * @var WP_Rocket_Mobile_Detect */ private $mobile_detect; /** * Options instance * * @var Options_Data */ private $options; /** * Instantiate the class * * @param WP_Rocket_Mobile_Detect $mobile_detect Mobile Detect instance. * @param Options_Data $options Options instance. */ public function __construct( WP_Rocket_Mobile_Detect $mobile_detect, Options_Data $options ) { $this->mobile_detect = $mobile_detect; $this->options = $options; } /** * Add the WP Rocket meta generator tag to the HTML * * @param string $html The HTML content. * @return string */ public function add_meta_generator( $html ): string { if ( rocket_bypass() ) { return $html; } if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return $html; } /** * Filters whether to disable the WP Rocket meta generator tag. * * @since 3.17.2 * * @param bool $disable True to disable, false otherwise. */ if ( wpm_apply_filters_typed( 'boolean', 'rocket_disable_meta_generator', false ) ) { return $html; } if ( rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_FOOTPRINT', false ) ) { return $this->remove_features_comments( $html ); } if ( is_user_logged_in() ) { return $this->remove_features_comments( $html ); } if ( false === preg_match_all( '/<!-- (?<feature>wpr_(?:[^-]*)) -->/i', $html, $comments, PREG_PATTERN_ORDER ) ) { return $html; } $meta = $this->get_meta_tag( $comments['feature'] ); if ( empty( $meta ) ) { return $html; } $result = preg_replace( '/<\/head>/i', $meta . '</head>', $html, 1 ); if ( null === $result ) { return $html; } return $this->remove_features_comments( $result ); } /** * Get the WP Rocket meta generator tag * * @param array $features Features to add to the meta tag. * * @return string */ private function get_meta_tag( array $features = [] ): string { $options = $this->options; // Feature mapping for meta tags. $features_to_check = [ 'wpr_preload_links' => 'preload_links', 'wpr_host_fonts_locally' => 'host_fonts_locally', ]; foreach ( $features_to_check as $meta_name => $option_name ) { if ( $options->get( $option_name, false ) ) { $features[] = $meta_name; } } // Mobile/Desktop caching. if ( $options->get( 'do_caching_mobile_files', false ) ) { $features[] = $this->mobile_detect->isMobile() ? 'wpr_mobile' : 'wpr_desktop'; } // CDN & DNS prefetch check. $dns_prefetch = rocket_get_dns_prefetch_domains(); if ( $dns_prefetch && ( ! $options->get( 'cdn', false ) || count( $dns_prefetch ) > 1 ) ) { $features[] = 'wpr_dns_prefetch'; } if ( ! $features ) { return ''; } // Check if WP Rocket version should be included. $version = wpm_apply_filters_typed( 'boolean', 'rocket_display_meta_generator_content_version', true ) ? ' ' . rocket_get_constant( 'WP_ROCKET_VERSION', '' ) : ''; return sprintf( '<meta name="generator" content="WP Rocket%s" data-wpr-features="%s" />', $version, implode( ' ', $features ) ); } /** * Remove WP Rocket features comments from the HTML * * @param string $html The HTML content. * * @return string */ private function remove_features_comments( $html ): string { $result = preg_replace( '/<!-- wpr_[^-]* -->/i', '', $html ); if ( null === $result ) { return $html; } return $result; } } Engine/Support/CommentTrait.php 0000644 00000001175 15174677547 0012577 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Support; trait CommentTrait { /** * Add a comment to the HTML * * @param string $feature The feature name. * @param string $html The HTML content. * * @return string */ public function add_meta_comment( $feature, $html ) { // This filter is documented in inc/Engine/Support/Meta.php. if ( wpm_apply_filters_typed( 'boolean', 'rocket_disable_meta_generator', false ) ) { return $html; } $result = preg_replace( '#</html>#', '</html><!-- wpr_' . $feature . ' -->', $html, 1 ); if ( null === $result ) { return $html; } return $result; } } Engine/Support/Rest.php 0000644 00000004332 15174677547 0011104 0 ustar 00 <?php namespace WP_Rocket\Engine\Support; use WP_REST_Response; use WP_Rocket\Admin\Options_Data; class Rest { const ROUTE_NAMESPACE = 'wp-rocket/v1'; /** * Data instance * * @var Data */ private $data; /** * Options instance * * @var Options_Data */ private $options; /** * Instantiate the class * * @param Data $data Data instance. * @param Options_Data $options Options instance. */ public function __construct( Data $data, Options_Data $options ) { $this->data = $data; $this->options = $options; } /** * Registers the REST route to get the support data * * @since 3.7.5 * * @return void */ public function register_route() { register_rest_route( self::ROUTE_NAMESPACE, 'support', [ 'methods' => 'POST', 'callback' => [ $this, 'get_support_data' ], 'args' => [ 'email' => [ 'required' => true, 'validate_callback' => [ $this, 'validate_email' ], ], 'key' => [ 'required' => true, 'validate_callback' => [ $this, 'validate_key' ], ], ], 'permission_callback' => '__return_true', ] ); } /** * Returns the support data if the referer is correct * * @since 3.7.5 * * @return WP_REST_Response */ public function get_support_data() { return rest_ensure_response( [ 'code' => 'rest_support_data_success', 'message' => 'Support data request successful', 'data' => [ 'status' => 200, 'content' => $this->data->get_support_data(), ], ] ); } /** * Checks that the email sent along the request corresponds to the one saved in the DB * * @since 3.7.5 * * @param string $param Parameter value to validate. * * @return bool */ public function validate_email( $param ) { return ! empty( $param ) && $param === $this->options->get( 'consumer_email', '' ); } /** * Checks that the key sent along the request corresponds to the one saved in the DB * * @since 3.7.5 * * @param string $param Parameter value to validate. * * @return bool */ public function validate_key( $param ) { return ! empty( $param ) && $param === $this->options->get( 'consumer_key', '' ); } } Engine/Support/ServiceProvider.php 0000644 00000002603 15174677547 0013301 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Support; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket_Mobile_Detect; class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'support_data', 'support_rest', 'support_meta', 'support_subscriber', 'mobile_detect', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the services in the container * * @return void */ public function register(): void { $this->getContainer()->add( 'mobile_detect', WP_Rocket_Mobile_Detect::class ); $this->getContainer()->add( 'support_data', Data::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'support_rest', Rest::class ) ->addArguments( [ 'support_data', 'options', ] ); $this->getContainer()->add( 'support_meta', Meta::class ) ->addArguments( [ 'mobile_detect', 'options', ] ); $this->getContainer()->addShared( 'support_subscriber', Subscriber::class ) ->addArguments( [ 'support_rest', 'support_meta', ] ); } } Engine/Support/Data.php 0000644 00000005677 15174677547 0011055 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Support; use WP_Rocket\Admin\Options_Data; class Data { /** * Options instance * * @var Options_Data */ private $options; /** * Array of WP Rocket options to send * * @var array */ private $to_send = [ 'cache_mobile' => 'Mobile Cache', 'do_caching_mobile_files' => 'Specific Cache for Mobile', 'cache_logged_user' => 'User Cache', 'emoji' => 'Disable Emojis', 'defer_all_js' => 'Defer JS', 'delay_js' => 'Delay JS', 'async_css' => 'Load CSS asynchronously', 'lazyload' => 'Lazyload Images', 'lazyload_css_bg_img' => 'Lazyload CSS Background Images', 'lazyload_iframes' => 'Lazyload Iframes', 'lazyload_youtube' => 'Lazyload Youtube', 'cache_webp' => 'WebP Cache', 'minify_css' => 'Minify CSS', 'remove_unused_css' => 'Remove Unused CSS', 'minify_js' => 'Minify JS', 'minify_concatenate_js' => 'Combine JS', 'minify_google_fonts' => 'Combine Google Fonts', 'manual_preload' => 'Preload', 'preload_links' => 'Preload Links', 'cdn' => 'CDN Enabled', 'do_cloudflare' => 'Cloudflare Enabled', 'varnish_auto_purge' => 'Varnish Purge Enabled', 'control_heartbeat' => 'Heartbeat Control', 'sucury_waf_cache_sync' => 'Sucuri Add-on', ]; /** * Instantiate the class * * @param Options_Data $options Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Returns the data to populate the support information * * @since 3.7.5 * * @return array */ public function get_support_data() { return [ 'Website' => home_url(), 'WordPress Version' => get_bloginfo( 'version' ), 'WP Rocket Version' => rocket_get_constant( 'WP_ROCKET_VERSION', '' ), 'Theme' => wp_get_theme()->get( 'Name' ), 'Plugins Enabled' => implode( ' - ', rocket_get_active_plugins() ), 'WP Rocket Active Options' => implode( ' - ', $this->get_active_options() ), ]; } /** * Returns the active options * * @return array */ public function get_active_options(): array { $active_options = array_intersect_key( $this->to_send, array_filter( $this->options->get_options() ) ); // This filter is documented in inc/Engine/Media/AboveTheFold/Context/Context.php. if ( wpm_apply_filters_typed( 'boolean', 'rocket_above_the_fold_optimization', true ) ) { $active_options['optimize_critical_images'] = 'Optimize Critical Images'; } // This filter is documented in inc/Engine/Optimization/LazyRenderContent/Context/Context.php. if ( wpm_apply_filters_typed( 'boolean', 'rocket_lrc_optimization', true ) ) { $active_options['automatic_lazy_rendering'] = 'Automatic Lazy Rendering'; } return $active_options; } } Engine/Deactivation/ServiceProvider.php 0000644 00000004115 15174677547 0014237 0 ustar 00 <?php namespace WP_Rocket\Engine\Deactivation; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\{AbstractServiceProvider, BootableServiceProviderInterface}; use WP_Rocket\Engine\Cache\{AdvancedCache, WPCache}; use WP_Rocket\Engine\Capabilities\Manager; use WP_Rocket\ThirdParty\Plugins\CDN\{Cloudflare, CloudflareFacade}; /** * Service Provider for the activation process. */ class ServiceProvider extends AbstractServiceProvider implements BootableServiceProviderInterface { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'advanced_cache', 'capabilities_manager', 'wp_cache', 'cloudflare_plugin_facade', 'cloudflare_plugin_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Executes this method when the service provider is registered * * @return void */ public function boot(): void { $this->getContainer() ->inflector( DeactivationInterface::class ) ->invokeMethod( 'deactivate', [] ); } /** * Registers the option array in the container. */ public function register(): void { $filesystem = rocket_direct_filesystem(); $this->getContainer()->add( 'cloudflare_plugin_facade', CloudflareFacade::class ); $this->getContainer() ->addShared( 'cloudflare_plugin_subscriber', Cloudflare::class ) ->addArgument( 'options' ) ->addArgument( 'options_api' ) ->addArgument( 'beacon' ) ->addArgument( 'cloudflare_plugin_facade' ); $this->getContainer()->add( 'advanced_cache', AdvancedCache::class ) ->addArgument( new StringArgument( $this->getContainer()->get( 'template_path' ) . '/cache/' ) ) ->addArgument( $filesystem ); $this->getContainer()->add( 'capabilities_manager', Manager::class ); $this->getContainer()->add( 'wp_cache', WPCache::class ) ->addArgument( $filesystem ); } } Engine/Deactivation/Deactivation.php 0000644 00000007153 15174677547 0013543 0 ustar 00 <?php namespace WP_Rocket\Engine\Deactivation; use WP_Rocket\Admin\Options; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\Container; use WP_Rocket\Engine\Admin\Beacon\ServiceProvider as BeaconServiceProvider; use WP_Rocket\Engine\Support\ServiceProvider as SupportServiceProvider; use WP_Rocket\ServiceProvider\Options as OptionsServiceProvider; use WP_Rocket\ThirdParty\Hostings\HostResolver; use WP_Rocket\ThirdParty\Hostings\ServiceProvider as HostingsServiceProvider; class Deactivation { const DEACTIVATION_ENDPOINT = 'https://api.wp-rocket.me/api/wp-rocket/deactivate-licence.php'; /** * Aliases in the container for each class that needs to call its deactivate method * * @var array */ private static $deactivators = [ 'advanced_cache', 'capabilities_manager', 'wp_cache', 'cloudflare_plugin_subscriber', ]; /** * Performs these actions during the plugin deactivation * * @return void */ public static function deactivate_plugin() { global $is_apache; $container = new Container(); $container->add( 'options_api', new Options( 'wp_rocket_' ) ); $container->add( 'template_path', new StringArgument( rocket_get_constant( 'WP_ROCKET_PATH', '' ) . 'views' ) ); $container->addServiceProvider( new OptionsServiceProvider() ); $container->addServiceProvider( new BeaconServiceProvider() ); $container->addServiceProvider( new SupportServiceProvider() ); $container->addServiceProvider( new ServiceProvider() ); $container->addServiceProvider( new HostingsServiceProvider() ); $host_type = HostResolver::get_host_service(); if ( ! empty( $host_type ) ) { array_unshift( self::$deactivators, $host_type ); } foreach ( self::$deactivators as $deactivator ) { $container->get( $deactivator ); } if ( ! isset( $_GET['rocket_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['rocket_nonce'] ), 'force_deactivation' ) ) { $causes = []; // .htaccess problem. if ( $is_apache && ! rocket_direct_filesystem()->is_writable( get_home_path() . '.htaccess' ) ) { $causes[] = 'htaccess'; } /** * Filters the causes which can prevent the deactivation of the plugin * * @since 3.6.3 * * @param array $causes An array of causes to pass to the notice. */ $causes = (array) apply_filters( 'rocket_prevent_deactivation', $causes ); if ( count( $causes ) ) { set_transient( get_current_user_id() . '_donotdeactivaterocket', $causes ); wp_safe_redirect( wp_get_referer() ); die(); } } // Delete config files. rocket_delete_config_file(); $sites_number = count( _rocket_get_php_files_in_dir( rocket_get_constant( 'WP_ROCKET_CONFIG_PATH' ) ) ); if ( ! $sites_number ) { // Delete All WP Rocket rules of the .htaccess file. flush_rocket_htaccess( true ); } // Update customer key & licence. wp_remote_get( self::DEACTIVATION_ENDPOINT, [ 'blocking' => false, ] ); // Delete transients. delete_transient( 'rocket_check_licence_30' ); delete_transient( 'rocket_check_licence_1' ); delete_transient( 'rocket_rucss_as_tables_count' ); delete_site_transient( 'update_wprocket_response' ); delete_site_transient( 'wp_rocket_update_data' ); // Delete user metadata. rocket_renew_box( 'preload_notice' ); // Unschedule WP Cron events. wp_clear_scheduled_hook( 'rocket_cache_dir_size_check' ); /** * WP Rocket deactivation. * * @since 3.6.3 add $sites_count parameter. * @since 3.1.5 * * @param int $sites_number Number of WP Rocket config files found. */ do_action( 'rocket_deactivation', $sites_number ); } } Engine/Deactivation/DeactivationInterface.php 0000644 00000000303 15174677547 0015352 0 ustar 00 <?php namespace WP_Rocket\Engine\Deactivation; interface DeactivationInterface { /** * Executes this method on plugin deactivation * * @return void */ public function deactivate(); } Engine/CDN/Subscriber.php 0000644 00000017777 15174677547 0011243 0 ustar 00 <?php namespace WP_Rocket\Engine\CDN; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the CDN feature * * @since 3.4 */ class Subscriber implements Subscriber_Interface { /** * WP Rocket Options instance * * @var Options_Data */ private $options; /** * CDN instance * * @var CDN */ private $cdn; /** * Constructor * * @param Options_Data $options WP Rocket Options instance. * @param CDN $cdn CDN instance. */ public function __construct( Options_Data $options, CDN $cdn ) { $this->options = $options; $this->cdn = $cdn; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.4 * * @return array */ public static function get_subscribed_events() { return [ 'rocket_buffer' => [ [ 'rewrite', 2 ], [ 'rewrite_srcset', 3 ], ], 'rocket_css_content' => 'rewrite_css_properties', 'rocket_usedcss_content' => 'rewrite_css_properties', 'rocket_cdn_hosts' => [ 'get_cdn_hosts', 10, 2 ], 'rocket_dns_prefetch' => 'add_dns_prefetch_cdn', 'rocket_facebook_sdk_url' => 'add_cdn_url', 'rocket_css_url' => [ 'add_cdn_url', 10, 2 ], 'rocket_js_url' => [ 'add_cdn_url', 10, 2 ], 'rocket_asset_url' => [ 'maybe_replace_url', 10, 2 ], 'wp_resource_hints' => [ 'add_preconnect_cdn', 10, 2 ], 'rocket_font_url' => [ 'add_cdn_url', 10, 2 ], ]; } /** * Rewrites URLs to the CDN URLs if allowed * * @since 3.4 * * @param string $html HTML content. * * @return string */ public function rewrite( $html ) { if ( ! $this->is_allowed() ) { return $html; } return $this->cdn->rewrite( $html ); } /** * Rewrites URLs in srcset attributes to the CDN URLs if allowed * * @since 3.4.0.4 * * @param string $html HTML content. * * @return string */ public function rewrite_srcset( $html ) { if ( ! $this->is_allowed() ) { return $html; } return $this->cdn->rewrite_srcset( $html ); } /** * Rewrites URLs to the CDN URLs in CSS files * * @since 3.4 * * @param string $content CSS content. * * @return string */ public function rewrite_css_properties( $content ) { /** * Filters the application of the CDN on CSS properties * * @since 2.6 * * @param bool $do_rewrite true to apply CDN to properties, false otherwise. */ $do_rewrite = apply_filters( 'do_rocket_cdn_css_properties', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals if ( ! $do_rewrite ) { return $content; } if ( ! $this->is_cdn_enabled() ) { return $content; } return $this->cdn->rewrite_css_properties( $content ); } /** * Gets the host value for each CDN URLs * * @since 3.4 * * @param array $hosts Base hosts. * @param array $zones Zones to get the CND URLs associated with. * * @return array */ public function get_cdn_hosts( array $hosts = [], array $zones = [ 'all' ] ) { $cdn_urls = $this->cdn->get_cdn_urls( $zones ); if ( empty( $cdn_urls ) ) { return $hosts; } foreach ( $cdn_urls as $cdn_url ) { $parsed = get_rocket_parse_url( rocket_add_url_protocol( $cdn_url ) ); if ( empty( $parsed['host'] ) ) { continue; } $hosts[] = untrailingslashit( $parsed['host'] . $parsed['path'] ); } return array_unique( $hosts ); } /** * Adds CDN URLs to the DNS prefetch links * * @since 3.4 * * @param array $domains Domain names to DNS prefetch. * * @return array */ public function add_dns_prefetch_cdn( $domains ) { if ( ! $this->is_allowed() || ! $this->can_insert_resource_hints() ) { return $domains; } $cdn_urls = $this->cdn->get_cdn_urls( [ 'all', 'images', 'css_and_js', 'css', 'js' ] ); if ( ! $cdn_urls ) { return $domains; } return array_merge( $domains, $cdn_urls ); } /** * Adds the CDN URL on the provided URL * * @since 3.4 * * @param string $url URL to rewrite. * @param string $original_url Original URL for this URL. Optional. * * @return string */ public function add_cdn_url( $url, $original_url = '' ) { if ( ! empty( $original_url ) ) { if ( $this->cdn->is_excluded( $original_url ) ) { return $url; } } return $this->cdn->rewrite_url( $url ); } /** * Replace CDN URL with site URL on the provided asset URL. * * @since 3.5.3 * * @param string $url URL of the asset. * @param array $zones Array of corresponding zones for the asset. * * @return string */ public function maybe_replace_url( $url, array $zones = [ 'all' ] ) { if ( ! $this->is_allowed() ) { return $url; } $url_parts = get_rocket_parse_url( $url ); if ( empty( $url_parts['host'] ) ) { return $url; } $site_url_parts = get_rocket_parse_url( site_url() ); if ( empty( $site_url_parts['host'] ) ) { return $url; } if ( $url_parts['host'] === $site_url_parts['host'] ) { return $url; } $cdn_urls = $this->cdn->get_cdn_urls( $zones ); if ( empty( $cdn_urls ) ) { return $url; } $cdn_urls = array_map( 'rocket_add_url_protocol', $cdn_urls ); $site_url = $site_url_parts['scheme'] . '://' . $site_url_parts['host']; foreach ( $cdn_urls as $cdn_url ) { if ( false === strpos( $url, $cdn_url ) ) { continue; } return str_replace( $cdn_url, $site_url, $url ); } return $url; } /** * Add a preconnect tag for the CDN. * * @since 3.8.3 * * @param array $urls The initial array of wp_resource_hint urls. * @param string $relation_type The relation type for the hint: eg., 'preconnect', 'prerender', etc. * * @return array The filtered urls. */ public function add_preconnect_cdn( array $urls, string $relation_type ): array { if ( 'preconnect' !== $relation_type || rocket_bypass() || ! $this->is_allowed() || ! $this->is_cdn_enabled() || ! $this->can_insert_resource_hints() ) { return $urls; } $cdn_urls = $this->cdn->get_cdn_urls( [ 'all', 'images', 'css_and_js', 'css', 'js' ] ); if ( empty( $cdn_urls ) ) { return $urls; } foreach ( $cdn_urls as $url ) { $url_parts = get_rocket_parse_url( $url ); if ( empty( $url_parts['scheme'] ) ) { if ( preg_match( '/^(?![\/])(?=[^\.]+\/).+/i', $url ) ) { continue; } $url = '//' . $url; $url_parts = get_rocket_parse_url( $url ); } $domain = empty( $url_parts['scheme'] ) ? '//' . $url_parts['host'] : $url_parts['scheme'] . '://' . $url_parts['host']; // Note: As of 22 Feb, 2021 we cannot add more than one instance of a domain url // on the wp_resource_hint() hook -- wp_resource_hint() will // only actually print the first one. // Ideally, we want both because CSS resources will use the crossorigin version, // But JS resources will not. // Jonathan has submitted a ticket to change this behavior: // @see https://core.trac.wordpress.org/ticket/52465 // Until then, we order these to prefer/print the non-crossorigin version. $urls[] = [ 'href' => $domain ]; $urls[] = [ 'href' => $domain, 'crossorigin' => 'anonymous', ]; } return $urls; } /** * Checks if CDN can be applied * * @since 3.4 * * @return boolean */ private function is_allowed() { if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE' ) ) { return false; } if ( ! $this->is_cdn_enabled() ) { return false; } if ( is_rocket_post_excluded_option( 'cdn' ) ) { return false; } return true; } /** * Checks if the CDN option is enabled * * @since 3.5.5 * * @return bool */ private function is_cdn_enabled() { return (bool) $this->options->get( 'cdn', 0 ); } /** * Check if CDN can insert resource hints into head. * * @return bool */ private function can_insert_resource_hints(): bool { /** * Enable adding resource hints by CDN feature. * * @since 3.19 * * @param bool $can_insert Can cdn insert resource hints or not, default is true. */ return wpm_apply_filters_typed( 'boolean', 'rocket_cdn_insert_resource_hints', true ); } } Engine/CDN/RocketCDN/APIClient.php 0000644 00000014566 15174677547 0012455 0 ustar 00 <?php namespace WP_Rocket\Engine\CDN\RocketCDN; use WP_Error; /** * Class to Interact with the RocketCDN API */ class APIClient { const ROCKETCDN_API = 'https://rocketcdn.me/api/'; /** * Gets current RocketCDN subscription data from cache if it exists * * Else do a request to the API to get fresh data * * @since 3.5 * * @return array */ public function get_subscription_data() { $status = get_transient( 'rocketcdn_status' ); if ( false !== $status ) { return $status; } return $this->get_remote_subscription_data(); } /** * Gets fresh RocketCDN subscription data from the API * * @since 3.5 * * @return array */ private function get_remote_subscription_data() { $default = [ 'id' => 0, 'is_active' => false, 'cdn_url' => '', 'subscription_next_date_update' => 0, 'subscription_status' => 'cancelled', ]; $token = get_option( 'rocketcdn_user_token' ); if ( empty( $token ) ) { return $default; } $args = [ 'headers' => [ 'Authorization' => 'Token ' . $token, ], ]; $response = wp_remote_get( self::ROCKETCDN_API . 'website/search/?url=' . home_url(), $args ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $this->set_status_transient( $default, 3 * MINUTE_IN_SECONDS ); return $default; } $data = wp_remote_retrieve_body( $response ); if ( empty( $data ) ) { $this->set_status_transient( $default, 3 * MINUTE_IN_SECONDS ); return $default; } $data = json_decode( $data, true ); $data = array_intersect_key( (array) $data, $default ); $data = array_merge( $default, $data ); $this->set_status_transient( $data, WEEK_IN_SECONDS ); return $data; } /** * Sets the RocketCDN status transient with the provided value * * @since 3.5 * * @param array $value Transient value. * @param int $duration Transient duration. * @return void */ private function set_status_transient( $value, $duration ) { set_transient( 'rocketcdn_status', $value, $duration ); } /** * Gets pricing & promotion data for RocketCDN from cache if it exists * * Else do a request to the API to get fresh data * * @since 3.5 * * @return array|WP_Error */ public function get_pricing_data() { $pricing = get_transient( 'rocketcdn_pricing' ); if ( false !== $pricing ) { return $pricing; } return $this->get_remote_pricing_data(); } /** * Gets fresh pricing & promotion data for RocketCDN * * @since 3.5 * * @return array|WP_Error */ private function get_remote_pricing_data() { $response = wp_remote_get( self::ROCKETCDN_API . 'pricing' ); if ( is_wp_error( $response ) ) { return $response; } if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return $this->get_wp_error( __( 'We could not fetch the current price because RocketCDN API returned an unexpected error code.', 'rocket' ) ); } $data = wp_remote_retrieve_body( $response ); if ( empty( $data ) ) { return $this->get_wp_error( __( 'RocketCDN is not available at the moment. Please retry later.', 'rocket' ) ); } $data = json_decode( $data, true ); set_transient( 'rocketcdn_pricing', $data, 6 * HOUR_IN_SECONDS ); return $data; } /** * Gets a new WP_Error instance * * @since 3.5 * * @param string $message Error message. * * @return WP_Error */ private function get_wp_error( string $message ) { return new WP_Error( 'rocketcdn_error', $message ); } /** * Sends a request to the API to purge the CDN cache * * @since 3.5 * * @return array */ public function purge_cache_request() { $subscription = $this->get_subscription_data(); $status = 'error'; if ( ! isset( $subscription['id'] ) || 0 === $subscription['id'] ) { return [ 'status' => $status, 'message' => __( 'RocketCDN cache purge failed: Missing identifier parameter.', 'rocket' ), ]; } $token = get_option( 'rocketcdn_user_token' ); if ( empty( $token ) ) { return [ 'status' => $status, 'message' => __( 'RocketCDN cache purge failed: Missing user token.', 'rocket' ), ]; } $args = [ 'method' => 'DELETE', 'headers' => [ 'Authorization' => 'Token ' . $token, ], ]; $response = wp_remote_request( self::ROCKETCDN_API . 'website/' . $subscription['id'] . '/purge/', $args ); if ( is_wp_error( $response ) ) { return [ 'status' => $status, 'message' => $response->get_error_message(), ]; } if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return [ 'status' => $status, 'message' => __( 'RocketCDN cache purge failed: The API returned an unexpected response code.', 'rocket' ), ]; } $data = wp_remote_retrieve_body( $response ); if ( empty( $data ) ) { return [ 'status' => $status, 'message' => __( 'RocketCDN cache purge failed: The API returned an empty response.', 'rocket' ), ]; } $data = json_decode( $data ); if ( ! isset( $data->success ) ) { return [ 'status' => $status, 'message' => __( 'RocketCDN cache purge failed: The API returned an unexpected response.', 'rocket' ), ]; } if ( ! $data->success ) { return [ 'status' => $status, 'message' => sprintf( // translators: %s = message returned by the API. __( 'RocketCDN cache purge failed: %s.', 'rocket' ), isset( $data->message ) ? $data->message : '' ), ]; } return [ 'status' => 'success', 'message' => __( 'RocketCDN cache purge successful.', 'rocket' ), ]; } /** * Filter the arguments used in an HTTP request, to make sure our user token has not been overwritten * by some other plugin. * * @since 3.5 * * @param array $args An array of HTTP request arguments. * @param string $url The request URL. * @return array */ public function preserve_authorization_token( $args, $url ) { if ( strpos( $url, self::ROCKETCDN_API ) === false ) { return $args; } if ( empty( $args['headers']['Authorization'] ) && self::ROCKETCDN_API . 'pricing' === $url ) { return $args; } $token = get_option( 'rocketcdn_user_token' ); if ( empty( $token ) ) { return $args; } $value = 'token ' . $token; if ( isset( $args['headers']['Authorization'] ) && $value === $args['headers']['Authorization'] ) { return $args; } $args['headers']['Authorization'] = $value; return $args; } } Engine/CDN/RocketCDN/views/cta-big.php 0000644 00000011103 15174677547 0013330 0 ustar 00 <?php /** * RocketCDN small CTA template. * * @since 3.5 * * @param array $data { * @type string $container_class container CSS class. * @type string $promotion_campaign Promotion campaign title. * @type string $promotion_end_date Promotion end date. * @type string $nopromo_variant CSS modifier for the no promotion display. * @type string $regular_price RocketCDN regular price. * @type string $current_price RocketCDN current price. * } */ defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); $data = isset( $data ) && is_array( $data ) ? $data : []; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <div class="wpr-rocketcdn-cta <?php echo esc_attr( $data['container_class'] ); ?>" id="wpr-rocketcdn-cta"> <?php if ( ! empty( $data['promotion_campaign'] ) ) : ?> <div class="wpr-flex wpr-rocketcdn-promo"> <h3 class="wpr-rocketcdn-promo-title"><?php echo esc_html( $data['promotion_campaign'] ); ?></h3> <p class="wpr-title2 wpr-rocketcdn-promo-date"> <?php printf( // Translators: %s = date formatted using date_i18n() and get_option( 'date_format' ). esc_html__( 'Valid until %s only!', 'rocket' ), esc_html( $data['promotion_end_date'] ) ); ?> </p> </div> <?php endif; ?> <section class="wpr-rocketcdn-cta-content<?php echo esc_attr( $data['nopromo_variant'] ); ?>"> <h3 class="wpr-title2">RocketCDN</h3> <p class="wpr-rocketcdn-cta-subtitle"><?php esc_html_e( 'Speed up your website thanks to:', 'rocket' ); ?></p> <div class="wpr-flex"> <ul class="wpr-rocketcdn-features"> <li class="wpr-rocketcdn-feature wpr-rocketcdn-bandwidth"> <?php // translators: %1$s = opening strong tag, %2$s = closing strong tag. printf( esc_html__( 'High performance Content Delivery Network (CDN) with %1$sunlimited bandwidth%2$s', 'rocket' ), '<strong>', '</strong>' ); ?> </li> <li class="wpr-rocketcdn-feature wpr-rocketcdn-configuration"> <?php // translators: %1$s = opening strong tag, %2$s = closing strong tag. printf( esc_html__( 'Easy configuration: the %1$sbest CDN settings%2$s are automatically applied', 'rocket' ), '<strong>', '</strong>' ); ?> </li> <li class="wpr-rocketcdn-feature wpr-rocketcdn-automatic"> <?php // translators: %1$s = opening strong tag, %2$s = closing strong tag. printf( esc_html__( 'WP Rocket integration: the CDN option is %1$sautomatically configured%2$s in our plugin', 'rocket' ), '<strong>', '</strong>' ); ?> </li> <li class="wpr-rocketcdn-cta-footer"> <a href="https://wp-rocket.me/rocketcdn/" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Learn more about RocketCDN', 'rocket' ); ?></a> </li> <?php if ( ! empty( $data['promotion_campaign'] ) ) : ?> <li class="wpr-rocketcdn-cta-promo-footer"> <?php printf( // translators: %1$s = discounted price, %2$s = regular price. esc_html__( '*$%1$s/month for 12 months then $%2$s/month. You can cancel your subscription at any time.', 'rocket' ), esc_html( str_replace( '*', '', $data['current_price'] ) ), esc_html( $data['regular_price'] ) ); ?> </li> <?php endif; ?> </ul> <div class="wpr-rocketcdn-pricing"> <?php if ( ! empty( $data['error'] ) ) : ?> <p><?php echo $data['message']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p> <?php else : ?> <?php if ( ! empty( $data['regular_price'] ) ) : ?> <h4 class="wpr-title2 wpr-rocketcdn-pricing-regular"><del>$<?php echo esc_html( $data['regular_price'] ); ?></del></h4> <?php endif; ?> <h4 class="wpr-rocketcdn-pricing-current"> <span class="wpr-rocketcdn-cta-currency-minor">$</span> <span class="wpr-rocketcdn-cta-currency-major"><?php echo esc_html( substr( $data['current_price'], 0, strpos( $data['current_price'], '.' ) ) ); ?></span> <span class="wpr-rocketcdn-cta-currency-minor"><?php echo esc_html( substr( $data['current_price'], strpos( $data['current_price'], '.' ) ) ); ?> </span> </h4> <p class="wpr-rocketcdn-cta-billing-detail"><?php esc_html_e( 'Billed monthly', 'rocket' ); ?></p> <button class="wpr-button wpr-rocketcdn-open" data-micromodal-trigger="wpr-rocketcdn-modal"><?php esc_html_e( 'Get Started', 'rocket' ); ?></button> <?php endif; ?> </div> </div> </section> <button class="wpr-rocketcdn-cta-close<?php echo esc_attr( $data['nopromo_variant'] ); ?>" id="wpr-rocketcdn-close-cta"><span class="screen-reader-text"><?php esc_html_e( 'Reduce this banner', 'rocket' ); ?></span></button> </div> Engine/CDN/RocketCDN/views/cta-small.php 0000644 00000001525 15174677547 0013706 0 ustar 00 <?php /** * RocketCDN small CTA template. * * @since 3.5 * * @param array $data { * @type string $container_class container CSS class. * } */ defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); $data = isset( $data ) && is_array( $data ) ? $data : []; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <div class="wpr-rocketcdn-cta-small notice-alt notice-warning <?php echo esc_attr( $data['container_class'] ); ?>" id="wpr-rocketcdn-cta-small"> <div class="wpr-flex"> <section> <h3 class="notice-title"><?php esc_html_e( 'Speed up your website with RocketCDN, WP Rocket’s Content Delivery Network.', 'rocket' ); ?></strong></h3> </section> <div> <button class="wpr-button" id="wpr-rocketcdn-open-cta"><?php esc_html_e( 'Learn More', 'rocket' ); ?></button> </div> </div> </div> Engine/CDN/RocketCDN/views/dashboard-status.php 0000644 00000002776 15174677547 0015312 0 ustar 00 <?php /** * RocketCDN status on dashboard tab template. * * @since 3.5 * * @param array $data { * @type bool $is_live_site Identifies if the current website is a live or local/staging one * @type string $container_class Flex container CSS class. * @type string $label Content label. * @type string $status_class CSS Class to display the status. * @type string $status_text Text to display the subscription status. * @type bool $is_active Boolean identifying the activation status. * } */ $data = isset( $data ) && is_array( $data ) ? $data : []; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <div class="wpr-optionHeader"> <h3 class="wpr-title2">RocketCDN</h3> </div> <div class="wpr-field wpr-field-account"> <?php if ( ! $data['is_live_site'] ) : ?> <span class="wpr-infoAccount wpr-isInvalid"><?php esc_html_e( 'RocketCDN is unavailable on local domains and staging sites.', 'rocket' ); ?></span> <?php else : ?> <div class="wpr-flex<?php echo esc_attr( $data['container_class'] ); ?>"> <div> <span class="wpr-title3"><?php echo esc_html( $data['label'] ); ?></span> <span class="wpr-infoAccount<?php echo esc_attr( $data['status_class'] ); ?>"><?php echo esc_html( $data['status_text'] ); ?></span> </div> <?php if ( ! $data['is_active'] ) : ?> <div> <a href="#page_cdn" class="wpr-button"><?php esc_html_e( 'Get RocketCDN', 'rocket' ); ?></a> </div> <?php endif; ?> </div> <?php endif; ?> </div> Engine/CDN/RocketCDN/views/promote-notice.php 0000644 00000001047 15174677547 0014774 0 ustar 00 <?php /** * Promote RocketCDN notice template. * * @since 3.5 */ defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); ?> <div class="notice notice-alt notice-warning is-dismissible" id="rocketcdn-promote-notice"> <h2 class="notice-title"><?php esc_html_e( 'New!', 'rocket' ); ?></h2> <p><?php esc_html_e( 'Speed up your website with RocketCDN, WP Rocket’s Content Delivery Network!', 'rocket' ); ?></p> <p><a href="#page_cdn" class="wpr-button" id="rocketcdn-learn-more-dismiss"><?php esc_html_e( 'Learn More', 'rocket' ); ?></a></p> </div> Engine/CDN/RocketCDN/DataManagerSubscriber.php 0000644 00000021410 15174677547 0015057 0 ustar 00 <?php namespace WP_Rocket\Engine\CDN\RocketCDN; use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the RocketCDN integration in WP Rocket settings page * * @since 3.5 */ class DataManagerSubscriber implements Subscriber_Interface { use RegexTrait; const CRON_EVENT = 'rocketcdn_check_subscription_status_event'; /** * RocketCDN API Client instance. * * @var APIClient */ private $api_client; /** * CDNOptionsManager instance. * * @var CDNOptionsManager */ private $cdn_options; /** * WP Options API instance * * @var Options */ private $options_api; /** * WP Rocket Options instance * * @var Options_Data */ private $options; /** * Constructor * * @param APIClient $api_client RocketCDN API Client instance. * @param CDNOptionsManager $cdn_options CDNOptionsManager instance. * @param Options_Data $options Options instance. * @param Options $options_api Options API instance. */ public function __construct( APIClient $api_client, CDNOptionsManager $cdn_options, Options_Data $options, Options $options_api ) { $this->api_client = $api_client; $this->cdn_options = $cdn_options; $this->options = $options; $this->options_api = $options_api; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'wp_ajax_save_rocketcdn_token' => 'update_user_token', 'wp_ajax_rocketcdn_enable' => 'enable', 'wp_ajax_rocketcdn_disable' => 'disable', 'wp_ajax_rocketcdn_process_set' => 'set_process_status', 'wp_ajax_rocketcdn_process_status' => 'get_process_status', 'wp_ajax_rocketcdn_validate_token_cname' => 'validate_token_cname', self::CRON_EVENT => 'maybe_disable_cdn', 'wp_rocket_upgrade' => [ 'refresh_cdn_cname', 10, 2 ], ]; } /** * Updates the RocketCDN user token value * * @since 3.5 * * @return void */ public function update_user_token() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_send_json_error( 'unauthorized_user' ); } if ( empty( $_POST['value'] ) ) { delete_option( 'rocketcdn_user_token' ); wp_send_json_success( 'user_token_deleted' ); } if ( ! is_string( $_POST['value'] ) ) { wp_send_json_error( 'invalid_token' ); } $token = sanitize_key( $_POST['value'] ); if ( 40 !== strlen( $token ) ) { wp_send_json_error( 'invalid_token_length' ); } update_option( 'rocketcdn_user_token', $token ); wp_send_json_success( 'user_token_saved' ); } /** * Ajax callback to enable RocketCDN * * @since 3.5 * * @return void */ public function enable() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); $data = [ 'process' => 'subscribe', ]; if ( ! current_user_can( 'rocket_manage_options' ) ) { $data['message'] = 'unauthorized_user'; wp_send_json_error( $data ); } if ( empty( $_POST['cdn_url'] ) ) { $data['message'] = 'cdn_url_empty'; wp_send_json_error( $data ); } $cdn_url = filter_var( wp_unslash( $_POST['cdn_url'] ), FILTER_VALIDATE_URL ); if ( ! $cdn_url ) { $data['message'] = 'cdn_url_invalid_format'; wp_send_json_error( $data ); } $this->cdn_options->enable( esc_url_raw( $cdn_url ) ); $subscription = $this->api_client->get_subscription_data(); $this->schedule_subscription_check( $subscription ); $this->delete_process(); $data['message'] = 'rocketcdn_enabled'; wp_send_json_success( $data ); } /** * AJAX callback to disable RocketCDN * * @since 3.5 * * @return void */ public function disable() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); $data = [ 'process' => 'unsubscribe', ]; if ( ! current_user_can( 'rocket_manage_options' ) ) { $data['message'] = 'unauthorized_user'; wp_send_json_error( $data ); } $this->cdn_options->disable(); $timestamp = wp_next_scheduled( self::CRON_EVENT ); if ( $timestamp ) { wp_unschedule_event( $timestamp, self::CRON_EVENT ); } $this->delete_process(); $data['message'] = 'rocketcdn_disabled'; wp_send_json_success( $data ); } /** * Delete the option tracking the RocketCDN process state * * @since 3.5 * * @return void */ private function delete_process() { delete_option( 'rocketcdn_process' ); } /** * Set the RocketCDN subscription process status * * @since 3.5 * * @return void */ public function set_process_status() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( empty( $_POST['status'] ) ) { return; } $status = filter_var( $_POST['status'], FILTER_VALIDATE_BOOLEAN ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Used as a boolean. if ( false === $status ) { delete_option( 'rocketcdn_process' ); return; } update_option( 'rocketcdn_process', $status ); } /** * Check for RocketCDN subscription process status * * @since 3.5 * * @return void */ public function get_process_status() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_send_json_error(); } if ( get_option( 'rocketcdn_process' ) ) { wp_send_json_success(); } wp_send_json_error(); } /** * Cron job to disable CDN if the subscription expired * * @since 3.5 * * @return void */ public function maybe_disable_cdn() { delete_transient( 'rocketcdn_status' ); $subscription = $this->api_client->get_subscription_data(); if ( rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ) { $subscription = apply_filters( 'rocket_pre_get_subscription_data', $subscription ); } if ( 'running' === $subscription['subscription_status'] ) { $this->schedule_subscription_check( $subscription ); return; } $this->cdn_options->disable(); } /** * Validates and updates the token and cname from RocketCDN Iframe. * * @return void */ public function validate_token_cname() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); $data = []; if ( ! current_user_can( 'rocket_manage_options' ) ) { $data['message'] = 'unauthorized_user'; wp_send_json_error( $data ); } if ( empty( $_POST['cdn_url'] ) || empty( $_POST['cdn_token'] ) ) { $data['message'] = 'cdn_values_empty'; wp_send_json_error( $data ); } $token = sanitize_key( $_POST['cdn_token'] ); $cdn_url = filter_var( wp_unslash( $_POST['cdn_url'] ), FILTER_VALIDATE_URL ); if ( ! $cdn_url ) { $data['message'] = 'cdn_url_invalid_format'; wp_send_json_error( $data ); } if ( 40 !== strlen( $token ) ) { $data['message'] = 'invalid_token_length'; wp_send_json_error( $data ); } $current_token = get_option( 'rocketcdn_user_token' ); $current_cname = $this->cdn_options->get_cdn_cnames(); if ( ! empty( $current_token ) ) { $data['message'] = 'token_already_set'; wp_send_json_error( $data ); } update_option( 'rocketcdn_user_token', $token ); $this->cdn_options->enable( esc_url_raw( $cdn_url ) ); $data['message'] = 'token_updated_successfully'; wp_send_json_success( $data ); } /** * Schedule the next cron subscription check * * @since 3.5 * * @param array $subscription Array containing the subscription data. * @return void */ private function schedule_subscription_check( $subscription ) { $timestamp = strtotime( $subscription['subscription_next_date_update'] ) + strtotime( '+2 days' ); if ( ! wp_next_scheduled( self::CRON_EVENT ) ) { wp_schedule_single_event( $timestamp, self::CRON_EVENT ); } } /** * Upgrade callback. * * @param string $new_version Plugin new version. * @param string $old_version Plugin old version. * @return void */ public function refresh_cdn_cname( $new_version, $old_version ): void { if ( version_compare( $old_version, '3.17.3', '>=' ) ) { return; } $cdn_cnames = $this->options->get( 'cdn_cnames', [] ); if ( empty( $cdn_cnames ) ) { return; } $subscription_data = $this->api_client->get_subscription_data(); if ( ! $subscription_data['is_active'] || empty( $subscription_data['cdn_url'] ) ) { return; } $cdn_matches = $this->find( 'https:\/\/(?<cdn_id>[a-zA-Z0-9]{8})\.rocketcdn\.me', $cdn_cnames[0] ); if ( empty( $cdn_matches ) || empty( $cdn_matches[0]['cdn_id'] ) ) { return; } $this->options_api->set( 'rocketcdn_old_url', $cdn_cnames[0] ); $cdn_cnames[0] = str_replace( $cdn_matches[0]['cdn_id'], $cdn_matches[0]['cdn_id'] . '.delivery', $cdn_cnames[0] ); $this->options->set( 'cdn_cnames', $cdn_cnames ); $this->options_api->set( 'settings', $this->options->get_options() ); } } Engine/CDN/RocketCDN/ServiceProvider.php 0000644 00000004425 15174677547 0014011 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\CDN\RocketCDN; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for RocketCDN */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'rocketcdn_api_client', 'rocketcdn_options_manager', 'rocketcdn_data_manager_subscriber', 'rocketcdn_rest_subscriber', 'rocketcdn_admin_subscriber', 'rocketcdn_notices_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { // RocketCDN API Client. $this->getContainer()->add( 'rocketcdn_api_client', APIClient::class ); // RocketCDN CDN options manager. $this->getContainer()->add( 'rocketcdn_options_manager', CDNOptionsManager::class ) ->addArguments( [ 'options_api', 'options', ] ); // RocketCDN Data manager subscriber. $this->getContainer()->addShared( 'rocketcdn_data_manager_subscriber', DataManagerSubscriber::class ) ->addArguments( [ 'rocketcdn_api_client', 'rocketcdn_options_manager', 'options', 'options_api', ] ); // RocketCDN REST API Subscriber. $this->getContainer()->addShared( 'rocketcdn_rest_subscriber', RESTSubscriber::class ) ->addArguments( [ 'rocketcdn_options_manager', 'options', ] ); // RocketCDN Notices Subscriber. $this->getContainer()->addShared( 'rocketcdn_notices_subscriber', NoticesSubscriber::class ) ->addArguments( [ 'rocketcdn_api_client', 'beacon', new StringArgument( __DIR__ . '/views' ), ] ); // RocketCDN settings page subscriber. $this->getContainer()->addShared( 'rocketcdn_admin_subscriber', AdminPageSubscriber::class ) ->addArguments( [ 'rocketcdn_api_client', 'options', 'beacon', new StringArgument( __DIR__ . '/views' ), ] ); } } Engine/CDN/RocketCDN/NoticesSubscriber.php 0000644 00000025263 15174677547 0014331 0 ustar 00 <?php namespace WP_Rocket\Engine\CDN\RocketCDN; use WP_Rocket\Abstract_Render; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the RocketCDN notices on WP Rocket settings page * * @since 3.5 */ class NoticesSubscriber extends Abstract_Render implements Subscriber_Interface { /** * RocketCDN API Client instance. * * @var APIClient */ private $api_client; /** * Beacon instance * * @var Beacon */ private $beacon; /** * Constructor * * @param APIClient $api_client RocketCDN API Client instance. * @param Beacon $beacon Beacon instance. * @param string $template_path Path to the templates. */ public function __construct( APIClient $api_client, Beacon $beacon, $template_path ) { parent::__construct( $template_path ); $this->api_client = $api_client; $this->beacon = $beacon; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'admin_notices' => [ [ 'promote_rocketcdn_notice' ], [ 'purge_cache_notice' ], [ 'change_cname_notice' ], ], 'rocket_before_cdn_sections' => 'display_rocketcdn_cta', 'wp_ajax_toggle_rocketcdn_cta' => 'toggle_cta', 'wp_ajax_rocketcdn_dismiss_notice' => 'dismiss_notice', 'admin_footer' => 'add_dismiss_script', ]; } /** * Adds notice to promote RocketCDN on settings page * * @since 3.5 * * @return void */ public function promote_rocketcdn_notice() { /** * Filters RocketCDN promotion notice. * * @param bool $promotion_notice; true to display, false otherwise. */ if ( ! apply_filters( 'rocket_promote_rocketcdn_notice', true ) ) { return; } if ( $this->is_white_label_account() ) { return; } if ( ! rocket_is_live_site() ) { return; } if ( ! $this->should_display_notice() ) { return; } echo $this->generate( 'promote-notice' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Adds inline script to permanently dismissing the RocketCDN promotion notice * * @since 3.5 * * @return void */ public function add_dismiss_script() { if ( $this->is_white_label_account() ) { return; } if ( ! rocket_is_live_site() ) { return; } if ( ! $this->should_display_notice() ) { return; } $nonce = wp_create_nonce( 'rocketcdn_dismiss_notice' ); ?> <script> window.addEventListener( 'load', function() { var dismissBtn = document.querySelectorAll( '#rocketcdn-promote-notice .notice-dismiss, #rocketcdn-promote-notice #rocketcdn-learn-more-dismiss' ); dismissBtn.forEach(function(element) { element.addEventListener( 'click', function( event ) { var httpRequest = new XMLHttpRequest(), postData = ''; postData += 'action=rocketcdn_dismiss_notice'; postData += '&nonce=<?php echo esc_attr( $nonce ); ?>'; httpRequest.open( 'POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>' ); httpRequest.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' ) httpRequest.send( postData ); }); }); }); </script> <?php } /** * Checks if the promotion notice should be displayed * * @since 3.5 * * @return boolean */ private function should_display_notice() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return false; } if ( 'settings_page_wprocket' !== get_current_screen()->id ) { return false; } if ( get_user_meta( get_current_user_id(), 'rocketcdn_dismiss_notice', true ) ) { return false; } $subscription_data = $this->api_client->get_subscription_data(); return 'running' !== $subscription_data['subscription_status']; } /** * Ajax callback to save the dismiss as a user meta * * @since 3.5 * * @return void */ public function dismiss_notice() { check_ajax_referer( 'rocketcdn_dismiss_notice', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_send_json_error( 'no permissions' ); } update_user_meta( get_current_user_id(), 'rocketcdn_dismiss_notice', true ); wp_send_json_success(); } /** * Displays the RocketCDN Call to Action on the CDN tab of WP Rocket settings page * * @since 3.5 * * @return void */ public function display_rocketcdn_cta() { /** * Filters the display of the RocketCDN cta banner. * * @param bool $display_cta_banner; true to display, false otherwise. */ if ( ! apply_filters( 'rocket_display_rocketcdn_cta', true ) ) { return; } if ( $this->is_white_label_account() ) { return; } if ( ! rocket_is_live_site() ) { return; } $subscription_data = $this->api_client->get_subscription_data(); if ( 'running' === $subscription_data['subscription_status'] ) { return; } $pricing = $this->api_client->get_pricing_data(); $regular_price = ''; $nopromo_variant = '--no-promo'; $cta_small_class = 'wpr-isHidden'; $cta_big_class = ''; if ( get_user_meta( get_current_user_id(), 'rocket_rocketcdn_cta_hidden', true ) ) { $cta_small_class = ''; $cta_big_class = 'wpr-isHidden'; } $small_cta_data = [ 'container_class' => $cta_small_class, ]; if ( is_wp_error( $pricing ) ) { $beacon = $this->beacon->get_suggest( 'rocketcdn_error' ); $more_info = sprintf( // translators: %1$is = opening link tag, %2$s = closing link tag. __( '%1$sMore Info%2$s', 'rocket' ), '<a href="' . esc_url( $beacon['url'] ) . '" data-beacon-article="' . esc_attr( $beacon['id'] ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); $message = $pricing->get_error_message() . ' ' . $more_info; $big_cta_data = [ 'container_class' => $cta_big_class, 'nopromo_variant' => $nopromo_variant, 'error' => true, 'message' => $message, ]; } else { $current_price = number_format_i18n( $pricing['monthly_price'], 2 ); $promotion_campaign = ''; $end_date = strtotime( $pricing['end_date'] ); $promotion_end_date = ''; if ( $pricing['is_discount_active'] && $end_date > time() ) { $promotion_campaign = $pricing['discount_campaign_name']; $regular_price = $current_price; $current_price = number_format_i18n( $pricing['discounted_price_monthly'], 2 ) . '*'; $nopromo_variant = ''; $promotion_end_date = date_i18n( get_option( 'date_format' ), $end_date ); } $big_cta_data = [ 'container_class' => $cta_big_class, 'promotion_campaign' => $promotion_campaign, 'promotion_end_date' => $promotion_end_date, 'nopromo_variant' => $nopromo_variant, 'regular_price' => $regular_price, 'current_price' => $current_price, ]; } echo $this->generate( 'cta-small', $small_cta_data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. echo $this->generate( 'cta-big', $big_cta_data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Toggles display of the RocketCDN CTAs on the settings page * * @since 3.5 * * @return void */ public function toggle_cta() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_send_json_error( 'no permissions' ); } if ( ! isset( $_POST['status'] ) ) { wp_send_json_error( 'missing status' ); } if ( 'big' === $_POST['status'] ) { delete_user_meta( get_current_user_id(), 'rocket_rocketcdn_cta_hidden' ); } elseif ( 'small' === $_POST['status'] ) { update_user_meta( get_current_user_id(), 'rocket_rocketcdn_cta_hidden', true ); } wp_send_json_success(); } /** * Displays a notice after purging the RocketCDN cache. * * @since 3.5 * * @return void */ public function purge_cache_notice() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== get_current_screen()->id ) { return; } $purge_response = get_transient( 'rocketcdn_purge_cache_response' ); if ( false === $purge_response ) { return; } $message = $purge_response['message']; if ( 'error' === $purge_response['status'] ) { $beacon = $this->beacon->get_suggest( 'rocketcdn_error' ); $more_info = sprintf( // translators: %1$is = opening link tag, %2$s = closing link tag. __( '%1$sMore Info%2$s', 'rocket' ), '<a href="' . esc_url( $beacon['url'] ) . '" data-beacon-article="' . esc_attr( $beacon['id'] ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); $message .= ' ' . $more_info; } delete_transient( 'rocketcdn_purge_cache_response' ); rocket_notice_html( [ 'status' => $purge_response['status'], 'message' => $message, ] ); } /** * Checks if white label is enabled * * @since 3.6 * * @return bool */ private function is_white_label_account() { return (bool) rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT' ); } /** * Change CName admin notice contents. * * @return void */ public function change_cname_notice() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== get_current_screen()->id ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( 'rocketcdn_change_cname', (array) $boxes, true ) ) { return; } $old_cname = get_option( 'wp_rocket_rocketcdn_old_url' ); if ( empty( $old_cname ) ) { return; } $new_subscription = $this->api_client->get_subscription_data(); if ( empty( $new_subscription['cdn_url'] ) || $old_cname === $new_subscription['cdn_url'] ) { return; } $support_url = rocket_get_external_url( 'support', [ 'utm_source' => 'wp_plugin', 'utm_medium' => 'wp_rocket', ] ); $message_lines = [ // translators: %1$s = Old CName, %2$s = New CName. sprintf( esc_html__( 'We\'ve updated your RocketCDN CNAME from %1$s to %2$s.', 'rocket' ), $old_cname, $new_subscription['cdn_url'] ), // translators: %1$s = New CName. sprintf( esc_html__( 'The change is already applied to the plugin settings. If you were using the CNAME in your code, make sure to update it to: %1$s.', 'rocket' ), $new_subscription['cdn_url'] ), ]; rocket_notice_html( [ 'status' => 'info', 'message' => implode( '<br>', $message_lines ), 'dismiss_button' => 'rocketcdn_change_cname', 'id' => 'rocketcdn_change_cname_notice', 'action' => sprintf( '<a href="%1$s" target="_blank" rel="noopener" class="wpr-button" id="rocketcdn-change-cname-button">%2$s</a>', $support_url, esc_html__( 'contact support', 'rocket' ) ), ] ); } } Engine/CDN/RocketCDN/AdminPageSubscriber.php 0000644 00000016130 15174677547 0014543 0 ustar 00 <?php namespace WP_Rocket\Engine\CDN\RocketCDN; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Subscriber for the RocketCDN integration in WP Rocket settings page * * @since 3.5 */ class AdminPageSubscriber extends Abstract_Render implements Subscriber_Interface { /** * RocketCDN API Client instance. * * @var APIClient */ private $api_client; /** * WP Rocket options instance * * @var Options_Data */ private $options; /** * Beacon instance * * @var Beacon */ private $beacon; /** * Constructor * * @param APIClient $api_client RocketCDN API Client instance. * @param Options_Data $options WP Rocket options instance. * @param Beacon $beacon Beacon instance. * @param string $template_path Path to the templates. */ public function __construct( APIClient $api_client, Options_Data $options, Beacon $beacon, $template_path ) { parent::__construct( $template_path ); $this->api_client = $api_client; $this->options = $options; $this->beacon = $beacon; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'rocket_dashboard_after_account_data' => 'display_rocketcdn_status', 'rocket_cdn_settings_fields' => 'rocketcdn_field', 'admin_post_rocket_purge_rocketcdn' => 'purge_cdn_cache', 'rocket_settings_page_footer' => 'add_subscription_modal', 'http_request_args' => [ 'preserve_authorization_token', PHP_INT_MAX, 2 ], ]; } /** * Displays the RocketCDN section on the dashboard tab * * @since 3.5 * * @return void */ public function display_rocketcdn_status() { /** * Filters the display of the RocketCDN status. * * @param bool $display_rocketcdn_status; true to display, false otherwise. */ if ( ! apply_filters( 'rocket_display_rocketcdn_status', true ) ) { return; } if ( $this->is_white_label_account() ) { return; } $subscription_data = $this->api_client->get_subscription_data(); $container_class = ''; $status_class = ''; $label = ''; $status_text = ''; $is_active = false; if ( 'running' === $subscription_data['subscription_status'] ) { $label = __( 'Next Billing Date', 'rocket' ); $status_class = ' wpr-isValid'; $status_text = date_i18n( get_option( 'date_format' ), strtotime( $subscription_data['subscription_next_date_update'] ) ); $is_active = true; } elseif ( 'cancelled' === $subscription_data['subscription_status'] ) { $status_class = ' wpr-isInvalid'; $container_class = ' wpr-flex--egal'; $status_text = __( 'No Subscription', 'rocket' ); } $data = [ 'is_live_site' => rocket_is_live_site(), 'container_class' => $container_class, 'label' => $label, 'status_class' => $status_class, 'status_text' => $status_text, 'is_active' => $is_active, ]; echo $this->generate( 'dashboard-status', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Adds the RocketCDN fields to the CDN section * * @since 3.5 * * @param array $fields CDN settings fields. * * @return array */ public function rocketcdn_field( $fields ) { if ( $this->is_white_label_account() ) { return $fields; } $subscription_data = $this->api_client->get_subscription_data(); if ( 'running' !== $subscription_data['subscription_status'] ) { return $fields; } $helper_text = __( 'Your RocketCDN subscription is currently active.', 'rocket' ); $cdn_cnames = $this->options->get( 'cdn_cnames', [] ); if ( empty( $cdn_cnames ) || $cdn_cnames[0] !== $subscription_data['cdn_url'] ) { $helper_text = sprintf( // translators: %1$s = opening <code> tag, %2$s = CDN URL, %3$s = closing </code> tag. __( 'To use RocketCDN, replace your CNAME with %1$s%2$s%3$s.', 'rocket' ), '<code>', $subscription_data['cdn_url'], '</code>' ); } $beacon = $this->beacon->get_suggest( 'rocketcdn' ); $more_info = sprintf( // translators: %1$is = opening link tag, %2$s = closing link tag. __( '%1$sMore Info%2$s', 'rocket' ), '<a href="' . esc_url( $beacon['url'] ) . '" data-beacon-article="' . esc_attr( $beacon['id'] ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ); $fields['cdn_cnames'] = [ 'type' => 'rocket_cdn', 'label' => __( 'CDN CNAME(s)', 'rocket' ), 'description' => __( 'Specify the CNAME(s) below', 'rocket' ), 'helper' => $helper_text . ' ' . $more_info, 'default' => '', 'section' => 'cnames_section', 'page' => 'page_cdn', 'beacon' => [ 'url' => $beacon['url'], 'id' => $beacon['id'], ], ]; return $fields; } /** * Purges the CDN cache and store the response in a transient. * * @since 3.5 * * @return void */ public function purge_cdn_cache() { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'rocket_purge_rocketcdn' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash wp_nonce_ays( '' ); } if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_die(); } set_transient( 'rocketcdn_purge_cache_response', $this->api_client->purge_cache_request(), HOUR_IN_SECONDS ); wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } /** * Adds the subscription modal on the WP Rocket settings page * * @since 3.5 * * @return void */ public function add_subscription_modal() { if ( $this->is_white_label_account() ) { return; } if ( ! rocket_is_live_site() ) { return; } $iframe_src = add_query_arg( [ 'website' => home_url(), 'callback' => rest_url( 'wp-rocket/v1/rocketcdn/' ), ], 'https://api.wp-rocket.me/cdn/iframe' ); ?> <div class="wpr-rocketcdn-modal" id="wpr-rocketcdn-modal" aria-hidden="true"> <div class="wpr-rocketcdn-modal__overlay" tabindex="-1"> <div class="wpr-rocketcdn-modal__container" role="dialog" aria-modal="true" aria-labelledby="wpr-rocketcdn-modal-title"> <div id="wpr-rocketcdn-modal-content"> <iframe id="rocketcdn-iframe" src="<?php echo esc_url( $iframe_src ); ?>" width="674" height="425"></iframe> </div> </div> </div> </div> <?php } /** * Filter the arguments used in an HTTP request, to make sure our user token has not been overwritten * by some other plugin. * * @since 3.5 * * @param array $args An array of HTTP request arguments. * @param string $url The request URL. * @return array */ public function preserve_authorization_token( $args, $url ) { return $this->api_client->preserve_authorization_token( $args, $url ); } /** * Checks if white label is enabled * * @since 3.6 * * @return bool */ private function is_white_label_account() { return (bool) rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT' ); } } Engine/CDN/RocketCDN/RESTSubscriber.php 0000644 00000010253 15174677547 0013473 0 ustar 00 <?php namespace WP_Rocket\Engine\CDN\RocketCDN; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\Options_Data; /** * Subscriber for RocketCDN REST API Integration * * @since 3.5 */ class RESTSubscriber implements Subscriber_Interface { const ROUTE_NAMESPACE = 'wp-rocket/v1'; /** * CDNOptionsManager instance * * @var CDNOptionsManager */ private $cdn_options; /** * WP Rocket Options instance * * @var Options_Data */ private $options; /** * Constructor * * @param CDNOptionsManager $cdn_options CDNOptionsManager instance. * @param Options_Data $options WP Rocket Options instance. */ public function __construct( CDNOptionsManager $cdn_options, Options_Data $options ) { $this->cdn_options = $cdn_options; $this->options = $options; } /** * {@inheritdoc} */ public static function get_subscribed_events() { return [ 'rest_api_init' => [ [ 'register_enable_route' ], [ 'register_disable_route' ], ], ]; } /** * Register Enable route in the WP REST API * * @since 3.5 * * @return void */ public function register_enable_route() { register_rest_route( self::ROUTE_NAMESPACE, 'rocketcdn/enable', [ 'methods' => 'PUT', 'callback' => [ $this, 'enable' ], 'args' => [ 'email' => [ 'required' => true, 'validate_callback' => [ $this, 'validate_email' ], ], 'key' => [ 'required' => true, 'validate_callback' => [ $this, 'validate_key' ], ], 'url' => [ 'required' => true, 'validate_callback' => function ( $param ) { $url = esc_url_raw( $param ); return ! empty( $url ); }, 'sanitize_callback' => function ( $param ) { return esc_url_raw( $param ); }, ], ], 'permission_callback' => '__return_true', ] ); } /** * Register Disable route in the WP REST API * * @since 3.5 * * @return void */ public function register_disable_route() { register_rest_route( self::ROUTE_NAMESPACE, 'rocketcdn/disable', [ 'methods' => 'PUT', 'callback' => [ $this, 'disable' ], 'args' => [ 'email' => [ 'required' => true, 'validate_callback' => [ $this, 'validate_email' ], ], 'key' => [ 'required' => true, 'validate_callback' => [ $this, 'validate_key' ], ], ], 'permission_callback' => '__return_true', ] ); } /** * Enable CDN and add RocketCDN URL to WP Rocket options * * @since 3.5 * * @param \WP_REST_Request $request the WP REST Request object. * * @return \WP_REST_Response|\WP_Error */ public function enable( \WP_REST_Request $request ) { $params = $request->get_body_params(); $this->cdn_options->enable( $params['url'] ); $response = [ 'code' => 'success', 'message' => __( 'RocketCDN enabled', 'rocket' ), 'data' => [ 'status' => 200, ], ]; return rest_ensure_response( $response ); } /** * Disable the CDN and remove the RocketCDN URL from WP Rocket options * * @since 3.5 * * @param \WP_REST_Request $request the WP Rest Request object. * * @return \WP_REST_Response|\WP_Error */ public function disable( \WP_REST_Request $request ) { $this->cdn_options->disable(); $response = [ 'code' => 'success', 'message' => __( 'RocketCDN disabled', 'rocket' ), 'data' => [ 'status' => 200, ], ]; return rest_ensure_response( $response ); } /** * Checks that the email sent along the request corresponds to the one saved in the DB * * @since 3.5 * * @param string $param Parameter value to validate. * * @return bool */ public function validate_email( $param ) { return ! empty( $param ) && $param === $this->options->get( 'consumer_email' ); } /** * Checks that the key sent along the request corresponds to the one saved in the DB * * @since 3.5 * * @param string $param Parameter value to validate. * * @return bool */ public function validate_key( $param ) { return ! empty( $param ) && $param === $this->options->get( 'consumer_key' ); } } Engine/CDN/RocketCDN/CDNOptionsManager.php 0000644 00000003365 15174677547 0014153 0 ustar 00 <?php namespace WP_Rocket\Engine\CDN\RocketCDN; use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; /** * Manager for WP Rocket CDN options * * @since 3.5 */ class CDNOptionsManager { /** * WP Options API instance * * @var Options */ private $options_api; /** * WP Rocket Options instance * * @var Options_Data */ private $options; /** * Constructor * * @param Options $options_api WP Options API instance. * @param Options_Data $options WP Rocket Options instance. */ public function __construct( Options $options_api, Options_Data $options ) { $this->options_api = $options_api; $this->options = $options; } /** * Enable CDN option, save CDN URL & delete RocketCDN status transient * * @since 3.5 * * @param string $cdn_url CDN URL. * @return void */ public function enable( $cdn_url ) { $this->options->set( 'cdn', 1 ); $this->options->set( 'cdn_cnames', [ $cdn_url ] ); $this->options->set( 'cdn_zone', [ 'all' ] ); $this->options_api->set( 'settings', $this->options->get_options() ); delete_transient( 'rocketcdn_status' ); rocket_clean_domain(); } /** * Disable CDN option, remove CDN URL & user token, delete RocketCDN status transient * * @since 3.5 * * @return void */ public function disable() { $this->options->set( 'cdn', 0 ); $this->options->set( 'cdn_cnames', [] ); $this->options->set( 'cdn_zone', [] ); $this->options_api->set( 'settings', $this->options->get_options() ); delete_option( 'rocketcdn_user_token' ); delete_transient( 'rocketcdn_status' ); rocket_clean_domain(); } /** * Get current CDN cnames. * * @return array */ public function get_cdn_cnames() { return $this->options->get( 'cdn_cnames', [] ); } } Engine/CDN/ServiceProvider.php 0000644 00000002200 15174677547 0012222 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\CDN; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\CDN\Admin\Subscriber as AdminSubscriber; /** * Service provider for WP Rocket CDN */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'cdn', 'cdn_subscriber', 'cdn_admin_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'cdn', CDN::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'cdn_subscriber', Subscriber::class ) ->addArguments( [ 'options', 'cdn', ] ); $this->getContainer()->addShared( 'cdn_admin_subscriber', AdminSubscriber::class ); } } Engine/CDN/Admin/Subscriber.php 0000644 00000001240 15174677547 0012245 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\CDN\Admin; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_meta_boxes_fields' => [ 'add_meta_box', 9 ], ]; } /** * Add the field to the WP Rocket metabox on the post edit page. * * @param string[] $fields Metaboxes fields. * * @return string[] */ public function add_meta_box( array $fields ) { $fields['cdn'] = __( 'CDN', 'rocket' ); return $fields; } } Engine/CDN/CDN.php 0000644 00000024170 15174677547 0007525 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\CDN; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Support\CommentTrait; /** * CDN class * * @since 3.4 */ class CDN { use CommentTrait; /** * WP Rocket Options instance * * @var Options_Data */ private $options; /** * Home URL host * * @var string */ private $home_host; /** * Constructor * * @param Options_Data $options WP Rocket Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Search & Replace URLs with the CDN URLs in the provided content * * @since 3.4 * * @param string $html HTML content. * @return string */ public function rewrite( $html ) { $relative_path_pattern = ''; $buffer = $html; /** * Filters the exclusion of CDN rewriting inside inline scripts * * @since 3.10.5 * * @param bool $enable True to exclude, false otherwise. */ if ( apply_filters( 'rocket_cdn_exclude_inline_scripts', true ) ) { $buffer = $this->remove_inline_scripts( $html ); } /** * Filters the CDN rewriting of relative paths * * @since 3.10.5 * * @param bool $enable True to enable, false otherwise. */ if ( apply_filters( 'rocket_cdn_relative_paths', true ) ) { $relative_path_pattern = '|\/[^/](?:[^"\')\s>]+\.[[:alnum:]]+)'; } $pattern = '#[("\']\s*(?<url>(?:(?:https?:|)' . preg_quote( $this->get_base_url(), '#' ) . ')\/(?:(?:(?:' . $this->get_allowed_paths() . ')[^"\',)]+))' . $relative_path_pattern . ')\s*["\')]#i'; if ( ! preg_match_all( $pattern, $buffer, $matches, PREG_SET_ORDER ) ) { return $html; } foreach ( $matches as $match ) { $cdn_url = str_replace( $match['url'], $this->rewrite_url( $match['url'] ), $match[0] ); $html = str_replace( $match[0], $cdn_url, $html ); } return $this->add_meta_comment( 'cdn', $html ); } /** * Rewrites URLs in a srcset attribute using the CDN URL * * @since 3.4.0.4 * * @param string $html HTML content. * @return string */ public function rewrite_srcset( $html ) { $pattern = '#\s+(?:' . $this->get_srcset_attributes() . ')?srcset\s*=\s*["\']\s*(?<sources>[^"\',\s]+\.[^"\',\s]+(?:\s+\d+[wx])?(?:\s*,\s*[^"\',\s]+\.[^"\',\s]+(?:\s+\d+[wx])?)*)\s*["\']#i'; if ( ! preg_match_all( $pattern, $html, $srcsets, PREG_SET_ORDER ) ) { return $html; } foreach ( $srcsets as $srcset ) { $sources = explode( ',', $srcset['sources'] ); $sources = array_unique( array_map( 'trim', $sources ) ); $cdn_srcset = $srcset['sources']; foreach ( $sources as $source ) { $url = preg_split( '#\s+#', trim( $source ) ); $cdn_source = str_replace( $url[0], $this->rewrite_url( $url[0] ), $source ); $cdn_srcset = str_replace( $source, $cdn_source, $cdn_srcset ); } $cdn_srcsets = str_replace( $srcset['sources'], $cdn_srcset, $srcset[0] ); $html = str_replace( $srcset[0], $cdn_srcsets, $html ); } return $html; } /** * Rewrites an URL with the CDN URL * * @since 3.4 * * @param string $url Original URL. * @return string */ public function rewrite_url( $url ) { if ( ! $this->options->get( 'cdn', 0 ) ) { return $url; } if ( $this->is_excluded( $url ) ) { return $url; } $cdn_urls = $this->get_cdn_urls( $this->get_zones_for_url( $url ) ); if ( ! $cdn_urls ) { return $url; } $parsed_url = wp_parse_url( $url ); $cdn_url = untrailingslashit( $cdn_urls[ ( abs( crc32( $parsed_url['path'] ) ) % count( $cdn_urls ) ) ] ); if ( ! isset( $parsed_url['host'] ) ) { return rocket_add_url_protocol( $cdn_url . '/' . ltrim( $url, '/' ) ); } $home_host = $this->get_home_host(); if ( ! isset( $parsed_url['scheme'] ) ) { return str_replace( $home_host, rocket_remove_url_protocol( $cdn_url ), $url ); } $home_url = [ 'http://' . $home_host, 'https://' . $home_host, ]; return str_replace( $home_url, rocket_add_url_protocol( $cdn_url ), $url ); } /** * Rewrites URLs to CDN URLs in CSS content * * @since 3.4 * * @param string $content CSS content. * @return string */ public function rewrite_css_properties( $content ) { if ( ! preg_match_all( '#url\(\s*(\'|")?\s*(?![\'"]?data)(?<url>(?:https?:|)' . preg_quote( $this->get_base_url(), '#' ) . '\/[^"|\'|\)|\s]+)\s*#i', $content, $matches, PREG_SET_ORDER ) ) { return $content; } foreach ( $matches as $property ) { /** * Filters the URL of the CSS property * * @since 2.8 * * @param string $url URL of the CSS property. */ $cdn_url = $this->rewrite_url( apply_filters( 'rocket_cdn_css_properties_url', $property['url'] ) ); $replacement = str_replace( $property['url'], $cdn_url, $property[0] ); $content = str_replace( $property[0], $replacement, $content ); } return $content; } /** * Get all CDN URLs for one or more zones. * * @since 2.1 * @since 3.0 Don't check for WP Rocket CDN option activated to be able to use the function on Hosting with CDN auto-enabled. * * @param array $zones List of zones. Default is [ 'all' ]. * @return array */ public function get_cdn_urls( $zones = [ 'all' ] ) { $hosts = []; $zones = (array) $zones; $cdn_urls = $this->options->get( 'cdn_cnames', [] ); if ( $cdn_urls ) { $cdn_zones = $this->options->get( 'cdn_zone', [] ); foreach ( $cdn_urls as $k => $urls ) { if ( ! in_array( $cdn_zones[ $k ], $zones, true ) ) { continue; } $urls = explode( ',', $urls ); $urls = array_map( 'trim', $urls ); foreach ( $urls as $url ) { $hosts[] = $url; } } } /** * Filter all CDN URLs. * * @since 2.7 * @since 3.4 Added $zone parameter. * * @param array $hosts List of CDN URLs. * @param array $zones List of zones. Default is [ 'all' ]. */ $hosts = (array) apply_filters( 'rocket_cdn_cnames', $hosts, $zones ); $hosts = array_filter( $hosts ); $hosts = array_flip( array_flip( $hosts ) ); $hosts = array_values( $hosts ); return $hosts; } /** * Gets the base URL for the website * * @since 3.4 * * @return string */ private function get_base_url() { return '//' . $this->get_home_host(); } /** * Gets the allowed paths as a regex pattern for the CDN rewrite * * @since 3.4 * * @return string */ private function get_allowed_paths() { $wp_content_dirname = ltrim( trailingslashit( wp_parse_url( content_url(), PHP_URL_PATH ) ), '/' ); $wp_includes_dirname = ltrim( trailingslashit( wp_parse_url( includes_url(), PHP_URL_PATH ) ), '/' ); $upload_dirname = ''; $uploads_info = wp_upload_dir(); if ( ! empty( $uploads_info['baseurl'] ) ) { $upload_dirname = '|' . ltrim( trailingslashit( wp_parse_url( $uploads_info['baseurl'], PHP_URL_PATH ) ), '/' ); } return $wp_content_dirname . $upload_dirname . '|' . $wp_includes_dirname; } /** * Checks if the provided URL can be rewritten with the CDN URL * * @since 3.4 * * @param string $url URL to check. * @return boolean */ public function is_excluded( $url ) { $path = wp_parse_url( $url, PHP_URL_PATH ); $excluded_extensions = [ 'php', 'html', 'htm', 'cfm', ]; if ( in_array( pathinfo( $path, PATHINFO_EXTENSION ), $excluded_extensions, true ) ) { return true; } if ( ! $path ) { return true; } if ( '/' === $path ) { return true; } if ( preg_match( '#^(' . $this->get_excluded_files( '#' ) . ')$#', $path ) ) { return true; } return false; } /** * Gets the home URL host * * @since 3.5.5 * * @return string */ private function get_home_host() { if ( empty( $this->home_host ) ) { $this->home_host = wp_parse_url( home_url(), PHP_URL_HOST ); } return $this->home_host; } /** * Gets the CDN zones for the provided URL * * @since 3.4 * * @param string $url URL to check. * @return array */ private function get_zones_for_url( $url ) { $zones = [ 'all' ]; $ext = pathinfo( wp_parse_url( $url, PHP_URL_PATH ), PATHINFO_EXTENSION ); $image_types = [ 'jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp', 'bmp', 'tiff', 'svg', ]; if ( 'css' === $ext || 'js' === $ext ) { $zones[] = 'css_and_js'; } if ( 'css' === $ext ) { $zones[] = 'css'; } if ( 'js' === $ext ) { $zones[] = 'js'; } if ( in_array( $ext, $image_types, true ) ) { $zones[] = 'images'; } return $zones; } /** * Get all files we don't allow to get in CDN. * * @since 2.5 * * @param string $delimiter RegEx delimiter. * @return string A pipe-separated list of excluded files. */ private function get_excluded_files( $delimiter ) { $files = $this->options->get( 'cdn_reject_files', [] ); /** * Filter the excluded files. * * @since 2.5 * * @param array $files List of excluded files. */ $files = (array) apply_filters( 'rocket_cdn_reject_files', $files ); $files = array_filter( $files ); if ( ! $files ) { return ''; } $files = array_flip( array_flip( $files ) ); $files = array_map( function ( $file ) use ( $delimiter ) { return str_replace( $delimiter, '\\' . $delimiter, $file ); }, $files ); return implode( '|', $files ); } /** * Get srcset attributes to rewrite to the CDN. * * @since 3.8.7 * * @return string A pipe-separated list of srcset attributes. */ private function get_srcset_attributes() { /** * Filter the srcset attributes. * * @since 3.8.7 * * @param array $srcset_attributes List of srcset attributes. */ $srcset_attributes = (array) apply_filters( 'rocket_cdn_srcset_attributes', [ 'data-lazy-', 'data-', ] ); return implode( '|', $srcset_attributes ); } /** * Removes inline scripts from the HTML * * @since 3.10.5 * * @param string $html HTML content. * * @return string */ private function remove_inline_scripts( $html ): string { if ( ! preg_match_all( '#<script(?:[^>]*)>(?<content>[\s\S]*?)</script>#msi', $html, $matches, PREG_SET_ORDER ) ) { return $html; } if ( empty( $matches ) ) { return $html; } foreach ( $matches as $inline_js ) { if ( empty( $inline_js['content'] ) ) { continue; } $html = str_replace( $inline_js[0], '', $html ); } return $html; } } Engine/Debug/Resolver.php 0000644 00000002014 15174677547 0011335 0 ustar 00 <?php namespace WP_Rocket\Engine\Debug; use WP_Rocket\Admin\Options_Data; /** * Resolver. */ class Resolver { /** * Array of WP Rocket Options. * * @var array */ private $options_services = [ 'remove_unused_css' => [ 'service' => 'rucss_debug_subscriber', 'class' => 'WP_Rocket\Engine\Debug\RUCSS\Subscriber', ], ]; /** * Debug options instance. * * @var Options_Data */ private $options; /** * Instantiate the class. * * @param Options_Data $options Options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Ships an array of available services. * * @return array Array of services. */ public function get_services(): array { $set_services = []; if ( empty( $this->options_services ) ) { return []; } foreach ( $this->options_services as $option => $services ) { if ( ! (bool) $this->options->get( $option, 0 ) ) { continue; } $set_services[] = $services; } return $set_services; } } Engine/Debug/ServiceProvider.php 0000644 00000003410 15174677547 0012650 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Debug; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Dependencies\League\Container\ServiceProvider\{AbstractServiceProvider, BootableServiceProviderInterface}; /** * Service provider for Debug */ class ServiceProvider extends AbstractServiceProvider implements BootableServiceProviderInterface { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'debug_subscriber', ]; /** * Array of available debug services. * * @var array */ protected $services = []; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Register the service in the provider array * * @return void */ public function boot(): void { $this->services = $this->getContainer()->get( 'debug_resolver' )->get_services(); if ( empty( $this->services ) ) { return; } $this->provides[] = 'options_debug'; foreach ( $this->services as $service ) { $this->provides[] = $service['service']; } } /** * Registers items with the container * * @return void */ public function register(): void { $this->container->addShared( 'debug_subscriber', DebugSubscriber::class ); if ( empty( $this->services ) ) { return; } $this->container->add( 'options_debug', Options_Data::class ) ->addArgument( $this->container->get( 'options_api' )->get( 'debug', [] ) ); foreach ( $this->services as $service ) { $this->getContainer()->add( $service['service'], $service['class'] ) ->addArgument( 'options_debug' ) ->addArgument( 'options_api' ); } } } Engine/Debug/RUCSS/Subscriber.php 0000644 00000007411 15174677547 0012544 0 ustar 00 <?php namespace WP_Rocket\Engine\Debug\RUCSS; use WP_Rocket\Admin\{Options, Options_Data}; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Logger\Logger; class Subscriber implements Subscriber_Interface { /** * Plugin options instance. * * @var Options_Data */ protected $options; /** * Options instance. * * @var Options */ private $options_api; /** * Instantiate the class * * @param Options_Data $options Options instance. * @param Options $options_api Options instance. */ public function __construct( Options_Data $options, Options $options_api ) { $this->options = $options; $this->options_api = $options_api; } /** * Returns an array of events this listens to * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_last_saas_job_added_time' => [ 'log_last_added_job_time', 10, 2 ], 'rocket_saas_process_pending_jobs_start' => [ 'log_process_pending_job_start_time', 10, 1 ], 'rocket_saas_process_pending_jobs_end' => [ 'log_process_pending_job_end_time', 10, 1 ], 'rocket_saas_check_job_status_end' => [ 'log_check_job_status_end', 10, 1 ], 'rocket_saas_process_on_submit_jobs_start' => [ 'log_process_on_submit_start', 10, 1 ], 'rocket_saas_process_on_submit_jobs_end' => [ 'log_process_on_submit_end', 10, 1 ], ]; } /** * Saves the last time a new job was added to rucss table. * * @param mixed $is_success New job status: ID of inserted row if successfully added; false otherwise. * @param string $timestamp Current timestamp. * @return void */ public function log_last_added_job_time( $is_success, $timestamp ) { if ( Logger::debug_enabled() ) { if ( ! $is_success ) { return; } $this->options->set( 'last_rucss_job_added', $timestamp ); $this->options_api->set( 'debug', $this->options->get_options() ); } } /** * Saves the time when the process pending jobs started. * * @param string $timestamp Current timestamp. * @return void */ public function log_process_pending_job_start_time( $timestamp ) { if ( Logger::debug_enabled() ) { $this->options->set( 'rucss_process_pending_jobs_start', $timestamp ); $this->options_api->set( 'debug', $this->options->get_options() ); } } /** * Saves the time when the process pending jobs ended. * * @param string $timestamp Current timestamp. * @return void */ public function log_process_pending_job_end_time( $timestamp ) { if ( Logger::debug_enabled() ) { $this->options->set( 'rucss_process_pending_jobs_end', $timestamp ); $this->options_api->set( 'debug', $this->options->get_options() ); } } /** * Saves the time when the check job status ended. * * @param string $timestamp Current timestamp. * @return void */ public function log_check_job_status_end( $timestamp ) { if ( Logger::debug_enabled() ) { $this->options->set( 'rucss_check_job_status_end', $timestamp ); $this->options_api->set( 'debug', $this->options->get_options() ); } } /** * Saves the time when the process on submit jobs started. * * @param string $timestamp Current timestamp. * @return void */ public function log_process_on_submit_start( $timestamp ) { if ( Logger::debug_enabled() ) { $this->options->set( 'rucss_process_on_submit_jobs_start', $timestamp ); $this->options_api->set( 'debug', $this->options->get_options() ); } } /** * Saves the time when the process on submit jobs ended. * * @param string $timestamp Current timestamp. * @return void */ public function log_process_on_submit_end( $timestamp ) { if ( Logger::debug_enabled() ) { $this->options->set( 'rucss_process_on_submit_jobs_end', $timestamp ); $this->options_api->set( 'debug', $this->options->get_options() ); } } } Engine/Debug/DebugSubscriber.php 0000644 00000002150 15174677547 0012607 0 ustar 00 <?php namespace WP_Rocket\Engine\Debug; use WP_Rocket\Event_Management\Subscriber_Interface; class DebugSubscriber implements Subscriber_Interface { /** * Returns an array of events this listens to * * @return array */ public static function get_subscribed_events(): array { return [ 'wp_rocket_first_install' => 'on_first_install', 'wp_rocket_upgrade' => [ 'on_upgrade', 10, 2 ], ]; } /** * Adds the debug option on first install. * * @return void */ public function on_first_install(): void { $this->add_debug_options(); } /** * Adds the debug option on upgrade. * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function on_upgrade( $new_version, $old_version ): void { if ( version_compare( $old_version, '3.16', '>=' ) ) { return; } $this->add_debug_options(); } /** * Adds the debug option. * * @return boolean */ private function add_debug_options(): bool { return add_option( 'wp_rocket_debug', [ 'last_rucss_job_added' => '', ] ); } } Engine/Tracking/Subscriber.php 0000644 00000004450 15174677547 0012361 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Tracking; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * The tracking service. * * @var Tracking */ private $tracking; /** * Constructor. * * @param Tracking $tracking The tracking service. */ public function __construct( Tracking $tracking ) { $this->tracking = $tracking; } /** * Returns an array of events this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'update_option_wp_rocket_settings' => [ 'track_option_change', 10, 2 ], 'wp_rocket_upgrade' => [ 'migrate_optin', 10, 2 ], 'rocket_dashboard_after_account_data' => [ 'render_optin', 9 ], 'wp_ajax_rocket_toggle_optin' => [ 'ajax_toggle_optin' ], 'admin_enqueue_scripts' => [ 'localize_optin_status', 15 ], 'admin_print_scripts' => [ 'inject_mixpanel_script' ], ]; } /** * Track option change. * * @param mixed $old_value The old value of the option. * @param mixed $value The new value of the option. * * @return void */ public function track_option_change( $old_value, $value ): void { $this->tracking->track_option_change( $old_value, $value ); } /** * Migrate opt-in to new package on upgrade * * @param string $new_version The new version of the plugin. * @param string $old_version The old version of the plugin. * * @return void */ public function migrate_optin( $new_version, $old_version ): void { $this->tracking->migrate_optin( $new_version, $old_version ); } /** * Render the opt-in section. * * @return void */ public function render_optin(): void { $this->tracking->render_optin(); } /** * Handle AJAX request to toggle opt-in. * * @return void */ public function ajax_toggle_optin(): void { $this->tracking->ajax_toggle_optin(); } /** * Localize opt-in status to JavaScript. * * @return void */ public function localize_optin_status(): void { $this->tracking->localize_optin_status(); } /** * Inject Mixpanel JavaScript SDK. * * @since 3.19.2 * @return void */ public function inject_mixpanel_script(): void { $this->tracking->inject_mixpanel_script(); } } Engine/Tracking/Tracking.php 0000644 00000015463 15174677547 0012026 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Tracking; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; use WPMedia\Mixpanel\Optin; use WPMedia\Mixpanel\TrackingPlugin as MixpanelTracking; class Tracking extends Abstract_Render { /** * Options Data instance. * * @var Options_Data */ private $options; /** * Optin instance. * * @var Optin */ private $optin; /** * Mixpanel Tracking instance. * * @var MixpanelTracking */ private $mixpanel; /** * Constructor. * * @param Options_Data $options Options Data instance. * @param Optin $optin Optin instance. * @param MixpanelTracking $mixpanel Mixpanel Tracking instance. * @param string $template_path Path to the template files. */ public function __construct( Options_Data $options, Optin $optin, MixpanelTracking $mixpanel, $template_path ) { parent::__construct( $template_path ); $this->options = $options; $this->optin = $optin; $this->mixpanel = $mixpanel; $this->mixpanel->identify( $this->options->get( 'consumer_email', '' ) ); } /** * Track option change. * * @param mixed $old_value The old value of the option. * @param mixed $value The new value of the option. */ public function track_option_change( $old_value, $value ) { if ( ! $this->optin->is_enabled() ) { return; } /* * Filters the tracked options. * * @since 3.19.2 * * @param string[] $options Array of options that are tracked by default. * @return string[] array of strings. */ $options_to_track = wpm_apply_filters_typed( 'string[]', 'rocket_mixpanel_tracked_options', [] ); foreach ( $options_to_track as $option_tracked ) { if ( ! isset( $old_value[ $option_tracked ], $value[ $option_tracked ] ) ) { continue; } if ( $old_value[ $option_tracked ] === $value[ $option_tracked ] ) { continue; } $this->mixpanel->track( 'WPM Option Changed', [ 'brand' => 'WP Media', 'product' => 'WP Rocket', 'context' => 'wp_plugin', 'option_name' => $option_tracked, 'previous_value' => $old_value[ $option_tracked ], 'new_value' => $value[ $option_tracked ], ] ); } } /** * Migrate opt-in to new package on upgrade * * @param string $new_version The new version of the plugin. * @param string $old_version The old version of the plugin. * * @return void */ public function migrate_optin( string $new_version, string $old_version ): void { if ( version_compare( $old_version, '3.19.1', '>=' ) ) { return; } if ( ! $this->options->get( 'analytics_enabled', false ) ) { return; } $this->optin->enable(); } /** * Render the opt-in section. * * @return void */ public function render_optin(): void { echo $this->generate( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'optin', [ 'current_value' => (int) $this->optin->is_enabled(), ] ); } /** * Handle AJAX request to toggle opt-in. * * @return void */ public function ajax_toggle_optin(): void { check_ajax_referer( 'rocket-ajax' ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_send_json_error( 'Missing capability' ); } if ( ! isset( $_POST['value'] ) ) { wp_send_json_error( 'Missing value parameter' ); } $value = sanitize_key( wp_unslash( $_POST['value'] ) ); if ( '1' === $value ) { $this->optin->enable(); wp_send_json_success( 'Opt-in enabled.' ); } elseif ( '0' === $value ) { $this->optin->disable(); wp_send_json_success( 'Opt-in disabled.' ); } wp_send_json_error( 'Invalid value parameter.' ); } /** * Add opt-in status to admin scripts. * * @return void */ public function localize_optin_status(): void { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } // Get the license email and hash it for privacy. $consumer_email = $this->options->get( 'consumer_email', '' ); $hashed_email = ! empty( $consumer_email ) ? $this->mixpanel->hash( $consumer_email ) : ''; wp_localize_script( 'wpr-admin-common', 'rocket_mixpanel_data', [ 'optin_enabled' => $this->optin->is_enabled() ? true : false, 'brand' => 'WP Media', 'product' => 'WP Rocket', 'context' => 'wp_plugin', 'path' => isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '', 'user_id' => $hashed_email, ] ); } /** * Injects Mixpanel JavaScript SDK when opt-in is enabled. * * @since 3.19.2 * @return void */ public function inject_mixpanel_script(): void { // Only inject if user has capability and opt-in is enabled. if ( ! current_user_can( 'rocket_manage_options' ) || ! $this->optin->is_enabled() ) { return; } ?> <!-- start Mixpanel --><script type="text/javascript">const MIXPANEL_CUSTOM_LIB_URL = "<?php echo esc_js( rocket_get_constant( 'WP_ROCKET_ASSETS_JS_URL' ) . 'mixpanel-2-latest.min.js' ); ?>";(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+"=([^&]*)")))?l[1]:null};g&&c(g,"state")&&(i=JSON.parse(decodeURIComponent(c(g,"state"))),"mpeditor"===i.action&&(b.sessionStorage.setItem("_mpcehash",g),history.replaceState(i.desiredHash||"",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(".");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments, 0)))}}var d=a;"undefined"!==typeof f?d=a[f]=[]:f="mixpanel";d.people=d.people||[];d.toString=function(b){var a="mixpanel";"mixpanel"!==f&&(a+="."+f);b||(a+=" (stub)");return a};d.people.toString=function(){return d.toString(1)+".people (stub)"};k="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" "); for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement("script");b.type="text/javascript";b.async=!0;b.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";c=e.getElementsByTagName("script")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]); mixpanel.init("<?php echo $this->mixpanel->get_token(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>", { 'ip':false, 'property_blacklist': ['$initial_referrer', '$current_url', '$initial_referring_domain', '$referrer', '$referring_domain'] } ); </script><!-- end Mixpanel --> <?php } } Engine/Tracking/ServiceProvider.php 0000644 00000003154 15174677547 0013371 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Tracking; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WPMedia\Mixpanel\Optin; use WPMedia\Mixpanel\TrackingPlugin as MixpanelTracking; class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'mixpanel_tracking', 'tracking', 'tracking_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the services in the container * * @return void */ public function register(): void { $this->getContainer()->add( 'mixpanel_optin', Optin::class ) ->addArguments( [ 'rocket', 'rocket_manage_options', ] ); $this->getContainer()->add( 'mixpanel_tracking', MixpanelTracking::class ) ->addArguments( [ 'e1135bbed811a82645a7df564f0278c4', 'WP Rocket ' . rocket_get_constant( 'WP_ROCKET_VERSION', '' ), ] ); $this->getContainer()->add( 'tracking', Tracking::class ) ->addArguments( [ 'options', 'mixpanel_optin', 'mixpanel_tracking', new StringArgument( $this->getContainer()->get( 'template_path' ) . '/settings/sections/' ), ] ); $this->getContainer()->add( 'tracking_subscriber', Subscriber::class ) ->addArgument( 'tracking' ); } } Engine/Heartbeat/HeartbeatSubscriber.php 0000644 00000010436 15174677547 0014337 0 ustar 00 <?php namespace WP_Rocket\Engine\Heartbeat; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\Options_Data as Options; /** * Event subscriber to control Heartbeat behavior. * * @since 3.7 Moved to new architecture. * @since 3.2 */ class HeartbeatSubscriber implements Subscriber_Interface { /** * Instance of the Option_Data class. * * @var Options * @since 3.2 * @access private */ private $options; /** * Constructor. * * @since 3.2 * @access public * @param Options $options Instance of the Option_Data class. */ public function __construct( Options $options ) { $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.2 * @access public * * @return array */ public static function get_subscribed_events() { $priority = PHP_INT_MAX - 60; return [ 'admin_enqueue_scripts' => [ 'maybe_disable', $priority ], 'wp_enqueue_scripts' => [ 'maybe_disable', $priority ], 'heartbeat_settings' => [ 'maybe_modify_period', $priority ], ]; } /** * Maybe disable Heartbeat. * * @since 3.2 * @access public */ public function maybe_disable() { if ( ! $this->behavior_match_context( 'disable' ) || rocket_bypass() ) { return; } wp_deregister_script( 'heartbeat' ); /** * Enqueue an empty heartbeat script to prevent query monitor error * Added to the footer */ wp_enqueue_script( 'heartbeat', WP_ROCKET_ASSETS_JS_URL . 'heartbeat.js', null, WP_ROCKET_VERSION, true ); } /** * Maybe modify Heartbeat periodicity. * * @since 3.2 * @access public * * @param array $settings The Heartbeat settings. * * @return array */ public function maybe_modify_period( $settings ) { if ( ! $this->behavior_match_context( 'reduce_periodicity' ) ) { return $settings; } $settings['interval'] = 120; $settings['minimalInterval'] = 120; return $settings; } /** * Tell if we're in frontend, backend, or a post edition page. * * @since 3.2 * @access private * * @return string Either 'site' (frontend), 'admin' (backend), or 'editor'. */ private function get_current_context() { $request_uri = ! empty( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $request_uri = explode( '?', $request_uri, 2 ); $request_uri = reset( $request_uri ); if ( $request_uri && preg_match( '@/wp-admin/post(-new)?\.php$@', $request_uri ) ) { $context = 'editor'; } elseif ( is_admin() ) { $context = 'admin'; if ( wp_doing_ajax() && ! empty( $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( 'wp-remove-post-lock' === sanitize_key( wp_unslash( $_POST['action'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $context = 'editor'; } elseif ( ! empty( $_POST['screen_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing switch ( $_POST['screen_id'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing case 'post': $context = 'editor'; break; case 'front': $context = 'site'; } } } } else { $context = 'site'; } /** * Filter the current context. * This can be useful for ajax requests or requests to admin-post.php, as there is no easy way to tell where those requests come from. * * @since 3.2 * * @param string $context string Either 'site' (frontend), 'admin' (backend), or 'editor'. */ $filtered_context = apply_filters( 'rocket_heartbeat_context', $context ); $contexts = [ 'editor' => 1, 'admin' => 1, 'site' => 1, ]; return isset( $contexts[ $filtered_context ] ) ? $filtered_context : $context; } /** * Tell if the given behavior is what is set in the addon settings, accordingly to the current context. * * @since 3.2 * @access private * * @param string $behavior Either '', 'disable', or 'reduce_periodicity'. * * @return bool */ private function behavior_match_context( $behavior ) { if ( ! $this->options->get( 'control_heartbeat', 0 ) ) { return false; } $context = $this->get_current_context(); return $behavior === $this->options->get( 'heartbeat_' . $context . '_behavior', '' ); } } Engine/Heartbeat/ServiceProvider.php 0000644 00000001556 15174677547 0013532 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Heartbeat; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for heartbeat module */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'heartbeat_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'heartbeat_subscriber', HeartbeatSubscriber::class ) ->addArgument( 'options' ); } } Engine/Activation/ActivationInterface.php 0000644 00000000273 15174677547 0014536 0 ustar 00 <?php namespace WP_Rocket\Engine\Activation; interface ActivationInterface { /** * Executes this method on plugin activation * * @return void */ public function activate(); } Engine/Activation/ServiceProvider.php 0000644 00000003642 15174677547 0013732 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Activation; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Dependencies\League\Container\ServiceProvider\BootableServiceProviderInterface; use WP_Rocket\Engine\Cache\AdvancedCache; use WP_Rocket\Engine\Cache\WPCache; use WP_Rocket\Engine\Capabilities\Manager; use WP_Rocket\Engine\HealthCheck\ActionSchedulerCheck; /** * Service Provider for the activation process. */ class ServiceProvider extends AbstractServiceProvider implements BootableServiceProviderInterface { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'advanced_cache', 'capabilities_manager', 'wp_cache', 'action_scheduler_check', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Executes this method when the service provider is registered * * @return void */ public function boot(): void { $this->getContainer() ->inflector( ActivationInterface::class ) ->invokeMethod( 'activate', [] ); } /** * Registers the option array in the container. */ public function register(): void { $filesystem = rocket_direct_filesystem(); $this->getContainer()->add( 'advanced_cache', AdvancedCache::class ) ->addArguments( [ new StringArgument( $this->getContainer()->get( 'template_path' ) . '/cache/' ), $filesystem, ] ); $this->getContainer()->add( 'capabilities_manager', Manager::class ); $this->getContainer()->add( 'wp_cache', WPCache::class ) ->addArgument( $filesystem ); $this->getContainer()->add( 'action_scheduler_check', ActionSchedulerCheck::class ); } } Engine/Activation/Activation.php 0000644 00000007212 15174677547 0012715 0 ustar 00 <?php namespace WP_Rocket\Engine\Activation; use WP_Rocket\Admin\Options; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\Container; use WP_Rocket\Engine\Common\PerformanceHints\Activation\ServiceProvider as PerformanceHintsActivationServiceProvider; use WP_Rocket\Engine\License\ServiceProvider as LicenseServiceProvider; use WP_Rocket\Engine\Preload\Activation\ServiceProvider as PreloadActivationServiceProvider; use WP_Rocket\Logger\ServiceProvider as LoggerServiceProvider; use WP_Rocket\ServiceProvider\Options as OptionsServiceProvider; use WP_Rocket\ThirdParty\Hostings\HostResolver; use WP_Rocket\ThirdParty\Hostings\ServiceProvider as HostingsServiceProvider; use WP_Rocket\Event_Management\Event_Manager; /** * Plugin activation controller * * @since 3.6.3 */ class Activation { const ACTIVATION_ENDPOINT = 'https://api.wp-rocket.me/api/wp-rocket/activate-licence.php'; /** * Aliases in the container for each class that needs to call its activate method * * @var array */ private static $activators = [ 'advanced_cache', 'capabilities_manager', 'wp_cache', 'action_scheduler_check', 'preload_activation', 'performance_hints_activation', ]; /** * Performs these actions during the plugin activation * * @return void */ public static function activate_plugin() { $container = new Container(); $event_manager = new Event_Manager(); $container->add( 'template_path', new StringArgument( rocket_get_constant( 'WP_ROCKET_PATH', '' ) . 'views' ) ); $options_api = new Options( 'wp_rocket_' ); $container->add( 'options_api', $options_api ); $container->addServiceProvider( new OptionsServiceProvider() ); $container->addServiceProvider( new PreloadActivationServiceProvider() ); $container->addServiceProvider( new ServiceProvider() ); $container->addServiceProvider( new HostingsServiceProvider() ); $container->addServiceProvider( new LicenseServiceProvider() ); $container->addServiceProvider( new LoggerServiceProvider() ); $container->get( 'logger' ); $container->addServiceProvider( new PerformanceHintsActivationServiceProvider() ); $event_manager->add_subscriber( $container->get( 'performance_hints_warmup_subscriber' ) ); $host_type = HostResolver::get_host_service(); if ( ! empty( $host_type ) ) { array_unshift( self::$activators, $host_type ); } foreach ( self::$activators as $activator ) { $container->get( $activator ); } // Last constants. define( 'WP_ROCKET_PLUGIN_NAME', 'WP Rocket' ); define( 'WP_ROCKET_PLUGIN_SLUG', sanitize_key( WP_ROCKET_PLUGIN_NAME ) ); if ( defined( 'SUNRISE' ) && SUNRISE === 'on' && function_exists( 'domain_mapping_siteurl' ) ) { require WP_ROCKET_INC_PATH . 'domain-mapping.php'; } require WP_ROCKET_FUNCTIONS_PATH . 'options.php'; require WP_ROCKET_FUNCTIONS_PATH . 'formatting.php'; require WP_ROCKET_FUNCTIONS_PATH . 'i18n.php'; require WP_ROCKET_FUNCTIONS_PATH . 'htaccess.php'; require WP_ROCKET_FUNCTIONS_PATH . 'api.php'; require WP_ROCKET_FUNCTIONS_PATH . 'admin.php'; /** * WP Rocket activation. * * @since 3.1.5 */ do_action( 'rocket_activation' ); if ( rocket_valid_key() ) { // Add All WP Rocket rules of the .htaccess file. flush_rocket_htaccess(); } // Create the cache folders (wp-rocket & min). rocket_init_cache_dir(); // Create the config folder (wp-rocket-config). rocket_init_config_dir(); // Update customer key & licence. wp_remote_get( self::ACTIVATION_ENDPOINT, [ 'blocking' => false, ] ); /** * Fires after WP Rocket is activated */ do_action( 'rocket_after_activation' ); } } Engine/Media/ServiceProvider.php 0000644 00000005364 15174677547 0012653 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media; use WP_Rocket\Buffer\{Config, Tests}; use WP_Rocket\Dependencies\League\Container\Argument\Literal\ArrayArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Dependencies\RocketLazyload\{Assets, Iframe, Image}; use WP_Rocket\Engine\Media\Emojis\EmojisSubscriber; use WP_Rocket\Engine\Media\ImageDimensions\{ AdminSubscriber as ImageDimensionsAdminSubscriber, ImageDimensions, Subscriber as ImageDimensionsSubscriber }; use WP_Rocket\Engine\Media\Lazyload\{ AdminSubscriber as LazyloadAdminSubscriber, Subscriber }; /** * Service provider for Media module */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'config', 'tests', 'lazyload_assets', 'lazyload_image', 'lazyload_iframe', 'lazyload_subscriber', 'lazyload_admin_subscriber', 'emojis_subscriber', 'image_dimensions', 'image_dimensions_subscriber', 'image_dimensions_admin_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'config', Config::class ) ->addArgument( new ArrayArgument( [ 'config_dir_path' => rocket_get_constant( 'WP_ROCKET_CONFIG_PATH' ), ] ) ); $this->getContainer()->add( 'tests', Tests::class ) ->addArgument( 'config' ); $this->getContainer()->add( 'lazyload_assets', Assets::class ); $this->getContainer()->add( 'lazyload_image', Image::class ); $this->getContainer()->add( 'lazyload_iframe', Iframe::class ); $this->getContainer()->addShared( 'lazyload_subscriber', Subscriber::class ) ->addArguments( [ 'options', 'lazyload_assets', 'lazyload_image', 'lazyload_iframe', ] ); $this->getContainer()->addShared( 'lazyload_admin_subscriber', LazyloadAdminSubscriber::class ); $this->getContainer()->addShared( 'emojis_subscriber', EmojisSubscriber::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'image_dimensions', ImageDimensions::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'image_dimensions_subscriber', ImageDimensionsSubscriber::class ) ->addArguments( [ 'image_dimensions', 'tests', ] ); $this->getContainer()->addShared( 'image_dimensions_admin_subscriber', ImageDimensionsAdminSubscriber::class ) ->addArgument( 'image_dimensions' ); } } Engine/Media/PreloadFonts/ServiceProvider.php 0000644 00000006424 15174677547 0015251 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreloadFonts; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Media\PreloadFonts\Database\Table\PreloadFonts as PreloadFontsTable; use WP_Rocket\Engine\Media\PreloadFonts\Database\Queries\PreloadFonts as PreloadFontsQuery; use WP_Rocket\Engine\Media\PreloadFonts\AJAX\Controller as AJAXController; use WP_Rocket\Engine\Media\PreloadFonts\Context\Context; use WP_Rocket\Engine\Media\PreloadFonts\Frontend\Controller as FrontendController; use WP_Rocket\Engine\Media\PreloadFonts\Frontend\Subscriber as FrontendSubscriber; use WP_Rocket\Engine\Media\PreloadFonts\Admin\Subscriber as AdminSubscriber; use WP_Rocket\Engine\Media\PreloadFonts\Admin\Settings as AdminSettings; class ServiceProvider extends AbstractServiceProvider { /** * The provides array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ 'preload_fonts_table', 'preload_fonts_query', 'preload_fonts_ajax_controller', 'preload_fonts_context', 'preload_fonts_frontend_subscriber', 'preload_fonts_front_controller', 'preload_fonts_factory', 'preload_fonts_admin_subscriber', 'preload_fonts_admin_settings', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the classes in the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'preload_fonts_table', PreloadFontsTable::class ); $this->getContainer()->get( 'preload_fonts_table' ); $this->getContainer()->add( 'preload_fonts_query', PreloadFontsQuery::class ); $this->getContainer()->add( 'preload_fonts_context', Context::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'preload_fonts_front_controller', FrontendController::class ) ->addArguments( [ 'options', 'preload_fonts_query', 'preload_fonts_context', ] ); $this->getContainer()->add( 'preload_fonts_ajax_controller', AJAXController::class ) ->addArguments( [ 'preload_fonts_query', 'preload_fonts_context', ] ); $this->getContainer()->addShared( 'preload_fonts_frontend_subscriber', FrontendSubscriber::class ) ->addArguments( [ 'preload_fonts_front_controller', 'dynamic_lists_defaultlists_data_manager', ] ); $this->getContainer()->addShared( 'preload_fonts_factory', Factory::class ) ->addArguments( [ 'preload_fonts_ajax_controller', 'preload_fonts_front_controller', 'preload_fonts_table', 'preload_fonts_query', 'preload_fonts_context', ] ); $this->getContainer()->addShared( 'preload_fonts_admin_settings', AdminSettings::class ) ->addArguments( [ 'options', 'options_api', ] ); $this->getContainer()->add( 'preload_fonts_admin_subscriber', AdminSubscriber::class ) ->addArguments( [ 'preload_fonts_admin_settings', ] ); } } Engine/Media/PreloadFonts/AJAX/Controller.php 0000644 00000013177 15174677547 0015007 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreloadFonts\AJAX; use WP_Rocket\Engine\Media\PreloadFonts\Context\Context; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\AJAXControllerTrait; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\ControllerInterface; use WP_Rocket\Engine\Media\PreloadFonts\Database\Queries\PreloadFonts as PreloadFontsQuery; use WP_Rocket\Engine\Optimization\UrlTrait; class Controller implements ControllerInterface { use AJAXControllerTrait; /** * PLFQuery instance * * @var PreloadFontsQuery */ private $query; /** * PreloadFonts Context. * * @var Context */ protected $context; /** * Constructor * * @param PreloadFontsQuery $query PLFQuery instance. * @param Context $context Context instance. */ public function __construct( PreloadFontsQuery $query, Context $context ) { $this->query = $query; $this->context = $context; } /** * Add Preload fonts data to the database * * @return array */ public function add_data(): array { check_ajax_referer( 'rocket_beacon', 'rocket_beacon_nonce' ); $payload = []; if ( ! $this->context->is_allowed() ) { $payload['preload_fonts'] = 'not allowed'; return $payload; } $url = isset( $_POST['url'] ) ? untrailingslashit( esc_url_raw( wp_unslash( $_POST['url'] ) ) ) : ''; $is_mobile = isset( $_POST['is_mobile'] ) ? filter_var( wp_unslash( $_POST['is_mobile'] ), FILTER_VALIDATE_BOOLEAN ) : false; $results = isset( $_POST['results'] ) ? json_decode( wp_unslash( $_POST['results'] ) ) : (object) [ 'preload_fonts' => [] ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $fonts = $results->preload_fonts ?? []; $preload_fonts = []; /** * Filters the maximum number of fonts being saved into the database. * * @param int $max_number Maximum number to allow. * @param string $url Current page url. * @param string[]|array $hashes Current list of preload fonts. */ $max_preload_fonts_number = wpm_apply_filters_typed( 'integer', 'rocket_preload_fonts_number', 20, $url, $fonts ); if ( 0 >= $max_preload_fonts_number ) { $max_preload_fonts_number = 1; } $fonts = $this->filter_fonts( $fonts, $this->context->get_exclusions() ); foreach ( (array) $fonts as $index => $font ) { $preload_fonts[ $index ] = sanitize_url( wp_unslash( $font ) ); --$max_preload_fonts_number; } $row = $this->query->get_row( $url, $is_mobile ); if ( ! empty( $row ) ) { $payload['preload_fonts'] = 'item already in the database'; return $payload; } $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : ''; list( $status_code, $status_message ) = $this->get_status_code_message( $status ); $item = [ 'url' => $url, 'is_mobile' => $is_mobile, 'status' => $status_code, 'error_message' => $status_message, 'fonts' => wp_json_encode( $preload_fonts ), 'created_at' => current_time( 'mysql', true ), 'last_accessed' => current_time( 'mysql', true ), ]; $result = $this->query->add_item( $item ); if ( ! $result ) { $payload['preload_fonts'] = 'error when adding the entry to the database'; return $payload; } $payload['preload_fonts'] = $item; return $payload; } /** * Checks if there is existing data for the current URL and device type from the beacon script. * * This method is called via AJAX. It checks if there is existing fonts data for the current URL and device type. * If the data exists, it returns a JSON success response with true. If the data does not exist, it returns a JSON success response with false. * If the context is not allowed, it returns a JSON error response with false. * * @return array */ public function check_data(): array { $payload = [ 'preload_fonts' => false, ]; check_ajax_referer( 'rocket_beacon', 'rocket_beacon_nonce' ); if ( ! $this->context->is_allowed() ) { $payload['preload_fonts'] = true; return $payload; } $is_mobile = isset( $_POST['is_mobile'] ) ? filter_var( wp_unslash( $_POST['is_mobile'] ), FILTER_VALIDATE_BOOLEAN ) : false; $url = isset( $_POST['url'] ) ? untrailingslashit( esc_url_raw( wp_unslash( $_POST['url'] ) ) ) : ''; $row = $this->query->get_row( $url, $is_mobile ); if ( ! empty( $row ) ) { $payload['preload_fonts'] = true; } return $payload; } /** * Filter font urls before saving into DB by checking exclusions list and extensions. * * @param array $fonts Array of fonts to be preloaded. * @param array $exclusions Array of fonts to be excluded. * * @return array Filtered array of fonts, excluding those specified in the exclusion list. */ private function filter_fonts( array $fonts, array $exclusions ): array { if ( empty( $exclusions ) ) { return $fonts; } /** * Create a single regex pattern from all exclusions. * Use a different delimiter (#) to avoid issues with URLs containing slashes. */ $pattern = '#(' . implode( '|', array_map( 'preg_quote', $exclusions ) ) . ')#i'; $extensions = $this->context->get_extensions(); // Filter out fonts that match the pattern. $filtered_fonts = array_filter( $fonts, function ( $font ) use ( $pattern, $exclusions, $extensions ) { if ( ! in_array( pathinfo( $font, PATHINFO_EXTENSION ), $extensions, true ) ) { return false; } // Check exact match ( Mainly url match ). if ( in_array( $font, $exclusions, true ) ) { return false; } // Check for substring match using regex. return ! preg_match( $pattern, $font ); } ); return array_values( $filtered_fonts ); } } Engine/Media/PreloadFonts/Database/Queries/PreloadFonts.php 0000644 00000004653 15174677547 0017661 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\PreloadFonts\Database\Queries; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\AbstractQueries; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\QueriesInterface; use WP_Rocket\Engine\Media\PreloadFonts\Database\Schema\PreloadFonts as PreloadFontsSchema; use WP_Rocket\Engine\Media\PreloadFonts\Database\Rows\PreloadFonts as PreloadFontsRows; class PreloadFonts extends AbstractQueries implements QueriesInterface { /** * Name of the database table to query. * * @var string */ protected $table_name = 'wpr_preload_fonts'; /** * String used to alias the database table in MySQL statement. * * Keep this short, but descriptive. I.E. "tr" for term relationships. * * This is used to avoid collisions with JOINs. * * @var string */ protected $table_alias = 'wpr_plf'; /** * Name of class used to setup the database schema. * * @var string */ protected $table_schema = PreloadFontsSchema::class; /** * Name for a single item. * * Use underscores between words. I.E. "term_relationship" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name = 'preload_fonts'; /** * Plural version for a group of items. * * Use underscores between words. I.E. "term_relationships" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name_plural = 'preload_fonts'; /** * Name of class used to turn IDs into first-class objects. * * This is used when looping through return values to guarantee their shape. * * @var mixed */ protected $item_shape = PreloadFontsRows::class; /** * Deletes old rows from the database. * * This method is used to delete rows from the database that have not been accessed in the last month. * * @return bool|int Returns a boolean or integer value. The exact return value depends on the implementation. */ public function delete_old_rows() { // Get the database interface. $db = $this->get_db(); // Early bailout if no database interface is available. if ( ! $db ) { return false; } $delete_interval = $this->cleanup_interval; $prefixed_table_name = $db->prefix . $this->table_name; $query = "DELETE FROM `$prefixed_table_name` WHERE status = 'failed' OR `last_accessed` <= date_sub(now(), interval $delete_interval month)"; return $db->query( $query ); } } Engine/Media/PreloadFonts/Database/Rows/PreloadFonts.php 0000644 00000003620 15174677547 0017167 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\PreloadFonts\Database\Rows; use WP_Rocket\Dependencies\BerlinDB\Database\Row; class PreloadFonts extends Row { /** * Row ID * * @var int */ public $id; /** * URL * * @var string */ public $url; /** * Is for mobile * * @var bool */ public $is_mobile; /** * Fonts * * @var string */ public $fonts; /** * Error message * * @var string */ public $error_message; /** * Status * * @var string */ public $status; /** * Last modified time * * @var int */ public $modified; /** * Last accessed time * * @var int */ public $last_accessed; /** * Created time * * @var int */ public $created_at; /** * Constructor. * * @param mixed $item Object Row. */ public function __construct( $item ) { parent::__construct( $item ); // Set the type of each column, and prepare. $this->id = (int) $this->id; $this->url = (string) $this->url; $this->is_mobile = (bool) $this->is_mobile; $this->fonts = (string) $this->fonts; $this->status = (string) $this->status; $this->error_message = (string) $this->error_message; $this->modified = empty( $this->modified ) ? 0 : strtotime( (string) $this->modified ); $this->last_accessed = empty( $this->last_accessed ) ? 0 : strtotime( (string) $this->last_accessed ); $this->created_at = empty( $this->created_at ) ? 0 : strtotime( (string) $this->created_at ); } /** * Checks if the object has a valid Preload Fonts value. * * @return bool Returns true if the object's status is 'completed' and the fonts value is not empty or '[]', false otherwise. */ public function has_preload_fonts() { if ( 'completed' !== $this->status ) { return false; } if ( empty( $this->fonts ) ) { return false; } if ( '[]' === $this->fonts ) { return false; } return true; } } Engine/Media/PreloadFonts/Database/Schema/PreloadFonts.php 0000644 00000004154 15174677547 0017440 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreloadFonts\Database\Schema; use WP_Rocket\Dependencies\BerlinDB\Database\Schema; class PreloadFonts extends Schema { /** * Array of database column objects * * @var array */ public $columns = [ // ID column. [ 'name' => 'id', 'type' => 'bigint', 'length' => '20', 'unsigned' => true, 'extra' => 'auto_increment', 'primary' => true, 'sortable' => true, ], // URL column. [ 'name' => 'url', 'type' => 'varchar', 'length' => '2000', 'default' => '', 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // IS_MOBILE column. [ 'name' => 'is_mobile', 'type' => 'tinyint', 'length' => '1', 'default' => 0, 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // Below the fold column. [ 'name' => 'fonts', 'type' => 'longtext', 'default' => '', 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // error_message column. [ 'name' => 'error_message', 'type' => 'longtext', 'default' => null, 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // STATUS column. [ 'name' => 'status', 'type' => 'varchar', 'length' => '255', 'default' => null, 'cache_key' => true, 'searchable' => true, 'sortable' => false, ], // MODIFIED column. [ 'name' => 'modified', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], // LAST_ACCESSED column. [ 'name' => 'last_accessed', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], // CREATED_AT column. [ 'name' => 'created_at', 'type' => 'timestamp', 'default' => null, 'created' => true, 'date_query' => true, 'sortable' => true, ], ]; } Engine/Media/PreloadFonts/Database/Table/PreloadFonts.php 0000644 00000002635 15174677547 0017271 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreloadFonts\Database\Table; use WP_Rocket\Engine\Common\PerformanceHints\Database\Table\AbstractTable; class PreloadFonts extends AbstractTable { /** * Table name * * @var string */ protected $name = 'wpr_preload_fonts'; /** * Database version key (saved in _options or _sitemeta) * * @var string */ protected $db_version_key = 'wpr_preload_fonts_version'; /** * Database version * * @var int */ protected $version = 20250204; /** * Key => value array of versions => methods. * * @var array */ protected $upgrades = []; /** * Table schema data. * * @var string */ protected $schema_data = " id bigint(20) unsigned NOT NULL AUTO_INCREMENT, url varchar(2000) NOT NULL default '', is_mobile tinyint(1) NOT NULL default 0, fonts longtext default '', error_message longtext default '', status varchar(255) NOT NULL default '', modified timestamp NOT NULL default '0000-00-00 00:00:00', last_accessed timestamp NOT NULL default '0000-00-00 00:00:00', created_at timestamp NULL, PRIMARY KEY (id), KEY url (url(150), is_mobile), KEY modified (modified), KEY last_accessed (last_accessed), INDEX `status_index` (`status`(191))"; } Engine/Media/PreloadFonts/Frontend/Subscriber.php 0000644 00000005576 15174677547 0016027 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreloadFonts\Frontend; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Engine\Media\PreloadFonts\Frontend\Controller as PreloadFonts; use WP_Rocket\Engine\Optimization\DynamicLists\DefaultLists\DataManager; class Subscriber implements Subscriber_Interface { /** * Preload Fonts controller instance. * * @var PreloadFonts */ private $preload_fonts; /** * DataManager instance * * @var DataManager */ private $data_manager; /** * Subscriber constructor. * * @param PreloadFonts $preload_fonts Preload Fonts controller instance. * @param DataManager $data_manager DataManager instance. */ public function __construct( PreloadFonts $preload_fonts, DataManager $data_manager ) { $this->preload_fonts = $preload_fonts; $this->data_manager = $data_manager; } /** * Returns an array of events that this subscriber wants to listen to. * * @since 3.19 * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_head_items' => [ 'add_preload_fonts_in_head', 30 ], 'rocket_enable_rucss_fonts_preload' => 'disable_rucss_preload_fonts', 'rocket_preload_fonts_excluded_fonts' => 'get_exclusions', 'rocket_buffer' => 'maybe_remove_existing_preloaded_fonts', ]; } /** * Add preload fonts into head. * * @param array $items Head items. * @return array */ public function add_preload_fonts_in_head( $items ) { return $this->preload_fonts->add_preload_fonts_in_head( $items ); } /** * Disables the preloading of fonts by the Remove Unused CSS (RUCSS) feature. * * This method is used to prevent RUCSS from preloading fonts when certain conditions are met. * * @param bool $status The current status of font preloading. * @return bool Modified status indicating whether font preloading should be disabled. */ public function disable_rucss_preload_fonts( $status ): bool { return $this->preload_fonts->disable_rucss_preload_fonts( $status ); } /** * Gets the list of fonts to be excluded from preloading. * Merges any existing exclusions with those from the dynamic lists. * * @param array $exclusions Array of font URLs to be excluded from preloading. * @return array */ public function get_exclusions( array $exclusions ): array { $lists = $this->data_manager->get_lists()->preload_fonts_exclusions ?? []; /** * Merge exclusions and lists. * Handle empty arrays gracefully. */ return array_merge( $exclusions, (array) $lists ); } /** * Removes existing preloaded font tags from the HTML buffer. * * @param string $html The HTML content. * @return string Modified HTML content. */ public function maybe_remove_existing_preloaded_fonts( string $html ): string { return $this->preload_fonts->maybe_remove_existing_preloaded_fonts( $html ); } } Engine/Media/PreloadFonts/Frontend/Controller.php 0000644 00000013166 15174677547 0016041 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreloadFonts\Frontend; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface; use WP_Rocket\Engine\Media\PreloadFonts\Database\Queries\PreloadFonts as PFQuery; use WP_Rocket\Engine\Media\PreloadFonts\Context\Context; use WP_Rocket\Engine\Media\PreloadFonts\Database\Rows\PreloadFonts; use WP_Rocket\Engine\Optimization\UrlTrait; use WP_Rocket\Engine\Support\CommentTrait; use WP_Rocket\Engine\Common\Head\ElementTrait; class Controller implements ControllerInterface { use UrlTrait; use CommentTrait; use ElementTrait; /** * Options instance * * @var Options_Data */ private $options; // @phpstan-ignore-line Use of this will come later. /** * Queries instance * * @var PFQuery */ private $query; // @phpstan-ignore-line Use of this will come later. /** * Context instance. * * @var Context */ private $context; // @phpstan-ignore-line Use of this will come later. /** * Constructor * * @param Options_Data $options Options instance. * @param PFQuery $query Queries instance. * @param Context $context Context instance. */ public function __construct( Options_Data $options, PFQuery $query, Context $context ) { $this->options = $options; $this->query = $query; $this->context = $context; } /** * Applies optimization. * * @param string $html HTML content. * @param object $row Database Row. * * @return string */ public function optimize( string $html, $row ): string { if ( ! $this->context->is_allowed() || ! $row->has_preload_fonts() ) { return $html; } return $this->add_meta_comment( 'auto_preload_fonts', $html ); } /** * Add custom data like the List of elements to be considered for optimization. * * @param array $data Array of data passed in beacon. * * @return array */ public function add_custom_data( array $data ): array { if ( ! $this->context->is_allowed() ) { return $data; } $data['preload_fonts_exclusions'] = $this->context->get_exclusions(); $data['status']['preload_fonts'] = $this->context->is_allowed(); $data['processed_extensions'] = $this->context->get_extensions(); return $data; } /** * Adds the preload fonts to the head tag. * * @param array $items added to the head. * @return array Items to be added to the head. */ public function add_preload_fonts_in_head( $items ) { if ( ! $this->context->is_allowed() ) { return $items; } $row = $this->get_current_url_row(); if ( empty( $row ) ) { return $items; } $fonts = json_decode( $row->fonts, true ); if ( empty( $fonts ) ) { return $items; } foreach ( $fonts as $font ) { $item_args = [ 'href' => esc_url( $font ), 'as' => 'font', 2 => 'crossorigin', ]; $items[] = $this->preload_link( $item_args ); } return $items; } /** * Get current visited page row in DB. * * @return false|PreloadFonts */ private function get_current_url_row() { global $wp; $url = untrailingslashit( home_url( add_query_arg( [], $wp->request ) ) ); $is_mobile = $this->context->is_mobile_allowed(); $row = $this->query->get_row( $url, $is_mobile ); if ( empty( $row ) || 'completed' !== $row->status || empty( $row->fonts ) || '[]' === $row->fonts ) { return false; } return $row; } /** * Disables the Remove Unused CSS (RUCSS) feature for preloading fonts. * * This method can be used as a filter callback to control whether the RUCSS feature * should be applied when preloading fonts. * * @param bool $status Current status of the RUCSS preload fonts feature. * @return bool Modified status indicating whether RUCSS should be disabled for preloading fonts. */ public function disable_rucss_preload_fonts( $status ) { if ( ! $this->context->is_allowed() ) { return $status; } $row = $this->get_current_url_row(); if ( empty( $row ) ) { return $status; } return false; } /** * Removes existing preloaded font links from the provided HTML content if necessary. * * This method scans the given HTML string and removes any <link rel="preload" as="font"> tags * that match certain criteria, to prevent duplicate or unnecessary font preloads. * Only removes preloaded fonts when WP Rocket's font optimization is actually applied. * * @param string $html The HTML content to process. * @return string The modified HTML content with preloaded font links removed if applicable. */ public function maybe_remove_existing_preloaded_fonts( string $html ): string { if ( ! $this->context->is_allowed() ) { return $html; } $row = $this->get_current_url_row(); if ( ! $row ) { return $html; } /** * Filter to enable or disable deleting existing preloaded tags. * * @param bool $should_remove */ $should_remove = wpm_apply_filters_typed( 'boolean', 'rocket_remove_existing_preloaded_fonts', true ); if ( ! $should_remove ) { return $html; } // One regex to skip scripts and remove any <link rel=preload as=font…> tag (entire line, including indentation and newline):. $html = preg_replace( '#<script\b[^>]*>[\s\S]*?<\/script\b[^>]*>(*SKIP)(*FAIL)' // skip <script> blocks. . '|^[ \t]*<link\b' // OR match a <link at line start (with optional indent). . '(?=[^>]*\brel\s*=\s*(["\']?)preload\1)' // lookahead rel=preload. . '(?=[^>]*\bas\s*=\s*(["\']?)font\2)' // lookahead as=font. . '[^>]*?\/?>[ \t]*(?:\r?\n|$)#im', // up to /> or >, then trim whitespace and newline. '', $html ); return $html; } } Engine/Media/PreloadFonts/Admin/Subscriber.php 0000644 00000002730 15174677547 0015265 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\PreloadFonts\Admin; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Engine\Media\PreloadFonts\Admin\Settings; /** * Preload Fonts admin subscriber * * @since 3.19 */ class Subscriber implements Subscriber_Interface { /** * Settings instance * * @var Settings */ private $settings; /** * Creates an instance of the object. * * @param Settings $settings Settings instance. */ public function __construct( Settings $settings ) { $this->settings = $settings; } /** * Returns an array of events this subscriber listens to * * @return array[] */ public static function get_subscribed_events(): array { return [ 'wp_rocket_upgrade' => [ 'maybe_enable_auto_preload_fonts', 9, 2 ], ]; } /** * Enables the auto preload fonts option if the old preload fonts option is not empty. * * This function checks the value of the `preload_fonts` option. * If it contains a non-empty value, it updates the `auto_preload_fonts` option to `true`. * This is useful for ensuring that automatic font preloading is enabled based on legacy settings. * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function maybe_enable_auto_preload_fonts( $new_version, $old_version ): void { if ( version_compare( $old_version, '3.19', '>' ) ) { return; } $this->settings->maybe_enable_auto_preload_fonts(); } } Engine/Media/PreloadFonts/Admin/Settings.php 0000644 00000002541 15174677547 0014762 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\PreloadFonts\Admin; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Admin\Options as Options_API; class Settings { /** * WP Rocket options instance. * * @var Options_Data */ private $options; /** * WP Rocket Options API Instance. * * @var Options_API */ private $options_api; /** * Creates an instance of the class. * * @param Options_Data $option_data WP Rocket Options instance. * @param Options_API $options_api WP Rocket Options API instance. */ public function __construct( Options_Data $option_data, Options_API $options_api ) { $this->options = $option_data; $this->options_api = $options_api; } /** * Enables the auto preload fonts option if the old preload fonts option is not empty. * * This function checks the value of the `preload_fonts` option. * If it contains a non-empty value, it updates the `auto_preload_fonts` option to `true`. * This is useful for ensuring that automatic font preloading is enabled based on legacy settings. * * @return void */ public function maybe_enable_auto_preload_fonts(): void { $options = $this->options_api->get( 'settings', [] ); if ( empty( $options['preload_fonts'] ) ) { return; } $this->options->set( 'auto_preload_fonts', true ); $this->options_api->set( 'settings', $this->options->get_options() ); } } Engine/Media/PreloadFonts/Factory.php 0000644 00000005672 15174677547 0013551 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreloadFonts; use WP_Rocket\Engine\Common\PerformanceHints\Cron\CronTrait; use WP_Rocket\Engine\Common\PerformanceHints\FactoryInterface; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\ControllerInterface as AjaxControllerInterface; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface as FrontendControllerInterface; use WP_Rocket\Engine\Common\PerformanceHints\Database\Table\TableInterface; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\QueriesInterface; use WP_Rocket\Engine\Common\Context\ContextInterface; class Factory implements FactoryInterface { use CronTrait; /** * Ajax Controller instance. * * @var AjaxControllerInterface */ protected $ajax_controller; /** * Frontend Controller instance. * * @var FrontendControllerInterface */ protected $frontend_controller; /** * Table instance. * * @var TableInterface */ protected $table; /** * Queries instance. * * @var QueriesInterface */ protected $queries; /** * Context instance. * * @var ContextInterface */ protected $context; /** * Instantiate the class. * * @param AjaxControllerInterface $ajax_controller PreloadFonts AJAX Controller instance. * @param FrontendControllerInterface $frontend_controller PreloadFonts Frontend Controller instance. * @param TableInterface $table PreloadFonts Table instance. * @param QueriesInterface $queries PreloadFonts Queries instance. * @param ContextInterface $context PreloadFonts Context instance. */ public function __construct( AjaxControllerInterface $ajax_controller, FrontendControllerInterface $frontend_controller, TableInterface $table, QueriesInterface $queries, ContextInterface $context ) { $this->ajax_controller = $ajax_controller; $this->frontend_controller = $frontend_controller; $this->table = $table; $this->queries = $queries; $this->context = $context; } /** * Provides an Ajax controller object. * * @return AjaxControllerInterface */ public function get_ajax_controller(): AjaxControllerInterface { return $this->ajax_controller; } /** * Provides a Frontend object. * * @return FrontendControllerInterface */ public function get_frontend_controller(): FrontendControllerInterface { return $this->frontend_controller; } /** * Provides a Table object. * * @return TableInterface */ public function table(): TableInterface { return $this->table; } /** * Provides a Queries object. * * @return QueriesInterface */ public function queries(): QueriesInterface { // Defines the interval for deletion and returns Queries object. return $this->deletion_interval( 'rocket_pf_cleanup_interval' ); } /** * Provides a Context object. * * @return ContextInterface */ public function get_context(): ContextInterface { return $this->context; } } Engine/Media/PreloadFonts/Context/Context.php 0000644 00000003634 15174677547 0015206 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreloadFonts\Context; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\Context\ContextInterface; class Context implements ContextInterface { /** * Instance of the Option_Data class. * * @var Options_Data */ private $options; /** * List of allowed extensions. * * @var string[] */ private $extensions = [ 'woff2', 'woff', 'ttf', ]; /** * Constructor. * * @param Options_Data $options Instance of the Option_Data class. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Determine if the action is allowed. * * @param array $data Data to pass to the context. * @return bool */ public function is_allowed( array $data = [] ): bool { if ( $this->options->get( 'wp_rocket_no_licence' ) ) { return false; } return (bool) $this->options->get( 'auto_preload_fonts', 0 ); } /** * Determines if the page is mobile and separate cache for mobile files is enabled. * * @return bool */ public function is_mobile_allowed(): bool { return $this->options->get( 'cache_mobile', 0 ) && $this->options->get( 'do_caching_mobile_files', 0 ) && wp_is_mobile(); } /** * Get array of fonts to be excluded. * * @return array */ public function get_exclusions(): array { /** * Filters excluded fonts. * @param string[] $exclusions Array of fonts to exclude. */ return wpm_apply_filters_typed( 'string[]', 'rocket_preload_fonts_excluded_fonts', [] ); } /** * Get filtered allowed list of extensions. * * @return string[] */ public function get_extensions(): array { /** * Filters the list of processed font extensions. * * @param string[] $processed_extensions Array of processed font extensions. */ return wpm_apply_filters_typed( 'string[]', 'rocket_preload_fonts_processed_extensions', $this->extensions ); } } Engine/Media/PreconnectExternalDomains/ServiceProvider.php 0000644 00000007203 15174677547 0017763 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Context\Context; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Queries\PreconnectExternalDomains as Query; use WP_Rocket\Engine\Media\PreconnectExternalDomains\AJAX\Controller as AJAXController; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Table\PreconnectExternalDomains as PreconnectTable; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Frontend\{Controller as FrontController, Subscriber as FrontendSubscriber}; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Admin\{ Settings as AdminSettings, Subscriber as AdminSubscriber }; class ServiceProvider extends AbstractServiceProvider { /** * The provides array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ 'preconnect_external_domains_admin_settings', 'preconnect_external_domains_admin_subscriber', 'preconnect_external_domains_query', 'preconnect_external_domains_context', 'preconnect_external_domains_ajax_controller', 'preconnect_frontend_subscriber', 'preconnect_external_domains_table', 'preconnect_external_domains_factory', 'preconnect_external_domains_controller', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the classes in the container * * @return void */ public function register(): void { $this->getContainer()->add( 'preconnect_external_domains_query', Query::class ); $this->getContainer()->addShared( 'preconnect_external_domains_table', PreconnectTable::class ); $this->getContainer()->get( 'preconnect_external_domains_table' ); $this->getContainer()->add( 'preconnect_external_domains_context', Context::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'preconnect_external_domains_ajax_controller', AJAXController::class ) ->addArguments( [ 'preconnect_external_domains_query', 'preconnect_external_domains_context', ] ); $this->getContainer()->add( 'preconnect_external_domains_controller', FrontController::class ) ->addArguments( [ 'preconnect_external_domains_query', 'preconnect_external_domains_context', ] ); $this->getContainer()->addShared( 'preconnect_frontend_subscriber', FrontendSubscriber::class ) ->addArguments( [ 'preconnect_external_domains_controller', 'dynamic_lists_defaultlists_data_manager', ] ); $this->getContainer()->addShared( 'preconnect_external_domains_factory', Factory::class ) ->addArguments( [ 'preconnect_external_domains_query', 'preconnect_external_domains_context', 'preconnect_external_domains_ajax_controller', 'preconnect_external_domains_table', 'preconnect_external_domains_controller', ] ); $this->getContainer()->add( 'preconnect_external_domains_admin_settings', AdminSettings::class ) ->addArguments( [ 'preconnect_external_domains_table', 'options', 'options_api', ] ); $this->getContainer()->addShared( 'preconnect_external_domains_admin_subscriber', AdminSubscriber::class ) ->addArgument( 'preconnect_external_domains_admin_settings' ); } } Engine/Media/PreconnectExternalDomains/AJAX/Controller.php 0000644 00000011151 15174677547 0017513 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\AJAX; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\AJAXControllerTrait; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\ControllerInterface; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Queries\PreconnectExternalDomains as PreconnectQuery; class Controller implements ControllerInterface { use AJAXControllerTrait; /** * Preconnect external domain instance * * @var PreconnectQuery */ private $query; /** * PreconnectExternalDomains Context. * * @var ContextInterface */ protected $context; /** * Constructor * * @param PreconnectQuery $query Preconnect External Domains Query instance. * @param ContextInterface $context Context interface. */ public function __construct( PreconnectQuery $query, ContextInterface $context ) { $this->query = $query; $this->context = $context; } /** * Add Preconnect external domains data to the database * * @return array */ public function add_data(): array { check_ajax_referer( 'rocket_beacon', 'rocket_beacon_nonce' ); $payload = []; if ( ! $this->context->is_allowed() ) { $payload['preconnect_external_domains'] = 'not allowed'; return $payload; } $url = isset( $_POST['url'] ) ? untrailingslashit( esc_url_raw( wp_unslash( $_POST['url'] ) ) ) : ''; $is_mobile = isset( $_POST['is_mobile'] ) ? filter_var( wp_unslash( $_POST['is_mobile'] ), FILTER_VALIDATE_BOOLEAN ) : false; $results = isset( $_POST['results'] ) ? json_decode( wp_unslash( $_POST['results'] ) ) : (object) [ 'preconnect_external_domain' => [] ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $domains = $results->preconnect_external_domain ?? []; $preconnect_domains = []; /** * Filters the maximum number of preconnect external domains being saved into the database. * * @param int $max_number Maximum number to allow. * @param string $url Current page url. * @param string[]|array $hashes Current list of preconnect external domains. */ $max_preconnect_domains_number = wpm_apply_filters_typed( 'integer', 'rocket_preconnect_external_domains_number', 20, $url, $domains ); if ( 0 >= $max_preconnect_domains_number ) { $max_preconnect_domains_number = 1; } foreach ( (array) $domains as $index => $domain ) { $preconnect_domains[ $index ] = sanitize_url( wp_unslash( $domain ) ); --$max_preconnect_domains_number; } $row = $this->query->get_row( $url, $is_mobile ); if ( ! empty( $row ) ) { $payload['preconnect_external_domains'] = 'item already in the database'; return $payload; } $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : ''; list( $status_code, $status_message ) = $this->get_status_code_message( $status ); $item = [ 'url' => $url, 'is_mobile' => $is_mobile, 'status' => $status_code, 'error_message' => $status_message, 'domains' => wp_json_encode( $preconnect_domains ), 'created_at' => current_time( 'mysql', true ), 'last_accessed' => current_time( 'mysql', true ), ]; $result = $this->query->add_item( $item ); if ( ! $result ) { $payload['preconnect_external_domains'] = 'error when adding the entry to the database'; return $payload; } $payload['preconnect_external_domains'] = $item; return $payload; } /** * Checks if there is existing data for the current URL and device type from the beacon script. * * This method is called via AJAX. It checks if there is existing preconnect domains data for the current URL and device type. * If the data exists, it returns a JSON success response with true. If the data does not exist, it returns a JSON success response with false. * If the context is not allowed, it returns a JSON error response with false. * * @return array */ public function check_data(): array { $payload = [ 'preconnect_external_domain' => false, ]; check_ajax_referer( 'rocket_beacon', 'rocket_beacon_nonce' ); if ( ! $this->context->is_allowed() ) { $payload['preconnect_external_domain'] = true; return $payload; } $is_mobile = isset( $_POST['is_mobile'] ) ? filter_var( wp_unslash( $_POST['is_mobile'] ), FILTER_VALIDATE_BOOLEAN ) : false; $url = isset( $_POST['url'] ) ? untrailingslashit( esc_url_raw( wp_unslash( $_POST['url'] ) ) ) : ''; $row = $this->query->get_row( $url, $is_mobile ); if ( ! empty( $row ) ) { $payload['preconnect_external_domain'] = true; } return $payload; } } Engine/Media/PreconnectExternalDomains/Database/Queries/PreconnectExternalDomains.php 0000644 00000005015 15174677547 0025106 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Queries; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\AbstractQueries; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\QueriesInterface; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Schema\PreconnectExternalDomains as PreconnectExternalDomainsSchema; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Row\PreconnectExternalDomains as PreconnectExternalDomainsRow; class PreconnectExternalDomains extends AbstractQueries implements QueriesInterface { /** * Name of the database table to query. * * @var string */ protected $table_name = 'wpr_preconnect_external_domains'; /** * String used to alias the database table in MySQL statement. * * Keep this short, but descriptive. I.E. "tr" for term relationships. * * This is used to avoid collisions with JOINs. * * @var string */ protected $table_alias = 'wpr_pre'; /** * Name of class used to setup the database schema. * * @var string */ protected $table_schema = PreconnectExternalDomainsSchema::class; /** Item ******************************************************************/ /** * Name for a single item. * * Use underscores between words. I.E. "term_relationship" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name = 'preconnect_external_domains'; /** * Plural version for a group of items. * * Use underscores between words. I.E. "term_relationships" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name_plural = 'preconnect_external_domains'; /** * Name of class used to turn IDs into first-class objects. * * This is used when looping through return values to guarantee their shape. * * @var mixed */ protected $item_shape = PreconnectExternalDomainsRow::class; /** * Delete all rows which were not accessed in the last month. * * @return bool|int */ public function delete_old_rows() { // Get the database interface. $db = $this->get_db(); // Early bailout if no database interface is available. if ( ! $db ) { return false; } $delete_interval = $this->cleanup_interval; $prefixed_table_name = $db->prefix . $this->table_name; $query = "DELETE FROM `$prefixed_table_name` WHERE status = 'failed' OR `last_accessed` <= date_sub(now(), interval $delete_interval month)"; return $db->query( $query ); } } Engine/Media/PreconnectExternalDomains/Database/Row/PreconnectExternalDomains.php 0000644 00000003752 15174677547 0024246 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Row; use WP_Rocket\Dependencies\BerlinDB\Database\Row; class PreconnectExternalDomains extends Row { /** * Row ID * * @var int */ public $id; /** * URL * * @var string */ public $url; /** * Is for mobile * * @var bool */ public $is_mobile; /** * Domains * * @var string */ public $domains; /** * Error message * * @var string */ public $error_message; /** * Status * * @var string */ public $status; /** * Last modified time * * @var int */ public $modified; /** * Last accessed time * * @var int */ public $last_accessed; /** * Created time * * @var int */ public $created_at; /** * Constructor. * * @param mixed $item Object Row. */ public function __construct( $item ) { parent::__construct( $item ); // Set the type of each column, and prepare. $this->id = (int) $this->id; $this->url = (string) $this->url; $this->is_mobile = (bool) $this->is_mobile; $this->domains = (string) $this->domains; $this->status = (string) $this->status; $this->error_message = (string) $this->error_message; $this->modified = empty( $this->modified ) ? 0 : strtotime( (string) $this->modified ); $this->last_accessed = empty( $this->last_accessed ) ? 0 : strtotime( (string) $this->last_accessed ); $this->created_at = empty( $this->created_at ) ? 0 : strtotime( (string) $this->created_at ); } /** * Checks if the object has a valid Preconnect external domains value. * * @return bool Returns true if the object's status is 'completed' and the domains value is not empty or '[]', false otherwise. */ public function has_preconnect_external_domains() { if ( 'completed' !== $this->status ) { return false; } if ( empty( $this->domains ) ) { return false; } if ( '[]' === $this->domains ) { return false; } return true; } } Engine/Media/PreconnectExternalDomains/Database/Schema/PreconnectExternalDomains.php 0000644 00000004210 15174677547 0024665 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Schema; use WP_Rocket\Dependencies\BerlinDB\Database\Schema; class PreconnectExternalDomains extends Schema { /** * Array of database column objects * * @var array */ public $columns = [ // ID column. [ 'name' => 'id', 'type' => 'bigint', 'length' => '20', 'unsigned' => true, 'extra' => 'auto_increment', 'primary' => true, 'sortable' => true, ], // URL column. [ 'name' => 'url', 'type' => 'varchar', 'length' => '2000', 'default' => '', 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // IS_MOBILE column. [ 'name' => 'is_mobile', 'type' => 'tinyint', 'length' => '1', 'default' => 0, 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // Below the fold column. [ 'name' => 'domains', 'type' => 'longtext', 'default' => '', 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // error_message column. [ 'name' => 'error_message', 'type' => 'longtext', 'default' => null, 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // STATUS column. [ 'name' => 'status', 'type' => 'varchar', 'length' => '255', 'default' => null, 'cache_key' => true, 'searchable' => true, 'sortable' => false, ], // MODIFIED column. [ 'name' => 'modified', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], // LAST_ACCESSED column. [ 'name' => 'last_accessed', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], // CREATED_AT column. [ 'name' => 'created_at', 'type' => 'timestamp', 'default' => null, 'created' => true, 'date_query' => true, 'sortable' => true, ], ]; } Engine/Media/PreconnectExternalDomains/Database/Table/PreconnectExternalDomains.php 0000644 00000002723 15174677547 0024523 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Table; use WP_Rocket\Engine\Common\PerformanceHints\Database\Table\AbstractTable; class PreconnectExternalDomains extends AbstractTable { /** * Table name * * @var string */ protected $name = 'wpr_preconnect_external_domains'; /** * Database version key (saved in _options or _sitemeta) * * @var string */ protected $db_version_key = 'wpr_preconnect_external_domains_version'; /** * Database version * * @var int */ protected $version = 20250217; /** * Key => value array of versions => methods. * * @var array */ protected $upgrades = []; /** * Table schema data. * * @var string */ protected $schema_data = " id bigint(20) unsigned NOT NULL AUTO_INCREMENT, url varchar(2000) NOT NULL default '', is_mobile tinyint(1) NOT NULL default 0, domains longtext default '', error_message longtext default '', status varchar(255) NOT NULL default '', modified timestamp NOT NULL default '0000-00-00 00:00:00', last_accessed timestamp NOT NULL default '0000-00-00 00:00:00', created_at timestamp NULL, PRIMARY KEY (id), KEY url (url(150), is_mobile), KEY modified (modified), KEY last_accessed (last_accessed), INDEX `status_index` (`status`(191))"; } Engine/Media/PreconnectExternalDomains/Frontend/Subscriber.php 0000644 00000004224 15174677547 0020532 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\Frontend; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Engine\Optimization\DynamicLists\DefaultLists\DataManager; class Subscriber implements Subscriber_Interface { /** * Controller instance. * * @var Controller */ private $controller; /** * DataManager instance * * @var DataManager */ private $data_manager; /** * Constructor. * * @param Controller $controller Controller instance. * @param DataManager $data_manager DataManager instance. */ public function __construct( Controller $controller, DataManager $data_manager ) { $this->controller = $controller; $this->data_manager = $data_manager; } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_head_items' => [ 'preconnect_domains', 10 ], 'rocket_cdn_insert_resource_hints' => 'stop_cdn_insert_resource_hints', 'preconnect_external_domain_exclusions' => 'get_exclusions', ]; } /** * Preconnect current page domains into head. * * @param array $items Head items. * @return array */ public function preconnect_domains( array $items ) { return $this->controller->add_preconnect_to_head( $items ); } /** * Stop CDN from adding resource hints into head. * * @param bool $status Current status. * @return bool */ public function stop_cdn_insert_resource_hints( $status ): bool { return $this->controller->can_cdn_insert_resource_hints( $status ); } /** Gets the exclusion patterns used to identify elements that should be excluded. * Merges any existing exclusions pattern with those from the dynamic lists. * * @param array $exclusions Array of exclusion patterns. * @return array */ public function get_exclusions( array $exclusions ): array { $lists = $this->data_manager->get_lists()->preconnect_external_domains_exclusions ?? []; /** * Merge exclusions and lists. * Handle empty arrays gracefully. */ return array_merge( $exclusions, (array) $lists ); } } Engine/Media/PreconnectExternalDomains/Frontend/Controller.php 0000644 00000011716 15174677547 0020556 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\Frontend; use WP_Rocket\Engine\Common\Head\ElementTrait; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Context\Context; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Queries\PreconnectExternalDomains as PreconnectDomains; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Row\PreconnectExternalDomains; use WP_Rocket\Engine\Support\CommentTrait; class Controller implements ControllerInterface { use CommentTrait; use ElementTrait; /** * Queries instance * * @var PreconnectDomains */ private $query; /** * Context instance. * * @var Context */ private $context; /** * Constructor * * @param PreconnectDomains $query Queries instance. * @param Context $context Context instance. */ public function __construct( PreconnectDomains $query, Context $context ) { $this->query = $query; $this->context = $context; } /** * Applies preconnect domains optimization. * * @param string $html HTML content. * @param object $row Database row. * @return string */ public function optimize( string $html, $row ): string { if ( ! $this->context->is_allowed() || ! $row->has_preconnect_external_domains() ) { return $html; } return $this->add_meta_comment( 'preconnect_external_domains', $html ); } /** * Add custom data like the List of elements to be considered for optimization. * * @param array $data Array of data passed in beacon. * * @return array */ public function add_custom_data( array $data ): array { if ( ! $this->context->is_allowed() ) { $data['status']['preconnect_external_domain'] = false; return $data; } $elements = [ 'link', 'script', 'iframe', ]; /** * Filters the array of eligible elements to be processed by the preconnect external domain beacon. * * @since 3.19 * * @param array $elements Array of elements */ $elements = wpm_apply_filters_typed( 'array', 'rocket_preconnect_external_domain_elements', $elements ); $elements = array_filter( $elements, 'is_string' ); $data['preconnect_external_domain_elements'] = $elements; /** * Filters the array of elements to be excluded from being processed by the preconnect external domain beacon. * * @since 3.19 * * @param string[] $exclusions Array of patterns used to identify elements that should be excluded. */ $exclusions = wpm_apply_filters_typed( 'string[]', 'preconnect_external_domain_exclusions', [] ); $data['preconnect_external_domain_exclusions'] = $exclusions; $data['status']['preconnect_external_domain'] = $this->context->is_allowed(); return $data; } /** * Add preconnect item into head. * * @param array $items Head items. * @return mixed */ public function add_preconnect_to_head( $items ) { if ( ! $this->context->is_allowed() ) { return $items; } $row = $this->get_current_url_row(); if ( empty( $row ) || ! $row->has_preconnect_external_domains() ) { return $items; } $domains = json_decode( $row->domains, true ); foreach ( $domains as $domain ) { $domain_item = $this->get_domain_preconnect_item( $domain ); if ( empty( $domain_item ) ) { continue; } $items[] = $domain_item; } return $items; } /** * Get current visited page row in DB. * * @return false|PreconnectExternalDomains */ private function get_current_url_row() { global $wp; $url = untrailingslashit( home_url( add_query_arg( [], $wp->request ) ) ); $is_mobile = $this->context->is_mobile_allowed(); return $this->query->get_row( $url, $is_mobile ); } /** * Get specific domain preconnect item to be added to head. * * @param string $domain Domain url. * @return array|string[] */ private function get_domain_preconnect_item( $domain ) { if ( $this->use_prefetch( $domain ) ) { // Use dns-prefetch. return $this->dns_prefetch_link( [ 'href' => esc_url( $domain ), 1 => 'data-rocket-prefetch', ] ); } // Use preconnect by default. return $this->preconnect_link( [ 'href' => esc_url( $domain ), 1 => 'crossorigin', 2 => 'data-rocket-preconnect', ] ); } /** * Check if we need to use prefetch instead of preconnect. * * @param string $domain Domain url. * @return bool */ private function use_prefetch( $domain ) { return wpm_apply_filters_typed( 'boolean', 'rocket_preconnect_external_domains_use_prefetch', false, $domain ); } /** * Check if we can let CDN inserts resource hints or not. * * @param bool $status Current status. * * @return bool */ public function can_cdn_insert_resource_hints( $status ): bool { if ( ! $status || ! $this->context->is_allowed() ) { return $status; } $row = $this->get_current_url_row(); if ( empty( $row ) || ! $row->has_preconnect_external_domains() ) { return $status; } return false; } } Engine/Media/PreconnectExternalDomains/Admin/Subscriber.php 0000644 00000003235 15174677547 0020004 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\Admin; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Preconnect External Domains admin controller * * @since 3.19 */ class Subscriber implements Subscriber_Interface { /** * Controller instance * * @var Settings */ private $settings; /** * Creates an instance of the object. * * @param Settings $settings Settings instance. */ public function __construct( Settings $settings ) { $this->settings = $settings; } /** * Returns an array of events this subscriber listens to * * @return array[] */ public static function get_subscribed_events(): array { return [ 'update_option_wp_rocket_settings' => [ 'maybe_clear_preconnect_domains', 12, 2 ], 'wp_rocket_upgrade' => [ 'maybe_clear_dns_prefetch_values', 10, 2 ], ]; } /** * Clears the preconnect domains table if relevant settings are changed. * * @param array $old_settings Old settings. * @param array $new_settings New settings. * @return void */ public function maybe_clear_preconnect_domains( array $old_settings, array $new_settings ): void { $this->settings->maybe_clear_preconnect_external_domains( $old_settings, $new_settings ); } /** * Removes old DNS prefetch values when upgrading from versions prior to 3.19. * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * * @return void */ public function maybe_clear_dns_prefetch_values( $new_version, $old_version ): void { if ( version_compare( $old_version, '3.19', '>' ) ) { return; } $this->settings->maybe_clear_dns_prefetch_values(); } } Engine/Media/PreconnectExternalDomains/Admin/Settings.php 0000644 00000005604 15174677547 0017503 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\Admin; use WP_Rocket\Engine\Media\PreconnectExternalDomains\Database\Table\PreconnectExternalDomains as PreconnectExternalDomainsTable; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Admin\Options as Options_API; class Settings { /** * PreconnectExternalDomainsTable Instance. * * @var PreconnectExternalDomainsTable */ private $table; /** * WP Rocket options instance. * * @var Options_Data */ private $options; /** * WP Rocket Options API Instance. * * @var Options_API */ private $options_api; /** * Constructor for the Settings class. * * Initializes the Settings instance with a PreconnectExternalDomainsTable object. * * @param PreconnectExternalDomainsTable $table The table instance used to manage preconnect external domains. * @param Options_Data $options Instance of the Option_Data class. * @param Options_API $options_api WP Rocket Options API instance. */ public function __construct( PreconnectExternalDomainsTable $table, Options_Data $options, Options_API $options_api ) { $this->table = $table; $this->options = $options; $this->options_api = $options_api; } /** * Clears the preconnect external domains cache if relevant settings have changed. * * This method compares the old and new settings arrays, and if changes affecting * preconnect external domains are detected, it triggers a cache clear or update. * * @param array $old_settings The previous settings values. * @param array $new_settings The new settings values. * * @return void */ public function maybe_clear_preconnect_external_domains( array $old_settings, array $new_settings ): void { $keys = [ 'minify_css', 'minify_js', 'exclude_css', 'exclude_js', 'cdn', 'cdn_cnames', 'host_fonts_locally', ]; foreach ( $keys as $key ) { if ( $this->did_setting_change( $key, $old_settings, $new_settings ) ) { $this->table->truncate_table(); break; } } } /** * Checks if the given setting's value changed. * * @param string $setting The settings's value to check in the old and new values. * @param mixed $old_value Old option value. * @param mixed $value New option value. * * @return bool */ private function did_setting_change( $setting, $old_value, $value ) { return ( array_key_exists( $setting, $old_value ) && array_key_exists( $setting, $value ) && $old_value[ $setting ] !== $value[ $setting ] ); } /** * Removes old DNS prefetch values from settings. * * @return void */ public function maybe_clear_dns_prefetch_values(): void { $options = $this->options_api->get( 'settings', [] ); if ( empty( $options['dns_prefetch'] ) ) { return; } $this->options->set( 'dns_prefetch', [] ); $this->options_api->set( 'settings', $this->options->get_options() ); } } Engine/Media/PreconnectExternalDomains/Factory.php 0000644 00000006110 15174677547 0016253 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\ControllerInterface as AjaxControllerInterface; use WP_Rocket\Engine\Common\PerformanceHints\Cron\CronTrait; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\QueriesInterface; use WP_Rocket\Engine\Common\PerformanceHints\Database\Table\TableInterface; use WP_Rocket\Engine\Common\PerformanceHints\FactoryInterface; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface as FrontendControllerInterface; class Factory implements FactoryInterface { use CronTrait; /** * Ajax Controller instance. * * @var AjaxControllerInterface */ protected $ajax_controller; /** * Frontend Controller instance. * * @var FrontendControllerInterface */ protected $frontend_controller; /** * Table instance. * * @var TableInterface */ protected $table; /** * Queries instance. * * @var QueriesInterface */ protected $queries; /** * Context instance. * * @var ContextInterface */ protected $context; /** * Instantiate the class. * * @param QueriesInterface $queries Preconnect external domains Queries instance. * @param ContextInterface $context Preconnect external domains Context instance. * @param AjaxControllerInterface $ajax_controller Preconnect external domains AJAX Controller instance. * @param TableInterface $table Preconnect external domains Table instance. * @param FrontendControllerInterface $frontend_controller Preconnect external domains Frontend Controller instance. */ public function __construct( QueriesInterface $queries, ContextInterface $context, AjaxControllerInterface $ajax_controller, TableInterface $table, FrontendControllerInterface $frontend_controller ) { $this->context = $context; $this->queries = $queries; $this->table = $table; $this->ajax_controller = $ajax_controller; $this->frontend_controller = $frontend_controller; } /** * Provides an Ajax controller object. * * @return AjaxControllerInterface */ public function get_ajax_controller(): AjaxControllerInterface { return $this->ajax_controller; } /** * Provides a Frontend controller object. * * @return FrontendControllerInterface */ public function get_frontend_controller(): FrontendControllerInterface { return $this->frontend_controller; } /** * Provides a Table interface object. * * @return TableInterface */ public function table(): TableInterface { return $this->table; } /** * Provides a Queries object. * * @return QueriesInterface */ public function queries(): QueriesInterface { // Defines the interval for deletion and returns Queries object. return $this->deletion_interval( 'rocket_preconnect_external_domains_cleanup_interval' ); } /** * Provides Context object. * * @return ContextInterface */ public function get_context(): ContextInterface { return $this->context; } } Engine/Media/PreconnectExternalDomains/Context/Context.php 0000644 00000002052 15174677547 0017715 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\PreconnectExternalDomains\Context; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Common\Context\AbstractContext; class Context extends AbstractContext implements ContextInterface { /** * Determine if the action is allowed. * * @param array $data Data to pass to the context. * @return bool */ public function is_allowed( array $data = [] ): bool { if ( $this->options->get( 'wp_rocket_no_licence', 0 ) ) { return false; } /** * Filters to manage above the fold optimization * * @param bool $allow True to allow, false otherwise. */ return wpm_apply_filters_typed( 'boolean', 'rocket_preconnect_external_domains_optimization', true ); } /** * Determines if the page is mobile and separate cache for mobile files is enabled. * * @return bool */ public function is_mobile_allowed(): bool { return $this->options->get( 'cache_mobile', 0 ) && $this->options->get( 'do_caching_mobile_files', 0 ) && wp_is_mobile(); } } Engine/Media/Fonts/Filesystem.php 0000644 00000013424 15174677547 0012751 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts; use WP_Filesystem_Direct; use WP_Rocket\Engine\Common\AbstractFileSystem; use WP_Rocket\Logger\Logger; class Filesystem extends AbstractFileSystem { /** * WP Filesystem instance * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Path to the fonts storage * * @var string */ private $path; /** * Instantiate the class * * @param WP_Filesystem_Direct $filesystem WP Filesystem instance. */ public function __construct( $filesystem = null ) { parent::__construct( is_null( $filesystem ) ? rocket_direct_filesystem() : $filesystem ); $this->path = rocket_get_constant( 'WP_ROCKET_CACHE_ROOT_PATH', '' ) . 'fonts/' . get_current_blog_id() . '/'; } /** * Hashes the url * * @param string $url URL to get the hash from. * * @return string */ private function hash_url( string $url ): string { return md5( $url ); } /** * Checks if the file exists * * @param string $file Absolute path to the file. * * @return bool */ public function exists( string $file ): bool { return $this->filesystem->exists( $file ); } /** * Writes CSS & fonts locally * * @param string $css_url The CSS url to save locally. * @param string $provider The font provider. * * @return bool */ public function write_font_css( string $css_url, string $provider ): bool { $font_provider_path = $this->get_font_provider_path( $provider ); $css_filepath = $this->get_absolute_path( $font_provider_path, 'css/' . $this->hash_to_path( $this->hash_url( $css_url ) ) . '.css' ); $fonts_basepath = $this->get_absolute_path( $font_provider_path, 'fonts' ); if ( ! rocket_mkdir_p( dirname( $css_filepath ) ) ) { return false; } $start_time = microtime( true ); $css_content = $this->get_remote_content( html_entity_decode( $css_url ) ); if ( ! $css_content ) { return false; } preg_match_all( '/url\((https:\/\/[^)]+)\)/i', $css_content, $matches ); $font_urls = $matches[1]; $local_css = $css_content; $count_fonts = 0; $download_average = 0; foreach ( $font_urls as $font_url ) { $font_path = wp_parse_url( $font_url, PHP_URL_PATH ); if ( ! $font_path ) { continue; } $local_path = $fonts_basepath . $font_path; $local_dir = dirname( $local_path ); if ( ! rocket_mkdir_p( $local_dir ) ) { continue; } if ( ! $this->filesystem->exists( $local_path ) ) { $download_start = microtime( true ); $font_content = $this->get_remote_content( $font_url ); if ( ! $font_content ) { Logger::debug( 'Font download was not successful', [ 'Host Fonts Locally' ] ); continue; } $this->write_file( $local_path, $font_content ); $download_end = microtime( true ); $download_time = $download_end - $download_start; $download_average += $download_time; ++$count_fonts; Logger::debug( "Font $font_url download duration -- $download_time", [ 'Host Fonts Locally' ] ); } $local_url = content_url( $this->get_fonts_relative_path( $font_provider_path, $font_path ) ); $local_css = str_replace( $font_url, $local_url, $local_css ); } // This filter is documented in inc/Engine/Optimization/CSSTrait.php. $local_css = wpm_apply_filters_typed( 'string', 'rocket_css_content', $local_css ); $end_time = microtime( true ); $duration = $end_time - $start_time; // Add for test purpose. Logger::debug( "Font download and optimization duration in seconds -- $duration", [ 'Host Fonts Locally' ] ); Logger::debug( "Number of fonts downloaded for $css_url -- $count_fonts", [ 'Host Fonts Locally' ] ); Logger::debug( 'Average download time per font -- ' . ( $count_fonts ? $download_average / $count_fonts : 0 ), [ 'Host Fonts Locally' ] ); return $this->write_file( $css_filepath, $local_css ); } /** * Gets the remote content of the URL * * @param string $url URL to request content for. * * @return string */ private function get_remote_content( string $url ): string { $response = wp_safe_remote_get( $url, [ 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'httpversion' => '2.0', ] ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return ''; } return wp_remote_retrieve_body( $response ); } /** * Get the absolute path for a file * * @param string $font_provider_path Font provider path. * @param string $path Path to the file. * * @return string */ private function get_absolute_path( string $font_provider_path, string $path ): string { return $this->path . $font_provider_path . $path; } /** * Get the fonts relative paths * * @param string $font_provider_path Font provider path. * @param string $path Path to the file. * * @return string */ private function get_fonts_relative_path( string $font_provider_path, string $path ): string { $base_path = $this->path . $font_provider_path . 'fonts'; $wp_content_dir = rocket_get_constant( 'WP_CONTENT_DIR', '' ); $relative_path = str_replace( $wp_content_dir, '', $base_path ); return $relative_path . $path; } /** * Get the fonts provider path * * @param string $provider The font provider. * * @return string */ private function get_font_provider_path( string $provider ): string { $provider = str_replace( '_', '-', $provider ); return $provider . '/'; } /** * Deletes the locally stored fonts for the corresponding url * * @since 3.18 * * @param string $url The url of the page to be deleted. * * @return bool */ public function delete_font_css( string $url ): bool { $dir = $this->get_absolute_path( $this->get_font_provider_path( $url ), $url ); return $this->delete_file( $dir ); } } Engine/Media/Fonts/Clean/Subscriber.php 0000644 00000003446 15174677547 0013755 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts\Clean; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Clean instance * * @var Clean */ private $clean; /** * Constructor * * @param Clean $clean Clean instance. */ public function __construct( Clean $clean ) { $this->clean = $clean; } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_after_clean_domain' => 'clean_fonts_css', 'switch_theme' => 'clean_fonts', 'rocket_domain_options_changed' => [ [ 'clean_fonts_css' ], [ 'clean_fonts' ], ], 'update_option_wp_rocket_settings' => [ [ 'clean_on_option_change', 10, 2 ], [ 'clean_on_cdn_change', 11, 2 ], ], ]; } /** * Clean fonts CSS files stored locally * * @return void */ public function clean_fonts_css() { $this->clean->clean_fonts_css(); } /** * Clean fonts files stored locally * * @return void */ public function clean_fonts() { $this->clean->clean_fonts(); } /** * Clean CSS & fonts files stored locally on option change * * @param mixed $old_value Old option value. * @param mixed $value New option value. * * @return void */ public function clean_on_option_change( $old_value, $value ) { $this->clean->clean_on_option_change( $old_value, $value ); } /** * Clean CSS & fonts files stored locally on CDN change * * @param mixed $old_value Old option value. * @param mixed $value New option value. * * @return void */ public function clean_on_cdn_change( $old_value, $value ) { $this->clean->clean_on_cdn_change( $old_value, $value ); } } Engine/Media/Fonts/Clean/Clean.php 0000644 00000005027 15174677547 0012671 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts\Clean; use WP_Rocket\Engine\Media\Fonts\Filesystem; class Clean { /** * Filesystem instance * * @var Filesystem */ private $filesystem; /** * Base path for fonts * * @var string */ private $base_path; /** * Constructor * * @param Filesystem $filesystem Filesystem instance. */ public function __construct( $filesystem ) { $this->filesystem = $filesystem; $this->base_path = rocket_get_constant( 'WP_ROCKET_CACHE_ROOT_PATH', '' ) . 'fonts/' . get_current_blog_id() . '/'; } /** * Clean fonts CSS files stored locally * * @return void */ public function clean_fonts_css() { $path = $this->base_path . 'google-fonts/css/'; $this->filesystem->delete_all_files_from_directory( $path ); } /** * Clean fonts files stored locally * * @return void */ public function clean_fonts() { $path = $this->base_path . 'google-fonts/fonts/'; $this->filesystem->delete_all_files_from_directory( $path ); } /** * Clean CSS & fonts files stored locally on option change * * @param mixed $old_value Old option value. * @param mixed $value New option value. * * @return void */ public function clean_on_option_change( $old_value, $value ) { if ( ! $this->did_setting_change( 'host_fonts_locally', $old_value, $value ) ) { return; } $this->clean_fonts_css(); /** * Fires when the option to host fonts locally is changed * * @since 3.18 */ do_action( 'rocket_host_fonts_locally_changed' ); } /** * Clean CSS & fonts files stored locally on CDN change * * @param mixed $old_value Old option value. * @param mixed $value New option value. * * @return void */ public function clean_on_cdn_change( $old_value, $value ) { if ( ! $this->did_setting_change( 'cdn', $old_value, $value ) ) { return; } if ( ! $this->did_setting_change( 'cdn_cnames', $old_value, $value ) ) { return; } $this->clean_fonts_css(); } /** * Checks if the given setting's value changed. * * @param string $setting The settings's value to check in the old and new values. * @param mixed $old_value Old option value. * @param mixed $value New option value. * * @return bool */ private function did_setting_change( $setting, $old_value, $value ) { return ( array_key_exists( $setting, $old_value ) && array_key_exists( $setting, $value ) && // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual $old_value[ $setting ] != $value[ $setting ] ); } } Engine/Media/Fonts/FontsTrait.php 0000644 00000003044 15174677547 0012717 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts; trait FontsTrait { /** * Get the list of patterns to exclude from media fonts rewrite. * * @return string[] */ protected function get_exclusions(): array { /** * Filters the list of patterns to exclude from media font rewrite. * * @since 3.18 * * @param string[] $exclusions The list of patterns to exclude from media fonts. */ return wpm_apply_filters_typed( 'string[]', 'rocket_exclude_locally_host_fonts', [] ); } /** * Checks if a font is excluded based on the provided exclusions. * * @param string $subject The string to check. * @param string[] $exclusions The list of exclusions. * * @return bool True if the URL is excluded, false otherwise. */ protected function is_excluded( string $subject, array $exclusions ): bool { // Bail out early if there are no exclusions. if ( empty( $exclusions ) ) { return false; } // Escape each exclusion pattern to prevent regex issues. $escaped_exclusions = array_map( function ( $exclusion ) { $query_string = preg_replace( '@(https?:)?(//)?fonts\.googleapis\.com/css2?\?@i', '', $exclusion ); return str_replace( [ '#', '+', '=' ], [ '\#', '\+', '\=' ], $query_string ); }, $exclusions ); // Combine all patterns into a single regex string. $exclusions_str = implode( '|', $escaped_exclusions ); // Check the URL against the combined regex pattern. return (bool) preg_match( '#(' . $exclusions_str . ')#i', $subject ); } } Engine/Media/Fonts/ServiceProvider.php 0000644 00000006130 15174677547 0013734 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Media\Fonts\Context\OptimizationContext; use WP_Rocket\Engine\Media\Fonts\Context\SaasContext; use WP_Rocket\Engine\Media\Fonts\Admin\Data; use WP_Rocket\Engine\Media\Fonts\Admin\Settings; use WP_Rocket\Engine\Media\Fonts\Admin\Subscriber as AdminSubscriber; use WP_Rocket\Engine\Media\Fonts\Clean\Clean; use WP_Rocket\Engine\Media\Fonts\Clean\Subscriber as CleanSubscriber; use WP_Rocket\Engine\Media\Fonts\Frontend\Controller as FrontendController; use WP_Rocket\Engine\Media\Fonts\Frontend\Subscriber as FrontendSubscriber; /** * Service provider for the WP Rocket Font Optimization */ class ServiceProvider extends AbstractServiceProvider { /** * The provides array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ 'media_fonts_filesystem', 'media_fonts_settings', 'media_fonts_data', 'media_fonts_admin_subscriber', 'media_fonts_optimization_context', 'media_fonts_saas_context', 'media_fonts_frontend_controller', 'media_fonts_frontend_subscriber', 'media_fonts_clean', 'media_fonts_clean_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the option array in the container * * @return void */ public function register(): void { $this->getContainer()->add( 'media_fonts_filesystem', Filesystem::class ) ->addArgument( rocket_direct_filesystem() ); $this->getContainer()->add( 'media_fonts_settings', Settings::class ); $this->getContainer()->add( 'media_fonts_data', Data::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'media_fonts_admin_subscriber', AdminSubscriber::class ) ->addArguments( [ 'media_fonts_settings', 'media_fonts_data', ] ); $this->getContainer()->add( 'media_fonts_clean', Clean::class ) ->addArgument( 'media_fonts_filesystem' ); $this->getContainer()->addShared( 'media_fonts_clean_subscriber', CleanSubscriber::class ) ->addArgument( 'media_fonts_clean' ); $this->getContainer()->add( 'media_fonts_optimization_context', OptimizationContext::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'media_fonts_saas_context', SaasContext::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'media_fonts_frontend_controller', FrontendController::class ) ->addArguments( [ 'media_fonts_optimization_context', 'media_fonts_saas_context', 'media_fonts_filesystem', ] ); $this->getContainer()->addShared( 'media_fonts_frontend_subscriber', FrontendSubscriber::class ) ->addArgument( 'media_fonts_frontend_controller' ); } } Engine/Media/Fonts/Frontend/Subscriber.php 0000644 00000004106 15174677547 0014504 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts\Frontend; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Frontend Controller instance. * * @var Controller */ private $frontend_controller; /** * Constructor. * * @param Controller $frontend_controller Frontend Controller instance. */ public function __construct( Controller $frontend_controller ) { $this->frontend_controller = $frontend_controller; } /** * Returns an array of events that this subscriber wants to listen to. * * @since 3.18 * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_buffer' => [ 'rewrite_fonts_for_optimizations', 18 ], 'rocket_disable_google_fonts_preload' => 'disable_google_fonts_preload', 'rocket_performance_hints_buffer' => 'rewrite_fonts_for_saas', 'rocket_head_items' => [ 'rewrite_fonts_in_head', 1000 ], ]; } /** * Rewrites the Google Fonts paths to local ones. * * @param string $html HTML content. * @return string */ public function rewrite_fonts_for_optimizations( $html ): string { return $this->frontend_controller->rewrite_fonts_for_optimizations( $html ); } /** * Rewrites the Google Fonts paths to local ones for SaaS. * * @param string $html HTML content. * @return string */ public function rewrite_fonts_for_saas( $html ): string { return $this->frontend_controller->rewrite_fonts_for_saas( $html ); } /** * Disables the preload of Google Fonts. * * @param bool $disable Whether to disable the preload of Google Fonts. * * @return bool */ public function disable_google_fonts_preload( $disable ): bool { return $this->frontend_controller->disable_google_fonts_preload( $disable ); } /** * Rewrite all google fonts found in head elements. * * @param array $items Head items. * @return array */ public function rewrite_fonts_in_head( $items ): array { return $this->frontend_controller->rewrite_fonts_in_head( $items ); } } Engine/Media/Fonts/Frontend/Controller.php 0000644 00000027512 15174677547 0014532 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts\Frontend; use WP_Rocket\Engine\Common\Head\ElementTrait; use WP_Rocket\Engine\Media\Fonts\Context\OptimizationContext; use WP_Rocket\Engine\Media\Fonts\Context\SaasContext; use WP_Rocket\Engine\Media\Fonts\Filesystem; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Logger\Logger; use WP_Rocket\Engine\Media\Fonts\FontsTrait; class Controller { use RegexTrait; use FontsTrait; use ElementTrait; /** * Optimization Context instance. * * @var OptimizationContext */ private $optimization_context; /** * SaaS Context instance. * * @var SaasContext */ private $saas_context; /** * Filesystem instance. * * @var Filesystem */ private $filesystem; /** * Base url. * * @var string */ private $base_url; /** * Base path. * * @var string */ private $base_path; /** * Error flag. * * @var bool */ private $error = false; /** * Constructor. * * @param OptimizationContext $optimization_context Optimization Context instance. * @param SaasContext $saas_context SaaS Context instance. * @param Filesystem $filesystem Filesystem instance. */ public function __construct( OptimizationContext $optimization_context, SaasContext $saas_context, Filesystem $filesystem ) { $this->optimization_context = $optimization_context; $this->saas_context = $saas_context; $this->base_path = rocket_get_constant( 'WP_ROCKET_CACHE_ROOT_PATH', '' ) . 'fonts/' . get_current_blog_id() . '/'; $this->base_url = rocket_get_constant( 'WP_ROCKET_CACHE_ROOT_URL', '' ) . 'fonts/' . get_current_blog_id() . '/'; $this->filesystem = $filesystem; } /** * Rewrites the Google Fonts paths to local ones. * * @param string $html HTML content. * @return string */ private function rewrite_fonts( $html ): string { // For test purposes. $start_time = microtime( true ); $html_nocomments = $this->hide_comments( $html ); $v1_fonts = $this->find( '<link(?:\s+(?:(?!href\s*=\s*)[^>])+)?(?:\s+href\s*=\s*([\'"])(?<url>(?:https?:)?\/\/fonts\.googleapis\.com\/css[^\d](?:(?!\1).)+)\1)(?:\s+[^>]*)?>', $html_nocomments ); $v2_fonts = $this->find( '<link(?:\s+(?:(?!href\s*=\s*)[^>])+)?(?:\s+href\s*=\s*([\'"])(?<url>(?:https?:)?\/\/fonts\.googleapis\.com\/css2(?:(?!\1).)+)\1)(?:\s+[^>]*)?>', $html_nocomments ); if ( ! $v1_fonts && ! $v2_fonts ) { Logger::debug( 'No Google Fonts found.', [ 'Host Fonts Locally' ] ); return $html; } $exclusions = $this->get_exclusions(); // Count fonts - for test purposes. $total_v1 = count( $v1_fonts ); $total_v2 = count( $v2_fonts ); $total_fonts = $total_v1 + $total_v2; foreach ( $v1_fonts as $font ) { if ( $this->is_excluded( $font[0], $exclusions ) ) { continue; } $html = $this->replace_font( $font, $html ); } foreach ( $v2_fonts as $font ) { if ( $this->is_excluded( $font[0], $exclusions ) ) { continue; } $html = $this->replace_font( $font, $html ); } if ( ! $this->error ) { $html = $this->remove_preconnect_and_prefetch( $html ); } // End time measurement. $end_time = microtime( true ); // Log the total execution time and number of fonts processed, with breakdown. $duration = $end_time - $start_time; Logger::debug( "Total execution time for Host Google Fonts Feature in seconds -- $duration. CSS files processed: $total_fonts | Total v1: $total_v1 | Total v2: $total_v2", [ 'Host Fonts Locally' ] ); return $html; } /** * Rewrite fonts for normal optimizations. * * @param string $html page HTML. * @return string */ public function rewrite_fonts_for_optimizations( $html ): string { if ( ! $this->optimization_context->is_allowed() ) { return $html; } return $this->rewrite_fonts( $html ); } /** * Rewrite fonts for SaaS visits optimizations. * * @param string $html page HTML. * @return string */ public function rewrite_fonts_for_saas( $html ): string { if ( ! $this->saas_context->is_allowed() ) { return $html; } return $this->rewrite_fonts( $html ); } /** * Replaces the Google Fonts URL with the local one. * * @param array $font Font data. * @param string $html HTML content. * @param string $font_provider Font provider. * * @return string */ private function replace_font( array $font, string $html, string $font_provider = 'google-fonts' ): string { $hash = md5( $font['url'] ); if ( $this->filesystem->exists( $this->get_css_path( $hash, $font_provider ) ) ) { $local = $this->get_optimized_markup( $hash, $font['url'], $font_provider ); return str_replace( $font[0], $local, $html ); } if ( ! $this->filesystem->write_font_css( $font['url'], $font_provider ) ) { $this->error = true; return $html; } $local = $this->get_optimized_markup( $hash, $font['url'], $font_provider ); return str_replace( $font[0], $local, $html ); } /** * Returns the optimized markup for Google Fonts * * @since 3.18 * * @param string $hash Font Url has. * @param string $original_url Fonts Url. * @param string $font_provider Fonts provider. * * @return string */ private function get_optimized_markup( string $hash, string $original_url, string $font_provider ): string { $font_provider_path = sprintf( '%s/', $font_provider ); $original_url = html_entity_decode( $original_url, ENT_QUOTES ); $gf_parameters = wp_parse_url( $original_url, PHP_URL_QUERY ); if ( $this->is_host_fonts_inline_css() ) { $local_css_path = $this->get_css_path( $hash, $font_provider ); $inline_css = $this->get_font_inline_css( $local_css_path, $gf_parameters ); if ( ! empty( $inline_css ) ) { return $inline_css; } } // This filter is documented in inc/classes/optimization/css/class-abstract-css-optimization.php. $url = wpm_apply_filters_typed( 'string', 'rocket_css_url', $this->base_url . $font_provider_path . 'css/' . $this->filesystem->hash_to_path( $hash ) . '.css' ); return sprintf( '<link rel="stylesheet" href="%1$s" data-wpr-hosted-gf-parameters="%2$s"/>', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet $url, $gf_parameters ); } /** * Check if we need to show local font styles as inline or not. * * @return bool */ private function is_host_fonts_inline_css() { /** * Filters to enable the inline css output. * * @since 3.18 * * @param bool $enable Tells if we are enabling or not the inline css output. */ return wpm_apply_filters_typed( 'boolean', 'rocket_host_fonts_locally_inline_css', false ); } /** * Get optimized local url. * * @param string $hash Font url hash. * @param string $font_provider Font provider. * @return string */ private function get_optimized_url( string $hash, string $font_provider ) { $font_provider_path = sprintf( '%s/', $font_provider ); // This filter is documented in inc/classes/optimization/css/class-abstract-css-optimization.php. return wpm_apply_filters_typed( 'string', 'rocket_css_url', $this->base_url . $font_provider_path . 'css/' . $this->filesystem->hash_to_path( $hash ) . '.css' ); } /** * Gets the CSS path for the font. * * @param string $hash Font hash. * @param string $font_provider Font provider. * * @return string */ private function get_css_path( string $hash, string $font_provider ): string { $font_provider_path = sprintf( '%s/', $font_provider ); return $this->base_path . $font_provider_path . 'css/' . $this->filesystem->hash_to_path( $hash ) . '.css'; } /** * Removes preconnect and prefetch links for Google Fonts from the HTML content. * * @param string $html HTML content. * * @return string Modified HTML content without preconnect and prefetch links. */ private function remove_preconnect_and_prefetch( string $html ) { /** * Filters the removal of Google preconnect/prefetch links. * * @since 3.18 * * @param bool $enable_removal Enable or disable removal of Google preconnect/prefetch links. */ $remove_links = wpm_apply_filters_typed( 'boolean', 'rocket_remove_font_pre_links', true ); if ( ! $remove_links ) { return $html; } $pattern = '/<link[^>]*\b(rel\s*=\s*[\'"](?:preconnect|dns-prefetch)[\'"]|href\s*=\s*[\'"](?:https?:)?\/\/(?:fonts\.(?:googleapis|gstatic)\.com)[\'"])[^>]*\b(rel\s*=\s*[\'"](?:preconnect|dns-prefetch)[\'"]|href\s*=\s*[\'"](?:https?:)?\/\/(?:fonts\.(?:googleapis|gstatic)\.com)[\'"])[^>]*>/i'; $html = preg_replace( $pattern, '', $html ); return $html; } /** * Disables the preload of Google Fonts. * * @param bool $disable Whether to disable the preload of Google Fonts. * * @return bool */ public function disable_google_fonts_preload( $disable ): bool { if ( ! $this->optimization_context->is_allowed() ) { return $disable; } return true; } /** * Gets the font inline css. * * @param string $local_css_path CSS file path. * @param string $gf_parameters Google Fonts parameters. * * @return string */ private function get_font_inline_css( string $local_css_path, string $gf_parameters ): string { $content = $this->filesystem->get_file_content( $local_css_path ); if ( empty( $content ) ) { return ''; } return sprintf( '<style data-wpr-hosted-gf-parameters="%1$s">%2$s</style>', $gf_parameters, $content ); } /** * Get local font url using the external one. * * @param string $font_url Font url. * @param string $font_provider Font provider. * @return string */ private function get_font_local_url( string $font_url, string $font_provider = 'google-fonts' ): string { $hash = md5( $font_url ); if ( $this->filesystem->exists( $this->get_css_path( $hash, $font_provider ) ) ) { return $this->get_optimized_url( $hash, $font_provider ); } if ( ! $this->filesystem->write_font_css( $font_url, $font_provider ) ) { $this->error = true; return ''; } return $this->get_optimized_url( $hash, $font_provider ); } /** * Is this a url of google font or not. * * @param string $url Font url to test. * @return bool */ private function is_google_font_url( string $url ): bool { return ! empty( $this->find( '(?:https?:)?\/\/fonts\.googleapis\.com\/css.+', $url ) ); } /** * Get font CSS styles as inline using font url. * * @param string $font_url Font url. * @param string $gf_parameters Google font query string. * @param string $font_provider Font provider. * * @return array|string[] */ private function get_font_styles_by_url( string $font_url, string $gf_parameters, string $font_provider = 'google-fonts' ) { $hash = md5( $font_url ); $local_css_path = $this->get_css_path( $hash, $font_provider ); $inline_css = $this->filesystem->get_file_content( $local_css_path ); if ( empty( $inline_css ) ) { return []; } return $this->style_tag( $inline_css, [ 'data-wpr-hosted-gf-parameters' => $gf_parameters, ] ); } /** * Rewrite fonts in head items. * * @param array $items Head items. * @return array */ public function rewrite_fonts_in_head( array $items ): array { if ( ! $this->optimization_context->is_allowed() ) { return $items; } $exclusions = $this->get_exclusions(); foreach ( $items as $key => &$item ) { if ( empty( $item['href'] ) || ! $this->is_google_font_url( $item['href'] ) ) { continue; } if ( $this->is_excluded( $item['href'], $exclusions ) ) { continue; } $font_url = html_entity_decode( $item['href'], ENT_QUOTES ); $gf_parameters = wp_parse_url( $font_url, PHP_URL_QUERY ); $local_font_url = $this->get_font_local_url( $item['href'] ); if ( $this->is_host_fonts_inline_css() ) { $items[] = $this->get_font_styles_by_url( $item['href'], $gf_parameters ); unset( $items[ $key ] ); continue; } $item['href'] = $local_font_url; $item['data-wpr-hosted-gf-parameters'] = $gf_parameters; } return $items; } } Engine/Media/Fonts/Admin/Subscriber.php 0000644 00000004451 15174677547 0013760 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts\Admin; use WP_Rocket\Engine\Admin\Settings\Settings; use WP_Rocket\Engine\Media\Fonts\Admin\Data; use WP_Rocket\Engine\Media\Fonts\Admin\Settings as FontsSettings; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Fonts Settings instance * * @var FontsSettings */ private $settings; /** * Fonts Data instance * * @var Data */ private $data; /** * Instantiate the class * * @param FontsSettings $settings Fonts Settings instance. * @param Data $data Fonts Data instance. */ public function __construct( FontsSettings $settings, Data $data ) { $this->settings = $settings; $this->data = $data; } /** * Returns an array of events this listens to * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_first_install_options' => [ 'add_option', 16 ], 'rocket_input_sanitize' => [ 'sanitize_option', 10, 2 ], 'admin_init' => 'schedule_data_collection', 'rocket_fonts_data_collection' => 'collect_data', 'rocket_deactivation' => 'unschedule_data_collection', ]; } /** * Add the images dimensions option to the WP Rocket options array * * @param array $options WP Rocket options array. * * @return array */ public function add_option( array $options ): array { return $this->settings->add_option( $options ); } /** * Sanitizes the option value when saving from the settings page * * @param array $input Array of sanitized values after being submitted by the form. * @param Settings $settings Settings class instance. * * @return array */ public function sanitize_option( array $input, Settings $settings ): array { return $this->settings->sanitize_option_value( $input, $settings ); } /** * Schedule data collection * * @return void */ public function schedule_data_collection() { $this->data->schedule_data_collection(); } /** * Unschedule data collection * * @return void */ public function unschedule_data_collection() { $this->data->unschedule_data_collection(); } /** * Collect data * * @return void */ public function collect_data() { $this->data->collect_data(); } } Engine/Media/Fonts/Admin/Settings.php 0000644 00000002134 15174677547 0013451 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts\Admin; use WP_Rocket\Engine\Admin\Settings\Settings as AdminSettings; class Settings { /** * Adds the host fonts locally option to WP Rocket options array * * @since 3.19 adds auto preload fonts * * @param array $options WP Rocket options array. * * @return array */ public function add_option( array $options ): array { $options['host_fonts_locally'] = 0; $options['auto_preload_fonts'] = 0; return $options; } /** * Sanitizes the option value when saving from the settings page * * @since 3.19 adds auto preload fonts * * @param array $input Array of sanitized values after being submitted by the form. * @param AdminSettings $settings Settings class instance. * * @return array */ public function sanitize_option_value( array $input, AdminSettings $settings ): array { $input['host_fonts_locally'] = $settings->sanitize_checkbox( $input, 'host_fonts_locally' ); $input['auto_preload_fonts'] = $settings->sanitize_checkbox( $input, 'auto_preload_fonts' ); return $input; } } Engine/Media/Fonts/Admin/Data.php 0000644 00000004724 15174677547 0012531 0 ustar 00 <?php declare( strict_types=1 ); namespace WP_Rocket\Engine\Media\Fonts\Admin; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\Queue\AbstractASQueue; use Exception; class Data extends AbstractASQueue { /** * Options data instance. * * @var Options_Data */ private $options; /** * Base path. * * @var string */ private $base_path; /** * Constructor. * * @param Options_Data $options Options data instance. */ public function __construct( Options_Data $options ) { $this->options = $options; $this->base_path = rocket_get_constant( 'WP_ROCKET_CACHE_ROOT_PATH', '' ) . 'fonts/' . get_current_blog_id() . '/'; } /** * Schedule data collection. * * @return void */ public function schedule_data_collection() { if ( ! $this->is_enabled() ) { return; } $this->schedule_recurring( time(), WEEK_IN_SECONDS, 'rocket_fonts_data_collection' ); } /** * Unschedule data collection. * * @return void */ public function unschedule_data_collection() { $this->cancel( 'rocket_fonts_data_collection' ); } /** * Collect data. * * @return void */ public function collect_data() { if ( ! $this->is_enabled() ) { return; } $fonts_data = get_transient( 'rocket_fonts_data_collection' ); // If data has been populated, bail out early. if ( false !== $fonts_data ) { return; } try { $fonts = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $this->base_path . 'google-fonts/fonts/' ) ); } catch ( Exception $exception ) { return; } $allowed_extensions = [ 'woff', 'woff2', 'ttf', 'otf', ]; $total_font_count = 0; $total_font_size = 0; foreach ( $fonts as $file ) { // check file is not a directory. if ( $file->isDir() ) { continue; } $extension = strtolower( pathinfo( $file->getFilename(), PATHINFO_EXTENSION ) ); if ( in_array( $extension, $allowed_extensions, true ) ) { ++$total_font_count; $total_font_size += $file->getSize(); } } set_transient( 'rocket_fonts_data_collection', [ 'fonts_total_number' => $total_font_count, 'fonts_total_size' => size_format( $total_font_size ), ], WEEK_IN_SECONDS ); } /** * Check if the feature & analytics are enabled. * * @return bool */ private function is_enabled(): bool { return $this->options->get( 'host_fonts_locally', 0 ) && $this->options->get( 'analytics_enabled', 0 ); } } Engine/Media/Fonts/Context/SaasContext.php 0000644 00000000730 15174677547 0014501 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts\Context; use WP_Rocket\Engine\Common\Context\AbstractContext; class SaasContext extends AbstractContext { /** * Checks if the feature is allowed. * * @param array $data Optional. Data to check against. * * @return bool */ public function is_allowed( array $data = [] ): bool { $checks = [ 'option' => 'host_fonts_locally', ]; return $this->run_common_checks( $checks ); } } Engine/Media/Fonts/Context/OptimizationContext.php 0000644 00000001047 15174677547 0016302 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Fonts\Context; use WP_Rocket\Engine\Common\Context\AbstractContext; class OptimizationContext extends AbstractContext { /** * Checks if the feature is allowed. * * @param array $data Optional. Data to check against. * * @return bool */ public function is_allowed( array $data = [] ): bool { $checks = [ 'option' => 'host_fonts_locally', 'do_not_optimize' => false, 'bypass' => false, ]; return $this->run_common_checks( $checks ); } } Engine/Media/ImageDimensions/Subscriber.php 0000644 00000003252 15174677547 0014710 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\ImageDimensions; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Images Subscriber * * @since 3.8 */ class Subscriber implements Subscriber_Interface { /** * Images dimensions instance * * @var ImageDimensions */ private $dimensions; /** * Subscriber constructor. * * @param ImageDimensions $dimensions Images dimensions class that handles all business logic. */ public function __construct( ImageDimensions $dimensions ) { $this->dimensions = $dimensions; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_buffer' => [ 'specify_image_dimensions', 17 ], 'rocket_performance_hints_buffer' => 'image_dimensions_query_string', ]; } /** * Update images that have no width/height with real dimensions. * * @param string $buffer Page HTML content. * * @return string Page HTML content after update. */ public function specify_image_dimensions( $buffer ) { if ( rocket_bypass() ) { return $buffer; } return $this->dimensions->specify_image_dimensions( $buffer ); } /** * Add image dimensions if the query string is in the URL. * * @param string $buffer Page HTML content. * * @return string */ public function image_dimensions_query_string( $buffer ): string { if ( empty( $_GET['wpr_imagedimensions'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return $buffer; } add_filter( 'rocket_specify_image_dimensions', '__return_true' ); return $this->dimensions->specify_image_dimensions( $buffer ); } } Engine/Media/ImageDimensions/ImageDimensions.php 0000644 00000035457 15174677547 0015674 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\ImageDimensions; use SplFileInfo; use WP_Filesystem_Direct; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Admin\Settings\Settings; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Engine\Support\CommentTrait; use WP_Rocket\Logger\Logger; class ImageDimensions { use RegexTrait; use CommentTrait; /** * Options_Data instance * * @var Options_Data */ private $options; /** * Filesystem instance * * @var WP_Filesystem_Direct */ private $filesystem; /** * Frontend constructor. * * @param Options_Data $options Options_Data instance. * @param WP_Filesystem_Direct $filesystem Filesystem instance. */ public function __construct( Options_Data $options, $filesystem = null ) { $this->options = $options; if ( null === $filesystem ) { $filesystem = rocket_direct_filesystem(); } $this->filesystem = $filesystem; } /** * Adds the images dimensions option to WP Rocket options array * * @since 3.8 * * @param array $options WP Rocket options array. * @return array */ public function add_option( array $options ): array { $options['image_dimensions'] = 0; return $options; } /** * Sanitizes the option value when saving from the settings page * * @since 3.8 * * @param array $input Array of sanitized values after being submitted by the form. * @param Settings $settings Settings class instance. * @return array */ public function sanitize_option_value( array $input, Settings $settings ): array { $input['image_dimensions'] = $settings->sanitize_checkbox( $input, 'image_dimensions' ); return $input; } /** * Specify image dimensions and insert it into images. * * @param string $html Buffer Page HTML contents. * * @return string Buffer Page HTML contents after inserting dimensions into images. */ public function specify_image_dimensions( $html ) { Logger::debug( 'Start Specify Image Dimensions.' ); if ( ! $this->can_specify_dimensions_images() ) { Logger::debug( 'Specify Image Dimensions failed because option is not enabled from admin or by filter (rocket_specify_image_dimensions).' ); return $html; } // Get all images without width and height attributes. $images_regex = '<img(?:[^>](?!height=[\'\"](?:\S+)[\'\"]))*+>|<img(?:[^>](?!width=[\'\"](?:\S+)[\'\"]))*+>'; /** * Filters Specify image dimensions inside picture tags also. * * @since 3.8 * * @param bool $skip_pictures Do or not. Default is True, so it will skip all img tags that are inside picture tag. */ if ( apply_filters( 'rocket_specify_dimension_skip_pictures', true ) ) { $images_regex = '<\s*picture[^>]*>.*<\s*\/\s*picture\s*>(*SKIP)(*FAIL)|' . $images_regex; } $clean_html = $this->hide_scripts( $html ); $clean_html = $this->hide_noscripts( $clean_html ); preg_match_all( "/{$images_regex}/Uis", $clean_html, $images_match ); if ( empty( $images_match ) ) { Logger::debug( 'Specify Image Dimensions failed because there is no image without dimensions on this page.' ); return $html; } $replaces = []; /** * Filters Page images passed to specify dimensions. * * @since 3.8 * * @param array $images Page images. */ $images = apply_filters( 'rocket_specify_dimension_images', $images_match[0] ); Logger::debug( 'Specify Image Dimensions found ( ' . count( $images ) . ' ).', $images ); foreach ( $images as $image ) { $image_url = $this->can_specify_dimensions_one_image( $image ); if ( ! $image_url ) { Logger::debug( 'Specify Image Dimensions failed because it has attribute (data-lazy-original or data-no-image-dimensions) or it\'s without src.', [ 'image' => $image ] ); continue; } $sizes = $this->get_image_sizes( $image_url ); if ( ! $sizes ) { continue; } $width_height = $this->set_dimensions( $image, $sizes ); if ( ! $width_height ) { continue; } // Replace image with new attributes, we will replace all images at once after the loop for optimizations. $replaces[ $image ] = $this->assign_width_height( $image, $width_height ); } if ( empty( $replaces ) ) { return $html; } $html = str_replace( array_keys( $replaces ), $replaces, $html ); return $this->add_meta_comment( 'image_dimensions', $html ); } /** * Determines if the file is external. * * @since 3.8 * * @param string $url URL of the file. * @return bool True if external, false otherwise. */ private function is_external_file( $url ) { $file = get_rocket_parse_url( $url ); if ( ! empty( $file['query'] ) ) { return true; } if ( empty( $file['path'] ) ) { return true; } $parsed_site_url = wp_parse_url( site_url() ); if ( empty( $parsed_site_url['host'] ) ) { return true; } /** * Filters the allowed hosts for optimization * * @since 3.4 * * @param array $hosts Allowed hosts. * @param array $zones Zones to check available hosts. */ $hosts = (array) apply_filters( 'rocket_cdn_hosts', [], [ 'all' ] ); $hosts[] = $parsed_site_url['host']; $langs = get_rocket_i18n_uri(); // Get host for all langs. foreach ( $langs as $lang ) { $url_host = wp_parse_url( $lang, PHP_URL_HOST ); if ( ! isset( $url_host ) ) { continue; } $hosts[] = $url_host; } $hosts = array_unique( $hosts ); // URL has domain and domain is part of the internal domains. if ( ! empty( $file['host'] ) ) { foreach ( $hosts as $host ) { if ( false !== strpos( $file['host'], $host ) ) { return false; } } return true; } return false; } /** * Get local absolute path for image. * * @param string $url Image url. * * @return string Image absolute local path. */ private function get_local_path( $url ) { $url = $this->normalize_url( $url ); $path = rocket_url_to_path( $url ); if ( $path ) { return $path; } $relative_url = ltrim( wp_make_link_relative( $url ), '/' ); $ds = rocket_get_constant( 'DIRECTORY_SEPARATOR' ); $base_path = isset( $_SERVER['DOCUMENT_ROOT'] ) ? ( sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $ds ) : ''; return $base_path . str_replace( '/', $ds, $relative_url ); } /** * Check if we can specify dimensions for external images. * * @return bool Valid to be parsed or not. */ private function can_specify_dimensions_external_images() { /** * Enable/Disable specify image dimensions for external images. * * @since 3.8 * * @param bool $specify_dimensions_external Specify image dimensions for external images or not. */ return ini_get( 'allow_url_fopen' ) && apply_filters( 'rocket_specify_image_dimensions_for_distant', false ); } /** * Sets the width and height dimensions string * * @param string $image Image HTML element. * @param array $sizes Array of data created by getimagesize(). * * @return string|false */ private function set_dimensions( string $image, array $sizes ) { preg_match( '/<img.*\sheight=[\'\"]?(?<height>[^\'\"\s]+)[\'\"]?.*>/i', $image, $initial_height ); preg_match( '/<img.*\swidth=[\'\"]?(?<width>[^\'\"\s]+)[\'\"]?.*>/i', $image, $initial_width ); if ( empty( $initial_height['height'] ) && empty( $initial_width['width'] ) ) { return $sizes[3]; } if ( ! empty( $initial_height['height'] ) ) { if ( ! is_numeric( $initial_height['height'] ) ) { Logger::debug( 'Specify Image Dimensions failed because specified height is not numeric.', [ 'image' => $image ] ); return false; } $ratio = $initial_height['height'] / $sizes[1]; return 'width="' . (int) round( $sizes[0] * $ratio ) . '" height="' . $initial_height['height'] . '"'; } if ( ! empty( $initial_width['width'] ) ) { if ( ! is_numeric( $initial_width['width'] ) ) { Logger::debug( 'Specify Image Dimensions failed because specified width is not numeric.', [ 'image' => $image ] ); return false; } $ratio = $initial_width['width'] / $sizes[0]; return 'width="' . $initial_width['width'] . '" height="' . (int) round( $sizes[1] * $ratio ) . '"'; } return false; } /** * Assign width and height attributes to the img tag. * * @param string $image IMG tag. * @param string $width_height Width/Height attributes in ready state like [height="100" width="100"]. * * @return string IMG tag after adding attributes otherwise return the input img when error. */ private function assign_width_height( string $image, string $width_height ): string { // Remove old width and height attributes if found. $changed_image = preg_replace( '/\s(height|width)=(?:[\'"]?(?:[^\'\"\s]+)*[\'"]?)?/i', '', $image ); $changed_image = preg_replace( '/<\s*img/i', '<img ' . $width_height, $changed_image ); if ( null === $changed_image ) { return $image; } return $changed_image; } /** * Check if the image exists, internal or external image. * * @param string $image Image Url for external and Image absolute path for internal. * @param bool $external If this image is external or not. * * @return bool If image exists or not. */ private function image_exists( string $image, $external = false ): bool { if ( ! $image ) { return false; } if ( ! $external ) { return $this->filesystem->exists( $image ); } $file_headers = get_headers( $image ); if ( ! $file_headers ) { return false; } return false !== strstr( $file_headers[0], '200' ); } /** * Check if we can specify image dimensions for all images. * * @return bool Can we or not. */ private function can_specify_dimensions_images(): bool { /** * Filter images dimensions attributes process. * * @since 2.2 * * @param bool $specify_dimensions Do the job or not. */ return apply_filters( 'rocket_specify_image_dimensions', false ) || $this->options->get( 'image_dimensions', false ); } /** * Check if we can specify image dimensions for one image. * * @param string $image Full img tag. * * @return false|string false if we can't specify for this image otherwise get img src attribute. */ private function can_specify_dimensions_one_image( string $image ) { // Don't touch lazy-load file (no conflict with Photon (Jetpack)). if ( false !== strpos( $image, 'data-lazy-original' ) || false !== strpos( $image, 'data-no-image-dimensions' ) || ! preg_match( '/\s+src\s*=\s*[\'"](?<url>[^\'"]+)/i', $image, $src_match ) ) { return false; } return $src_match['url']; } /** * Get Image sizes. * * @param string $image_url Image url to get sizes for. * * @return array|false Get image sizes otherwise false. */ private function get_image_sizes( string $image_url ) { if ( $this->is_external_file( $image_url ) ) { $image_url = $this->normalize_url( $image_url ); if ( ! $this->can_specify_dimensions_external_images() ) { Logger::debug( 'Specify Image Dimensions failed because you/server disabled specifying dimensions for external images.', [ 'image_url' => $image_url ] ); return false; } if ( ! $this->image_exists( $image_url, true ) ) { Logger::debug( 'Specify Image Dimensions failed because external image not found.', [ 'image_url' => $image_url ] ); return false; } $sizes = $this->getimagesize( $image_url ); if ( ! $sizes ) { Logger::debug( 'Specify Image Dimensions failed because image is not valid.', [ 'image_url' => $image_url ] ); return false; } return $sizes; } $local_path = $this->get_local_path( $image_url ); if ( ! $this->image_exists( $local_path, false ) ) { Logger::debug( 'Specify Image Dimensions failed because internal image is not found.', [ 'image_url' => $image_url ] ); return false; } $sizes = $this->getimagesize( $local_path ); if ( ! $sizes ) { Logger::debug( 'Specify Image Dimensions failed because image is not valid.', [ 'image_url' => $image_url ] ); return false; } return $sizes; } /** * Gets image sizes for the given file * * @param string $filename File we want to retrieve information about. * * @return array|false */ private function getimagesize( string $filename ) { $file = new SplFileInfo( strtok( $filename, '?' ) ); if ( 'svg' === $file->getExtension() ) { return $this->svg_getimagesize( $filename ); } return getimagesize( $filename ); } /** * Gets image sizes for the given SVG file * * Uses the width/height attributes if present, or fallback to viewBox attribute * * @param string $filename File we want to retrieve information about. * * @return array|false */ private function svg_getimagesize( string $filename ) { $svgfile = simplexml_load_file( rawurlencode( $filename ), 'SimpleXMLElement', rocket_get_constant( 'LIBXML_NOERROR', 32 ) | rocket_get_constant( 'LIBXML_NOWARNING', 64 ) ); if ( ! $svgfile ) { return false; } $width = $this->format_svg_value( (string) $svgfile->attributes()->width ); $height = $this->format_svg_value( (string) $svgfile->attributes()->height ); $size = []; if ( ! empty( $width ) && ! empty( $height ) ) { $size[0] = $width; $size[1] = $height; $size[2] = 0; $size[3] = 'width="' . absint( $width ) . '" height="' . absint( $height ) . '"'; return $size; } $view_box = preg_split( '/[\s,]+/', (string) $svgfile->attributes()->viewBox ); if ( ! empty( $view_box ) ) { if ( ! empty( $view_box[2] ) && ! empty( $view_box[3] ) ) { $size[0] = $view_box[2]; $size[1] = $view_box[3]; $size[2] = 0; $size[3] = 'width="' . absint( $size[0] ) . '" height="' . absint( $size[1] ) . '"'; return $size; } return false; } return false; } /** * Formats the SVG width/height value in case of unusual units * * @since 3.10.8 * * @param string $value The value of the SVG width/height attribute. * * @return string */ private function format_svg_value( string $value ): string { // No unit, we can use the value directly. if ( is_numeric( $value ) ) { return $value; } if ( empty( $value ) ) { return $value; } $px_pattern = '/([0-9]+)\s*px/i'; // If pixel unit, remove the unit and return the numeric value. if ( preg_match( $px_pattern, $value ) ) { return preg_replace( $px_pattern, '$1', $value ); } // Return an empty string for other units. return ''; } /** * Normalize relative url to full url. * * @param string $url Url to be normalized. * * @return string Normalized url. */ private function normalize_url( string $url ): string { $url_host = wp_parse_url( $url, PHP_URL_HOST ); if ( empty( $url_host ) ) { $relative_url = ltrim( wp_make_link_relative( $url ), '/' ); $site_url_components = wp_parse_url( site_url( '/' ) ); return $site_url_components['scheme'] . '://' . $site_url_components['host'] . '/' . $relative_url; } return $url; } } Engine/Media/ImageDimensions/AdminSubscriber.php 0000644 00000002701 15174677547 0015657 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\ImageDimensions; use WP_Rocket\Engine\Admin\Settings\Settings; use WP_Rocket\Event_Management\Subscriber_Interface; class AdminSubscriber implements Subscriber_Interface { /** * ImageDimensions instance * * @var ImageDimensions */ private $dimensions; /** * Instantiate the class * * @param ImageDimensions $dimensions ImageDimensions instance. */ public function __construct( ImageDimensions $dimensions ) { $this->dimensions = $dimensions; } /** * Returns an array of events this listens to * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_first_install_options' => [ 'add_option', 14 ], 'rocket_input_sanitize' => [ 'sanitize_option', 10, 2 ], ]; } /** * Add the images dimensions option to the WP Rocket options array * * @param array $options WP Rocket options array. * @return array */ public function add_option( array $options ): array { return $this->dimensions->add_option( $options ); } /** * Sanitizes the option value when saving from the settings page * * @since 3.8 * * @param array $input Array of sanitized values after being submitted by the form. * @param Settings $settings Settings class instance. * @return array */ public function sanitize_option( array $input, Settings $settings ): array { return $this->dimensions->sanitize_option_value( $input, $settings ); } } Engine/Media/AboveTheFold/ServiceProvider.php 0000644 00000004405 15174677547 0015150 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Media\AboveTheFold\AJAX\Controller as AJAXController; use WP_Rocket\Engine\Media\AboveTheFold\Context\Context; use WP_Rocket\Engine\Media\AboveTheFold\Database\Tables\AboveTheFold as ATFTable; use WP_Rocket\Engine\Media\AboveTheFold\Database\Queries\AboveTheFold as ATFQuery; use WP_Rocket\Engine\Media\AboveTheFold\Frontend\{Controller as FrontController, Subscriber as FrontSubscriber}; class ServiceProvider extends AbstractServiceProvider { /** * The provides array is a way to let the container * know that a service is provided by this service * provider. Every service that is registered via * this service provider must have an alias added * to this array or it will be ignored. * * @var array */ protected $provides = [ 'atf_table', 'atf_query', 'atf_context', 'atf_controller', 'atf_subscriber', 'atf_ajax_controller', 'atf_factory', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the classes in the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'atf_table', ATFTable::class ); $this->getContainer()->add( 'atf_query', ATFQuery::class ); $this->getContainer()->add( 'atf_context', Context::class ); $this->getContainer()->get( 'atf_table' ); $this->getContainer()->add( 'atf_controller', FrontController::class ) ->addArguments( [ 'options', 'atf_query', 'atf_context', ] ); $this->getContainer()->addShared( 'atf_subscriber', FrontSubscriber::class ) ->addArgument( 'atf_controller' ); $this->getContainer()->add( 'atf_ajax_controller', AJAXController::class ) ->addArguments( [ 'atf_query', 'atf_context', ] ); $this->getContainer()->addShared( 'atf_factory', Factory::class ) ->addArguments( [ 'atf_ajax_controller', 'atf_controller', 'atf_table', 'atf_query', 'atf_context', ] ); } } Engine/Media/AboveTheFold/AJAX/Controller.php 0000644 00000023043 15174677547 0014702 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold\AJAX; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\AJAXControllerTrait; use WP_Rocket\Engine\Media\AboveTheFold\Database\Queries\AboveTheFold as ATFQuery; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Optimization\UrlTrait; use WP_Rocket\Logger\Logger; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\ControllerInterface; class Controller implements ControllerInterface { use UrlTrait; use AJAXControllerTrait; /** * ATFQuery instance * * @var ATFQuery */ private $query; /** * LCP Context. * * @var ContextInterface */ protected $context; /** * An array of unsupported atf schemes. * * @var array */ private $invalid_schemes = [ 'chrome-[^:]+://', ]; /** * Constructor * * @param ATFQuery $query ATFQuery instance. * @param ContextInterface $context Context interface. */ public function __construct( ATFQuery $query, ContextInterface $context ) { $this->query = $query; $this->context = $context; } /** * Add LCP data to the database * * @return array */ public function add_data(): array { $payload = [ 'lcp' => '', ]; check_ajax_referer( 'rocket_beacon', 'rocket_beacon_nonce' ); if ( ! $this->context->is_allowed() ) { $payload['lcp'] = 'not allowed'; return $payload; } $url = isset( $_POST['url'] ) ? untrailingslashit( esc_url_raw( wp_unslash( $_POST['url'] ) ) ) : ''; $is_mobile = isset( $_POST['is_mobile'] ) ? filter_var( wp_unslash( $_POST['is_mobile'] ), FILTER_VALIDATE_BOOLEAN ) : false; $results = isset( $_POST['results'] ) ? json_decode( wp_unslash( $_POST['results'] ) ) : (object) [ 'lcp' => [] ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $images = $results->lcp ?? []; $lcp = 'not found'; $viewport = []; /** * Filters the maximum number of ATF images being saved into the database. * * @param int $max_number Maximum number to allow. * @param string $url Current page url. * @param string[]|array $images Current list of ATF images. */ $max_atf_images_number = (int) apply_filters( 'rocket_atf_images_number', 20, $url, $images ); if ( 0 >= $max_atf_images_number ) { $max_atf_images_number = 1; } $keys = [ 'bg_set', 'src' ]; foreach ( (array) $images as $image ) { if ( empty( $image->type ) ) { continue; } $image_object = $this->create_object( $image, $keys ); if ( ! $image_object || ! $this->validate_image( $image_object ) ) { continue; } if ( isset( $image->label ) && 'lcp' === $image->label ) { $lcp = $image_object; continue; } if ( isset( $image->label ) && 'above-the-fold' === $image->label && 0 < $max_atf_images_number ) { $viewport[] = $image_object; --$max_atf_images_number; } } $row = $this->query->get_row( $url, $is_mobile ); if ( ! empty( $row ) ) { $payload['lcp'] = 'item already in the database'; return $payload; } $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : ''; list( $status_code, $status_message ) = $this->get_status_code_message( $status ); $item = [ 'url' => $url, 'is_mobile' => $is_mobile, 'status' => $status_code, 'error_message' => $status_message, 'lcp' => is_object( $lcp ) ? wp_json_encode( $lcp ) : $lcp, 'viewport' => wp_json_encode( $viewport ), 'last_accessed' => current_time( 'mysql', true ), ]; $result = $this->query->add_item( $item ); if ( ! $result ) { $payload['lcp'] = 'error when adding the entry to the database'; return $payload; } $payload['lcp'] = $item; return $payload; } /** * Creates an object with the 'type' property and the first key that exists in the image object. * * @param object $image The image object. * @param array $keys An array of keys in the order of their priority. * * @return object|null Returns an object with the 'type' property and the first key that exists in the image object. If none of the keys exist in the image object, it returns null. */ private function create_object( $image, $keys ) { $object = new \stdClass(); $object->type = $image->type ?? 'img'; switch ( $object->type ) { case 'img-srcset': // If the type is 'img-srcset', add all the required parameters to the object. if ( isset( $image->src ) && ! empty( $image->src ) && is_string( $image->src ) ) { $object->src = $this->sanitize_image_url( $image->src ); } $object->srcset = $image->srcset; $object->sizes = $image->sizes; break; case 'picture': if ( isset( $image->src ) && ! empty( $image->src ) && is_string( $image->src ) ) { $object->src = $this->sanitize_image_url( $image->src ); } $object->sources = array_map( function ( $source ) { if ( empty( $source->type ) ) { Logger::notice( 'The source type is missing in the image object.', [ 'source' => $source ] ); } return (object) wp_parse_args( $source, [ 'media' => '', 'sizes' => '', 'srcset' => '', 'type' => '', ] ); }, $image->sources ); break; default: // For other types, add the first non-empty key to the object. foreach ( $keys as $key ) { if ( isset( $image->$key ) && ! empty( $image->$key ) ) { if ( is_array( $image->$key ) ) { $sanitized_array = array_map( function ( $item ) { if ( ! empty( $item->src ) ) { $item->src = $this->sanitize_image_url( $item->src ); } return $item; }, $image->$key ); $object->$key = $sanitized_array; } else { $object->$key = $this->sanitize_image_url( $image->$key ); } break; } } break; } // If none of the keys exist in the image object, return null. if ( count( (array) $object ) <= 1 ) { return null; } // Returned objects must always have a src for front-end optimization. // Except bg-img and bg-img-set for which we use bg_set only. // To keep it simple and safe for now, we enforce src for all, pending a refactor. if ( ! isset( $object->src ) ) { $object->src = ''; } return $object; } /** * Sanitize image url before saving them into database. * * @param string $url The image url. * @return string */ private function sanitize_image_url( string $url ) { $sanitize_url = esc_url_raw( $url ); if ( $this->is_relative( $url ) && strpos( $url, '/' ) !== 0 ) { $sanitize_url = esc_url_raw( '/' . $url ); $sanitize_url = substr( $sanitize_url, 1 ); } return $sanitize_url; } /** * Checks if there is existing data for the current URL and device type from the beacon script. * * This method is called via AJAX. It checks if there is existing LCP data for the current URL and device type. * If the data exists, it returns a JSON success response with true. If the data does not exist, it returns a JSON success response with false. * If the context is not allowed, it returns a JSON error response with false. * * @return array */ public function check_data(): array { $payload = [ 'lcp' => false, ]; check_ajax_referer( 'rocket_beacon', 'rocket_beacon_nonce' ); if ( ! $this->context->is_allowed() ) { $payload['lcp'] = true; return $payload; } $url = isset( $_POST['url'] ) ? untrailingslashit( esc_url_raw( wp_unslash( $_POST['url'] ) ) ) : ''; $is_mobile = isset( $_POST['is_mobile'] ) ? filter_var( wp_unslash( $_POST['is_mobile'] ), FILTER_VALIDATE_BOOLEAN ) : false; $row = $this->query->get_row( $url, $is_mobile ); if ( ! empty( $row ) ) { $payload['lcp'] = true; return $payload; } return $payload; } /** * Validate image object. * * @param object $image_object Image full object. * @return bool */ private function validate_image( $image_object ): bool { $valid_image = ! empty( $image_object->src ) ? $this->validate_image_src( $image_object->src ?? '' ) : true; /** * Filters If the image src is a valid image or not. * * @param bool $valid_image Valid image or not. * @param string $image_src_url Image src url. * @param object $image_object Image object with full details. */ return (bool) apply_filters( 'rocket_atf_valid_image', $valid_image, $image_object->src, $image_object ); } /** * Make sure that this url is valid image without loading the image itself. * * @param string $image_src Image src url. * @return bool */ private function validate_image_src( string $image_src ): bool { if ( empty( $image_src ) ) { return false; } /** * Filters the supported schemes of LCP/ATF images. * * @param array $invalid_schemes Array of invalid schemes. */ $invalid_schemes = wpm_apply_filters_typed( 'array', 'rocket_atf_invalid_schemes', $this->invalid_schemes ); $invalid_schemes = implode( '|', $invalid_schemes ); if ( preg_match( '#^' . $invalid_schemes . '#', $image_src ) ) { return false; } // Here we get the url PATH part only to strip all query strings. $image_src_path = wp_parse_url( $image_src, PHP_URL_PATH ); if ( empty( $image_src_path ) ) { return false; } // Add svg to allowed mime types. $allowed_mime_types = get_allowed_mime_types(); $allowed_mime_types['svg'] = 'image/svg+xml'; $image_src_filetype_array = wp_check_filetype( $image_src_path, $allowed_mime_types ); return ! empty( $image_src_filetype_array['type'] ) && str_starts_with( $image_src_filetype_array['type'], 'image/' ); } } Engine/Media/AboveTheFold/Database/Queries/AboveTheFold.php 0000644 00000005130 15174677547 0017454 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold\Database\Queries; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\AbstractQueries; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\QueriesInterface; use WP_Rocket\Engine\Media\AboveTheFold\Database\Schemas\AboveTheFold as AboveTheFoldSchema; use WP_Rocket\Engine\Media\AboveTheFold\Database\Rows\AboveTheFold as AboveTheFoldRow; class AboveTheFold extends AbstractQueries implements QueriesInterface { /** * Name of the database table to query. * * @var string */ protected $table_name = 'wpr_above_the_fold'; /** * String used to alias the database table in MySQL statement. * * Keep this short, but descriptive. I.E. "tr" for term relationships. * * This is used to avoid collisions with JOINs. * * @var string */ protected $table_alias = 'wpr_atf'; /** * Name of class used to setup the database schema. * * @var string */ protected $table_schema = AboveTheFoldSchema::class; /** Item ******************************************************************/ /** * Name for a single item. * * Use underscores between words. I.E. "term_relationship" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name = 'above_the_fold'; /** * Plural version for a group of items. * * Use underscores between words. I.E. "term_relationships" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name_plural = 'above_the_fold'; /** * Name of class used to turn IDs into first-class objects. * * This is used when looping through return values to guarantee their shape. * * @var mixed */ protected $item_shape = AboveTheFoldRow::class; /** * Complete a job. * * @param string $url Url from DB row. * @param boolean $is_mobile Is mobile from DB row. * @param array $data LCP & Above the fold data. * * @return boolean|int */ /** * Delete all rows which were not accessed in the last month. * * @return bool|int */ public function delete_old_rows() { // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $delete_interval = $this->cleanup_interval; $prefixed_table_name = $db->prefix . $this->table_name; $query = "DELETE FROM `$prefixed_table_name` WHERE status = 'failed' OR `last_accessed` <= date_sub(now(), interval $delete_interval month)"; $rows_affected = $db->query( $query ); return $rows_affected; } } Engine/Media/AboveTheFold/Database/Rows/AboveTheFold.php 0000644 00000003603 15174677547 0016774 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold\Database\Rows; use WP_Rocket\Dependencies\BerlinDB\Database\Row; class AboveTheFold extends Row { /** * Row ID * * @var int */ public $id; /** * URL * * @var string */ public $url; /** * LCP * * @var string */ public $lcp; /** * Viewport * * @var string */ public $viewport; /** * Is CSS for mobile * * @var bool */ public $is_mobile; /** * Error message * * @var string */ public $error_message; /** * Status * * @var string */ public $status; /** * Last modified time * * @var int */ public $modified; /** * Last accessed time * * @var int */ public $last_accessed; /** * Constructor. * * @param mixed $item Object Row. */ public function __construct( $item ) { parent::__construct( $item ); // Set the type of each column, and prepare. $this->id = (int) $this->id; $this->url = (string) $this->url; $this->lcp = (string) $this->lcp; $this->viewport = (string) $this->viewport; $this->error_message = (string) $this->error_message; $this->is_mobile = (bool) $this->is_mobile; $this->status = (string) $this->status; $this->modified = empty( $this->modified ) ? 0 : strtotime( (string) $this->modified ); $this->last_accessed = empty( $this->last_accessed ) ? 0 : strtotime( (string) $this->last_accessed ); } /** * Checks if the object has a valid LCP (Largest Contentful Paint) value. * * @return bool Returns true if the object's status is 'completed' and the LCP value is not empty or 'not found', false otherwise. */ public function has_lcp() { if ( 'completed' !== $this->status ) { return false; } if ( empty( $this->lcp ) ) { return false; } if ( 'not found' === $this->lcp ) { return false; } return true; } } Engine/Media/AboveTheFold/Database/Schemas/AboveTheFold.php 0000644 00000004133 15174677547 0017424 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold\Database\Schemas; use WP_Rocket\Dependencies\BerlinDB\Database\Schema; class AboveTheFold extends Schema { /** * Array of database column objects * * @var array */ public $columns = [ // ID column. [ 'name' => 'id', 'type' => 'bigint', 'length' => '20', 'unsigned' => true, 'extra' => 'auto_increment', 'primary' => true, 'sortable' => true, ], // URL column. [ 'name' => 'url', 'type' => 'varchar', 'length' => '2000', 'default' => '', 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // LCP column. [ 'name' => 'lcp', 'type' => 'longtext', 'default' => '', 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // Viewport column. [ 'name' => 'viewport', 'type' => 'longtext', 'default' => '', 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // IS_MOBILE column. [ 'name' => 'is_mobile', 'type' => 'tinyint', 'length' => '1', 'default' => 0, 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], // error_message column. [ 'name' => 'error_message', 'type' => 'longtext', 'default' => null, 'cache_key' => false, 'searchable' => true, 'sortable' => true, ], // STATUS column. [ 'name' => 'status', 'type' => 'varchar', 'length' => '255', 'default' => null, 'cache_key' => true, 'searchable' => true, 'sortable' => false, ], // MODIFIED column. [ 'name' => 'modified', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], // LAST_ACCESSED column. [ 'name' => 'last_accessed', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], ]; } Engine/Media/AboveTheFold/Database/Tables/AboveTheFold.php 0000644 00000005463 15174677547 0017262 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold\Database\Tables; use WP_Rocket\Engine\Common\PerformanceHints\Database\Table\AbstractTable; class AboveTheFold extends AbstractTable { /** * Table name * * @var string */ protected $name = 'wpr_above_the_fold'; /** * Database version key (saved in _options or _sitemeta) * * @var string */ protected $db_version_key = 'wpr_above_the_fold_version'; /** * Database version * * @var int */ protected $version = 20240313; /** * Key => value array of versions => methods. * * @var array */ protected $upgrades = [ 20240313 => 'delete_old_atf_implementation', ]; /** * Table schema data. * * @var string */ protected $schema_data = " id bigint(20) unsigned NOT NULL AUTO_INCREMENT, url varchar(2000) NOT NULL default '', is_mobile tinyint(1) NOT NULL default 0, lcp longtext default '', viewport longtext default '', error_message longtext NULL default NULL, status varchar(255) NOT NULL default '', modified timestamp NOT NULL default '0000-00-00 00:00:00', last_accessed timestamp NOT NULL default '0000-00-00 00:00:00', PRIMARY KEY (id), KEY url (url(150), is_mobile), KEY modified (modified), KEY last_accessed (last_accessed), INDEX `status_index` (`status`(191))"; /** * This function is responsible for deleting old columns from the 'wpr_above_the_fold' table. * The columns to be deleted are: 'error_code', 'retries', 'job_id', 'queue_name', 'submitted_at', 'next_retry_time'. * * @return bool Returns true if all specified columns are successfully deleted or do not exist, false otherwise. */ public function delete_old_atf_implementation() { // An array of column names to be deleted. $columns_to_delete = [ 'error_code', 'retries', 'job_id', 'queue_name', 'submitted_at', 'next_retry_time' ]; // A flag to indicate the success of the operation. It is initially set to true. $is_successful = true; // Iterate over each column name in the array. foreach ( $columns_to_delete as $column ) { // Check if the column exists in the table. if ( $this->column_exists( $column ) ) { // If the column exists, attempt to delete it. $query_result = $this->get_db()->query( "ALTER TABLE `{$this->table_name}` DROP COLUMN `{$column}`" ); // If the deletion query fails, set the success flag to false. if ( false === $query_result ) { $is_successful = false; } } } // Return the success flag. If all deletion queries were successful (or the columns did not exist), this will be true. If any query failed, this will be false. return $is_successful; } } Engine/Media/AboveTheFold/Activation/ActivationFactory.php 0000644 00000001314 15174677547 0017563 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold\Activation; use WP_Rocket\Engine\Common\PerformanceHints\ActivationFactoryInterface; use WP_Rocket\Engine\Common\Context\ContextInterface; class ActivationFactory implements ActivationFactoryInterface { /** * Context instance. * * @var ContextInterface */ protected $context; /** * Instatiate the class. * * @param ContextInterface $context ATF Context instance. */ public function __construct( ContextInterface $context ) { $this->context = $context; } /** * Provides a Context object. * * @return ContextInterface */ public function get_context(): ContextInterface { return $this->context; } } Engine/Media/AboveTheFold/Frontend/Subscriber.php 0000644 00000001620 15174677547 0015713 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold\Frontend; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Controller instance * * @var Controller */ private $controller; /** * Constructor * * @param Controller $controller Controller instance. */ public function __construct( Controller $controller ) { $this->controller = $controller; } /** * Array of events to listen to * * @return array */ public static function get_subscribed_events() { return [ 'rocket_lazyload_excluded_src' => 'add_exclusions', ]; } /** * Add above the fold images to lazyload exclusions * * @param array $exclusions Array of excluded patterns. * * @return array */ public function add_exclusions( $exclusions ): array { return $this->controller->add_exclusions( $exclusions ); } } Engine/Media/AboveTheFold/Frontend/Controller.php 0000644 00000030253 15174677547 0015737 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold\Frontend; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Media\AboveTheFold\Database\Queries\AboveTheFold as ATFQuery; use WP_Rocket\Engine\Media\AboveTheFold\Context\Context; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Engine\Optimization\UrlTrait; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface; use WP_Rocket\Engine\Support\CommentTrait; class Controller implements ControllerInterface { use RegexTrait; use UrlTrait; use CommentTrait; /** * Options instance * * @var Options_Data */ private $options; /** * Queries instance * * @var ATFQuery */ private $query; /** * Context instance. * * @var Context */ private $context; /** * Constructor * * @param Options_Data $options Options instance. * @param ATFQuery $query Queries instance. * @param Context $context Context instance. */ public function __construct( Options_Data $options, ATFQuery $query, Context $context ) { $this->options = $options; $this->query = $query; $this->context = $context; } /** * Optimize the LCP image * * @param string $html HTML content. * @param object $row Database Row. * * @return string */ public function optimize( string $html, $row ): string { if ( ! $row->has_lcp() ) { return $html; } $html = $this->preload_lcp( $html, $row ); return $this->add_meta_comment( 'oci', $html ); } /** * Preload the LCP image * * @param string $html HTML content. * @param object $row Corresponding DB row. * * @return string */ private function preload_lcp( $html, $row ) { if ( rocket_bypass() ) { // Bail out if rocket_bypass() returns true. return $html; } if ( ! preg_match( '#</title\s*>#', $html, $matches ) ) { return $html; } $title = $matches[0]; $preload = $title; if ( ! $this->is_valid_data( $row->lcp ) ) { return $html; } $lcp = json_decode( $row->lcp ); $preload .= $this->preload_tag( $lcp ); $replace = preg_replace( '#' . $title . '#', $preload, $html, 1 ); if ( null === $replace ) { return $html; } $replace = $this->set_fetchpriority( $lcp, $replace ); return $replace; } /** * Builds the preload tag * * @param object $lcp LCP object. * * @return string */ private function preload_tag( $lcp ): string { $tags = $this->generate_lcp_link_tag_with_sources( $lcp ); return $tags['tags']; } /** * Alters the preload element tag (img|img-srcset) * * @param object $lcp LCP object. * @param string $html HTML content. * @return string */ private function set_fetchpriority( $lcp, string $html ): string { $allowed_types = [ 'img', 'img-srcset', 'picture', ]; if ( empty( (array) $lcp ) || empty( $lcp->type ) || ! in_array( $lcp->type, $allowed_types, true ) ) { return $html; } if ( empty( $lcp->src ) ) { return $html; } $html = $this->replace_html_comments( $html ); $url = urldecode( preg_quote( $lcp->src, '/' ) ); $pattern = '#<img(?:[^>]*?\s+)?src=["\']' . $url . '["\'](?:\s+[^>]*?)?>#'; if ( wp_http_validate_url( $lcp->src ) && ! $this->is_external_file( $lcp->src ) ) { $url = preg_quote( wp_parse_url( $lcp->src, PHP_URL_PATH ), '/' ); $pattern = '#<img(?:[^>]*?\s+)?src\s*=\s*["\'](?:https?:)?(?:\/\/(?:[^\/]+)\/?)?\/?' . $url . '["\'](?:\s+[^>]*?)?>#i'; } $html = preg_replace_callback( $pattern, function ( $matches ) { // Check if the fetchpriority attribute already exists. if ( preg_match( '/<img[^>]*\sfetchpriority(?:\s*=\s*["\'][^"\']*["\'])?[^>]*>/i', $matches[0] ) ) { // If it exists, don't modify the tag. return $matches[0]; } // If it doesn't exist, add the fetchpriority attribute. $replace = preg_replace( '/<img/', '<img fetchpriority="high"', $matches[0] ); if ( null === $replace ) { return $matches[0]; } return $replace; }, $html, 1 ); return $this->restore_html_comments( $html ); } /** * Add above the fold images to lazyload exclusions * * @param array $exclusions Array of excluded patterns. * * @return array */ public function add_exclusions( $exclusions ): array { if ( ! $this->context->is_allowed() ) { return $exclusions; } list($atf, $lcp) = [ [], [] ]; global $wp; $url = untrailingslashit( home_url( add_query_arg( [], $wp->request ) ) ); $row = $this->query->get_row( $url, $this->is_mobile() ); if ( ! $row ) { return $exclusions; } if ( $row->lcp && 'not found' !== $row->lcp && $this->is_valid_data( $row->lcp ) ) { $lcp = $this->generate_lcp_link_tag_with_sources( json_decode( $row->lcp ) ); $lcp = $lcp['sources']; $lcp = $this->get_path_for_exclusion( $lcp ); } if ( $row->viewport && 'not found' !== $row->viewport && $this->is_valid_data( $row->viewport ) ) { $atf = $this->get_atf_sources( json_decode( $row->viewport ) ); $atf = $this->get_path_for_exclusion( $atf ); } $exclusions = array_merge( $exclusions, $lcp, $atf ); // Remove lcp candidate from the atf array. $exclusions = array_unique( $exclusions ); return $exclusions; } /** * Check if lcp/viewport is valid * * @param string $data The lcp/viewport data. * * @return bool */ private function is_valid_data( string $data ): bool { return ! empty( json_decode( $data, true ) ); } /** * Get only the url path to exclude. * * @param array $exclusions Array of exclusions. * @return array */ private function get_path_for_exclusion( array $exclusions ): array { $sanitized_exclusions = []; foreach ( $exclusions as $exclusion ) { if ( empty( $exclusion ) ) { continue; } $path = wp_parse_url( $exclusion, PHP_URL_PATH ); $path = ! empty( $path ) ? ltrim( $path, '/' ) : ''; if ( empty( $path ) ) { continue; } $sanitized_exclusions[] = $path; } return $sanitized_exclusions; } /** * Generate preload link tags with sources for LCP. * * @param object $lcp LCP Object. * @return array */ private function generate_lcp_link_tag_with_sources( object $lcp ): array { $pairs = [ 'tags' => '', 'sources' => [], ]; $tag = ''; $start_tag = '<link rel="preload" data-rocket-preload as="image" '; $end_tag = ' fetchpriority="high">'; $sources = []; switch ( $lcp->type ) { case 'img': $sources[] = $lcp->src; $tag .= $start_tag . 'href="' . ( $this->is_relative( $lcp->src ) ? esc_attr( $lcp->src ) : esc_url( $lcp->src ) ) . '"' . $end_tag; break; case 'img-srcset': $sources[] = $lcp->src; $tag .= $start_tag . 'href="' . ( $this->is_relative( $lcp->src ) ? esc_attr( $lcp->src ) : esc_url( $lcp->src ) ) . '" imagesrcset="' . esc_attr( $lcp->srcset ) . '" imagesizes="' . esc_attr( $lcp->sizes ) . '"' . $end_tag; break; case 'bg-img-set': foreach ( $lcp->bg_set as $set ) { $sources[] = $set->src; } $tag .= $start_tag . 'imagesrcset="' . esc_attr( implode( ',', $sources ) ) . '"' . $end_tag; break; case 'bg-img': foreach ( $lcp->bg_set as $set ) { $sources[] = $set->src; $tag .= $start_tag . 'href="' . ( $this->is_relative( $set->src ) ? esc_attr( $set->src ) : esc_url( $set->src ) ) . '"' . $end_tag; } break; case 'picture': $result = $this->generate_source_tags( $lcp, $start_tag, $end_tag ); $sources = $result['sources']; $tag .= $result['tag']; break; } $pairs['tags'] = $tag; $pairs['sources'] = $sources; return $pairs; } /** * Get above the fold images sources. * * @param array $atfs Above the fold object. * @return array */ private function get_atf_sources( array $atfs ): array { $sources = []; foreach ( $atfs as $atf ) { switch ( $atf->type ) { case 'img': case 'img-srcset': $sources[] = $atf->src; break; case 'bg-img-set': case 'bg-img': foreach ( $atf->bg_set as $set ) { $sources[] = $set->src; } break; case 'picture': if ( ! empty( $atf->sources ) ) { foreach ( $atf->sources as $source ) { $sources[] = $source->srcset; } } $sources[] = $atf->src; break; } } return $sources; } /** * Determines if the page is mobile and separate cache for mobile files is enabled. * * @return bool */ private function is_mobile(): bool { return $this->options->get( 'cache_mobile', 0 ) && $this->options->get( 'do_caching_mobile_files', 0 ) && wp_is_mobile(); } /** * Generates the source tags for the given LCP object. * * This method is used to generate the source tags for the given LCP object. It iterates over the sources of the LCP object, * and for each source, it generates a media query and adds the source to the sources array. It also constructs a tag string * with the source and media query. If a previous max-width is found, it is used to update the media query. The method also * handles the case where a max-width is found in the source's media attribute. * * @param object $lcp The LCP object containing the sources. * @param string $start_tag The starting tag for each source. * @param string $end_tag The ending tag for each source. * * @return array An associative array containing the sources array and the tag string. */ private function generate_source_tags( $lcp, $start_tag, $end_tag ) { $prev_max_width = null; $sources = []; $tag = ''; $prev_type = null; // Iterate over the sources in the LCP object. foreach ( $lcp->sources as $i => $source ) { // If the type of the previous source is not equal to the type of the current source, break the loop. if ( ! empty( $source->type ) && $prev_type !== $source->type && null !== $prev_type ) { break; } $media = ! empty( $source->media ) ? $source->media : ''; // If a previous max-width is found, update the media query. if ( null !== $prev_max_width && false === strpos( $media, 'min-width' ) ) { $media = '(min-width: ' . ( $prev_max_width + 0.1 ) . 'px) and ' . $media; } // Add the media attribute to the media string. $media = ! empty( $media ) ? ' media="' . $media . '"' : ''; $sources[] = $source->srcset; // Get the sizes attribute of the source, if it exists. $sizes = ! empty( $source->sizes ) ? ' imagesizes="' . $source->sizes . '"' : ''; // Determine whether to use 'href' or 'imagesrcset' based on the srcset attribute. $link_attribute = ( substr_count( $source->srcset, ',' ) > 0 ) ? 'imagesrcset' : 'href'; // Append the source and media query to the tag string. $tag .= $start_tag . $link_attribute . '="' . $source->srcset . '"' . ( $media ) . $sizes . $end_tag; // If a max-width is found in the source's media attribute, update the previous max-width. if ( preg_match( '/\(max-width: (\d+(\.\d+)?)px\)/', $source->media, $matches ) ) { $prev_max_width = floatval( $matches[1] ); } $prev_type = $source->type; } // If a previous max-width is found, update the media query and add the LCP source to the sources array and the tag string. if ( null !== $prev_max_width ) { $media = ' media="(min-width: ' . ( $prev_max_width + 0.1 ) . 'px)"'; $sources[] = $lcp->src; $tag .= $start_tag . 'href="' . $lcp->src . '"' . $media . $end_tag; } // Return an associative array containing the sources array and the tag string. return [ 'sources' => $sources, 'tag' => $tag, ]; } /** * Add custom data like the comma-separated list of elements * to be considered for the lcp/above-the-fold optimization. * * @param array $data Array of data passed in beacon. * * @return array */ public function add_custom_data( array $data ): array { $elements = [ 'img', 'video', 'picture', 'p', 'main', 'div', 'li', 'svg', 'section', 'header', 'span', ]; $default_elements = $elements; /** * Filters the array of elements * * @since 3.16 * * @param array $formats Array of elements */ $elements = wpm_apply_filters_typed( 'array', 'rocket_atf_elements', $default_elements ); $elements = array_filter( $elements, 'is_string' ); $data['elements'] = implode( ', ', $elements ); $data['status']['atf'] = $this->context->is_allowed(); return $data; } } Engine/Media/AboveTheFold/Factory.php 0000644 00000005616 15174677547 0013451 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold; use WP_Rocket\Engine\Common\PerformanceHints\Cron\CronTrait; use WP_Rocket\Engine\Common\PerformanceHints\FactoryInterface; use WP_Rocket\Engine\Common\PerformanceHints\AJAX\ControllerInterface as AjaxControllerInterface; use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface as FrontendControllerInterface; use WP_Rocket\Engine\Common\PerformanceHints\Database\Table\TableInterface; use WP_Rocket\Engine\Common\PerformanceHints\Database\Queries\QueriesInterface; use WP_Rocket\Engine\Common\Context\ContextInterface; class Factory implements FactoryInterface { use CronTrait; /** * Ajax Controller instance. * * @var AjaxControllerInterface */ protected $ajax_controller; /** * Frontend Controller instance. * * @var FrontendControllerInterface */ protected $frontend_controller; /** * Table instance. * * @var TableInterface */ protected $table; /** * Queries instance. * * @var QueriesInterface */ protected $queries; /** * Context instance. * * @var ContextInterface */ protected $context; /** * Instantiate the class. * * @param AjaxControllerInterface $ajax_controller ATF AJAX Controller instance. * @param FrontendControllerInterface $frontend_controller ATF Frontend Controller instance. * @param TableInterface $table ATF Table instance. * @param QueriesInterface $queries ATF Queries instance. * @param ContextInterface $context ATF Context instance. */ public function __construct( AjaxControllerInterface $ajax_controller, FrontendControllerInterface $frontend_controller, TableInterface $table, QueriesInterface $queries, ContextInterface $context ) { $this->ajax_controller = $ajax_controller; $this->frontend_controller = $frontend_controller; $this->table = $table; $this->queries = $queries; $this->context = $context; } /** * Provides an Ajax controller object. * * @return AjaxControllerInterface */ public function get_ajax_controller(): AjaxControllerInterface { return $this->ajax_controller; } /** * Provides a Frontend object. * * @return FrontendControllerInterface */ public function get_frontend_controller(): FrontendControllerInterface { return $this->frontend_controller; } /** * Provides a Table object. * * @return TableInterface */ public function table(): TableInterface { return $this->table; } /** * Provides a Queries object. * * @return QueriesInterface */ public function queries(): QueriesInterface { // Defines the interval for deletion and returns Queries object. return $this->deletion_interval( 'rocket_atf_cleanup_interval' ); } /** * Provides a Context object. * * @return ContextInterface */ public function get_context(): ContextInterface { return $this->context; } } Engine/Media/AboveTheFold/Context/Context.php 0000644 00000001221 15174677547 0015076 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\AboveTheFold\Context; use WP_Rocket\Engine\Common\Context\ContextInterface; class Context implements ContextInterface { /** * Determine if the action is allowed. * * @param array $data Data to pass to the context. * @return bool */ public function is_allowed( array $data = [] ): bool { if ( get_option( 'wp_rocket_no_licence' ) ) { return false; } /** * Filters to manage above the fold optimization * * @param bool $allow True to allow, false otherwise. */ return wpm_apply_filters_typed( 'boolean', 'rocket_above_the_fold_optimization', true ); } } Engine/Media/Emojis/EmojisSubscriber.php 0000644 00000003477 15174677547 0014243 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Emojis; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Event subscriber to control Emoji behavior. * * @since 3.7 */ class EmojisSubscriber implements Subscriber_Interface { /** * The Options Data instance. * * @var Options_Data */ private $options; /** * EmojisSubscriber constructor. * * @param Options_Data $options An Options Data instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.7 * * @return array */ public static function get_subscribed_events() { return [ 'init' => 'disable_emoji', ]; } /** * Disable the emoji functionality to reduce then number of external HTTP requests. * * @since 3.7 Moved to new architecture. * @since 2.7 */ public function disable_emoji() { if ( ! $this->can_disable_emoji() ) { return; } remove_action( 'wp_head', 'print_emoji_detection_script', 7 ); add_filter( 'emoji_svg_url', '__return_false' ); } /** * Remove the tinymce emoji plugin. * * @since 3.8 deprecated * @since 3.7 Moved to new architecture. * @since 2.7 * * @param array $plugins Plugins loaded for TinyMCE. * * @return array */ public function disable_emoji_tinymce( array $plugins ) { _deprecated_function( 'WP_Rocket\Engine\Media\Emojis\EmojisSubscriber::disable_emoji_tinymce', '3.8' ); if ( ! $this->can_disable_emoji() ) { return $plugins; } return array_diff( $plugins, [ 'wpemoji' ] ); } /** * Check for emoji option enabled & not bypassed. * * @since 3.7 * * @return bool */ private function can_disable_emoji() { return ! rocket_bypass() && (bool) $this->options->get( 'emoji', 0 ); } } Engine/Media/Lazyload/Subscriber.php 0000644 00000031443 15174677547 0013417 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload; use WP_Rocket\Dependencies\Minify\JS; use WP_Rocket\Dependencies\RocketLazyload\Assets; use WP_Rocket\Dependencies\RocketLazyload\Image; use WP_Rocket\Dependencies\RocketLazyload\Iframe; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Engine\Support\CommentTrait; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Lazyload Subscriber * * @since 3.3 */ class Subscriber implements Subscriber_Interface { use RegexTrait; use CanLazyloadTrait; use CommentTrait; const SCRIPT_VERSION = '17.8.3'; /** * Options_Data instance * * @var Options_Data */ private $options; /** * Assets instance * * @var Assets */ private $assets; /** * Image instance * * @var Image */ private $image; /** * Iframe instance * * @var Iframe */ private $iframe; /** * Constructor * * @since 3.3 * * @param Options_Data $options Options_Data instance. * @param Assets $assets Assets instance. * @param Image $image Image instance. * @param Iframe $iframe Iframe instance. */ public function __construct( Options_Data $options, Assets $assets, Image $image, Iframe $iframe ) { $this->options = $options; $this->assets = $assets; $this->image = $image; $this->iframe = $iframe; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3 * * @return array */ public static function get_subscribed_events() { return [ 'wp_footer' => [ [ 'insert_lazyload_script', PHP_INT_MAX ], [ 'insert_youtube_thumbnail_script', PHP_INT_MAX ], ], 'wp_head' => [ 'insert_nojs_style', PHP_INT_MAX ], 'wp_enqueue_scripts' => [ 'insert_youtube_thumbnail_style', PHP_INT_MAX ], 'rocket_buffer' => [ 'lazyload', 18 ], 'rocket_lazyload_html' => 'lazyload_responsive', 'init' => 'lazyload_smilies', 'wp' => 'deactivate_lazyload_on_specific_posts', 'wp_lazy_loading_enabled' => [ 'maybe_disable_core_lazyload', 10, 2 ], 'rocket_lazyload_excluded_attributes' => 'add_exclusions', 'rocket_lazyload_excluded_src' => 'add_exclusions', 'rocket_lazyload_iframe_excluded_patterns' => 'add_exclusions', 'rocket_lazyload_exclude_youtube_thumbnail' => 'add_exclusions', ]; } /** * Inserts the lazyload script in the footer * * @since 3.3 * * @return void */ public function insert_lazyload_script() { if ( ! $this->can_lazyload_images() && ! $this->can_lazyload_iframes() ) { return; } if ( ! $this->should_lazyload() ) { return; } $script_args = [ 'base_url' => rocket_get_constant( 'WP_ROCKET_ASSETS_JS_URL' ) . 'lazyload/', 'version' => self::SCRIPT_VERSION, ]; $this->add_inline_script(); $this->assets->insertLazyloadScript( $script_args ); } /** * Adds the inline lazyload script * * @since 3.6 * * @return void */ private function add_inline_script() { $inline_script = $this->assets->getInlineLazyloadScript( $this->set_inline_script_args() ); if ( ! rocket_get_constant( 'SCRIPT_DEBUG' ) ) { $inline_script = $this->minify_script( $inline_script ); } echo '<script>' . $inline_script . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Sets the arguments array for the inline lazyload script * * @since 3.6 * * @return array */ private function set_inline_script_args() { /** * Filters the threshold at which lazyload is triggered * * @since 1.2 * * @param int $threshold Threshold value. */ $threshold = (int) apply_filters( 'rocket_lazyload_threshold', 300 ); $inline_args = [ 'threshold' => $threshold, ]; /** * Filters the use of native lazyload * * @since 3.4 * * @param bool $use_native True to use native lazyload, false otherwise. */ if ( (bool) apply_filters( 'rocket_use_native_lazyload', false ) ) { $inline_args['options']['use_native'] = true; $inline_args['elements']['loading'] = '[loading=lazy]'; } if ( $this->options->get( 'lazyload', 0 ) ) { if ( ! $this->is_native_images() ) { $inline_args['elements']['image'] = 'img[data-lazy-src]'; } $inline_args['elements']['background_image'] = '.rocket-lazyload'; } if ( (bool) $this->options->get( 'lazyload_iframes', 0 ) ) { $inline_args['elements']['iframe'] = 'iframe[data-lazy-src]'; } /** * Filters the arguments array for the lazyload script options * * @since 3.3 * * @param array $inline_args Arguments used for the lazyload script options. */ return (array) apply_filters( 'rocket_lazyload_script_args', $inline_args ); } /** * Minifies the inline script * * @since 3.6 * * @param string $script Inline script to minify. * @return string */ private function minify_script( $script ) { $minify = new JS( $script ); return $minify->minify(); } /** * Inserts the Youtube thumbnail script in the footer * * @since 3.3 * * @return void */ public function insert_youtube_thumbnail_script() { if ( ! $this->options->get( 'lazyload_youtube' ) || ! $this->can_lazyload_iframes() ) { return; } if ( ! $this->should_lazyload() ) { return; } /** * Filters the resolution of the YouTube thumbnail * * @since 1.4.8 * @deprecated 3.3 * * @param string $thumbnail_resolution The resolution of the thumbnail. Accepted values: default, mqdefault, hqdefault, sddefault, maxresdefault */ $thumbnail_resolution = apply_filters_deprecated( 'rocket_youtube_thumbnail_resolution', [ 'hqdefault' ], '3.3', 'rocket_lazyload_youtube_thumbnail_resolution' ); /** * Filters the resolution of the YouTube thumbnail * * @since 1.4.8 * * @param string $thumbnail_resolution The resolution of the thumbnail. Accepted values: default, mqdefault, hqdefault, sddefault, maxresdefault */ $thumbnail_resolution = apply_filters( 'rocket_lazyload_youtube_thumbnail_resolution', $thumbnail_resolution ); /** * Extension from the thumbnail from Youtube video. * * @param string $extension extension from the thumbnail from Youtube video. * @returns string */ $extension = wpm_apply_filters_typed( 'string', 'rocket_lazyload_youtube_thumbnail_extension', 'jpg' ); if ( ! in_array( $extension, [ 'jpg', 'webp' ], true ) ) { $extension = 'jpg'; } $this->assets->insertYoutubeThumbnailScript( [ 'resolution' => $thumbnail_resolution, 'lazy_image' => (bool) $this->options->get( 'lazyload' ), 'native' => $this->is_native_images(), 'extension' => $extension, 'button_aria_label' => esc_html__( 'Play Youtube video', 'rocket' ), ] ); } /** * Inserts the no JS CSS compatibility in the header * * @since 3.3 * * @return void */ public function insert_nojs_style() { if ( ! $this->should_lazyload() ) { return; } if ( ! $this->can_lazyload_images() && ! $this->can_lazyload_iframes() ) { return; } if ( ! $this->options->get( 'lazyload' ) && ! $this->options->get( 'lazyload_iframes' ) ) { return; } $this->assets->insertNoJSCSS(); } /** * Inserts the Youtube thumbnail CSS in the header * * @since 3.3 * * @return void */ public function insert_youtube_thumbnail_style() { if ( ! $this->options->get( 'lazyload_youtube' ) || ! $this->can_lazyload_iframes() ) { return; } if ( ! $this->should_lazyload() ) { return; } $this->assets->insertYoutubeThumbnailCSS( [ 'base_url' => WP_ROCKET_ASSETS_URL, 'responsive_embeds' => current_theme_supports( 'responsive-embeds' ), ] ); } /** * Applies lazyload on the provided content * * @since 3.3 * * @param string $html HTML content. * @return string */ public function lazyload( $html ) { if ( ! $this->should_lazyload() ) { return $html; } $buffer = $this->hide_scripts( $html ); $buffer = $this->hide_noscripts( $buffer ); if ( $this->can_lazyload_iframes() ) { $args = [ 'youtube' => $this->options->get( 'lazyload_youtube' ), ]; $html = $this->iframe->lazyloadIframes( $html, $buffer, $args ); $html = $this->add_meta_comment( 'lazyload_iframes', $html ); } if ( $this->can_lazyload_images() ) { if ( ! $this->is_native_images() ) { $html = $this->image->lazyloadPictures( $html, $buffer ); $buffer = $this->hide_scripts( $html ); $buffer = $this->hide_noscripts( $buffer ); } $html = $this->image->lazyloadImages( $html, $buffer, $this->is_native_images() ); /** * Filters the application of lazyload on background images * * @since 3.3 * * @param bool $lazyload True to apply, false otherwise. */ if ( apply_filters( 'rocket_lazyload_background_images', true ) ) { $html = $this->image->lazyloadBackgroundImages( $html, $buffer ); } $html = $this->add_meta_comment( 'lazyload_images', $html ); } return $html; } /** * Applies lazyload on responsive images attributes srcset and sizes * * @since 3.3 * * @param string $html Image HTML. * @return string */ public function lazyload_responsive( $html ) { if ( $this->is_native_images() ) { return $html; } return $this->image->lazyloadResponsiveAttributes( $html ); } /** * Applies lazyload on WordPress smilies * * @since 3.3 * * @return void */ public function lazyload_smilies() { if ( ! $this->should_lazyload() ) { return; } if ( ! $this->options->get( 'lazyload' ) ) { return; } $filters = [ 'the_content' => 10, 'the_excerpt' => 10, 'comment_text' => 20, ]; foreach ( $filters as $filter => $prio ) { if ( ! has_filter( $filter ) ) { continue; } remove_filter( $filter, 'convert_smilies', $prio ); add_filter( $filter, [ $this->image, 'convertSmilies' ], $prio ); } } /** * Prevents lazyload if the option is unchecked on the WP Rocket options metabox for a post * * @since 3.3 * * @return void */ public function deactivate_lazyload_on_specific_posts() { if ( is_rocket_post_excluded_option( 'lazyload' ) ) { add_filter( 'do_rocket_lazyload', '__return_false' ); } if ( is_rocket_post_excluded_option( 'lazyload_iframes' ) ) { add_filter( 'do_rocket_lazyload_iframes', '__return_false' ); } } /** * Disable WP core lazyload if our images lazyload is active * * @since 3.5 * * @param bool $value Current value for the enabling variable. * @param string $tag_name The tag name. * * @return bool */ public function maybe_disable_core_lazyload( $value, $tag_name ) { if ( false === $value || rocket_bypass() ) { return $value; } if ( 'img' === $tag_name ) { return ! (bool) $this->can_lazyload_images(); } if ( 'iframe' === $tag_name ) { return ! (bool) $this->can_lazyload_iframes(); } return $value; } /** * Adds the exclusions from the options to the exclusions arrays * * @since 3.8 * * @param array $exclusions Array of excluded patterns. * @return array */ public function add_exclusions( array $exclusions = [] ): array { $exclude_lazyload = $this->options->get( 'exclude_lazyload', [] ); if ( empty( $exclude_lazyload ) ) { return $exclusions; } return array_unique( array_merge( $exclusions, $exclude_lazyload ) ); } /** * Checks if we can lazyload images. * * @since 3.3 * * @return boolean */ private function can_lazyload_images() { if ( ! $this->options->get( 'lazyload', 0 ) ) { return false; } /** * Filters the lazyload application on images * * @since 2.0 * * @param bool $do_rocket_lazyload True to apply lazyload, false otherwise. */ return apply_filters( 'do_rocket_lazyload', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Checks if we can lazyload iframes * * @since 3.3 * * @return boolean */ private function can_lazyload_iframes() { if ( ! $this->options->get( 'lazyload_iframes', 0 ) ) { return false; } /** * Filters the lazyload application on iframes * * @since 2.0 * * @param bool $do_rocket_lazyload_iframes True to apply lazyload, false otherwise. */ return apply_filters( 'do_rocket_lazyload_iframes', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals } /** * Checks if native lazyload is enabled for images * * @since 3.10.2 * * @return bool */ private function is_native_images(): bool { /** * Filters the use of native lazyload for images * * @since 3.10.2 * * @param bool $use_native True to use native lazyload for images, false otherwise. */ return (bool) apply_filters( 'rocket_use_native_lazyload_images', false ); } } Engine/Media/Lazyload/CanLazyloadTrait.php 0000644 00000002070 15174677547 0014513 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Lazyload; trait CanLazyloadTrait { /** * Checks if lazyload should be applied * * @since 3.3 * * @return bool */ protected function should_lazyload() { if ( rocket_get_constant( 'REST_REQUEST', false ) || rocket_get_constant( 'DONOTLAZYLOAD', false ) || rocket_get_constant( 'DONOTROCKETOPTIMIZE', false ) || rocket_get_constant( 'DONOTCACHEPAGE', false ) ) { return false; } if ( is_admin() || is_feed() || is_preview() ) { return false; } if ( is_search() && // This filter is documented in inc/classes/Buffer/class-tests.php. ! (bool) apply_filters( 'rocket_cache_search', false ) ) { return false; } // Exclude Page Builders editors. $excluded_parameters = [ 'fl_builder', 'et_fb', 'ct_builder', ]; foreach ( $excluded_parameters as $excluded ) { if ( isset( $_GET[ $excluded ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return false; } } return true; } } Engine/Media/Lazyload/CSS/Subscriber.php 0000644 00000042702 15174677547 0014047 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS; use WP_Filesystem_Direct; use WP_Post; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\Context\ContextInterface; use WP_Rocket\Engine\Media\Lazyload\CSS\Data\LazyloadedContent; use WP_Rocket\Engine\Media\Lazyload\CSS\Data\LazyloadCSSContentFactory; use WP_Rocket\Engine\Media\Lazyload\CSS\Data\ProtectedContent; use WP_Rocket\Engine\Media\Lazyload\CSS\Front\{ContentFetcher, Extractor, FileResolver, MappingFormatter, RuleFormatter, TagGenerator}; use WP_Rocket\Engine\Common\Cache\CacheInterface; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Engine\Support\CommentTrait; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Logger\LoggerAware; use WP_Rocket\Logger\LoggerAwareInterface; class Subscriber implements Subscriber_Interface, LoggerAwareInterface { use LoggerAware; use RegexTrait; use CommentTrait; /** * Extract background images from CSS. * * @var Extractor */ protected $extractor; /** * Cache instance. * * @var CacheInterface */ protected $cache; /** * Format the CSS rule inside the CSS content. * * @var RuleFormatter */ protected $rule_formatter; /** * Resolves the name from the file from its URL. * * @var FileResolver */ protected $file_resolver; /** * Format data for the Mapping file. * * @var MappingFormatter */ protected $mapping_formatter; /** * Generate tags from the mapping of lazyloaded images. * * @var TagGenerator */ protected $tag_generator; /** * Fetch content. * * @var ContentFetcher */ protected $fetcher; /** * Context. * * @var ContextInterface */ protected $context; /** * WPR Options. * * @var Options_Data */ protected $options; /** * Make LazyloadedContent instance. * * @var LazyloadCSSContentFactory */ protected $lazyloaded_content_factory; /** * WordPress filesystem. * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Instantiate class. * * @param Extractor $extractor Extract background images from CSS. * @param RuleFormatter $rule_formatter Format the CSS rule inside the CSS content. * @param FileResolver $file_resolver Resolves the name from the file from its URL. * @param CacheInterface $cache Cache instance. * @param MappingFormatter $mapping_formatter Format data for the Mapping file. * @param TagGenerator $tag_generator Generate tags from the mapping of lazy loaded images. * @param ContentFetcher $fetcher Fetch content. * @param ContextInterface $context Context. * @param Options_Data $options WPR Options. * @param LazyloadCSSContentFactory $lazyloaded_content_factory Make LazyloadedContent instance. * @param WP_Filesystem_Direct|null $filesystem WordPress filesystem. */ public function __construct( Extractor $extractor, RuleFormatter $rule_formatter, FileResolver $file_resolver, CacheInterface $cache, MappingFormatter $mapping_formatter, TagGenerator $tag_generator, ContentFetcher $fetcher, ContextInterface $context, Options_Data $options, LazyloadCSSContentFactory $lazyloaded_content_factory, ?WP_Filesystem_Direct $filesystem = null ) { $this->extractor = $extractor; $this->cache = $cache; $this->rule_formatter = $rule_formatter; $this->file_resolver = $file_resolver; $this->mapping_formatter = $mapping_formatter; $this->tag_generator = $tag_generator; $this->context = $context; $this->options = $options; $this->fetcher = $fetcher; $this->lazyloaded_content_factory = $lazyloaded_content_factory; $this->filesystem = $filesystem ?: rocket_direct_filesystem(); } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_generate_lazyloaded_css' => [ [ 'create_lazy_css_files', 18 ], [ 'create_lazy_inline_css', 21 ], [ 'add_lazy_tag', 24 ], ], 'rocket_buffer' => [ 'maybe_replace_css_images', 1002 ], 'rocket_after_clean_domain' => 'clear_generated_css', 'wp_enqueue_scripts' => 'insert_lazyload_script', 'rocket_css_image_lazyload_images_load' => [ 'exclude_rocket_lazyload_excluded_src', 10, 2 ], 'rocket_lazyload_css_ignored_urls' => 'remove_svg_from_lazyload_css', ]; } /** * Replace CSS images by the lazyloaded version. * * @param string $html page HTML. * @return string */ public function maybe_replace_css_images( string $html ): string { if ( ! $this->context->is_allowed() ) { return $html; } $this->logger::debug( 'Starting lazyload', $this->generate_log_context( [ 'data' => $html, ] ) ); /** * Generate lazyload CSS for the page. * * @param array $data Data passed to generate the lazyload CSS. */ $output = wpm_apply_filters_typed( 'array', 'rocket_generate_lazyloaded_css', [ 'html' => $html, ] ); if ( ! key_exists( 'html', $output ) ) { $this->logger::debug( 'Lazyload bailed out', $this->generate_log_context( [ 'data' => $html, ] ) ); return $html; } $this->logger::debug( 'Ending lazyload', $this->generate_log_context( [ 'data' => $html, ] ) ); $html = $this->add_meta_comment( 'lazyload_css_bg_img', $output['html'] ); return $html; } /** * Clear the lazyload CSS files. * * @return void */ public function clear_generated_css() { $this->logger::debug( 'Clear lazy CSS', $this->generate_log_context() ); $this->cache->clear(); } /** * Insert the lazyload script. * * @return void */ public function insert_lazyload_script() { if ( ! $this->context->is_allowed() ) { return; } /** * Filters the threshold at which lazyload is triggered * * @since 1.2 * * @param int $threshold Threshold value. */ $threshold = (int) apply_filters( 'rocket_lazyload_threshold', 300 ); $script_path = rocket_get_constant( 'WP_ROCKET_ASSETS_JS_PATH' ) . 'lazyload-css.min.js'; if ( ! $this->filesystem->exists( $script_path ) ) { return; } $content = $this->filesystem->get_contents( $script_path ); if ( ! $content ) { return; } wp_register_script( 'rocket_lazyload_css', '', [], false, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion wp_enqueue_script( 'rocket_lazyload_css' ); wp_add_inline_script( 'rocket_lazyload_css', $content, 'after' ); wp_localize_script( 'rocket_lazyload_css', 'rocket_lazyload_css_data', [ 'threshold' => $threshold, ] ); } /** * Create the lazyload file for CSS files. * * @param array $data Data sent. * @return array */ public function create_lazy_css_files( array $data ): array { if ( ! key_exists( 'html', $data ) || ! key_exists( 'css_files', $data ) ) { $this->logger::debug( 'Create lazy css files bailed out', $this->generate_log_context( [ 'data' => $data, ] ) ); return $data; } $html = $data['html']; $html = $this->replace_html_comments( $html ); $mapping = []; $css_files = array_unique( $data['css_files'] ); $protected_content = $this->protect_css_urls( $html, $css_files ); $html = $protected_content->get_content(); $css_files_mapping = $protected_content->get_protected_files_mapping(); foreach ( $css_files as $url ) { if ( $this->is_excluded( $url ) ) { $this->logger::debug( "Excluded lazy css files $url", $this->generate_log_context() ); continue; } $url_key = $this->format_url( $url ); if ( ! $this->cache->has( $url_key ) ) { $this->logger::debug( "Generate lazy css files $url", $this->generate_log_context() ); $file_mapping = $this->generate_css_file( $url ); if ( empty( $file_mapping ) ) { $this->logger::debug( "Create lazy css files $url bailed out", $this->generate_log_context() ); continue; } $mapping = array_merge( $mapping, $file_mapping ); } else { $this->logger::debug( "Load lazy css files $url", $this->generate_log_context() ); $mapping = array_merge( $mapping, $this->load_existing_mapping( $url ) ); } $cached_url = $this->generate_asset_url( $url ); $html = str_replace( $css_files_mapping[ $url ], $cached_url, $html ); } foreach ( $css_files_mapping as $url => $placeholder ) { $html = str_replace( $placeholder, $url, $html ); } $html = $this->restore_html_comments( $html ); $data['html'] = $html; if ( ! key_exists( 'lazyloaded_images', $data ) ) { $data['lazyloaded_images'] = []; } $data['lazyloaded_images'] = array_merge( $data['lazyloaded_images'], $mapping ); $data['lazyloaded_images'] = array_unique( $data['lazyloaded_images'], SORT_REGULAR ); return $data; } /** * Add the lazy tag to the current HTML. * * @param array $data Data sent. * @return array */ public function add_lazy_tag( array $data ): array { if ( ! key_exists( 'html', $data ) || ! key_exists( 'lazyloaded_images', $data ) ) { $this->logger::debug( 'Add lazy tag bailed out', $this->generate_log_context( [ 'data' => $data, ] ) ); return $data; } $lazyload_images = $data['lazyloaded_images']; /** * Lazyload background CSS excluded urls. * * @param array $excluded Excluded URLs. * @param array $urls List of Urls processed. */ $loaded = apply_filters( 'rocket_css_image_lazyload_images_load', [], $lazyload_images ); $tags = $this->tag_generator->generate( $lazyload_images, $loaded ); $this->logger::debug( 'Add lazy tag generated', $this->generate_log_context( [ 'data' => $tags, ] ) ); $data['html'] = str_replace( '</head>', "$tags</head>", $data['html'] ); return $data; } /** * Generate lazy CSS for a file. * * @param string $url Url from the CSS. * @return array */ protected function generate_css_file( string $url ) { $path = $this->file_resolver->resolve( $url ); if ( ! $path ) { $path = $url; } $content = $this->fetcher->fetch( $path, $this->cache->generate_path( $url ) ); if ( ! $content ) { return []; } $output = $this->generate_content( $content, $this->cache->generate_url( $url ) ); if ( count( $output->get_urls() ) === 0 ) { return []; } if ( ! $this->cache->set( $this->format_url( $url ), $output->get_content() ) ) { return []; } $this->cache->set( $this->get_mapping_file_url( $url ), json_encode( $output->get_urls() ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode return $output->get_urls(); } /** * Generate lazy content for a certain content. * * @param string $content Content to generate lazy for. * @param string $url URL of the file we are extracting content from. * @return LazyloadedContent */ protected function generate_content( string $content, string $url = '' ): LazyloadedContent { $urls = $this->extractor->extract( $content, $url ); $formatted_urls = []; foreach ( $urls as $url_tags ) { $url_tags = $this->add_hashes( $url_tags ); $content = $this->rule_formatter->format( $content, $url_tags ); $formatted_urls = array_merge( $formatted_urls, $this->mapping_formatter->format( $url_tags ) ); } return $this->lazyloaded_content_factory->make_lazyloaded_content( $formatted_urls, $content ); } /** * Load existing mapping for a URL. * * @param string $url Url we load mapping for. * @return array */ protected function load_existing_mapping( string $url ) { $content = $this->cache->get( $this->get_mapping_file_url( $url ) ); $urls = json_decode( $content, true ); if ( ! $urls ) { return []; } return $urls; } /** * Create the lazyload file for inline CSS. * * @param array $data Data sent. * @return array */ public function create_lazy_inline_css( array $data ): array { if ( ! key_exists( 'html', $data ) || ! key_exists( 'css_inline', $data ) ) { $this->logger::debug( 'Create lazy css inline bailed out', $this->generate_log_context( [ 'data' => $data, ] ) ); return $data; } $html = $data['html']; if ( ! key_exists( 'lazyloaded_images', $data ) ) { $data['lazyloaded_images'] = []; } foreach ( $data['css_inline'] as $content ) { $output = $this->generate_content( $content ); $html = str_replace( $content, $output->get_content(), $html ); $data['lazyloaded_images'] = array_merge( $data['lazyloaded_images'], $output->get_urls() ); } $data['html'] = $html; return $data; } /** * Format a URL. * * @param string $url URL to format. * @return string */ protected function format_url( string $url ): string { return strtok( $url, '?' ); } /** * Check of the string is excluded. * * @param string $string String to check. * @return bool */ protected function is_excluded( string $string ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound $values = [ $string, ]; $parsed_url_host = wp_parse_url( $string, PHP_URL_HOST ); if ( ! $parsed_url_host ) { $values [] = rocket_get_home_url() . $string; } /** * Filters the src used to prevent lazy load from being applied. * * @param array $excluded_src An array of excluded src. */ $excluded_values = wpm_apply_filters_typed( 'array', 'rocket_lazyload_excluded_src', [] ); $excluded_values = array_filter( $excluded_values ); if ( empty( $excluded_values ) ) { return false; } foreach ( $values as $value ) { foreach ( $excluded_values as $excluded_value ) { if ( strpos( $value, $excluded_value ) !== false ) { return true; } } } return false; } /** * Is the feature activated. * * @return bool */ protected function is_activated(): bool { return (bool) $this->options->get( 'lazyload_css_bg_img', false ); } /** * Add lazyload_excluded_src to excluded filters. * * @param array $excluded Excluded URLs. * @param array $urls List of Urls processed. * @return mixed */ public function exclude_rocket_lazyload_excluded_src( $excluded, $urls ) { /** * Filters the src used to prevent lazy load from being applied. * * @param array $excluded_src An array of excluded src. */ $excluded_values = wpm_apply_filters_typed( 'array', 'rocket_lazyload_excluded_src', [] ); $excluded_values = array_filter( $excluded_values ); if ( empty( $excluded_values ) ) { return $excluded; } foreach ( $urls as $url ) { foreach ( $excluded_values as $excluded_value ) { if ( strpos( $url['selector'], $excluded_value ) !== false || strpos( $url['style'], $excluded_value ) !== false ) { $excluded[] = $url; break; } } } return $excluded; } /** * Add hashes. * * @param array $tags Tags. * @return array */ protected function add_hashes( array $tags ): array { return array_map( function ( $url_tag ) { /** * Lazyload CSS hash. * * @param string $hash Lazyload CSS hash. * @param mixed $url_tag URL tag. */ $url_tag['hash'] = apply_filters( 'rocket_lazyload_css_hash', wp_generate_uuid4(), $url_tag ); return $url_tag; }, $tags ); } /** * Return mapping file URL. * * @param string $url Resource URL. * @return string */ protected function get_mapping_file_url( string $url ): string { return $this->format_url( $url ) . '.json'; } /** * Add timestamp to URL. * * @param string $url Asset Url. * * @return string */ protected function generate_asset_url( string $url ): string { $parsed_query = wp_parse_url( $url, PHP_URL_QUERY ); $queries = []; if ( $parsed_query ) { parse_str( $parsed_query, $queries ); } $queries['wpr_t'] = current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested $cached_url = $this->cache->generate_url( $this->format_url( $url ) ); $this->logger::debug( "Generated url lazy css files $url", $this->generate_log_context( [ 'data' => $cached_url, ] ) ); return add_query_arg( $queries, $cached_url ); } /** * Generate the context for logs. * * @param array $data Data to pass to logs. * @return array */ protected function generate_log_context( array $data = [] ): array { return array_merge( $data, [ 'type' => 'lazyload_css_bg_images', ] ); } /** * Protect URL inside the content. * * @param string $content Content to protect. * @param array $css_files CSS files from the content. * @return ProtectedContent */ protected function protect_css_urls( string $content, array $css_files ): ProtectedContent { usort( $css_files, function ( $url1, $url2 ) { return strlen( $url1 ) < strlen( $url2 ) ? 1 : -1; } ); $css_files_mapping = []; foreach ( $css_files as $url ) { $placeholder = uniqid( 'url_bg_css_' ); $content = str_replace( $url, $placeholder, $content ); $css_files_mapping[ $url ] = $placeholder; } return $this->lazyloaded_content_factory->make_protected_content( $css_files_mapping, $content ); } /** * Exclude SVG from lazy loaded images. * * @param array $urls URLs to exclude. * @return array */ public function remove_svg_from_lazyload_css( array $urls ): array { $urls[] = 'data:image/svg+xml'; return $urls; } } Engine/Media/Lazyload/CSS/ServiceProvider.php 0000644 00000004675 15174677547 0015066 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Lazyload\CSS; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Media\Lazyload\CSS\Context\LazyloadCSSContext; use WP_Rocket\Engine\Media\Lazyload\CSS\Data\LazyloadCSSContentFactory; use WP_Rocket\Engine\Media\Lazyload\CSS\Front\{ContentFetcher, Extractor, FileResolver, MappingFormatter, RuleFormatter, TagGenerator}; /** * Service provider. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'lazyload_css_context', 'lazyload_css_content_fetcher', 'lazyload_css_extractor', 'lazyload_css_file_resolver', 'lazyload_css_mapping_formatter', 'lazyload_css_rule_formatter', 'lazyload_css_tag_generator', 'lazyload_css_content_factory', 'lazyload_css_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'lazyload_css_context', LazyloadCSSContext::class ) ->addArguments( [ 'options', 'lazyload_css_cache', ] ); $this->getContainer()->add( 'lazyload_css_content_fetcher', ContentFetcher::class ); $this->getContainer()->add( 'lazyload_css_extractor', Extractor::class ); $this->getContainer()->add( 'lazyload_css_file_resolver', FileResolver::class ); $this->getContainer()->add( 'lazyload_css_mapping_formatter', MappingFormatter::class ); $this->getContainer()->add( 'lazyload_css_rule_formatter', RuleFormatter::class ); $this->getContainer()->add( 'lazyload_css_tag_generator', TagGenerator::class ); $this->getContainer()->add( 'lazyload_css_content_factory', LazyloadCSSContentFactory::class ); $this->getContainer()->addShared( 'lazyload_css_subscriber', Subscriber::class ) ->addArguments( [ 'lazyload_css_extractor', 'lazyload_css_rule_formatter', 'lazyload_css_file_resolver', 'lazyload_css_cache', 'lazyload_css_mapping_formatter', 'lazyload_css_tag_generator', 'lazyload_css_content_fetcher', 'lazyload_css_context', 'options', 'lazyload_css_content_factory', ] ); } } Engine/Media/Lazyload/CSS/Front/RuleFormatter.php 0000644 00000002754 15174677547 0015632 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Front; class RuleFormatter { /** * Format the CSS rule inside the CSS content. * * @param string $css CSS content. * @param array $data Data to format. * @return string */ public function format( string $css, array $data ): string { if ( count( $data ) === 0 ) { return $css; } $block = ''; $replaced_block = null; $blocks = []; foreach ( $data as $datum ) { $added = false; foreach ( $blocks as &$block ) { if ( $block['block'] === $datum['block'] ) { $block['items'] [] = $datum; $added = true; break; } } if ( $added ) { continue; } $blocks [] = [ 'items' => [ $datum, ], 'block' => $datum['block'], ]; } foreach ( $blocks as $block ) { $replaced_block = null; foreach ( $block['items'] as $datum ) { if ( ! key_exists( 'selector', $datum ) || ! key_exists( 'original', $datum ) || ! key_exists( 'block', $datum ) || ! key_exists( 'hash', $datum ) ) { return $css; } $original_block = $datum['block']; $replaced_block = $replaced_block ?: $datum['block']; $url = $datum['original']; $hash = $datum['hash']; $placeholder = "--wpr-bg-$hash"; $variable_placeholder = "var($placeholder)"; $replaced_block = str_replace( $url, $variable_placeholder, $replaced_block ); } $css = str_replace( $original_block, $replaced_block, $css ); } return $css; } } Engine/Media/Lazyload/CSS/Front/TagGenerator.php 0000644 00000002722 15174677547 0015414 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Front; class TagGenerator { /** * Generate tags from the mapping of lazyloaded images. * * @param array $mapping Lazyload images mapping. * @param array $loaded Excluded images. * @return string */ public function generate( array $mapping, array $loaded = [] ): string { $mapping = array_filter( $mapping, function ( $mapping_item ) use ( $loaded ) { foreach ( $loaded as $exclude_item ) { if ( $exclude_item['style'] === $mapping_item['style'] ) { return false; // Exclude matching elements. } } return true; // Include non-matching elements. } ); $mapping = array_values( $mapping ); $loaded_content = ''; foreach ( $loaded as $item ) { $loaded_content .= $item['style']; } $loaded_tag = "<style id=\"wpr-lazyload-bg-container\"></style><style id=\"wpr-lazyload-bg-exclusion\">$loaded_content</style>"; $nostyle_content = ''; foreach ( $mapping as $item ) { $nostyle_content .= $item['style']; } $nostyle_tag = "<noscript> <style id=\"wpr-lazyload-bg-nostyle\">$nostyle_content</style> </noscript>"; $mapping_json = wp_json_encode( $mapping ); $loaded_json = wp_json_encode( $loaded ); $script_content = "const rocket_pairs = $mapping_json; const rocket_excluded_pairs = $loaded_json;"; $script_tag = "<script type=\"application/javascript\">$script_content</script>"; return "$loaded_tag\n$nostyle_tag\n$script_tag"; } } Engine/Media/Lazyload/CSS/Front/MappingFormatter.php 0000644 00000005373 15174677547 0016316 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Front; class MappingFormatter { /** * Format data for the Mapping file. * * @param array $data Data to format. * @return array */ public function format( array $data ): array { $formatted_urls = []; foreach ( $data as $datum ) { $hash = $datum['hash']; $selector = $datum['selector']; $selector = $this->remove_pseudo_classes( $selector ); $url = $datum['url']; $placeholder = "--wpr-bg-$hash"; $img_url = "url('$url')"; $variable_placeholder = $datum['selector'] . '{' . $placeholder . ': ' . $img_url . ';}'; $formatted_urls[] = [ 'selector' => $selector, 'style' => $variable_placeholder, 'hash' => $hash, 'url' => $url, ]; } return $formatted_urls; } /** * Get pseudo elements to remove. * * @return string[] */ private function get_pseudo_elements_to_remove() { $original_pseudo_elements = [ ':before', ':after', ':first-line', ':first-letter', ':active', ':hover', ':focus', ':visited', ':focus-within', ':focus-visible', ]; /** * Pseudo elements to remove from lazyload CSS selector. * * @param string[] $original_pseudo_elements Pseudo elements to remove. */ $pseudo_elements_to_remove = wpm_apply_filters_typed( 'array', 'rocket_lazyload_css_ignored_pseudo_elements', $original_pseudo_elements ); usort( $pseudo_elements_to_remove, static function ( $first, $second ) { if ( strlen( $first ) === strlen( $second ) ) { return 0; } return ( strlen( $first ) > strlen( $second ) ) ? -1 : 1; } ); return $pseudo_elements_to_remove; } /** * Remove pseudo classes from the selector while mapping on each selector. * * @param string $selector Selector to clean. * @return string */ public function remove_pseudo_classes_for_selector( string $selector ): string { $selector = preg_replace( '/::[\w-]+/', '', $selector ); $pseudo_elements_to_remove = $this->get_pseudo_elements_to_remove(); foreach ( $pseudo_elements_to_remove as $element ) { $selector = str_replace( $element, '', $selector ); } if ( in_array( substr( $selector, -1 ), [ '&', '~', '+', '>' ], true ) ) { $selector .= '*'; } if ( ! $selector ) { return 'body'; } return $selector; } /** * Remove pseudo classes from the selector. * * @param string $selector Selector to clean. * @return string */ protected function remove_pseudo_classes( string $selector ): string { $selectors = explode( ',', $selector ); $selectors = array_map( [ $this, 'remove_pseudo_classes_for_selector' ], $selectors ); $selectors = array_unique( $selectors ); $selector = implode( ',', $selectors ); return (string) $selector; } } Engine/Media/Lazyload/CSS/Front/ContentFetcher.php 0000644 00000002740 15174677547 0015745 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Front; use WP_Filesystem_Direct; use WP_Rocket\Engine\Optimization\CSSTrait; class ContentFetcher { use CSSTrait; /** * WordPress filesystem. * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Instance. * * @param WP_Filesystem_Direct|null $filesystem WordPress filesystem. */ public function __construct( ?WP_Filesystem_Direct $filesystem = null ) { $this->filesystem = $filesystem ?: rocket_direct_filesystem(); } /** * Fetch content from the resource. * * @param string $path Path from the resource. * @param string $destination Destination path. * @return false|string */ public function fetch( string $path, string $destination ) { $content = $this->fetch_content( $path ); if ( ! $content ) { return false; } $content = $this->move( $this->get_converter( $path, $destination ), $content, $path ); $this->set_cached_import( $path ); $content = $this->combine_imports( $content, $destination ); return $content; } /** * Fetch the content from the file. * * @param string $path Path to fetch. * * @return false|string */ protected function fetch_content( string $path ) { if ( ! wp_http_validate_url( $path ) ) { $file_extension = pathinfo( $path, PATHINFO_EXTENSION ); if ( strtolower( $file_extension ) !== 'php' ) { return $this->filesystem->get_contents( $path ); } } return wp_remote_retrieve_body( wp_remote_get( $path ) ); } } Engine/Media/Lazyload/CSS/Front/FileResolver.php 0000644 00000000577 15174677547 0015441 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Front; class FileResolver { /** * Resolves the name from the file from its URL. * * @param string $url URL from the file to resolve. * @return string */ public function resolve( string $url ): string { $path = rocket_url_to_path( strtok( $url, '?' ) ); if ( ! $path ) { return ''; } return $path; } } Engine/Media/Lazyload/CSS/Front/Extractor.php 0000644 00000022415 15174677547 0015006 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Front; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Engine\Optimization\UrlTrait; class Extractor { use RegexTrait; use UrlTrait; /** * Comment mapping. * * @var array */ protected $comments_mapping = []; /** * Extract background images from CSS. * * @param string $content CSS content. * @param string $file_url File we are extracting css from. * @return array */ public function extract( string $content, string $file_url = '' ): array { $this->comments_mapping = []; $comment_regex = '#/\*[^*]*\*+([^/][^*]*\*+)*/#'; $content = preg_replace_callback( $comment_regex, function ( $matches ) { $id = '/*' . uniqid( 'bg_css_comment' ) . '*/'; $this->comments_mapping[ $id ] = $matches[0]; return $id; }, $content ); $background_regex = '\s?{[^{}]*background\s*:(?<property>[^;}]*)[^}]*}'; $background_image_regex = '(?:background-image)\s*:(?<property>[^;}]*)[^}]*}'; $content_reversed = strrev( $content ); $format_content = $this->reformat_css( $content ); /** * Lazyload background property regex. * * @param string $regex Lazyload background property regex. */ $background_regex = $this->apply_string_filter( 'lazyload_css_extract_bg_property_regex', $background_regex ); /** * Lazyload background property regex. * * @param string $regex Lazyload background property regex. */ $background_image_regex = $this->apply_string_filter( 'lazyload_css_extract_bg_img_property_regex', $background_image_regex ); $background_matches = $this->find( $background_regex, $content, 'mi', PREG_OFFSET_CAPTURE ); $background_image_matches = $this->find( $background_image_regex, $content, 'mi', PREG_OFFSET_CAPTURE ); $background_property_matches = $background_matches['property'] ?? []; $background_image_property_matches = $background_image_matches['property'] ?? []; $matches = array_merge( $background_property_matches, $background_image_property_matches ); if ( empty( $matches ) ) { return []; } $results = []; $arr_selector = []; foreach ( $matches as $match ) { $original_offset = $match[1]; $adjusted_offset = strlen( $content ) - $original_offset - strlen( $match[0] ); // Reverse the selector regex. $reversed_selector_regex = '/dnuorgkcab[^{}]*{\s?(?<reversed_selector>(?:[ \-,:\w\.\(\)\n\r\$\|^>[*"\'=~\]#]|(?:\][^\]]+\[))+)/mi'; // Execute preg_match to find reversed selector. preg_match( $reversed_selector_regex, $content_reversed, $reversed_selector_matches, PREG_OFFSET_CAPTURE, $adjusted_offset ); if ( empty( $reversed_selector_matches ) ) { continue; } // Extract reversed selector and reverse it back. $reversed_selector = $reversed_selector_matches['reversed_selector'][0]; $selector = trim( strrev( $reversed_selector ) ); $property = trim( $match[0] ); $block_regex_selector = $selector; $escaped_block_selector = preg_quote( $block_regex_selector, '/' ); preg_match_all( "/^\s*$escaped_block_selector\s*{(.*?)}/ms", $format_content, $block_matches ); $block_index = 0; $default_index = 0; /** * If block matches is empty then try to use the second regex pattern. * The first pattern will match the selector literally but sometimes fails to match * selector within an internal style, the second make sure we capture it. * e.g. style>.selector_name within <style>.selector_name will not be capture * with the first pattern. */ if ( empty( $block_matches[ $default_index ] ) ) { preg_match_all( "/\s*$escaped_block_selector\s*{(.*?)}/ms", $format_content, $block_matches ); } $urls = $this->extract_urls( $property, $file_url ); if ( in_array( $selector, $arr_selector, true ) ) { $indexes = array_keys( $arr_selector, trim( $selector ), true ); $block_index = count( $indexes ); } $arr_selector[] = $selector; if ( empty( $block_matches ) ) { continue; } $block = trim( $block_matches[ $default_index ][ $default_index ] ); if ( ! empty( $block_matches[ $default_index ][ $block_index ] ) ) { $block = trim( $block_matches[ $default_index ][ $block_index ] ); } if ( ! empty( $this->comments_mapping ) ) { foreach ( $this->comments_mapping as $id => $comment ) { $block = str_replace( $id, $comment, $block ); } } foreach ( $urls as $url ) { $results[ $selector ][] = [ 'selector' => $selector, 'url' => $url['url'], 'original' => $url['original'], 'block' => $block, ]; } } return $results; } /** * Extract URLS from a CSS property. * * @param string $content Content from the CSS property. * @param string $file_url URL of the css file. * @return array */ protected function extract_urls( string $content, string $file_url = '' ): array { /** * Lazyload URL regex. * * @param string $regex Lazyload URL regex. */ $regex = $this->apply_string_filter( 'lazyload_css_extract_url_regex', '(?<tag>url\([\'"]?(?<url>[^\)]*)[\'"]?\))' ); $matches = $this->find( $regex, $content, 'mi' ); $output = []; /** * Lazyload ignored URLs. * * @param string[] $urls Ignored URLs. */ $ignored_urls = (array) apply_filters( 'rocket_lazyload_css_ignored_urls', [ 'trustindex-google-widget.css', 'cdn.trustindex.io', ] ); foreach ( $matches as $match ) { $url = $match['url'] ?: ''; $url = str_replace( '"', '', $url ); $url = str_replace( "'", '', $url ); $url = trim( $url ); $url = $this->make_url_complete( $url, $file_url ); if ( ! key_exists( 'tag', $match ) || ! key_exists( 'url', $match ) || ! $url || $this->is_url_ignored( $url, $ignored_urls ) ) { continue; } if ( ! $this->is_url_external( $url ) && ! $this->is_relative( $url ) ) { $url = $this->apply_string_filter( 'css_url', $url ); } $original_url = trim( $match['tag'], ' ,' ); $output[] = [ 'url' => $url, 'original' => $original_url, ]; } return $output; } /** * Reformat css most especially minified css to make sure each rule are on * a separate line. * * @param string $css_content The css content to be formatted. * * @return string */ protected function reformat_css( string $css_content ): string { $matches = $this->find( '(.*?\{.*?\})', $css_content, 's', PREG_PATTERN_ORDER ); $formatted_css = ''; if ( empty( $matches ) ) { return $css_content; } foreach ( $matches[0] as $class_block ) { $formatted_css .= $class_block . "\n"; } return $formatted_css; } /** * Apply a string filter. * * @param string $name Name from the filter. * @param string $value Value to pass to the filter that will be returned. * @param array ...$args Additional values. * @return string */ protected function apply_string_filter( string $name, string $value, ...$args ) { $output = apply_filters( 'rocket_' . $name, $value, ...$args ); if ( ! is_string( $output ) ) { return $value; } return $output; } /** * Check if the URLs is ignored. * * @param string $url URL to check. * @param array $ignored_urls List of ignored URLs. * * @return bool */ protected function is_url_ignored( string $url, array $ignored_urls ): bool { foreach ( $ignored_urls as $ignored_url ) { if ( strpos( $url, $ignored_url ) !== false ) { return true; } } return false; } /** * Complete the URL if necessary. * * @param string $url URL to complete. * @param string $file_url URL of the CSS File. * * @return string */ protected function make_url_complete( string $url, string $file_url ): string { $host = wp_parse_url( $url, PHP_URL_HOST ); if ( $host || ( $this->is_relative( $url ) && ! empty( $file_url ) ) ) { return $this->transform_relative_to_absolute( $url, $file_url ); } return rocket_get_home_url() . '/' . trim( $url, '/ ' ); } /** * Transform a relative URL to an absolute URL based on the base URL. * * @param string $rel Relative URL to transform. * @param string $base Base URL. * * @return string */ protected function transform_relative_to_absolute( string $rel, string $base = '' ): string { if ( ! is_null( wp_parse_url( $rel, PHP_URL_SCHEME ) ) ) { return $rel; } if ( '#' === $rel[0] || '?' === $rel[0] ) { return $base . $rel; } $base_url_parts = wp_parse_url( $base ); $path = ! empty( $base_url_parts['path'] ) ? $base_url_parts['path'] : '/'; $path = preg_replace( '#/[^/]*$#', '', $path ); if ( '/' === $rel[0] ) { $path = ''; } $abs = "$path/$rel"; $re = [ '#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#' ]; $dots_count = substr_count( $rel, '..' ); foreach ( range( 1, $dots_count ) as $n ) { $abs_before = $abs; $abs = preg_replace( $re, '/', $abs, -1, $n ); if ( $abs_before === $abs ) { break; } } return trailingslashit( rocket_get_home_url() ) . ltrim( $abs, '/' ); } /** * Check if the URL is external. * * @param string $url URL to check. * * @return bool */ protected function is_url_external( string $url ): bool { $host = wp_parse_url( $url, PHP_URL_HOST ); if ( ! $host ) { return false; } $home_host = wp_parse_url( rocket_get_home_url(), PHP_URL_HOST ); return $host !== $home_host; } } Engine/Media/Lazyload/CSS/Data/ProtectedContent.php 0000644 00000002210 15174677547 0016067 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Data; class ProtectedContent { /** * Protected content. * * @var string */ protected $content = ''; /** * Mapping between protected files and their placeholder. * * @var array */ protected $protected_files_mapping = []; /** * Set protected content. * * @param string $content Protected content. * @return void */ public function set_content( string $content ): void { $this->content = $content; } /** * Get protected content. * * @return string */ public function get_content(): string { return $this->content; } /** * Set mapping between protected files and their placeholder. * * @param array $protected_files_mapping Mapping between protected files and their placeholder. * @return void */ public function set_protected_files_mapping( array $protected_files_mapping ): void { $this->protected_files_mapping = $protected_files_mapping; } /** * Get mapping between protected files and their placeholder. * * @return array */ public function get_protected_files_mapping(): array { return $this->protected_files_mapping; } } Engine/Media/Lazyload/CSS/Data/LazyloadCSSContentFactory.php 0000644 00000001772 15174677547 0017632 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Data; class LazyloadCSSContentFactory { /** * Make LazyloadedContent instance. * * @param array $formatted_urls Formatted URls. * @param string $content Content. * @return LazyloadedContent */ public function make_lazyloaded_content( array $formatted_urls, string $content ): LazyloadedContent { $lazyloaded_content = new LazyloadedContent(); $lazyloaded_content->set_urls( $formatted_urls ); $lazyloaded_content->set_content( $content ); return $lazyloaded_content; } /** * Make ProtectedContent instance. * * @param array $protected_files Protected files. * @param string $content Content. * @return ProtectedContent */ public function make_protected_content( array $protected_files, string $content ): ProtectedContent { $protected_content = new ProtectedContent(); $protected_content->set_content( $content ); $protected_content->set_protected_files_mapping( $protected_files ); return $protected_content; } } Engine/Media/Lazyload/CSS/Data/LazyloadedContent.php 0000644 00000001635 15174677547 0016240 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Data; class LazyloadedContent { /** * URls extracted from the content. * * @var array */ protected $urls = []; /** * Lazyloaded content. * * @var string */ protected $content = ''; /** * Get URls extracted from the content. * * @return array */ public function get_urls(): array { return $this->urls; } /** * Set URls extracted from the content. * * @param array $urls URls extracted from the content. * @return void */ public function set_urls( array $urls ): void { $this->urls = $urls; } /** * Get lazyloaded content. * * @return string */ public function get_content(): string { return $this->content; } /** * Set lazyloaded content. * * @param string $content Lazyloaded content. * @return void */ public function set_content( string $content ): void { $this->content = $content; } } Engine/Media/Lazyload/CSS/Admin/Subscriber.php 0000644 00000004551 15174677547 0015077 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Lazyload\CSS\Admin; use WP_Rocket\Engine\Common\Cache\CacheInterface; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Cache instance. * * @var CacheInterface */ protected $cache; /** * Instantiate class. * * @param CacheInterface $cache Cache instance. */ public function __construct( CacheInterface $cache ) { $this->cache = $cache; } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_meta_boxes_fields' => [ 'add_meta_box', 8 ], 'admin_notices' => 'maybe_add_error_notice', 'rocket_safe_mode_reset_options' => 'add_option_safemode', 'wp_rocket_upgrade' => [ 'clear_background_css_with_upgrade', 10, 2 ], ]; } /** * Add the field to the WP Rocket metabox on the post edit page. * * @param string[] $fields Metaboxes fields. * * @return string[] */ public function add_meta_box( array $fields ) { $fields['lazyload_css_bg_img'] = __( 'LazyLoad CSS backgrounds', 'rocket' ); return $fields; } /** * Maybe display the error notice. * * @return void */ public function maybe_add_error_notice() { if ( ! current_user_can( 'rocket_manage_options' ) || $this->cache->is_accessible() ) { return; } rocket_notice_html( [ 'status' => 'error', 'dismissible' => '', 'message' => rocket_notice_writing_permissions( $this->cache->get_root_path() ), ] ); } /** * Add option to safe mode. * * @param array $options Safe mode options. * * @return array */ public function add_option_safemode( array $options ) { $options['lazyload_css_bg_img'] = 0; return $options; } /** * Upgrade callback. * * @param string $new_version Plugin new version. * @param string $old_version Plugin old version. * @return void */ public function clear_background_css_with_upgrade( $new_version, $old_version ) { if ( empty( $old_version ) || version_compare( $old_version, '3.18', '>' ) ) { return; } $preserve_dirs = is_multisite() ? get_sites( [ 'fields' => 'ids' ] ) : [ get_current_blog_id() ]; // Completely clear background-css directory. $this->cache->full_clear( $preserve_dirs ); } } Engine/Media/Lazyload/CSS/Admin/ServiceProvider.php 0000644 00000001575 15174677547 0016112 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Lazyload\CSS\Admin; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'lazyload_css_admin_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'lazyload_css_admin_subscriber', Subscriber::class ) ->addArgument( 'lazyload_css_cache' ); } } Engine/Media/Lazyload/CSS/Context/LazyloadCSSContext.php 0000644 00000002412 15174677547 0017057 0 ustar 00 <?php namespace WP_Rocket\Engine\Media\Lazyload\CSS\Context; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\Cache\CacheInterface; use WP_Rocket\Engine\Common\Context\AbstractContext; use WP_Rocket\Engine\Media\Lazyload\CanLazyloadTrait; class LazyloadCSSContext extends AbstractContext { use CanLazyloadTrait; /** * Cache instance. * * @var CacheInterface */ protected $cache; /** * Instantiate the class. * * @param Options_Data $options WPR options. * @param CacheInterface $cache Cache instance. */ public function __construct( Options_Data $options, CacheInterface $cache ) { parent::__construct( $options ); $this->cache = $cache; } /** * Determine if the action is allowed. * * @param array $data Data to pass to the context. * @return bool */ public function is_allowed( array $data = [] ): bool { $is_allowed = $this->run_common_checks( [ 'do_not_optimize' => false, 'bypass' => false, 'option' => 'lazyload_css_bg_img', 'password_protected' => false, 'post_excluded' => 'lazyload_css_bg_img', 'logged_in' => false, ] ); if ( ! $is_allowed || ! $this->should_lazyload() ) { return false; } return $this->cache->is_accessible(); } } Engine/Media/Lazyload/AdminSubscriber.php 0000644 00000003132 15174677547 0014362 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Media\Lazyload; use WP_Rocket\Event_Management\Subscriber_Interface; class AdminSubscriber implements Subscriber_Interface { /** * Returns an array of events this listens to * * @return array */ public static function get_subscribed_events(): array { return [ 'rocket_first_install_options' => [ 'add_option', 15 ], 'rocket_input_sanitize' => 'sanitize_exclude_lazyload', 'rocket_meta_boxes_fields' => [ 'add_meta_box', 7 ], ]; } /** * Adds the exclude lazyload option to WP Rocket options array * * @since 3.8 * * @param array $options WP Rocket options array. * @return array */ public function add_option( array $options ): array { $options['exclude_lazyload'] = []; return $options; } /** * Sanitizes the exclude lazyload input when saving the option * * @since 3.8 * * @param array $input Input array. * @return array */ public function sanitize_exclude_lazyload( array $input ): array { if ( empty( $input['exclude_lazyload'] ) ) { $input['exclude_lazyload'] = []; } $input['exclude_lazyload'] = rocket_sanitize_textarea_field( 'exclude_lazyload', $input['exclude_lazyload'] ); return $input; } /** * Add the field to the WP Rocket metabox on the post edit page. * * @param string[] $fields Metaboxes fields. * * @return string[] */ public function add_meta_box( array $fields ) { $fields['lazyload'] = __( 'LazyLoad for images', 'rocket' ); $fields['lazyload_iframes'] = __( 'LazyLoad for iframes/videos', 'rocket' ); return $fields; } } Engine/HealthCheck/HealthCheck.php 0000644 00000007016 15174677547 0013023 0 ustar 00 <?php namespace WP_Rocket\Engine\HealthCheck; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class HealthCheck implements Subscriber_Interface { /** * Instance of options. * * @var Options_Data */ private $options; /** * Creates an instance of the health checker. * * @param Options_Data $options Options_Data instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'admin_notices' => 'missed_cron', ]; } /** * Display a warning notice if WP Rocket scheduled events are not running properly. * * @since 3.5.4 */ public function missed_cron() { if ( ! $this->should_check() ) { return; } $delay = rocket_get_constant( 'DISABLE_WP_CRON' ) ? HOUR_IN_SECONDS : 5 * MINUTE_IN_SECONDS; $list = ''; $events = $this->get_events(); foreach ( $events as $event => $description ) { $timestamp = wp_next_scheduled( $event ); if ( false === $timestamp || ( $timestamp + $delay - time() ) > 0 ) { unset( $events[ $event ] ); continue; } $list .= "<li>{$description}</li>"; } if ( empty( $events ) ) { return; } $message = sprintf( '<p>%1$s</p> <ul>%2$s</ul> <p>%3$s</p>', _n( 'The following scheduled event failed to run. This may indicate the CRON system is not running properly, which can prevent some WP Rocket features from working as intended:', 'The following scheduled events failed to run. This may indicate the CRON system is not running properly, which can prevent some WP Rocket features from working as intended:', count( $events ), 'rocket' ), $list, __( 'Please contact your host to check if CRON is working.', 'rocket' ) ); rocket_notice_html( [ 'status' => 'warning', 'dismissible' => '', 'message' => $message, 'dismiss_button' => 'rocket_warning_cron', ] ); } /** * Checks if health check should run. * * @since 3.5.4 * * @return bool true when should do health check; else, false. */ protected function should_check() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return false; } if ( 'settings_page_wprocket' !== get_current_screen()->id ) { return false; } $dismissed = (array) get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( 'rocket_warning_cron', $dismissed, true ) ) { return false; } return ! ( 0 === (int) $this->options->get( 'purge_cron_interval', 0 ) && 0 === (int) $this->options->get( 'async_css', 0 ) && 0 === (int) $this->options->get( 'manual_preload', 0 ) && 0 === (int) $this->options->get( 'schedule_automatic_cleanup', 0 ) ); } /** * Gets an array of events with their descriptions. * * @since 3.5.4 * * @return array array of events => descriptions. */ protected function get_events() { return [ 'rocket_purge_time_event' => __( 'Scheduled Cache Purge', 'rocket' ), 'rocket_database_optimization_time_event' => __( 'Scheduled Database Optimization', 'rocket' ), 'rocket_database_optimization_cron_interval' => __( 'Database Optimization Process', 'rocket' ), 'rocket_preload_cron_interval' => _x( 'Preload', 'noun', 'rocket' ), 'rocket_critical_css_generation_cron_interval' => __( 'Critical Path CSS Generation Process', 'rocket' ), ]; } } Engine/HealthCheck/ServiceProvider.php 0000644 00000001731 15174677547 0013771 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\HealthCheck; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service Provider for health check subscribers */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'health_check', 'action_scheduler_check', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'health_check', HealthCheck::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'action_scheduler_check', ActionSchedulerCheck::class ); } } Engine/HealthCheck/ActionSchedulerCheck.php 0000644 00000010113 15174677547 0014662 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\HealthCheck; use ActionScheduler_Versions; use ActionScheduler; use ActionScheduler_StoreSchema; use ActionScheduler_LoggerSchema; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Engine\Activation\ActivationInterface; class ActionSchedulerCheck implements Subscriber_Interface, ActivationInterface { /** * Array of events this subscriber listens to. * * @return array */ public static function get_subscribed_events(): array { $slug = rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ); return [ 'update_option_' . $slug => [ 'check_on_update_options', 10, 2 ], 'wp_rocket_update' => 'maybe_recreate_as_tables', ]; } /** * Actions to perform on plugin activation * * @return void */ public function activate() { // @phpstan-ignore-next-line add_action( 'rocket_activation', [ $this, 'maybe_recreate_as_tables' ] ); } /** * Maybe recreate Action Scheduler tables if they are missing * * @return bool */ public function maybe_recreate_as_tables(): bool { if ( ! $this->is_valid_action_scheduler_version() ) { return false; } if ( $this->is_valid_as_tables() ) { return false; } $store_schema = new ActionScheduler_StoreSchema(); $logger_schema = new ActionScheduler_LoggerSchema(); $store_schema->register_tables( true ); $logger_schema->register_tables( true ); return true; } /** * Maybe recreate tables on preload or RUCSS activation * * @param mixed $old_value The old option value. * @param mixed $value The new option value. * * @return bool */ public function check_on_update_options( $old_value, $value ): bool { if ( ! isset( $old_value['remove_unused_css'], $value['remove_unused_css'], $old_value['manual_preload'], $value['manual_preload'] ) ) { return false; } if ( $old_value['remove_unused_css'] === $value['remove_unused_css'] && $old_value['manual_preload'] === $value['manual_preload'] ) { return false; } if ( 0 === (int) $value['remove_unused_css'] && 0 === (int) $value['manual_preload'] ) { return false; } if ( ( $old_value['remove_unused_css'] !== $value['remove_unused_css'] && 1 !== (int) $value['remove_unused_css'] ) || ( $old_value['manual_preload'] !== $value['manual_preload'] && 1 !== (int) $value['manual_preload'] ) ) { return false; } return $this->maybe_recreate_as_tables(); } /** * Check if Action Scheduler tables exists * * @return bool */ private function is_valid_as_tables(): bool { $cached_count = get_transient( 'rocket_rucss_as_tables_count' ); if ( false !== $cached_count && ! is_admin() ) { // Stop caching in admin UI. return 4 === (int) $cached_count; } global $wpdb; $exp = "'^" . $wpdb->prefix . "actionscheduler_(logs|actions|groups|claims)$'"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $found_as_tables = $wpdb->get_col( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->prepare( 'SHOW TABLES FROM `' . DB_NAME . '` WHERE `Tables_in_' . DB_NAME . '` LIKE %s AND `Tables_in_' . DB_NAME . '` REGEXP ' . $exp, '%actionscheduler%' ) ); set_transient( 'rocket_rucss_as_tables_count', count( $found_as_tables ), rocket_get_constant( 'DAY_IN_SECONDS', 24 * 60 * 60 ) ); return 4 === count( $found_as_tables ); } /** * Validate if the currenlt loaded action scheduler's version is more than 3.0.0. * Note: Latest_version method in ActionScheduler_Versions class will return false with first activation * in case we don't have any other active plugin which loads Action Scheduler. * Because with activation, our Action Scheduler still not initialized yet. * * @return bool */ private function is_valid_action_scheduler_version() { if ( ! class_exists( 'ActionScheduler_Versions' ) || ! class_exists( 'ActionScheduler' ) ) { return false; } $version = ActionScheduler_Versions::instance()->latest_version(); return ! $version || version_compare( $version, '3.0.0', '>=' ); } } Engine/Capabilities/Subscriber.php 0000644 00000005230 15174677547 0013205 0 ustar 00 <?php namespace WP_Rocket\Engine\Capabilities; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Manage WP Rocket custom capabilities * * @since 3.4 */ class Subscriber implements Subscriber_Interface { /** * Capabilities manager instance * * @var Manager */ private $capabilities; /** * Instantiate the subscriber * * @param Manager $capabilities Capabilities manager instance. */ public function __construct( Manager $capabilities ) { $this->capabilities = $capabilities; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.4 * * @return array */ public static function get_subscribed_events() { $slug = rocket_get_constant( 'WP_ROCKET_PLUGIN_SLUG' ); return [ "option_page_capability_{$slug}" => 'required_capability', 'ure_built_in_wp_caps' => 'add_caps_to_ure', 'ure_capabilities_groups_tree' => 'add_group_to_ure', 'members_register_cap_groups' => 'add_cap_group_to_members', 'members_register_caps' => 'add_caps_to_members', 'wp_rocket_upgrade' => [ 'add_capabilities_on_upgrade', 12, 2 ], ]; } /** * Sets the capability for the options page. * * @since 3.4 * * @param string $capability The capability used for the page, which is manage_options by default. * @return string */ public function required_capability( $capability ) { return $this->capabilities->required_capability( $capability ); } /** * Adds WP Rocket capabilities to User Role Editor * * @since 3.4 * * @param array $caps Array of existing capabilities. * @return array */ public function add_caps_to_ure( $caps ) { return $this->capabilities->add_caps_to_ure( $caps ); } /** * Adds WP Rocket as a group in User Role Editor * * @since 3.4 * * @param array $groups Array of existing groups. * @return array */ public function add_group_to_ure( $groups ) { return $this->capabilities->add_group_to_ure( $groups ); } /** * Add WP Rocket as a cap group in Members */ public function add_cap_group_to_members() { $this->capabilities->add_cap_group_to_members(); } /** * Add WP Rocket capabilities to Members */ public function add_caps_to_members() { $this->capabilities->add_caps_to_members(); } /** * Adds WP Rocket capabilities on plugin upgrade * * @since 3.6.3 * * @param string $wp_rocket_version Latest WP Rocket version. * @param string $actual_version Installed WP Rocket version. * @return void */ public function add_capabilities_on_upgrade( $wp_rocket_version, $actual_version ) { $this->capabilities->add_capabilities_on_upgrade( $wp_rocket_version, $actual_version ); } } Engine/Capabilities/ServiceProvider.php 0000644 00000001731 15174677547 0014217 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Capabilities; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service Provider for capabilities */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'capabilities_manager', 'capabilities_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'capabilities_manager', Manager::class ); $this->getContainer()->addShared( 'capabilities_subscriber', Subscriber::class ) ->addArgument( 'capabilities_manager' ); } } Engine/Capabilities/Manager.php 0000644 00000007622 15174677547 0012463 0 ustar 00 <?php namespace WP_Rocket\Engine\Capabilities; use WP_Rocket\Engine\Activation\ActivationInterface; use WP_Rocket\Engine\Deactivation\DeactivationInterface; use WP_Role; class Manager implements ActivationInterface, DeactivationInterface { /** * List of WP Rocket capabilities * * @var array */ private $capabilities = [ 'rocket_manage_options', 'rocket_purge_cache', 'rocket_purge_posts', 'rocket_purge_terms', 'rocket_purge_users', 'rocket_purge_cloudflare_cache', 'rocket_purge_sucuri_cache', 'rocket_preload_cache', 'rocket_regenerate_critical_css', 'rocket_remove_unused_css', ]; /** * Gets the WP Rocket capabilities * * @since 3.4 * * @return array */ private function get_capabilities() { return $this->capabilities; } /** * Performs these actions during the plugin activation * * @return void */ public function activate() { add_action( 'rocket_activation', [ $this, 'add_rocket_capabilities' ] ); } /** * Performs these actions during the plugin deactivation * * @return void */ public function deactivate() { add_action( 'rocket_deactivation', [ $this, 'remove_rocket_capabilities' ] ); } /** * Add WP Rocket capabilities to the administrator role * * @since 3.4 * * @return void */ public function add_rocket_capabilities() { $role = $this->get_administrator_role_object(); if ( is_null( $role ) ) { return; } foreach ( $this->get_capabilities() as $cap ) { $role->add_cap( $cap ); } } /** * Remove WP Rocket capabilities from the administrator role * * @since 3.4 * * @return void */ public function remove_rocket_capabilities() { $role = $this->get_administrator_role_object(); if ( is_null( $role ) ) { return; } foreach ( $this->get_capabilities() as $cap ) { $role->remove_cap( $cap ); } } /** * Sets the capability for the options page. * * @since 3.4 * * @param string $capability The capability used for the page, which is manage_options by default. * @return string */ public function required_capability( $capability ) { return 'rocket_manage_options'; } /** * Add WP Rocket capabilities to User Role Editor * * @since 3.4 * * @param array $caps Array of existing capabilities. * @return array */ public function add_caps_to_ure( $caps ) { foreach ( $this->get_capabilities() as $cap ) { $caps[ $cap ] = [ 'custom', 'wp_rocket', ]; } return $caps; } /** * Add WP Rocket as a group in User Role Editor * * @since 3.4 * * @param array $groups Array of existing groups. * @return array */ public function add_group_to_ure( $groups ) { $groups['wp_rocket'] = [ 'caption' => esc_html( 'WP Rocket' ), 'parent' => 'custom', 'level' => 2, ]; return $groups; } /** * Add WP Rocket as a cap group in Members */ public function add_cap_group_to_members() { // @phpstan-ignore-next-line \members_register_cap_group( 'wp_rocket', [ 'label' => esc_html( 'WP Rocket' ), 'priority' => 42, 'caps' => $this->get_capabilities(), ] ); } /** * Add WP Rocket capabilities to Members */ public function add_caps_to_members() { foreach ( $this->get_capabilities() as $cap ) { // @phpstan-ignore-next-line \members_register_cap( $cap, [ 'label' => $cap ] ); } } /** * Adds WP Rocket capabilities on plugin upgrade * * @since 3.6.3 * * @param string $wp_rocket_version Latest WP Rocket version. * @param string $actual_version Installed WP Rocket version. * @return void */ public function add_capabilities_on_upgrade( $wp_rocket_version, $actual_version ) { if ( version_compare( $actual_version, '3.9', '<' ) ) { $this->add_rocket_capabilities(); } } /** * Returns the object for the administrator roll * * @since 3.6.3 * * @return WP_Role|null */ private function get_administrator_role_object() { return get_role( 'administrator' ); } } Engine/License/API/PricingClient.php 0000644 00000003303 15174677547 0013235 0 ustar 00 <?php namespace WP_Rocket\Engine\License\API; use WP_Rocket\Engine\Common\JobManager\APIHandler\AbstractSafeAPIClient; class PricingClient extends AbstractSafeAPIClient { const PRICING_ENDPOINT = 'https://api.wp-rocket.me/stat/1.0/wp-rocket/pricing-2023.php'; /** * Get the transient key for plugin updates. * * This method returns the transient key used for caching plugin updates. * * @return string The transient key for plugin updates. */ protected function get_transient_key() { return 'wp_rocket_pricing'; } /** * Get the API URL for plugin updates. * * This method returns the API URL used for fetching plugin updates. * * @return string The API URL for plugin updates. */ protected function get_api_url() { return self::PRICING_ENDPOINT; } /** * Gets pricing data from cache if it exists, else gets it from the pricing endpoint * * Cache the pricing data for 6 hours in a transient * * @since 3.7.3 * * @return bool|object */ public function get_pricing_data() { $cached_data = get_transient( 'wp_rocket_pricing' ); if ( false !== $cached_data ) { return $cached_data; } $data = $this->get_raw_pricing_data(); if ( false === $data ) { return false; } set_transient( 'wp_rocket_pricing', $data, 12 * HOUR_IN_SECONDS ); return $data; } /** * Gets the pricing data from the pricing endpoint * * @since 3.7.3 * * @return bool|object */ private function get_raw_pricing_data() { $response = $this->send_get_request( [], true ); if ( is_wp_error( $response ) || ( is_array( $response ) && 200 !== $response['response']['code'] ) ) { return false; } return json_decode( wp_remote_retrieve_body( $response ) ); } } Engine/License/API/User.php 0000644 00000005173 15174677547 0011430 0 ustar 00 <?php namespace WP_Rocket\Engine\License\API; class User { /** * The user object * * @var object */ private $user; /** * Instantiate the class * * @param object|false $user The user object. */ public function __construct( $user ) { $this->user = is_object( $user ) ? $user : new \stdClass(); } /** * Gets the user license type * * @return int */ public function get_license_type() { if ( ! isset( $this->user->licence_account ) ) { return 0; } return (int) $this->user->licence_account; } /** * Gets the user license expiration timestamp * * @return int */ public function get_license_expiration() { if ( ! isset( $this->user->licence_expiration ) ) { return 0; } return (int) $this->user->licence_expiration; } /** * Checks if the user license is expired * * @return boolean */ public function is_license_expired() { return time() > $this->get_license_expiration(); } /** * Gets the user license creation date * * @return int */ public function get_creation_date() { if ( ! isset( $this->user->date_created ) ) { return time(); } return (int) $this->user->date_created > 0 ? (int) $this->user->date_created : time(); } /** * Checks if user has auto-renew enabled * * @return boolean */ public function is_auto_renew() { if ( ! isset( $this->user->has_auto_renew ) ) { return false; } return (bool) $this->user->has_auto_renew; } /** * Gets the upgrade to plus URL * * @return string */ public function get_upgrade_plus_url() { if ( ! isset( $this->user->upgrade_plus_url ) ) { return ''; } return $this->user->upgrade_plus_url; } /** * Gets the upgrade to infinite url * * @return string */ public function get_upgrade_infinite_url() { if ( ! isset( $this->user->upgrade_infinite_url ) ) { return ''; } return $this->user->upgrade_infinite_url; } /** * Gets the renewal url * * @return string */ public function get_renewal_url() { if ( ! isset( $this->user->renewal_url ) ) { return ''; } return $this->user->renewal_url; } /** * Checks if the user license has expired for more than 15 days * * @return boolean */ public function is_license_expired_grace_period() { if ( $this->is_license_expired() && ( time() - $this->get_license_expiration() > 15 * 24 * 60 * 60 ) ) { return true; } return false; } /** * Get available upgrades from the API. * * @return array */ public function get_available_upgrades() { if ( empty( $this->user->licence->prices->upgrades ) ) { return []; } return (array) $this->user->licence->prices->upgrades; } } Engine/License/API/UserClient.php 0000644 00000004654 15174677547 0012572 0 ustar 00 <?php namespace WP_Rocket\Engine\License\API; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\JobManager\APIHandler\AbstractSafeAPIClient; class UserClient extends AbstractSafeAPIClient { const USER_ENDPOINT = 'https://api.wp-rocket.me/stat/1.0/wp-rocket/user.php'; /** * WP Rocket options instance * * @var Options_Data */ private $options; /** * Get the transient key for plugin updates. * * This method returns the transient key used for caching plugin updates. * * @return string The transient key for plugin updates. */ protected function get_transient_key() { return 'wpr_user_information'; } /** * Get the API URL for plugin updates. * * This method returns the API URL used for fetching plugin updates. * * @return string The API URL for plugin updates. */ protected function get_api_url() { return self::USER_ENDPOINT; } /** * Instantiate the class * * @param Options_Data $options WP Rocket options instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Gets user data from cache if it exists, else gets it from the user endpoint * * Cache the user data for 24 hours in a transient * * @since 3.7.3 * * @return bool|object */ public function get_user_data() { $cached_data = get_transient( 'wp_rocket_customer_data' ); if ( false !== $cached_data ) { return $cached_data; } $data = $this->get_raw_user_data(); if ( false === $data ) { return false; } set_transient( 'wp_rocket_customer_data', $data, DAY_IN_SECONDS ); return $data; } /** * Gets the user data from the user endpoint * * @since 3.7.3 * * @return bool|object */ private function get_raw_user_data() { $customer_key = ! empty( $this->options->get( 'consumer_key', '' ) ) ? $this->options->get( 'consumer_key', '' ) : rocket_get_constant( 'WP_ROCKET_KEY', '' ); $customer_email = ! empty( $this->options->get( 'consumer_email', '' ) ) ? $this->options->get( 'consumer_email', '' ) : rocket_get_constant( 'WP_ROCKET_EMAIL', '' ); $response = $this->send_post_request( [ 'body' => 'user_id=' . rawurlencode( $customer_email ) . '&consumer_key=' . sanitize_key( $customer_key ), ], true ); if ( is_wp_error( $response ) ) { return false; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { return false; } return json_decode( $body ); } } Engine/License/API/Pricing.php 0000644 00000013702 15174677547 0012102 0 ustar 00 <?php namespace WP_Rocket\Engine\License\API; class Pricing { /** * The pricing data object * * @var object */ private $pricing; /** * Instantiate the class * * @param object $pricing The pricing object. */ public function __construct( $pricing ) { $this->pricing = $pricing; } /** * Gets the single license pricing data * * @return null|object */ public function get_single_pricing() { if ( ! isset( $this->pricing->licenses->single ) || ! is_object( $this->pricing->licenses->single ) ) { return null; } return $this->pricing->licenses->single; } /** * Gets the plus license pricing data * * @return null|object */ public function get_plus_pricing() { if ( ! isset( $this->pricing->licenses->plus ) || ! is_object( $this->pricing->licenses->plus ) ) { return null; } return $this->pricing->licenses->plus; } /** * Gets the infinite license pricing data * * @return null|object */ public function get_infinite_pricing() { if ( ! isset( $this->pricing->licenses->infinite ) || ! is_object( $this->pricing->licenses->infinite ) ) { return null; } return $this->pricing->licenses->infinite; } /** * Gets the license renewal pricing data * * @return null|object */ public function get_renewals_data() { if ( ! isset( $this->pricing->renewals ) || ! is_object( $this->pricing->renewals ) ) { return null; } return $this->pricing->renewals; } /** * Gets the promotion data * * @return null|object */ public function get_promo_data() { if ( ! isset( $this->pricing->promo ) || ! is_object( $this->pricing->promo ) ) { return null; } return $this->pricing->promo; } /** * Checks if a promotion is currently active * * @return boolean */ public function is_promo_active() { $promo_data = $this->get_promo_data(); if ( is_null( $promo_data ) ) { return false; } if ( ! isset( $promo_data->start_date, $promo_data->end_date ) ) { return false; } $current_time = time(); return ( absint( $promo_data->start_date ) < $current_time && absint( $promo_data->end_date ) > $current_time ); } /** * Gets promotion end date * * @return int */ public function get_promo_end() { $promo = $this->get_promo_data(); if ( is_null( $promo ) || ! isset( $promo->end_date ) ) { return 0; } return absint( $promo->end_date ); } /** * Gets the regular upgrade price from single to plus * * @return int */ public function get_regular_single_to_plus_price() { $plus_pricing = $this->get_plus_pricing(); if ( is_null( $plus_pricing ) || ! isset( $plus_pricing->prices->from_single->regular ) ) { return 0; } return $plus_pricing->prices->from_single->regular; } /** * Gets the current upgrade price from single to plus * * @return int */ public function get_single_to_plus_price() { $plus_pricing = $this->get_plus_pricing(); $regular = $this->get_regular_single_to_plus_price(); if ( is_null( $plus_pricing ) || ! isset( $plus_pricing->prices->from_single->sale ) ) { return $regular; } return $this->is_promo_active() ? $plus_pricing->prices->from_single->sale : $regular; } /** * Gets the regular upgrade price from single to infinite * * @return int */ public function get_regular_single_to_infinite_price() { $infinite_pricing = $this->get_infinite_pricing(); if ( is_null( $infinite_pricing ) || ! isset( $infinite_pricing->prices->from_single->regular ) ) { return 0; } return $infinite_pricing->prices->from_single->regular; } /** * Gets the current upgrade price from single to plus * * @return int */ public function get_single_to_infinite_price() { $infinite_pricing = $this->get_infinite_pricing(); $regular = $this->get_regular_single_to_infinite_price(); if ( is_null( $infinite_pricing ) || ! isset( $infinite_pricing->prices->from_single->sale ) ) { return $regular; } return $this->is_promo_active() ? $infinite_pricing->prices->from_single->sale : $regular; } /** * Gets the regular upgrade price from plus to infinite * * @return int */ public function get_regular_plus_to_infinite_price() { $infinite_pricing = $this->get_infinite_pricing(); if ( is_null( $infinite_pricing ) || ! isset( $infinite_pricing->prices->from_plus->regular ) ) { return 0; } return $infinite_pricing->prices->from_plus->regular; } /** * Gets the current upgrade price from plus to infinite * * @return int */ public function get_plus_to_infinite_price() { $infinite_pricing = $this->get_infinite_pricing(); $regular = $this->get_regular_plus_to_infinite_price(); if ( is_null( $infinite_pricing ) || ! isset( $infinite_pricing->prices->from_plus->sale ) ) { return $regular; } return $this->is_promo_active() ? $infinite_pricing->prices->from_plus->sale : $regular; } /** * Gets the number of websites allowed for the single license * * @return int */ public function get_single_websites_count() { $single_pricing = $this->get_single_pricing(); if ( is_null( $single_pricing ) || ! isset( $single_pricing->websites ) ) { return 0; } return (int) $single_pricing->websites; } /** * Gets the number of websites allowed for the plus license * * @return int */ public function get_plus_websites_count() { $plus_pricing = $this->get_plus_pricing(); if ( is_null( $plus_pricing ) || ! isset( $plus_pricing->websites ) ) { return 0; } return (int) $plus_pricing->websites; } /** * Gets the number of websites allowed for the infinite license * * @return int */ public function get_infinite_websites_count() { $infinite_pricing = $this->get_infinite_pricing(); if ( is_null( $infinite_pricing ) || ! isset( $infinite_pricing->websites ) ) { return 0; } return (int) $infinite_pricing->websites; } } Engine/License/Subscriber.php 0000644 00000013431 15174677547 0012200 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\License; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Upgrade instance * * @var Upgrade */ private $upgrade; /** * Renewal instance * * @var Renewal */ private $renewal; /** * Instantiate the class * * @param Upgrade $upgrade Upgrade instance. * @param Renewal $renewal Renewal instance. */ public function __construct( Upgrade $upgrade, Renewal $renewal ) { $this->upgrade = $upgrade; $this->renewal = $renewal; } /** * Events this subscriber listens to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_dashboard_license_info' => 'display_upgrade_section', 'rocket_settings_page_footer' => 'display_upgrade_popin', 'rocket_menu_title' => [ [ 'add_notification_bubble' ], [ 'add_notification_bubble_expired' ], ], 'admin_footer-settings_page_wprocket' => [ [ 'dismiss_notification_bubble' ], [ 'set_dashboard_seen_transient' ], ], 'rocket_before_dashboard_content' => [ [ 'display_promo_banner' ], [ 'display_renewal_soon_banner', 11 ], [ 'display_renewal_expired_banner', 12 ], ], 'wp_ajax_rocket_dismiss_promo' => 'dismiss_promo_banner', 'wp_ajax_rocket_dismiss_renewal' => 'dismiss_renewal_banner', 'rocket_localize_admin_script' => 'add_localize_script_data', 'wp_rocket_upgrade' => [ 'clean_user_transient', 15, 2 ], 'rocket_before_add_field_to_settings' => [ [ 'maybe_disable_ocd', 11 ], [ 'add_license_expire_warning' ], ], 'get_rocket_option_remove_unused_css' => [ 'maybe_disable_option', PHP_INT_MAX ], 'get_rocket_option_async_css' => [ 'maybe_disable_option', PHP_INT_MAX ], ]; } /** * Displays the upgrade section in the license info block * * @since 3.7.3 * * @return void */ public function display_upgrade_section() { $this->upgrade->display_upgrade_section(); } /** * Displays the upgrade popin * * @since 3.7.3 * * @return void */ public function display_upgrade_popin() { $this->upgrade->display_upgrade_popin(); } /** * Adds the notification bubble to the menu title if a promotion is active * * @since 3.7.4 * * @param string $menu_title The text to be used for the menu. * @return string */ public function add_notification_bubble( $menu_title ) { return $this->upgrade->add_notification_bubble( $menu_title ); } /** * Prevents the notification bubble from showing once the user accessed the dashboard once * * @since 3.7.4 * * @return void */ public function dismiss_notification_bubble() { $this->upgrade->dismiss_notification_bubble(); } /** * Displays the promotions banner when a promotion is active * * @since 3.7.4 * * @return void */ public function display_promo_banner() { $this->upgrade->display_promo_banner(); } /** * AJAX callback to dismiss the promotion banner * * @since 3.7.4 * * @return void */ public function dismiss_promo_banner() { $this->upgrade->dismiss_promo_banner(); } /** * Adds the current time and promotion end time to WP Rocket localize script data * * @since 3.7.5 Add the renewal localize data * @since 3.7.4 * * @param array $data Localize script data. * @return array */ public function add_localize_script_data( $data ) { $data = $this->upgrade->add_localize_script_data( $data ); return $this->renewal->add_localize_script_data( $data ); } /** * Deletes the user data transient on 3.7.4 update * * @since 3.7.4 * * @param string $new_version New version of the plugin. * @param string $old_version Installed version of the plugin. * @return void */ public function clean_user_transient( $new_version, $old_version ) { if ( version_compare( $old_version, '3.7.4', '>' ) ) { return; } delete_transient( 'wp_rocket_customer_data' ); } /** * Displays the renewal banner for users expiring in less than 30 days * * @since 3.7.5 * * @return void */ public function display_renewal_soon_banner() { $this->renewal->display_renewal_soon_banner(); } /** * Displays the renewal banner for expired users * * @since 3.7.5 * * @return void */ public function display_renewal_expired_banner() { $this->renewal->display_renewal_expired_banner(); } /** * AJAX callback to dismiss the renewal banner * * @since 3.7.5 * * @return void */ public function dismiss_renewal_banner() { $this->renewal->dismiss_renewal_expired_banner(); } /** * Add license expiring warning to OCD label * * @param array $args Setting field arguments. * * @return array */ public function add_license_expire_warning( $args ): array { return $this->renewal->add_license_expire_warning( $args ); } /** * Adds the notification bubble to WP Rocket menu item when expired * * @param string $menu_title Menu title. * * @return string */ public function add_notification_bubble_expired( $menu_title ) { return $this->renewal->add_expired_bubble( $menu_title ); } /** * Sets the dashboard seen transient to hide the expired bubble * * @return void */ public function set_dashboard_seen_transient() { $this->renewal->set_dashboard_seen_transient(); } /** * Disable optimize CSS delivery setting * * @param array $args Array of setting field arguments. * * @return array */ public function maybe_disable_ocd( $args ) { return $this->renewal->maybe_disable_ocd( $args ); } /** * Disables the RUCSS & Async CSS options if license is expired * * @param mixed $value Current option value. * * @return mixed */ public function maybe_disable_option( $value ) { return $this->renewal->maybe_disable_option( $value ); } } Engine/License/views/upgrade-section.php 0000644 00000000633 15174677547 0014323 0 ustar 00 <?php /** * Upgrade section template. * * @since 3.7.3 */ defined( 'ABSPATH' ) || exit; ?> <p> <?php esc_html_e( 'You can use WP Rocket on more websites by upgrading your license (you will only pay the price difference between your current and new licenses).', 'rocket' ); ?> <button class="wpr-license-upgrade-button wpr-popin-upgrade-toggle"><?php esc_html_e( 'Upgrade now', 'rocket' ); ?></button> </p> Engine/License/views/upgrade-popin.php 0000644 00000003135 15174677547 0014004 0 ustar 00 <?php /** * Upgrade section template. * * @since 3.7.3 * * @var array $data * @var object $this */ defined( 'ABSPATH' ) || exit; ?> <div class="wpr-Popin wpr-Popin-Upgrade"> <div class="wpr-Popin-header"> <h2 class="wpr-title1"><?php esc_html_e( 'Speed Up More Websites', 'rocket' ); ?></h2> <button class="wpr-Popin-close wpr-Popin-Upgrade-close wpr-icon-close"></button> </div> <div class="wpr-Popin-content"> <p> <?php // translators: %1$s = opening strong tag, %2$s = closing strong tag. printf( esc_html__( 'You can use WP Rocket on more websites by upgrading your license. To upgrade, simply pay the %1$sprice difference%2$s between your current and new licenses, as shown below.', 'rocket' ), '<strong>', '</strong>' ); ?> </p> <p> <?php // translators: %1$s = opening strong tag, %2$s = closing strong tag. printf( esc_html__( '%1$sN.B.%2$s: Upgrading your license does not change your expiration date', 'rocket' ), '<strong>', '</strong>' ); ?> </p> <div class="wpr-Popin-flex"> <?php foreach ( $data['upgrades'] as $rocket_upgrade_type => $rocket_upgrade ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->generate( 'upgrade-item', [ 'type' => $rocket_upgrade_type, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'item' => $rocket_upgrade, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'is_promo_active' => $data['is_promo_active'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ] ); } ?> </div> </div> </div> Engine/License/views/renewal-expired-banner-ocd.php 0000644 00000003626 15174677547 0016340 0 ustar 00 <?php /** * Renewal expired banner with OCD. * * @since 3.11.5 */ defined( 'ABSPATH' ) || exit; $data = isset( $data ) ? $data : []; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <section class="rocket-renewal-expired-banner" id="rocket-renewal-banner"> <div class="banner-copy"> <h3 class="rocket-expired-title"><?php esc_html_e( 'You will soon lose access to some features.', 'rocket' ); ?></h3> <div class="rocket-renewal-expired-banner-container"> <div class="rocket-expired-message"> <p> <?php printf( // translators: %1$s = <strong>, %2$s = </strong>. esc_html__( 'You need an %1$sactive license to continue optimizing your CSS delivery%2$s.', 'rocket' ), '<strong>', '</strong>' ); ?> <br> <?php esc_html_e( 'The Remove Unused CSS and Load CSS asynchronously features are great options to address the PageSpeed Insights recommendations and improve your website performance.', 'rocket' ); ?> <br> <?php printf( // translators: %1$s = <strong>, %2$s = </strong>, %3$s = date. esc_html__( 'These features will be %1$sautomatically disabled on %3$s%2$s.', 'rocket' ), '<strong>', '</strong>', esc_html( $data['disabled_date'] ) ); ?> </p> <?php if ( ! empty( $data['message'] ) ) : ?> <p><?php echo $data['message']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p> <?php endif; ?> </div> </div> </div> <div class="rocket-expired-cta-container"> <a href="<?php echo esc_url( $data['renewal_url'] ); ?>" class="rocket-renew-cta" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Renew now', 'rocket' ); ?></a> </div> <button class="wpr-notice-close wpr-icon-close" id="rocket-dismiss-renewal"><span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice', 'rocket' ); ?></span></button> </section> Engine/License/views/renewal-expired-banner.php 0000644 00000002650 15174677547 0015571 0 ustar 00 <?php /** * Renewal expired banner. * * @since 3.7.5 */ defined( 'ABSPATH' ) || exit; $data = isset( $data ) ? $data : []; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <section class="rocket-renewal-expired-banner" id="rocket-renewal-banner"> <div class="banner-copy"> <h3 class="rocket-expired-title"><?php esc_html_e( 'Your WP Rocket license is expired!', 'rocket' ); ?></h3> <div class="rocket-renewal-expired-banner-container"> <div class="rocket-expired-message"> <p> <?php printf( // translators: %1$s = <strong>, %2$s = </strong>. esc_html__( 'Your website could be much faster if it could take advantage of our %1$snew features and enhancements%2$s. 🚀', 'rocket' ), '<strong>', '</strong>' ); ?> </p> <?php if ( ! empty( $data['message'] ) ) : ?> <p><?php echo $data['message']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p> <?php endif; ?> </div> </div> </div> <div class="rocket-expired-cta-container"> <a href="<?php echo esc_url( $data['renewal_url'] ); ?>" class="rocket-renew-cta" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Renew now', 'rocket' ); ?></a> </div> <button class="wpr-notice-close wpr-icon-close" id="rocket-dismiss-renewal"><span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice', 'rocket' ); ?></span></button> </section> Engine/License/views/renewal-soon-banner.php 0000644 00000003546 15174677547 0015114 0 ustar 00 <?php /** * Renewal soon banner. * * @since 3.7.5 */ defined( 'ABSPATH' ) || exit; $data = isset( $data ) ? $data : []; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <div class="rocket-renewal-banner"> <ul class="rocket-promo-countdown" id="rocket-renew-countdown"> <li class="rocket-countdown-item"><span class="rocket-countdown-value rocket-countdown-days"><?php echo esc_html( $data['countdown']['days'] ); ?></span> <?php esc_html_e( 'Days', 'rocket' ); ?></li> <li class="rocket-countdown-item"><span class="rocket-countdown-value rocket-countdown-hours"><?php echo esc_html( $data['countdown']['hours'] ); ?></span> <?php esc_html_e( 'Hours', 'rocket' ); ?></li> <li class="rocket-countdown-item"><span class="rocket-countdown-value rocket-countdown-minutes"><?php echo esc_html( $data['countdown']['minutes'] ); ?></span> <?php esc_html_e( 'Minutes', 'rocket' ); ?></li> <li class="rocket-countdown-item"><span class="rocket-countdown-value rocket-countdown-seconds"><?php echo esc_html( $data['countdown']['seconds'] ); ?></span> <?php esc_html_e( 'Seconds', 'rocket' ); ?></li> </ul> <div class="rocket-renew-message"> <p> <?php printf( // translators: %1$s = <strong>, %2$s = </strong>. esc_html__( 'Your %1$sWP Rocket license is about to expire%2$s: you will soon lose access to product updates and support.', 'rocket' ), '<strong>', '</strong>' ); ?> </p> <?php if ( isset( $data['more_info'] ) && $data['more_info'] ) : ?> <p><?php echo $data['message']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p> <?php endif; ?> </div> <div class="rocket-renew-cta-container"> <a href="<?php echo esc_url( $data['renewal_url'] ); ?>" class="rocket-renew-cta" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Renew now', 'rocket' ); ?></a> </div> </div> Engine/License/views/promo-banner.php 0000644 00000004117 15174677547 0013632 0 ustar 00 <?php /** * Promo banner. * * @since 3.7.4 */ defined( 'ABSPATH' ) || exit; $data = isset( $data ) ? $data : []; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <div class="rocket-promo-banner" id="rocket-promo-banner"> <div> <h3 class="rocket-promo-title"> <span class="rocket-promo-discount"> <?php // translators: %s = promotion discount percentage. printf( esc_html__( '%s off', 'rocket' ), esc_html( $data['discount_percent'] . '%' ) ); ?> </span> <?php // translators: %s = promotion name. printf( esc_html__( '%s promotion is live!', 'rocket' ), esc_html( $data['name'] ) ); ?> </h3> <p class="rocket-promo-message"><?php echo wp_kses_post( $data['message'] ); ?></p> </div> <div class="rocket-promo-cta-block"> <p class="rocket-promo-deal"><?php esc_html_e( 'Hurry Up! Deal ends in:', 'rocket' ); ?></p> <ul class="rocket-promo-countdown" id="rocket-promo-countdown"> <li class="rocket-countdown-item"><span class="rocket-countdown-value rocket-countdown-days"><?php echo esc_html( $data['countdown']['days'] ); ?></span> <?php esc_html_e( 'Days', 'rocket' ); ?></li> <li class="rocket-countdown-item"><span class="rocket-countdown-value rocket-countdown-hours"><?php echo esc_html( $data['countdown']['hours'] ); ?></span> <?php esc_html_e( 'Hours', 'rocket' ); ?></li> <li class="rocket-countdown-item"><span class="rocket-countdown-value rocket-countdown-minutes"><?php echo esc_html( $data['countdown']['minutes'] ); ?></span> <?php esc_html_e( 'Minutes', 'rocket' ); ?></li> <li class="rocket-countdown-item"><span class="rocket-countdown-value rocket-countdown-seconds"><?php echo esc_html( $data['countdown']['seconds'] ); ?></span> <?php esc_html_e( 'Seconds', 'rocket' ); ?></li> </ul> <button class="rocket-promo-cta wpr-popin-upgrade-toggle"><?php esc_html_e( 'Upgrade now', 'rocket' ); ?></button> </div> <button class="wpr-notice-close wpr-icon-close" id="rocket-dismiss-promotion"><span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice', 'rocket' ); ?></span></button> </div> Engine/License/views/renewal-expired-banner-ocd-disabled.php 0000644 00000003172 15174677547 0020101 0 ustar 00 <?php /** * Renewal expired banner with OCD disabled. * * @since 3.11.5 */ defined( 'ABSPATH' ) || exit; $data = isset( $data ) ? $data : []; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <section class="rocket-renewal-expired-banner" id="rocket-renewal-banner"> <div class="banner-copy"> <h3 class="rocket-expired-title"><?php esc_html_e( 'The Optimize CSS Delivery feature is disabled.', 'rocket' ); ?></h3> <div class="rocket-renewal-expired-banner-container"> <div class="rocket-expired-message"> <p> <?php esc_html_e( 'You can no longer use the Remove Unused CSS or Load CSS asynchronously options.', 'rocket' ); ?> <br> <?php printf( // translators: %1$s = <strong>, %2$s = </strong>. esc_html__( 'You need an %1$sactive license%2$s to keep optimizing your CSS delivery, which addresses a PageSpeed Insights recommendation and improves your page performance.', 'rocket' ), '<strong>', '</strong>' ); ?> </p> <?php if ( ! empty( $data['message'] ) ) : ?> <p><?php echo $data['message']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p> <?php endif; ?> </div> </div> </div> <div class="rocket-expired-cta-container"> <a href="<?php echo esc_url( $data['renewal_url'] ); ?>" class="rocket-renew-cta" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Renew now', 'rocket' ); ?></a> </div> <button class="wpr-notice-close wpr-icon-close" id="rocket-dismiss-renewal"><span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice', 'rocket' ); ?></span></button> </section> Engine/License/views/upgrade-item.php 0000644 00000006027 15174677547 0013620 0 ustar 00 <?php /** * Upgrade item template. * * @var array $data */ defined( 'ABSPATH' ) || exit; $rocket_initial_item = 'stacked' === $data['type'] ? reset( $data['item'] ) : $data['item']; ?> <div class="wpr-upgrade-item wpr-Upgrade-<?php echo esc_attr( $rocket_initial_item['name'] ); ?>"> <?php if ( $data['is_promo_active'] ) { ?> <div class="wpr-upgrade-saving"> <?php // translators: %1$s = span opening tag, %2$s = price, %3$s = span closing tag. printf( esc_html__( 'Save $%1$s%2$s%3$s', 'rocket' ), '<span>', esc_html( $rocket_initial_item['saving'] ), '</span>' ); ?> </div> <?php } ?> <h3 class="wpr-upgrade-title"><?php echo esc_html( $rocket_initial_item['name'] ); ?></h3> <div class="wpr-upgrade-prices"><span class="wpr-upgrade-price-symbol">$</span> <span class="wpr-upgrade-price-value"><?php echo esc_html( $rocket_initial_item['price'] ); ?></span> <?php if ( $data['is_promo_active'] ) { ?> <del class="wpr-upgrade-price-regular">$ <span><?php echo esc_html( $rocket_initial_item['regular_price'] ); ?></span></del> <?php } ?> </div> <div class="wpr-upgrade-websites <?php if ( 'stacked' !== $data['type'] ) { ?> notstacked<?php } ?>"> <?php if ( 'stacked' === $data['type'] && 1 < count( $data['item'] ) ) { ?> <div class="custom-select" id="rocket_stacked_select"> <button class="select-button" role="combobox" aria-label="select button" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-dropdown"> <span class="selected-value has-style-bold"><?php echo esc_html( $rocket_initial_item['websites'] ) . ' ' . esc_html__( 'Websites', 'rocket' ); ?></span> <span class="custom-select-arrow"></span> </button> <ul class="select-dropdown" role="listbox" id="select-dropdown"> <?php foreach ( $data['item'] as $rocket_stacked_item_key => $rocket_stacked ) { ?> <li role="option" data-name="<?php echo esc_attr( $rocket_stacked['name'] ); ?>" data-price="<?php echo esc_attr( $rocket_stacked['price'] ); ?>" data-url="<?php echo esc_url( $rocket_stacked['upgrade_url'] ); ?>" <?php if ( $data['is_promo_active'] ) { ?> data-saving="<?php echo esc_attr( $rocket_stacked['saving'] ); ?>" data-regular-price="<?php echo esc_attr( $rocket_stacked['regular_price'] ); ?>" <?php } ?> > <input type="radio" id="plan_<?php echo esc_attr( $rocket_stacked_item_key ); ?>" name="multi-plans"/> <label for="multi50"><?php echo esc_html( $rocket_stacked['websites'] ) . ' ' . esc_html__( 'Websites', 'rocket' ); ?></label> </li> <?php } ?> </ul> </div> <?php } else { ?> <?php // translators: %s = number of websites. printf( esc_html__( '%s websites', 'rocket' ), esc_html( $rocket_initial_item['websites'] ) ); ?> <?php } ?> </div> <a href="<?php echo esc_url( $rocket_initial_item['upgrade_url'] ); ?>" class="wpr-upgrade-link" target="_blank" rel="noopener noreferrer"> <?php // translators: %s = license name. printf( esc_html__( 'Upgrade to %s', 'rocket' ), esc_html( $rocket_initial_item['name'] ) ); ?> </a> </div> Engine/License/ServiceProvider.php 0000644 00000003730 15174677547 0013211 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\License; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\License\API\{PricingClient, Pricing, UserClient, User}; use WP_Rocket\Engine\License\{Renewal, Upgrade, Subscriber}; /** * Service Provider for the License module */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'pricing_client', 'user_client', 'pricing', 'user', 'upgrade', 'renewal', 'license_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $views = new StringArgument( __DIR__ . '/views' ); $this->getContainer()->add( 'pricing_client', PricingClient::class ); $this->getContainer()->add( 'user_client', UserClient::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'pricing', Pricing::class ) ->addArgument( $this->getContainer()->get( 'pricing_client' )->get_pricing_data() ); $this->getContainer()->addShared( 'user', User::class ) ->addArgument( $this->getContainer()->get( 'user_client' )->get_user_data() ); $this->getContainer()->add( 'upgrade', Upgrade::class ) ->addArguments( [ 'pricing', 'user', $views, ] ); $this->getContainer()->add( 'renewal', Renewal::class ) ->addArguments( [ 'pricing', 'user', 'options', $views, ] ); $this->getContainer()->addShared( 'license_subscriber', Subscriber::class ) ->addArguments( [ 'upgrade', 'renewal', ] ); } } Engine/License/Renewal.php 0000644 00000037231 15174677547 0011476 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\License; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\License\API\Pricing; use WP_Rocket\Engine\License\API\User; class Renewal extends Abstract_Render { /** * Pricing instance * * @var Pricing */ private $pricing; /** * User instance * * @var User */ private $user; /** * Options_Data instance * * @var Options_Data */ private $options; /** * Instantiate the class * * @param Pricing $pricing Pricing instance. * @param User $user User instance. * @param Options_Data $options Options_Data instance. * @param string $template_path Path to the views. */ public function __construct( Pricing $pricing, User $user, Options_Data $options, $template_path ) { parent::__construct( $template_path ); $this->pricing = $pricing; $this->user = $user; $this->options = $options; } /** * Displays the renewal banner for users expiring in less than 30 days * * @since 3.7.5 * * @return void */ public function display_renewal_soon_banner() { if ( rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT' ) ) { return; } if ( $this->user->is_license_expired() ) { return; } if ( ! $this->is_expired_soon() ) { return; } $data = $this->get_banner_data(); $data['countdown'] = $this->get_countdown_data(); $data['more_info'] = true; if ( -1 === $this->user->get_license_type() ) { $data['message'] = sprintf( // translators: %1$s WP Rocket plugin name. esc_html__( 'Your %1$s%2$s license is about to expire%3$s: you will soon lose access to product updates and support.', 'rocket' ), '<strong>', WP_ROCKET_PLUGIN_NAME, '</strong>' ); $data['more_info'] = false; } echo $this->generate( 'renewal-soon-banner', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Displays the renewal banner for expired users * * @since 3.7.5 * * @return void */ public function display_renewal_expired_banner() { if ( rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT' ) ) { return; } if ( 0 === $this->user->get_license_expiration() ) { return; } if ( ! $this->user->is_license_expired() ) { return; } if ( false !== get_transient( 'rocket_renewal_banner_' . get_current_user_id() ) ) { return; } $expiration = $this->user->get_license_expiration(); $expired_since = ( time() - $expiration ) / DAY_IN_SECONDS; if ( $this->user->is_auto_renew() && 4 > $expired_since ) { return; } $ocd_enabled = $this->options->get( 'optimize_css_delivery', 0 ); $renewal_url = $this->user->get_renewal_url(); $message = null; if ( $ocd_enabled ) { if ( 15 > $expired_since ) { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->generate( 'renewal-expired-banner-ocd', [ 'renewal_url' => $renewal_url, 'message' => $message, 'disabled_date' => date_i18n( get_option( 'date_format' ), $expiration + 15 * DAY_IN_SECONDS ), ] ); // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped } elseif ( 90 > $expired_since ) { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->generate( 'renewal-expired-banner-ocd-disabled', [ 'renewal_url' => $renewal_url, 'message' => $message, ] ); // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped } elseif ( 90 < $expired_since ) { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->generate( 'renewal-expired-banner', [ 'renewal_url' => $renewal_url, 'message' => $message, ] ); // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped } } elseif ( ! $ocd_enabled ) { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->generate( 'renewal-expired-banner', [ 'renewal_url' => $renewal_url, 'message' => $message, ] ); // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Get base data to display in the banners * * @since 3.7.5 * * @return array */ private function get_banner_data() { $price = esc_html( '$' . number_format_i18n( $this->get_price(), 2 ) ); $message = sprintf( // translators: %1$s = <strong>, %2$s = </strong>, %3$s = discount price. esc_html__( 'Renew before it is too late, you will pay %1$s%3$s%2$s.', 'rocket' ), '<strong>', '</strong>', $price ); if ( $this->is_grandfather() ) { $message = sprintf( // translators: %1$s = <strong>, %2$s = discount percentage, %3$s = </strong>, %4$s = discount price. esc_html__( 'Renew with a %1$s%2$s discount%3$s before it is too late, you will only pay %1$s%4$s%3$s!', 'rocket' ), '<strong>', esc_html( $this->get_discount_percent() . '%' ), '</strong>', $price ); } return [ 'message' => $message, 'renewal_url' => $this->user->get_renewal_url(), ]; } /** * AJAX callback to dismiss the renewal banner for expired users * * @since 3.7.5 * * @return void */ public function dismiss_renewal_expired_banner() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } $transient = 'rocket_renewal_banner_' . get_current_user_id(); if ( false !== get_transient( $transient ) ) { return; } set_transient( $transient, 1, MONTH_IN_SECONDS ); wp_send_json_success(); } /** * Adds the license expiration time to WP Rocket localize script data * * @since 3.7.5 * * @param array $data Localize script data. * @return array */ public function add_localize_script_data( array $data ) { if ( $this->user->is_license_expired() ) { return $data; } if ( ! $this->is_expired_soon() ) { return $data; } $data['license_expiration'] = $this->user->get_license_expiration(); return $data; } /** * Checks if the license expires in less than 30 days * * @since 3.7.5 * * @return boolean */ private function is_expired_soon() { if ( $this->user->is_auto_renew() ) { return false; } $expiration_delay = $this->user->get_license_expiration() - time(); return 30 * DAY_IN_SECONDS > $expiration_delay; } /** * Gets the discount corresponding to the current user status * * @since 3.7.5 * * @return int * @phpstan-ignore-next-line */ private function get_discount_percent() { $prices = $this->get_license_pricing_data(); $renewals = $this->get_user_renewal_status(); if ( ! isset( $prices->prices, $prices->prices->renewal ) ) { return 0; } $prices = $prices->prices; if ( $renewals['is_grandfather'] ) { return $renewals['discount_percent']->is_grandfather; } return 0; } /** * Is user grandfathered * * @return bool */ private function is_grandfather(): bool { $renewals = $this->get_user_renewal_status(); return key_exists( 'is_grandfather', $renewals ) && $renewals['is_grandfather']; } /** * Is user grandmothered * * @return bool * @phpstan-ignore-next-line */ private function has_grandmother(): bool { $renewals = $this->get_user_renewal_status(); return key_exists( 'is_grandmother', $renewals ) && $renewals['is_grandmother']; } /** * Gets the price corresponding to the current user status * * @since 3.7.5 * * @return int */ private function get_price() { $renewals = $this->get_user_renewal_status(); $license = $this->get_license_pricing_data(); if ( $renewals['is_grandfather'] && ! $renewals['is_expired'] ) { return isset( $license->prices->renewal->is_grandfather ) ? $license->prices->renewal->is_grandfather : 0; } if ( $renewals['is_grandmother'] && ! $renewals['is_expired'] ) { return isset( $license->prices->renewal->is_grandmother ) ? $license->prices->renewal->is_grandmother : 0; } return isset( $license->prices->renewal->not_grandfather ) ? $license->prices->renewal->not_grandfather : 0; } /** * Gets the user renewal status * * @since 3.7.5 * * @return array */ private function get_user_renewal_status(): array { $renewals = $this->pricing->get_renewals_data(); if ( ! isset( $renewals->extra_days, $renewals->grandfather_date, $renewals->discount_percent, $renewals->grandmother_date ) ) { return []; } return [ 'discount_percent' => $renewals->discount_percent, 'is_expired' => time() > ( $this->user->get_license_expiration() + ( $renewals->extra_days * DAY_IN_SECONDS ) ), 'is_grandfather' => $renewals->grandfather_date > $this->user->get_creation_date(), 'is_grandmother' => $renewals->grandmother_date > $this->user->get_creation_date(), ]; } /** * Gets the license pricing data corresponding to the user license * * @since 3.7.5 * * @return object|null */ private function get_license_pricing_data() { $license = $this->user->get_license_type(); $plus_websites = $this->pricing->get_plus_websites_count(); if ( $license === $plus_websites ) { return $this->pricing->get_plus_pricing(); } elseif ( $license >= $this->pricing->get_single_websites_count() && $license < $plus_websites ) { return $this->pricing->get_single_pricing(); } return $this->pricing->get_infinite_pricing(); } /** * Gets the countdown data to display for the renewal soon banner * * @since 3.7.5 * * @return array */ private function get_countdown_data() { $data = [ 'days' => 0, 'hours' => 0, 'minutes' => 0, 'seconds' => 0, ]; if ( rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ) { return $data; } $expiration = $this->user->get_license_expiration(); if ( 0 === $expiration ) { return $data; } $now = date_create(); $end = date_timestamp_set( date_create(), $expiration ); if ( $now > $end ) { return $data; } $remaining = date_diff( $now, $end ); $format = explode( ' ', $remaining->format( '%d %H %i %s' ) ); $data['days'] = $format[0]; $data['hours'] = $format[1]; $data['minutes'] = $format[2]; $data['seconds'] = $format[3]; return $data; } /** * Add license expiring warning to OCD label * * @param array $args Setting field arguments. * * @return array */ public function add_license_expire_warning( $args ): array { if ( 'optimize_css_delivery' !== $args['id'] ) { return $args; } if ( ! $this->user->is_license_expired() ) { return $args; } $ocd = $this->options->get( 'optimize_css_delivery', 0 ); $whitelabel = rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT', false ); $expired_since = ( time() - $this->user->get_license_expiration() ) / DAY_IN_SECONDS; $message = ' <span class="wpr-icon-important wpr-checkbox-warning">'; if ( ( $whitelabel && 15 > $expired_since && $ocd ) || ( ! $whitelabel && $this->user->is_auto_renew() && 4 > $expired_since ) || ( $whitelabel && $this->user->is_auto_renew() && 4 > $expired_since && ! $ocd ) ) { return $args; } elseif ( ! $whitelabel && 15 > $expired_since && $ocd ) { $message .= sprintf( // translators: %1$s = <a>, %2$s = </a>. __( 'You need a valid license to continue using this feature. %1$sRenew now%2$s before losing access.', 'rocket' ), '<a href="' . esc_url( $this->user->get_renewal_url() ) . '" target="_blank">', '</a>' ); } elseif ( ( ! $whitelabel && 15 < $expired_since ) || ( ! $whitelabel && 15 > $expired_since && ! $ocd ) ) { $message .= sprintf( // translators: %1$s = <a>, %2$s = </a>. __( 'You need an active license to enable this option. %1$sRenew now%2$s.', 'rocket' ), '<a href="' . esc_url( $this->user->get_renewal_url() ) . '" target="_blank">', '</a>' ); } elseif ( ( $whitelabel && 15 < $expired_since ) || ( $whitelabel && 15 > $expired_since && ! $ocd ) ) { $doc = 'https://docs.wp-rocket.me/article/1711-what-happens-if-my-license-expires'; $locale = current( array_slice( explode( '_', get_user_locale() ), 0, 1 ) ); if ( 'fr' === $locale ) { $doc = 'https://fr.docs.wp-rocket.me/article/1712-que-se-passe-t-il-si-ma-licence-expire'; } $message .= sprintf( // translators: %1$s = <a>, %2$s = </a>. __( 'You need an active license to enable this option. %1$sMore info%2$s.', 'rocket' ), '<a href="' . $doc . '?utm_source=wp_plugin&utm_medium=wp_rocket" target="_blank">', '</a>' ); } $message .= '</span>'; $args['label'] = $args['label'] . $message; return $args; } /** * Adds the notification bubble to WP Rocket menu item when expired * * @param string $menu_title Menu title. * * @return string */ public function add_expired_bubble( $menu_title ): string { if ( rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT', false ) ) { return $menu_title; } if ( ! $this->user->is_license_expired() ) { return $menu_title; } if ( false !== get_transient( 'wpr_dashboard_seen_' . get_current_user_id() ) ) { return $menu_title; } $expired_since = ( time() - $this->user->get_license_expiration() ) / DAY_IN_SECONDS; $auto_renew = $this->user->is_auto_renew(); $ocd_enabled = $this->options->get( 'optimize_css_delivery', 0 ); if ( $ocd_enabled && $auto_renew && 4 > $expired_since ) { return $menu_title; } if ( ! $auto_renew && ! $ocd_enabled && 4 < $expired_since ) { return $menu_title; } if ( $auto_renew && ! $ocd_enabled && ( 4 > $expired_since || 15 < $expired_since ) ) { return $menu_title; } return $menu_title . ' <span class="awaiting-mod">!</span>'; } /** * Sets the dashboard seen transient to hide the expired bubble * * @return void */ public function set_dashboard_seen_transient() { if ( ! $this->user->is_license_expired() ) { return; } if ( ! $this->options->get( 'optimize_css_delivery', 0 ) ) { return; } $current_user = get_current_user_id(); if ( false !== get_transient( "wpr_dashboard_seen_{$current_user}" ) ) { return; } $expired_since = ( time() - $this->user->get_license_expiration() ) / DAY_IN_SECONDS; if ( 15 > $expired_since ) { set_transient( "wpr_dashboard_seen_{$current_user}", 1, 15 * DAY_IN_SECONDS ); } elseif ( 15 < $expired_since ) { set_transient( "wpr_dashboard_seen_{$current_user}", 1, YEAR_IN_SECONDS ); } } /** * Disable optimize CSS delivery setting * * @param array $args Array of setting field arguments. * * @return array */ public function maybe_disable_ocd( $args ) { if ( 'optimize_css_delivery' !== $args['id'] ) { return $args; } if ( ! $this->user->is_license_expired() ) { return $args; } $expired_since = ( time() - $this->user->get_license_expiration() ) / DAY_IN_SECONDS; if ( 15 > $expired_since || ( $this->user->is_auto_renew() && 4 > $expired_since ) ) { return $args; } $args['value'] = 0; if ( isset( $args['container_class'] ) && ! in_array( 'wpr-isDisabled', $args['container_class'], true ) ) { $args['container_class'][] = 'wpr-isDisabled'; } $args['input_attr']['disabled'] = 1; return $args; } /** * Disables the RUCSS & Async CSS options if license is expired since more than 15 days * * @param mixed $value Current option value. * * @return mixed */ public function maybe_disable_option( $value ) { $expired_since = ( time() - $this->user->get_license_expiration() ) / DAY_IN_SECONDS; if ( 15 > $expired_since ) { return $value; } return 0; } } Engine/License/Upgrade.php 0000644 00000020524 15174677547 0011465 0 ustar 00 <?php namespace WP_Rocket\Engine\License; use WP_Rocket\Abstract_Render; use WP_Rocket\Engine\License\API\Pricing; use WP_Rocket\Engine\License\API\User; class Upgrade extends Abstract_Render { /** * Pricing instance * * @var Pricing */ private $pricing; /** * User instance * * @var User */ private $user; /** * Instantiate the class * * @param Pricing $pricing Pricing instance. * @param User $user User instance. * @param string $template_path Path to the views. */ public function __construct( Pricing $pricing, User $user, $template_path ) { parent::__construct( $template_path ); $this->pricing = $pricing; $this->user = $user; } /** * Displays the upgrade section in the license block on the dashboard * * @return void */ public function display_upgrade_section() { if ( ! $this->can_upgrade() ) { return; } echo $this->generate( 'upgrade-section' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Displays the upgrade pop on the dashboard * * @return void */ public function display_upgrade_popin() { if ( rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT' ) ) { return; } if ( ! $this->can_upgrade() ) { return; } $data = [ 'is_promo_active' => $this->pricing->is_promo_active(), 'upgrades' => $this->get_upgrade_choices(), ]; echo $this->generate( 'upgrade-popin', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Adds the notification bubble to WP Rocket menu item when a promo is active * * @param string $menu_title Menu title. * @return string */ public function add_notification_bubble( $menu_title ) { if ( ! $this->can_use_promo() ) { return $menu_title; } if ( false !== get_transient( 'rocket_promo_seen_' . get_current_user_id() ) ) { return $menu_title; } return $menu_title . ' <span class="rocket-promo-bubble">!</span>'; } /** * Prevents the notification bubble from showing once the user accessed the dashboard once * * @return void */ public function dismiss_notification_bubble() { if ( ! $this->can_use_promo() ) { return; } $user_id = get_current_user_id(); if ( false !== get_transient( "rocket_promo_seen_{$user_id}" ) ) { return; } set_transient( "rocket_promo_seen_{$user_id}", 1, 2 * WEEK_IN_SECONDS ); } /** * Displays the promotion banner * * @return void */ public function display_promo_banner() { if ( ! $this->can_use_promo() ) { return; } if ( false !== get_transient( 'rocket_promo_banner_' . get_current_user_id() ) ) { return; } $promo = $this->pricing->get_promo_data(); $promo_name = isset( $promo->name ) ? $promo->name : ''; $promo_discount = isset( $promo->discount_percent ) ? $promo->discount_percent : 0; $data = [ 'name' => $promo_name, 'discount_percent' => $promo_discount, 'countdown' => $this->get_countdown_data(), 'message' => $this->get_promo_message( $promo_name, $promo_discount ), ]; echo $this->generate( 'promo-banner', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * AJAX callback to dismiss the promotion banner * * @return void */ public function dismiss_promo_banner() { check_ajax_referer( 'rocket-ajax', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } $user = get_current_user_id(); if ( false !== get_transient( "rocket_promo_banner_{$user}" ) ) { return; } set_transient( "rocket_promo_banner_{$user}", 1, 2 * WEEK_IN_SECONDS ); wp_send_json_success(); } /** * Adds the promotion end time to WP Rocket localize script data * * @since 3.7.4 * * @param array $data Localize script data. * @return array */ public function add_localize_script_data( array $data ) { if ( ! $this->can_use_promo() ) { return $data; } $data['promo_end'] = $this->pricing->get_promo_end(); return $data; } /** * Returns an array containing the remaining days, hours, minutes & seconds for the promotion * * @since 3.7.4 * * @return array */ private function get_countdown_data() { $data = [ 'days' => 0, 'hours' => 0, 'minutes' => 0, 'seconds' => 0, ]; if ( rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ) { return $data; } $promo_end = $this->pricing->get_promo_end(); if ( 0 === $promo_end ) { return $data; } $now = date_create(); $end = date_timestamp_set( date_create(), $promo_end ); if ( $now > $end ) { return $data; } $remaining = date_diff( $now, $end ); $format = explode( ' ', $remaining->format( '%d %H %i %s' ) ); $data['days'] = $format[0]; $data['hours'] = $format[1]; $data['minutes'] = $format[2]; $data['seconds'] = $format[3]; return $data; } /** * Get upgrade types * * @return array */ private function get_upgrade_types(): array { $types = []; foreach ( $this->get_upgrade_choices() as $choice_key => $choice ) { $types[] = 'stacked' === $choice_key ? end( $choice )['name'] : $choice['name']; } return $types; } /** * Returns the promotion message to display in the banner * * @param string $promo_name Name of the promotion. * @param int $promo_discount Discount percentage. * * @return string */ private function get_promo_message( $promo_name = '', $promo_discount = 0 ) { $license_types = $this->get_upgrade_types(); return sprintf( // translators: %1$s = promotion name, %2$s = <br>, %3$s = <strong>, %4$s = promotion discount percentage, %5$s = </strong>, %6$s = Growth or Multi. esc_html__( 'Take advantage of %1$s to speed up more websites:%2$s get a %3$s%4$s off%5$s for %3$supgrading your license to %6$s!%5$s', 'rocket' ), $promo_name, '<br>', '<strong>', $promo_discount . '%', '</strong>', implode( ' ' . esc_html__( 'or', 'rocket' ) . ' ', $license_types ), ); } /** * Checks if current user can use the promotion * * @since 3.7.4 * * @return boolean */ private function can_use_promo() { if ( rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT' ) ) { return false; } if ( ! $this->can_upgrade() ) { return false; } if ( $this->is_expired_soon() ) { return false; } if ( $this->is_new_user() ) { return false; } return $this->pricing->is_promo_active(); } /** * Checks if the license expires in less than 30 days * * @return boolean */ private function is_expired_soon() { $expiration_delay = $this->user->get_license_expiration() - time(); return 30 * DAY_IN_SECONDS > $expiration_delay; } /** * Checks if the User license bought less than 14 days * * @return boolean */ private function is_new_user() { return ( 14 * DAY_IN_SECONDS ) > time() - $this->user->get_creation_date(); } /** * Checks if the current license can upgrade * * @return boolean */ private function can_upgrade() { return ( ! $this->user->is_license_expired() && ! empty( $this->user->get_available_upgrades() ) ); } /** * Gets the upgrade choices depending on the current license level * * @return array */ private function get_upgrade_choices() { $choices = []; foreach ( $this->user->get_available_upgrades() as $available_upgrade ) { $upgrade_data = $this->get_generic_upgrade_data( $available_upgrade ); if ( ! empty( $available_upgrade->stack ) && ! empty( $available_upgrade->slug ) ) { if ( ! isset( $choices['stacked'] ) ) { $choices['stacked'] = []; } $choices['stacked'][ $available_upgrade->slug ] = $upgrade_data; continue; } $choices[ $available_upgrade->slug ] = $upgrade_data; } return $choices; } /** * Prepare the upgrade array based on the upgrade object from the API. * * @param object $upgrade_item Upgrade item object from the API. * @return array */ private function get_generic_upgrade_data( $upgrade_item ) { $data = [ 'name' => $upgrade_item->name, 'price' => $this->pricing->is_promo_active() ? $upgrade_item->saving : $upgrade_item->regular_price, 'websites' => $upgrade_item->websites, 'upgrade_url' => $upgrade_item->upgrade_url, ]; if ( $this->pricing->is_promo_active() ) { $data['saving'] = $upgrade_item->regular_price - $upgrade_item->saving; $data['regular_price'] = $upgrade_item->regular_price; } return $data; } } Engine/Admin/API/Subscriber.php 0000644 00000004065 15174677547 0012262 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\API; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { const ROUTE_NAMESPACE = 'wp-rocket/v1'; /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rest_api_init' => 'register_route', 'admin_enqueue_scripts' => [ 'enqueue_url', 999 ], ]; } /** * Enqueue the URL for option exporting. * * @return void */ public function enqueue_url() { wp_localize_script( 'wpr-admin-common', 'rocket_option_export', [ 'rest_url_option_export' => wp_nonce_url( admin_url( 'admin-post.php?action=rocket_export' ), 'rocket_export' ), ] ); } /** * Register REST route. * * @return void */ public function register_route() { register_rest_route( self::ROUTE_NAMESPACE, 'options/export', [ 'methods' => 'GET', 'callback' => [ $this, 'export_options' ], 'permission_callback' => [ $this, 'has_permissions' ], ] ); } /** * Export options. * * @return void */ public function export_options() { list( $filename, $options ) = rocket_export_options(); nocache_headers(); @header( 'Content-Type: application/json' ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @header( 'Content-Transfer-Encoding: binary' ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @header( 'Content-Length: ' . strlen( $options ) ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @header( 'Connection: close' ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged echo $options; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped exit(); } /** * Has permission to use the API route. * * @return bool */ public function has_permissions() { return current_user_can( 'rocket_manage_options' ); } } Engine/Admin/API/ServiceProvider.php 0000644 00000001431 15174677547 0013264 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Admin\API; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'admin_api_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the option array in the container * * @return void */ public function register(): void { $this->getContainer()->add( 'admin_api_subscriber', Subscriber::class ); } } Engine/Admin/ServiceProvider.php 0000644 00000004000 15174677547 0012646 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Admin; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Engine\Admin\Deactivation\{DeactivationIntent, Subscriber}; use WP_Rocket\Engine\Admin\Metaboxes\PostEditOptionsSubscriber; use WP_Rocket\ThirdParty\Plugins\Optimization\Hummingbird; /** * Service Provider for admin subscribers. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'deactivation_intent', 'deactivation_intent_subscriber', 'hummingbird_subscriber', 'actionscheduler_admin_subscriber', 'post_edit_options_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'deactivation_intent', DeactivationIntent::class ) ->addArguments( [ new StringArgument( $this->getContainer()->get( 'template_path' ) . '/deactivation-intent' ), 'options_api', 'options', ] ); $this->getContainer()->addShared( 'deactivation_intent_subscriber', Subscriber::class ) ->addArgument( $this->getContainer()->get( 'deactivation_intent' ) ); $this->getContainer()->addShared( 'hummingbird_subscriber', Hummingbird::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'actionscheduler_admin_subscriber', ActionSchedulerSubscriber::class ); $this->getContainer()->addShared( 'post_edit_options_subscriber', PostEditOptionsSubscriber::class ) ->addArguments( [ 'options', new StringArgument( $this->getContainer()->get( 'template_path' ) . '/metaboxes' ), ] ); } } Engine/Admin/DomainChange/Subscriber.php 0000644 00000013333 15174677547 0014164 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\DomainChange; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Engine\Common\Ajax\AjaxHandler; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Handle basic ajax operations. * * @var AjaxHandler */ protected $ajax_handler; /** * Beacon instance * * @var Beacon */ protected $beacon; /** * Name of the option saving the last base URL. * * @var string */ const LAST_BASE_URL_OPTION = 'wp_rocket_last_base_url'; /** * Instantiate the class. * * @param AjaxHandler $ajax_handler Handle basic ajax operations. * @param Beacon $beacon Beacon instance. */ public function __construct( AjaxHandler $ajax_handler, Beacon $beacon ) { $this->ajax_handler = $ajax_handler; $this->beacon = $beacon; } /** * Return an array of events that this subscriber wants to listen to. * * @return string[] */ public static function get_subscribed_events() { return [ 'admin_init' => 'maybe_launch_domain_changed', 'admin_notices' => 'maybe_display_domain_change_notice', 'rocket_domain_changed' => 'maybe_clean_cache_domain_change', 'rocket_notice_args' => 'add_regenerate_configuration_action', 'admin_post_rocket_regenerate_configuration' => 'regenerate_configuration', ]; } /** * Maybe launch the domain changed event. * * @return void */ public function maybe_launch_domain_changed() { if ( wp_doing_ajax() ) { return; } $base_url = trailingslashit( get_option( 'home' ) ); $base_url_encoded = base64_encode( $base_url ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $last_base_url_encoded = get_option( self::LAST_BASE_URL_OPTION ); if ( ! $last_base_url_encoded ) { update_option( self::LAST_BASE_URL_OPTION, $base_url_encoded, true ); return; } if ( $base_url_encoded === $last_base_url_encoded ) { return; } update_option( self::LAST_BASE_URL_OPTION, $base_url_encoded, true ); $last_base_url = base64_decode( $last_base_url_encoded ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode set_transient( 'rocket_domain_changed', $last_base_url_encoded, 2 * rocket_get_constant( 'WEEK_IN_SECONDS', 604800 ) ); /** * Fires when the domain of the website has been changed. * * @param string $current_url current URL from the website. * @param string $old_url old URL from the website. */ do_action( 'rocket_detected_domain_changed', $base_url, $last_base_url ); } /** * Maybe clean cache on domain change. * * @return void */ public function maybe_clean_cache_domain_change() { $options = get_option( rocket_get_constant( 'WP_ROCKET_SLUG' ) ); if ( ! $options ) { return; } /** * Fires after WP Rocket options that require a cache purge have changed * * @param array $value An array of submitted values for the settings. */ do_action( 'rocket_domain_options_changed', $options ); } /** * Maybe display a notice when domain change. * * @return void */ public function maybe_display_domain_change_notice() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } $notice = get_transient( 'rocket_domain_changed' ); if ( ! $notice || is_multisite() ) { return; } $beacon = $this->beacon->get_suggest( 'domain_change' ); $args = [ 'status' => 'warning', 'dismissible' => '', 'dismiss_button' => false, 'message' => sprintf( // translators: %1$s = <strong>, %2$s = </strong>, %3$s = <a>, %4$s = </a>. __( '%1$sWP Rocket:%2$s We detected that the website domain has changed. The configuration files must be regenerated for the page cache and all other optimizations to work as intended. %3$sLearn More%4$s', 'rocket' ), '<strong>', '</strong>', '<a href="' . esc_url( $beacon['url'] ) . '" data-beacon-article="' . esc_attr( $beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ), 'action' => 'regenerate_configuration', ]; rocket_notice_html( $args ); } /** * Add mapping on notice. * * @param array $args Arguments from the notice. * * @return array */ public function add_regenerate_configuration_action( $args ) { if ( ! key_exists( 'action', $args ) || 'regenerate_configuration' !== $args['action'] ) { return $args; } $params = [ 'action' => 'rocket_regenerate_configuration', ]; $args['action'] = '<a class="wp-core-ui button" href="' . add_query_arg( $params, wp_nonce_url( admin_url( 'admin-post.php' ), 'rocket_regenerate_configuration' ) ) . '">' . __( 'Regenerate WP Rocket configuration files now', 'rocket' ) . '</a>'; return $args; } /** * Regenerate configurations. * * @return void */ public function regenerate_configuration() { if ( ! $this->ajax_handler->validate_referer( 'rocket_regenerate_configuration', 'rocket_manage_options' ) ) { return; } $last_base_url_encoded = get_transient( 'rocket_domain_changed' ); if ( ! $last_base_url_encoded ) { return; } $last_base_url = base64_decode( $last_base_url_encoded ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $base_url = trailingslashit( get_option( 'home' ) ); /** * Fires when the domain of the website has been changed and user clicked on notice. * * @param string $current_url current URL from the website. * @param string $old_url old URL from the website. */ do_action( 'rocket_domain_changed', $base_url, $last_base_url ); delete_transient( 'rocket_domain_changed' ); $this->ajax_handler->redirect(); } } Engine/Admin/DomainChange/ServiceProvider.php 0000644 00000001764 15174677547 0015201 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Admin\DomainChange; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Common\Ajax\AjaxHandler; class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'domain_change_subscriber', 'ajax_handler', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'ajax_handler', AjaxHandler::class ); $this->getContainer()->addShared( 'domain_change_subscriber', Subscriber::class ) ->addArguments( [ 'ajax_handler', 'beacon', ] ); } } Engine/Admin/Deactivation/DeactivationIntent.php 0000644 00000010646 15174677547 0015756 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Deactivation; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; class DeactivationIntent extends Abstract_Render { /** * Options instance. * * @var Options */ private $options_api; /** * Options_Data instance. * * @since 3.0 * * @var Options_Data */ private $options; /** * Constructor * * @since 3.0 * * @param string $template Template path. * @param Options $options_api Options instance. * @param Options_Data $options Options_Data instance. */ public function __construct( $template, Options $options_api, Options_Data $options ) { parent::__construct( $template ); $this->options_api = $options_api; $this->options = $options; } /** * Checks if the deactivation modal is snoozed * * @since 3.11.1 * * @return bool */ private function is_snoozed(): bool { if ( 1 === (int) get_option( 'wp_rocket_hide_deactivation_form', 0 ) ) { return true; } if ( false !== get_transient( 'rocket_hide_deactivation_form' ) ) { return true; } return false; } /** * Inserts the deactivation intent form on plugins page * * @since 3.0 * * @return void */ public function insert_deactivation_intent_form() { if ( $this->is_snoozed() ) { return; } $data = [ 'form_action' => admin_url( 'admin-post.php?action=rocket_deactivation' ), ]; echo $this->generate( 'form', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Deactivate the plugin and set the snooze value * * @since 3.11.1 * * @param int $snooze Snooze value. * * @return void */ public function deactivate_and_snooze( $snooze ) { $this->set_snooze( $snooze ); deactivate_plugins( 'wp-rocket/wp-rocket.php' ); } /** * Sets the snooze value * * @since 3.11.1 * * @param int $snooze Snooze value. * * @return void */ private function set_snooze( int $snooze ) { if ( 0 === $snooze ) { add_option( 'wp_rocket_hide_deactivation_form', 1 ); return; } set_transient( 'rocket_hide_deactivation_form', 1, $snooze * DAY_IN_SECONDS ); } /** * Activates WP Rocket safe mode by deactivating possibly layout breaking options * * @since 3.0 * * @return void */ public function activate_safe_mode() { /** * Filters the array of options to reset when activating safe mode * * @since 3.7 * * @param array $options Array of options to reset. */ $reset_options = apply_filters( 'rocket_safe_mode_reset_options', [ 'async_css' => 0, 'lazyload' => 0, 'lazyload_iframes' => 0, 'lazyload_youtube' => 0, 'minify_css' => 0, 'minify_js' => 0, 'minify_concatenate_js' => 0, 'defer_all_js' => 0, 'delay_js' => 0, 'remove_unused_css' => 0, 'minify_google_fonts' => 0, 'cdn' => 0, ] ); $this->options->set_values( $reset_options ); $this->options_api->set( 'settings', $this->options->get_options() ); } /** * Add modal assets on the plugins page * * @since 3.11.1 * * @param string $hook The current admin page. * * @return void */ public function add_modal_assets( $hook ) { if ( 'plugins.php' !== $hook ) { return; } if ( $this->is_snoozed() ) { return; } wp_enqueue_style( 'wpr-modal', rocket_get_constant( 'WP_ROCKET_ASSETS_CSS_URL' ) . 'wpr-modal.css', null, rocket_get_constant( 'WP_ROCKET_VERSION' ) ); wp_enqueue_script( 'micromodal', rocket_get_constant( 'WP_ROCKET_ASSETS_JS_URL' ) . 'micromodal.min.js', null, '0.4.10', true ); wp_add_inline_script( 'micromodal', 'window.addEventListener("DOMContentLoaded", (event) => { document.getElementById("deactivate-wp-rocket").addEventListener("click", (event) => {event.preventDefault();});MicroModal.init(); });' ); } /** * Add data attribute to WP Rocket deactivation link for the modal * * @since 3.11.1 * * @param string[] $actions An array of plugin action links. * * @return array */ public function add_data_attribute( $actions ) { if ( ! isset( $actions['deactivate'] ) ) { return $actions; } $deactivate_link = str_replace( '<a', '<a data-micromodal-trigger="wpr-deactivation-modal"', $actions['deactivate'] ); $actions['deactivate'] = $deactivate_link; return $actions; } } Engine/Admin/Deactivation/Subscriber.php 0000644 00000004714 15174677547 0014264 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Deactivation; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * DeactivationIntent instance * * @var DeactivationIntent */ private $deactivation; /** * Constructor * * @param DeactivationIntent $deactivation DeactivationIntent instance. */ public function __construct( DeactivationIntent $deactivation ) { $this->deactivation = $deactivation; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'admin_footer-plugins.php' => 'insert_deactivation_intent_form', 'admin_enqueue_scripts' => 'add_modal_assets', 'admin_post_rocket_deactivation' => 'safe_mode_or_deactivate', 'plugin_action_links_wp-rocket/wp-rocket.php' => 'add_data_attribute', ]; } /** * Inserts the deactivation intent form on plugins page * * @since 3.11.1 * * @return void */ public function insert_deactivation_intent_form() { $this->deactivation->insert_deactivation_intent_form(); } /** * Add modal assets on the plugins page * * @since 3.11.1 * * @param string $hook The current admin page. * * @return void */ public function add_modal_assets( $hook ) { $this->deactivation->add_modal_assets( $hook ); } /** * Apply safe mode or deactivate the plugin * * @since 3.11.1 * * @return void */ public function safe_mode_or_deactivate() { check_admin_referer( 'rocket_deactivation', '_wpnonce' ); $referer = wp_get_referer(); if ( ! current_user_can( 'manage_options' ) ) { wp_safe_redirect( $referer ); exit; } $mode = isset( $_POST['mode'] ) ? sanitize_key( $_POST['mode'] ) : ''; if ( 'safe_mode' === $mode ) { $this->deactivation->activate_safe_mode(); } elseif ( 'deactivate' === $mode ) { $snooze = isset( $_POST['snooze'] ) ? absint( $_POST['snooze'] ) : 0; $this->deactivation->deactivate_and_snooze( $snooze ); $referer = add_query_arg( 'deactivate', '1', $referer ); } wp_safe_redirect( $referer ); exit; } /** * Add data attribute to WP Rocket deactivation link for the modal * * @since 3.11.1 * * @param string[] $actions An array of plugin action links. * * @return array */ public function add_data_attribute( $actions ): array { return $this->deactivation->add_data_attribute( $actions ); } } Engine/Admin/Beacon/ServiceProvider.php 0000644 00000002016 15174677547 0014042 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Admin\Beacon; use WP_Rocket\Dependencies\League\Container\Argument\Literal\StringArgument; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service Provider for Beacon */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'beacon', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'beacon', Beacon::class ) ->addArguments( [ 'options', new StringArgument( $this->getContainer()->get( 'template_path' ) . '/settings' ), 'support_data', ] ); } } Engine/Admin/Beacon/Beacon.php 0000644 00000076465 15174677547 0012141 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Beacon; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Support\Data; use WP_Rocket\Event_Management\Subscriber_Interface; /** * Helpscout Beacon integration * * @since 3.2 */ class Beacon extends Abstract_Render implements Subscriber_Interface { /** * Options_Data instance * * @since 3.2 * * @var Options_Data $options */ private $options; /** * Current user locale * * @since 3.2 * * @var string $locale */ private $locale; /** * Support data instance * * @var Data */ private $support_data; /** * Constructor * * @since 3.2 * * @param Options_Data $options Options instance. * @param string $template_path Absolute path to the views/settings. * @param Data $support_data Support data instance. */ public function __construct( Options_Data $options, $template_path, Data $support_data ) { parent::__construct( $template_path ); $this->options = $options; $this->support_data = $support_data; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.2 * * @return array */ public static function get_subscribed_events() { return [ 'admin_print_footer_scripts-settings_page_wprocket' => 'insert_script', ]; } /** * Configures and returns beacon javascript * * @since 3.2 * * @return void */ public function insert_script() { if ( rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT' ) || ! current_user_can( 'rocket_manage_options' ) ) { return; } switch ( $this->get_user_locale() ) { case 'fr': $form_id = '9db9417a-5e2f-41dd-8857-1421d5112aea'; break; default: $form_id = '44cc73fb-7636-4206-b115-c7b33823551b'; break; } $data = [ 'form_id' => $form_id, 'identify' => wp_json_encode( $this->identify_data() ), 'session' => wp_json_encode( $this->support_data->get_support_data() ), 'prefill' => wp_json_encode( $this->prefill_data() ), 'config' => wp_json_encode( $this->config_data() ), ]; echo $this->generate( 'beacon', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Sets the locale property with the current user locale if not set yet * * @since 3.5 * * @return string */ private function get_user_locale() { if ( empty( $this->locale ) ) { $this->locale = current( array_slice( explode( '_', get_user_locale() ), 0, 1 ) ); } /** * Filters the locale ID for Beacon * * @since 3.6 * * @param string $locale The locale ID. */ return wpm_apply_filters_typed( 'string', 'rocket_beacon_locale', $this->locale ); } /** * Returns Identify data to pass to Beacon * * @since 3.0 * * @return array */ private function identify_data() { $identify_data = [ 'email' => $this->options->get( 'consumer_email' ), 'Website' => home_url(), ]; $customer_data = get_transient( 'wp_rocket_customer_data' ); if ( false !== $customer_data && isset( $customer_data->status ) ) { $identify_data['status'] = $customer_data->status; } return $identify_data; } /** * Returns prefill data to pass to Beacon * * @since 3.6 * * @return array */ private function prefill_data() { $prefill_data = [ 'fields' => [ [ 'id' => 21728, 'value' => 108003, // default to nulled. ], ], ]; $customer_data = get_transient( 'wp_rocket_customer_data' ); if ( false === $customer_data || ! isset( $customer_data->licence_account ) ) { return $prefill_data; } $licenses = [ 'Single' => 108000, 'Plus' => 108001, 'Infinite' => 108002, 'Unavailable' => 108003, ]; if ( isset( $licenses[ $customer_data->licence_account ] ) ) { $prefill_data['fields'][0]['value'] = $licenses[ $customer_data->licence_account ]; } return $prefill_data; } /** * Returns config data to pass to Beacon * * @since 3.8.5 * * @return array */ private function config_data(): array { return [ 'display' => [ 'position' => is_rtl() ? 'left' : 'right', ], ]; } /** * Returns the IDs for the HelpScout docs for the corresponding section and language. * * @since 3.0 * * @param string $doc_id Section identifier. * * @return string|array */ public function get_suggest( $doc_id ) { $suggest = [ 'faq' => [ 'en' => [ [ 'id' => '5569b671e4b027e1978e3c51', 'url' => 'https://docs.wp-rocket.me/article/99-pages-are-not-cached-or-css-and-js-minification-are-not-working/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => 'Pages Are Not Cached or CSS and JS Minification Are Not Working', ], [ 'id' => '556778c8e4b01a224b426fad', 'url' => 'https://docs.wp-rocket.me/article/85-google-page-speed-grade-does-not-improve/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => 'Google PageSpeed Grade does not Improve', ], [ 'id' => '556ef48ce4b01a224b428691', 'url' => 'https://docs.wp-rocket.me/article/106-my-site-is-broken/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => 'My Site Is Broken', ], [ 'id' => '6001a83b2e764327f87bf189', 'url' => 'https://docs.wp-rocket.me/article/1407-eliminate-render-blocking-resources/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => 'Eliminate Render Blocking Resources', ], [ 'id' => '54e6f7e5e4b034c37ea9095f', 'url' => 'https://docs.wp-rocket.me/article/46-how-to-check-if-wp-rocket-is-caching-your-pages/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => 'How to check if WP Rocket is caching your pages', ], ], 'fr' => [ [ 'id' => '5697d2dc9033603f7da31041', 'url' => 'https://fr.docs.wp-rocket.me/article/264-les-pages-ne-sont-pas-mises-en-cache-ou-la-minification-css-et-js-ne-fonctionne-pas/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => 'Les pages ne sont pas mises en cache, ou la minification CSS et JS ne fonctionne pas', ], [ 'id' => '569564dfc69791436155e0b0', 'url' => 'https://fr.docs.wp-rocket.me/article/218-la-note-google-page-speed-ne-sameliore-pas/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => "La note Google Page Speed ne s'améliore pas", ], [ 'id' => '5697d03bc69791436155ed69', 'url' => 'https://fr.docs.wp-rocket.me/article/263-site-casse/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => 'Mon site est cassé', ], [ 'id' => '601d4b83ac2f834ec5385ca5', 'url' => 'https://fr.docs.wp-rocket.me/article/1440-eliminez-les-ressources-qui-bloquent-le-rendu/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => 'Éliminez les ressources qui bloquent le rendu', ], [ 'id' => '568fe9ebc69791436155cd32', 'url' => 'https://fr.docs.wp-rocket.me/article/180-verifier-cache/?utm_source=wp_plugin&utm_medium=wp_rocket', 'title' => 'Comment vérifier si WP Rocket met bien en cache vos pages', ], ], ], 'user_cache_section' => [ 'en' => '56b55ba49033600da1c0b687,587920b5c697915403a0e1f4,560c66b0c697917e72165a6d', 'fr' => '56cb9ba990336008e9e9e3d9,5879230cc697915403a0e211,569410999033603f7da2fa94', ], 'user_cache' => [ 'en' => [ 'id' => '56b55ba49033600da1c0b687', 'url' => 'https://docs.wp-rocket.me/article/313-user-cache/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '56cb9ba990336008e9e9e3d9', 'url' => 'https://fr.docs.wp-rocket.me/article/333-cache-utilisateurs-connectes/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'mobile_cache_section' => [ 'en' => '577a5f1f903360258a10e52a,5678aa76c697914361558e92,5745b9a6c697917290ddc715', 'fr' => '589b17a02c7d3a784630b249,5a6b32830428632faf6233dc,58a480e5dd8c8e56bfa7b85c', ], 'mobile_cache' => [ 'en' => [ 'id' => '577a5f1f903360258a10e52a', 'url' => 'https://docs.wp-rocket.me/article/708-mobile-caching/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '589b17a02c7d3a784630b249', 'url' => 'https://fr.docs.wp-rocket.me/article/934-mise-en-cache-pour-mobile/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'cache_ssl' => [ 'en' => [ 'id' => '56c24fd3903360436857f1ed', 'url' => 'https://docs.wp-rocket.me/article/314-using-ssl-with-wp-rocket/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '56cb9d24c6979102ccfc801c', 'url' => 'https://fr.docs.wp-rocket.me/article/335-utiliser-ssl-wp-rocket/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'cache_lifespan' => [ 'en' => [ 'id' => '555c7e9ee4b027e1978e17a5', 'url' => 'https://docs.wp-rocket.me/article/78-how-often-is-the-cache-updated/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '568f7df49033603f7da2ec72', 'url' => 'https://fr.docs.wp-rocket.me/article/171-intervalle-cache/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'cache_lifespan_section' => [ 'en' => '555c7e9ee4b027e1978e17a5,5922fd0e0428634b4a33552c', 'fr' => '568f7df49033603f7da2ec72,598080e1042863033a1b890e', ], 'nonce' => [ 'en' => [ 'id' => '5922fd0e0428634b4a33552c', 'url' => 'https://docs.wp-rocket.me/article/975-nonces-and-cache-lifespan/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '598080e1042863033a1b890e', 'url' => 'https://fr.docs.wp-rocket.me/article/1015-nonces-delai-nettoyage-cache/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'css_section' => [ 'en' => '556ef48ce4b01a224b428691,6001a83b2e764327f87bf189,5569b671e4b027e1978e3c51,5d5214d10428631e94f94ae6', 'fr' => '5697d2dc9033603f7da31041,5d5abcce0428634552d85c1c,5697d03bc69791436155ed69,601d4b83ac2f834ec5385ca5', ], 'js_section' => [ 'en' => '54b9509de4b07997ea3f27c7,59236dfb0428634b4a3358f9,5f359695042863444aa04e26,556ef48ce4b01a224b428691,6001a83b2e764327f87bf189', 'fr' => '56967eebc69791436155e649,593fe9882c7d3a0747cddb77,5f523c46c9e77c0016384ba0,5697d03bc69791436155ed69,601d4b83ac2f834ec5385ca5', ], 'file_optimization' => [ 'en' => [ 'id' => '6001a83b2e764327f87bf189', 'url' => 'https://docs.wp-rocket.me/article/1407-eliminate-render-blocking-resources/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '601d4b83ac2f834ec5385ca5', 'url' => 'https://fr.docs.wp-rocket.me/article/1440-eliminez-les-ressources-qui-bloquent-le-rendu/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'combine' => [ 'en' => [ 'id' => '596eaf7d2c7d3a73488b3661', 'url' => 'https://docs.wp-rocket.me/article/1009-configuration-for-http-2/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '59a418ad042863033a1c572e', 'url' => 'https://fr.docs.wp-rocket.me/article/1018-configuration-http-2/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'remove_unused_css' => [ 'en' => [ 'id' => '6076083ff8c0ef2d98df1f97', 'url' => 'https://docs.wp-rocket.me/article/1529-remove-unused-css?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '60d499a705ff892e6bc2a89e', 'url' => 'https://fr.docs.wp-rocket.me/article/1577-supprimer-les-ressources-css-inutilisees?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'exclude_inline_js' => [ 'en' => [ 'id' => '5b4879100428630abc0c0713', 'url' => 'https://docs.wp-rocket.me/article/1104-excluding-inline-js-from-combine/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5b4dd9290428631d7a89023c', 'url' => 'https://fr.docs.wp-rocket.me/article/1109-exclure-les-js-inline-de-la-combinaison?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'exclude_js' => [ 'en' => [ 'id' => '54b9509de4b07997ea3f27c7', 'url' => 'https://docs.wp-rocket.me/article/39-excluding-external-js-from-concatenation/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '56967eebc69791436155e649', 'url' => 'https://fr.docs.wp-rocket.me/article/243-exclure-js-externe-minification?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'exclude_css' => [ 'en' => [ 'id' => '5bf339b12c7d3a31944e2111', 'url' => 'https://docs.wp-rocket.me/article/1131-resolving-issues-with-css-minify-combine?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5bf3bece04286304a71c6d35', 'url' => 'https://fr.docs.wp-rocket.me/article/1132-resoudre-problemes-minification-combinaison-css?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'defer_js' => [ 'en' => [ 'id' => '5d52138d2c7d3a68825e8faa', 'url' => 'https://docs.wp-rocket.me/article/1265-load-javascript-deferred/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5d5ac08b2c7d3a7920be3649', 'url' => 'https://fr.docs.wp-rocket.me/article/1270-chargement-differe-des-fichiers-js/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'delay_js' => [ 'en' => [ 'id' => '5f359695042863444aa04e26', 'url' => 'https://docs.wp-rocket.me/article/1349-delay-javascript-execution/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '60e5b05605ff892e6bc2e86c', 'url' => 'https://fr.docs.wp-rocket.me/article/1626-reporter-l-execution-du-javascript?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'delay_js_exclusions' => [ 'en' => [ 'id' => '', 'url' => 'https://docs.wp-rocket.me/article/1560-delay-javascript-execution-compatibility-exclusions/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'async' => [ 'en' => [ 'id' => '5d52144c0428631e94f94ae2', 'url' => 'https://docs.wp-rocket.me/article/1266-optimize-css-delivery/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5d5abada0428634552d85bff', 'url' => 'https://fr.docs.wp-rocket.me/article/1268-optimiser-le-chargement-du-css/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'lazyload' => [ 'en' => [ 'id' => '5c884cf80428633d2cf38314', 'url' => 'https://docs.wp-rocket.me/article/1141-using-lazyload-in-wp-rocket/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5c98ff532c7d3a1544614cf4', 'url' => 'https://fr.docs.wp-rocket.me/article/1146-utiliser-lazyload-images-wp-rocket/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'webp' => [ 'fr' => [ 'id' => '5d7b495e04286364bc8f12ef', 'url' => 'https://fr.docs.wp-rocket.me/article/1286-compatibilite-webp?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'en' => [ 'id' => '5d72919704286364bc8ed49d', 'url' => 'https://docs.wp-rocket.me/article/1282-webp?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'lazyload_section' => [ 'en' => '5c884cf80428633d2cf38314,54b85754e4b0512429883a86,5418c792e4b0e7b8127bed99,569ec4a69033603f7da32c93,5419e246e4b099def9b5561e', 'fr' => '56967a859033603f7da30858,56967952c69791436155e60a,56cb9c9d90336008e9e9e3dc,569676ea9033603f7da3083d', ], 'sitemap_preload' => [ 'en' => '541780fde4b005ed2d11784c,5a71c8ab2c7d3a4a4198a9b3,55b282ede4b0b0593824f852', 'fr' => '5693d582c69791436155d645', ], 'preload_bot' => [ 'en' => '541780fde4b005ed2d11784c,55b282ede4b0b0593824f852,559113eae4b027e1978eba11', 'fr' => '5693d582c69791436155d645,569433d1c69791436155d99c', ], 'preload_exclusions' => [ 'en' => [ 'id' => '6349682bde258f5018eb456d', 'url' => 'https://docs.wp-rocket.me/article/1721-exclude-urls-from-being-preloaded?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '640b30058ca4460845b4a1c4', 'url' => 'https://fr.docs.wp-rocket.me/article/1739-comment-exclure-urls-prechargement?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'bot' => [ 'en' => [ 'id' => '541780fde4b005ed2d11784c', 'url' => 'https://docs.wp-rocket.me/article/8-how-the-cache-is-preloaded/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5693d582c69791436155d645', 'url' => 'https://fr.docs.wp-rocket.me/article/188-comment-est-pre-charge-le-cache/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'fonts_preload' => [ 'en' => [ 'id' => '5eab7729042863474d19f647', 'url' => 'https://docs.wp-rocket.me/article/1317-preload-fonts/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5eb3add02c7d3a5ea54aa66d', 'url' => 'https://fr.docs.wp-rocket.me/article/1319-precharger-polices/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'preload_links' => [ 'en' => [ 'id' => '5f35939b042863444aa04df9', 'url' => 'https://docs.wp-rocket.me/article/1348-preload-links/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5f58527cc9e77c001603746c', 'url' => 'https://fr.docs.wp-rocket.me/article/1358-precharger-les-liens/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'ecommerce' => [ 'en' => [ 'id' => '548f492de4b034fd4862493e', 'url' => 'https://docs.wp-rocket.me/article/27-using-wp-rocket-on-your-ecommerce-site/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '569431189033603f7da2fc13', 'url' => 'https://fr.docs.wp-rocket.me/article/198-ecommerce?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'cache_query_strings' => [ 'en' => [ 'id' => '590a83610428634b4a32d52c', 'url' => 'https://docs.wp-rocket.me/article/971-caching-query-strings/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '597a04fd042863033a1b6da4', 'url' => 'https://fr.docs.wp-rocket.me/article/1014-cache-query-strings/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'exclude_cache' => [ 'en' => [ 'id' => '5519ab03e4b061031402119f', 'url' => 'https://docs.wp-rocket.me/article/54-exclude-pages-from-the-cache/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '56941c0cc69791436155d8ab', 'url' => 'https://fr.docs.wp-rocket.me/article/196-exclure-pages-cache/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'exclude_cookie' => [ 'en' => [ 'id' => '5fe5462df24ccf588e3fe804', 'url' => 'https://docs.wp-rocket.me/article/1382-never-cache-cookies/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'exclude_user_agent' => [ 'en' => [ 'id' => '5ff728d3551e0c2853f3a245', 'url' => 'https://docs.wp-rocket.me/article/1389-never-cache-user-agents/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'always_purge' => [ 'en' => [ 'id' => '5ff72b4dfd168b77735328b7', 'url' => 'https://docs.wp-rocket.me/article/1391-always-purge-url-s/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'db_optimization' => [ 'en' => [ 'id' => '60259156b3ebfb109b58182d', 'url' => 'https://docs.wp-rocket.me/article/1443-database-optimizations-are-not-working/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '6040c5b90a2dae5b58fb5d29', 'url' => 'https://fr.docs.wp-rocket.me/article/1486-les-optimisations-de-la-base-de-donnees-ne-fonctionne-pas/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'cdn_section' => [ 'en' => '5e4c84bd04286364bc958833,54c7fa3de4b0512429885b5c,54a6d578e4b047ebb774a687,56b2b4459033603f7da37acf,566f749f9033603f7da28459,5434667fe4b0310ce5ee867a', 'fr' => '5f351e42042863444aa04652,5696830b9033603f7da308ac,569685749033603f7da308c0,57a4961190336059d4edc9d8,5697d5f8c69791436155ed8e,569684d29033603f7da308b9', ], 'cdn' => [ 'en' => [ 'id' => '54c7fa3de4b0512429885b5c', 'url' => 'https://docs.wp-rocket.me/article/42-using-wp-rocket-with-a-cdn/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5696830b9033603f7da308ac', 'url' => 'https://fr.docs.wp-rocket.me/article/246-utiliser-wp-rocket-avec-un-cdn/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'rocketcdn' => [ 'en' => [ 'id' => '5e4c84bd04286364bc958833', 'url' => 'https://docs.wp-rocket.me/article/1307-rocketcdn/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5f351e42042863444aa04652', 'url' => 'https://fr.docs.wp-rocket.me/article/1343-comment-utiliser-rocketcdn/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'rocketcdn_error' => [ 'en' => [ 'id' => '60ddc72d9e87cb3d01249270', 'url' => 'https://docs.wp-rocket.me/article/1608-error-notices-during-the-rocketcdn-subscription-process/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '60df1cb200fd0d7c253fc044', 'url' => 'https://fr.docs.wp-rocket.me/article/1620-messages-derreur-pendant-le-processus-dabonnement/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'exclude_cdn' => [ 'en' => [ 'id' => '5434667fe4b0310ce5ee867a', 'url' => 'https://docs.wp-rocket.me/article/24-resolving-issues-with-cdn-and-fonts-icons/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '569684d29033603f7da308b9', 'url' => 'https://fr.docs.wp-rocket.me/article/248-resoudre-des-problemes-avec-cdn-et-les-polices-icones/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'cloudflare_credentials' => [ 'en' => [ 'id' => '54205619e4b0e7b8127bf849', 'url' => 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5696837e9033603f7da308ae', 'url' => 'https://fr.docs.wp-rocket.me/article/247-utiliser-wp-rocket-avec-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'cloudflare_settings' => [ 'en' => [ 'id' => '54205619e4b0e7b8127bf849', 'url' => 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5696837e9033603f7da308ae', 'url' => 'https://fr.docs.wp-rocket.me/article/247-utiliser-wp-rocket-avec-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'cloudflare_credentials_api' => [ 'en' => [ 'id' => '54205619e4b0e7b8127bf849', 'url' => 'https://docs.wp-rocket.me/article/18-using-wp-rocket-with-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', ], 'fr' => [ 'id' => '5696837e9033603f7da308ae', 'url' => 'https://fr.docs.wp-rocket.me/article/247-utiliser-wp-rocket-avec-cloudflare/?utm_source=wp_plugin&utm_medium=wp_rocket#add-on', ], ], 'cloudflare_apo' => [ 'en' => [ 'id' => '602593e90a2dae5b58faee1e', 'url' => 'https://docs.wp-rocket.me/article/1444-using-cloudflare-apo-with-wp-rocket?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '6486cb4147772865db893c7c', 'url' => 'https://fr.docs.wp-rocket.me/article/1757-utiliser-cloudflare-apo-avec-wp-rocket?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'sucuri_credentials' => [ 'en' => [ 'id' => '5bce07be2c7d3a04dd5bf94d', 'url' => 'https://docs.wp-rocket.me/article/1120-sucuri-add-on/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5bcf39c72c7d3a4db66085b9', 'url' => 'https://fr.docs.wp-rocket.me/article/1122-sucuri-add-on/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'varnish' => [ 'en' => [ 'id' => '56f48132c6979115a34095bd', 'url' => 'https://docs.wp-rocket.me/article/493-using-varnish-with-wp-rocket/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '56fd2f789033601d6683e574', 'url' => 'https://fr.docs.wp-rocket.me/article/512-varnish-wp-rocket-2-7/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'heartbeat_settings' => [ 'en' => [ 'id' => '5bcdfecd042863158cc7b672', 'url' => 'https://docs.wp-rocket.me/article/1119-control-wordpress-heartbeat-api/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5bcf4378042863215a46bc00', 'url' => 'https://fr.docs.wp-rocket.me/article/1124-controler-api-wordpress-heartbeat/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'google_fonts' => [ 'en' => [ 'id' => '5e8687c22c7d3a7e9aea4c4a', 'url' => 'https://docs.wp-rocket.me/article/1312-optimize-google-fonts', ], 'fr' => [ 'id' => '5e970f512c7d3a7e9aeaf9fb', 'url' => 'https://fr.docs.wp-rocket.me/article/1314-optimiser-les-google-fonts/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'dynamic_lists' => [ 'en' => [ 'id' => '63234712b0f178684ee3b04a', 'url' => 'https://docs.wp-rocket.me/article/1716-dynamic-exclusions-and-inclusions/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '6323604341e1a47267b8d0e3', 'url' => 'https://fr.docs.wp-rocket.me/article/1717-inclusions-et-exclusions-dynamiques/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'image_dimensions' => [ 'en' => [ 'id' => '5fc70216de1bfa158fb54737', 'url' => 'https://docs.wp-rocket.me/article/1366-add-missing-image-dimensions/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '5fd20dcab6c6251cd1c35079', 'url' => 'https://fr.docs.wp-rocket.me/article/1369-ajouter-les-dimensions-dimage-manquantes/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'exclude_defer_js' => [ 'en' => [ 'id' => '59236dfb0428634b4a3358f9', 'url' => 'https://docs.wp-rocket.me/article/976-exclude-files-from-defer-js/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'exclude_lazyload' => [ 'fr' => [ 'id' => '56967952c69791436155e60a', 'url' => 'https://fr.docs.wp-rocket.me/article/235-desactivez-le-lazyload-sur-des-images-specifiques?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'en' => [ 'id' => '5418c792e4b0e7b8127bed99', 'url' => 'https://docs.wp-rocket.me/article/15-disabling-lazy-load-on-specific-images/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'invalid_exclusions' => [ 'en' => [ 'id' => '619e90a3d3efbe495c3b26b8', 'url' => 'https://docs.wp-rocket.me/article/1657-invalid-patterns-of-exclusions', ], 'fr' => [ 'id' => '61b21c1297682b790dad345a', 'url' => 'https://fr.docs.wp-rocket.me/article/1659-motifs-exclusion-non-valables', ], ], 'async_opti' => [ 'en' => [ 'id' => '622a725a2ce7ed0fb0914056', 'url' => 'https://docs.wp-rocket.me/article/1688-asynchronous-optimizations?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '6231fc24c1688a6d26a75ee1', 'url' => 'https://fr.docs.wp-rocket.me/article/1689-optimisations-asynchrones?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'offline' => [ 'en' => [ 'id' => '60623465c44f5d025f4491de', 'url' => 'https://docs.wp-rocket.me/article/1514-private-intranet-offline?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '6065cb184466ce6ddc5f05fb', 'url' => 'https://fr.docs.wp-rocket.me/article/1521-utiliser-wp-rocket-sur-un-intranet-prive-ou-hors-ligne?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'fallback_css' => [ 'en' => [ 'id' => '5ec5c4072c7d3a5ea54b7de7', 'url' => 'https://docs.wp-rocket.me/article/1321-critical-css-issues-fouc#use-fallback-critical-css', ], 'fr' => [ 'id' => '5edf8a5504286306f804e1dc', 'url' => 'https://fr.docs.wp-rocket.me/article/1327-problemes-critical-css-fouc#critical-path-css-de-secours', ], ], 'domain_change' => [ 'en' => [ 'id' => '577578b1903360258a10d8ba', 'url' => 'https://docs.wp-rocket.me/article/705-changing-domains-migrating-sites-with-wp-rocket?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '57868414c697912dee72a98a', 'url' => 'https://fr.docs.wp-rocket.me/article/837-changer-de-domaine-migrer-un-site-avec-wp-rocket?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'rucss_firewall_ips' => [ 'en' => [ 'id' => '60ed8bde00fd0d7c253ff547', 'url' => 'https://docs.wp-rocket.me/article/1628-which-ip-do-i-need-to-allow-for-wp-rocket?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '6246fe1a2ce7ed0fb091c543', 'url' => 'https://fr.docs.wp-rocket.me/article/1690-quelles-adresses-ip-url-autoriser-pour-wp-rocket?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'optimize_critical_images' => [ 'en' => [ 'id' => '662c1a144c3ddc1d4e7a1d25', 'url' => 'https://docs.wp-rocket.me/article/1816-optimize-critical-images?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '6634e9fe0cfcb4508af6b290', 'url' => 'https://fr.docs.wp-rocket.me/article/1819-optimiser-images-essentielle?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'remove_cache_tab' => [ 'en' => [ 'id' => '6633b5df1009cb439ac6a432', 'url' => 'https://docs.wp-rocket.me/article/1817-removal-of-the-cache-tab?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '6634e9b21009cb439ac6a6fb', 'url' => 'https://fr.docs.wp-rocket.me/article/1818-suppression-onglet-cache?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'rucss_database' => [ 'en' => [ 'id' => '668f1284f0fdf93e4cf10825', 'url' => 'https://docs.wp-rocket.me/article/1828-could-not-create-the-rucss-usedcss-table/?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '66a32d970d7d86166241eff1', 'url' => 'https://fr.docs.wp-rocket.me/article/1833-impossible-creer-table-rucssusedcss/?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'host_fonts_locally' => [ 'en' => [ 'id' => '673358b02ddbd952f6241b38', 'url' => 'https://docs.wp-rocket.me/article/1847-self-host-google-fonts?utm_source=wp_plugin&utm_medium=wp_rocket', ], 'fr' => [ 'id' => '675ab51d46b8d26833b2af82', 'url' => 'https://fr.docs.wp-rocket.me/article/1852-auto-heberger-google-fonts?utm_source=wp_plugin&utm_medium=wp_rocket', ], ], 'preconnect_domains' => [ 'en' => [ 'id' => '681b61d889bd957cd04bd2d9', 'url' => 'https://docs.wp-rocket.me/article/1869-preconnect-to-external-domains', ], 'fr' => [ 'id' => '681da5ae11561a04f5de356e', 'url' => 'https://fr.docs.wp-rocket.me/article/1870-preconnexion-aux-domaines-externes', ], ], ]; return isset( $suggest[ $doc_id ][ $this->get_user_locale() ] ) ? $suggest[ $doc_id ][ $this->get_user_locale() ] : $suggest[ $doc_id ]['en']; } } Engine/Admin/Metaboxes/PostEditOptionsSubscriber.php 0000644 00000012573 15174677547 0016633 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Admin\Metaboxes; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class PostEditOptionsSubscriber extends Abstract_Render implements Subscriber_Interface { /** * Options instance * * @var Options_data */ private $options; /** * Constructor * * @param Options_Data $options Options instance. * @param string $template_path Path to the views. */ public function __construct( Options_Data $options, $template_path ) { parent::__construct( $template_path ); $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'add_meta_boxes' => 'options_metabox', 'save_post' => 'save_metabox_options', ]; } /** * Add options metabox on post edit page * * @return void */ public function options_metabox() { if ( ! rocket_can_display_options() ) { return; } if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } $cpts = get_post_types( [ 'public' => true, ], 'objects' ); unset( $cpts['attachment'] ); /** * Filters the post types to add the options metabox to * * @param array $cpts Array of post types. */ $cpts = apply_filters( 'rocket_metabox_options_post_types', $cpts ); foreach ( $cpts as $cpt => $cpt_object ) { $label = $cpt_object->labels->singular_name; add_meta_box( 'rocket_post_exclude', sprintf( __( 'WP Rocket Options', 'rocket' ), $label ), [ $this, 'display_metabox' ], $cpt, 'side', 'core' ); } } /** * Displays checkboxes to de/activate some options * * @return void */ public function display_metabox() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } global $post, $pagenow; $excluded_url = false; if ( 'post-new.php' !== $pagenow ) { $path = rocket_clean_exclude_file( get_permalink( $post->ID ) ); if ( in_array( $path, $this->options->get( 'cache_reject_uri', [] ), true ) ) { $excluded_url = true; } } $original_fields = []; /** * WP Rocket Metabox fields on post edit page. * * @param string[] $original_fields Metaboxes fields. */ $fields = wpm_apply_filters_typed( 'array', 'rocket_meta_boxes_fields', $original_fields ); $fields_attributes = []; foreach ( $fields as $field => $label ) { $disabled = disabled( ! $this->options->get( $field ), true, false ); $fields_attributes[ $field ]['id'] = $field; $fields_attributes[ $field ]['label'] = $label; // translators: %s is the name of the option. $fields_attributes[ $field ]['title'] = $disabled ? ' title="' . esc_attr( sprintf( __( 'Activate first the %s option.', 'rocket' ), $label ) ) . '"' : ''; $fields_attributes[ $field ]['class'] = $disabled ? ' class="rkt-disabled"' : ''; $fields_attributes[ $field ]['checked'] = ! $disabled ? checked( ! get_post_meta( $post->ID, '_rocket_exclude_' . $field, true ), true, false ) : ''; $fields_attributes[ $field ]['disabled'] = $disabled; } echo $this->generate( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'post_edit_options', [ 'excluded_url' => $excluded_url, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'fields' => $fields_attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ] ); } /** * Updates the options from the metabox. * * @return void */ public function save_metabox_options() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( ! isset( $_POST['post_ID'], $_POST['rocket_post_exclude_hidden'] ) ) { return; } check_admin_referer( 'rocket_box_option', '_rocketnonce' ); // No cache field. if ( isset( $_POST['post_status'] ) && 'publish' === $_POST['post_status'] ) { $new_cache_reject_uri = $cache_reject_uri = $this->options->get( 'cache_reject_uri', [] ); // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found $rejected_uris = array_flip( $cache_reject_uri ); $path = rocket_clean_exclude_file( get_permalink( (int) $_POST['post_ID'] ) ); if ( isset( $_POST['rocket_post_nocache'] ) ) { if ( ! isset( $rejected_uris[ $path ] ) ) { array_push( $new_cache_reject_uri, $path ); } } elseif ( isset( $rejected_uris[ $path ] ) ) { unset( $new_cache_reject_uri[ $rejected_uris[ $path ] ] ); } if ( $new_cache_reject_uri !== $cache_reject_uri ) { // Update the "Never cache the following pages" option. update_rocket_option( 'cache_reject_uri', $new_cache_reject_uri ); // Update config file. rocket_generate_config_file(); } } $original_fields = []; /** * Metaboxes fields. * * @param string[] $original_fields Metaboxes fields. */ $fields = wpm_apply_filters_typed( 'array', 'rocket_meta_boxes_fields', $original_fields ); $fields = array_keys( $fields ); foreach ( $fields as $field ) { if ( ! isset( $_POST['rocket_post_exclude_hidden'][ $field ] ) ) { continue; } if ( isset( $_POST['rocket_post_exclude'][ $field ] ) ) { delete_post_meta( (int) $_POST['post_ID'], '_rocket_exclude_' . $field ); continue; } if ( $this->options->get( $field ) ) { update_post_meta( (int) $_POST['post_ID'], '_rocket_exclude_' . $field, true ); } } } } Engine/Admin/Database/Subscriber.php 0000644 00000013201 15174677547 0013345 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Database; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Admin\Options_Data; /** * Subscriber for the database optimization */ class Subscriber implements Subscriber_Interface { /** * Optimization process instance. * * @since 3.4 * * @var Optimization */ private $optimize; /** * WP Rocket Options instance. * * @since 3.4 * * @var Options_Data */ private $options; /** * Constructor * * @param Optimization $optimize Optimize instance. * @param Options_Data $options WP Rocket options. */ public function __construct( Optimization $optimize, Options_Data $options ) { $this->optimize = $optimize; $this->options = $options; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3 * * @return array */ public static function get_subscribed_events() { return [ 'cron_schedules' => 'add_cron_schedule', 'init' => 'database_optimization_scheduled', 'rocket_database_optimization_time_event' => 'cron_optimize', 'pre_update_option_' . WP_ROCKET_SLUG => 'save_optimize', 'admin_notices' => [ [ 'notice_process_running' ], [ 'notice_process_complete' ], ], ]; } /** * Add a new interval for the cron job. * This adds a weekly/monthly interval for database optimization. * * @since 3.4 * * @param array $schedules An array of intervals used by cron jobs. * @return array Updated array of intervals. */ public function add_cron_schedule( $schedules ) { if ( ! $this->options->get( 'schedule_automatic_cleanup', false ) ) { return $schedules; } switch ( $this->options->get( 'automatic_cleanup_frequency', 'weekly' ) ) { case 'weekly': $schedules['weekly'] = [ 'interval' => 604800, 'display' => __( 'weekly', 'rocket' ), ]; break; case 'monthly': $schedules['monthly'] = [ 'interval' => 2592000, 'display' => __( 'monthly', 'rocket' ), ]; break; } return $schedules; } /** * Plans database optimization cron * If the task is not programmed, it is automatically triggered * * @since 2.8 * * @see process_handler() */ public function database_optimization_scheduled() { if ( ! $this->options->get( 'schedule_automatic_cleanup', false ) ) { return; } if ( ! wp_next_scheduled( 'rocket_database_optimization_time_event' ) ) { wp_schedule_event( time(), $this->options->get( 'automatic_cleanup_frequency', 'weekly' ), 'rocket_database_optimization_time_event' ); } } /** * Database Optimization cron callback * * @since 3.0.4 */ public function cron_optimize() { $items = array_filter( array_keys( $this->optimize->get_options() ), [ $this->options, 'get' ] ); if ( empty( $items ) ) { return; } $this->optimize->process_handler( $items ); } /** * Launches the database optimization when the settings are saved with optimize button * * @since 2.8 * * @see process_handler() * * @param array $value The new, unserialized option value. * @return array */ public function save_optimize( $value ) { if ( empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return $value; } if ( empty( $value ) || ! isset( $value['submit_optimize'] ) ) { return $value; } unset( $value['submit_optimize'] ); if ( ! current_user_can( 'rocket_manage_options' ) ) { return $value; } $items = []; $db_options = $this->optimize->get_options(); foreach ( $value as $key => $option_value ) { if ( isset( $db_options[ $key ] ) && 1 === $option_value ) { $items[] = $key; } } if ( empty( $items ) ) { return $value; } $this->optimize->process_handler( $items ); return $value; } /** * This notice is displayed after launching the database optimization process * * @since 2.11 */ public function notice_process_running() { $screen = get_current_screen(); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } $notice = get_transient( 'rocket_database_optimization_process' ); if ( ! $notice ) { return; } \rocket_notice_html( [ 'status' => 'info', 'message' => esc_html__( 'Database optimization process is running', 'rocket' ), ] ); } /** * This notice is displayed when the database optimization process is complete * * @since 2.11 */ public function notice_process_complete() { $screen = get_current_screen(); if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== $screen->id ) { return; } $optimized = get_transient( 'rocket_database_optimization_process_complete' ); if ( false === $optimized ) { return; } $db_options = $this->optimize->get_options(); delete_transient( 'rocket_database_optimization_process_complete' ); $message = esc_html__( 'Database optimization process is complete. Everything was already optimized!', 'rocket' ); if ( ! empty( $optimized ) ) { $message = esc_html__( 'Database optimization process is complete. List of optimized items below:', 'rocket' ); } if ( ! empty( $optimized ) ) { $message .= '<ul>'; foreach ( $optimized as $key => $number ) { $message .= '<li>' . /* translators: %1$d = number of items optimized, %2$s = type of optimization */ sprintf( esc_html__( '%1$d %2$s optimized.', 'rocket' ), $number, $db_options[ $key ] ) . '</li>'; } $message .= '</ul>'; } \rocket_notice_html( [ 'message' => $message, ] ); } } Engine/Admin/Database/ServiceProvider.php 0000644 00000002323 15174677547 0014360 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Admin\Database; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service Provider for database optimization */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'db_optimization_process', 'db_optimization', 'db_optimization_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the option array in the container * * @return void */ public function register(): void { $this->getContainer()->add( 'db_optimization_process', OptimizationProcess::class ); $this->getContainer()->add( 'db_optimization', Optimization::class ) ->addArgument( 'db_optimization_process' ); $this->getContainer()->addShared( 'db_optimization_subscriber', Subscriber::class ) ->addArguments( [ 'db_optimization', 'options', ] ); } } Engine/Admin/Database/Optimization.php 0000644 00000005746 15174677547 0013747 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Database; /** * Handles the database optimization process. */ class Optimization { /** * Background process instance * * @var OptimizationProcess $process Background Process instance. */ protected $process; /** * Class constructor. * * @param OptimizationProcess $process Background process instance. */ public function __construct( OptimizationProcess $process ) { $this->process = $process; } /** * Get Database options * * @since 3.0.4 * * @return array */ public function get_options() { return [ 'database_revisions' => __( 'Revisions', 'rocket' ), 'database_auto_drafts' => __( 'Auto Drafts', 'rocket' ), 'database_trashed_posts' => __( 'Trashed Posts', 'rocket' ), 'database_spam_comments' => __( 'Spam Comments', 'rocket' ), 'database_trashed_comments' => __( 'Trashed Comments', 'rocket' ), 'database_all_transients' => __( 'Transients', 'rocket' ), 'database_optimize_tables' => __( 'Tables', 'rocket' ), ]; } /** * Performs the database optimization * * @since 2.11 * * @param array $options WP Rocket Database options. */ public function process_handler( $options ) { if ( method_exists( $this->process, 'cancel_process' ) ) { // @phpstan-ignore-line $this->process->cancel_process(); } array_map( [ $this->process, 'push_to_queue' ], $options ); $this->process->save()->dispatch(); } /** * Count the number of items concerned by the database cleanup * * @since 2.8 * * @param string $type Item type to count. * @return int Number of items for this type */ public function count_cleanup_items( $type ) { global $wpdb; $count = 0; switch ( $type ) { case 'database_revisions': $count = $wpdb->get_var( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_type = 'revision'" ); break; case 'database_auto_drafts': $count = $wpdb->get_var( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_status = 'auto-draft'" ); break; case 'database_trashed_posts': $count = $wpdb->get_var( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_status = 'trash'" ); break; case 'database_spam_comments': $count = $wpdb->get_var( "SELECT COUNT(comment_ID) FROM $wpdb->comments WHERE comment_approved = 'spam'" ); break; case 'database_trashed_comments': $count = $wpdb->get_var( "SELECT COUNT(comment_ID) FROM $wpdb->comments WHERE (comment_approved = 'trash' OR comment_approved = 'post-trashed')" ); break; case 'database_all_transients': $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(option_id) FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_site_transient_' ) . '%' ) ); break; case 'database_optimize_tables': $count = $wpdb->get_var( "SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema = '" . DB_NAME . "' and Engine <> 'InnoDB' and data_free > 0" ); break; } return $count; } } Engine/Admin/Database/OptimizationProcess.php 0000644 00000010177 15174677547 0015300 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Database; use WP_Rocket_WP_Background_Process; /** * Extends the background process class for the database optimization background process. * * @see WP_Rocket_WP_Background_Process */ class OptimizationProcess extends WP_Rocket_WP_Background_Process { /** * Prefix * * @var string */ protected $prefix = 'rocket'; /** * Specific action identifier for sitemap preload. * * @var string Action identifier */ protected $action = 'database_optimization'; /** * Count the number of optimized items. * * @var array $count An array of indexed number of optimized items. */ protected $count = []; /** * Dispatch * * @return void */ public function dispatch() { set_transient( 'rocket_database_optimization_process', 'running', HOUR_IN_SECONDS ); // Perform remote post. parent::dispatch(); } /** * Perform the optimization corresponding to $item * * @param mixed $item Queue item to iterate over. * * @return bool false */ protected function task( $item ) { global $wpdb; switch ( $item ) { case 'database_revisions': $query = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_type = 'revision'" ); if ( $query ) { $number = 0; foreach ( $query as $id ) { $number += wp_delete_post_revision( intval( $id ) ) instanceof \WP_Post ? 1 : 0; } $this->count[ $item ] = $number; } break; case 'database_auto_drafts': $query = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_status = 'auto-draft'" ); if ( $query ) { $number = 0; foreach ( $query as $id ) { $number += wp_delete_post( intval( $id ), true ) instanceof \WP_Post ? 1 : 0; } $this->count[ $item ] = $number; } break; case 'database_trashed_posts': $query = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_status = 'trash'" ); if ( $query ) { $number = 0; foreach ( $query as $id ) { $number += wp_delete_post( $id, true ) instanceof \WP_Post ? 1 : 0; } $this->count[ $item ] = $number; } break; case 'database_spam_comments': $query = $wpdb->get_col( "SELECT comment_ID FROM $wpdb->comments WHERE comment_approved = 'spam'" ); if ( $query ) { $number = 0; foreach ( $query as $id ) { $number += (int) wp_delete_comment( intval( $id ), true ); } $this->count[ $item ] = $number; } break; case 'database_trashed_comments': $query = $wpdb->get_col( "SELECT comment_ID FROM $wpdb->comments WHERE (comment_approved = 'trash' OR comment_approved = 'post-trashed')" ); if ( $query ) { $number = 0; foreach ( $query as $id ) { $number += (int) wp_delete_comment( intval( $id ), true ); } $this->count[ $item ] = $number; } break; case 'database_all_transients': $query = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_site_transient_' ) . '%' ) ); if ( $query ) { $number = 0; foreach ( $query as $transient ) { if ( strpos( $transient, '_site_transient_' ) !== false ) { $number += (int) delete_site_transient( str_replace( '_site_transient_', '', $transient ) ); } else { $number += (int) delete_transient( str_replace( '_transient_', '', $transient ) ); } } $this->count[ $item ] = $number; } break; case 'database_optimize_tables': $query = $wpdb->get_results( "SELECT table_name AS table_name, data_free AS data_free FROM information_schema.tables WHERE table_schema = '" . DB_NAME . "' and Engine <> 'InnoDB' and data_free > 0" ); if ( $query ) { $number = 0; foreach ( $query as $table ) { $number += (int) $wpdb->query( "OPTIMIZE TABLE $table->table_name" ); } $this->count[ $item ] = $number; } break; } return false; } /** * Complete */ protected function complete() { delete_transient( 'rocket_database_optimization_process' ); set_transient( 'rocket_database_optimization_process_complete', $this->count ); parent::complete(); } } Engine/Admin/ActionSchedulerSubscriber.php 0000644 00000001676 15174677547 0014653 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\ThirdParty\ReturnTypesTrait; class ActionSchedulerSubscriber implements Subscriber_Interface { use ReturnTypesTrait; /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'action_scheduler_check_pastdue_actions' => 'return_false', 'action_scheduler_extra_action_counts' => 'hide_pastdue_status_filter', ]; } /** * Hide past-due from status filter in Action Scheduler tools page. * * @param array $extra_actions Array with format action_count_identifier => action count. * * @return array */ public function hide_pastdue_status_filter( array $extra_actions ) { if ( ! isset( $extra_actions['past-due'] ) ) { return $extra_actions; } unset( $extra_actions['past-due'] ); return $extra_actions; } } Engine/Admin/Settings/Subscriber.php 0000644 00000021703 15174677547 0013447 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Settings; use Imagify_Partner; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket\Dependencies\WPMedia\PluginFamily\Controller\{ PluginFamily, PluginFamilyInterface }; /** * WP Rocket settings page subscriber. * * @since 3.5.5 Moves into the new architecture. * @since 3.3 */ class Subscriber implements Subscriber_Interface, PluginFamilyInterface { /** * Page instance * * @var Page */ private $page; /** * PluginFamily instance * * @var PluginFamily * * @since 3.17.2 */ protected $plugin_family; /** * Creates an instance of the object. * * @param Page $page Page instance. * @param PluginFamily $plugin_family Plugin Family Instance. */ public function __construct( Page $page, PluginFamily $plugin_family ) { $this->page = $page; $this->plugin_family = $plugin_family; } /** * Return an array of events that this subscriber wants to listen to. * * @since 3.3 * * @return array */ public static function get_subscribed_events() { $events = [ 'admin_menu' => 'add_admin_page', 'admin_init' => 'configure', 'wp_ajax_rocket_refresh_customer_data' => 'refresh_customer_data', 'wp_ajax_rocket_toggle_option' => 'toggle_option', 'rocket_settings_menu_navigation' => [ [ 'add_menu_tools_page' ], [ 'add_imagify_page', 9 ], [ 'add_tutorials_page', 11 ], [ 'add_plugins_page', 12 ], ], 'admin_enqueue_scripts' => [ [ 'enqueue_rocket_scripts' ], [ 'enqueue_url' ], ], 'script_loader_tag' => [ 'async_wistia_script', 10, 2 ], 'rocket_after_settings_radio_options' => [ 'display_radio_options_sub_fields', 11 ], 'rocket_settings_tools_content' => 'display_mobile_cache_option', 'wp_ajax_rocket_enable_mobile_cache' => 'enable_mobile_cache', 'wp_rocket_upgrade' => [ 'enable_separate_cache_files_mobile', 9, 2 ], 'admin_notices' => 'display_update_notice', ]; foreach ( PluginFamily::get_subscribed_events() as $hook => $callback ) { if ( isset( $events[ $hook ] ) ) { // Make sure it's an array of callbacks. if ( ! is_array( $events[ $hook ][0] ) ) { $events[ $hook ] = [ $events[ $hook ] ]; } // Wrap single callback in array if needed. $events[ $hook ][] = is_array( $callback ) ? $callback : [ $callback ]; } else { $events[ $hook ] = is_array( $callback ) ? $callback : [ [ $callback ] ]; } } return $events; } /** * Enqueue the URL for option exporting. * * @return void */ public function enqueue_url() { wp_localize_script( 'wpr-admin-common', 'rocket_option_export', [ 'rest_url_option_export' => rest_url( 'wp-rocket/v1/options/export/' ), ] ); } /** * Enqueues WP Rocket scripts on the settings page * * @since 3.6 * * @param string $hook The current admin page. * * @return void */ public function enqueue_rocket_scripts( $hook ) { $this->page->enqueue_rocket_scripts( $hook ); } /** * Adds the async attribute to the Wistia script * * @param string $tag The <script> tag for the enqueued script. * @param string $handle The script's registered handle. * * @return string */ public function async_wistia_script( $tag, $handle ) { return $this->page->async_wistia_script( $tag, $handle ); } /** * Adds plugin page to the Settings menu. * * @since 3.0 */ public function add_admin_page() { add_options_page( $this->page->get_title(), /** * Filters the menu title to display in the Settings sub-menu * * @since 3.7.4 * * @param string $menu_title The text to be used for the menu. */ apply_filters( 'rocket_menu_title', $this->page->get_title() ), $this->page->get_capability(), $this->page->get_slug(), [ $this->page, 'render_page' ] ); } /** * Registers the settings, page sections, fields sections and fields. * * @since 3.0 */ public function configure() { $this->page->configure(); } /** * Gets customer data to refresh it on the dashboard with AJAX. * * @since 3.0 * * @return string */ public function refresh_customer_data() { check_ajax_referer( 'rocket-ajax' ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_die(); } delete_transient( 'wp_rocket_customer_data' ); delete_transient( 'wpr_user_information_timeout_active' ); delete_transient( 'wpr_user_information_timeout' ); return wp_send_json_success( $this->page->customer_data() ); } /** * Toggle sliding checkboxes option value. * * @since 3.0 */ public function toggle_option() { $this->page->toggle_option(); } /** * Add Tools section to navigation. * * @since 3.0 * * @param array $navigation Array of menu items. * @return array */ public function add_menu_tools_page( $navigation ) { $navigation['tools'] = [ 'id' => 'tools', 'title' => __( 'Tools', 'rocket' ), 'menu_description' => __( 'Import, Export, Rollback', 'rocket' ), ]; return $navigation; } /** * Add Imagify section to navigation. * * @since 3.2 * * @param array $navigation Array of menu items. * @return array */ public function add_imagify_page( $navigation ) { if ( rocket_get_constant( 'WP_ROCKET_WHITE_LABEL_ACCOUNT' ) || Imagify_Partner::has_imagify_api_key() ) { return $navigation; } $navigation['imagify'] = [ 'id' => 'imagify', 'title' => __( 'Image Optimization', 'rocket' ), 'menu_description' => __( 'Compress your images', 'rocket' ), ]; return $navigation; } /** * Add Tutorials section to navigation. * * @since 3.4 * * @param array $navigation Array of menu items. * @return array */ public function add_tutorials_page( $navigation ) { $navigation['tutorials'] = [ 'id' => 'tutorials', 'title' => __( 'Tutorials', 'rocket' ), 'menu_description' => __( 'Getting started and how to videos', 'rocket' ), ]; return $navigation; } /** * Displays the radio option sub fields * * @since 3.10 * * @param array $option_data array of option_id and sub_fields of the option. * * @return void */ public function display_radio_options_sub_fields( $option_data ) { if ( empty( $option_data['sub_fields'] ) ) { return; } $this->page->display_radio_options_sub_fields( $option_data['sub_fields'] ); } /** * Render mobile cache option. * * @return void */ public function display_mobile_cache_option(): void { $this->page->display_mobile_cache_option(); } /** * Callback method for the AJAX request to mobile cache. * * @return void */ public function enable_mobile_cache(): void { $this->page->enable_mobile_cache(); } /** * Enable Separate cache files for mobile devices on upgrade. * * @param string $new_version New plugin version. * @param string $old_version Previous plugin version. * @return void */ public function enable_separate_cache_files_mobile( $new_version, $old_version ): void { if ( version_compare( $old_version, '3.16', '>' ) ) { return; } $this->page->enable_separate_cache_files_mobile(); } /** * Display the update notice. * * @return void */ public function display_update_notice() { $this->page->display_update_notice(); } /** * Add Plugins section to navigation. * * @since 3.17.2 * * @param array $navigation Array of menu items. * @return array */ public function add_plugins_page( $navigation ) { $navigation['plugins'] = [ 'id' => 'plugins', 'title' => __( 'Our Plugins', 'rocket' ), 'menu_description' => __( 'Build Better, Faster, Safer', 'rocket' ), ]; return $navigation; } /** * Install and activate plugin method for plugin family * * @return void */ public function install_activate(): void { $this->plugin_family->install_activate(); } /** * Display error related to plugin family * * @return void */ public function display_error_notice(): void { $this->plugin_family->display_error_notice(); } /** * Enqueue block editor assets. * * @return void */ public function enqueue_assets(): void { $this->plugin_family->enqueue_assets(); } /** * Install Imagify using the ajax request. * * @return void */ public function install_imagify(): void { $this->plugin_family->install_imagify(); } /** * Enqueue Admin assets. * * @param string $page Page ID. * @return void */ public function enqueue_admin_assets( $page ): void { $this->plugin_family->enqueue_admin_assets( $page ); } /** * Insert admin footer JS templates. * * @return void */ public function insert_footer_templates(): void { $this->plugin_family->insert_footer_templates(); } /** * Dismiss promote Imagify using the ajax request. * * @return void */ public function dismiss_promote_imagify(): void { $this->plugin_family->dismiss_promote_imagify(); } } Engine/Admin/Settings/DataClearingTrait.php 0000644 00000002073 15174677547 0014665 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Settings; trait DataClearingTrait { /** * Truncate data table when action is taken * * @param array $data An array containing the status and message. * @param string $transient The transient key to set after cleaning. * * @return void */ protected function clean_data( $data, string $transient ): void { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( empty( $data ) || 'die' === $data['status'] ) { wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } if ( 'error' === $data['status'] ) { wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } rocket_clean_domain(); rocket_dismiss_box( 'rocket_warning_plugin_modification' ); set_transient( $transient, $data ); wp_safe_redirect( esc_url_raw( wp_get_referer() ) ); rocket_get_constant( 'WP_ROCKET_IS_TESTING', false ) ? wp_die() : exit; } } Engine/Admin/Settings/Page.php 0000644 00000253325 15174677547 0012227 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Settings; use WP_Rocket\Engine\Admin\Database\Optimization; use WP_Rocket\Engine\Admin\Beacon\Beacon; use WP_Rocket\Engine\License\API\UserClient; use WP_Rocket\Engine\Optimization\DelayJS\Admin\SiteList; use WP_Rocket\Engine\Optimization\DelayJS\Admin\Settings as DelayJSSettings; use WP_Rocket\Abstract_Render; use WP_Rocket\Admin\Options_Data; /** * Registers the admin page and WP Rocket settings. * * @since 3.5.5 Moves into the new architecture. * @since 3.0 */ class Page extends Abstract_Render { /** * Plugin slug. * * @since 3.0 * * @var string */ private $slug; /** * Plugin page title. * * @since 3.0 * * @var string */ private $title; /** * Required capability to access the page. * * @since 3.0 * * @var string */ private $capability; /** * Settings instance. * * @since 3.0 * * @var Settings */ private $settings; /** * Render implementation. * * @since 3.0 * * @var Render */ private $render; /** * Beacon instance. * * @since 3.2 * * @var Beacon */ private $beacon; /** * Database optimization instance. * * @since 3.3 * * @var Optimization */ private $optimize; /** * User client instance. * * @var UserClient */ private $user_client; /** * Delay JS Site List controller. * * @var SiteList */ protected $delayjs_sitelist; /** * WP Rocket options instance * * @var Options_Data */ private $options; /** * Creates an instance of the Page object. * * @since 3.0 * * @param array $args Array of required arguments to add the admin page. * @param Settings $settings Instance of Settings class. * @param Render $render Render instance. * @param Beacon $beacon Beacon instance. * @param Optimization $optimize Database optimization instance. * @param UserClient $user_client User client instance. * @param SiteList $delayjs_sitelist User client instance. * @param string $template_path Path to views. * @param Options_Data $options WP Rocket options instance. */ public function __construct( array $args, Settings $settings, Render $render, Beacon $beacon, Optimization $optimize, UserClient $user_client, SiteList $delayjs_sitelist, $template_path, Options_Data $options ) { parent::__construct( $template_path ); $args = array_merge( [ 'slug' => 'wprocket', 'title' => 'WP Rocket', 'capability' => 'rocket_manage_options', ], $args ); $this->slug = $args['slug']; $this->title = $args['title']; $this->capability = $args['capability']; $this->settings = $settings; $this->render = $render; $this->beacon = $beacon; $this->optimize = $optimize; $this->user_client = $user_client; $this->delayjs_sitelist = $delayjs_sitelist; $this->options = $options; } /** * Returns the settings page title. * * @since 3.3 * * @return string */ public function get_title() { return $this->title; } /** * Returns the settings page slug. * * @since 3.3 * * @return string */ public function get_slug() { return $this->slug; } /** * Returns the settings page capability. * * @since 3.3 * * @return string */ public function get_capability() { return $this->capability; } /** * Registers the settings, page sections, fields sections and fields. * * @since 3.0 */ public function configure() { register_setting( $this->slug, WP_ROCKET_SLUG, [ $this->settings, 'sanitize_callback' ] ); } /** * Renders the settings page. * * @since 3.0 */ public function render_page() { $rocket_valid_key = rocket_valid_key(); if ( $rocket_valid_key ) { $this->dashboard_section(); $this->assets_section(); $this->media_section(); $this->preload_section(); $this->advanced_cache_section(); $this->database_section(); $this->cdn_section(); $this->heartbeat_section(); $this->addons_section(); $this->cloudflare_section(); $this->sucuri_section(); } else { $this->license_section(); } $this->render->set_settings( $this->settings->get_settings() ); $this->hidden_fields(); $this->render->set_hidden_settings( $this->settings->get_hidden_settings() ); $btn_submit_text = $rocket_valid_key ? __( 'Save Changes', 'rocket' ) : __( 'Validate License', 'rocket' ); echo $this->render->generate( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. 'page', [ 'slug' => $this->slug, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. 'btn_submit_text' => $btn_submit_text, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. ] ); } /** * Enqueues WP Rocket scripts on the settings page * * @since 3.6.1 * * @param string $hook The current admin page. * * @return void */ public function enqueue_rocket_scripts( $hook ) { if ( 'settings_page_wprocket' !== $hook ) { return; } wp_enqueue_script( 'wistia-e-v1', 'https://fast.wistia.com/assets/external/E-v1.js', [], null, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion } /** * Adds the async attribute to the Wistia script * * @since 3.6.1 * * @param string $tag The <script> tag for the enqueued script. * @param string $handle The script's registered handle. * * @return string */ public function async_wistia_script( $tag, $handle ) { if ( 'wistia-e-v1' !== $handle ) { return $tag; } return str_replace( ' src', ' async src', $tag ); } /** * Returns the customer data to display on the dashboard * * @since 3.7.3 Update to use the user client class to get the data * @since 3.0 * * @return array */ public function customer_data() { $user = $this->user_client->get_user_data(); $data = [ 'license_type' => __( 'Unavailable', 'rocket' ), 'license_expiration' => __( 'Unavailable', 'rocket' ), 'license_class' => 'wpr-isInvalid', 'is_from_one_dot_com' => false, ]; $data['license_type'] = rocket_get_license_type( $user ); if ( ! is_object( $user ) ) { return $data; } if ( ! empty( $user->licence_expiration ) ) { $data['license_class'] = time() < $user->licence_expiration ? 'wpr-isValid' : 'wpr-isInvalid'; } if ( ! empty( $user->licence_expiration ) ) { $data['license_expiration'] = date_i18n( get_option( 'date_format' ), (int) $user->licence_expiration ); } if ( isset( $user->{'has_one-com_account'} ) ) { $data['is_from_one_dot_com'] = (bool) $user->{'has_one-com_account'}; } return $data; } /** * Toggle sliding checkboxes option value. * * @since 3.0 */ public function toggle_option() { check_ajax_referer( 'rocket-ajax' ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_die(); } $allowed = [ 'debug_enabled' => 1, 'varnish_auto_purge' => 1, 'do_cloudflare' => 1, 'cloudflare_protocol_rewrite' => 1, 'sucury_waf_cache_sync' => 1, 'sucury_waf_api_key' => 1, 'cache_webp' => 1, 'cache_logged_user' => 1, ]; if ( ! isset( $_POST['option']['name'] ) || ! isset( $allowed[ $_POST['option']['name'] ] ) ) { wp_die(); } $value = (int) ! empty( $_POST['option']['value'] ); update_rocket_option( sanitize_key( $_POST['option']['name'] ), $value ); wp_die(); } /** * Forces the value for the mobile options if a mobile plugin is active. * * @since 3.0 * @since 3.2 Not used anymore. * @see \WP_Rocket\Subscriber\Third_Party\Plugins\Mobile_Subscriber::is_mobile_plugin_active_callback() * * @param mixed $value Option value. * * @return mixed */ public function is_mobile_plugin_active( $value ) { if ( rocket_is_mobile_plugin_active() ) { return 1; } return $value; } /** * Registers License section. * * @since 3.0 */ private function license_section() { $this->settings->add_page_section( 'license', [ 'title' => __( 'License', 'rocket' ), ] ); $this->settings->add_settings_sections( [ 'license_section' => [ 'type' => 'nocontainer', 'page' => 'license', ], ] ); $this->settings->add_settings_fields( [ 'consumer_key' => [ 'type' => 'text', 'label' => __( 'API key', 'rocket' ), 'default' => '', 'container_class' => [ 'wpr-field--split', 'wpr-isDisabled', ], 'section' => 'license_section', 'page' => 'license', 'sanitize_callback' => 'sanitize_text_field', 'input_attr' => [ 'disabled' => 1, ], ], 'consumer_email' => [ 'type' => 'text', 'label' => __( 'Email address', 'rocket' ), 'default' => '', 'container_class' => [ 'wpr-field--split', 'wpr-isDisabled', ], 'section' => 'license_section', 'page' => 'license', 'sanitize_callback' => 'sanitize_email', 'input_attr' => [ 'disabled' => 1, ], ], ] ); } /** * Registers Dashboard section. * * @since 3.0 */ private function dashboard_section() { $this->settings->add_page_section( 'dashboard', [ 'title' => __( 'Dashboard', 'rocket' ), 'menu_description' => __( 'Get help, account info', 'rocket' ), 'faq' => $this->beacon->get_suggest( 'faq' ), 'customer_data' => $this->customer_data(), ] ); } /** * Registers CSS & Javascript section. * * @since 3.0 */ private function assets_section() { $combine_beacon = $this->beacon->get_suggest( 'combine' ); $defer_js_beacon = $this->beacon->get_suggest( 'defer_js' ); $async_beacon = $this->beacon->get_suggest( 'async' ); $files_beacon = $this->beacon->get_suggest( 'file_optimization' ); $inline_js_beacon = $this->beacon->get_suggest( 'exclude_inline_js' ); $exclude_js_beacon = $this->beacon->get_suggest( 'exclude_js' ); $exclude_css_beacon = $this->beacon->get_suggest( 'exclude_css' ); $delay_js_beacon = $this->beacon->get_suggest( 'delay_js' ); $delay_js_exclusions_beacon = $this->beacon->get_suggest( 'delay_js_exclusions' ); $exclude_defer_js = $this->beacon->get_suggest( 'exclude_defer_js' ); $rucss_beacon = $this->beacon->get_suggest( 'remove_unused_css' ); $offline_beacon = $this->beacon->get_suggest( 'offline' ); $fallback_css_beacon = $this->beacon->get_suggest( 'fallback_css' ); $disable_combine_js = $this->disable_combine_js(); $disable_ocd = 'local' === wp_get_environment_type(); /** * Filters the status of the RUCSS option. * * @param array $should_disable will return array with disable status and text. */ $rucss_status = apply_filters( 'rocket_disable_rucss_setting', [ 'disable' => false, 'text' => '', ] ); $invalid_license = get_option( 'wp_rocket_no_licence' ); $this->settings->add_page_section( 'file_optimization', [ 'title' => __( 'File Optimization', 'rocket' ), 'menu_description' => __( 'Optimize CSS & JS', 'rocket' ), ] ); $css_section_helper = []; if ( rocket_maybe_disable_minify_css() ) { // translators: %1$s = type of minification (HTML, CSS or JS), %2$s = “WP Rocket”. $css_section_helper[] = sprintf( __( '%1$s Minification is currently activated in <strong>Autoptimize</strong>. If you want to use %2$s’s minification, disable this option in Autoptimize.', 'rocket' ), 'CSS', WP_ROCKET_PLUGIN_NAME ); } if ( $rucss_status['disable'] ) { $css_section_helper[] = $rucss_status['text']; } $this->settings->add_settings_sections( [ 'css' => [ 'title' => __( 'CSS Files', 'rocket' ), 'help' => [ 'id' => $this->beacon->get_suggest( 'css_section' ), 'url' => $files_beacon['url'], ], 'page' => 'file_optimization', 'helper' => $css_section_helper, ], 'js' => [ 'title' => __( 'JavaScript Files', 'rocket' ), 'help' => [ 'id' => $this->beacon->get_suggest( 'js_section' ), 'url' => $files_beacon['url'], ], 'page' => 'file_optimization', // translators: %1$s = type of minification (HTML, CSS or JS), %2$s = “WP Rocket”. 'helper' => rocket_maybe_disable_minify_js() ? sprintf( __( '%1$s Minification is currently activated in <strong>Autoptimize</strong>. If you want to use %2$s’s minification, disable those options in Autoptimize.', 'rocket' ), 'JS', WP_ROCKET_PLUGIN_NAME ) : '', ], ] ); $delay_js_list_helper = sprintf( // translators: %1$s = opening </a> tag, %2$s = closing </a> tag. esc_html__( 'Also, please check our %1$sdocumentation%2$s for a list of compatibility exclusions.', 'rocket' ), '<a href="' . esc_url( $delay_js_exclusions_beacon['url'] ) . '" target="_blank" rel="noopener">', '</a>' ); $delay_js_found_list_helper = esc_html__( 'Internal scripts are excluded by default to prevent issues. Remove them to take full advantage of this option.', 'rocket' ); $delay_js_found_list_helper .= '<br>' . sprintf( // translators: %1$s = opening </a> tag, %2$s = closing </a> tag. esc_html__( 'If this causes trouble, restore the default exclusions, found %1$shere%2$s', 'rocket' ), '<a href="' . esc_url( $delay_js_beacon['url'] ) . '" target="_blank" rel="noopener">', '</a>' ); $this->settings->add_settings_fields( [ 'minify_css' => [ 'type' => 'checkbox', 'label' => __( 'Minify CSS files', 'rocket' ), 'description' => __( 'Minify CSS removes whitespace and comments to reduce the file size.', 'rocket' ), 'container_class' => [ rocket_maybe_disable_minify_css() ? 'wpr-isDisabled' : '', ], 'section' => 'css', 'page' => 'file_optimization', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', 'input_attr' => [ 'disabled' => rocket_maybe_disable_minify_css() ? 1 : 0, ], ], 'exclude_css' => [ 'type' => 'textarea', 'label' => __( 'Excluded CSS Files', 'rocket' ), 'description' => __( 'Specify URLs of CSS files to be excluded from minification (one per line).', 'rocket' ), 'helper' => __( '<strong>Internal:</strong> The domain part of the URL will be stripped automatically. Use (.*).css wildcards to exclude all CSS files located at a specific path.', 'rocket' ) . '<br>' . // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. sprintf( __( '<strong>3rd Party:</strong> Use either the full URL path or only the domain name, to exclude external CSS. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $exclude_css_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $exclude_css_beacon['id'] ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ), 'container_class' => [ 'wpr-field--children', ], 'placeholder' => '/wp-content/plugins/some-plugin/(.*).css', 'parent' => 'minify_css', 'section' => 'css', 'page' => 'file_optimization', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], 'optimize_css_delivery' => [ 'type' => 'checkbox', 'label' => __( 'Optimize CSS delivery', 'rocket' ), 'container_class' => [ $disable_ocd ? 'wpr-isDisabled' : '', 'wpr-isParent', ], 'description' => $invalid_license ? __( 'Optimize CSS delivery eliminates render-blocking CSS on your website. Only one method can be selected. Remove Unused CSS is recommended for optimal performance, but limited only to the users with active license.', 'rocket' ) : __( 'Optimize CSS delivery eliminates render-blocking CSS on your website. Only one method can be selected. Remove Unused CSS is recommended for optimal performance.', 'rocket' ), 'section' => 'css', 'page' => 'file_optimization', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', 'input_attr' => [ 'disabled' => $disable_ocd ? 1 : 0, ], 'helper' => $disable_ocd ? sprintf( // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. __( 'Optimize CSS Delivery features are disabled on local environments. %1$sLearn more%2$s', 'rocket' ), '<a href="' . esc_url( $offline_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $offline_beacon['id'] ) . '" target="_blank">', '</a>' ) : '', ], 'optimize_css_delivery_method' => [ 'type' => 'radio_buttons', 'label' => __( 'Optimize CSS delivery', 'rocket' ), 'container_class' => [ 'wpr-field--children', 'wpr-field--optimize-css-delivery', ], 'buttons_container_class' => '', 'parent' => 'optimize_css_delivery', 'section' => 'css', 'page' => 'file_optimization', 'default' => 'remove_unused_css', 'sanitize_callback' => 'sanitize_checkbox', 'options' => [ 'remove_unused_css' => [ 'label' => __( 'Remove Unused CSS', 'rocket' ), 'disabled' => $invalid_license || $rucss_status['disable'] ? 'disabled' : false, // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Removes unused CSS per page and helps to reduce page size and HTTP requests. Recommended for best performance. Test thoroughly! %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $rucss_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $rucss_beacon['id'] ) . '" target="_blank">', '</a>' ), 'warning' => $invalid_license ? [] : [ 'title' => __( 'This could break things!', 'rocket' ), 'description' => __( 'If you notice any errors on your website after having activated this setting, just deactivate it again, and your site will be back to normal.', 'rocket' ), 'button_label' => __( 'Activate Remove Unused CSS', 'rocket' ), ], 'sub_fields' => $invalid_license ? [] : [ 'remove_unused_css_safelist' => [ 'type' => 'textarea', 'label' => __( 'CSS safelist', 'rocket' ), 'description' => __( 'Specify CSS filenames, IDs or classes that should not be removed (one per line).', 'rocket' ), 'placeholder' => "/wp-content/plugins/some-plugin/(.*).css\n.css-class\n#css_id\ntag", 'default' => [], 'value' => [], 'sanitize_callback' => 'sanitize_textarea', 'parent' => '', 'section' => 'css', 'page' => 'file_optimization', 'input_attr' => [ 'disabled' => get_rocket_option( 'remove_unused_css' ) ? 0 : 1, ], ], ], ], 'async_css' => [ 'label' => __( 'Load CSS asynchronously', 'rocket' ), 'description' => is_plugin_active( 'wp-criticalcss/wp-criticalcss.php' ) ? // translators: %1$s = plugin name. sprintf( _x( 'Load CSS asynchronously is currently handled by the %1$s plugin. If you want to use WP Rocket’s load CSS asynchronously option, disable the %1$s plugin.', 'WP Critical CSS compatibility', 'rocket' ), 'WP Critical CSS' ) : // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. sprintf( __( 'Generates critical path CSS and loads CSS asynchronously. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $async_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $async_beacon['id'] ) . '" target="_blank">', '</a>' ), 'disabled' => is_plugin_active( 'wp-criticalcss/wp-criticalcss.php' ) ? 'disabled' : '', 'sub_fields' => [ 'critical_css' => [ 'type' => 'textarea', 'label' => __( 'Fallback critical CSS', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'helper' => sprintf( __( 'Provides a fallback if auto-generated critical path CSS is incomplete. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $fallback_css_beacon['url'] ) . '#fallback" data-beacon-article="' . esc_attr( $fallback_css_beacon['id'] ) . '" target="_blank">', '</a>' ), 'sanitize_callback' => 'sanitize_textarea', 'parent' => '', 'section' => 'css', 'page' => 'file_optimization', 'placeholder' => '', 'default' => [], 'value' => [], ], ], ], ], ], 'minify_js' => [ 'type' => 'checkbox', 'label' => __( 'Minify JavaScript files', 'rocket' ), 'description' => __( 'Minify JavaScript removes whitespace and comments to reduce the file size.', 'rocket' ), 'container_class' => [ rocket_maybe_disable_minify_js() ? 'wpr-isDisabled' : '', ], 'section' => 'js', 'page' => 'file_optimization', 'default' => 0, 'input_attr' => [ 'disabled' => rocket_maybe_disable_minify_js() ? 1 : 0, ], 'sanitize_callback' => 'sanitize_checkbox', ], 'minify_concatenate_js' => [ 'type' => 'checkbox', 'label' => __( 'Combine JavaScript files <em>(Enable Minify JavaScript files to select)</em>', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Combine JavaScript files combines your site’s internal, 3rd party and inline JS reducing HTTP requests. Not recommended if your site uses HTTP/2. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $combine_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $combine_beacon['id'] ) . '" target="_blank">', '</a>' ), 'helper' => get_rocket_option( 'delay_js' ) ? __( 'For compatibility and best results, this option is disabled when delay javascript execution is enabled.', 'rocket' ) : '', 'container_class' => [ $disable_combine_js ? 'wpr-isDisabled' : '', 'wpr-field--parent', 'wpr-NoPaddingBottom', ], 'section' => 'js', 'page' => 'file_optimization', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', 'input_attr' => [ 'disabled' => $disable_combine_js ? 1 : 0, ], 'warning' => [ 'title' => __( 'This could break things!', 'rocket' ), 'description' => __( 'If you notice any errors on your website after having activated this setting, just deactivate it again, and your site will be back to normal.', 'rocket' ), 'button_label' => __( 'Activate combine JavaScript', 'rocket' ), ], ], 'exclude_inline_js' => [ 'type' => 'textarea', 'label' => __( 'Excluded Inline JavaScript', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Specify patterns of inline JavaScript to be excluded from concatenation (one per line). %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $inline_js_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $inline_js_beacon['id'] ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ), 'container_class' => [ 'wpr-field--children', ], 'placeholder' => 'recaptcha', 'parent' => 'minify_concatenate_js', 'section' => 'js', 'page' => 'file_optimization', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', 'input_attr' => [ 'disabled' => get_rocket_option( 'minify_concatenate_js' ) ? 0 : 1, ], ], 'exclude_js' => [ 'type' => 'textarea', 'label' => __( 'Excluded JavaScript Files', 'rocket' ), 'description' => __( 'Specify URLs of JavaScript files to be excluded from minification and concatenation (one per line).', 'rocket' ), 'helper' => __( '<strong>Internal:</strong> The domain part of the URL will be stripped automatically. Use (.*).js wildcards to exclude all JS files located at a specific path.', 'rocket' ) . '<br>' . // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. sprintf( __( '<strong>3rd Party:</strong> Use either the full URL path or only the domain name, to exclude external JS. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $exclude_js_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $exclude_js_beacon['id'] ) . '" rel="noopener noreferrer" target="_blank">', '</a>' ), 'container_class' => [ 'wpr-field--children', ], 'placeholder' => '/wp-content/themes/some-theme/(.*).js', 'parent' => 'minify_js', 'section' => 'js', 'page' => 'file_optimization', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], 'defer_all_js' => [ 'container_class' => [ 'wpr-isParent', ], 'type' => 'checkbox', 'label' => __( 'Load JavaScript deferred', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Load JavaScript deferred eliminates render-blocking JS on your site and can improve load time. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $defer_js_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $defer_js_beacon['id'] ) . '" target="_blank">', '</a>' ), 'section' => 'js', 'page' => 'file_optimization', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'exclude_defer_js' => [ 'container_class' => [ 'wpr-field--children', ], 'type' => 'textarea', 'label' => __( 'Excluded JavaScript Files', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Specify URLs or keywords of JavaScript files to be excluded from defer (one per line). %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $exclude_defer_js['url'] ) . '" data-beacon-article="' . esc_attr( $exclude_defer_js['id'] ) . '" target="_blank">', '</a>' ), 'placeholder' => '/wp-content/themes/some-theme/(.*).js', 'parent' => 'defer_all_js', 'section' => 'js', 'page' => 'file_optimization', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], 'delay_js' => apply_filters( 'rocket_delay_js_settings_field', [ 'container_class' => [ 'wpr-isParent', 'wpr-Delayjs', ], 'type' => 'checkbox', 'label' => __( 'Delay JavaScript execution', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Improves performance by delaying the loading of JavaScript files until user interaction (e.g. scroll, click). %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $delay_js_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $delay_js_beacon['id'] ) . '" target="_blank">', '</a>' ), 'section' => 'js', 'page' => 'file_optimization', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ] ), 'delay_js_exclusions_selected' => [ 'type' => 'categorized_multiselect', 'label' => __( 'One-click exclusions', 'rocket' ), 'description' => __( 'When using the Delay JavaScript feature, you might notice that some elements in the viewport take time to appear.', 'rocket' ), 'sub_description' => __( 'If you need these elements to load immediately, select the related plugins, themes, or services below to ensure they appear without delay.', 'rocket' ), 'container_class' => [ 'wpr-field--children', ], 'parent' => 'delay_js', 'section' => 'js', 'page' => 'file_optimization', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', 'input_attr' => [ 'disabled' => get_rocket_option( 'delay_js' ) ? 0 : 1, ], 'items' => $this->delayjs_sitelist->prepare_delayjs_ui_list(), ], 'delay_js_exclusions' => [ 'type' => 'textarea', 'container_class' => [ 'wpr-field--children', ], 'label' => __( 'Excluded JavaScript Files', 'rocket' ), 'description' => __( 'Specify URLs or keywords that can identify inline or JavaScript files to be excluded from delaying execution (one per line).', 'rocket' ), 'parent' => 'delay_js', 'section' => 'js', 'page' => 'file_optimization', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', 'input_attr' => [ 'disabled' => get_rocket_option( 'delay_js' ) ? 0 : 1, ], 'helper' => DelayJSSettings::exclusion_list_has_default() ? $delay_js_found_list_helper : $delay_js_list_helper, 'placeholder' => '', ], 'delay_js_execution_safe_mode' => [ 'type' => 'checkbox', 'label' => __( 'Safe Mode for Delay JavaScript Execution', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => __( 'The Safe Mode mode prevents all internal scripts from being delayed.', 'rocket' ), 'helper' => '', 'container_class' => [ 'wpr-field--parent', 'wpr-NoPaddingBottom', 'wpr-field--children', ], 'section' => 'js', 'page' => 'file_optimization', 'parent' => 'delay_js', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', 'input_attr' => [ 'disabled' => 0, ], 'warning' => [ 'title' => __( 'Performance impact', 'rocket' ), 'description' => __( 'By enabling Safe Mode, you significantly reduce your website performance improvements. We recommend using it only as a temporary solution. If you’re experiencing issues with the Delay JavaScript feature, our support team can help you troubleshoot.', 'rocket' ), 'button_label' => __( 'ACTIVATE SAFE MODE', 'rocket' ), ], ], ], ); } /** * Registers Media section. * * @since 3.0 */ private function media_section() { $lazyload_beacon = $this->beacon->get_suggest( 'lazyload' ); $exclude_lazyload = $this->beacon->get_suggest( 'exclude_lazyload' ); $dimensions = $this->beacon->get_suggest( 'image_dimensions' ); $fonts = $this->beacon->get_suggest( 'host_fonts_locally' ); $fonts_preload = $this->beacon->get_suggest( 'fonts_preload' ); $this->settings->add_page_section( 'media', [ 'title' => __( 'Media', 'rocket' ), 'menu_description' => __( 'LazyLoad, image dimensions, font optimization', 'rocket' ), ] ); $disable_images_lazyload = []; $disable_iframes_lazyload = []; $disable_youtube_lazyload = []; if ( rocket_maybe_disable_lazyload() ) { $disable_images_lazyload[] = __( 'Autoptimize', 'rocket' ); } /** * Lazyload Helper filter which disables WPR lazyload functionality for images. * * @since 3.4.2 * * @param array $disable_images_lazyload Will return the array with all plugin names which should disable LazyLoad */ $disable_images_lazyload = (array) apply_filters( 'rocket_maybe_disable_lazyload_helper', $disable_images_lazyload ); $disable_images_lazyload = $this->sanitize_and_format_list( $disable_images_lazyload ); /** * Lazyload Helper filter which disables WPR lazyload functionality for iframes. * * @since 3.5.5 * * @param array $disable_iframes_lazyload Will return the array with all plugin names which should disable LazyLoad */ $disable_iframes_lazyload = (array) apply_filters( 'rocket_maybe_disable_iframes_lazyload_helper', $disable_iframes_lazyload ); $disable_iframes_lazyload = $this->sanitize_and_format_list( $disable_iframes_lazyload ); $disable_css_bg_img_lazyload = false; /** * Lazyload Helper filter which disables WPR lazyload functionality for bg css. * * @param bool $disable_css_bg_img_lazyload Should the lazyload CSS be disabled. */ $disable_css_bg_img_lazyload = (bool) apply_filters( 'rocket_maybe_disable_css_bg_img_lazyload_helper', $disable_css_bg_img_lazyload ); /** * Lazyload Helper filter which disables WPR lazyload functionality to replace YouTube iframe with preview image. * * @since 3.6.3 * * @param array $disable_youtube_lazyload Will return the array with all plugin/themes names which should disable replace YouTube iframe with preview image */ $disable_youtube_lazyload = (array) apply_filters( 'rocket_maybe_disable_youtube_lazyload_helper', $disable_youtube_lazyload ); $disable_youtube_lazyload = $this->sanitize_and_format_list( $disable_youtube_lazyload ); $disable_youtube_lazyload = array_merge( $disable_youtube_lazyload, $disable_iframes_lazyload ); $disable_youtube_lazyload = array_unique( $disable_youtube_lazyload ); $disable_lazyload = array_merge( $disable_images_lazyload, $disable_iframes_lazyload ); $disable_lazyload = array_unique( $disable_lazyload ); $disable_lazyload = wp_sprintf_l( '%l', $disable_lazyload ); $disable_images_lazyload = wp_sprintf_l( '%l', $disable_images_lazyload ); $disable_youtube_lazyload = wp_sprintf_l( '%l', $disable_youtube_lazyload ); $this->settings->add_settings_sections( [ 'lazyload_section' => [ 'title' => __( 'LazyLoad', 'rocket' ), 'type' => 'fields_container', // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'It can improve actual and perceived loading time as images, iframes, and videos will be loaded only as they enter (or about to enter) the viewport and reduces the number of HTTP requests. %1$sMore Info%2$s', 'rocket' ), '<a href="' . esc_url( $lazyload_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $lazyload_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ), 'help' => [ 'id' => $this->beacon->get_suggest( 'lazyload_section' ), 'url' => $lazyload_beacon['url'], ], 'page' => 'media', // translators: %1$s = “WP Rocket”, %2$s = a list of plugin names. 'helper' => ! empty( $disable_lazyload ) ? sprintf( __( 'LazyLoad is currently activated in %2$s. If you want to use WP Rocket’s LazyLoad, disable this option in %2$s.', 'rocket' ), WP_ROCKET_PLUGIN_NAME, $disable_lazyload ) : '', ], 'dimensions_section' => [ 'title' => __( 'Image Dimensions', 'rocket' ), 'type' => 'fields_container', // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Add missing width and height attributes to images. Helps prevent layout shifts and improve the reading experience for your visitors. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $dimensions['url'] ) . '" data-beacon-article="' . esc_attr( $dimensions['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ), 'help' => $dimensions, 'page' => 'media', ], 'font_optimization_section' => [ 'title' => __( 'Fonts', 'rocket' ), 'type' => 'fields_container', 'help' => $fonts, 'page' => 'media', ], ] ); /** * Add more content to the 'cache_webp' setting field. * * @since 3.4 * * @param array $cache_webp_field Data to be added to the setting field. */ $this->settings->add_settings_fields( [ 'lazyload' => [ 'type' => 'checkbox', 'label' => __( 'Enable for images', 'rocket' ), 'section' => 'lazyload_section', 'page' => 'media', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', 'container_class' => [ ! empty( $disable_images_lazyload ) ? 'wpr-isDisabled' : '', ], 'input_attr' => [ 'disabled' => ! empty( $disable_images_lazyload ) ? 1 : 0, ], // translators: %1$s = “WP Rocket”, %2$s = a list of plugin names. 'description' => ! empty( $disable_images_lazyload ) ? sprintf( __( 'LazyLoad for images is currently activated in %2$s. If you want to use %1$s’s LazyLoad, disable this option in %2$s.', 'rocket' ), WP_ROCKET_PLUGIN_NAME, $disable_images_lazyload ) : '', ], 'lazyload_css_bg_img' => [ 'container_class' => [ $disable_css_bg_img_lazyload ? 'wpr-isDisabled' : '', 'wpr-isParent', ], 'type' => 'checkbox', 'label' => __( 'Enable for CSS background images', 'rocket' ), 'section' => 'lazyload_section', 'page' => 'media', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', 'input_attr' => [ 'disabled' => $disable_css_bg_img_lazyload ? 1 : 0, ], ], 'lazyload_iframes' => [ 'container_class' => [ ! empty( $disable_iframes_lazyload ) ? 'wpr-isDisabled' : '', 'wpr-isParent', ], 'type' => 'checkbox', 'label' => __( 'Enable for iframes and videos', 'rocket' ), 'section' => 'lazyload_section', 'page' => 'media', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', 'input_attr' => [ 'disabled' => ! empty( $disable_iframes_lazyload ) ? 1 : 0, ], ], 'lazyload_youtube' => [ 'container_class' => [ ! empty( $disable_youtube_lazyload ) ? 'wpr-isDisabled' : '', 'wpr-field--children', ], 'type' => 'checkbox', 'label' => __( 'Replace YouTube iframe with preview image', 'rocket' ), // translators: %1$s = “WP Rocket”, %2$s = a list of plugin or themes names. 'description' => ! empty( $disable_youtube_lazyload ) ? sprintf( __( 'Replace YouTube iframe with preview image is not compatible with %2$s.', 'rocket' ), WP_ROCKET_PLUGIN_NAME, $disable_youtube_lazyload ) : __( 'This can significantly improve your loading time if you have a lot of YouTube videos on a page.', 'rocket' ), 'parent' => 'lazyload_iframes', 'section' => 'lazyload_section', 'page' => 'media', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', 'input_attr' => [ 'disabled' => ! empty( $disable_youtube_lazyload ) ? 1 : 0, ], ], 'exclude_lazyload' => [ 'container_class' => [ 'wpr-Delayjs', ], 'type' => 'textarea', 'label' => __( 'Excluded images or iframes', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Specify keywords (e.g. image filename, CSS filename, CSS class, domain) from the image or iframe code to be excluded (one per line). %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $exclude_lazyload['url'] ) . '" data-beacon-article="' . esc_attr( $exclude_lazyload['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ), 'section' => 'lazyload_section', 'page' => 'media', 'default' => [], 'placeholder' => "example-image.jpg\nslider-image\nbackground-image-style.css", ], 'image_dimensions' => [ 'type' => 'checkbox', 'label' => __( 'Add missing image dimensions', 'rocket' ), 'section' => 'dimensions_section', 'page' => 'media', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'auto_preload_fonts' => [ 'type' => 'checkbox', 'label' => __( 'Preload fonts', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Preload above-the-fold fonts to enhance layout stability and optimize text-based LCP elements. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $fonts_preload['url'] ) . '" data-beacon-article="' . esc_attr( $fonts_preload['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ), 'section' => 'font_optimization_section', 'page' => 'media', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'host_fonts_locally' => [ 'type' => 'checkbox', 'label' => __( 'Self-host Google Fonts', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Download and serve fonts directly from your server. Reduces connections to external servers and minimizes font shifts. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $fonts['url'] ) . '" data-beacon-article="' . esc_attr( $fonts['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ), 'section' => 'font_optimization_section', 'page' => 'media', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], ] ); } /** * Registers Preload section. * * @since 3.0 */ private function preload_section() { $this->settings->add_page_section( 'preload', [ 'title' => __( 'Preload', 'rocket' ), 'menu_description' => __( 'Generate cache files', 'rocket' ), ] ); $bot_beacon = $this->beacon->get_suggest( 'bot' ); $fonts_preload = $this->beacon->get_suggest( 'fonts_preload' ); $preload_links = $this->beacon->get_suggest( 'preload_links' ); $exclusions = $this->beacon->get_suggest( 'preload_exclusions' ); $this->settings->add_settings_sections( [ 'preload_section' => [ 'title' => __( 'Preload Cache', 'rocket' ), 'type' => 'fields_container', // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => __( 'When you enable preloading WP Rocket will automatically detect your sitemaps and save all URLs to the database. The plugin will make sure that your cache is always preloaded.', 'rocket' ), 'help' => [ 'id' => $this->beacon->get_suggest( 'sitemap_preload' ), 'url' => $bot_beacon['url'], ], 'page' => 'preload', ], 'preload_links_section' => [ 'title' => __( 'Preload Links', 'rocket' ), 'type' => 'fields_container', // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Link preloading improves the perceived load time by downloading a page when a user hovers over the link. %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $preload_links['url'] ) . '" data-beacon-article="' . esc_attr( $preload_links['id'] ) . '" target="_blank">', '</a>' ), 'help' => [ 'id' => $preload_links['id'], 'url' => $preload_links['url'], ], 'page' => 'preload', ], ] ); $this->settings->add_settings_fields( [ 'manual_preload' => [ 'type' => 'checkbox', 'label' => __( 'Activate Preloading', 'rocket' ), 'section' => 'preload_section', 'page' => 'preload', 'default' => 1, 'sanitize_callback' => 'sanitize_checkbox', 'container_class' => [ 'wpr-isParent', ], ], 'preload_excluded_uri' => [ 'type' => 'textarea', 'label' => __( 'Exclude URLs', 'rocket' ), 'container_class' => [ 'wpr-field--children', ], // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Specify URLs to be excluded from the preload feature (one per line). %1$sMore info%2$s', 'rocket' ), '<a href="' . esc_url( $exclusions['url'] ) . '" data-beacon-article="' . esc_attr( $exclusions['id'] ) . '" target="_blank">', '</a>' ), 'placeholder' => '/author/(.*)', 'helper' => 'Use (.*) wildcards to address multiple URLs under a given path.', 'parent' => 'manual_preload', 'section' => 'preload_section', 'page' => 'preload', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], 'preload_links' => [ 'type' => 'checkbox', 'label' => __( 'Enable link preloading', 'rocket' ), 'section' => 'preload_links_section', 'page' => 'preload', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], ] ); } /** * Registers Advanced Cache section. * * @since 3.0 */ private function advanced_cache_section() { $this->settings->add_page_section( 'advanced_cache', [ 'title' => __( 'Advanced Rules', 'rocket' ), 'menu_description' => __( 'Fine-tune cache rules', 'rocket' ), ] ); $ecommerce_beacon = $this->beacon->get_suggest( 'ecommerce' ); $cache_query_strings_beacon = $this->beacon->get_suggest( 'cache_query_strings' ); $never_cache_beacon = $this->beacon->get_suggest( 'exclude_cache' ); $never_cache_cookie_beacon = $this->beacon->get_suggest( 'exclude_cookie' ); $exclude_user_agent_beacon = $this->beacon->get_suggest( 'exclude_user_agent' ); $always_purge_beacon = $this->beacon->get_suggest( 'always_purge' ); $cache_life_beacon = $this->beacon->get_suggest( 'cache_lifespan' ); $nonce_beacon = $this->beacon->get_suggest( 'nonce' ); $ecommerce_plugin = ''; $reject_uri_desc = __( 'Sensitive pages like custom login/logout URLs should be excluded from cache.', 'rocket' ); if ( function_exists( 'WC' ) && function_exists( 'wc_get_page_id' ) ) { $ecommerce_plugin = _x( 'WooCommerce', 'plugin name', 'rocket' ); } elseif ( function_exists( 'EDD' ) ) { $ecommerce_plugin = _x( 'Easy Digital Downloads', 'plugin name', 'rocket' ); } elseif ( function_exists( 'it_exchange_get_page_type' ) && function_exists( 'it_exchange_get_page_url' ) ) { $ecommerce_plugin = _x( 'iThemes Exchange', 'plugin name', 'rocket' ); } elseif ( defined( 'JIGOSHOP_VERSION' ) && function_exists( 'jigoshop_get_page_id' ) ) { $ecommerce_plugin = _x( 'Jigoshop', 'plugin name', 'rocket' ); } elseif ( defined( 'WPSHOP_VERSION' ) && class_exists( 'wpshop_tools' ) && method_exists( 'wpshop_tools', 'get_page_id' ) ) { // @phpstan-ignore-line $ecommerce_plugin = _x( 'WP-Shop', 'plugin name', 'rocket' ); } if ( ! empty( $ecommerce_plugin ) ) { $reject_uri_desc .= sprintf( // translators: %1$s = opening <a> tag, %2$s = plugin name, %3$s closing </a> tag. __( '<br>Cart, checkout and "my account" pages set in <strong>%1$s%2$s%3$s</strong> will be detected and never cached by default.', 'rocket' ), '<a href="' . esc_url( $ecommerce_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $ecommerce_beacon['id'] ) . '" target="_blank">', $ecommerce_plugin, '</a>' ); } $this->settings->add_settings_sections( [ 'cache_lifespan' => [ 'title' => __( 'Cache Lifespan', 'rocket' ), 'type' => 'fields_container', // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Cache files older than the specified lifespan will be deleted.<br>Enable %1$spreloading%2$s for the cache to be rebuilt automatically after lifespan expiration.', 'rocket' ), '<a href="#preload">', '</a>' ), 'help' => [ 'url' => $cache_life_beacon['url'], 'id' => $this->beacon->get_suggest( 'cache_lifespan_section' ), ], 'page' => 'advanced_cache', ], 'cache_reject_uri_section' => [ 'title' => __( 'Never Cache URL(s)', 'rocket' ), 'type' => 'fields_container', // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => $reject_uri_desc, 'help' => $never_cache_beacon, 'page' => 'advanced_cache', ], 'cache_reject_cookies_section' => [ 'title' => __( 'Never Cache Cookies', 'rocket' ), 'type' => 'fields_container', 'page' => 'advanced_cache', 'help' => $never_cache_cookie_beacon, ], 'cache_reject_ua_section' => [ 'title' => __( 'Never Cache User Agent(s)', 'rocket' ), 'type' => 'fields_container', 'help' => $exclude_user_agent_beacon, 'page' => 'advanced_cache', ], 'cache_purge_pages_section' => [ 'title' => __( 'Always Purge URL(s)', 'rocket' ), 'type' => 'fields_container', 'help' => $always_purge_beacon, 'page' => 'advanced_cache', ], 'cache_query_strings_section' => [ 'title' => __( 'Cache Query String(s)', 'rocket' ), 'type' => 'fields_container', // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( '%1$sCache for query strings%2$s enables you to force caching for specific GET parameters.', 'rocket' ), '<a href="' . esc_url( $cache_query_strings_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $cache_query_strings_beacon['id'] ) . '" target="_blank">', '</a>' ), 'help' => $cache_query_strings_beacon, 'page' => 'advanced_cache', ], ] ); $this->settings->add_settings_fields( [ 'purge_cron_interval' => [ 'type' => 'cache_lifespan', 'label' => __( 'Specify time after which the global cache is cleared<br>(0 = unlimited )', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Reduce lifespan to 10 hours or less if you notice issues that seem to appear periodically. %1$sWhy?%2$s', 'rocket' ), '<a href="' . esc_url( $nonce_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $nonce_beacon['id'] ) . '" target="_blank">', '</a>' ), 'section' => 'cache_lifespan', 'page' => 'advanced_cache', 'default' => 10, 'sanitize_callback' => 'sanitize_cache_lifespan', 'choices' => [ 'HOUR_IN_SECONDS' => __( 'Hours', 'rocket' ), 'DAY_IN_SECONDS' => __( 'Days', 'rocket' ), ], ], 'cache_reject_uri' => [ 'type' => 'textarea', 'description' => __( 'Specify URLs of pages or posts that should never be cached (one per line)', 'rocket' ), 'helper' => __( 'The domain part of the URL will be stripped automatically.<br>Use (.*) wildcards to address multiple URLs under a given path.', 'rocket' ), 'placeholder' => '/example/(.*)', 'section' => 'cache_reject_uri_section', 'page' => 'advanced_cache', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], 'cache_reject_cookies' => [ 'type' => 'textarea', 'description' => __( 'Specify full or partial IDs of cookies that, when set in the visitor\'s browser, should prevent a page from getting cached (one per line)', 'rocket' ), 'section' => 'cache_reject_cookies_section', 'page' => 'advanced_cache', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], 'cache_reject_ua' => [ 'type' => 'textarea', 'description' => __( 'Specify user agent strings that should never see cached pages (one per line)', 'rocket' ), 'helper' => __( 'Use (.*) wildcards to detect parts of UA strings.', 'rocket' ), 'placeholder' => '(.*)Mobile(.*)Safari(.*)', 'section' => 'cache_reject_ua_section', 'page' => 'advanced_cache', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], 'cache_purge_pages' => [ 'type' => 'textarea', 'description' => __( 'Specify URLs you always want purged from cache whenever you update any post or page (one per line)', 'rocket' ), 'helper' => __( 'The domain part of the URL will be stripped automatically.<br>Use (.*) wildcards to address multiple URLs under a given path.', 'rocket' ), 'section' => 'cache_purge_pages_section', 'page' => 'advanced_cache', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], 'cache_query_strings' => [ 'type' => 'textarea', 'description' => __( 'Specify query strings for caching (one per line)', 'rocket' ), 'section' => 'cache_query_strings_section', 'page' => 'advanced_cache', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], ] ); } /** * Registers Database section. * * @since 3.0 */ private function database_section() { $total = []; foreach ( array_keys( $this->optimize->get_options() ) as $key ) { $total[ $key ] = $this->optimize->count_cleanup_items( $key ); } $this->settings->add_page_section( 'database', [ 'title' => __( 'Database', 'rocket' ), 'menu_description' => __( 'Optimize, reduce bloat', 'rocket' ), ] ); $this->settings->add_settings_sections( [ 'post_cleanup_section' => [ 'title' => __( 'Post Cleanup', 'rocket' ), 'type' => 'fields_container', 'description' => __( 'Post revisions and drafts will be permanently deleted. Do not use this option if you need to retain revisions or drafts.', 'rocket' ), 'help' => $this->beacon->get_suggest( 'db_optimization' ), 'page' => 'database', ], 'comments_cleanup_section' => [ 'title' => __( 'Comments Cleanup', 'rocket' ), 'type' => 'fields_container', 'description' => __( 'Spam and trashed comments will be permanently deleted.', 'rocket' ), 'page' => 'database', ], 'transients_cleanup_section' => [ 'title' => __( 'Transients Cleanup', 'rocket' ), 'type' => 'fields_container', 'description' => __( 'Transients are temporary options; they are safe to remove. They will be automatically regenerated as your plugins require them.', 'rocket' ), 'page' => 'database', ], 'database_cleanup_section' => [ 'title' => __( 'Database Cleanup', 'rocket' ), 'type' => 'fields_container', 'description' => __( 'Reduces overhead of database tables', 'rocket' ), 'page' => 'database', ], 'schedule_cleanup_section' => [ 'title' => __( 'Automatic Cleanup', 'rocket' ), 'type' => 'fields_container', 'page' => 'database', ], ] ); $this->settings->add_settings_fields( [ 'database_revisions' => [ 'type' => 'checkbox', 'label' => __( 'Revisions', 'rocket' ), // translators: %s is the number of revisions found in the database. It's a formatted number, don't use %d. 'description' => sprintf( _n( '%s revision in your database.', '%s revisions in your database.', $total['database_revisions'], 'rocket' ), number_format_i18n( $total['database_revisions'] ) ), 'section' => 'post_cleanup_section', 'page' => 'database', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'database_auto_drafts' => [ 'type' => 'checkbox', 'label' => __( 'Auto Drafts', 'rocket' ), // translators: %s is the number of revisions found in the database. It's a formatted number, don't use %d. 'description' => sprintf( _n( '%s draft in your database.', '%s drafts in your database.', $total['database_auto_drafts'], 'rocket' ), number_format_i18n( $total['database_auto_drafts'] ) ), 'section' => 'post_cleanup_section', 'page' => 'database', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'database_trashed_posts' => [ 'type' => 'checkbox', 'label' => __( 'Trashed Posts', 'rocket' ), // translators: %s is the number of revisions found in the database. It's a formatted number, don't use %d. 'description' => sprintf( _n( '%s trashed post in your database.', '%s trashed posts in your database.', $total['database_trashed_posts'], 'rocket' ), $total['database_trashed_posts'] ), 'section' => 'post_cleanup_section', 'page' => 'database', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'database_spam_comments' => [ 'type' => 'checkbox', 'label' => __( 'Spam Comments', 'rocket' ), // translators: %s is the number of revisions found in the database. It's a formatted number, don't use %d. 'description' => sprintf( _n( '%s spam comment in your database.', '%s spam comments in your database.', $total['database_spam_comments'], 'rocket' ), number_format_i18n( $total['database_spam_comments'] ) ), 'section' => 'comments_cleanup_section', 'page' => 'database', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'database_trashed_comments' => [ 'type' => 'checkbox', 'label' => __( 'Trashed Comments', 'rocket' ), // translators: %s is the number of revisions found in the database. It's a formatted number, don't use %d. 'description' => sprintf( _n( '%s trashed comment in your database.', '%s trashed comments in your database.', $total['database_trashed_comments'], 'rocket' ), number_format_i18n( $total['database_trashed_comments'] ) ), 'section' => 'comments_cleanup_section', 'page' => 'database', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'database_all_transients' => [ 'type' => 'checkbox', 'label' => __( 'All transients', 'rocket' ), // translators: %s is the number of revisions found in the database. It's a formatted number, don't use %d. 'description' => sprintf( _n( '%s transient in your database.', '%s transients in your database.', $total['database_all_transients'], 'rocket' ), number_format_i18n( $total['database_all_transients'] ) ), 'section' => 'transients_cleanup_section', 'page' => 'database', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'database_optimize_tables' => [ 'type' => 'checkbox', 'label' => __( 'Optimize Tables', 'rocket' ), // translators: %s is the number of revisions found in the database. It's a formatted number, don't use %d. 'description' => sprintf( _n( '%s table to optimize in your database.', '%s tables to optimize in your database.', $total['database_optimize_tables'], 'rocket' ), number_format_i18n( $total['database_optimize_tables'] ) ), 'section' => 'database_cleanup_section', 'page' => 'database', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'schedule_automatic_cleanup' => [ 'container_class' => [ 'wpr-isParent', ], 'type' => 'checkbox', 'label' => __( 'Schedule Automatic Cleanup', 'rocket' ), 'description' => '', 'section' => 'schedule_cleanup_section', 'page' => 'database', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'automatic_cleanup_frequency' => [ 'container_class' => [ 'wpr-field--children', ], 'type' => 'select', 'label' => __( 'Frequency', 'rocket' ), 'description' => '', 'parent' => 'schedule_automatic_cleanup', 'section' => 'schedule_cleanup_section', 'page' => 'database', 'default' => 'daily', 'sanitize_callback' => 'sanitize_text_field', 'choices' => [ 'daily' => __( 'Daily', 'rocket' ), 'weekly' => __( 'Weekly', 'rocket' ), 'monthly' => __( 'Monthly', 'rocket' ), ], ], ] ); } /** * Registers CDN section * * @since 3.0 */ private function cdn_section() { $this->settings->add_page_section( 'page_cdn', [ 'title' => __( 'CDN', 'rocket' ), 'menu_description' => __( 'Integrate your CDN', 'rocket' ), ] ); $cdn_beacon = $this->beacon->get_suggest( 'cdn' ); $cdn_exclude_beacon = $this->beacon->get_suggest( 'exclude_cdn' ); $this->settings->add_settings_sections( [ 'cdn_section' => [ 'title' => __( 'CDN', 'rocket' ), 'type' => 'fields_container', 'description' => __( 'All URLs of static files (CSS, JS, images) will be rewritten to the CNAME(s) you provide.', 'rocket' ) . '<br><em>' . sprintf( // translators: %1$s = opening link tag, %2$s = closing link tag. __( 'Not required for services like Cloudflare and Sucuri. Please see our available %1$sAdd-ons%2$s.', 'rocket' ), '<a href="#addons">', '</a>' ) . '</em>', 'help' => [ 'id' => $this->beacon->get_suggest( 'cdn_section' ), 'url' => $cdn_beacon['url'], ], 'page' => 'page_cdn', ], 'cnames_section' => [ 'type' => 'nocontainer', 'page' => 'page_cdn', ], 'exclude_cdn_section' => [ 'title' => __( 'Exclude files from CDN', 'rocket' ), 'type' => 'fields_container', 'help' => [ 'id' => $cdn_exclude_beacon['id'], 'url' => $cdn_exclude_beacon['url'], ], 'page' => 'page_cdn', ], ] ); $maybe_display_cdn_helper = ''; /** * Filters the addons names requiring the helper message. * * @param array $addons Array of addons. */ $addons = wpm_apply_filters_typed( 'array', 'rocket_cdn_helper_addons', [] ); $addons = array_unique( $addons ); if ( ! empty( $addons ) ) { $maybe_display_cdn_helper = wp_sprintf( // translators: %1$s = opening em tag, %2$l = list of add-on name(s), %3$s = closing em tag. _n( '%1$s%2$l Add-on%3$s is currently enabled. Configuration of the CDN settings is not required for %2$l to work on your site.', '%1$s%2$l Add-ons%3$s are currently enabled. Configuration of the CDN settings is not required for %2$l to work on your site.', count( $addons ), 'rocket' ), '<em>', $addons, '</em>' ) . '<br>'; } $this->settings->add_settings_fields( /** * Filters the fields for the CDN section. * * @since 3.5 * @author Remy Perona * * @param array $cdn_settings_fields Data to be added to the CDN section. */ apply_filters( 'rocket_cdn_settings_fields', [ 'cdn' => [ 'type' => 'checkbox', 'label' => __( 'Enable Content Delivery Network', 'rocket' ), 'helper' => $maybe_display_cdn_helper, 'section' => 'cdn_section', 'page' => 'page_cdn', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], 'cdn_cnames' => [ 'type' => 'cnames', 'label' => __( 'CDN CNAME(s)', 'rocket' ), 'description' => __( 'Specify the CNAME(s) below', 'rocket' ), 'default' => [], 'section' => 'cnames_section', 'page' => 'page_cdn', ], 'cdn_reject_files' => [ 'type' => 'textarea', 'description' => __( 'Specify URL(s) of files that should not get served via CDN (one per line).', 'rocket' ), 'helper' => __( 'The domain part of the URL will be stripped automatically.<br>Use (.*) wildcards to exclude all files of a given file type located at a specific path.', 'rocket' ), 'placeholder' => '/wp-content/plugins/some-plugins/(.*).css', 'section' => 'exclude_cdn_section', 'page' => 'page_cdn', 'default' => [], 'sanitize_callback' => 'sanitize_textarea', ], ] ) ); } /** * Registers Heartbeat section. * * @since 3.2 */ private function heartbeat_section() { $heartbeat_beacon = $this->beacon->get_suggest( 'heartbeat_settings' ); $this->settings->add_page_section( 'heartbeat', [ 'title' => __( 'Heartbeat', 'rocket' ), 'menu_description' => __( 'Control WordPress Heartbeat API', 'rocket' ), ] ); $this->settings->add_settings_sections( [ 'heartbeat_section' => [ 'title' => __( 'Heartbeat', 'rocket' ), 'description' => __( 'Reducing or disabling the Heartbeat API’s activity can help save some of your server’s resources.', 'rocket' ), 'type' => 'fields_container', 'page' => 'heartbeat', 'help' => $heartbeat_beacon, ], 'heartbeat_settings' => [ 'title' => __( 'Reduce or disable Heartbeat activity', 'rocket' ), 'description' => __( 'Reducing activity will change Heartbeat frequency from one hit each minute to one hit every 2 minutes.', 'rocket' ) . '<br/>' . __( 'Disabling Heartbeat entirely may break plugins and themes using this API.', 'rocket' ), 'type' => 'fields_container', 'page' => 'heartbeat', ], ] ); $fields_default = [ 'type' => 'select', 'page' => 'heartbeat', 'section' => 'heartbeat_settings', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'reduce_periodicity', 'choices' => [ '' => __( 'Do not limit', 'rocket' ), 'reduce_periodicity' => __( 'Reduce activity', 'rocket' ), 'disable' => __( 'Disable', 'rocket' ), ], ]; $this->settings->add_settings_fields( [ 'control_heartbeat' => [ 'type' => 'checkbox', 'label' => __( 'Control Heartbeat', 'rocket' ), 'page' => 'heartbeat', 'section' => 'heartbeat_section', 'sanitize_callback' => 'sanitize_checkbox', 'default' => 0, ], 'heartbeat_admin_behavior' => array_merge( $fields_default, [ 'label' => __( 'Behavior in backend', 'rocket' ), 'description' => '', ] ), 'heartbeat_editor_behavior' => array_merge( $fields_default, [ 'label' => __( 'Behavior in post editor', 'rocket' ), ] ), 'heartbeat_site_behavior' => array_merge( $fields_default, [ 'label' => __( 'Behavior in frontend', 'rocket' ), ] ), ] ); } /** * Registers Add-ons section. * * @since 3.0 */ private function addons_section() { $webp_beacon = $this->beacon->get_suggest( 'webp' ); $user_cache_beacon = $this->beacon->get_suggest( 'user_cache' ); $this->settings->add_page_section( 'addons', [ 'title' => __( 'Add-ons', 'rocket' ), 'menu_description' => __( 'Add more features', 'rocket' ), ] ); $this->settings->add_settings_sections( [ 'one_click' => [ 'title' => __( 'One-click Rocket Add-ons', 'rocket' ), 'description' => __( 'One-Click Add-ons are features extending available options without configuration needed. Switch the option "on" to enable from this screen.', 'rocket' ), 'type' => 'addons_container', 'page' => 'addons', ], ] ); $this->settings->add_settings_sections( [ 'addons' => [ 'title' => __( 'Rocket Add-ons', 'rocket' ), 'description' => __( 'Rocket Add-ons are complementary features extending available options.', 'rocket' ), 'type' => 'addons_container', 'page' => 'addons', ], ] ); $this->settings->add_settings_fields( [ 'cache_logged_user' => [ 'type' => 'one_click_addon', 'label' => __( 'User Cache', 'rocket' ), 'logo' => [ 'url' => WP_ROCKET_ASSETS_IMG_URL . 'icon-user-cache.svg', 'width' => 152, 'height' => 135, ], 'title' => __( 'If you need to create a dedicated set of cache files for each logged-in WordPress user, you must activate this add-on.', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'User cache is great when you have user-specific or restricted content on your website.<br>%1$sLearn more%2$s', 'rocket' ), '<a href="' . esc_url( $user_cache_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $user_cache_beacon['id'] ) . '" target="_blank">', '</a>' ), 'section' => 'one_click', 'page' => 'addons', 'settings_page' => 'user_cache', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], ] ); $default_cf_settings = [ 'do_cloudflare' => [ 'type' => 'rocket_addon', 'label' => __( 'Cloudflare', 'rocket' ), 'logo' => [ 'url' => rocket_get_constant( 'WP_ROCKET_ASSETS_IMG_URL', '' ) . 'logo-cloudflare2.svg', 'width' => 153, 'height' => 51, ], 'title' => __( 'Integrate your Cloudflare account with this add-on.', 'rocket' ), 'description' => __( 'Provide your account email, global API key, and domain to use options such as clearing the Cloudflare cache and enabling optimal settings with WP Rocket.', 'rocket' ), 'helper' => sprintf( // translators: %1$s = opening span tag, %2$s = closing span tag. __( '%1$sPlanning on using Automatic Platform Optimization (APO)?%2$s Just activate the official Cloudflare plugin and configure it. WP Rocket will automatically enable compatibility.', 'rocket' ), '<span class="wpr-helper-title">', '</span>' ), 'section' => 'addons', 'page' => 'addons', 'settings_page' => 'cloudflare', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], ]; /** * Filters the Cloudflare Addon field values * * @since 3.14 * * @param array $cf_settings Array of values to populate the field. */ $cf_settings = (array) apply_filters( 'rocket_cloudflare_field_settings', $default_cf_settings ); $cf_settings = wp_parse_args( $cf_settings, $default_cf_settings ); $this->settings->add_settings_fields( $cf_settings ); /** * Allow to display the "Varnish" tab in the settings page * * @since 2.7 * * @param bool $display true will display the "Varnish" tab. */ if ( apply_filters( 'rocket_display_varnish_options_tab', true ) ) { $varnish_beacon = $this->beacon->get_suggest( 'varnish' ); $this->settings->add_settings_fields( /** * Filters the Varnish field settings data * * @since 3.0 * @author Remy Perona * * @param array $settings Field settings data. */ apply_filters( 'rocket_varnish_field_settings', [ 'varnish_auto_purge' => [ 'type' => 'one_click_addon', 'label' => __( 'Varnish', 'rocket' ), 'logo' => [ 'url' => WP_ROCKET_ASSETS_IMG_URL . 'logo-varnish.svg', 'width' => 152, 'height' => 135, ], 'title' => __( 'If Varnish runs on your server, you must activate this add-on.', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( __( 'Varnish cache will be purged each time WP Rocket clears its cache to ensure content is always up-to-date.<br>%1$sLearn more%2$s', 'rocket' ), '<a href="' . esc_url( $varnish_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $varnish_beacon['id'] ) . '" target="_blank">', '</a>' ), 'section' => 'one_click', 'page' => 'addons', 'settings_page' => 'varnish', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], ] ) ); } $webp_beacon = $this->beacon->get_suggest( 'webp' ); if ( rocket_valid_key() && ! \Imagify_Partner::has_imagify_api_key() ) { $imagify_link = '<a href="#imagify">'; } else { $imagify_link = '<a href="https://wordpress.org/plugins/imagify/" target="_blank" rel="noopener noreferrer">'; } $this->settings->add_settings_fields( [ 'cache_webp' => /** * Add more content to the 'cache_webp' setting field. * * @since 3.10 moved to add-on section * @since 3.4 * * @param array $cache_webp_field Data to be added to the setting field. */ apply_filters( 'rocket_cache_webp_setting_field', [ 'type' => 'one_click_addon', 'label' => __( 'WebP Compatibility', 'rocket' ), 'logo' => [ 'url' => WP_ROCKET_ASSETS_IMG_URL . 'logo-webp.svg', 'width' => 152, 'height' => 135, ], 'title' => __( 'Improve browser compatibility for WebP images.', 'rocket' ), // translators: %1$s = opening <a> tag, %2$s = closing </a> tag. 'description' => sprintf( // translators: %1$s and %3$s = opening <a> tag, %2$s = closing </a> tag. __( 'Enable this option if you would like WP Rocket to serve WebP images to compatible browsers. Please note that WP Rocket cannot create WebP images for you. To create WebP images we recommend %1$sImagify%2$s. %3$sMore info%2$s', 'rocket' ), $imagify_link, '</a>', '<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">' ), 'section' => 'one_click', 'page' => 'addons', 'settings_page' => 'webp', 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', 'container_class' => [ 'wpr-webp-addon', ], ] ), ] ); if ( defined( 'WP_ROCKET_SUCURI_API_KEY_HIDDEN' ) && WP_ROCKET_SUCURI_API_KEY_HIDDEN ) { // No need to display the dedicated tab if there is nothing to display on it. $description = __( 'Clear the Sucuri cache when WP Rocket’s cache is cleared.', 'rocket' ); $settings_page = false; } else { $description = __( 'Provide your API key to clear the Sucuri cache when WP Rocket’s cache is cleared.', 'rocket' ); $settings_page = 'sucuri'; } $this->settings->add_settings_fields( [ 'sucury_waf_cache_sync' => [ 'type' => 'rocket_addon', 'label' => __( 'Sucuri', 'rocket' ), 'logo' => [ 'url' => WP_ROCKET_ASSETS_IMG_URL . 'logo-sucuri.png', 'width' => 152, 'height' => 56, ], 'title' => __( 'Synchronize Sucuri cache with this add-on.', 'rocket' ), 'description' => $description, 'section' => 'addons', 'page' => 'addons', 'settings_page' => $settings_page, 'default' => 0, 'sanitize_callback' => 'sanitize_checkbox', ], ] ); } /** * Registers Cloudflare section. * * @since 3.0 */ private function cloudflare_section() { $this->settings->add_page_section( 'cloudflare', [ 'title' => __( 'Cloudflare', 'rocket' ), 'menu_description' => '', 'class' => [ 'wpr-subMenuItem', 'wpr-addonSubMenuItem', ], ] ); $beacon_cf_credentials = $this->beacon->get_suggest( 'cloudflare_credentials' ); $beacon_cf_settings = $this->beacon->get_suggest( 'cloudflare_settings' ); $beacon_cf_credentials_api = $this->beacon->get_suggest( 'cloudflare_credentials_api' ); $this->settings->add_settings_sections( [ 'cloudflare_credentials' => [ 'type' => 'fields_container', 'title' => __( 'Cloudflare credentials', 'rocket' ), 'help' => [ 'id' => $beacon_cf_credentials['id'], 'url' => $beacon_cf_credentials['url'], ], 'page' => 'cloudflare', ], 'cloudflare_settings' => [ 'type' => 'fields_container', 'title' => __( 'Cloudflare settings', 'rocket' ), 'help' => [ 'id' => $beacon_cf_settings['id'], 'url' => $beacon_cf_settings['url'], ], 'page' => 'cloudflare', ], ] ); if ( ! defined( 'WP_ROCKET_CF_API_KEY_HIDDEN' ) || ! WP_ROCKET_CF_API_KEY_HIDDEN ) { $this->settings->add_settings_fields( [ 'cloudflare_api_key_mask' => [ 'label' => _x( 'Global API key:', 'Cloudflare', 'rocket' ), 'description' => sprintf( '<a href="%1$s" target="_blank">%2$s</a>', esc_url( $beacon_cf_credentials_api['url'] ), _x( 'Find your API key', 'Cloudflare', 'rocket' ) ), 'default' => '', 'section' => 'cloudflare_credentials', 'page' => 'cloudflare', ], ] ); } $this->settings->add_settings_fields( [ 'cloudflare_email' => [ 'label' => _x( 'Account email', 'Cloudflare', 'rocket' ), 'default' => '', 'container_class' => [ 'wpr-field--split', ], 'section' => 'cloudflare_credentials', 'page' => 'cloudflare', ], 'cloudflare_zone_id_mask' => [ 'label' => _x( 'Zone ID', 'Cloudflare', 'rocket' ), 'default' => '', 'container_class' => [ 'wpr-field--split', ], 'section' => 'cloudflare_credentials', 'page' => 'cloudflare', ], 'cloudflare_devmode' => [ 'type' => 'sliding_checkbox', 'label' => __( 'Development mode', 'rocket' ), // translators: %1$s = link opening tag, %2$s = link closing tag. 'description' => sprintf( __( 'Temporarily activate development mode on your website. This setting will automatically turn off after 3 hours. %1$sLearn more%2$s', 'rocket' ), '<a href="https://support.cloudflare.com/hc/en-us/articles/200168246" target="_blank">', '</a>' ), 'default' => 0, 'section' => 'cloudflare_settings', 'page' => 'cloudflare', 'sanitize_callback' => 'sanitize_checkbox', ], 'cloudflare_auto_settings' => [ 'type' => 'sliding_checkbox', 'label' => __( 'Optimal settings', 'rocket' ), 'description' => __( 'Automatically enhances your Cloudflare configuration for speed, performance grade and compatibility.', 'rocket' ), 'default' => 0, 'section' => 'cloudflare_settings', 'page' => 'cloudflare', 'sanitize_callback' => 'sanitize_checkbox', ], 'cloudflare_protocol_rewrite' => [ 'type' => 'sliding_checkbox', 'label' => __( 'Relative protocol', 'rocket' ), 'description' => __( 'Should only be used with Cloudflare\'s flexible SSL feature. URLs of static files (CSS, JS, images) will be rewritten to use // instead of http:// or https://.', 'rocket' ), 'default' => 0, 'section' => 'cloudflare_settings', 'page' => 'cloudflare', 'sanitize_callback' => 'sanitize_checkbox', ], ] ); } /** * Registers Sucuri cache section. * * @since 3.2 */ private function sucuri_section() { if ( defined( 'WP_ROCKET_SUCURI_API_KEY_HIDDEN' ) && WP_ROCKET_SUCURI_API_KEY_HIDDEN ) { return; } $sucuri_beacon = $this->beacon->get_suggest( 'sucuri_credentials' ); $this->settings->add_page_section( 'sucuri', [ 'title' => __( 'Sucuri', 'rocket' ), 'menu_description' => '', 'class' => [ 'wpr-subMenuItem', 'wpr-addonSubMenuItem', ], ] ); $this->settings->add_settings_sections( [ 'sucuri_credentials' => [ 'type' => 'fields_container', 'title' => __( 'Sucuri credentials', 'rocket' ), 'page' => 'sucuri', 'help' => [ 'id' => $sucuri_beacon['id'], 'url' => $sucuri_beacon['url'], ], ], ] ); $this->settings->add_settings_fields( [ 'sucury_waf_api_key' => [ 'label' => _x( 'Firewall API key (for plugin), must be in format {32 characters}/{32 characters}:', 'Sucuri', 'rocket' ), 'description' => sprintf( '<a href="%1$s" target="_blank">%2$s</a>', 'https://kb.sucuri.net/firewall/Performance/clearing-cache', _x( 'Find your API key', 'Sucuri', 'rocket' ) ), 'default' => '', 'section' => 'sucuri_credentials', 'page' => 'sucuri', ], ] ); } /** * Sets hidden fields. * * @since 3.0 */ private function hidden_fields() { $hidden_fields = [ 'consumer_key', 'consumer_email', 'secret_key', 'license', 'secret_cache_key', 'minify_css_key', 'minify_js_key', 'version', 'previous_version', 'cloudflare_old_settings', 'cache_ssl', 'minify_google_fonts', 'emoji', 'remove_unused_css', 'async_css', 'cache_mobile', 'do_caching_mobile_files', 'minify_concatenate_css', 'cloudflare_api_key', 'cloudflare_zone_id', 'dns_prefetch', ]; $this->settings->add_hidden_settings_fields( /** * Filters the hidden settings fields * * @since 3.5 * @author Remy Perona * * @param array $hidden_settings_fields An array of hidden settings fields ID */ apply_filters( 'rocket_hidden_settings_fields', $hidden_fields ) ); } /** * Sanitize and format a list. * * @since 3.5.5 * * @param array $list A list of strings. * @param string $tag_name Name of the HTML tag that will wrap each element of the list. * @return array */ private function sanitize_and_format_list( array $list, $tag_name = 'strong' ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.listFound if ( empty( $list ) ) { return []; } $list = array_filter( $list ); if ( empty( $list ) ) { return []; } $list = array_unique( $list ); if ( empty( $tag_name ) ) { return $list; } $format = "<$tag_name>%s</$tag_name>"; return array_map( 'sprintf', array_fill( 0, count( $list ), $format ), $list ); } /** * Checks if combine JS option should be disabled * * @since 3.9 * * @return bool */ private function disable_combine_js(): bool { if ( (bool) get_rocket_option( 'delay_js', 0 ) ) { return true; } return ! (bool) get_rocket_option( 'minify_js', 0 ); } /** * Render radio options sub fields. * * @since 3.10 * * @param array $sub_fields Array of fields to display. */ public function display_radio_options_sub_fields( $sub_fields ) { $sub_fields = $this->settings->set_radio_buttons_sub_fields_value( $sub_fields ); $this->render->render_fields( $sub_fields ); } /** * Render mobile cache option. * * @return void */ public function display_mobile_cache_option(): void { if ( (bool) $this->options->get( 'cache_mobile', 0 ) ) { return; } $data = $this->beacon->get_suggest( 'mobile_cache' ); echo $this->generate( 'settings/mobile-cache', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Callback method for the AJAX request to mobile cache. * * @return void */ public function enable_mobile_cache(): void { check_ajax_referer( 'rocket-ajax', 'nonce', true ); if ( ! current_user_can( 'rocket_manage_options' ) ) { wp_send_json_error(); return; // @phpstan-ignore-line } $this->options->set( 'cache_mobile', 1 ); $this->options->set( 'do_caching_mobile_files', 1 ); update_option( rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ), $this->options->get_options() ); wp_send_json_success(); } /** * Enable Separate cache files option on upgrade. * * @return void */ public function enable_separate_cache_files_mobile(): void { if ( ! (bool) $this->options->get( 'cache_mobile', 0 ) ) { return; } if ( (bool) $this->options->get( 'do_caching_mobile_files', 0 ) ) { return; } $this->options->set( 'do_caching_mobile_files', 1 ); update_option( rocket_get_constant( 'WP_ROCKET_SLUG', 'wp_rocket_settings' ), $this->options->get_options() ); } /** * Display an update notice when the plugin is updated. * * @return void */ public function display_update_notice() { if ( ! current_user_can( 'rocket_manage_options' ) ) { return; } if ( 'settings_page_wprocket' !== get_current_screen()->id ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( 'rocket_update_notice', (array) $boxes, true ) ) { return; } $previous_version = $this->options->get( 'previous_version' ); // Bail-out if previous version is greater than or equal to 3.19. if ( version_compare( $previous_version, '3.19', '>=' ) ) { return; } $preconnect_content = $this->beacon->get_suggest( 'preconnect_domains' ); rocket_notice_html( [ 'status' => 'info', 'dismissible' => '', 'message' => sprintf( // translators: %1$s: opening strong tag, %2$s: closing strong tag, %3$s: opening a tag, %4$s: closing a tag. __( '%1$sWP Rocket:%2$s the plugin has been updated to the 3.19 version. New feature: %3$sPreconnect to external domains%4$s. Check out our documentation to learn more about it.', 'rocket' ), '<strong>', '</strong>', '<a href="' . esc_url( $preconnect_content['url'] ) . '" data-beacon-article="' . esc_attr( $preconnect_content['id'] ) . '" target="_blank" rel="noopener noreferrer">', '</a>' ), 'dismiss_button' => 'rocket_update_notice', ] ); } } Engine/Admin/Settings/Settings.php 0000644 00000042435 15174677547 0013151 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Settings; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Addon\Sucuri\Subscriber as SucuriSubscriber; /** * Settings class. * * @since 3.5.5 Moves into the new architecture. */ class Settings { /** * Options_Data instance. * * @since 3.0 * * @var Options_Data */ private $options; /** * Array of settings to build the settings page. * * @since 3.0 * * @var array */ private $settings; /** * Hidden settings on the settings page. * * @since 3.0 * * @var array */ private $hidden_settings; /** * Constructor * * @since 3.0 * * @param Options_Data $options Options_Data instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Adds a page section to the settings. * * A page section is a top-level block containing settings sections. * * @since 3.0 * * @param string $id Page section identifier. * @param array $args { * Data to build the section. * * @type string $title Section title. * @type string $menu_description Description displayed in the navigation. * } * @return void */ public function add_page_section( $id, $args ) { $args['id'] = $id; $this->settings[ $id ] = $args; } /** * Adds settings sections to the settings. * * A setting section is a block containing settings fields. * * @since 3.0 * * @param array $settings_sections { * Data to build the section. * * @type string $title Section title. * @type string $type Type of section. * @type string $page Page section identifier it belongs to. * @type string $description Section description. * @type string $help Helper IDs for beacon. * } * @return void */ public function add_settings_sections( $settings_sections ) { foreach ( $settings_sections as $id => $args ) { $args['id'] = $id; $this->settings[ $args['page'] ]['sections'][ $id ] = $args; } } /** * Adds settings fields to the settings. * * @since 3.0 * * @param array $settings_fields { * Data to build the section. * * @type string $id Identifier. * @type string $title Field title. * } * @return void */ public function add_settings_fields( $settings_fields ) { foreach ( $settings_fields as $id => $args ) { $args['id'] = $id; $args['value'] = $this->options->get( $id, $args['default'] ); $page = $args['page']; $section = $args['section']; unset( $args['page'], $args['section'] ); /** * Filters the field before add to the settings * * @since 3.10 * * @param array $input Array of sanitized values after being submitted by the form. */ $args = apply_filters( 'rocket_before_add_field_to_settings', $args ); $this->settings[ $page ]['sections'][ $section ]['fields'][ $id ] = $args; } } /** * Adds hidden settings fields to the settings. * * @since 3.0 * * @param array $hidden_settings_fields { * Data to build the section. * * @type string $id Identifier. * } * @return void */ public function add_hidden_settings_fields( $hidden_settings_fields ) { foreach ( $hidden_settings_fields as $id ) { $value = $this->options->get( $id ); $this->hidden_settings[] = [ 'id' => $id, 'value' => $value, ]; } } /** * Returns the plugin settings * * @since 3.0 * * @return array */ public function get_settings() { return $this->settings; } /** * Returns the plugin hidden settings * * @since 3.0 * * @return array */ public function get_hidden_settings() { return $this->hidden_settings; } /** * Sanitizes the submitted values. * * @since 3.0 * * @param array $input Array of values submitted by the form. * @return array */ public function sanitize_callback( $input ) { global $wp_settings_errors; $input['cache_logged_user'] = ! empty( $input['cache_logged_user'] ) ? 1 : 0; $input['cache_ssl'] = ! empty( $input['cache_ssl'] ) ? 1 : 0; $input['cache_mobile'] = ! empty( $input['cache_mobile'] ) ? 1 : 0; $input['do_caching_mobile_files'] = ! empty( $input['do_caching_mobile_files'] ) ? 1 : 0; $input['minify_google_fonts'] = ! empty( $input['minify_google_fonts'] ) ? 1 : 0; // Option : Minification CSS & JS. $input['minify_css'] = ! empty( $input['minify_css'] ) ? 1 : 0; $input['minify_js'] = ! empty( $input['minify_js'] ) ? 1 : 0; $input['minify_concatenate_js'] = ! empty( $input['minify_concatenate_js'] ) ? 1 : 0; $input['defer_all_js'] = ! empty( $input['defer_all_js'] ) ? 1 : 0; $input['exclude_defer_js'] = ! empty( $input['exclude_defer_js'] ) ? rocket_sanitize_textarea_field( 'exclude_defer_js', $input['exclude_defer_js'] ) : []; $input['emoji'] = ! empty( $input['emoji'] ) ? 1 : 0; $input['lazyload'] = ! empty( $input['lazyload'] ) ? 1 : 0; $input['lazyload_iframes'] = ! empty( $input['lazyload_iframes'] ) ? 1 : 0; $input['lazyload_youtube'] = ! empty( $input['lazyload_youtube'] ) ? 1 : 0; // If iframes lazyload is not checked, uncheck youtube thumbnail option too. if ( 0 === $input['lazyload_iframes'] ) { $input['lazyload_youtube'] = 0; } // Option : Purge interval. $input['purge_cron_interval'] = isset( $input['purge_cron_interval'] ) ? (int) $input['purge_cron_interval'] : $this->options->get( 'purge_cron_interval' ); $allowed_cron_units = [ 'HOUR_IN_SECONDS' => 1, 'DAY_IN_SECONDS' => 1, ]; $input['purge_cron_unit'] = isset( $input['purge_cron_unit'], $allowed_cron_units[ $input['purge_cron_unit'] ] ) ? $input['purge_cron_unit'] : $this->options->get( 'purge_cron_unit' ); // Option : Prefetch DNS requests. $input['dns_prefetch'] = $this->sanitize_dns_prefetch( $input ); // Option : Empty the cache of the following pages when updating a post. if ( ! empty( $input['cache_purge_pages'] ) ) { $input['cache_purge_pages'] = rocket_sanitize_textarea_field( 'cache_purge_pages', $input['cache_purge_pages'] ); } else { $input['cache_purge_pages'] = []; } // Option : Never cache the following pages. if ( ! empty( $input['cache_reject_uri'] ) ) { $input['cache_reject_uri'] = rocket_sanitize_textarea_field( 'cache_reject_uri', $input['cache_reject_uri'] ); $input['cache_reject_uri'] = $this->check_global_exclusion( $input['cache_reject_uri'] ); } else { $input['cache_reject_uri'] = []; } // Option : Don't cache pages that use the following cookies. if ( ! empty( $input['cache_reject_cookies'] ) ) { $input['cache_reject_cookies'] = rocket_sanitize_textarea_field( 'cache_reject_cookies', $input['cache_reject_cookies'] ); } else { $input['cache_reject_cookies'] = []; } // Option : Cache pages that use the following query strings (GET parameters). if ( ! empty( $input['cache_query_strings'] ) ) { $input['cache_query_strings'] = rocket_sanitize_textarea_field( 'cache_query_strings', $input['cache_query_strings'] ); } else { $input['cache_query_strings'] = []; } // Option : Never send cache pages for these user agents. if ( ! empty( $input['cache_reject_ua'] ) ) { $input['cache_reject_ua'] = rocket_sanitize_textarea_field( 'cache_reject_ua', $input['cache_reject_ua'] ); } else { $input['cache_reject_ua'] = []; } // Option : CSS files to exclude from the minification. if ( ! empty( $input['exclude_css'] ) ) { $input['exclude_css'] = rocket_sanitize_textarea_field( 'exclude_css', $input['exclude_css'] ); } else { $input['exclude_css'] = []; } // Option : JS files to exclude from the minification. if ( ! empty( $input['exclude_js'] ) ) { $input['exclude_js'] = rocket_sanitize_textarea_field( 'exclude_js', $input['exclude_js'] ); } else { $input['exclude_js'] = []; } // Option: inline JS patterns to exclude from combine JS. if ( ! empty( $input['exclude_inline_js'] ) ) { $input['exclude_inline_js'] = rocket_sanitize_textarea_field( 'exclude_inline_js', $input['exclude_inline_js'] ); } else { $input['exclude_inline_js'] = []; } // Option: Async CSS. $input['async_css'] = ! empty( $input['async_css'] ) ? 1 : 0; // Option: Critical CSS. $input['critical_css'] = ! empty( $input['critical_css'] ) ? wp_strip_all_tags( str_replace( [ '<style>', '</style>' ], '', $input['critical_css'] ), [ "\'", '\"' ] ) : ''; // Database options. $input['database_revisions'] = ! empty( $input['database_revisions'] ) ? 1 : 0; $input['database_auto_drafts'] = ! empty( $input['database_auto_drafts'] ) ? 1 : 0; $input['database_trashed_posts'] = ! empty( $input['database_trashed_posts'] ) ? 1 : 0; $input['database_spam_comments'] = ! empty( $input['database_spam_comments'] ) ? 1 : 0; $input['database_trashed_comments'] = ! empty( $input['database_trashed_comments'] ) ? 1 : 0; $input['database_all_transients'] = ! empty( $input['database_all_transients'] ) ? 1 : 0; $input['database_optimize_tables'] = ! empty( $input['database_optimize_tables'] ) ? 1 : 0; $input['schedule_automatic_cleanup'] = ! empty( $input['schedule_automatic_cleanup'] ) ? 1 : 0; $cleanup_frequencies = [ 'daily' => 1, 'weekly' => 1, 'monthly' => 1, ]; $input['automatic_cleanup_frequency'] = isset( $input['automatic_cleanup_frequency'], $cleanup_frequencies[ $input['automatic_cleanup_frequency'] ] ) ? $input['automatic_cleanup_frequency'] : $this->options->get( 'automatic_cleanup_frequency' ); $allowed_frequencies = [ 'daily', 'weekly', 'monthly' ]; if ( 1 !== $input['schedule_automatic_cleanup'] && ! in_array( $input['automatic_cleanup_frequency'], $allowed_frequencies, true ) ) { $input['automatic_cleanup_frequency'] = $this->options->get( 'automatic_cleanup_frequency' ); } // Options: Activate bot preload. $input['manual_preload'] = ! empty( $input['manual_preload'] ) ? 1 : 0; // Options: Sucuri cache. And yeah, there's a typo, but now it's too late to fix ^^'. $input['sucury_waf_cache_sync'] = ! empty( $input['sucury_waf_cache_sync'] ) ? 1 : 0; if ( defined( 'WP_ROCKET_SUCURI_API_KEY' ) ) { $input['sucury_waf_api_key'] = WP_ROCKET_SUCURI_API_KEY; } else { $input['sucury_waf_api_key'] = isset( $input['sucury_waf_api_key'] ) ? sanitize_text_field( $input['sucury_waf_api_key'] ) : ''; } $input['sucury_waf_api_key'] = trim( $input['sucury_waf_api_key'] ); if ( ! SucuriSubscriber::is_api_key_valid( $input['sucury_waf_api_key'] ) ) { $input['sucury_waf_api_key'] = ''; if ( $input['sucury_waf_cache_sync'] && empty( $input['ignore'] ) ) { add_settings_error( 'general', 'sucuri_waf_api_key_invalid', __( 'Sucuri Add-on: The API key for the Sucuri firewall must be in format <code>{32 characters}/{32 characters}</code>.', 'rocket' ), 'error' ); } } // Options : Heartbeat. $choices = [ '' => 1, 'reduce_periodicity' => 1, 'disable' => 1, ]; $input['control_heartbeat'] = ! empty( $input['control_heartbeat'] ) ? 1 : 0; $input['heartbeat_site_behavior'] = isset( $input['heartbeat_site_behavior'], $choices[ $input['heartbeat_site_behavior'] ] ) ? $input['heartbeat_site_behavior'] : ''; $input['heartbeat_admin_behavior'] = isset( $input['heartbeat_admin_behavior'], $choices[ $input['heartbeat_admin_behavior'] ] ) ? $input['heartbeat_admin_behavior'] : ''; $input['heartbeat_editor_behavior'] = isset( $input['heartbeat_editor_behavior'], $choices[ $input['heartbeat_editor_behavior'] ] ) ? $input['heartbeat_editor_behavior'] : ''; // Option : CDN. $input['cdn'] = ! empty( $input['cdn'] ) ? 1 : 0; // Option : CDN Cnames. if ( isset( $input['cdn_cnames'] ) ) { $input['cdn_cnames'] = $this->sanitize_cdn_cnames( $input['cdn_cnames'] ); } else { $input['cdn_cnames'] = []; } if ( ! $input['cdn_cnames'] ) { $input['cdn_zone'] = []; } else { $total_cdn_cnames = max( array_keys( $input['cdn_cnames'] ) ); for ( $i = 0; $i <= $total_cdn_cnames; $i++ ) { if ( ! isset( $input['cdn_cnames'][ $i ] ) ) { unset( $input['cdn_zone'][ $i ] ); } else { $allowed_cdn_zones = [ 'all' => 1, 'images' => 1, 'css_and_js' => 1, 'css' => 1, 'js' => 1, ]; $input['cdn_zone'][ $i ] = isset( $allowed_cdn_zones[ $input['cdn_zone'][ $i ] ] ) ? $input['cdn_zone'][ $i ] : 'all'; } } $input['cdn_cnames'] = array_values( $input['cdn_cnames'] ); $input['cdn_cnames'] = array_map( 'untrailingslashit', $input['cdn_cnames'] ); ksort( $input['cdn_zone'] ); $input['cdn_zone'] = array_values( $input['cdn_zone'] ); } // Option : Files to exclude from the CDN process. if ( ! empty( $input['cdn_reject_files'] ) ) { $input['cdn_reject_files'] = rocket_sanitize_textarea_field( 'cdn_reject_files', $input['cdn_reject_files'] ); } else { $input['cdn_reject_files'] = []; } $input['varnish_auto_purge'] = ! empty( $input['varnish_auto_purge'] ) ? 1 : 0; if ( ! rocket_valid_key() ) { $checked = rocket_check_key(); } if ( isset( $checked ) && is_array( $checked ) ) { $input['consumer_key'] = $checked['consumer_key']; $input['consumer_email'] = $checked['consumer_email']; $input['secret_key'] = $checked['secret_key']; } if ( ! empty( $input['secret_key'] ) && empty( $input['ignore'] ) && rocket_valid_key() ) { // Add a "Settings saved." admin notice only if not already added. $notices = array_merge( (array) $wp_settings_errors, (array) get_transient( 'settings_errors' ) ); $notices = array_filter( $notices, function ( $error ) { if ( ! $error || ! is_array( $error ) ) { return false; } if ( ! isset( $error['setting'], $error['code'], $error['type'] ) ) { return false; } return 'general' === $error['setting'] && 'settings_updated' === $error['code'] && 'updated' === $error['type']; } ); if ( ! $notices ) { add_settings_error( 'general', 'settings_updated', __( 'Settings saved.', 'rocket' ), 'updated' ); } } unset( $input['ignore'] ); /** * Filters the sanitized input before returning the array * * @param array $input Array of sanitized values after being submitted by the form. * @param Settings $settings Current class instance. */ return apply_filters( 'rocket_input_sanitize', $input, $this ); } /** * Sanitizes the returned value of a checkbox * * @since 3.0 * * @param array $array Options array. * @param string $key Array key to check. * @return int */ public function sanitize_checkbox( $array, $key ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.arrayFound return isset( $array[ $key ] ) && ! empty( $array[ $key ] ) ? 1 : 0; } /** * Sanitizes the DNS Prefetch sub-option value * * @since 3.5.1 * * @param array $input Array of values for the WP Rocket settings option. * @return array Sanitized array for the DNS Prefetch sub-option */ private function sanitize_dns_prefetch( array $input ) { if ( empty( $input['dns_prefetch'] ) ) { return []; } $value = $input['dns_prefetch']; if ( ! is_array( $value ) ) { $value = explode( "\n", $value ); } $urls = []; foreach ( $value as $url ) { $url = trim( $url ); if ( empty( $url ) ) { continue; } $url = preg_replace( '/^(?:https?)?:?\/{3,}/i', 'http://', $url ); $url = esc_url_raw( $url ); if ( empty( $url ) ) { continue; } $urls[] = $url; } if ( empty( $urls ) ) { return []; } return array_unique( array_map( function ( $url ) { return '//' . wp_parse_url( $url, PHP_URL_HOST ); }, $urls ) ); } /** * Sets radio buttons sub fields value from wp options. * * @since 3.10 * * @param array $sub_fields Array of fields to display.. * @return array */ public function set_radio_buttons_sub_fields_value( $sub_fields ) { foreach ( $sub_fields as $id => &$args ) { $args['id'] = $id; $args['value'] = $this->options->get( $id, $args['default'] ); $args = apply_filters( 'rocket_before_render_option_extra_field', $args ); } return $sub_fields; } /** * Checks if the global exclusion pattern is used in the given field * * @since 3.10.3 * * @param array $field A field array value. * * @return array */ private function check_global_exclusion( $field ) { if ( ! in_array( '/(.*)', $field, true ) ) { return $field; } add_settings_error( 'general', 'reject_uri_global_exclusion', __( 'Sorry! Adding /(.*) in Advanced Rules > Never Cache URL(s) was not saved because it disables caching and optimizations for every page on your site.', 'rocket' ) ); return array_diff_key( $field, array_flip( array_keys( $field, '/(.*)', true ) ) ); } /** * Sanitizes the CDN cnames values * * @param array $cnames Array of user submitted values for the cnames. * * @return array */ private function sanitize_cdn_cnames( array $cnames ) { $cnames = array_map( function ( $cname ) { $cname = trim( $cname ); if ( empty( $cname ) ) { return false; } $cname_parts = get_rocket_parse_url( rocket_add_url_protocol( $cname ) ); if ( false === filter_var( $cname_parts['host'], FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME ) ) { return false; } return $cname_parts['scheme'] . '://' . $cname_parts['host'] . $cname_parts['path']; }, $cnames ); return array_unique( array_filter( $cnames ) ); } } Engine/Admin/Settings/AdminBarMenuTrait.php 0000644 00000006075 15174677547 0014657 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Admin\Settings; use WP_Admin_Bar; trait AdminBarMenuTrait { /** * Admin menu to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance. * @param string $id The menu id. * @param string $title The menu title. * @param string $action Menu action. */ protected function add_menu_to_admin_bar( WP_Admin_Bar $wp_admin_bar, string $id, string $title, string $action ) { if ( ! rocket_valid_key() ) { return; } if ( ! is_admin() ) { return; } $referer = ''; if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { $referer_url = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ); $referer = '&_wp_http_referer=' . rawurlencode( remove_query_arg( 'fl_builder', $referer_url ) ); } $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => $id, 'title' => $title, 'href' => wp_nonce_url( admin_url( "admin-post.php?action={$action}{$referer}" ), $action ), ] ); } /** * Admin menu to WP Rocket admin bar menu * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance. * @param string $id The menu id. * @param string $title The menu title. * @param string $action Menu action. * @param bool $context Context. * * @return void */ protected function add_url_menu_item_to_admin_bar( WP_Admin_Bar $wp_admin_bar, string $id, string $title, string $action, bool $context ) { global $post; if ( is_admin() ) { return; } if ( $post && ! rocket_can_display_options() ) { return; } if ( ! $context ) { return; } $referer = ''; if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { $referer_url = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ); /** * Filters to act on the referer url for the admin bar. * * @param string $uri Current uri. */ $referer = wpm_apply_filters_typed( 'string', 'rocket_admin_bar_referer', esc_url( $referer_url ) ); $referer = '&_wp_http_referer=' . rawurlencode( remove_query_arg( 'fl_builder', $referer ) ); } $wp_admin_bar->add_menu( [ 'parent' => 'wp-rocket', 'id' => $id, 'title' => $title, 'href' => wp_nonce_url( admin_url( 'admin-post.php?action=' . $action . $referer ), $action ), ] ); } /** * Add button to dashboard * * @param bool $context The feature context. * @param string $title The button title. * @param string $label Button label. * @param string $action Button action. * @param string $description Button description text. * * @return void */ public function dashboard_button( bool $context, string $title, string $label, string $action, string $description = '' ): void { if ( ! $context ) { return; } $data = [ 'action' => $action, 'title' => $title, 'label' => $label, 'description' => $description, ]; echo $this->generate( 'sections/clean-section', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } Engine/Admin/Settings/ServiceProvider.php 0000644 00000004341 15174677547 0014456 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Admin\Settings; use WP_Rocket\Dependencies\League\Container\Argument\Literal\{ArrayArgument, StringArgument}; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Dependencies\WPMedia\PluginFamily\Model\PluginFamily as PluginFamilyModel; use WP_Rocket\Dependencies\WPMedia\PluginFamily\Controller\PluginFamily as PluginFamilyController; /** * Service provider for the WP Rocket settings. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'plugin_family_model', 'plugin_family_controller', 'settings', 'settings_render', 'settings_page', 'settings_page_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers items with the container * * @return void */ public function register(): void { $this->getContainer()->add( 'plugin_family_model', PluginFamilyModel::class ); $this->getContainer()->add( 'plugin_family_controller', PluginFamilyController::class ); $this->getContainer()->add( 'settings', Settings::class ) ->addArgument( 'options' ); $this->getContainer()->add( 'settings_render', Render::class ) ->addArguments( [ new StringArgument( $this->getContainer()->get( 'template_path' ) . '/settings' ), 'plugin_family_model', ] ); $this->getContainer()->add( 'settings_page', Page::class ) ->addArguments( [ new ArrayArgument( [ 'slug' => WP_ROCKET_PLUGIN_SLUG, 'title' => WP_ROCKET_PLUGIN_NAME, 'capability' => 'rocket_manage_options', ] ), 'settings', 'settings_render', 'beacon', 'db_optimization', 'user_client', 'delay_js_sitelist', 'template_path', 'options', ] ); $this->getContainer()->addShared( 'settings_page_subscriber', Subscriber::class ) ->addArguments( [ 'settings_page', 'plugin_family_controller', ] ); } } Engine/Admin/Settings/Render.php 0000644 00000041211 15174677547 0012557 0 ustar 00 <?php namespace WP_Rocket\Engine\Admin\Settings; use stdClass; use WP_Rocket\Abstract_Render; use WP_Rocket\Dependencies\WPMedia\PluginFamily\Model\PluginFamily; defined( 'ABSPATH' ) || exit; /** * Handle rendering of HTML content for the settings page. * * @since 3.5.5 Moves into the new architecture. * @since 3.0 */ class Render extends Abstract_render { /** * Settings array. * * @since 3.0 * * @var array */ private $settings = []; /** * Hidden settings array. * * @since 3.0 * * @var array */ private $hidden_settings; /** * Plugin family * * @var PluginFamily * * @since 3.17.2 */ protected $plugin_family; /** * Creates an instance of the object. * * @param string $template_path Template path. * @param PluginFamily $plugin_family Plugin Family Instance. */ public function __construct( string $template_path, PluginFamily $plugin_family ) { parent::__construct( $template_path ); $this->plugin_family = $plugin_family; } /** * Sets the settings value. * * @since 3.0 * * @param array $settings Array of settings. * @return void */ public function set_settings( $settings ) { $this->settings = (array) $settings; } /** * Sets the hidden settings value. * * @since 3.0 * * @param array $hidden_settings Array of hidden settings. */ public function set_hidden_settings( $hidden_settings ) { $this->hidden_settings = $hidden_settings; } /** * Renders the page sections navigation. * * @since 3.0 */ public function render_navigation() { /** * Filters WP Rocket settings page navigation items. * * @since 3.0 * * @param array $navigation { * Items to populate the navigation. * * @type string $id Page section identifier. * @type string $title Menu item title. * @type string $menu_description Menu item description. * @type string $class Menu item classes * } */ $navigation = (array) apply_filters( 'rocket_settings_menu_navigation', $this->settings ); $default = [ 'id' => '', 'title' => '', 'menu_description' => '', 'class' => '', ]; $navigation = array_map( function ( array $item ) use ( $default ) { $item = wp_parse_args( $item, $default ); if ( ! empty( $item['class'] ) ) { $item['class'] = implode( ' ', array_map( 'sanitize_html_class', $item['class'] ) ); } unset( $item['sections'] ); return $item; }, $navigation ); echo $this->generate( 'navigation', $navigation ); // phpcs:ignore WordPress.Security.EscapeOutput -- Dynamic content is properly escaped in the view. } /** * Render the page sections. * * @since 3.0 */ public function render_form_sections() { foreach ( $this->settings as $id => $args ) { $default = [ 'title' => '', 'menu_description' => '', 'class' => '', ]; $args = wp_parse_args( $args, $default ); $id = str_replace( '_', '-', $id ); echo $this->generate( 'page-sections/' . $id, $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } } /** * Render the Imagify page section. * * @since 3.2 */ public function render_imagify_section() { // @phpstan-ignore-next-line require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; $plugin_data = get_transient( 'rocket_imagify_plugin_data' ); if ( ! $plugin_data ) { $query_args = [ 'slug' => 'imagify', 'fields' => [ 'icons' => true, 'active_installs' => true, 'rating' => true, 'ratings' => true, 'short_description' => false, 'sections' => false, 'last_updated' => false, 'added' => false, 'tags' => false, 'homepage' => false, 'donate_link' => false, 'screenshots' => false, 'versions' => false, 'banners' => false, 'contributors' => false, 'requires' => false, 'tested' => false, 'requires_php' => false, 'support_url' => false, 'upgrade_notice' => false, 'business_model' => false, 'repository_url' => false, 'commercial_support_url' => false, 'preview_link' => false, ], ]; $plugin_data = plugins_api( 'plugin_information', $query_args ); if ( is_wp_error( $plugin_data ) ) { $plugin_data = []; } set_transient( 'rocket_imagify_plugin_data', $plugin_data, WEEK_IN_SECONDS ); } echo $this->generate( 'page-sections/imagify', $plugin_data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Render the Tutorials page section. * * @since 3.4 */ public function render_tutorials_section() { echo $this->generate( 'page-sections/tutorials' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Render the tools page section. * * @since 3.0 */ public function render_tools_section() { echo $this->generate( 'page-sections/tools' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Render the plugins page section. * * @since 3.17.2 */ public function render_plugin_section() { $plugin_family = $this->plugin_family->get_filtered_plugins( 'wp-rocket/wp-rocket' ); $data = $plugin_family['categorized']; echo $this->generate( 'page-sections/plugins', $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Renders the settings sections for a page section. * * @since 3.0 * * @param string $page Page section identifier. * @return void */ public function render_settings_sections( $page ) { if ( ! isset( $this->settings[ $page ]['sections'] ) ) { return; } foreach ( $this->settings[ $page ]['sections'] as $args ) { $default = [ 'type' => 'fields_container', 'title' => '', 'description' => '', 'class' => '', 'help' => '', 'helper' => '', 'page' => '', ]; $args = wp_parse_args( $args, $default ); if ( ! empty( $args['class'] ) ) { $args['class'] = implode( ' ', array_map( 'sanitize_html_class', $args['class'] ) ); } call_user_func_array( [ $this, $args['type'] ], [ $args ] ); } } /** * Renders the settings fields for a setting section and page. * * @since 3.0 * * @param string $page Page section identifier. * @param string $section Settings section identifier. * @return void */ public function render_settings_fields( $page, $section ) { if ( ! isset( $this->settings[ $page ]['sections'][ $section ]['fields'] ) ) { return; } $this->render_fields( $this->settings[ $page ]['sections'][ $section ]['fields'] ); } /** * Renders hidden fields in the form. * * @since 3.0 */ public function render_hidden_fields() { foreach ( $this->hidden_settings as $setting ) { call_user_func_array( [ $this, 'hidden' ], [ $setting ] ); } } /** * Displays the fields container section template. * * @since 3.0 * @author Remy Perona * * @param array $args Array of arguments to populate the template. */ public function fields_container( $args ) { echo $this->generate( 'sections/fields-container', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the no container section template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function nocontainer( $args ) { echo $this->generate( 'sections/nocontainer', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the add-ons container section template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function addons_container( $args ) { echo $this->generate( 'sections/addons-container', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the text field template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function text( $args ) { echo $this->generate( 'fields/text', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the checkbox field template. * * @since 3.0 * @author Remy Perona * * @param array $args Array of arguments to populate the template. * @return void */ public function checkbox( $args ) { echo $this->generate( 'fields/checkbox', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the textarea field template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function textarea( $args ) { if ( is_array( $args['value'] ) ) { $args['value'] = implode( "\n", $args['value'] ); } $args['value'] = empty( $args['value'] ) ? '' : $args['value']; echo $this->generate( 'fields/textarea', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the sliding checkbox field template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function sliding_checkbox( $args ) { echo $this->generate( 'fields/sliding-checkbox', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the number input field template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function number( $args ) { echo $this->generate( 'fields/number', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the multiselect field template. * * @param array $args Array of arguments to populate the template. */ public function categorized_multiselect( $args ) { $args['items'] = empty( $args['items'] ) ? new stdClass() : $args['items']; $args['selected'] = get_rocket_option( sanitize_key( $args['id'] ), [] ); echo $this->generate( 'fields/categorized_multiselect', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the select field template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function select( $args ) { echo $this->generate( 'fields/select', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the clear cache lifespan block template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function cache_lifespan( $args ) { echo $this->generate( 'fields/cache-lifespan', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the hidden field template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function hidden( $args ) { if ( is_array( $args['value'] ) ) { $args['value'] = implode( "\n", $args['value'] ); } echo $this->generate( 'fields/hidden', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the CDN CNAMES template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function cnames( $args ) { echo $this->generate( 'fields/cnames', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the RocketCDN template. * * @since 3.5 * * @param array $args Array of arguments to populate the template. */ public function rocket_cdn( $args ) { echo $this->generate( 'fields/rocket-cdn', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the one-click add-on field template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function one_click_addon( $args ) { echo $this->generate( 'fields/one-click-addon', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the Rocket add-on field template. * * @since 3.0 * * @param array $args Array of arguments to populate the template. */ public function rocket_addon( $args ) { echo $this->generate( 'fields/rocket-addon', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the import form template. * * @since 3.0 */ public function render_import_form() { $args = []; /** * Filter the maximum allowed upload size for import files. * * @since (WordPress) 2.3.0 * * @see wp_max_upload_size() * * @param int $max_upload_size Allowed upload size. Default 1 MB. */ $args['bytes'] = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $args['size'] = size_format( $args['bytes'] ); $args['upload_dir'] = wp_upload_dir(); $args['action'] = 'rocket_import_settings'; $args['submit_text'] = __( 'Upload file and import settings', 'rocket' ); echo $this->generate( 'fields/import-form', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays a partial template. * * @since 3.0 * * @param string $part Partial template name. */ public function render_part( $part ) { echo $this->generate( 'partials/' . $part ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Displays the radio_buttons field template. * * @since 3.10 * * @param array $args Array of arguments to populate the template. */ public function radio_buttons( $args ) { echo $this->generate( 'fields/radio-buttons', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view. } /** * Renders the fields. * * @since 3.10 * * @param array $fields fields to render. * * @return void */ public function render_fields( $fields ) { foreach ( $fields as $id => $args ) { $default = [ 'type' => 'text', 'label' => '', 'description' => '', 'class' => '', 'container_class' => '', 'default' => '', 'helper' => '', 'placeholder' => '', 'parent' => '', 'section' => '', 'page' => '', 'sanitize_callback' => 'sanitize_text_field', 'input_attr' => '', 'warning' => [], ]; $args = wp_parse_args( $args, $default ); if ( empty( $args['id'] ) ) { $args['id'] = $id; } if ( ! empty( $args['input_attr'] ) ) { $input_attr = ''; foreach ( $args['input_attr'] as $key => $value ) { if ( 'disabled' === $key ) { if ( 1 === $value ) { $input_attr .= ' disabled'; } continue; } $input_attr .= ' ' . sanitize_key( $key ) . '="' . esc_attr( $value ) . '"'; } $args['input_attr'] = $input_attr; } if ( ! empty( $args['parent'] ) ) { $args['parent'] = ' data-parent="' . esc_attr( $args['parent'] ) . '"'; } if ( ! empty( $args['class'] ) ) { $args['class'] = implode( ' ', array_map( 'sanitize_html_class', $args['class'] ) ); } if ( ! empty( $args['container_class'] ) ) { $args['container_class'] = implode( ' ', array_map( 'sanitize_html_class', $args['container_class'] ) ); } call_user_func_array( [ $this, $args['type'] ], [ $args ] ); } } } Engine/Preload/Subscriber.php 0000644 00000032755 15174677547 0012216 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Preload; use WP_Post; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Preload\Activation\Activation; use WP_Rocket\Engine\Preload\Controller\CheckExcludedTrait; use WP_Rocket\Engine\Preload\Controller\ClearCache; use WP_Rocket\Engine\Preload\Controller\LoadInitialSitemap; use WP_Rocket\Engine\Preload\Controller\Queue; use WP_Rocket\Engine\Preload\Database\Queries\Cache; use WP_Rocket\Event_Management\Subscriber_Interface; use WP_Rocket_Mobile_Detect; use WP_Rocket\Logger\LoggerAware; use WP_Rocket\Logger\LoggerAwareInterface; class Subscriber implements Subscriber_Interface, LoggerAwareInterface { use LoggerAware; use CheckExcludedTrait; /** * Options instance. * * @var Options_Data */ protected $options; /** * Controller to load initial tasks. * * @var LoadInitialSitemap */ protected $controller; /** * Clear cache controller. * * @var ClearCache */ protected $clear_cache; /** * Cache query instance. * * @var Cache */ private $query; /** * Activation manager. * * @var Activation */ protected $activation; /** * Mobile detector instance. * * @var WP_Rocket_Mobile_Detect */ protected $mobile_detect; /** * Preload queue. * * @var Queue */ protected $queue; /** * Creates an instance of the class. * * @param Options_Data $options Options instance. * @param LoadInitialSitemap $controller controller creating the initial task. * @param Cache $query Cache query instance. * @param Activation $activation Activation manager. * @param WP_Rocket_Mobile_Detect $mobile_detect Mobile detector instance. * @param ClearCache $clear_cache Clear cache controller. * @param Queue $queue Preload queue. */ public function __construct( Options_Data $options, LoadInitialSitemap $controller, $query, Activation $activation, WP_Rocket_Mobile_Detect $mobile_detect, ClearCache $clear_cache, Queue $queue ) { $this->options = $options; $this->controller = $controller; $this->query = $query; $this->activation = $activation; $this->mobile_detect = $mobile_detect; $this->clear_cache = $clear_cache; $this->queue = $queue; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events() { return [ 'update_option_' . WP_ROCKET_SLUG => [ [ 'maybe_load_initial_sitemap', 10, 2 ], [ 'maybe_cancel_preload', 10, 2 ], ], 'rocket_after_process_buffer' => 'update_cache_row', 'rocket_deactivation' => 'on_deactivation', 'rocket_reset_preload' => 'on_permalink_changed', 'permalink_structure_changed' => 'on_permalink_changed', 'rocket_domain_changed' => 'on_permalink_changed', 'wp_rocket_upgrade' => [ 'on_update', 16, 2 ], 'rocket_saas_complete_job_status' => 'clean_url', 'rocket_rucss_after_clearing_usedcss' => [ 'clean_url', 20 ], 'rocket_after_automatic_cache_purge' => 'preload_after_automatic_cache_purge', 'after_rocket_clean_post' => [ 'clean_partial_cache', 10, 3 ], 'after_rocket_clean_term' => [ 'clean_partial_cache', 10, 3 ], 'after_rocket_clean_file' => 'clean_url', 'set_404' => 'delete_url_on_not_found', 'rocket_after_clean_terms' => 'clean_urls', 'rocket_after_clean_domain' => 'clean_full_cache', 'delete_post' => 'delete_post_preload_cache', 'pre_delete_term' => 'delete_term_preload_cache', 'rocket_preload_format_url' => 'format_preload_url', 'rocket_preload_lock_url' => 'lock_url', 'rocket_preload_unlock_url' => 'unlock_url', 'rocket_preload_unlock_all_urls' => 'unlock_all_urls', 'rocket_preload_exclude_urls' => [ [ 'add_preload_excluded_uri' ], [ 'add_cache_reject_uri_to_excluded' ], ], 'rocket_rucss_after_clearing_failed_url' => [ 'clean_urls', 20 ], 'rocket_atf_after_clearing_failed_url' => [ 'clean_urls', 20 ], 'transition_post_status' => [ 'remove_private_post', 10, 3 ], 'rocket_preload_exclude' => [ 'exclude_private_url', 10, 2 ], ]; } /** * Load first tasks from preload when preload option is enabled. * * @param array $old_value old configuration values. * @param array $value new configuration values. * @return void */ public function maybe_load_initial_sitemap( $old_value, $value ) { if ( ! isset( $value['manual_preload'], $old_value['manual_preload'] ) ) { return; } if ( $value['manual_preload'] === $old_value['manual_preload'] ) { return; } if ( ! $value['manual_preload'] ) { return; } rocket_renew_box( 'preload_notice' ); $this->controller->load_initial_sitemap(); } /** * Cancel preload when configuration from sitemap changed. * * @param array $old_value old configuration values. * @param array $value new configuration values. * @return void */ public function maybe_cancel_preload( $old_value, $value ) { if ( ! isset( $value['manual_preload'], $old_value['manual_preload'] ) ) { return; } if ( $value['manual_preload'] === $old_value['manual_preload'] ) { return; } if ( $value['manual_preload'] ) { return; } $this->controller->cancel_preload(); } /** * Create or update the cache row after processing the buffer * * @return void */ public function update_cache_row() { global $wp; if ( is_user_logged_in() ) { return; } if ( (bool) ! $this->options->get( 'manual_preload', true ) ) { return; // Bail out if preload is disabled. } $url = home_url( add_query_arg( [], $wp->request ) ); $detected = $this->mobile_detect->isMobile() && ! $this->mobile_detect->isTablet() ? 'mobile' : 'desktop'; /** * Fires when the preload from an URL is completed. * * @param string $url URL preladed. * @param string $device Device from the cache. */ do_action( 'rocket_preload_completed', $url, $detected ); if ( ! empty( (array) $_GET ) || ( $this->query->is_pending( $url ) && $this->options->get( 'do_caching_mobile_files', false ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( $this->is_excluded_by_filter( $url ) ) { $this->query->delete_by_url( $url ); return; } $this->query->create_or_update( [ 'url' => $url, 'status' => 'completed', 'last_accessed' => true, ] ); } /** * Delete url from the Preload when a 404 is risen. * * @return void */ public function delete_url_on_not_found() { global $wp; $url = home_url( $wp->request ); $this->query->delete_by_url( $url ); } /** * Preload on permalink changed. * * @return void */ public function on_permalink_changed() { $this->query->remove_all(); $this->queue->cancel_pending_jobs(); if ( ! $this->options->get( 'manual_preload', false ) ) { return; } $this->queue->add_job_preload_job_load_initial_sitemap_async(); } /** * Disable cron and jobs on update. * * @param string $new_version new version from the plugin. * @param string $old_version old version from the plugin. * @return void */ public function on_update( $new_version, $old_version ) { $this->activation->clean_on_update( $new_version, $old_version ); if ( ! $this->options->get( 'manual_preload', false ) ) { return; } $this->activation->refresh_on_update( $new_version, $old_version ); } /** * Clear preload on deactivation. * * @return void */ public function on_deactivation() { $this->activation->deactivation(); } /** * Clean the url. * * @param string $url url. * @return void */ public function clean_url( string $url ) { if ( ! $this->options->get( 'manual_preload', 0 ) ) { return; } $this->clear_cache->partial_clean( [ $url ] ); } /** * Preload after clearing full cache. * * @return void */ public function clean_full_cache() { if ( ! $this->options->get( 'manual_preload', 0 ) ) { return; } set_transient( 'wpr_preload_running', true ); $this->queue->add_job_preload_job_check_finished_async(); $this->clear_cache->full_clean(); } /** * Preload after clearing some cache. * * @param object $object object modified. * @param array $urls urls cleaned. * @param string $lang lang from the website. * @return void */ public function clean_partial_cache( $object, array $urls, $lang ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound if ( ! $this->options->get( 'manual_preload', false ) ) { return; } // Add Homepage URL to $purge_urls for preload. $urls[] = get_rocket_i18n_home_url( $lang ); $urls = array_filter( $urls ); $this->clear_cache->partial_clean( $urls ); } /** * Clean the list of urls. * * @param array $urls urls. * @return void */ public function clean_urls( array $urls ) { if ( ! $this->options->get( 'manual_preload', 0 ) ) { return; } $this->clear_cache->partial_clean( $urls ); } /** * Delete URL from a post from the preload. * * @param int $post_id ID from the post. * @return void */ public function delete_post_preload_cache( $post_id ) { if ( ! $this->options->get( 'manual_preload', 0 ) ) { return; } $url = get_permalink( $post_id ); if ( empty( $url ) ) { return; } $this->clear_cache->delete_url( $url ); } /** * Delete URL from a term from the preload. * * @param int $term_id ID from the term. * @return void */ public function delete_term_preload_cache( $term_id ) { if ( ! $this->options->get( 'manual_preload', 0 ) ) { return; } $url = get_term_link( (int) $term_id ); if ( empty( $url ) ) { return; } $this->clear_cache->delete_url( $url ); } /** * Pushes URLs to preload to the queue after cache directories are purged. * * @since 3.4 * * @param array $deleted { * An array of arrays, described like: {. * @type string $home_url The home URL. * @type string $home_path Path to home. * @type bool $logged_in True if the home path corresponds to a logged in user’s folder. * @type array $files A list of paths of files that have been deleted. * } * } */ public function preload_after_automatic_cache_purge( $deleted ) { if ( ! $deleted || ! $this->options->get( 'manual_preload' ) ) { return; } foreach ( $deleted as $data ) { if ( $data['logged_in'] ) { // Logged in user: no need to preload those since we would need the corresponding cookies. continue; } foreach ( $data['files'] as $file_path ) { if ( strpos( $file_path, '#' ) ) { // URL with query string. $file_path = preg_replace( '/#/', '?', $file_path, 1 ); } else { $file_path = untrailingslashit( $file_path ); $data['home_path'] = untrailingslashit( $data['home_path'] ); $data['home_url'] = untrailingslashit( $data['home_url'] ); if ( '/' === substr( get_option( 'permalink_structure' ), -1 ) ) { $file_path .= '/'; $data['home_path'] .= '/'; $data['home_url'] .= '/'; } } $this->clear_cache->partial_clean( [ str_replace( $data['home_path'], $data['home_url'], $file_path ) ] ); } } } /** * Remove index from url. * * @param string $url url to reformat. * * @return string */ public function format_preload_url( string $url ) { return preg_replace( '/(index(-https)?\.html$)|(index(-https)?\.html_gzip$)/', '', $url ); } /** * Lock a URL. * * @param string $url URL to lock. * * @return void */ public function lock_url( string $url ) { $this->query->lock( $url ); } /** * Unlock all URL. * * @return void */ public function unlock_all_urls() { $this->query->unlock_all(); } /** * Unlock a URL. * * @param string $url URL to unlock. * * @return void */ public function unlock_url( string $url ) { $this->query->unlock( $url ); } /** * Add the excluded uri from the preload to the filter. * * @param array $regexes regexes containing excluded uris. * @return array */ public function add_preload_excluded_uri( $regexes ): array { $preload_excluded_uri = (array) $this->options->get( 'preload_excluded_uri', [] ); if ( empty( $preload_excluded_uri ) ) { return $regexes; } return array_merge( $regexes, $preload_excluded_uri ); } /** * Remove private post from cache. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Wp post object. * @return void */ public function remove_private_post( string $new_status, string $old_status, $post ) { if ( $new_status === $old_status ) { return; } if ( 'private' !== $new_status ) { return; } $this->delete_post_preload_cache( $post->ID ); } /** * Exclude private urls. * * @param bool $excluded In case we want to exclude that url. * @param string $url Current URL to test. * * @return bool Tells if it's excluded or not. */ public function exclude_private_url( $excluded, string $url ): bool { if ( $excluded ) { return true; } $is_private = ! empty( rocket_url_to_postid( $url, [ 'private' ] ) ); if ( $is_private ) { $this->logger::debug( "Private URL excluded from preload: {$url}", [ 'method' => __METHOD__, ] ); } return $is_private; } } Engine/Preload/Links/Subscriber.php 0000644 00000013516 15174677547 0013270 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Links; use WP_Filesystem_Direct; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Options Data instance * * @var Options_Data */ private $options; /** * WP_Filesystem_Direct instance. * * @var WP_Filesystem_Direct */ private $filesystem; /** * Script enqueued status. * * @var bool */ private $is_enqueued = false; /** * Instantiate the class. * * @param Options_Data $options Options Data instance. * @param WP_Filesystem_Direct $filesystem The Filesystem object. */ public function __construct( Options_Data $options, $filesystem ) { $this->options = $options; $this->filesystem = $filesystem; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'wp_enqueue_scripts' => 'add_preload_script', ]; } /** * Adds the inline script to the footer when the option is enabled * * @since 3.7 * * @return void */ public function add_preload_script() { /** * Bail out if user is logged in * Don't add preload link script */ if ( is_user_logged_in() ) { return; } if ( $this->is_enqueued ) { return; } if ( ! (bool) $this->options->get( 'preload_links', 0 ) || rocket_bypass() ) { return; } $js_assets_path = rocket_get_constant( 'WP_ROCKET_PATH' ) . 'assets/js/'; if ( ! wp_script_is( 'rocket-browser-checker' ) ) { $checker_filename = rocket_get_constant( 'SCRIPT_DEBUG' ) ? 'browser-checker.js' : 'browser-checker.min.js'; // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion wp_register_script( 'rocket-browser-checker', '', [], rocket_get_constant( WP_ROCKET_VERSION, '' ), true ); wp_enqueue_script( 'rocket-browser-checker' ); wp_add_inline_script( 'rocket-browser-checker', $this->filesystem->get_contents( "{$js_assets_path}{$checker_filename}" ) ); } $preload_filename = rocket_get_constant( 'SCRIPT_DEBUG' ) ? 'preload-links.js' : 'preload-links.min.js'; // Register handle with no src to add the inline script after. // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion wp_register_script( 'rocket-preload-links', '', [ 'rocket-browser-checker', ], rocket_get_constant( WP_ROCKET_VERSION, '' ), true ); wp_enqueue_script( 'rocket-preload-links' ); wp_add_inline_script( 'rocket-preload-links', $this->filesystem->get_contents( "{$js_assets_path}{$preload_filename}" ) ); wp_localize_script( 'rocket-preload-links', 'RocketPreloadLinksConfig', $this->get_preload_links_config() ); $this->is_enqueued = true; } /** * Gets the Preload Links script configuration parameters. * * @since 3.7 * * @return array Preload Links script configuration parameters. */ private function get_preload_links_config() { $use_trailing_slash = $this->use_trailing_slash(); $images_ext = 'jpg|jpeg|gif|png|tiff|bmp|webp|avif|pdf|doc|docx|xls|xlsx|php'; $config = [ 'excludeUris' => $this->get_uris_to_exclude( $use_trailing_slash ), 'usesTrailingSlash' => $use_trailing_slash, 'imageExt' => $images_ext, 'fileExt' => $images_ext . '|html|htm', 'siteUrl' => home_url(), 'onHoverDelay' => 100, // milliseconds. -1 disables the "on hover" feature. 'rateThrottle' => 3, // on hover: limits the number of links preloaded per second. ]; /** * Preload Links script configuration parameters. * * This array of parameters are passed as RocketPreloadLinksConfig object and used by the * `preload-links.min.js` script to configure the behavior of the Preload Links feature. * * @since 3.7 * * @param array $config Preload Links script configuration parameters. */ $filtered_config = wpm_apply_filters_typed( 'array', 'rocket_preload_links_config', $config ); return array_merge( $config, $filtered_config ); } /** * Gets the URIs to exclude. * * @since 3.7 * * @param bool $use_trailing_slash When true, uses trailing slash. * * @return string */ private function get_uris_to_exclude( $use_trailing_slash ) { $site_url = site_url(); $uris = get_rocket_cache_reject_uri( false, false ); $uris = str_replace( [ '/(.*)|', '/(.*)/|' ], '/|', $uris ); $default = [ '/refer/', '/go/', '/recommend/', '/recommends/', ]; $excluded = $default; /** * Filters the patterns excluded from links preload * * @since 3.10.8 * * @param string[] $excluded Array of excluded patterns. * @param string[] $default Array of default excluded patterns. */ $excluded = wpm_apply_filters_typed( 'array', 'rocket_preload_links_exclusions', $excluded, $default ); $excluded = array_filter( $excluded ); $login_url = wp_login_url(); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $login_uri = str_replace( home_url(), '', $login_url ); $excluded = array_filter( $excluded, function ( $uri ) use ( $login_uri ) { return ! str_contains( $login_uri, $uri ); } ); $excluded_patterns = ''; if ( ! empty( $excluded ) ) { $excluded_patterns = '|' . implode( '|', $excluded ); } return $uris . $excluded_patterns; } /** * Checks if the given URL has a trailing slash. * * @since 3.7 * * @param string $url URL to check. * * @return bool */ private function has_trailing_slash( $url ) { return substr( $url, -1 ) === '/'; } /** * Indicates if the site uses a trailing slash in the permalink structure. * * @since 3.7 * * @return bool when true, uses `/`; else, no. */ private function use_trailing_slash() { return $this->has_trailing_slash( get_permalink() ); } } Engine/Preload/Links/ServiceProvider.php 0000644 00000002122 15174677547 0014267 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Preload\Links; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for WP Rocket preload links. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'preload_links_admin_subscriber', 'preload_links_subscriber', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the subscribers in the container * * @return void */ public function register(): void { $this->getContainer()->addShared( 'preload_links_admin_subscriber', AdminSubscriber::class ) ->addArgument( 'options' ); $this->getContainer()->addShared( 'preload_links_subscriber', Subscriber::class ) ->addArgument( 'options' ) ->addArgument( rocket_direct_filesystem() ); } } Engine/Preload/Links/AdminSubscriber.php 0000644 00000003006 15174677547 0014232 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Links; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Event_Management\Subscriber_Interface; class AdminSubscriber implements Subscriber_Interface { /** * Options Data instance * * @var Options_Data */ private $options; /** * Instantiate the class. * * @param Options_Data $options Options Data instance. */ public function __construct( Options_Data $options ) { $this->options = $options; } /** * Events this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_first_install_options' => 'add_option', 'rocket_plugins_to_deactivate' => 'add_incompatible_plugins', ]; } /** * Adds the option key & value to the WP Rocket options array * * @since 3.7 * * @param array $options WP Rocket options array. * @return array */ public function add_option( $options ) { $options = (array) $options; $options['preload_links'] = 1; return $options; } /** * Adds plugins incompatible with preload links to our notice. * * @since 3.7 * * @param array $plugins Array of incompatible plugins. * @return array */ public function add_incompatible_plugins( $plugins ) { if ( ! (bool) $this->options->get( 'preload_links', 0 ) ) { return $plugins; } $plugins['flying-pages'] = 'flying-pages/flying-pages.php'; $plugins['instant-page'] = 'instant-page/instantpage.php'; $plugins['quicklink'] = 'quicklink/quicklink.php'; return $plugins; } } Engine/Preload/Cron/Subscriber.php 0000644 00000014030 15174677547 0013101 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Cron; use WP_Rocket\Engine\Common\Queue\Cleaner; use WP_Rocket\Engine\Preload\Admin\Settings; use WP_Rocket\Engine\Preload\Controller\PreloadUrl; use WP_Rocket\Engine\Preload\Database\Queries\Cache; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Preload settings. * * @var Settings */ protected $settings; /** * Db query. * * @var Cache */ protected $query; /** * Preload url controller. * * @var PreloadUrl */ protected $preload_controller; /** * Creates an instance of the class. * * @param Settings $settings Preload settings. * @param Cache $query Db query. * @param PreloadUrl $preload_controller Preload url controller. */ public function __construct( Settings $settings, Cache $query, PreloadUrl $preload_controller ) { $this->settings = $settings; $this->query = $query; $this->preload_controller = $preload_controller; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_preload_clean_rows_time_event' => 'remove_old_rows', 'rocket_preload_process_pending' => [ [ 'process_pending_urls' ], [ 'clean_preload_jobs' ], ], 'rocket_preload_revert_old_failed_rows' => 'revert_old_failed_rows', 'cron_schedules' => [ [ 'add_interval' ], [ 'add_revert_old_failed_interval' ], ], 'init' => [ [ 'schedule_clean_not_commonly_used_rows' ], [ 'schedule_pending_jobs' ], [ 'schedule_revert_old_failed_rows' ], ], ]; } /** * Schedule clean from removing of old urls. * * @return void */ public function schedule_clean_not_commonly_used_rows() { /** * Delay before the not accessed row is deleted. * * @param string $delay delay before the not accessed row is deleted. */ $delay = (string) apply_filters( 'rocket_preload_delay_delete_non_accessed', '1 month' ); if ( '' === $delay || wp_next_scheduled( 'rocket_preload_clean_rows_time_event' ) ) { return; } wp_schedule_event( time() + 10 * MINUTE_IN_SECONDS, 'weekly', 'rocket_preload_clean_rows_time_event' ); } /** * Preload Url jobs. * * @return void */ public function process_pending_urls() { if ( ! $this->settings->is_enabled() ) { return; } $this->preload_controller->process_pending_jobs(); } /** * Clean Action Scheduler jobs for preload. * * @return void */ public function clean_preload_jobs() { $clean_batch_size = (int) apply_filters( 'rocket_action_scheduler_clean_batch_size', 100, 'rocket-preload' ); $cleaner = new Cleaner( null, $clean_batch_size, 'rocket-preload' ); $cleaner->clean(); } /** * Add the interval for the cron. * * @param array $schedules Cron schedules. * @return mixed */ public function add_interval( $schedules ) { if ( ! $this->settings->is_enabled() ) { return $schedules; } /** * Filters the cron interval. * * @since 3.11 * * @param int $interval Interval in seconds. */ $interval = apply_filters( 'rocket_preload_pending_jobs_cron_interval', 1 * rocket_get_constant( 'MINUTE_IN_SECONDS', 60 ) ); $schedules['rocket_preload_process_pending'] = [ 'interval' => $interval, 'display' => esc_html__( 'WP Rocket Preload pending jobs', 'rocket' ), ]; return $schedules; } /** * Add the interval for the cron. * * @param array $schedules Cron schedules. * @return mixed */ public function add_revert_old_failed_interval( $schedules ) { if ( ! $this->settings->is_enabled() ) { return $schedules; } /** * Filters the cron interval. * * @since 3.11 * * @param int $interval Interval in seconds. */ $interval = apply_filters( 'rocket_preload_revert_old_failed_rows_cron_interval', 12 * rocket_get_constant( 'HOUR_IN_SECONDS', 60 * 60 ) ); $schedules['rocket_revert_old_failed_rows'] = [ 'interval' => $interval, 'display' => esc_html__( 'WP Rocket Preload revert stuck failed jobs', 'rocket' ), ]; return $schedules; } /** * Schedule pending preload urls. * * @return void */ public function schedule_pending_jobs() { if ( ! $this->settings->is_enabled() && wp_next_scheduled( 'rocket_preload_process_pending' ) ) { wp_clear_scheduled_hook( 'rocket_preload_process_pending' ); return; } if ( ! $this->settings->is_enabled() ) { return; } if ( wp_next_scheduled( 'rocket_preload_process_pending' ) ) { return; } wp_schedule_event( time() + MINUTE_IN_SECONDS, 'rocket_preload_process_pending', 'rocket_preload_process_pending' ); } /** * Schedule revert stuck failed row cron. * * @return void */ public function schedule_revert_old_failed_rows() { if ( ! $this->settings->is_enabled() && wp_next_scheduled( 'rocket_preload_revert_old_failed_rows' ) ) { wp_clear_scheduled_hook( 'rocket_preload_revert_old_failed_rows' ); return; } if ( ! $this->settings->is_enabled() ) { return; } if ( wp_next_scheduled( 'rocket_preload_revert_old_failed_rows' ) ) { return; } wp_schedule_event( time() + MINUTE_IN_SECONDS, 'rocket_revert_old_failed_rows', 'rocket_preload_revert_old_failed_rows' ); } /** * Remove old urls. * * @return void */ public function remove_old_rows() { /** * Delay before the not accessed row is deleted. * * @param string $delay delay before the not accessed row is deleted. */ $delay = (string) apply_filters( 'rocket_preload_delay_delete_non_accessed', '1 month' ); $parts = explode( ' ', $delay ); if ( '' === $delay || '0' === $delay ) { return; } $value = 1; $unit = 'month'; if ( count( $parts ) === 2 && $parts[0] >= 0 ) { $value = $parts[0]; $unit = $parts[1]; } $this->query->remove_all_not_accessed_rows( $value, $unit ); } /** * Remove old failed urls. * * @return void */ public function revert_old_failed_rows() { $this->query->revert_old_failed(); } } Engine/Preload/ServiceProvider.php 0000644 00000011045 15174677547 0013213 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Preload; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Preload\Activation\Activation; use WP_Rocket\Engine\Preload\Admin\Settings; use WP_Rocket\Engine\Preload\Admin\Subscriber as AdminSubscriber; use WP_Rocket\Engine\Preload\Controller\{CheckFinished, ClearCache, CrawlHomepage, LoadInitialSitemap, PreloadUrl, Queue}; use WP_Rocket\Engine\Preload\Cron\Subscriber as CronSubscriber; use WP_Rocket\Engine\Preload\Database\Queries\Cache as CacheQuery; use WP_Rocket\Engine\Preload\Database\Tables\Cache as CacheTable; use WP_Rocket\Engine\Preload\Frontend\FetchSitemap; use WP_Rocket\Engine\Preload\Frontend\SitemapParser; use WP_Rocket\Engine\Preload\Frontend\Subscriber as FrontEndSubscriber; use WP_Rocket_Mobile_Detect; /** * Service provider for the WP Rocket preload. */ class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'mobile_detect', 'preload_queue', 'sitemap_parser', 'fetch_sitemap_controller', 'check_finished_controller', 'load_initial_sitemap_controller', 'preload_url_controller', 'preload_caches_table', 'preload_caches_query', 'preload_admin_subscriber', 'preload_clean_controller', 'preload_subscriber', 'preload_front_subscriber', 'preload_cron_subscriber', 'preload_activation', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the subscribers in the container * * @since 3.3 * * @return void */ public function register(): void { $this->getContainer()->add( 'mobile_detect', WP_Rocket_Mobile_Detect::class ); $this->getContainer()->add( 'preload_caches_table', CacheTable::class ); $this->getContainer()->add( 'preload_caches_query', CacheQuery::class ) ->addArgument( 'logger' ); $this->getContainer()->get( 'preload_caches_table' ); $this->getContainer()->add( 'preload_queue', Queue::class ); $this->getContainer()->add( 'preload_url_controller', PreloadUrl::class ) ->addArguments( [ 'options', 'preload_queue', 'preload_caches_query', rocket_direct_filesystem(), ] ); $this->getContainer()->add( 'homepage_crawler', CrawlHomepage::class ); $this->getContainer()->add( 'sitemap_parser', SitemapParser::class ); $this->getContainer()->add( 'fetch_sitemap_controller', FetchSitemap::class ) ->addArguments( [ 'sitemap_parser', 'preload_queue', 'preload_caches_query', ] ); $this->getContainer()->add( 'load_initial_sitemap_controller', LoadInitialSitemap::class ) ->addArguments( [ 'preload_queue', 'preload_caches_query', 'homepage_crawler', ] ); $this->getContainer()->add( 'preload_activation', Activation::class ) ->addArguments( [ 'preload_url_controller', 'preload_queue', 'preload_caches_query', 'options', ] ); $this->getContainer()->add( 'preload_settings', Settings::class ) ->addArguments( [ 'options', 'preload_url_controller', 'load_initial_sitemap_controller', 'preload_caches_table', ] ); $this->getContainer()->add( 'check_finished_controller', CheckFinished::class ) ->addArguments( [ 'preload_settings', 'preload_caches_query', 'preload_queue', ] ); $this->getContainer()->addShared( 'preload_front_subscriber', FrontEndSubscriber::class ) ->addArguments( [ 'fetch_sitemap_controller', 'preload_url_controller', 'check_finished_controller', 'load_initial_sitemap_controller', ] ); $this->getContainer()->add( 'preload_clean_controller', ClearCache::class ) ->addArgument( 'preload_caches_query' ); $this->getContainer()->addShared( 'preload_subscriber', Subscriber::class ) ->addArguments( [ 'options', 'load_initial_sitemap_controller', 'preload_caches_query', 'preload_activation', 'mobile_detect', 'preload_clean_controller', 'preload_queue', ] ); $this->getContainer()->addShared( 'preload_cron_subscriber', CronSubscriber::class ) ->addArguments( [ 'preload_settings', 'preload_caches_query', 'preload_url_controller', ] ); $this->getContainer()->addShared( 'preload_admin_subscriber', AdminSubscriber::class ) ->addArgument( 'preload_settings' ); } } Engine/Preload/Controller/ClearCache.php 0000644 00000002375 15174677547 0014203 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Controller; use WP_Rocket\Engine\Preload\Database\Queries\Cache; class ClearCache { use CheckExcludedTrait; /** * DB query. * * @var Cache */ protected $query; /** * Initialise ClearCache. * * @param Cache $query DB query. */ public function __construct( Cache $query ) { $this->query = $query; } /** * Clear urls listed. * * @param array $urls urls to clean. * @return void */ public function partial_clean( array $urls ) { foreach ( $urls as $url ) { // if it has a regex, remove the regex part completely. if ( str_contains( $url, '*' ) ) { $regex_part = basename( $url ); $url = str_replace( $regex_part, '', $url ); } if ( ! $this->is_excluded_by_filter( $url ) ) { $this->query->create_or_update( [ 'url' => $url, 'status' => 'pending', ] ); } else { $this->query->delete_by_url( $url ); } } } /** * Clear all urls. * * @return void */ public function full_clean() { $this->query->set_all_to_pending(); } /** * Delete a URL from the preload. * * @param string $url URL to delete. * @return void */ public function delete_url( string $url ) { $this->query->delete_by_url( $url ); } } Engine/Preload/Controller/LoadInitialSitemap.php 0000644 00000007603 15174677547 0015744 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Controller; use WP_Rocket\Engine\Preload\Database\Queries\Cache; /** * Controller to load initial sitemap tasks. */ class LoadInitialSitemap { /** * Queue group. * * @var Queue */ protected $queue; /** * DB query. * * @var Cache */ protected $query; /** * Homepage crawler. * * @var CrawlHomepage */ protected $crawl_homepage; /** * Instantiate the class. * * @param Queue $queue Queue group. * @param Cache $query DB query. * @param CrawlHomepage $crawl_homepage Homepage crawler. */ public function __construct( Queue $queue, $query, CrawlHomepage $crawl_homepage ) { $this->queue = $queue; $this->query = $query; $this->crawl_homepage = $crawl_homepage; } /** * Load the initial sitemap to the queue. */ public function load_initial_sitemap() { /** * Filter custom preload URL. * * @param array $custom_urls Array of custom preload URLs. */ $urls = apply_filters( 'rocket_preload_load_custom_urls', [] ); $urls [] = home_url(); $urls = array_filter( $urls ); foreach ( $urls as $url ) { $this->query->create_or_nothing( [ 'url' => $url, ] ); $this->queue->add_job_preload_job_preload_url_async( $url ); } /** * Filter sitemaps URLs. * * @param array $sitemaps Array of sitemaps URLs. */ $sitemaps = (array) apply_filters( 'rocket_sitemap_preload_list', [] ); if ( count( $sitemaps ) > 0 ) { /** * Filter sitemaps URLs that will be preloaded. * * @param array $sitemaps Array of sitemaps URLs. */ $sitemaps = apply_filters( 'rocket_preload_sitemap_before_queue', $sitemaps ); $this->add_task_to_queue( $sitemaps ); return; } $sitemap = $this->load_wordpress_sitemap(); if ( ! $sitemap ) { $this->add_homepage_urls(); return; } /** * Filter sitemaps URL that will be preloaded. * * @param array $sitemaps Array of sitemaps URL. */ $sitemaps = apply_filters( 'rocket_preload_sitemap_before_queue', [ $sitemap ] ); $this->add_task_to_queue( $sitemaps ); } /** * Add homepage urls to the preload. * * @return void */ protected function add_homepage_urls() { $urls = $this->crawl_homepage->crawl(); if ( ! $urls ) { return; } foreach ( $urls as $url ) { $this->query->create_or_nothing( [ 'url' => $url, ] ); } } /** * Add initial sitemap tasks. * * @param array $sitemaps sitemap used for creating tasks. */ protected function add_task_to_queue( array $sitemaps ) { set_transient( 'wpr_preload_running', true ); foreach ( $sitemaps as $sitemap ) { $this->queue->add_job_preload_job_parse_sitemap_async( $sitemap ); } $this->queue->add_job_preload_job_check_finished_async(); } /** * Load default WordPress sitemap. * * @return false|string */ protected function load_wordpress_sitemap() { if ( ! $this->sitemaps_enabled() ) { return false; } $sitemaps = wp_sitemaps_get_server(); return $sitemaps->index->get_index_url(); } /** * Cancel the preloading. * * @return void */ public function cancel_preload() { $this->queue->cancel_pending_jobs(); $this->query->revert_in_progress(); } /** * Check if sitemap is enabled. * * @return bool */ protected function sitemaps_enabled() { $is_enabled = (bool) get_option( 'blog_public' ); /** * Filters whether XML Sitemaps are enabled or not. * * When XML Sitemaps are disabled via this filter, rewrite rules are still * in place to ensure a 404 is returned. * * @see WP_Sitemaps::register_rewrites() * * @since 5.5.0 * * @param bool $is_enabled Whether XML Sitemaps are enabled or not. Defaults * to true for public sites. */ return (bool) apply_filters( 'wp_sitemaps_enabled', $is_enabled ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } } Engine/Preload/Controller/PreloadUrl.php 0000644 00000022367 15174677547 0014305 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Preload\Controller; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Preload\Database\Queries\Cache; use WP_Filesystem_Direct; class PreloadUrl { use CheckExcludedTrait; /** * Preload queue. * * @var Queue */ protected $queue; /** * Preload database query. * * @var Cache */ protected $query; /** * Configurations options. * * @var Options_Data */ protected $options; /** * Filesystem. * * @var WP_Filesystem_Direct */ protected $filesystem; /** * Instantiate preload controller. * * @param Options_Data $options configuration options. * @param Queue $queue preload queue. * @param Cache $rocket_cache preload database query. * @param WP_Filesystem_Direct $filesystem Filesystem. */ public function __construct( Options_Data $options, Queue $queue, Cache $rocket_cache, WP_Filesystem_Direct $filesystem ) { $this->options = $options; $this->query = $rocket_cache; $this->queue = $queue; $this->filesystem = $filesystem; } /** * Preload an url. * * @param string $url url to preload. * * @return void */ public function preload_url( string $url ) { $start = 0; $duration = 0; $is_mobile = $this->options->get( 'do_caching_mobile_files', false ); if ( $this->is_already_cached( $url ) && ( ! $is_mobile || $this->is_already_cached( $url, true ) ) ) { $this->query->make_status_complete( $url ); return; } // Should we perform a duration check? $check_duration = ( false === get_transient( 'rocket_preload_check_duration' ) ) ? true : false; $requests = [ [ 'url' => $url, 'is_mobile' => false, 'headers' => [ 'blocking' => false, 'timeout' => 0.01, 'user-agent' => 'WP Rocket/Preload', ], ], ]; if ( $is_mobile ) { $requests[] = [ 'url' => $url, 'headers' => [ 'user-agent' => $this->get_mobile_user_agent_prefix(), ], 'is_mobile' => true, ]; } /** * Filters to modify requests done to preload an url. * * @param array $requests Requests that will be done. */ $requests = wpm_apply_filters_typed( 'array', 'rocket_preload_before_preload_url', $requests ); $requests = array_filter( $requests ); foreach ( $requests as $request ) { if ( ! isset( $request['url'] ) || ! is_string( $request['url'] ) ) { continue; } $headers = isset( $request['headers'] ) && is_array( $request['headers'] ) ? $request['headers'] : []; $headers = array_merge( $headers, [ 'blocking' => false, 'timeout' => 0.01, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound ] ); if ( $check_duration ) { $headers['blocking'] = true; $headers['timeout'] = 20; } /** * Filters the arguments for the preload request. * * @param array $headers Request arguments. */ $headers = wpm_apply_filters_typed( 'array', 'rocket_preload_url_request_args', $headers ); if ( $check_duration ) { $start = microtime( true ); } wp_safe_remote_get( user_trailingslashit( $request['url'] ), $headers ); if ( $check_duration ) { $duration = ( microtime( true ) - $start ); // Duration of the request. } $default_delay_between = 500000; /** * Filter the delay between each preload request. * * @param int $delay_between the defined delay. */ $delay_between = apply_filters( 'rocket_preload_delay_between_requests', $default_delay_between ); $delay_between = absint( $delay_between ); if ( empty( $delay_between ) ) { $delay_between = $default_delay_between; } usleep( $delay_between ); if ( ! $check_duration ) { continue; } $duration_transient = get_transient( 'rocket_preload_previous_requests_durations' ); $average_duration = ( false !== $duration_transient ) ? $duration_transient : 0; // Update average duration. $average_duration = ( $average_duration <= 0 ) ? $duration : ( $average_duration * 0.7 + $duration * 0.3 ); set_transient( 'rocket_preload_previous_requests_durations', $average_duration, 5 * MINUTE_IN_SECONDS ); set_transient( 'rocket_preload_check_duration', $duration, MINUTE_IN_SECONDS ); // Don't check request duration for 1 minute. $check_duration = false; } } /** * Get the prefix to prepend to the user agent used for preload to make a HTTP request detected as a mobile device. * * @return string */ protected function get_mobile_user_agent_prefix() { $prefix = 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1'; /** * Filter the prefix to prepend to the user agent used for preload to make a HTTP request detected as a mobile device. * * @param string $prefix The prefix. */ $new_prefix = wpm_apply_filters_typed( 'string', 'rocket_mobile_preload_user_agent_prefix', $prefix ); if ( empty( $new_prefix ) ) { return $prefix; } return 'WP Rocket/Preload ' . $new_prefix; } /** * Process pending jobs inside CRON iteration. * * @return void */ public function process_pending_jobs() { $pending_actions = $this->queue->get_pending_preload_actions(); // Retrieve batch size limits and request timing estimation. /** * Get the number of pending cron job * * @param int $args Maximum number of job size. */ $max_batch_size = ( (int) apply_filters( 'rocket_preload_cache_pending_jobs_cron_rows_count', 45 ) ) - count( $pending_actions ); /** * Get the number of in progress cron job * * @param int $args Minimum number of job size. */ $min_batch_size = ( (int) apply_filters( 'rocket_preload_cache_min_in_progress_jobs_count', 5 ) ); $average_duration = get_transient( 'rocket_preload_previous_requests_durations' ); /** * Estimate batch size based on request duration. * In case no estimation or there is an issue with the value use $min_batch_size. */ $next_batch_size = (int) ( ! $average_duration ? $min_batch_size : round( -5 * $average_duration + 55 ) ); // Limit next_batch_size. $next_batch_size = max( $next_batch_size, $min_batch_size ); // Not lower than 5. $next_batch_size = min( $next_batch_size, $max_batch_size ); // Not higher than 45. $next_batch_size = max( $next_batch_size, 0 ); // Not lower than 0. // Get all in-progress jobs with request sent and no results. /** * Set the delay before an in-progress row is considered as outdated. * * @param int $delay delay. * @return int */ $delay = (int) apply_filters( 'rocket_preload_outdated', /** * Set the max number of rows in batches. * * @param int $count number of rows in batches. * @return int */ (int) ( $max_batch_size / 15 ) ); $stuck_rows = $this->query->get_outdated_in_progress_jobs( $delay ); // Make sure the request has been sent for those jobs. $stuck_rows = array_filter( $stuck_rows, function ( $row ) use ( $pending_actions ) { foreach ( $pending_actions as $action ) { if ( count( $action->get_args() ) > 0 && $row->url === $action->get_args()[0] ) { return false; } } return true; } ); // Make those hanging jobs failed. foreach ( $stuck_rows as $row ) { $this->query->make_status_failed( (int) $row->id ); } // Add new jobs in progress. $rows = $this->query->get_pending_jobs( $next_batch_size ); foreach ( $rows as $row ) { if ( $this->is_excluded_by_filter( $row->url ) ) { $this->query->delete_by_url( $row->url ); continue; } $this->query->make_status_inprogress( (int) $row->id ); $this->queue->add_job_preload_job_preload_url_async( $row->url ); } } /** * Check if the cache file for $item already exists. * * @param string $url The URL to preload. * @param bool $is_mobile is mobile text. * * @return bool */ public function is_already_cached( string $url, bool $is_mobile = false ) { static $https; if ( ! isset( $https ) ) { $https = is_ssl() && $this->options->get( 'cache_ssl' ) ? '-https' : ''; } $url = get_rocket_parse_url( $url ); /** This filter is documented in inc/functions/htaccess.php */ if ( apply_filters( 'rocket_url_no_dots', false ) ) { $url['host'] = str_replace( '.', '_', $url['host'] ); } $url['path'] = trailingslashit( $url['path'] ); if ( '' !== $url['query'] ) { $url['query'] = '#' . $url['query'] . '/'; } $mobile = $is_mobile ? '-mobile' : ''; $file_cache_path = rocket_get_constant( 'WP_ROCKET_CACHE_PATH' ) . $url['host'] . strtolower( $url['path'] . $url['query'] ) . 'index' . $mobile . $https . '.html'; if ( ! $this->options->get( 'cache_webp', false ) ) { return $this->filesystem->exists( $file_cache_path ); } $webp_path = rocket_get_constant( 'WP_ROCKET_CACHE_PATH' ) . $url['host'] . strtolower( $url['path'] . $url['query'] ) . 'index' . $mobile . $https . '-webp.html'; $no_webp_path = rocket_get_constant( 'WP_ROCKET_CACHE_PATH' ) . $url['host'] . strtolower( $url['path'] . $url['query'] ) . '.no-webp'; return $this->filesystem->exists( $webp_path ) || ( $this->filesystem->exists( $no_webp_path ) && $this->filesystem->exists( $file_cache_path ) ); } } Engine/Preload/Controller/CheckFinished.php 0000644 00000002067 15174677547 0014716 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Controller; use WP_Rocket\Engine\Preload\Admin\Settings; use WP_Rocket\Engine\Preload\Database\Queries\Cache; class CheckFinished { /** * Preload settings. * * @var Settings */ protected $settings; /** * Preload queue. * * @var Queue */ protected $queue; /** * Db query. * * @var Cache */ protected $query; /** * Instantiate class. * * @param Settings $settings Preload settings. * @param Cache $cache Db query. * @param Queue $queue Preload queue. */ public function __construct( Settings $settings, Cache $cache, Queue $queue ) { $this->settings = $settings; $this->query = $cache; $this->queue = $queue; } /** * Check if the preload is finished. * * @return void */ public function check_finished() { if ( ( ! $this->queue->has_remaining_tasks() && ! $this->query->has_pending_jobs() ) || ! $this->settings->is_enabled() ) { delete_transient( 'wpr_preload_running' ); return; } $this->queue->add_job_preload_job_check_finished_async(); } } Engine/Preload/Controller/CrawlHomepage.php 0000644 00000003055 15174677547 0014743 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Controller; class CrawlHomepage { /** * Crawl the homepage. * * @return array|false */ public function crawl() { $user_agent = 'WP Rocket/Preload'; /** * Filters the arguments for the partial preload request. * * @param array $args Request arguments. */ $args = apply_filters( 'rocket_homepage_preload_url_request_args', [ 'timeout' => 10, 'user-agent' => $user_agent, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound ] ); $response = wp_remote_get( esc_url_raw( home_url() ), $args ); if ( is_wp_error( $response ) ) { return false; } $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $response_code ) { return false; } $content = wp_remote_retrieve_body( $response ); preg_match_all( '/<a\s+(?:[^>]+?[\s"\']|)href\s*=\s*(["\'])(?<href>[^"\']+)\1/imU', $content, $urls ); $home_url = home_url(); $urls = array_map( static function ( $url ) use ( $home_url ) { if ( wp_parse_url( $url, PHP_URL_HOST ) || strpos( $url, '#' ) !== false ) { return $url; } return trailingslashit( $home_url ) . ltrim( wp_parse_url( $url, PHP_URL_PATH ), '/' ); }, $urls['href'] ); $urls = array_filter( $urls, static function ( $url ) use ( $home_url ) { return strpos( $url, $home_url ) !== false && strpos( $url, '#' ) === false; } ); return array_values( array_unique( $urls ) ); } } Engine/Preload/Controller/CheckExcludedTrait.php 0000644 00000004023 15174677547 0015720 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Controller; trait CheckExcludedTrait { /** * Add new pattern of excluded uri. * * @param array $regexes regexes used to exclude urls. * @return array */ public function add_cache_reject_uri_to_excluded( array $regexes ): array { $user_added_cache_reject_uri = (array) get_rocket_option( 'cache_reject_uri', [] ); if ( count( $user_added_cache_reject_uri ) === 0 ) { return $regexes; } $altered_user_added_cache_reject_uri = implode( '$|', $user_added_cache_reject_uri ); $user_added_cache_reject_uri = implode( '|', $user_added_cache_reject_uri ); $cache_reject_uri = get_rocket_cache_reject_uri(); $regexes[] = str_replace( $user_added_cache_reject_uri . '|', $altered_user_added_cache_reject_uri . '$|', $cache_reject_uri ); return $regexes; } /** * Check if the url is excluded by using a filter. * * @param string $url url to check. * @return bool */ protected function is_excluded_by_filter( string $url ): bool { global $wp_rewrite; $pagination_regex = "/$wp_rewrite->pagination_base/\d+"; $url = strtok( $url, '?' ); $url = user_trailingslashit( $url ); /** * Check if the url is excluded or not. * * @param bool $isExcluded Default excluded or not. * @param string $url URL to check. */ $is_excluded = apply_filters( 'rocket_preload_exclude', false, $url ); if ( $is_excluded ) { return true; } /** * Regex to exclude URI from preload. * * @param string[] $regexes Regexes to check. * @param string $url Current preloading url. */ $regexes = (array) apply_filters( 'rocket_preload_exclude_urls', [ $pagination_regex ], $url ); if ( empty( $regexes ) ) { return false; } $regexes = array_unique( $regexes ); foreach ( $regexes as $regex ) { if ( ! is_string( $regex ) ) { // @phpstan-ignore-line continue; } $regex = user_trailingslashit( $regex ); if ( preg_match( "@$regex$@m", $url ) ) { return true; } } return false; } } Engine/Preload/Controller/Queue.php 0000644 00000005410 15174677547 0013306 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Controller; use ActionScheduler_Store; use WP_Rocket\Engine\Common\Queue\AbstractASQueue; class Queue extends AbstractASQueue { /** * Group from the queue. * * @var string */ protected $group = 'rocket-preload'; /** * Add Async load initial sitemap job. * * @return int */ public function add_job_preload_job_load_initial_sitemap_async() { return $this->add_async( 'rocket_preload_job_load_initial_sitemap' ); } /** * Add Async parse sitemap job with url. * * @param string $sitemap_url sitemap url. * * @return int */ public function add_job_preload_job_parse_sitemap_async( string $sitemap_url ) { return $this->add_async( 'rocket_preload_job_parse_sitemap', [ $sitemap_url, ] ); } /** * Add Async preload url job with url. * * @param string $url url to preload. * * @return int */ public function add_job_preload_job_preload_url_async( string $url ) { return $this->add_async( 'rocket_preload_job_preload_url', [ $url, ] ); } /** * Add a job that check if the preload is finished. * * @return int */ public function add_job_preload_job_check_finished_async() { if ( $this->job_preload_job_check_finished_async_exists() ) { return 0; } return $this->schedule_single( time() + MINUTE_IN_SECONDS, 'rocket_preload_job_check_finished', [ time() ] ); } /** * Check if a task job_preload_job_check_finished_async_exists already exists. * * @return bool */ public function job_preload_job_check_finished_async_exists() { if ( ! did_action( 'init' ) || doing_action( 'init' ) ) { return true; } $row_found = $this->search( [ 'hook' => 'rocket_preload_job_check_finished', 'status' => ActionScheduler_Store::STATUS_PENDING, ], 'ids' ); return count( $row_found ) > 0; } /** * Check if some task is remaining. * * @return bool */ public function has_remaining_tasks() { $parse_sitemap = $this->search( [ 'hook' => 'rocket_preload_job_parse_sitemap', 'status' => ActionScheduler_Store::STATUS_PENDING, ], 'ids' ); $preload_url = $this->search( [ 'hook' => 'rocket_preload_job_preload_url', 'status' => ActionScheduler_Store::STATUS_PENDING, ], 'ids' ); return count( $parse_sitemap ) > 0 || count( $preload_url ) > 0; } /** * Cancel pending jobs. * * @return void */ public function cancel_pending_jobs() { $this->cancel_all( '' ); } /** * Return pending actions inside AS scheduler queue. * * @return array */ public function get_pending_preload_actions(): array { return $this->search( [ 'hook' => 'rocket_preload_job_preload_url', 'status' => ActionScheduler_Store::STATUS_PENDING, 'per_page' => -1, ] ); } } Engine/Preload/Database/Queries/Cache.php 0000644 00000040060 15174677547 0014223 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Database\Queries; use WP_Rocket\Logger\Logger; use WP_Rocket\Dependencies\BerlinDB\Database\Query; use WP_Rocket\Engine\Preload\Database\Rows\CacheRow; use WP_Rocket\Engine\Preload\Database\Schemas\Cache as Schema; class Cache extends Query { /** * Logger instance. * * @var Logger */ protected $logger; /** * Name of the database table to query. * * @var string */ protected $table_name = 'wpr_rocket_cache'; /** * String used to alias the database table in MySQL statement. * * Keep this short, but descriptive. I.E. "tr" for term relationships. * * This is used to avoid collisions with JOINs. * * @var string */ protected $table_alias = 'wpr_cache'; /** * Name of class used to setup the database schema. * * @var string */ protected $table_schema = Schema::class; /** Item ******************************************************************/ /** * Name for a single item. * * Use underscores between words. I.E. "term_relationship" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name = 'cache'; /** * Plural version for a group of items. * * Use underscores between words. I.E. "term_relationships" * * This is used to automatically generate action hooks. * * @var string */ protected $item_name_plural = 'caches'; /** * Name of class used to turn IDs into first-class objects. * * This is used when looping through return values to guarantee their shape. * * @var mixed */ protected $item_shape = CacheRow::class; /** * Instantiate query. * * @param Logger $logger logger instance. * * @param string|array $query { * Optional. Array or query string of item query parameters. * Default empty. * * @type string $fields Site fields to return. Accepts 'ids' (returns an array of item IDs) * or empty (returns an array of complete item objects). Default empty. * To do a date query against a field, append the field name with _query * @type bool $count Whether to return a item count (true) or array of item objects. * Default false. * @type int $number Limit number of items to retrieve. Use 0 for no limit. * Default 100. * @type int $offset Number of items to offset the query. Used to build LIMIT clause. * Default 0. * @type bool $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query. * Default true. * @type string|array $orderby Accepts false, an empty array, or 'none' to disable `ORDER BY` clause. * Default 'id'. * @type string $item How to item retrieved items. Accepts 'ASC', 'DESC'. * Default 'DESC'. * @type string $search Search term(s) to retrieve matching items for. * Default empty. * @type array $search_columns Array of column names to be searched. * Default empty array. * @type bool $update_item_cache Whether to prime the cache for found items. * Default false. * @type bool $update_meta_cache Whether to prime the meta cache for found items. * Default false. * } */ public function __construct( Logger $logger, $query = [] ) { parent::__construct( $query ); $this->logger = $logger; } /** * Create new resource row or update its contents if not created before. * * @since 3.9 * * @param array $resource Resource array. * * @return bool */ public function create_or_update( array $resource ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.resourceFound /** * Format the url. * * @param string $url url to format. * @return string */ $url = apply_filters( 'rocket_preload_format_url', $resource['url'] ); $url = untrailingslashit( strtok( $url, '?' ) ); if ( $this->is_rejected( $resource['url'] ) || get_transient( 'wp_rocket_updating' ) ) { return false; } // check the database if those resources added before. $rows = $this->query( [ 'url' => $url, ], false ); if ( count( $rows ) === 0 ) { // Create this new row in DB. $resource_id = $this->add_item( [ 'url' => $url, 'status' => key_exists( 'status', $resource ) ? $resource['status'] : 'pending', 'is_locked' => key_exists( 'is_locked', $resource ) ? $resource['is_locked'] : false, 'last_accessed' => current_time( 'mysql', true ), ] ); if ( $resource_id ) { return $resource_id; } $this->logger->error( "Cannot insert {$resource['url']} into {$this->table_name}" ); return false; } $db_row = array_pop( $rows ); $data = [ 'url' => $url, 'status' => key_exists( 'status', $resource ) ? $resource['status'] : $db_row->status, 'is_locked' => key_exists( 'is_locked', $resource ) ? $resource['is_locked'] : $db_row->is_locked, 'modified' => current_time( 'mysql', true ), ]; if ( key_exists( 'last_accessed', $resource ) && (bool) $resource['last_accessed'] ) { $data['last_accessed'] = current_time( 'mysql', true ); } // Update this row with the new content. $this->update_item( $db_row->id, $data ); return $db_row->id; } /** * Create new resource row or update its contents if not created before. * * @since 3.9 * * @param array $resource Resource array. * * @return bool */ public function create_or_nothing( array $resource ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.resourceFound if ( $this->is_rejected( $resource['url'] ) ) { return false; } /** * Format the url. * * @param string $url url to format. * @return string */ $url = apply_filters( 'rocket_preload_format_url', $resource['url'] ); $url = strtok( $url, '?' ); // check the database if those resources added before. $rows = $this->query( [ 'url' => untrailingslashit( $url ), ], false ); if ( count( $rows ) > 0 ) { return false; } // Create this new row in DB. $resource_id = $this->add_item( [ 'url' => untrailingslashit( $url ), 'status' => key_exists( 'status', $resource ) ? $resource['status'] : 'pending', 'is_locked' => key_exists( 'is_locked', $resource ) ? $resource['is_locked'] : false, 'last_accessed' => current_time( 'mysql', true ), ] ); if ( $resource_id ) { return $resource_id; } $this->logger->error( "Cannot insert {$resource['url']} into {$this->table_name}" ); return false; } /** * Get all rows with the same url (desktop and mobile versions). * * @param string $url Page url. * * @return array|false */ public function get_rows_by_url( string $url ) { $url = strtok( $url, '?' ); $query = $this->query( [ 'url' => untrailingslashit( $url ), ] ); if ( empty( $query ) ) { return false; } return $query; } /** * Delete DB row by url. * * @param string $url Page url to be deleted. * * @return bool */ public function delete_by_url( string $url ) { $items = $this->get_rows_by_url( $url ); if ( ! $items ) { return false; } $deleted = true; foreach ( $items as $item ) { if ( ! is_bool( $item ) ) { $deleted = $deleted && $this->delete_item( $item->id ); } } return $deleted; } /** * Get all preload caches which were not accessed in the last month. * * @param float $delay delay before the not accessed row is deleted. * @param string $unit unit from the delay. * @return array */ public function get_old_cache( float $delay = 1, string $unit = 'month' ): array { // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return []; } $prefixed_table_name = $db->prefix . $this->table_name; $query = "SELECT id FROM `$prefixed_table_name` WHERE `last_accessed` <= date_sub(now(), interval $delay $unit)"; $rows_affected = $db->get_results( $query ); return $rows_affected; } /** * Remove all completed rows one by one. * * @param float $delay delay before the not accessed row is deleted. * @param string $unit unit from the delay. * @return void */ public function remove_all_not_accessed_rows( float $delay = 1, string $unit = 'month' ) { $rows = $this->get_old_cache( $delay, $unit ); foreach ( $rows as $row ) { if ( ! is_bool( $row ) ) { $this->delete_item( $row->id ); } } } /** * Fetch pending jobs. * * @param int $total total of jobs to fetch. * @return array */ public function get_pending_jobs( int $total = 45 ) { $inprogress_count = $this->query( [ 'count' => true, 'status' => 'in-progress', 'is_locked' => false, ], false ); if ( $total <= 0 || (int) $inprogress_count >= $total ) { return []; } $orderby = 'modified'; /** * Filter order for preloading pending urls. * * @param bool $orderby order for preloading pending urls. * * @returns bool */ if ( apply_filters( 'rocket_preload_order', false ) ) { $orderby = 'id'; } return $this->query( [ 'number' => ( $total - $inprogress_count ), 'status' => 'pending', 'fields' => [ 'id', 'url', ], 'job_id__not_in' => [ 'not_in' => '', ], 'is_locked' => false, 'orderby' => $orderby, 'order' => 'asc', ] ); } /** * Change the status from the task to inprogress. * * @param int $id id from the task. * @return bool */ public function make_status_inprogress( int $id ) { return $this->update_item( $id, [ 'status' => 'in-progress', 'modified' => current_time( 'mysql', true ), ] ); } /** * Make the status from the task to complete. * * @param string $url url from the task. * @return bool */ public function make_status_complete( string $url ) { $tasks = $this->query( [ 'url' => $url, ] ); if ( count( $tasks ) === 0 ) { return false; } $task = array_pop( $tasks ); return $this->update_item( $task->id, [ 'status' => 'completed', 'modified' => current_time( 'mysql', true ), ] ); } /** * Check if pending jobs are remaining. * * @return bool */ public function has_pending_jobs() { $pending_count = $this->query( [ 'count' => true, 'status' => 'pending', ] ); return 0 !== $pending_count; } /** * Revert in-progress urls. * * @return void */ public function revert_in_progress() { $in_progress_list = $this->query( [ 'status' => 'in-progress', ] ); foreach ( $in_progress_list as $in_progress ) { $this->update_item( $in_progress->id, [ 'status' => 'pending', 'modified' => current_time( 'mysql', true ), ] ); } } /** * Revert old in-progress rows * * @deprecated */ public function revert_old_in_progress() { // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $prefixed_table_name = $db->prefix . $this->table_name; $db->query( "UPDATE `$prefixed_table_name` SET status = 'pending', modified = '" . current_time( 'mysql', true ) . "' WHERE status = 'in-progress' AND `modified` <= date_sub(now(), interval 12 hour)" ); } /** * Revert old failed rows. */ public function revert_old_failed() { // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $prefixed_table_name = $db->prefix . $this->table_name; return $db->query( "UPDATE `$prefixed_table_name` SET status = 'pending', modified = '" . current_time( 'mysql', true ) . "' WHERE status = 'failed' AND `modified` <= date_sub(now(), interval 12 hour)" ); } /** * Set all rows to pending. */ public function set_all_to_pending() { // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $prefixed_table_name = $db->prefix . $this->table_name; /** * Filter condition for cleaning URLS in the database. * * @param string $condition condition for cleaning URLS in the database. * @returns string */ $condition = apply_filters( 'rocket_preload_all_to_pending_condition', ' WHERE 1 = 1' ); $db->query( "UPDATE `$prefixed_table_name` SET status = 'pending', modified = '" . current_time( 'mysql', true ) . "'$condition" ); } /** * Check if the page is preloaded. * * @param string $url url from the page to check. * @return bool */ public function is_preloaded( string $url ): bool { $pending_count = $this->query( [ 'count' => true, 'status' => 'in-progress', 'url' => untrailingslashit( $url ), ] ); return 0 !== $pending_count; } /** * Check if the page is pending. * * @param string $url url from the page to check. * @return bool */ public function is_pending( string $url ): bool { $pending_count = $this->query( [ 'count' => true, 'status' => 'pending', 'url' => untrailingslashit( $url ), ] ); return 0 !== $pending_count; } /** * Remove all entries from the table. * * @return false|void */ public function remove_all() { // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $prefixed_table_name = $db->prefix . $this->table_name; $db->query( "DELETE FROM `$prefixed_table_name` WHERE 1 = 1" ); } /** * Lock a URL. * * @param string $url URL to lock. * * @return void */ public function lock( string $url ) { $this->create_or_update( [ 'url' => $url, 'is_locked' => true, ] ); } /** * Unlock all URLs. * * @return false|void */ public function unlock_all() { // Get the database interface. $db = $this->get_db(); // Bail if no database interface is available. if ( ! $db ) { return false; } $prefixed_table_name = $db->prefix . $this->table_name; $db->query( "UPDATE `$prefixed_table_name` SET is_locked = false;" ); } /** * Unlock a URL. * * @param string $url URL to unlock. * * @return void */ public function unlock( string $url ) { $this->create_or_update( [ 'url' => $url, 'is_locked' => false, ] ); } /** * Check if the url is rejected. * * @param string $url url to check. * @return bool */ protected function is_rejected( string $url ): bool { $extensions = [ 'php' => 1, 'xml' => 1, 'xsl' => 1, 'kml' => 1, ]; $extension = pathinfo( $url, PATHINFO_EXTENSION ); return $extension && isset( $extensions[ $extension ] ); } /** * Make the status from the task to failed. * * @param int $id id from the task. * @return bool */ public function make_status_failed( int $id ) { return $this->update_item( $id, [ 'status' => 'failed', 'modified' => current_time( 'mysql', true ), ] ); } /** * Update last accessed from the row. * * @param int $id id from the row. * @return bool */ public function update_last_access( int $id ) { return $this->update_item( $id, [ 'last_accessed' => current_time( 'mysql', true ), ] ); } /** * Return outdated in-progress jobs. * * @param int $delay delay to delete. * @return array|int */ public function get_outdated_in_progress_jobs( int $delay = 3 ) { return $this->query( [ 'status' => 'in-progress', 'is_locked' => false, 'date_query' => [ [ 'column' => 'modified', 'before' => "$delay minute ago", ], ], ], false ); } } Engine/Preload/Database/Rows/CacheRow.php 0000644 00000002132 15174677547 0014226 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Database\Rows; use WP_Rocket\Dependencies\BerlinDB\Database\Row; class CacheRow extends Row { /** * Row ID * * @var int */ public $id; /** * URL * * @var string */ public $url; /** * Status * * @var string */ public $status; /** * Last modified time * * @var int */ public $modified; /** * Last accessed time * * @var int */ public $last_accessed; /** * Is row locked * * @var bool */ public $is_locked; /** * Unused variable * * @var bool */ public $is_mobile; /** * CacheRow constructor. * * @param object $item Current row details. */ public function __construct( $item ) { parent::__construct( $item ); $this->id = (int) $this->id; $this->url = (string) $this->url; $this->status = (string) $this->status; $this->modified = empty( $this->modified ) ? 0 : strtotime( $this->modified ); $this->last_accessed = empty( $this->last_accessed ) ? 0 : strtotime( $this->last_accessed ); $this->is_locked = (bool) $this->is_locked; } } Engine/Preload/Database/Schemas/Cache.php 0000644 00000002766 15174677547 0014204 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Database\Schemas; use WP_Rocket\Dependencies\BerlinDB\Database\Schema; class Cache extends Schema { /** * Array of database column objects * * @var array */ public $columns = [ // ID column. [ 'name' => 'id', 'type' => 'bigint', 'length' => '20', 'unsigned' => true, 'extra' => 'auto_increment', 'primary' => true, 'sortable' => true, ], // URL column. [ 'name' => 'url', 'type' => 'varchar', 'length' => '2000', 'default' => null, 'cache_key' => true, 'searchable' => true, 'sortable' => false, ], // STATUS column. [ 'name' => 'status', 'type' => 'varchar', 'length' => '255', 'default' => null, 'cache_key' => true, 'searchable' => true, 'sortable' => false, ], // MODIFIED column. [ 'name' => 'modified', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], // LAST_ACCESSED column. [ 'name' => 'last_accessed', 'type' => 'timestamp', 'default' => '0000-00-00 00:00:00', 'created' => true, 'date_query' => true, 'sortable' => true, ], // IS_LOCKED column. [ 'name' => 'is_locked', 'type' => 'tinyint', 'length' => '1', 'default' => 0, 'cache_key' => true, 'searchable' => true, 'sortable' => true, ], ]; } Engine/Preload/Database/Tables/Cache.php 0000644 00000004571 15174677547 0014027 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Preload\Database\Tables; use WP_Rocket\Dependencies\BerlinDB\Database\Table; class Cache extends Table { /** * Hook into queries, admin screens, and more! * * @since 1.0.0 */ public function __construct() { parent::__construct(); add_action( 'rocket_preload_activation', [ $this, 'maybe_upgrade' ] ); add_action( 'init', [ $this, 'maybe_upgrade' ] ); add_action( 'admin_init', [ $this, 'maybe_trigger_recreate_table' ], 9 ); } /** * Table name * * @var string */ protected $name = 'wpr_rocket_cache'; /** * Database version key (saved in _options or _sitemeta) * * @var string */ protected $db_version_key = 'wpr_rocket_cache_version'; /** * Database version * * @var int */ protected $version = 20220927; /** * Key => value array of versions => methods. * * @var array */ protected $upgrades = [ 20220927 => 'add_is_locked_column', ]; /** * Setup the database schema * * @return void */ protected function set_schema() { $this->schema = " id bigint(20) unsigned NOT NULL AUTO_INCREMENT, url varchar(2000) NOT NULL default '', status varchar(255) NOT NULL default '', modified timestamp NOT NULL default '0000-00-00 00:00:00', last_accessed timestamp NOT NULL default '0000-00-00 00:00:00', is_locked tinyint(1) NOT NULL default 0, PRIMARY KEY (id), KEY url (url(191)), KEY modified (modified), KEY last_accessed (last_accessed)"; } /** * Trigger recreation of cache table if not exist. * * @return void */ public function maybe_trigger_recreate_table() { if ( $this->exists() ) { return; } delete_option( $this->db_version_key ); } /** * Add is_locked column. * * @return bool */ public function add_is_locked_column() { $hash_column_exists = $this->column_exists( 'is_locked' ); $created = true; if ( ! $hash_column_exists ) { $created &= $this->get_db()->query( "ALTER TABLE {$this->table_name} ADD COLUMN is_locked tinyint(1) NOT NULL default 0 AFTER last_accessed" ); } return $this->is_success( $created ); } /** * Truncate cache table. * * @return bool */ public function truncate_cache_table(): bool { if ( ! $this->exists() ) { return false; } return $this->truncate(); } } Engine/Preload/Activation/ServiceProvider.php 0000644 00000003324 15174677547 0015315 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Preload\Activation; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use WP_Rocket\Engine\Preload\Controller\{PreloadUrl, Queue}; use WP_Rocket\Engine\Preload\Database\Queries\Cache as CacheQuery; use WP_Rocket\Engine\Preload\Database\Tables\Cache as CacheTable; class ServiceProvider extends AbstractServiceProvider { /** * Array of services provided by this service provider * * @var array */ protected $provides = [ 'preload_cache_table', 'preload_caches_query', 'preload_url_controller', 'preload_queue', 'preload_activation', ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the subscribers in the container * * @since 3.3 * * @return void */ public function register(): void { $this->getContainer()->add( 'preload_cache_table', CacheTable::class ); $this->getContainer()->get( 'preload_cache_table' ); $this->getContainer()->add( 'preload_cache_query', CacheQuery::class ) ->addArgument( 'logger' ); $this->getContainer()->add( 'preload_queue', Queue::class ); $this->getContainer()->add( 'preload_url_controller', PreloadUrl::class ) ->addArguments( [ 'options', 'preload_queue', 'preload_cache_query', rocket_direct_filesystem(), ] ); $this->getContainer()->add( 'preload_activation', Activation::class ) ->addArguments( [ 'preload_url_controller', 'preload_queue', 'preload_cache_query', 'options', ] ); } } Engine/Preload/Activation/Activation.php 0000644 00000005754 15174677547 0014314 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Preload\Activation; use WP_Rocket\Engine\Activation\ActivationInterface; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Preload\Controller\{PreloadUrl, Queue}; use WP_Rocket\Engine\Preload\Database\Queries\Cache; class Activation implements ActivationInterface { /** * Preload controller * * @var PreloadUrl */ private $preload_url; /** * Preload queue. * * @var Queue */ protected $queue; /** * DB query. * * @var Cache */ protected $query; /** * Options. * * @var Options_Data */ protected $options; /** * Instantiate class. * * @param PreloadUrl $preload_url PreloadUrl instance. * @param Queue $queue Preload queue. * @param Cache $query DB query. * @param Options_Data $options Options. */ public function __construct( PreloadUrl $preload_url, Queue $queue, Cache $query, Options_Data $options ) { $this->preload_url = $preload_url; $this->queue = $queue; $this->query = $query; $this->options = $options; } /** * Launch preload on activation. */ public function activate() { add_action( 'rocket_activation', [ $this, 'preload_activation' ], 15 ); add_action( 'rocket_after_activation', [ $this, 'preload_homepage' ] ); } /** * Run actions on activation. * * @return void */ public function preload_activation() { if ( ! $this->options->get( 'manual_preload', true ) ) { return; } /** * Action that fires before the preload does. */ do_action( 'rocket_preload_activation' ); $this->queue->add_job_preload_job_load_initial_sitemap_async(); } /** * Preloads the homepage on activation * * @return void */ public function preload_homepage() { $this->preload_url->preload_url( home_url() ); } /** * Disable cron and jobs on update. * * @param string $new_version new version from the plugin. * @param string $old_version old version from the plugin. * @return void */ public function clean_on_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.12.0', '>=' ) ) { return; } $this->query->remove_all(); $this->queue->cancel_pending_jobs(); if ( ! wp_next_scheduled( 'rocket_preload_process_pending' ) ) { return; } wp_clear_scheduled_hook( 'rocket_preload_process_pending' ); } /** * Reload sitemap on update. * * @param string $new_version new version from the plugin. * @param string $old_version old version from the plugin. * @return void */ public function refresh_on_update( $new_version, $old_version ) { if ( version_compare( $old_version, '3.12.0.2', '>' ) ) { return; } $this->queue->add_job_preload_job_load_initial_sitemap_async(); } /** * Clear preload on deactivation. */ public function deactivation() { wp_clear_scheduled_hook( 'rocket_preload_clean_rows_time_event' ); wp_clear_scheduled_hook( 'rocket_preload_process_pending' ); wp_clear_scheduled_hook( 'rocket_preload_revert_old_failed_rows' ); } } Engine/Preload/Frontend/Subscriber.php 0000644 00000005101 15174677547 0013756 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Frontend; use WP_Rocket\Engine\Preload\Controller\CheckFinished; use WP_Rocket\Engine\Preload\Controller\LoadInitialSitemap; use WP_Rocket\Engine\Preload\Controller\PreloadUrl; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Controller fetching the sitemap. * * @var FetchSitemap */ protected $fetch_sitemap; /** * Controller preloading urls. * * @var PreloadUrl */ protected $preload_controller; /** * Controller checking if the preload is finished. * * @var CheckFinished */ protected $check_finished; /** * Controller loading the initial sitemap. * * @var LoadInitialSitemap */ protected $initial_sitemap; /** * Creates an instance of the class. * * @param FetchSitemap $fetch_sitemap controller fetching the sitemap. * @param PreloadUrl $preload_controller controller preloading urls. * @param CheckFinished $check_finished controller checking if the preload is finished. * @param LoadInitialSitemap $initial_sitemap Controller loading the initial sitemap. */ public function __construct( FetchSitemap $fetch_sitemap, PreloadUrl $preload_controller, CheckFinished $check_finished, LoadInitialSitemap $initial_sitemap ) { $this->fetch_sitemap = $fetch_sitemap; $this->preload_controller = $preload_controller; $this->check_finished = $check_finished; $this->initial_sitemap = $initial_sitemap; } /** * Return an array of events that this subscriber listens to. * * @return array */ public static function get_subscribed_events() { return [ 'rocket_preload_job_parse_sitemap' => 'parse_sitemap', 'rocket_preload_job_preload_url' => 'preload_url', 'rocket_preload_job_check_finished' => 'check_finished', 'rocket_preload_job_load_initial_sitemap' => 'load_initial_sitemap', ]; } /** * Parse the sitemap. * * @param string $url url to parse. * @return void */ public function parse_sitemap( string $url ) { $this->fetch_sitemap->parse_sitemap( $url ); } /** * Preload url. * * @param string $url url to preload. * @return void */ public function preload_url( string $url ) { $this->preload_controller->preload_url( $url ); } /** * Check if the preload is finished. * * @return void */ public function check_finished() { $this->check_finished->check_finished(); } /** * Load the initial sitemap. * * @return void */ public function load_initial_sitemap() { $this->initial_sitemap->load_initial_sitemap(); } } Engine/Preload/Frontend/SitemapParser.php 0000644 00000002343 15174677547 0014437 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Frontend; use SimpleXMLElement; class SitemapParser { /** * XML document to parse. * * @var SimpleXMLElement|false */ protected $xml; /** * Set the content from the sitemap to parse. * * @param string $content content from the sitemap to parse. */ public function set_content( string $content ) { libxml_use_internal_errors( true ); $this->xml = simplexml_load_string( $content ); } /** * Get links to sitemaps. * * @return array */ public function get_links(): array { $links = []; if ( false === $this->xml ) { return []; } $url_count = count( $this->xml->url ); for ( $i = 0; $i < $url_count; $i++ ) { $url = (string) $this->xml->url[ $i ]->loc; if ( ! $url ) { continue; } $links [] = $url; } return $links; } /** * Get children sitemaps. * * @return array */ public function get_children(): array { $children = []; if ( false === $this->xml ) { return []; } $sitemap_children = count( $this->xml->sitemap ); for ( $i = 0; $i < $sitemap_children; $i++ ) { $url = (string) $this->xml->sitemap[ $i ]->loc; if ( ! $url ) { continue; } $children [] = $url; } return $children; } } Engine/Preload/Frontend/FetchSitemap.php 0000644 00000003206 15174677547 0014233 0 ustar 00 <?php namespace WP_Rocket\Engine\Preload\Frontend; use WP_Rocket\Engine\Preload\Controller\CheckExcludedTrait; use WP_Rocket\Engine\Preload\Controller\Queue; use WP_Rocket\Engine\Preload\Database\Queries\Cache; class FetchSitemap { use CheckExcludedTrait; /** * Parse controller. * * @var SitemapParser */ protected $sitemap_parser; /** * Queue instance. * * @var Queue */ protected $queue; /** * DB query. * * @var Cache */ protected $query; /** * Instantiate the class. * * @param SitemapParser $sitemap_parser Parse controller. * @param Queue $queue Queue instance. * @param Cache $rocket_cache DB query. */ public function __construct( SitemapParser $sitemap_parser, Queue $queue, Cache $rocket_cache ) { $this->sitemap_parser = $sitemap_parser; $this->queue = $queue; $this->query = $rocket_cache; } /** * Parse a sitemap. * * @param string $url url from the sitemap. */ public function parse_sitemap( string $url ) { $response = wp_safe_remote_get( $url ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return; } $data = wp_remote_retrieve_body( $response ); if ( ! $data ) { return; } $this->sitemap_parser->set_content( $data ); $links = $this->sitemap_parser->get_links(); foreach ( $links as $link ) { if ( ! $this->is_excluded_by_filter( $link ) ) { $this->query->create_or_nothing( [ 'url' => $link, ] ); } } $children = $this->sitemap_parser->get_children(); foreach ( $children as $child ) { $this->queue->add_job_preload_job_parse_sitemap_async( $child ); } } } Engine/Preload/Admin/Subscriber.php 0000644 00000004563 15174677547 0013242 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Preload\Admin; use WP_Rocket\Event_Management\Subscriber_Interface; class Subscriber implements Subscriber_Interface { /** * Settings instance. * * @var Settings */ protected $settings; /** * Creates an instance of the class. * @param Settings $settings Settings instance. */ public function __construct( Settings $settings ) { $this->settings = $settings; } /** * Return an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events() { return [ 'admin_notices' => [ [ 'maybe_display_preload_notice' ], ], 'rocket_options_changed' => 'preload_homepage', 'switch_theme' => 'preload_homepage', 'rocket_after_clean_used_css' => 'preload_homepage', 'rocket_domain_options_changed' => 'clear_and_preload', 'rocket_input_sanitize' => 'sanitize_options', 'wp_rocket_upgrade' => [ 'maybe_clean_cron', 15, 2 ], ]; } /** * Maybe display the preload notice. * * @return void */ public function maybe_display_preload_notice() { $this->settings->maybe_display_preload_notice(); } /** * Preload the homepage * * @return void */ public function preload_homepage() { $this->settings->preload_homepage(); } /** * Clear the cache table and preload * * @return void */ public function clear_and_preload() { $this->settings->clear_and_preload(); } /** * Sanitizes Preload Excluded URI option when saving the settings * * @param array $input Array of values submitted from the form. * * @return array */ public function sanitize_options( $input ): array { if ( empty( $input['preload_excluded_uri'] ) ) { $input['preload_excluded_uri'] = []; return $input; } $input['preload_excluded_uri'] = rocket_sanitize_textarea_field( 'preload_excluded_uri', $input['preload_excluded_uri'] ); return $input; } /** * Unlock all preload URL on update. * * @param string $wp_rocket_version Latest WP Rocket version. * @param string $actual_version Installed WP Rocket version. */ public function maybe_clean_cron( $wp_rocket_version, $actual_version ) { if ( version_compare( $actual_version, '3.12.5', '<' ) ) { return; } wp_clear_scheduled_hook( 'rocket_preload_revert_old_in_progress_rows' ); } } Engine/Preload/Admin/Settings.php 0000644 00000006243 15174677547 0012734 0 ustar 00 <?php declare(strict_types=1); namespace WP_Rocket\Engine\Preload\Admin; use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Preload\Controller\{LoadInitialSitemap, PreloadUrl}; use WP_Rocket\Engine\Preload\Database\Tables\Cache as CacheTable; class Settings { /** * Instance of options handler. * * @var Options_Data */ private $options; /** * PreloadUrl instance * * @var PreloadUrl */ private $preload_url; /** * LoadInitialSitemap instance * * @var LoadInitialSitemap */ private $load_initial_sitemap; /** * CacheTable instance * * @var CacheTable */ private $cache_table; /** * Instantiate the class * * @param Options_Data $options Instance of options handler. * @param PreloadUrl $preload_url PreloadUrl instance. * @param LoadInitialSitemap $load_initial_sitemap LoadInitialSitemap instance. * @param CacheTable $cache_table CacheTable instance. */ public function __construct( Options_Data $options, PreloadUrl $preload_url, LoadInitialSitemap $load_initial_sitemap, CacheTable $cache_table ) { $this->options = $options; $this->preload_url = $preload_url; $this->load_initial_sitemap = $load_initial_sitemap; $this->cache_table = $cache_table; } /** * Maybe display the preload notice. * * @return void */ public function maybe_display_preload_notice() { if ( ! $this->can_display_notice() ) { return; } if ( false === get_transient( 'wpr_preload_running' ) ) { return; } $boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true ); if ( in_array( 'preload_notice', (array) $boxes, true ) ) { return; } $message = sprintf( // translators: %1$s = plugin name. __( '%1$s: The preload service is now active. After the initial preload it will continue to cache all your pages whenever they are purged. No further action is needed.', 'rocket' ), '<strong>WP Rocket</strong>' ); rocket_dismiss_box( 'preload_notice' ); rocket_notice_html( [ 'status' => 'info', 'message' => $message, 'id' => 'rocket-notice-preload-processing', ] ); } /** * Checks if we can display the Preload notices. * * @return bool */ private function can_display_notice(): bool { $screen = get_current_screen(); if ( isset( $screen->id ) && 'settings_page_wprocket' !== $screen->id ) { return false; } if ( ! current_user_can( 'rocket_manage_options' ) ) { return false; } return $this->is_enabled(); } /** * Determines if Preload option is enabled. * * @return boolean */ public function is_enabled(): bool { return (bool) $this->options->get( 'manual_preload', 0 ); } /** * Preload the homepage. * * @return void */ public function preload_homepage() { if ( ! $this->is_enabled() ) { return; } $this->preload_url->preload_url( home_url() ); } /** * Clear cache table and preload. * * @return void */ public function clear_and_preload() { $this->cache_table->truncate_cache_table(); if ( ! $this->is_enabled() ) { return; } $this->load_initial_sitemap->load_initial_sitemap(); $this->preload_url->preload_url( home_url() ); } }
| ver. 1.6 |
Github
|
.
| PHP 8.3.30 | Генерация страницы: 0.09 |
proxy
|
phpinfo
|
Настройка