Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.91% covered (success)
90.91%
90 / 99
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Settings
90.91% covered (success)
90.91%
90 / 99
0.00% covered (danger)
0.00%
0 / 2
28.59
0.00% covered (danger)
0.00%
0 / 1
 get
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
5.01
 update
90.80% covered (success)
90.80%
79 / 87
0.00% covered (danger)
0.00%
0 / 1
23.41
1<?php
2/**
3 * The Stats settings a site owner can change.
4 *
5 * @package automattic/jetpack-stats
6 */
7
8namespace Automattic\Jetpack\Stats;
9
10use WP_Error;
11
12/**
13 * Reads and validates changes to the owner-facing keys of `stats_options`.
14 */
15class Settings {
16    const ERROR_PREFIX = 'jetpack_stats_';
17
18    /**
19     * The `stats_options` keys a site owner can change. Each key's type comes from `Options::get_defaults()`.
20     *
21     * @var string[]
22     */
23    const KEYS = array( 'admin_bar', 'roles', 'count_roles', 'do_not_track' );
24
25    /**
26     * Get the current values of some `stats_options` keys.
27     *
28     * @param string[] $keys The keys to read.
29     * @return array
30     */
31    public static function get( array $keys ): array {
32        $options  = Options::get_options();
33        $defaults = Options::get_defaults();
34        $out      = array();
35        foreach ( $keys as $key ) {
36            $raw     = $options[ $key ] ?? null;
37            $default = $defaults[ $key ] ?? null;
38            if ( is_bool( $default ) ) {
39                $out[ $key ] = (bool) $raw;
40            } elseif ( is_array( $default ) ) {
41                $out[ $key ] = is_array( $raw ) ? array_values( $raw ) : array();
42            } else {
43                $out[ $key ] = $raw;
44            }
45        }
46        return $out;
47    }
48
49    /**
50     * Validate new values and save the ones that change.
51     *
52     * @param array    $values New values, keyed by `stats_options` key.
53     * @param string[] $keys   The keys the caller may change.
54     * @return array|WP_Error `changed` and the `settings` after the save, or why the values were refused.
55     */
56    public static function update( array $values, array $keys ) {
57        $unknown = array_diff( $keys, self::KEYS );
58        if ( ! empty( $unknown ) ) {
59            return new WP_Error(
60                self::ERROR_PREFIX . 'unknown_setting',
61                sprintf(
62                    /* translators: %s: comma-separated list of setting names. */
63                    __( 'Unknown Stats settings: %s.', 'jetpack-stats-pkg' ),
64                    implode( ', ', $unknown )
65                )
66            );
67        }
68
69        $provided = array_intersect_key( $values, array_flip( $keys ) );
70        if ( empty( $provided ) ) {
71            return new WP_Error(
72                self::ERROR_PREFIX . 'missing_setting_field',
73                sprintf(
74                    /* translators: %s: comma-separated list of writable field names. */
75                    __( 'Provide at least one of: %s.', 'jetpack-stats-pkg' ),
76                    implode( ', ', $keys )
77                )
78            );
79        }
80
81        $defaults    = Options::get_defaults();
82        $before      = self::get( $keys );
83        $known_roles = null;
84        foreach ( $keys as $role_field ) {
85            if ( ! array_key_exists( $role_field, $provided ) ) {
86                continue;
87            }
88            if ( ! is_array( $defaults[ $role_field ] ?? null ) ) {
89                continue;
90            }
91            if ( null === $known_roles ) {
92                $known_roles = array_keys( wp_roles()->roles );
93            }
94            if ( ! is_array( $provided[ $role_field ] ) ) {
95                return new WP_Error(
96                    self::ERROR_PREFIX . 'invalid_' . $role_field,
97                    sprintf(
98                        /* translators: %s: the offending field name. */
99                        __( 'Field `%s` must be an array of role slugs.', 'jetpack-stats-pkg' ),
100                        $role_field
101                    )
102                );
103            }
104            // Direct PHP callers skip the REST schema's `minItems` check.
105            if ( 'roles' === $role_field && empty( $provided[ $role_field ] ) ) {
106                return new WP_Error(
107                    self::ERROR_PREFIX . 'invalid_roles',
108                    __( 'Field `roles` must be a non-empty array of role slugs.', 'jetpack-stats-pkg' )
109                );
110            }
111            $sanitized = array();
112            foreach ( $provided[ $role_field ] as $role ) {
113                if ( ! is_string( $role ) || '' === $role ) {
114                    return new WP_Error(
115                        self::ERROR_PREFIX . 'invalid_role',
116                        sprintf(
117                            /* translators: 1: field name, 2: comma-separated list of valid role slugs. */
118                            __( 'Role slugs in `%1$s` must be non-empty strings. Known roles: %2$s.', 'jetpack-stats-pkg' ),
119                            $role_field,
120                            implode( ', ', $known_roles )
121                        )
122                    );
123                }
124                if ( ! in_array( $role, $known_roles, true ) && ! in_array( $role, $before[ $role_field ], true ) ) {
125                    return new WP_Error(
126                        self::ERROR_PREFIX . 'invalid_role',
127                        sprintf(
128                            /* translators: 1: unknown role slug, 2: field name, 3: comma-separated list of valid role slugs. */
129                            __( 'Unknown role `%1$s` in `%2$s`. Known roles: %3$s.', 'jetpack-stats-pkg' ),
130                            $role,
131                            $role_field,
132                            implode( ', ', $known_roles )
133                        )
134                    );
135                }
136                $sanitized[] = $role;
137            }
138            // Administrators keep `view_stats`, because the screen that changes this list needs it.
139            if ( 'roles' === $role_field && ! in_array( 'administrator', $sanitized, true ) ) {
140                array_unshift( $sanitized, 'administrator' );
141            }
142            $provided[ $role_field ] = array_values( array_unique( $sanitized ) );
143        }
144
145        $changes = array();
146        foreach ( $provided as $key => $value ) {
147            if ( is_bool( $defaults[ $key ] ?? null ) ) {
148                $value = (bool) $value;
149            }
150            $current = $before[ $key ] ?? null;
151            if ( $current === $value ) {
152                continue;
153            }
154            $changes[ $key ] = $value;
155        }
156
157        if ( ! empty( $changes ) ) {
158            Options::set_options( $changes );
159        }
160
161        $after = self::get( $keys );
162        foreach ( $changes as $key => $value ) {
163            if ( $after[ $key ] !== $value ) {
164                return new WP_Error(
165                    self::ERROR_PREFIX . 'save_failed',
166                    __( 'The Stats settings could not be saved.', 'jetpack-stats-pkg' )
167                );
168            }
169        }
170
171        return array(
172            'changed'  => $after !== $before,
173            'settings' => $after,
174        );
175    }
176}