PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
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.7.0, at includes/abilities/content/class-update-term-seo.php

259 lines 8.0 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\Object_Redirect;
14 use ThinkRank\SEO\Pattern_Resolver;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit; // Exit if accessed directly.
18 }
19
20 /**
21 * Updates ThinkRank SEO data for a taxonomy term.
22 */
23 class Update_Term_Seo extends Ability_Base {
24 /**
25 * Constructor.
26 */
27 public function __construct() {
28 $this->id = 'thinkrank/update-term-seo';
29 $this->label = __( 'Update ThinkRank Term SEO', 'thinkrank' );
30 $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' );
31 }
32
33 /**
34 * {@inheritDoc}
35 *
36 * @return array<string, mixed>
37 */
38 public function get_input_schema() {
39 return [
40 'type' => 'object',
41 'additionalProperties' => false,
42 'properties' => [
43 'term_id' => [
44 'type' => 'integer',
45 'description' => __( 'The target term ID.', 'thinkrank' ),
46 ],
47 'settings' => [
48 'type' => 'object',
49 'additionalProperties' => true,
50 'description' => __( 'ThinkRank term SEO fields to update.', 'thinkrank' ),
51 'properties' => [
52 'title' => [ 'type' => 'string' ],
53 'description' => [ 'type' => 'string' ],
54 'canonical_url' => [ 'type' => 'string' ],
55 'redirect_url' => [
56 'type' => 'string',
57 'description' => __( 'Send visitors from this term archive to this URL. Empty string removes the redirect. Requires ThinkRank Pro.', 'thinkrank' ),
58 ],
59 'redirect_type' => [
60 'type' => 'integer',
61 'enum' => Object_Redirect::TYPES,
62 'description' => __( 'Redirect status code. Defaults to 301.', 'thinkrank' ),
63 ],
64 'focus_keyword' => [ 'type' => 'string' ],
65 'robots_meta_enabled' => [ 'type' => 'boolean' ],
66 'robots_meta' => [ 'type' => 'object' ],
67 'advanced_robots_meta' => [ 'type' => 'object' ],
68 'og_title' => [ 'type' => 'string' ],
69 'og_description' => [ 'type' => 'string' ],
70 'og_image' => [ 'type' => 'string' ],
71 'twitter_title' => [ 'type' => 'string' ],
72 'twitter_description' => [ 'type' => 'string' ],
73 'twitter_image' => [ 'type' => 'string' ],
74 ],
75 ],
76 ],
77 'required' => [ 'term_id', 'settings' ],
78 ];
79 }
80
81 /**
82 * {@inheritDoc}
83 *
84 * @return array<string, mixed>
85 */
86 public function get_output_schema() {
87 return [
88 'type' => 'object',
89 'properties' => [
90 'success' => [ 'type' => 'boolean' ],
91 'message' => [ 'type' => 'string' ],
92 'term_id' => [ 'type' => 'integer' ],
93 ],
94 ];
95 }
96
97 /**
98 * Execute ability.
99 *
100 * @param array<string, mixed> $input Ability input payload.
101 * @return array<string, mixed>|\WP_Error
102 */
103 public function execute( $input ) {
104 $term_id = isset( $input['term_id'] ) ? absint( $input['term_id'] ) : 0;
105 $settings = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : [];
106
107 if ( ! $term_id || empty( $settings ) ) {
108 return new \WP_Error(
109 'thinkrank_invalid_term_update',
110 __( 'A valid term ID and settings payload are required.', 'thinkrank' ),
111 [ 'status' => 400 ]
112 );
113 }
114
115 if ( ! get_term( $term_id ) instanceof \WP_Term ) {
116 return new \WP_Error(
117 'thinkrank_term_not_found',
118 __( 'No term was found for the provided ID.', 'thinkrank' ),
119 [ 'status' => 404 ]
120 );
121 }
122
123 if ( ! current_user_can( 'manage_options' ) ) {
124 return new \WP_Error(
125 'thinkrank_cannot_edit_term',
126 __( 'You are not allowed to edit term SEO settings.', 'thinkrank' ),
127 [ 'status' => 403 ]
128 );
129 }
130
131 $touched = $this->save_term_meta( $term_id, $settings );
132
133 // The redirect is not term meta — Pro's rules table holds it — so it is
134 // saved separately, and unlike the meta fields it can be refused.
135 $redirect_touched = array_key_exists( 'redirect_url', $settings );
136 if ( $redirect_touched ) {
137 $redirect_result = Object_Redirect::save(
138 'term',
139 $term_id,
140 (string) $settings['redirect_url'],
141 $settings['redirect_type'] ?? Object_Redirect::DEFAULT_TYPE
142 );
143
144 if ( is_wp_error( $redirect_result ) ) {
145 return new \WP_Error(
146 $redirect_result->get_error_code(),
147 $redirect_result->get_error_message(),
148 [ 'status' => 400 ]
149 );
150 }
151 }
152
153 if ( ! $touched && ! $redirect_touched ) {
154 return new \WP_Error(
155 'thinkrank_no_valid_term_meta_keys',
156 __( 'No valid ThinkRank term SEO keys were provided.', 'thinkrank' ),
157 [ 'status' => 400 ]
158 );
159 }
160
161 return [
162 'success' => true,
163 'message' => __( 'Term SEO metadata updated.', 'thinkrank' ),
164 'term_id' => $term_id,
165 ];
166 }
167
168 /**
169 * Persist provided term SEO meta, sanitizing per field type.
170 *
171 * @param int $term_id Term ID.
172 * @param array<string, mixed> $settings Normalized settings payload.
173 * @return bool Whether any meta key was written or deleted.
174 */
175 private function save_term_meta( $term_id, array $settings ) {
176 $touched = false;
177
178 // The title/description fields are variable-tag templates — the frontend
179 // runs each through Pattern_Resolver::resolve_term_value() — so they are
180 // sanitized as templates. sanitize_text_field() would strip %date% and
181 // %category% as percent-encoding and store "te%" / "tegory%" (#521).
182 $template_fields = [
183 'title' => '_thinkrank_seo_title',
184 'og_title' => '_thinkrank_og_title',
185 'twitter_title' => '_thinkrank_twitter_title',
186 ];
187 foreach ( $template_fields as $key => $meta_key ) {
188 if ( array_key_exists( $key, $settings ) ) {
189 $this->save_meta( $term_id, $meta_key, Pattern_Resolver::sanitize_template( (string) $settings[ $key ] ) );
190 $touched = true;
191 }
192 }
193
194 // Plain text, not a template.
195 if ( array_key_exists( 'focus_keyword', $settings ) ) {
196 $this->save_meta( $term_id, '_thinkrank_focus_keyword', sanitize_text_field( (string) $settings['focus_keyword'] ) );
197 $touched = true;
198 }
199
200 $textarea_fields = [
201 'description' => '_thinkrank_meta_description',
202 'og_description' => '_thinkrank_og_description',
203 'twitter_description' => '_thinkrank_twitter_description',
204 ];
205 foreach ( $textarea_fields as $key => $meta_key ) {
206 if ( array_key_exists( $key, $settings ) ) {
207 $this->save_meta( $term_id, $meta_key, Pattern_Resolver::sanitize_template_textarea( (string) $settings[ $key ] ) );
208 $touched = true;
209 }
210 }
211
212 $url_fields = [
213 'canonical_url' => '_thinkrank_canonical_url',
214 'og_image' => '_thinkrank_og_image',
215 'twitter_image' => '_thinkrank_twitter_image',
216 ];
217 foreach ( $url_fields as $key => $meta_key ) {
218 if ( array_key_exists( $key, $settings ) ) {
219 $this->save_meta( $term_id, $meta_key, esc_url_raw( (string) $settings[ $key ] ) );
220 $touched = true;
221 }
222 }
223
224 if ( array_key_exists( 'robots_meta_enabled', $settings ) ) {
225 update_term_meta( $term_id, '_thinkrank_robots_meta_enabled', (int) (bool) $settings['robots_meta_enabled'] );
226 $touched = true;
227 }
228
229 if ( array_key_exists( 'robots_meta', $settings ) ) {
230 update_term_meta( $term_id, '_thinkrank_robots_meta', (string) wp_json_encode( (array) $settings['robots_meta'] ) );
231 $touched = true;
232 }
233
234 if ( array_key_exists( 'advanced_robots_meta', $settings ) ) {
235 update_term_meta( $term_id, '_thinkrank_advanced_robots_meta', (string) wp_json_encode( (array) $settings['advanced_robots_meta'] ) );
236 $touched = true;
237 }
238
239 return $touched;
240 }
241
242 /**
243 * Update a term meta value, deleting it when the sanitized value is empty.
244 *
245 * @param int $term_id Term ID.
246 * @param string $meta_key Meta key.
247 * @param string $value Sanitized value.
248 * @return void
249 */
250 private function save_meta( $term_id, $meta_key, $value ) {
251 if ( '' === $value ) {
252 delete_term_meta( $term_id, $meta_key );
253 return;
254 }
255
256 update_term_meta( $term_id, $meta_key, $value );
257 }
258 }
259