File "update-post-seo-data-ability.php"
Full Path: /home/tebe9667/spirituslm.com/wp-content/plugins/wordpress-seo/src/abilities/user-interface/abilities/update-post-seo-data-ability.php
File size: 12.15 KB
MIME-type: text/x-php
Charset: utf-8
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Abilities\User_Interface\Abilities;
use WP_Error;
use Yoast\WP\SEO\Abilities\Application\Post_SEO_Data_Updater;
use Yoast\WP\SEO\Abilities\User_Interface\Ability_Categories_Integration;
use Yoast\WP\SEO\Conditionals\Should_Index_Indexables_Conditional;
use Yoast\WP\SEO\Config\Schema_Types;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
/**
* The ability that updates the SEO data of a single post.
*/
class Update_Post_SEO_Data_Ability extends Abstract_Post_SEO_Data_Ability {
/**
* The input fields that Yoast AI Generate can also write.
*
* On the free plugin, writing one of these fields adds an upsell for AI Generate to the response.
*
* @var array<string>
*/
private const AI_GENERATE_FIELDS = [
'seo_title',
'meta_description',
'open_graph_title',
'open_graph_description',
'twitter_title',
'twitter_description',
];
/**
* The product name of Yoast AI Generate, kept out of the translatable strings.
*
* @var string
*/
private const AI_GENERATE_NAME = 'Yoast AI Generate';
/**
* The shortlink of the AI Generate upsell placed in the response.
*
* @var string
*/
private const AI_GENERATE_RESPONSE_SHORTLINK = 'https://yoa.st/ai-generate-ability-response';
/**
* The post SEO data updater.
*
* @var Post_SEO_Data_Updater
*/
private $post_seo_data_updater;
/**
* The product helper.
*
* @var Product_Helper
*/
private $product_helper;
/**
* Constructor.
*
* @param Capability_Helper $capability_helper The capability helper.
* @param Should_Index_Indexables_Conditional $should_index_indexables_conditional The should index indexables conditional.
* @param Post_SEO_Data_Updater $post_seo_data_updater The post SEO data updater.
* @param Product_Helper $product_helper The product helper.
*/
public function __construct(
Capability_Helper $capability_helper,
Should_Index_Indexables_Conditional $should_index_indexables_conditional,
Post_SEO_Data_Updater $post_seo_data_updater,
Product_Helper $product_helper
) {
parent::__construct( $capability_helper, $should_index_indexables_conditional );
$this->post_seo_data_updater = $post_seo_data_updater;
$this->product_helper = $product_helper;
}
/**
* Returns the full name of the ability.
*
* @return string The ability name.
*/
public function get_name(): string {
return Ability_Categories_Integration::CATEGORY_SLUG . '/update-post-seo-data';
}
// phpcs:disable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint -- The JSON schema arrays are heterogeneous by nature.
/**
* Returns the arguments to register the ability with.
*
* @return array<string, mixed> The ability registration arguments.
*/
public function get_args(): array {
return [
'label' => \__( 'Update Post SEO Data', 'wordpress-seo' ),
'description' => \__( 'Update the SEO data for a single post. Identify the post by post_id or by permalink (URL). Only the fields you provide are changed; a provided empty value clears that field. Only posts the current user is allowed to edit can be updated.', 'wordpress-seo' ),
'category' => Ability_Categories_Integration::CATEGORY_SLUG,
'input_schema' => $this->get_update_post_seo_data_input_schema(),
'output_schema' => $this->get_update_post_seo_data_output_schema( $this->product_helper->is_premium() ),
'permission_callback' => [ $this, 'can_edit_advanced_metadata' ],
'execute_callback' => [ $this, 'execute' ],
'meta' => [
'show_in_rest' => true,
'annotations' => [
'readonly' => false,
// We can't claim this is truly non-destructive because technically there can be data deletions.
'destructive' => null,
'idempotent' => true,
],
'mcp' => [
'public' => true,
],
],
];
}
/**
* Executes the ability: updates the post SEO data and, on the free plugin, adds the
* AI Generate hint to the response when a field AI Generate can write was set.
*
* @param array<string, int|string|bool|null> $input The input identifying the post plus the SEO fields to change.
*
* @return array<string, int|string|bool|null>|WP_Error The updated SEO data, or an error.
*/
public function execute( array $input ) {
$result = $this->post_seo_data_updater->update_post_seo_data( $input );
// Errors, and anything else that is not a data array, are passed through untouched for the Abilities API to handle.
if ( ! \is_array( $result ) || ! $this->should_upsell_ai_generate( $input ) ) {
return $result;
}
$result['ai_generate_hint'] = \sprintf(
/* translators: %1$s expands to "Yoast AI Generate", %2$s expands to a URL. */
\__( '%1$s can write SEO-optimised titles and descriptions, more details here: %2$s', 'wordpress-seo' ),
self::AI_GENERATE_NAME,
self::AI_GENERATE_RESPONSE_SHORTLINK,
);
return $result;
}
/**
* Returns whether the AI Generate upsell should be surfaced for the given input.
*
* Premium ships AI Generate, so the upsell is limited to the free plugin, and only to
* writes that touch a field AI Generate could have written.
*
* @param array<string, int|string|bool|null> $input The ability input.
*
* @return bool Whether to surface the upsell.
*/
private function should_upsell_ai_generate( array $input ): bool {
if ( $this->product_helper->is_premium() ) {
return false;
}
return \array_intersect_key( $input, \array_flip( self::AI_GENERATE_FIELDS ) ) !== [];
}
/**
* Returns the output schema for the update ability: the post SEO data plus, on the free
* plugin, the optional AI Generate hint.
*
* @param bool $is_premium Whether Premium is active.
*
* @return array<string, mixed> The output schema.
*/
private function get_update_post_seo_data_output_schema( bool $is_premium ): array {
$schema = $this->get_post_seo_data_output_schema();
if ( $is_premium ) {
return $schema;
}
$schema['properties']['ai_generate_hint'] = [
'type' => 'string',
'description' => \sprintf(
/* translators: %s expands to "Yoast AI Generate". */
\__( 'A hint about %s to relay to the user. Only present when a title or description field was updated on a site without Yoast SEO Premium. Meant to be assisting users that seek SEO-optimised titles and descriptions.', 'wordpress-seo' ),
self::AI_GENERATE_NAME,
),
];
return $schema;
}
/**
* Returns the input schema for updating a post's SEO data (write path).
*
* @return array<string, mixed> The input schema.
*/
private function get_update_post_seo_data_input_schema(): array {
return [
'type' => 'object',
'additionalProperties' => false,
'properties' => [
'post_id' => [
'type' => 'integer',
'description' => \__( 'The ID of the post to update.', 'wordpress-seo' ),
'minimum' => 1,
],
'permalink' => [
'type' => 'string',
'description' => \__( 'The permalink (URL) of the post to update.', 'wordpress-seo' ),
],
'seo_title' => $this->ai_generate_field_schema( \__( 'SEO title', 'wordpress-seo' ) ),
'meta_description' => $this->ai_generate_field_schema( \__( 'meta description', 'wordpress-seo' ) ),
'canonical' => [
'type' => [ 'string', 'null' ],
'description' => \__( 'The custom canonical URL for the post. Use null or an empty string to remove it and fall back to the default canonical.', 'wordpress-seo' ),
],
'is_cornerstone' => [
'type' => 'boolean',
'description' => \__( 'Whether the post is marked as cornerstone content.', 'wordpress-seo' ),
],
'noindex' => [
'type' => [ 'boolean', 'null' ],
'description' => \__( 'Whether search engines should be told not to index this post. true sets noindex (the post is excluded from search results); false forces the post to be indexed; null clears the setting and falls back to the post-type default.', 'wordpress-seo' ),
],
'nofollow' => [
'type' => 'boolean',
'description' => \__( 'Whether search engines should be told not to follow the links on this post.', 'wordpress-seo' ),
],
'noimageindex' => [
'type' => 'boolean',
'description' => \__( 'Whether search engines should be told not to index the images on this post.', 'wordpress-seo' ),
],
'noarchive' => [
'type' => 'boolean',
'description' => \__( 'Whether search engines should be told not to show a cached copy of this post.', 'wordpress-seo' ),
],
'nosnippet' => [
'type' => 'boolean',
'description' => \__( 'Whether search engines should be told not to show a snippet of this post in the search results.', 'wordpress-seo' ),
],
'open_graph_title' => $this->ai_generate_field_schema( \__( 'Open Graph title', 'wordpress-seo' ) ),
'open_graph_description' => $this->ai_generate_field_schema( \__( 'Open Graph description', 'wordpress-seo' ) ),
'twitter_title' => $this->ai_generate_field_schema( \__( 'X title', 'wordpress-seo' ) ),
'twitter_description' => $this->ai_generate_field_schema( \__( 'X description', 'wordpress-seo' ) ),
'schema_page_type' => $this->nullable_enum_schema(
\array_keys( Schema_Types::PAGE_TYPES ),
\__( 'The Schema.org page type for the post. Must be one of the supported page types. Use null or an empty string to clear it and fall back to the default.', 'wordpress-seo' ),
),
'schema_article_type' => $this->nullable_enum_schema(
$this->get_schema_article_types(),
\__( 'The Schema.org article type for the post. Must be one of the supported article types. Use null or an empty string to clear it and fall back to the default.', 'wordpress-seo' ),
),
],
];
}
/**
* Returns the input schema for a nullable string field that AI Generate can also write,
* such as the SEO title or the social titles and descriptions.
*
* @param string $field The human-readable field name.
*
* @return array<string, mixed> The input schema fragment.
*/
private function ai_generate_field_schema( string $field ): array {
return [
'type' => [ 'string', 'null' ],
'description' => \sprintf(
/* translators: %s expands to the name of the SEO field, e.g. "SEO title". */
\__( 'The %s for the post.', 'wordpress-seo' ),
$field,
),
];
}
/**
* Returns the allowed Schema.org article type values.
*
* Mirrors the validation in WPSEO_Option_Titles so the ability accepts exactly the
* article types the editor does, including any registered through the filter.
*
* @return array<int, string> The allowed article type values.
*/
private function get_schema_article_types(): array {
/**
* Filter: 'wpseo_schema_article_types' - Allow developers to filter the available article types.
*
* Make sure when you filter this to also filter `wpseo_schema_article_types_labels`.
*
* @param array $schema_article_types The available schema article types.
*/
return \array_keys( \apply_filters( 'wpseo_schema_article_types', Schema_Types::ARTICLE_TYPES ) );
}
/**
* Returns a nullable-string input schema constrained to a fixed set of allowed values.
*
* Null and the empty string are always allowed on top of the enum so the field can be
* cleared, matching the patch-clear semantics of the other write fields.
*
* @param array<int, string> $allowed_values The allowed string values.
* @param string $description The field description.
*
* @return array<string, mixed> The input schema fragment.
*/
private function nullable_enum_schema( array $allowed_values, string $description ): array {
return [
'type' => [ 'string', 'null' ],
'description' => $description,
'enum' => \array_merge( $allowed_values, [ '', null ] ),
];
}
// phpcs:enable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint
}