| 1 |
<?php |
| 2 |
/** |
| 3 |
* Update site identity settings ability. |
| 4 |
* |
| 5 |
* @package ThinkRank\Abilities\Settings |
| 6 |
*/ |
| 7 |
|
| 8 |
declare(strict_types=1); |
| 9 |
|
| 10 |
namespace ThinkRank\Abilities\Settings; |
| 11 |
|
| 12 |
use ThinkRank\Abilities\Ability_Base; |
| 13 |
use ThinkRank\SEO\Site_Identity_Manager; |
| 14 |
|
| 15 |
if ( ! defined( 'ABSPATH' ) ) { |
| 16 |
exit; // Exit if accessed directly. |
| 17 |
} |
| 18 |
|
| 19 |
/** |
| 20 |
* Updates ThinkRank site identity settings. |
| 21 |
* |
| 22 |
* Only the non-robots identity keys are writable here. The robots.txt keys |
| 23 |
* (robots_txt_enabled, allow_search_engines, robots_txt_content) are never |
| 24 |
* touched so this ability cannot clobber the robots.txt configuration. |
| 25 |
*/ |
| 26 |
class Update_Site_Identity_Settings extends Ability_Base { |
| 27 |
/** |
| 28 |
* Constructor. |
| 29 |
*/ |
| 30 |
public function __construct() { |
| 31 |
$this->id = 'thinkrank/update-site-identity-settings'; |
| 32 |
$this->label = __( 'Update ThinkRank Site Identity Settings', 'thinkrank' ); |
| 33 |
$this->description = __( 'Update ThinkRank site identity settings: the homepage/category/tag/author/search/archive title templates, site name and description, breadcrumb configuration, brand imagery, homepage hero, and the business details behind LocalBusiness schema. Robots.txt contents and schema toggles are never modified by this ability. Read the current values with get-site-identity-settings first; only the keys you pass are changed.', 'thinkrank' ); |
| 34 |
} |
| 35 |
|
| 36 |
/** |
| 37 |
* {@inheritDoc} |
| 38 |
* |
| 39 |
* @return array<string, bool|float|string> |
| 40 |
*/ |
| 41 |
public function get_annotations() { |
| 42 |
return [ |
| 43 |
'readonly' => false, |
| 44 |
// Settings writes are recoverable: the matching get-* ability reads |
| 45 |
// the previous value, so nothing is lost that cannot be put back. |
| 46 |
// `destructive` is reserved for calls that lose data or reach |
| 47 |
// outside the site, and marking routine configuration with it made |
| 48 |
// MCP clients demand a human approval for every save — which users |
| 49 |
// reported as a permission bug, because the client's refusal reads |
| 50 |
// as "No approval received" (#675). |
| 51 |
'destructive' => false, |
| 52 |
'idempotent' => true, |
| 53 |
'priority' => 2.0, |
| 54 |
'openWorldHint' => false, |
| 55 |
]; |
| 56 |
} |
| 57 |
|
| 58 |
/** |
| 59 |
* Build the JSON schema properties for site identity settings. |
| 60 |
* |
| 61 |
* @return array<string, mixed> |
| 62 |
*/ |
| 63 |
private static function schema_properties() { |
| 64 |
return Settings_Key_Map::site_identity(); |
| 65 |
} |
| 66 |
|
| 67 |
/** |
| 68 |
* {@inheritDoc} |
| 69 |
* |
| 70 |
* @return array<string, mixed> |
| 71 |
*/ |
| 72 |
public function get_input_schema() { |
| 73 |
return [ |
| 74 |
'type' => 'object', |
| 75 |
'additionalProperties' => false, |
| 76 |
'properties' => [ |
| 77 |
'settings' => [ |
| 78 |
'type' => 'object', |
| 79 |
'description' => __( 'Site identity settings to update.', 'thinkrank' ), |
| 80 |
'properties' => self::schema_properties(), |
| 81 |
], |
| 82 |
], |
| 83 |
'required' => [ 'settings' ], |
| 84 |
]; |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* {@inheritDoc} |
| 89 |
* |
| 90 |
* @return array<string, mixed> |
| 91 |
*/ |
| 92 |
public function get_output_schema() { |
| 93 |
return [ |
| 94 |
'type' => 'object', |
| 95 |
'properties' => [ |
| 96 |
'success' => [ 'type' => 'boolean' ], |
| 97 |
'message' => [ 'type' => 'string' ], |
| 98 |
], |
| 99 |
]; |
| 100 |
} |
| 101 |
|
| 102 |
/** |
| 103 |
* Execute ability. |
| 104 |
* |
| 105 |
* @param array<string, mixed> $input Ability input payload. |
| 106 |
* @return array<string, mixed>|\WP_Error |
| 107 |
*/ |
| 108 |
public function execute( $input ) { |
| 109 |
$settings = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : []; |
| 110 |
|
| 111 |
if ( empty( $settings ) ) { |
| 112 |
return new \WP_Error( |
| 113 |
'thinkrank_missing_site_identity_settings_payload', |
| 114 |
__( 'A site identity settings payload is required.', 'thinkrank' ), |
| 115 |
[ 'status' => 400 ] |
| 116 |
); |
| 117 |
} |
| 118 |
|
| 119 |
$mgr = new Site_Identity_Manager(); |
| 120 |
$merged = $mgr->get_settings( 'site', null ); |
| 121 |
$patch = Settings_Key_Map::coerce( Settings_Key_Map::site_identity(), $settings ); |
| 122 |
$merged = array_merge( $merged, $patch ); |
| 123 |
|
| 124 |
if ( empty( $patch ) ) { |
| 125 |
return new \WP_Error( |
| 126 |
'thinkrank_no_valid_site_identity_setting_keys', |
| 127 |
__( 'No valid site identity setting keys were provided.', 'thinkrank' ), |
| 128 |
[ 'status' => 400 ] |
| 129 |
); |
| 130 |
} |
| 131 |
|
| 132 |
$result = $mgr->save_settings( 'site', null, $merged ); |
| 133 |
|
| 134 |
if ( $result ) { |
| 135 |
// Mirror the REST route (class-site-identity-endpoint.php): a stored |
| 136 |
// setting is not the served file. When a physical robots.txt exists, |
| 137 |
// it keeps serving the previous body — and the previous allow/block |
| 138 |
// set — until it is rewritten, so an agent that blocked GPTBot got |
| 139 |
// `success: true` while the crawler was still allowed. Worse, |
| 140 |
// get_robots_txt_delivery() strips the AI block before comparing, on |
| 141 |
// the assumption the two are identical by construction, so the admin |
| 142 |
// screen reported "in sync" over the drift. Only the keys that |
| 143 |
// change the served output trigger the write. |
| 144 |
if ( array_key_exists( 'robots_txt_content', $patch ) |
| 145 |
|| array_key_exists( 'robots_txt_enabled', $patch ) |
| 146 |
|| array_key_exists( 'ai_crawler_rules', $patch ) |
| 147 |
) { |
| 148 |
$mgr->sync_robots_txt_file(); |
| 149 |
} |
| 150 |
|
| 151 |
return [ |
| 152 |
'success' => true, |
| 153 |
'message' => __( 'Site identity settings updated.', 'thinkrank' ), |
| 154 |
]; |
| 155 |
} |
| 156 |
|
| 157 |
// The manager recorded why the save failed; pass it on rather than |
| 158 |
// leaving the caller with a fixed string it cannot act on. |
| 159 |
$reason = $mgr->get_last_save_error(); |
| 160 |
|
| 161 |
return [ |
| 162 |
'success' => false, |
| 163 |
'message' => '' !== $reason |
| 164 |
/* translators: %s: reason the save failed. */ |
| 165 |
? sprintf( __( 'Failed to update site identity settings: %s', 'thinkrank' ), $reason ) |
| 166 |
: __( 'Failed to update site identity settings.', 'thinkrank' ), |
| 167 |
]; |
| 168 |
} |
| 169 |
} |
| 170 |
|