PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-stats/src/abilities/class-stats-abilities.php +10 -148 16.2 → 16.3 View file →
@@ -8,13 +8,17 @@
8 8 */
9 9
10 10 namespace Automattic\Jetpack\Stats\Abilities;
11 11
12 -use Automattic\Jetpack\Stats\Options;
12 +use Automattic\Jetpack\Stats\Settings;
13 13 use Automattic\Jetpack\Stats\WPCOM_Stats;
14 14 use Automattic\Jetpack\WP_Abilities\Registrar;
15 15 use WP_Error;
16 16
17 +if ( ! defined( 'ABSPATH' ) ) {
18 + exit( 0 );
19 +}
20 +
17 21 /**
18 22 * Registers Jetpack Stats abilities with the WordPress Abilities API.
19 23 *
20 24 * Exposes a small, consolidated surface for reading Jetpack Stats traffic
@@ -25,24 +29,11 @@
25 29 */
26 30 class Stats_Abilities extends Registrar {
27 31
28 32 const CATEGORY_SLUG = 'jetpack-stats';
29 - const ERROR_PREFIX = 'jetpack_stats_';
33 + const ERROR_PREFIX = Settings::ERROR_PREFIX;
30 34
31 35 /**
32 - * Whitelist of stats_options keys exposed via the settings abilities.
33 - *
34 - * Internal keys (`blog_id`, `notices`, `views`, `collapse_nudges`,
35 - * `version`, `odyssey_stats_changed_at`) are deliberately excluded —
36 - * agents can't act on them and they'd bloat the response.
37 - * `enable_odyssey_stats` is also excluded: it's a UI dashboard toggle
38 - * with no meaningful agent use case. The per-key type (bool /
39 - * role-array) is read from `Options::get_defaults()` at runtime, not
40 - * duplicated here.
41 - */
42 - const SETTINGS_KEYS = array( 'admin_bar', 'roles', 'count_roles', 'do_not_track' );
43 -
44 - /**
45 36 * Allowed `type` values for `get-top-content`.
46 37 */
47 38 const TOP_CONTENT_TYPES = array( 'posts', 'referrers', 'search-terms', 'clicks', 'tags', 'authors', 'countries', 'downloads', 'video-plays' );
48 39
@@ -503,9 +494,9 @@
503 494 private static function spec_update_settings(): array {
504 495 return array(
505 496 'label' => __( 'Update Stats settings', 'jetpack-stats-pkg' ),
506 497 'description' => __(
507 - 'Update one or more Jetpack Stats settings. All fields are optional; only fields present in the call are written, and unrelated keys are preserved. Idempotent — setting a value to its current state returns changed=false. Shape: { changed, settings: { admin_bar, roles, count_roles, do_not_track } }. Role slugs in `roles` and `count_roles` are validated against the site\'s registered roles; unknown slugs return jetpack_stats_invalid_role. Narrowing `roles` can revoke Stats access for whole groups of users — confirm with the user before removing roles.',
498 + 'Update one or more Jetpack Stats settings. All fields are optional; only fields present in the call are written, and unrelated keys are preserved. Idempotent — setting a value to its current state returns changed=false. Shape: { changed, settings: { admin_bar, roles, count_roles, do_not_track } }. Role slugs added to `roles` or `count_roles` must be registered roles on the site; unknown slugs return jetpack_stats_invalid_role. A slug that is already saved is kept even when its role no longer exists. Narrowing `roles` can revoke Stats access for whole groups of users — confirm with the user before removing roles.',
508 499 'jetpack-stats-pkg'
509 500 ),
510 501 'input_schema' => array(
511 502 'type' => 'object',
@@ -515,9 +506,9 @@
515 506 'description' => __( 'Whether to show the Stats item in the admin bar for users who can view Stats.', 'jetpack-stats-pkg' ),
516 507 ),
517 508 'roles' => array(
518 509 'type' => 'array',
519 - 'description' => __( 'Role slugs that can view Stats. Must be non-empty; each slug must be a registered role.', 'jetpack-stats-pkg' ),
510 + 'description' => __( 'Role slugs that can view Stats. Must be non-empty; each added slug must be a registered role. `administrator` is always kept.', 'jetpack-stats-pkg' ),
520 511 'items' => array( 'type' => 'string' ),
521 512 'minItems' => 1,
522 513 ),
523 514 'count_roles' => array(
@@ -883,9 +874,9 @@
883 874 * @return array
884 875 */
885 876 public static function get_settings( $input = null ) {
886 877 unset( $input );
887 - return self::settings_snapshot();
878 + return Settings::get( Settings::KEYS );
888 879 }
889 880
890 881 /**
891 882 * Execute: update-settings.
@@ -893,115 +884,9 @@
893 884 * @param array|null $input Input matching the ability's input_schema.
894 885 * @return array|WP_Error
895 886 */
896 887 public static function update_settings( $input = null ) {
897 - $input = is_array( $input ) ? $input : array();
898 -
899 - // At least one of the whitelisted keys must be present.
900 - $provided = array_intersect_key( $input, array_flip( self::SETTINGS_KEYS ) );
901 - if ( empty( $provided ) ) {
902 - return new WP_Error(
903 - self::ERROR_PREFIX . 'missing_setting_field',
904 - sprintf(
905 - /* translators: %s: comma-separated list of writable field names. */
906 - __( 'Provide at least one of: %s.', 'jetpack-stats-pkg' ),
907 - implode( ', ', self::SETTINGS_KEYS )
908 - )
909 - );
910 - }
911 -
912 - // Validate role slugs against registered roles — but only load the role list
913 - // if the caller is actually writing a role field. Boolean-only writes skip
914 - // the wp_roles() resolution entirely. Role fields are detected from the
915 - // option's default value type (array → role list).
916 - $defaults = Options::get_defaults();
917 - $known_roles = null;
918 - foreach ( self::SETTINGS_KEYS as $role_field ) {
919 - if ( ! array_key_exists( $role_field, $provided ) ) {
920 - continue;
921 - }
922 - if ( ! is_array( $defaults[ $role_field ] ?? null ) ) {
923 - continue;
924 - }
925 - if ( null === $known_roles ) {
926 - $known_roles = array_keys( wp_roles()->roles );
927 - }
928 - if ( ! is_array( $provided[ $role_field ] ) ) {
929 - return new WP_Error(
930 - self::ERROR_PREFIX . 'invalid_' . $role_field,
931 - sprintf(
932 - /* translators: %s: the offending field name. */
933 - __( 'Field `%s` must be an array of role slugs.', 'jetpack-stats-pkg' ),
934 - $role_field
935 - )
936 - );
937 - }
938 - // `roles` gates `view_stats` — an empty array would lock every user out, including
939 - // the caller. Schema validation enforces minItems=1 on REST input, but direct PHP
940 - // callers bypass that path; reject explicitly here.
941 - if ( 'roles' === $role_field && empty( $provided[ $role_field ] ) ) {
942 - return new WP_Error(
943 - self::ERROR_PREFIX . 'invalid_roles',
944 - __( 'Field `roles` must be a non-empty array of role slugs — an empty list would revoke Stats access for every user.', 'jetpack-stats-pkg' )
945 - );
946 - }
947 - $sanitized = array();
948 - foreach ( $provided[ $role_field ] as $role ) {
949 - if ( ! is_string( $role ) || '' === $role ) {
950 - return new WP_Error(
951 - self::ERROR_PREFIX . 'invalid_role',
952 - sprintf(
953 - /* translators: 1: field name, 2: comma-separated list of valid role slugs. */
954 - __( 'Role slugs in `%1$s` must be non-empty strings. Known roles: %2$s.', 'jetpack-stats-pkg' ),
955 - $role_field,
956 - implode( ', ', $known_roles )
957 - )
958 - );
959 - }
960 - if ( ! in_array( $role, $known_roles, true ) ) {
961 - return new WP_Error(
962 - self::ERROR_PREFIX . 'invalid_role',
963 - sprintf(
964 - /* translators: 1: unknown role slug, 2: field name, 3: comma-separated list of valid role slugs. */
965 - __( 'Unknown role `%1$s` in `%2$s`. Known roles: %3$s.', 'jetpack-stats-pkg' ),
966 - $role,
967 - $role_field,
968 - implode( ', ', $known_roles )
969 - )
970 - );
971 - }
972 - $sanitized[] = $role;
973 - }
974 - $provided[ $role_field ] = array_values( array_unique( $sanitized ) );
975 - }
976 -
977 - $before = self::settings_snapshot();
978 - $changes = array();
979 - foreach ( $provided as $key => $value ) {
980 - if ( is_bool( $defaults[ $key ] ?? null ) ) {
981 - $value = (bool) $value;
982 - }
983 - $current = $before[ $key ] ?? null;
984 - if ( $current === $value ) {
985 - continue;
986 - }
987 - $changes[ $key ] = $value;
988 - }
989 -
990 - // `changed` is derived from the POST-WRITE snapshot, not from `$changes` alone —
991 - // if update_option fails or refuses to persist for any reason (DB error,
992 - // serialization mismatch), we must not claim a change that didn't happen.
993 - if ( ! empty( $changes ) ) {
994 - // One merged write instead of N get+update cycles via set_option.
995 - Options::set_options( $changes );
996 - }
997 -
998 - $after = self::settings_snapshot();
999 -
1000 - return array(
1001 - 'changed' => $after !== $before,
1002 - 'settings' => $after,
1003 - );
888 + return Settings::update( is_array( $input ) ? $input : array(), Settings::KEYS );
1004 889 }
1005 890
1006 891 /*
1007 892 ---------------------------------------------------------------------
@@ -1007,31 +892,8 @@
1007 892 ---------------------------------------------------------------------
1008 893 * Helpers
1009 894 * ---------------------------------------------------------------------
1010 895 */
1011 -
1012 - /**
1013 - * Build a whitelisted snapshot of the current Stats configuration.
1014 - *
1015 - * @return array
1016 - */
1017 - private static function settings_snapshot(): array {
1018 - $options = Options::get_options();
1019 - $defaults = Options::get_defaults();
1020 - $out = array();
1021 - foreach ( self::SETTINGS_KEYS as $key ) {
1022 - $raw = $options[ $key ] ?? null;
1023 - $default = $defaults[ $key ] ?? null;
1024 - if ( is_bool( $default ) ) {
1025 - $out[ $key ] = (bool) $raw;
1026 - } elseif ( is_array( $default ) ) {
1027 - $out[ $key ] = is_array( $raw ) ? array_values( $raw ) : array();
1028 - } else {
1029 - $out[ $key ] = $raw;
1030 - }
1031 - }
1032 - return $out;
1033 - }
1034 896
1035 897 /**
1036 898 * Compose multiple WPCOM sub-call results into a partial-tolerant envelope.
1037 899 *