����JFIF���������
__ __ __ __ _____ _ _ _____ _ _ _ | \/ | \ \ / / | __ \ (_) | | / ____| | | | | | \ / |_ __\ V / | |__) | __ ___ ____ _| |_ ___ | (___ | |__ ___| | | | |\/| | '__|> < | ___/ '__| \ \ / / _` | __/ _ \ \___ \| '_ \ / _ \ | | | | | | |_ / . \ | | | | | |\ V / (_| | || __/ ____) | | | | __/ | | |_| |_|_(_)_/ \_\ |_| |_| |_| \_/ \__,_|\__\___| |_____/|_| |_|\___V 2.1 if you need WebShell for Seo everyday contact me on Telegram Telegram Address : @jackleetFor_More_Tools:
Requests.php 0000666 00000102321 15216466524 0007106 0 ustar 00 <?php
/**
* Requests for PHP
*
* Inspired by Requests for Python.
*
* Based on concepts from SimplePie_File, RequestCore and WP_Http.
*
* @package Requests
*/
namespace WpOrg\Requests;
use WpOrg\Requests\Auth\Basic;
use WpOrg\Requests\Capability;
use WpOrg\Requests\Cookie\Jar;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Hooks;
use WpOrg\Requests\IdnaEncoder;
use WpOrg\Requests\Iri;
use WpOrg\Requests\Proxy\Http;
use WpOrg\Requests\Response;
use WpOrg\Requests\Transport\Curl;
use WpOrg\Requests\Transport\Fsockopen;
use WpOrg\Requests\Utility\InputValidator;
/**
* Requests for PHP
*
* Inspired by Requests for Python.
*
* Based on concepts from SimplePie_File, RequestCore and WP_Http.
*
* @package Requests
*/
class Requests {
/**
* POST method
*
* @var string
*/
const POST = 'POST';
/**
* PUT method
*
* @var string
*/
const PUT = 'PUT';
/**
* GET method
*
* @var string
*/
const GET = 'GET';
/**
* HEAD method
*
* @var string
*/
const HEAD = 'HEAD';
/**
* DELETE method
*
* @var string
*/
const DELETE = 'DELETE';
/**
* OPTIONS method
*
* @var string
*/
const OPTIONS = 'OPTIONS';
/**
* TRACE method
*
* @var string
*/
const TRACE = 'TRACE';
/**
* PATCH method
*
* @link https://tools.ietf.org/html/rfc5789
* @var string
*/
const PATCH = 'PATCH';
/**
* Default size of buffer size to read streams
*
* @var integer
*/
const BUFFER_SIZE = 1160;
/**
* Option defaults.
*
* @see \WpOrg\Requests\Requests::get_default_options()
* @see \WpOrg\Requests\Requests::request() for values returned by this method
*
* @since 2.0.0
*
* @var array
*/
const OPTION_DEFAULTS = [
'timeout' => 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<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => 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<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => 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<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => 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;
}
}
Exception/ArgumentCount.php 0000666 00000002664 15216466524 0012035 0 ustar 00 <?php
namespace WpOrg\Requests\Exception;
use WpOrg\Requests\Exception;
/**
* Exception for when an incorrect number of arguments are passed to a method.
*
* Typically, this exception is used when all arguments for a method are optional,
* but certain arguments need to be passed together, i.e. a method which can be called
* with no arguments or with two arguments, but not with one argument.
*
* Along the same lines, this exception is also used if a method expects an array
* with a certain number of elements and the provided number of elements does not comply.
*
* @package Requests\Exceptions
* @since 2.0.0
*/
final class ArgumentCount extends Exception {
/**
* Create a new argument count exception with a standardized text.
*
* @param string $expected The argument count expected as a phrase.
* For example: `at least 2 arguments` or `exactly 1 argument`.
* @param int $received The actual argument count received.
* @param string $type Exception type.
*
* @return \WpOrg\Requests\Exception\ArgumentCount
*/
public static function create($expected, $received, $type) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
return new self(
sprintf(
'%s::%s() expects %s, %d given',
$stack[1]['class'],
$stack[1]['function'],
$expected,
$received
),
$type
);
}
}
Exception/InvalidArgument.php 0000666 00000002122 15216466524 0012320 0 ustar 00 <?php
namespace WpOrg\Requests\Exception;
use InvalidArgumentException;
/**
* Exception for an invalid argument passed.
*
* @package Requests\Exceptions
* @since 2.0.0
*/
final class InvalidArgument extends InvalidArgumentException {
/**
* Create a new invalid argument exception with a standardized text.
*
* @param int $position The argument position in the function signature. 1-based.
* @param string $name The argument name in the function signature.
* @param string $expected The argument type expected as a string.
* @param string $received The actual argument type received.
*
* @return \WpOrg\Requests\Exception\InvalidArgument
*/
public static function create($position, $name, $expected, $received) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
return new self(
sprintf(
'%s::%s(): Argument #%d (%s) must be of type %s, %s given',
$stack[1]['class'],
$stack[1]['function'],
$position,
$name,
$expected,
$received
)
);
}
}
Utility/FilteredIterator.php 0000666 00000004155 15216466524 0012214 0 ustar 00 <?php
/**
* Iterator for arrays requiring filtered values
*
* @package Requests\Utilities
*/
namespace WpOrg\Requests\Utility;
use ArrayIterator;
use ReturnTypeWillChange;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Utility\InputValidator;
/**
* Iterator for arrays requiring filtered values
*
* @package Requests\Utilities
*/
final class FilteredIterator extends ArrayIterator {
/**
* Callback to run as a filter
*
* @var callable
*/
private $callback;
/**
* Create a new iterator
*
* @param array $data The array or object to be iterated on.
* @param callable $callback Callback to be called on each value
*
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data argument is not iterable.
*/
public function __construct($data, $callback) {
if (InputValidator::is_iterable($data) === false) {
throw InvalidArgument::create(1, '$data', 'iterable', gettype($data));
}
parent::__construct($data);
if (is_callable($callback)) {
$this->callback = $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) {}
}
Cache/CallableNameFilter.php 0000666 00000006452 15216467623 0011755 0 ustar 00 <?php
/**
* SimplePie
*
* A PHP-Based RSS and Atom Feed Framework.
* Takes the hard work out of managing a complete RSS/Atom solution.
*
* Copyright (c) 2004-2022, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* * Neither the name of the SimplePie Team nor the names of its contributors may be used
* to endorse or promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS
* AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package SimplePie
* @copyright 2004-2022 Ryan Parman, Sam Sneddon, Ryan McCue
* @author Ryan Parman
* @author Sam Sneddon
* @author Ryan McCue
* @link http://simplepie.org/ SimplePie
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*/
namespace SimplePie\Cache;
/**
* Creating a cache filename with callables
*
* @package SimplePie
* @subpackage Caching
*/
final class CallableNameFilter implements NameFilter
{
/**
* @var callable
*/
private $callable;
public function __construct(callable $callable)
{
$this->callable = $callable;
}
/**
* Method to create cache filename with.
*
* The returning name MUST follow the rules for keys in PSR-16.
*
* @link https://www.php-fig.org/psr/psr-16/
*
* The returning name MUST be a string of at least one character
* that uniquely identifies a cached item, MUST only contain the
* characters A-Z, a-z, 0-9, _, and . in any order in UTF-8 encoding
* and MUST not longer then 64 characters. The following characters
* are reserved for future extensions and MUST NOT be used: {}()/\@:
*
* A provided implementing library MAY support additional characters
* and encodings or longer lengths, but MUST support at least that
* minimum.
*
* @param string $name The name for the cache will be most likly an url with query string
*
* @return string the new cache name
*/
public function filter(string $name): string
{
return call_user_func($this->callable, $name);
}
}
memoizers/meta-tags-context-memoizer.php 0000666 00000012063 15220430626 0014466 0 ustar 00 <?php
namespace Yoast\WP\SEO\Memoizers;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Blocks_Helper;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* The meta tags context memoizer.
*/
class Meta_Tags_Context_Memoizer {
/**
* The blocks helper.
*
* @var Blocks_Helper
*/
protected $blocks;
/**
* The current page helper.
*
* @var Current_Page_Helper
*/
protected $current_page;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $repository;
/**
* The meta tags context.
*
* @var Meta_Tags_Context
*/
protected $context_prototype;
/**
* The presentation memoizer.
*
* @var Presentation_Memoizer
*/
protected $presentation_memoizer;
/**
* The meta tags context.
*
* @var Meta_Tags_Context[]
*/
protected $cache = [];
/**
* Meta_Tags_Context_Memoizer constructor.
*
* @param Blocks_Helper $blocks The blocks helper.
* @param Current_Page_Helper $current_page The current page helper.
* @param Indexable_Repository $repository Indexable repository.
* @param Meta_Tags_Context $context_prototype The meta tags context prototype.
* @param Presentation_Memoizer $presentation_memoizer Memoizer for the presentation.
*/
public function __construct(
Blocks_Helper $blocks,
Current_Page_Helper $current_page,
Indexable_Repository $repository,
Meta_Tags_Context $context_prototype,
Presentation_Memoizer $presentation_memoizer
) {
$this->blocks = $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 ] );
return;
}
if ( $indexable !== null ) {
unset( $this->cache[ $indexable ] );
return;
}
$this->cache = [];
}
}
loader.php 0000666 00000015166 15220430626 0006540 0 ustar 00 <?php
namespace Yoast\WP\SEO;
use Throwable;
use WP_CLI;
use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class that manages loading integrations if and only if all their conditionals are met.
*/
class Loader {
/**
* The registered integrations.
*
* @var string[]
*/
protected $integrations = [];
/**
* The registered integrations.
*
* @var string[]
*/
protected $initializers = [];
/**
* The registered routes.
*
* @var string[]
*/
protected $routes = [];
/**
* The registered commands.
*
* @var string[]
*/
protected $commands = [];
/**
* The registered migrations.
*
* @var string[]
*/
protected $migrations = [];
/**
* The dependency injection container.
*
* @var ContainerInterface
*/
protected $container;
/**
* Loader constructor.
*
* @param ContainerInterface $container The dependency injection container.
*/
public function __construct( ContainerInterface $container ) {
$this->container = $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;
}
}
}
editors/domain/integrations/integration-data-provider-interface.php 0000666 00000001560 15220430626 0021731 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Editors\Domain\Integrations;
/**
* Describes the interface for integration domain objects which can be enabled or not
*/
interface Integration_Data_Provider_Interface {
/**
* If the integration is activated.
*
* @return bool If the integration is activated.
*/
public function is_enabled(): bool;
/**
* Return this object represented by a key value array.
*
* @return array<string, bool> 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<string, bool> Returns the legacy key and if the feature is enabled.
*/
public function to_legacy_array(): array;
}
editors/domain/analysis-features/analysis-features-list.php 0000666 00000002410 15220430626 0020245 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Editors\Domain\Analysis_Features;
/**
* This class describes a list of analysis features.
*/
class Analysis_Features_List {
/**
* The features.
*
* @var array<Analysis_Feature>
*/
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<string, bool> 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<string, bool> 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/seo/description.php 0000666 00000002432 15220430626 0013313 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Editors\Domain\Seo;
/**
* This class describes the description SEO data.
*/
class Description implements Seo_Plugin_Data_Interface {
/**
* The formatted description date.
*
* @var string
*/
private $description_date;
/**
* The description template.
*
* @var string
*/
private $description_template;
/**
* The constructor.
*
* @param string $description_date The description date.
* @param string $description_template The description template.
*/
public function __construct( string $description_date, string $description_template ) {
$this->description_date = $description_date;
$this->description_template = $description_template;
}
/**
* Returns the data as an array format.
*
* @return array<string>
*/
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<string>
*/
public function to_legacy_array(): array {
return [
'metadesc_template' => $this->description_template,
'metaDescriptionDate' => $this->description_date,
];
}
}
editors/domain/seo/social.php 0000666 00000004422 15220430626 0012243 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Editors\Domain\Seo;
/**
* This class describes the social SEO data.
*/
class Social implements Seo_Plugin_Data_Interface {
/**
* The Social title template.
*
* @var string
*/
private $social_title_template;
/**
* The Social description template.
*
* @var string
*/
private $social_description_template;
/**
* The Social image template.
*
* @var string
*/
private $social_image_template;
/**
* The first content image for the social preview.
*
* @var string
*/
private $social_first_content_image;
/**
* The constructor.
*
* @param string $social_title_template The Social title template.
* @param string $social_description_template The Social description template.
* @param string $social_image_template The Social image template.
* @param string $social_first_content_image The first content image for the social preview.
*/
public function __construct(
string $social_title_template,
string $social_description_template,
string $social_image_template,
string $social_first_content_image
) {
$this->social_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<string>
*/
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<string>
*/
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/title.php 0000666 00000002544 15220430626 0012115 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Editors\Domain\Seo;
/**
* This class describes the title SEO data.
*/
class Title implements Seo_Plugin_Data_Interface {
/**
* The title template.
*
* @var string
*/
private $title_template;
/**
* The title template without the fallback.
*
* @var string
*/
private $title_template_no_fallback;
/**
* The constructor.
*
* @param string $title_template The title template.
* @param string $title_template_no_fallback The title template without the fallback.
*/
public function __construct( string $title_template, string $title_template_no_fallback ) {
$this->title_template = $title_template;
$this->title_template_no_fallback = $title_template_no_fallback;
}
/**
* Returns the data as an array format.
*
* @return array<string>
*/
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<string>
*/
public function to_legacy_array(): array {
return [
'title_template' => $this->title_template,
'title_template_no_fallback' => $this->title_template_no_fallback,
];
}
}
editors/domain/seo/keyphrase.php 0000666 00000002713 15220430626 0012765 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Editors\Domain\Seo;
/**
* This class describes the keyphrase SEO data.
*/
class Keyphrase implements Seo_Plugin_Data_Interface {
/**
* The keyphrase and the associated posts that use it.
*
* @var array<string>
*/
private $keyphrase_usage_count;
/**
* The post types for the given post IDs.
*
* @var array<string>
*/
private $keyphrase_usage_per_type;
/**
* The constructor.
*
* @param array<string> $keyphrase_usage_count The keyphrase and the associated posts that use it.
* @param array<string> $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<string>
*/
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<string>
*/
public function to_legacy_array(): array {
return [
'keyword_usage' => $this->keyphrase_usage_count,
'keyword_usage_post_types' => $this->keyphrase_usage_per_type,
];
}
}
editors/application/site/website-information-repository.php 0000666 00000002714 15220430626 0020407 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Editors\Application\Site;
use Yoast\WP\SEO\Editors\Framework\Site\Post_Site_Information;
use Yoast\WP\SEO\Editors\Framework\Site\Term_Site_Information;
/**
* This class manages getting the two site information wrappers.
*
* @makePublic
*/
class Website_Information_Repository {
/**
* The post site information wrapper.
*
* @var Post_Site_Information
*/
private $post_site_information;
/**
* The term site information wrapper.
*
* @var Term_Site_Information
*/
private $term_site_information;
/**
* The constructor.
*
* @param Post_Site_Information $post_site_information The post specific wrapper.
* @param Term_Site_Information $term_site_information The term specific wrapper.
*/
public function __construct(
Post_Site_Information $post_site_information,
Term_Site_Information $term_site_information
) {
$this->post_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/integrations/integration-information-repository.php 0000666 00000002027 15220430626 0023027 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Editors\Application\Integrations;
use Yoast\WP\SEO\Editors\Domain\Integrations\Integration_Data_Provider_Interface;
/**
* The repository to get all enabled integrations.
*
* @makePublic
*/
class Integration_Information_Repository {
/**
* All plugin integrations.
*
* @var Integration_Data_Provider_Interface[]
*/
private $plugin_integrations;
/**
* The constructor.
*
* @param Integration_Data_Provider_Interface ...$plugin_integrations All integrations.
*/
public function __construct( Integration_Data_Provider_Interface ...$plugin_integrations ) {
$this->plugin_integrations = $plugin_integrations;
}
/**
* Returns the analysis list.
*
* @return array<array<string, bool>> 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/framework/cornerstone-content.php 0000666 00000002164 15220430626 0014743 0 ustar 00 <?php
namespace Yoast\WP\SEO\Editors\Framework;
use Yoast\WP\SEO\Editors\Domain\Analysis_Features\Analysis_Feature_Interface;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Describes if the Cornerstone content features is enabled.
*/
class Cornerstone_Content implements Analysis_Feature_Interface {
public const NAME = 'cornerstoneContent';
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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.php 0000666 00000002256 15220430626 0015166 0 ustar 00 <?php
namespace Yoast\WP\SEO\Editors\Framework;
use Yoast\WP\SEO\Editors\Domain\Analysis_Features\Analysis_Feature_Interface;
use Yoast\WP\SEO\Helpers\Language_Helper;
/**
* Describes if the word for recognition analysis is enabled
*/
class Word_Form_Recognition implements Analysis_Feature_Interface {
public const NAME = 'wordFormRecognition';
/**
* The language helper.
*
* @var Language_Helper
*/
private $language_helper;
/**
* The constructor.
*
* @param Language_Helper $language_helper The language helper.
*/
public function __construct( Language_Helper $language_helper ) {
$this->language_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/integrations/woocommerce.php 0000666 00000003077 15220430626 0015763 0 ustar 00 <?php
// @phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- This namespace should reflect the namespace of the original class.
namespace Yoast\WP\SEO\Editors\Framework\Integrations;
use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Editors\Domain\Integrations\Integration_Data_Provider_Interface;
/**
* Describes if the Woocommerce plugin is enabled.
*/
class WooCommerce implements Integration_Data_Provider_Interface {
/**
* The WooCommerce conditional.
*
* @var WooCommerce_Conditional
*/
private $woocommerce_conditional;
/**
* The constructor.
*
* @param WooCommerce_Conditional $woocommerce_conditional The WooCommerce conditional.
*/
public function __construct( WooCommerce_Conditional $woocommerce_conditional ) {
$this->woocommerce_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<string, bool> 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<string, bool> Returns the legacy key and if the feature is enabled.
*/
public function to_legacy_array(): array {
return [ 'isWooCommerceActive' => $this->is_enabled() ];
}
}
editors/framework/integrations/jetpack-markdown.php 0000666 00000003665 15220430626 0016710 0 ustar 00 <?php
// @phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- This namespace should reflect the namespace of the original class.
namespace Yoast\WP\SEO\Editors\Framework\Integrations;
use Jetpack;
use Yoast\WP\SEO\Editors\Domain\Integrations\Integration_Data_Provider_Interface;
/**
* Describes if the Jetpack markdown integration is enabled.
*/
class Jetpack_Markdown implements Integration_Data_Provider_Interface {
/**
* If the integration is activated.
*
* @return bool If the integration is activated.
*/
public function is_enabled(): bool {
return $this->is_markdown_enabled();
}
/**
* Return this object represented by a key value array.
*
* @return array<string, bool> 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<string, bool> 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/woocommerce-seo.php 0000666 00000003025 15220430626 0016540 0 ustar 00 <?php
// @phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- This namespace should reflect the namespace of the original class.
namespace Yoast\WP\SEO\Editors\Framework\Integrations;
use WPSEO_Addon_Manager;
use Yoast\WP\SEO\Editors\Domain\Integrations\Integration_Data_Provider_Interface;
/**
* Describes if the Woocommerce SEO addon is enabled.
*/
class WooCommerce_SEO implements Integration_Data_Provider_Interface {
/**
* The addon manager.
*
* @var WPSEO_Addon_Manager
*/
private $addon_manager;
/**
* The constructor.
*
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
*/
public function __construct( WPSEO_Addon_Manager $addon_manager ) {
$this->addon_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<string, bool> 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<string, bool> Returns the legacy key and if the feature is enabled.
*/
public function to_legacy_array(): array {
return [ 'isWooCommerceSeoActive' => $this->is_enabled() ];
}
}
editors/framework/integrations/semrush.php 0000666 00000005465 15220430626 0015135 0 ustar 00 <?php
// @phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- This namespace should reflect the namespace of the original class.
namespace Yoast\WP\SEO\Editors\Framework\Integrations;
use Yoast\WP\SEO\Config\SEMrush_Client;
use Yoast\WP\SEO\Editors\Domain\Integrations\Integration_Data_Provider_Interface;
use Yoast\WP\SEO\Exceptions\OAuth\Authentication_Failed_Exception;
use Yoast\WP\SEO\Exceptions\OAuth\Tokens\Empty_Property_Exception;
use Yoast\WP\SEO\Exceptions\OAuth\Tokens\Empty_Token_Exception;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Describes if the Semrush integration is enabled.
*/
class Semrush implements Integration_Data_Provider_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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<string, bool> 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<string, bool> 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/integrations/multilingual.php 0000666 00000005570 15220430626 0016152 0 ustar 00 <?php
// @phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- This namespace should reflect the namespace of the original class.
namespace Yoast\WP\SEO\Editors\Framework\Integrations;
use Yoast\WP\SEO\Conditionals\Third_Party\Polylang_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\TranslatePress_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\WPML_Conditional;
use Yoast\WP\SEO\Editors\Domain\Integrations\Integration_Data_Provider_Interface;
/**
* Describes if the Multilingual integration is enabled.
*/
class Multilingual implements Integration_Data_Provider_Interface {
/**
* The WPML conditional.
*
* @var WPML_Conditional
*/
private $wpml_conditional;
/**
* The Polylang conditional.
*
* @var Polylang_Conditional
*/
private $polylang_conditional;
/**
* The TranslatePress conditional.
*
* @var TranslatePress_Conditional
*/
private $translate_press_conditional;
/**
* The constructor.
*
* @param WPML_Conditional $wpml_conditional The wpml conditional.
* @param Polylang_Conditional $polylang_conditional The polylang conditional.
* @param TranslatePress_Conditional $translate_press_conditional The translate press conditional.
*/
public function __construct( WPML_Conditional $wpml_conditional, Polylang_Conditional $polylang_conditional, TranslatePress_Conditional $translate_press_conditional ) {
$this->wpml_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<string, bool> 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<string, bool> 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/wincher.php 0000666 00000004451 15220430626 0015100 0 ustar 00 <?php
// @phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- This namespace should reflect the namespace of the original class.
namespace Yoast\WP\SEO\Editors\Framework\Integrations;
use Yoast\WP\SEO\Editors\Domain\Integrations\Integration_Data_Provider_Interface;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Wincher_Helper;
/**
* Describes if the Wincher integration is enabled.
*/
class Wincher implements Integration_Data_Provider_Interface {
/**
* The Wincher helper.
*
* @var Wincher_Helper
*/
private $wincher_helper;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The constructor.
*
* @param Wincher_Helper $wincher_helper The Wincher helper.
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Wincher_Helper $wincher_helper, Options_Helper $options_helper ) {
$this->wincher_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<string, bool> 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<string, bool> 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 ),
];
}
}
integrations/blocks/breadcrumbs-block.php 0000666 00000006656 15220430626 0014642 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Blocks;
use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Presenters\Breadcrumbs_Presenter;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
/**
* Siblings block class
*/
class Breadcrumbs_Block extends Dynamic_Block_V3 {
/**
* The name of the block.
*
* @var string
*/
protected $block_name = 'breadcrumbs';
/**
* The editor script for the block.
*
* @var string
*/
protected $script = 'yoast-seo-dynamic-blocks';
/**
* The Meta_Tags_Context_Memoizer.
*
* @var Meta_Tags_Context_Memoizer
*/
private $context_memoizer;
/**
* The Replace vars helper.
*
* @var WPSEO_Replace_Vars
*/
private $replace_vars;
/**
* The helpers surface.
*
* @var Helpers_Surface
*/
private $helpers;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* Siblings_Block constructor.
*
* @param Meta_Tags_Context_Memoizer $context_memoizer The context.
* @param WPSEO_Replace_Vars $replace_vars The replace variable helper.
* @param Helpers_Surface $helpers The Helpers surface.
* @param Indexable_Repository $indexable_repository The indexable repository.
*/
public function __construct(
Meta_Tags_Context_Memoizer $context_memoizer,
WPSEO_Replace_Vars $replace_vars,
Helpers_Surface $helpers,
Indexable_Repository $indexable_repository
) {
$this->context_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<string, bool|string|int|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 '<div class="' . $class_name . '">' . $presenter->present() . '</div>';
}
}
integrations/academy-integration.php 0000666 00000011007 15220430626 0013712 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
/**
* Class Academy_Integration.
*/
class Academy_Integration implements Integration_Interface {
public const PAGE = 'wpseo_page_academy';
/**
* Holds the WPSEO_Admin_Asset_Manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $asset_manager;
/**
* Holds the Current_Page_Helper.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* Holds the Product_Helper.
*
* @var Product_Helper
*/
private $product_helper;
/**
* Holds the Short_Link_Helper.
*
* @var Short_Link_Helper
*/
private $shortlink_helper;
/**
* Constructs Academy_Integration.
*
* @param WPSEO_Admin_Asset_Manager $asset_manager The WPSEO_Admin_Asset_Manager.
* @param Current_Page_Helper $current_page_helper The Current_Page_Helper.
* @param Product_Helper $product_helper The Product_Helper.
* @param Short_Link_Helper $shortlink_helper The Short_Link_Helper.
*/
public function __construct(
WPSEO_Admin_Asset_Manager $asset_manager,
Current_Page_Helper $current_page_helper,
Product_Helper $product_helper,
Short_Link_Helper $shortlink_helper
) {
$this->asset_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 '<div id="yoast-seo-academy"></div>';
}
/**
* 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/exclude-attachment-post-type.php 0000666 00000001452 15220430626 0015512 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
use Yoast\WP\SEO\Conditionals\Attachment_Redirections_Enabled_Conditional;
/**
* Excludes Attachment post types from the indexable table.
*
* Posts with these post types will not be saved to the indexable table.
*/
class Exclude_Attachment_Post_Type extends Abstract_Exclude_Post_Type {
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Attachment_Redirections_Enabled_Conditional::class ];
}
/**
* 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.
*/
public function get_post_type() {
return [ 'attachment' ];
}
}
integrations/breadcrumbs-integration.php 0000666 00000004025 15220430626 0014602 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Presenters\Breadcrumbs_Presenter;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
/**
* Adds customizations to the front end for breadcrumbs.
*/
class Breadcrumbs_Integration implements Integration_Interface {
/**
* The breadcrumbs presenter.
*
* @var Breadcrumbs_Presenter
*/
private $presenter;
/**
* The meta tags context memoizer.
*
* @var Meta_Tags_Context_Memoizer
*/
private $context_memoizer;
/**
* Breadcrumbs integration constructor.
*
* @param Helpers_Surface $helpers The helpers.
* @param WPSEO_Replace_Vars $replace_vars The replace vars.
* @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer.
*/
public function __construct(
Helpers_Surface $helpers,
WPSEO_Replace_Vars $replace_vars,
Meta_Tags_Context_Memoizer $context_memoizer
) {
$this->context_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/settings-integration.php 0000666 00000110060 15220430626 0014146 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
use WP_Post;
use WP_Post_Type;
use WP_Taxonomy;
use WP_User;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Admin_Editor_Specific_Replace_Vars;
use WPSEO_Admin_Recommended_Replace_Vars;
use WPSEO_Option_Titles;
use WPSEO_Options;
use WPSEO_Replace_Vars;
use WPSEO_Shortlinker;
use WPSEO_Sitemaps_Router;
use Yoast\WP\SEO\Conditionals\Settings_Conditional;
use Yoast\WP\SEO\Config\Schema_Types;
use Yoast\WP\SEO\Content_Type_Visibility\Application\Content_Type_Visibility_Dismiss_Notifications;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Language_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Schema\Article_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Helpers\Woocommerce_Helper;
use Yoast\WP\SEO\Llms_Txt\Application\Configuration\Llms_Txt_Configuration;
use Yoast\WP\SEO\Llms_Txt\Application\Health_Check\File_Runner;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Manual_Post_Collection;
use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;
/**
* Class Settings_Integration.
*/
class Settings_Integration implements Integration_Interface {
public const PAGE = 'wpseo_page_settings';
/**
* Holds the included WordPress options.
*
* @var string[]
*/
public const WP_OPTIONS = [ 'blogdescription' ];
/**
* Holds the allowed option groups.
*
* @var array
*/
public const ALLOWED_OPTION_GROUPS = [ 'wpseo', 'wpseo_titles', 'wpseo_social', 'wpseo_llmstxt' ];
/**
* Holds the disallowed settings, per option group.
*
* @var array
*/
public const DISALLOWED_SETTINGS = [
'wpseo' => [
'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;
/**
* The manual post collection.
*
* @var Manual_Post_Collection
*/
private $manual_post_collection;
/**
* Runs the health check.
*
* @var File_Runner
*/
private $runner;
/**
* 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.
*/
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
) {
$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;
}
/**
* 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 '<div id="yoast-seo-settings"></div>';
}
/**
* 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 ),
];
}
/**
* 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<int, string> 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<int, string> $pages The existing pages.
* @param int $page_id The page id to check.
* @param string $key The option key name.
*
* @return array<int, string> 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<string, array<string, int|string>> 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<string> 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<string|int|bool> $defaults The settings defaults.
*
* @return array<string|int|bool> 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->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->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;
}
/**
* Gets the route from a name, rewrite and rest_base.
*
* @param string $name The name.
* @param array $rewrite The rewrite data.
* @param string $rest_base The rest base.
*
* @return string The route.
*/
protected function get_route( $name, $rewrite, $rest_base ) {
$route = $name;
if ( isset( $rewrite['slug'] ) ) {
$route = $rewrite['slug'];
}
if ( ! empty( $rest_base ) ) {
$route = $rest_base;
}
// Always strip leading slashes.
while ( \substr( $route, 0, 1 ) === '/' ) {
$route = \substr( $route, 1 );
}
return $route;
}
/**
* 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/watchers/addon-update-watcher.php 0000666 00000016215 15220430626 0015614 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Enables Yoast add-on auto updates when Yoast SEO is enabled and the other way around.
*
* Also removes the auto-update toggles from the Yoast SEO add-ons.
*/
class Addon_Update_Watcher implements Integration_Interface {
/**
* ID string used by WordPress to identify the free plugin.
*
* @var string
*/
public const WPSEO_FREE_PLUGIN_ID = 'wordpress-seo/wp-seo.php';
/**
* A list of Yoast add-on identifiers.
*
* @var string[]
*/
public const ADD_ON_PLUGIN_FILES = [
'wordpress-seo-premium/wp-seo-premium.php',
'wpseo-video/video-seo.php',
'wpseo-local/local-seo.php', // When installing Local through a released zip, the path is different from the path on a dev environment.
'wpseo-woocommerce/wpseo-woocommerce.php',
'wpseo-news/wpseo-news.php',
'acf-content-analysis-for-yoast-seo/yoast-acf-analysis.php', // When installing ACF for Yoast through a released zip, the path is different from the path on a dev environment.
];
/**
* Registers the hooks.
*
* @return void
*/
public function register_hooks() {
\add_action( 'add_site_option_auto_update_plugins', [ $this, 'call_toggle_auto_updates_with_empty_array' ], 10, 2 );
\add_action( 'update_site_option_auto_update_plugins', [ $this, 'toggle_auto_updates_for_add_ons' ], 10, 3 );
\add_filter( 'plugin_auto_update_setting_html', [ $this, 'replace_auto_update_toggles_of_addons' ], 10, 2 );
\add_action( 'activated_plugin', [ $this, 'maybe_toggle_auto_updates_for_new_install' ] );
}
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return string[] The conditionals.
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Replaces the auto-update toggle links for the Yoast add-ons
* with a text explaining that toggling the Yoast SEO auto-update setting
* automatically toggles the one for the setting for the add-ons as well.
*
* @param string $old_html The old HTML.
* @param string $plugin The plugin.
*
* @return string The new HTML, with the auto-update toggle link replaced.
*/
public function replace_auto_update_toggles_of_addons( $old_html, $plugin ) {
if ( ! \is_string( $old_html ) ) {
return $old_html;
}
$not_a_yoast_addon = ! \in_array( $plugin, self::ADD_ON_PLUGIN_FILES, true );
if ( $not_a_yoast_addon ) {
return $old_html;
}
$auto_updated_plugins = \get_site_option( 'auto_update_plugins' );
if ( $this->are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $auto_updated_plugins ) ) {
return \sprintf(
'<em>%s</em>',
\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(
'<em>%s</em>',
\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-type-archive-watcher.php 0000666 00000007735 15220430626 0020572 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Post type archive watcher to save the meta data to an Indexable.
*
* Watches the home page options to save the meta information when updated.
*/
class Indexable_Post_Type_Archive_Watcher implements Integration_Interface {
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $repository;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* The indexable builder.
*
* @var Indexable_Builder
*/
protected $builder;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Migrations_Conditional::class ];
}
/**
* Indexable_Post_Type_Archive_Watcher constructor.
*
* @param Indexable_Repository $repository The repository to use.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Indexable_Builder $builder The post builder to use.
*/
public function __construct(
Indexable_Repository $repository,
Indexable_Helper $indexable_helper,
Indexable_Builder $builder
) {
$this->repository = $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/indexable-static-home-page-watcher.php 0000666 00000004315 15220430626 0020325 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Watcher that checks for changes in the page used as homepage.
*
* Watches the static homepage option and updates the permalinks accordingly.
*/
class Indexable_Static_Home_Page_Watcher implements Integration_Interface {
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $repository;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Indexable_Static_Home_Page_Watcher constructor.
*
* @codeCoverageIgnore
*
* @param Indexable_Repository $repository The repository to use.
*/
public function __construct( Indexable_Repository $repository ) {
$this->repository = $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/search-engines-discouraged-watcher.php 0000666 00000015752 15220430626 0020436 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Notification_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Search_Engines_Discouraged_Presenter;
use Yoast_Notification;
use Yoast_Notification_Center;
/**
* Shows a notification for users who have access for robots disabled.
*/
class Search_Engines_Discouraged_Watcher implements Integration_Interface {
use No_Conditionals;
/**
* The notification ID.
*/
public const NOTIFICATION_ID = 'wpseo-search-engines-discouraged';
/**
* The Yoast notification center.
*
* @var Yoast_Notification_Center
*/
protected $notification_center;
/**
* The notification helper.
*
* @var Notification_Helper
*/
protected $notification_helper;
/**
* The search engines discouraged presenter.
*
* @var Search_Engines_Discouraged_Presenter
*/
protected $presenter;
/**
* The current page helper.
*
* @var Current_Page_Helper
*/
protected $current_page_helper;
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* The capability helper.
*
* @var Capability_Helper
*/
protected $capability_helper;
/**
* Search_Engines_Discouraged_Watcher constructor.
*
* @param Yoast_Notification_Center $notification_center The notification center.
* @param Notification_Helper $notification_helper The notification helper.
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Options_Helper $options_helper The options helper.
* @param Capability_Helper $capability_helper The capability helper.
*/
public function __construct(
Yoast_Notification_Center $notification_center,
Notification_Helper $notification_helper,
Current_Page_Helper $current_page_helper,
Options_Helper $options_helper,
Capability_Helper $capability_helper
) {
$this->notification_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(
'<div id="robotsmessage" class="notice notice-error">%1$s</div>',
// 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-attachment-watcher.php 0000666 00000011421 15220430626 0017322 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Attachment_Cleanup_Helper;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification_Center;
/**
* Watches the disable-attachment key in wpseo_titles, in order to clear the permalink of the category indexables.
*/
class Indexable_Attachment_Watcher implements Integration_Interface {
/**
* The indexing helper.
*
* @var Indexing_Helper
*/
protected $indexing_helper;
/**
* The attachment cleanup helper.
*
* @var Attachment_Cleanup_Helper
*/
protected $attachment_cleanup;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
protected $indexable_helper;
/**
* The notifications center.
*
* @var Yoast_Notification_Center
*/
private $notification_center;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array<string> 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-category-permalink-watcher.php 0000666 00000003364 15220430626 0020776 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use WPSEO_Utils;
use Yoast\WP\SEO\Config\Indexing_Reasons;
/**
* Watches the stripcategorybase key in wpseo_titles, in order to clear the permalink of the category indexables.
*/
class Indexable_Category_Permalink_Watcher extends Indexable_Permalink_Watcher {
/**
* 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 stripcategorybase key in wpseo_titles has a change in value, and if so,
* clears the permalink for category indexables.
*
* @param array $old_value The old value of the wpseo_titles option.
* @param array $new_value The new value of the wpseo_titles option.
*
* @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['stripcategorybase'] ) && ! isset( $new_value['stripcategorybase'] ) ) {
return;
}
// If a new value has been set for 'stripcategorybase', clear the category permalinks.
if ( $old_value['stripcategorybase'] !== $new_value['stripcategorybase'] ) {
$this->indexable_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-ancestor-watcher.php 0000666 00000020337 15220430626 0017016 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Permalink_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Ancestor watcher to update the ancestor's children.
*
* Updates its children's permalink when the ancestor itself is updated.
*/
class Indexable_Ancestor_Watcher implements Integration_Interface {
/**
* Represents the indexable repository.
*
* @var Indexable_Repository
*/
protected $indexable_repository;
/**
* Represents the indexable hierarchy builder.
*
* @var Indexable_Hierarchy_Builder
*/
protected $indexable_hierarchy_builder;
/**
* Represents the indexable hierarchy repository.
*
* @var Indexable_Hierarchy_Repository
*/
protected $indexable_hierarchy_repository;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* Represents the permalink helper.
*
* @var Permalink_Helper
*/
protected $permalink_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type_helper;
/**
* Sets the needed dependencies.
*
* @param Indexable_Repository $indexable_repository The indexable repository.
* @param Indexable_Hierarchy_Builder $indexable_hierarchy_builder The indexable hierarchy builder.
* @param Indexable_Hierarchy_Repository $indexable_hierarchy_repository The indexable hierarchy repository.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Permalink_Helper $permalink_helper The permalink helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
*/
public function __construct(
Indexable_Repository $indexable_repository,
Indexable_Hierarchy_Builder $indexable_hierarchy_builder,
Indexable_Hierarchy_Repository $indexable_hierarchy_repository,
Indexable_Helper $indexable_helper,
Permalink_Helper $permalink_helper,
Post_Type_Helper $post_type_helper
) {
$this->indexable_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<Migrations_Conditional>
*/
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<Indexable> $child_indexables The already known child indexables.
*
* @return array<Indexable> 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<Indexable> $child_indexables The child indexables.
*
* @return array<int> 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-author-watcher.php 0000666 00000007010 15220430626 0016473 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Watches an Author to save the meta information to an Indexable when updated.
*/
class Indexable_Author_Watcher implements Integration_Interface {
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $repository;
/**
* The indexable builder.
*
* @var Indexable_Builder
*/
protected $builder;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Migrations_Conditional::class ];
}
/**
* Indexable_Author_Watcher constructor.
*
* @param Indexable_Repository $repository The repository to use.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Indexable_Builder $builder The builder to use.
*/
public function __construct(
Indexable_Repository $repository,
Indexable_Helper $indexable_helper,
Indexable_Builder $builder
) {
$this->repository = $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/indexable-homeurl-watcher.php 0000666 00000005410 15220430626 0016646 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use WP_CLI;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Home url option watcher.
*
* Handles updates to the home URL option for the Indexables table.
*/
class Indexable_HomeUrl_Watcher implements Integration_Interface {
/**
* Represents the options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
protected $indexable_helper;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Migrations_Conditional::class ];
}
/**
* Indexable_HomeUrl_Watcher constructor.
*
* @param Post_Type_Helper $post_type The post type helper.
* @param Options_Helper $options The options helper.
* @param Indexable_Helper $indexable The indexable helper.
*/
public function __construct( Post_Type_Helper $post_type, Options_Helper $options, Indexable_Helper $indexable ) {
$this->post_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/indexable-permalink-watcher.php 0000666 00000017404 15220430626 0017163 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* WordPress Permalink structure watcher.
*
* Handles updates to the permalink_structure for the Indexables table.
*/
class Indexable_Permalink_Watcher implements Integration_Interface {
/**
* Represents the options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* The taxonomy helper.
*
* @var Taxonomy_Helper
*/
protected $taxonomy_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
protected $indexable_helper;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Migrations_Conditional::class ];
}
/**
* Indexable_Permalink_Watcher constructor.
*
* @param Post_Type_Helper $post_type The post type helper.
* @param Options_Helper $options The options helper.
* @param Indexable_Helper $indexable The indexable helper.
* @param Taxonomy_Helper $taxonomy_helper The taxonomy helper.
*/
public function __construct( Post_Type_Helper $post_type, Options_Helper $options, Indexable_Helper $indexable, Taxonomy_Helper $taxonomy_helper ) {
$this->post_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-post-type-change-watcher.php 0000666 00000011776 15220430626 0020376 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification_Center;
/**
* Post type change watcher.
*/
class Indexable_Post_Type_Change_Watcher implements Integration_Interface {
/**
* The indexing helper.
*
* @var Indexing_Helper
*/
protected $indexing_helper;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options;
/**
* Holds the Post_Type_Helper instance.
*
* @var Post_Type_Helper
*/
private $post_type_helper;
/**
* The notifications center.
*
* @var Yoast_Notification_Center
*/
private $notification_center;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array<string> 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/watchers/auto-update-watcher.php 0000666 00000002772 15220430626 0015502 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Watchers;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification_Center;
/**
* Shows a notification for users who have WordPress auto updates enabled but not Yoast SEO auto updates.
*/
class Auto_Update_Watcher implements Integration_Interface {
use No_Conditionals;
/**
* The notification ID.
*/
public const NOTIFICATION_ID = 'wpseo-auto-update';
/**
* The Yoast notification center.
*
* @var Yoast_Notification_Center
*/
protected $notification_center;
/**
* Auto_Update constructor.
*
* @param Yoast_Notification_Center $notification_center The notification center.
*/
public function __construct( Yoast_Notification_Center $notification_center ) {
$this->notification_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/uninstall-integration.php 0000666 00000001756 15220430626 0014332 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
/**
* Class to manage the integration with the WP uninstall flow.
*/
class Uninstall_Integration implements Integration_Interface {
use No_Conditionals;
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
\add_action( 'uninstall_' . \WPSEO_BASENAME, [ $this, 'wpseo_uninstall' ] );
}
/**
* Performs all necessary actions that should happen upon plugin uninstall.
*
* @return void
*/
public function wpseo_uninstall() {
$this->clear_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/front-end/backwards-compatibility.php 0000666 00000003301 15220430626 0016470 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Front_End;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Adds actions that were previously called and are now deprecated.
*/
class Backwards_Compatibility implements Integration_Interface {
/**
* Represents the options helper.
*
* @var Options_Helper
*/
protected $options;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Front_End_Conditional::class ];
}
/**
* Backwards_Compatibility constructor
*
* @param Options_Helper $options The options helper.
*/
public function __construct( Options_Helper $options ) {
$this->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/handle-404.php 0000666 00000005201 15220430626 0013421 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Front_End;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Wrappers\WP_Query_Wrapper;
/**
* Handles intercepting requests.
*/
class Handle_404 implements Integration_Interface {
/**
* The WP Query wrapper.
*
* @var WP_Query_Wrapper
*/
private $query_wrapper;
/**
* 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_filter( 'pre_handle_404', [ $this, 'handle_404' ] );
}
/**
* Handle_404 constructor.
*
* @codeCoverageIgnore Handles dependencies.
*
* @param WP_Query_Wrapper $query_wrapper The query wrapper.
*/
public function __construct( WP_Query_Wrapper $query_wrapper ) {
$this->query_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/wp-robots-integration.php 0000666 00000012742 15220430626 0016146 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Front_End;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Conditionals\WP_Robots_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Presenters\Robots_Presenter;
/**
* Class WP_Robots_Integration
*
* @package Yoast\WP\SEO\Integrations\Front_End
*/
class WP_Robots_Integration implements Integration_Interface {
/**
* The meta tags context memoizer.
*
* @var Meta_Tags_Context_Memoizer
*/
protected $context_memoizer;
/**
* Sets the dependencies for this integration.
*
* @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer.
*/
public function __construct( Meta_Tags_Context_Memoizer $context_memoizer ) {
$this->context_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/cleanup-integration.php 0000666 00000023335 15220430626 0013745 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
use Closure;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Repositories\Indexable_Cleanup_Repository;
/**
* Adds cleanup hooks.
*/
class Cleanup_Integration implements Integration_Interface {
/**
* Identifier used to determine the current task.
*/
public const CURRENT_TASK_OPTION = 'wpseo-cleanup-current-task';
/**
* Identifier for the cron job.
*/
public const CRON_HOOK = 'wpseo_cleanup_cron';
/**
* Identifier for starting the cleanup.
*/
public const START_HOOK = 'wpseo_start_cleanup_indexables';
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* The cleanup repository.
*
* @var Indexable_Cleanup_Repository
*/
private $cleanup_repository;
/**
* The constructor.
*
* @param Indexable_Cleanup_Repository $cleanup_repository The cleanup repository.
* @param Indexable_Helper $indexable_helper The indexable helper.
*/
public function __construct(
Indexable_Cleanup_Repository $cleanup_repository,
Indexable_Helper $indexable_helper
) {
$this->cleanup_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<string> 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/abstract-exclude-post-type.php 0000666 00000002216 15220430626 0015164 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
/**
* Abstract class for excluding certain post types from being indexed.
*/
abstract class Abstract_Exclude_Post_Type implements Integration_Interface {
/**
* Initializes the integration.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'wpseo_indexable_excluded_post_types', [ $this, 'exclude_post_types' ] );
}
/**
* Exclude the post type from the indexable table.
*
* @param array $excluded_post_types The excluded post types.
*
* @return array The excluded post types, including the specific post type.
*/
public function exclude_post_types( $excluded_post_types ) {
return \array_merge( $excluded_post_types, $this->get_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/alerts/abstract-dismissable-alert.php 0000666 00000001645 15220430626 0016474 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Alerts;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Dismissable_Alert class.
*/
abstract class Abstract_Dismissable_Alert implements Integration_Interface {
/**
* Holds the alert identifier.
*
* @var string
*/
protected $alert_identifier;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [];
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_filter( 'wpseo_allowed_dismissable_alerts', [ $this, 'register_dismissable_alert' ] );
}
/**
* Registers the dismissable alert.
*
* @param string[] $allowed_dismissable_alerts The allowed dismissable alerts.
*
* @return string[] The allowed dismissable alerts.
*/
public function register_dismissable_alert( $allowed_dismissable_alerts ) {
$allowed_dismissable_alerts[] = $this->alert_identifier;
return $allowed_dismissable_alerts;
}
}
integrations/alerts/trustpilot-review-notification.php 0000666 00000000461 15220430626 0017476 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Alerts;
/**
* Trustpilot_Review_Notification class.
*/
class Trustpilot_Review_Notification extends Abstract_Dismissable_Alert {
/**
* Holds the alert identifier.
*
* @var string
*/
protected $alert_identifier = 'trustpilot-review-notification';
}
integrations/integration-interface.php 0000666 00000000652 15220430626 0014253 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
use Yoast\WP\SEO\Loadable_Interface;
/**
* An interface for registering integrations with WordPress.
*
* @codeCoverageIgnore It represents an interface.
*/
interface Integration_Interface extends Loadable_Interface {
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks();
}
integrations/exclude-oembed-cache-post-type.php 0000666 00000001460 15220430626 0015655 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
/**
* Excludes certain oEmbed Cache-specific post types from the indexable table.
*
* Posts with these post types will not be saved to the indexable table.
*/
class Exclude_Oembed_Cache_Post_Type extends Abstract_Exclude_Post_Type {
/**
* This integration is only active when the database migrations have been run.
*
* @return string[] The conditionals.
*/
public static function get_conditionals() {
return [ Migrations_Conditional::class ];
}
/**
* 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.
*/
public function get_post_type() {
return [ 'oembed_cache' ];
}
}
integrations/primary-category.php 0000666 00000003716 15220430626 0013274 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations;
use stdClass;
use WP_Error;
use WP_Post;
use WPSEO_Primary_Term;
use Yoast\WP\SEO\Conditionals\Primary_Category_Conditional;
/**
* Adds customizations to the front end for the primary category.
*/
class Primary_Category implements Integration_Interface {
/**
* Returns the conditionals based on which this loadable should be active.
*
* In this case only when on the frontend, the post overview, post edit or new post admin page.
*
* @return array The conditionals.
*/
public static function get_conditionals() {
return [ Primary_Category_Conditional::class ];
}
/**
* Registers a filter to change a post's primary category.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'post_link_category', [ $this, 'post_link_category' ], 10, 3 );
}
/**
* Filters post_link_category to change the category to the chosen category by the user.
*
* @param stdClass $category The category that is now used for the post link.
* @param array|null $categories This parameter is not used.
* @param WP_Post|null $post The post in question.
*
* @return array|object|WP_Error|null The category we want to use for the post link.
*/
public function post_link_category( $category, $categories = null, $post = null ) {
$post = \get_post( $post );
if ( $post === null ) {
return $category;
}
$primary_category = $this->get_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/admin/installation-success-integration.php 0000666 00000010063 15220430626 0017547 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Installation_Success_Integration class
*/
class Installation_Success_Integration implements Integration_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* The product helper.
*
* @var Product_Helper
*/
protected $product_helper;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Installation_Success_Integration constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param Product_Helper $product_helper The product helper.
*/
public function __construct( Options_Helper $options_helper, Product_Helper $product_helper ) {
$this->options_helper = $options_helper;
$this->product_helper = $product_helper;
}
/**
* {@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( 'tailwind' );
$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' ) ),
]
);
}
/**
* Renders the installation success page.
*
* @return void
*/
public function render_page() {
echo '<div id="wpseo-installation-successful-free" class="yoast"></div>';
}
/**
* Wrap the `exit` function to make unit testing easier.
*
* @return void
*/
public function terminate_execution() {
exit;
}
}
integrations/admin/integrations-page.php 0000666 00000023470 15220430626 0014505 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use Easy_Digital_Downloads;
use SeriouslySimplePodcasting\Integrations\Yoast\Schema\PodcastEpisode;
use TEC\Events\Integrations\Plugins\WordPress_SEO\Events_Schema;
use WP_Recipe_Maker;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Jetpack_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Activated_Conditional;
use Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints\Site_Kit_Consent_Management_Endpoint;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Woocommerce_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Integrations_Page class
*/
class Integrations_Page implements Integration_Interface {
/**
* The Woocommerce helper.
*
* @var Woocommerce_Helper
*/
private $woocommerce_helper;
/**
* The admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $admin_asset_manager;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The elementor conditional.
*
* @var Elementor_Activated_Conditional
*/
private $elementor_conditional;
/**
* The jetpack conditional.
*
* @var Jetpack_Conditional
*/
private $jetpack_conditional;
/**
* The site kit integration configuration data.
*
* @var Site_Kit
*/
private $site_kit_integration_data;
/**
* The site kit consent management endpoint.
*
* @var Site_Kit_Consent_Management_Endpoint
*/
private $site_kit_consent_management_endpoint;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Workouts_Integration constructor.
*
* @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
* @param Options_Helper $options_helper The options helper.
* @param Woocommerce_Helper $woocommerce_helper The WooCommerce helper.
* @param Elementor_Activated_Conditional $elementor_conditional The elementor conditional.
* @param Jetpack_Conditional $jetpack_conditional The Jetpack conditional.
* @param Site_Kit $site_kit_integration_data The site kit integration
* configuration data.
* @param Site_Kit_Consent_Management_Endpoint $site_kit_consent_management_endpoint The site kit consent
* management endpoint.
*/
public function __construct(
WPSEO_Admin_Asset_Manager $admin_asset_manager,
Options_Helper $options_helper,
Woocommerce_Helper $woocommerce_helper,
Elementor_Activated_Conditional $elementor_conditional,
Jetpack_Conditional $jetpack_conditional,
Site_Kit $site_kit_integration_data,
Site_Kit_Consent_Management_Endpoint $site_kit_consent_management_endpoint
) {
$this->admin_asset_manager = $admin_asset_manager;
$this->options_helper = $options_helper;
$this->woocommerce_helper = $woocommerce_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;
}
/**
* {@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' );
$woocommerce_seo_file = 'wpseo-woocommerce/wpseo-woocommerce.php';
$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';
$addon_manager = new WPSEO_Addon_Manager();
$woocommerce_seo_installed = $addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG );
$woocommerce_seo_active = \is_plugin_active( $woocommerce_seo_file );
$woocommerce_active = $this->woocommerce_helper->is_active();
$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 );
$edd_active = \class_exists( Easy_Digital_Downloads::class );
$old_algolia_active = \is_plugin_active( $old_algolia_file );
$tec_active = \class_exists( Events_Schema::class );
$ssp_active = \class_exists( PodcastEpisode::class );
$wp_recipe_maker_active = \class_exists( WP_Recipe_Maker::class );
$mastodon_active = $this->is_mastodon_active();
$woocommerce_seo_activate_url = \wp_nonce_url(
\self_admin_url( 'plugins.php?action=activate&plugin=' . $woocommerce_seo_file ),
'activate-plugin_' . $woocommerce_seo_file
);
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(),
]
);
}
/**
* Renders the target for the React to mount to.
*
* @return void
*/
public function render_target() {
?>
<div class="wrap yoast wpseo-admin-page page-wpseo">
<div class="wp-header-end" style="height: 0; width: 0;"></div>
<div id="wpseo-integrations"></div>
</div>
<?php
}
/**
* Checks whether the Mastodon profile field has been filled in.
*
* @return bool
*/
private function is_mastodon_active() {
return \apply_filters( 'wpseo_mastodon_active', false );
}
}
integrations/admin/workouts-integration.php 0000666 00000026062 15220430626 0015303 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Notice_Presenter;
/**
* WorkoutsIntegration class
*/
class Workouts_Integration implements Integration_Interface {
/**
* The admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $admin_asset_manager;
/**
* The addon manager.
*
* @var WPSEO_Addon_Manager
*/
private $addon_manager;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The product helper.
*
* @var Product_Helper
*/
private $product_helper;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Workouts_Integration constructor.
*
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
* @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
* @param Options_Helper $options_helper The options helper.
* @param Product_Helper $product_helper The product helper.
*/
public function __construct(
WPSEO_Addon_Manager $addon_manager,
WPSEO_Admin_Asset_Manager $admin_asset_manager,
Options_Helper $options_helper,
Product_Helper $product_helper
) {
$this->addon_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' ) . ' <span class="yoast-badge yoast-premium-badge"></span>',
'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 '<div id="wpseo-workouts-container-free" class="yoast"></div>';
}
/**
* 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 = '<a class="yoast-button yoast-button-upsell yoast-button--small" href="' . \esc_url( $url ) . '" target="_blank">'
. \esc_html__( 'Renew your subscription', 'wordpress-seo' )
/* translators: Hidden accessibility text. */
. '<span class="screen-reader-text">' . \__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>'
. '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>'
. '</a>';
}
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',
'<a href="' . \esc_url( $url ) . '">',
'</a>'
);
$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',
'<a href="' . \esc_url( $url ) . '">',
'</a>'
);
$button = '<a class="yoast-button yoast-button--primary yoast-button--small" href="' . \esc_url( $url_button ) . '" target="_blank">'
. \esc_html__( 'Get help activating your subscription', 'wordpress-seo' )
/* translators: Hidden accessibility text. */
. '<span class="screen-reader-text">' . \__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>'
. '</a>';
}
$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/old-configuration-integration.php 0000666 00000003225 15220430626 0017025 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Old_Configuration_Integration class
*/
class Old_Configuration_Integration implements Integration_Interface {
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_filter( 'admin_menu', [ $this, 'add_submenu_page' ], 11 );
\add_action( 'admin_init', [ $this, 'redirect_to_new_configuration' ] );
}
/**
* Adds the old configuration 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(
'',
\__( 'Old Configuration Wizard', 'wordpress-seo' ),
'',
'manage_options',
'wpseo_configurator',
[ $this, 'render_page' ]
);
return $submenu_pages;
}
/**
* Renders the old configuration page.
*
* @return void
*/
public function render_page() {
// This page is never to be displayed.
}
/**
* Redirects from the old configuration page to the new configuration page.
*
* @return void
*/
public function redirect_to_new_configuration() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Data is not processed or saved.
if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_configurator' ) {
return;
}
$redirect_url = 'admin.php?page=wpseo_dashboard#/first-time-configuration';
\wp_safe_redirect( \admin_url( $redirect_url ), 302, 'Yoast SEO' );
exit;
}
}
integrations/admin/indexing-notification-integration.php 0000666 00000015710 15220430626 0017675 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use WPSEO_Addon_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Environment_Helper;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Helpers\Notification_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Indexing_Failed_Notification_Presenter;
use Yoast\WP\SEO\Presenters\Admin\Indexing_Notification_Presenter;
use Yoast_Notification;
use Yoast_Notification_Center;
/**
* Class Indexing_Notification_Integration.
*
* @package Yoast\WP\SEO\Integrations\Admin
*/
class Indexing_Notification_Integration implements Integration_Interface {
/**
* The notification ID.
*/
public const NOTIFICATION_ID = 'wpseo-reindex';
/**
* The Yoast notification center.
*
* @var Yoast_Notification_Center
*/
protected $notification_center;
/**
* The product helper.
*
* @var Product_Helper
*/
protected $product_helper;
/**
* The current page helper.
*
* @var Current_Page_Helper
*/
protected $page_helper;
/**
* The short link helper.
*
* @var Short_Link_Helper
*/
protected $short_link_helper;
/**
* The notification helper.
*
* @var Notification_Helper
*/
protected $notification_helper;
/**
* The indexing helper.
*
* @var Indexing_Helper
*/
protected $indexing_helper;
/**
* The Addon Manager.
*
* @var WPSEO_Addon_Manager
*/
protected $addon_manager;
/**
* The Environment Helper.
*
* @var Environment_Helper
*/
protected $environment_helper;
/**
* Indexing_Notification_Integration constructor.
*
* @param Yoast_Notification_Center $notification_center The notification center.
* @param Product_Helper $product_helper The product helper.
* @param Current_Page_Helper $page_helper The current page helper.
* @param Short_Link_Helper $short_link_helper The short link helper.
* @param Notification_Helper $notification_helper The notification helper.
* @param Indexing_Helper $indexing_helper The indexing helper.
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
* @param Environment_Helper $environment_helper The environment helper.
*/
public function __construct(
Yoast_Notification_Center $notification_center,
Product_Helper $product_helper,
Current_Page_Helper $page_helper,
Short_Link_Helper $short_link_helper,
Notification_Helper $notification_helper,
Indexing_Helper $indexing_helper,
WPSEO_Addon_Manager $addon_manager,
Environment_Helper $environment_helper
) {
$this->notification_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/deactivated-premium-integration.php 0000666 00000010331 15220430626 0017327 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Non_Multisite_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Notice_Presenter;
/**
* Deactivated_Premium_Integration class
*/
class Deactivated_Premium_Integration implements Integration_Interface {
/**
* The options' helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $admin_asset_manager;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class, Non_Multisite_Conditional::class ];
}
/**
* First_Time_Configuration_Notice_Integration constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
*/
public function __construct(
Options_Helper $options_helper,
WPSEO_Admin_Asset_Manager $admin_asset_manager
) {
$this->options_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',
'<a href="' . \esc_url(
\wp_nonce_url(
\self_admin_url( 'plugins.php?action=activate&plugin=' . $premium_file ),
'activate-plugin_' . $premium_file
)
) . '">',
'</a>'
);
// 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 "<script>
function dismiss_premium_deactivated_notice(){
var data = {
'action': 'dismiss_premium_deactivated_notice',
};
jQuery( '#yoast-premium-deactivated-notice' ).hide();
jQuery.post( ajaxurl, data, function( response ) {});
}
jQuery( document ).ready( function() {
jQuery( 'body' ).on( 'click', '#yoast-premium-deactivated-notice .notice-dismiss', function() {
dismiss_premium_deactivated_notice();
} );
} );
</script>";
}
}
/**
* 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/import-integration.php 0000666 00000021206 15220430626 0014713 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Import_Tool_Selected_Conditional;
use Yoast\WP\SEO\Conditionals\Yoast_Tools_Page_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter;
use Yoast\WP\SEO\Routes\Importing_Route;
use Yoast\WP\SEO\Services\Importing\Importable_Detector_Service;
/**
* Loads import script when on the Tool's page.
*/
class Import_Integration implements Integration_Interface {
/**
* Contains the asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
protected $asset_manager;
/**
* The Importable Detector service.
*
* @var Importable_Detector_Service
*/
protected $importable_detector;
/**
* The Importing Route class.
*
* @var Importing_Route
*/
protected $importing_route;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [
Import_Tool_Selected_Conditional::class,
Yoast_Tools_Page_Conditional::class,
];
}
/**
* Import Integration constructor.
*
* @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager.
* @param Importable_Detector_Service $importable_detector The importable detector.
* @param Importing_Route $importing_route The importing route.
*/
public function __construct(
WPSEO_Admin_Asset_Manager $asset_manager,
Importable_Detector_Service $importable_detector,
Importing_Route $importing_route
) {
$this->asset_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 .= '<br/>';
$content .= '<ol><li>';
$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 .= '</li>';
$content .= '<li>';
$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 .= '</li></ol>';
$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 .= '<br/><br/>';
$content .= \esc_html( '%s' );
$import_failure_alert = new Alert_Presenter( $content, 'error' );
return $import_failure_alert->present();
}
}
integrations/admin/migration-error-integration.php 0000666 00000002543 15220430626 0016524 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Config\Migration_Status;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Migration_Error_Presenter;
/**
* Migration_Error_Integration class.
*/
class Migration_Error_Integration implements Integration_Interface {
/**
* The migration status object.
*
* @var Migration_Status
*/
protected $migration_status;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Migration_Error_Integration constructor.
*
* @param Migration_Status $migration_status The migration status object.
*/
public function __construct( Migration_Status $migration_status ) {
$this->migration_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/activation-cleanup-integration.php 0000666 00000003545 15220430626 0017175 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* This integration registers a run of the cleanup routine whenever the plugin is activated.
*/
class Activation_Cleanup_Integration implements Integration_Interface {
use No_Conditionals;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
protected $indexable_helper;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Activation_Cleanup_Integration constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param Indexable_Helper $indexable_helper The indexable helper.
*/
public function __construct( Options_Helper $options_helper, Indexable_Helper $indexable_helper ) {
$this->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/background-indexing-integration.php 0000666 00000025403 15220430626 0017326 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Indexing_Complete_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexation_Action_Interface;
use Yoast\WP\SEO\Conditionals\Get_Request_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\WP_CRON_Enabled_Conditional;
use Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Class Background_Indexing_Integration.
*
* @package Yoast\WP\SEO\Integrations\Admin
*/
class Background_Indexing_Integration implements Integration_Interface {
/**
* Represents the indexing completed action.
*
* @var Indexable_Indexing_Complete_Action
*/
protected $complete_indexation_action;
/**
* Represents the indexing helper.
*
* @var Indexing_Helper
*/
protected $indexing_helper;
/**
* An object that checks if we are on the Yoast admin or on the dashboard page.
*
* @var Yoast_Admin_And_Dashboard_Conditional
*/
protected $yoast_admin_and_dashboard_conditional;
/**
* All available indexing actions.
*
* @var Indexation_Action_Interface[]
*/
protected $indexing_actions;
/**
* An object that checks if we are handling a GET request.
*
* @var Get_Request_Conditional
*/
private $get_request_conditional;
/**
* An object that checks if WP_CRON is enabled.
*
* @var WP_CRON_Enabled_Conditional
*/
private $wp_cron_enabled_conditional;
/**
* The indexable helper
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* Shutdown_Indexing_Integration constructor.
*
* @param Indexable_Indexing_Complete_Action $complete_indexation_action The complete indexing action.
* @param Indexing_Helper $indexing_helper The indexing helper.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Yoast_Admin_And_Dashboard_Conditional $yoast_admin_and_dashboard_conditional An object that checks if we are on the Yoast admin or on the dashboard page.
* @param Get_Request_Conditional $get_request_conditional An object that checks if we are handling a GET request.
* @param WP_CRON_Enabled_Conditional $wp_cron_enabled_conditional An object that checks if WP_CRON is enabled.
* @param Indexation_Action_Interface ...$indexing_actions A list of all available indexing actions.
*/
public function __construct(
Indexable_Indexing_Complete_Action $complete_indexation_action,
Indexing_Helper $indexing_helper,
Indexable_Helper $indexable_helper,
Yoast_Admin_And_Dashboard_Conditional $yoast_admin_and_dashboard_conditional,
Get_Request_Conditional $get_request_conditional,
WP_CRON_Enabled_Conditional $wp_cron_enabled_conditional,
Indexation_Action_Interface ...$indexing_actions
) {
$this->indexing_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/addon-installation/dialog-integration.php 0000666 00000006661 15220430626 0020434 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Discussed in Tech Council, a better solution is being worked on.
namespace Yoast\WP\SEO\Integrations\Admin\Addon_Installation;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional;
use Yoast\WP\SEO\Conditionals\Admin\Licenses_Page_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Represents the Addon installation feature.
*/
class Dialog_Integration implements Integration_Interface {
/**
* The addon manager.
*
* @var WPSEO_Addon_Manager
*/
protected $addon_manager;
/**
* The addons.
*
* @var array
*/
protected $owned_addons;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [
Admin_Conditional::class,
Licenses_Page_Conditional::class,
Addon_Installation_Conditional::class,
];
}
/**
* Addon_Installation constructor.
*
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
*/
public function __construct( WPSEO_Addon_Manager $addon_manager ) {
$this->addon_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 '<div class="notice notice-warning"><p>'
. \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'
)
. '</p></div>';
}
/**
* 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.php 0000666 00000014010 15220430626 0021661 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Discussed in Tech Council, a better solution is being worked on.
namespace Yoast\WP\SEO\Integrations\Admin\Addon_Installation;
use WPSEO_Addon_Manager;
use Yoast\WP\SEO\Actions\Addon_Installation\Addon_Activate_Action;
use Yoast\WP\SEO\Actions\Addon_Installation\Addon_Install_Action;
use Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional;
use Yoast\WP\SEO\Conditionals\Admin\Licenses_Page_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Activation_Error_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Already_Installed_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Installation_Error_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Activate_Plugins_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Install_Plugins_Exception;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Plans\User_Interface\Plans_Page_Integration;
/**
* Represents the Addon installation feature.
*/
class Installation_Integration implements Integration_Interface {
/**
* The installation action.
*
* @var Addon_Install_Action
*/
protected $addon_install_action;
/**
* The activation action.
*
* @var Addon_Activate_Action
*/
protected $addon_activate_action;
/**
* The addon manager.
*
* @var WPSEO_Addon_Manager
*/
protected $addon_manager;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [
Admin_Conditional::class,
Licenses_Page_Conditional::class,
Addon_Installation_Conditional::class,
];
}
/**
* Addon_Installation constructor.
*
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
* @param Addon_Activate_Action $addon_activate_action The addon activate action.
* @param Addon_Install_Action $addon_install_action The addon install action.
*/
public function __construct(
WPSEO_Addon_Manager $addon_manager,
Addon_Activate_Action $addon_activate_action,
Addon_Install_Action $addon_install_action
) {
$this->addon_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 '<div class="wrap yoast wpseo_table_page">';
\printf(
'<h1 id="wpseo-title" class="yoast-h1">%s</h1>',
\esc_html__( 'Installing and activating addons', 'wordpress-seo' )
);
$licensed_addons = $this->addon_manager->get_myyoast_site_information()->subscriptions;
foreach ( $licensed_addons as $addon ) {
\printf( '<p><strong>%s</strong></p>', \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 '<p>';
echo \implode( '<br />', \array_map( 'esc_html', $output ) );
echo '</p>';
}
\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' ),
'<a href="' . \esc_url( \admin_url( 'admin.php?page=' . Plans_Page_Integration::PAGE ) ) . '">',
'Yoast SEO Premium',
'</a>'
);
echo '</div>';
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/indexables-exclude-taxonomy-integration.php 0000666 00000002613 15220430626 0021023 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Admin;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Indexables_Exclude_Taxonomy_Integration class
*/
class Indexables_Exclude_Taxonomy_Integration implements Integration_Interface {
use No_Conditionals;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Indexables_Exclude_Taxonomy_Integration constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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/third-party/woocommerce-permalinks.php 0000666 00000005714 15220430626 0016727 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Third_Party;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* The permalink watcher.
*/
class Woocommerce_Permalinks implements Integration_Interface {
/**
* Represents the indexable helper.
*
* @var Indexable_Helper
*/
protected $indexable_helper;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ WooCommerce_Conditional::class, Migrations_Conditional::class ];
}
/**
* Constructor.
*
* @param Indexable_Helper $indexable_helper Indexable Helper.
*/
public function __construct( Indexable_Helper $indexable_helper ) {
$this->indexable_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/web-stories.php 0000666 00000007731 15220430626 0014511 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Third_Party;
use Yoast\WP\SEO\Conditionals\Web_Stories_Conditional;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Integrations\Front_End_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Presenters\Title_Presenter;
/**
* Web Stories integration.
*/
class Web_Stories implements Integration_Interface {
/**
* The front end integration.
*
* @var Front_End_Integration
*/
protected $front_end;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Web_Stories_Conditional::class ];
}
/**
* Constructs the Web Stories integration
*
* @param Front_End_Integration $front_end The front end integration.
*/
public function __construct( Front_End_Integration $front_end ) {
$this->front_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 <head> 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/amp.php 0000666 00000002723 15220430626 0013017 0 ustar 00 <?php
namespace Yoast\WP\SEO\Integrations\Third_Party;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Integrations\Front_End_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* AMP integration.
*/
class AMP implements Integration_Interface {
/**
* The front end integration.
*
* @var Front_End_Integration
*/
protected $front_end;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Front_End_Conditional::class ];
}
/**
* Constructs the AMP integration
*
* @param Front_End_Integration $front_end The front end integration.
*/
public function __construct( Front_End_Integration $front_end ) {
$this->front_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' );
}
}
initializers/woocommerce.php 0000666 00000001417 15220430626 0012311 0 ustar 00 <?php
namespace Yoast\WP\SEO\Initializers;
use Automattic\WooCommerce\Utilities\FeaturesUtil;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
/**
* Declares compatibility with the WooCommerce HPOS feature.
*/
class Woocommerce implements Initializer_Interface {
use No_Conditionals;
/**
* Hooks into WooCommerce.
*
* @return void
*/
public function initialize() {
\add_action( 'before_woocommerce_init', [ $this, 'declare_custom_order_tables_compatibility' ] );
}
/**
* Declares compatibility with the WooCommerce HPOS feature.
*
* @return void
*/
public function declare_custom_order_tables_compatibility() {
if ( \class_exists( FeaturesUtil::class ) ) {
FeaturesUtil::declare_compatibility( 'custom_order_tables', \WPSEO_FILE, true );
}
}
}
initializers/initializer-interface.php 0000666 00000000531 15220430626 0014247 0 ustar 00 <?php
namespace Yoast\WP\SEO\Initializers;
use Yoast\WP\SEO\Loadable_Interface;
/**
* Integration interface definition.
*
* An interface for registering integrations with WordPress.
*/
interface Initializer_Interface extends Loadable_Interface {
/**
* Runs this initializer.
*
* @return void
*/
public function initialize();
}
presenters/url-list-presenter.php 0000666 00000002431 15220430626 0013233 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters;
/**
* Presenter class for the URL list.
*/
class Url_List_Presenter extends Abstract_Presenter {
/**
* If the url should be target blank.
*
* @var bool
*/
private $target_blank;
/**
* A list of arrays containing titles and URLs.
*
* @var array
*/
private $links;
/**
* Classname for the URL list.
*
* @var string
*/
private $class_name;
/**
* Url_List_Presenter constructor.
*
* @param array $links A list of arrays containing titles and urls.
* @param string $class_name Classname for the url list.
* @param bool $target_blank If the url should be target blank.
*/
public function __construct( $links, $class_name = 'yoast-url-list', $target_blank = false ) {
$this->links = $links;
$this->class_name = $class_name;
$this->target_blank = $target_blank;
}
/**
* Presents the URL list.
*
* @return string The URL list.
*/
public function present() {
$output = '<ul class="' . $this->class_name . '">';
foreach ( $this->links as $link ) {
$output .= '<li><a';
if ( $this->target_blank ) {
$output .= ' target = "_blank"';
}
$output .= ' href="' . $link['permalink'] . '">' . $link['title'] . '</a></li>';
}
$output .= '</ul>';
return $output;
}
}
presenters/admin/meta-fields-presenter.php 0000666 00000003100 15220430626 0014734 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Admin;
use WP_Post;
use WPSEO_Meta;
use Yoast\WP\SEO\Presenters\Abstract_Presenter;
/**
* Presenter class for meta fields in the post editor.
*
* Outputs the hidden fields for a particular field group and post.
*/
class Meta_Fields_Presenter extends Abstract_Presenter {
/**
* The meta fields for which we are going to output hidden input.
*
* @var array
*/
private $meta_fields;
/**
* The metabox post.
*
* @var WP_Post The metabox post.
*/
private $post;
/**
* Meta_Fields_Presenter constructor.
*
* @param WP_Post $post The metabox post.
* @param string $field_group The key under which a group of fields is grouped.
* @param string $post_type The post type.
*/
public function __construct( $post, $field_group, $post_type = 'post' ) {
$this->post = $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 .= '<input type="hidden" id="' . $form_key . '" name="' . $form_key . '" value="' . \esc_attr( $meta_value ) . '"' . $default . '/>' . "\n";
}
return $output;
}
}
presenters/admin/indexing-list-item-presenter.php 0000666 00000003012 15220430626 0016256 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Admin;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Presenters\Abstract_Presenter;
/**
* Class Indexing_List_Item_Presenter.
*
* @package Yoast\WP\SEO\Presenters\Admin
*/
class Indexing_List_Item_Presenter extends Abstract_Presenter {
/**
* The short link helper.
*
* @var Short_Link_Helper
*/
protected $short_link_helper;
/**
* Indexing_List_Item_Presenter constructor.
*
* @param Short_Link_Helper $short_link_helper Represents the short link helper.
*/
public function __construct( Short_Link_Helper $short_link_helper ) {
$this->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( '<li><strong>%s</strong><br/>', \esc_html__( 'Optimize SEO Data', 'wordpress-seo' ) );
$output .= \sprintf(
'%1$s <a href="%2$s" target="_blank">%3$s</a>',
\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 .= '<div id="yoast-seo-indexing-action" style="margin: 16px 0;"></div>';
$output .= '</li>';
return $output;
}
}
presenters/admin/help-link-presenter.php 0000666 00000004121 15220430626 0014431 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Admin;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Presenters\Abstract_Presenter;
/**
* Represents the presenter class for Help link.
*/
class Help_Link_Presenter extends Abstract_Presenter {
/**
* Help link.
*
* @var string
*/
private $link;
/**
* Help link visually hidden text.
*
* @var string
*/
private $link_text;
/**
* Whether the Help link opens in a new browser tab.
*
* @var bool
*/
private $opens_in_new_browser_tab;
/**
* An instance of the WPSEO_Admin_Asset_Manager class.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $asset_manager;
/**
* Help_Link_Presenter constructor.
*
* @param string $link Help link.
* @param string $link_text Help link visually hidden text.
* @param bool $opens_in_new_browser_tab Whether the link opens in a new browser tab. Default true.
*/
public function __construct( $link = '', $link_text = '', $opens_in_new_browser_tab = true ) {
$this->link = $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(
'<a href="%1$s"%2$s class="yoast_help yoast-help-link dashicons"><span class="yoast-help-icon" aria-hidden="true"></span><span class="screen-reader-text">%3$s</span></a>',
\esc_url( $this->link ),
$target_blank_attribute,
\esc_html( $this->link_text . $new_tab_message )
);
}
}
presenters/admin/beta-badge-presenter.php 0000666 00000002447 15220430626 0014532 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Admin;
use Yoast\WP\SEO\Presenters\Abstract_Presenter;
/**
* Represents the presenter class for "Beta" badges.
*/
class Beta_Badge_Presenter extends Abstract_Presenter {
/**
* Identifier of the badge.
*
* @var string
*/
private $id;
/**
* Optional link of the badge.
*
* @var string
*/
private $link;
/**
* Beta_Badge_Presenter constructor.
*
* @param string $id Id of the badge.
* @param string $link Optional link of the badge.
*/
public function __construct( $id, $link = '' ) {
$this->id = $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(
'<a class="yoast-badge yoast-badge__is-link yoast-beta-badge" id="%1$s-beta-badge" href="%2$s">%3$s</a>',
\esc_attr( $this->id ),
\esc_url( $this->link ),
'Beta' // We don't want this string to be translatable.
);
}
return \sprintf(
'<span class="yoast-badge yoast-beta-badge" id="%1$s-beta-badge">%2$s</span>',
\esc_attr( $this->id ),
'Beta' // We don't want this string to be translatable.
);
}
}
presenters/admin/indexing-notification-presenter.php 0000666 00000013141 15220430626 0017041 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Admin;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Presenters\Abstract_Presenter;
/**
* Class Indexing_Notification_Presenter.
*
* @package Yoast\WP\SEO\Presenters\Admin
*/
class Indexing_Notification_Presenter extends Abstract_Presenter {
/**
* The total number of unindexed objects.
*
* @var int
*/
protected $total_unindexed;
/**
* The message to show in the notification.
*
* @var string
*/
protected $reason;
/**
* The short link helper.
*
* @var Short_Link_Helper
*/
protected $short_link_helper;
/**
* Indexing_Notification_Presenter constructor.
*
* @param Short_Link_Helper $short_link_helper The short link helper.
* @param int $total_unindexed Total number of unindexed objects.
* @param string $reason The reason to show in the notification.
*/
public function __construct( $short_link_helper, $total_unindexed, $reason ) {
$this->short_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 = '<p>' . $this->get_message( $this->reason );
$notification_text .= $this->get_time_estimate( $this->total_unindexed ) . '</p>';
$notification_text .= '<a class="button" href="' . \get_admin_url( null, 'admin.php?page=wpseo_tools&start-indexation=true' ) . '">';
$notification_text .= \esc_html__( 'Start SEO data optimization', 'wordpress-seo' );
$notification_text .= '</a>';
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 .= '<ul class="ul-disc">';
$estimate .= '<li>';
$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 .= '</li>';
$estimate .= '<li>';
$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' ),
'<a href="' . \esc_url( $this->short_link_helper->get( 'https://yoa.st/3-w' ) ) . '" target="_blank">',
'</a>',
'<a href="https://wp-cli.org/" target="_blank">'
);
$estimate .= '</li>';
$estimate .= '</ul>';
return $estimate;
}
}
presenters/admin/light-switch-presenter.php 0000666 00000010764 15220430626 0015166 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Admin;
use Yoast\WP\SEO\Presenters\Abstract_Presenter;
/**
* Class Light_Switch_Presenter.
*
* @package Yoast\WP\SEO\Presenters\Admin
*/
class Light_Switch_Presenter extends Abstract_Presenter {
/**
* The variable to create the checkbox for.
*
* @var string
*/
protected $var;
/**
* The visual label text for the toggle.
*
* @var string
*/
protected $label;
/**
* Array of two visual labels for the buttons.
*
* @var array
*/
protected $buttons;
/**
* The name of the underlying checkbox.
*
* @var string
*/
protected $name;
/**
* The variable current value.
*
* @var string|bool
*/
protected $value;
/**
* Reverse order of buttons.
*
* @var bool
*/
protected $reverse;
/**
* The inline Help HTML.
*
* @var string
*/
protected $help;
/**
* Whether the visual label is displayed in strong text.
*
* @var bool
*/
protected $strong;
/**
* The disabled attribute HTML.
*
* @var string
*/
protected $disabled_attribute;
/**
* Light_Switch_Presenter constructor.
*
* @param string $variable The variable to create the checkbox for.
* @param string $label The visual label text for the toggle.
* @param array $buttons Array of two visual labels for the buttons (defaults Disabled/Enabled).
* @param string $name The name of the underlying checkbox.
* @param string|bool $value The variable current value, to determine the checked attribute.
* @param bool $reverse Optional. Reverse order of buttons (default true).
* @param string $help Optional. Inline Help HTML that will be printed out before the toggle. Default is empty.
* @param bool $strong Optional. Whether the visual label is displayed in strong text. Default is false.
* Starting from Yoast SEO 16.5, the visual label is forced to bold via CSS.
* @param string $disabled_attribute Optional. The disabled HTML attribute. Default is empty.
*/
public function __construct(
$variable,
$label,
$buttons,
$name,
$value,
$reverse = true,
$help = '',
$strong = false,
$disabled_attribute = ''
) {
$this->var = $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 = '<div class="switch-container' . $help_class . '">';
$output .= \sprintf(
'<span class="switch-light-visual-label%1$s" id="%2$s">%3$s</span>%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 .= '<label class="' . $class . '"><b class="switch-yoast-seo-jaws-a11y"> </b>';
$output .= \sprintf(
'<input type="checkbox" aria-labelledby="%1$s" id="%2$s" name="%3$s" value="on"%4$s%5$s/>',
\esc_attr( $this->var . '-label' ),
\esc_attr( $this->var ),
\esc_attr( $this->name ),
\checked( $this->value, 'on', false ), // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: The output is hardcoded by WordPress.
$this->disabled_attribute // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $disabled_attribute output is hardcoded.
);
$output .= '<span aria-hidden="true">';
$output .= '<span>' . \esc_html( $off_button ) . '</span>';
$output .= '<span>' . \esc_html( $on_button ) . '</span>';
$output .= '<a></a>';
$output .= '</span></label><div class="clear"></div></div>';
return $output;
}
}
presenters/robots-txt-presenter.php 0000666 00000010267 15220430626 0013613 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters;
use Yoast\WP\SEO\Helpers\Robots_Txt_Helper;
/**
* Presenter class for the robots.txt file helper.
*/
class Robots_Txt_Presenter extends Abstract_Presenter {
public const YOAST_OUTPUT_BEFORE_COMMENT = '# START YOAST BLOCK' . \PHP_EOL . '# ---------------------------' . \PHP_EOL;
public const YOAST_OUTPUT_AFTER_COMMENT = '# ---------------------------' . \PHP_EOL . '# END YOAST BLOCK';
/**
* Text to be outputted for the allow directive.
*
* @var string
*/
public const ALLOW_DIRECTIVE = 'Allow';
/**
* Text to be outputted for the disallow directive.
*
* @var string
*/
public const DISALLOW_DIRECTIVE = 'Disallow';
/**
* Text to be outputted for the user-agent rule.
*
* @var string
*/
public const USER_AGENT_FIELD = 'User-agent';
/**
* Text to be outputted for the sitemap rule.
*
* @var string
*/
public const SITEMAP_FIELD = 'Sitemap';
/**
* Holds the Robots_Txt_Helper.
*
* @var Robots_Txt_Helper
*/
protected $robots_txt_helper;
/**
* Constructor.
*
* @param Robots_Txt_Helper $robots_txt_helper The robots txt helper.
*/
public function __construct( Robots_Txt_Helper $robots_txt_helper ) {
$this->robots_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 );
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;
}
}
presenters/abstract-indexable-presenter.php 0000666 00000003063 15220430626 0015216 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters;
use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
/**
* Abstract presenter class for indexable presentations.
*/
abstract class Abstract_Indexable_Presenter extends Abstract_Presenter {
/**
* The WPSEO Replace Vars object.
*
* @var WPSEO_Replace_Vars
*/
public $replace_vars;
/**
* The indexable presentation.
*
* @var Indexable_Presentation
*/
public $presentation;
/**
* The helpers surface
*
* @var Helpers_Surface
*/
public $helpers;
/**
* The tag key name.
*
* @var string
*/
protected $key = 'NO KEY PROVIDED';
/**
* Gets the raw value of a presentation.
*
* @return string|array The raw value.
*/
abstract public function get();
/**
* Transforms an indexable presenter's key to a json safe key string.
*
* @return string|null
*/
public function escape_key() {
if ( $this->key === '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/open-graph/locale-presenter.php 0000666 00000002007 15220430626 0014756 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Open_Graph;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Tag_Presenter;
/**
* Final presenter class for the Open Graph locale.
*/
final class Locale_Presenter extends Abstract_Indexable_Tag_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'og:locale';
/**
* The tag format including placeholders.
*
* @var string
*/
protected $tag_format = self::META_PROPERTY_CONTENT;
/**
* Run the locale through the `wpseo_og_locale` filter.
*
* @return string The filtered locale.
*/
public function get() {
/**
* Filter: 'wpseo_og_locale' - Allow changing the Yoast SEO Open Graph locale.
*
* @param string $locale The locale string
* @param Indexable_Presentation $presentation The presentation of an indexable.
*/
return (string) \trim( \apply_filters( 'wpseo_og_locale', $this->presentation->open_graph_locale, $this->presentation ) );
}
}
presenters/open-graph/image-presenter.php 0000666 00000010377 15220430626 0014612 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Open_Graph;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter;
/**
* Presenter class for the Open Graph image.
*/
class Image_Presenter extends Abstract_Indexable_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'og:image';
/**
* Image tags that we output for each image.
*
* @var array<string>
*/
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 .= '<meta property="og:image" content="' . \esc_url( $image_url, null, 'attribute' ) . '"' . $class . ' />';
foreach ( static::$image_tags as $key => $value ) {
if ( empty( $image_meta[ $key ] ) ) {
continue;
}
$return .= \PHP_EOL . "\t" . '<meta property="og:image:' . \esc_attr( $key ) . '" content="' . \esc_attr( $image_meta[ $key ] ) . '"' . $class . ' />';
}
}
return $return;
}
/**
* Gets the raw value of a presentation.
*
* @return array<string, int> 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<string, string|int> $image The image.
*
* @return array<string, string|int> 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/title-presenter.php 0000666 00000002215 15220430626 0014641 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Open_Graph;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Tag_Presenter;
/**
* Presenter class for the Open Graph title.
*/
class Title_Presenter extends Abstract_Indexable_Tag_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'og:title';
/**
* The tag format including placeholders.
*
* @var string
*/
protected $tag_format = self::META_PROPERTY_CONTENT;
/**
* Run the title content through replace vars, the `wpseo_opengraph_title` filter and sanitization.
*
* @return string The filtered title.
*/
public function get() {
$title = $this->replace_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/url-presenter.php 0000666 00000002161 15220430626 0014322 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Open_Graph;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Tag_Presenter;
/**
* Presenter class for the Open Graph URL.
*/
class Url_Presenter extends Abstract_Indexable_Tag_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'og:url';
/**
* The tag format including placeholders.
*
* @var string
*/
protected $tag_format = self::META_PROPERTY_CONTENT;
/**
* The method of escaping to use.
*
* @var string
*/
protected $escaping = 'attribute';
/**
* Run the url content through the `wpseo_opengraph_url` filter.
*
* @return string The filtered url.
*/
public function get() {
/**
* Filter: 'wpseo_opengraph_url' - Allow changing the Yoast SEO generated open graph URL.
*
* @param string $url The open graph URL.
* @param Indexable_Presentation $presentation The presentation of an indexable.
*/
return \urldecode( (string) \apply_filters( 'wpseo_opengraph_url', $this->presentation->open_graph_url, $this->presentation ) );
}
}
presenters/open-graph/type-presenter.php 0000666 00000001762 15220430626 0014507 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Open_Graph;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Tag_Presenter;
/**
* Presenter class for the Open Graph type.
*/
class Type_Presenter extends Abstract_Indexable_Tag_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'og:type';
/**
* The tag format including placeholders.
*
* @var string
*/
protected $tag_format = self::META_PROPERTY_CONTENT;
/**
* Run the opengraph type content through the `wpseo_opengraph_type` filter.
*
* @return string The filtered type.
*/
public function get() {
/**
* Filter: 'wpseo_opengraph_type' - Allow changing the opengraph type.
*
* @param string $type The type.
* @param Indexable_Presentation $presentation The presentation of an indexable.
*/
return (string) \apply_filters( 'wpseo_opengraph_type', $this->presentation->open_graph_type, $this->presentation );
}
}
presenters/open-graph/article-author-presenter.php 0000666 00000002251 15220430626 0016443 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Open_Graph;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Tag_Presenter;
/**
* Presenter class for the Open Graph article author.
*/
class Article_Author_Presenter extends Abstract_Indexable_Tag_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'article:author';
/**
* The tag format including placeholders.
*
* @var string
*/
protected $tag_format = self::META_PROPERTY_CONTENT;
/**
* Run the article author's Facebook URL through the `wpseo_opengraph_author_facebook` filter.
*
* @return string The filtered article author's Facebook URL.
*/
public function get() {
/**
* Filter: 'wpseo_opengraph_author_facebook' - Allow developers to filter the article author's Facebook URL.
*
* @param bool|string $article_author The article author's Facebook URL, return false to disable.
* @param Indexable_Presentation $presentation The presentation of an indexable.
*/
return \trim( \apply_filters( 'wpseo_opengraph_author_facebook', $this->presentation->open_graph_article_author, $this->presentation ) );
}
}
presenters/breadcrumbs-presenter.php 0000666 00000017055 15220430626 0013761 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
/**
* Presenter class for the breadcrumbs.
*/
class Breadcrumbs_Presenter extends Abstract_Indexable_Presenter {
/**
* The id attribute.
*
* @var string
*/
private $id;
/**
* The class name attribute.
*
* @var string
*/
private $class;
/**
* The wrapper element name.
*
* @var string
*/
private $wrapper;
/**
* Separator to use.
*
* @var string
*/
private $separator;
/**
* The element.
*
* @var string
*/
private $element;
/**
* Presents the breadcrumbs.
*
* @return string The breadcrumbs HTML.
*/
public function present() {
$breadcrumbs = $this->get();
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 . '</' . $this->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 .= '<a';
if ( $this->should_link_target_blank() ) {
$link .= ' target="_blank"';
}
$link .= ' href="' . \esc_url( $breadcrumb['url'] ) . '"' . $title_attr . '>' . $text . '</a>';
$link .= '</' . $this->get_element() . '>';
}
elseif ( $index === ( $total - 1 ) ) {
// If it's the last element.
if ( $this->helpers->options->get( 'breadcrumbs-boldlast' ) === true ) {
$text = '<strong>' . $text . '</strong>';
}
$link .= '<' . $this->get_element() . ' class="breadcrumb_last" aria-current="page">' . $text . '</' . $this->get_element() . '>';
}
else {
// It's not the last element and has no url.
$link .= '<' . $this->get_element() . '>' . $text . '</' . $this->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/meta-author-presenter.php 0000666 00000002541 15220430626 0013710 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters;
use WP_User;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
/**
* Presenter class for the meta author tag.
*/
class Meta_Author_Presenter extends Abstract_Indexable_Tag_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'author';
/**
* Returns the author for a post in a meta author tag.
*
* @return string The meta author tag.
*/
public function present() {
$output = parent::present();
if ( ! empty( $output ) ) {
return $output;
}
return '';
}
/**
* Get the author's display name.
*
* @return string The author's display name.
*/
public function get() {
if ( $this->presentation->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/rel-next-presenter.php 0000666 00000003203 15220430626 0013214 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
/**
* Presenter class for the rel next meta tag.
*/
class Rel_Next_Presenter extends Abstract_Indexable_Tag_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'next';
/**
* The tag format including placeholders.
*
* @var string
*/
protected $tag_format = self::LINK_REL_HREF;
/**
* The method of escaping to use.
*
* @var string
*/
protected $escaping = 'url';
/**
* Returns the rel next meta tag.
*
* @return string The rel next tag.
*/
public function present() {
$output = parent::present();
if ( ! empty( $output ) ) {
/**
* Filter: 'wpseo_next_rel_link' - Allow changing link rel output by Yoast SEO.
*
* @param string $unsigned The full `<link` element.
*/
return \apply_filters( 'wpseo_next_rel_link', $output );
}
return '';
}
/**
* Run the canonical content through the `wpseo_adjacent_rel_url` filter.
*
* @return string The filtered adjacent link.
*/
public function get() {
if ( \in_array( 'noindex', $this->presentation->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/debug/marker-open-presenter.php 0000666 00000003132 15220430626 0014765 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Debug;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter;
/**
* Presenter class for the debug open marker.
*/
final class Marker_Open_Presenter extends Abstract_Indexable_Presenter {
/**
* Returns the debug close marker.
*
* @return string The debug close marker.
*/
public function present() {
/**
* Filter: 'wpseo_debug_markers' - Allow disabling the debug markers.
*
* @param bool $show_markers True when the debug markers should be shown.
*/
if ( ! \apply_filters( 'wpseo_debug_markers', true ) ) {
return '';
}
$version_info = 'v' . \WPSEO_VERSION;
if ( $this->helpers->product->is_premium() ) {
$version_info = $this->construct_version_info();
}
return \sprintf(
'<!-- This site is optimized with the %1$s %2$s - https://yoast.com/wordpress/plugins/seo/ -->',
\esc_html( $this->helpers->product->get_name() ),
$version_info
);
}
/**
* 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/debug/marker-close-presenter.php 0000666 00000001505 15220430626 0015133 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Debug;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter;
/**
* Presenter class for the debug close marker.
*/
final class Marker_Close_Presenter extends Abstract_Indexable_Presenter {
/**
* Returns the debug close marker.
*
* @return string The debug close marker.
*/
public function present() {
/**
* Filter: 'wpseo_debug_markers' - Allow disabling the debug markers.
*
* @param bool $show_markers True when the debug markers should be shown.
*/
if ( ! \apply_filters( 'wpseo_debug_markers', true ) ) {
return '';
}
return \sprintf(
'<!-- / %s. -->',
\esc_html( $this->helpers->product->get_name() )
);
}
/**
* Gets the raw value of a presentation.
*
* @return string The raw value.
*/
public function get() {
return '';
}
}
presenters/rel-prev-presenter.php 0000666 00000003301 15220430626 0013211 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters;
/**
* Presenter class for the rel prev meta tag.
*/
class Rel_Prev_Presenter extends Abstract_Indexable_Tag_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'prev';
/**
* The tag format including placeholders.
*
* @var string
*/
protected $tag_format = self::LINK_REL_HREF;
/**
* The method of escaping to use.
*
* @var string
*/
protected $escaping = 'url';
/**
* Returns the rel prev meta tag.
*
* @param bool $output_tag Optional. Whether or not to output the HTML tag. Defaults to true.
*
* @return string The rel prev tag.
*/
public function present( $output_tag = true ) {
$output = parent::present();
if ( ! empty( $output ) ) {
/**
* Filter: 'wpseo_prev_rel_link' - Allow changing link rel output by Yoast SEO.
*
* @param string $unsigned The full `<link` element.
*/
return \apply_filters( 'wpseo_prev_rel_link', $output );
}
return '';
}
/**
* Run the rel prev content through the `wpseo_adjacent_rel_url` filter.
*
* @return string The filtered adjacent link.
*/
public function get() {
if ( \in_array( 'noindex', $this->presentation->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/score-icon-presenter.php 0000666 00000001660 15220430626 0013524 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters;
/**
* Presenter class for a score icon.
*/
class Score_Icon_Presenter extends Abstract_Presenter {
/**
* Holds the title.
*
* @var string
*/
protected $title;
/**
* Holds the CSS class.
*
* @var string
*/
protected $css_class;
/**
* Constructs a Score_Icon_Presenter.
*
* @param string $title The title and screen reader text.
* @param string $css_class The CSS class.
*/
public function __construct( $title, $css_class ) {
$this->title = $title;
$this->css_class = $css_class;
}
/**
* Presents the score icon.
*
* @return string The score icon.
*/
public function present() {
return \sprintf(
'<div aria-hidden="true" title="%1$s" class="wpseo-score-icon %3$s"><span class="wpseo-score-text screen-reader-text">%2$s</span></div>',
\esc_attr( $this->title ),
\esc_html( $this->title ),
\esc_attr( $this->css_class )
);
}
}
presenters/webmaster/yandex-presenter.php 0000666 00000001123 15220430626 0014736 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presenters\Webmaster;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Tag_Presenter;
/**
* Presenter class for the Yandex Webmaster verification setting.
*/
class Yandex_Presenter extends Abstract_Indexable_Tag_Presenter {
/**
* The tag key name.
*
* @var string
*/
protected $key = 'yandex-verification';
/**
* Retrieves the webmaster tool site verification value from the settings.
*
* @return string The webmaster tool site verification value.
*/
public function get() {
return $this->helpers->options->get( 'yandexverify', '' );
}
}
analytics/application/missing-indexables-collector.php 0000666 00000004521 15220430626 0017326 0 ustar 00 <?php
namespace Yoast\WP\SEO\Analytics\Application;
use WPSEO_Collection;
use Yoast\WP\SEO\Actions\Indexing\Indexation_Action_Interface;
use Yoast\WP\SEO\Analytics\Domain\Missing_Indexable_Bucket;
use Yoast\WP\SEO\Analytics\Domain\Missing_Indexable_Count;
/**
* Manages the collection of the missing indexable data.
*
* @makePublic
*/
class Missing_Indexables_Collector implements WPSEO_Collection {
/**
* All the indexation actions.
*
* @var array<Indexation_Action_Interface>
*/
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 );
}
);
}
}
services/health-check/runner-interface.php 0000666 00000000425 15220430626 0014674 0 ustar 00 <?php
namespace Yoast\WP\SEO\Services\Health_Check;
/**
* Interface for the health check runner. The abstract Health_Check uses this to run a health check.
*/
interface Runner_Interface {
/**
* Runs the health check.
*
* @return void
*/
public function run();
}
services/health-check/health-check.php 0000666 00000004455 15220430626 0013754 0 ustar 00 <?php
namespace Yoast\WP\SEO\Services\Health_Check;
/**
* Abstract class for all health checks. Provides a uniform interface for the Health_Check_Integration.
*/
abstract class Health_Check {
/**
* The prefix to add to the test identifier. Used to differentiate between Yoast's health checks, and other health checks.
*/
public const TEST_IDENTIFIER_PREFIX = 'yoast-';
/**
* The object that runs the actual health check.
*
* @var Runner_Interface
*/
private $runner;
/**
* The health check implementation sets the runner so this class can start a health check.
*
* @param Runner_Interface $runner The health check runner.
* @return void
*/
protected function set_runner( $runner ) {
$this->runner = $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/reports-trait.php 0000666 00000001637 15220430626 0014252 0 ustar 00 <?php
namespace Yoast\WP\SEO\Services\Health_Check;
/**
* Used by classes that use a health check Report_Builder.
*/
trait Reports_Trait {
/**
* The factory for the builder object that generates WordPress-friendly test results.
*
* @var Report_Builder_Factory
*/
private $report_builder_factory;
/**
* The test identifier that's set on the Report_Builder.
*
* @var string
*/
private $test_identifier = '';
/**
* Sets the name that WordPress uses to identify this health check.
*
* @param string $test_identifier The identifier.
* @return void
*/
public function set_test_identifier( $test_identifier ) {
$this->test_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 );
}
}
functions.php 0000666 00000001271 15220430626 0007272 0 ustar 00 <?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Internals
*/
if ( ! defined( 'WPSEO_VERSION' ) ) {
header( 'Status: 403 Forbidden' );
header( 'HTTP/1.1 403 Forbidden' );
exit();
}
use Yoast\WP\SEO\Main;
if ( is_dir( WPSEO_PATH . YOAST_VENDOR_PREFIX_DIRECTORY ) ) {
require_once WPSEO_PATH . YOAST_VENDOR_PREFIX_DIRECTORY . '/guzzlehttp/guzzle/src/functions.php';
}
/**
* Retrieves the main instance.
*
* @phpcs:disable WordPress.NamingConventions -- Should probably be renamed, but leave for now.
*
* @return Main The main instance.
*/
function YoastSEO() {
// phpcs:enable
static $main;
if ( $main === null ) {
$main = new Main();
$main->load();
}
return $main;
}
ai-http-request/domain/request.php 0000666 00000003431 15220430626 0013255 0 ustar 00 <?php
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain;
/**
* Class Request
* Represents a request to the AI Generator API.
*/
class Request {
/**
* The action path for the request.
*
* @var string
*/
private $action_path;
/**
* The body of the request.
*
* @var array<string>
*/
private $body;
/**
* The headers for the request.
*
* @var array<string>
*/
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<string> $body The body of the request.
* @param array<string> $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<string> The body of the request.
*/
public function get_body(): array {
return $this->body;
}
/**
* Get the headers for the request.
*
* @return array<string> 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/response.php 0000666 00000003734 15220430626 0013431 0 ustar 00 <?php
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain;
/**
* Class Response
* Represents a response from the AI Generator API.
*/
class Response {
/**
* The response body.
*
* @var string
*/
private $body;
/**
* The response code.
*
* @var int
*/
private $response_code;
/**
* The response message.
*
* @var string
*/
private $message;
/**
* The error code.
*
* @var string
*/
private $error_code;
/**
* The missing licenses.
*
* @var array<string>
*/
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<string> $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<string> The missing licenses.
*/
public function get_missing_licenses(): array {
return $this->missing_licenses;
}
}
ai-http-request/domain/exceptions/wp-request-exception.php 0000666 00000001307 15220430626 0020056 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions;
use Throwable;
/**
* Class to manage an error response in wp_remote_*() requests.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class WP_Request_Exception extends Remote_Request_Exception {
/**
* WP_Request_Exception constructor.
*
* @param string $message The error message.
* @param Throwable| null $previous The previously thrown exception.
*/
public function __construct( $message = '', $previous = null ) {
parent::__construct( $message, 400, 'WP_HTTP_REQUEST_ERROR', $previous );
}
}
routes/first-time-configuration-route.php 0000666 00000020076 15220430626 0014673 0 ustar 00 <?php
namespace Yoast\WP\SEO\Routes;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Actions\Configuration\First_Time_Configuration_Action;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Main;
/**
* First_Time_Configuration_Route class.
*/
class First_Time_Configuration_Route implements Route_Interface {
use No_Conditionals;
/**
* Represents the first time configuration route.
*
* @var string
*/
public const CONFIGURATION_ROUTE = '/configuration';
/**
* Represents a site representation route.
*
* @var string
*/
public const SITE_REPRESENTATION_ROUTE = '/site_representation';
/**
* Represents a social profiles route.
*
* @var string
*/
public const SOCIAL_PROFILES_ROUTE = '/social_profiles';
/**
* Represents a route to enable/disable tracking.
*
* @var string
*/
public const ENABLE_TRACKING_ROUTE = '/enable_tracking';
/**
* Represents a route to check if current user has the correct capabilities to edit another user's profile.
*
* @var string
*/
public const CHECK_CAPABILITY_ROUTE = '/check_capability';
/**
* Represents a route to save the first time configuration state.
*
* @var string
*/
public const SAVE_CONFIGURATION_STATE_ROUTE = '/save_configuration_state';
/**
* Represents a route to save the first time configuration state.
*
* @var string
*/
public const GET_CONFIGURATION_STATE_ROUTE = '/get_configuration_state';
/**
* The first tinme configuration action.
*
* @var First_Time_Configuration_Action
*/
private $first_time_configuration_action;
/**
* First_Time_Configuration_Route constructor.
*
* @param First_Time_Configuration_Action $first_time_configuration_action The first-time configuration action.
*/
public function __construct( First_Time_Configuration_Action $first_time_configuration_action ) {
$this->first_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/abstract-action-route.php 0000666 00000001172 15220430626 0013015 0 ustar 00 <?php
namespace Yoast\WP\SEO\Routes;
use WP_REST_Response;
/**
* Abstract_Action_Route class.
*
* Abstract class for action routes.
*/
abstract class Abstract_Action_Route implements Route_Interface {
/**
* Responds to an indexing request.
*
* @param array $objects The objects that have been indexed.
* @param string $next_url The url that should be called to continue reindexing. False if done.
*
* @return WP_REST_Response The response.
*/
protected function respond_with( $objects, $next_url ) {
return new WP_REST_Response(
[
'objects' => $objects,
'next_url' => $next_url,
]
);
}
}
routes/supported-features-route.php 0000666 00000002436 15220430626 0013604 0 ustar 00 <?php
namespace Yoast\WP\SEO\Routes;
use WP_REST_Response;
use Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional;
use Yoast\WP\SEO\Main;
/**
* Supported_Features_Route class.
*/
class Supported_Features_Route implements Route_Interface {
/**
* Represents the supported features route.
*
* @var string
*/
public const SUPPORTED_FEATURES_ROUTE = '/supported-features';
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [
Addon_Installation_Conditional::class,
];
}
/**
* Registers routes with WordPress.
*
* @return void
*/
public function register_routes() {
$supported_features_route = [
'methods' => '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/workouts-route.php 0000666 00000006145 15220430626 0011641 0 ustar 00 <?php
namespace Yoast\WP\SEO\Routes;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Main;
/**
* Workouts_Route class.
*/
class Workouts_Route implements Route_Interface {
use No_Conditionals;
/**
* Represents workouts route.
*
* @var string
*/
public const WORKOUTS_ROUTE = '/workouts';
/**
* The Options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Workouts_Route constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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 );
}
}
builders/indexable-author-builder.php 0000666 00000017117 15220430626 0013760 0 ustar 00 <?php
namespace Yoast\WP\SEO\Builders;
use Yoast\WP\SEO\Exceptions\Indexable\Author_Not_Built_Exception;
use Yoast\WP\SEO\Helpers\Author_Archive_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Author Builder for the indexables.
*
* Formats the author meta to indexable format.
*/
class Indexable_Author_Builder {
use Indexable_Social_Image_Trait;
/**
* The author archive helper.
*
* @var Author_Archive_Helper
*/
private $author_archive;
/**
* The latest version of the Indexable_Author_Builder.
*
* @var int
*/
protected $version;
/**
* Holds the options helper instance.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* Holds the taxonomy helper instance.
*
* @var Post_Helper
*/
protected $post_helper;
/**
* Indexable_Author_Builder constructor.
*
* @param Author_Archive_Helper $author_archive The author archive helper.
* @param Indexable_Builder_Versions $versions The Indexable version manager.
* @param Options_Helper $options_helper The options helper.
* @param Post_Helper $post_helper The post helper.
*/
public function __construct(
Author_Archive_Helper $author_archive,
Indexable_Builder_Versions $versions,
Options_Helper $options_helper,
Post_Helper $post_helper
) {
$this->author_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.
if ( $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 );
}
}
builders/indexable-system-page-builder.php 0000666 00000004150 15220430626 0014705 0 ustar 00 <?php
namespace Yoast\WP\SEO\Builders;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* System page builder for the indexables.
*
* Formats system pages ( search and error ) meta to indexable format.
*/
class Indexable_System_Page_Builder {
/**
* Mapping of object type to title option keys.
*/
public const OPTION_MAPPING = [
'search-result' => [
'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-term-builder.php 0000666 00000021030 15220430626 0013412 0 ustar 00 <?php
namespace Yoast\WP\SEO\Builders;
use Yoast\WP\SEO\Exceptions\Indexable\Invalid_Term_Exception;
use Yoast\WP\SEO\Exceptions\Indexable\Term_Not_Built_Exception;
use Yoast\WP\SEO\Exceptions\Indexable\Term_Not_Found_Exception;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Term Builder for the indexables.
*
* Formats the term meta to indexable format.
*/
class Indexable_Term_Builder {
use Indexable_Social_Image_Trait;
/**
* Holds the taxonomy helper instance.
*
* @var Taxonomy_Helper
*/
protected $taxonomy_helper;
/**
* The latest version of the Indexable_Term_Builder.
*
* @var int
*/
protected $version;
/**
* Holds the taxonomy helper instance.
*
* @var Post_Helper
*/
protected $post_helper;
/**
* Indexable_Term_Builder constructor.
*
* @param Taxonomy_Helper $taxonomy_helper The taxonomy helper.
* @param Indexable_Builder_Versions $versions The latest version of each Indexable Builder.
* @param Post_Helper $post_helper The post helper.
*/
public function __construct(
Taxonomy_Helper $taxonomy_helper,
Indexable_Builder_Versions $versions,
Post_Helper $post_helper
) {
$this->taxonomy_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/primary-term-builder.php 0000666 00000005222 15220430626 0013147 0 ustar 00 <?php
namespace Yoast\WP\SEO\Builders;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Meta_Helper;
use Yoast\WP\SEO\Helpers\Primary_Term_Helper;
use Yoast\WP\SEO\Repositories\Primary_Term_Repository;
/**
* Primary term builder.
*
* Creates the primary term for a post.
*/
class Primary_Term_Builder {
/**
* The primary term repository.
*
* @var Primary_Term_Repository
*/
protected $repository;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* The primary term helper.
*
* @var Primary_Term_Helper
*/
private $primary_term;
/**
* The meta helper.
*
* @var Meta_Helper
*/
private $meta;
/**
* Primary_Term_Builder constructor.
*
* @param Primary_Term_Repository $repository The primary term repository.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Primary_Term_Helper $primary_term The primary term helper.
* @param Meta_Helper $meta The meta helper.
*/
public function __construct(
Primary_Term_Repository $repository,
Indexable_Helper $indexable_helper,
Primary_Term_Helper $primary_term,
Meta_Helper $meta
) {
$this->repository = $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-post-type-archive-builder.php 0000666 00000012324 15220430626 0016034 0 ustar 00 <?php
namespace Yoast\WP\SEO\Builders;
use Yoast\WP\SEO\Exceptions\Indexable\Post_Type_Not_Built_Exception;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Post type archive builder for the indexables.
*
* Formats the post type archive meta to indexable format.
*/
class Indexable_Post_Type_Archive_Builder {
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options;
/**
* The latest version of the Indexable_Post_Type_Archive_Builder.
*
* @var int
*/
protected $version;
/**
* Holds the post helper instance.
*
* @var Post_Helper
*/
protected $post_helper;
/**
* Holds the post type helper instance.
*
* @var Post_Type_Helper
*/
protected $post_type_helper;
/**
* Indexable_Post_Type_Archive_Builder constructor.
*
* @param Options_Helper $options The options helper.
* @param Indexable_Builder_Versions $versions The latest version of each Indexable builder.
* @param Post_Helper $post_helper The post helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
*/
public function __construct(
Options_Helper $options,
Indexable_Builder_Versions $versions,
Post_Helper $post_helper,
Post_Type_Helper $post_type_helper
) {
$this->options = $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-post-builder.php 0000666 00000030307 15220430626 0013437 0 ustar 00 <?php
namespace Yoast\WP\SEO\Builders;
use WP_Error;
use WP_Post;
use Yoast\WP\SEO\Exceptions\Indexable\Post_Not_Built_Exception;
use Yoast\WP\SEO\Exceptions\Indexable\Post_Not_Found_Exception;
use Yoast\WP\SEO\Helpers\Meta_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Post Builder for the indexables.
*
* Formats the post meta to indexable format.
*/
class Indexable_Post_Builder {
use Indexable_Social_Image_Trait;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $indexable_repository;
/**
* Holds the Post_Helper instance.
*
* @var Post_Helper
*/
protected $post_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type_helper;
/**
* Knows the latest version of the Indexable post builder type.
*
* @var int
*/
protected $version;
/**
* The meta helper.
*
* @var Meta_Helper
*/
protected $meta;
/**
* Indexable_Post_Builder constructor.
*
* @param Post_Helper $post_helper The post helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
* @param Indexable_Builder_Versions $versions The indexable builder versions.
* @param Meta_Helper $meta The meta helper.
*/
public function __construct(
Post_Helper $post_helper,
Post_Type_Helper $post_type_helper,
Indexable_Builder_Versions $versions,
Meta_Helper $meta
) {
$this->post_helper = $post_helper;
$this->post_type_helper = $post_type_helper;
$this->version = $versions->get_latest_version_for_type( 'post' );
$this->meta = $meta;
}
/**
* 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->get_permalink( $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;
}
/**
* 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.
*/
protected function get_permalink( $post_type, $post_id ) {
if ( $post_type !== 'attachment' ) {
return \get_permalink( $post_id );
}
return \wp_get_attachment_url( $post_id );
}
/**
* 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, '<!--nextpage-->' ) + 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.php 0000666 00000010542 15220430626 0014313 0 ustar 00 <?php
namespace Yoast\WP\SEO\Builders;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Homepage Builder for the indexables.
*
* Formats the homepage meta to indexable format.
*/
class Indexable_Home_Page_Builder {
use Indexable_Social_Image_Trait;
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options;
/**
* The URL helper.
*
* @var Url_Helper
*/
protected $url_helper;
/**
* The latest version of the Indexable-Home-Page-Builder.
*
* @var int
*/
protected $version;
/**
* Holds the taxonomy helper instance.
*
* @var Post_Helper
*/
protected $post_helper;
/**
* Indexable_Home_Page_Builder constructor.
*
* @param Options_Helper $options The options helper.
* @param Url_Helper $url_helper The url helper.
* @param Indexable_Builder_Versions $versions Knows the latest version of each Indexable type.
* @param Post_Helper $post_helper The post helper.
*/
public function __construct(
Options_Helper $options,
Url_Helper $url_helper,
Indexable_Builder_Versions $versions,
Post_Helper $post_helper
) {
$this->options = $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-link-builder.php 0000666 00000043671 15220430626 0013417 0 ustar 00 <?php
namespace Yoast\WP\SEO\Builders;
use WPSEO_Image_Utils;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Images\Application\Image_Content_Extractor;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Models\SEO_Links;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Repositories\SEO_Links_Repository;
/**
* Indexable link builder.
*/
class Indexable_Link_Builder {
/**
* The SEO links repository.
*
* @var SEO_Links_Repository
*/
protected $seo_links_repository;
/**
* The url helper.
*
* @var Url_Helper
*/
protected $url_helper;
/**
* The image helper.
*
* @var Image_Helper
*/
protected $image_helper;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
protected $indexable_helper;
/**
* The post helper.
*
* @var Post_Helper
*/
protected $post_helper;
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $indexable_repository;
/**
* Class that finds all images in a content string and extracts them.
*
* @var Image_Content_Extractor
*/
private $image_content_extractor;
/**
* Indexable_Link_Builder constructor.
*
* @param SEO_Links_Repository $seo_links_repository The SEO links repository.
* @param Url_Helper $url_helper The URL helper.
* @param Post_Helper $post_helper The post helper.
* @param Options_Helper $options_helper The options helper.
* @param Indexable_Helper $indexable_helper The indexable helper.
*/
public function __construct(
SEO_Links_Repository $seo_links_repository,
Url_Helper $url_helper,
Post_Helper $post_helper,
Options_Helper $options_helper,
Indexable_Helper $indexable_helper,
Image_Content_Extractor $image_content_extractor
) {
$this->seo_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 = '<a\s[^>]*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.
*
* @internal
*/
\do_action( 'wpseo_related_indexables_incoming_links_updated' );
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<string|int> $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-date-archive-builder.php 0000666 00000003227 15220430626 0015007 0 ustar 00 <?php
namespace Yoast\WP\SEO\Builders;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Date Archive Builder for the indexables.
*
* Formats the date archive meta to indexable format.
*/
class Indexable_Date_Archive_Builder {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* The latest version of the Indexable_Date_Archive_Builder.
*
* @var int
*/
protected $version;
/**
* Indexable_Date_Archive_Builder constructor.
*
* @param Options_Helper $options The options helper.
* @param Indexable_Builder_Versions $versions The latest version for all indexable builders.
*/
public function __construct( Options_Helper $options, Indexable_Builder_Versions $versions ) {
$this->options = $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;
}
}
conditionals/attachment-redirections-enabled-conditional.php 0000666 00000001605 15220430626 0020462 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Conditional that is only met when the 'Redirect attachment URLs to the attachment itself' setting is enabled.
*/
class Attachment_Redirections_Enabled_Conditional implements Conditional {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* Attachment_Redirections_Enabled_Conditional constructor.
*
* @param Options_Helper $options The options helper.
*/
public function __construct( Options_Helper $options ) {
$this->options = $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/premium-inactive-conditional.php 0000666 00000000611 15220430626 0015524 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Abstract class for creating conditionals based on feature flags.
*/
class Premium_Inactive_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
return ! \YoastSEO()->helpers->product->is_premium();
}
}
conditionals/semrush-enabled-conditional.php 0000666 00000001343 15220430626 0015327 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Conditional that is only met when the SEMrush integration is enabled.
*/
class SEMrush_Enabled_Conditional implements Conditional {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* SEMrush_Enabled_Conditional constructor.
*
* @param Options_Helper $options The options helper.
*/
public function __construct( Options_Helper $options ) {
$this->options = $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/woocommerce-conditional.php 0000666 00000000634 15220430626 0014572 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional that is only met when WooCommerce is active.
*/
class WooCommerce_Conditional implements Conditional {
/**
* Returns `true` when the WooCommerce plugin is installed and activated.
*
* @return bool `true` when the WooCommerce plugin is installed and activated.
*/
public function is_met() {
return \class_exists( 'WooCommerce' );
}
}
conditionals/updated-importer-framework-conditional.php 0000666 00000000637 15220430626 0017536 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Feature flag conditional for the updated importer framework.
*/
class Updated_Importer_Framework_Conditional extends Feature_Flag_Conditional {
/**
* Returns the name of the updated importer framework feature flag.
*
* @return string The name of the feature flag.
*/
protected function get_feature_flag() {
return 'UPDATED_IMPORTER_FRAMEWORK';
}
}
conditionals/wp-cron-enabled-conditional.php 0000666 00000000537 15220430626 0015232 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Class that checks if WP_CRON is enabled.
*/
class WP_CRON_Enabled_Conditional implements Conditional {
/**
* Checks if WP_CRON is enabled.
*
* @return bool True when WP_CRON is enabled.
*/
public function is_met() {
return ! ( \defined( 'DISABLE_WP_CRON' ) && \DISABLE_WP_CRON );
}
}
conditionals/text-formality-conditional.php 0000666 00000000661 15220430626 0015243 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Checks if the YOAST_SEO_TEXT_FORMALITY constant is set.
*/
class Text_Formality_Conditional extends Feature_Flag_Conditional {
/**
* 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.
*/
public function get_feature_flag() {
return 'TEXT_FORMALITY';
}
}
conditionals/third-party/translatepress-conditional.php 0000666 00000000710 15220430626 0017567 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals\Third_Party;
use Yoast\WP\SEO\Conditionals\Conditional;
/**
* Conditional that is only met when the TranslatePress plugin is active.
*/
class TranslatePress_Conditional implements Conditional {
/**
* Checks whether the TranslatePress plugin is active.
*
* @return bool Whether the TranslatePress plugin is active.
*/
public function is_met() {
return \class_exists( 'TRP_Translate_Press' );
}
}
conditionals/third-party/elementor-activated-conditional.php 0000666 00000000745 15220430626 0020461 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals\Third_Party;
use Yoast\WP\SEO\Conditionals\Conditional;
/**
* Conditional that is met when the Elementor plugin is installed and activated.
*/
class Elementor_Activated_Conditional implements Conditional {
/**
* Checks if the Elementor plugins is installed and activated.
*
* @return bool `true` when the Elementor plugin is installed and activated.
*/
public function is_met() {
return \defined( 'ELEMENTOR__FILE__' );
}
}
conditionals/premium-active-conditional.php 0000666 00000000546 15220430626 0015204 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Class Premium_Active_Conditional.
*/
class Premium_Active_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
return \YoastSEO()->helpers->product->is_premium();
}
}
conditionals/user-profile-conditional.php 0000666 00000001041 15220430626 0014660 0 ustar 00 <?php // phpcs:ignore Yoast.Files.FileName.InvalidClassFileName -- Reason: this explicitly concerns the Yoast tools page.
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional that is only met when current page is the tools page.
*/
class User_Profile_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
global $pagenow;
if ( $pagenow !== 'profile.php' ) {
return false;
}
return true;
}
}
conditionals/settings-conditional.php 0000666 00000001740 15220430626 0014112 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Class Settings_Conditional.
*/
class Settings_Conditional implements Conditional {
/**
* Holds User_Can_Manage_Wpseo_Options_Conditional.
*
* @var User_Can_Manage_Wpseo_Options_Conditional
*/
protected $user_can_manage_wpseo_options_conditional;
/**
* Constructs Settings_Conditional.
*
* @param User_Can_Manage_Wpseo_Options_Conditional $user_can_manage_wpseo_options_conditional The User_Can_Manage_Wpseo_Options_Conditional.
*/
public function __construct(
User_Can_Manage_Wpseo_Options_Conditional $user_can_manage_wpseo_options_conditional
) {
$this->user_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/news-conditional.php 0000666 00000000550 15220430626 0013224 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional that is only met when news SEO is activated.
*/
class News_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
return \defined( 'WPSEO_NEWS_VERSION' );
}
}
conditionals/admin/post-conditional.php 0000666 00000001340 15220430626 0014323 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals\Admin;
use Yoast\WP\SEO\Conditionals\Conditional;
/**
* Conditional that is only met when on a post edit or new post page.
*/
class Post_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
global $pagenow;
// Current page is the creation of a new post (type, i.e. post, page, custom post or attachment).
if ( $pagenow === 'post-new.php' ) {
return true;
}
// Current page is the edit page of an existing post (type, i.e. post, page, custom post or attachment).
if ( $pagenow === 'post.php' ) {
return true;
}
return false;
}
}
conditionals/xmlrpc-conditional.php 0000666 00000000667 15220430626 0013566 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional that is met when the current request is an XML-RPC request.
*/
class XMLRPC_Conditional implements Conditional {
/**
* Returns whether the current request is an XML-RPC request.
*
* @return bool `true` when the current request is an XML-RPC request, `false` if not.
*/
public function is_met() {
return ( \defined( 'XMLRPC_REQUEST' ) && \XMLRPC_REQUEST );
}
}
conditionals/no-tool-selected-conditional.php 0000666 00000000775 15220430626 0015436 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional that is only met when current page is not a specific tool's page.
*/
class No_Tool_Selected_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We just check whether a URL parameter does not exist.
return ! isset( $_GET['tool'] );
}
}
conditionals/migrations-conditional.php 0000666 00000001457 15220430626 0014433 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
use Yoast\WP\SEO\Config\Migration_Status;
/**
* Class for integrations that depend on having all migrations run.
*/
class Migrations_Conditional implements Conditional {
/**
* The migration status.
*
* @var Migration_Status
*/
protected $migration_status;
/**
* Migrations_Conditional constructor.
*
* @param Migration_Status $migration_status The migration status object.
*/
public function __construct( Migration_Status $migration_status ) {
$this->migration_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/jetpack-conditional.php 0000666 00000000637 15220430626 0013677 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional that is only met when Jetpack exists.
*/
class Jetpack_Conditional implements Conditional {
/**
* Returns `true` when the Jetpack plugin exists on this
* WordPress installation.
*
* @return bool `true` when the Jetpack plugin exists on this WordPress installation.
*/
public function is_met() {
return \class_exists( 'Jetpack' );
}
}
conditionals/addon-installation-conditional.php 0000666 00000000666 15220430626 0016044 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Checks if the Addon_Installation constant is set.
*/
class Addon_Installation_Conditional extends Feature_Flag_Conditional {
/**
* 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.
*/
protected function get_feature_flag() {
return 'ADDON_INSTALLATION';
}
}
conditionals/user-edit-conditional.php 0000666 00000000751 15220430626 0014154 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional that is only met when current page is the Edit User page or the User's profile page.
*/
class User_Edit_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
global $pagenow;
if ( $pagenow !== 'profile.php' && $pagenow !== 'user-edit.php' ) {
return false;
}
return true;
}
}
conditionals/wp-robots-conditional.php 0000666 00000000525 15220430626 0014206 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Class that checks if wp_robots exists.
*/
class WP_Robots_Conditional implements Conditional {
/**
* Checks if the wp_robots function exists.
*
* @return bool True when the wp_robots function exists.
*/
public function is_met() {
return \function_exists( 'wp_robots' );
}
}
conditionals/primary-category-conditional.php 0000666 00000003254 15220430626 0015552 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
/**
* Conditional that is only met when in frontend or page is a post overview or post add/edit form.
*/
class Primary_Category_Conditional implements Conditional {
/**
* The current page helper.
*
* @var Current_Page_Helper
*/
private $current_page;
/**
* Primary_Category_Conditional constructor.
*
* @param Current_Page_Helper $current_page The current page helper.
*/
public function __construct( Current_Page_Helper $current_page ) {
$this->current_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/new-settings-ui-conditional.php 0000666 00000000543 15220430626 0015314 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Feature flag conditional for the new settings UI.
*/
class New_Settings_Ui_Conditional extends Feature_Flag_Conditional {
/**
* Returns the name of the feature flag.
*
* @return string The name of the feature flag.
*/
protected function get_feature_flag() {
return 'NEW_SETTINGS_UI';
}
}
conditionals/conditional-interface.php 0000666 00000000452 15220430626 0014211 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional interface, used to prevent integrations from loading.
*/
interface Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met();
}
conditionals/yoast-tools-page-conditional.php 0000666 00000001176 15220430626 0015464 0 ustar 00 <?php // phpcs:ignore Yoast.Files.FileName.InvalidClassFileName -- Reason: this explicitly concerns the Yoast tools page.
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional that is only met when current page is the tools page.
*/
class Yoast_Tools_Page_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
global $pagenow;
if ( $pagenow !== 'admin.php' ) {
return false;
}
if ( isset( $_GET['page'] ) && $_GET['page'] === 'wpseo_tools' ) {
return true;
}
return false;
}
}
conditionals/development-conditional.php 0000666 00000000601 15220430626 0014567 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
use WPSEO_Utils;
/**
* Conditional that is only met when in development mode.
*/
class Development_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
return WPSEO_Utils::is_development_mode();
}
}
conditionals/wincher-conditional.php 0000666 00000000240 15220430626 0013703 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
/**
* Conditional for the Wincher integration.
*/
class Wincher_Conditional extends Non_Multisite_Conditional {}
conditionals/wincher-automatically-track-conditional.php 0000666 00000001367 15220430626 0017666 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Conditional that is only met when the Wincher automatic tracking is enabled.
*/
class Wincher_Automatically_Track_Conditional implements Conditional {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* Wincher_Automatically_Track_Conditional constructor.
*
* @param Options_Helper $options The options helper.
*/
public function __construct( Options_Helper $options ) {
$this->options = $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/wincher-enabled-conditional.php 0000666 00000001343 15220430626 0015300 0 ustar 00 <?php
namespace Yoast\WP\SEO\Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Conditional that is only met when the Wincher integration is enabled.
*/
class Wincher_Enabled_Conditional implements Conditional {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* Wincher_Enabled_Conditional constructor.
*
* @param Options_Helper $options The options helper.
*/
public function __construct( Options_Helper $options ) {
$this->options = $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 );
}
}
actions/wincher/wincher-account-action.php 0000666 00000005010 15220430626 0014720 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Wincher;
use Exception;
use Yoast\WP\SEO\Config\Wincher_Client;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Class Wincher_Account_Action
*/
class Wincher_Account_Action {
public const ACCOUNT_URL = 'https://api.wincher.com/beta/account';
public const UPGRADE_CAMPAIGN_URL = 'https://api.wincher.com/v1/yoast/upgrade-campaign';
/**
* The Wincher_Client instance.
*
* @var Wincher_Client
*/
protected $client;
/**
* The Options_Helper instance.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* Wincher_Account_Action constructor.
*
* @param Wincher_Client $client The API client.
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Wincher_Client $client, Options_Helper $options_helper ) {
$this->client = $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/semrush/semrush-phrases-action.php 0000666 00000004441 15220430626 0015016 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\SEMrush;
use Exception;
use Yoast\WP\SEO\Config\SEMrush_Client;
/**
* Class SEMrush_Phrases_Action
*/
class SEMrush_Phrases_Action {
/**
* The transient cache key.
*/
public const TRANSIENT_CACHE_KEY = 'wpseo_semrush_related_keyphrases_%s_%s';
/**
* The SEMrush keyphrase URL.
*
* @var string
*/
public const KEYPHRASES_URL = 'https://oauth.semrush.com/api/v1/keywords/phrase_fullsearch';
/**
* The SEMrush_Client instance.
*
* @var SEMrush_Client
*/
protected $client;
/**
* SEMrush_Phrases_Action constructor.
*
* @param SEMrush_Client $client The API client.
*/
public function __construct( SEMrush_Client $client ) {
$this->client = $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/semrush/semrush-options-action.php 0000666 00000002140 15220430626 0015036 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\SEMrush;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Class SEMrush_Options_Action
*/
class SEMrush_Options_Action {
/**
* The Options_Helper instance.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* SEMrush_Options_Action constructor.
*
* @param Options_Helper $options_helper The WPSEO options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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/alert-dismissal-action.php 0000666 00000012576 15220430626 0013304 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions;
use Yoast\WP\SEO\Helpers\User_Helper;
/**
* Class Alert_Dismissal_Action.
*/
class Alert_Dismissal_Action {
public const USER_META_KEY = '_yoast_alerts_dismissed';
/**
* Holds the user helper instance.
*
* @var User_Helper
*/
protected $user;
/**
* Constructs Alert_Dismissal_Action.
*
* @param User_Helper $user User helper.
*/
public function __construct( User_Helper $user ) {
$this->user = $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;
}
}
actions/indexing/indexable-post-type-archive-indexation-action.php 0000666 00000014026 15220430626 0021460 0 ustar 00 <?php
namespace Yoast\WP\SEO\Actions\Indexing;
use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
/**
* Reindexing action for post type archive indexables.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Indexable_Post_Type_Archive_Indexation_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {
/**
* The transient cache key.
*/
public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_post_type_archives';
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $repository;
/**
* The indexable builder.
*
* @var Indexable_Builder
*/
protected $builder;
/**
* The current version of the post type archive indexable builder.
*
* @var int
*/
protected $version;
/**
* Indexation_Post_Type_Archive_Action constructor.
*
* @param Indexable_Repository $repository The indexable repository.
* @param Indexable_Builder $builder The indexable builder.
* @param Post_Type_Helper $post_type The post type helper.
* @param Indexable_Builder_Versions $versions The current versions of all indexable builders.
*/
public function __construct(
Indexable_Repository $repository,
Indexable_Builder $builder,
Post_Type_Helper $post_type,
Indexable_Builder_Versions $versions
) {
$this->repository = $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/importing/aioseo/aioseo-custom-archive-settings-importing-action.php 0000666 00000007366 15220430627 0023552 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
/**
* Importing action for AIOSEO custom archive settings data.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Aioseo_Custom_Archive_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {
/**
* The plugin of the action.
*/
public const PLUGIN = 'aioseo';
/**
* The type of the action.
*/
public const TYPE = 'custom_archive_settings';
/**
* The option_name of the AIOSEO option that contains the settings.
*/
public const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';
/**
* The map of aioseo_options to yoast settings.
*
* @var array
*/
protected $aioseo_options_to_yoast_map = [];
/**
* The tab of the aioseo settings we're working with.
*
* @var string
*/
protected $settings_tab = 'archives';
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type;
/**
* Aioseo_Custom_Archive_Settings_Importing_Action constructor.
*
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
* @param Options_Helper $options The options helper.
* @param Sanitization_Helper $sanitization The sanitization helper.
* @param Post_Type_Helper $post_type The post type 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.
*/
public function __construct(
Import_Cursor_Helper $import_cursor,
Options_Helper $options,
Sanitization_Helper $sanitization,
Post_Type_Helper $post_type,
Aioseo_Replacevar_Service $replacevar_handler,
Aioseo_Robots_Provider_Service $robots_provider,
Aioseo_Robots_Transformer_Service $robots_transformer
) {
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
$this->post_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',
];
}
}
}
}
llms-txt/application/markdown-builders/markdown-builder.php 0000666 00000005146 15220430627 0020256 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Llms_Txt_Renderer;
/**
* The builder of the markdown file.
*/
class Markdown_Builder {
/**
* The renderer of the LLMs.txt file.
*
* @var Llms_Txt_Renderer
*/
protected $llms_txt_renderer;
/**
* The intro builder.
*
* @var Intro_Builder
*/
protected $intro_builder;
/**
* The title builder.
*
* @var Title_Builder
*/
protected $title_builder;
/**
* The description builder.
*
* @var Description_Builder
*/
protected $description_builder;
/**
* The link lists builder.
*
* @var Link_Lists_Builder
*/
protected $link_lists_builder;
/**
* The markdown escaper.
*
* @var Markdown_Escaper
*/
protected $markdown_escaper;
/**
* The constructor.
*
* @param Llms_Txt_Renderer $llms_txt_renderer The renderer of the LLMs.txt file.
* @param Intro_Builder $intro_builder The intro builder.
* @param Title_Builder $title_builder The title builder.
* @param Description_Builder $description_builder The description builder.
* @param Link_Lists_Builder $link_lists_builder The link lists builder.
* @param Markdown_Escaper $markdown_escaper The markdown escaper.
*/
public function __construct(
Llms_Txt_Renderer $llms_txt_renderer,
Intro_Builder $intro_builder,
Title_Builder $title_builder,
Description_Builder $description_builder,
Link_Lists_Builder $link_lists_builder,
Markdown_Escaper $markdown_escaper
) {
$this->llms_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;
}
/**
* Renders the markdown.
*
* @return string The rendered markdown.
*/
public function render(): string {
$this->llms_txt_renderer->add_section( $this->intro_builder->build_intro() );
$this->llms_txt_renderer->add_section( $this->title_builder->build_title() );
$this->llms_txt_renderer->add_section( $this->description_builder->build_description() );
foreach ( $this->link_lists_builder->build_link_lists() as $link_list ) {
$this->llms_txt_renderer->add_section( $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/available-posts/available-posts-repository.php 0000666 00000003413 15220430627 0021743 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Llms_Txt\Application\Available_Posts;
use Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts\Data_Provider\Available_Posts_Data;
use Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts\Data_Provider\Available_Posts_Repository_Interface;
use Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts\Data_Provider\Data_Container;
use Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts\Data_Provider\Parameters;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Automatic_Post_Collection;
/**
* The data provider for available posts.
*/
class Available_Posts_Repository implements Available_Posts_Repository_Interface {
/**
* The automatic post collection.
*
* @var Automatic_Post_Collection $automatic_post_collection
*/
private $automatic_post_collection;
/**
* The constructor.
*
* @param Automatic_Post_Collection $automatic_post_collection The automatic post collection.
*/
public function __construct(
Automatic_Post_Collection $automatic_post_collection
) {
$this->automatic_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/file/commands/remove-file-command-handler.php 0000666 00000003463 15220430627 0021340 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\File\Commands;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_File_System_Adapter;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_Llms_Txt_Permission_Gate;
/**
* Handles the removal of the llms.txt
*/
class Remove_File_Command_Handler {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The file system adapter.
*
* @var WordPress_File_System_Adapter
*/
private $file_system_adapter;
/**
* The permission gate.
*
* @var WordPress_Llms_Txt_Permission_Gate $permission_gate
*/
private $permission_gate;
/**
* Constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param WordPress_File_System_Adapter $file_system_adapter The file system adapter.
* @param WordPress_Llms_Txt_Permission_Gate $permission_gate The permission gate.
*/
public function __construct(
Options_Helper $options_helper,
WordPress_File_System_Adapter $file_system_adapter,
WordPress_Llms_Txt_Permission_Gate $permission_gate
) {
$this->options_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/domain/content-types/content-type-entry.php 0000666 00000006205 15220430627 0016724 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Llms_Txt\Domain\Content_Types;
use WP_Post;
use Yoast\WP\SEO\Surfaces\Values\Meta;
/**
* This class describes a Content Type Entry.
*/
class Content_Type_Entry {
/**
* The ID of the content type entry.
*
* @var int
*/
private $id;
/**
* The title of the content type entry.
*
* @var string
*/
private $title;
/**
* The URL of the content type entry.
*
* @var string
*/
private $url;
/**
* The description of the content type entry.
*
* @var string
*/
private $description;
/**
* The slug of the content type entry.
*
* @var string
*/
private $slug;
/**
* The constructor.
*
* @param int $id The ID of the content type entry.
* @param string $title The title of the content type entry.
* @param string $url The URL of the content type entry.
* @param string $description The description of the content type entry.
* @param string $slug The slug of the content type entry.
*/
public function __construct(
int $id,
?string $title = null,
?string $url = null,
?string $description = null,
?string $slug = null
) {
$this->id = $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/available-posts/data-provider/data-container.php 0000666 00000002232 15220430627 0021034 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -- Needed in the folder structure.
namespace Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts\Data_Provider;
/**
* The data container.
*/
class Data_Container {
/**
* All the data points.
*
* @var array<Data_Interface>
*/
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<string, string> 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/available-posts/data-provider/data-interface.php 0000666 00000000714 15220430627 0021015 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -- Needed in the folder structure.
namespace Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts\Data_Provider;
/**
* The interface to describe the data domain.
*/
interface Data_Interface {
/**
* A to array method.
*
* @return array<string>
*/
public function to_array(): array;
}
llms-txt/domain/available-posts/data-provider/parameters.php 0000666 00000002100 15220430627 0020300 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -- Needed in the folder structure.
namespace Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts\Data_Provider;
/**
* Object representation of the request parameters.
*/
class Parameters {
/**
* The post type.
*
* @var string
*/
private $post_type;
/**
* The search filter.
*
* @var string
*/
private $search_filter;
/**
* Class constructor.
*
* @param string $post_type The post type.
* @param string $search_filter The search filter.
*/
public function __construct( string $post_type, string $search_filter ) {
$this->post_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.php 0000666 00000001221 15220430627 0025401 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -- Needed in the folder structure.
namespace Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts\Data_Provider;
/**
* Interface describing the way to get data for a specific data provider.
*/
interface Available_Posts_Repository_Interface {
/**
* Method to get available posts from a provider.
*
* @param Parameters $parameters The parameter to get the available posts for.
*
* @return Data_Container
*/
public function get_posts( Parameters $parameters ): Data_Container;
}
llms-txt/domain/available-posts/data-provider/available-posts-data.php 0000666 00000002162 15220430627 0022142 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -- Needed in the folder structure.
namespace Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts\Data_Provider;
use Yoast\WP\SEO\Llms_Txt\Domain\Content_Types\Content_Type_Entry;
/**
* Domain object that represents a Available Posts data record.
*/
class Available_Posts_Data implements Data_Interface {
/**
* The content type entry.
*
* @var Content_Type_Entry
*/
private $content_type_entry;
/**
* The constructor.
*
* @param Content_Type_Entry $content_type_entry The content type entry.
*/
public function __construct( Content_Type_Entry $content_type_entry ) {
$this->content_type_entry = $content_type_entry;
}
/**
* The array representation of this domain object.
*
* @return array<string|float|int|string[]>
*/
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/invalid-post-type-exception.php 0000666 00000000672 15220430627 0020774 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Llms_Txt\Domain\Available_Posts;
use Exception;
/**
* Exception for when the post type asked is invalid.
*/
class Invalid_Post_Type_Exception extends Exception {
/**
* Constructor of the exception.
*/
public function __construct() {
parent::__construct( 'The post type asked is not valid', 400 );
}
}
llms-txt/domain/markdown/sections/link-list.php 0000666 00000003421 15220430627 0015674 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items\Link;
/**
* Represents a link list markdown section.
*/
class Link_List implements Section_Interface {
/**
* The type of the links.
*
* @var string
*/
private $type;
/**
* The links.
*
* @var Link[]
*/
private $links = [];
/**
* Class constructor.
*
* @param string $type The type of the links.
* @param Link[] $links The links.
*/
public function __construct( string $type, array $links ) {
$this->type = $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/items/item-interface.php 0000666 00000001060 15220430627 0016151 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
/**
* Represents a markdown item.
*/
interface Item_Interface {
/**
* Renders the item.
*
* @return string
*/
public function render(): string;
/**
* Escapes the markdown content.
*
* @param Markdown_Escaper $markdown_escaper The markdown escaper.
*
* @return void
*/
public function escape_markdown( Markdown_Escaper $markdown_escaper ): void;
}
llms-txt/domain/markdown/items/link.php 0000666 00000003063 15220430627 0014217 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
/**
* Represents a link markdown item.
*/
class Link implements Item_Interface {
/**
* The description that is part of this link.
*
* @var string
*/
private $description;
/**
* The link text.
*
* @var string
*/
private $text;
/**
* The anchor text.
*
* @var string
*/
private $anchor;
/**
* Class constructor.
*
* @param string $text The link text.
* @param string $anchor The anchor text.
* @param string $description The description.
*/
public function __construct( string $text, string $anchor, string $description = '' ) {
$this->text = $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 );
}
}
deprecated/frontend/breadcrumbs.php 0000666 00000006307 15220430627 0013500 0 ustar 00 <?php
/**
* Backwards compatibility class for breadcrumbs.
*
* @package Yoast\YoastSEO\Backwards_Compatibility
*/
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Presenters\Breadcrumbs_Presenter;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
/**
* Class WPSEO_Breadcrumbs
*
* @codeCoverageIgnore Because of deprecation.
*/
class WPSEO_Breadcrumbs {
/**
* Instance of this class.
*
* @var WPSEO_Breadcrumbs
*/
public static $instance;
/**
* Last used 'before' string.
*
* @var string
*/
public static $before = '';
/**
* Last used 'after' string.
*
* @var string
*/
public static $after = '';
/**
* The memoizer for the meta tags context.
*
* @var Meta_Tags_Context_Memoizer
*/
protected $context_memoizer;
/**
* The helpers surface.
*
* @var Helpers_Surface
*/
protected $helpers;
/**
* The replace vars helper
*
* @var WPSEO_Replace_Vars
*/
protected $replace_vars;
/**
* WPSEO_Breadcrumbs constructor.
*/
public function __construct() {
$this->context_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() {
if ( self::$instance === null ) {
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/frontend/frontend.php 0000666 00000017040 15220430627 0013022 0 ustar 00 <?php
/**
* Backwards compatibility class for WPSEO_Frontend.
*
* @package Yoast\YoastSEO\Backwards_Compatibility
*/
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Presenters\Canonical_Presenter;
use Yoast\WP\SEO\Presenters\Meta_Description_Presenter;
use Yoast\WP\SEO\Presenters\Rel_Next_Presenter;
use Yoast\WP\SEO\Presenters\Rel_Prev_Presenter;
use Yoast\WP\SEO\Presenters\Robots_Presenter;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
/**
* Class WPSEO_Frontend
*
* @codeCoverageIgnore Because of deprecation.
*/
class WPSEO_Frontend {
/**
* Instance of this class.
*
* @var WPSEO_Frontend
*/
public static $instance;
/**
* The memoizer for the meta tags context.
*
* @var Meta_Tags_Context_Memoizer
*/
private $context_memoizer;
/**
* The WPSEO Replace Vars object.
*
* @var WPSEO_Replace_Vars
*/
private $replace_vars;
/**
* The helpers surface.
*
* @var Helpers_Surface
*/
private $helpers;
/**
* WPSEO_Frontend constructor.
*/
public function __construct() {
$this->context_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() {
if ( self::$instance === null ) {
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/src/helpers/request-helper.php 0000666 00000001057 15220430627 0014563 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
/**
* A helper object for the request state.
*
* @deprecated 23.6
* @codeCoverageIgnore Because of deprecation.
*/
class Request_Helper {
/**
* Checks if the current request is a REST request.
*
* @deprecated 23.6
* @codeCoverageIgnore
*
* @return bool True when the current 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 === true;
}
}
generators/schema/abstract-schema-piece.php 0000666 00000001432 15220430627 0015017 0 ustar 00 <?php
namespace Yoast\WP\SEO\Generators\Schema;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
/**
* Class Abstract_Schema_Piece.
*/
abstract class Abstract_Schema_Piece {
/**
* The meta tags context.
*
* @var Meta_Tags_Context
*/
public $context;
/**
* The helpers surface
*
* @var Helpers_Surface
*/
public $helpers;
/**
* Optional identifier for this schema piece.
*
* Used in the `Schema_Generator::filter_graph_pieces_to_generate()` method.
*
* @var string
*/
public $identifier;
/**
* Generates the schema piece.
*
* @return mixed
*/
abstract public function generate();
/**
* Determines whether the schema piece is needed.
*
* @return bool
*/
abstract public function is_needed();
}
generators/schema/organization.php 0000666 00000005145 15220430627 0013404 0 ustar 00 <?php
namespace Yoast\WP\SEO\Generators\Schema;
use Yoast\WP\SEO\Config\Schema_IDs;
/**
* Returns schema Organization data.
*/
class Organization extends Abstract_Schema_Piece {
/**
* Determines whether an Organization graph piece should be added.
*
* @return bool
*/
public function is_needed() {
return $this->context->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/author.php 0000666 00000005736 15220430627 0012210 0 ustar 00 <?php
namespace Yoast\WP\SEO\Generators\Schema;
/**
* Returns schema Author data.
*/
class Author extends Person {
/**
* Determine whether we should return Person schema.
*
* @return bool
*/
public function is_needed() {
if ( $this->context->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.php 0000666 00000012070 15220430627 0013001 0 ustar 00 <?php
namespace Yoast\WP\SEO\Generators\Schema;
use Yoast\WP\SEO\Config\Schema_IDs;
/**
* Returns schema Breadcrumb data.
*/
class Breadcrumb extends Abstract_Schema_Piece {
/**
* Determine if we should add a breadcrumb attribute.
*
* @return bool
*/
public function is_needed() {
if ( $this->context->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.php 0000666 00000014114 15220430627 0014540 0 ustar 00 <?php
namespace Yoast\WP\SEO\Generators;
use Error;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Open_Graph\Image_Helper as Open_Graph_Image_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Open_Graph\Images;
/**
* Represents the generator class for the Open Graph images.
*/
class Open_Graph_Image_Generator implements Generator_Interface {
/**
* The Open Graph image helper.
*
* @var Open_Graph_Image_Helper
*/
protected $open_graph_image;
/**
* The image helper.
*
* @var Image_Helper
*/
protected $image;
/**
* The URL helper.
*
* @var Url_Helper
*/
protected $url;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* Images constructor.
*
* @codeCoverageIgnore
*
* @param Open_Graph_Image_Helper $open_graph_image Image helper for Open Graph.
* @param Image_Helper $image The image helper.
* @param Options_Helper $options The options helper.
* @param Url_Helper $url The url helper.
*/
public function __construct(
Open_Graph_Image_Helper $open_graph_image,
Image_Helper $image,
Options_Helper $options,
Url_Helper $url
) {
$this->open_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.php 0000666 00000031167 15220430627 0013720 0 ustar 00 <?php
namespace Yoast\WP\SEO\Generators;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Pagination_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Represents the generator class for the breadcrumbs.
*/
class Breadcrumbs_Generator implements Generator_Interface {
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $repository;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* The current page helper.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type_helper;
/**
* The URL helper.
*
* @var Url_Helper
*/
private $url_helper;
/**
* The pagination helper.
*
* @var Pagination_Helper
*/
private $pagination_helper;
/**
* Breadcrumbs_Generator constructor.
*
* @param Indexable_Repository $repository The repository.
* @param Options_Helper $options The options helper.
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
* @param Url_Helper $url_helper The URL helper.
* @param Pagination_Helper $pagination_helper The pagination helper.
*/
public function __construct(
Indexable_Repository $repository,
Options_Helper $options,
Current_Page_Helper $current_page_helper,
Post_Type_Helper $post_type_helper,
Url_Helper $url_helper,
Pagination_Helper $pagination_helper
) {
$this->repository = $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<array<int, string>> 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<int, string> 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<int, string> 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/generator-interface.php 0000666 00000000544 15220430627 0013362 0 ustar 00 <?php
namespace Yoast\WP\SEO\Generators;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
interface Generator_Interface {
/**
* Returns a string, or other Thing that the associated presenter can handle.
*
* @param Meta_Tags_Context $context The meta tags context.
*
* @return mixed
*/
public function generate( Meta_Tags_Context $context );
}
generators/open-graph-locale-generator.php 0000666 00000012767 15220430627 0014731 0 ustar 00 <?php
namespace Yoast\WP\SEO\Generators;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
/**
* Class Open_Graph_Locale_Generator.
*/
class Open_Graph_Locale_Generator implements Generator_Interface {
/**
* Generates the OG Locale.
*
* @param Meta_Tags_Context $context The context.
*
* @return string The OG locale.
*/
public function generate( Meta_Tags_Context $context ) {
/**
* Filter: 'wpseo_locale' - Allow changing the locale output.
*
* Note that this filter is different from `wpseo_og_locale`, which is run _after_ the OG specific filtering.
*
* @param string $locale Locale string.
*/
$locale = \apply_filters( 'wpseo_locale', \get_locale() );
// Catch some weird locales served out by WP that are not easily doubled up.
$fix_locales = [
'ca' => '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;
}
}
loadable-interface.php 0000666 00000000436 15220430627 0010766 0 ustar 00 <?php
namespace Yoast\WP\SEO;
/**
* An interface for registering integrations with WordPress
*/
interface Loadable_Interface {
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals();
}
user-meta/framework/custom-meta/noindex-author.php 0000666 00000005155 15220430627 0016371 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\User_Meta\Framework\Custom_Meta;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\User_Meta\Domain\Custom_Meta_Interface;
/**
* The Noindex_Author custom meta.
*/
class Noindex_Author implements Custom_Meta_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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 '
<input
class="yoast-settings__checkbox double"
type="checkbox"
id="' . \esc_attr( $this->get_field_id() ) . '"
name="' . \esc_attr( $this->get_field_id() ) . '"
value="on" '
. \checked( $this->get_value( $user_id ), 'on', false )
. '/>';
echo '
<label class="yoast-label-strong" for="' . \esc_attr( $this->get_field_id() ) . '">'
. \esc_html__( 'Do not allow search engines to show this author\'s archives in search results.', 'wordpress-seo' )
. '</label><br>';
}
}
user-meta/framework/custom-meta/author-metadesc.php 0000666 00000005100 15220430627 0016500 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\User_Meta\Framework\Custom_Meta;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\User_Meta\Domain\Custom_Meta_Interface;
/**
* The Author_Metadesc custom meta.
*/
class Author_Metadesc implements Custom_Meta_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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 '
<label for="' . \esc_attr( $this->get_field_id() ) . '">'
. \esc_html__( 'Meta description to use for Author page', 'wordpress-seo' )
. '</label>';
echo '
<textarea
rows="5"
cols="30"
id="' . \esc_attr( $this->get_field_id() ) . '"
class="yoast-settings__textarea yoast-settings__textarea--medium"
name="' . \esc_attr( $this->get_field_id() ) . '">'
. \esc_textarea( $this->get_value( $user_id ) )
. '</textarea><br>';
}
}
user-meta/framework/custom-meta/author-title.php 0000666 00000004765 15220430627 0016054 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\User_Meta\Framework\Custom_Meta;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\User_Meta\Domain\Custom_Meta_Interface;
/**
* The Author_Title custom meta.
*/
class Author_Title implements Custom_Meta_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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 '
<label for="' . \esc_attr( $this->get_field_id() ) . '">'
. \esc_html__( 'Title to use for Author page', 'wordpress-seo' )
. '</label>';
echo '
<input
class="yoast-settings__text regular-text"
type="text" id="' . \esc_attr( $this->get_field_id() ) . '"
name="' . \esc_attr( $this->get_field_id() ) . '"
value="' . \esc_attr( $this->get_value( $user_id ) )
. '"/><br>';
}
}
user-meta/framework/additional-contactmethods/linkedin.php 0000666 00000001310 15220430627 0020076 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods;
use Yoast\WP\SEO\User_Meta\Domain\Additional_Contactmethod_Interface;
/**
* The Linkedin contactmethod.
*/
class Linkedin implements Additional_Contactmethod_Interface {
/**
* Returns the key of the Linkedin contactmethod.
*
* @return string The key of the Linkedin contactmethod.
*/
public function get_key(): string {
return 'linkedin';
}
/**
* Returns the label of the Linkedin field.
*
* @return string The label of the Linkedin field.
*/
public function get_label(): string {
return \__( 'LinkedIn profile URL', 'wordpress-seo' );
}
}
user-meta/framework/additional-contactmethods/instagram.php 0000666 00000001320 15220430627 0020267 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods;
use Yoast\WP\SEO\User_Meta\Domain\Additional_Contactmethod_Interface;
/**
* The Instagram contactmethod.
*/
class Instagram implements Additional_Contactmethod_Interface {
/**
* Returns the key of the Instagram contactmethod.
*
* @return string The key of the Instagram contactmethod.
*/
public function get_key(): string {
return 'instagram';
}
/**
* Returns the label of the Instagram field.
*
* @return string The label of the Instagram field.
*/
public function get_label(): string {
return \__( 'Instagram profile URL', 'wordpress-seo' );
}
}
user-meta/framework/additional-contactmethods/pinterest.php 0000666 00000001320 15220430627 0020317 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods;
use Yoast\WP\SEO\User_Meta\Domain\Additional_Contactmethod_Interface;
/**
* The Pinterest contactmethod.
*/
class Pinterest implements Additional_Contactmethod_Interface {
/**
* Returns the key of the Pinterest contactmethod.
*
* @return string The key of the Pinterest contactmethod.
*/
public function get_key(): string {
return 'pinterest';
}
/**
* Returns the label of the Pinterest field.
*
* @return string The label of the Pinterest field.
*/
public function get_label(): string {
return \__( 'Pinterest profile URL', 'wordpress-seo' );
}
}
user-meta/framework/additional-contactmethods/soundcloud.php 0000666 00000001330 15220430627 0020462 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods;
use Yoast\WP\SEO\User_Meta\Domain\Additional_Contactmethod_Interface;
/**
* The Soundcloud contactmethod.
*/
class Soundcloud implements Additional_Contactmethod_Interface {
/**
* Returns the key of the SoundCloud contactmethod.
*
* @return string The key of the SoundCloud contactmethod.
*/
public function get_key(): string {
return 'soundcloud';
}
/**
* Returns the label of the SoundCloud field.
*
* @return string The label of the SoundCloud field.
*/
public function get_label(): string {
return \__( 'SoundCloud profile URL', 'wordpress-seo' );
}
}
user-meta/framework/additional-contactmethods/wikipedia.php 0000666 00000001435 15220430627 0020257 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods;
use Yoast\WP\SEO\User_Meta\Domain\Additional_Contactmethod_Interface;
/**
* The Wikipedia contactmethod.
*/
class Wikipedia implements Additional_Contactmethod_Interface {
/**
* Returns the key of the Wikipedia contactmethod.
*
* @return string The key of the Wikipedia contactmethod.
*/
public function get_key(): string {
return 'wikipedia';
}
/**
* Returns the label of the Wikipedia field.
*
* @return string The label of the Wikipedia field.
*/
public function get_label(): string {
return \__( 'Wikipedia page about you', 'wordpress-seo' ) . '<br/><small>' . \__( '(if one exists)', 'wordpress-seo' ) . '</small>';
}
}
user-meta/framework/additional-contactmethods/facebook.php 0000666 00000001310 15220430627 0020052 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\User_Meta\Framework\Additional_Contactmethods;
use Yoast\WP\SEO\User_Meta\Domain\Additional_Contactmethod_Interface;
/**
* The Facebook contactmethod.
*/
class Facebook implements Additional_Contactmethod_Interface {
/**
* Returns the key of the Facebook contactmethod.
*
* @return string The key of the Facebook contactmethod.
*/
public function get_key(): string {
return 'facebook';
}
/**
* Returns the label of the Facebook field.
*
* @return string The label of the Facebook field.
*/
public function get_label(): string {
return \__( 'Facebook profile URL', 'wordpress-seo' );
}
}
user-meta/domain/custom-meta-interface.php 0000666 00000002163 15220430627 0014631 0 ustar 00 <?php
namespace Yoast\WP\SEO\User_Meta\Domain;
/**
* This interface describes a custom meta.
*/
interface Custom_Meta_Interface {
/**
* Returns the priority which the custom meta's form field should be rendered with.
*
* @return int.
*/
public function get_render_priority(): int;
/**
* Returns the db key of the custom meta.
*
* @return string
*/
public function get_key(): string;
/**
* Returns the id of the custom meta's form field.
*
* @return string
*/
public function get_field_id(): string;
/**
* Returns the meta value.
*
* @param int $user_id The user ID.
*
* @return string
*/
public function get_value( $user_id ): string;
/**
* Returns whether the respective global setting is enabled.
*
* @return bool
*/
public function is_setting_enabled(): bool;
/**
* Returns whether the custom meta is allowed to be empty.
*
* @return bool
*/
public function is_empty_allowed(): bool;
/**
* 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;
}
presentations/archive-adjacent-trait.php 0000666 00000003235 15220430627 0014474 0 ustar 00 <?php
namespace Yoast\WP\SEO\Presentations;
use Yoast\WP\SEO\Helpers\Pagination_Helper;
use Yoast\WP\SEO\Models\Indexable;
/**
* Class Archive_Adjacent.
*
* Presentation object for indexables.
*
* @property Indexable $model The indexable.
*/
trait Archive_Adjacent {
/**
* Holds the Pagination_Helper instance.
*
* @var Pagination_Helper
*/
protected $pagination;
/**
* Sets the helpers for the trait.
*
* @required
*
* @codeCoverageIgnore
*
* @param Pagination_Helper $pagination The pagination helper.
*
* @return void
*/
public function set_archive_adjacent_helpers( Pagination_Helper $pagination ) {
$this->pagination = $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 ) );
}
}
dashboard/infrastructure/nonces/nonce-repository.php 0000666 00000000607 15220430627 0017100 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Nonces;
/**
* Repository for WP nonces.
*/
class Nonce_Repository {
/**
* Creates the nonce for a WP REST request.
*
* @return string
*/
public function get_rest_nonce(): string {
return \wp_create_nonce( 'wp_rest' );
}
}
dashboard/infrastructure/browser-cache/browser-cache-configuration.php 0000666 00000004301 15220430627 0022404 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Browser_Cache;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
/**
* Responsible for the browser cache configuration.
*/
class Browser_Cache_Configuration {
/**
* The Site Kit conditional.
*
* @var Google_Site_Kit_Feature_Conditional
*/
private $google_site_kit_feature_conditional;
/**
* The constructor.
*
* @param Google_Site_Kit_Feature_Conditional $google_site_kit_feature_conditional The Site Kit conditional.
*/
public function __construct( Google_Site_Kit_Feature_Conditional $google_site_kit_feature_conditional ) {
$this->google_site_kit_feature_conditional = $google_site_kit_feature_conditional;
}
/**
* Gets the Time To Live for each widget's cache.
*
* @return array<string, array<string, int>> 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 = isset( $auth_cookie['token'] ) ? $auth_cookie['token'] : '';
return \wp_hash( $current_user->user_login . '|' . $session_token . '|' . $blog_id );
}
/**
* Returns the browser cache configuration.
*
* @return array<string, string|array<string, array<string, int>>>
*/
public function get_configuration(): array {
if ( ! $this->google_site_kit_feature_conditional->is_met() ) {
return [];
}
return [
'storagePrefix' => $this->get_storage_prefix(),
'yoastVersion' => \WPSEO_VERSION,
'widgetsCacheTtl' => $this->get_widgets_cache_ttl(),
];
}
}
infrastructure/score-results/readability-score-results/readability-score-results-collector.php 0000666 00000013205 15220430627 0031213 0 ustar 00 dashboard <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Readability_Score_Results;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Readability_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Results_Not_Found_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface;
/**
* Getting readability score results from the indexable database table.
*/
class Readability_Score_Results_Collector implements Score_Results_Collector_Interface {
/**
* Retrieves readability score results for a content type.
*
* @param Readability_Score_Groups_Interface[] $readability_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<string, object|bool|float> 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<string, string> 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/score-results/seo-score-results/cached-seo-score-results-collector.php 0000666 00000005511 15220430627 0027272 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results;
use WPSEO_Utils;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Results_Not_Found_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface;
/**
* The caching decorator to get readability score results.
*/
class Cached_SEO_Score_Results_Collector implements Score_Results_Collector_Interface {
public const SEO_SCORES_TRANSIENT = 'wpseo_seo_scores';
/**
* The actual collector implementation.
*
* @var SEO_Score_Results_Collector
*/
private $seo_score_results_collector;
/**
* The constructor.
*
* @param SEO_Score_Results_Collector $seo_score_results_collector The collector implementation to use.
*/
public function __construct( SEO_Score_Results_Collector $seo_score_results_collector ) {
$this->seo_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<string, object|bool|float> 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.php 0000666 00000012666 15220430627 0026076 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Results_Not_Found_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface;
/**
* Getting SEO score results from the indexable database table.
*/
class SEO_Score_Results_Collector implements Score_Results_Collector_Interface {
/**
* Retrieves the SEO score results for a content type.
*
* @param SEO_Score_Groups_Interface[] $seo_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<string, object|bool|float> 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<string, string> 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/search-console/search-console-parameters.php 0000666 00000001463 15220430627 0022250 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
/**
* Domain object to add search console specific data to the parameters.
*/
class Search_Console_Parameters extends Parameters {
/**
* The search dimensions to query.
*
* @var string[]
*/
private $dimensions;
/**
* Sets the dimension parameter.
*
* @param array<string> $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/infrastructure/search-console/site-kit-search-console-adapter.php 0000666 00000015665 15220430627 0023265 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\ApiDataRow;
use WP_REST_Response;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Search_Console\Failed_Request_Exception;
use Yoast\WP\SEO\Dashboard\Domain\Search_Console\Unexpected_Response_Exception;
use Yoast\WP\SEO\Dashboard\Domain\Search_Rankings\Comparison_Search_Ranking_Data;
use Yoast\WP\SEO\Dashboard\Domain\Search_Rankings\Search_Ranking_Data;
/**
* The site API adapter to make calls to the Search Console API, via the Site_Kit plugin.
*/
class Site_Kit_Search_Console_Adapter {
/**
* Holds the api call class.
*
* @var Site_Kit_Search_Console_Api_Call $site_kit_search_console_api_call
*/
private $site_kit_search_console_api_call;
/**
* The constructor.
*
* @param Site_Kit_Search_Console_Api_Call $site_kit_search_console_api_call The api call class.
*/
public function __construct( Site_Kit_Search_Console_Api_Call $site_kit_search_console_api_call ) {
$this->site_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<string, array<string, string>> 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/site-kit-search-console-api-call.php 0000666 00000002023 15220430627 0023307 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console;
use WP_REST_Request;
use WP_REST_Response;
/**
* Class that hold the code to do the REST call to the Site Kit api.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Site_Kit_Search_Console_Api_Call {
/**
* The search analytics API route path.
*/
private const SEARCH_CONSOLE_DATA_SEARCH_ANALYTICS_ROUTE = '/google-site-kit/v1/modules/search-console/data/searchanalytics';
/**
* Runs the internal REST api call.
*
* @param array<string, array<string, string>> $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/taxonomies/taxonomy-validator.php 0000666 00000001460 15220430627 0020321 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies;
use WP_Taxonomy;
/**
* Class that validates taxonomies.
*/
class Taxonomy_Validator {
/**
* Returns whether the taxonomy in question is valid and associated with a given content type.
*
* @param WP_Taxonomy|false|null $taxonomy The taxonomy to check.
* @param string $content_type The name of the content type to check.
*
* @return bool Whether the taxonomy in question is valid.
*/
public function is_valid_taxonomy( $taxonomy, string $content_type ): bool {
return \is_a( $taxonomy, 'WP_Taxonomy' )
&& $taxonomy->public
&& $taxonomy->show_in_rest
&& \in_array( $taxonomy->name, \get_object_taxonomies( $content_type ), true );
}
}
dashboard/infrastructure/taxonomies/taxonomies-collector.php 0000666 00000006341 15220430627 0020635 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies;
use WP_Taxonomy;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
/**
* Class that collects taxonomies and relevant information.
*/
class Taxonomies_Collector {
/**
* The taxonomy validator.
*
* @var Taxonomy_Validator
*/
private $taxonomy_validator;
/**
* The constructor.
*
* @param Taxonomy_Validator $taxonomy_validator The taxonomy validator.
*/
public function __construct( Taxonomy_Validator $taxonomy_validator ) {
$this->taxonomy_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/indexables/top-page-indexable-collector.php 0000666 00000007126 15220430627 0022046 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Indexables;
use Yoast\WP\SEO\Dashboard\Application\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\No_SEO_Score_Group;
use Yoast\WP\SEO\Dashboard\Domain\Search_Rankings\Top_Page_Data;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* The indexable collector that gets SEO scores from the indexables of top pages.
*/
class Top_Page_Indexable_Collector {
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* The SEO score groups repository.
*
* @var SEO_Score_Groups_Repository
*/
private $seo_score_groups_repository;
/**
* The constructor.
*
* @param Indexable_Repository $indexable_repository The indexable repository.
* @param SEO_Score_Groups_Repository $seo_score_groups_repository The SEO score groups repository.
*/
public function __construct(
Indexable_Repository $indexable_repository,
SEO_Score_Groups_Repository $seo_score_groups_repository
) {
$this->indexable_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/tracking/setup-steps-tracking-repository.php 0000666 00000003112 15220430627 0022401 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Tracking;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Stores and retrieves data about Site Kit usage.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Setup_Steps_Tracking_Repository implements Setup_Steps_Tracking_Repository_Interface {
/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructs the class.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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/endpoints/setup-steps-tracking-endpoint.php 0000666 00000002130 15220430627 0022202 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints;
use Exception;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\User_Interface\Tracking\Setup_Steps_Tracking_Route;
/**
* Represents the setup steps tracking endpoint.
*/
class Setup_Steps_Tracking_Endpoint implements Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string {
return 'setupStepsTracking';
}
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string {
return Setup_Steps_Tracking_Route::ROUTE_NAMESPACE;
}
/**
* Gets the route.
*
* @throws Exception If the route prefix is not overwritten this throws.
* @return string
*/
public function get_route(): string {
return Setup_Steps_Tracking_Route::ROUTE_PREFIX;
}
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string {
return \rest_url( $this->get_namespace() . $this->get_route() );
}
}
dashboard/infrastructure/endpoints/readability-scores-endpoint.php 0000666 00000002232 15220430627 0021676 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints;
use Exception;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\User_Interface\Scores\Abstract_Scores_Route;
use Yoast\WP\SEO\Dashboard\User_Interface\Scores\Readability_Scores_Route;
/**
* Represents the readability scores endpoint.
*/
class Readability_Scores_Endpoint implements Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string {
return 'readabilityScores';
}
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string {
return Abstract_Scores_Route::ROUTE_NAMESPACE;
}
/**
* Gets the route.
*
* @return string
*
* @throws Exception If the route prefix is not overwritten this throws.
*/
public function get_route(): string {
return Readability_Scores_Route::get_route_prefix();
}
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string {
return \rest_url( $this->get_namespace() . $this->get_route() );
}
}
dashboard/infrastructure/endpoints/seo-scores-endpoint.php 0000666 00000002162 15220430627 0020175 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints;
use Exception;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\User_Interface\Scores\Abstract_Scores_Route;
use Yoast\WP\SEO\Dashboard\User_Interface\Scores\SEO_Scores_Route;
/**
* Represents the SEO scores endpoint.
*/
class SEO_Scores_Endpoint implements Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string {
return 'seoScores';
}
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string {
return Abstract_Scores_Route::ROUTE_NAMESPACE;
}
/**
* Gets the route.
*
* @return string
*
* @throws Exception If the route prefix is not overwritten this throws.
*/
public function get_route(): string {
return SEO_Scores_Route::get_route_prefix();
}
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string {
return \rest_url( $this->get_namespace() . $this->get_route() );
}
}
dashboard/infrastructure/configuration/site-kit-consent-repository.php 0000666 00000002457 15220430627 0022565 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Configuration;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Stores and retrieves the Site Kit consent status.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Site_Kit_Consent_Repository implements Site_Kit_Consent_Repository_Interface {
/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructs the class.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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.php 0000666 00000003011 15220430627 0030264 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Configuration;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Stores and retrieves whether the Site Kit configuration is permanently dismissed.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Permanently_Dismissed_Site_Kit_Configuration_Repository implements Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface {
/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructs the class.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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 );
}
}
infrastructure/configuration/permanently-dismissed-site-kit-configuration-repository-interface.php 0000666 00000001635 15220430627 0032155 0 ustar 00 dashboard <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Configuration;
/**
* Interface for the Permanently Dismissed Site Kit configuration Repository.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
interface Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface {
/**
* Sets the Site Kit configuration 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;
/**
* 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;
}
dashboard/infrastructure/integrations/site-kit.php 0000666 00000026342 15220430627 0016537 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Integrations;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface as Configuration_Repository;
use Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Site_Kit_Consent_Repository_Interface;
use Yoast\WP\SEO\Dashboard\Infrastructure\Connection\Site_Kit_Is_Connected_Call;
use Yoast\WP\SEO\Dashboard\User_Interface\Setup\Setup_Url_Interceptor;
/**
* Describes if the Site kit integration is enabled and configured.
*/
class Site_Kit {
private const SITE_KIT_FILE = 'google-site-kit/google-site-kit.php';
/**
* The Site Kit feature conditional.
*
* @var Google_Site_Kit_Feature_Conditional
*/
protected $site_kit_feature_conditional;
/**
* The Site Kit consent repository.
*
* @var Site_Kit_Consent_Repository_Interface
*/
private $site_kit_consent_repository;
/**
* The Site Kit consent repository.
*
* @var Configuration_Repository
*/
private $permanently_dismissed_site_kit_configuration_repository;
/**
* The call wrapper.
*
* @var Site_Kit_Is_Connected_Call $site_kit_is_connected_call
*/
private $site_kit_is_connected_call;
/**
* The search console module data.
*
* @var array<string, bool> $search_console_module
*/
private $search_console_module = [
'can_view' => null,
];
/**
* The analytics module data.
*
* @var array<string, bool> $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 Google_Site_Kit_Feature_Conditional $site_kit_feature_conditional The Site Kit feature 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,
Google_Site_Kit_Feature_Conditional $site_kit_feature_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_feature_conditional = $site_kit_feature_conditional;
}
/**
* If the integration is activated.
*
* @return bool If the integration is activated.
*/
public function is_enabled(): bool {
return \is_plugin_active( self::SITE_KIT_FILE );
}
/**
* 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<array|null> $module The module.
*
* @return bool If the user can read the data.
*/
private function can_read_data( array $module ): bool {
return ( ! \is_null( $module['can_view'] ) ? $module['can_view'] : false );
}
/**
* Return this object represented by a key value array.
*
* @return array<string, bool> Returns the name and if the feature is enabled.
*/
public function to_array(): array {
if ( ! $this->site_kit_feature_conditional->is_met() ) {
return [];
}
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<string, bool> 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<array|null> 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/application/score-results/seo-score-results/seo-score-results-repository.php 0000666 00000002211 15220430627 0025533 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Application\Score_Results\SEO_Score_Results;
use Yoast\WP\SEO\Dashboard\Application\Score_Results\Abstract_Score_Results_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results\Cached_SEO_Score_Results_Collector;
/**
* The repository to get SEO score results.
*/
class SEO_Score_Results_Repository extends Abstract_Score_Results_Repository {
/**
* The constructor.
*
* @param Cached_SEO_Score_Results_Collector $seo_score_results_collector The cached SEO score results collector.
* @param SEO_Score_Groups_Interface ...$seo_score_groups All SEO score groups.
*/
public function __construct(
Cached_SEO_Score_Results_Collector $seo_score_results_collector,
SEO_Score_Groups_Interface ...$seo_score_groups
) {
$this->score_results_collector = $seo_score_results_collector;
$this->score_groups = $seo_score_groups;
}
}
application/score-results/readability-score-results/readability-score-results-repository.php 0000666 00000002441 15220430627 0030667 0 ustar 00 dashboard <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Application\Score_Results\Readability_Score_Results;
use Yoast\WP\SEO\Dashboard\Application\Score_Results\Abstract_Score_Results_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Readability_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Readability_Score_Results\Cached_Readability_Score_Results_Collector;
/**
* The repository to get readability score results.
*/
class Readability_Score_Results_Repository extends Abstract_Score_Results_Repository {
/**
* The constructor.
*
* @param Cached_Readability_Score_Results_Collector $readability_score_results_collector The cached readability score results collector.
* @param Readability_Score_Groups_Interface ...$readability_score_groups All readability score groups.
*/
public function __construct(
Cached_Readability_Score_Results_Collector $readability_score_results_collector,
Readability_Score_Groups_Interface ...$readability_score_groups
) {
$this->score_results_collector = $readability_score_results_collector;
$this->score_groups = $readability_score_groups;
}
}
dashboard/application/filter-pairs/filter-pairs-repository.php 0000666 00000003053 15220430627 0020734 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\Filter_Pairs;
use Yoast\WP\SEO\Dashboard\Domain\Filter_Pairs\Filter_Pairs_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
use Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies\Taxonomies_Collector;
/**
* The repository to get hardcoded filter pairs.
*/
class Filter_Pairs_Repository {
/**
* The taxonomies collector.
*
* @var Taxonomies_Collector
*/
private $taxonomies_collector;
/**
* All filter pairs.
*
* @var Filter_Pairs_Interface[]
*/
private $filter_pairs;
/**
* The constructor.
*
* @param Taxonomies_Collector $taxonomies_collector The taxonomies collector.
* @param Filter_Pairs_Interface ...$filter_pairs All filter pairs.
*/
public function __construct(
Taxonomies_Collector $taxonomies_collector,
Filter_Pairs_Interface ...$filter_pairs
) {
$this->taxonomies_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/score-groups/seo-score-groups/seo-score-groups-repository.php 0000666 00000003050 15220430627 0025007 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\Score_Groups\SEO_Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\No_SEO_Score_Group;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
/**
* The repository to get SEO score groups.
*/
class SEO_Score_Groups_Repository {
/**
* All SEO score groups.
*
* @var SEO_Score_Groups_Interface[]
*/
private $seo_score_groups;
/**
* The constructor.
*
* @param SEO_Score_Groups_Interface ...$seo_score_groups All SEO score groups.
*/
public function __construct( SEO_Score_Groups_Interface ...$seo_score_groups ) {
$this->seo_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/endpoints/endpoints-repository.php 0000666 00000001725 15220430627 0017744 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Endpoints;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_List;
/**
* Repository for endpoints.
*/
class Endpoints_Repository {
/**
* Holds the endpoints.
*
* @var array<Endpoint_Interface>
*/
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/taxonomies/taxonomies-repository.php 0000666 00000003727 15220430627 0020316 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\Taxonomies;
use Yoast\WP\SEO\Dashboard\Application\Filter_Pairs\Filter_Pairs_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
use Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies\Taxonomies_Collector;
/**
* The repository to get taxonomies.
*/
class Taxonomies_Repository {
/**
* The taxonomies collector.
*
* @var Taxonomies_Collector
*/
private $taxonomies_collector;
/**
* The filter pairs repository.
*
* @var Filter_Pairs_Repository
*/
private $filter_pairs_repository;
/**
* The constructor.
*
* @param Taxonomies_Collector $taxonomies_collector The taxonomies collector.
* @param Filter_Pairs_Repository $filter_pairs_repository The filter pairs repository.
*/
public function __construct(
Taxonomies_Collector $taxonomies_collector,
Filter_Pairs_Repository $filter_pairs_repository
) {
$this->taxonomies_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/configuration/dashboard-configuration.php 0000666 00000012772 15220430627 0021170 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\Configuration;
use Yoast\WP\SEO\Dashboard\Application\Content_Types\Content_Types_Repository;
use Yoast\WP\SEO\Dashboard\Application\Endpoints\Endpoints_Repository;
use Yoast\WP\SEO\Dashboard\Application\Tracking\Setup_Steps_Tracking;
use Yoast\WP\SEO\Dashboard\Infrastructure\Browser_Cache\Browser_Cache_Configuration;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
use Yoast\WP\SEO\Dashboard\Infrastructure\Nonces\Nonce_Repository;
use Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository;
use Yoast\WP\SEO\Editors\Framework\Keyphrase_Analysis;
use Yoast\WP\SEO\Editors\Framework\Readability_Analysis;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
/**
* Responsible for the dashboard configuration.
*/
class Dashboard_Configuration {
/**
* The content types repository.
*
* @var Content_Types_Repository
*/
private $content_types_repository;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* The user helper.
*
* @var User_Helper
*/
private $user_helper;
/**
* The repository.
*
* @var Enabled_Analysis_Features_Repository
*/
private $enabled_analysis_features_repository;
/**
* The endpoints repository.
*
* @var Endpoints_Repository
*/
private $endpoints_repository;
/**
* The nonce repository.
*
* @var Nonce_Repository
*/
private $nonce_repository;
/**
* The Site Kit integration data.
*
* @var Site_Kit
*/
private $site_kit_integration_data;
/**
* The setup steps tracking data.
*
* @var Setup_Steps_Tracking
*/
private $setup_steps_tracking;
/**
* The browser cache configuration.
*
* @var Browser_Cache_Configuration
*/
private $browser_cache_configuration;
/**
* The constructor.
*
* @param Content_Types_Repository $content_types_repository The content types repository.
* @param Indexable_Helper $indexable_helper The indexable helper
* repository.
* @param User_Helper $user_helper The user helper.
* @param Enabled_Analysis_Features_Repository $enabled_analysis_features_repository The analysis feature.
* repository.
* @param Endpoints_Repository $endpoints_repository The endpoints repository.
* @param Nonce_Repository $nonce_repository The nonce repository.
* @param Site_Kit $site_kit_integration_data The Site Kit integration data.
* @param Setup_Steps_Tracking $setup_steps_tracking The setup steps tracking data.
* @param Browser_Cache_Configuration $browser_cache_configuration The browser cache configuration.
*/
public function __construct(
Content_Types_Repository $content_types_repository,
Indexable_Helper $indexable_helper,
User_Helper $user_helper,
Enabled_Analysis_Features_Repository $enabled_analysis_features_repository,
Endpoints_Repository $endpoints_repository,
Nonce_Repository $nonce_repository,
Site_Kit $site_kit_integration_data,
Setup_Steps_Tracking $setup_steps_tracking,
Browser_Cache_Configuration $browser_cache_configuration
) {
$this->content_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<string, array<string>|array<string, string|array<string, array<string, int>>>>
*/
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/traffic/organic-sessions-daily-repository.php 0000666 00000004122 15220430627 0021734 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Traffic;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Dashboard_Repository_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
use Yoast\WP\SEO\Dashboard\Domain\Time_Based_Seo_Metrics\Data_Source_Not_Available_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4\Site_Kit_Analytics_4_Adapter;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
/**
* The data provider for daily organic sessions data.
*/
class Organic_Sessions_Daily_Repository implements Dashboard_Repository_Interface {
/**
* The adapter.
*
* @var Site_Kit_Analytics_4_Adapter
*/
private $site_kit_analytics_4_adapter;
/**
* The site kit configuration object.
*
* @var Site_Kit
*/
private $site_kit_configuration;
/**
* The constructor.
*
* @param Site_Kit_Analytics_4_Adapter $site_kit_analytics_4_adapter The adapter.
* @param Site_Kit $site_kit_configuration The site kit configuration object.
*/
public function __construct(
Site_Kit_Analytics_4_Adapter $site_kit_analytics_4_adapter,
Site_Kit $site_kit_configuration
) {
$this->site_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.php 0000666 00000004142 15220430627 0022262 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Traffic;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Dashboard_Repository_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
use Yoast\WP\SEO\Dashboard\Domain\Time_Based_Seo_Metrics\Data_Source_Not_Available_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4\Site_Kit_Analytics_4_Adapter;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
/**
* The data provider for comparison organic sessions data.
*/
class Organic_Sessions_Compare_Repository implements Dashboard_Repository_Interface {
/**
* The adapter.
*
* @var Site_Kit_Analytics_4_Adapter
*/
private $site_kit_analytics_4_adapter;
/**
* The site kit configuration object.
*
* @var Site_Kit
*/
private $site_kit_configuration;
/**
* The constructor.
*
* @param Site_Kit_Analytics_4_Adapter $site_kit_analytics_4_adapter The adapter.
* @param Site_Kit $site_kit_configuration The site kit configuration object.
*/
public function __construct(
Site_Kit_Analytics_4_Adapter $site_kit_analytics_4_adapter,
Site_Kit $site_kit_configuration
) {
$this->site_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/domain/content-types/content-type.php 0000666 00000003364 15220430627 0015733 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Content_Types;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
/**
* This class describes a Content Type.
*/
class Content_Type {
/**
* The name of the content type.
*
* @var string
*/
private $name;
/**
* The label of the content type.
*
* @var string
*/
private $label;
/**
* The taxonomy that filters the content type.
*
* @var Taxonomy
*/
private $taxonomy;
/**
* The constructor.
*
* @param string $name The name of the content type.
* @param string $label The label of the content type.
* @param Taxonomy|null $taxonomy The taxonomy that filters the content type.
*/
public function __construct( string $name, string $label, ?Taxonomy $taxonomy = null ) {
$this->name = $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/content-types/content-types-list.php 0000666 00000002570 15220430627 0017065 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Content_Types;
/**
* This class describes a list of content types.
*/
class Content_Types_List {
/**
* The content types.
*
* @var array<Content_Type>
*/
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<Content_Type> 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<array<string, array<string, array<string, array<string, string|null>>>>> 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/traffic/traffic-data.php 0000666 00000002331 15220430627 0014422 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Traffic;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
/**
* Domain object that represents a single Traffic record.
*/
class Traffic_Data implements Data_Interface {
/**
* The sessions, if any.
*
* @var int|null
*/
private $sessions;
/**
* The total users, if any.
*
* @var int|null
*/
private $total_users;
/**
* The array representation of this domain object.
*
* @return array<string, int>
*/
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/search-rankings/comparison-search-ranking-data.php 0000666 00000005153 15220430627 0021516 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Search_Rankings;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
/**
* Domain object that represents a Comparison Search Ranking record.
*/
class Comparison_Search_Ranking_Data implements Data_Interface {
/**
* The current search ranking data.
*
* @var Search_Ranking_Data[]
*/
private $current_search_ranking_data = [];
/**
* The previous search ranking data.
*
* @var Search_Ranking_Data[]
*/
private $previous_search_ranking_data = [];
/**
* Sets the current search ranking data.
*
* @param Search_Ranking_Data $current_search_ranking_data The current search ranking data.
*
* @return void
*/
public function add_current_traffic_data( Search_Ranking_Data $current_search_ranking_data ): void {
\array_push( $this->current_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<array<string, int>>
*/
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<string, int> 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.php 0000666 00000004675 15220430627 0017356 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Search_Rankings;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
/**
* Domain object that represents a single Search Ranking Data record.
*/
class Search_Ranking_Data implements Data_Interface {
/**
* The amount of clicks a `subject` gets.
*
* @var int
*/
private $clicks;
/**
* The click-through rate a `subject` gets.
*
* @var float
*/
private $ctr;
/**
* The amount of impressions a `subject` gets.
*
* @var int
*/
private $impressions;
/**
* The average position for the given `subject`.
*
* @var float
*/
private $position;
/**
* In the context of this domain object subject can represent a `URI` or a `search term`
*
* @var string
*/
private $subject;
/**
* The constructor.
*
* @param int $clicks The clicks.
* @param float $ctr The ctr.
* @param int $impressions The impressions.
* @param float $position The position.
* @param string $subject The subject of the data.
*/
public function __construct( int $clicks, float $ctr, int $impressions, float $position, string $subject ) {
$this->clicks = $clicks;
$this->ctr = $ctr;
$this->impressions = $impressions;
$this->position = $position;
$this->subject = $subject;
}
/**
* The array representation of this domain object.
*
* @return array<string|float|int|string[]>
*/
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/search-rankings/top-page-data.php 0000666 00000003501 15220430627 0016161 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Search_Rankings;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
/**
* Domain object that represents a single Top Page Data record.
*/
class Top_Page_Data implements Data_Interface {
/**
* The search ranking data for the top page.
*
* @var Search_Ranking_Data
*/
private $search_ranking_data;
/**
* The SEO score group the top page belongs to.
*
* @var SEO_Score_Groups_Interface
*/
private $seo_score_group;
/**
* The edit link of the top page.
*
* @var string
*/
private $edit_link;
/**
* The constructor.
*
* @param Search_Ranking_Data $search_ranking_data The search ranking data for the top page.
* @param SEO_Score_Groups_Interface $seo_score_group The SEO score group the top page belongs to.
* @param string $edit_link The edit link of the top page.
*/
public function __construct(
Search_Ranking_Data $search_ranking_data,
SEO_Score_Groups_Interface $seo_score_group,
?string $edit_link = null
) {
$this->search_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<string|float|int|string[]>
*/
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/taxonomies/taxonomy.php 0000666 00000002522 15220430627 0014525 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Taxonomies;
/**
* This class describes a Taxonomy.
*/
class Taxonomy {
/**
* The name of the taxonomy.
*
* @var string
*/
private $name;
/**
* The label of the taxonomy.
*
* @var string
*/
private $label;
/**
* The REST URL of the taxonomy.
*
* @var string
*/
private $rest_url;
/**
* The constructor.
*
* @param string $name The name of the taxonomy.
* @param string $label The label of the taxonomy.
* @param string $rest_url The REST URL of the taxonomy.
*/
public function __construct(
string $name,
string $label,
string $rest_url
) {
$this->name = $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<string, array<string, string>> 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/endpoint/endpoint-interface.php 0000666 00000001056 15220430627 0016060 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Endpoint;
interface Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string;
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string;
/**
* Gets the route.
*
* @return string
*/
public function get_route(): string;
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string;
}
dashboard/domain/endpoint/endpoint-list.php 0000666 00000001501 15220430627 0015066 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Endpoint;
/**
* List of endpoints.
*/
class Endpoint_List {
/**
* Holds the endpoints.
*
* @var array<Endpoint_Interface>
*/
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<string, string> 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/score-results/current-scores-list.php 0000666 00000002535 15220430627 0017226 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Results;
/**
* This class describes a list of current scores.
*/
class Current_Scores_List {
/**
* The current scores.
*
* @var Current_Score[]
*/
private $current_scores = [];
/**
* Adds a current score to the list.
*
* @param Current_Score $current_score The current score to add.
* @param int $position The position to add the current score.
*
* @return void
*/
public function add( Current_Score $current_score, int $position ): void {
$this->current_scores[ $position ] = $current_score;
}
/**
* Parses the current score list to the expected key value representation.
*
* @return array<array<string, string|int|array<string, string>>> 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/filter-pairs/filter-pairs-interface.php 0000666 00000000770 15220430627 0017424 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Filter_Pairs;
/**
* This interface describes a Filter Pair implementation.
*/
interface Filter_Pairs_Interface {
/**
* Gets the filtering taxonomy.
*
* @return string
*/
public function get_filtering_taxonomy(): string;
/**
* Gets the filtered content type.
*
* @return string
*/
public function get_filtered_content_type(): string;
}
dashboard/domain/score-groups/seo-score-groups/seo-score-groups-interface.php 0000666 00000000533 15220430627 0023477 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface;
/**
* This interface describes an SEO score group implementation.
*/
interface SEO_Score_Groups_Interface extends Score_Groups_Interface {}
dashboard/domain/score-groups/seo-score-groups/abstract-seo-score-group.php 0000666 00000001350 15220430627 0023155 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Abstract_Score_Group;
/**
* Abstract class for an SEO score group.
*/
abstract class Abstract_SEO_Score_Group extends Abstract_Score_Group implements SEO_Score_Groups_Interface {
/**
* Gets the key of the SEO score group that is used when filtering on the posts page.
*
* @return string The name of the SEO score group that is used when filtering on the posts page.
*/
public function get_filter_key(): string {
return 'seo_filter';
}
}
dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php 0000666 00000002465 15220430627 0022110 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
/**
* This class describes a bad SEO score group.
*/
class Bad_SEO_Score_Group extends Abstract_SEO_Score_Group {
/**
* Gets the name of the SEO score group.
*
* @return string The name of the SEO score group.
*/
public function get_name(): string {
return 'bad';
}
/**
* Gets the value of the SEO score group that is used when filtering on the posts page.
*
* @return string The name of the SEO score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'bad';
}
/**
* Gets the position of the SEO score group.
*
* @return int The position of the SEO score group.
*/
public function get_position(): int {
return 2;
}
/**
* Gets the minimum score of the SEO score group.
*
* @return int|null The minimum score of the SEO score group.
*/
public function get_min_score(): ?int {
return 1;
}
/**
* Gets the maximum score of the SEO score group.
*
* @return int|null The maximum score of the SEO score group.
*/
public function get_max_score(): ?int {
return 40;
}
}
dashboard/domain/score-groups/seo-score-groups/good-seo-score-group.php 0000666 00000002473 15220430627 0022311 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
/**
* This class describes a good SEO score group.
*/
class Good_SEO_Score_Group extends Abstract_SEO_Score_Group {
/**
* Gets the name of the SEO score group.
*
* @return string The name of the SEO score group.
*/
public function get_name(): string {
return 'good';
}
/**
* Gets the value of the SEO score group that is used when filtering on the posts page.
*
* @return string The name of the SEO score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'good';
}
/**
* Gets the position of the SEO score group.
*
* @return int The position of the SEO score group.
*/
public function get_position(): int {
return 0;
}
/**
* Gets the minimum score of the SEO score group.
*
* @return int|null The minimum score of the SEO score group.
*/
public function get_min_score(): ?int {
return 71;
}
/**
* Gets the maximum score of the SEO score group.
*
* @return int|null The maximum score of the SEO score group.
*/
public function get_max_score(): ?int {
return 100;
}
}
dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php 0000666 00000002645 15220430627 0025316 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
/**
* This class describes a bad readability score group.
*/
class Bad_Readability_Score_Group extends Abstract_Readability_Score_Group {
/**
* Gets the name of the readability score group.
*
* @return string The name of the readability score group.
*/
public function get_name(): string {
return 'bad';
}
/**
* Gets the value of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'bad';
}
/**
* Gets the position of the readability score group.
*
* @return int The position of the readability score group.
*/
public function get_position(): int {
return 2;
}
/**
* Gets the minimum score of the readability score group.
*
* @return int|null The minimum score of the readability score group.
*/
public function get_min_score(): ?int {
return 1;
}
/**
* Gets the maximum score of the readability score group.
*
* @return int|null The maximum score of the readability score group.
*/
public function get_max_score(): ?int {
return 40;
}
}
dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php 0000666 00000002664 15220430627 0025205 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
/**
* This class describes a missing readability score group.
*/
class No_Readability_Score_Group extends Abstract_Readability_Score_Group {
/**
* Gets the name of the readability score group.
*
* @return string The name of the readability score group.
*/
public function get_name(): string {
return 'notAnalyzed';
}
/**
* Gets the value of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'na';
}
/**
* Gets the position of the readability score group.
*
* @return int The position of the readability score group.
*/
public function get_position(): int {
return 3;
}
/**
* Gets the minimum score of the readability score group.
*
* @return int|null The minimum score of the readability score group.
*/
public function get_min_score(): ?int {
return null;
}
/**
* Gets the maximum score of the readability score group.
*
* @return int|null The maximum score of the readability score group.
*/
public function get_max_score(): ?int {
return null;
}
}
dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php 0000666 00000002647 15220430627 0025203 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
/**
* This class describes an OK readability score group.
*/
class Ok_Readability_Score_Group extends Abstract_Readability_Score_Group {
/**
* Gets the name of the readability score group.
*
* @return string The the name of the readability score group.
*/
public function get_name(): string {
return 'ok';
}
/**
* Gets the value of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'ok';
}
/**
* Gets the position of the readability score group.
*
* @return int The position of the readability score group.
*/
public function get_position(): int {
return 1;
}
/**
* Gets the minimum score of the readability score group.
*
* @return int|null The minimum score of the readability score group.
*/
public function get_min_score(): ?int {
return 41;
}
/**
* Gets the maximum score of the readability score group.
*
* @return int|null The maximum score of the readability score group.
*/
public function get_max_score(): ?int {
return 70;
}
}
dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php 0000666 00000001437 15220430627 0026371 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Abstract_Score_Group;
/**
* Abstract class for a readability score group.
*/
abstract class Abstract_Readability_Score_Group extends Abstract_Score_Group implements Readability_Score_Groups_Interface {
/**
* Gets the key of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_key(): string {
return 'readability_filter';
}
}
dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php 0000666 00000002653 15220430627 0025517 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
/**
* This class describes a good readability score group.
*/
class Good_Readability_Score_Group extends Abstract_Readability_Score_Group {
/**
* Gets the name of the readability score group.
*
* @return string The name of the readability score group.
*/
public function get_name(): string {
return 'good';
}
/**
* Gets the value of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'good';
}
/**
* Gets the position of the readability score group.
*
* @return int The position of the readability score group.
*/
public function get_position(): int {
return 0;
}
/**
* Gets the minimum score of the readability score group.
*
* @return int|null The minimum score of the readability score group.
*/
public function get_min_score(): ?int {
return 71;
}
/**
* Gets the maximum score of the readability score group.
*
* @return int|null The maximum score of the readability score group.
*/
public function get_max_score(): ?int {
return 100;
}
}
dashboard/user-interface/time-based-seo-metrics/time-based-seo-metrics-route.php 0000666 00000024526 15220430627 0023776 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Time_Based_SEO_Metrics;
use DateTime;
use DateTimeZone;
use Exception;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Site_Kit_Conditional;
use Yoast\WP\SEO\Dashboard\Application\Search_Rankings\Search_Ranking_Compare_Repository;
use Yoast\WP\SEO\Dashboard\Application\Search_Rankings\Top_Page_Repository;
use Yoast\WP\SEO\Dashboard\Application\Search_Rankings\Top_Query_Repository;
use Yoast\WP\SEO\Dashboard\Application\Traffic\Organic_Sessions_Compare_Repository;
use Yoast\WP\SEO\Dashboard\Application\Traffic\Organic_Sessions_Daily_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
use Yoast\WP\SEO\Dashboard\Domain\Time_Based_SEO_Metrics\Repository_Not_Found_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4\Analytics_4_Parameters;
use Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console\Search_Console_Parameters;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Abstract scores route.
*/
final class Time_Based_SEO_Metrics_Route implements Route_Interface {
/**
* The namespace of the route.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix of the route.
*
* @var string
*/
public const ROUTE_NAME = '/time_based_seo_metrics';
/**
* The data provider for page based search rankings.
*
* @var Top_Page_Repository
*/
private $top_page_repository;
/**
* The data provider for query based search rankings.
*
* @var Top_Query_Repository
*/
private $top_query_repository;
/**
* The data provider for comparison organic session traffic.
*
* @var Organic_Sessions_Compare_Repository
*/
private $organic_sessions_compare_repository;
/**
* The data provider for daily organic session traffic.
*
* @var Organic_Sessions_Daily_Repository
*/
private $organic_sessions_daily_repository;
/**
* The data provider for searching ranking comparison.
*
* @var Search_Ranking_Compare_Repository
*/
private $search_ranking_compare_repository;
/**
* Holds the capability helper instance.
*
* @var Capability_Helper
*/
private $capability_helper;
/**
* Returns the needed conditionals.
*
* @return array<string> The conditionals that must be met to load this.
*/
public static function get_conditionals(): array {
return [ Google_Site_Kit_Feature_Conditional::class, 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' );
}
}
introductions/user-interface/introductions-integration.php 0000666 00000015472 15220430627 0020340 0 ustar 00 <?php
namespace Yoast\WP\SEO\Introductions\User_Interface;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Introductions\Application\Current_Page_Trait;
use Yoast\WP\SEO\Introductions\Application\Introductions_Collector;
use Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository;
use Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository;
/**
* Loads introduction modal scripts, when there are applicable introductions.
*/
class Introductions_Integration implements Integration_Interface {
use Current_Page_Trait;
public const SCRIPT_HANDLE = 'introductions';
/**
* Holds the admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $admin_asset_manager;
/**
* Holds the introduction collector.
*
* @var Introductions_Collector
*/
private $introductions_collector;
/**
* Holds the product helper.
*
* @var Product_Helper
*/
private $product_helper;
/**
* Holds the user helper.
*
* @var User_Helper
*/
private $user_helper;
/**
* Holds the short link helper.
*
* @var Short_Link_Helper
*/
private $short_link_helper;
/**
* Holds the repository.
*
* @var Wistia_Embed_Permission_Repository
*/
private $wistia_embed_permission_repository;
/**
* Holds the WooCommerce conditional.
*
* @var WooCommerce_Conditional
*/
private $woocommerce_conditional;
/**
* Returns the conditionals based in which this loadable should be active.
*
* In this case: when on an admin page.
*
* @return array<string>
*/
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/infrastructure/introductions-seen-repository.php 0000666 00000007362 15220430627 0021327 0 ustar 00 <?php
namespace Yoast\WP\SEO\Introductions\Infrastructure;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Introductions\Domain\Invalid_User_Id_Exception;
/**
* Stores and retrieves whether the user has seen certain introductions.
*/
class Introductions_Seen_Repository {
public const USER_META_KEY = '_yoast_wpseo_introductions';
public const DEFAULT_VALUE = [];
/**
* Holds the User_Helper instance.
*
* @var User_Helper
*/
private $user_helper;
/**
* Constructs the class.
*
* @param User_Helper $user_helper The User_Helper.
*/
public function __construct( User_Helper $user_helper ) {
$this->user_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/domain/introductions-bucket.php 0000666 00000001537 15220430627 0015622 0 ustar 00 <?php
namespace Yoast\WP\SEO\Introductions\Domain;
/**
* A collection domain object.
*/
class Introductions_Bucket {
/**
* Holds the introductions.
*
* @var Introduction_Item[]
*/
private $introductions;
/**
* The constructor.
*/
public function __construct() {
$this->introductions = [];
}
/**
* 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-item.php 0000666 00000001745 15220430627 0015121 0 ustar 00 <?php
namespace Yoast\WP\SEO\Introductions\Domain;
/**
* Domain object that holds introduction information.
*/
class Introduction_Item {
/**
* The ID.
*
* @var string
*/
private $id;
/**
* The priority.
*
* @var int
*/
private $priority;
/**
* Constructs the instance.
*
* @param string $id The ID.
* @param int $priority The priority.
*/
public function __construct( $id, $priority ) {
$this->id = $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/domain/introduction-interface.php 0000666 00000001126 15220430627 0016114 0 ustar 00 <?php
namespace Yoast\WP\SEO\Introductions\Domain;
/**
* Represents an introduction.
*/
interface Introduction_Interface {
/**
* Returns the ID.
*
* @return string
*/
public function get_id();
/**
* Returns the unique name.
*
* @deprecated 21.6
* @codeCoverageIgnore
*
* @return string
*/
public function get_name();
/**
* Returns the requested pagination priority. Lower means earlier.
*
* @return int
*/
public function get_priority();
/**
* Returns whether this introduction should show.
*
* @return bool
*/
public function should_show();
}
introductions/application/black-friday-announcement.php 0000666 00000004200 15220430627 0017505 0 ustar 00 <?php
namespace Yoast\WP\SEO\Introductions\Application;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Introductions\Domain\Introduction_Interface;
use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;
/**
* Represents the introduction for the Black Friday announcement.
*/
class Black_Friday_Announcement implements Introduction_Interface {
public const ID = 'black-friday-announcement';
/**
* Holds the current page helper.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* Holds the promotion manager.
*
* @var Promotion_Manager
*/
private $promotion_manager;
/**
* Holds the product helper.
*
* @var Product_Helper
*/
private $product_helper;
/**
* Constructs the introduction.
*
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Promotion_Manager $promotion_manager The promotion manager.
* @param Product_Helper $product_helper The product helper.
*/
public function __construct(
Current_Page_Helper $current_page_helper,
Promotion_Manager $promotion_manager,
Product_Helper $product_helper
) {
$this->current_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 name of the introduction.
*
* @return string The name.
*/
public function get_name() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 21.6', 'Please use get_id() instead' );
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/application/version-trait.php 0000666 00000001141 15220430627 0015274 0 ustar 00 <?php
namespace Yoast\WP\SEO\Introductions\Application;
trait Version_Trait {
/**
* Determines whether the version is between a min (inclusive) and max (exclusive).
*
* @param string $version The version to compare.
* @param string $min_version The minimum version.
* @param string $max_version The maximum version.
*
* @return bool Whether the version is between a min and max.
*/
private function is_version_between( $version, $min_version, $max_version ) {
return ( \version_compare( $version, $min_version, '>=' )
&& \version_compare( $version, $max_version, '<' )
);
}
}
introductions/application/introductions-collector.php 0000666 00000006757 15220430627 0017400 0 ustar 00 <?php
namespace Yoast\WP\SEO\Introductions\Application;
use Yoast\WP\SEO\Introductions\Domain\Introduction_Interface;
use Yoast\WP\SEO\Introductions\Domain\Introduction_Item;
use Yoast\WP\SEO\Introductions\Domain\Introductions_Bucket;
use Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository;
/**
* Manages the collection of introductions.
*/
class Introductions_Collector {
/**
* Holds all the introductions.
*
* @var Introduction_Interface[]
*/
private $introductions;
/**
* Constructs the collector.
*
* @param Introduction_Interface ...$introductions All the introductions.
*/
public function __construct( Introduction_Interface ...$introductions ) {
$this->introductions = $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/delayed-premium-upsell.php 0000666 00000012251 15220430627 0017057 0 ustar 00 <?php
namespace Yoast\WP\SEO\Introductions\Application;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Introductions\Domain\Introduction_Interface;
use Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository;
/**
* Represents the Premium upsell that shows on Yoast SEO pages after a delay.
*/
class Delayed_Premium_Upsell implements Introduction_Interface {
public const ID = 'delayed-premium-upsell';
public const DELAY_DAYS = 14;
/**
* Holds the repository.
*
* @var Introductions_Seen_Repository
*/
private $introductions_seen_repository;
/**
* Holds the product helper.
*
* @var Product_Helper
*/
private $product_helper;
/**
* Holds the current page helper.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Delayed_Premium_Upsell constructor.
*
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Introductions_Seen_Repository $introductions_seen_repository The introductions seen repository.
* @param Options_Helper $options_helper The options helper.
* @param Product_Helper $product_helper The product helper.
*/
public function __construct( Current_Page_Helper $current_page_helper, Introductions_Seen_Repository $introductions_seen_repository, Options_Helper $options_helper, Product_Helper $product_helper ) {
$this->current_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 name of the introduction.
*
* @return string The name.
*/
public function get_name(): string {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 21.6', 'Please use get_id() instead' );
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;
}
}
exceptions/addon-installation/user-cannot-install-plugins-exception.php 0000666 00000000306 15220430627 0022603 0 ustar 00 <?php
namespace Yoast\WP\SEO\Exceptions\Addon_Installation;
use Exception;
/**
* Class User_Cannot_Install_Plugins_Exception
*/
class User_Cannot_Install_Plugins_Exception extends Exception {}
exceptions/addon-installation/addon-installation-error-exception.php 0000666 00000000266 15220430627 0022142 0 ustar 00 <?php
namespace Yoast\WP\SEO\Exceptions\Addon_Installation;
use Exception;
/**
* Class Addon_Installation_Error
*/
class Addon_Installation_Error_Exception extends Exception {}
exceptions/importing/aioseo-validation-exception.php 0000666 00000000653 15220430627 0017062 0 ustar 00 <?php
namespace Yoast\WP\SEO\Exceptions\Importing;
use Exception;
/**
* Class Aioseo_Validation_Exception
*/
class Aioseo_Validation_Exception extends Exception {
/**
* Exception that is thrown whenever validation of the
* AIOSEO data structure has failed.
*/
public function __construct() {
parent::__construct( \esc_html__( 'The validation of the AIOSEO data structure has failed.', 'wordpress-seo' ) );
}
}
exceptions/oauth/authentication-failed-exception.php 0000666 00000001340 15220430627 0017016 0 ustar 00 <?php
namespace Yoast\WP\SEO\Exceptions\OAuth;
use Exception;
/**
* Class Authentication_Failed_Exception
*/
class Authentication_Failed_Exception extends Exception {
/**
* Authentication_Failed_Exception constructor.
*
* @param Exception $original_exception The original exception.
*/
public function __construct( Exception $original_exception ) {
parent::__construct( 'Authentication failed', 401, $original_exception );
}
/**
* Returns a formatted response object.
*
* @return object The response object.
*/
public function get_response() {
return (object) [
'tokens' => [],
'error' => $this->getMessage() . ': ' . $this->getPrevious()->getMessage(),
'status' => $this->getCode(),
];
}
}
exceptions/indexable/indexable-exception.php 0000666 00000000244 15220430627 0015325 0 ustar 00 <?php
namespace Yoast\WP\SEO\Exceptions\Indexable;
use Exception;
/**
* Class Indexable_Exception
*/
abstract class Indexable_Exception extends Exception {
}
exceptions/indexable/author-not-built-exception.php 0000666 00000003141 15220430627 0016606 0 ustar 00 <?php
namespace Yoast\WP\SEO\Exceptions\Indexable;
/**
* For when an author indexable is not being built.
*/
class Author_Not_Built_Exception extends Not_Built_Exception {
/**
* Named constructor for creating an Author_Not_Built_Exception
* when author archives are disabled for users without posts.
*
* @param string $user_id The user id.
*
* @return Author_Not_Built_Exception The exception.
*/
public static function author_archives_are_not_indexed_for_users_without_posts( $user_id ) {
return new self(
'Indexable for author with id ' . $user_id . ' is not being built, since author archives are not indexed for users without posts.'
);
}
/**
* Named constructor for creating an Author_Not_Built_Exception
* when author archives are disabled.
*
* @param string $user_id The user id.
*
* @return Author_Not_Built_Exception The exception.
*/
public static function author_archives_are_disabled( $user_id ) {
return new self(
'Indexable for author with id ' . $user_id . ' is not being built, since author archives are disabled.'
);
}
/**
* Named constructor for creating an Author_Not_Build_Exception
* when an author is excluded because of the `'wpseo_should_build_and_save_user_indexable'` filter.
*
* @param string $user_id The user id.
*
* @return Author_Not_Built_Exception The exception.
*/
public static function author_not_built_because_of_filter( $user_id ) {
return new self(
'Indexable for author with id ' . $user_id . ' is not being built, since it is excluded because of the \'wpseo_should_build_and_save_user_indexable\' filter.'
);
}
}
exceptions/indexable/post-type-not-built-exception.php 0000666 00000001232 15220430627 0017247 0 ustar 00 <?php
namespace Yoast\WP\SEO\Exceptions\Indexable;
/**
* Exception that is thrown whenever a post type could not be built
* in the context of the indexables.
*/
class Post_Type_Not_Built_Exception extends Not_Built_Exception {
/**
* Throws an exception if the post is not indexable.
*
* @param string $post_type The post type.
*
* @return Post_Type_Not_Built_Exception
*/
public static function because_not_indexable( $post_type ) {
/* translators: %s: expands to the post type */
return new self( \sprintf( \__( 'The post type %s could not be indexed because it does not meet indexing requirements.', 'wordpress-seo' ), $post_type ) );
}
}
exceptions/indexable/source-exception.php 0000666 00000000231 15220430627 0014666 0 ustar 00 <?php
namespace Yoast\WP\SEO\Exceptions\Indexable;
/**
* Class Indexable_Source_Exception
*/
class Source_Exception extends Indexable_Exception {
}
exceptions/forbidden-property-mutation-exception.php 0000666 00000002232 15220430627 0017112 0 ustar 00 <?php
namespace Yoast\WP\SEO\Exceptions;
use RuntimeException;
/**
* Exception for attempting a mutation on properties that are made readonly through magic getters and setters.
*/
class Forbidden_Property_Mutation_Exception extends RuntimeException {
/**
* Creates a Forbidden_Property_Mutation_Exception exception when an attempt is made
* to assign a value to an immutable property.
*
* @param string $property_name The name of the immutable property.
*
* @return Forbidden_Property_Mutation_Exception The exception.
*/
public static function cannot_set_because_property_is_immutable( $property_name ) {
return new self( \sprintf( 'Setting property $%s is not supported.', $property_name ) );
}
/**
* Creates a Forbidden_Property_Mutation_Exception exception when an attempt is made to unset an immutable property.
*
* @param string $property_name The name of the immutable property.
*
* @return Forbidden_Property_Mutation_Exception The exception.
*/
public static function cannot_unset_because_property_is_immutable( $property_name ) {
return new self( \sprintf( 'Unsetting property $%s is not supported.', $property_name ) );
}
}
elementor/infrastructure/request-post.php 0000666 00000011161 15220430627 0015007 0 ustar 00 <?php
namespace Yoast\WP\SEO\Elementor\Infrastructure;
use WP_Post;
/**
* Retrieve the WP_Post from the request.
*/
class Request_Post {
/**
* Retrieves the WP_Post, applicable to the current request.
*
* @return WP_Post|null
*/
public function get_post(): ?WP_Post {
return \get_post( $this->get_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;
}
}
helpers/twitter/image-helper.php 0000666 00000002312 15220430627 0012763 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers\Twitter;
use Yoast\WP\SEO\Helpers\Image_Helper as Base_Image_Helper;
/**
* A helper object for Twitter images.
*/
class Image_Helper {
/**
* The base image helper.
*
* @var Base_Image_Helper
*/
private $image;
/**
* Image_Helper constructor.
*
* @codeCoverageIgnore
*
* @param Base_Image_Helper $image The image helper.
*/
public function __construct( Base_Image_Helper $image ) {
$this->image = $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/social-profiles-helper.php 0000666 00000025356 15220430627 0013307 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
/**
* Class Social_Profiles_Helper.
*/
class Social_Profiles_Helper {
/**
* The fields for the person social profiles payload.
*
* @var array
*/
private $person_social_profile_fields = [
'facebook' => '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<string, string> 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/date-helper.php 0000666 00000006210 15220430627 0011115 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
use DateTime;
use DateTimeZone;
use Exception;
/**
* A helper object for dates.
*/
class Date_Helper {
/**
* Convert given date string to the W3C format.
*
* If $translate is true then the given date and format string will
* be passed to date_i18n() for translation.
*
* @param string $date Date string to convert.
* @param bool $translate Whether the return date should be translated. Default false.
*
* @return string Formatted date string.
*/
public function mysql_date_to_w3c_format( $date, $translate = false ) {
return \mysql2date( \DATE_W3C, $date, $translate );
}
/**
* Formats a given date in UTC TimeZone format.
*
* @param string $date String representing the date / time.
* @param string $format The format that the passed date should be in.
*
* @return string The formatted date.
*/
public function format( $date, $format = \DATE_W3C ) {
if ( ! \is_string( $date ) ) {
return $date;
}
$immutable_date = \date_create_immutable_from_format( 'Y-m-d H:i:s', $date, new DateTimeZone( 'UTC' ) );
if ( ! $immutable_date ) {
return $date;
}
return $immutable_date->format( $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/aioseo-helper.php 0000666 00000002362 15220430627 0011463 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
use wpdb;
/**
* The AIOSEO Helper.
*/
class Aioseo_Helper {
/**
* The WordPress database instance.
*
* @var wpdb
*/
protected $wpdb;
/**
* The wpdb helper.
*
* @var Wpdb_Helper
*/
protected $wpdb_helper;
/**
* Class constructor.
*
* @param wpdb $wpdb The WordPress database instance.
* @param Wpdb_Helper $wpdb_helper The wpdb helper.
*/
public function __construct( wpdb $wpdb, Wpdb_Helper $wpdb_helper ) {
$this->wpdb = $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/string-helper.php 0000666 00000002302 15220430627 0011504 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
/**
* A helper object for string operations.
*/
class String_Helper {
/**
* Strips all HTML tags including script and style.
*
* @param string $text The text to strip the tags from.
*
* @return string The processed string.
*/
public function strip_all_tags( $text ) {
return \wp_strip_all_tags( $text );
}
/**
* Standardize whitespace in a string.
*
* Replace line breaks, carriage returns, tabs with a space, then remove double spaces.
*
* @param string $text Text input to standardize.
*
* @return string
*/
public function standardize_whitespace( $text ) {
return \trim( \str_replace( ' ', ' ', \str_replace( [ "\t", "\n", "\r", "\f" ], ' ', $text ) ) );
}
/**
* First strip out registered and enclosing shortcodes using native WordPress strip_shortcodes function.
* Then strip out the shortcodes with a filthy regex, because people don't properly register their shortcodes.
*
* @param string $text Input string that might contain shortcodes.
*
* @return string String without shortcodes.
*/
public function strip_shortcode( $text ) {
return \preg_replace( '`\[[^\]]+\]`s', '', \strip_shortcodes( $text ) );
}
}
helpers/post-type-helper.php 0000666 00000016265 15220430627 0012157 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
use WP_Post_Type;
/**
* A helper object for post types.
*/
class Post_Type_Helper {
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* Post_Type_Helper constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_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/crawl-cleanup-helper.php 0000666 00000020145 15220430627 0012740 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
/**
* Class Crawl_Cleanup_Helper.
*
* Used by the Crawl_Cleanup_Permalinks class.
*/
class Crawl_Cleanup_Helper {
/**
* The current page helper
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The URL helper.
*
* @var Url_Helper
*/
private $url_helper;
/**
* The Redirect Helper.
*
* @var Redirect_Helper
*/
private $redirect_helper;
/**
* Crawl Cleanup Basic integration constructor.
*
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Options_Helper $options_helper The option helper.
* @param Url_Helper $url_helper The URL helper.
* @param Redirect_Helper $redirect_helper The Redirect Helper.
*/
public function __construct(
Current_Page_Helper $current_page_helper,
Options_Helper $options_helper,
Url_Helper $url_helper,
Redirect_Helper $redirect_helper
) {
$this->current_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, '<!--nextpage-->' );
$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/current-page-helper.php 0000666 00000037222 15220430627 0012603 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
use WP_Post;
use Yoast\WP\SEO\Wrappers\WP_Query_Wrapper;
/**
* A helper object for WordPress posts.
*/
class Current_Page_Helper {
/**
* The WP Query wrapper.
*
* @var WP_Query_Wrapper
*/
private $wp_query_wrapper;
/**
* Current_Page_Helper constructor.
*
* @codeCoverageIgnore It only sets dependencies.
*
* @param WP_Query_Wrapper $wp_query_wrapper The wrapper for WP_Query.
*/
public function __construct( WP_Query_Wrapper $wp_query_wrapper ) {
$this->wp_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 \intval( $post->ID ) === \intval( \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/primary-term-helper.php 0000666 00000002566 15220430627 0012642 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
use stdClass;
/**
* A helper object for primary terms.
*/
class Primary_Term_Helper {
/**
* Generate the primary term taxonomies.
*
* @param int $post_id ID of the post.
*
* @return array The taxonomies.
*/
public function get_primary_term_taxonomies( $post_id ) {
$post_type = \get_post_type( $post_id );
$all_taxonomies = \get_object_taxonomies( $post_type, 'objects' );
$all_taxonomies = \array_filter( $all_taxonomies, [ $this, 'filter_hierarchical_taxonomies' ] );
/**
* Filters which taxonomies for which the user can choose the primary term.
*
* @param array $taxonomies An array of taxonomy objects that are primary_term enabled.
* @param string $post_type The post type for which to filter the taxonomies.
* @param array $all_taxonomies All taxonomies for this post types, even ones that don't have primary term
* enabled.
*/
$taxonomies = (array) \apply_filters( 'wpseo_primary_term_taxonomies', $all_taxonomies, $post_type, $all_taxonomies );
return $taxonomies;
}
/**
* Returns whether or not a taxonomy is hierarchical.
*
* @param stdClass $taxonomy Taxonomy object.
*
* @return bool True for hierarchical taxonomy.
*/
protected function filter_hierarchical_taxonomies( $taxonomy ) {
return (bool) $taxonomy->hierarchical;
}
}
helpers/open-graph/image-helper.php 0000666 00000006011 15220430627 0013321 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers\Open_Graph;
use Yoast\WP\SEO\Helpers\Image_Helper as Base_Image_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
/**
* A helper object for Open Graph images.
*/
class Image_Helper {
/**
* The URL helper.
*
* @var Url_Helper
*/
private $url;
/**
* The base image helper.
*
* @var Base_Image_Helper
*/
private $image;
/**
* Image_Helper constructor.
*
* @codeCoverageIgnore
*
* @param Url_Helper $url The url helper.
* @param Base_Image_Helper $image The image helper.
*/
public function __construct( Url_Helper $url, Base_Image_Helper $image ) {
$this->url = $url;
$this->image = $image;
}
/**
* Determines whether the passed URL is considered valid.
*
* @deprecated 22.4
* @codeCoverageIgnore
*
* @param array<array<string, string|int>> $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<string, string|int>|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/open-graph/values-helper.php 0000666 00000005134 15220430627 0013543 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers\Open_Graph;
/**
* A helper object for the filtering of values.
*/
class Values_Helper {
/**
* Filters the Open Graph title.
*
* @param string $title The default title.
* @param string $object_type The object type.
* @param string $object_subtype The object subtype.
*
* @return string The open graph title.
*/
public function get_open_graph_title( $title, $object_type, $object_subtype ) {
/**
* Allow changing the Open Graph title.
*
* @param string $title The default title.
* @param string $object_subtype The object subtype.
*/
return \apply_filters( 'Yoast\WP\SEO\open_graph_title_' . $object_type, $title, $object_subtype );
}
/**
* Filters the Open Graph description.
*
* @param string $description The default description.
* @param string $object_type The object type.
* @param string $object_subtype The object subtype.
*
* @return string The open graph description.
*/
public function get_open_graph_description( $description, $object_type, $object_subtype ) {
/**
* Allow changing the Open Graph description.
*
* @param string $description The default description.
* @param string $object_subtype The object subtype.
*/
return \apply_filters( 'Yoast\WP\SEO\open_graph_description_' . $object_type, $description, $object_subtype );
}
/**
* Filters the Open Graph image ID.
*
* @param int $image_id The default image ID.
* @param string $object_type The object type.
* @param string $object_subtype The object subtype.
*
* @return string The open graph image ID.
*/
public function get_open_graph_image_id( $image_id, $object_type, $object_subtype ) {
/**
* Allow changing the Open Graph image ID.
*
* @param int $image_id The default image ID.
* @param string $object_subtype The object subtype.
*/
return \apply_filters( 'Yoast\WP\SEO\open_graph_image_id_' . $object_type, $image_id, $object_subtype );
}
/**
* Filters the Open Graph image URL.
*
* @param string $image The default image URL.
* @param string $object_type The object type.
* @param string $object_subtype The object subtype.
*
* @return string The open graph image URL.
*/
public function get_open_graph_image( $image, $object_type, $object_subtype ) {
/**
* Allow changing the Open Graph image URL.
*
* @param string $image The default image URL.
* @param string $object_subtype The object subtype.
*/
return \apply_filters( 'Yoast\WP\SEO\open_graph_image_' . $object_type, $image, $object_subtype );
}
}
helpers/import-helper.php 0000666 00000001314 15220430627 0011512 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
/**
* The Import Helper.
*/
class Import_Helper {
/**
* Flattens a multidimensional array of settings. Recursive.
*
* @param array $array_to_flatten The array to be flattened.
* @param string $key_prefix The key to be used as a prefix.
*
* @return array The flattened array.
*/
public function flatten_settings( $array_to_flatten, $key_prefix = '' ) {
$result = [];
foreach ( $array_to_flatten as $key => $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/schema/image-helper.php 0000666 00000014022 15220430627 0012522 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers\Schema;
use Yoast\WP\SEO\Helpers\Image_Helper as Main_Image_Helper;
/**
* Class Image_Helper.
*/
class Image_Helper {
/**
* The HTML helper.
*
* @var HTML_Helper
*/
private $html;
/**
* The language helper.
*
* @var Language_Helper
*/
private $language;
/**
* Represents the main image helper.
*
* @var Main_Image_Helper
*/
private $image;
/**
* Image_Helper constructor.
*
* @codeCoverageIgnore It handles dependencies.
*
* @param HTML_Helper $html The HTML helper.
* @param Language_Helper $language The language helper.
* @param Main_Image_Helper $image The 'main' image helper.
*/
public function __construct( HTML_Helper $html, Language_Helper $language, Main_Image_Helper $image ) {
$this->html = $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/schema/html-helper.php 0000666 00000003756 15220430627 0012420 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers\Schema;
/**
* Class HTML_Helper.
*/
class HTML_Helper {
/**
* Sanitizes a HTML string by stripping all tags except headings, breaks, lists, links, paragraphs and formatting.
*
* @param string $html The original HTML.
*
* @return string The sanitized HTML.
*/
public function sanitize( $html ) {
if ( ! $this->is_non_empty_string_or_stringable( $html ) ) {
if ( \is_int( $html ) || \is_float( $html ) ) {
return (string) $html;
}
return '';
}
return \strip_tags( $html, '<h1><h2><h3><h4><h5><h6><br><ol><ul><li><a><p><b><strong><i><em>' );
}
/**
* 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 <br> tags with spaces.
$html = \preg_replace( '/<br(\s*)?\/?>/i', ' ', $html );
// Replace closing </p> 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', '</$1> ', $html );
// Replace list items with list identifiers so it still looks natural.
$html = \preg_replace( '/(<li[^>]*>)/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/replace-vars-helper.php 0000666 00000006764 15220430627 0014042 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers\Schema;
use Closure;
use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Config\Schema_IDs;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Date_Helper;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
/**
* Registers the Schema replace variables and exposes a method to replace variables on a Schema graph.
*/
class Replace_Vars_Helper {
use No_Conditionals;
/**
* The replace vars.
*
* @var WPSEO_Replace_Vars
*/
protected $replace_vars;
/**
* The Schema ID helper.
*
* @var ID_Helper
*/
protected $id_helper;
/**
* The date helper.
*
* @var Date_Helper
*/
protected $date_helper;
/**
* Replace_Vars_Helper constructor.
*
* @param WPSEO_Replace_Vars $replace_vars The replace vars.
* @param ID_Helper $id_helper The Schema ID helper.
* @param Date_Helper $date_helper The date helper.
*/
public function __construct(
WPSEO_Replace_Vars $replace_vars,
ID_Helper $id_helper,
Date_Helper $date_helper
) {
$this->replace_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/author-archive-helper.php 0000666 00000012377 15220430627 0013134 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
use WP_Query;
use Yoast\WP\Lib\Model;
/**
* A helper object for author archives.
*/
class Author_Archive_Helper {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type_helper;
/**
* Creates a new author archive helper.
*
* @param Options_Helper $options_helper The options helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
*/
public function __construct( Options_Helper $options_helper, Post_Type_Helper $post_type_helper ) {
$this->options_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/asset-helper.php 0000666 00000005014 15220430627 0011320 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
/**
* A helper object for author archives.
*/
class Asset_Helper {
/**
* Recursively retrieves all dependency urls of a given handle.
*
* @param string $handle The handle.
*
* @return string[] All dependency urls of the given handle.
*/
public function get_dependency_urls_by_handle( $handle ) {
$urls = [];
foreach ( $this->get_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/options-helper.php 0000666 00000010462 15220430627 0011677 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
use WPSEO_Option_Llmstxt;
use WPSEO_Option_Social;
use WPSEO_Option_Titles;
use WPSEO_Options;
/**
* A helper object for options.
*/
class Options_Helper {
/**
* Retrieves a single field from any option for the SEO plugin. Keys are always unique.
*
* @codeCoverageIgnore We have to write test when this method contains own code.
*
* @param string $key The key it should return.
* @param mixed $default_value The default value that should be returned if the key isn't set.
*
* @return mixed|null Returns value if found, $default_value if not.
*/
public function get( $key, $default_value = null ) {
return WPSEO_Options::get( $key, $default_value );
}
/**
* Sets a single field to the options.
*
* @param string $key The key to set.
* @param mixed $value The value to set.
* @param string $option_group The lookup table which represents the option_group where the key is stored.
*
* @return mixed|null Returns value if found.
*/
public function set( $key, $value, $option_group = '' ) {
return WPSEO_Options::set( $key, $value, $option_group );
}
/**
* Get a specific default value for an option.
*
* @param string $option_name The option for which you want to retrieve a default.
* @param string $key The key within the option who's default you want.
*
* @return mixed The default value.
*/
public function get_default( $option_name, $key ) {
return WPSEO_Options::get_default( $option_name, $key );
}
/**
* Retrieves the title separator.
*
* @return string The title separator.
*/
public function get_title_separator() {
$default = $this->get_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();
}
/**
* 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/robots-helper.php 0000666 00000003361 15220430627 0011514 0 ustar 00 <?php
namespace Yoast\WP\SEO\Helpers;
use Yoast\WP\SEO\Models\Indexable;
/**
* A helper object for the robots meta tag.
*/
class Robots_Helper {
/**
* Holds the Post_Type_Helper.
*
* @var Post_Type_Helper
*/
protected $post_type_helper;
/**
* Holds the Taxonomy_Helper.
*
* @var Taxonomy_Helper
*/
protected $taxonomy_helper;
/**
* Constructs a Score_Helper.
*
* @param Post_Type_Helper $post_type_helper The Post_Type_Helper.
* @param Taxonomy_Helper $taxonomy_helper The Taxonomy_Helper.
*/
public function __construct( Post_Type_Helper $post_type_helper, Taxonomy_Helper $taxonomy_helper ) {
$this->post_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;
}
}
user-profiles-additions/user-interface/user-profiles-additions-ui.php 0000666 00000004323 15220430627 0022141 0 ustar 00 <?php
namespace Yoast\WP\SEO\User_Profiles_Additions\User_Interface;
use WP_User;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\User_Profile_Conditional;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Adds a new hook in the user profiles edit screen to add content.
*/
class User_Profiles_Additions_Ui implements Integration_Interface {
/**
* Holds the Product_Helper.
*
* @var Product_Helper
*/
private $product_helper;
/**
* Holds the WPSEO_Admin_Asset_Manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $asset_manager;
/**
* Constructs Academy_Integration.
*
* @param WPSEO_Admin_Asset_Manager $asset_manager The WPSEO_Admin_Asset_Manager.
* @param Product_Helper $product_helper The Product_Helper.
*/
public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, Product_Helper $product_helper ) {
$this->asset_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 '<div class="yoast yoast-settings">';
/**
* Fires in the user profile.
*
* @internal
*
* @param WP_User $user The current WP_User object.
*/
\do_action( 'wpseo_user_profile_additions', $user );
echo '</div>';
}
}
wrappers/wp-rewrite-wrapper.php 0000666 00000000444 15220430627 0012712 0 ustar 00 <?php
namespace Yoast\WP\SEO\Wrappers;
use WP_Rewrite;
/**
* Wrapper for WP_Rewrite.
*/
class WP_Rewrite_Wrapper {
/**
* Returns the global WP_Rewrite_Wrapper object.
*
* @return WP_Rewrite The WP_Query object.
*/
public function get() {
return $GLOBALS['wp_rewrite'];
}
}
wrappers/wp-remote-handler.php 0000666 00000003564 15220430627 0012467 0 ustar 00 <?php
namespace Yoast\WP\SEO\Wrappers;
use Exception;
use YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise;
use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface;
use YoastSEO_Vendor\GuzzleHttp\Promise\RejectedPromise;
use YoastSEO_Vendor\GuzzleHttp\Psr7\Response;
use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
/**
* Wraps wp_remote_get in an interface compatible with Guzzle.
*/
class WP_Remote_Handler {
/**
* Calls the handler.
* Cookies are currently not supported as they are not used by OAuth.
* Writing responses to files is also not supported for the same reason.
*
* @param RequestInterface $request The request.
* @param array $options The request options.
*
* @return PromiseInterface The promise interface.
*
* @throws Exception If the request fails.
*/
public function __invoke( RequestInterface $request, array $options ) {
$headers = [];
foreach ( $request->getHeaders() 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 );
}
}
values/oauth/oauth-token.php 0000666 00000006220 15220430627 0012137 0 ustar 00 <?php
namespace Yoast\WP\SEO\Values\OAuth;
use Yoast\WP\SEO\Exceptions\OAuth\Tokens\Empty_Property_Exception;
use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessTokenInterface;
/**
* Class OAuth_Token
*/
class OAuth_Token {
/**
* The access token.
*
* @var string
*/
public $access_token;
/**
* The refresh token.
*
* @var string
*/
public $refresh_token;
/**
* The expiration date.
*
* @var int
*/
public $expires;
/**
* Whether or not the token has expired.
*
* @var bool
*/
public $has_expired;
/**
* The timestamp at which the token was created.
*
* @var int
*/
public $created_at;
/**
* The number of times we've gotten an error trying to refresh this token.
*
* @var int
*/
public $error_count;
/**
* OAuth_Token constructor.
*
* @param string $access_token The access token.
* @param string $refresh_token The refresh token.
* @param int $expires The date and time at which the token will expire.
* @param bool $has_expired Whether or not the token has expired.
* @param int $created_at The timestamp of when the token was created.
* @param int $error_count The number of times we've gotten an error trying to refresh this token.
*
* @throws Empty_Property_Exception Exception thrown if a token property is empty.
*/
public function __construct( $access_token, $refresh_token, $expires, $has_expired, $created_at, $error_count = 0 ) {
if ( empty( $access_token ) ) {
throw new Empty_Property_Exception( 'access_token' );
}
$this->access_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/robots/user-agent.php 0000666 00000003226 15220430627 0012146 0 ustar 00 <?php
namespace Yoast\WP\SEO\Values\Robots;
/**
* Class Directive
*/
class User_Agent {
/**
* The user agent identifier.
*
* @var string
*/
private $user_agent;
/**
* All directives that are allowed for this user agent.
*
* @var Directive
*/
private $allow_directive;
/**
* All directives that are disallowed for this user agent.
*
* @var Directive
*/
private $disallow_directive;
/**
* Constructor of the user agent value object.
*
* @param string $user_agent The user agent identifier.
*/
public function __construct( $user_agent ) {
$this->user_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/open-graph/images.php 0000666 00000002232 15220430627 0012065 0 ustar 00 <?php
namespace Yoast\WP\SEO\Values\Open_Graph;
use Yoast\WP\SEO\Helpers\Open_Graph\Image_Helper as Open_Graph_Image_Helper;
use Yoast\WP\SEO\Values\Images as Base_Images;
/**
* Value object for the Open Graph Images.
*/
class Images extends Base_Images {
/**
* The Open Graph image helper.
*
* @var Open_Graph_Image_Helper
*/
protected $open_graph_image;
/**
* Sets the helpers.
*
* @required
*
* @codeCoverageIgnore - Is handled by DI-container.
*
* @param Open_Graph_Image_Helper $open_graph_image Image helper for Open Graph.
*
* @return void
*/
public function set_helpers( Open_Graph_Image_Helper $open_graph_image ) {
$this->open_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 );
}
}
}
ai-generator/domain/suggestions-bucket.php 0000666 00000001453 15220430627 0014736 0 ustar 00 <?php
namespace Yoast\WP\SEO\AI_Generator\Domain;
/**
* Class Suggestion_Bucket
* Represents a collection of Suggestion objects.
*/
class Suggestions_Bucket {
/**
* The suggestions.
*
* @var array<Suggestion>
*/
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<string>
*/
public function to_array() {
return \array_map(
static function ( $item ) {
return $item->get_value();
},
$this->suggestions
);
}
}
ai-generator/domain/endpoint/endpoint-list.php 0000666 00000001504 15220430627 0015517 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\AI_Generator\Domain\Endpoint;
/**
* List of endpoints.
*/
class Endpoint_List {
/**
* Holds the endpoints.
*
* @var array<Endpoint_Interface>
*/
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<string, string> 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/urls-interface.php 0000666 00000001270 15220430627 0014031 0 ustar 00 <?php
namespace Yoast\WP\SEO\AI_Generator\Domain;
/**
* Helper class to get the URLs needed for the AI Generator API.
*/
interface URLs_Interface {
/**
* Gets the licence URL.
*
* @return string The license URL.
*/
public function get_license_url(): string;
/**
* Gets the callback URL to be used by the API to send back the access token, refresh token and code challenge.
*
* @return string The callback URL.
*/
public function get_callback_url(): string;
/**
* Gets the callback URL to be used by the API to send back the refreshed JWTs once they expire.
*
* @return string The refresh callback URL.
*/
public function get_refresh_callback_url(): string;
}
plans/application/add-ons-collector.php 0000666 00000002227 15220430627 0014216 0 ustar 00 <?php
namespace Yoast\WP\SEO\Plans\Application;
use Yoast\WP\SEO\Plans\Domain\Add_Ons\Add_On_Interface;
/**
* The collector to get add-ons.
*/
class Add_Ons_Collector {
/**
* All add-ons.
*
* @var array<Add_On_Interface>
*/
private $add_ons;
/**
* Constructs the instance.
*
* @param Add_On_Interface ...$add_ons All add-ons.
*/
public function __construct( Add_On_Interface ...$add_ons ) {
$this->add_ons = $add_ons;
}
/**
* Returns all the add-ons.
*
* @return array<Add_On_Interface> 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<string, string|bool|array<string, string>> The add-ons in an array format.
*/
public function to_array(): array {
$result = [];
foreach ( $this->add_ons as $add_on ) {
$result[ $add_on->get_id() ] = [
'id' => $add_on->get_id(),
'isActive' => $add_on->is_active(),
'hasLicense' => $add_on->has_license(),
'ctb' => [
'action' => $add_on->get_ctb_action(),
'id' => $add_on->get_ctb_id(),
],
];
}
return $result;
}
}
context/meta-tags-context.php 0000666 00000046254 15220430627 0012325 0 ustar 00 <?php
namespace Yoast\WP\SEO\Context;
use WP_Block_Parser_Block;
use WP_Post;
use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Config\Schema_IDs;
use Yoast\WP\SEO\Config\Schema_Types;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Permalink_Helper;
use Yoast\WP\SEO\Helpers\Schema\ID_Helper;
use Yoast\WP\SEO\Helpers\Site_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Presentations\Abstract_Presentation;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Class Meta_Tags_Context.
*
* Class that contains all relevant data for rendering the meta tags.
*
* @property string $canonical
* @property string $permalink
* @property string $title
* @property string $description
* @property string $id
* @property string $site_name
* @property string $alternate_site_name
* @property string $wordpress_site_name
* @property string $site_url
* @property string $company_name
* @property string $company_alternate_name
* @property int $company_logo_id
* @property array $company_logo_meta
* @property int $person_logo_id
* @property array $person_logo_meta
* @property int $site_user_id
* @property string $site_represents
* @property array|false $site_represents_reference
* @property string|string[] $schema_page_type
* @property string|string[] $schema_article_type Represents the type of article.
* @property string $main_schema_id
* @property string|array $main_entity_of_page
* @property bool $open_graph_enabled
* @property string $open_graph_publisher
* @property string $twitter_card
* @property string $page_type
* @property bool $has_article
* @property bool $has_image
* @property int $main_image_id
* @property string $main_image_url
*/
class Meta_Tags_Context extends Abstract_Presentation {
/**
* The indexable.
*
* @var Indexable
*/
public $indexable;
/**
* The WP Block Parser Block.
*
* @var WP_Block_Parser_Block[]
*/
public $blocks;
/**
* The WP Post.
*
* @var WP_Post
*/
public $post;
/**
* The indexable presentation.
*
* @var Indexable_Presentation
*/
public $presentation;
/**
* Determines whether we have an Article piece. Set to true by the Article piece itself.
*
* @var bool
*/
public $has_article = false;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* The URL helper.
*
* @var Url_Helper
*/
private $url;
/**
* The image helper.
*
* @var Image_Helper
*/
private $image;
/**
* The ID helper.
*
* @var ID_Helper
*/
private $id_helper;
/**
* The WPSEO Replace Vars object.
*
* @var WPSEO_Replace_Vars
*/
private $replace_vars;
/**
* The site helper.
*
* @var Site_Helper
*/
private $site;
/**
* The user helper.
*
* @var User_Helper
*/
private $user;
/**
* The permalink helper.
*
* @var Permalink_Helper
*/
private $permalink_helper;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* Meta_Tags_Context constructor.
*
* @param Options_Helper $options The options helper.
* @param Url_Helper $url The url helper.
* @param Image_Helper $image The image helper.
* @param ID_Helper $id_helper The schema id helper.
* @param WPSEO_Replace_Vars $replace_vars The replace vars helper.
* @param Site_Helper $site The site helper.
* @param User_Helper $user The user helper.
* @param Permalink_Helper $permalink_helper The permalink helper.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Indexable_Repository $indexable_repository The indexable repository.
*/
public function __construct(
Options_Helper $options,
Url_Helper $url,
Image_Helper $image,
ID_Helper $id_helper,
WPSEO_Replace_Vars $replace_vars,
Site_Helper $site,
User_Helper $user,
Permalink_Helper $permalink_helper,
Indexable_Helper $indexable_helper,
Indexable_Repository $indexable_repository
) {
$this->options = $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<string|array<int>>|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<string|array<int>>|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<string>|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<string> 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;
if ( $additional_type === null ) {
$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<string> The schema article type.
*/
public function generate_schema_article_type() {
$additional_type = $this->indexable->schema_article_type;
if ( $additional_type === null ) {
$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<Indexable|Indexable_Presentation>
*/
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-consent/user-interface/consent-route.php 0000666 00000010570 15220430627 0015046 0 ustar 00 <?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\AI_Consent\User_Interface;
use RuntimeException;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\AI_Authorization\Application\Token_Manager;
use Yoast\WP\SEO\AI_Consent\Application\Consent_Handler;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
use Yoast\WP\SEO\Conditionals\AI_Conditional;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Registers a route toget suggestions from the AI API
*
* @makePublic
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Consent_Route implements Route_Interface {
/**
* The namespace for this route.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix for this route.
*
* @var string
*/
public const ROUTE_PREFIX = '/ai_generator/consent';
/**
* The consent handler instance.
*
* @var Consent_Handler
*/
private $consent_handler;
/**
* The token manager instance.
*
* @var Token_Manager
*/
private $token_manager;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array<string> 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 = \boolval( $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/application/consent-handler.php 0000666 00000002135 15220430627 0014712 0 ustar 00 <?php
namespace Yoast\WP\SEO\AI_Consent\Application;
use Yoast\WP\SEO\Helpers\User_Helper;
/**
* Class Consent_Handler
* Handles the consent given or revoked by the user.
*
* @makePublic
*/
class Consent_Handler implements Consent_Handler_Interface {
/**
* Holds the user helper instance.
*
* @var User_Helper
*/
private $user_helper;
/**
* Class constructor.
*
* @param User_Helper $user_helper The user helper.
*/
public function __construct( User_Helper $user_helper ) {
$this->user_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 );
}
}
content-type-visibility/user-interface/content-type-visibility-dismiss-new-route.php 0000666 00000010412 15220430627 0025224 0 ustar 00 <?php
namespace Yoast\WP\SEO\Content_Type_Visibility\User_Interface;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Content_Type_Visibility\Application\Content_Type_Visibility_Dismiss_Notifications;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Handles the dismiss route for "New" badges of new content types in settings menu.
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Content_Type_Visibility_Dismiss_New_Route implements Route_Interface {
use No_Conditionals;
/**
* Represents the alerts route prefix.
*
* @var string
*/
public const ROUTE_PREFIX = 'new-content-type-visibility';
/**
* Represents post type dismiss route.
*
* @var string
*/
public const POST_TYPE_DISMISS_ROUTE = self::ROUTE_PREFIX . '/dismiss-post-type';
/**
* Represents taxonomy dismiss route.
*
* @var string
*/
public const TAXONOMY_DISMISS_ROUTE = self::ROUTE_PREFIX . '/dismiss-taxonomy';
/**
* Holds the Options_Helper instance.
*
* @var Content_Type_Visibility_Dismiss_Notifications
*/
private $dismiss_notifications;
/**
* Constructs Content_Type_Visibility_Dismiss_New_Route.
*
* @param Content_Type_Visibility_Dismiss_Notifications $dismiss_notifications The options helper.
*/
public function __construct( Content_Type_Visibility_Dismiss_Notifications $dismiss_notifications ) {
$this->dismiss_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']
);
}
}
content-type-visibility/application/content-type-visibility-watcher-actions.php 0000666 00000013747 15220430627 0024326 0 ustar 00 <?php
namespace Yoast\WP\SEO\Content_Type_Visibility\Application;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification;
use Yoast_Notification_Center;
/**
* Handles showing notifications for new content types.
* Actions are used in the indexable taxonomy and post type change watchers.
*/
class Content_Type_Visibility_Watcher_Actions implements Integration_Interface {
/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options;
/**
* The notifications center.
*
* @var Yoast_Notification_Center
*/
private $notification_center;
/**
* The notifications center.
*
* @var Content_Type_Visibility_Dismiss_Notifications
*/
private $content_type_dismiss_notifications;
/**
* Indexable_Post_Type_Change_Watcher constructor.
*
* @param Options_Helper $options The options helper.
* @param Yoast_Notification_Center $notification_center The notification center.
* @param Content_Type_Visibility_Dismiss_Notifications $content_type_dismiss_notifications The content type dismiss notifications.
*/
public function __construct(
Options_Helper $options,
Yoast_Notification_Center $notification_center,
Content_Type_Visibility_Dismiss_Notifications $content_type_dismiss_notifications
) {
$this->options = $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' ),
'<a href="' . \esc_url( \admin_url( 'admin.php?page=wpseo_page_settings' ) ) . '">',
'</a>'
);
$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.php 0000666 00000010522 15220430627 0025541 0 ustar 00 <?php
namespace Yoast\WP\SEO\Content_Type_Visibility\Application;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast_Notification_Center;
/**
* Handles dismissing notifications and "New" badges for new content types.
*/
class Content_Type_Visibility_Dismiss_Notifications {
/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options;
/**
* Constructs Content_Type_Visibility_Dismiss_New_Route.
*
* @param Options_Helper $options The options helper.
*/
public function __construct( Options_Helper $options ) {
$this->options = $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;
}
}
repositories/indexable-hierarchy-repository.php 0000666 00000007622 15220430627 0016144 0 ustar 00 <?php
namespace Yoast\WP\SEO\Repositories;
use Yoast\WP\Lib\Model;
use Yoast\WP\Lib\ORM;
use Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Models\Indexable;
/**
* Class Indexable_Hierarchy_Repository.
*/
class Indexable_Hierarchy_Repository {
/**
* Represents the indexable hierarchy builder.
*
* @var Indexable_Hierarchy_Builder
*/
protected $builder;
/**
* Represents the indexable helper.
*
* @var Indexable_Helper
*/
protected $indexable_helper;
/**
* Sets the hierarchy builder.
*
* @required
*
* @param Indexable_Hierarchy_Builder $builder The indexable hierarchy builder.
*
* @return void
*/
public function set_builder( Indexable_Hierarchy_Builder $builder ) {
$this->builder = $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/indexable-cleanup-repository.php 0000666 00000066314 15220430627 0015620 0 ustar 00 <?php
namespace Yoast\WP\SEO\Repositories;
use mysqli_result;
use Yoast\WP\Lib\Model;
use Yoast\WP\Lib\ORM;
use Yoast\WP\SEO\Helpers\Author_Archive_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
/**
* Repository containing all cleanup queries.
*/
class Indexable_Cleanup_Repository {
/**
* A helper for taxonomies.
*
* @var Taxonomy_Helper
*/
private $taxonomy;
/**
* A helper for post types.
*
* @var Post_Type_Helper
*/
private $post_type;
/**
* A helper for author archives.
*
* @var Author_Archive_Helper
*/
private $author_archive;
/**
* The constructor.
*
* @param Taxonomy_Helper $taxonomy A helper for taxonomies.
* @param Post_Type_Helper $post_type A helper for post types.
* @param Author_Archive_Helper $author_archive A helper for author archives.
*/
public function __construct( Taxonomy_Helper $taxonomy, Post_Type_Helper $post_type, Author_Archive_Helper $author_archive ) {
$this->taxonomy = $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/indexable-repository.php 0000666 00000042131 15220430627 0014162 0 ustar 00 <?php
namespace Yoast\WP\SEO\Repositories;
use Psr\Log\LoggerInterface;
use wpdb;
use Yoast\WP\Lib\Model;
use Yoast\WP\Lib\ORM;
use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Loggers\Logger;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager;
/**
* Class Indexable_Repository.
*/
class Indexable_Repository {
/**
* The indexable builder.
*
* @var Indexable_Builder
*/
private $builder;
/**
* Represents the hierarchy repository.
*
* @var Indexable_Hierarchy_Repository
*/
protected $hierarchy_repository;
/**
* The current page helper.
*
* @var Current_Page_Helper
*/
protected $current_page;
/**
* The logger object.
*
* @var LoggerInterface
*/
protected $logger;
/**
* The WordPress database.
*
* @var wpdb
*/
protected $wpdb;
/**
* Represents the indexable helper.
*
* @var Indexable_Helper
*/
protected $indexable_helper;
/**
* Checks if Indexables are up to date.
*
* @var Indexable_Version_Manager
*/
protected $version_manager;
/**
* Returns the instance of this class constructed through the ORM Wrapper.
*
* @param Indexable_Builder $builder The indexable builder.
* @param Current_Page_Helper $current_page The current post helper.
* @param Logger $logger The logger.
* @param Indexable_Hierarchy_Repository $hierarchy_repository The hierarchy repository.
* @param wpdb $wpdb The WordPress database instance.
* @param Indexable_Version_Manager $version_manager The indexable version manager.
*/
public function __construct(
Indexable_Builder $builder,
Current_Page_Helper $current_page,
Logger $logger,
Indexable_Hierarchy_Repository $hierarchy_repository,
wpdb $wpdb,
Indexable_Version_Manager $version_manager
) {
$this->builder = $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 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.
*
* @return int|bool The number of permalinks changed if the query was succesful. False otherwise.
*/
public function reset_permalink( $type = null, $subtype = 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 );
}
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();
}
}
config/migrations/20171228151841_WpYoastPrimaryTerm.php 0000666 00000003022 15220430627 0016154 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Migration for the Primary Term.
*/
class WpYoastPrimaryTerm extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$table_name = $this->get_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/migrations/20201216124002_ExpandIndexableIDColumnLengths.php 0000666 00000002023 15220430627 0020301 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* ExpandIndexableIDColumnLengths class.
*/
class ExpandIndexableIDColumnLengths extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* The columns to change the column type and length of.
*
* @var string[]
*/
protected static $columns_to_change = [
'object_id',
'author_id',
'post_parent',
];
/**
* Migration up.
*
* @return void
*/
public function up() {
foreach ( self::$columns_to_change as $column ) {
$this->change_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/20200428194858_ExpandIndexableColumnLengths.php 0000666 00000003413 15220430627 0020126 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class ExpandIndexableColumnLengths.
*/
class ExpandIndexableColumnLengths extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->change_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/20200408101900_AddCollationToTables.php 0000666 00000001676 15220430627 0016342 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class AddCollationToTables.
*/
class AddCollationToTables extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
global $wpdb;
$charset_collate = $wpdb->get_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.php 0000666 00000002057 15220430627 0016330 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class ClearIndexableTables.
*/
class ClearIndexableTables extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->query( '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/20200702141921_CreateIndexableSubpagesIndex.php 0000666 00000002354 15220430627 0020046 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* CreateIndexableSubpagesIndex class.
*/
class CreateIndexableSubpagesIndex extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->change_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/20200420073606_AddColumnsToIndexables.php 0000666 00000004124 15220430627 0016676 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class AddColumnsToIndexables.
*/
class AddColumnsToIndexables extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$tables = $this->get_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/20190529075038_WpYoastDropIndexableMetaTableIfExists.php 0000666 00000001573 15220430627 0021700 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class WpYoastDropIndexableMetaTableIfExists.
*/
class WpYoastDropIndexableMetaTableIfExists extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$table_name = $this->get_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.php 0000666 00000001703 15220430627 0017004 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* AddEstimatedReadingTime class.
*/
class AddEstimatedReadingTime extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$table_name = $this->get_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/20211020091404_AddObjectTimestamps.php 0000666 00000003007 15220430627 0016222 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* AddObjectTimestamps class.
*/
class AddObjectTimestamps extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->add_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/20210817092415_AddVersionColumnToIndexables.php 0000666 00000001547 15220430627 0020101 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* AddVersionColumnToIndexables class.
*/
class AddVersionColumnToIndexables extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->add_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/20200728095334_AddIndexesForProminentWordsOnIndexables.php 0000666 00000002265 15220430627 0022252 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* AddIndexesForProminentWordsOnIndexables class.
*/
class AddIndexesForProminentWordsOnIndexables extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* The columns on which an index should be added.
*
* @var string[]
*/
private $columns_with_index = [
'prominent_words_version',
'object_type',
'object_sub_type',
'post_status',
];
/**
* Migration up.
*
* @return void
*/
public function up() {
$table_name = $this->get_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/20171228151840_WpYoastIndexable.php 0000666 00000014256 15220430627 0015606 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Indexable migration.
*/
class WpYoastIndexable extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->add_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/20191011111109_WpYoastIndexableHierarchy.php 0000666 00000003072 15220430627 0017421 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class WpYoastIndexableHierarchy.
*/
class WpYoastIndexableHierarchy extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$table_name = $this->get_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/20200513133401_ResetIndexableHierarchyTable.php 0000666 00000001366 15220430627 0020046 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class ResetIndexableHierarchyTable.
*/
class ResetIndexableHierarchyTable extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->query( '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/20230417083836_AddInclusiveLanguageScore.php 0000666 00000001662 15220430627 0017400 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* AddInclusiveLanguageScore class.
*/
class AddInclusiveLanguageScore extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$table_name = $this->get_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/20200616130143_ReplacePermalinkHashIndex.php 0000666 00000004365 15220430627 0017361 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* ReplacePermalinkHashIndex class.
*/
class ReplacePermalinkHashIndex extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$table_name = $this->get_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/20200430075614_AddIndexableObjectIdAndTypeIndex.php 0000666 00000001737 15220430627 0020541 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class AddIndexableObjectIdAndTypeIndex.
*/
class AddIndexableObjectIdAndTypeIndex extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->add_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/20201216141134_ExpandPrimaryTermIDColumnLengths.php 0000666 00000002005 15220430627 0020666 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* ExpandPrimaryTermIDColumnLengths class.
*/
class ExpandPrimaryTermIDColumnLengths extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* The columns to change the column type and length of.
*
* @var string[]
*/
protected static $columns_to_change = [
'post_id',
'term_id',
];
/**
* Migration up.
*
* @return void
*/
public function up() {
foreach ( self::$columns_to_change as $column ) {
$this->change_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/20200507054848_DeleteDuplicateIndexables.php 0000666 00000002113 15220430627 0017410 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class DeleteDuplicateIndexables.
*/
class DeleteDuplicateIndexables extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$table_name = $this->get_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/20200609154515_AddHasAncestorsColumn.php 0000666 00000001661 15220430627 0016542 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\WordPress\Wrapper;
/**
* Class AddHasAncestorsColumn.
*/
class AddHasAncestorsColumn extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->add_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/20200617122511_CreateSEOLinksTable.php 0000666 00000004735 15220430627 0016074 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* CreateSEOLinksTable class.
*/
class CreateSEOLinksTable extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$table_name = $this->get_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/20200428123747_BreadcrumbTitleAndHierarchyReset.php 0000666 00000002356 15220430627 0020717 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class BreadcrumbTitleAndHierarchyReset.
*/
class BreadcrumbTitleAndHierarchyReset extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->change_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/20200429105310_TruncateIndexableTables.php 0000666 00000002065 15220430627 0017076 0 ustar 00 <?php
namespace Yoast\WP\SEO\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* Class TruncateIndexableTables.
*/
class TruncateIndexableTables extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'free';
/**
* Migration up.
*
* @return void
*/
public function up() {
$this->query( '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' );
}
}
promotions/domain/promotion-interface.php 0000666 00000000171 15220430627 0014725 0 ustar 00 <?php
namespace Yoast\WP\SEO\Promotions\Domain;
/**
* Interface for a Promotion.
*/
interface Promotion_Interface {}
promotions/application/promotion-manager.php 0000666 00000003310 15220430627 0015431 0 ustar 00 <?php
namespace Yoast\WP\SEO\Promotions\Application;
use Yoast\WP\SEO\Promotions\Domain\Promotion_Interface;
/**
* Class to manage promotional promotions.
*
* @makePublic
*/
class Promotion_Manager implements Promotion_Manager_Interface {
/**
* The centralized list of promotions: all promotions should be passed to the constructor.
*
* @var array<Abstract_Promotion>
*/
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<Abstract_Promotion> The list of promotions.
*/
public function get_promotions_list(): array {
return $this->promotions_list;
}
/**
* Get the names of currently active promotions.
*
* @return array<string> 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;
}
}
promotions/application/promotion-manager-interface.php 0000666 00000000773 15220430627 0017401 0 ustar 00 <?php
namespace Yoast\WP\SEO\Promotions\Application;
/**
* Interface for the promotion manager.
*/
interface Promotion_Manager_Interface {
/**
* 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;
/**
* Get the list of promotions.
*
* @return array The list of promotions.
*/
public function get_promotions_list(): array;
}
surfaces/values/meta.php 0000666 00000027632 15220430627 0011334 0 ustar 00 <?php
namespace Yoast\WP\SEO\Surfaces\Values;
use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Exceptions\Forbidden_Property_Mutation_Exception;
use Yoast\WP\SEO\Integrations\Front_End_Integration;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter;
use Yoast\WP\SEO\Presenters\Rel_Next_Presenter;
use Yoast\WP\SEO\Presenters\Rel_Prev_Presenter;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Meta value object.
*
* @property array $breadcrumbs The breadcrumbs array for the current page.
* @property string $canonical The canonical URL for the current page.
* @property string $company_name The company name from the Knowledge graph settings.
* @property int $company_logo_id The attachment ID for the company logo.
* @property string $description The meta description for the current page, if set.
* @property int $estimated_reading_time_minutes The estimated reading time in minutes for posts.
* @property Indexable $indexable The indexable object.
* @property string $main_schema_id Schema ID that points to the main Schema thing on the page, usually the webpage or article Schema piece.
* @property string $meta_description The meta description for the current page, if set.
* @property string $open_graph_article_author The article:author value.
* @property string $open_graph_article_modified_time The article:modified_time value.
* @property string $open_graph_article_published_time The article:published_time value.
* @property string $open_graph_article_publisher The article:publisher value.
* @property string $open_graph_description The og:description.
* @property bool $open_graph_enabled Whether OpenGraph is enabled on this site.
* @property string $open_graph_fb_app_id The Facebook App ID.
* @property array $open_graph_images The array of images we have for this page.
* @property string $open_graph_locale The og:locale for the current page.
* @property string $open_graph_publisher The OpenGraph publisher reference.
* @property string $open_graph_site_name The og:site_name.
* @property string $open_graph_title The og:title.
* @property string $open_graph_type The og:type.
* @property string $open_graph_url The og:url.
* @property string $page_type The Schema page type.
* @property array $robots An array of the robots values set for the current page.
* @property string $rel_next The next page in the series, if any.
* @property string $rel_prev The previous page in the series, if any.
* @property array $schema The entire Schema array for the current page.
* @property string $schema_page_type The Schema page type.
* @property string $site_name The site name from the Yoast SEO settings.
* @property string $site_represents Whether the site represents a 'person' or a 'company'.
* @property array|false $site_represents_reference The schema reference ID for what this site represents.
* @property string $site_url The site's main URL.
* @property int $site_user_id If the site represents a 'person', this is the ID of the accompanying user profile.
* @property string $title The SEO title for the current page.
* @property string $twitter_card The Twitter card type for the current page.
* @property string $twitter_creator The Twitter card author for the current page.
* @property string $twitter_description The Twitter card description for the current page.
* @property string $twitter_image The Twitter card image for the current page.
* @property string $twitter_site The Twitter card site reference for the current page.
* @property string $twitter_title The Twitter card title for the current page.
* @property string $wordpress_site_name The site name from the WordPress settings.
*/
class Meta {
/**
* The container.
*
* @var ContainerInterface
*/
protected $container;
/**
* The meta tags context.
*
* @var Meta_Tags_Context
*/
protected $context;
/**
* The front end integration.
*
* @var Front_End_Integration
*/
protected $front_end;
/**
* The helpers surface.
*
* @var Helpers_Surface
*/
protected $helpers;
/**
* The replace vars helper
*
* @var WPSEO_Replace_Vars
*/
protected $replace_vars;
/**
* Collection of properties dynamically set via the magic __get() method.
*
* @var array<string, mixed> 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,
];
}
}
surfaces/schema-helpers-surface.php 0000666 00000005546 15220430627 0013435 0 ustar 00 <?php
namespace Yoast\WP\SEO\Surfaces;
use Yoast\WP\SEO\Exceptions\Forbidden_Property_Mutation_Exception;
use Yoast\WP\SEO\Helpers\Schema;
use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class Schema_Helpers_Surface.
*
* Surface for the indexables.
*
* @property Schema\Article_Helper $article
* @property Schema\HTML_Helper $html
* @property Schema\ID_Helper $id
* @property Schema\Image_Helper $image
* @property Schema\Language_Helper $language
*/
class Schema_Helpers_Surface {
/**
* The DI container.
*
* @var ContainerInterface
*/
private $container;
/**
* Helpers that should be fully capitalized.
*
* @var array
*/
private $capitalized_helpers = [ 'html', 'id' ];
/**
* Loader constructor.
*
* @param ContainerInterface $container The dependency injection container.
*/
public function __construct( ContainerInterface $container ) {
$this->container = $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/meta-surface.php 0000666 00000024373 15220430627 0011462 0 ustar 00 <?php
namespace Yoast\WP\SEO\Surfaces;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Surfaces\Values\Meta;
use Yoast\WP\SEO\Wrappers\WP_Rewrite_Wrapper;
use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Meta_Surface class.
*
* Surface for the indexables.
*/
class Meta_Surface {
/**
* The container.
*
* @var ContainerInterface
*/
private $container;
/**
* The memoizer for the meta tags context.
*
* @var Meta_Tags_Context_Memoizer
*/
private $context_memoizer;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $repository;
/**
* Holds the WP rewrite wrapper instance.
*
* @var WP_Rewrite_Wrapper
*/
private $wp_rewrite_wrapper;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* Meta_Surface constructor.
*
* @param ContainerInterface $container The DI container.
* @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer.
* @param Indexable_Repository $indexable_repository The indexable repository.
* @param WP_Rewrite_Wrapper $wp_rewrite_wrapper The WP rewrite wrapper.
* @param Indexable_Helper $indexable_helper The indexable helper.
*/
public function __construct(
ContainerInterface $container,
Meta_Tags_Context_Memoizer $context_memoizer,
Indexable_Repository $indexable_repository,
WP_Rewrite_Wrapper $wp_rewrite_wrapper,
Indexable_Helper $indexable_helper
) {
$this->container = $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 ) {
if ( $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;
}
if ( $page_type === null ) {
$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;
if ( $this_page_type === null ) {
$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/open-graph-helpers-surface.php 0000666 00000004757 15220430627 0014240 0 ustar 00 <?php
namespace Yoast\WP\SEO\Surfaces;
use Yoast\WP\SEO\Exceptions\Forbidden_Property_Mutation_Exception;
use Yoast\WP\SEO\Helpers\Open_Graph;
use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class Open_Graph_Helpers_Surface.
*
* Surface for the indexables.
*
* @property Open_Graph\Image_Helper $image
*/
class Open_Graph_Helpers_Surface {
/**
* The DI container.
*
* @var ContainerInterface
*/
private $container;
/**
* Loader constructor.
*
* @param ContainerInterface $container The dependency injection container.
*/
public function __construct( ContainerInterface $container ) {
$this->container = $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";
}
}
customizer-controls.js 0000666 00000036014 15221010077 0011152 0 ustar 00 /**
* File customizer-controls.js
*
* The file for generic customizer controls.
*
* @package Hestia
*/
/* global imageObject */
/**
* Check if we have a sale event and try to add a banner to the WP Customizer.
*
* The position of the banner is right before the "Themes" section.
*
* @returns {undefined}
*/
function tryAddSaleBanner () {
if (typeof window.hestiaSaleEvents === 'undefined' || typeof window.hestiaSaleEvents.customizerBannerUrl === 'undefined' || ! window.hestiaSaleEvents.customizerBannerUrl ) {
return;
}
var sectionStart = document.querySelector('#accordion-section-themes');
if ( ! sectionStart ) {
return;
}
var saleLink = document.createElement('a');
saleLink.href = window.hestiaSaleEvents.customizerBannerStoreUrl;
saleLink.target = '_blank';
var bannerImg = document.createElement('img');
bannerImg.src = window.hestiaSaleEvents.customizerBannerUrl;
bannerImg.style.width = '100%'; // Make sure the image is responsive.
saleLink.appendChild(bannerImg);
sectionStart.parentNode.insertBefore(saleLink, sectionStart.nextSibling);
}
/**
* Attempts to add a lock icon to the specified HTML element.
*
* @param {string} elementId - The ID of the target element where the lock icon should be added.
* @param {string} elementSuffix - A suffix to be appended to the element ID for identifying the lock icon element.
*/
function tryAddLockIcon( elementId, elementSuffix) {
var hasIcon = jQuery( '#' + elementId );
if ( hasIcon.length === 0 ) {
jQuery( '#' + elementId + elementSuffix )
.addClass( 'customize-locked-control' )
.find('input:checkbox')
.attr( 'readonly', true )
.next( 'label' )
.replaceWith( function(index, labelHtml) {
return jQuery(this).text().trim() + ' <span class="dashicons dashicons-lock"></span>';
} );
}
}
function toggleControls( isChecked ) {
var $controls = jQuery('#customize-control-hestia_top_bar_alignment, #customize-control-hestia_link_to_top_menu, #customize-control-hestia_link_to_top_widgets');
if ( isChecked ) {
$controls.addClass( 'customize-locked-control' );
} else {
$controls.removeClass( 'customize-locked-control' );
}
}
jQuery( document ).ready(
function () {
'use strict';
jQuery.aboutBackground = {
init: function () {
this.updateBackgroundControl();
this.updateBackgroundControlBuffer();
this.focusMenu();
this.updateEditorLink();
this.scrollToTop();
},
updateBackgroundControl: function () {
wp.customize(
'page_on_front', function( value ) {
value.bind(
function( newval ) {
jQuery.ajax({
type: 'POST',
data: {
action: 'update_image_buffer',
pid: newval,
nonce: imageObject.imagenonce
},
url: imageObject.ajaxurl,
success: function (result) {
var image = result.data;
var html;
if (image !== '' && image !== 'undefined') {
html = '<label for="hestia_feature_thumbnail-button">' +
'<span class="customize-control-title">About background</span>' +
'</label>' +
'<div class="attachment-media-view attachment-media-view-image landscape">' +
'<div class="thumbnail thumbnail-image">' +
'<img class="attachment-thumb" src="' + image + '" draggable="false" alt=""> ' +
'</div>' +
'<div class="actions">' +
'<button type="button" class="button remove-button">Remove</button>' +
'<button type="button" class="button upload-button control-focus" id="hestia_feature_thumbnail-button">Change Image</button> ' +
'<div style="clear:both"></div>' +
'</div>' +
'</div>';
} else {
html = '<label class="customize-control-title" for="customize-media-control-button-105">About background</label>' +
'<div class="customize-control-notifications-container" style="display: none;"><ul></ul></div>' +
'<div class="attachment-media-view">\n' +
'<div class="placeholder">' +
'No image selected' +
'</div>' +
'<div class="actions">' +
'<button type="button" class="button default-button">Default</button>' +
'<button type="button" class="button upload-button" id="customize-media-control-button-105">Select image</button>' +
'</div>' +
'</div>';
}
wp.customize.control( 'hestia_feature_thumbnail' ).container['0'].innerHTML = html;
wp.customize.instance( 'hestia_feature_thumbnail' ).previewer.refresh();
}
});
}
);
}
);
},
updateBackgroundControlBuffer: function () {
/**
* Update the buffer for about background.
*/
wp.customize( 'hestia_feature_thumbnail', function ( value ) {
value.bind( function ( newval ) {
jQuery.ajax({
type: 'POST',
url: imageObject.ajaxurl,
data: {
action: 'update_image_buffer',
value: newval,
nonce: imageObject.imagenonce,
}
});
});
});
},
/**
* Focus menu when the user clicks on customizer shortcut of the menu.
*/
focusMenu: function () {
wp.customize.previewer.bind(
'trigger-focus-menu', function() {
wp.customize.section( 'menu_locations' ).focus();
}
);
},
updateEditorLink: function () {
wp.customize(
'page_on_front', function( value ) {
value.bind(
function( newval ) {
if( typeof wp.customize.control( 'hestia_shortcut_editor' ) !== 'undefined' ){
var newLink = wp.customize.control( 'hestia_shortcut_editor' ).container['0'].innerHTML .replace(/(post=).*?(&)/,'$1' + newval + '$2');
wp.customize.control( 'hestia_shortcut_editor' ).container['0'].innerHTML = newLink;
}
}
);
}
);
},
/**
* Scroll to top feature
*/
scrollToTop: function () {
wp.customize( 'hestia_scroll_to_top_offset', function ( value ) {
value.bind( function ( newval ) {
var showScrollToTop = 0;
jQuery( window ).on( 'scroll', function () {
console.log(newval);
var y_scroll_pos = window.pageYOffset;
var headerElement = jQuery( '.page-header' );
var scroll_pos_test = 0;
if ( headerElement.length > 0 ) {
scroll_pos_test = headerElement.height();
} else {
headerElement = jQuery( '.container' );
scroll_pos_test = headerElement.offset().top - jQuery( '.navbar' ).height();
scroll_pos_test = scroll_pos_test > 0 ? scroll_pos_test : 0;
}
scroll_pos_test += newval;
if ( y_scroll_pos >= scroll_pos_test && showScrollToTop === 0 ) {
jQuery( '.hestia-scroll-to-top' ).addClass( 'hestia-fade' );
showScrollToTop = 1;
}
if ( y_scroll_pos <= scroll_pos_test && showScrollToTop === 1 ) {
jQuery( '.hestia-scroll-to-top' ).removeClass( 'hestia-fade' );
showScrollToTop = 0;
}
} );
jQuery( '.hestia-scroll-to-top' ).on( 'click', function () {
window.scroll( {
top: 0,
behavior: 'smooth'
} );
} );
} );
} );
},
};
jQuery.aboutBackground.init();
wp.customize(
'hestia_team_content', function ( value ) {
value.bind(
function () {
var authors_values;
var result = '';
if ( jQuery.isFunction( wp.customize._value.hestia_authors_on_blog ) ) {
authors_values = wp.customize._value.hestia_authors_on_blog();
}
jQuery( '#customize-control-hestia_team_content .customizer-repeater-general-control-repeater-container' ).each(
function () {
var title = jQuery( this ).find( '.customizer-repeater-title-control' ).val();
var id = jQuery( this ).find( '.social-repeater-box-id' ).val();
if ( typeof (title) !== 'undefined' && title !== '' && typeof (id) !== 'undefined' && id !== '' ) {
result += '<option value="' + id + '" ';
if ( authors_values && authors_values !== 'undefined' ) {
if ( authors_values.indexOf( id ) !== -1 ) {
result += 'selected';
}
}
result += '>' + title + '</option>';
}
}
);
jQuery( '#customize-control-hestia_authors_on_blog .repeater-multiselect-team' ).html( result );
}
);
}
);
/* Move controls to Widgets sections. Used for sidebar placeholders */
if ( typeof wp.customize.control( 'hestia_placeholder_sidebar_1' ) !== 'undefined' ) {
wp.customize.control( 'hestia_placeholder_sidebar_1' ).section( 'sidebar-widgets-sidebar-1' );
}
if ( typeof wp.customize.control( 'hestia_placeholder_sidebar_woocommerce' ) !== 'undefined' ) {
wp.customize.control( 'hestia_placeholder_sidebar_woocommerce' ).section( 'sidebar-widgets-sidebar-woocommerce' );
}
jQuery(document).on( 'click', '.quick-links a', function(e) {
e.preventDefault();
e.stopPropagation();
var control = jQuery( this ).data( 'control-focus' );
if ( control ){
wp.customize.control( control ).focus();
jQuery( 'label.' + control ).click();
}
var section = jQuery( this ).data( 'section-focus' );
if( section ){
wp.customize.section( section ).focus();
}
} );
jQuery( '.focus-customizer-header-image' ).on( 'click', function ( e ) {
e.preventDefault();
wp.customize.section( 'header_image' ).focus();
} );
/**
* Toggle section user clicks on customizer shortcut.
*/
var customize = wp.customize;
if( typeof customize !== 'undefined' && customize.hasOwnProperty('previewer') ) {
customize.previewer.bind(
'hestia-customize-disable-section', function ( data ) {
jQuery( '[data-customize-setting-link=' + data + ']' ).trigger( 'click' );
}
);
customize.previewer.bind(
'hestia-customize-focus-control', function ( data ) {
wp.customize.control( data ).focus();
}
);
}
// Toggle visibility of Header Video notice when active state change.
customize.control( 'header_video', function( headerVideoControl ) {
headerVideoControl.deferred.embedded.done( function() {
var toggleNotice = function() {
var section = customize.section( headerVideoControl.section() ), noticeCode = 'video_header_not_available';
section.notifications.remove( noticeCode );
};
toggleNotice();
headerVideoControl.active.bind( toggleNotice );
} );
} );
// Open navigation panel.
wp.customize.section( 'hestia_navigation' ).expanded.bind( function ( isExpanded ) {
if ( isExpanded ) {
// Lock minicart option.
tryAddLockIcon( 'customize-control-hestia_cart_icon', '_status' );
// Lock mobile menu option.
tryAddLockIcon( 'customize-control-hestia_mobile_menu_icon', '_status' );
}
} );
// Choose cart icon.
wp.customize.control( 'hestia_cart_icon', function( control ) {
control.container.on( 'change', 'input:radio', function() {
if ( 'code' === jQuery( this ).val() ) {
jQuery( '#customize-control-hestia_cart_custom_icon' ).addClass( 'hestia-show-custom-icon' );
} else {
jQuery( '#customize-control-hestia_cart_custom_icon' ).removeClass( 'hestia-show-custom-icon' );
}
} );
jQuery( 'input:radio:checked', control.container ).trigger( 'change' );
} );
// Toggle cart icon.
wp.customize.control( 'hestia_cart_icon_status', function( control ) {
control.container.on( 'change', 'input:checkbox', function() {
if ( jQuery( this ).is( ':checked' ) ) {
jQuery( '#customize-control-hestia_cart_icon' ).fadeIn( 'show' );
} else {
jQuery( '#customize-control-hestia_cart_icon' ).fadeOut();
}
} );
jQuery( 'input:checkbox:checked', control.container ).trigger( 'change' );
} );
wp.customize.control( 'hestia_top_bar_hide', function( control ) {
wp.customize.section('hestia_top_bar', function( section ) {
section.expanded.bind(function( isExpanded ) {
if ( isExpanded ) {
var isChecked = control.container.find('input:checkbox').is(':checked');
toggleControls( isChecked );
}
});
});
control.container.on( 'change', 'input:checkbox', function() {
var isChecked = jQuery(this).is(':checked');
toggleControls( isChecked );
});
});
wp.customize.control( 'hestia_navbar_transparent', function( control ) {
wp.customize.section('hestia_navigation', function( section ) {
section.expanded.bind(function( isExpanded ) {
if ( isExpanded ) {
if ( control.container.find('input:checkbox').is(':checked') ) {
wp.customize.control('hestia_link_to_add_icon').container.show();
} else {
wp.customize.control('hestia_link_to_add_icon').container.hide();
}
}
});
});
control.container.on( 'change', 'input:checkbox', function() {
if ( jQuery(this).is(':checked') ) {
wp.customize.control('hestia_link_to_add_icon').container.show();
} else {
wp.customize.control('hestia_link_to_add_icon').container.hide();
}
});
});
tryAddSaleBanner();
}
);