PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.4
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.4
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / bulk-editor / user-interface / scores-route.php

scores-route.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.4, at src/bulk-editor/user-interface/scores-route.php

236 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4 namespace Yoast\WP\SEO\Bulk_Editor\User_Interface;
5
6 use WP_Error;
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use Yoast\WP\SEO\Bulk_Editor\Application\Updates\Score_Updater;
10 use Yoast\WP\SEO\Bulk_Editor\Domain\Updates\Batch_Limit;
11 use Yoast\WP\SEO\Bulk_Editor\Domain\Updates\Post_Score_Update;
12 use Yoast\WP\SEO\Conditionals\No_Conditionals;
13 use Yoast\WP\SEO\Main;
14 use Yoast\WP\SEO\Routes\Route_Interface;
15
16 /**
17 * Registers the route that persists the per-field scores the bulk editor computes after a save.
18 */
19 class Scores_Route implements Route_Interface {
20
21 use No_Conditionals;
22
23 /**
24 * The namespace for this route.
25 *
26 * @var string
27 */
28 public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
29
30 /**
31 * The prefix for this route.
32 *
33 * @var string
34 */
35 public const ROUTE_PREFIX = '/bulk_editor/update_scores';
36
37 /**
38 * The request field carrying the SEO title score.
39 *
40 * @var string
41 */
42 private const SEO_TITLE_SCORE_FIELD = 'seo_title_score';
43
44 /**
45 * The request field carrying the meta description score.
46 *
47 * @var string
48 */
49 private const META_DESCRIPTION_SCORE_FIELD = 'meta_description_score';
50
51 /**
52 * The lowest score a field can carry.
53 *
54 * @var int
55 */
56 private const MIN_SCORE = 0;
57
58 /**
59 * The highest score a field can carry.
60 *
61 * @var int
62 */
63 private const MAX_SCORE = 100;
64
65 /**
66 * The score updater.
67 *
68 * @var Score_Updater
69 */
70 private $score_updater;
71
72 /**
73 * The constructor.
74 *
75 * @param Score_Updater $score_updater The score updater.
76 */
77 public function __construct( Score_Updater $score_updater ) {
78 $this->score_updater = $score_updater;
79 }
80
81 /**
82 * Registers routes with WordPress.
83 *
84 * @return void
85 */
86 public function register_routes() {
87 \register_rest_route(
88 self::ROUTE_NAMESPACE,
89 self::ROUTE_PREFIX,
90 [
91 'methods' => 'POST',
92 'args' => [
93 'items' => [
94 'required' => true,
95 'type' => 'array',
96 'description' => 'The per-post score updates to apply.',
97 'validate_callback' => [ $this, 'validate_items' ],
98 ],
99 ],
100 'callback' => [ $this, 'update' ],
101 'permission_callback' => [ $this, 'check_permissions' ],
102 ],
103 );
104 }
105
106 // phpcs:disable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint -- The validate callback receives whatever the client sent.
107
108 /**
109 * Validates the items argument.
110 *
111 * @param mixed $items The items argument value.
112 *
113 * @return true|WP_Error True when valid, a WP_Error otherwise.
114 */
115 public function validate_items( $items ) {
116 if ( ! \is_array( $items ) ) {
117 return $this->reject( 'rest_invalid_items', 'The items argument must be an array.' );
118 }
119
120 $count = \count( $items );
121
122 if ( $count < 1 ) {
123 return $this->reject( 'rest_no_items', 'A batch must contain at least one item.' );
124 }
125
126 if ( ! Batch_Limit::is_within_limit( $count ) ) {
127 return $this->reject(
128 'rest_too_many_items',
129 \sprintf( 'A batch may contain at most %d items.', Batch_Limit::MAX_ITEMS ),
130 );
131 }
132
133 foreach ( $items as $item ) {
134 $error = $this->validate_item( $item );
135 if ( $error !== true ) {
136 return $error;
137 }
138 }
139
140 return true;
141 }
142
143 /**
144 * Validates a single item.
145 *
146 * @param mixed $item The item to validate.
147 *
148 * @return true|WP_Error True when valid, a WP_Error otherwise.
149 */
150 private function validate_item( $item ) {
151 if ( ! \is_array( $item ) ) {
152 return $this->reject( 'rest_invalid_item', 'Each item must be an object.' );
153 }
154
155 if ( ! \array_key_exists( 'id', $item ) || ! \is_int( $item['id'] ) ) {
156 return $this->reject( 'rest_invalid_item_id', 'Each item must contain an integer id.' );
157 }
158
159 $has_seo_title = \array_key_exists( self::SEO_TITLE_SCORE_FIELD, $item );
160 $has_meta_description = \array_key_exists( self::META_DESCRIPTION_SCORE_FIELD, $item );
161
162 if ( ! $has_seo_title && ! $has_meta_description ) {
163 return $this->reject(
164 'rest_no_scores_to_update',
165 \sprintf( 'Each item must contain a %s or a %s.', self::SEO_TITLE_SCORE_FIELD, self::META_DESCRIPTION_SCORE_FIELD ),
166 );
167 }
168
169 foreach ( [ self::SEO_TITLE_SCORE_FIELD, self::META_DESCRIPTION_SCORE_FIELD ] as $field ) {
170 if ( \array_key_exists( $field, $item ) && ! $this->is_valid_score( $item[ $field ] ) ) {
171 return $this->reject(
172 'rest_invalid_item_score',
173 \sprintf( 'The %s field must be an integer between %d and %d.', $field, self::MIN_SCORE, self::MAX_SCORE ),
174 );
175 }
176 }
177
178 return true;
179 }
180
181 /**
182 * Whether the given value is a valid score.
183 *
184 * @param mixed $value The value to check.
185 *
186 * @return bool Whether the value is an integer within the allowed range.
187 */
188 private function is_valid_score( $value ): bool {
189 return \is_int( $value ) && $value >= self::MIN_SCORE && $value <= self::MAX_SCORE;
190 }
191
192 // phpcs:enable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint
193
194 /**
195 * Builds the matching error response.
196 *
197 * @param string $code The error code.
198 * @param string $message The human-readable error message.
199 *
200 * @return WP_Error The error response with a 400 status.
201 */
202 private function reject( string $code, string $message ): WP_Error {
203 return new WP_Error( $code, $message, [ 'status' => 400 ] );
204 }
205
206 /**
207 * Checks whether the current user is allowed to use the bulk editor.
208 *
209 * @return bool Whether the current user is allowed to use the bulk editor.
210 */
211 public function check_permissions(): bool {
212 return \current_user_can( 'wpseo_manage_options' );
213 }
214
215 /**
216 * Applies the requested score updates.
217 *
218 * @param WP_REST_Request $request The request object.
219 *
220 * @return WP_REST_Response The per-item results of the update.
221 */
222 public function update( WP_REST_Request $request ): WP_REST_Response {
223 $updates = [];
224
225 foreach ( $request->get_param( 'items' ) as $item ) {
226 $updates[] = new Post_Score_Update(
227 (int) $item['id'],
228 ( \array_key_exists( self::SEO_TITLE_SCORE_FIELD, $item ) ) ? (int) $item[ self::SEO_TITLE_SCORE_FIELD ] : null,
229 ( \array_key_exists( self::META_DESCRIPTION_SCORE_FIELD, $item ) ) ? (int) $item[ self::META_DESCRIPTION_SCORE_FIELD ] : null,
230 );
231 }
232
233 return new WP_REST_Response( $this->score_updater->update( $updates )->to_array() );
234 }
235 }
236