PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.1.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.1.1
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / abilities / content / class-update-term-seo.php

class-update-term-seo.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.1.1, at includes/abilities/content/class-update-term-seo.php

229 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Update term SEO ability.
4 *
5 * @package ThinkRank\Abilities\Content
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Abilities\Content;
11
12 use ThinkRank\Abilities\Ability_Base;
13 use ThinkRank\SEO\Pattern_Resolver;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Updates ThinkRank SEO data for a taxonomy term.
21 */
22 class Update_Term_Seo extends Ability_Base {
23 /**
24 * Constructor.
25 */
26 public function __construct() {
27 $this->id = 'thinkrank/update-term-seo';
28 $this->label = __( 'Update ThinkRank Term SEO', 'thinkrank' );
29 $this->description = __( 'Update ThinkRank SEO metadata for a taxonomy term. Read the current values with get-term-seo first; only the fields you pass are changed.', 'thinkrank' );
30 }
31
32 /**
33 * {@inheritDoc}
34 *
35 * @return array<string, mixed>
36 */
37 public function get_input_schema() {
38 return [
39 'type' => 'object',
40 'additionalProperties' => false,
41 'properties' => [
42 'term_id' => [
43 'type' => 'integer',
44 'description' => __( 'The target term ID.', 'thinkrank' ),
45 ],
46 'settings' => [
47 'type' => 'object',
48 'additionalProperties' => true,
49 'description' => __( 'ThinkRank term SEO fields to update.', 'thinkrank' ),
50 'properties' => [
51 'title' => [ 'type' => 'string' ],
52 'description' => [ 'type' => 'string' ],
53 'canonical_url' => [ 'type' => 'string' ],
54 'focus_keyword' => [ 'type' => 'string' ],
55 'robots_meta_enabled' => [ 'type' => 'boolean' ],
56 'robots_meta' => [ 'type' => 'object' ],
57 'advanced_robots_meta' => [ 'type' => 'object' ],
58 'og_title' => [ 'type' => 'string' ],
59 'og_description' => [ 'type' => 'string' ],
60 'og_image' => [ 'type' => 'string' ],
61 'twitter_title' => [ 'type' => 'string' ],
62 'twitter_description' => [ 'type' => 'string' ],
63 'twitter_image' => [ 'type' => 'string' ],
64 ],
65 ],
66 ],
67 'required' => [ 'term_id', 'settings' ],
68 ];
69 }
70
71 /**
72 * {@inheritDoc}
73 *
74 * @return array<string, mixed>
75 */
76 public function get_output_schema() {
77 return [
78 'type' => 'object',
79 'properties' => [
80 'success' => [ 'type' => 'boolean' ],
81 'message' => [ 'type' => 'string' ],
82 'term_id' => [ 'type' => 'integer' ],
83 ],
84 ];
85 }
86
87 /**
88 * Execute ability.
89 *
90 * @param array<string, mixed> $input Ability input payload.
91 * @return array<string, mixed>|\WP_Error
92 */
93 public function execute( $input ) {
94 $term_id = isset( $input['term_id'] ) ? absint( $input['term_id'] ) : 0;
95 $settings = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : [];
96
97 if ( ! $term_id || empty( $settings ) ) {
98 return new \WP_Error(
99 'thinkrank_invalid_term_update',
100 __( 'A valid term ID and settings payload are required.', 'thinkrank' ),
101 [ 'status' => 400 ]
102 );
103 }
104
105 if ( ! get_term( $term_id ) instanceof \WP_Term ) {
106 return new \WP_Error(
107 'thinkrank_term_not_found',
108 __( 'No term was found for the provided ID.', 'thinkrank' ),
109 [ 'status' => 404 ]
110 );
111 }
112
113 if ( ! current_user_can( 'manage_options' ) ) {
114 return new \WP_Error(
115 'thinkrank_cannot_edit_term',
116 __( 'You are not allowed to edit term SEO settings.', 'thinkrank' ),
117 [ 'status' => 403 ]
118 );
119 }
120
121 $touched = $this->save_term_meta( $term_id, $settings );
122
123 if ( ! $touched ) {
124 return new \WP_Error(
125 'thinkrank_no_valid_term_meta_keys',
126 __( 'No valid ThinkRank term SEO keys were provided.', 'thinkrank' ),
127 [ 'status' => 400 ]
128 );
129 }
130
131 return [
132 'success' => true,
133 'message' => __( 'Term SEO metadata updated.', 'thinkrank' ),
134 'term_id' => $term_id,
135 ];
136 }
137
138 /**
139 * Persist provided term SEO meta, sanitizing per field type.
140 *
141 * @param int $term_id Term ID.
142 * @param array<string, mixed> $settings Normalized settings payload.
143 * @return bool Whether any meta key was written or deleted.
144 */
145 private function save_term_meta( $term_id, array $settings ) {
146 $touched = false;
147
148 // The title/description fields are variable-tag templates — the frontend
149 // runs each through Pattern_Resolver::resolve_term_value() — so they are
150 // sanitized as templates. sanitize_text_field() would strip %date% and
151 // %category% as percent-encoding and store "te%" / "tegory%" (#521).
152 $template_fields = [
153 'title' => '_thinkrank_seo_title',
154 'og_title' => '_thinkrank_og_title',
155 'twitter_title' => '_thinkrank_twitter_title',
156 ];
157 foreach ( $template_fields as $key => $meta_key ) {
158 if ( array_key_exists( $key, $settings ) ) {
159 $this->save_meta( $term_id, $meta_key, Pattern_Resolver::sanitize_template( (string) $settings[ $key ] ) );
160 $touched = true;
161 }
162 }
163
164 // Plain text, not a template.
165 if ( array_key_exists( 'focus_keyword', $settings ) ) {
166 $this->save_meta( $term_id, '_thinkrank_focus_keyword', sanitize_text_field( (string) $settings['focus_keyword'] ) );
167 $touched = true;
168 }
169
170 $textarea_fields = [
171 'description' => '_thinkrank_meta_description',
172 'og_description' => '_thinkrank_og_description',
173 'twitter_description' => '_thinkrank_twitter_description',
174 ];
175 foreach ( $textarea_fields as $key => $meta_key ) {
176 if ( array_key_exists( $key, $settings ) ) {
177 $this->save_meta( $term_id, $meta_key, Pattern_Resolver::sanitize_template_textarea( (string) $settings[ $key ] ) );
178 $touched = true;
179 }
180 }
181
182 $url_fields = [
183 'canonical_url' => '_thinkrank_canonical_url',
184 'og_image' => '_thinkrank_og_image',
185 'twitter_image' => '_thinkrank_twitter_image',
186 ];
187 foreach ( $url_fields as $key => $meta_key ) {
188 if ( array_key_exists( $key, $settings ) ) {
189 $this->save_meta( $term_id, $meta_key, esc_url_raw( (string) $settings[ $key ] ) );
190 $touched = true;
191 }
192 }
193
194 if ( array_key_exists( 'robots_meta_enabled', $settings ) ) {
195 update_term_meta( $term_id, '_thinkrank_robots_meta_enabled', (int) (bool) $settings['robots_meta_enabled'] );
196 $touched = true;
197 }
198
199 if ( array_key_exists( 'robots_meta', $settings ) ) {
200 update_term_meta( $term_id, '_thinkrank_robots_meta', (string) wp_json_encode( (array) $settings['robots_meta'] ) );
201 $touched = true;
202 }
203
204 if ( array_key_exists( 'advanced_robots_meta', $settings ) ) {
205 update_term_meta( $term_id, '_thinkrank_advanced_robots_meta', (string) wp_json_encode( (array) $settings['advanced_robots_meta'] ) );
206 $touched = true;
207 }
208
209 return $touched;
210 }
211
212 /**
213 * Update a term meta value, deleting it when the sanitized value is empty.
214 *
215 * @param int $term_id Term ID.
216 * @param string $meta_key Meta key.
217 * @param string $value Sanitized value.
218 * @return void
219 */
220 private function save_meta( $term_id, $meta_key, $value ) {
221 if ( '' === $value ) {
222 delete_term_meta( $term_id, $meta_key );
223 return;
224 }
225
226 update_term_meta( $term_id, $meta_key, $value );
227 }
228 }
229