PluginProbe
Parse.ly / 3.24.3
Parse.ly v3.24.3
3.24.3 3.24.2 3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 All 107 releases
← All changes | src/rest-api/settings/class-base-settings-endpoint.php +321 -35 3.23.4 → 3.24.3 View file →
@@ -26,8 +26,18 @@
26 26 * @phpstan-type Subvalue_Spec array{values: array<mixed>, default: mixed}
27 27 */
28 28 abstract class Base_Settings_Endpoint extends Base_Endpoint {
29 29 /**
30 + * The format that stored settings are expected to be in. Recorded per user,
31 + * and bumped whenever stored values need converting.
32 + *
33 + * @since 3.24.1
34 + *
35 + * @var int
36 + */
37 + protected const STORED_FORMAT_VERSION = 1;
38 +
39 + /**
30 40 * The meta entry's default value. Initialized in the constructor.
31 41 *
32 42 * @since 3.13.0
33 43 * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
@@ -47,18 +57,8 @@
47 57 */
48 58 protected $valid_subvalues = array();
49 59
50 60 /**
51 - * The current user's ID.
52 - *
53 - * @since 3.14.0
54 - * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
55 - *
56 - * @var int
57 - */
58 - protected $current_user_id = 0;
59 -
60 - /**
61 61 * Returns the meta entry's key.
62 62 *
63 63 * @since 3.13.0
64 64 * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
@@ -77,8 +77,37 @@
77 77 */
78 78 abstract protected function get_subvalues_specs(): array;
79 79
80 80 /**
81 + * Returns the composite keys of the settings that inherit their default.
82 + *
83 + * These are stored only once the user deviates from the default, so that a
84 + * later change to it still reaches users who never set them.
85 + *
86 + * @since 3.24.1
87 + *
88 + * @return array<string> The composite keys.
89 + */
90 + protected function get_inheritable_keys(): array {
91 + return array();
92 + }
93 +
94 + /**
95 + * Returns the defaults that inheritable settings had before they became
96 + * inheritable, keyed by composite key.
97 + *
98 + * A stored value matching one of these was written when the setting had no
99 + * configurable default, so it cannot be told apart from an untouched one.
100 + *
101 + * @since 3.24.1
102 + *
103 + * @return array<string, mixed> The defaults, as composite key => value pairs.
104 + */
105 + protected function get_legacy_defaults(): array {
106 + return array();
107 + }
108 +
109 + /**
81 110 * Constructor.
82 111 *
83 112 * @since 3.13.0
84 113 * @since 3.17.0 Moved from Base_Endpoint_User_Meta.
@@ -96,18 +125,8 @@
96 125 }
97 126 }
98 127
99 128 /**
100 - * Initializes the endpoint and sets the current user ID.
101 - *
102 - * @since 3.17.0
103 - */
104 - public function init(): void {
105 - parent::init();
106 - $this->current_user_id = get_current_user_id();
107 - }
108 -
109 - /**
110 129 * Registers the routes for the endpoint.
111 130 *
112 131 * @since 3.17.0
113 132 */
@@ -168,21 +187,24 @@
168 187 * API Endpoint: GET settings/{endpoint}/get
169 188 *
170 189 * Retrieves the settings for the current user.
171 190 *
191 + * The stored value is resolved against the current specifications, so that
192 + * a setting added after the user's last save still reaches them.
193 + *
172 194 * @since 3.17.0
195 + * @since 3.24.0 The stored value is resolved against the current
196 + * specifications.
197 + * @since 3.24.1 A stored value predating the current format is normalized
198 + * and stored anew.
173 199 *
174 200 * @return WP_REST_Response The response object.
175 201 */
176 202 public function get_settings(): WP_REST_Response {
177 - $meta_key = $this->get_meta_key();
178 - $settings = get_user_meta( $this->current_user_id, $meta_key, true );
179 -
180 - if ( ! is_array( $settings ) || 0 === count( $settings ) ) {
181 - $settings = $this->default_value;
182 - }
183 -
184 - return new WP_REST_Response( $settings, 200 );
203 + return new WP_REST_Response(
204 + $this->sanitize_value( $this->get_stored_settings() ),
205 + 200
206 + );
185 207 }
186 208
187 209 /**
188 210 * API Endpoint: PUT settings/{endpoint}/set
@@ -188,9 +210,14 @@
188 210 * API Endpoint: PUT settings/{endpoint}/set
189 211 *
190 212 * Updates the settings for the current user.
191 213 *
214 + * Settings absent from the request keep their current value, rather than
215 + * being reset to their default.
216 + *
192 217 * @since 3.17.0
218 + * @since 3.24.1 The request is merged into the current settings, and
219 + * inheritable settings are stored only once set.
193 220 *
194 221 * @param WP_REST_Request $request The request object.
195 222 * @return WP_REST_Response|WP_Error The response object.
196 223 */
@@ -204,20 +231,23 @@
204 231 __( 'Settings must be a valid JSON array', 'wp-parsely' )
205 232 );
206 233 }
207 234
208 - $sanitized_value = $this->sanitize_value( $meta_value );
235 + $stored_settings = $this->get_stored_settings();
236 + $current_settings = $this->sanitize_value( $stored_settings );
237 + $sanitized_value = $this->sanitize_value(
238 + $this->merge_settings( $meta_value, $current_settings )
239 + );
209 240
210 241 // If the current settings are the same as the new settings, return early.
211 - $current_settings = $this->get_settings();
212 - if ( $current_settings->get_data() === $sanitized_value ) {
213 - return $current_settings;
242 + if ( $current_settings === $sanitized_value ) {
243 + return new WP_REST_Response( $current_settings, 200 );
214 244 }
215 245
216 246 $update_meta = update_user_meta(
217 - $this->current_user_id,
247 + get_current_user_id(),
218 248 $this->get_meta_key(),
219 - $sanitized_value
249 + $this->strip_inherited_values( $sanitized_value, $stored_settings )
220 250 );
221 251
222 252 if ( false === $update_meta ) {
223 253 return new WP_Error(
@@ -225,12 +255,165 @@
225 255 __( 'Failed to update settings', 'wp-parsely' )
226 256 );
227 257 }
228 258
259 + $this->record_stored_format();
260 +
229 261 return new WP_REST_Response( $sanitized_value, 200 );
230 262 }
231 263
232 264 /**
265 + * Returns the current user's stored settings, normalizing them first if
266 + * they were stored in an earlier format.
267 + *
268 + * @since 3.24.1
269 + *
270 + * @return array<string, mixed> The stored settings.
271 + */
272 + protected function get_stored_settings(): array {
273 + $settings = get_user_meta( get_current_user_id(), $this->get_meta_key(), true );
274 + $settings = is_array( $settings ) ? $settings : array();
275 +
276 + return $this->needs_normalizing( $settings )
277 + ? $this->normalize_stored_settings( $settings )
278 + : $settings;
279 + }
280 +
281 + /**
282 + * Returns whether the passed stored settings need normalizing before use.
283 + *
284 + * @since 3.24.1
285 + *
286 + * @param array<string, mixed> $settings The stored settings.
287 + * @return bool
288 + */
289 + protected function needs_normalizing( array $settings ): bool {
290 + if ( 0 === count( $settings ) || 0 === count( $this->get_inheritable_keys() ) ) {
291 + return false;
292 + }
293 +
294 + $format = get_user_meta( get_current_user_id(), $this->get_format_meta_key(), true );
295 +
296 + return static::STORED_FORMAT_VERSION !== ( is_numeric( $format ) ? (int) $format : 0 );
297 + }
298 +
299 + /**
300 + * Drops the stored inheritable settings that cannot be told apart from an
301 + * untouched default, and stores the result.
302 + *
303 + * A value matching the setting's current default was stored by a full-tree
304 + * save; one matching its legacy default was stored when the setting had no
305 + * configurable default. Neither expresses a choice.
306 + *
307 + * @since 3.24.1
308 + *
309 + * @param array<string, mixed> $settings The stored settings.
310 + * @return array<string, mixed> The normalized settings.
311 + */
312 + protected function normalize_stored_settings( array $settings ): array {
313 + $legacy_defaults = $this->get_legacy_defaults();
314 +
315 + foreach ( $this->get_inheritable_keys() as $composite_key ) {
316 + $keys = explode( '.', $composite_key );
317 + $value = $this->get_path_value( $settings, $keys );
318 +
319 + if ( null === $value ) {
320 + continue;
321 + }
322 +
323 + $defaults = array( $this->get_default( $keys ) );
324 + if ( array_key_exists( $composite_key, $legacy_defaults ) ) {
325 + $defaults[] = $legacy_defaults[ $composite_key ];
326 + }
327 +
328 + if ( in_array( $value, $defaults, true ) ) {
329 + $this->unset_path_value( $settings, $keys );
330 + }
331 + }
332 +
333 + update_user_meta( get_current_user_id(), $this->get_meta_key(), $settings );
334 + $this->record_stored_format();
335 +
336 + return $settings;
337 + }
338 +
339 + /**
340 + * Removes the inheritable settings that the user has not set, so that they
341 + * keep following their default.
342 + *
343 + * A setting counts as set once it is stored, so that a choice is not undone
344 + * by a later change to the default that happens to match it.
345 + *
346 + * @since 3.24.1
347 + *
348 + * @param array<string, mixed> $settings The settings about to be stored.
349 + * @param array<string, mixed> $stored The settings as they are stored.
350 + * @return array<string, mixed> The settings to store.
351 + */
352 + protected function strip_inherited_values( array $settings, array $stored ): array {
353 + foreach ( $this->get_inheritable_keys() as $composite_key ) {
354 + $keys = explode( '.', $composite_key );
355 +
356 + if ( $this->is_stored_override( $stored, $composite_key ) ||
357 + $this->get_path_value( $settings, $keys ) !== $this->get_default( $keys )
358 + ) {
359 + continue;
360 + }
361 +
362 + $this->unset_path_value( $settings, $keys );
363 + }
364 +
365 + return $settings;
366 + }
367 +
368 + /**
369 + * Returns whether the user set the passed setting.
370 + *
371 + * A value that sanitization rejects resolves to the setting's default on
372 + * read, so treating it as set would pin the user to that default.
373 + *
374 + * @since 3.24.1
375 + *
376 + * @param array<string, mixed> $stored The settings as they are stored.
377 + * @param string $composite_key The setting's composite key.
378 + * @return bool
379 + */
380 + protected function is_stored_override( array $stored, string $composite_key ): bool {
381 + $value = $this->get_path_value( $stored, explode( '.', $composite_key ) );
382 +
383 + return null !== $value && $this->sanitize_subvalue( $composite_key, $value ) === $value;
384 + }
385 +
386 + /**
387 + * Records that the current user's settings are stored in the current
388 + * format.
389 + *
390 + * @since 3.24.1
391 + */
392 + protected function record_stored_format(): void {
393 + if ( 0 === count( $this->get_inheritable_keys() ) ) {
394 + return;
395 + }
396 +
397 + update_user_meta(
398 + get_current_user_id(),
399 + $this->get_format_meta_key(),
400 + static::STORED_FORMAT_VERSION
401 + );
402 + }
403 +
404 + /**
405 + * Returns the key of the meta entry holding the stored format's version.
406 + *
407 + * @since 3.24.1
408 + *
409 + * @return string The meta entry's key.
410 + */
411 + protected function get_format_meta_key(): string {
412 + return $this->get_meta_key() . '_format';
413 + }
414 +
415 + /**
233 416 * Returns whether the endpoint is available for access by the current
234 417 * user.
235 418 *
236 419 * @since 3.14.0
@@ -240,9 +423,9 @@
240 423 * @param WP_REST_Request|null $request The request object.
241 424 * @return bool
242 425 */
243 426 public function is_available_to_current_user( ?WP_REST_Request $request = null ): bool {
244 - return current_user_can( 'edit_user', $this->current_user_id );
427 + return current_user_can( 'edit_user', get_current_user_id() );
245 428 }
246 429
247 430 /**
248 431 * Sanitizes the passed meta value.
@@ -294,8 +477,60 @@
294 477 return $sanitized_value;
295 478 }
296 479
297 480 /**
481 + * Merges the passed settings into the current ones.
482 + *
483 + * Recursion follows the specifications, so that a setting holding a list of
484 + * values is replaced rather than merged into.
485 + *
486 + * @since 3.24.1
487 + *
488 + * @param array<string, mixed> $settings The settings to merge in.
489 + * @param array<string, mixed> $current The settings to merge into.
490 + * @param string $parent_key The parent key for the current level of the settings.
491 + * @return array<string, mixed> The merged settings.
492 + */
493 + protected function merge_settings(
494 + array $settings,
495 + array $current,
496 + string $parent_key = ''
497 + ): array {
498 + /**
499 + * Current level's specifications.
500 + *
501 + * @var array<string, Subvalue_Spec> $current_specs
502 + */
503 + $current_specs = ( '' === $parent_key ) ? $this->get_subvalues_specs() : $this->get_nested_specs( $parent_key );
504 +
505 + foreach ( $current_specs as $key => $spec ) {
506 + if ( ! array_key_exists( $key, $settings ) ) {
507 + continue;
508 + }
509 +
510 + $value = $settings[ $key ];
511 +
512 + /**
513 + * Spec for the current key.
514 + *
515 + * @var array{default: mixed, values?: array<mixed>} $spec
516 + */
517 + if ( is_array( $value ) && isset( $spec['values'] ) ) {
518 + $current_value = $current[ $key ] ?? null;
519 + $current[ $key ] = $this->merge_settings(
520 + $value,
521 + is_array( $current_value ) ? $current_value : array(),
522 + '' === $parent_key ? $key : $parent_key . '.' . $key
523 + );
524 + } else {
525 + $current[ $key ] = $value;
526 + }
527 + }
528 +
529 + return $current;
530 + }
531 +
532 + /**
298 533 * Sanitizes the passed subvalue.
299 534 *
300 535 * @since 3.13.0
301 536 * @since 3.14.0 Added support for nested arrays.
@@ -409,6 +644,57 @@
409 644 }
410 645 }
411 646
412 647 return $specs;
648 + }
649 +
650 + /**
651 + * Returns the value held at the passed path.
652 + *
653 + * @since 3.24.1
654 + *
655 + * @param array<string, mixed> $settings The settings to look in.
656 + * @param array<string> $keys The path to the setting.
657 + * @return mixed The value, or null if the path holds none.
658 + */
659 + protected function get_path_value( array $settings, array $keys ) {
660 + $current = $settings;
661 +
662 + foreach ( $keys as $key ) {
663 + if ( ! is_array( $current ) || ! array_key_exists( $key, $current ) ) {
664 + return null;
665 + }
666 +
667 + $current = $current[ $key ];
668 + }
669 +
670 + return $current;
671 + }
672 +
673 + /**
674 + * Removes the value held at the passed path.
675 + *
676 + * @since 3.24.1
677 + *
678 + * @param array<string, mixed> $settings The settings to remove from.
679 + * @param array<string> $keys The path to the setting.
680 + */
681 + protected function unset_path_value( array &$settings, array $keys ): void {
682 + $last_key = array_pop( $keys );
683 +
684 + if ( null === $last_key ) {
685 + return;
686 + }
687 +
688 + $current = &$settings;
689 +
690 + foreach ( $keys as $key ) {
691 + if ( ! isset( $current[ $key ] ) || ! is_array( $current[ $key ] ) ) {
692 + return;
693 + }
694 +
695 + $current = &$current[ $key ];
696 + }
697 +
698 + unset( $current[ $last_key ] );
413 699 }
414 700 }